만쥬의 개발일기

쿼리 컨텍스트 vs 필터 컨텍스트

엘라스틱의 검색은 다음 두 가지로 나뉜다.

쿼리 컨텍스트

  • 질의에 대한 유사도를 계산해 더 정확한 결과를 먼저 보여준다.

필터 컨텍스트

  • 유사도를 계산하지 않고 일치 여부에 따른 결과만을 반환한다.
  • 스코어 계산 과정을 생략해 쿼리 속도를 올릴 수 있다.
  • 캐시를 이용할 수 있다.

 

_search는 검색 쿼리를 위해 제공되는 REST API이고, match는 전문 검색을 위한 쿼리이다.

다음 예시를 보면 clothing 용어가 있는 도큐먼트를 찾아 총 3927개의 도큐먼트 중 스코어가 높은 순으로 정렬된다.

 

필터 컨텍스트와 쿼리 컨텍스트는 모두 search API를 사용한다.

단지 내용에 따라 쿼리가 구분되는데, 필터 컨텍스트는 논리(bool) 쿼리 내부의 filter 타입에 적용된다. 따라서 다음과 같이 day_of_week가 일치하는 770개의 도큐먼트를 찾고, 스코어는 계산되지 않아 0.0으로 나온다.

 

쿼리 컨텍스트와 필터 컨텍스트를 적절히 조합해 사용하는 것이 가장 바람직하다.

 

유사도 스코어

엘라스틱은 다양한 스코어 알고리즘을 제공하는데, 기본적으로 BM25 알고리즘을 이용해 유사도 스코어를 계산한다. 스코어 계산을 위한 알고리즘 동작 방식을 이해하고 있다면 더 똑똑한 쿼리를 작성할 수 있고, 인덱스를 효율적으로 디자인 할 수 있다.

 

BM25 알고리즘은 TF-IDF 알고리즘에 문서 길이까지 고려한 알고리즘이다.

따라서 TF와 IDF 값을 알면 스코어를 구할 수 있다.

 

IDF(inverse Document Frequency) 계산

DF는 문서 빈도로, 특정 용어가 얼마나 자주 등장했는지를 의미한다. 그리고 IDF는 문서 빈도의 역수로, 도큐먼트 내에서 발생 빈도가 적을수록 가중치를 높게 준다.

쉽게 말해 전체 문서에서 자주 발생하는 단어들 (the, is, to 등등)은 중요하지 않은 단어로 인식하는 것이다.

 

IDF식 = log(1 + (N - n + 0.5) / (n + 0.5))

n = term이 몇 개의 도큐먼트에 있는지

N = 인덱스 내의 전체 도큐먼트 수

 

TF(Term Frequency)

TF는 특정 term이 하나의 도큐먼트에 얼마나 많이 등장했는지를 의미한다.

일반적으로 특정 용어가 한 도큐먼트에서 많이 반복되면, 주제와 연관될 확률이 높다.

 

TF식 = (freq + k1 * (1 - b + b * dl / avgdl))

freq = 도큐먼트 내에서 용어가 나온 횟수

k1,b = 알고리즘 정규화를 위한 가중치, ES가 디폴트로 정한 상수

dl = 필드 길이

avgdl = 전체 도큐먼트에서 평균 필드 길이

➡️이는 짧은 글에서 찾고자 하는 용어가 포함될수록 가중치가 높다는 뜻이다.

 

최종 스코어는 IDF * TF * boost로, boost는 ES가 지정한 상수이다. (값은 2.2)

 

쿼리의 종류

ES에서 검색을 위해 지원하는 쿼리의 종류는 다음과 같다.

 

리프 쿼리(leaf query)

  • match 쿼리
  • term 쿼리
  • range 쿼리

복합 쿼리(compound query)

  • bool 쿼리

 

전문 쿼리(full text query)

전문 쿼리는 전문 검색을 위해 사용한다.

전문 검색을 할 필드는 매핑 시 반드시 텍스트 타입으로 매핑한다.

전문 쿼리 동작 과정은 다음과 같다.

전문 쿼리를 사용하면 검색어도 분석기에 의해 토큰으로 분리하고, 역 인덱스 사전과 매칭해 스코어를 계산하여 검색한다.

 

전문 쿼리는 일반적으로 블로그와 같이 텍스트가 많은 필드에서 특정 용어 검색 시 사용한다.

전문 쿼리 종류

  • 매치 쿼리
  • 매치 프레이즈 쿼리
  • 멀티 매치 쿼리
  • 쿼리 스트링 쿼리

 

용어 수준 쿼리(term level query)

term level 쿼리는 정확히 일치하는 용어를 찾기 위해 사용한다.

따라서 인덱스 매핑 시 키워드 타입으로 매핑한다.

term level 쿼리는 분석기를 사용하지 않는다.

그리고 정확히 일치할 때만 검색이 가능하다.

 

일반적으로 숫자, 날짜, 범주형 데이터 등을 정확하게 검색할 때 사용한다.

 

term level 쿼리 종류

  • term 쿼리
  • terms 쿼리
  • fuzzy 쿼리

 

매치 쿼리(match query)

매치 쿼리는 대표적인 전문 쿼리로, 전체 텍스트 중에서 특정 용어를 검색할 때 사용한다.

매치 쿼리를 사용하려면 필드명을 알아야하는데, 모른다면 GET {index명}/_mapping 으로 필드를 확인하자.

 

예시

get kibana_sample_data_ecommerce/_search
{
  "_source" : ["customer_full_name"],
  "query":{
    "match": {
      "customer_full_name": "Mary bailey"
    }
  }
}

위 예시와 같이 _source 파라미터를 사용하면 원하는 필드만 결과로 볼 수 있다.

또한 검색어도 토큰화되기 때문에 [mary, bailey] 두개의 토큰으로 검색하게 된다.

 

매치 쿼리의 term 간의 공백은 OR로 인식되는 것을 주의하자.

 

get kibana_sample_data_ecommerce/_search
{
  "_source" : ["customer_full_name"],
  "query":{
    "match": {
      "customer_full_name": {
        "query": "Mary bailey",
        "operator": "and"
      }
    }
  }
}

만약 OR로 검색되는 것을 방지하려면, 위와 같이 operator 파라미터(and)를 명시하여 문장으로서 검색할 수 있다.

 

매치 프레이즈 쿼리(match_phrase query)

get kibana_sample_data_ecommerce/_search
{
  "_source" : ["customer_full_name"],
  "query":{
    "match": {
      "customer_full_name": {
        "query": "bailey Mary",
        "operator": "and"
      }
    }
  }
}

 

그러나 다음과 같이 순서를 바꿔서 검색해도, 결과로 “Mary bailey”가 나오는 것을 확인할 수 있다.

 

만약 순서까지 검색어와 통일하고 싶다면, match_phrase를 사용하면 된다.

하지만 match_phrase는 검색 시 많은 리소스를 요구하므로 자주 사용하지 말자.

 

용어 쿼리(term query)

용어 쿼리는 용어 수준 쿼리의 대표적 쿼리이다.

용어 쿼리는 검색어를 토큰화하지 않기 때문에, 검색어 “Mary bailey” 가 분석기에 의해 토큰화되지 않는다. 그리고 정확히 일치하는 용어가 있는 경우에만 검색이 된다.

따라서 “mary bailey”“mary” , “Mary Bailey” 등은 검색되지 않는다.

 

용어 쿼리와 매치 쿼리는 반드시 잘 구분하여 용도를 명확히하자.

 

복수 용어 쿼리(terms query)

복수 용어 쿼리는 용어 수준 쿼리로, 여러 용어를 검색해준다.

그리고 용어 쿼리와 마찬가지로 분석기를 거치지 않으므로 대소문자도 신경써주자.

 

예시

멀티 매치 쿼리(multi match query)

멀티 매치 쿼리는 전문 검색 쿼리의 일종으로, 텍스트 타입 필드에서 주로 사용한다.

지금까지는 쿼리를 이용해 검색할 때 반드시 필드명을 적어야 했다.

 

하지만 전문 검색 시 우리는 용어나 구절이 어떤 필드에 있는지 모를 때가 있다.

예를 들어 구글에서 ‘핫도그’를 검색할 때, 핫도그가 어떤 필드(블로그 제목, 내용, 뉴스 기사 등)에 저장되어 있는지 정확히 알고 쓰지 못한다.

 

이런 경우는 하나의 필드가 아닌 여러 필드에서 검색 해야한다.

 

예시

get kibana_sample_data_ecommerce/_search?
{
  "_source": [
    "customer_first_name",
    "customer_last_name",
    "customer_full_name"
  ],
  "query": {
    "multi_match": {
      "query": "mary",
      "fields": [
        "customer_first_name",
        "customer_last_name",
        "customer_full_name"
      ]
    }
  }
}

위 예시 처럼 멀티 매치 쿼리는 1개 이상의 필드에 쿼리를 요청한다.

그리고 각각의 필드에서 개별 스코어를 구하고, 그 중 가장 큰 값을 대표 스코어로 구한다.

get kibana_sample_data_ecommerce/_search?
{
  "_source": [
    "customer_*_name"
  ],
  "query": {
    "multi_match": {
      "query": "mary",
      "fields": [
        "customer_*_name"
      ]
    }
  }
}

검색하려는 필드가 너무 많을 때는 위처럼 필드명에 와일드카드를 사용해 이름이 유사한 복수 필드를 선택할 수도 있다.

 

필드 가중치(부스팅 기법)

만약 검색 시 특정 필드가 더 중요하게 여겨진다면, 해당 필드에 가중치를 두어 스코어를 더 높게 책정할 수 있다. 예를 들어 블로그 검색시 일반적으로 내용보다 제목에 용어가 있는 것이 더 중요할 가능성이 있다.

get kibana_sample_data_ecommerce/_search?
{
  "_source": [
    "customer_first_name",
    "customer_last_name",
    "customer_full_name"
  ],
  "query": {
    "multi_match": {
      "query": "mary",
      "fields": [
        "customer_first_name",
        "customer_last_name",
        "customer_full_name^2"
      ]
    }
  }
}

가중치는 위 예시와 같이 원하는 필드에 ^n 을 붙여 스코어 값을 n배로 늘려줄 수 있다.

 

범위 쿼리

특정 날짜나 숫자의 범위를 지정해 범위 안의 데이터를 검색할 때 사용한다.

날짜/숫자/IP 타입에서만 사용이 가능하다.

예시

get kibana_sample_data_flights/_search
{
  "query": {
    "range": {
      "timestamp": {
        "gte": "2024-01-15",
        "lt": "2024-03-16"
      }
    }
  }
}

샘플 데이터는 로드한 날짜 기준으로 타임스탬프가 설정되니 실습 시 주의하자.

그리고 쿼리에서 사용하는 날짜/시간 포맷과 도큐먼트에 실제 저장된 날짜/시간 포맷이 맞아야 검색이 가능하다.

 

파라미터는 다음과 같이 사용한다.

  • gte: 10 ➡️ 10과 같거나 10보다 큰 값
  • gte: 2021-01-21 ➡️ 2021년 01월 21일 이거나 그 이후의 날짜
  • gt: 10 ➡️ 10보다 큰 값
  • gt: 2021-01-21 ➡️ 2021년 01월 21일 이후의 날짜
  • lte: 20 ➡️ 20과 같거나 20보다 작은 값
  • lte: 2021-01-21 ➡️ 2021년 01월 21일이거나 그 이전의 날짜
  • lt: 20 ➡️ 20보다 작은값
  • lt: 2021-01-21 ➡️ 2021년 01월 21일 이전의 날짜

 

날짜/시간 관련 범위 표현식

  • now = 현재 시각
  • now+1d = 현재 시각 +1일
  • now- +1h+30m+10s = 현재 시각 + 1시간 30분 10초
  • 2021-01-21 || +1M = 2021년 1월 21일 + 1달

 

날짜/시간 단위 표기법

  • y = 연
  • M = 월
  • w = 주
  • d = 일
  • H or h = 시
  • m = 분
  • s = 초

여기서는 M과 m만 잘 구분해서 사용하자.

 

범위 데이터 타입

매핑 타입 중에는 다음 여섯가지 범위 데이터 타입이 있다.

  • integer_range
  • float_range
  • long_range
  • double_range
  • date_range
  • ip_range

다음과 같이 범위 데이터 타입을 가지는 예시 인덱스를 생성해보자.

PUT range_test_index
{
  "mappings": {
    "properties": {
      "test_date":{
        "type": "date_range"
      }
    }
  }
}

그러면 다음과 같이 범위 데이터를 가진 도큐먼를 추가할 수 있다.

해당 인덱스에 평범한 date 데이터를 넣으려고 하면 에러가 발생한다.

 

relation을 이용한 범위 검색

GET range_test_index/_search
{
  "query": {
    "range": {
      "test_date": {
        "gte": "2024-03-10",
        "lte": "2024-03-20",
        "relation": "contains"
      }
    }
  }
}

위와 같이 relation 파라미터를 이용해 범위 검색 시 옵션을 줄 수 있다.

옵션은 다음과 같다.

  • intersects(기본값) = 쿼리 범위 값이 도큐먼트의 범위 데이터를 일부라도 포함하면 된다.
  • contatins = 도큐먼트의 범위 데이터가 쿼리 범위 값을 모두 포함해야 한다.
  • within = 도큐먼트의 범위 데이터가 쿼리 범위 값 내에 전부 속해야 한다.
  •  

논리 쿼리

논리 쿼리는 복합 쿼리로, 다양한 쿼리를 조합해 사용하는 것이다.

논리 쿼리는 다음과 같이 작성할 수 있다.

GET <index>/_search
{
  "query": {
    "bool" : {
      "must" : [
        { 쿼리문 }, ...
      ],
      "must_not": [
        { 쿼리문 }, ...
      ],
      "should" : [
        { 쿼리문 }, ...
      ],
      " filter " : [
        { 쿼리문 }, ...
      ]
    }
  }
}

각각의 논리 쿼리 타입에 대해 알아보자.

  • must
    • 쿼리를 실행하여 참인 도큐먼트를 찾는다.
    • 복수의 쿼리를 실행하면 AND 연산을 한다.
  • must_not
    • 쿼리를 실행하여 거짓인 도큐먼트를 찾는다.
    • 다른 타입과 같이 사용할 경우 must_not 조건을 만족하는 도큐먼트를 제외한다.
  • should
    • 단독으로 사용 시 쿼리를 실행하여 참인 도큐먼트를 찾는다.
    • 복수의 쿼리를 실행하면 OR 연산을 한다.
    • 다른 타입과 같이 사용할 경우 스코어에만 활용된다.
  • filter
    • 쿼리를 실행하여 예/아니요 형식의 필터 컨텍스트를 수행한다.
GET kibana_sample_data_ecommerce/_search
{
  "_source": [
    "day_of_week",
    "customer_full_name"
  ],
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "day_of_week": "Sunday"
          }
        }
      ],
      "must": [
        {
          "match": {
            "customer_full_name": "mary"
          }
        }
      ]
    }
  }
}

위와 같이 쿼리를 작성하면, filter를 통해 먼저 Sunday인 도큐먼트를 필터링하고,이러면 필터를 통해 불필요한 스코어 계산을 줄여 검색 성능을 높일 수 있다.

 

와일드카드 쿼리

와일드카드 쿼리는 우리가 아는 와일드카드 사용법과 같다.

몇가지 예시만 보자.

*자리에는 어떤 길이의 어떤 문자가 와도 된다.

?는 하나의 문자를 의미한다.

와일드카드 패턴은 다음과 같이 사용할 수 있다.

GET kibana_sample_data_ecommerce/_search
{
  "_source": "customer_full_name",
  "query": {
    "wildcard": {
      "customer_full_name.keyword": "M?r*"
    }
  }
}

정규식 쿼리

정규식 또한 일반적인 사용법과 같다.

*과 ?의 쓰임새가 와일드카드와 다르다는 것을 유의하자.

.은 하나의 문자를 의미한다.

+는 + 기호 앞 문자와 같은 문자가 한 번 이상 반복되면 매칭되었다고 판단한다.

*는 +와 유사하나 *의앞 문자와 같은 문자가 0번 혹은 한 번 이상 반복되면 매칭이다.

?은 ? 기호 앞 문자가 0번 혹은 1번 나타나면 매칭이다.

()기호는 문자를 그룹핑하여 반복되는 문자를 매칭한다.

[ ]기호는 문자를 클래스화하여 특정 범위의 문자를 매칭한다. 다양한 사용법이 있다.

정리

필터 컨텍스트와 쿼리 컨텍스트를 잘 조합하되, 쿼리 속도를 높이고 싶다면 필터 컨텍스트를 적극 활용하자.

profile

만쥬의 개발일기

@KangManJoo

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!