Полнотекстовый поиск
Введение
Полнотекстовый поиск (или просто текстовый поиск) предоставляет возможность идентифицировать документы на естественном языке, удовлетворяющие запросу, и при необходимости сортировать их по релевантности для этого запроса. Наиболее распространенный тип поиска состоит в нахождении всех документов, содержащих заданные ключевые слова запроса и возвращении их ранжированными по степени соответствия запросу. Понятия запроса и соответствия крайне размыты и зависят от конкретного приложения. При самом простом поиске запрос рассматривается как набор слов, а соответствие — как частота встречаемости слов запроса в документе.
Операторы текстового поиска существуют в базах данных уже многие годы. В
QHB для текстовых типов данных имеются операторы ~,
~*
,
LIKE
и ILIKE
, но у них отсутствуют многие существенные свойства, требуемые для
современных информационных систем:
-
Отсутствует лингвистическая поддержка, даже для английского языка. Регулярных выражений недостаточно, поскольку они не могут с легкостью обрабатывать словоформы, например удовлетворяет и удовлетворять. Из-за этого вы можете пропустить документы, содержащие слово удовлетворяет, несмотря на то, что вам, вероятно, хотелось бы найти их при поиске по слову удовлетворять. Для поиска по нескольким словоформам можно использовать OR, но это трудоемко и ненадежно (некоторые слова могут иметь несколько тысяч производных).
-
Они не обеспечивают сортировку (ранжирование) результатов поиска, что делает их неэффективными при нахождении тысяч соответствующих документов.
-
Как правило, они выполняются медленно, поскольку не поддерживают индексы и при каждом поиске должны обрабатывать все документы.
Полнотекстовая индексация позволяет предварительно обработать документы и сохранить индексы для последующего быстрого поиска. Предварительная обработка включает в себя:
-
Разбор документов на синтаксические единицы. При этом полезно выделить различные классы синтаксических единиц, например числа, слова, словосочетания, адреса электронной почти, чтобы можно было обрабатывать их по-разному. В принципе, классы синтаксических единиц зависят от конкретного приложения, но для большинства целей достаточно использовать предопределенный набор классов. Для выполнения этого шага в QHB используется синтаксический анализатор. Предоставляется стандартный анализатор, но для особых нужд можно создать и специализированные.
-
Преобразование синтаксических единиц в лексемы. Лексема, как и синтаксическая единица, является строкой, но только нормализованная, чтобы разные формы одного слова стали одной. К примеру, нормализация почти всегда включает приведение букв верхнего регистра к нижнему и зачастую удаление суффиксов/окончаний (например, s или es в английском). Это позволяет находить при поиске различные формы одного слова без трудоемкого ввода всех возможных вариантов. Кроме того, на этом шаге обычно исключаются стоп-слова, то есть слова настолько распространенные, что искать их бесполезно. (Иначе говоря, синтаксические единицы являются исходными фрагментами текста документа, тогда как лексемы — это слова, считающиеся полезными для индексации и поиска.) Для выполнения этого шага в QHB используются словари. Предоставляются различные стандартные словари, но для особых нужд можно создать и специализированные.
-
Хранение предварительно обработанных документов, оптимизированных для поиска. Например, каждый документ может быть представлен в виде сортированного массива нормализованных лексем. Помимо лексем часто желательно хранить информацию об их местоположении для ранжирования по близости, чтобы документу, в котором слова запроса расположены «плотнее», присваивался более высокий ранг, чем документу с разбросанными словами.
Словари позволяют более тщательно управлять нормализацией синтаксических единиц. С подходящими словарями можно:
-
Определять стоп-слова, которые не должны индексироваться.
-
Сопоставлять синонимы с единственным словом, используя Ispell.
-
Сопоставлять фразы с единственным словом, используя тезаурус.
-
Сопоставлять различные вариации слова с канонической формой, используя словарь Ispell.
-
Сопоставлять различные вариации слова с канонической формой, используя правила парадигматического модуля Snowball.
Для хранения предварительно обработанных документов имеется тип данных tsvector,
а для представления обработанных запросов — тип tsquery (раздел
Типы для текстового поиска). Для работы с этими типами имеется множество функций
и операторов (раздел Функции и операторы текстового поиска), наиболее важный
из которых — оператор сопоставления @@
, который мы рассмотрим в подразделе
Простое сопоставление текста. Полнотекстовый поиск можно ускорить с помощью
индексов (раздел Предпочтительные типы индексов для текстового поиска).
Что такое документ?
Документ — это единица просмотра в системе полнотекстового поиска; например, журнальная статья или почтовое сообщение. Механизм текстового поиска должен уметь анализировать документы и сохранять связи лексем (ключевых слов) с их родительским документом. Впоследствии эти ассоциации используются для поиска документов, содержащих слова запроса.
Для поиска в QHB документ обычно представляет собой текстовое поле в строке таблицы базы данных или, возможно, сочетание (конкатенация) таких полей, предположительно хранящихся в нескольких таблицах или полученных динамически. Другими словами, для индексации документ может быть собран из нескольких частей и не храниться где-либо как единое целое. Например:
SELECT title || ' ' || author || ' ' || abstract || ' ' || body AS document
FROM messages
WHERE mid = 12;
SELECT m.title || ' ' || m.author || ' ' || m.abstract || ' ' || d.body AS document
FROM messages m, docs d
WHERE m.mid = d.did AND m.mid = 12;
Примечание
На самом деле в этих примерах запросов следует использовать функцию coalesce, чтобы единственный атрибут NULL не привел к результату NULL для целого документа.
Другой вариант — хранение документов в виде обычных текстовых файлов в файловой системе. В этом случае базу данных можно использовать для хранения полнотекстового индекса и выполнения поиска, извлекать документ из файловой системы можно с помощью уникального идентификатора. Однако для извлечения файлов вне базы данных требуются права суперпользователя или поддержка специальных функций, так что обычно это менее удобно, чем хранить все данные внутри QHB. Кроме того, если все хранится в базе данных, это упрощает доступ к метаданным документов, участвующим в индексации и отображении результатов.
Для реализации текстового поиска каждый документ должен быть сведен к предварительно обработанному формату tsvector. Поиск и ранжирование выполняются исключительно с этим представлением документа — оригинальный текст нужно извлекать, только когда документ был выбран для отображения перед пользователем. Поэтому мы часто говорим о tsvector как о документе, но, разумеется, это всего лишь компактное представление полного документа.
Простое сопоставление текста
В QHB полнотекстовый поиск работает на основе оператора
сопоставления @@
, который возвращает true, если tsvector (документ)
соответствует tsquery (запросу). Какой тип данных записан первым, значения не
имеет:
SELECT 'a fat cat sat on a mat and ate a fat rat'::tsvector @@ 'cat & rat'::tsquery;
?column?
----------
t
SELECT 'fat & cow'::tsquery @@ 'a fat cat sat on a mat and ate a fat rat'::tsvector;
?column?
----------
f
Как подсказывает пример выше, tsquery — это не просто необработанный текст, как и tsvector. Тип tsquery содержит ключевые слова, которые должны быть уже нормализованными лексемами и могут объединяться по несколько штук с помощью операторов И, ИЛИ, НЕ и ПРЕДШЕСТВУЕТ. (Подробно синтаксис описан в подразделе tsquery.) Также существуют функции to_tsquery, plainto_tsquery и phraseto_tsquery, помогающие преобразовать заданный пользователем текст в требуемое значение tsquery, в первую очередь нормализуя имеющиеся в этом тексте слова. Аналогичным образом функция to_tsvector применяется для анализа и нормализации строки документа. Поэтому на практике текстовый поиск с сопоставлением будет выглядеть скорее так:
SELECT to_tsvector('fat cats ate fat rats') @@ to_tsquery('fat & rat');
?column?
----------
t
Обратите внимание, что это сопоставление не сработает, если будет записано как
SELECT 'fat cats ate fat rats'::tsvector @@ to_tsquery('fat & rat');
?column?
----------
f
поскольку слово rats не будет нормализовано. Элементами tsvector являются лексемы, предположительно уже нормализованные, поэтому rats не соответствует rat.
Оператор @@
также поддерживает входные данные типа text, позволяя в простых
случаях пропускать явное преобразование текстовой строки в tsvector или tsquery.
Имеющиеся варианты этого оператора:
tsvector @@ tsquery
tsquery @@ tsvector
text @@ tsquery
text @@ text
Первые два из них мы уже видели. Форма text @@ tsquery
равнозначнаto_tsvector(x) @@ y
, а форма text @@ text
— to_tsvector(x) @@ plainto_tsquery(y)
.
В значении tsquery оператор &
(И) указывает, что для соответствия оба его
аргумента должны присутствовать в документе. Аналогичным образом оператор |
(ИЛИ) указывает, что должен присутствовать хотя бы один из его аргументов, тогда
как оператор !
(НЕ) указывает, что для соответствия ни один из его аргументов
в документе присутствовать не должен. Например, запрос fat & ! rat
соответствует документам, содержащим fat, но не содержащим rat.
Поиск по фразам возможен с помощью оператора <->
(ПРЕДШЕСТВУЕТ) типа tsquery,
который находит совпадение, только если в документе его аргументы находятся
рядом и в заданном порядке. Например:
SELECT to_tsvector('fatal error') @@ to_tsquery('fatal <-> error');
?column?
----------
t
SELECT to_tsvector('error is not fatal') @@ to_tsquery('fatal <-> error');
?column?
----------
f
Более общая версия оператора ПРЕДШЕСТВУЕТ имеет форму <N>
, где N — целое
число, означающее разность между позициями сопоставляемых лексем. Запись <1>
равнозначна <->
, тогда как <2>
допускает существование ровно одной отличной
лексемы между заданными, и т. д. Функция phraseto_tsquery задействует этот
оператор для создания tsquery, который можно сопоставлять с многословной фразой,
включающей в себя стоп-слова. Например:
SELECT phraseto_tsquery('cats ate rats');
phraseto_tsquery
-------------------------------
'cat' <-> 'ate' <-> 'rat'
SELECT phraseto_tsquery('the cats ate the rats');
phraseto_tsquery
-------------------------------
'cat' <-> 'ate' <2> 'rat'
Особым случаем, который иногда может быть полезен, является запись <0>
, с
помощью которой можно потребовать, чтобы обоим шаблонам соответствовало одно слово.
Для управления операторами tsquery можно применять скобки. Без скобок |
имеет
наименьшую степень привязки, за ним идет &
, затем <->
, а привязка !
максимальна.
Стоит отметить, что операторы И/ИЛИ/НЕ имеют слегка иное значение, когда находятся
в аргументах оператора ПРЕДШЕСТВУЕТ, поскольку в нем важна точная позиция совпадения.
Например, обычному !x
соответствуют только документы, нигде не содержащие x.
Но условию !x <-> y
соответствует y, если оно не идет сразу за x; вхождение
x в любом другом месте документа не исключает сопоставления. Другой пример:
обычному x & y
требуется только, чтобы x и y фигурировали где-то в документе,
но (x & y) <-> z
требуется, чтобы x и y находились в одном месте,
непосредственно перед z. Следовательно, этот запрос ведет себя по-другому, нежели
x <-> z & y <-> z
, которому будет соответствовать документ, содержащий две
отдельные последовательности x z и y z. (Этот конкретный запрос в данном виде
неприменим, поскольку x и y не могут находиться в одном месте, но в более
сложных ситуациях, например, с шаблонами сопоставления по префиксу, запрос в такой
форме может оказаться полезен.)
Конфигурации
Выше мы привели простые примеры текстового поиска. Как упоминалось ранее, функциональность
полнотекстового поиска включает средства, позволяющие делать гораздо большее:
пропускать индексацию определенных слов (стоп-слов), обрабатывать синонимы и
применять сложный синтаксический анализ, например, выделять синтаксические
элементы не только на основе пробелов. Эта функциональность управляется конфигурациями
текстового поиска. В QHB имеются предопределенные конфигурации
для многих языков, и вы можете с легкостью создавать собственные. (Команда psql
\dF
выводит все доступные конфигурации.)
Подходящая конфигурация выбирается во время установки и записывается в параметре
default_text_search_config в файле qhb.conf. Если вы используете для
всего кластера одну конфигурацию текстового поиска, достаточно будет этого значения
в qhb.conf. Чтобы использовать в кластере разные конфигурации, но для каждой
базы данных какую-то одну, выполните ALTER DATABASE ... SET
. В противном случае
можно устанавливать default_text_search_config в рамках каждого сеанса.
У каждой функции текстового поиска, зависящей от конфигурации, есть необязательный аргумент regconfig, в котором можно явно указать используемую конфигурацию. Значение default_text_search_config используется, только когда этот аргумент опущен.
Для упрощения создания специальных конфигураций текстового поиска они строятся из более простых объектов базы данных. Механизм текстового поиска QHB предоставляет четыре типа объектов базы данных, связанных с конфигурацией:
-
Синтаксические анализаторы текстового поиска разбивают документы на синтаксические единицы и классифицируют их (например, как слова или числа).
-
Словари текстового поиска приводят эти единицы в нормализованную форму и отбрасывают стоп-слова.
-
Шаблоны текстового поиска предоставляют функции, которые лежат в основе словарей. (В словаре просто задается шаблон и набор параметров для него.)
-
Конфигурации текстового поиска выбирают синтаксический анализатор и набор словарей, который будет использоваться для нормализации синтаксический единиц, выданных этим анализатором.
Анализаторы и шаблоны текстового поиска строятся из низкоуровневых функций на C/RUST; следовательно, чтобы разработать новые, требуется умение программировать на C/RUST, а чтобы подключить их к базе данных — права суперпользователя. (В каталоге share/extension установки QHB можно найти примеры дополнительных анализаторов и шаблонов.) Поскольку словари и конфигурации просто параметризуют и соединяют вместе некоторые из нижележащих анализаторов и шаблонов, для их создания не требуется специальных прав. Далее в этой главе приводятся примеры создания пользовательских словарей и конфигураций.
Таблицы и индексы
Примеры из предыдущего раздела иллюстрировали выполнение сопоставления при полнотекстовом поиске с использованием простых строковых констант. В этом разделе показывается, как находить данные в таблице, возможно, с применением индексов.
Поиск в таблице
Полнотекстовый поиск можно выполнить и без индекса. Следующий простой запрос выводит заголовок (title) каждой строки, содержащей слово friend в поле body:
SELECT title
FROM pgweb
WHERE to_tsvector('english', body) @@ to_tsquery('english', 'friend');
Также он найдет связанные слова, такие как friends и friendly, поскольку все они сводятся к одной нормализованной лексеме.
В запросе выше указано, что для анализа и нормализации строк будет использоваться конфигурация english. Как вариант, параметры конфигурации можно опустить:
SELECT title
FROM pgweb
WHERE to_tsvector(body) @@ to_tsquery('friend');
В этом запросе будет использоваться конфигурация, установленная в параметре default_text_search_config.
В следующем, более сложном примере выбирается десять последних документов, содержащих слова create и table в полях title или body:
SELECT title
FROM pgweb
WHERE to_tsvector(title || ' ' || body) @@ to_tsquery('create & table')
ORDER BY last_mod_date DESC
LIMIT 10;
Для наглядности мы опустили вызовы функции coalesce, которые нужны, чтобы найти строки, содержащие NULL в одном из полей.
Хотя эти запросы будут работать и без индекса, большинство приложений посчитают такой подход слишком медленным, за исключением разве что нерегулярных узконаправленных поисков. Практическое применение текстового поиска обычно требует создания индекса.
Создание индексов
Чтобы ускорить текстовый поиск, мы можем создать индекс GIN (см. раздел Предпочтительные типы индексов для текстового поиска):
CREATE INDEX qhbweb_idx ON qhbweb USING GIN (to_tsvector('english', body));
Обратите внимание, что здесь используется версия to_tsvector с двумя аргументами. В выражениях индексов (см. раздел Индексы по выражениям) могут применяться только те функции текстового поиска, в которых задается имя конфигурации. Это связано с тем, что на содержимое индекса не должно влиять значение параметра default_text_search_config. Иначе содержимое индекса может быть несогласованным вследствие того, что разные записи могут содержать значения tsvector, которые были созданы с разными конфигурациями текстового поиска, и нельзя будет отличить одну от другой. Выгрузить и восстановить такой индекс корректно будет невозможно.
Поскольку в индексе выше применялась версия to_tsvector с двумя аргументами,
этот индекс будет использоваться только в тех запросах, где функция to_tsvector
вызывается с двумя аргументами и с тем же именем конфигурации. То есть
WHERE to_tsvector('english', body) @@ 'a & b'
может использовать этот индекс,
но WHERE to_tsvector(body) @@ 'a & b'
— нет. Это гарантирует, что индекс будет
использоваться только с той конфигурацией, с которой создавались его записи.
Можно устанавливать и более сложные выражения индекса, где имя конфигурации задается в другом столбце, например:
CREATE INDEX qhbweb_idx ON qhbweb USING GIN (to_tsvector(config_name, body));
где config_name — это столбец в таблице qhbweb. Это позволяет смешивать в
одном индексе несколько конфигураций, записывая при этом, какая конфигурация
использовалась для каждой записи индекса. Это может быть полезно, например, если
коллекция документов содержит документы на разных языках. Опять же, запросы, в
которых предполагается использовать этот индекс, должны быть составлены
соответствующим образом, например, WHERE to_tsvector(config_name, body) @@ 'a & b'
.
В индексах можно даже конкатенировать столбцы:
CREATE INDEX qhbweb_idx ON qhbweb USING GIN (to_tsvector('english', title || ' ' || body));
Еще один способ — создать отдельный столбец tsvector для сохранения вывода функции to_tsvector. Чтобы этот столбец автоматически синхронизировался с исходными данными, создайте его как сохраненный генерируемый столбец. В следующем примере используется конкатенация столбцов title и body, и вызывается функция coalesce, чтобы гарантировать, что даже если один из столбцов равен NULL, другой по-прежнему будет индексироваться:
ALTER TABLE qhbweb
ADD COLUMN textsearchable_index_col tsvector
GENERATED ALWAYS AS (to_tsvector('english', coalesce(title, '') || ' ' || coalesce(body, ''))) STORED;
Затем мы создаем индекс GIN для ускорения поиска:
CREATE INDEX textsearch_idx ON pgweb USING GIN (textsearchable_index_col);
Теперь мы готовы выполнить быстрый полнотекстовый поиск:
SELECT title
FROM qhbweb
WHERE textsearchable_index_col @@ to_tsquery('create & table')
ORDER BY last_mod_date DESC
LIMIT 10;
Одно из преимуществ подхода с хранением выражения индекса в отдельном столбце заключается в том, что для использования индекса нет необходимости явно указывать в запросах конфигурацию текстового поиска. Как показано в примере выше, запрос может зависеть от параметра default_text_search_config. Еще одно преимущество состоит в том, что поиск будет проходить быстрее, так как повторно вызывать to_tsvector для проверки соответствия данных индексу не понадобится. (Это более актуально для индексов GiST, чем для GIN; см. раздел Предпочтительные типы индексов для текстового поиска.) Тем не менее подход с индексом по выражению проще реализовать, и для него требуется меньше места на диске, так как представление tsvector не хранится явно.
Управление текстовым поиском
Для реализации полнотекстового поиска требуется функция, чтобы создавать tsvector из документа и tsquery из пользовательского запроса. Кроме того, нам нужно возвращать результаты в удобном порядке, поэтому нам требуется функция, сравнивающая документы с учетом их релевантности для этого запроса. Также важно иметь возможность правильно отображать результаты. QHB предоставляет поддержку для всех этих функций.
Анализ документов
Для преобразования документа в тип данных tsvector QHB предоставляет функцию to_tsvector.
to_tsvector([ конфигурация regconfig, ] документ text) returns tsvector
to_tsvector разбирает текстовый документ на синтаксические единицы, сводит эти единицы в лексемы и возвращает значение tsvector, в котором перечисляются лексемы и их позиции в документе. Документ обрабатывается в соответствии с заданной конфигурацией текстового поиска или конфигурацией по умолчанию. Вот простой пример:
SELECT to_tsvector('english', 'a fat cat sat on a mat - it ate a fat rats');
to_tsvector
-----------------------------------------------------
'ate':9 'cat':3 'fat':2,11 'mat':7 'rat':12 'sat':4
В примере выше мы видим, что результирующий tsvector не содержит слова a, on и it, слово rats превратилось в rat, а знак препинания «-» был проигнорирован.
Функция to_tsvector внутри вызывает синтаксический анализатор, который разбивает текст документа на синтаксические единицы и присваивает тип каждой из них. Для каждой единицы проверяется список словарей (раздел Словари), который может варьировать в зависимости от типа единицы. Первый же словарь, распознавший единицу, выдает одну или несколько представляющих ее нормализованных лексем. Например, rats превращается в rat, потому что один из словарей распознает, что слово rats — это форма множественного числа от rat. Некоторые слова распознаются как стоп-слова (подраздел Стоп-слова) и поэтому игнорируются, так как они встречаются слишком часть, чтобы использоваться в поиске. В нашем примере это a, on и it. Если ни один словарь в списке не распознает синтаксическую единицу, она тоже игнорируется. В данном примере это произошло со знаком препинания «-», поскольку в действительности для его типа единицы (символы-разделители) не назначен ни один словарь, то есть единицы разделителей никогда не будут индексироваться. Выбор анализатора, словарей и индексируемых типов единиц определяется выбранной конфигурацией текстового поиска (раздел Пример конфигурации). В одной базе данных можно иметь много разных конфигураций, и для разных языков доступны предопределенные конфигурации. В нашем примере мы использовали english, конфигурацию по умолчанию для английского языка.
Для назначения записям tsvector заданного веса (где вес задается буквой A, B, C или D) можно применять функцию setweight. Обычно это используется для маркировки записей, находящихся в разных частях документа, например в заголовке или теле. Затем эту информацию можно использовать для ранжирования результатов поиска.
Поскольку to_tsvector(NULL) вернет NULL, рекомендуется вызывать coalesce везде, где поле может содержать NULL. Вот рекомендуемый метод создания tsvector из структурированного документа:
UPDATE tt SET ti =
setweight(to_tsvector(coalesce(title,'')), 'A') ||
setweight(to_tsvector(coalesce(keyword,'')), 'B') ||
setweight(to_tsvector(coalesce(abstract,'')), 'C') ||
setweight(to_tsvector(coalesce(body,'')), 'D');
Здесь мы использовали setweight, чтобы отметить происхождение каждой лексемы в
итоговых значениях tsvector, а затем объединили помеченные значения с помощью
оператора конкатенации типов tsvector ||
. (Подробную информацию об этих
операциях см. в подразделе Обработка документов.)
Анализ запросов
Для преобразования запроса в тип данных tsquery QHB предоставляет функции to_tsquery, plainto_tsquery, phraseto_tsquery и websearch_to_tsquery. Функция to_tsquery дает доступ к более широким возможностям, чем plainto_tsquery или phraseto_tsquery, но более строга к входным данным. Функция websearch_to_tsquery является упрощенной версией to_tsquery с альтернативным синтаксисом, похожим на тот, что применяется в поисковых системах в Интернете.
to_tsquery
([ конфигурация
regconfig
, ] текст_запроса
text
) returns
tsquery
Функцияto_tsquery создает значение tsquery из текста_запроса, который
должен состоять из одиночных синтаксических единиц, разделенных операторами типов
tsquery &
(И), |
(ИЛИ), !
(НЕ) и <->
(ПРЕДШЕСТВУЕТ), возможно,
сгруппированных с помощью скобок. Другими словами, входное значение для
to_tsquery уже должно следовать общим правилам для входных значений tsquery,
описанным в подразделе tsquery. Разница состоит в том, что в то время как в
простом входном значении tsquery синтаксические единицы воспринимаются буквально,
функция to_tsquery нормализует каждую единицу в лексему, используя конфигурацию
по умолчанию или заданную, и отбрасывает те единицы, которые согласно этой
конфигурации являются стоп-словам. Например:
SELECT to_tsquery('english', 'The & Fat & Rats');
to_tsquery
---------------
'fat' & 'rat'
Как и для простого входного значения tsquery, каждой лексеме можно назначить вес(а), чтобы они строго соответствовали только лексемами tsvector с такими же весами. Например:
SELECT to_tsquery('english', 'Fat | Rats:AB');
to_tsquery
------------------
'fat' | 'rat':AB
Также к лексеме можно добавить *, чтобы задать сопоставление по префиксу:
SELECT to_tsquery('supern:*A & star:A*B');
to_tsquery
--------------------------
'supern':*A & 'star':*AB
Такая лексема будет соответствовать любому слову в tsvector, начинающемуся с заданной строки.
Кроме того, to_tsquery может принимать фразы в апострофах. Это полезно в первую очередь когда конфигурация включает тезаурус, который может среагировать на такие фразы. В примере ниже тезаурус содержит правило supernovae stars : sn:
SELECT to_tsquery('''supernovae stars'' & !crab');
to_tsquery
---------------
'sn' & !'crab'
Без апострофов to_tsquery будет выдавать синтаксическую ошибку для единиц, не разделенных оператором И, ИЛИ или ПРЕДШЕСТВУЕТ.
plainto_tsquery
([ конфигурация
regconfig
, ] текст_запроса
text
) returns
tsquery
Функция plainto_tsquery преобразует неотформатированный текст_запроса
в значение tsquery. Текст анализируется и нормализуется почти так же, как для
to_tsvector, затем между оставшимися словами вставляется оператор tsquery &
(И).
Пример:
SELECT plainto_tsquery('english', 'The Fat Rats');
plainto_tsquery
-----------------
'fat' & 'rat'
Обратите внимание, что plainto_tsquery не будет распознавать во входном значении операторы tsquery и метки весов и префиксов:
SELECT plainto_tsquery('english', 'The Fat & Rats:C');
plainto_tsquery
---------------------
'fat' & 'rat' & 'c'
В данном случае были отброшены все пунктуационные знаки.
phraseto_tsquery
([ конфигурация
regconfig
, ] текст_запроса
text
) returns
tsquery
Функция phraseto_tsquery ведет себя подобно plainto_tsquery, за исключением
того, что вместо оператора &
(И) она вставляет между оставшимися словами
оператор <->
(ПРЕДШЕСТВУЕТ). Кроме того, стоп-слова не просто отбрасываются, а
подсчитываются путем вставления операторов <N>
вместо <->
. Эта функция полезна
при поиске точных последовательностей лексем, поскольку операторы ПРЕДШЕСТВУЕТ
проверяют не только наличие всех лексем, но и их порядок.
Пример:
SELECT phraseto_tsquery('english', 'The Fat Rats');
phraseto_tsquery
------------------
'fat' <-> 'rat'
Как и plainto_tsquery, функция phraseto_tsquery не будет распознавать во входном значении операторы tsquery и метки весов и префиксов:
SELECT phraseto_tsquery('english', 'The Fat & Rats:C');
phraseto_tsquery
-----------------------------
'fat' <-> 'rat' <-> 'c'
websearch_to_tsquery
([ конфигурация
regconfig
, ] текст_запроса
text
) returns
tsquery
Функция websearch_to_tsquery создает значение tsquery из текста_запроса, используя альтернативный синтаксис, в котором простой неформатированный текст является допустимым запросом. В отличие от plainto_tsquery и phraseto_tsquery, она также распознает определенные операторы. Более того, эта функция никогда не будет выдавать синтаксические ошибки, что позволяет использовать для поиска необработанное значение, введенное пользователем. Поддерживается следующий синтаксис:
-
текст не в кавычках: текст, не заключенный в кавычки, будет преобразован в ключевые слова, разделенные операторами
&
, как если бы его обрабатывала plainto_tsquery. -
"текст в кавычках": текст, заключенный в кавычки, будет преобразован в ключевые слова, разделенные операторами
<->
, как если бы его обрабатывала phraseto_tsquery. -
OR: слово «or» будет преобразовано в оператор
|
. -
-: минус будет преобразован в оператор
!
.
Другая пунктуация игнорируется. Поэтому, как и plainto_tsquery и phraseto_tsquery, функция websearch_to_tsquery не будет распознавать во входном значении операторы tsquery и метки весов и префиксов.
Примеры:
SELECT websearch_to_tsquery('english', 'The fat rats');
websearch_to_tsquery
----------------------
'fat' & 'rat'
(1 row)
SELECT websearch_to_tsquery('english', '"supernovae stars" -crab');
websearch_to_tsquery
----------------------------------
'supernova' <-> 'star' & !'crab'
(1 row)
SELECT websearch_to_tsquery('english', '"sad cat" or "fat rat"');
websearch_to_tsquery
-----------------------------------
'sad' <-> 'cat' | 'fat' <-> 'rat'
(1 row)
SELECT websearch_to_tsquery('english', 'signal -"segmentation fault"');
websearch_to_tsquery
---------------------------------------
'signal' & !( 'segment' <-> 'fault' )
(1 row)
SELECT websearch_to_tsquery('english', '""" )( dummy \\ query <->');
websearch_to_tsquery
----------------------
'dummi' & 'queri'
(1 row)
Ранжирование результатов поиска
При ранжировании происходит попытка оценить, насколько документы релевантны конкретному запросу, чтобы при большом количестве совпадений выводить наиболее релевантные из них первыми. QHB предоставляет две предопределенные функции ранжирования, принимающие в расчет лексическую, дистанционную и структурную информацию; то есть они учитывают, как часто и насколько близко встречаются в документе ключевые слова запроса и насколько важна часть документа, в которой они располагаются. Однако само понятие релевантности размыто и в многом определяется приложением. Разные приложения могут требовать для ранжирования дополнительную информацию, например, время изменения документа. Встроенные функции ранжирования представляют собой всего лишь примеры. Для своих конкретных нужд вы можете написать собственные функции ранжирования и/или комбинировать их результаты с дополнительными факторами.
В настоящее время доступны две функции ранжирования:
ts_rank
([ веса
float4[]
, ] вектор
tsvector
, запрос
tsquery
[, нормализация
integer
]) returns
float4
Ранжирует векторы, основываясь на частоте соответствующих им лексем.
ts_rank_cd
([ веса
float4[]
, ] вектор
tsvector
, запрос
tsquery
[, нормализация
integer
]) returns
float4
Эта функция вычисляет плотность покрытия для данного вектора аргумента и запроса, как описано в статье Кларка, Кормана и Тадхоупа «Relevance Ranking for One to Three Term Queries» в журнале «Information Processing and Management», 1999 г. Плотность покрытия схожа с ранжированием ts_rank, но учитывается еще и близость соответствующих лексем друг к другу.
Для проведения расчетов этой функции требуется информация о позиции лексем. Поэтому она игнорирует все «очищенные» (от информации) лексемы в значении tsvector. Если во входных данных нет неочищенных лексем, результатом будет ноль. (Более подробную информацию о функции strip и позиционной информации в значениях tsvector см. в подразделе Обработка документов.)
Для обеих этих функций необязательный аргумент веса предоставляет возможность придавать вхождениям слов больший или меньший вес в зависимости от их меток. В массивах весов указывается, насколько весома каждая категория слов, в следующем порядке:
{D-weight, C-weight, B-weight, A-weight}
Если веса не предоставляются, то используются эти значения по умолчанию:
{0.1, 0.2, 0.4, 1.0}
Обычно весами маркируются слова из особых областей документа, например из заголовка или краткого обзора, чтобы эти слова могли восприниматься как более или менее важные, чем слова в теле документа.
Поскольку в большом документе вероятность содержания ключевых слова запроса выше,
имеет смысл принимать во внимание размер документа, например, документ с сотней
слов с пятью вхождениями искомого слова, вероятно, более релевантен, чем документ
с тысячей слов, содержащий те же пять вхождений. Обе функции ранжирования принимают
целочисленный параметр нормализация, где указывается, будет ли размер
документа влиять на его ранг и каким образом. Этот параметр управляет несколькими
вариантами поведения, поэтому представляет собой битовую маску: вы можете задать
один или несколько вариантов с помощью оператора |
(например, 2|4
).
-
0 (по умолчанию): размер документа игнорируется
-
1: ранг делится на 1 + логарифм размера документа
-
2: ранг делится на размер документа
-
4: ранг делится на среднее гармоническое расстояние между областями (это реализовано только в ts_rank_cd)
-
8: ранг делится на количество уникальных слов в документе
-
16: ранг делится на 1 + логарифм количества уникальных слов в документе
-
32: ранг делится на свое же значение + 1
Если указывается несколько флагов, трансформации применяются в порядке, приведенном выше.
Следует отметить, что функции ранжирования не используют никакую глобальную
информацию, поэтому добиться чистовой нормализации до 1% или 100% невозможно, хотя
иногда и желательно. Можно применить параметр нормализации 32 (rank/(rank+1)
),
чтобы свести все ранги к диапазону от нуля до единицы, но, разумеется, это будет
всего лишь косметическое изменение; оно не повлияет на порядок сортировки
результатов поиска.
Вот пример, где выбираются только десять соответствий с наивысшим рангом:
SELECT title, ts_rank_cd(textsearch, query) AS rank
FROM apod, to_tsquery('neutrino|(dark & matter)') query
WHERE query @@ textsearch
ORDER BY rank DESC
LIMIT 10;
title | rank
-----------------------------------------------+----------
Neutrinos in the Sun | 3.1
The Sudbury Neutrino Detector | 2.4
A MACHO View of Galactic Dark Matter | 2.01317
Hot Gas and Dark Matter | 1.91171
The Virgo Cluster: Hot Plasma and Dark Matter | 1.90953
Rafting for Solar Neutrinos | 1.9
NGC 4650A: Strange Galaxy and Dark Matter | 1.85774
Hot Gas and Dark Matter | 1.6123
Ice Fishing for Cosmic Neutrinos | 1.6
Weak Lensing Distorts the Universe | 0.818218
Тот же пример с нормализованным ранжированием:
SELECT title, ts_rank_cd(textsearch, query, 32 /* rank/(rank+1) */ ) AS rank
FROM apod, to_tsquery('neutrino|(dark & matter)') query
WHERE query @@ textsearch
ORDER BY rank DESC
LIMIT 10;
title | rank
-----------------------------------------------+-------------------
Neutrinos in the Sun | 0.756097569485493
The Sudbury Neutrino Detector | 0.705882361190954
A MACHO View of Galactic Dark Matter | 0.668123210574724
Hot Gas and Dark Matter | 0.65655958650282
The Virgo Cluster: Hot Plasma and Dark Matter | 0.656301290640973
Rafting for Solar Neutrinos | 0.655172410958162
NGC 4650A: Strange Galaxy and Dark Matter | 0.650072921219637
Hot Gas and Dark Matter | 0.617195790024749
Ice Fishing for Cosmic Neutrinos | 0.615384618911517
Weak Lensing Distorts the Universe | 0.450010798361481
Ранжирование может быть затратным, поскольку требует обращения к значению tsvector каждого соответствующего документа, что может быть ограничено скоростью ввода/вывода и, следовательно, происходить медленно. К сожалению, избежать этого практически невозможно, так как на практике по запросам часто выдается большое количество совпадений.
Выделение результатов
Чтобы представить результаты поиска, в идеале нужно отобразить часть каждого документа и показать, как она связана с запросом. Обычно поисковые системы отображают фрагменты документа с отмеченными ключевыми словами. Для реализации этой функциональности QHB предоставляет функцию ts_headline.
ts_headline
([ конфигурация
regconfig
, ] документ
text
, запрос
tsquery
[, параметры
text
]) returns
text
ts_headline принимает документ вместе с запросом и возвращает отрывок из документа, в котором выделены ключевые слова из запроса. Конфигурацию, используемую для синтаксического анализа документа, можно указать в параметре конфигурация; если он опущен, применяется конфигурация default_text_search_config.
Если задается строка параметры, она должна состоять из списка разделенных запятыми пар параметр=значение (их может быть одна или несколько). Доступные параметры:
-
MaxWords, MinWords (целочисленные): эти числа определяют максимальную и минимальную длину отображаемой выдержки. Значения по умолчанию — 35 и 15 соответственно.
-
ShortWord (целочисленный): слова этой длины или короче будут отбрасываться в начале и конце выдержки, если только они не являются ключевыми словами запроса. Значение по умолчанию — три, что исключает распространенные английские артикли.
-
HighlightAll (логический): при значении true в качестве выдержки будет использован весь документ, и предыдущие три параметра игнорируются. Значение по умолчанию — false.
-
MaxFragments (целочисленный): максимальное количество отображаемых фрагментов текста. При значении ноль (по умолчанию) выбирается метод генерирования выдержек без фрагментов. При значении больше нуля выбирается генерирование выдержек с фрагментами (см. ниже).
-
StartSel, StopSel (строки): строки, которые будут разграничивать имеющиеся в документе слова запроса, чтобы выделить их среди остальных слов в выдержке. Значения по умолчанию — «<b>» и «</b>», что может быть удобно для вывода HTML.
-
FragmentDelimiter (строка): При отображении нескольких фрагментов они будут разделяться этой строкой. Значение по умолчанию — « ... ».
Имена этих параметров распознаются без учета регистра. Строковые значения, содержащие пробелы или запятые, следует заключать в кавычки.
При генерировании выдержек без фрагментов функция ts_headline находит вхождения, соответствующие заданному запросу и выбирает одно из них для отображения, предпочитая те, что содержат в пределах допустимой длины выдержки больше слов из запроса. При генерировании выдержек с фрагментами функция ts_headline находит вхождения, соответствующие запросу, и разделяет их на «фрагменты», состоящие не более чем из MaxWords слов каждый, предпочитая фрагменты, содержащие больше слов из запроса, а затем может «растянуть» фрагменты, включив в них окружающие слова. Таким образом, режим с фрагментами более полезен, когда вхождения слов разбросаны по большим разделам документа или когда желательно отобразить несколько вхождений. Если ни одного соответствия выявить не удается, в обоих режимах будет выведен один фрагмент с первыми MinWords словами из документа.
Например:
SELECT ts_headline('english',
'The most common type of search
is to find all documents containing given query terms
and return them in order of their similarity to the
query.',
to_tsquery('english', 'query & similarity'));
ts_headline
------------------------------------------------------------
containing given <b>query</b> terms +
and return them in order of their <b>similarity</b> to the+
<b>query</b>.
SELECT ts_headline('english',
'Search terms may occur
many times in a document,
requiring ranking of the search matches to decide which
occurrences to display in the result.',
to_tsquery('english', 'search & term'),
'MaxFragments=10, MaxWords=7, MinWords=3, StartSel=<<, StopSel=>>');
ts_headline
------------------------------------------------------------
<<Search>> <<terms>> may occur +
many times ... ranking of the <<search>> matches to decide
Функция ts_headline использует оригинальный документ, а не сжатый конспект tsvector, поэтому она может быть медленной, и применять ее следует с осторожностью.
Дополнительная функциональность
В этом разделе описываются дополнительные функции и операторы, которые могут быть полезны применительно к текстовому поиску.
Обработка документов
В подразделе Анализ документов описывалось, как исходные текстовые документы можно преобразовать в значения типа tsvector. QHB предоставляет также функции и операторы, которые можно использовать для обработки документов, уже представленных в виде tsvector.
tsvector || tsvector
Оператор конкатенации значений tsvector возвращает вектор, объединяющий лексемы и позиционную информацию двух векторов, заданных в аргументах. Позиции и метки весов во время конкатенации сохраняются. Позиции, находящиеся в правом векторе, сдвигаются на максимальное значение позиции, указанной в левом векторе, так что результат почти равнозначен применению to_tsvector к результату конкатенации двух исходных строк документа. (Идентичность не полная, поскольку стоп-слова, удаленные в конце левого аргумента, не повлияют на результат, тогда как при использовании текстовой конкатенации они бы повлияли на позиции лексем в правом аргументе.)
Преимущество применения конкатенации в векторной форме по сравнению с конкатенацией текста перед вызовом to_tsvector заключается в том, что так для анализа разных частей документа можно использовать разные конфигурации. Кроме того, поскольку функция setweight помечает все лексемы данного вектора одинаково, то если вы хотите присвоить разным частям документа разные веса, необходимо проанализировать текст и вызвать setweight до конкатенации.
setweight
(вектор
tsvector
, вес
"char"
) returns
tsvector
Функция setweight возвращает копию входного вектора, в котором каждая позиция была помечена заданным весом: A, B, C или D. (D является меткой по умолчанию для новых векторов, поэтому не отображается в выводе.) Эти метки сохраняются при конкатенации векторов, позволяя функциям ранжировать слова из разных частей документа по-разному.
Обратите внимание, что метки весов применяются к позициям, а не к лексемам. Если входной вектор был очищен от позиционной информации, то setweight ничего не делает.
length
(вектор
tsvector
) returns
integer
Возвращает количество лексем, сохраненных в векторе.
strip
(вектор
tsvector
) returns
tsvector
Возвращает вектор, в котором перечисляются те же лексемы, что и в заданном, но
без информации о позиции и весе. Обычно результат гораздо меньше неочищенного
вектора, но при этом он менее полезен. С очищенными векторами хуже работает
ранжирование по релевантности. Кроме того, оператор tsquery <->
(ПРЕДШЕСТВУЕТ)
никогда не найдет соответствие в очищенном входном значении, так как не сможет
определить расстояние между вхождениями лексем.
Полный список связанных с tsvector функций приведен в таблице Функции текстового поиска.
Обработка запросов
В подразделе Анализ запросов описывалось, как исходные текстовые запросы можно преобразовать в значения типа tsquery. QHB предоставляет также функции и операторы, которые можно использовать для обработки запросов, уже представленных в виде tsquery.
tsquery && tsquery
Возвращает логическое произведение (И) двух данных запросов.
tsquery || tsquery
Возвращает логическое объединение (ИЛИ) двух данных запросов.
!! tsquery
Возвращает логическое отрицание (НЕ) данного запроса.
tsquery <-> tsquery
Возвращает запрос, который ищет соответствие первому данному запросу, сразу за
которым следует соответствие второму данному запросу, используя оператор tsquery
<->
(ПРЕДШЕСТВУЕТ). Например:
SELECT to_tsquery('fat') <-> to_tsquery('cat | rat');
?column?
----------------------------
'fat' <-> ( 'cat' | 'rat' )
tsquery_phrase
(запрос1
tsquery
, запрос2
tsquery
[, расстояние
integer
]) returns
tsquery
Возвращает запрос, который ищет соответствие первому данному запросу, за которым
точно на заданном расстоянии (указывается в лексемах) следует соответствие
второму данному запросу, используя оператор tsquery <N>
. Например:
SELECT tsquery_phrase(to_tsquery('fat'), to_tsquery('cat'), 10);
tsquery_phrase
------------------
'fat' <10> 'cat'
numnode
(запрос
tsquery
) returns
integer
Возвращает количество узлов (лексемы плюс операторы) в значении tsquery. Эта функция помогает определить, имеет ли запрос смысл (возвращает значение > 0), или он содержит только стоп-слова (возвращает 0). Примеры:
SELECT numnode(plainto_tsquery('the any'));
NOTICE: query contains only stopword(s) or does not contain lexeme(s), ignored
-- ЗАМЕЧАНИЕ: запрос содержит только стоп-слово(а) или не содержит лексем, поэтому игнорируется
numnode
---------
0
SELECT numnode('foo & bar'::tsquery);
numnode
---------
3
querytree
(запрос
tsquery
) returns
text
Возвращает часть значения tsquery, которую можно использовать для поиска по индексу. Эта функция помогает выявить неиндексируемые запросы, например такие, которые содержат только стоп-слова или только ключевые слова отрицания. Например:
SELECT querytree(to_tsquery('defined'));
querytree
-----------
'defin'
SELECT querytree(to_tsquery('!defined'));
querytree
-----------
T
Перезапись запросов
Семейство функций ts_rewrite ищет в данном значении tsquery вхождения целевого подзапроса и заменяет каждое из них указанным подставляемым подзапросом. По сути эта операция является специфичной для tsquery версией замены подстроки в строке. Сочетание цели и подстановки можно считать правилом перезаписи запросов. Коллекция таких правил перезаписи может быть полезным средством поиска. Например, можно расширить поиск с помощью синонимов (например, new york, big apple, nyc, gotham) или сузить область поиска, чтобы нацелить пользователя на некоторую актуальную тему. Эта функциональность в некотором смысле пересекается с функциональностью тезаурусов (см. подраздел Тезаурус). Однако при этом можно изменять набор правил перезаписи на лету, тогда как для вступления в силу обновления тезауруса требуется переиндексация.
ts_rewrite
(запрос
tsquery
, цель
tsquery
, подстановка
tsquery
) returns
tsquery
Эта форма ts_rewrite просто применяет одно правило перезаписи: цель заменяется подстановкой везде, где она встречается в запросе. Например:
SELECT ts_rewrite('a & b'::tsquery, 'a'::tsquery, 'c'::tsquery);
ts_rewrite
------------
'b' & 'c'
ts_rewrite
(запрос
tsquery
, select
text
) returns
tsquery
Эта форма ts_rewrite принимает начальный запрос и команду SQL select, которая задается текстовой строкой. Команда select должна выдавать два столбца типа tsquery. Для каждой строки результата select вхождения значения первого столбца (цели) заменяются значением второго столбца (подстановкой) в текущем значении запроса. Например:
CREATE TABLE aliases (t tsquery PRIMARY KEY, s tsquery);
INSERT INTO aliases VALUES('a', 'c');
SELECT ts_rewrite('a & b'::tsquery, 'SELECT t,s FROM aliases');
ts_rewrite
------------
'b' & 'c'
Обратите внимание, что когда таким способом применяются несколько правил перезаписи, порядок их применения может быть важен, поэтому на практике рекомендуется в исходном запросе добавить ORDER BY с каким-либо ключом упорядочивания.
Давайте рассмотрим практический пример, касающийся астрономии. Мы развернем запрос supernovae, используя правила перезаписи в таблице:
CREATE TABLE aliases (t tsquery primary key, s tsquery);
INSERT INTO aliases VALUES(to_tsquery('supernovae'), to_tsquery('supernovae|sn'));
SELECT ts_rewrite(to_tsquery('supernovae & crab'), 'SELECT * FROM aliases');
ts_rewrite
---------------------------------
'crab' & ( 'supernova' | 'sn' )
Мы можем изменить правила перезаписи, просто изменив таблицу:
UPDATE aliases
SET s = to_tsquery('supernovae|sn & !nebulae')
WHERE t = to_tsquery('supernovae');
SELECT ts_rewrite(to_tsquery('supernovae & crab'), 'SELECT * FROM aliases');
ts_rewrite
---------------------------------------------
'crab' & ( 'supernova' | 'sn' & !'nebula' )
Перезапись может быть медленной, когда задано много правил перезаписи, так как она проверяет каждое из них на возможное соответствие. Чтобы отфильтровать явно неподходящие правила, можно воспользоваться операторами включения для типа tsquery. В примере ниже мы выбираем только те правила, которые могут соответствовать исходному запросу:
SELECT ts_rewrite('a & b'::tsquery,
'SELECT t,s FROM aliases WHERE ''a & b''::tsquery @> t');
ts_rewrite
------------
'b' & 'c'
Триггеры для автоматических обновлений
Примечание
Метод, описанный в этом подразделе, считается устаревшим по сравнению с использованием сохраненных генерируемых столбцов, как описано в подразделе Создание индексов.
При использовании отдельного столбца для хранения представления документов в виде значений tsvector необходимо создать триггер для обновления столбца tsvector при изменении столбцов с содержимым документа. Для этого имеются две встроенные функции, но можно и написать свои собственные.
tsvector_update_trigger
(имя_столбца_tsvector
, имя_конфигурации
, имя_столбца_текста
[, ... ])
tsvector_update_trigger_column
(имя_столбца_tsvector
, имя_столбца_конфигурации
, имя_столбца_текста
[, ... ])
Эти триггерные функции автоматически вычисляют значение для столбца tsvector
из одного или нескольких текстовых столбцов под контролем параметров, заданных в
команде CREATE TRIGGER
. Пример их использования:
CREATE TABLE messages (
title text,
body text,
tsv tsvector
);
CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE
ON messages FOR EACH ROW EXECUTE FUNCTION
tsvector_update_trigger(tsv, 'pg_catalog.english', title, body);
INSERT INTO messages VALUES('title here', 'the body text is here');
SELECT * FROM messages;
title | body | tsv
------------+-----------------------+----------------------------
title here | the body text is here | 'bodi':4 'text':5 'titl':1
SELECT title, body FROM messages WHERE tsv @@ to_tsquery('title & body');
title | body
------------+-----------------------
title here | the body text is here
После создания этого триггера любое изменение в поле title или body будет автоматически отражаться в содержимом tsv, так что приложению не придется заниматься этим.
Первым документом триггера должно быть имя обновляемого столбца tsvector. Во втором аргументе задается конфигурация текстового поиска, которая будет применяться для преобразования. Для функции tsvector_update_trigger имя конфигурации передается просто как второй аргумент триггера. Оно должно быть дополнено схемой, как показано выше, чтобы поведение триггера не менялось при изменениях в пути поиска search_path. Для функции tsvector_update_trigger_column второй аргумент триггера представляет собой имя другого столбца таблицы, который должен иметь тип regconfig. Это позволяет выбирать для разных строк разные конфигурации. Оставшиеся аргументы являются именами текстовых столбцов (типа text, varchar или char). Их содержимое будет включено в документ в заданном порядке. Значения NULL будут пропущены (но другие столбцы при этом все равно будут индексироваться).
Ограничение этих встроенных триггеров состоит в том, что они обрабатывают все входные столбцы одинаково. Чтобы столбцы обрабатывались по-разному — например, чтобы заголовку присваивался вес, отличный от веса тела документа, — нужно будет написать специальный триггер. Вот пример того, как это можно сделать на языке PL/pgSQL:
CREATE FUNCTION messages_trigger() RETURNS trigger AS $$
begin
new.tsv :=
setweight(to_tsvector('pg_catalog.english', coalesce(new.title,'')), 'A') ||
setweight(to_tsvector('pg_catalog.english', coalesce(new.body,'')), 'D');
return new;
end
$$ LANGUAGE plpgsql;
CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE
ON messages FOR EACH ROW EXECUTE FUNCTION messages_trigger();
Учтите, что, создавая в триггерах значения tsvector, важно явно задавать имя конфигурации, чтобы на содержимое столбца не влияли изменения параметра default_text_search_config. Если этого не сделать, скорее всего возникнут проблемы; например, результаты поиска изменятся после выгрузки и восстановления данных.
Сбор статистики по документу
Функция ts_stat полезна для проверки конфигурации и нахождения потенциальных стоп-слов.
ts_stat(запрос_sql text, [ веса text, ]
OUT слово text, OUT число_док integer,
OUT число_вхожд integer) returns setof record
Здесь запрос_sql — это текстовое значение, содержащее запрос SQL, который должен возвращать единственный столбец tsvector. Функция ts_stat выполняет этот запрос и возвращает статистику по каждой отдельной лексеме (слову), содержащейся в данных tsvector. Возвращаются следующие столбцы:
-
слово text — значение лексемы
-
число_док integer — количество документов (значений tsvector), в которых встретилось слово
-
число_вхожд integer — общее количество вхождений слова
Если предоставляются веса, то подсчитываются только вхождения с одним из этих весов.
Например, чтобы найти десять наиболее часто встречающихся слов в коллекции документов, можно выполнить:
SELECT * FROM ts_stat('SELECT vector FROM apod')
ORDER BY nentry DESC, ndoc DESC, word
LIMIT 10;
То же самое, но подсчитываются только вхождения слов с весом A или B:
SELECT * FROM ts_stat('SELECT vector FROM apod', 'ab')
ORDER BY nentry DESC, ndoc DESC, word
LIMIT 10;
Синтаксические анализаторы
Синтаксические анализаторы отвечают за разделение исходного текста документа на синтаксические единицы и присвоение типа каждой из них, причем набор возможных типов определяется в самом анализаторе. Обратите внимание, что анализатор никак не меняет текст — он просто выявляет вероятные границы слов. Из-за этой ограниченной сферы действий специальные анализаторы для приложений нужны гораздо реже, чем специальные словари. В настоящее время QHB предоставляет только один встроенный синтаксический анализатор, полезность которого была подтверждена для широкого спектра приложений.
Этот встроенный синтаксический анализатор называется pg_catalog.default. Он распознает 23 типа синтаксических единиц, перечисленные в Таблице 1.
Таблица 1. Типы синтаксических единиц, выделяемых стандартным анализатором
Псевдоним | Описание | Пример |
---|---|---|
asciiword | Слово только из букв ASCII | elephant |
word | Слово из любых букв | mañana |
numword | Слово из букв и цифр | beta1 |
asciihword | Слово только из букв ASCII с дефисами | up-to-date |
hword | Слово из любых букв с дефисами | lógico-matemática |
numhword | Слово из букв и цифр с дефисами | qhb-beta1 |
hword_asciipart | Часть слова с дефисами, только из букв ASCII | qhb в контексте qhb-beta1 |
hword_part | Часть слова с дефисами, из любых букв | lógico или matemática в контексте lógico-matemática |
hword_numpart | Часть слова с дефисами, из букв и цифр | beta1 в контексте qhb-beta1 |
Адрес электронной почты | foo@example.com | |
protocol | Префикс протокола | http:// |
url | URL | example.com/stuff/index.html |
host | Имя хоста | example.com |
url_path | Путь в URL | /stuff/index.html, в контексте URL |
file | Путь или имя файла | /usr/local/foo.txt, если не входит в URL |
sfloat | Научная запись числа | -1.234e56 |
float | Десятичная запись числа | -1.234 |
int | Целое число со знаком | -1234 |
uint | Целое число без знака | 1234 |
version | Номер версии | 1.5.1 |
tag | Тег XML | <a href="dictionaries.html"> |
entity | Сущность XML | & |
blank | Символы-разделители | (любые пробельные символы или знаки препинания, не вошедшие в другие категории) |
Примечание
Для анализатора понятие «буква» определяется настройками локали базы данных, а именно параметром lc_ctype. Слова, содержащие только буквы из ASCII распознаются как отдельный тип единиц, поскольку иногда бывает полезно их выделить. В большинстве европейских языков типы единиц word и asciiword должны обрабатываться одинаково.Тип email поддерживает не все символы, считающиеся допустимыми согласно стандарту RFC 5322. В частности, для имен почтовых ящиков помимо буквенно- цифровых символов поддерживаются только точка, минус и подчеркивание.
Анализатор может выдавать перекрывающиеся единицы из одного места текста. Например, слово с дефисом будет выдано и как целое слово, и по частям:
SELECT alias, description, token FROM ts_debug('foo-bar-beta1');
alias | description | token
-----------------+------------------------------------------+---------------
numhword | Hyphenated word, letters and digits | foo-bar-beta1
hword_asciipart | Hyphenated word part, all ASCII | foo
blank | Space symbols | -
hword_asciipart | Hyphenated word part, all ASCII | bar
blank | Space symbols | -
hword_numpart | Hyphenated word part, letters and digits | beta1
Это поведение является желательным, поскольку позволяет искать как по слову целиком, так и по его частям. Вот еще один яркий пример:
SELECT alias, description, token FROM ts_debug('http://example.com/stuff/index.html');
alias | description | token
----------+---------------+------------------------------
protocol | Protocol head | http://
url | URL | example.com/stuff/index.html
host | Host | example.com
url_path | URL path | /stuff/index.html
Словари
Словари используются для исключения слов, которые не должны учитываться при поиске (стоп-слова), и нормализации слов, чтобы разные формы одного слова считались совпадающими. Успешно нормализованное слово называется лексемой. Вдобавок к улучшению качества поиска нормализация и удаление стоп-слов уменьшают размер представления документа в виде tsvector, тем самым увеличивая производительность. Нормализация не всегда имеет лингвистический смысл и обычно зависит от семантики приложения.
Несколько примеров нормализации:
-
Лингвистическая — словари Ispell пытаются свести входные слова к нормализованной форме; стеммеры убирают окончания слов
-
Адреса URL могут быть канонизированы, чтобы равнозначные URL считались совпадающими:
- http://www.pgsql.ru/db/mw/index.html
- http://www.pgsql.ru/db/mw/
- http://www.pgsql.ru/db/../db/mw/index.html
-
Названия цветов могут быть заменены их шестнадцатеричными значениями, например
red, green, blue, magenta -> FF0000, 00FF00, 0000FF, FF00FF
-
При индексировании чисел мы можем отбросить несколько цифр в дробной части для сокращения диапазона возможных чисел, чтобы, например, 3.14159265359, 3.1415926 и 3.14 стали одинаковыми после нормализации, если после десятичной точки останутся только две цифры.
Словарь — это программа, которая принимает на вход синтаксическую единицу и возвращает:
-
массив лексем, если входная единица известна словарю (обратите внимание, что из одной синтаксической единицы может получиться несколько лексем)
-
одну лексему с установленным флагом TSL_FILTER для замены исходной синтаксической единицы на новую, чтобы передать ее следующим словарям (словарь, который делает это, называется фильтрующим словарем)
-
пустой массив, если словарь узнает эту синтаксическую единицу, но она является стоп-словом
-
NULL, если словарь не распознает введенную синтаксическую единицу
QHB предоставляет предопределенные словари для многих языков. Кроме того, имеется несколько предопределенных шаблонов, которые можно использовать для создания новых словарей с индивидуальными параметрами. Все эти шаблоны описаны ниже. Если ни один из существующих шаблонов не подходит, можно создать новые; примеры см. в каталоге share/extension установки QHB.
Конфигурация текстового поиска связывает синтаксический анализатор с набором словарей для обработки выданных им синтаксических единиц. Для каждого типа единицы, который может вернуть анализатор, в конфигурации задается отдельный список словарей. Когда анализатор находит синтаксическую единицу этого типа, он по порядку сверяется с каждым словарем в списке, пока какой-либо из них не распознает ее как знакомое слово. Если эта единица идентифицируется как стоп-слово или не распознается ни одним словарем, она отбрасывается и не будет индексироваться или учитываться при поиске. Обычно первый же словарь, вернувший значение, отличное от NULL, определяет результат, и остальные словари уже не проверяются, но фильтрующий словарь может заменить заданное слово модифицированным, которое затем передается следующим словарям.
Общее правило конфигурации списка словарей заключается в том, чтобы поместить в начало наиболее узкопрофильные и специфические словари, затем более общие и закончить самым общим словарем, например стеммером Snowball или словарем simple, распознающим все. Например, для поиска по теме астрономии (конфигурация astro_en) тип единиц asciiword (слово из букв ASCII) можно связать со словарем синонимов астрономических терминов, с обычным английским словарем и со стеммером английских окончаний Snowball:
ALTER TEXT SEARCH CONFIGURATION astro_en
ADD MAPPING FOR asciiword WITH astrosyn, english_ispell, english_stem;
Фильтрующий словарь можно поместить в любое место списка, за исключением конца, где он будет бесполезен. Фильтрующие словари полезны для частичной нормализации слов в целях упрощения задачи для последующих словарей. К примеру, фильтрующий словарь можно использовать для удаления с акцентированных букв диакритических знаков, как это делает модуль unaccent.
Стоп-слова
Стоп-слова — это слова, которые встречаются очень часто, есть почти в каждом документе и не имеют различительной ценности. Поэтому при полнотекстовом поиске их можно игнорировать. К примеру, каждый английский текст содержит слова a и the, так что хранить их в индексе не имеет смысла. Тем не менее стоп-слова влияют на позиции лексем в значении tsvector, что, в свою очередь, влияет на ранжирование:
SELECT to_tsvector('english', 'in the list of stop words');
to_tsvector
----------------------------
'list':3 'stop':5 'word':6
Позиции 1,2,4 отсутствуют, поскольку являются стоп-словами. Ранги, вычисленные для документов со стоп-словами и без них, значительно различаются:
SELECT ts_rank_cd (to_tsvector('english', 'in the list of stop words'), to_tsquery('list & stop'));
ts_rank_cd
------------
0.05
SELECT ts_rank_cd (to_tsvector('english', 'list stop words'), to_tsquery('list & stop'));
ts_rank_cd
------------
0.1
Как именно обрабатывать стоп-слова, определяет конкретный словарь. Например, словари ispell сначала нормализуют слова, а потом просматривают список стоп-слов, тогда как стеммеры Snowball сначала просматривают список стоп-слов. Причина такого разного поведения объясняется стремлением снизить количество «фоновых» слов.
Простой словарь
Работа шаблона словарей simple заключается в преобразовании входной синтаксической единицы в нижний регистр и проверке ее по файлу стоп-слов. Если эта единица обнаруживается в файле, то возвращается пустой массив, а единица отбрасывается. В противном случае это слово в нижнем регистре возвращается в виде нормализованной лексемы. Как вариант, этот словарь можно сконфигурировать так, чтобы он выдавал слова, отличные от стоп-слов, как нераспознанные, позволяя передавать их следующему словарю в списке.
Вот пример определения словаря с помощью шаблона simple:
CREATE TEXT SEARCH DICTIONARY public.simple_dict (
TEMPLATE = pg_catalog.simple,
STOPWORDS = english
);
Здесь english — это базовое имя файла стоп-слов. Полным именем файла будет
$SHAREDIR/tsearch_data/english.stop, где $SHAREDIR означает
каталог с разделяемыми данными установки QHB, часто это
/usr/local/share/qhb (если вы в этом не уверены, точно узнать его имя можно
с помощью команды pg_config --sharedir
). Формат этого файла представляет собой
просто список слов, по одному в строке. Пустые строки и завершающие пробелы
игнорируются, а символы в верхнем регистре переводятся в нижний, но больше
содержимое файла никак не обрабатывается.
Теперь мы можем протестировать наш словарь:
SELECT ts_lexize('public.simple_dict', 'YeS');
ts_lexize
-----------
{yes}
SELECT ts_lexize('public.simple_dict', 'The');
ts_lexize
-----------
{}
Также мы можем сделать так, чтобы словарь возвращал NULL вместо слова в нижнем регистре, если то не обнаруживается в файле стоп-слов. Это поведение выбирается установкой в параметре словаря Accept значения false. Продолжая наш пример:
ALTER TEXT SEARCH DICTIONARY public.simple_dict ( Accept = false );
SELECT ts_lexize('public.simple_dict', 'YeS');
ts_lexize
-----------
SELECT ts_lexize('public.simple_dict', 'The');
ts_lexize
-----------
{}
Со значением Accept = true (по умолчанию) словарь simple имеет смысл помещать только в конец списка словарей, поскольку он никогда не передаст синтаксическую единицу следующему словарю. И наоборот, Accept = false имеет смысл, только когда за нашим словарем идет еще хотя бы один словарь.
ВНИМАНИЕ!
Большинство типов словарей полагаются на файлы конфигурации, например, на файлы стоп-слов. Эти файлы должны храниться в кодировке UTF-8. Если текущая кодировка базы данных отличается, они будут переведены в нее, когда их прочитает сервер.
ВНИМАНИЕ!
Обычно сеанс базы данных прочитывает конфигурационный файл словаря только один раз, при первом использовании. Если вы изменяете этот файл и хотите, чтобы существующие сеансы работали с новым содержимым, выполните для этого словаря командуALTER TEXT SEARCH DICTIONARY
. Это обновление может быть «пустышкой» и в действительности не менять значения никаких параметров.
Словарь синонимов
Этот шаблон словарей используется для создания словарей, заменяющих слова их синонимами. Словосочетания не поддерживаются (для этого воспользуйтесь шаблоном тезауруса (подраздел Тезаурус)). Словарь синонимов можно использовать для преодоления лингвистических проблем, например, не дав стеммеру английского сократить слово «Paris» до «pari». Для этого достаточно записать в словаре синонимов строку Paris paris и поместить его перед словарем english_stem. Например:
SELECT * FROM ts_debug('english', 'Paris');
alias | description | token | dictionaries | dictionary | lexemes
-----------+-----------------+-------+----------------+--------------+---------
asciiword | Word, all ASCII | Paris | {english_stem} | english_stem | {pari}
CREATE TEXT SEARCH DICTIONARY my_synonym (
TEMPLATE = synonym,
SYNONYMS = my_synonyms
);
ALTER TEXT SEARCH CONFIGURATION english
ALTER MAPPING FOR asciiword
WITH my_synonym, english_stem;
SELECT * FROM ts_debug('english', 'Paris');
alias | description | token | dictionaries | dictionary | lexemes
-----------+-----------------+-------+---------------------------+------------+---------
asciiword | Word, all ASCII | Paris | {my_synonym,english_stem} | my_synonym | {paris}
Единственный параметр, который нужен шаблону synonym, это SYNONYMS, в котором задается базовое имя его файла конфигурации — в примере выше это my_synonyms. Полным именем этого файла будет $SHAREDIR/tsearch_data/my_synonyms.syn (где $SHAREDIR означает каталог с разделяемыми данными установки QHB). Формат этого файла представляет собой просто строки, в каждой из которых сначала идет заменяемое слово, а затем, после пробела, его синоним. Пустые строки и завершающие пробелы игнорируются.
Шаблон synonym также принимает необязательный параметр CaseSensitive, который по умолчанию имеет значение false. При CaseSensitive равном false слова в файле синонимов приводятся к нижнему регистру, равно как и входные синтаксические единицы. При значении true слова и синтаксические единицы не приводятся к нижнему регистру, а сравниваются как есть.
В конце синонима в этом файле конфигурации можно добавить звездочку (*). Она указывает на то, что синоним является префиксом. В функции to_tsvector() звездочка игнорируется, но при ее использовании в to_tsquery(), результатом будет запрос с маркером сопоставления префиксов (см. подраздел Анализ запросов). Например, предположим, что в нашем файле $SHAREDIR/tsearch_data/ synonym_sample.syn имеются следующие записи:
quantumhybrid qhb
quantumhybridbase qhb
quantum qhb
gogle googl
indices index*
Тогда мы получим такие результаты:
mydb=# CREATE TEXT SEARCH DICTIONARY syn (template=synonym, synonyms='synonym_sample');
mydb=# SELECT ts_lexize('syn', 'indices');
ts_lexize
-----------
{index}
(1 row)
mydb=# CREATE TEXT SEARCH CONFIGURATION tst (copy=simple);
mydb=# ALTER TEXT SEARCH CONFIGURATION tst ALTER MAPPING FOR asciiword WITH syn;
mydb=# SELECT to_tsvector('tst', 'indices');
to_tsvector
-------------
'index':1
(1 row)
mydb=# SELECT to_tsquery('tst', 'indices');
to_tsquery
------------
'index':*
(1 row)
mydb=# SELECT 'indexes are very useful'::tsvector;
tsvector
---------------------------------
'are' 'indexes' 'useful' 'very'
(1 row)
mydb=# SELECT 'indexes are very useful'::tsvector @@ to_tsquery('tst', 'indices');
?column?
----------
t
(1 row)
Тезаурус
Тезаурус (Thesaurus или сокращенно TZ) представляет собой коллекцию слов и включает информацию о взаимосвязях слов и словосочетаний, т. е. более широкие понятия (broader terms, BT), более узкие понятия (narrower terms, NT), предпочтительные термины, нежелательные термины, родственные термины и т. д.
В общем и целом, тезаурус заменяет все нежелательные термины одним предпочтительным и при необходимости также сохраняет исходные термины для индексации. Текущая реализация тезауруса QHB является расширением словаря синонимов с дополнительной поддержкой словосочетаний. Тезаурусу требуется файл конфигурации следующего формата:
# это комментарий
sample word(s) : indexed word(s)
more sample word(s) : more indexed word(s)
...
где символ двоеточия (:) служит разделителем между словосочетанием и его заменой.
Перед проверкой соответствия словосочетаний тезаурус нормализует вводимый текст с помощью внутреннего словаря (который задается в его конфигурации). Можно выбрать только один внутренний словарь. Если он не может распознать слово, выдается ошибка. В этом случае следует удалить это слово или добавить его во внутренний словарь. Перед индексируемым словом можно добавить звездочку (*), чтобы внутренний словарь его не обрабатывал, но он должен знать все слова-образцы.
Если входной синтаксической единице соответствует несколько словосочетаний, тезаурус выбирает самое длинное определение, а если таких оказывается несколько, то самое последние из них.
Конкретные стоп-слова, распознанные внутренним словарем, указать нельзя; вместо этого используйте ?, чтобы пометить позицию, где может находиться стоп-слово. Например, предположим, что, согласно внутреннему словарю, a и the являются стоп-словами:
? one ? two : swsw
соответствует входным строкам a one the two и the one a two; обе эти строки будут заменены на swsw.
Поскольку тезаурус способен распознавать словосочетания, он должен запоминать свое состояние и взаимодействовать с синтаксическим анализатором. Тезаурус использует эти привязки для проверки того, должен ли он обработать следующее слово или прекратить накопление словосочетания. Поэтому конфигурировать тезаурус следует аккуратно. К примеру, если назначить тезаурусу обработку только единиц asciiword, тогда определение one 7 в нем не будет работать, поскольку тип uint этому тезаурусу не назначен.
ВНИМАНИЕ!
Тезаурусы используются при индексации, поэтому любое изменение его параметров требует переиндексации. Для большинства других типов словарей небольшие изменения, например добавление или удаление стоп-слов, переиндексации не требуют.
Конфигурирование тезауруса
Для определения нового тезауруса используется шаблон thesaurus. Например:
CREATE TEXT SEARCH DICTIONARY thesaurus_simple (
TEMPLATE = thesaurus,
DictFile = mythesaurus,
Dictionary = pg_catalog.english_stem
);
Здесь:
-
thesaurus_simple — это имя нового словаря
-
mythesaurus — это базовое имя файла конфигурации тезаурусы. (Полным именем будет $SHAREDIR/tsearch_data/mythesaurus.ths, где $SHAREDIR означает каталог разделяемых данных этой установки.)
-
pg_catalog.english_stem — это внутренний словарь (в данном случае это английский стеммер Snowball), который будет использоваться для нормализации тезауруса. Обратите внимание, что внутренний словарь будет иметь собственную конфигурацию (например, стоп-слова), которая здесь не показана.
Теперь можно связать тезаурус thesaurus_simple с желаемыми типами синтаксических единиц в конфигурации, например:
ALTER TEXT SEARCH CONFIGURATION russian
ALTER MAPPING FOR asciiword, asciihword, hword_asciipart
WITH thesaurus_simple;
Пример тезауруса
Рассмотрим простой астрономический тезаурус thesaurus_astro, содержащий несколько комбинаций астрономических терминов:
supernovae stars : sn
crab nebulae : crab
Ниже мы создаем словарь и привязываем некоторые типы синтаксических единиц к астрономическому тезаурусу и английскому стеммеру:
CREATE TEXT SEARCH DICTIONARY thesaurus_astro (
TEMPLATE = thesaurus,
DictFile = thesaurus_astro,
Dictionary = english_stem
);
ALTER TEXT SEARCH CONFIGURATION russian
ALTER MAPPING FOR asciiword, asciihword, hword_asciipart
WITH thesaurus_astro, english_stem;
Теперь можно посмотреть, как он работает. Функция ts_lexize не очень полезна для проверки тезауруса, поскольку она обрабатывает входное значение как одну синтаксическую единицу. Вместо нее можно вызвать функции plainto_tsquery и to_tsvector, которые разобьют свои входные данные на несколько синтаксических единиц:
SELECT plainto_tsquery('supernova star');
plainto_tsquery
-----------------
'sn'
SELECT to_tsvector('supernova star');
to_tsvector
-------------
'sn':1
В принципе, можно воспользоваться и to_tsquery, если заключить аргумент в кавычки:
SELECT to_tsquery('''supernova star''');
to_tsquery
------------
'sn'
Обратите внимание, что supernova star совпадает с supernovae stars в thesaurus_astro, поскольку в определении тезауруса мы указали стеммер english_stem. Этот стеммер удалил окончания e и s.
Чтобы проиндексировать исходное словосочетание вместе с его заменой, просто включите его в правую часть соответствующего определения:
supernovae stars : sn supernovae stars
SELECT plainto_tsquery('supernova star');
plainto_tsquery
-----------------------------
'sn' & 'supernova' & 'star'
Словарь Ispell
Шаблон словарей Ispell поддерживает морфологические словари, которые могут нормализовать множество разных лингвистических форм слова, сведя их к одной лексеме. Например, английский словарь Ispell может сопоставить все склонения и спряжения ключевого слова bank, например banking, banked, banks, banks' и bank's.
Стандартный дистрибутив QHB не включает файлы конфигурации Ispell. Словари для огромного числа языков можно загрузить со страницы Ispell. Кроме того, поддерживаются и некоторые другие современные форматы словарей — MySpell (OO < 2.0.1) и Hunspell (OO >= 2.0.2). Большой список словарей можно найти на странице OpenOffice Wiki.
Чтобы создать словарь Ispell, выполните следующие операции:
-
загрузите файлы конфигурации словаря. Пакет OpenOffice имеет расширение .oxt. Из него необходимо извлечь файлы .aff и .dic и поменять их расширения на .affix и .dict соответственно. Для некоторых файлов словарей также нужно преобразовать символы в кодировку UTF-8 с помощью команд (например, для словаря норвежского языка):
iconv -f ISO_8859-1 -t UTF-8 -o nn_no.affix nn_NO.aff iconv -f ISO_8859-1 -t UTF-8 -o nn_no.dict nn_NO.dic
-
скопируйте файлы в каталог $SHAREDIR/tsearch_data
-
загрузите файлы в QHB с помощью следующей команды:
CREATE TEXT SEARCH DICTIONARY english_hunspell ( TEMPLATE = ispell, DictFile = en_us, AffFile = en_us, Stopwords = english);
Здесь в параметрах DictFile, AffFile и StopWords задаются базовые имена файлов словаря, аффиксов и стоп-слов. Файл стоп-слов имеет тот же формат, что описывался выше для типа словаря simple. Формат остальных файлов здесь не описывается, но его можно посмотреть на указанных выше сайтах.
Словари Ispell обычно распознают ограниченный набор слов, поэтому за ними должен идти другой, более общий словарь, например Snowball, который распознает все.
Файл .affix для Ispell имеет такую структуру:
prefixes
flag *A:
. > RE # As in enter > reenter
suffixes
flag T:
E > ST # As in late > latest
[^AEIOU]Y > -Y,IEST # As in dirty > dirtiest
[AEIOU]Y > EST # As in gray > grayest
[^EY] > EST # As in small > smallest
А файл .dict — такую:
lapse/ADGRS
lard/DGRS
large/PRTY
lark/MRS
Формат файла .dict:
basic_form/affix_class_name
В файле .affix каждый флаг аффиксов описывается в следующем формате:
условие > [-отсекаемые_буквы,] добавляемый_аффикс
Здесь условие имеет формат, схожий с форматом регулярных выражений. В нем можно
использовать группирование [...]
и [^...]
. Например, [AEIOU]Y
означает, что
последняя буква слова "y", а предпоследняя может быть "a", "e", "i", "o"
или "u". [^EY]
означает, что последняя буква не "e" и "y".
Словари Ispell поддерживают разделение составных слов; полезная функциональность.
Обратите внимание, что в файле аффиксов нужно с помощью оператора compoundwords controlled
задать специальный флаг, помечающий словарные слова, которые могут
участвовать в составных образованиях:
compoundwords controlled z
Вот несколько примеров для норвежского языка:
SELECT ts_lexize('norwegian_ispell', 'overbuljongterningpakkmesterassistent');
{over,buljong,terning,pakk,mester,assistent}
SELECT ts_lexize('norwegian_ispell', 'sjokoladefabrikk');
{sjokoladefabrikk,sjokolade,fabrikk}
Формат MySpell является подмножеством формата Hunspell. Файл .affix словаря Hunspell имеет следующую структуру:
PFX A Y 1
PFX A 0 re .
SFX T N 4
SFX T 0 st e
SFX T y iest [^aeiou]y
SFX T 0 est [aeiou]y
SFX T 0 est [^ey]
Первая строка класса аффиксов — заголовок. Поля правил аффиксов перечисляются после этого заголовка:
-
имя параметра (PFX или SFX)
-
флаг (имя класса префиксов)
-
отсекаемые символы в начале (в префиксе) или в конце (в суффиксе) слова
-
добавляемый аффикс
-
условие в формате, схожем с форматом регулярных выражений.
Файл .dict похож на файл .dict словаря Ispell:
larder/M
lardy/RT
large/RSPMYT
largehearted
Примечание
Словарь MySpell не поддерживает составные слова. В словаре Hunspell имеется полноценная поддержка этих слов. В настоящее время QHB реализует только наиболее простые операции с составными словами из предлагаемых Hunspell.
Словарь Snowball
Шаблон словарей Snowball основан на проекте Мартина Портера, изобретателя популярного алгоритма стемминга для английского языка. Сейчас Snowball предлагает стемминговые алгоритмы и для многих других языков (подробную информацию см. на сайте Snowball). Каждый алгоритм понимает, как выделить основу слова (т. е. свести распространенные словоформы к начальной форме) в своем языке. Для словаря Snowball задается обязательный параметр language, определяющий, какой именно стеммер использовать, и может задаваться необязательный параметр stopword, где указывается имя файла со списком исключаемых слов. (Стандартные списки стоп-слов QHB тоже используются в проекте Snowball.) Например, имеется встроенное определение, равнозначное выполнению
CREATE TEXT SEARCH DICTIONARY english_stem (
TEMPLATE = snowball,
Language = english,
StopWords = english
);
Формат файла стоп-слов такой же, как у описанного выше.
Словарь Snowball распознает все, независимо от того, может он упростить слово или нет, поэтому его следует помещать в конце списка словарей. Ставить его перед другими словарями бесполезно, поскольку синтаксическая единица никогда не будет передана следующему словарю.
Пример конфигурации
В конфигурации текстового поиска задаются все параметры, необходимые для
преобразования документа в формат tsvector: синтаксический анализатор, который
будет разбивать текст на синтаксические единицы, и словари, которые будут
преобразовывать их в лексемы. При каждом вызове to_tsvector или to_tsquery
для выполнения операций требуется конфигурация текстового поиска. В параметре
конфигурации default_text_search_config задается имя конфигурации текстового
поиска по умолчанию, которая будет использоваться функциями текстового поиска,
если явный параметр конфигурации опущен. Этот параметр можно задать в
qhb.conf, или установить в рамках отдельного сеанса с помощью команды SET
.
В QHB доступны несколько предопределенных конфигураций текстового поиска, и вы можете с легкостью создать собственную конфигурацию. Для удобства управления объектами текстового поиска имеется набор команд SQL и несколько команд psql, выводящих информацию об объектах текстового поиска (раздел Поддержка psql).
В качестве примера мы создадим конфигурацию qhb, начав с копирования встроенной конфигурации english:
CREATE TEXT SEARCH CONFIGURATION public.qhb ( COPY = pg_catalog.english );
Мы будем использовать список синонимов, специфичных для QHB, который сохраним в файле $SHAREDIR/tsearch_data/qhb_dict.syn. Содержимое файла выглядит так:
QHBase qhb
QHB qhb
QuantumHybridBase qhb
Мы определяем словарь синонимов следующим образом:
CREATE TEXT SEARCH DICTIONARY qhb_dict (
TEMPLATE = synonym,
SYNONYMS = qhb_dict
);
Затем мы регистрируем словарь Ispell english_ispell, имеющий собственные файлы конфигурации:
CREATE TEXT SEARCH DICTIONARY english_ispell (
TEMPLATE = ispell,
DictFile = english,
AffFile = english,
StopWords = english
);
Теперь мы можем настроить сопоставления для слов в конфигурации qhb:
ALTER TEXT SEARCH CONFIGURATION qhb
ALTER MAPPING FOR asciiword, asciihword, hword_asciipart,
word, hword, hword_part
WITH qhb_dict, english_ispell, english_stem;
Мы решаем не индексировать и не искать некоторые типы синтаксических единиц, которые обрабатывает встроенная конфигурация:
ALTER TEXT SEARCH CONFIGURATION qhb
DROP MAPPING FOR email, url, url_path, sfloat, float;
Теперь мы можем протестировать нашу конфигурацию:
SELECT * FROM ts_debug('public.qhb', '
QHB, the highly scalable, SQL compliant, object-relational
database management system, is now undergoing beta testing of the next
version of our software.
');
Следующий шаг — настроить текущий сеанс на использование новой конфигурации, которая была создана в схеме public:
=> \dF
List of text search configurations
Schema | Name | Description
---------+------+-------------
public | qhb |
SET default_text_search_config = 'public.qhb';
SET
SHOW default_text_search_config;
default_text_search_config
----------------------------
public.qhb
Тестирование и отладка текстового поиска
Поведение пользовательской конфигурации текстового поиска с легкостью может стать запутанным. Функции, описываемые в этом разделе, полезны для тестирования объектов текстового поиска. Вы можете протестировать как конфигурацию целиком, так и отдельные синтаксические анализаторы и словари.
Тестирование конфигурации
Функция ts_debug позволяет с легкостью протестировать конфигурацию текстового поиска.
ts_debug([ конфигурация regconfig, ] документ text,
OUT псевдоним text,
OUT описание text,
OUT синтаксическая_единица text,
OUT словари regdictionary[],
OUT словарь regdictionary,
OUT лексемы text[])
returns setof record
ts_debug выводит информацию обо всех синтаксических единицах документа, выданных анализатором и обработанных сконфигурированными словарями. Она использует заданную конфигурацию или значение параметра default_text_search_config, если этот аргумент опущен.
ts_debug возвращает по одной строке для каждой синтаксической единицы, выявленной в тексте анализатором. Выводятся следующие столбцы:
-
псевдоним text — краткое имя типа синтаксической единицы
-
описание text — описание типа синтаксической единицы
-
синтаксическая_единица text — текст синтаксической единицы
-
словари regdictionary[] — словари, выбранные конфигурацией для этого типа синтаксической единицы
-
словарь regdictionary — словарь, распознавший эту синтаксическую единицу, или NULL, если ее не распознал ни один словарь
-
лексемы text[] — лексемы, выданные словарем, распознавшим синтаксическую единицу, или NULL, если ее не распознал ни один словарь; пустой массив ({}) означает, что единица была распознана как стоп-слово
Простой пример:
SELECT * FROM ts_debug('english', 'a fat cat sat on a mat - it ate a fat rats');
alias | description | token | dictionaries | dictionary | lexemes
-----------+-----------------+-------+----------------+--------------+---------
asciiword | Word, all ASCII | a | {english_stem} | english_stem | {}
blank | Space symbols | | {} | |
asciiword | Word, all ASCII | fat | {english_stem} | english_stem | {fat}
blank | Space symbols | | {} | |
asciiword | Word, all ASCII | cat | {english_stem} | english_stem | {cat}
blank | Space symbols | | {} | |
asciiword | Word, all ASCII | sat | {english_stem} | english_stem | {sat}
blank | Space symbols | | {} | |
asciiword | Word, all ASCII | on | {english_stem} | english_stem | {}
blank | Space symbols | | {} | |
asciiword | Word, all ASCII | a | {english_stem} | english_stem | {}
blank | Space symbols | | {} | |
asciiword | Word, all ASCII | mat | {english_stem} | english_stem | {mat}
blank | Space symbols | | {} | |
blank | Space symbols | - | {} | |
asciiword | Word, all ASCII | it | {english_stem} | english_stem | {}
blank | Space symbols | | {} | |
asciiword | Word, all ASCII | ate | {english_stem} | english_stem | {ate}
blank | Space symbols | | {} | |
asciiword | Word, all ASCII | a | {english_stem} | english_stem | {}
blank | Space symbols | | {} | |
asciiword | Word, all ASCII | fat | {english_stem} | english_stem | {fat}
blank | Space symbols | | {} | |
asciiword | Word, all ASCII | rats | {english_stem} | english_stem | {rat}
Для более полной демонстрации мы сначала создадим конфигурацию public.english и словарь Ispell для английского языка:
CREATE TEXT SEARCH CONFIGURATION public.english ( COPY = pg_catalog.english );
CREATE TEXT SEARCH DICTIONARY english_ispell (
TEMPLATE = ispell,
DictFile = english,
AffFile = english,
StopWords = english
);
ALTER TEXT SEARCH CONFIGURATION public.english
ALTER MAPPING FOR asciiword WITH english_ispell, english_stem;
SELECT * FROM ts_debug('public.english', 'The Brightest supernovaes');
alias | description | token | dictionaries | dictionary | lexemes
-----------+-----------------+-------------+-------------------------------+----------------+-------------
asciiword | Word, all ASCII | The | {english_ispell,english_stem} | english_ispell | {}
blank | Space symbols | | {} | |
asciiword | Word, all ASCII | Brightest | {english_ispell,english_stem} | english_ispell | {bright}
blank | Space symbols | | {} | |
asciiword | Word, all ASCII | supernovaes | {english_ispell,english_stem} | english_stem | {supernova}
В этом примере слово Brightest было распознано анализатором как слово из символов ASCII (псевдоним asciiword). Для этого типа синтаксических единиц список словарей включает english_ispell и english_stem. Слово было распознано english_ispell, который свел его к существительному bright. Слово supernovaes незнакомо словарю english_ispell поэтому оно было передано следующему словарю и, к счастью, было распознано (на самом деле english_stem является словарем Snowball, который распознает все; вот почему он был помещен в конец списка словарей).
Слово The было распознано словарем english_ispell как стоп-слово (подраздел Стоп-слова) и не будет индексироваться. Пробелы тоже отбрасываются, поскольку в этой конфигурации для них нет словарей.
Вы можете уменьшить ширину вывода, явно указав, какие столбцы вы хотите видеть:
SELECT alias, token, dictionary, lexemes
FROM ts_debug('public.english', 'The Brightest supernovaes');
alias | token | dictionary | lexemes
-----------+-------------+----------------+-------------
asciiword | The | english_ispell | {}
blank | | |
asciiword | Brightest | english_ispell | {bright}
blank | | |
asciiword | supernovaes | english_stem | {supernova}
Тестирование синтаксического анализатора
Следующие функции позволяют напрямую протестировать синтаксический анализатор текстового поиска.
ts_parse(имя_анализатора text, документ text,
OUT код_единицы integer, OUT синтаксическая_единица text) returns setof record
ts_parse(oid_анализатора oid, документ text,
OUT код_единицы integer, OUT синтаксическая_единица text) returns setof record
Функция ts_parse анализирует данный документ и возвращает последовательность записей, по одной для каждой синтаксической единицы, выделенной анализом. Каждая запись включает код_единицы, показывающий тип связанной синтаксической единицы и собственно синтаксическую_единицу в текстовом виде. Например:
SELECT * FROM ts_parse('default', '123 - a number');
tokid | token
-------+--------
22 | 123
12 |
12 | -
1 | a
12 |
1 | number
ts_token_type(имя_анализатора text, OUT код_единицы integer,
OUT псевдоним text, OUT описание text) returns setof record
ts_token_type(oid_анализатора oid, OUT код_единицы integer,
OUT псевдоним text, OUT описание text) returns setof record
Функция ts_token_type возвращает таблицу, описывающую все типы синтаксических единиц, которые может распознать заданный анализатор. Для каждого типа в таблице указывается целочисленный код_единицы, который анализатор использует для маркировки синтаксической единицы этого типа, псевдоним, с которым этот тип фигурирует в командах конфигурации, и краткое описание. Например:
SELECT * FROM ts_token_type('default');
tokid | alias | description
-------+-----------------+------------------------------------------
1 | asciiword | Word, all ASCII
2 | word | Word, all letters
3 | numword | Word, letters and digits
4 | email | Email address
5 | url | URL
6 | host | Host
7 | sfloat | Scientific notation
8 | version | Version number
9 | hword_numpart | Hyphenated word part, letters and digits
10 | hword_part | Hyphenated word part, all letters
11 | hword_asciipart | Hyphenated word part, all ASCII
12 | blank | Space symbols
13 | tag | XML tag
14 | protocol | Protocol head
15 | numhword | Hyphenated word, letters and digits
16 | asciihword | Hyphenated word, all ASCII
17 | hword | Hyphenated word, all letters
18 | url_path | URL path
19 | file | File or path name
20 | float | Decimal notation
21 | int | Signed integer
22 | uint | Unsigned integer
23 | entity | XML entity
Тестирование словаря
Функция ts_lexize обеспечивает тестирование словаря.
ts_lexize(словарь regdictionary, синтаксическая_единица text) returns text[]
ts_lexize возвращает массив лексем, если входная синтаксическая_единица известна словарю, или пустой массив, если единица известна словарю, но является стоп-словом, или NULL, если это незнакомое слово.
Примеры:
SELECT ts_lexize('english_stem', 'stars');
ts_lexize
-----------
{star}
SELECT ts_lexize('english_stem', 'a');
ts_lexize
-----------
{}
Примечание
Функция ts_lexize принимает одиночную синтаксическую единицу, а не текст. Вот пример возможной путаницы:SELECT ts_lexize('thesaurus_astro', 'supernovae stars') is null; ?column? ---------- t
Тезаурусу thesaurus_astro известно словосочетание supernovae stars, но функция ts_lexize не работает, поскольку она не анализирует входной текст, а воспринимает его как одну синтаксическую единицу. Поэтому для проверки тезаурусов воспользуйтесь функциями plainto_tsquery или to_tsvector, например:
SELECT plainto_tsquery('supernovae stars'); plainto_tsquery ----------------- 'sn'
Предпочтительные типы индексов для текстового поиска
Существует два вида индексов, которые можно применять для ускорения полнотекстового поиска: GIN и GiST. Обратите внимание, что индексы необязательны для выполнения полнотекстового поиска, но в случаях, когда по какому-либо столбцу регулярно проводится поиск, индексы обычно желательны.
Чтобы создать такой индекс, выполните одну из следующих команд:
CREATE INDEX имя ON таблица USING GIN (столбец);
Создает индекс на основе GIN (Generalized Inverted Index, обобщенный обратный индекс). Столбец должен иметь тип tsvector.
CREATE INDEX имя ON таблица USING GIST (столбец [ { DEFAULT | tsvector_ops } (siglen = число) ] );
Создает индекс на основе GiST (Generalized Search Tree, обобщенное дерево поиска). Столбец может иметь тип tsvector или tsquery. Необязательный целочисленный параметр siglen определяет длину сигнатуры в байтах (подробную информацию см. ниже).
Более предпочтительным типом для текстового поиска являются индексы GIN. Будучи обратными индексами, они содержат запись для каждого отдельного слова (лексемы) со сжатым списком их позиций. При поиске нескольких слов можно найти первое соответствие, а затем воспользоваться индексом, чтобы удалить строки, в которых отсутствуют дополнительные слова. Индексы GIN хранят только слова (лексемы) из значений tsvector, но не метки их весов. Таким образом, при выполнении запроса с весами требуется повторная проверка строк в таблице.
Индекс GiST является неточным, то есть он может выдавать ложные соответствия, и для их исключения нужно проверить фактические строки таблицы. (при необходимости QHB делает это автоматически.) Индексы GiST неточные, поскольку каждый документ представляется в них сигнатурой фиксированной длины. Длина сигнатуры в байтах определяется значением необязательного целочисленного параметра siglen. По умолчанию (когда siglen не указан) длина сигнатуры равна 124 байтам, а максимальная длина составляет 2024 байта. Сигнатура генерируется путем хеширования каждого слова в один бит в строке из n-бит, а затем логического объединения всех этих битов в n-битовую сигнатуру документа. Когда два слова хешируются в один бит, появляется ложное соответствие. Если для всех слов в запросе обнаруживаются соответствия (фактические или ложные), для проверки их корректности следует извлечь строку таблицы. При увеличении длины сигнатуры поиск становится более точным (сканируется меньшая часть индекса и меньше страниц кучи), но за счет увеличения размера самого индекса.
Индекс GiST может быть покрывающим, т. е. использовать предложение INCLUDE. Включаемые столбцы могут иметь типы данных, для которых отсутствует класс операторов GiST. Включаемые атрибуты будут сохраняться без сжатия.
Неточность вызывает снижение производительности вследствие лишних извлечений записей таблицы, оказавшихся ложными соответствиями. Поскольку произвольное обращение к записям таблицы происходит медленно, это ограничивает полезность индексов GiST. Вероятность ложных соответствий зависит от ряда факторов, в частности, от количества уникальных слов, поэтому для его уменьшения рекомендуется использовать словари.
Обратите внимание, что время построения индекса GIN можно сократить, увеличив значение maintenance_work_mem, тогда как на время построения индекса GiST этот параметр не влияет.
Партиционирование больших коллекций документов и правильное применение индексов GIN и GiST позволяют реализовать очень быстрый поиск с обновлением в режиме реального времени. Партиционирование можно проводить на уровне базы данных, используя наследование таблиц, либо распределяя документы по серверам и собирая внешние результаты поиска посредством, например, средствами доступа к сторонним данным. Второй вариант возможен благодаря тому, что функции ранжирования используют только локальную информацию.
Поддержка psql
Информацию об объектах конфигурации текстового поиска можно получить в psql, используя следующий набор команд:
\dF{d,p,t}[+] [ШАБЛОН]
При добавлении необязательного + выводятся более подробные сведения.
Необязательный параметр ШАБЛОН может быть именем объекта текстового поиска, возможно, дополненным схемой. Если ШАБЛОН опущен, будет отображаться информация обо всех видимых объектах. ШАБЛОН может быть регулярным выражением и предоставлять отдельные шаблоны для схемы и объекта. Это иллюстрируют следующие примеры:
=> \dF *fulltext*
List of text search configurations
Schema | Name | Description
--------+--------------+-------------
public | fulltext_cfg |
=> \dF *.fulltext*
List of text search configurations
Schema | Name | Description
----------+----------------------------
fulltext | fulltext_cfg |
public | fulltext_cfg |
Доступные команды:
\dF[+]
[ШАБЛОН
]
Список конфигураций текстового поиска (добавьте + для дополнительных сведений).
=> \dF russian
List of text search configurations
Schema | Name | Description
------------+---------+------------------------------------
pg_catalog | russian | configuration for russian language
=> \dF+ russian
Text search configuration "pg_catalog.russian"
Parser: "pg_catalog.default"
Token | Dictionaries
-----------------+--------------
asciihword | english_stem
asciiword | english_stem
email | simple
file | simple
float | simple
host | simple
hword | russian_stem
hword_asciipart | english_stem
hword_numpart | simple
hword_part | russian_stem
int | simple
numhword | simple
numword | simple
sfloat | simple
uint | simple
url | simple
url_path | simple
version | simple
word | russian_stem
\dFd[+]
[ШАБЛОН
]
Список словарей текстового поиска (добавьте + для дополнительных сведений).
=> \dFd
List of text search dictionaries
Schema | Name | Description
------------+-----------------+-----------------------------------------------------------
pg_catalog | arabic_stem | snowball stemmer for arabic language
pg_catalog | armenian_stem | snowball stemmer for armenian language
pg_catalog | basque_stem | snowball stemmer for basque language
pg_catalog | catalan_stem | snowball stemmer for catalan language
pg_catalog | danish_stem | snowball stemmer for danish language
pg_catalog | dutch_stem | snowball stemmer for dutch language
pg_catalog | english_stem | snowball stemmer for english language
pg_catalog | finnish_stem | snowball stemmer for finnish language
pg_catalog | french_stem | snowball stemmer for french language
pg_catalog | german_stem | snowball stemmer for german language
pg_catalog | greek_stem | snowball stemmer for greek language
pg_catalog | hindi_stem | snowball stemmer for hindi language
pg_catalog | hungarian_stem | snowball stemmer for hungarian language
pg_catalog | indonesian_stem | snowball stemmer for indonesian language
pg_catalog | irish_stem | snowball stemmer for irish language
pg_catalog | italian_stem | snowball stemmer for italian language
pg_catalog | lithuanian_stem | snowball stemmer for lithuanian language
pg_catalog | nepali_stem | snowball stemmer for nepali language
pg_catalog | norwegian_stem | snowball stemmer for norwegian language
pg_catalog | portuguese_stem | snowball stemmer for portuguese language
pg_catalog | romanian_stem | snowball stemmer for romanian language
pg_catalog | russian_stem | snowball stemmer for russian language
pg_catalog | serbian_stem | snowball stemmer for serbian language
pg_catalog | simple | simple dictionary: just lower case and check for stopword
pg_catalog | spanish_stem | snowball stemmer for spanish language
pg_catalog | swedish_stem | snowball stemmer for swedish language
pg_catalog | tamil_stem | snowball stemmer for tamil language
pg_catalog | turkish_stem | snowball stemmer for turkish language
pg_catalog | yiddish_stem | snowball stemmer for yiddish language
\dFp[+]
[ШАБЛОН
]
Список синтаксических анализаторов текстового поиска (добавьте + для дополнительных сведений).
=> \dFp
List of text search parsers
Schema | Name | Description
------------+---------+---------------------
pg_catalog | default | default word parser
=> \dFp+
Text search parser "pg_catalog.default"
Method | Function | Description
-----------------+----------------+-------------
Start parse | prsd_start |
Get next token | prsd_nexttoken |
End parse | prsd_end |
Get headline | prsd_headline |
Get token types | prsd_lextype |
Token types for parser "pg_catalog.default"
Token name | Description
-----------------+------------------------------------------
asciihword | Hyphenated word, all ASCII
asciiword | Word, all ASCII
blank | Space symbols
email | Email address
entity | XML entity
file | File or path name
float | Decimal notation
host | Host
hword | Hyphenated word, all letters
hword_asciipart | Hyphenated word part, all ASCII
hword_numpart | Hyphenated word part, letters and digits
hword_part | Hyphenated word part, all letters
int | Signed integer
numhword | Hyphenated word, letters and digits
numword | Word, letters and digits
protocol | Protocol head
sfloat | Scientific notation
tag | XML tag
uint | Unsigned integer
url | URL
url_path | URL path
version | Version number
word | Word, all letters
(23 rows)
\dFt[+]
[ШАБЛОН
]
Список шаблонов текстового поиска (добавьте + для дополнительных сведений).
=> \dFt
List of text search templates
Schema | Name | Description
------------+-----------+-----------------------------------------------------------
pg_catalog | ispell | ispell dictionary
pg_catalog | simple | simple dictionary: just lower case and check for stopword
pg_catalog | snowball | snowball stemmer
pg_catalog | synonym | synonym dictionary: replace word by its synonym
pg_catalog | thesaurus | thesaurus dictionary: phrase by phrase substitution
Ограничения
Функциональность текстового поиска в QHB имеет следующие ограничения:
-
Длина каждой лексемы должна быть меньше 2 килобайт
-
Длина значения tsvector (лексемы + их позиции) должна быть меньше 1 мегабайта
-
Количество лексем должно быть меньше 264
-
Значения позиций в tsvector должны находиться в диапазоне от 0 до 16 383 (включая верхнюю границу)
-
Расстояние между вхождениями соответствий в операторе
<N>
(ПРЕДШЕСТВУЕТ) типа tsquery не может превышать 16 384 -
Не больше 256 позиций для одной лексемы
-
Количество узлов (лексемы + операторы) в значении tsquery должно быть меньше 32 768