Расширяемость
Обзор полнотекстового поиска
12
Авторские права
© Postgres Professional, 2020 год.
Авторы: Егор Рогов, Павел Лузанов
Использование материалов курса
Некоммерческое использование материалов курса (презентации,
демонстрации) разрешается без ограничений. Коммерческое
использование возможно только с письменного разрешения компании
Postgres Professional. Запрещается внесение изменений в материалы
курса.
Обратная связь
Отзывы, замечания и предложения направляйте по адресу:
Отказ от ответственности
Компания Postgres Professional не несет никакой ответственности за
любые повреждения и убытки, включая потерю дохода, нанесенные
прямым или непрямым, специальным или случайным использованием
материалов курса. Компания Postgres Professional не предоставляет
каких-либо гарантий на материалы курса. Материалы курса
предоставляются на основе принципа «как есть» и компания Postgres
Professional не обязана предоставлять сопровождение, поддержку,
обновления, расширения и изменения.
2
Темы
Зачем нужен полнотекстовый поиск?
Документы и запросы
Анализаторы
Словари и шаблоны
Конфигурации
Индексная поддержка
3
Зачем текстовый поиск?
Средства SQLLIKE, регулярные выражения
нет морфологического поиска
нет возможности ранжирования результатов
нет индексной поддержки
Внешние поисковые системы
сложно синхронизировать с базой данных
отсутствие транзакционности
нет доступа к метаданным
сложности с разграничением доступа
В инструментарии SQL уже есть средства поиска по тексту. Это и
совсем простые команды LIKE и ILIKE, и упрощенные регулярные
выражения SIMILAR TO, и полноценные регулярные выражения
(оператор ~). Зачем нужен отдельный механизм?
Имеющиеся средства не позволяют искать с учетом разных словоформ,
не позволяют отранжировать результаты по релевантности, имеют
ограниченную индексную поддержку (для простого случая префиксного
поиска, либо с помощью триграмм, как было показано в практике темы
«Классы операторов»).
Для поиска можно использовать внешние поисковые системы. Но и они
имеют ряд ограничений, которые невозможно преодолеть из-за того,
что такие системы оторваны от базы данных. Их сложно
синхронизировать с актуальным содержимым БД; они не
транзакционны; они видят только те документы, которые им
показывают, и не видят остальной информации в базе; сложно
реализовать разграничение прав доступа к информации.
Полнотекстовый поиск позволяет преодолеть ограничения обычных
средств SQL и сохранить все их преимущества.
4
Документы и запросы
Документ
произвольный текст, который можно разобрать на слова (лексемы)
docx, pdf и т. п. надо предварительно преобразовать в текст
внутреннее компактное представление tsvector
Запрос
одна или несколько лексем, соединенных логическими связками:
& и
| или
! не
<-> предшествует (фразовый поиск)
( ) для изменения приоритета
внутреннее представление tsquery
Документ, по которому нужен поиск, должен быть предварительно
переведен в специальное представление — тип данных tsvector.
Исходный документ может быть произвольным текстом, который можно
разобрать на отдельные слова (точнее, лексемы — разницу подробнее
рассмотрим дальше). По двоичным документам тоже можно искать,
если предварительно перевести их в текстовый вид (с помощью
сторонних библиотек).
Поисковый запрос также должен быть представлен значением
специального типа — tsquery. Запрос может состоять либо из одной
лексемы, либо из нескольких лексем, связанных логическими
операторами «и», «или», «не». Начиная с версии 9.6 также
поддерживается оператор предшествования, который обеспечивает
фразовый поиск: можно найти документ, содержащий заданные слова
не в любом месте, а стоящие рядом (или на определенном расстоянии
друг от друга).
5
Соответствие
Соответствие документа запросу
оператор @@
Релевантность
веса лексем (A, B, C, D в порядке убывания важности)
ts_rank — по частоте найденных лексем
ts_rank_cd — также учитывает близость лексем
оператор расстояния <=> (расширение rum)
Чтобы проверить, соответствует ли документ (точнее, его
представление в виде tsvector) запросу (точнее, его представлению
tsquery), надо использовать оператор @@.
Результаты запроса можно ранжировать по «релевантности», чтобы
понять, какие из найденных документов больше соответствуют запросу,
а какие — меньше.
В PostgreSQL есть две встроенные функции: ts_rank (учитывает,
насколько часто лексемы из запроса встречаются в документе) и
ts_rank_cd (дополнительно учитывает близость найденных лексем).
Обе функции позволяют отмасштабировать свой результат с учетом
размера документа.
Расширение rum (https://github.com/postgrespro/rum) вводит еще одну
возможность: оператор расстояния <=>, представляющий собой
комбинацию функций ts_rank и ts_rank_cd.
Заметим, что в PostgreSQL не реализована возможность поиска
документов, «похожих» на другой документ (основанная, например, на
методе TF-IDF).
7
Анализатор
документ → анализатор → фрагмент+тип, фрагмент+тип, …
Выделяет в тексте документа фрагменты
и назначает фрагментам типы
Штатный анализатор default
23 типа фрагментов (слова, числа, адреса, теги XML и т. п.)
Другие анализаторы
расширения
набор функций на языке Си
Рассмотрим, как происходит превращение исходного документа
в tsvector (и запроса в tsquery).
Вначале документ пропускается через анализатор (parser), который
выделяет в нем фрагменты (tokens). Более того, каждому фрагменту
анализатор сопоставляет тип, который используется для дальнейшей
обработки. Например, стандартный анализатор (единственный
встроенный в PostgreSQL) выделяет 23 типа: слова, числа, адреса url
и email, XML-теги и другие. За счет этого он довольно универсален.
Чтобы использовать другой анализатор, его нужно написать на языке
Си, скомпилировать и подключить. Существуют расширения, которые
устанавливают уже готовые анализаторы. Например,
https://github.com/postgrespro/pg_tsparser — анализатор, построенный
на базе стандартного, но он не считает подчеркивание и минус
символами-разделителями слов (что полезно для индексации
программного кода).
9
Словари
фрагмент → словарь → лексема, лексема, …
Словарь превращает фрагмент в лексему
убрать стоп-слова
привести буквы к одному регистру
привести словоформы к общему виду
привести синонимы к одному варианту
и т. п.
Штатные словари
simple — нижний регистр и стоп-слова
стемминг для 21 языка
Полученный от анализатора фрагмент пропускается через словарь и
превращается в лексему (lexeme) или в несколько лексем.
Смысл такого преобразования в том, чтобы «нормализовать» слова,
привести их к такому виду, чтобы их было легко найти. Например:
- из текста можно убрать стоп-слова (которые встречаются почти
в каждом документе, так что искать их бессмысленно);
- можно привести слова к одному регистру.
- можно отрезать от слов изменяющиеся части (окончания), чтобы
находить любые словоформы (стемминг).
- Можно свести все синонимы к одному.
В PostgreSQL 12 изначально установлен словарь simple, а также
словари стемминга для 21 языка, которые отрезают от слов
изменяемые части. Такие словари работают быстро, но не всегда
точно, потому что опираются не на словарь, а на алгоритмы.
10
Шаблоны словарей
Словарь — параметризованный шаблон
набор функций на языке Си
Штатные шаблоны
стеммер snowball
словарь ispell
синонимы: приведение синонимов к одному виду
тезаурус: приведение фраз к одному виду
Чтобы добавить новый словарь, нужно воспользоваться шаблоном
(template), параметризовав его. Шаблон — это, по сути, набор функций
на Си, реализующих словарь.
В PostgreSQL входят несколько шаблонов:
- Стеммер snowball (параметризация позволяет задать свой список
стоп-слов).
- Словарь ispell (поддерживает в том числе словари hunspell,
используемые, например, в OpenOffice).
- Словарь синонимов, позволяющий определить свои синонимы.
- «Тезаурус», позволяющий заменять фразы на синонимы.
12
Конфигурация
фрагмент+тип → словари → лексемы
документ → анализатор → фрагмент+тип → словари → лексемы
фрагмент+тип → словари → лексемы
Своя цепочка словарей для каждого типа фрагмента
первый словарь, распознавший фрагмент, возвращает лексему
фильтрующий словарь передает лексему дальше по цепочке
конфигурация
Итак, документ пропускается через анализатор, который выделяет
в нем фрагменты. Какие именно словари будут использоваться для
фрагмента, определяется его типом. Таким образом можно по-разному
обрабатывать слова, числа и т. п.
Анализатор и цепочки словарей для каждого типа фрагмента
настраиваются и называются конфигурацией.
Каждый словарь в цепочке решает, что именно делать с фрагментом:
- Фрагмент можно отфильтровать как стоп-слово. На этом обработка
фрагмента, очевидно, прекращается.
- Фрагмент можно заменить на лексему и прекратить обработку.
Например, словарь simple переводит любое слово в нижний регистр
(и умеет фильтровать стоп-слова).
- Фрагмент можно заменить на лексему, но передать ее дальше, чтобы
другие словари также смогли обработать ее (такой словарь называется
фильтрующим).
- Наконец, словарь может не опознать фрагмент и передать его дальше
без изменений. Так, например, поступает словарь синонимов — если
синоним не найден, фрагмент передается дальше.
14
Индексная поддержка
GiST
неточное представление с обязательной перепроверкой по таблице
эффективность уменьшается при увеличении количества слов
быстрое обновление (зависит от числа документов)
GIN
точное и компактное представление
эффективность не теряется при увеличении количества слов
медленное обновление (зависит от числа слов в документах)
RUM (расширение на основе GIN)
дополнительная информация в индексе (позиции лексем и др.)
выдача результатов сразу в порядке релевантности
От полнотекстового поиска требуется не только функциональность,
но и скорость работы. Для ускорения поиска используются индексы
нескольких типов.
Индекс GiST хорош быстрым обновлением (что важно, если документы
активно изменяются), однако не обеспечивает высокой скорости поиска
и его эффективность понижается с ростом числа слов.
Индекс GIN — основной выбор для полнотекстового поиска. Он
обеспечивает быстрый поиск, не деградирует с увеличением числа
слов, хотя и медленно обновляется при изменении документов.
Проблемы этих двух традиционных индексов в том, что они учитывают
только лексемы, но отбрасывают их позиции в документе и другую
вспомогательную информацию. Поэтому они хороши для поиска, но
в принципе не могут ускорить ранжирование. По той же причине не
ускоряется фразовый поиск.
Индекс RUM (доступный как расширение:
https://github.com/postgrespro/rum) нацелен на устранение этого
недостатка: он позволяет выдавать результаты сразу в порядке
релевантности и ускоряет поиск фраз.
16
Итоги
Интегрированный полнотекстовый поиск обеспечивает
актуальность результатов
транзакционность
учет языковых особенностей
фразовый поиск
ранжирование результатов
индексную поддержку
Поиск охватывает любую информацию, приводимую
к текстовому виду
Возможна тонкая настройка под конкретные нужды
17
Практика
1. Сейчас поиск в книжном магазине работает только по
названиям книг.
Замените его на полнотекстовый поиск по названиям книг,
полным именам авторов и аннотациям.
Проверьте сделанные изменения в приложении. Убедитесь,
что поиск не зависит от формы слов в запросе.
1. Добавьте в таблицу books столбец типа tsvector, генерируемый
по необходимым значениям. Учтите, что значение tsvector должно
зависеть от данных из нескольких таблиц.
Посмотрите код функции public.get_catalog и подумайте, что нужно
сделать, чтобы заменить существующее условие поиска на
полнотекстовое.
Для проверки найдите книги по запросам «проблема» и «проблемы».