# 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.`"会被分词为：

```sql
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++`是一个单独的单词。对人类来说很容易理解，但对全文搜索算法来说却不是，因为它看到加号，没有在它的单词字符列表中找到它，并将其从分词中移除，因此您最终会得到：

```sql
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#`，例如，您将找到上面的句子：

```sql
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`匹配，因此您会得到它。

解决方案是什么？有几个选项。第一个可能想到的选项是：

> 好的，为什么不把`+`和`#`添加到单词字符列表中？

这是一个好问题。让我们试试看。

```sql
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)
```

它有效，但列表中的`+`立即开始影响其他单词和搜索，例如：

```sql
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++`设为例外词：
```bash
➜  ~ cat /tmp/exceptions
c++ => c++
```

并在创建表时使用该文件：

```sql
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++`。让我们尝试上面的例外词与大写形式？

```sql
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++`的映射？

```sql
➜  ~ 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`。

因此，使用例外词的规则是：

{{< notice "tip" >}}
如果一个术语包含特殊字符，并且这是它在文本和搜索查询中通常的写法 - 请将其设为例外词。
{{< /notice >}}

# 同义词

Manticore Search用户也经常将`例外词`称为同义词，因为它们的另一个用途不是仅仅保留特殊字符和字母大小写，而是将完全不同的写法映射到相同的分词，例如：

```bash
MS Windows => ms windows
Microsoft Windows => ms windows
```

为什么这很重要？因为它使您能够轻松地通过`MS Windows`找到`Microsoft Windows`，反之亦然。

示例：

```sql
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`等等吗？”

是的，他们可以。因此，如果您想使用例外词来处理这种情况，请准备好所谓的数学中的组合爆炸。

> 看起来一点都不好。我们能做些什么？

## 词形

另一个与例外词类似的工具是`词形`。与例外词不同，词形是在对传入文本进行分词后应用的。因此它们是：
* 不区分大小写（除非您的`charset_table`启用了大小写敏感）
* 不关心特殊字符

它们本质上允许您将一个词替换为另一个词。通常，这用于将不同的词形归一化为一个标准形式。例如，将所有变体如"walks"、"walked"、"walking"归一化为标准形式"walk"：

```bash
➜  ~ cat /tmp/wordforms
walks => walk
walked => walk
walking => walk
```

```sql
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)
```

正如你所看到的，这三个词都被转换成了`walk`，请注意，第二个词`_WaLkeD!`即使被严重变形，也被正确地规范化了。你明白我的意思了吗？是的，`MS Windows`的例子。让我们测试一下词形是否能帮助解决这个问题。

让我们只向词形文件添加两行：

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

并用几个文档填充表格：

```sql
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)
```

现在让我们尝试各种查询：
```sql
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`。

```sql
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`同样可以正常工作。


```sql
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`也能找到相同的文档。

```sql
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`有助于解决这个问题。

关于词形的规则是：

{{< notice "tip" >}}
对于可以以不同形式书写且不包含特殊字符的单词和短语，使用词形。
{{< /notice >}}

## Floor & Decor

让我们再看一个例子：我们想改进对品牌名称`Floor & Decor`的搜索。我们可以假设人们可能会以以下形式书写这个名称：

```bash
Floor & Decor
Floor & decor
floor & decor
Floor and Decor
floor and decor
```
以及其他大小写组合。

另外：
```bash
Floor & Decor Holdings
Floor & Decor Holdings, inc.
```
以及，再次，各种大小写不同的组合。

现在我们知道了`exceptions`和`wordforms`的工作原理，我们该如何覆盖这个品牌名称呢？

首先，我们可以很容易地注意到规范的品牌名称是`Floor & Decor`，即它包含一个通常被视为单词分隔符的特殊字符，那么我们应该使用`exceptions`吗？但这个名称较长，可以以许多方式书写。如果我们使用`exceptions`，可能会得到一个包含所有组合的巨大列表。此外还有扩展形式`Floor & Decor Holdings`和`Floor & Decor Holdings, inc.`，这会使列表更长。

在这种情况下，最**最优**的解决方案是直接使用`wordforms`，如下所示：

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

为什么包含`&`？实际上你可以跳过它：
```bash
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`，这将成为我们这个品牌名称的快捷方式。

```sql
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)
```


这是完美的终极解决方案吗？不幸的是，正如全文搜索领域的许多其他事情一样，不是。总有一些罕见的情况，这个情况也不例外。例如：

```sql
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`：

```sql
mysql> call keywords('floor. Decor', 't');
+------+-------------+------------+
| qpos | tokenized   | normalized |
+------+-------------+------------+
| 1    | floor decor | fnd        |
+------+-------------+------------+
1 row in set (0.00 sec)
```

这种错误匹配并不好。为了解决这个问题，我们可以使用Manticore的功能来[detect sentences and paragraphs](https://manual.manticoresearch.com/Creating_a_table/NLP_and_tokenization/Advanced_HTML_tokenization#index_sp)。

现在如果我们启用它，我们可以看到文档不再与关键字匹配：

```sql
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`，如我们所记得的，通过`wordforms`被转换为`fnd`
2. `index_sp='1'`将文本拆分为句子
3. 拆分后，`floor.`和`Decor`出现在不同的句子中
4. 因此不再匹配`fnd`，也不再匹配其所有原始形式


# 结论

Manticore的异常和词形是强大的工具，可以帮助你微调搜索，特别是在需要保留特殊字符的短语和需要相互别名的长语句方面提高召回率和精确度。但你需要帮助Manticore做到这一点，因为它无法为你决定名称应该是什么。

感谢你阅读这篇文章！

#### 参考资料：

* [关于`exceptions`的文档](https://manual.manticoresearch.com/Creating_a_table/NLP_and_tokenization/Exceptions)
* [关于`wordforms`的文档](https://manual.manticoresearch.com/Creating_a_table/NLP_and_tokenization/Wordforms)
