Query DSL: структура запроса и контекст query vs filter

Query DSL — JSON-язык, на котором вы общаетесь с Elasticsearch. Каждый поисковый запрос — это тело HTTP-запроса к эндпоинту _search. Прежде чем разбирать конкретные типы запросов (match, term, bool), нужно понять общую схему: что куда идёт и почему устроено именно так.

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

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

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

Check yourself
Вы добавляете условие `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

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

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

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

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

УсловиеКонтекстПричина
Полнотекстовый поиск (match, match_phrase)queryНужен _score для ранжирования
Точное значение по keyword-полюfilterБинарное условие, кешируется
Диапазон дат или чисел (range)filterБинарное условие, кешируется
Проверка наличия поля (exists)filterБинарное условие
Поиск с управлением релевантностью (boost)query_score нужен для управления рангом

Распространённая ошибка — помещать все условия в must, включая бинарные «только активные» или «только за последние 30 дней». Такие условия лучше перенести в filter: качество поиска не изменится, а скорость вырастет за счёт кеширования.

См. также

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