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. Разбор запросов
PostgreSQL предоставляет функции to_tsquery
, plainto_tsquery
, phraseto_tsquery
и websearch_to_tsquery
для приведения запроса к типу tsquery
. Функция to_tsquery
даёт больше возможностей, чем plainto_tsquery
или phraseto_tsquery
, но более строга к входным данным. Функция websearch_to_tsquery
представляет собой упрощённую версию to_tsquery
с альтернативным синтаксисом, подобным тому, что принят в поисковых системах в Интернете.
to_tsquery([конфигурация
regconfig
,]текст_запроса
text
) returnstsquery
to_tsquery
создаёт значение tsquery
из текста_запроса
, который может состоять из простых фрагментов, разделённых логическими операторами tsquery
: &
(И), |
(ИЛИ), !
(НЕ) и <->
(ПРЕДШЕСТВУЕТ), возможно, сгруппированных скобками. Другими словами, входное значение для to_tsquery
должно уже соответствовать общим правилам для значений tsquery
, описанным в Подразделе 8.11.2. Различие их состоит в том, что во вводимом в 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
) returnstsquery
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
) returnstsquery
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
) returnstsquery
Функция websearch_to_tsquery
создаёт значение tsquery
из текста_запроса
, используя альтернативный синтаксис, в котором запрос задаётся просто неформатированным текстом. В отличие от plainto_tsquery
и phraseto_tsquery
, она также принимает определённые операторы. Более того, эта функция никогда не выдаёт синтаксические ошибки, что позволяет осуществлять поиск по произвольному заданному пользователем запросу. Она поддерживает следующий синтаксис:
текст не в кавычках
: текст, не заключённый в кавычки, который будет преобразован в слова, разделяемые операторами&
, как его восприняла бы функцияplainto_tsquery
."текст в кавычках"
: текст, заключённый в кавычки, будет преобразован в слова, разделяемые операторами<->
, как его восприняла бы функцияphraseto_tsquery
.OR
: слово «or» будет преобразовано в оператор|
.-
: знак минуса будет преобразован в оператор!
.
Другая пунктуация игнорируется. Поэтому функция websearch_to_tsquery
, как и plainto_tsquery
с phraseto_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)
12.3.3. Ранжирование результатов поиска
Ранжирование документов можно представить как попытку оценить, насколько они релевантны заданному запросу и отсортировать их так, чтобы наиболее релевантные выводились первыми. В PostgreSQL встроены две функции ранжирования, принимающие во внимание лексическую, позиционную и структурную информацию; то есть, они учитывают, насколько часто и насколько близко встречаются в документе ключевые слова и какова важность содержащей их части документа. Однако само понятие релевантности довольно размытое и во многом определяется приложением. Приложения могут использовать для ранжирования и другую информацию, например, время изменения документа. Встроенные функции ранжирования можно рассматривать лишь как примеры реализации. Для своих конкретных задач вы можете разработать собственные функции ранжирования и/или учесть при обработке их результатов дополнительные факторы.
Ниже описаны две встроенные функции ранжирования:
-
ts_rank([
веса
float4[]
,]вектор
tsvector
,query
tsquery
[,нормализация
integer
]) returnsfloat4
Ранжирует векторы по частоте найденных лексем.
-
ts_rank_cd([
веса
float4[]
,]вектор
tsvector
,query
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
,query
tsquery
[,параметры
text
]) returnstext
ts_headline
принимает документ вместе с запросом и возвращает выдержку из документа, в которой выделяются слова из запроса. Применяемую для разбора документа конфигурацию можно указать в параметре config
; если этот параметр опущен, применяется конфигурация default_text_search_config
.
Если в параметрах передаётся строка options
, она должна состоять из списка разделённых запятыми пар параметр
=
значение
. Параметры могут быть следующими:
MaxWords
,MinWords
(целочисленные): эти числа определяют нижний и верхний предел размера выдержки. Значения по умолчанию: 35 и 15, соответственно.ShortWord
(целочисленный): слова такой длины или короче в начале и конце выдержки будут отбрасываться, за исключением искомых слов. Значение по умолчанию, равное 3, исключает распространённые английские артикли.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
, так что она может быть медленной и использовать её следует осмотрительно.