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 쿼리는 네 가지 주요 절을 허용합니다:

  1. must: 이 배열 내의 모든 절이 일치해야 합니다. must의 절은 관련성 점수에 기여합니다.
  2. filter: 이 배열 내의 모든 절이 일치해야 하지만, 점수가 계산되지 않는 컨텍스트에서 실행됩니다. 따라서 엄격한 포함/제외 기준에 대해 훨씬 빠릅니다.
  3. should: 이 배열에서 적어도 하나의 절이 일치해야 합니다. 이러한 절은 관련성 점수에 영향을 미치지만 일치에는 선택 사항입니다.
  4. 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와 일치하는 문서는 관련성 점수에서 부스트를 받지만, mustfilter 절을 충족하는 낮은 우선순위의 문서도 계속 반환됩니다.
  • 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" }
    }
  }
}

분석된 전문 검색 동작이 필요하면 titlematch를 사용하세요. 정확한 제목 값이 필요하면 title.keywordterm을 사용하세요. 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에 titlebrand의 일치가 description의 일치보다 더 중요하게 계산되어야 함을 알려줍니다. 부스트는 문서가 반드시 첫 번째로 순위가 매겨질 것이라는 보장이 아닙니다. 점수 힌트입니다. 부스트를 너무 공격적으로 조정하기 전에 실제 쿼리로 테스트하세요.

클러스터에 부담을 주지 않는 페이지 매김

기본 fromsize 매개변수는 얕은 페이지 매김에 적합합니다:

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에 일치하는 문서가 아닌 집계 결과만 원한다고 알려줍니다. 이는 응답을 더 깔끔하게 유지하는 작은 습관입니다.

explainprofile로 쿼리 디버깅

결과 순위가 이상할 때 단일 문서에 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" } }
      ]
    }
  }
}

프로필 출력은 장황하지만, 시간이 텍스트 쿼리, 필터, 스크립트 또는 요청의 다른 부분에서 소비되는지 보여줄 수 있습니다. 애플리케이션 코드에서 프로파일링을 활성화된 상태로 두지 마세요. 디버깅 도구로 사용하세요.

현명한 쿼리 작성 습관

대부분의 애플리케이션 검색의 경우 다음 순서로 요청을 작성하세요:

  1. 정확한 제약 조건을 filter에 넣습니다: 테넌트 ID, 상태, 지역, 날짜 범위, 권한.
  2. 사용자가 입력한 텍스트를 mustmatch, match_phrase 또는 multi_match와 함께 넣습니다.
  3. should는 하드 요구 사항이 아닌 순위 기본 설정에 사용하고, minimum_should_match를 설정하지 않은 경우에만 사용합니다.
  4. _source를 호출자가 필요한 필드로 제한합니다.
  5. 페이지 매김 또는 내보내기가 중요한 경우 안정적인 정렬을 추가합니다.
  6. Elasticsearch를 탓하기 전에 매핑을 확인합니다.

Query DSL은 필터링, 점수 계산, 정렬 및 응답 형태를 분리하기 때문에 강력합니다. 이러한 작업을 분리하면 쿼리를 더 쉽게 읽고, 조정하고, 프로덕션에서 덜 놀라게 됩니다.

작은 문제 해결 예시

사용자가 ACME-1000을 검색했지만 제품이 존재함에도 결과가 없다고 가정해 보겠습니다. 즉시 와일드카드를 추가하지 마세요. 먼저 매핑을 확인하세요. skukeyword이면 다음이 작동해야 합니다:

GET /products/_search
{
  "query": {
    "term": {
      "sku": "ACME-1000"
    }
  }
}

sku가 실수로 text로 매핑된 경우 분석이 값을 분할하거나 변경했을 수 있습니다. 경우에 따라 계속 쿼리할 수 있지만, 더 나은 수정은 일반적으로 향후 인덱스에 대한 매핑 변경입니다. 정확한 식별자, 상태, 지역 및 테넌트 ID는 키워드와 같은 필드여야 합니다. 사람이 작성한 설명과 제목은 텍스트 필드여야 합니다. 매핑이 사람들이 실제로 데이터를 검색하는 방식과 일치하면 Query DSL이 훨씬 쉬워집니다.