⚠️ Эта страница автоматически переведена, и перевод может быть несовершенным.
blog-post

Wordforms vs exceptions

Исключения и словоформы — два полезных инструмента, встроенных в Manticore Search, которые вы можете использовать для повышения полноты и точности поиска. У них много общего, но также есть важные различия, которые я хотел бы рассмотреть в этой статье.

О токенизации

В чём разница между полнотекстовым поиском (также называемым свободным текстовым поиском) и поиском с подстановочными знаками, таким как:

  • широко известный оператор LIKE в той или иной форме
  • или более сложные регулярные выражения

? Конечно, различий множество, но всё начинается с того, что мы делаем с исходным вводным текстом в каждом из подходов:

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

Когда вы хотите токенизировать текст, вам нужно решить, как это сделать, в частности:

  1. Что должно быть разделителями и символами слов. Обычно разделителем считается символ, который не встречается внутри слова, например знаки пунктуации: ., ,, ?, !, - и т.д.
  2. Нужно ли сохранять регистр букв токенов. Обычно нет, поскольку это плохо для поиска, если вы не находите 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++ особым образом, вы можете сделать его исключением.

Исключения

Итак, exceptions (также известные как синонимы) позволяют сопоставлять один или несколько терминов (включая термины с символами, которые обычно исключаются) с одним ключевым словом.

Давайте сделаем 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        | is         |
| 2    | c++       | c++        |
| 3    | the       | the        |
| 4    | most      | most       |
| 5    | powerful  | powerful   |
| 6    | language  | language   |
| 7    | what      | what       |
| 8    | is        | is         |
| 9    | 2         | 2          |
| 10   | 2         | 2          |
+------+-----------+------------+
10 rows in set (0.01 sec)

Ура, 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        | is         |
| 2    | c         | c          |
| 3    | the       | the        |
| 4    | most      | most       |
| 5    | powerful  | powerful   |
| 6    | language  | language   |
| 7    | what      | what       |
| 8    | is        | is         |
| 9    | 2         | 2          |
| 10   | 2         | 2          |
+------+-----------+------------+
10 rows in set (0.00 sec)

Упс, 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        | is         |
| 2    | c++       | c++        |
| 3    | the       | the        |
| 4    | most      | most       |
| 5    | powerful  | powerful   |
| 6    | language  | language   |
| 7    | how       | how        |
| 8    | about     | about      |
| 9    | c++       | c++        |
+------+-----------+------------+
9 rows in set (0.00 sec)

Хорошо, теперь всё снова в порядке, поскольку и 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 также часто называют exceptions синонимами, потому что другой случай их использования — не просто сохранять специальные символы и регистр, а сопоставлять термины, написанные совершенно по‑разному, одному токену, например:

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 row in set (0.00 sec)

Так что на первый взгляд всё работает нормально, но при дальнейшем размышлении и учитывая, что исключения чувствительны к регистру и байтам, вы можете задать себе вопрос: «Не могут ли люди писать MicroSoft windows, MS WINDOWS, microsoft Windows и так далее?»

Да, могут. Поэтому если вы хотите использовать исключения для этого, будьте готовы к тому, что в математике это называется комбинаторным взрывом.

Выглядит совсем неплохо. Что мы можем с этим сделать?

Словоформы

Другой инструмент, похожий на исключения, — это wordforms. В отличие от исключений, словоформы применяются после токенизации входящего текста. Поэтому они:

  • регистронезависимы (если только ваш 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 rows in set (0.00 sec)

Как вы можете видеть, все 3 слова были преобразованы в просто walk, и обратите внимание, что второе слово _WaLkeD!, даже будучи сильно деформированным, также было нормально преобразовано. Вы понимаете, к чему я веду? Да, пример с MS Windows. Давайте проверим, могут ли формы слов быть полезны для решения этой проблемы.

Давайте добавим всего 2 строки в файл форм слов:

➜  ~ cat /tmp/wordforms
ms windows => ms windows
microsoft windows => ms windows

и заполним таблицу несколькими документами:

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)

Теперь давайте попробуем различные запросы:

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 находит Microsoft Windows нормально.

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 тоже работает нормально.

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)

✅ И даже mIcRoSoFt WiNdOwS находит тот же документ.

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)

✅ Просто базовое windows находит все документы.

Таким образом, wordforms действительно помогает решить проблему.

Общее правило для форм слов:

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

Floor & Decor

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

Floor & Decor
Floor & decor
floor & decor
Floor and Decor
floor and decor

и других комбинациях с разным регистром букв.

Также:

Floor & Decor Holdings
Floor & Decor Holdings, inc.

и, снова, различные комбинации с разными заглавными буквами.

Теперь, когда мы знаем, как работают exceptions и wordforms, что мы можем сделать, чтобы охватить это название бренда?

Прежде всего, мы можем легко заметить, что каноническое название бренда — это Floor & Decor, т.е. оно включает специальный символ, который обычно считается разделителем слов, так что нам следует использовать exceptions? Но название длинное и может быть написано многими способами. Если мы используем exceptions, мы можем получить огромный список всех комбинаций. Более того, есть расширенные формы Floor & Decor Holdings и Floor & Decor Holdings, inc., которые могут сделать список еще длиннее.

Наиболее оптимальное решение в этом случае — просто использовать wordforms вот так:

➜  ~ 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

Почему оно включает &? На самом деле, вы можете его пропустить:

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

потому что wordforms все равно игнорирует несловесные символы, но просто для удобства чтения он был оставлен.

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

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,'It\'s located on the 2nd floor. Decor is also nice');
mysql> select * from t where match('Floor & Decor Holdings');

+---------------------+---------------------------------------------------+
| id                  | f                                                 |
+---------------------+---------------------------------------------------+
| 1514841286668976231 | It's located on the 2nd floor. Decor is also nice |
+---------------------+---------------------------------------------------+
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,'It\'s located on the 2nd floor. Decor is also nice');
mysql> select * from t where match('Floor & Decor Holdings');

Empty set (0.00 sec)

потому что:

  1. Floor & Decor, как мы помним, преобразуется в fnd с помощью wordforms
  2. index_sp='1' разбивает текст на предложения
  3. после разбиения floor. и Decor оказываются в разных предложениях
  4. и больше не соответствуют fnd, и, следовательно, всем оригинальным формам этого слова

Заключение

Исключения и формы слов Manticore — это мощные инструменты, которые могут помочь вам точно настроить ваш поиск, в частности, улучшить полноту и точность, когда дело касается коротких терминов со специальными символами, которые должны быть сохранены, и более длинных терминов, которые должны быть связаны друг с другом. Но вам нужно помочь Manticore в этом, так как он не может решить, какими должны быть названия для вас.

Спасибо за чтение этой статьи!

Ссылки:

Установить Manticore Search

Установить Manticore Search