Elasticsearch Query DSL 마스터하기: 데이터 검색을 위한 필수 명령어
Query DSL을 마스터하여 Elasticsearch 검색의 강력함을 활용하세요. 이 가이드에서는 `match`, `term`, range 쿼리의 실용적인 사용법을 중심으로 필수 JSON 쿼리 구조를 설명합니다. 기본 `bool` 쿼리 내에서 `must`(점수 계산)와 `filter`(캐싱) 절의 중요한 차이점을 배워 복잡하고 고성능의 데이터 검색을 효율적으로 구성할 수 있습니다.
Elasticsearch Query DSL 마스터하기: 데이터 검색을 위한 필수 명령어
Elasticsearch Query DSL은 단순한 검색 상자만으로는 부족할 때 사용하는 JSON 언어입니다. 이를 통해 하나의 요청에서 전문 검색, 정확한 필터, 날짜 범위, 정렬, 페이지 매김, 집계를 혼합할 수 있습니다. 이러한 유연성은 유용하지만, 잘못된 문서를 반환하거나 테스트에서는 잘 작동하다가 프로덕션에서 느려지는 쿼리를 작성하기 쉽습니다.
Query DSL을 배우는 가장 좋은 방법은 "관련성을 위해 텍스트를 검색하고 있는가?"와 "정확한 값을 필터링하고 있는가?"라는 두 가지 질문을 염두에 두는 것입니다. 대부분의 쿼리 선택은 이 구분에서 비롯됩니다.
Elasticsearch 검색 요청의 구조
모든 Elasticsearch 검색은 특정 인덱스(또는 인덱스들)의 _search 엔드포인트에 대해 수행됩니다. 기본 검색 요청은 쿼리 매개변수를 정의하는 JSON 본문을 포함하는 POST 요청입니다. 이 본문에서 가장 중요한 부분은 query 객체입니다.
기본 구조:
POST /your_index_name/_search
{
"query": { ... 여기에 쿼리 구조를 정의하세요 ... },
"size": 10,
"from": 0
}
핵심 쿼리 유형: 정밀성과 관련성
Query DSL은 다양한 데이터 유형과 일치 요구 사항에 맞게 조정된 광범위한 쿼리를 제공합니다. 쿼리 선택은 관련성 점수와 성능 모두에 큰 영향을 미칩니다.
1. 전문 검색: match 쿼리
match 쿼리는 분석된 필드에 대한 전문 검색의 표준입니다. 검색어를 토큰화하고 지정된 필드에서 일치하는 토큰을 확인합니다.
사용 사례: 관련성 점수가 중요한 자연어 텍스트 검색.
예시: 'description' 필드에 'cloud' 또는 'computing'이라는 단어가 포함된 문서 찾기.
GET /products/_search
{
"query": {
"match": {
"description": "cloud computing"
}
}
}
2. 정확한 값 일치: term 쿼리
term 쿼리는 지정된 정확한 용어를 포함하는 문서를 검색합니다. match와 달리 검색 문자열에 대한 분석을 수행하지 않으므로 키워드, ID 또는 숫자로 인덱싱된 필드에 대한 정확한 일치에 이상적입니다.
사용 사례: 분석되지 않은 필드(예: keyword 필드 또는 숫자)에서 정확한 값으로 필터링.
예시: 정확한 ID가 SKU10021인 제품 검색.
GET /products/_search
{
"query": {
"term": {
"product_id": "SKU10021"
}
}
}
3. 범위 쿼리
범위 쿼리를 사용하면 필드 값이 지정된 범위(숫자, 날짜 또는 문자열) 내에 있는 문서를 필터링할 수 있습니다.
구문: gt(초과), gte(이상), lt(미만), lte(이하)를 사용합니다.
예시: 2024년 1월 1일 이후에 주문된 주문 찾기.
GET /orders/_search
{
"query": {
"range": {
"order_date": {
"gte": "2024-01-01",
"lt": "2025-01-01"
}
}
}
}
4. 존재 여부 필터링: exists 쿼리
exists 쿼리는 특정 필드가 존재하는(즉, null이 아니거나 누락되지 않은) 문서를 식별합니다.
예시: 이메일 주소를 제공한 모든 사용자 찾기.
GET /users/_search
{
"query": {
"exists": {
"field": "email_address"
}
}
}
bool 쿼리로 복잡한 로직 구성하기
실제 검색 애플리케이션의 거의 모든 경우 여러 기준을 결합해야 합니다. bool 쿼리는 이를 위한 필수 도구로, Boolean 논리를 사용하여 다른 쿼리 절을 결합할 수 있습니다.
bool 내의 절
bool 쿼리는 네 가지 주요 절을 허용합니다:
must: 이 배열 내의 모든 절이 일치해야 합니다.must의 절은 관련성 점수에 기여합니다.filter: 이 배열 내의 모든 절이 일치해야 하지만, 점수가 계산되지 않는 컨텍스트에서 실행됩니다. 따라서 엄격한 포함/제외 기준에 대해 훨씬 빠릅니다.should: 이 배열에서 적어도 하나의 절이 일치해야 합니다. 이러한 절은 관련성 점수에 영향을 미치지만 일치에는 선택 사항입니다.must_not: 이 배열의 절 중 어느 것도 일치해서는 안 됩니다(논리적 NOT과 동일).
실용적인 bool 쿼리 예시
여러 개념을 결합하여 'security'를 언급하고 초안을 제외하며 'US' 지역에서 사용 가능한 높은 우선순위의 문서를 찾아보겠습니다.
GET /logs/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"content": "security breach"
}
}
],
"filter": [
{
"term": {
"region.keyword": "US"
}
}
],
"should": [
{
"term": {
"priority": 5
}
}
],
"must_not": [
{
"term": {
"status.keyword": "DRAFT"
}
}
]
}
}
}
예시 설명:
- Must: 문서는 분석된 콘텐츠 필드에 "security breach"라는 구문을 포함해야 합니다.
- Filter: 문서는 'US' 지역으로 태그되어야 합니다 (빠르고 정확한 일치).
- Should:
priority: 5와 일치하는 문서는 관련성 점수에서 부스트를 받지만,must및filter절을 충족하는 낮은 우선순위의 문서도 계속 반환됩니다. - Must Not: 'DRAFT'로 표시된 문서는 엄격히 제외됩니다.
쿼리 구성을 위한 모범 사례
검색이 정확하고 성능이 좋도록 하려면 다음 지침을 따르세요:
- 점수가 필요 없는 기준에는
must보다filter를 선호하세요. 포함/제외만 확인하는 경우(예: ID, 정확한 날짜 또는 상태로 필터링) 항상bool쿼리 내에서filter절을 사용하세요. 이는 캐싱을 활용하고 비용이 많이 드는 점수 계산을 피합니다. - 정확한 쿼리를 현명하게 사용하세요:
text(분석됨)로 매핑된 필드에는match를 사용하세요.keyword(분석되지 않음)로 매핑된 필드에는term또는 범위 쿼리를 사용하세요. - 깊은 중첩을 피하세요: 가능하지만, 깊게 중첩된
bool쿼리는 읽고 디버그하기 어려워지고 때로는 성능 저하로 이어질 수 있습니다. minimum_should_match활용:should절의 경우minimum_should_match(예:1또는2로)를 설정하면 해당 선택적 기준 중 특정 수를 충족하도록 강제하여, 점수에 기여할 수 있도록 하면서 필수 기준으로 효과적으로 전환합니다.
매핑이 쿼리의 의미를 결정합니다
대부분의 Query DSL 실수는 매핑에서 시작됩니다. 쿼리가 올바르게 보여도 필드가 생각한 것과 다르게 매핑되면 혼란스러운 결과를 반환할 수 있습니다.
일반적인 패턴은 키워드 하위 필드가 있는 텍스트 필드입니다:
{
"mappings": {
"properties": {
"title": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"status": { "type": "keyword" },
"created_at": { "type": "date" },
"price": { "type": "double" }
}
}
}
분석된 전문 검색 동작이 필요하면 title에 match를 사용하세요. 정확한 제목 값이 필요하면 title.keyword에 term을 사용하세요. status는 이미 키워드이므로 term을 사용하세요. created_at 또는 price는 날짜 및 숫자 값이므로 range를 사용하세요.
텍스트 필드에 대한 term 쿼리가 예상대로 작동하지 않으면 문제는 종종 분석에 있습니다. 저장된 토큰이 소문자화, 분할, 형태소 분석 또는 기타 방식으로 변경되었을 수 있습니다. 쿼리를 변경하기 전에 매핑을 확인하세요.
GET /products/_mapping
텍스트 분석 문제의 경우 _analyze가 유용합니다:
GET /products/_analyze
{
"field": "description",
"text": "Cloud Computing"
}
이를 통해 Elasticsearch가 검색할 토큰을 보여줍니다.
match, match_phrase, multi_match
match는 일상적인 전문 검색 쿼리이지만, 사용할 유일한 쿼리는 아닙니다.
단어 순서가 중요할 때는 match_phrase를 사용하세요:
GET /products/_search
{
"query": {
"match_phrase": {
"description": "wireless charging stand"
}
}
}
이는 제품명, 로그 메시지, 문서 제목 및 정확한 순서가 의미를 갖는 구문에 유용합니다. match보다 더 엄격하므로 더 적은 문서를 반환할 수 있습니다.
동일한 사용자 입력이 여러 필드를 검색해야 할 때는 multi_match를 사용하세요:
GET /products/_search
{
"query": {
"multi_match": {
"query": "noise cancelling headphones",
"fields": ["title^3", "description", "brand^2"]
}
}
}
^3 및 ^2 부스트는 Elasticsearch에 title 및 brand의 일치가 description의 일치보다 더 중요하게 계산되어야 함을 알려줍니다. 부스트는 문서가 반드시 첫 번째로 순위가 매겨질 것이라는 보장이 아닙니다. 점수 힌트입니다. 부스트를 너무 공격적으로 조정하기 전에 실제 쿼리로 테스트하세요.
클러스터에 부담을 주지 않는 페이지 매김
기본 from 및 size 매개변수는 얕은 페이지 매김에 적합합니다:
GET /products/_search
{
"from": 20,
"size": 10,
"query": {
"match": {
"description": "laptop sleeve"
}
}
}
깊은 페이지 매김은 다릅니다. 1,000페이지를 요청하면 Elasticsearch가 많은 결과를 정렬하고 건너뛰어야 합니다. 사용자 대상 검색의 경우 무제한 깊은 페이지 매김을 피하세요. 내보내기 또는 백그라운드 스캔의 경우 안정적인 정렬과 함께 search_after를 사용하세요:
GET /products/_search
{
"size": 100,
"sort": [
{ "created_at": "asc" },
{ "_id": "asc" }
],
"search_after": ["2025-01-10T12:00:00Z", "abc123"],
"query": {
"term": {
"status": "active"
}
}
}
search_after의 값은 이전 응답의 마지막 히트에서 sort 배열에서 가져옵니다. 이 접근 방식은 큰 결과 집합을 탐색하는 데 더 안정적입니다.
소스 필터링으로 응답 유용성 유지
검색 성능은 쿼리 실행만이 아닙니다. 거대한 문서를 반환하면 클라이언트, 네트워크 및 조정 노드가 느려질 수 있습니다. UI에 몇 개의 필드만 필요한 경우 해당 필드만 요청하세요:
GET /orders/_search
{
"_source": ["order_id", "customer_id", "total", "created_at", "status"],
"query": {
"bool": {
"filter": [
{ "term": { "status": "paid" } },
{ "range": { "created_at": { "gte": "now-7d/d" } } }
]
}
}
}
이렇게 하면 응답을 더 쉽게 읽을 수 있고 페이로드 크기를 줄일 수 있습니다. 좋은 인덱스 설계를 대체하지는 않지만, 문서에 현재 페이지에 필요하지 않은 큰 설명, 메타데이터 블롭 또는 중첩 배열이 포함된 경우 도움이 됩니다.
정렬 및 집계에는 올바른 필드가 필요합니다
분석된 텍스트에 대한 정렬은 일반적으로 실수입니다. 키워드, 숫자 또는 날짜 필드로 정렬하세요:
GET /products/_search
{
"sort": [
{ "price": "asc" },
{ "title.keyword": "asc" }
],
"query": {
"term": {
"status": "active"
}
}
}
많은 집계에도 동일하게 적용됩니다. 상태별 개수를 원하면 키워드 필드에 대해 집계하세요:
GET /orders/_search
{
"size": 0,
"aggs": {
"orders_by_status": {
"terms": {
"field": "status"
}
}
},
"query": {
"range": {
"created_at": {
"gte": "now-30d/d"
}
}
}
}
size: 0은 Elasticsearch에 일치하는 문서가 아닌 집계 결과만 원한다고 알려줍니다. 이는 응답을 더 깔끔하게 유지하는 작은 습관입니다.
explain 및 profile로 쿼리 디버깅
결과 순위가 이상할 때 단일 문서에 explain을 사용하세요:
GET /products/_explain/SKU10021
{
"query": {
"match": {
"description": "cloud computing"
}
}
}
쿼리가 느릴 때 비프로덕션 또는 신중하게 제어된 프로덕션 테스트에서 profile을 사용하세요:
GET /products/_search
{
"profile": true,
"query": {
"bool": {
"must": [
{ "match": { "description": "cloud computing" } }
],
"filter": [
{ "term": { "status": "active" } }
]
}
}
}
프로필 출력은 장황하지만, 시간이 텍스트 쿼리, 필터, 스크립트 또는 요청의 다른 부분에서 소비되는지 보여줄 수 있습니다. 애플리케이션 코드에서 프로파일링을 활성화된 상태로 두지 마세요. 디버깅 도구로 사용하세요.
현명한 쿼리 작성 습관
대부분의 애플리케이션 검색의 경우 다음 순서로 요청을 작성하세요:
- 정확한 제약 조건을
filter에 넣습니다: 테넌트 ID, 상태, 지역, 날짜 범위, 권한. - 사용자가 입력한 텍스트를
must에match,match_phrase또는multi_match와 함께 넣습니다. should는 하드 요구 사항이 아닌 순위 기본 설정에 사용하고,minimum_should_match를 설정하지 않은 경우에만 사용합니다._source를 호출자가 필요한 필드로 제한합니다.- 페이지 매김 또는 내보내기가 중요한 경우 안정적인 정렬을 추가합니다.
- Elasticsearch를 탓하기 전에 매핑을 확인합니다.
Query DSL은 필터링, 점수 계산, 정렬 및 응답 형태를 분리하기 때문에 강력합니다. 이러한 작업을 분리하면 쿼리를 더 쉽게 읽고, 조정하고, 프로덕션에서 덜 놀라게 됩니다.
작은 문제 해결 예시
사용자가 ACME-1000을 검색했지만 제품이 존재함에도 결과가 없다고 가정해 보겠습니다. 즉시 와일드카드를 추가하지 마세요. 먼저 매핑을 확인하세요. sku가 keyword이면 다음이 작동해야 합니다:
GET /products/_search
{
"query": {
"term": {
"sku": "ACME-1000"
}
}
}
sku가 실수로 text로 매핑된 경우 분석이 값을 분할하거나 변경했을 수 있습니다. 경우에 따라 계속 쿼리할 수 있지만, 더 나은 수정은 일반적으로 향후 인덱스에 대한 매핑 변경입니다. 정확한 식별자, 상태, 지역 및 테넌트 ID는 키워드와 같은 필드여야 합니다. 사람이 작성한 설명과 제목은 텍스트 필드여야 합니다. 매핑이 사람들이 실제로 데이터를 검색하는 방식과 일치하면 Query DSL이 훨씬 쉬워집니다.