Elasticsearch 오타 처리 — 퍼지 검색과 한글 자모 분해

검색창에 “겔럭시”라고 오타를 냈더니 검색 결과가 0건이었다. 사람은 “아 갤럭시 찾는구나” 하고 알아듣는데, ES는 정확히 일치하는 토큰이 없으니 아무것도 못 찾는다.

오타는 검색 트래픽의 10~20%에 달한다. 이걸 그냥 0건으로 보내면 그만큼 매출이 날아간다. 오타 처리는 여러 레이어로 막아야 한다.


오타가 나면 왜 0건인가?

ES 검색은 기본적으로 토큰 정확 일치다. “겔럭시”를 색인된 “갤럭시”와 비교하면 다른 토큰이다. 역색인에서 “겔럭시”를 찾으면 posting list가 비어 있으니 0건이다.

사람은 “겔”과 “갤”이 한 글자 차이라는 걸 알지만, ES는 기본적으로 그 “비슷함”을 모른다. 그 비슷함을 계산하게 해주는 게 fuzzy 검색이다.

fuzzy가 어떻게 오타를 잡나?

fuzzy는 편집 거리(edit distance) 로 비슷한 단어를 찾는다. 한 단어를 다른 단어로 바꾸는 데 필요한 최소 연산(삽입/삭제/치환/인접 전치) 횟수다.

겔럭시 → 갤럭시 : "겔"을 "갤"로 치환 = 편집 거리 1
{
  "match": {
    "name": { "query": "겔럭시", "fuzziness": "AUTO" }
  }
}

fuzziness를 숫자로 주면 그 거리까지 허용한다. AUTO 가 권장값인데, 단어 길이에 따라 허용 거리를 자동 조절한다(짧은 단어는 0, 길수록 1~2). 짧은 단어에 큰 거리를 허용하면 전혀 다른 단어까지 매칭되기 때문이다.

fuzzy를 전체에 쓰면 왜 느린가?

fuzzy는 공짜가 아니다. 편집 거리 안에 드는 후보 단어를 term 사전에서 다 찾아 확장한 뒤 검색한다. 단어 사전이 크면 후보가 많아져 느려진다.

그래서 모든 검색에 fuzzy를 남발하면 안 된다. 보통 이렇게 쓴다.

  • 메인 검색은 정확 매칭 우선
  • 결과가 0건이거나 빈약할 때 fuzzy로 재질의(fallback)
  • 또는 중요한 필드(상품명) 한정으로만 fuzzy 허용

비용을 깎는 튜닝 파라미터도 있다.

  • fuzziness: AUTO: 단어 길이로 허용 거리를 자동 조절(짧으면 0, 길수록 1~2). 짧은 단어에 큰 거리를 막아 오매칭과 비용을 동시에 줄인다.
  • prefix_length: 앞 N글자는 오타를 허용하지 않는다. 첫 글자를 고정하면 확장 후보가 그 prefix로 시작하는 것만 남아 검색이 크게 빨라지고, 보통 오타는 첫 글자보다 뒤에 나므로 정확도 손해도 작다.
  • max_expansions: 편집 거리 안에서 확장할 term 개수 상한(기본 50). 단어 사전이 크면 후보가 폭발하므로 이 상한으로 막는다.

자동완성은 fuzzy로 하면 안 되나?

“갤”만 쳐도 “갤럭시”, “갤럭시 케이스”가 떠야 하는 자동완성은 fuzzy로 풀면 느리고 부정확하다. 이건 다른 도구를 쓴다.

edge_ngram: 단어를 앞에서부터 잘라 미리 색인한다.

"갤럭시" → 갤, 갤럭, 갤럭시

색인 때 이렇게 만들어두면 “갤럭”을 검색했을 때 바로 매칭된다. 검색어는 그대로 두고(검색 시엔 ngram 안 함) 색인만 펼치는 게 포인트다.

Completion Suggester: 자동완성 전용 자료구조(FST 기반)를 쓴다. 매우 빠르고, 가중치(weight)로 인기 검색어를 위로 올릴 수 있다. 별도 completion 타입 필드를 구성한다.

{ "suggest": { "name-suggest": {
  "prefix": "갤", "completion": { "field": "name_suggest" }
}}}

영어 편집 거리가 한글엔 왜 약한가?

여기가 한국어 검색의 핵심이다. 편집 거리를 글자 단위로 계산하면 한글에선 정밀도가 떨어진다.

"삼성" vs "삼셩" : 글자 단위로는 "성"≠"셩", 편집 거리 1

겉보기엔 잘 잡힐 것 같지만, 한글은 초성·중성·종성이 합쳐진 글자라 글자 단위 비교는 거칠다. “성”과 “셩”이 얼마나 비슷한지(ㅓ↔ㅕ 하나 차이)를 글자 단위론 모른다.

해결은 자모 분해다. 글자를 초성/중성/종성으로 풀어서 색인한다.

"삼성" → ㅅㅏㅁ ㅅㅓㅇ
"삼셩" → ㅅㅏㅁ ㅅㅕㅇ   → ㅓ↔ㅕ 차이 = 자모 단위 편집 거리 1 (정밀)

자모 단위로 풀면 오타가 자모 하나 차이라는 걸 정확히 잡아낸다. 구현은 보통 두 가지를 맞춰야 한다. (a) 색인·검색 양쪽에 같은 자모 분해 필터를 적용한다 — 색인은 자모로 풀어 저장하고 검색어도 자모로 풀어 질의해야 매칭이 성립한다. 분해는 icu_transform(예: Hangul-Latin/NFD 계열) 같은 char/token 필터나 javacafe-analyzer 류의 jamo 필터로 한다. (b) 분해의 핵심은 글자를 초성/중성/종성으로 펼치는 것이라, “삼성”을 ㅅㅏㅁㅅㅓㅇ처럼 자모열로 만들면 종성 일부만 틀린 부분 오타까지 자모 단위 편집 거리로 잡힌다.

초성 검색과 자판 오타도 같이

자모를 다루기 시작하면 한국어 특유의 검색을 더 할 수 있다.

  • 초성 검색: “ㅅㅅㅈㅈ” → “삼성전자”. 초성만 따로 색인해두면 된다.
  • 영한 자판 오타: “tjsdma”(한글 자판 그대로 영문 입력) → “선물”. 자판 매핑 테이블로 변환.
  • 인접키 오타: 두벌식 키보드에서 가까운 키 오타에 가중치.

이커머스 검색창은 이런 걸 조합해서 오타·초성·자판 실수를 모두 흡수한다.

”이거 찾으세요?”는 어떻게 만드나?

검색 결과 위에 “겔럭시 → 갤럭시로 검색하시겠어요?”를 띄우는 건 매칭이 아니라 교정 제안(suggester) 이다.

  • Term Suggester: 토큰 단위로 비슷한 단어 후보를 준다. 단어별 교정.
  • Phrase Suggester: 문장 단위로 가장 그럴듯한 교정문을 준다. 문맥을 고려해 “did you mean”에 적합.

매칭(fuzzy)은 결과를 직접 가져오는 것이고, suggester는 사용자에게 교정어를 제안하는 것이다. 둘은 역할이 다르므로 보통 함께 쓴다.

실전 조합 전략

오타 처리는 단일 기능이 아니라 레이어의 조합이다.

1) 자동완성:   completion suggester (+ edge_ngram 백업)
2) 검색 매칭:   형태소 + 동의어로 정확 매칭 우선
3) 결과 빈약:   fuzziness AUTO + 자모 분해로 재질의(fallback)
4) 교정 제안:   phrase suggester로 "이거 찾으세요?" 노출

운영상 주의사항

fuzzy는 비싸다. 전 필드·전 검색에 기본으로 켜면 검색 레이턴시가 망가진다. 정확 매칭으로 먼저 시도하고, 결과가 부족할 때만 fuzzy로 넘어가는 단계적 전략이 안전하다. 자모 분해·초성·자판 매핑은 색인 용량과 복잡도를 늘리므로, 실제 오타 로그(검색 후 결과 0건 → 재검색 패턴)를 분석해서 효과가 큰 것부터 적용하는 게 맞다.