About

이미 이전 포스트multi_match에서 fuzziness 패러미터를 이용하는 법을 간단히 소개한 적이 있다. 하지만 그때는 그냥 fuzziness 값으로 ‘auto’를 넣고 사용하는 법만 알려주었는데 원하는 결과를 얻기위해서는 조금 더 이해할 필요가 있다는 것을 필자도 최근에 알게됐다. 그래서 이번 글을 통해서는 fuzzy search에 대해서 조금 더 살펴본 것을 정리해보도록 하겠다.

글은 대부분 Elasticsearch 공식 문서를 번역한 것임을 미리 밝혀둔다.

만약 Elasticsearch와 python의 elasticsearch-dsl 패키지에 대해 잘 모르는 분들은 아래의 글들을 먼저 보고 오는 것을 추천한다.

Damerau-Lavenshtein vs Levenshtein

fuzzy query가 기본적으로 사용하게되는 알고리즘은 Damerau-Lavenshtein distance이다. 간단하게는 a=‘ax’, b=‘axe’라는 두가지의 문자열이 있을 때, a와 b의 유사도(거리)를 재는 방법이다. 몇개의 문자를 더하고, 빼고, 대체해야하는지를 계산하는 방법으로 위의 예를 이용하면 ‘ax’와 ‘axe’는 ‘e’를 하나만 빼는 걸로 같아질 수 있기 때문에 거리가 1이 되는 것이다.

기본으로 사용되는 알고리즘이 Damerau-Lavenshtein distance이지만, 원하면 Levenshtein distance도 사용할 수 있다. 차이점은 전자는 전위(순서 바꾸기)가 가능하지만 후자는 불가능하다는 점이다. 예를 들어서 ‘aex’와 ‘axe’를 비교한다고 해보자. 전자를 사용하게되면 e와 x의 순서만 바꾸면 되므로 거리는 1이되고 후자를 사용하게 되면 먼저 e를 빼고 다시 뒤에 e를 더하게 되므로 거리가 2가 된다. 이 의미는 후자를 이용할 때는 ‘aex’와 ‘axe’의 거리가 ‘faxes’와 ‘axe’의 거리와 같게 된다. 그러한 이유로 직관성이 더 떨어지기 때문에 대부분의 경우에는 Damerau-Lavenshtein distance를 사용하게 된다.

경우에 따라서는 Levenshtein distance를 사용해야 하는 경우가 있으니 아래에서 변경하는 방법을 알려주도록 하겠다.

Fuzzy search를 이해하기 위해서는 먼저 elasticsearch가 analyzer를 통과하게 된다는 점이다. 만약 analyzer에 대해서 처음 접하는 분이라면 Elasticsearch의 기본 개념과 Python 패키지 elasticsearch-dsl 기본 사용법에서 설명한 부분을 다시 확인하고 오는 것을 추천한다. 어쨋든 공식 문서에서는 아래의 이미지처럼 Snowball Analyzer를 쓴 경우를 예로 들고 있는데 그 이유는 fuzzy search를 할 때는 simple analyzer(standard analyzer를 의미하는 듯하다. standard는 띄어쓰기 부분에서 split한 리스트를 저장하는 단순한 방식이다.)를 권장하기 위함이다.

자세한 설명을 읽기 힘든 분은 fuzzy search를 사용할 경우엔 해당 field는 default analyzer를 사용하라는 것만 기억하면 되겠다.

어쨋든 자세히 공식 문서의 예를 살펴보도록 하자. 먼저 이해해야 하는 부분은

text = 'The quick for jumped over the lazy dog.'

Snowball Analyzer를 이용해서 elasticsearch로 저장하려고 하면 실제로 저장하는 값은 위에서 볼 수 있듯이, quick, fox, jump, over, lazi, dog이며, 검색을 할 때면 실제 저장된 문장이 아닌 위와 같은 terms 중에서 찾게 된다는 점이다. 그러다보니 fuzzy query를 실행하게 되면 query text가 우리가 예상치 못한 단어와 비교한 분석 결과를 보여주기도 한다. 같은 이유로 원문에는 등장하지 않았던 유사어나 동의어를 이용해서도 찾을 수 있는 경우가 생긴다.

위의 이미지의 예제로 돌아가면, 검색하려는 문구로 lazzy를 넣었을 때 Snowball analyzer를 먼저 통과하고 lazzi로 analyzed query로 검색을 시작하게 된다. 이 경우에는 lazzilazi가 1의 거리에 있으므로 찾는 것을 성공하게 되는 것이다.

Implementation

복잡한 내용은 이 정도로 두고 먼저 코드를 살펴보도록 하자.

import elasticsearch
from elasticsearch_dsl import Search, Q, Index

# 먼저 elasticsearch 객체를 생성한다.
es = elasticsearch.Elasticsearch('localhost:9200')

s = Search(using=es, index='index_name')
s = s.query(
      'multi_match',
      query=name,
      fuzziness=2,
      fuzzy_transpositions=False,
      fields=['item_name', 'brand_name']
)
res = s.execute()
  • fuzziness의 값은 최대 거리를 의미한다. 2로 해놓으면 3이상 거리가 차이나는 것은 결과로 돌려주지 않는다.

공식 문서에서는 성능 문제상 1이나 2까지만 사용할 것을 권장하므로 더 큰 값을 사용하는 것은 주의하자.

  • fuzzy_transpositions가 위에서 설명했던 Levenshtein distance를 사용하도록 하는 방법이다. default는 True로 들어가있어서 기본적으로는 Damerau-Lavenshtein distance를 사용하게 된다.

Conclusion

기억해야 할 점은 3가지다.

  • fuzziness 값은 1, 2 중에 하나로 설정하도록 하자.
  • fuzzy_transpositions를 이용하면 edit distance 알고리즘을 변경할 수 있다.
  • fuzzy query를 사용해서 검색하게 될 field는 간단한 analyzer(그냥 default를 사용하면 되는 듯하다)를 사용하도록 하자.

이번 글에서는 간단하게 match query + fuzziness option에 대해서만 살펴봤는데, fuzzy query, fuzzy_like_this/fuzzy_like_this_field, suggesters 등의 방법으로 query를 날리는 방법도 있다. 이 방법들에 대해서는 다른 블로그에서 설명을 이어가도록 하겠다.