Кто не знает, что такое полнотекстовый поиск, как он организован в postgresql, рекомендую прочитать тут. В этой статье рассмотрены ключевые моменты настройки полнотектового поиска.
Наиболее важными элементами настройки полнотектового поиска (fts) - являются словари и конфигурации. Но начшем мы с другого элемента настройки fts.
Парсер (анализатор)
Задача парсера - разбить фрагмент текста на слова для последующего анализа каждого слова словарями, включенными в конфигурацию полнотектового поиска.
Рассмотрение парсера fts выходит за замки данной статьи, и вот почему.
В postgresql из коробки реализован единственный парсер default, который естественным (для большинства задач) способом разделяет фрагмент текста на слова - каждое слово отделяется от остальных пробелами, знаками препинания, переносом строки и пр. Кроме того, парсер default каждое слово присваивает к одной из 23 предопределенных групп. Это позволяет в конфигурации fts настраивать словари для анализа не всех, а только необходимых групп слов. Посмотреть, какие группы слов различает парсер default можно так
select * from ts_token_type('default')
А посмотреть пример как парсер default разделяет фрагмент текста на слова и как классифицируюет слова по группам можно так
select * from ts_parse('default','email:osp2003@inbox.ru;пользователь User;http://goszakupki.tk;rating:123.12') as bar
INNER JOIN (select * from ts_token_type('default')) as foo ON bar.tokid=foo.tokid;
Парсера defaul достаточно для решения многих прикладных задач, поэтому при настройке полнотектового поиска вопрос выбора парсера не актуален. Но кому интересно - может написать собственный парсер fts и интегрировать его в postgresql. Парсеры пишутся на C. Пример написания собственного парсера можно найти в этой статье
Словари
Словарь - это программа, которая выполняет две задачи 1) нормализацию слов, 2) исключение стоп-слов. Стоп-слова - это такие слова, которые не должны учитываться в результатах полнотектового поиска. Потому что не несут никакой информационной нагрузки. К стоп-словам прежде всего относятся такие части речи, как предлоги, союзы и пр. Но в итоге администратор базы данных сам определяет список стоп слов для конкретного проекта.
Все словари (программы) на выходе работают по одинаковому алгоритму - если слово является стопом - возвращается пустой массив, если нет - возвращается преобразованное значение слова (преобразование выполняется каждым словарем по разному, в зависимости от его назначения)
"Из коробки" в postgresql включает следующие типы (шаблоны) словарей:
- simple - возвращает слово, приведенное к нижнему регистру. Дополнителный параметр Accept может переопределить работу алгоритма, вовращая NULL
- словарь синонимов - Возвращает синоним найденного слова. Не поддерживает словосочетания
- тезаурус - расширение словаря синонимов (поддерживает словосочетания).
- словарь ispell - выполняет лингвистическую нормализацию слов. обращается к файлам словарям, составленным на различных языках. скачать словари можно по этому адресу
- словарь Snowball - выполняет обрезание слов оставляя его значимую часть.
Еще "из коробки" в postgresql есть словари стоп-слов, том числе и для русского языка - russian.stop. Словарь небольшой, содержит предлоги, союзы и пр.
Все словари хранятся в
/usr/share/postgresql/9.6/tsearch_data/
Созданием пользовательского словаря в postgresql управляет команда
CREATE TEXT SEARCH DICTIONARY dict_name (
TEMPLATE = ispell,
...другие параметры...
);
т.е., обязательный параметр - указание шаблона для создаваемого словаря. Изначально в postgresql встроено несколько типов шаблонов, если их недостаточно можно написать собственный. Шаблоны пишутся на языке C, рассмотрение этого процесса выходит за рамки статьи.
В качестве необязательного параметра при создании пользовательского словаря можно указать список стоп-слов. Здесь нужно учитывать один момент.
В каждом наборе текстов определенной тематике существуют частоупотребимые слова, включенные почти в каждый текст. Очевидно, поиск по таким словам бесполезен - в результате запроса мы получим выборку из всех текстов, где встречаются эти слова. Поэтому, частоупотребимые слова логично отнести к списку стоп-слов. Как найти такие слова?
Выполним запрос
SELECT * FROM ts_stat(
'select to_tsvector(''{{fts_configuration}}'', {{_field_name_}}) as word from {{_schema_name_}}.{{_table_name_}}'
)
ORDER BY nentry DESC, ndoc DESC, word
LIMIT 100;
Функция to_tsvector преобразует исходный текст в формат tsvector. Первый параметр (не обязательный) - это имя конфигурации, которая будет проводить преобразование. Если параметр не указывается, используется конфигурация по умолчанию. конфигурация 'russian' как правило доступна "из коробки".
Посмотреть, какие словари доступны можно так
select d_t.dictname, d_t.dictinitoption, d_t.tmplname, n.nspname, n.nspacl from
(select d.dictname, d.dictinitoption, d.dictnamespace, t.tmplname from pg_ts_dict as d join pg_ts_template as t on (d.dicttemplate=t.oid)) as d_t join
pg_namespace as n on (d_t.dictnamespace=n.oid)
К словарям, созданным на основе шаблонов simple, ispell, snowball можно подключить собственный список стоп-слов. Это важно потому, что шаблоны по разному работают с этим списком.
- ispell - сначала нормализует слова, а затем просматривает список стоп-слов
- snowball - сначала просматривает список, а затем работает
- simple - ...!!!
Это важно учитывать когда мы будем создавать конфигурацию fts. Проверить, как словарь, созданный на основе конкретного шаблона обработает слово можно так
select ts_lexize('russian_stem', 'Новостной');
Словари и конфигурации создаются для каждой схемы базы данных. Кроме того, можно использовать встроенные словари и конфигурации, если алгоритм их работы подходит для конкретной практической задачи.
Конфигурация
Конфигурация - это способ соединения созданных словарей в цепочку. Еще конфигурация определяет используемый тип парсера, но в рамках данной статьи (и многих приктических проектов) используется парсер по умолчанию, поэтому не будем акцентировать на нем внимание.
Создание конфигурации
Создать собственную конфигурацию можно двумя способами
- Скопировав в нее настройки одной из встроенных конфигураций
CREATE TEXT SEARCH CONFIGURATION {{confname}} (
COPY = {{existconf}}
)
В этом случае созданная конфигурация наследует от встроенной все настройки (которые можно потом менять)
- парсер, разбивающий текст на слова и классифицирующий их по типам
- набор словарей, каждый из которых обрабатывает один или несколько типов слов
- порядок, в котором словари будут обрабатывать слова
- Создать "чистую" конфигурацию
CREATE TEXT SEARCH CONFIGURATION {{confname}} (
PARSER = {{parsername}}
)
В этом случае мы только задаем парсер, разбивающий текст на слова. Настройка цепочко словарей для каждого типа слов, распознанных парсером, выполняется самостоятельно.
Кстати, за конфигурацию fts по умолчанию отвечает параметр postgresql default_text_search_config.
show default_text_search_config;
set default_text_search_config=simple;
Просмотр конфигураций
Первый вариант создания конфигурации более простой и быстрый - основные настройки будут скопированы из встроенной конфигурации и нужно лишь добавить собственные (иначе, зачем создавать новую конфигурацию когда можно воспользоваться встроенной?)
Для того, чтобы посмотреть информацию о встроенных конфигурациях можно выполнить запрос.
select res.cfgname, tt.alias, res.mapseqno, res.dictname, tt.description from
(
select cm_d.mapseqno, c.cfgname, cm_d.maptokentype, cm_d.dictname from pg_ts_config as c join
(select * from pg_ts_config_map as cm join pg_ts_dict as d on (cm.mapdict=d.oid)) as cm_d
on (c.oid= cm_d.mapcfg)
) as res join (select * from ts_token_type('default')) as tt on (res.maptokentype=tt.tokid)
order by res.cfgname, res.maptokentype, res.mapseqno
В результате получим
- cfgname - имя конфигурации
- alias - типы слов для которых в конфигурации задана обработка
- dictname - словари, обрабатывающие слова данного типа
- mapseqno - порядок обработки словарями данного типа слов
- description - описание типа слов
Если создать конфигурацию на основе встроенной, то выполнив запрос выше увидим, что встроенная и пользовательская конфигурции одинаковы и можно вносить в пользовательскую необходимые изменения
Если же мы создаем "чистую" конфигурацию (вариант 2), в результатах запроса выше ее не будет потому что мы пока еще не назначили словари для обработки типов слов
Настройка конфигурации
Для настройки созданной конфигурации используется команда
ALTER TEXT SEARCH CONFIGURATION
Она имеет несколько контекстов вызовов, прочитать прокоторые можно в документации
Пример создания конфигурации
Чтобы лучше понимать, как работает конфигурация полнотекстового поиска, будем создавать "чистую" конфигурацию.
Допустим, в нашей базе уже имеются два пользовательских рускоязычных словаря - словарь существительных noun и словарь глаголов infn, созданных на базе собственных словарей ispell. Как генерировать собственные словари ispell можно прочитать в этой публикации.
Создаем конфигурацию
CREATE TEXT SEARCH CONFIGURATION conf_test (
PARSER = default
)
Планируем работу конфигурации
В исходном тексте конфигурация должна различать только русскоязычные слова, являющиеся глаголом или существительным. Все остальные слова должны игнорироваться. Приоритета распознавания между глаголом и существительным нет.
Определяемся с нужными для конфигурации типами слов (на примере произвольной тестовой строки).
select * from ts_parse('default','email:osp2003@inbox.ru;пользователь User;http://goszakupki.tk;rating:123.12') as bar
INNER JOIN (select * from ts_token_type('default')) as foo ON bar.tokid=foo.tokid;
По результатам видим, что русскоязычные слова относятся к типу word.
Создаем конфигурацию
ALTER TEXT SEARCH CONFIGURATION corpora.conf_test
ADD MAPPING FOR word WITH noun, infn
Тестируем
select * from to_tsvector('conf_test', 'Встретились с ПУТИНЫМ вчера ранним вечером в 19')
Что-то не так - конфигурация распознала лексему 'сереть' которой быть не должно. Причина в побочном эффекте - союз 'с' не вколючен в список стоп-слов словарей noun и infn. Поэтому было применено одно из заданных в ispell правил - "если из слова исключить -ереть и ничего не добавлять - получится сереть. Поэтому союз c и был преобразован в слово сереть.Выход - пересоздать словари существительных и глаголов - добавив в них список стоп-слов
Релевантность поиска напрямую зависит от созданной цепочки словарей. Поэтому важно понимать как цепочка работает. Каждый словарь в цепочке работает по единому принципу
- возвращает массив лексем, если входное слово известно словарю (заметьте, один фрагмент может породить несколько лексем)
- возвращает одну лексему с установленным флагом TSL_FILTER для замены исходного фрагмента новым, чтобы следующие словари работали с новым вариантом (словарь, который делает это, называется фильтрующим словарём)
- возвращает пустой массив, если словарь воспринимает этот фрагмент, но считает его стоп-словом NULL, если словарь не воспринимает полученный фрагмент
Релевантность поиска
Использование индексов
Практика создания полнотекстового поиска разобрана на примере в следуюшей статье.