Расширяемость
Обзор полнотекстового поиска
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 и подумайте, что нужно
сделать, чтобы заменить существующее условие поиска на
полнотекстовое.
Для проверки найдите книги по запросам «проблема» и «проблемы».
18
Практика
1. Восстановите базу данных с архивом рассылок
из резервной копии.
Добавьте в таблицу mail_messages столбец search_vector
и заполните его так же, как в демонстрации.
2. Сколько документов содержат одновременно фразы
«vacuum full» и «index page»?
Сравните скорость поиска без индекса и с индексами
разных типов (GiST, GIN, RUM).
3. Сколько документов содержит слово «vacuuming»
именно в такой форме?
2. Создавайте индексы по одному и удаляйте их сразу после
эксперимента перед созданием следующего, чтобы в запросе
гарантированно использовался нужный индекс.
3. Проще всего ответить на этот вопрос с помощью регулярных
выражений. Но, поскольку тема посвящена полнотекстовому поиску,
измените конфигурацию поиска, установив для лексем типа «asciiword»
(к которым относятся слова из латинских букв) словарь «simple».
После этого необходимо пересоздать столбец search_vector, так как он
будет содержать значение, не соответствующие новой конфигурации.