В современных проектах, где хранится телефонный справочник (например, база контактов «гадалок», где каждый клиент может искать по части номера), обычный term-поиск оказывается недостаточным. Нужно уметь находить записи по:
- частичному номеру (например,
2354в+7 (495) -35-46-79); - номеру без префикса (без
+7или8); - номеру с небольшими опечатками (фаззи‑поиск);
- разным форматам записи (пробелы, скобки, дефисы).
В этой статье мы разберём, как правильно спроектировать индекс и какие возможности query_string использовать, чтобы обеспечить быстрый и точный поиск.
Планирование схемы индекса
Хранение телефона как строки
Телефонный номер – это не число, а набор символов, поэтому в маппинге его следует объявлять типом text (или keyword в сочетании с дополнительными анализаторами). Пример базового маппинга:
{
"mappings": {
"properties": {
"name": {
"type": "text"
},
"phone_raw": {
"type": "keyword"
},
"phone": {
"type": "text",
"analyzer": "phone_analyzer",
"search_analyzer": "phone_search_analyzer"
}
}
}
}
Поле phone_raw хранит оригинальное значение (для вывода), а phone – нормализованное и готовое к поиску.
Анализатор phone_analyzer
Самый надёжный способ – создать кастомный анализатор, который:
- Удаляет все нецифровые символы (пробелы, скобки, дефисы, «+», «8», «7»).
- Разбивает номер на n‑gram‑ы (обычно
edge_ngramот 3 до ) – это даёт возможность искать по началу и по любой части. - Опционально добавляет
reverseтокен, чтобы поддерживать поиск по окончанию;
Пример конфигурации:
{
"settings": {
"analysis": {
"filter": {
"phone_edge_ngram": {
"type": "edge_ngram",
"min_gram": 3,
"max_gram": 7
},
"phone_reverse": {
"type": "reverse"
} },
"char_filter": {
"phone_digits": {
"type": "pattern_replace",
"pattern": "[^0-9]",
"replacement": ""
} },
"analyzer": {
"phone_analyzer": {
"type": "custom",
"char_filter": ["phone_digits"],
"tokenizer": "standard",
"filter": ["lowercase", "phone_edge_ngram"]
}, "phone_search_analyzer": {
"type": "custom",
"char_filter": ["phone_digits"],
"tokenizer": "standard",
"filter": ["lowercase"]
}, "phone_reverse_analyzer": {
"type": "custom",
"char_filter": ["phone_digits"],
"tokenizer": "standard",
"filter": ["lowercase", "phone_reverse", "phone_edge_ngram"]
}
}
}
}
}
В результате в индексе будет два токена: обычный (для поиска с начала) и обратный (для поиска с конца).
Техники query_string для частичных номеров
Запрос query_string позволяет использовать гибкий синтаксис, включающий:
- wildcard‑символы
*и?; - фаззи‑поиск
~; - операторы
AND,OR,NOT; - поиск по нескольким полям через
fields; - префиксный поиск
field:prefix*;
Поиск по началу номера
Если пользователь вводит первые цифры, достаточно использовать префиксный запрос:
GET contacts/_search
{
"query": {
"query_string": {
"default_field": "phone",
"query": "2354*"
}
}
}
Благодаря edge_ngram такой запрос будет находить любые номера, где 2354 находится в любой позиции (не только в начале).
Поиск по любой части номера
Для «серединного» поиска удобно использовать wildcard‑запрос с обеих сторон:
GET contacts/_search
{
"query": {
"query_string": {
"default_field": "phone",
"query": "2354"
} }
}
Впрочем, такой запрос может быть тяжёлым, если в индексе миллионы записей. Чтобы ускорить его, рекомендуется добавить reverse‑анализатор и искать через phone_rev:
GET contacts/_search
{
"query": {
"bool": {
"should": [
{
"query_string": {
"default_field": "phone",
"query": "2354"
}
},
{
"query_string": {
"default_field": "phone_rev",
"query": "4352"
}
}
]
}
}}
Поиск без префикса (7/8/+7)
Поскольку в char_filter мы удаляем все нецифровые символы, номер +7 (495) -35‑46‑79 будет проиндексирован как 74952354679. Чтобы пользователь мог вводить 4952354 или просто 2354, достаточно использовать wildcard‑запрос без привязки к началу:
GET contacts/_search
{
"query": {
"query_string": {
"default_field": "phone",
"query": "4952354"
}
}}
Фаззи‑поиск (небольшие опечатки)
Если клиент иногда ошибается в наборе цифр, можно добавить ~ с указанием расстояния Левенштейна:
GET contacts/_search
{
"query": {
"query_string": {
"default_field": "phone",
"query": "2354~1"
}
}
}
Здесь ~1 означает «разрешить одну замену, вставку или удаление». Для телефонных номеров обычно достаточно ~1 или ~2.
Комбинация полей
В справочнике часто нужны одновременные запросы по имени и номеру. Для этого указываем список полей через параметр fields:
GET contacts/_search
{
"query": {
"query_string": {
"fields": ["name^2", "phone"],
"query": "Иван* 2354*"
}
}
}
Знак ^2 повышает вес совпадения по полю name в два раза.
Практические рекомендации по производительности
- Избегайте ведущих wildcard‑символов (
*123) без обратного индекса – они заставляют Elasticsearch сканировать все термины; - Для поиска «по любой части» используйте
edge_ngram+reverse‑анализатор, как описано выше; - Ограничьте
max_expansionsвwildcard‑запросах, если работаете с большими объёмами. - Если нужен поиск по международному формату, храните отдельное поле
phone_normalizedбез префикса7и создавайте alias‑поле. - Регулярные выражения (
regexp) работают медленно; используйте их только в редких случаях, когда нужен поиск по шаблону вроде^7[0-9]{9}$.
Полный пример создания индекса и запросов
Создание индекса
PUT /gadalka_contacts
{
"settings": {
"analysis": {
"char_filter": {
"digits_only": {
"type": "pattern_replace",
"pattern": "[^0-9]",
"replacement": ""
}
},
"filter": {
"phone_edge_ngram": {
"type": "edge_ngram",
"min_gram": 3,
"max_gram": 7
},
"reverse": {
"type": "reverse"
}
},
"analyzer": {
"phone_analyzer": {
"type": "custom",
"char_filter": ["digits_only"],
"tokenizer": "standard",
"filter": ["lowercase", "phone_edge_ngram"]
},
"phone_search_analyzer": {
"type": "custom",
"char_filter": ["digits_only"],
"tokenizer": "standard",
"filter": ["lowercase"]
},
"phone_rev_analyzer": {
"type": "custom",
"char_filter": ["digits_only"],
"tokenizer": "standard",
"filter": ["lowercase", "reverse", "phone_edge_ngram"]
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "russian"
},
"phone_raw": {
"type": "keyword"
},
"phone": {
"type": "text",
"analyzer": "phone_analyzer",
"search_analyzer": "phone_search_analyzer"
},
"phone_rev": {
"type": "text",
"analyzer": "phone_rev_analyzer",
"search_analyzer": "phone_search_analyzer"
}
}
}
}
Индексация примеров
POST /gadalka_contacts/_doc
{ "name": "Алексей Петров",
"phone_raw": "+7 (495) -35-46-79",
"phone": "+7 (495) -35-46-79",
"phone_rev": "+7 (495) -35-46-79"
}
POST /gadalka_contacts/_doc
{
"name": "Марина Сидорова",
"phone_raw": "###-###-####",
"phone": "###-###-####",
"phone_rev": "###-###-####"
} Примеры запросов
- Поиск по части «2354» (любой позиции):
GET /gadalka_contacts/_search { "query": { "bool": { "should": [ { "query_string": { "default_field": "phone", "query": "2354" } }, { "query_string": { "default_field": "phone_rev", "query": "4352" } } ] } } } - Поиск с ошибкой в одной цифре:
GET /gadalka_contacts/_search { "query": { "query_string": { "default_field": "phone", "query": "2354~1" } } } - Поиск по имени и номеру одновременно:
GET /gadalka_contacts/_search { "query": { "query_string": { "fields": ["name^3", "phone"], "query": "Марина 9215*" } } }
Ошибки, которые часто встречаются
- Не задали
search_analyzer– запросы проходят через стандартный анализатор и «теряют» цифры. - Слишком маленькие
min_gram(например, 1) приводят к огромному числу токенов и падению производительности. - Использование
regexpбез ограничения длины может привести кTooManyClausesException. - Запросы вида
*123без обратного индекса заставляют Elasticsearch сканировать весь инвертированный список терминов.
Для поиска по частичным номерам телефонов (в т.ч. в «справочнике гадалок») оптимальный набор техник выглядит так:
- Создать кастомный анализатор, который оставляет только цифры и генерирует
edge_ngramтокены. - Добавить
reverse‑анализатор, чтобы ускорить поиск по окончанию. - Использовать
query_stringс wildcard‑символами*и, при необходимости, фаззи‑поиском~. - Комбинировать поиск по имени и номеру через параметр
fields, задавая веса. - Следить за производительностью: ограничивать
max_gram, избегать ведущих*и использоватьbool/shouldвместо одного огромного wildcard‑запроса.
Следуя этим рекомендациям, вы получите быстрый, гибкий и «чувствительный» к ошибкам поиск по телефонным номерам, который будет удобен как для операторов колл‑центра, так и для клиентов, ищущих «гадалку» по части номера.
Статья написана в 2026 году, основываясь на актуальных практиках Elasticsearch 8.x и опыте решения задач поиска телефонных справочников.