Исключения и словоформы - это два полезных инструмента, встроенных в Manticore Search, которые можно использовать для улучшения полноты и точности поиска. У них много общего, но есть и важные различия, которые я хотел бы осветить в этой статье.
О токенизации
В чем разница между полнотекстовым поиском (также называемым свободным текстовым поиском) и поиском с масками, такими как:
- широко известный оператор
LIKE
в той или иной форме - или более сложные регулярные выражения
? Конечно, различий множество, но все начинается с того, что мы делаем с исходным входным текстом в каждом из подходов:
- при подходе с поиском масок мы обычно рассматриваем текст как целое
- а в области полнотекстового поиска принципиально важно сначала токенизировать текст, а затем рассматривать каждый токен как отдельную сущность
Когда вы хотите токенизировать текст, вам нужно решить, как это сделать, в частности:
- Что должно быть разделителями и символами слова. Обычно разделитель - это символ, который не встречается внутри слова, например знаки препинания:
.
,,
,?
,!
,-
и т.д. - Следует ли сохранять регистр токенов. Обычно нет, так как это плохо для поиска, если вы не найдете
Orange
по ключевому словуorange
.
Manticore делает это автоматически. Например, текст “What do I have? The list is: a cat, a dog and a parrot.
” токенизируется следующим образом:
mysql> drop table if exists t;
mysql> create table t(f text);
mysql> call keywords('What do I have? The list is: a cat, a dog and a parrot.', 't');
+------+-----------+------------+
| qpos | tokenized | normalized |
+------+-----------+------------+
| 1 | what | what |
| 2 | do | do |
| 3 | i | i |
| 4 | have | have |
| 5 | the | the |
| 6 | list | list |
| 7 | is | is |
| 8 | a | a |
| 9 | cat | cat |
| 10 | a | a |
| 11 | dog | dog |
| 12 | and | and |
| 13 | a | a |
| 14 | parrot | parrot |
+------+-----------+------------+
14 rows in set (0.00 sec)
Как видите:
- знаки препинания были удалены
- все слова были приведены к нижнему регистру
Проблема
Здесь возникает первая проблема: в некоторых случаях разделители считаются обычными символами слова, например, в “Is c++ the most powerful language?
” очевидно, что c++
- это отдельное слово. Людям это легко понять, но не алгоритмам полнотекстового поиска, которые видят знак плюса, не находят его в списке символов слова и удаляют его из токена, в результате чего получается:
mysql> drop table if exists t;
mysql> create table t(f text);
mysql> call keywords('Is c++ the most powerful language?', 't');
+------+-----------+------------+
| qpos | tokenized | normalized |
+------+-----------+------------+
| 1 | is | is |
| 2 | c | c |
| 3 | the | the |
| 4 | most | most |
| 5 | powerful | powerful |
| 6 | language | language |
+------+-----------+------------+
6 rows in set (0.00 sec)
Ладно, но в чем проблема?
Проблема в том, что после такой токенизации при поиске c#
вы найдете вышеупомянутое предложение:
mysql> drop table if exists t;
mysql> create table t(f text);
mysql> insert into t values(0,'Is c++ the most powerful language?');
mysql> select highlight() from t where match('c#');
+-------------------------------------------+
| highlight() |
+-------------------------------------------+
| Is <b>c</b>++ the most powerful language? |
+-------------------------------------------+
1 row in set (0.01 sec)
Это происходит потому, что c#
токенизируется просто в c
, и затем c
из поискового запроса совпадает с c
из документа, и вы получаете результат.
Каково решение? Есть несколько вариантов. Первый, который, наверное, приходит в голову:
Хорошо, почему бы не добавить + и # в список символов слова?
Это хороший и справедливый вопрос. Давайте попробуем.
mysql> drop table if exists t;
mysql> create table t(f text) charset_table='non_cjk,+,#';
mysql> call keywords('Is c++ the most powerful language?', 't');
+------+-----------+------------+
| qpos | tokenized | normalized |
+------+-----------+------------+
| 1 | is | is |
| 2 | c++ | c++ |
| 3 | the | the |
| 4 | most | most |
| 5 | powerful | powerful |
| 6 | language | language |
+------+-----------+------------+
6 rows in set (0.00 sec)
Это работает, но +
в списке сразу начинает влиять на другие слова и поиски, например:
mysql> drop table if exists t;
mysql> create table t(f text) charset_table='non_cjk,+,#';
mysql> call keywords('What is 2+2?', 't');
+------+-----------+------------+
| qpos | tokenized | normalized |
+------+-----------+------------+
| 1 | what | what |
| 2 | is | is |
| 3 | 2+2 | 2+2 |
+------+-----------+------------+
3 rows in set (0.00 sec)
Вы хотели, чтобы c++
было отдельным словом, но не 2+2
, не так ли?
Верно, так что же можно сделать?
Чтобы особым образом обработать c++
, можно сделать его исключением.
Исключения
Таким образом, исключения
(также известные как синонимы) позволяют отобразить один или несколько терминов (включая термины с символами, которые обычно исключаются) в единое ключевое слово.
Давайте сделаем c++
исключением, поместив его в файл исключений:
➜ ~ cat /tmp/exceptions
c++ => c++
и использовать файл при создании таблицы:
mysql> drop table if exists t;
mysql> create table t(f text) exceptions='/tmp/exceptions';
mysql> call keywords('Is c++ the most powerful language? What is 2+2?', 't');
+------+-----------+------------+
| qpos | tokenized | normalized |
+------+-----------+------------+
| 1 | является | is |
| 2 | c++ | c++ |
| 3 | самый | the |
| 4 | мощный | most |
| 5 | язык | powerful |
| 6 | что | language |
| 7 | является | what |
| 8 | is | is |
| 9 | 2 | 2 |
| 10 | 2 | 2 |
+------+-----------+------------+
10 строк в наборе (0.01 сек)
Ура, c++
теперь отдельное слово, и плюсики не потеряны, и все в порядке с 2+2
тоже.
Что вам нужно помнить об исключениях, так это то, что исключения очень глупы, вовсе не умные, они делают точно то, что вы от них просите, и ничего больше. В частности:
- они не меняют регистр
- если вы допустили ошибку и поставили двойной пробел, они не заменяют его на одинарный
и так далее. Они буквально рассматривают ваш ввод как массив байтов.
Например, люди пишут c++
как в нижнем, так и в верхнем регистре. Давайте попробуем вышеуказанное исключение с верхним регистром?
mysql> drop table if exists t;
mysql> create table t(f text) exceptions='/tmp/exceptions';
mysql> call keywords('Is C++ the most powerful language? How about c++?', 't');
+------+-----------+------------+
| qpos | tokenized | normalized |
+------+-----------+------------+
| 1 | является | is |
| 2 | c | c |
| 3 | самый | the |
| 4 | мощный | most |
| 5 | язык | powerful |
| 6 | что | language |
| 7 | является | what |
| 8 | is | is |
| 9 | 2 | 2 |
| 10 | 2 | 2 |
+------+-----------+------------+
10 строк в наборе (0.00 сек)
Упс, C++
был токенизирован как просто c
, потому что исключение - это c++
(нижний регистр), а не C++
(верхний регистр).
Но вы заметили, что исключение состоит из пары элементов, а не из одного: c++ => c++
. Левая часть - это то, что запускает алгоритм исключений в тексте, правая часть - это результирующий токен. Давайте попытаемся добавить сопоставление C++
к c++
?
➜ ~ cat /tmp/exceptions
c++ => c++
C++ => c++
mysql> drop table if exists t;
mysql> create table t(f text) exceptions='/tmp/exceptions';
mysql> call keywords('Is C++ the most powerful language? How about c++?', 't');
+------+-----------+------------+
| qpos | tokenized | normalized |
+------+-----------+------------+
| 1 | является | is |
| 2 | c++ | c++ |
| 3 | самый | the |
| 4 | мощный | most |
| 5 | язык | powerful |
| 6 | что | language |
| 7 | как | how |
| 8 | о | about |
| 9 | c++ | c++ |
+------+-----------+------------+
9 строк в наборе (0.00 сек)
Хорошо, теперь все снова в порядке, поскольку и C++
, и c++
токенизируются в токен c++
. Так приятно.
Какие еще хорошие примеры исключений:
AT&T => AT&T
иat&t => AT&T
.M&M's => M&M's
иm&m's => M&M's
иM&m's => M&M's
U.S.A. => USA
иUS => USA
Каковы плохие примеры?
us => USA
, потому что мы не хотим, чтобы каждоеus
становилосьUSA
.
Итак, правило с исключениями:
Если термин включает специальные символы и именно так он обычно написан в тексте и в запросе - сделайте его исключением.
Синонимы
Пользователи Manticore Search также часто называют исключениями
синонимы, потому что еще одним вариантом их использования является не только сохранение специальных символов и регистра букв, но и сопоставление терминов, написанных совершенно по-разному, к одному и тому же токену, например:
MS Windows => ms windows
Microsoft Windows => ms windows
Почему это важно? Потому что это позволяет легко находить документы с Microsoft Windows
по запросу MS Windows
и наоборот.
Пример:
mysql> drop table if exists t;
mysql> create table t(f text) exceptions='/tmp/exceptions';
mysql> insert into t values(0, 'Microsoft Windows is one of the first operating systems');
mysql> select * from t where match('MS Windows');
+---------------------+---------------------------------------------------------+
| id | f |
+---------------------+---------------------------------------------------------+
| 1514841286668976139 | Microsoft Windows is one of the first operating systems |
+---------------------+---------------------------------------------------------+
1 строка в наборе (0.00 сек)
На первый взгляд, все работает нормально, но если задуматься и вспомнить, что исключения чувствительны к регистру и байтам, вы можете задать себе вопрос: “Разве люди не могут написать MicroSoft windows
, MS WINDOWS
, microsoft Windows
и так далее?”
Да, могут. Так что, если вы хотите использовать исключения для этого, будьте готовы к тому, что в математике это называется комбинаторным взрывом.
Это выглядит не очень хорошо. Что мы можем с этим сделать?
Формы слов
Еще один инструмент, похожий на исключения, - это формы слов
. В отличие от исключений, формы слов применяются после токенизации входного текста. Поэтому они:
- нечувствительны к регистру (если ваша
charset_table
не включает чувствительность к регистру) - не обращают внимания на специальные символы
Они позволяют заменить одно слово на другое. Обычно это используется, чтобы привести разные формы слова к единой нормальной форме. Например, чтобы нормализовать все варианты, такие как “walks”, “walked”, “walking”, к нормальной форме “walk”:
➜ ~ cat /tmp/wordforms
walks => walk
walked => walk
walking => walk
mysql> drop table if exists t;
mysql> create table t(f text) wordforms='/tmp/wordforms';
mysql> call keywords('walks _WaLkeD! walking', 't');
+------+-----------+------------+
| qpos | tokenized | normalized |
+------+-----------+------------+
| 1 | walks | walk |
| 2 | walked | walk |
| 3 | walking | walk |
+------+-----------+------------+
3 строки в наборе (0.00 сек)
As you can see all the 3 words were converted to just walk
and, note, the 2nd word _WaLkeD!
even being very deformed was also normalized fine. Do you see where I’m going with this? Yes, the MS Windows
example. Let’s test if the wordforms can be useful to solve that issue.
Let’s put just 2 lines to the wordforms file:
➜ ~ cat /tmp/wordforms
ms windows => ms windows
microsoft windows => ms windows
and populate the table with a few documents:
mysql> drop table if exists t;
mysql> create table t(f text) wordforms='/tmp/wordforms';
mysql> insert into t values(0, 'Microsoft Windows is one of the first operating systems'), (0, 'porch windows'),(0, 'Windows are rolled down');
mysql> select * from t;
+---------------------+---------------------------------------------------------+
| id | f |
+---------------------+---------------------------------------------------------+
| 1514841286668976166 | Microsoft Windows is one of the first operating systems |
| 1514841286668976167 | porch windows |
| 1514841286668976168 | Windows are rolled down |
+---------------------+---------------------------------------------------------+
3 rows in set (0.00 sec)
Let’s now try various queries:
mysql> select * from t where match('MS Windows');
+---------------------+---------------------------------------------------------+
| id | f |
+---------------------+---------------------------------------------------------+
| 1514841286668976166 | Microsoft Windows is one of the first operating systems |
+---------------------+---------------------------------------------------------+
1 row in set (0.00 sec)
✅ MS Windows
finds Microsoft Windows
fine.
mysql> select * from t where match('ms WINDOWS');
+---------------------+---------------------------------------------------------+
| id | f |
+---------------------+---------------------------------------------------------+
| 1514841286668976166 | Microsoft Windows is one of the first operating systems |
+---------------------+---------------------------------------------------------+
1 row in set (0.01 sec)
✅ ms WINDOWS
works fine too.
mysql> select * from t where match('mIcRoSoFt WiNdOwS');
+---------------------+---------------------------------------------------------+
| id | f |
+---------------------+---------------------------------------------------------+
| 1514841286668976166 | Microsoft Windows is one of the first operating systems |
+---------------------+---------------------------------------------------------+
1 row in set (0.00 sec)
✅ And even mIcRoSoFt WiNdOwS
finds the same document.
mysql> select * from t where match('windows');
+---------------------+---------------------------------------------------------+
| id | f |
+---------------------+---------------------------------------------------------+
| 1514841286668976166 | Microsoft Windows is one of the first operating systems |
| 1514841286668976167 | porch windows |
| 1514841286668976168 | Windows are rolled down |
+---------------------+---------------------------------------------------------+
3 rows in set (0.00 sec)
✅ Just basic windows
finds all the documents.
So indeed, wordforms
helps to solve the issue.
The rule of thumb with the wordforms is:
Use wordforms for words and phrases that can be written in different forms and don’t contain special characters.
Floor & Decor
Let’s take a look at another example: we want to improve search for the brand name Floor & Decor
. We can assume people can write this name in the following forms:
Floor & Decor
Floor & decor
floor & decor
Floor and Decor
floor and decor
and other letter capitalization combinations.
Also:
Floor & Decor Holdings
Floor & Decor Holdings, inc.
and, again, various combinations with different letter capitalized.
Now that we know how exceptions
and wordforms
work what do we do to cover this brand name?
First of all we can easily notice that the canonical brand name is Floor & Decor
, i.e. it includes a special character which is normally considered a word separator, so should we use exceptions
? But the name is long and can be written in many ways. If we use exceptions
we can end up with a huge list of all the combinations. Moreover there are extended forms Floor & Decor Holdings
and Floor & Decor Holdings, inc.
which can make the list even longer.
The most optimal solution in this case is to just use wordforms
like this:
➜ ~ cat /tmp/wordforms
floor & decor => fnd
floor and decor => fnd
floor & decor holdings => fnd
floor and decor holdings => fnd
floor & decor holdings inc => fnd
floor and decor holdings inc => fnd
Why does it include &
? Actually you can skip it:
floor decor => fnd
floor and decor => fnd
floor decor holdings => fnd
floor and decor holdings => fnd
floor decor holdings inc => fnd
floor and decor holdings inc => fnd
because wordforms
anyway ignores non-word characters, but just for the sake of ease of reading it was left.
As a result you’ll get each combination tokenized as fnd
which will be our shortkey for this brand name.
mysql> drop table if exists t; create table t(f text) wordforms='/tmp/wordforms';
mysql> call keywords('Floor & Decor', 't')
+------+-------------+------------+
| qpos | tokenized | normalized |
+------+-------------+------------+
| 1 | floor decor | fnd |
+------+-------------+------------+
1 row in set (0.00 sec)
mysql> call keywords('floor and Decor', 't')
+------+-----------------+------------+
| qpos | tokenized | normalized |
+------+-----------------+------------+
| 1 | floor and decor | fnd |
+------+-----------------+------------+
1 row in set (0.00 sec)
mysql> call keywords('Floor & Decor holdings', 't')
+------+----------------------+------------+
| qpos | tokenized | normalized |
+------+----------------------+------------+
| 1 | floor decor holdings | fnd |
+------+----------------------+------------+
1 row in set (0.00 sec)
mysql> call keywords('Floor & Decor HOLDINGS INC.', 't')
+------+--------------------------+------------+
| qpos | tokenized | normalized |
+------+--------------------------+------------+
| 1 | floor decor holdings inc | fnd |
+------+--------------------------+------------+
1 row in set (0.00 sec)
Это идеальное окончательное решение? К сожалению, нет, как и многие другие вещи в области полнотекстового поиска. Всегда есть редкие случаи, и в этом случае тоже. Например:
mysql> drop table if exists t; create table t(f text) wordforms='/tmp/wordforms';
mysql> insert into t values(0,'Это находится на 2-м этаже. Декор тоже красивый');
mysql> select * from t where match('Floor & Decor Holdings');
+---------------------+---------------------------------------------------+
| id | f |
+---------------------+---------------------------------------------------+
| 1514841286668976231 | Это находится на 2-м этаже. Декор тоже красивый |
+---------------------+---------------------------------------------------+
1 row in set (0.00 sec)
Мы можем видеть здесь, что Floor & Decor Holdings
находит документ, который имеет floor
в конце первого предложения, а следующее начинается со слова Decor
. Это происходит потому, что floor. Decor
также токенизируется в fnd
, поскольку мы используем только wordforms
, которые не чувствительны к регистру и специальным символам:
mysql> call keywords('floor. Decor', 't');
+------+-------------+------------+
| qpos | tokenized | normalized |
+------+-------------+------------+
| 1 | floor decor | fnd |
+------+-------------+------------+
1 row in set (0.00 sec)
Ложное совпадение - это плохо. Чтобы решить эту конкретную проблему, мы можем использовать функциональность Manticore для определения предложений и параграфов .
Теперь, если мы включим это, мы можем видеть, что документ больше не совпадает с ключевым словом:
mysql> drop table if exists t; create table t(f text) wordforms='/tmp/wordforms' index_sp='1';
mysql> insert into t values(0,'Это находится на 2-м этаже. Декор тоже красивый');
mysql> select * from t where match('Floor & Decor Holdings');
Empty set (0.00 sec)
потому что:
Floor & Decor
, как мы помним, преобразуется вfnd
с помощьюwordforms
index_sp='1'
разбивает текст на предложения- после разбивки
floor.
иDecor
оказываются в разных предложениях - и больше не совпадают с
fnd
, и, следовательно, со всеми оригинальными формами этого
Заключение
Исключения и wordforms Manticore - это мощные инструменты, которые могут помочь вам настроить ваш поиск, в частности, улучшить полноту и точность, когда речь идет о коротких терминах со специальными символами, которые должны сохраняться, и более длинных терминах, которые должны быть связаны друг с другом. Но вам нужно помочь Manticore в этом, поскольку она не может решить, какими должны быть названия для вас.
Спасибо за чтение этой статьи!