Полнотекстовый поиск – один из самых востребованных механизмов в современных информационных системах

Устали от сломанных запросов? Разбираем, почему токенизация рвет фразы на куски, и показываем секреты, как заставить поиск понимать вас с полуслова.

Полнотекстовый поиск – один из самых востребованных механизмов в современных информационных системах. Он позволяет находить документы по смыслу, а не только по точному совпадению строк. Однако при работе с ним часто возникает проблема: разделение слов (токенизация) может «разорвать» целую фразу запроса, из‑за чего поиск становится менее точным. В этой статье мы подробно разберём, почему это происходит, какие есть способы сохранить фразы запросов «intact» (неразделёнными) и как правильно реализовать их в популярных системах поиска.


Что такое полнотекстовый поиск?

Полнотекстовый поиск (Full‑Text Search, FTS) – это набор алгоритмов и индексов, позволяющих искать документы по содержимому текста. Основные задачи FTS:

  1. Разбить (токенизировать) текст на отдельные термы (слова, корни, n‑граммы).
  2. Преобразовать термы (стемминг, лемматизацию, удаление стоп‑слов).
  3. Построить обратный индекс, связывающий каждый терм с документами, где он встречается.
  4. Обрабатывать запросы пользователя, учитывая операторы (AND, OR, NOT, кавычки, proximity и др.).

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

Почему фразы «разбиваются»?

Разделение слов происходит по нескольким причинам:

  • Стандартные анализаторы используют набор разделителей (пробел, пунктуация, цифры). Любой символ, не являющийся буквой, считается границей.
  • Стемминг и лемматизация приводят слово к базовой форме, иногда изменяя его структуру.
  • Удаление стоп‑слов (например, «и», «в», «на») может «разорвать» фразу, если стоп‑слово находится внутри.
  • Нормализация (приведение к нижнему регистру, удаление диакритики) иногда приводит к потере информации о оригинальном порядке слов.

Если система не учитывает контекст расположения слов, запрос «новый iPhone 13» может быть интерпретирован как поиск любого из трёх слов, а не как конкретная фраза.

Как сохранить фразы «intact»?

Существует несколько проверенных подходов, позволяющих обеспечить поиск точных фраз без их разбиения:

Кавычки в запросе

Самый простой способ – требовать от пользователя обрамлять фразу в двойные кавычки. Большинство поисковых движков (MySQL, PostgreSQL, Elasticsearch, Solr) интерпретируют такой запрос как phrase query и ищут термы в точном порядке.

SELECT * FROM articles
WHERE MATCH(content) AGAINST('"красный автомобиль"' IN BOOLEAN MODE);

Специальные анализаторы (phrase‑aware analyzers)

В Elasticsearch и Solr можно создать custom analyzer, который сохраняет оригинальную фразу в виде отдельного токена (shingle). Пример:

{
 "settings": {
 "analysis": {
 "filter": {
 "shingle_filter": {
 "type": "shingle",
 "min_shingle_size": 2,
 "max_shingle_size": 3,
 "output_unigrams": false
 }
 },
 "analyzer": {
 "phrase_analyzer": {
 "type": "custom",
 "tokenizer": "standard",
 "filter": ["lowercase", "shingle_filter"]
 }
 }
 }
 }
}

Такой анализатор создаёт токены‑шинглы («красный автомобиль», «красный автомобиль 2022»), позволяя искать фразы без потери точности.

Индексация n‑грамм

Для языков с агглютинативной морфологией (например, русский) часто используют edge‑ngrams или ngram‑фильтры. Они позволяют находить части фраз, но при этом необходимо хранить оригинальный порядок слов в отдельном поле.

Поле «raw» (неанализируемое)

Создайте в индексе дополнительное поле, в котором хранится оригинальная строка без токенизации. По этому полю можно выполнять точный поиск с оператором keyword (Elasticsearch) или LIKE (SQL).

CREATE TABLE articles (
 id INT PRIMARY KEY,
 content TEXT,
 content_raw TEXT GENERATED ALWAYS AS (content) STORED,
 FULLTEXT(content)
);

Запрос:

SELECT * FROM articles
WHERE content_raw LIKE '%красный автомобиль%';

Операторы proximity (близости)

Если требуется гибкость (слова могут быть разделены небольшим числом слов), используют оператор NEAR или ~ в Elasticsearch:

GET /articles/_search
{
 "query": {
 "match_phrase": {
 "content": {
 "query": "красный автомобиль",
 "slop": 2 // допускает до 2 промежуточных слов
 }
 }
 }
}

Практические примеры в популярных СУБД и поисковых движках

MySQL

  • Создайте FULLTEXT индекс.
  • Для точных фраз используйте IN BOOLEAN MODE с кавычками.
  • Если нужны шинглы, храните отдельное поле content_phrase и индексируйте его как VARCHAR.

PostgreSQL (pg_trgm, tsvector)

Комбинация tsvector и pg_trgm позволяет искать как по токенам, так и по подстрокам;

CREATE EXTENSION pg_trgm;
ALTER TABLE articles ADD COLUMN content_tsv tsvector;
UPDATE articles SET content_tsv = to_tsvector('russian', content);
-- Поиск точной фразы
SELECT * FROM articles
WHERE content ILIKE '%красный автомобиль%';

Elasticsearch

Пример маппинга с двумя полями: content (анализируемый) и content_raw (keyword).

{
 "mappings": {
 "properties": {
 "content": {
 "type": "text",
 "analyzer": "russian"
 },
 "content_raw": {
 "type": "keyword"
 }
 } }
}

Запросы:

  • Точная фраза: match_phrase.
  • Точное совпадение без анализа: term по content_raw.

Apache Solr

В Solr используется field type text_general для токенизации и string для «raw»‑полей.

<field name="content" type="text_ru" indexed="true" stored="true"/>
<field name="content_raw" type="string" indexed="true" stored="false"/>

Запросы:

q="content:"красный автомобиль""
q="content_raw:"красный автомобиль""

Sphinx

В Sphinx можно задать phrase boundaries с помощью phrase_boundary в конфигурации.

index articles
{
 source = src_articles
 path = /var/data/sphinx/articles
 morphology = stem_ru
 phrase_boundary = 1
}

Лучшие практики

  • Всегда храните оригинальный текст в отдельном неанализируемом поле – это спасёт, если понадобится точный поиск.
  • Для пользовательских запросов поощряйте использование кавычек (можно добавить UI‑подсказку).
  • Если требуется гибкость, используйте shingle‑анализаторы или proximity operators с небольшим slop.
  • Не удаляйте стоп‑слова в полях, где важен порядок слов (например, в названиях книг).
  • Тестируйте запросы на реальном наборе данных: сравните результаты match, match_phrase и LIKE.
  • Для мультиязычных проектов используйте отдельные анализаторы для каждого языка, учитывающие особенности морфологии.

Сохранение фраз запросов «intact» в полнотекстовом поиске – задача, решаемая на нескольких уровнях: от правильного построения запросов (кавычки, proximity) до настройки индекса (shingle‑анализаторы, raw‑поля). Выбор конкретного подхода зависит от:

  1. Объёма данных и частоты запросов.
  2. Требуемой точности vs. гибкости поиска.
  3. Технологического стека (MySQL, PostgreSQL, Elasticsearch, Solr, Sphinx и др.).

Применяя описанные в статье методы, вы сможете обеспечить пользователям поиск, который будет находить именно те документы, где фразы находятся в нужном порядке, не «разрывая» их на отдельные слова. Это повышает релевантность, улучшает пользовательский опыт и снижает количество нерелевантных результатов.


Автор: ваш виртуальный помощник.