OpenSearch와 CJK를 활용한 오타 허용 검색 구현 방법

검색 결과가 없는 쿼리는 우리의 시청 시간을 갉아먹고 있었습니다.

1년 동안 TopVideoHub는 검색을 위해 SQLite FTS5를 사용했습니다. 정확한 일치에는 효과적이었지만, 사용자가 오타를 내면 실패했습니다.

사람들은 "demon slayer" 대신 "demon slyer"라고 검색했습니다. 일본어나 한국어 제목에 불필요한 공백을 넣기도 했습니다. FTS5는 정확한 토큰을 매칭하기 때문에, 단 하나의 오타만 있어도 결과가 0건이었습니다. 사용자들은 다시 검색하지 않고 그냥 떠나버렸습니다.

제목 검색을 OpenSearch로 옮겼습니다. 다국어 사용자를 위해 이를 어떻게 해결했는지 소개합니다.

표준 퍼지성(Fuzziness)의 문제점

대부분의 튜토리얼은 오타를 해결하기 위해 "fuzziness"를 사용하라고 권장합니다. 이는 영어에는 작동하지만, 중국어, 일본어, 한국어(CJK) 텍스트에는 실패합니다.

  • 편집 거리(Edit distance)는 CJK에 적합하지 않습니다. CJK에서 오타는 종종 아예 다른 문자를 사용하는 것을 의미합니다.
  • 표준 퍼지성은 의미 없는 결과를 생성합니다. 편집 거리가 1 차이 난다는 이유로 "fire"와 "water"를 매칭할 수도 있습니다.
  • 토큰화가 어렵습니다. CJK 언어는 단어 사이에 공백을 사용하지 않습니다.

해결책: 멀티 필드 접근 방식

모든 텍스트를 동일하게 취급하는 것을 중단했습니다. 세 가지 다른 방식으로 인덱싱하는 하나의 논리적 제목 필드를 만들었습니다.

  • Latin 및 Romaji: 퍼지성을 활성화한 표준 토큰화를 사용했습니다. 접두사 길이(prefix length)를 1로 설정했습니다. 이를 통해 "demon"은 "demn"과 매칭되지만, "lemon"은 "demon"과 매칭되지 않도록 했습니다.
  • CJK 텍스트: CJK 바이그램 분석기(bigram analyzer)와 ICU 정규화 도구(normalizer)를 사용했습니다. 퍼지성은 껐습니다(OFF). 대신 최소 일치 임계값(minimum match threshold)을 70%로 설정했습니다.
  • 자동 완성: 타이핑 중 검색(search-as-you-type) 결과를 구현하기 위해 edge-ngram 필드를 사용했습니다.

아키텍처 및 데이터 안전성

SQLite를 단일 데이터 원천(single source of truth)으로 유지했습니다. OpenSearch는 빠르고 재구축 가능한 인덱스 역할을 합니다.

  • PHP를 사용하여 OpenSearch에 대량 배치(bulk batches)로 업데이트를 전송합니다.
  • 사용자 요청 중에 인덱싱을 실행하지 않습니다.
  • Python 스크립트를 실행하여 "드리프트(drift)"를 확인합니다. 이를 통해 OpenSearch의 카운트가 SQLite의 카운트와 일치하는지 확인합니다.

결과

변화는 엄청났습니다:

  • 결과 없음 쿼리가 14%에서 3% 미만으로 감소했습니다.
  • 사용자가 원하는 것을 즉시 찾을 수 있게 되어 검색 세션이 길어졌습니다.
  • 지연 시간(Latency)은 약 40ms로 낮게 유지됩니다.

다국어 사용자를 대상으로 한다면 기억하세요. 오타 허용과 CJK 매칭은 서로 다른 문제입니다. 두 가지 다른 해결책이 필요합니다.

Source: https://dev.to/ahmet_gedik778845/how-i-added-typo-tolerant-video-title-search-with-opensearch-and-cjk-3e5d