Elasticsearch를 이용한 자동 완성 프로그램 만들기 - Search Query

허석진·2024년 1월 7일
0

Elasticsearch

목록 보기
4/5
post-thumbnail
  1. 사용하는 elasticsearch와 kibana의 버전은 8.11.1입니다
    1-1. 8.x 버전 끼리는 크게 차이 없을 것으로 예상되나 이외의 버전에서 진행에 문제가 생긴다면 반드시 검색을 통해 확인해 봐야합니다

Elasticsearch를 이용한 자동 완성 프로그램 만들기 - Mapping편에 이어 작성하는 Query편!
이번 글에서는 먼저 자동완성을 위해 작성한 Query를 설명하고 추가로 해당 QuerySpring에서 Elasticsearch java low level client를 사용해 보내는 코드까지 설명할 예정이다.

그럼 바로 시작!

Search Query

우선, 사용하는 Query를 검색어에 한글이 포함된 경우, 그렇지 않은 경우 2개로 나눴다.
이유는 한글이 포함되지 않은 경우, 자모나 초성, 영한, 한글 분석기의 필드는 검색할 필요가 없기 때문이다.
반대로 한글이 포함된 경우, 영어 분석기를 적용한 필드는 검색할 필요가 없으므로 제외된다.

원래는 bool Queryshould를 사용했는데, 그렇게 쿼리를 구성하면, 하위 쿼리들의 스코어들을 모두 합산해서 반환하게 되어 원하는 문서가 검색되지 않았다.

그래서 아래와 같이 여러 하위 쿼리 중 가장 높은 스코어를 갖는 하위 쿼리에 의해 계산된 결과를 반환하는 dis_max 쿼리를 사용해 여러 필드에서 낮은 점수를 받는 것보다. 한 가지 필드에서 높은 점수를 받는 것이 높은 스코어를 받는데 유리하게했다.

검색어에 한글이 포함된 경우

GET /books/_search
{
  "size": 10,
  "query": {
    "dis_max": {
      "queries": [
        {"match": { "title":                  {"query": "검색어", "boost": "2"}}},
        {"match": { "title_chosung":          {"query": "검색어", "boost": "2"}}},
        {"match": { "title_jamo":             {"query": "검색어", "boost": "2"}}},
        {"match": { "title.kor":              {"query": "검색어", "boost": "1.2"}}},
        {"match": { "title.edge":             {"query": "검색어", "boost": "1"}}},
        {"match": { "title_chosung.edge":     {"query": "검색어", "boost": "1"}}},   
        {"match": { "title_jamo.edge":        {"query": "검색어", "boost": "1"}}},
        {"match": { "title.partial":          {"query": "검색어", "boost": "0.5"}}},
        {"match": { "title_chosung.partial":  {"query": "검색어", "boost": "0.5"}}},
        {"match": { "title_jamo.partial":     {"query": "검색어", "boost": "0.5"}}},
        {"match_phrase": { "title.kor":       {"query": "검색어", "boost": "1.5"}}}
      ]
    }
  },
  "highlight": {
    "fields": {
      "title": {
        "post_tags": ["</strong>"],
        "pre_tags": ["<strong>"]
      },
      "title.en": {
        "post_tags": ["</strong>"],
        "pre_tags": ["<strong>"]
      },
      "title.kor": {
        "post_tags": ["</strong>"],
        "pre_tags": ["<strong>"]
      },
      "title.edge": {
        "post_tags": ["</strong>"],
        "pre_tags": ["<strong>"]
      },
      "title.partial": {
        "post_tags": ["</strong>"],
        "pre_tags": ["<strong>"]
      }
    }
  }
}

검색어에 한글이 포함되지 않은 경우

GET /books/_search
{
  "size": 10,
  "query": {
    "dis_max": {
      "queries": [
        {"match": { "title":                  {"query": "search word", "boost": "2"}}},
        {"match": { "title_engtokor":         {"query": "search word", "boost": "2"}}},
        {"match": { "title.en":               {"query": "search word", "boost": "1.2"}}},
        {"match": { "title.edge":             {"query": "search word", "boost": "1"}}},
        {"match": { "title_engtokor.edge":    {"query": "search word", "boost": "1"}}},
        {"match": { "title.partial":          {"query": "search word", "boost": "0.5"}}},
        {"match": { "title_engtokor.partial": {"query": "search word", "boost": "0.5"}}},
        {"match_phrase": { "title.en":        {"query": "search word", "boost": "1.5"}}}
      ]
    }
  },
  "highlight": {
    "fields": {
      "title": {
        "post_tags": ["</strong>"],
        "pre_tags": ["<strong>"]
      },
      "title.en": {
        "post_tags": ["</strong>"],
        "pre_tags": ["<strong>"]
      },
      "title.kor": {
        "post_tags": ["</strong>"],
        "pre_tags": ["<strong>"]
      },
      "title.edge": {
        "post_tags": ["</strong>"],
        "pre_tags": ["<strong>"]
      },
      "title.partial": {
        "post_tags": ["</strong>"],
        "pre_tags": ["<strong>"]
      }
    }
  }
}

Boost Value 2인 필드

우선 위에 Boost Value를 2나 받고 있는 title, title_chosung, title_jamo, title_engtokor는 모두 keyword 타입의 필드들이다.

즉, 검색어와 완벽하게 일치한 경우만 탐색되기 때문에 높은 Boost Value를 부여했다.

Boost Value 1.5인 필드

다음으로 높은 Boost Value를 부여받은 필드는 match_phrase 쿼리의 title.kortitle.en이다.
match_phrase의 경우 analyzer로 분석한 검색어의 token의 순서가 문서의 token 순서와 동일한 경우에 높은 스코어를 받는다.

예를 들면, "quick brown fox"라는 제목의 책이 있으면, "quick fox"라고 검색한 경우 낮은 스코어를 받아 검색 결과에서 제외될 가능성이 높아지고, "brown fox"라고 검색한 경우 기존 문서의 token순서와 동일하기 때문에 높은 스코어를 받어 검색 결과 상위에 노출될 가능성이 높아진다.

따라서, 해당 쿼리에서 검색이 될 경우, 원하는 문서와 제목이 일치할 확률이 높기 때문에 완전 일치 다음가는 Boost Value인 1.5를 부여했다.

Boost Value 1.2인 필드

match 쿼리의 title.kortitle.en 필드는 Boost Value를 1.5 받은 쿼리의 필드와 동일하지만, match 쿼리를 사용하여 token의 순서를 고려하지 않기 때문에 상대적으로 적은 Boost Value를 부여했다.

하지만 다른 필드보다는 높은 Boost Value를 부여 받는 이유는 다른 필드들과 비교해 검색어가 한국어, 영어에서 각각의 언어에 맞는 적절한 토큰을 생성하는 언어 분석기를 사용했기 때문이다.

Boost Value 1인 필드

title.edge, title_chosung.edge, title_jamo.edge, title_engtokor.edge 필드들이 1의 Boost Value를 부여 받았다.

*.edge필드는 모두 whitespace로 구분된 단어들을 앞에서부터 1개씩 늘려가며 토큰화한 것들이다.
예를 들면, "This is a" -> "T", "Th", "Thi", "This", "i", "is", "a"와 같이 토큰화한다.

이런 필드는 원하는 문서의 앞부분만 입력하면 높은 스코어를 부여해 자동 완성 기능에 큰 도움을 주기 때문에 1의 Boost Value를 부여했다.

Boost Value 0.5인 필드

title.partial, title_chosung.partial, title_jamo.partial, title_engtokor.partial 필드들은 제일 작은 값인 0.5의 Boost Value를 부여 받았다.

*.partial 필드는 모두 ngram 방식을 사용해서 토큰화한 필드들인데, 주어진 텍스트에 대해서 너무 많은 경우의 수를 토큰으로 생성하다보니 원하지 않는 문서임에도 불구하고 토큰 적중률이 높아 기본적으로 높은 스코어를 얻어 상위에 노출되기 쉽다.

그럼에도 해당 필드가 존재하는 이유는, 위에 모든 경우에서 적절한 문서가 검색되지 않은 경우, SQL로 치면 like %검색어%과 같이 부분 일치에서

그래서 다른 쿼리들에 비교해 굉장히 낮은 값인 0.5을 Boost Value로 부여하게 되었다.

마무리

원래는 Java Spring에서 Java low level client를 설정하고 위에 쿼리를 코드로 작성하는 것까지 설명하려 했으나, 쿼리를 왜 그렇게 작성했는지 그 이유를 설명하는 것만으로 글이 꽤 길고 읽기 힘들어지는 것같아 다음 글에 나눠서 작성하는게 맞을 것 같다.

쿼리를 작성하면서 블로그에 글쓸 것을 상상할 때는 이렇게 길지 않았는데 직접 머리 속에 있던 것들을 직접 글로 만들어 늘어놓다보니 생각 보다 많이 길어지는거 같다 ㅋㅋ

0개의 댓글