Типы полей: text, keyword, числа и даты
Почему тип поля — это не просто «формат»
В реляционной базе данных тип колонки влияет на хранение и валидацию. В Elasticsearch тип поля определяет, как именно данные индексируются и какие запросы к ним допустимы. Неправильный тип — и поиск молча вернёт ноль документов, а причина не очевидна.
Всё это прописывается в маппинге — схеме индекса. Задать маппинг можно явно при создании индекса или положиться на dynamic mapping: ES угадает тип по первому вставленному значению. Подробнее — в статье Маппинг: явный, динамический и шаблоны индексов. Здесь сосредоточимся на самих типах.
text и keyword: главное различие
Это важнейшая пара типов — оба хранят строки, но устроены принципиально по-разному.
text — строка проходит через анализатор: делится на токены, приводится к нижнему регистру, отсеиваются стоп-слова. В инвертированном индексе оказываются именно токены, а не исходная строка. Именно это позволяет полнотекстовому поиску находить «поисковый движок» по запросу «поиск».
keyword — строка хранится как есть, без анализа. В индексе лежит точная строка "Quick Brown Fox", а не токены quick, brown, fox. Этот тип нужен для точных совпадений, сортировки и агрегаций.
flowchart TD
A["Строка: Quick Brown Fox"] --> B["Поле text"]
A --> C["Поле keyword"]
B --> D["Анализатор: токенизация + lowercase"]
D --> E1["quick"]
D --> E2["brown"]
D --> E3["fox"]
C --> F["Строка без изменений: Quick Brown Fox"]
style E1 fill:#d4edda,stroke:#28a745
style E2 fill:#d4edda,stroke:#28a745
style E3 fill:#d4edda,stroke:#28a745
style F fill:#cce5ff,stroke:#004085Практическое следствие: term-запрос не работает по text-полю так, как кажется. Если поле title имеет тип text и хранит строку "Elasticsearch 8", то запрос term: { "title": "Elasticsearch 8" } не найдёт ничего — в индексе лежат токены elasticsearch и 8, а не исходная строка. Правило простое:
| Задача | Нужный тип | Запрос |
|---|---|---|
| Полнотекстовый поиск | text | match, match_phrase |
| Точное совпадение (статус, тег) | keyword | term, terms |
| Сортировка по алфавиту | keyword | sort |
| Агрегация по значению | keyword | terms aggregation |
Multi-field: одно поле — два индекса
На практике часто нужно и то и другое: искать полнотекстово и при этом агрегировать точные значения. Для этого в ES есть multi-field — один источник данных индексируется несколькими способами через параметр fields.
Пример: поле title одновременно как text (для поиска) и как title.keyword (для сортировки и агрегаций):
PUT /products
{
"mappings": {
"properties": {
"title": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}После этого title работает как text, а title.keyword — как keyword. Запрашивают их независимо:
GET /products/_search
{
"query": {
"match": { "title": "поиск по тексту" }
},
"sort": [{ "title.keyword": "asc" }]
}ignore_above: 256 означает: если строка длиннее 256 символов, keyword-вариант её игнорирует — защита от случайного индексирования длинных строк.
Dynamic mapping по умолчанию создаёт multi-field для любой строки: поле автоматически получает text + .keyword. Это удобно, но при явном маппинге лучше добавлять keyword только туда, где он реально нужен — экономит место.
Числовые типы
ES поддерживает несколько числовых типов. Выбирайте наименьший достаточный — это экономит RAM и ускоряет агрегации:
| Тип | Диапазон / точность |
|---|---|
byte | $-128 \ldots 127$ |
short | $-32\,768 \ldots 32\,767$ |
integer | $\approx \pm 2.1 \times 10^{9}$ |
long | $\approx \pm 9.2 \times 10^{18}$ |
float | 32-битное IEEE 754 |
double | 64-битное IEEE 754 |
scaled_float | хранится как long, делится на scaling_factor |
scaled_float удобен для денег: цену в рублях с копейками лучше хранить как целое, умноженное на 100 (scaling_factor: 100), — точнее и компактнее, чем float.
PUT /orders
{
"mappings": {
"properties": {
"price": { "type": "scaled_float", "scaling_factor": 100 },
"quantity": { "type": "integer" },
"user_id": { "type": "long" }
}
}
}Числовые поля работают с запросами range, term/terms и со всеми метрическими агрегациями: avg, sum, min, max.
Даты
Тип date принимает строки в формате ISO 8601 ("2024-06-15T10:30:00Z"), миллисекунды с эпохи UNIX и кастомные форматы. Внутри ES всегда хранит дату как long — миллисекунды с 01.01.1970 UTC; формат нужен только для парсинга при записи.
PUT /events
{
"mappings": {
"properties": {
"created_at": {
"type": "date",
"format": "strict_date_optional_time||epoch_millis"
}
}
}
}Два формата через || означают: принять любой из них. strict_date_optional_time — ISO 8601 с обязательной датой и опциональным временем.
По датам работают range-запросы с date math — встроенными выражениями вроде now-7d/d (семь дней назад, округлить до начала дня):
GET /events/_search
{
"query": {
"range": {
"created_at": {
"gte": "now-30d/d",
"lte": "now/d"
}
}
}
}Объекты и вложенные структуры
ES автоматически «уплощает» вложенные JSON-объекты:
{
"author": {
"first_name": "Иван",
"last_name": "Петров"
}
}В маппинге появятся поля author.first_name и author.last_name — тип object. Для большинства задач этого достаточно. Но если у вас массив объектов и нужно сохранить связи внутри каждого элемента — например, гарантировать, что запрос first_name: Иван AND last_name: Сидоров не найдёт документ с Иваном Петровым и Алексеем Сидоровым, — понадобится тип nested. Это отдельный сценарий, разобранный в Маппинг: явный, динамический и шаблоны индексов.
Как посмотреть маппинг индекса
Чтобы узнать, какие типы назначены полям:
GET /products/_mappingОтвет — дерево полей с типами. Проверяйте его перед написанием запросов: он сразу объяснит, почему term не находит то, что ожидаешь.
См. также
- Документ, индекс и JSON: модель данных
- Маппинг: явный, динамический и шаблоны индексов
- Инвертированный индекс и движок Lucene
- Анатомия анализатора: char filters, токенизатор, token filters
- Query DSL: структура запроса и контекст query vs filter
- Полнотекстовые запросы: match, match_phrase, multi_match
- Точные совпадения и фильтры: term, range, exists
- Метрические и бакетные агрегации