Сделать полнотекстовый поиск управляемым и удобным

Необходимы перепосты и переводы на другие языки. Газета "Компьютерные вести" kv.by/index2010213801.htm Издание 2-е, исправленное. Другие статьи (оглавление)


Когда мы набираем в Google или Yandex, к примеру, "Чань-буддизм иерархия язык", то вместе со ссылками получаем некоторый комментарий, например, "Иерархия деструктивных средств чань-буддийского языка", в комментарии сервер выделяет слова, которые нашел. Конечно, сервер не ищет в интернете - он ищет в своей базе данных. Но запросы в базу с выделением найденного нужны не только ему, но и нам, обычным пользователям. Роль "случайных" пользователей СУБД с каждым днем все возрастает, фактически мы составляем уже большинство. А простому смертному реализовать такое крайне сложно. В коммерческих СУБД не воплощают, а в бесплатных несут такую околесицу, что с нормальными когнитивными способностями и применить невозможно, см. например неудачную попытку в Postgres. Воистину очень печально отсутствие в каких-либо намерений по поддержке работы не-программистов. Конечно, совсем нелегко найти простой и элегантный синтаксис для того, чтобы указать сколько слов до и после найденных надо вывести, какими тегами обрамлять найденные слова, в т.ч. в зависимости от того, в какой части строки они найдены, какие и сколько вариантов перестановок найденных слов выводить. Однако автор хотел бы предпринять такую попытку и услышать комментарии общественности, насколько ему это удалось. Несколько слайдов, иллюстрирующих идеи, вы можете видеть в sql5.19.3.pdf на с.188-194. Предложения дополняют статью "Систематика и обобщение методологий моделирования и языков программирования"

Вложенные поля

Пусть у нас есть таблица 's' с полями 'pk' (в ней первичный ключ), 's1' и 's2', единственная запись которой содержит

1, 10, "In the morning, dog comes, cat comes home too. Continue in the NEXT issue."

Вообразим, что строка разбита на слова, а слова хранятся в некоторой таблице с колонками @TOKEN (само слово),@SN (порядковый номер слова в поле), @BEGINNING (смещение первой буквы слова), @END (смещение последней буквы слова).

@TOKEN@SN@BEGINNING@END
In112
the246
morning3814
dog41719
comes52124
cat62729
comes73134
home83639
too94143
Continue104653
in115556
the125860
NEXT136265
issue146771

Пусть эта таблица вложена в текстовое поле 's2' (мы такое вообразим, т.е. это nested table), и мы синтаксически имеем доступ к этим колонкам как к s2.@TOKEN, s2.@SN, s2.@BEGINNING, s2.@END. Будем называть их вложенными колонками (nested columns). Упоминание их после WHERE позволяет гибко формулировать условия для полнотекстового поиска. Т.е. в подзапросах они ведут себя как колонки, но во внешний мир СУБД возвращает их как обычное поле под именем 's2', образованное агрегатом конкатенации, прошедшимся по вложенной колонке. Это позволяет автоматически видеть результаты поиска как текстовую строку. Например, вывод поля s2.@SN возвращает строку, состоящую из порядковых номеров найденных слов, а не из самих слов; поля s2.@BEGINNING - из смещений первых букв слов, s2.@END - из смещений последних букв слов. При конкатенации гарантируется, что Чтобы обрамить то, что пожелаем, тегами-константами, операции с вложенными колонками обладают следующими свойствами Для полноты картины отметим также вопрос, остающийся за пределами настоящей статьи, а именно возможность изменять текстовые поля с помощью SQL, не прибегая в СУБД к громаде строковых функций, например

Несколько совпадений

Даже в одной строке может быть найдено несколько образцов, а значит каждая запись может быть возвращена несколько раз: будем называть этот процесс размножением (propagation), а записи, порожденные из одной - порожденной группой (propagated group). Чтобы в клиентской программе выбрать конкретный экземпляр группы, СУБД автоматически добавляет целочисленное поле SYS_CLUE, содержащее уникальный идентификатор записи внутри группы. В разных группах поле может содержать одинаковые значения, неоднозначность разрешает первичный ключ записей. Например, запрос слов из множества "comes next", который выводит найденные, а также и по одному слову слева и справа от них

SELECT s1, s2.@TOKEN
FROM   s
WHERE  s2.@SN IN (
  SELECT s2.@SN
  FROM   s, (
    SELECT s2.@SN AS fn
    FROM   s
    WHERE  s2.@TOKEN IN "comes next"
            )
  WHERE  ABS(s2.@SN-fn) <= 1
                 );
находит два образца и возвращает две записи (для строки "comes next" автоматически генерируется её вложенная таблица как только строка введена в СУБД, эту вложенную таблицу и использует оператор IN)

s1s2SYS_CLUE
10dog comes, cat ... the NEXT issue1
10cat comes home ... the NEXT issue2

Можно указать перестановку слов с помощью 'WHERE s2.@TOKEN =~ "come next" ' (подробнее о перестановке на с.180-183 вышеупомянутого pdf-документа), в т.ч. с ограничением количества перестановок 'WHERE s2.@TOKEN TO "come next" PERMUTATIONS <=2', результаты всегда выдаются, начиная с наименьшего количества перестановок, в направлении возрастания количества.

И опять же для полноты картины определимся, что комбинация полнотекстовых поисков обладает следующими свойствами

Обрамление тегами

Чтобы в момент вывода слов во внешний мир проводить с ними различные операции, в т.ч. чтобы обрамлять разными тегами, достаточно давать алиасы аргументам функций. Тогда например, запрос, возвращающий найденные слова обрамленными тегами <b> и </b>, по одному слову слева и справа от них - тегами <em> и </em>, а все остальные слова между найденными без обрамления, выглядит как показано ниже. Чтобы читателю легче было визуально сгруппировать @TOKEN, относящиеся к разным подзапросам, автор выделил некоторые из них строчными буквами; а также для удобства чтения перенес DISTINCT от нескольких аргументов под WHERE - таков уж был убогий синтаксис современных СУБД. DISTINCT нужен ввиду декартового произведения колонок '@sn' (соответствующих @token) и 'fn'.

SELECT s1, ("<b>" +s2.@TOKEN AS f1 +"</b>" ) +
           ("<em>"+s2.@TOKEN AS f2 +"</em>") +
           (       s2.@TOKEN AS f3         ) 
FROM   s
WHERE  f1 IN "comes next"
  AND  f2 IN (
            SELECT s2.@token
            FROM   s, (
              SELECT s2.@SN AS fn
              FROM   s
              WHERE  s2.@TOKEN IN "comes next"
                      )
            WHERE  ABS(s2.@sn-fn)=1
              AND  DISTINCT(s2.@token, s2.@sn)
             )
  AND  f3 BETWEEN             
            SELECT MIN(s2.@SN)
            FROM   s
            WHERE  s2.@TOKEN IN "comes next"
          AND
            SELECT MAX(s2.@SN)
            FROM   s
            WHERE  s2.@TOKEN IN "comes next"
  AND  f3 NOT IN ( -- not in <em>, i.e. not f2
            SELECT s2.@token
            FROM   s, (
              SELECT s2.@SN AS fn
              FROM   s
              WHERE  s2.@TOKEN IN "comes next"
                      )
            WHERE  ABS(s2.@sn-fn)=1
              AND  DISTINCT(s2.@token, s2.@sn)
                 );
Этот запрос возвращает следующий результат

s1s2SYS_CLUE
10<em>dog</em> <b>comes</b>, <em>cat</em> comes home too. Continue in <em>the</em> <b>NEXT</b> <em>issue</em>1
10<em>cat</em> <b>comes</b> <em>home</em> too. Continue in <em>the</em> <b>NEXT</b> <em>issue</em>2

Индексация

Для индексации нужна прежде всего некоторая таблица 'delimiters' из двух полей, первое содержит первичный ключ, второе - знаки-разделители: пробел, табуляцию, возврат каретки, переход на новую строку, все знаки пунктуации. Кроме того, если нужно рассматривать все грамматические формы одного слова как одну лексему, то нужна таблица, группирующая грамматические формы. Например, как это сделано в таблице 'tokens'. В её первые два поля удобно заносить командой 'TOKENIZE s(s2) INTO tokens DELIMITING delimiters [, delimiters2]', а во второе и третье - стандартной командой заполнения таблицы из файла 'COPY tokens( idlexeme, token ) FROM c:/lexeme.txt'.

tokens
idtokentokenidlexeme
1in1
2the2
3morning3
4dog4
5comes5
12come5
6cat6
7home7
8too8
9continue9
10next10
11issue11

Далее, нам нужна некоторая таблица типа 'items', поле 'idtoken' которой ссылается внешним ключем на одноименное поле таблицы 'tokens', и в поля 'idfield', 'pk', 'sn', 'beginning', 'end' которой скопированы: идентификатор поля 's2', взятый из системных таблиц, описывающих схему базы данных; первичный ключ таблицы 's'; поля @SN, @BEGINNING, @END таблицы, вложенной в поле 's2'. Поля 'own name' и 'abbreviation' указывают, является ли слово именем собственным или аббревиатурой. Поле 'idfield' нужно, чтобы командой 'SELECT ... FROM items' искать сразу во многих таблицах базы данных. Разложение поля 's2' сразу всех записей будем производить командой 'ITEMIZE s(s2) INTO items DELIMITING delimiters [, delimiters2] TOKENIZING tokens'.

items
idfieldpkidtokenown nameabbreviationsnbeginningend
50511yes 112
50511  115556
50512  246
50512  125860
50513  3814
50514  41719
50515  52124
50515  73134
50516  62729
50517  83639
50518  94143
50519yes 104653
505110 yes136265
505111  146771

Теперь индексирование представляет собой стандартную команду CREATE INDEX для таблиц 'tokens' и 'items'. Все эти индексы автоматически удаляются при удалении любой из таблиц 'delimiters', 'tokens', 'items'. Может понадобиться отключать одни 'items' и подключать к полнотекстовому поиску другие 'items' - будем делать это с помощью 'SET NOMENCLARURE items [, items2]'. Параметр NOMENCLARURE является сессионным, т.е. два одновременных соединения одного и того же пользователя с СУБД могут иметь различные значения этого параметра.

Если 'tokens' не содержит некоторых слов или их форм, то сам факт наличия индексов переключает поиск по всем словам или формам на поиск только по проиндексированным. Т.е. не только увеличивается скорость поиска, но и может сужаться диапазон слов, по которым поиск производится. Чтобы одновременно по оставшимся словам произвести не-индексированный поиск, будем указывать квантор ALL перед названием вложенной колонки 'SELECT s1, ALL s2.@TOKEN FROM s'. Команда TOKENIZE добавляет во вложенной таблице колонки 's2.@IDTOKEN', 's2.@IDFIELD', а команда ITEMIZE - колонку 's2.@IDLEXEME'. Этими тремя новыми колонками также можно пользоваться в запросах.

Дмитрий Тюрин (DmitryTurin.narod.ru):
dima.turin@centrum.cz (все письма из домена .ru попадают в спам), dima.turin@gmail.com