쿼리 DSL (Query DSL)
쿼리 DSL (Query DSL)
쿼리 DSL이 뭔가요?
쿼리 DSL(Query DSL) 은 복잡한 검색, 필터링, 집계를 모두 처리할 수 있는 완전한 기능을 갖춘 JSON 스타일의 쿼리 언어예요. 오늘날 Elasticsearch에서 쓰는 가장 오래되고 가장 강력한 쿼리 언어이기도 하고요.
_search 엔드포인트는 쿼리 DSL 문법으로 작성된 쿼리를 받아요.
쿼리 DSL로 검색하고 필터링하기
쿼리 DSL은 아주 다양한 검색 기법을 지원하는데, 대표적으로 이런 것들이 있어요.
- 전문(full-text) 검색: 분석되고 인덱싱된 텍스트를 검색해서 구(phrase) 쿼리나 근접(proximity) 쿼리, 퍼지(fuzzy) 매칭 같은 것들을 지원해요.
- 키워드 검색:
keyword필드를 사용해 정확히 일치하는 값을 검색해요. - 시맨틱 검색: Elasticsearch 클러스터에서 생성한 임베딩을 대상으로 밀집(dense) 또는 희소(sparse) 벡터 검색을 통해
semantic_text필드를 검색해요. - 벡터 검색: Elasticsearch 밖에서 생성한 임베딩을 kNN 알고리즘으로 비슷한 밀집 벡터를 검색해요.
- 지리 공간 검색: 지리 공간 쿼리를 사용해 위치를 검색하고 공간 관계를 계산해요.
쿼리 DSL로 데이터를 필터링할 수도 있어요. 필터는 특정 필드 수준의 기준에 맞는 문서만 가져와서 문서를 포함시키거나 제외시키는 기능이에요. filter 파라미터를 사용하는 쿼리는 필터 컨텍스트(filter context) 를 나타내요.
쿼리 DSL로 분석하기
집계(Aggregations) 는 쿼리 DSL로 Elasticsearch 데이터를 분석할 때 쓰는 핵심 도구예요. 집계로 데이터의 복잡한 요약을 만들고 주요 지표, 패턴, 추세에 대한 통찰을 얻을 수 있어요.
집계는 검색에 쓰이는 것과 동일한 데이터 구조를 활용하기 때문에 매우 빨라요. 그래서 데이터를 실시간으로 분석하고 시각화할 수 있어요. 검색으로 문서를 찾고, 결과를 필터링하고, 분석까지 — 같은 데이터에 대해 단일 요청으로 동시에 처리할 수 있는 거죠. 즉 집계는 검색 쿼리의 컨텍스트 안에서 계산된다는 뜻이에요.
사용할 수 있는 집계 유형은 다음과 같아요.
- 메트릭(Metric): 필드 값에서 합(sum)이나 평균(average) 같은 메트릭을 계산해요.
- 버킷(Bucket): 필드 값, 범위, 그 밖의 기준에 따라 문서를 버킷으로 묶어요.
- 파이프라인(Pipeline): 다른 집계의 결과에 대해 집계를 실행해요.
집계는 search API의 aggs 파라미터에 지정해서 실행해요. 자세한 내용은 [Run an aggregation]을 참고하세요.
어떻게 동작하나요?
쿼리 DSL을 각각 두 종류의 절(clause)로 이루어진 쿼리들의 AST(Abstract Syntax Tree, 추상 구문 트리)라고 생각해 보세요.
리프 쿼리 절(leaf query clause): 특정 필드에서 특정 값을 찾는 절이에요. [match], [term], [range] 쿼리 같은 것들이죠. 이런 쿼리는 단독으로도 사용할 수 있어요.
복합 쿼리 절(compound query clause): 다른 리프 쿼리 또는 복합 쿼리를 감싸는 절이에요. 여러 쿼리를 논리적으로 결합하거나([bool], [dis_max] 쿼리처럼), 동작을 바꾸는 데([constant_score] 쿼리처럼) 사용해요.
쿼리 절은 쿼리 컨텍스트에서 쓰이느냐 필터 컨텍스트에서 쓰이느냐에 따라 동작이 달라져요.
비싼 쿼리 허용(Allow expensive queries): 특정 유형의 쿼리는 구현 방식 때문에 일반적으로 느리게 실행되는데, 이게 클러스터의 안정성에 영향을 줄 수 있어요. 그런 쿼리는 다음과 같이 분류할 수 있어요.
- 일치 항목을 찾기 위해 선형 스캔이 필요한 쿼리:
- [
script쿼리] - 인덱싱되지 않았지만 [doc values]가 활성화된 [numeric], [date], [boolean], [ip], [geo_point], [keyword] 필드에 대한 쿼리
- [
- 사전 비용이 높은 쿼리:
- [
fuzzy쿼리] ([wildcard] 필드를 제외) - [
regexp쿼리] ([wildcard] 필드를 제외) - [
prefix쿼리] ([wildcard] 필드나 [index_prefixes]가 없는 필드를 제외) - [
wildcard쿼리] ([wildcard] 필드를 제외) - [
text] 및 [keyword] 필드에 대한 [range쿼리]
- [
- [조인(joining) 쿼리]
- 문서당 비용이 높을 수 있는 쿼리:
- [
script_score쿼리] - [
percolate쿼리]
- [
이런 쿼리의 실행은 search.allow_expensive_queries 설정 값을 false로 지정하면 막을 수 있어요(기본값은 true예요).
쿼리 컨텍스트와 필터 컨텍스트
관련성 점수
기본적으로 Elasticsearch는 일치하는 검색 결과를 관련성 점수(relevance score) 로 정렬해요. 이 점수는 각 문서가 쿼리에 얼마나 잘 맞는지를 측정하는 값이에요.
관련성 점수는 양의 부동 소수점 숫자이며, search API의 _score 메타데이터 필드에 반환돼요. _score가 높을수록 문서의 관련성이 높은 거예요. 쿼리 유형마다 관련성 점수를 다르게 계산할 수 있지만, 점수 계산은 쿼리 절이 쿼리 컨텍스트에서 실행되느냐 필터 컨텍스트에서 실행되느냐에 따라서도 달라져요.
쿼리 컨텍스트
쿼리 컨텍스트에서 쿼리 절은 *이 문서가 이 쿼리 절에 얼마나 잘 맞을까?*라는 질문에 답해요. 쿼리 절은 문서가 맞는지 아닌지를 결정하는 것뿐 아니라 _score 메타데이터 필드에 관련성 점수도 계산해요.
쿼리 컨텍스트는 쿼리 절이 query 파라미터(예: search API의 query 파라미터)에 전달될 때마다 적용돼요.
필터 컨텍스트
필터는 "이 문서가 이 쿼리 절에 맞나요?" 라는 이진 질문에 답해요. 답은 단순히 "예" 또는 "아니요"예요. 필터링에는 몇 가지 이점이 있어요.
- 단순한 이진 로직: 필터 컨텍스트에서는 쿼리 절이 점수 계산 없이 yes/no 기준으로 문서 일치 여부를 결정해요.
- 성능: 관련성 점수를 계산하지 않기 때문에 필터가 쿼리보다 빠르게 실행돼요.
- 캐싱: Elasticsearch는 자주 사용되는 필터를 자동으로 캐시해서 이후 검색 성능을 높여줘요.
- 리소스 효율: 필터는 전문 쿼리에 비해 CPU 리소스를 덜 소모해요.
- 쿼리 결합: 필터를 점수 계산이 있는 쿼리와 결합해 결과 집합을 효율적으로 다듬을 수 있어요.
필터는 특히 구조화된 데이터를 조회하거나 복잡한 검색에서 "반드시 있어야 하는(must have)" 기준을 구현할 때 효과적이에요.
구조화된 데이터(structured data)란 미리 정의된 방식으로 체계적으로 정리되고 형식화된 정보를 말해요. Elasticsearch에서 여기에는 보통 다음이 포함돼요.
- 숫자 필드(정수, 부동 소수점)
- 날짜와 타임스탬프
- 불리언 값
- 키워드 필드(정확히 일치하는 문자열)
- 지리 좌표(geo-point)와 지리 도형(geo-shape)
전문 필드와 달리 구조화된 데이터는 일관되고 예측 가능한 형식을 갖고 있어서 정밀한 필터링 작업에 아주 적합해요.
필터의 일반적인 사용처에는 이런 것들이 있어요.
- 날짜 범위 확인: 예를 들어 timestamp 필드가 2015년과 2016년 사이인지
- 특정 필드 값 확인: 예를 들어 status 필드가 "published"와 같은지, author 필드가 "John Doe"와 같은지
필터 컨텍스트는 쿼리 절이 filter 파라미터에 전달될 때 적용돼요. 예를 들면:
bool쿼리의filter또는must_not파라미터constant_score쿼리의filter파라미터filter집계
필터는 특히 구조화된 데이터 쿼리와 전문 검색과 결합할 때 쿼리 성능과 효율성을 최적화해요.
쿼리 컨텍스트와 필터 컨텍스트 예시
아래는 search API에서 쿼리 절을 쿼리 컨텍스트와 필터 컨텍스트로 사용하는 예시예요. 이 쿼리는 다음 조건이 모두 충족되는 문서를 매칭해요.
title필드에search라는 단어가 포함.content필드에elasticsearch라는 단어가 포함.status필드에published라는 정확한 단어가 포함.publish_date필드에 2015년 1월 1일 이후의 날짜가 포함.
GET /_search
{
"query": {
"bool": {
"must": [
{ "match": { "title": "Search" }},
{ "match": { "content": "Elasticsearch" }}
],
"filter": [
{ "term": { "status": "published" }},
{ "range": { "publish_date": { "gte": "2015-01-01" }}}
]
}
}
}
query 파라미터는 쿼리 컨텍스트를 나타내요. bool과 두 개의 match 절은 쿼리 컨텍스트에서 사용되므로 각 문서가 얼마나 잘 맞는지 점수를 매기는 데 쓰여요.
filter 파라미터는 필터 컨텍스트를 나타내요. 여기서 term과 range 절은 필터 컨텍스트로 사용돼요. 이들은 일치하지 않는 문서를 걸러내지만, 일치하는 문서의 점수에는 영향을 주지 않아요.
경고
쿼리 컨텍스트에서 계산된 쿼리의 점수는 단정밀도(single precision) 부동 소수점 숫자로 표현돼요. 즉 유효숫자(significand) 정밀도에 24비트만 할당돼요. 이 유효숫자 정밀도를 초과하는 점수 계산은 정밀도가 손실된 채 float로 변환돼요.
팁
일치하는 문서의 점수에 영향을 줘야 하는 조건(즉, 문서가 얼마나 잘 맞는지)은 쿼리 컨텍스트의 쿼리 절로 사용하고, 그 외의 모든 쿼리 절은 필터 컨텍스트로 사용하세요.