Точные совпадения и фильтры: term, range, exists
Запросы без анализа ввода
В предыдущей статье мы разбирали, как match прогоняет строку через анализатор перед поиском. Запросы этой статьи работают иначе: входное значение передаётся буквально — без токенизации, нормализации и стемминга.
Три сценария, где это нужно:
- keyword-поля — статусы (
"published"), категории, теги, ID - Числа и диапазоны — цены, количества, рейтинги
- Даты и временны́е окна — логи, заказы, события
Эти запросы логично помещать в filter context (внутри bool.filter): ES кеширует результаты и не тратит ресурсы на вычисление _score. О разнице между query и filter context — в Query DSL: структура запроса и контекст query vs filter.
term: точное совпадение одного значения
GET /products/_search
{
"query": {
"term": {
"status": "published"
}
}
}ES ищет документы, где status содержит в точности строку "published". Никакого lower-case, никакой морфологии — значение проверяется «как есть». Поле status должно быть типа keyword.
Расширенный синтаксис, когда нужен буст:
GET /products/_search
{
"query": {
"term": {
"category": {
"value": "electronics",
"boost": 1.5
}
}
}
}#### terms: несколько значений (аналог SQL IN)
GET /products/_search
{
"query": {
"terms": {
"status": ["published", "featured", "archived"]
}
}
}Документ совпадёт, если поле содержит хотя бы одно из перечисленных значений.
Классическая ловушка: term по text-полю
Это самая распространённая ошибка при знакомстве с ES. Разберём на конкретном примере.
В индексе лежит документ:
PUT /articles/_doc/1
{
"title": "Elasticsearch Полнотекстовый поиск"
}При индексации анализатор standard разбил строку на токены и привёл их к нижнему регистру. В инвертированном индексе хранится: elasticsearch, полнотекстовый, поиск.
Теперь запрос:
GET /articles/_search
{
"query": {
"term": {
"title": "Elasticsearch"
}
}
}Результат: 0 документов. Запрос term ищет строку "Elasticsearch" (с заглавной E) буквально, а в индексе лежит "elasticsearch" (строчная). У match здесь проблемы бы не было — он сам прогоняет ввод через анализатор и получает токен "elasticsearch".
flowchart TD
STORE[("Инвертированный индекс: токены elasticsearch, поиск")]
Q1["match: 'Elasticsearch Поиск'"] --> AN["Анализатор standard → elasticsearch, поиск"]
AN -->|"✅ найдено"| STORE
Q2["term: 'Elasticsearch'"] --> NO["Без анализатора → 'Elasticsearch' буквально"]
NO -->|"❌ нет совпадения"| STOREПравило без исключений: для text-полей — match; для keyword-полей — term. Проверить тип поля: GET /my_index/_mapping. О типах подробнее — в Типы полей: text, keyword, числа и даты.
range: диапазоны по числам и датам
Четыре параметра задают границы интервала:
| Параметр | Смысл |
|---|---|
gt | строго больше ($>$) |
gte | больше или равно ($\geq$) |
lt | строго меньше ($<$) |
lte | меньше или равно ($\leq$) |
#### Числовой диапазон
GET /products/_search
{
"query": {
"range": {
"price": {
"gte": 1000,
"lte": 5000
}
}
}
}Найдёт продукты с ценой $1000 \leq \text{price} \leq 5000$.
#### Диапазоны по датам
ES принимает ISO 8601 и удобные относительные выражения:
GET /logs/_search
{
"query": {
"range": {
"timestamp": {
"gte": "now-7d/d",
"lte": "now/d"
}
}
}
}now-7d/d — текущий момент минус 7 дней, округлённый до начала суток. Округление (/d, /h, /M) критично для кеширования: точная метка now меняется каждую секунду и не кешируется. С округлением фильтр стабилен в течение суток и хорошо ложится в кеш.
Фиксированный диапазон с явным форматом:
GET /orders/_search
{
"query": {
"range": {
"created_at": {
"gte": "2024-01-01",
"lt": "2024-02-01",
"format": "yyyy-MM-dd"
}
}
}
}format нужен только если формат строки в запросе отличается от формата, заданного в маппинге.
exists: есть ли поле в документе
GET /products/_search
{
"query": {
"exists": {
"field": "description"
}
}
}Вернёт документы, у которых поле description присутствует и не равно null. Для поиска документов без этого поля — оберните в must_not:
GET /products/_search
{
"query": {
"bool": {
"must_not": {
"exists": {
"field": "description"
}
}
}
}
}Аналог WHERE description IS NULL в SQL. Подробнее о bool-запросе — в Составные запросы: bool и комбинирование условий.
Тонкость: пустая строка "" для keyword-поля — это существующее значение, exists её найдёт. Поле считается отсутствующим, если его нет в JSON-документе совсем, оно явно равно null, или массив целиком состоит из null.
Всё вместе: фильтры в bool.filter
На практике эти запросы редко стоят самостоятельно. Классический паттерн — полнотекстовый поиск в must плюс фильтры в filter:
GET /products/_search
{
"query": {
"bool": {
"must": [
{ "match": { "title": "ноутбук" } }
],
"filter": [
{ "term": { "status": "published" } },
{ "range": { "price": { "gte": 30000, "lte": 150000 } } },
{ "exists": { "field": "image_url" } }
]
}
}
}match в must вычисляет _score и ранжирует результаты. Три фильтра в filter работают в filter context — кешируются и не влияют на релевантность. Это стандартная архитектура поискового запроса в Elasticsearch.