필자는 Elasticsearch(이하 ES)를 기반으로 검색엔진 개발하는 일을 하고 있다.

    ES를 통해 검색엔진을 개발 하면서 글로 남기고픈 여러 주제들이 있었는데, 그 중 첫 번째로 bool query에 대해서 글을 남기고자 한다.

    사실 연재하려고 하지만 다음편은 영원히 나오지 않을 수 있다 이 글도 개인 사정으로 작성한 지 6개월 만에 블로그에 올리게 되었다.


    검색쿼리가 헷갈리거나, 검색 결과가 생각과 다를 때마다 ES Definitive GuideES Reference를 가장먼저 참고하곤 한다.

    ES Definitive Guide의 경우 최신 버전의 ES에 맞지 않는 내용도 있다.

    렇지만 ES를 공부하는데 여전히 좋은 문서 인건 확언 할 수 있다.

     

    필자가 개발하면서 가장많이 참조한 페이지 중 하나는 bool query라고 확신하다.
    "겨우 bool query를 제일 많이 찾아보다니!" 라고 하는 사람도 있을 것이다.

     

    ES를 공부해본 사람이라면 거의 가장 처음 접하는 쿼리가 match query와 bool query가 아닐까 싶다. 그만큼 사용하기 쉽고, 원하는 결과를 도출 할 수 있게끔 강력한 검색 쿼리를 만들어준다. Bool query는 compound query의 대표격으로 compound 또는 leaf query를 결합하기 위하여 자주 사용되며, compound query 중에서도 사용이 가장 쉽다.

     

    이렇게 쉬운 bool query에 대해서 글을 작성하고 있는 이유는 bool query의 동작이 context에 따라 달라질 수 있고, 이런 동작을 다시 사용자 의도에 맞게 변경할 수 있으며, 최근에 생각과 다르게 동작한 경험을 공유하고 싶어서 이다.

     

    그럼 bool query를 사용해본 개발자에게 먼저 질문을 하나 하려고 한다.
    "bool query에서 must, filter, should, must_not의 차이를 설명해 보시오."

     

    대부분 정답을 말할 수 있을 것이다.
    "must는 반드시 포함 하는 조건이고, filter는 필터링 할 조건이고, should는 여러개 중 하나를 포함 할 것이고, 등등..."

     

    그럼 여기서 다시 질문하여 "must와 filter의 차이를 설명해 보시오"라고 한다면 대답하는 이는 아주 아주 조금, 아주 약간 줄어들 것이다.
    "must는 score에 영향을 끼치고, filter는 score에 영향을 끼치지 않고.... 블라블라"

     

    사실 이 정도만 이해하고 있어도 충분히 잘 알고 있다고 할 수 있다. 적어도 자신이 만든 쿼리의 점수가 어떻게 계산 될지 유추 할 수 있으니 말이다.


    그럼 조금 더 must와 filter의 차이를 알아보자(

    오늘도 서론이 매우 길다ㅠㅠ

    )

     

    must cluase와 filter cluase는 동작하는 context가 서로 다르다.
    (사실 이 부분도 대부분의 개발자가 알고 있는 내용이겠지만, 지금부터는 그냥 하려던 말을 쭉 서술 하려고 한다)

     

    must는 query context, filter는 filter context에서 동작한다. 편하게 설명하려고 must와 filter에 관해서만 이야기했지만, should는 query context, must_not는 filter context다.

     

    ES 1.x나 2.x까지는 filter와 query가 나뉘어져 있었다(filtered query 등이 존재 했었다.

    이제는 구시대 유물 느낌이다

    )(사실 ES 2.0 부터 query와 filter가 합쳐졌고, filter는 deprecated 된 상태로 사용 할 수 있었다).

    query와 filter의 중요한 차이점 중 하나는 cache를 할 수 있는지 여부다. 이는 filter와 query가 나뉘어져있을 때도, 현재의 filter 또는 query가 context에 따라 동작할 때에도 마찬가지다.
    최근 버전의 ES에서는 context에 따라서 알아서 filter context로 동작하고 cache가 되므로 좀더 똑똑하게 동작한다. ES 아주 칭찬해 : )

     

    다시 bool query로 돌아와 글을 이어가자면, must와 should에서는 score 계산을 해야 하므로 query context, 즉 cache가 동작하지 않고, filter와 must_not에서는 score 계산이 필요 없으므로 fitler context로 동작해 cache의 득을 볼 수 있다.
    query context와 filter context의 자세한 설명은 Query and filter Context를 참고 하면 된다.

     

    사실 본 글에서 이야기 하고자 하는 bool query의 내용도 reference 문서인 bool query에 자세히 나와있다.
    (본 글의 최종 목적지인 filter context에서의 should clause 동작 내용은 5.3 이전 버전에도 언급되어 있긴 하지만, 5.3 버전 이후의 reference 부터 조금 더 명확하게 설명이 추가 되었다.)


    본 글을 읽는 대부분의 개발자가 bool query에 should만 있는 경우와 filter(또는 must)와 함께 should가 있는 경우의 차이를 잘 알고 있을 것이다.
    should만 있는 경우라면 should clause에 적어도 하나가 match 되어야 하지만 filter(또는 must)가 함께 왔을 때는 should clause가 하나도 match 되지 않더라도 검색 대상이 될 수 있다.

     

    하지만 위 동작은 전제 조건이 있다. bool query 전체가 query context에 있을 때만 위와 같이 동작하고, filter context에 있을 때에는 동작이 상이하다.

     

    만약 bool query가 filter context에 있다면?
    filter나 must와는 관계 없이 적어도 하나의 should clause가 match 되어야 한다.

     

    위 동작을 좀더 자세히 알아보기 위하여 아래와 같이 index를 생성 후 검색 해보자(물론 억지로 든 예시다)

     

    • 인덱스 생성 쿼리

      PUT /test_index
      {
      "settings": {
        "index": {
          "number_of_shards": 1,
          "number_of_replicas": 0
        }
      },
      "mappings": {
        "test_type": {
          "_all": {
            "enabled": false
          },
          "properties": {
            "filed_for_search": {
              "type": "text"
            },
            "field_for_filter": {
              "type": "integer"
            }
          }
        }
      }
      }
    • 데이터 색인

      POST _bulk
      { "index" : { "_index" : "test_index", "_type" : "test_type", "_id" : "doc_1" } }
      { "field_for_search": "hello_1", "field_for_filter": 100 }
      { "index" : { "_index" : "test_index", "_type" : "test_type", "_id" : "doc_2" } }
      { "field_for_search": "hello_2", "field_for_filter": 100 }
      { "index" : { "_index" : "test_index", "_type" : "test_type", "_id" : "doc_3" } }
      { "field_for_search": "hello_3", "field_for_filter": 0 }
    • 검색 쿼리 1

      GET /test_index/_search
      {
      "query": {
        "constant_score": {
          "filter": {
            "bool": {
              "filter": {
                "term": {
                  "field_for_filter": 100
                }
              },
              "should": [
                {
                  "match": {
                    "field_for_search": "hello_2"
                  }
                }
              ]
            }
          }
        }
      }
      }
    • 검색 쿼리 2

      GET /test_index/_search
      {
      "query": {
        "bool": {
          "filter": {
            "term": {
              "field_for_filter": 100
            }
          },
          "should": [
            {
              "match": {
                "field_for_search": "hello_2"
              }
            }
          ]
        }
      }
      }

    위 "검색 쿼리 1"과 "검색 쿼리 2"의 차이는 constant score query로 감싸져있는지 아닌지만 다르다.
    두 검색 쿼리의 결과 차이를 예상할 수 있는가?

     

     

    아래는 "검색 쿼리 1"의 검색 결과이다.

     

    아래는 "검색 쿼리 2"의 결과이다.

     

    위 결과와 같이 "검색 쿼리 1"은 하나의 문서만 검색되는 반면, "검색 쿼리 2"는 두개의 문서가 검색 된다.

     

    이는 constant score query의 filter으로 인해 bool query 전체가 filter context로 동작하기 때문이고, bool query의 should clause는 context에 따라 다르게 동작하기 때문이다.

     

    그럼 filter context에서의 should cluase는 항상 적어도 하나가 match 되어야만 할 까?
    (검색 쿼리를 작성하다 보면 간혹 위 동작을 

    온 우주의 기운을 모아  

    변경 하고 싶을 때가 있다. 적어도 필자에게는 있었다ㅠㅠ)

     

    "검색 쿼리 1"을 아래와 같이 변경해 보자

    GET /test_index/_search
    {
      "query": {
        "constant_score": {
          "filter": {
            "bool": {
              "filter": {
                "term": {
                  "field_for_filter": 100
                }
              },
              "should": [
                {
                  "match": {
                    "field_for_search": "hello_2"
                  }
                }
              ],
              "minimum_should_match": 0
            }
          }
        }
      }
    }

    검색결과를 확인해보자. 아래와 같이 constant score query를 적용하지 않은 "검색 쿼리 2"와 같이 두개의 문서가 검색 됨을 알 수 있다.

     

    위 내용은 5.3 이후의 bool query reference 문서에서 명확하게 언급되어 있는데,
    filter context의 should clause라고 하더라도 minimum_should_match를 사용하여 동작을 명시적으로 변경 할 수 있다.


    부끄럽게도 필자가 이 글을 쓰게 된 계기는 사실 minimum_should_match로 filter context의 should clause 동작을 변경 할 수 있는지 몰랐기 때문이다.
    비록 5.2 이전 버전의 reference 문서에는 두루뭉실 하게 나와있다고 하더라도, 이후 버전의 reference 문서를 한번쯤 봤을텐데 자세히 보지 않고 넘긴 듯 하다.

     

    앞으로 반성하며 좀 더 꼼꼼히 문서를 읽고, 기억하기 위하여 이 글을 남긴다.

     

    주절주절 하는 긴 글을 읽어 주셔서 감사합니다 : )

    '프로그래밍/Elasticsearch' 관련 글 more
    Posted by 이거니거니료니