ElasticSearch 에서 검색을 위한 가장 기본적인 쿼리 DSL 을 작성한다고 하면 대부분 match 쿼리나 term 쿼리를 베이스로 작성합니다. 하지만 저는 이 두 쿼리의 차이를 명확히 이해하지 않은 채 “keyword 필드 검색할 때는 term 쿼리 쓰는구나~” 정도로만 대충 짚고 넘어갔습니다. 사실 대부분의 경우 match 쿼리만 써도 문제가 없었거든요.
하지만 도구를 제대로 이해하지 않고 사용하다보면 언젠가 예기치 못한 결과를 마주하기 마련이죠. 평소보다 조금 복잡한 쿼리를 작성하고 실행하는데 제가 생각했던 결과가 나오지 않는겁니다. 분명 해당 쿼리로 조회될거라 생각했던 문서가 조회되지 않았습니다. 쿼리의 동작을 잘못 이해한거죠. 그래서 기본적인 match, term 쿼리의 동작을 자세히 정리해봤습니다.
match vs term
match 와 term 쿼리 모두 특정 필드의 내용이 질의어와 일치하는 문서를 찾는데 사용합니다. 하지만 일치의 여부를 어떻게 찾는지에 대한 그 세부사항은 굉장히 다릅니다.
match query
match 쿼리는 지정한 필드가 질의어와 매치되는 문서를 찾는 쿼리입니다. 중요한건, 만약 지정한 필드에 analyzer 가 존재하거나 search_analyzer 가 지정되어있다면 질의어 또한 analyzing 과정을 거칩니다.
GET index_for_search/_search
{
"query": {
"match": {
"fieldForSearch": {
"query": "this is something"
}
}
}
}
위에서 index_for_search 인덱스의 fieldForSearch 필드가 text 타입이고 standard analyzer 를 사용한다면 질의어인 “this is something” 이라는 텍스트는 “this”, “is”, “something” 이라는 3개의 텀으로 분리되어 역색인 검색이 수행됩니다.
여기서 또 주의할점은 match 쿼리는 기본적으로 OR 로 동작한다는 겁니다. 이말인 즉슨, “this”, “is”, “something” 3개의 텀으로 검색을 수행한 후 셋 중 하나의 검색에서만 나온 문서라도 결과로 인정된다는 뜻입니다. 물론 아래처럼 operator 필드 값을 통해 동작을 AND 로 수정할 수도 있습니다.
GET index_for_search/_search
{
"query": {
"match": {
"fieldForSearch": {
"query": "this is something",
"operator": "and"
}
}
}
}
그리고 또 match 쿼리는 아래처럼 fuzziness 옵션을 통해 Fuzzy 검색(유사성 검색)도 수행할 수 있습니다.
GET index_for_search/_search
{
"query": {
"match": {
"fieldForSearch": {
"query": "this is something",
"fuzziness": "AUTO"
}
}
}
}
내부적으로 Levenshtein distance 알고리즘을 사용해서 단어간 유사도를 측정합니다. 이 유사도를 어디까지 허용할건지 그 정도를 fuzziness 필드를 통해 조절합니다. 0,1,2 사이의 값으로 명확히 지정하거나 단어의 길이에 따라 유동적으로 조절하는 AUTO 옵션을 사용할 수 있습니다.
term query
term 쿼리는 지정한 필드가 질의어와 정확히 일치하는 문서를 찾는 쿼리입니다. analyzer 처리 과정은 거치지 않으나 대상 필드에 normalizer 가 적용되어 있다면 질의어도 동일하게 normalizer 처리 과정을 거칩니다.
GET index_for_search/_search
{
"query": {
"term": {
"fieldForSearch": {
"value": "Hello"
}
}
}
}
analyzer 처리 과정을 거치지 않고 말그대로 질의어와 정확히 일치하는 값을 찾는데에 사용됩니다. 그래서 keyword 필드에 검색을 수행할 때 주로 사용합니다. 게다가 keyword 필드는 normalizer 를 구성할 수 있기에 색인과 검색이 직관적이기도 합니다.
analyzer 과정이 없는 탓에 일반적으로 생각하는 ‘전문검색’을 수행하기엔 적합하지 않습니다. 그리고 문제는 여기서 발생하죠. term 쿼리와 match 쿼리의 차이를 제대로 이해하지 않고 term 쿼리로 전문검색을 기대하고 쿼리를 수행하면 예상했던 결과가 나오지 않을겁니다.
실험
실제로 쿼리를 날려보면서 차이를 비교해봅시다. 아래처럼 keyword 필드와 text 필드 각각 하나씩 가진 인덱스를 하나 생성했습니다. keyword 필드는 기본적으로 아무런 설정을 하지 않으면 normalizer 가 없기 때문에 확실한 비교를 위해 lowercase normalizer 를 keyword_field 에 추가했습니다.
PUT /index_for_test
{
"mappings": {
"properties": {
"keyword_field": {
"type": "keyword",
"normalizer": "lowercase"
},
"text_field": {
"type": "text"
}
}
}
}
그 “Hello, World!” 라는 문자열을 두 필드에 동일하게 입력한 문서를 하나 저장합니다.
POST /index_for_test/_doc
{
"keyword_field": "Hello, World!",
"text_field": "Hello, World!"
}
각 필드가 어떤 형태로 색인되었을지 상상이 되시나요? 워낙 간단한 인덱스라 어떤 형태로 저장되었을지 머릿속에 그려지긴 하지만 그래도 혹시 모르니 직접 확인해봅시다.
ElasticSearch 에는 특정 문서의 역색인 형태를 확인할 수 있는 **Term vectors API 를 제공합니다. 아래처럼 문서 ID 와 확인할 필드 이름으로 조회합니다.
GET /index_for_test/_termvectors/rhY9I4oBvzqFsTDTSC2O?fields=text_field
그럼 아래처럼 해당 문서의 특정 필드가 어떻게 역색인 되어있는지 텀별로 보여줍니다.
{
"_index": "index_for_test",
"_id": "rhY9I4oBvzqFsTDTSC2O",
"_version": 1,
"found": true,
"took": 0,
"term_vectors": {
"text_field": {
"field_statistics": {
"sum_doc_freq": 2,
"doc_count": 1,
"sum_ttf": 2
},
"terms": {
"hello": {
"term_freq": 1,
"tokens": [
{
"position": 0,
"start_offset": 0,
"end_offset": 5
}
]
},
"world": {
"term_freq": 1,
"tokens": [
{
"position": 1,
"start_offset": 7,
"end_offset": 12
}
]
}
}
}
}
}
terms 항목을 보시면 “hello” 와 “world” 라는 두개의 텀으로 나눠서 저장된 모습이 보입니다.
keyword_field 까지 확인해봅시다.
GET /index_for_test/_termvectors/rhY9I4oBvzqFsTDTSC2O?fields=keyword_field
{
"_index": "index_for_test",
"_id": "rhY9I4oBvzqFsTDTSC2O",
"_version": 1,
"found": true,
"took": 0,
"term_vectors": {
"keyword_field": {
"field_statistics": {
"sum_doc_freq": 1,
"doc_count": 1,
"sum_ttf": 1
},
"terms": {
"hello, world!": {
"term_freq": 1,
"tokens": [
{
"position": 0,
"start_offset": 0,
"end_offset": 13
}
]
}
}
}
}
}
“hello, world!” 라는 텀이 생성된게 보입니다. keyword 필드라서 analyzer 절차는 없고 미리 설정해놓은 lowercase normalizer 만 적용되었습니다.
이제 위 인덱스에 match 와 term 쿼리를 각각 날려서 실제 동작을 확인해봅시다.
- text 필드, match 쿼리
// 쿼리
GET /index_for_test/_search
{
"query": {
"match": {
"text_field": "Hello, World!"
}
}
}
// 결과
"hits": [
{
"_index": "index_for_test",
"_id": "rhY9I4oBvzqFsTDTSC2O",
"_score": 0.5753642,
"_source": {
"keyword_field": "Hello, World!",
"text_field": "Hello, World!"
}
}
]
일반적인 형태입니다. text_field 에 적용된 standard analyzer 가 동작해서 질의어는 hello, world 텀을 생성하고 검색을 수행합니다.
2. text 필드, term 쿼리
// 쿼리
GET /index_for_test/_search
{
"query": {
"term": {
"text_field": "Hello, World!"
}
}
}
// 결과
"hits": []
// 쿼리
GET /index_for_test/_search
{
"query": {
"term": {
"text_field": "Hello"
}
}
}
// 결과
"hits": []
// 쿼리
GET /index_for_test/_search
{
"query": {
"term": {
"text_field": "hello"
}
}
}
// 결과
"hits": [
{
"_index": "index_for_test",
"_id": "rhY9I4oBvzqFsTDTSC2O",
"_score": 0.2876821,
"_source": {
"keyword_field": "Hello, World!",
"text_field": "Hello, World!"
}
}
]
text 필드인데 term 쿼리로 검색해서 analyzer 가 동작하지 않아 “Hello, World!”, “Hello” 와 같은 질의어로는 문서가 검색되지 않았고, “hello” 처럼 실제 역색인 된 텀의 본래 형태로 질의했더니 검색되었습니다.
3. keyword 필드, match 쿼리
// 쿼리
GET /index_for_test/_search
{
"query": {
"match": {
"keyword_field": "Hello, World!"
}
}
}
// 결과
"hits": [
{
"_index": "index_for_test",
"_id": "rhY9I4oBvzqFsTDTSC2O",
"_score": 0.2876821,
"_source": {
"keyword_field": "Hello, World!",
"text_field": "Hello, World!"
}
}
]
keyword 필드에 match 쿼리로 검색을 수행합니다. normalizer 가 동작해서 “Hello, World!” 형태로 질의해도 문서가 검색됩니다.
4. keyword 필드, term 쿼리
// 쿼리
GET /index_for_test/_search
{
"query": {
"term": {
"keyword_field": "Hello, World!"
}
}
}
// 결과
"hits": [
{
"_index": "index_for_test",
"_id": "rhY9I4oBvzqFsTDTSC2O",
"_score": 0.2876821,
"_source": {
"keyword_field": "Hello, World!",
"text_field": "Hello, World!"
}
}
]
keyword 필드를 검색하는 일반적인 형태죠. term 쿼리로 검색을 수행합니다. 당연히 normalizer 는 잘 동작했고 문서 검색도 성공적입니다.
위 실험 결과를 토대로 match, query 의 text, keyword 필드에 대한 동작을 표로 정리하면 아래와 같습니다.
정리
match-text, term-keyword 형태는 일반적이라 예상대로 동작했지만 각각의 반대 형태는 (그렇게 사용하는 경우도 잘 없겠지만) 사용시 주의가 필요합니다. term 쿼리를 text 필드에 사용하면 analyzer 가 동작하지 않아 질의어 원본 형태 그대로 검색이 수행되어 예상과는 다른 검색 결과가 나올 수 있습니다.
그래서 ElasticSearch 공식 문서의 Term query 항목을 보면 아래처럼 term 쿼리를 text 필드에 사용하는건 피하라는 주의사항이 적혀있습니다.
다만 여기서 한가지 의문이 들었습니다. “왜 match 쿼리로 keyword 필드를 검색할 때는 normalizer 가 제대로 동작하게 만들었을까?”
그래서 조금 생각해봤는데요.
term 쿼리의 설계 의도는 “질의어 큰 수정을 가하지 않고 단어 그대로 검색하기 위함” 입니다. 그래서 keyword 필드처럼 텀 한개의 형태로 저장되는 필드의 검색에 알맞죠. 반대로 match 쿼리는 “전문 검색” 입니다. 질의어에 도 analyzer 를 적용하여 질의어로 검색 가능한 모든 문서를 검색하는게 그 목적인겁니다. 이를 위해 fuzziness 같은 옵션도 제공하죠.
그래서 keyword 필드에 term 쿼리로 검색하는 형태가 자연스럽긴 하지만 match 쿼리로 검색한다고 해서 normalizer 가 적용이 되지 않게 만드는 건 match 쿼리의 설계 의도와 맞지 않습니다.