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 DSL
  • size — сколько документов вернуть (по умолчанию 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"]
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"]
Структура тела _search: поле query делится на два контекста — с вычислением _score и без

Два режима выполнения: 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": "Поиск по документам..." } }
    ]
  }
}

Первым стоит документ с более высоким баллом — он признан более релевантным.

Проверь себя
Два документа прошли условие `match` по полю «description». У первого `_score` = 3.2, у второго `_score` = 1.1. В каком порядке они появятся в ответе и почему?

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 закешит эти фильтры: повторный запрос с теми же условиями выполнится значительно быстрее.

Проверь себя
Вы добавляете условие `range` по цене в секцию `filter`. Как изменится `_score` документов, которые прошли этот фильтр?

Комбинация: ранжирование внутри отфильтрованной выборки

Настоящая сила 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. Только для оставшихся документов mustmatch вычисляет _score: насколько описание соответствует запросу «беспроводные наушники»

3. Результаты сортируются по _score

Документы, не прошедшие фильтры, отсеиваются до расчёта релевантности. Это и быстро, и логично: цена и статус не должны влиять на то, насколько товар совпадает с поисковым запросом пользователя.

Проверь себя
Почему условие «только товары в наличии» (status = in_stock) лучше поместить в `filter`, а не в `must`? Дайте два довода.

Когда что выбирать

Простое правило: спросите себя — нужно ли здесь «насколько хорошо» или просто «да/нет»?

УсловиеКонтекстПричина
Полнотекстовый поиск (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 пустым — полезно, когда нужны только агрегации без самих документов.

Быстрое повторение
Почему Elasticsearch кеширует filter-условия?
Быстрое повторение
Query context вычисляет _score, но filter context — нет. Какое ещё принципиальное различие?

См. также

Быстрое повторение
Вы пишете полнотекстовый поиск по описанию товара и хотите результаты отсортированы по релевантности. Query или filter context?