Поиск контактов по номеру телефона в Elasticsearch с использованием query_string и n-грамм

Забудьте о сложном DSL: query_string дает Lucene-синтаксис для мгновенного поиска номеров по префиксу, суффиксу или фразе в Elasticsearch.

В современных приложениях часто требуется быстро находить записи по частичному или полному совпадению телефонного номера. В случае с «магами» (контактами, хранящимися в Elasticsearch) удобно воспользоваться запросом query_string, который позволяет задавать сложные условия поиска в одной строке, включая префиксные, суффиксные и фразовые совпадения.

Почему именно query_string?

  • Поддерживает синтаксис, близкий к Lucene (например, *, ?, ~, операторы AND, OR, NOT).
  • Можно комбинировать поиск по нескольким полям (ФИО, номер, заметка) без необходимости писать несколько отдельных запросов.
  • Лёгко интегрируется в пользовательский интерфейс: пользователь вводит строку, а приложение передаёт её напрямую в query_string.

Подготовка данных: какие поля нужны

Для поиска магов по номерам телефонов обычно достаточно трёх полей:

  1. full_name – ФИО мага.
  2. phone – очищенный номер телефона (только цифры).
  3. note – произвольная заметка, где может храниться дополнительная информация.

Перед индексацией рекомендуется привести номер к единому формату: удалить все пробелы, дефисы, скобки и символ «+», оставить только цифры. Это упрощает дальнейшую токенизацию.

Настройка анализатора для поля phone

Ключевой момент – обеспечить возможность поиска:

  • по полному номеру;
  • по префиксу (например, 8999**);
  • по суффиксу (последние 4‑6 цифр);
  • по произвольной подстроке внутри номера.
  • Для этого удобно комбинировать keyword‑токенизатор (чтобы сохранять исходную строку) с фильтрами edge_ngram и reverse‑edge_ngram. Ниже пример настроек анализатора, которые можно разместить в разделе settings индекса:

    settings:
    {
      «analysis»: {
        «char_filter»: {
          «phone_clean»: {
            «type»: «pattern_replace»,
            «pattern»: «[^0-9]»,
            «replace»: «»
           }
        },
        «tokenizer»: {
          «phone_keyword»: {
            «type»: «keyword»
          }
        },
        «filter»: {
          «phone_edge_ngram»: {
            «type»: «edge_ngram»,
            «min_gram»: 2,
            «max_gram»: 20
          },
          «phone_reverse_edge»: {
            «type»: «edge_ngram»,
            «min_gram»: 2,
            «max_gram»: 20
          }
        },
        «analyzer»: {
          «phone_analyzer»: {
            «char_filter»: [«phone_clean»],
            «tokenizer»: «phone_keyword»,
            «filter»: [«phone_edge_ngram», «phone_reverse_edge»]
          }
        }
      }
    }

    Что делает эта конфигурация:

    • char_filter phone_clean удаляет всё, кроме цифр.
    • tokenizer phone_keyword оставляет строку как одно целое (не разбивает на токены по пробелам).
    • filter phone_edge_ngram генерирует все начальные подстроки (префиксы) длиной от 2 до .
    • filter phone_reverse_edge работает аналогично, но над перевёрнутой строкой, тем самым создавая суффиксы.
    • В результате поле phone содержит токены, покрывающие любые префиксы и суффиксы номера, что позволяет эффективно использовать query_string с wildcard‑поиском.

    Маппинг индекса

    Пример маппинга, где поле phone использует наш анализатор, а остальные поля – стандартные:

    mappings:
    {
      «properties»: {
        «full_name»: { «type»: «text», «analyzer»: «standard» },
        «phone»: {
          «type»: «text»,
          «analyzer»: «phone_analyzer»
        },
        «note»: { «type»: «text», «analyzer»: «standard» }
      }
    }

    Формирование запроса query_string

    Теперь, когда данные готовы, можно построить запрос. Пользователь может ввести:

    • полный номер: ###-###-####;
    • префикс: 8999;
    • суффикс: 5678;
    • любую часть: 456;
    • комбинацию с другими полями: Иванов AND phone:8912.
    • Пример JSON‑запроса к Elasticsearch:

      {  «query»: {
          «query_string»: {
            «query»: «(full_name:Иванов OR note:заговор) AND phone:8999»,
            «default_operator»: «AND»
          }
        }
      }

      Пояснения:

      • Часть query_string принимает строку, где можно использовать операторы AND, OR, скобки и wildcard‑символы (любое количество символов) и ? (один символ).
      • Поле phone уже содержит префикс‑ и суффикс‑ngram‑токены, поэтому запрос phone:8999 будет быстро находить все номера, начинающиеся с 8999.
      • Если нужно искать по суффиксу, достаточно указать phone:*5678 – обратный ngram‑фильтр обеспечит соответствие.
      • Параметр default_operator задаёт, как обрабатывать условия без явного оператора (по умолчанию OR, но мы ставим AND для более строгого совпадения);

      Советы по производительности

      • Ограничьте размер edge_ngram: слишком большие max_gram увеличивают объём индекса. Для российских номеров достаточно 10‑.
      • Используйте doc_values для поля phone, если планируете сортировать или агрегировать по номеру.
      • Регулярно проверяйте размер индекса через _cat/indices?v и при необходимости пересоздавайте его с более жёсткими лимитами.
      • Для очень больших наборов данных можно добавить отдельное подполе phone.prefix с анализатором только edge_ngram (префиксы) и phone.suffix с обратным ngram – тогда запрос будет обращаться к нужному полю напрямую, экономя ресурсы.

      Использование query_string в сочетании с правильно настроенным анализатором (char_filter + keyword + edge_ngram/reverse_edge_ngram) позволяет реализовать гибкий и быстрый поиск контактов магов по телефонным номерам: по полному номеру, по префиксу оператора, по последним цифрам или по любой подстроке. Такой подход легко масштабируется, поддерживает сложные комбинации с другими полями (ФИО, заметка) и остаёться понятен конечным пользователям благодаря привычному синтаксису wildcard‑поиска.

      Если вам понадобится дополнительно искать по фонетическим вариантам или обрабатывать опечатки в номерах, можно добавить фильтр phonetic или fuzzy в запрос, но для большинства задач, описанных выше, представленная конфигурация будет более чем достаточной.