Составные запросы: bool и комбинирование условий

match, term, range — каждый из этих запросов решает одну задачу. В предыдущей статье мы разобрали точные фильтры, и в финальном примере они уже оказались внутри bool.filter. Пришло время разобрать bool-запрос целиком — он и есть каркас, в который монтируются любые условия.

Четыре секции bool

bool сам по себе не задаёт условие — он комбинирует другие запросы через четыре секции:

{
  "query": {
    "bool": {
      "must":     [...],
      "filter":   [...],
      "should":   [...],
      "must_not": [...]
    }
  }
}

Все четыре секции опциональны: можно использовать одну, можно все сразу.

flowchart LR Q([bool query]) --> M[must] Q --> F[filter] Q --> S[should] Q --> N[must_not] style Q fill:#e8f4f8,stroke:#4a90d9,color:#000 style M fill:#d4edda,stroke:#28a745,color:#000 style F fill:#cce5ff,stroke:#004085,color:#000 style S fill:#fff3cd,stroke:#856404,color:#000 style N fill:#f8d7da,stroke:#721c24,color:#000 M --- MA["Обязательно\nВлияет на _score"] F --- FA["Обязательно\nНе влияет на _score\nКешируется"] S --- SA["Опционально\nВлияет на _score"] N --- NA["Исключает документы\nКешируется"]
flowchart LR
    Q([bool query]) --> M[must]
    Q --> F[filter]
    Q --> S[should]
    Q --> N[must_not]
    style Q fill:#e8f4f8,stroke:#4a90d9,color:#000
    style M fill:#d4edda,stroke:#28a745,color:#000
    style F fill:#cce5ff,stroke:#004085,color:#000
    style S fill:#fff3cd,stroke:#856404,color:#000
    style N fill:#f8d7da,stroke:#721c24,color:#000
    M --- MA["Обязательно\nВлияет на _score"]
    F --- FA["Обязательно\nНе влияет на _score\nКешируется"]
    S --- SA["Опционально\nВлияет на _score"]
    N --- NA["Исключает документы\nКешируется"]
Четыре секции bool-запроса и их свойства

must: обязательно и влияет на оценку

Документ обязан совпасть с каждым условием в must. Каждый клоз вносит вклад в итоговый _score — чем точнее совпадение, тем выше оценка. Если клозов несколько, их скоры суммируются:

$$\text{score}_{\text{bool}} = \sum_{i \in \text{must}} \text{score}(c_i) + \sum_{j \in \text{should}} \text{score}(c_j)$$

Пример — ищем документы, где в заголовке есть «ноутбук» и в описании есть «SSD»:

GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title": "ноутбук" } },
        { "match": { "description": "SSD" } }
      ]
    }
  }
}

Документ, который лучше совпадает с обоими условиями сразу, окажется выше в выдаче.

filter: обязательно, без влияния на оценку

filter тоже требует совпадения, но не участвует в расчёте _score — это бинарное «да/нет». Elasticsearch кеширует результаты filter-клозов: если тот же фильтр встречается в следующем запросе, ES использует готовый список документов без повторного обхода индекса.

Классический паттерн — полнотекстовый поиск в must, бизнес-фильтры в filter:

GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title": "ноутбук" } }
      ],
      "filter": [
        { "term":  { "status": "in_stock" } },
        { "range": { "price": { "lte": 100000 } } }
      ]
    }
  }
}

match ранжирует ноутбуки по релевантности. Фильтры по статусу и цене просто отсекают неподходящие — без влияния на порядок выдачи.

Правило: всё, что не должно менять ранжирование (статус, цена, диапазон дат, наличие поля) — в filter, а не в must. Подробнее о разнице контекстов — в статье Query DSL: структура запроса и контекст query vs filter.

Проверь себя
Что изменится, если переместить `{ "term": { "status": "in_stock" } }` из секции `filter` в `must`?

must_not: исключение из результатов

Документ, совпавший с любым условием в must_not, полностью исключается из выдачи. На _score не влияет; работает в filter context — результаты кешируются.

GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title": "ноутбук" } }
      ],
      "must_not": [
        { "term": { "brand": "NoName" } },
        { "term": { "status": "discontinued" } }
      ]
    }
  }
}

Ноутбуки марки NoName и снятые с производства не попадут в результаты — остальные ранжируются как обычно.

should: предпочтение, не требование

Это самая тонкая секция. При наличии must или filter условия в should необязательны: документ попадёт в выдачу, даже если ни одно из них не выполнено. Но каждое совпавшее should-условие добавляет к _score:

GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title": "ноутбук" } }
      ],
      "should": [
        { "term": { "brand": "apple" } },
        { "range": { "rating": { "gte": 4.5 } } }
      ]
    }
  }
}

Ноутбук без Apple и с низким рейтингом всё равно появится в выдаче — но окажется ниже ноутбуков, которые совпали с одним или обоими should-клозами.

Если в bool нет must и filter — только should — поведение меняется: документ обязан совпасть хотя бы с одним клозом. Это аналог OR:

GET /products/_search
{
  "query": {
    "bool": {
      "should": [
        { "match": { "title": "ноутбук" } },
        { "match": { "title": "laptop" } }
      ]
    }
  }
}
Проверь себя
В `bool`-запросе только три `should`-клоза — никаких `must` и `filter`. Сколько из них должны совпасть, чтобы документ попал в выдачу?

minimum_should_match: сколько should обязаны совпасть

По умолчанию при наличии must/filter совпадение с нулём should-клозов — это нормально. Параметр minimum_should_match поднимает этот порог:

GET /products/_search
{
  "query": {
    "bool": {
      "filter": [
        { "term": { "category": "смартфоны" } }
      ],
      "should": [
        { "term": { "brand": "apple" } },
        { "term": { "brand": "samsung" } },
        { "term": { "brand": "google" } }
      ],
      "minimum_should_match": 1
    }
  }
}

Теперь смартфон обязан быть от Apple, Samsung или Google — остальные бренды отсекаются. При этом совпавший бренд всё равно добавляет к _score — это не просто фильтр, а мягкое ранжирование одновременно.

minimum_should_match принимает несколько форматов:

  • целое число: 1, 2
  • процент от числа клозов: "75%"
  • относительное выражение: "-1" (все клозы, кроме одного)

Вложенные bool: составная логика

Плоский bool не может выразить (A AND B) OR C в одном уровне. Для этого вкладывают один bool внутрь другого:

GET /products/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "bool": {
            "must": [
              { "match": { "title": "ноутбук" } },
              { "term":  { "brand": "apple" } }
            ]
          }
        },
        { "match": { "title": "MacBook" } }
      ],
      "minimum_should_match": 1
    }
  }
}

Документ совпадёт, если: «ноутбук» И бренд Apple — или «MacBook» в заголовке. Каждый вложенный bool считает свой скор, который суммируется в родительском.

Итог: когда какую секцию использовать

СекцияСовпадение обязательно?Влияет на _scoreКешируется
mustДаДаНет
filterДаНетДа
shouldНет (если есть must/filter)ДаНет
must_not— (исключает)НетДа
Быстрое повторение
Если в `bool` есть только `should` без `must` и `filter`, как документ попадает в выдачу?
Быстрое повторение
В чём ключевое отличие между `must` и `filter` в bool-запросе?
Быстрое повторение
Если документ не совпадает с условием в `must`, что произойдёт?

См. также