본문 바로가기

elastic search

Elastic Search DSL(Search)

ES DSL 패키지에서 Search 객체를 활용하면 조건에 맞는 검색 및 집계가 가능하다.

 

ES query 에서 여러가지 조건을 한 번에 만족시키는 쿼리를 구성할 때, bool 이라는 연산자를 사용한다.

마찬가지로 Search 객체에도 query 함수를 사용하여, 연산자와 세부 조건을 Q 라는 객체를 통해 넣을 수 있다.

from elasticsearch_dsl import Search, Q


s = Search(using=es, index="text-index-v1").query(
    "bool",
    filter=[
        Q("range", **{"created_dt": {"gte": "2021-01-01", "lte": "2021-02-01"}}),
        Q("terms", **{"name": ["abc", "bcx", "def"]}}
        ...
        ]
)

 

filter 에 list 형태로 Q 객체를 넣어주면 해당 조건을 모두 만족하는 데이터를 추출한다.

각가의 Q 객체에는 다양한 조건을 위한 쿼리를 넣을 수 있는데, range 는 주로 날짜 기간을 설정하는 조건이다.

terms 는 특정 필드의 특정 값을 만족시키는 조건이다.

 

bool 연산자에는 should, filter, must, must_not 의 4가지 인자가 있으며, 각각의 조건은 문서를 통해 확인할 수 있다.

위의 예시에서 filter 를 사용한 이유는, filter 와 must 둘 다 특정 조건을 만족하는 document 를 검색하지만 filter 는 스코어를 따로 계산하지 않는다.

 

ES 는 full text 검색에 특화되어있다. 따라서 주어진 조건에 얼만큼 정확하게 부합하는지 여부를 내부적은 알고리즘에 의해 계산하고 이를 스코어로 나타낸다. must 는 해당 스코어를 계산해서 결과를 가져오는 반면, filter 는 스코어 계산없이 정확하게 일치하는 조건만 가져온다. 따라서 검색 속도가 must 보다 빠르고 또한 캐싱도 가능하다.

 

Q 객체 내부에서는 dictionary 언패킹 방식으로 표현했다. 필드를 문자열로 감싸지 않고 field="~" 와 같이 표현도 가능하다.

다만 dictionary 로 사용한 이유는 다음과 같은 케이스에서 확인 가능하다.

 

Q(
    "nested",
    path="innerdoc",
    query=Q(
        "terms",
        **{"innerdoc.inner_str_field": ["abc"]}, # **{f"innerdoc.{inner_field}": ["abc"]}
    ),
)

Q("terms", innerdoc__inner_str_field=["abc"])

 

만약 nested 된 필드에 접근하기 위해서는 nested 쿼리를 사용해야 한다. nested 쿼리는 nested type 으로 정의된 필드를 path 에 넣어주고, 쿼리인자에 다시 Q 객체를 넣어준다.

이 때, nested 필드 안에 있는 내부 필드에 접근하기 위해서는 위와 같이 . 혹은 __ 로 내부 필드와 nested type 필드를 이어줄 수 있다.

 

만약에 조건에 따라서 nested 필드 안에 있는 여러 내부 필드 중에서 하나에 접근해야 한다고 했을 때, 상황에 따라 필드명을 바꿔주기 위해서는 dictionary 형태로 표현해야 f-string 으로 필드명을 바꿔줄 수 있다. 따라서 보다 유연하게 활용하기 위해 dict 형태로 언패킹 하는 방식을 사용하였다.

 

위와 같은 쿼리를 ES 기본 쿼리로 변경하면 아래와 같다.

{
  "query": {
    "bool": {
      "filter": [
        {
          "range": { "created_dt": { "gte": "2021-01-01", "lte": "2021-02-01" } }
        },
        {
          "terms": { "name": ["abc", "bcx", "def"] }
        },
        {
          "nested": {
            "path": "innerdoc",
            "query": {
              "terms": {
                "innerdoc.inner_str_field": ["abc"]
              }
            }
          }
        }
      ]
    }
  }
}

 

mysql 은 아래와 같다.

SELECT	IDX.*
FROM	TEST_INDEX_V1 AS IDX
JOIN	INNER_DOC AS IDX2
USING	(PK)
WHERE	CREATED_DT BETWEEN '2021-01-01' AND '2021-02-01'
    AND `NAME` IN ('abc', 'bcx', 'def')
    AND IDX2.INNER_STR_FIELD = 'abc'

RDB 는 일반적으로 정규화를 통해 테이블을 분리하므로 nested field 안에 있던 데이터가 다른 테이블로 분리되어 있었다고 가정하면 위와 같은 방식으로 구성됐을 것이다.

 

아무래도 json 형태로 길게 늘어나는 json 형태의 쿼리는 가독성 측면에서 다소 불편함이 있을 수 있다. 사실 어떤 방식에 익숙한가에 따라서 다를 수 있다고 생각한다. DSL 패키지를 처음 쓰면 오히려 ES의 json 쿼리가 더 편할 수도 있다.

 

이번에 DSL 과 json 쿼리를 둘 다 살펴보면서 쿼리를 짤 때 어려운 부분을 마주하게 되면 다양한 방식으로 생각할 수 있는 생각의 폭을 넓힐 수 있었다는 점에서 가능하면, 특히 파이썬으로 개발하는 상황이라면 둘 다 함께 보는 것이 좋을 것 같다.

'elastic search' 카테고리의 다른 글

Elastic Search DSL(Aggregation)  (0) 2021.11.10
Elastic Search DSL(Connection, Index, Document)  (0) 2021.11.08