같지만 다른 단어: 동의어로 Elasticsearch의 성능 강화

동의어 사용은 검색 엔지니어의 작업 도구 가운데 의심할 바 없이 가장 중요한 테크닉 중 하나입니다. 초보자는 이따금씩 그 중요성을 과소평가하지만, 거의 모든 실생활의 검색 시스템은 동의어 사용 없이는 작동할 수 없습니다. 동시에, 이러한 동의어 사용으로 인해 발생하는 일부 복잡성과 미묘함은 고급 사용자조차 과소평가하기도 합니다. 동의어 필터는 텍스트를 검색 가능한 용어로 변환하는 분석 프로세스의 일부이며, 시작하기가 비교적 쉽지만, 사용은 꽤 다양할 수 있으며 실제 시나리오에서 성공적으로 적용하기 전에 그 개념들에 대한 어느 정도 좀더 심층적인 이해가 필요할 수 있습니다.

최근 Elasticsearch에서의 분석에 대한 개선이 일부 이루어지고 있습니다. 가장 눈에 띄는 것은 아마도 검색 시간 분석기를 다시 로드할 수 있는 기능일 것입니다. 이 기능을 사용하면 검색 시 동의어를 변경하고 다시 로드할 수 있게 됩니다. 이 새로운 API를 제공하는 것에 더하여, 이 블로그에서는 동의어 사용에 대해 흔히 묻는 질문들 몇 가지에 대해 답해드리고, 사용 시 자주 발생하는 문제에 대한 주의 사항을 짚어드리겠습니다.

동의어를 사용하는 이유

동의어의 유용함과 유연함에 대해 이해하기 위해, 먼저 오늘날의 대부분의 검색 엔진이 내부적으로 어떻게 작동하는지를 잠시 살펴보겠습니다. 문서와 쿼리는 분석되고, 종종 토큰이라고 부르는 가장 작은 단위로 축소됩니다. 이 토큰은 본질적으로 추상적인 기호입니다. 검색할 때 일치 프로세스는 간단한 스트링 유사성을 사용하기 때문에 쿼리의 아주 작은 철자 오류(“hous”)나 어느 단어의 복수형 사용(“houses”)조차도 단수형(“house”)만 포함하는 문서와는 일치하지 않습니다. 형태소 분석기부분 일치 쿼리 같은 것들은 이 중 가장 흔히 발생하는 문제의 일부를 처리하지만, 관련 개념과 아이디어 간의 차이나 또는 문서와 쿼리에서 약간 다른 어휘를 사용하는 것으로 인한 차이를 채워주지는 않습니다.

바로 이 대목에서 동의어가 빛을 발합니다. 이 단어의 그리스어 어원은 접두어 σύν(syn, “함께”) ὄνομα(ónoma, “이름”)입니다. 이 용어의 어원에서 이미 동의어가 동일한 언어나 영역에서 정확히 또는 거의 동일한 의미를 갖는 다른 단어들을 설명하고 있음을 알 수 있습니다. 사실상, 이것은 일반적인 동의어(“tired”와 “sleepy”), 약어(“lb.”와 “pound”), 전자상거래 검색 시 제품의 다른 철자 변형(“iPod”과 “i-Pod”), 언어 간의 작은 차이(영국식 영어 “lift”와 미국식 영어 “elevator” 등), 전문가와 비전문가의 언어 차이(“canine”과 “dog”), 또는 동일한 개념을 단순히 두 가지 다른 말로 나타내는 것(“universe”와 “cosmos”) 등에 걸쳐 다양할 수 있습니다. 적절한 동의어 규칙을 제공함으로써, 검색 엔지니어는 각 영역에서 어느 단어가 유사한 것들을 의미하는지, 따라서 유사하게 다뤄져야 하는지에 대한 정보를 제공할 수 있습니다.

검색 엔진의 경우, 문서와 쿼리의 어느 용어들이 달라보여도 사실상 일치시켜야 하는 것인지를 아는 것이 중요합니다. 이것은 대단히 영역별로 특화되어 있기 때문에, 사용자들은 적절한 규칙을 제공해야 합니다. 동의어 필터사용자 정의 분석기에서 사용될 수 있는데, 색인 시에 색인되는 문서에서 예를 들어, 한 단어의 두 가지 변형을 저장할 수 있도록, 또는 쿼리 시에 쿼리 용어를 확장하고 더 많은 관련 문서를 일치시킬 수 있도록 사용자가 정의한 규칙을 대체하거나 이를 기반으로 추가 토큰을 더할 수 있습니다. 이러한 두 가지 접근법의 장점과 단점에 대해 나중에 좀더 다룰 것입니다.

동의어 사용에 대해 신중해져야 할 때

동의어 필터는 아주 유연한 도구이며, 사람들은 특정 상황에서 이 필터를 남용하게 되기도 합니다. 예를 들어, 동사와 명사의 문법적인 변형을 포함하는 대규모의 동의어 파일에서 형태소 분석기 대신 무차별적으로 사용될 때도 간간히 있습니다. 이러한 접근이 가능하긴 하지만, 성능은 보통 더 안좋으며, 유지 관리는 실제 형태소 분석기나 표제어 분석기를 사용할 때보다 더 어렵습니다. 철자 오류를 정정하는 데 있어서도 마찬가지입니다. 전자상거래 환경에서와 같이 대단히 흔한 철자 오류는 아주 소수이지만, 동의어를 사용해 이 철자 오류를 정정하려는 시도는 때로는 바람직합니다. 그러나 문제가 좀더 일반적인 경우에는 부분 일치 쿼리를 사용하거나 문자 ngram 기술을 사용하는 것이 좀더 지속가능한 접근입니다. 또한 분석 체인에서 동의어 확장에 대한 대안도 고려해보시기 바랍니다. 때로는 수집 파이프라인이나 다른 일부 클라이언트 쪽 프로세스에서 문서를 향상시키는 것이 훨씬 제한적인 분석 프로세스에서 동의어를 사용하는 것보다 한결 유연하며 쉽게 관리가 가능합니다. 예를 들어, 일반적인 개체명 인식(NER) 프레임워크를 사용하여 문서의 개체명을 검색하고 전처리 파이프라인에서 또는 수집 시에 고유한 식별자로 이를 인코딩할 수 있습니다. 그리고 나서 사용자의 쿼리를 Elasticsearch로 보내기 전에 동일한 프로세스를 적용하는 경우, 동일한 효과를 얻지만 일반적으로 한결 더 관리가 가능합니다.

아울러, 일반적인 용어 하에서 동물의 특정 종을 그룹화하는 것이나 또는 특정 영역에 대한 분류 지원을 구축하는 것 같이, “동일성”의 다른 개념에 대해 동의어를 사용하고 싶어질 수도 있습니다. 이 부분이 정말로 흥미로워지는 지점이며, 탐색해 볼 만한 것들이 훨씬 더 많습니다. 그러나 동의어가 언제나 최선의 선택은 아니며, 신중하게 사용되지 않는다면 시스템이 예상치 못한 방식으로 작동하게 될 수도 있음을 염두에 두셔야 합니다.

색인 시와 검색 시 동의어 사용 비교

동의어는 색인 시에 또는 검색 시에 사용될 수 있는 분석기에서 사용됩니다. Elasticsearch에서 동의어 필터 사용에 대해 가장 자주 묻는 질문 중 하나는 “색인 시에 사용해야 하나요, 검색 시에 사용해야 하나요, 아니면 둘 다 사용해야 하나요?”입니다. 먼저 색인 시에 동의어 필터를 적용하는 것을 살펴봅시다. 이것은 색인되는 문서의 용어들이 최종적으로 대체되거나 확장되며, 그 결과는 검색 인덱스에서 계속 지속된다는 뜻입니다.

색인 시 동의어는 여러 가지 단점이 있습니다.

  • 인덱스가 더 커질 수 있습니다. 모든 동의어가 색인되어야 하기 때문입니다.
  • 용어 통계에 의존하는 검색 점수는 동의어들도 개수 계산에 포함되기 때문에 문제가 생길 수 있으며, 좀더 일반적으로 사용되지 않는 단어들에 대한 통계는 왜곡됩니다.
  • 동의어 규칙은 재색인 없이 기존 문서에 대해 변경될 수 없습니다.

특히, 마지막 두 가지는 큰 단점입니다. 색인 시 동의어의 가장 잠재적인 장점은 성능입니다. 확장 프로세스에 대한 대가를 미리 다 지불하며, 쿼리 시마다 매번 수행해야 할 필요가 없기 때문에, 일치되어야 하는 좀더 많은 용어들이 잠재적으로 결과에 포함될 수 있습니다. 하지만 이것이 실제로 그렇게 큰 문제가 되지는 않습니다.

반면에, 검색 시 분석기에서 동의어를 사용하면 위에서 언급한 문제들 중 많은 부분이 발생하지 않습니다.

  • 인덱스 크기에 영향을 미치지 않습니다.
  • 모음의 용어 통계가 동일하게 유지됩니다.
  • 동의어 규칙이 변경되어도 문서를 재색인할 필요가 없습니다.

쿼리 시마다 매번 동의어 확장을 수행해야 하며 잠재적으로 일치시켜야 하는 단어가 더 많다는 유일한 단점보다 이러한 장점이 보통 훨씬 더 큽니다. 뿐만 아니라, 검색 시 동의어 확장은 보다 정교한 synonym_graph 토큰 필터의 사용을 감안한 것인데, 이것은 여러 단어의 동의어를 정확하게 처리할 수 있고, 검색 분석기의 일환으로서만 사용되도록 설계된 것입니다.

일반적으로, 검색 시 동의어 사용의 장점은 보통 색인 시 동의어를 사용할 때 얻을 수 있는 약간의 성능 상의 이득보다 앞섭니다.

그러나, 검색 시 동의어를 사용할 때는 알아두어야 할 점이 예전에는 하나 더 있었습니다. 동의어 규칙을 바꾸는 데 문서 재색인이 필요하진 않더라도, 동의어 규칙을 바꾸기 위해서는 일시적으로 인덱스를 닫았다가 다시 열어야 했습니다. 노드가 다시 시작되거나 닫힌 인덱스가 다시 열릴 때, 인덱스 생성 시에 분석기가 인스턴스화되기 때문에 이 작업이 필요했습니다. 동의어 규칙 파일의 변경 사항이 인덱스에 나타나도록 하려면, 먼저 모든 노드에서 파일을 업데이트한 다음 인덱스를 닫았다가 다시 열어야 했습니다. 하지만 더 이상 이렇게 할 필요가 없습니다.

다시 로드되는 동의어

Elasticsearch 7.3부터, 동의어 파일에서 변경 사항을 보기 위해 인덱스를 다시 열어야 하는 이런 번거로움이 더 이상 필요하지 않습니다. 우리는 필요할 때 분석기 리소스의 다시 로드를 트리거하도록 새로운 엔드포인트를 추가했습니다. 이 새로운 엔드포인트를 호출하면 업데이트 가능으로 표시되는 구성 요소가 있는 인덱스의 모든 분석기가 다시 로드됩니다. 그리고 나서 이제 그러한 구성 요소가 검색 시에만 사용 가능하도록 합니다.

동의어 필터의 경우, 업데이트 가능으로 표시하고 다시 로드 API를 호출하면 분석 프로세스에서 보이는 각 노드마다 동의어 구성 파일에 변경이 이루어집니다. (synonyms 매개변수를 통해) 필터 정의의 일부인 동의어 규칙을 업데이트하는 것은 가능하지 않지만, 임시 테스팅 목적으로는 대부분 사용되어야 합니다. 어떤 경우든, 구성 파일을 사용해 동의어를 구성하는 것에는 다음과 같이 몇 가지 장점이 있습니다.

  • 관리하기가 더 쉽습니다! 프로덕션 시스템에서는 수많은 동의어 규칙이 있을 수 있으며, 그러한 규칙이 검색 정확도에 큰 영향을 미치기 때문에, 모든 업데이트와 함께 버전을 관리하고 테스트해야 하는 구성의 중요한 부분으로 취급되어야 합니다.
  • 동의어는 종종 다른 소스에서 파생되거나 데이터에서 실행되는 알고리즘에 의해 생성됩니다. 파일로부터 읽으면 필터 구성에 이를 넣어야 할 필요를 생략할 수 있습니다.
  • 동일한 동의어 파일이 다른 필터에서 사용될 수 있습니다.
  • 더 큰 동의어 규칙 세트는 인덱스 설정에 대한 메타 정보를 저장하는 Elasticsearch 클러스터 상태에서 메모리를 많이 차지합니다. 불필요하게 클러스터 크기를 증가시키지 않기 위해서는, 구성 파일에 더 큰 동의어 규칙 세트를 저장하는 것이 바람직합니다.

데모 목적으로, 다음의 단일한 규칙을 포함하는 초기 my_synonyms.txt 파일을 Elasticsearch 노드의 config 디렉터리에 더한다고 가정해 보겠습니다. 처음에는 파일에 다음 규칙만 포함되어 있다고 가정해 봅시다.

universe, cosmos

그 다음으로, 동의어 필터에서 이 파일을 참조하는 분석기를 정의해야 합니다.

PUT /synonym_test
{
  "settings": {
    "index": {
      "analysis": {
        "analyzer": {
          "synonym_analyzer": {
            "tokenizer": "whitespace",
            "filter": ["my_synonyms"]
          }
        },
        "filter": {
          "my_synonyms": {
            "type": "synonym",
            "synonyms_path": "my_synonyms.txt",
            "updateable": true
          }
        }
      }
    }
  }
}

동의어 필터에 updateable로 표시한 것을 눈여겨 보세요. 이것이 중요합니다. 다시 로드되는 새로운 엔드포인트를 호출할 때 업데이트 가능한 파일만이 다시 로드되기 때문입니다. 그러나, 여기에는 또한 업데이트 가능한 필터를 포함하는 분석기는 색인 시에 더 이상 사용될 수 없다는 부작용도 있습니다. 그러나 _analyze 엔드포인트를 통해 간단한 테스트를 실행하여 먼저 동의어가 정확하게 적용되는지 확인해 봅시다.

GET /synonym_test/_analyze
{
  "analyzer": "synonym_analyzer",
  "text": "cosmos"
}

이것은 두 개의 토큰을 반환해야 합니다. 또한 하나는 예상대로 “universe”입니다. 두 번째 줄을 추가하여 이제 다른 규칙을 synonyms.txt 파일에 더해봅시다.

lift, elevator

여기가 예전에 이러한 변경 사항이 나타나도록 하기 위해 인덱스를 닫았다가 다시 열어야 했던 지점입니다. 이제는 다음의 새로운 엔드포인트를 호출하기만 하면 됩니다.

POST /synonym_test/_reload_search_analyzers

이 요청은 본문이 필요하지는 않지만 통상적인 인덱스 와일드카드 패턴을 사용하여 하나 이상의 인덱스로 제한될 수 있습니다. 응답에는 어느 분석기가 다시 로드되었는지 그리고 어느 노드가 영향을 받았는지에 대한 정보가 포함됩니다.

{
  [...],
  "reload_details": [{
    "index": "synonym_test",
    "reloaded_analyzers": ["synonym_analyzer"],
    "reloaded_node_ids": ["FXbmbgG_SsOrNRssrYcPow"]
  }]
}

용어 “lift”에 대해 위의 _analyze 요청을 실행하면 이제 두 번째 동의어 토큰으로 “elevator”를 반환합니다.

하지만 몇 가지 기억해 두어야 할 사항이 있습니다. 위에서 언급한 대로, updateable이라고 표시된 필터는 검색 시에 사용되어야 합니다. 따라서 우리가 필드에 대해 위에서 정의한 동의어 분석기를 사용하는 정확한 방법은 다음과 같습니다.

POST /synonym_test/_mapping
{
  "properties": {
    "text_field": {
      "type": "text",
      "analyzer": "standard",
      "search_analyzer": "synonym_analyzer"
    }
  }
}

또한, 다시 로드하는 것은 파일로부터 로드되는 동의어에 대해서만 작동합니다. 필터에서 설정을 통해 정의된 동의어를 변경하는 것은 지원되지 않습니다. 끝으로, 실제로는 클러스터의 모든 노드에 걸쳐 동의어 파일에 업데이트를 적용하는지 확인해야 합니다. 일부 노드의 분석기가 파일의 다른 버전을 보는 경우, 검색에 어느 노드가 사용되는지에 따라 다른 검색 결과를 얻을 수도 있습니다. 동의어와 관련해 이런 일이 발생하면, 각 노드에서 동의어 파일이 동일한지를 제일 먼저 확인해야 합니다 그리고 나서 다시 로드를 다시 트리거합니다.

요약하자면, 새로운 _reload_search_analyzer 엔드포인트는 인덱스를 다시 열 필요 없이 쿼리 시 동의어를 신속하게 수정하고 변경할 수 있게 해줍니다. 예를 들어, 쿼리 로그를 분석함으로써, 사용자가 색인된 문서에 존재하지 않는 다른 용어들을 검색하는지를 확인할 수 있습니다. 그러나 동의어를 추가하면 정확도 점수에 예기치 못한 부작용이 발생할 수 있습니다. 따라서 프로덕션에서 바로 변경 사항을 적용하기 전에 먼저 일종의 테스팅(A/B 테스팅 또는 순위 평가 API 같은 것)을 수행해보는 것이 바람직합니다.

(분석) 체인의 활용

동의어 필터에 대한 또 하나의 자주 묻는 질문은 보다 복잡한 분석 체인에서의 작동입니다. 대부분의 시나리오에서는, 소문자 필터처럼 일부 흔한 문자나 토큰 필터를 동의어 필터 앞에 놓게 됩니다. 이것은 분석 체인을 통과하는 모든 토큰이 동의어 필터를 적용하기 전에 소문자가 된다는 뜻입니다. 이것은 동의어 규칙의 입력 동의어가 일치되기 위해서는 역시 소문자여야 한다는 뜻일까요? 이 간단한 예제를 살펴보겠습니다.

PUT /test_index
{
  "settings": {
    "index": {
      "analysis": {
        "analyzer": {
          "synonym_analyzer": {
            "tokenizer": "whitespace",
            "filter": ["lowercase", "my_synonyms"]
          }
        },
        "filter": {
          "my_synonyms": {
            "type": "synonym",
            "synonyms": ["Eins, Uno, One", "Cosmos => Universe"]
          }
        }
      }
    }
  }
}
GET /test_index/_analyze
{
  "analyzer": "synonym_analyzer",
  "text": "one"
}

소문자 입력 텍스트가 위의 예제에서 세 개의 토큰으로 확장되는 것을 확인할 수 있습니다. 이것은 소문자화 또한 동의어 필터 규칙에 적용됨을 보여줍니다. 아울러, 위의 “Cosmos => Universe” 같은 교체 규칙의 오른쪽은 다음의 소문자 출력에서 볼 수 있듯이 다시 작성됩니다.

GET /test_index/_analyze
{
  "analyzer": "synonym_analyzer",
  "text": "cosmos"
}

일반적으로, 동의어 필터는 선행 분석 체인에서 사용되는 토크나이저 및 필터에 대한 입력을 다시 작성합니다. 그러나, 여기에는 다음과 같이 주목할 만한 예외도 몇 가지 있습니다. (common_grams 또는 phonetic 필터처럼) 스택화된 토큰을 출력하는 일부 필터는 동의어 필터에 선행할 수 없으며, 이를 시도할 경우 오류가 발생하게 됩니다. 단어 복합 필터 또는 동의어 필터 그 자체처럼 다른 경우에는 체인에서 또 다른 동의어 필터에 선행할 때 건너뛰어집니다. 후자 규칙은 동의어 필터 체인이 가능하게 하는 데 중요합니다. 다음 예제에서 이것이 어떻게 작동하는지 보겠습니다.

그럼 두 개 이상의 동의어 필터를 나란히 놓으면 어떻게 될까요? 전자의 출력이 후자의 입력이 되어서, 어느 정도 전이적으로 작동하는 동의어 필터 체인을 만들게 될까요? 다음 예제를 살펴봅시다.

PUT /synonym_chaining
{
  "settings": {
    "index": {
      "analysis": {
        "filter": {
          "first_synonyms": {
            "type": "synonym",
            "synonyms": ["a => b", "e => f"]
          },
          "second_synonyms": {
            "type": "synonym",
            "synonyms": ["b => c", "d => e"]
          }
        },
        "analyzer": {
          "synonym_analyzer": {
            "filter": [
              "first_synonyms",
              "second_synonyms"
            ],
            "tokenizer": "whitespace"
          }
        }
      }
    }
  }
}
GET /synonym_chaining/_analyze
{
  "analyzer": "synonym_analyzer",
  "text": "a"
}

출력 토큰이 “c”가 되며, 이는 연이은 순서로 양쪽 필터가 적용됨을 보여줍니다. 첫 번째 필터는 “a”를 “b”로 교체하고, 두 번째는 이 입력을 “c”와 교체합니다. 대신에 입력으로 “d”를 넣는다면, 이것은 “e”로 교체되지만(첫 번째 규칙이 적용되지 않음) 대신에 “e”를 사용한다면, 토큰은 첫 번째 필터에서 “f”와 교체되고 두 번째 필터는 일치된 것 없이 남겨지게 됩니다.

조금 전에 선행 토큰 필터에 대한 다시 작성의 예외 사항에 대해 얘기했던 것을 기억하시나요? 위의 예제에서 second_synonyms 필터가 첫 번째 필터의 규칙을 그 규칙 세트에 적용했더라면, 자체의 d => e 규칙이 d => f으로 변경되었을 것입니다. (선행 필터의 e => f 규칙이 적용되었을 것이기 때문입니다.) 이전 버전의 Elasticsearch에서 혼란스럽게 만드는 원인이었던 이 동작이 바로 이제 동의어 필터가 다음 필터의 동의어 규칙을 처리할 때 건너뛰어지는 이유입니다. 이것은 6.6 버전부터 설명한 대로 작동하게 됩니다.

앞으로의 기대

이 짧은 블로그에서, 우리는 동의어를 사용하여 할 수 있는 것을 대략적으로나마 살펴보고, 그 사용과 관련해 자주 묻는 질문 몇 가지에 대해 답변해 보고자 했습니다. 동의어는 검색 시스템의 재현율을 증가시키기 위해 활용될 수 있지만, 알아두고 실험해보아야 할 미묘하고 중요한 사항들이 많이 있습니다. 특히, 시스템의 정확도 테스팅과 관련해 그렇습니다.

Elasticsearch 7.3에서 추가된 검색 시 분석기를 다시 로드하는 새로운 API 덕분에 과거에 그랬던 것처럼 인덱스를 닫고 다시 열어야 할 필요가 없이 이런 종류의 실험을 좀더 손쉽게 할 수 있게 되었습니다. 또한 이 API는 인덱스를 오프라인 상태로 전환하지 않고도 검색 시 적용되는 동의어 규칙을 업데이트할 수 있는 방법도 제공합니다. 그러나, 이것은 대규모 클러스터에 걸쳐 사용자가 좀더 손쉽게 동의어 관리를 할 수 있도록 하기 위해 도입하고자 하는 일련의 개선 사항 중 첫 단계일 뿐입니다. 어떻게 생각하시는지 알려주시고, 저희 토론 포럼에 피드백이나 질문을 남겨주시기 바랍니다. 그때까지 즐거운 분석 시간 되세요!