Полнотекстовый поиск

PostgreSQL позволяет организовать полнотекстовый поиск, дающий возможность находить документы, соответствующие запросу, а также дополнительно сортировать их по релевантности.

Поиск по шаблону, который с самого начала являлся частью стандарта SQL, не отвечает современным требованиям информационных систем. В PostgreSQL он поддерживается операторами ~, ~*, LIKE и ILIKE. Поиск по шаблону не учитывает словоформы. Например, если вы ищете "соответствовать", слово "соответствуешь" не будет включено в результат. Этот вид поиска также не ранжирует результаты. Когда найдены тысячи документов, соответствующих критериям поиска, более релевантные документы могут оказаться в конце списка результатов. Операторы поиска по шаблону работают медленно, поскольку не поддерживают индексы и должны обрабатывать все документы для каждого поискового запроса.

Полнотекстовый поиск решает эти проблемы. В PostgreSQL есть функция to_tsvector для преобразования документа в тип данных tsvector. Значение tsvector представляет собой отсортированный список уникальных лексем, нормализованных для объединения различных форм одного и того же слова. PostgreSQL также включает to_tsquery и другие функции, которые анализируют пользовательский запрос и создают tsquery из текста запроса. Тип tsquery содержит нормализованные лексемы с логическими и фразовыми операторами поиска. PostgreSQL использует словари для преобразования текста в лексемы. Эти словари определяют стоп-слова, которые следует игнорировать и объединяют разные словоформы в одну лексему. Такой подход позволяет PostgreSQL преодолеть ограничения поиска по шаблону.

Форматы tsvector и tsquery

В этом разделе описываются внутренние форматы типов tsvector и tsquery.

tsvector

Выполним приведенный ниже запрос, чтобы увидеть, в каком формате PostgreSQL хранит данные tsvector. Будем использовать словари английского языка, чтобы объединить разные словоформы в одну лексему. Для этого укажем параметр конфигурации english в функции to_tsvector. Для дополнительной информации по настройке конфигурации обратитесь к статье Configuration Example.

SELECT to_tsvector('english', 'A row satisfies the condition if it returns true.');

Результат:

                  to_tsvector
--------------------------------------------------
'condit':5 'return':8 'row':2 'satisfi':3 'true':9

Функция to_tsvector вызывает парсер, который разбивает текст документа на токены и присваивает каждому токену тип. PostgreSQL использует список словарей для каждого токена. Этот список может варьироваться в зависимости от типа токена. Первый словарь, который распознает токен, выдает одну или несколько нормализованных лексем для представления токена. В нашем примере слова satisfies, condition и returns становятся лексемами satisfi, condit и return соответственно.

Результирующее значение tsvector не содержит слов a, the, if и it. Эти слова распознались как стоп-слова и исключаются из результатов, поскольку они встречаются слишком часто, чтобы их можно было использовать при поиске.

Числа в результате запроса определяют позицию слова в исходной строке (5 для 'condit', 8 для 'return', 2 для 'row' и т.д.).

Лексемы tsvector могут иметь метку веса, которая может быть A, B, C или D. D используется по умолчанию и не отображается в выводе.

Пример:

SELECT 'condit:5B return:8C row:2A satisfi:3C true:9D'::tsvector;

Результат:

                        tsvector
--------------------------------------------------------
 'condit':5B 'return':8C 'row':2A 'satisfi':3C 'true':9

Метки веса обычно используются для обозначения лексем из разных частей документа (заголовок, тело документа и т.д.).

PostgreSQL также содержит функцию setweight для установки веса лексем tsvector. В приведенном ниже примере используется setweight для маркировки лексем. После этого обработанные значения tsvector объединяются с помощью оператора конкатенации ||. Для получения дополнительной информации об этой операции обратитесь к статье Manipulating Documents.

UPDATE docs SET tsvector_document =
    setweight(to_tsvector(coalesce(title,'')), 'A')    ||
    setweight(to_tsvector(coalesce(keyword,'')), 'B')  ||
    setweight(to_tsvector(coalesce(summary,'')), 'C') ||
    setweight(to_tsvector(coalesce(body,'')), 'D');

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

tsquery

Тип данных tsquery содержит лексемы, разделенные оператором поиска фраз <-> (ПРЕДШЕСТВУЕТ) и логическими операторами & (И), | (ИЛИ), ! (НЕ). Выполним следующий запрос, чтобы увидеть значение tsquery:

SELECT to_tsquery('english', 'row & satisfy');

Результат:

   to_tsquery
-----------------
'row' & 'satisfi'

Результат содержит лексемы и логический оператор &.

Формат tsquery может также включать метки веса.

SELECT to_tsquery('english', 'row & satisfy:AB');

Результат:

      to_tsquery
----------------------
 'row' & 'satisfi':AB

Лексемы tsquery с метками веса соответствуют только лексемам tsvector с таким же весом.

Лексема tsquery может также содержать * для указания префикса соответствия:

SELECT to_tsquery('super:*');

Эта лексема соответствует любой лексеме в tsvector, которая начинается со строки super.

to_tsquery также может обрабатывать фразы в одинарных кавычках. Это полезно, если в конфигурацию включен словарь тезаурус, который содержит эти фразы.

В дополнение к to_tsquery, в PostgreSQL есть функции plainto_tsquery, phraseto_tsquery и websearch_to_tsquery для преобразования запросов в тип данных tsquery.

plainto_tsquery преобразует неформатированный текст в значение tsquery. Функция анализирует и нормализует текст и вставляет оператор & (И) между словами. plainto_tsquery не распознает операторы, метки весов или метки префикса соответствия во входных данных.

Пример:

SELECT plainto_tsquery('english', 'A joined table:B');

Результат:

    plainto_tsquery
-----------------------
 'join' & 'tabl' & 'b'

phraseto_tsquery анализирует и нормализует текст и вставляет оператор <->(ПРЕДШЕСТВУЕТ) между словами. Оператор ПРЕДШЕСТВУЕТ может быть представлен как <N>, где N — целочисленное значение, указывающее расстояние между двумя лексемами. <-> эквивалентно <1>. Если фраза содержит стоп-слова, они отбрасываются, но оператор <N> показывает расстояние, включающее их. Функция phraseto_tsquery полезна для поиска последовательностей лексем (фраз), так как операторы ПРЕДШЕСТВУЕТ проверяют порядок лексем. phraseto_tsquery не распознает операторы, метки весов или метки префикса соответствия во входных данных.

Пример:

SELECT phraseto_tsquery('english', 'A row satisfies the condition if it returns true.');

Результат:

                     phraseto_tsquery
----------------------------------------------------------
 'row' <-> 'satisfi' <2> 'condit' <3> 'return' <-> 'true'

websearch_to_tsquery использует альтернативный синтаксис для создания значения tsquery из текста запроса. Функция поддерживает следующий синтаксис:

  • Текст без кавычек преобразуется в лексемы, разделенные оператором &.

  • Текст в кавычках преобразуется в лексемы, разделенные оператором <N>.

  • OR преобразуется в оператор |.

  • - преобразуется в оператор !.

Пример:

SELECT websearch_to_tsquery('english', '"A row satisfies the condition" if it returns true OR "false" -"index".');

Результат:

                           websearch_to_tsquery
--------------------------------------------------------------------------
 'row' <-> 'satisfi' <2> 'condit' & 'return' & 'true' | 'fals' & !'index'

Функция websearch_to_tsquery не вызывает синтаксических ошибок, поэтому её можно использовать для необработанного пользовательского ввода. websearch_to_tsquery не распознает операторы, весовые метки или метки префикса соответствия во входных данных.

РЕКОМЕНДАЦИЯ
Вы можете настроить парсер, словари и типы токенов, которые должны быть проиндексированы для функций to_tsvector и to_tsquery. Обратитесь к статье Configuration Example за дополнительными сведениями.

Оператор соответствия @@

Полнотекстовый поиск в PostgreSQL реализован через оператор соответствия @@. Создадим таблицу, чтобы посмотреть, как он работает:

CREATE TABLE documents(
    document_id SERIAL,
    document_text TEXT
);

INSERT INTO documents (document_text) VALUES
('If the condition is not satisfied, rows are not returned.'),
('A joined table is a table derived from two other tables according to the rules of the particular join type.'),
('Indexes can be added to and removed from tables at any time.'),
('An index defined on a column that is part of a join condition can also significantly speed up queries with joins.'),
('A row satisfies the condition if it returns true.'),
('The type numeric can store numbers with a very large number of digits.'),
('It allows you to specify that the value in a certain column must satisfy a boolean expression.');

Оператор соответствия @@ возвращает true, если значение tsvector (документ) совпадает со значением tsquery (запрос). Чтобы использовать словари английского языка для нормализации лексем, укажите параметр конфигурации english в функциях to_tsvector и to_tsquery:

SELECT * FROM documents
    WHERE to_tsvector('english', document_text) @@ to_tsquery('english', 'satisfy');

Результат включает документы, содержащие satisfy и его формы, satisfies и satisfied:

 document_id |                 document_text
-------------+----------------------------------------------------
           5 | A row satisfies the condition if it returns true.
           7 | It allows you to specify that the value in a certain column
               must satisfy a boolean expression.
           1 | If the condition is not satisfied, rows are not returned.

Увеличение скорости поиска

Чтобы ускорить поиск, можно использовать индексы.

Создадим GIN-индекс по выражению, содержащему вызов функции to_tsvector:

CREATE INDEX idx_gin_document_text ON documents
    USING GIN (to_tsvector('english', document_text));

Поисковый запрос остается прежним:

SELECT * FROM documents
    WHERE to_tsvector('english', document_text) @@ to_tsquery('english', 'satisfy');

Также можно сохранить результаты функции to_tsvector в отдельный столбец типа tsvector и создать для него индексы:

ALTER TABLE documents
    ADD COLUMN tsv tsvector
        GENERATED ALWAYS AS (to_tsvector('english',document_text))
        STORED;

CREATE INDEX idx_gin_document_text
    ON documents USING gin (tsv);

После того как таблица обновлена, используйте поле tsv для поиска:

SELECT document_id, document_text FROM documents
    WHERE tsv @@ to_tsquery('english', 'satisfy');

Возвращаются те же строки:

 document_id |                 document_text
-------------+----------------------------------------------------
           7 | It allows you to specify that the value in a certain column
               must satisfy a boolean expression.
           1 | If the condition is not satisfied, rows are not returned.
           5 | A row satisfies the condition if it returns true.

В подходе с отдельным столбцом не нужно явно указывать конфигурацию словаря. Кроме того, поиск выполняется быстрее, поскольку PostgreSQL не повторяет вызовы to_tsvector при перепроверке значений, найденных при поиске по индексу.

Подход, основанный на создании индексов выражения, проще в настройке и требует меньше места на диске, поскольку значения tsvector не сохраняются явно.

Ранжирование результатов поиска

Когда поиск производится в базе данных с большим количеством строк и в результат включено много документов, более релевантные документы могут оказаться в конце списка. Чтобы избежать этой проблемы, PostgreSQL предоставляет две предопределенные функции для ранжирования результатов ts_rank и ts_rank_cd.

ts_rank ранжирует значения tsvector на основе частоты найденных лексем. ts_rank_cd вычисляет рейтинг плотности покрытия для данного документа и запроса. Для дополнительной информации обратитесь к статье Ranking Search Results.

Эти функции имеют похожие сигнатуры:

ts_rank([ <веса> float4[], ] <вектор> tsvector, <запрос> tsquery [, <нормализация> integer ]) returns float4

ts_rank_cd([ <веса> float4[], ] <вектор> tsvector, <запрос> tsquery [, <нормализация> integer ]) returns float4

Где:

  • вектор — значение документа tsvector.

  • запрос — значение запроса tsquery.

  • веса указывают значения весов для разных весовых меток (необязательный параметр).

  • нормализация указывает, как длина документа влияет на его ранг (необязательный параметр).

Выполните запрос, содержащий функцию ts_rank без необязательных параметров:

SELECT document_id, ts_rank(tsv, query) AS rank, document_text
FROM documents, to_tsquery('english', 'table') query
WHERE tsv @@ query
ORDER BY rank DESC;

Результат:

 document_id |    rank     |                 document_text
-------------+-------------+---------------------------------------------------------
           2 | 0.082745634 | A joined table is a table derived from two other tables
             |             | according to the rules of the particular join type.
           3 |  0.06079271 | Indexes can be added to and removed from tables at any time.

Документ, в котором table повторяется два раза, имеет более высокий ранг.

Необязательный параметр веса позволяет придать больший или меньший вес лексемам в зависимости от их меток. Метка веса является частью описанных выше форматов tsvector и tsquery. Массив весов задаёт значения весов для каждой категории лексем в следующем порядке: {D-вес, C-вес, B-вес, A-вес}. Если этот параметр не задан, функции ранжирования используют значение по умолчанию {0.1, 0.2, 0.4, 1.0}.

Обновим значение поля tsv для третьей строки, указав вес лексем:

UPDATE documents SET tsv = setweight(tsv,'A')
WHERE document_id = 3
RETURNING tsv;

Результат:

                        tsv
----------------------------------------------------
 'ad':4A 'index':1A 'remov':7A 'tabl':9A 'time':12A

Выполним запрос с указанным массивом весов:

SELECT document_id, ts_rank('{0.05, 0.2, 0.4, 1.0}', tsv, query) AS rank, document_text
FROM documents, to_tsquery('english', 'table') query
WHERE tsv @@ query
ORDER BY rank DESC;

Получен другой результат. Строка, в которой лексема tabl имеет метку веса A, получила более высокий ранг:

 document_id |    rank     |             document_text
-------------+-------------+--------------------------------------------------------------------
           3 |   0.6079271 | Indexes can be added to and removed from tables at any time.
           2 | 0.041372817 | A joined table is a table derived from two other
                            tables according to the rules of the particular join type.

Вероятность найти слова из поискового запроса увеличивается с размером документа. При ранжировании имеет смысл учитывать размер документа, чтобы, например, документ с сотней слов, содержащий пять вхождений искомых слов, считался более релевантным, чем документ с тысячей слов и теми же пятью вхождениями. Обе функции ранжирования принимают целочисленный параметр нормализация, определяющий, каким образом ранг будет зависеть от размера документа. В таблице ниже приведены значения параметра нормализация:

Значение Описание

0

Значение по умолчанию. Длина документа не учитывается

1

Ранг документа делится на 1 + логарифм длины документа

2

Ранг документа делится на его длину

4

Ранг документа делится на среднее гармоническое расстояние между блоками (только для ts_rank_cd)

8

Ранг документа делится на число уникальных слов в документе

16

Ранг документа делится на 1 + логарифм числа уникальных слов в документе

32

Ранг делится на своё значение + 1

Вы можете использовать | для указания нескольких значений, например 2|4. Преобразования применяются в указанном порядке.

Добавим значения параметра нормализация к запросу выше:

SELECT document_id, ts_rank('{0.05, 0.2, 0.4, 1.0}', tsv, query, 8) AS rank, document_text
    FROM documents, to_tsquery('english', 'table') query
    WHERE tsv @@ query
    ORDER BY rank DESC;

Результат:

 document_id |    rank     |             document_text
-------------+-------------+--------------------------------------------------------------------
           3 | 0.121585414 | Indexes can be added to and removed from tables at any time.
           2 | 0.005171602 | A joined table is a table derived from two other tables according
                                to the rules of the particular join type.
ПРИМЕЧАНИЕ
Ранжирование результатов поиска может быть дорогостоящей операцией, поскольку для этого требуется прочитать значение tsvector каждого документа, который соответствует поисковому запросу. Считывание значения tsvector с диска может занять значительное время. Этого практически невозможно избежать, поскольку поисковые запросы часто приводят к большому количеству совпадений.
Нашли ошибку? Выделите текст и нажмите Ctrl+Enter чтобы сообщить о ней