Query DSL: структура запроса и контекст query vs filter
Query DSL — JSON-язык, на котором вы общаетесь с Elasticsearch. Каждый поисковый запрос — это тело HTTP-запроса к эндпоинту _search. Прежде чем разбирать конкретные типы запросов (match, term, bool), нужно понять общую схему: что куда идёт и почему устроено именно так.
Анатомия тела _search
Минимальный запрос — пустой: GET /my_index/_search без тела вернёт первые 10 документов. В реальных задачах тело запроса выглядит так:
GET /products/_search
{
"query": { ... },
"size": 10,
"from": 0,
"_source": ["title", "price"],
"sort": [...],
"highlight": { ... },
"aggs": { ... }
}Ключевые поля верхнего уровня:
query— само условие поиска; здесь и живёт Query DSLsize— сколько документов вернуть (по умолчанию 10)from— смещение для пагинации_source— какие поля включить в ответsort— порядок сортировкиhighlight— подсветка совпадений в текстеaggs— агрегации (подробно — в Метрические и бакетные агрегации)
Всё поле query — это и есть Query DSL. Туда вкладывается вся логика: от простого match до сложного bool с десятком вложенных условий.
flowchart TD
A["GET /index/_search"] --> B["query"]
A --> REST["size · from · _source · sort · highlight · aggs"]
B --> BOOL["bool"]
BOOL --> QC["must / should<br/>query context<br/>→ вычисляет _score"]
BOOL --> FC["filter / must_not<br/>filter context<br/>→ да/нет · кешируется"]
QC --> QT["match · match_phrase · multi_match"]
FC --> FT["term · range · exists"]Два режима выполнения: query context и filter context
Фундаментальный концепт, без которого остальные запросы непонятны. В Elasticsearch каждое условие поиска работает в одном из двух режимов.
Query context — режим «насколько хорошо документ совпадает с условием?». Elasticsearch вычисляет для каждого документа числовой балл _score. Чем выше — тем документ релевантнее. По умолчанию результаты сортируются именно по этому баллу: от большего к меньшему.
Filter context — режим «документ удовлетворяет условию или нет?». Ответ строго бинарный: да или нет. _score не вычисляется и не меняется. Зато фильтры Elasticsearch кеширует — результаты хранятся в оперативной памяти, и при повторном запросе с тем же условием ES выдаёт ответ почти мгновенно.
Разница не просто теоретическая: она влияет и на скорость запросов, и на качество ранжирования результатов.
Query context: когда важен _score
Query context нужен там, где результаты должны быть упорядочены по релевантности. Типичный пример — полнотекстовый поиск:
GET /articles/_search
{
"query": {
"match": {
"body": "elasticsearch поиск"
}
}
}Условие match работает в query context: ES анализирует текст запроса (применяет тот же анализатор, что и при индексации — подробнее об этом в Анатомия анализатора: char filters, токенизатор, token filters), ищет совпадения и вычисляет _score через алгоритм BM25. Как именно считается этот балл — отдельная тема, разобранная в Релевантность и BM25: как считается _score.
В ответе каждый документ получает своё _score:
{
"hits": {
"hits": [
{ "_score": 2.31, "_source": { "body": "Введение в Elasticsearch..." } },
{ "_score": 1.47, "_source": { "body": "Поиск по документам..." } }
]
}
}Первым стоит документ с более высоким баллом — он признан более релевантным.
Filter context: быстро и без оценки
Filter context нужен для условий, где важно «включить или исключить», а не «насколько хорошо совпадает». Типичные кандидаты: точный статус, категория, тег, диапазон цен или дат.
Filter context задаётся через ключевое слово filter внутри bool-запроса:
GET /products/_search
{
"query": {
"bool": {
"filter": [
{ "term": { "status": "active" } },
{ "range": { "price": { "lte": 5000 } } }
]
}
}
}Оба условия работают в filter context: документы либо проходят оба, либо нет. _score у всех результатов одинаков и не несёт смысла для ранжирования. Зато ES закешит эти фильтры: повторный запрос с теми же условиями выполнится значительно быстрее.
Комбинация: ранжирование внутри отфильтрованной выборки
Настоящая сила Query DSL проявляется, когда оба контекста работают вместе. Классический паттерн: filter сужает выборку до нужного подмножества, а must ранжирует оставшееся по релевантности:
GET /products/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"description": "беспроводные наушники"
}
}
],
"filter": [
{ "term": { "status": "in_stock" } },
{ "range": { "price": { "gte": 1000, "lte": 10000 } } }
]
}
}
}Что происходит пошагово:
1. Фильтры (filter) убирают из выборки всё, кроме товаров в наличии и в ценовом диапазоне от 1000 до 10 000 рублей — быстро, по кешу
2. Только для оставшихся документов must → match вычисляет _score: насколько описание соответствует запросу «беспроводные наушники»
3. Результаты сортируются по _score
Документы, не прошедшие фильтры, отсеиваются до расчёта релевантности. Это и быстро, и логично: цена и статус не должны влиять на то, насколько товар совпадает с поисковым запросом пользователя.
Когда что выбирать
Простое правило: спросите себя — нужно ли здесь «насколько хорошо» или просто «да/нет»?
| Условие | Контекст | Причина |
|---|---|---|
Полнотекстовый поиск (match, match_phrase) | query | Нужен _score для ранжирования |
| Точное значение по keyword-полю | filter | Бинарное условие, кешируется |
Диапазон дат или чисел (range) | filter | Бинарное условие, кешируется |
Проверка наличия поля (exists) | filter | Бинарное условие |
| Поиск с управлением релевантностью (boost) | query | _score нужен для управления рангом |
Распространённая ошибка — помещать все условия в must, включая бинарные «только активные» или «только за последние 30 дней». Такие условия лучше перенести в filter: качество поиска не изменится, а скорость вырастет за счёт кеширования.
Структура ответа _search
Ответ ES имеет устойчивую структуру, которая повторяется во всех запросах:
{
"took": 5,
"timed_out": false,
"_shards": { "total": 1, "successful": 1, "failed": 0 },
"hits": {
"total": { "value": 42, "relation": "eq" },
"max_score": 2.31,
"hits": [
{
"_index": "products",
"_id": "1",
"_score": 2.31,
"_source": { "title": "Наушники Sony WH-1000XM5", "price": 3500 }
}
]
}
}took— время выполнения в миллисекундахhits.total.value— полное количество совпавших документовhits.hits— массив результатов (доsizeштук)_score— балл релевантности; будетnull, если вы сортируете по другому полю (например, по дате или цене)
Частный случай: "size": 0 делает hits.hits пустым — полезно, когда нужны только агрегации без самих документов.
См. также
- Полнотекстовые запросы: match, match_phrase, multi_match
- Точные совпадения и фильтры: term, range, exists
- Составные запросы: bool и комбинирование условий
- Релевантность и BM25: как считается _score
- Бустинг полей и function_score: управление ранжированием
- Инвертированный индекс и движок Lucene