12.3. Управление полнотекстовым поиском#

12.3. Управление полнотекстовым поиском

12.3. Управление полнотекстовым поиском

Чтобы реализовать полнотекстовый поиск, необходимо иметь функцию для создания tsvector из документа и tsquery из запроса пользователя. Также нам нужна функция, которая сравнивает документы с учетом их релевантности для запроса, чтобы вернуть результаты в полезном порядке. Важно также уметь красиво отображать результаты. Tantor SE предоставляет поддержку всех этих функций.

12.3.1. Разбор документов

Tantor SE предоставляет функцию to_tsvector для преобразования документа в тип данных tsvector.

to_tsvector([ config regconfig, ] document 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 вызывает внутренний парсер, который разбивает текст документа на компоненты и назначает каждому компоненту тип. Для каждого компонента консультируется список словарей (Раздел 12.6), который может варьироваться в зависимости от типа компонента. Первый словарь, который распознает компонент, генерирует один или несколько нормализованных лексем для представления компонента. Например, слово rats становится rat, потому что один из словарей распознал, что слово rats является множественным числом слова rat. Некоторые слова распознаются как стоп-слова (Раздел 12.6.1), что приводит к их игнорированию, так как они встречаются слишком часто, чтобы быть полезными при поиске. В нашем примере это слова a, on, и it. Если ни один словарь из списка не распознает компонент, то он также игнорируется. В этом примере это произошло с знаком пунктуации -, так как на самом деле нет словарей, назначенных для его типа компонента (Space symbols), что означает, что пробельные компоненты никогда не будут индексироваться. Выбор парсера, словарей и типов компонентов для индексации определяется выбранной конфигурацией текстового поиска (Раздел 12.7). В одной базе данных может быть много разных конфигураций, и предопределенные конфигурации доступны для разных языков. В нашем примере мы использовали конфигурацию по умолчанию english для английского языка.

Функция setweight может быть использована для пометки записей tsvector с заданным весом, где весом может быть одна из букв 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. Разбор запросов

Tantor SE предоставляет функции to_tsquery, plainto_tsquery, phraseto_tsquery и websearch_to_tsquery для преобразования запроса в тип данных tsquery. Функция to_tsquery предлагает больше возможностей, чем функции plainto_tsquery и phraseto_tsquery, но она более строга в отношении своего ввода. Функция websearch_to_tsquery является упрощенной версией функции to_tsquery с альтернативным синтаксисом, подобным тому, который используется поисковыми системами веб-страниц.

to_tsquery([ config regconfig, ] querytext text) returns tsquery

to_tsquery создает значение tsquery из querytext, который должен состоять из отдельных компонентов, разделенных операторами 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 будет генерировать синтаксическую ошибку для компонентов, которые не разделены оператором AND, OR или FOLLOWED BY.

plainto_tsquery([ config regconfig, ] querytext text) returns tsquery

plainto_tsquery преобразует неформатированный текст querytext в значение tsquery. Текст анализируется и нормализуется так же, как и для функции to_tsvector, затем оператор & (AND) 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([ config regconfig, ] querytext text) returns tsquery

phraseto_tsquery ведет себя почти так же, как и plainto_tsquery, за исключением того, что он вставляет оператор <-> (FOLLOWED BY) между оставшимися словами вместо оператора & (AND). Кроме того, стоп-слова не просто отбрасываются, а учитываются путем вставки операторов <N> вместо операторов <->. Эта функция полезна при поиске точных последовательностей лексем, так как операторы FOLLOWED BY проверяют порядок лексем, а не только наличие всех лексем.

Пример:

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([ config regconfig, ] querytext text) returns tsquery

websearch_to_tsquery создает значение tsquery из querytext с использованием альтернативного синтаксиса, в котором простой неформатированный текст является допустимым запросом. В отличие от функций plainto_tsquery и phraseto_tsquery, она также распознает определенные операторы. Кроме того, эта функция никогда не вызывает синтаксические ошибки, что позволяет использовать необработанный пользовательский ввод для поиска. Поддерживается следующий синтаксис:

  • unquoted text: текст, без кавычек будет преобразован в термины, разделенные операторами &, как если бы он был обработан функцией plainto_tsquery.

  • "quoted text": текст внутри кавычек будет преобразован в термины, разделенные операторами <->, как если бы они были обработаны функцией 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)

12.3.3. Ранжирование результатов поиска

Ранжирование пытается измерить, насколько релевантны документы для конкретного запроса, чтобы при наличии множества совпадений наиболее релевантные могли быть показаны первыми. Tantor SE предоставляет две предопределенные функции ранжирования, которые учитывают лексическую, пространственную и структурную информацию; то есть они учитывают, как часто термины запроса появляются в документе, насколько близко друг к другу находятся термины в документе и насколько важна часть документа, где они встречаются. Однако понятие релевантности является нечетким и очень зависит от конкретного приложения. Различные приложения могут требовать дополнительной информации для ранжирования, например, времени модификации документа. Встроенные функции ранжирования являются только примерами. Вы можете написать свои собственные функции ранжирования и/или комбинировать их результаты с дополнительными факторами, чтобы соответствовать вашим конкретным потребностям.

Две функции ранжирования, доступные в настоящее время, это:

ts_rank([ weights float4[], ] vector tsvector, query tsquery [, normalization integer ]) returns float4

Ранжирует векторы на основе частоты совпадения их лексем.

ts_rank_cd([ weights float4[], ] vector tsvector, query tsquery [, normalization integer ]) returns float4

Эта функция вычисляет ранжирование плотности покрытия для данного вектора документа и запроса, как описано в статье Кларка, Кормака и Тудхоупа "Ранжирование релевантности для запросов от одного до трех терминов" в журнале "Information Processing and Management", 1999 года. Плотность покрытия похожа на ранжирование ts_rank, за исключением того, что учитывается близость совпадающих лексем друг к другу.

Эта функция требует информации о позиции лексем для выполнения своего вычисления. Поэтому она игнорирует любые удаленные лексемы в tsvector. Если во входных данных нет непроанализированных лексем, результат будет равен нулю. (См. Раздел 12.4.1 для получения дополнительной информации о функции strip и позиционной информации в tsvectors).

Для обоих этих функций, необязательный аргумент weights предлагает возможность взвешивать экземпляры слов более или менее сильно в зависимости от их маркировки. Массивы весов указывают, насколько сильно взвешивать каждую категорию слов, в следующем порядке:

{D-weight, C-weight, B-weight, A-weight}

Если не предоставлены weights, то используются следующие значения по умолчанию:

{0.1, 0.2, 0.4, 1.0}

Обычно веса используются для отметки слов из специальных областей документа, таких как заголовок или начальный абстракт, чтобы они могли быть рассмотрены с большей или меньшей важностью, чем слова в теле документа.

Поскольку в длинном документа вероятнее найти искомый термин, разумно учитывать размер документа, например, сто слов документа с пятью вхождениями искомого слова, вероятно, более релевантен, чем тысяча слов документа с пятью вхождениями. Обе функции ранжирования принимают целочисленный параметр normalization, который определяет, должна ли и каким образом длина документа влиять на его ранг. Целочисленный параметр управляет несколькими поведениями, поэтому он является битовой маской: вы можете указать одно или несколько поведений, используя | (например, 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 каждого соответствующего документа, что может быть связано с операциями ввода-вывода и, следовательно, замедлить процесс. К сожалению, практически невозможно избежать этого, так как обычно запросы приводят к большому количеству совпадений.

12.3.4. Выделение результатов

Для представления результатов поиска идеально показывать часть каждого документа и как он связан с запросом. Обычно поисковые системы показывают фрагменты документа с выделенными поисковыми терминами. Tantor SE предоставляет функцию ts_headline, которая реализует эту функциональность.

ts_headline([ config regconfig, ] document text, query tsquery [, options text ]) returns text

ts_headline принимает документ вместе с запросом и возвращает отрывок из документа, в котором выделены термины из запроса. Конфигурация, которая будет использоваться для разбора документа, может быть указана с помощью config; если config не указан, используется конфигурация default_text_search_config.

Если указана строка options, она должна состоять из списка, разделенного запятыми, из одной или нескольких пар option=value. Доступные варианты:

  • MaxWords, MinWords (целые числа): эти числа определяют самые длинные и самые короткие заголовки для вывода. Значения по умолчанию - 35 и 15.

  • ShortWord (integer): слова этой длины или меньше будут удалены в начале и конце заголовка, если они не являются поисковыми терминами. Значение по умолчанию - три, что исключает общие английские артикли.

  • HighlightAll (boolean): если true, весь документ будет использоваться в качестве заголовка, игнорируя предыдущие три параметра. По умолчанию false.

  • MaxFragments (целое число): максимальное количество текстовых фрагментов для отображения. Значение по умолчанию - ноль, что выбирает метод генерации заголовков без фрагментации. Значение больше нуля выбирает метод генерации заголовков с фрагментацией (см. ниже).

  • StartSel, StopSel (строки): строки, которыми отделяются слова запроса, появляющиеся в документе, чтобы отличить их от других слов. Значения по умолчанию - <b> и </b>, которые могут быть подходящими для вывода в HTML.

  • FragmentDelimiter (строка): Когда отображается более одного фрагмента, они будут разделены этой строкой. По умолчанию используется ... .

Эти имена опций распознаются без учета регистра. Вы должны заключать строковые значения в двойные кавычки, если они содержат пробелы или запятые.

В генерации заголовков, не основанной на фрагментах, функция ts_headline находит совпадения для заданного запроса query и выбирает одно из них для отображения, предпочитая совпадения, содержащие больше слов запроса в пределах допустимой длины заголовка. В генерации заголовков на основе фрагментов функция 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 сводку, поэтому он может работать медленно и должен использоваться с осторожностью.