Поиск по частичному номеру документа — распространенная задача в системах управления документами, CRM и ERP-системах. В Elasticsearch для этого существует несколько подходов, но использование query_string требует понимания того, как работает анализ текста, экранирование спецсимволов и выбор правильной стратегии индексирования.
Самое главное помнить: стандартный анализатор (standard analyzer) разбивает номера документов вроде INV-2023-001 на отдельные токены (inv, 2023, 001). Поиск подстроки 2023-001 через query_string по полю с таким анализатором не сработает, так как дефис является разделителем. Для корректного частичного поиска необходимо заранее настроить маппинг (mapping) поля.
Подготовка маппинга (Mapping) — фундамент успеха
Прежде чем писать запрос, нужно определить, как данные будут храниться в индексе. Есть три основные стратегии:
Стратегия А: Поле keyword + wildcard (Рекомендуемая для точных префиксов/суффиксов)
Тип wildcard (доступен с Elasticsearch 7.9+) оптимизирован для запросов с подстановочными знаками (*, ?) без необходимости использования тяжелых n-gram’ов.
PUT /documents
{
"mappings": {
"properties": {
"doc_number": {
"type": "wildcard"
}
}
}
} Стратегия Б: Поле keyword + ngram / edge_ngram (Для поиска подстроки в середине)
Если нужно искать 2023 внутри INV-2023-001 без указания префикса INV-*, используют n-gram токенизацию на этапе индексирования.
PUT /documents
{
"settings": {
"analysis": {
"filter": {
"autocomplete_filter": {
"type": "edge_ngram",
"min_gram": 2,
"max_gram": 20
}
},
"analyzer": {
"autocomplete": {
"type": "custom",
"tokenizer": "keyword",
"filter": ["lowercase", "autocomplete_filter"]
}
}
}
},
"mappings": {
"properties": {
"doc_number": {
"type": "text",
"analyzer": "autocomplete",
"search_analyzer": "standard",
"fields": {
"keyword": { "type": "keyword" }
}
}
}
}
} Стратегия В: Мульти-поля (Best of both worlds)
Часто хранят номер и как keyword (для точного совпадения/агрегаций), и как wildcard или text с n-gram (для поиска).
"doc_number": {
"type": "keyword",
"fields": {
"wildcard": { "type": "wildcard" },
"ngram": {
"type": "text",
"analyzer": "autocomplete",
"search_analyzer": "standard"
} }
} Синтаксис query_string для частичного поиска
Запрос query_string использует синтаксис Lucene. Основные операторы для частичного совпадения:
*— заменяет ноль или более символов.?— заменяет ровно один символ."...", точная фраза (экранирует спецсимволы внутри).
Пример 1: Поиск по префиксу (начинается на…)
Ищем документы, номер которых начинается на INV-2023.
GET /documents/_search
{
"query": {
"query_string": {
"query": "INV-2023*",
"fields": ["doc_number.wildcard"],
"default_operator": "AND"
}
}
} Пример 2: Поиск по суффиксу (заканчивается на…)
Ищем документы, заканчивающиеся на 001.
Важно: Поиск с ведущей звездочкой (*001) очень ресурсоемок для обычных инвертированных индексов. Тип wildcard справляется с этим лучше, но все равно нагружает CPU. Для частых суффиксных поисков лучше хранить реверсную копию номера.
GET /documents/_search
{
"query": {
"query_string": {
"query": "*001",
"fields": ["doc_number.wildcard"],
"allow_leading_wildcard": true
}
}
} Пример 3: Поиск подстроки в середине (содержит…)
Ищем 2023 в любом месте.
GET /documents/_search
{
"query": {
"query_string": {
"query": "2023",
"fields": ["doc_number.wildcard"],
"allow_leading_wildcard": true
}
}
} Пример 4: Маска с неизвестными символами (например, формат INV-YYYY-NNN)
Ищем документы 2023 года с любым номером: INV-2023-??? или INV-2023-*.
GET /documents/_search
{
"query": {
"query_string": {
"query": "INV-2023-???",
"fields": ["doc_number.wildcard"]
}
}} Критически важные параметры query_string
| Параметр | Описание | Рекомендация |
|---|---|---|
fields | Массив полей для поиска. Поддерживает бусты (^) | Всегда указывайте явно: ["doc_number.wildcard^2", "doc_number.ngram"] |
allow_leading_wildcard | Разрешает * или ? в начале строки | По умолчанию true. Для wildcard поля работает быстро. Для text/keyword без n-gram — крайне медленно. Ставьте false, если пользователь не должен искать по суффиксу. |
analyze_wildcard | Пытается проанализировать wildcard-термы через поисковый анализатор | Обычно false. Для wildcard типа поля не требуется. |
default_operator | OR или AND между термами | Для номеров документов лучше AND (если пользователь ввел «INV 2023», ищем оба токена). |
lenient | Игнорирует ошибки формата (например, поиск текста в numeric поле) | true, если схема данных нестабильна. |
quote_field_suffix | Суффикс поля для точного поиска в кавычках | Удобно с мульти-полями: ".keyword". Тогда "INV-2023" пойдет в doc_number.keyword, а INV*, в doc_number.wildcard. |
Экранирование спецсимволов (Security & Correctness)
Синтаксис query_string полон зарезервированных символов: + ⎯ = && || > < ! ( ) { } [ ] ^ " ~ * ? : /.
Если номер документа содержит их (например, INV/2023/001 или DOC#123), запрос сломается или вернет неверный результат.
Правила экранирования:
- Используйте обратный слеш перед спецсимволом:
INV/2023/001*. - Или оберните весь терм в кавычки для точного совпадения:
"INV/2023/001"(но тогда wildcard внутри кавычек не сработает). - Лучшая практика: Экранируйте пользовательский ввод на стороне приложения (бэкенда) перед подстановкой в JSON.
// Пример на Python (elasticsearch-dsl / low level client)
from elasticsearch.helpers import escape_query_string
user_input = "INV/2023/001" # Пользователь ввел слеш
safe_query = escape_query_string(user_input) + ""
Внимание: Если вы используете quote_field_suffix, то запрос в кавычках "INV/2023" автоматически перенаправится на поле .keyword, где спецсимволы не имеют силы, и экранирование не требуется для точного совпадения.
Альтернативы query_string (Когда не использовать)
Несмотря на гибкость, query_string имеет недостатки: сложный парсинг, риск ошибок синтаксиса (400 Bad Request), сложность тюнинга релевантности.
Альтернатива 1: wildcard query (Структурированный, безопаснее)
Не парсит синтаксис Lucene, просто ищет по маске. Идеален для поля типа wildcard.
GET /documents/_search
{
"query": {
"wildcard": {
"doc_number.wildcard": {
"value": "INV-2023-*",
"case_insensitive": true
}
}
}} Альтернатива 2: match_phrase_prefix (Для n-gram полей)
Если вы используете стратегию с edge_ngram, этот запрос работает очень быстро и предсказуемо.
GET /documents/_search
{
"query": {
"match_phrase_prefix": {
"doc_number.ngram": "INV-2023"
}
}
} Альтернатива 3: regexp query (Для сложных масок)
Если нужна сложная валидация формата (например, только цифры в конце), но он медленнее wildcard.
GET /documents/_search
{
"query": {
"regexp": {
"doc_number.keyword": {
"value": "INV-2023-\d{3}",
"flags": "ALL"
}
}
}
} Производительность и Best Practices
- Избегайте ведущих wildcard (
*term) на обычныхkeyword/textполях. Это заставляет Elasticsearch сканировать весь индекс (Full Index Scan). Используйте типwildcardили реверсные токены (reverse token filter). - Ограничивайте длину минимального префикса. Не позволяйте пользователю искать
*или??. Валидируйте ввод: минимум 3- перед первой звездочкой. - Используйте
query_stringтолько для «умного» поискового бара. Если у вас отдельные поля «Префикс», «Год», «Номер» — собирайтеboolзапрос изterm/wildcardпрограммно. Это быстрее и безопаснее. - Кешируйте частые запросы. Wildcard-запросы не кешируются стандартным query cache. Рассмотрите использование
index.prefixesна keyword поле (для префиксов до определенной длины) вместо n-gram/wildcard типа, если память позволяет. - Мониторьте
search.slowlog. Запросы сleading_wildcardчасто попадают в медленные.
- [ ] Маппинг: Поле
doc_numberимеет подполеwildcard(типwildcard) илиngram(типtextс edge_ngram). - [ ] Валидация ввода: На бэкенде проверяем длину префикса (min ), экранируем спецсимволы (
+ ⎯ = && || > < ! ( ) { } [ ] ^ " ~ * ? : /). - [ ] Формирование запроса:
- Если нужен префикс:
query_string: { query: "PREFIX*", fields: ["doc_number.wildcard"], allow_leading_wildcard: false }. - Если нужна подстрока/суффикс:
allow_leading_wildcard: true(только на полеwildcard!). - Лучше: используйте
wildcardquery вместоquery_stringдля программных запросов.
- Если нужен префикс:
- [ ] Тестирование: Проверьте на датасете 1М+ доков. Запрос должен выполняться < 50-100мс (p99).
- [ ] Фоллбэк: Если пользователь ввел «мусор» (только спецсимволы), возвращайте понятную ошибку, а не 400 от ES.
Правильная архитектура — это заранее подготовленный маппинг под тип wildcard и использование структурированного wildcard query вместо парсинга строки query_string везде, где это возможно. Оставьте query_string только для единой строки глобального поиска, где пользователь ожидает синтаксис Google.