학습 목표
Query DSL JSON 구조와 _search 요청 형태를 읽고 작성할 수 있다.
match·term·range를 text·keyword mapping에 맞게 선택할 수 있다.
bool의 must·filter·should·must_not 조합으로 로그 검색 쿼리를 짤 수 있다.
query context와 filter context 차이(점수·캐시)를 설명할 수 있다.
from/size 한계와 search_after·PIT 기반 deep pagination을 설명할 수 있다.
문제 상황
- 3편 mapping 후
level: ERROR를 match에 넣었더니 WARN도 섞임 — keyword는 term message에payment timeout구문 검색이 안 됨 — match_phrase 필요bool에 조건을 나열했는데 점수가 이상하고 느림 — filter vs must 구분 없음- Kibana에서 2페이지 넘기면 느리거나 결과가 중복 —
from: 10000deep pagination sort없이 search_after 쓰다 순서가 바뀜 — tie-breaker_shard_doc필요- 2편
match맛보기만으로는 운영 로그 쿼리 패턴이 안 잡힘
앞 편에서 mapping·analyzer로 필드 타입을 고정했다. 이번 편은 검색 언어 — Query DSL. aggregation·shard는 5편, Kibana KQL은 10편이다.
1. Query DSL이란?
Query DSL — Elasticsearch 검색·필터를 표현하는 JSON 트리.
POST /app-logs/_search
{
"query": { ... },
"sort": [ ... ],
"from": 0,
"size": 20
}
| 최상위 키 | 역할 |
|---|---|
| query | 어떤 document를 고를지 |
| sort | 정렬 (시간·필드·_score) |
| from / size | offset 페이지 (얕은 페이징) |
| search_after | 커서 페이징 (deep) |
| _source | 반환 필드 제한 |
| aggs | 집계 — 5편 |
- Kibana Discover — UI 필터가 내부적으로 bool + filter DSL (또는 KQL → DSL 변환)
- GET
_search+sourceURL 인코딩 가능하지만 POST + JSON이 일반적
2. match · term · range
3편 text vs keyword에 맞는 쿼리 선택.
| 쿼리 | 필드 | 동작 |
|---|---|---|
| match | text | analyzer 적용 후 term 매칭 + 관련도 점수 |
| match_phrase | text | 구문 순서 유지 |
| term | keyword·숫자·date | 정확 값 (analyze 안 함) |
| range | 숫자·date | gte·lte 구간 |
// code/log-search-examples.json (발췌)
{
"query": {
"bool": {
"filter": [
{ "term": { "level": "ERROR" } },
{ "term": { "service": "order-api" } },
{ "range": { "@timestamp": { "gte": "now-1h" } } }
],
"must": [
{ "match": { "message": "payment timeout" } }
]
}
}
}
term값은 mapping과 동일 —ERROR(대문자 keyword)match는messagetext — analyzer가payment·timeoutterm으로 검색- range on date —
@timestamp는 date mapping 필수
자주 하는 실수
| 증상 | 원인 | 수정 |
|---|---|---|
| level 필터 안 먹음 | match on keyword |
term |
| 대소문자 mismatch | term에 error |
keyword 저장값과 일치 |
| 구문 검색 실패 | match만 사용 |
match_phrase |
| 날짜 range 0건 | text 필드에 range | date mapping |
3. Query context vs Filter context

| Query context | Filter context | |
|---|---|---|
| 예 | match, must 안의 query |
filter 절, term, range |
| _score | 계산 | 0 (무시) |
| 캐시 | 매번 평가 | bitset 캐시 가능 |
| 용도 | 관련도 순위 | 예/아니오 필터 |
- bool의
filter절에 넣으면 — 점수 없이 빠르게 걸러짐 - 관련도 필요 없는 로그 조회 — 전부 filter로도 가능 (
constant_score는 생략) - Kibana 필터 pill ≈ filter context
4. bool 쿼리
여러 조건을 조합하는 컨테이너.

| 절 | 의미 | 점수 |
|---|---|---|
| must | 반드시 만족 | 영향 (query context) |
| filter | 반드시 만족 | 영향 없음 (filter context) |
| should | 가산 (선택) | 만족 시 boost |
| must_not | 제외 | 영향 없음 |
// code/bool-filter-query.json
{
"query": {
"bool": {
"filter": [
{ "range": { "@timestamp": { "gte": "now-24h" } } },
{ "terms": { "level": ["ERROR", "WARN"] } }
],
"must": [
{ "match": { "message": "timeout" } }
],
"must_not": [
{ "term": { "service": "health-check" } }
],
"should": [
{ "match": { "message": "payment" } }
],
"minimum_should_match": 0
}
}
}
- 로그 tail 패턴 —
filter에 시간·level·service (캐시·빠름),must에 본문 검색 terms— keyword 배열 OR (levelERROR or WARN)minimum_should_match— should만 있을 때 최소 개수
5. 정렬과 페이지네이션
from · size (얕은 페이징)
{
"from": 0,
"size": 50,
"sort": [{ "@timestamp": "desc" }],
"query": { "match_all": {} }
}
from+size≤ 10,000 (기본index.max_result_window) — 그 이상은 거부- UI 1~N 페이지 — 보통 충분
- deep offset — shard마다 정렬·skip 비용 큼
search_after (deep pagination)

// code/search-after.example.json
{
"size": 50,
"sort": [
{ "@timestamp": "desc" },
{ "_shard_doc": "desc" }
],
"query": {
"bool": {
"filter": [
{ "range": { "@timestamp": { "gte": "now-7d" } } }
]
}
},
"search_after": [1704067200000, 12345]
}
| from / size | search_after | |
|---|---|---|
| 방식 | offset | 이전 페이지 마지막 sort 값 커서 |
| deep | 비권장 (10k 제한) | 권장 |
| sort | 선택 | 필수·유일해야 함 |
| 일관성 | index 변경 시 중복/누락 | PIT와 함께 쓰면 안정 |
- Point in Time (PIT) —
open_point_in_time으로 스냅샷 뷰 — 대량 export·scroll 대체 - scroll — legacy bulk export; 신규는 PIT + search_after 권장
- 응답
hits.hits[-1].sort값을 다음 요청search_after에 전달
6. 로그 검색 패턴 모음
최근 ERROR + 본문
{
"size": 100,
"sort": [{ "@timestamp": "desc" }],
"query": {
"bool": {
"filter": [
{ "term": { "level": "ERROR" } },
{ "range": { "@timestamp": { "gte": "now-15m" } } }
],
"must": [
{ "match_phrase": { "message": "connection refused" } }
]
}
}
}
trace_id로 한 요청 추적
{
"query": {
"bool": {
"filter": [
{ "term": { "trace_id": "a1b2c3d4e5f6" } }
]
}
},
"sort": [{ "@timestamp": "asc" }]
}
- keyword exact — term only
- 시간순 asc — 요청 흐름 재구성
_source 제한
{
"_source": ["@timestamp", "level", "service", "message"],
"query": { "match_all": {} }
}
- 대용량 필드 제외 — 네트워크·Kibana 부담 감소
7. 실무 체크리스트
| 항목 | 습관 |
|---|---|
| keyword 필터 | term / terms in filter |
| 본문 검색 | match / match_phrase in must |
| 시간 범위 | range on @timestamp in filter |
| 정렬 | 로그는 @timestamp desc 기본 |
| deep page | search_after + PIT, not from: 50000 |
| mapping 확인 | 쿼리 전 필드 타입 (3편) |
- Slow query — filter 과다·wildcard·leading
*— 21편 slow log - 10편 — Kibana에서 동일 조건 KQL로 표현
정리
- Query DSL — JSON
query트리; bool로 조건 조합 - match (text) vs term (keyword) — mapping과 쌍으로 기억
- filter — 점수 없음·캐시; 로그 차원 필터에 적합
- from/size — 얕은 페이징; search_after — deep·export
- 5편 — aggregation·shard·routing
다음에 다룰 것
- Aggregation과 클러스터
- terms·date histogram, node·shard·replica, routing
해당 내용은 Elasticsearch Guide (Query DSL, Search your data) 를 기반으로 합니다.
'ELK & Elastic Cloud' 카테고리의 다른 글
| Beats (0) | 2026.07.11 |
|---|---|
| Aggregation과 클러스터 (0) | 2026.07.08 |
| Mapping과 Analyzer (0) | 2026.07.03 |
| Elasticsearch란 (0) | 2026.07.02 |
| ELK · Elastic Cloud 오리엔테이션 (0) | 2026.06.30 |