12.3. Управление текстовым поиском
Для реализации полнотекстового поиска необходимы функции, позволяющие создать tsvector
из документа и tsquery
из запроса пользователя. Кроме того, результаты нужно выдавать в удобном порядке, так что нам потребуется функция, оценивающая релевантность документа для данного запроса. Важно также иметь возможность выводить найденный текст подходящим образом. В PostgreSQL есть все необходимые для этого функции.
12.3.1. Разбор документов
Для преобразования документа в тип tsvector
PostgreSQL предоставляет функцию to_tsvector
.
to_tsvector([конфигурация
regconfig
,]документ
text
) returnstsvector
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
внутри вызывает анализатор, который разбивает текст документа на фрагменты и классифицирует их. Для каждого фрагмента она проверяет список словарей (Раздел 12.6), определяемый типом фрагмента. Первый же словарь, распознавший фрагмент, выдаёт одну или несколько представляющих его лексем. Например, rats
превращается в rat
, так как один из словарей понимает, что слово rats
— это слово rat
во множественном числе. Некоторое слова распознаются как стоп-слова (Подраздел 12.6.1) и игнорируются как слова, фигурирующие в тексте настолько часто, что искать их бессмысленно. В нашем примере это a
, on
и it
. Если фрагмент не воспринимается ни одним словарём из списка, он так же игнорируется. В данном примере это происходит со знаком препинания -
, так как с таким типом фрагмента (символы-разделители
) не связан никакой словарь и значит такие фрагменты никогда не будут индексироваться. Выбор анализатора, словарей и индексируемых типов фрагментов определяется конфигурацией текстового поиска (Раздел 12.7). В одной базе данных можно использовать разные конфигурации, в том числе, предопределённые конфигурации для разных языков. В нашем примере мы использовали конфигурацию по умолчанию для английского языка — english
.
Для назначения элементам tsvector
разных весов используется функция setweight
. Вес элемента задаётся буквой A
, B
, C
или D
. Обычно это применяется для обозначения важности слов в разных частях документа, например в заголовке или в теле документа. Затем эта информация может использоваться при ранжировании результатов поиска.
Так как 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
||
. (Подробнее эти операции рассматриваются в Подразделе 12.4.1.)
12.3.2. Разбор запросов
Для преобразования запросов в тип tsquery
в PostgreSQL реализованы функции to_tsquery
и plainto_tsquery
. Функция to_tsquery
более мощная, чем plainto_tsquery
, но и более требовательная к входным данным.
to_tsquery([конфигурация
regconfig
,]текст_запроса
text
) returnstsquery
to_tsquery
создаёт значение tsquery
из текста_запроса
, который может состоять из простых фрагментов, разделённых логическими операторами &
(AND), |
(OR) и !
(NOT). Эти операторы могут быть заключены в скобки. Другими словами, входное значение для to_tsquery
должно уже соответствовать общим правилам для значений tsquery
, описанным в Разделе 8.11. Различие их состоит в том, что во вводимом в 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
не примет фрагменты, не разделённые операторами AND и OR, и выдаст синтаксическую ошибку.
plainto_tsquery([конфигурация
regconfig
,]текст_запроса
text
) returnstsquery
plainto_tsquery
преобразует неформатированный текст_запроса
в значение tsquery
. Текст разбирается и нормализуется подобно тому, как это делает to_tsvector
, а затем между оставшимися словами вставляются логические операторы &
(AND).
Пример:
SELECT plainto_tsquery('english', 'The Fat Rats'); plainto_tsquery ----------------- 'fat' & 'rat'
Заметьте, что plainto_tsquery
не распознаёт во входной строке логические операторы, метки весов или обозначения префиксов:
SELECT plainto_tsquery('english', 'The Fat & Rats:C'); plainto_tsquery --------------------- 'fat' & 'rat' & 'c'
В данном случае все знаки пунктуации были отброшены как символы-разделители.
12.3.3. Ранжирование результатов поиска
Ранжирование документов можно представить как попытку оценить, насколько они релевантны заданному запросу и отсортировать их так, чтобы наиболее релевантные выводились первыми. В PostgreSQL встроены две функции ранжирования, принимающие во внимание лексическую, позиционную и структурную информацию; то есть, они учитывают, насколько часто и насколько близко встречаются в документе ключевые слова и какова важность содержащей их части документа. Однако само понятие релевантности довольно размытое и во многом определяется приложением. Приложения могут использовать для ранжирования и другую информацию, например, время изменения документа. Встроенные функции ранжирования можно рассматривать лишь как примеры реализации. Для своих конкретных задач вы можете разработать собственные функции ранжирования и/или учесть при обработке их результатов дополнительные факторы.
Ниже описаны две встроенные функции ранжирования:
-
ts_rank([
веса
float4[]
,]вектор
tsvector
,запрос
tsquery
[,нормализация
integer
]) returnsfloat4
Ранжирует векторы по частоте найденных лексем.
-
ts_rank_cd([
веса
float4[]
,]вектор
tsvector
,запрос
tsquery
[,нормализация
integer
]) returnsfloat4
Эта функция вычисляет плотность покрытия для данного вектора документа и запроса, используя метод, разработанный Кларком, Кормаком и Тадхоуп и описанный в статье "Relevance Ranking for One to Three Term Queries" в журнале "Information Processing and Management" в 1999 г. Плотность покрытия вычисляется подобно рангу
ts_rank
, но в расчёт берётся ещё и близость соответствующих лексем друг к другу.Для вычисления результата этой функции требуется информация о позиции лексем. Поэтому она игнорируют «очищенные» от этой информации лексемы в
tsvector
. Если во входных данных нет неочищенных лексем, результат будет равен нулю. (За дополнительными сведениями о функцииstrip
и позиционной информации в данныхtsvector
обратитесь к Подразделу 12.4.1.)
Для обеих этих функций аргумент веса
позволяет придать больший или меньший вес словам, в зависимости от их меток. В передаваемом массиве весов определяется, насколько весома каждая категория слов, в следующем порядке:
{вес D, вес C, вес B, вес A}
Если этот аргумент опускается, подразумеваются следующие значения:
{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)
), можно свести все ранги к диапазону 0..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
каждого подходящего документа и это займёт значительное время, если придётся обращаться к диску. К сожалению, избежать этого вряд ли возможно, так как на практике по многим запросам выдаётся большое количество результатов.
12.3.4. Выделение результатов
Представляя результаты поиска, в идеале нужно выделять часть документа и показывать, как он связан с запросом. Обычно поисковые системы показывают фрагменты документа с отмеченными искомыми словами. В PostgreSQL для реализации этой возможности представлена функция ts_headline
.
ts_headline([конфигурация
regconfig
,]документ
text
,запрос
tsquery
[,параметры
text
]) returnstext
ts_headline
принимает документ вместе с запросом и возвращает выдержку из документа, в которой выделяются слова из запроса. Применяемую для разбора документа конфигурацию можно указать в параметре config
; если этот параметр опущен, применяется конфигурация default_text_search_config
.
Если в параметрах передаётся строка options
, она должна состоять из списка разделённых запятыми пар параметр
=
значение
. Параметры могут быть следующими:
StartSel
,StopSel
: строки, которые будут разграничивать слова запроса в документе, выделяя их среди остальных. Если эти строки содержат пробелы или запятые, их нужно заключить в кавычки.MaxWords
,MinWords
: эти числа определяет нижний и верхний предел размера выдержки.ShortWord
: слова такой длины или короче в начале и конце выдержки будут отбрасываться. Значение по умолчанию, равное 3, исключает распространённые английские артикли.HighlightAll
: логический флаг; если он равенtrue
, выдержкой будет весь документ и три предыдущие параметра игнорируются.MaxFragments
: максимальное число выводимых текстовых выдержек или фрагментов. Значение по умолчанию, равное 0, выбирает метод создания выдержки без фрагментов. При значении большем 0 выбирается метод с фрагментами, когда находятся все фрагменты, содержащие как можно больше слов запроса, а затем они сжимаются до слов запроса. Такие фрагменты могут содержать какие-то ключевые слова в середине и ограничиваются двумя искомыми словами. При этом фрагменты могут содержать не большеMaxWords
слов, а в начале и конце они будут очищены от слов длиныShortWord
и меньше. Если в документе найдены не все слова запроса, выводится один фрагмент, включающий первыеMinWords
слов в документе.FragmentDelimiter
: Когда выводятся несколько фрагментов, они будут разделяться этой строкой.
Все явно не определённые параметры получают такие значения по умолчанию:
StartSel=<b>, StopSel=</b>, MaxWords=35, MinWords=15, ShortWord=3, HighlightAll=FALSE, MaxFragments=0, FragmentDelimiter=" ... "
Пример использования:
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('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', '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('query & similarity'), 'StartSel = <, StopSel = >'); ts_headline ------------------------------------------------------- containing given <query> terms and return them in order of their <similarity> to the <query>.
Функция ts_headline
работает с оригинальным документом, а не с его сжатым представлением tsvector
, так что она может быть медленной и использовать её следует осмотрительно. Типичная ошибка — вызывать ts_headline
для всех подходящих документов, когда показываются только десять. Правильный подход можно реализовать, применив подзапросы SQL, например так:
SELECT id, ts_headline(body, q), rank FROM (SELECT id, body, q, ts_rank_cd(ti, q) AS rank FROM apod, to_tsquery('stars') q WHERE ti @@ q ORDER BY rank DESC LIMIT 10) AS foo;