blog-post

使用 Manticore 实现简单的自动完成

本文描述了在 Manticore Search 中实现单词自动完成的一种方法。

什么是自动完成?

自动完成(或单词完成)是一个允许应用程序在用户输入单词时预测剩余部分的功能。它的工作原理通常是:用户在搜索栏中开始输入一个单词,然后弹出一个包含建议的下拉列表,用户可以从列表中选择一个。

建议的来源可能有多种。最好是显示的单词或句子可以在现有数据集中找到,这样用户就不会选择返回空结果的内容。但在某些情况下,自动完成是基于先前(成功的)搜索,理论上可能会找到零结果,但仍可能有意义。这完全取决于您应用程序的具体情况。

最简单的自动完成可以通过从数据集中的项目标题中查找建议来实现。这可以是文章/新闻的标题、产品名称,或者正如我们即将展示的电影名称。为此,我们需要将字段定义为 字符串属性存储字段 。这样就不需要额外查找原始数据。

由于用户提供的是不完整的单词,我们需要执行通配符搜索。通过激活前缀或中缀可以进行通配符搜索。由于这可能 影响响应时间 ,您需要决定是在用于搜索的索引中启用它,还是仅在专门用于自动完成功能的特殊索引中启用。另一个这样做的原因是使后者尽可能紧凑,以提供最小的延迟,这在用户体验方面对自动完成特别重要。通常我们会在右侧添加通配符星号,因为我们假设用户开始输入一个单词,但为了获得更广泛的结果,我们在两侧添加星号,以获取可能有前缀的单词。在这个电影数据集课程中,让我们选择中缀,因为它还启用了单词纠正的 SUGGEST 功能(请参见 此课程 中的工作原理)。我们的索引声明将是:

index movies {
  type            = plain
  path            = /var/lib/manticore/data/movies
  source          = movies
  min_infix_len   = 3
}

由于我们将从电影标题提供自动完成,我们的查询将限制在 ‘movie_title’ 字段。

电影标题自动完成


您的应用程序的前端可以从搜索框中输入的第一个字符开始查询建议。但是,对于大型索引,这可能会给系统带来更多压力,因为它将向服务器发出更多请求,而且1-2个字符的通配符搜索可能会更慢。假设用户输入 ‘sha’。

MySQL [(none)]> SELECT id, movie_title FROM movies WHERE MATCH('@movie_title sha*');
+------+---------------------------------------+
| id   | movie_title                           |
+------+---------------------------------------+
|  118 | A Low Down Dirty Shame                |
|  394 | Austin Powers: The Spy Who Shagged Me |
|  604 | Book of Shadows: Blair Witch 2        |
|  951 | Dark Shadows                          |
| 1318 | Fifty Shades of Black                 |
| 1319 | Fifty Shades of Grey                  |
| 1389 | Forty Shades of Blue                  |
| 1853 | In the Shadow of the Moon             |
| 1928 | Jack Ryan: Shadow Recruit             |
| 3114 | Shade                                 |
| 3115 | Shadow Conspiracy                     |
| 3116 | Shadow of the Vampire                 |
| 3117 | Shadowlands                           |
| 3118 | Shaft                                 |
| 3119 | Shakespeare in Love                   |
| 3120 | Shalako                               |
| 3121 | Shall We Dance                        |
| 3122 | Shallow Hal                           |
| 3123 | Shame                                 |
| 3124 | Shanghai Calling                      |
+------+---------------------------------------+
20 rows in set (0.00 sec)

我们主要关心电影标题,所以我们不返回所有列。正如我们所看到的,返回了很多结果。我们可以尝试通过例如添加次要排序按照 facebook 点赞数,但是现在还太早无法准确猜测用户想要什么:

MySQL [(none)]> SELECT id, movie_title FROM movies WHERE MATCH('@movie_title sha*') ORDER BY WEIGHT() DESC, cast_total_facebook_likes DESC;
+------+--------------------------------------------------+
| id   | movie_title                                      |
+------+--------------------------------------------------+
|  951 | Dark Shadows                                     |
| 3131 | Shark Tale                                       |
|  394 | Austin Powers: The Spy Who Shagged Me            |
| 3118 | Shaft                                            |
| 4326 | The Shaggy Dog                                   |
| 3142 | Sherlock Holmes: A Game of Shadows               |
| 3134 | Shattered                                        |
| 3123 | Shame                                            |
| 3525 | The Adventures of Sharkboy and Lavagirl 3-D      |
| 3117 | Shadowlands                                      |
| 3129 | Shark Lake                                       |
| 4328 | The Shawshank Redemption                         |
| 3494 | Teenage Mutant Ninja Turtles: Out of the Shadows |
| 3135 | Shattered Glass                                  |
| 3130 | Shark Night 3D                                   |
| 1319 | 五十度灰                                          |
| 4619 | 特里斯特拉姆·香迪:一部鸡和牛的故事              |
|  118 | 卑鄙的低下骄傲                                   |
| 3132 | 鲨卷风                                          |
| 1318 | 五十度黑                                          |
+------+--------------------------------------------------+
20 rows in set (0.00 sec)

假设用户输入另一个字母:

MySQL [(none)]> SELECT id, movie_title FROM movies WHERE MATCH('@movie_title shaf*')  ORDER BY WEIGHT() DES , cast_total_facebook_likes DESC;
+------+-------------+
| id   | movie_title |
+------+-------------+
| 3118 | 沙夫       |
+------+-------------+
1 row in set (0.00 sec)

现在我们有一个单一结果。

让我们再来看一个例子,输入 ‘shad*’。

MySQL [(none)]> SELECT id, movie_title FROM movies WHERE MATCH('@movie_title shad*')  ORDER BY WEIGHT() DES , cast_total_facebook_likes DESC;
+------+--------------------------------------------------+
| id   | movie_title                                      |
+------+--------------------------------------------------+
|  951 | 黑暗阴影                                       |
| 3142 | 福尔摩斯:阴影游戏                             |
| 3117 | 阴影之地                                       |
| 3494 | 青少年忍者海龟:阴影中的冒险                   |
| 1319 | 五十度灰                                          |
| 1318 | 五十度黑                                          |
| 4325 | 阴影                                           |
| 3115 | 阴谋论                                       |
| 3116 | 吸血鬼之影                                   |
| 1928 | 杰克·瑞安:暗影招募                            |
| 1389 | 四十度蓝                                      |
|  604 | 阴影书籍:布莱尔女巫2                          |
| 3114 | 阴影                                            |
| 1853 | 在月亮的阴影下                                  |
| 4353 | 声音与阴影                                       |
+------+--------------------------------------------------+
15 rows in set (0.00 sec)

然后是 shado*

MySQL [(none)]> SELECT id, movie_title FROM movies WHERE MATCH('@movie_title shado*')  ORDER BY WEIGHT() DE C, cast_total_facebook_likes DESC;
+------+--------------------------------------------------+
| id   | movie_title                                      |
+------+--------------------------------------------------+
|  951 | 黑暗阴影                                       |
| 3142 | 福尔摩斯:阴影游戏                             |
| 3117 | 阴影之地                                       |
| 3494 | 青少年忍者海龟:阴影中的冒险                   |
| 4325 | 阴影                                           |
| 3115 | 阴谋论                                       |
| 3116 | 吸血鬼之影                                   |
| 1928 | 杰克·瑞安:暗影招募                            |
|  604 | 阴影书籍:布莱尔女巫2                          |
| 1853 | 在月亮的阴影下                                  |
| 4353 | 声音与阴影                                       |
+------+--------------------------------------------------+
11 rows in set (0.00 sec)

以及 ‘shadow’:

MySQL [(none)]> SELECT id, movie_title FROM movies WHERE MATCH('@movie_title shadow')  ORDER BY WEIGHT() DE C, cast_total_facebook_likes DESC;
+------+---------------------------+
| id   | movie_title               |
+------+---------------------------+
| 4325 | 阴影                      |
| 3115 | 阴谋论                   |
| 3116 | 吸血鬼之影              |
| 1928 | 杰克·瑞安:暗影招募      |
| 1853 | 在月亮的阴影下           |
| 4353 | 声音与阴影               |
+------+---------------------------+
6 rows in set (0.00 sec)

假设用户正在寻找 ‘shadow’ 作为第一个单词,他将继续输入另一个单词,例如 ‘shadow c’:

MySQL [(none)]> SELECT id, movie_title FROM movies WHERE MATCH('@movie_title shadow c*')  ORDER BY WEIGHT() DESC, cast_total_facebook_likes DESC;
+------+-------------------+
| id   | movie_title       |
+------+-------------------+
| 3115 | 阴谋论            |
+------+-------------------+
1 row in set (0.01 sec)

在这种情况下,我们得到一个单一结果,应该能满足用户,但在其他情况下我们可能会得到更多,用户将继续输入字母,就像第一个单词那样,Manticore 将根据输入返回更多建议:

img

添加更多过滤器


在之前的例子中,匹配术语的唯一限制是必须是指定字段的一部分。如果我们想要更严格的自动完成,我们可以做到。

例如,在这里我们得到以 ‘americ’ 开头的匹配项,如 ‘美国骗局’,还有 ‘美国队长:内战’:

MySQL [(none)]> SELECT id, movie_title FROM movies WHERE MATCH('@movie_title americ* ') ORDER BY WEIGHT() D SC, cast_total_facebook_likes DESC;
+------+---------------------------------------------------+
| id   | movie_title                                       |
+------+---------------------------------------------------+
|  277 | 美国骗局                                         |
|  701 | 美国队长:内战                                   |
|  703 | 美国队长:冬日战士                               |
|  282 | 美国精神病                                       |
| 2612 | 在美国的一次童话                                 |
|  272 | 美国黑帮                                         |
|  702 | 美国队长:第一复仇者                             |
|  269 | 美国美丽                                         |
|  478 | 比维斯与巴特海德:去美国                       |
|  284 | 美国狙击手                                       |
| 4036 | 地狱门传说:一个美国阴谋                         |
|  273 | 美国涂鸦                                       |
|  285 | 美国辉煌                                         |
|  274 | 美国抢劫                                          |
|  287 | 美国甜心                                       |
|  283 | 美国派重聚                                     |
|  280 | 美国派                                         |
|  281 | 美国派2                                        |
|  271 | 美国梦想                                       |
|  286 | 美国婚礼                                       |
+------+---------------------------------------------------+
20 rows in set (0.00 sec)

我们可以使用开始字段运算符来仅显示以输入项开头的记录:

MySQL [(none)]> SELECT id, movie_title FROM movies WHERE MATCH('@movie_title ^americ* ') ORDER BY WEIGHT()  ESC, cast_total_facebook_likes DESC;
+------+-------------------------------------+
| id   | movie_title                         |
+------+-------------------------------------+
|  277 | 美国骗局                             |
|  282 | 美国精神病患者                       |
|  272 | 美国黑帮                             |
|  269 | 美国美人                             |
|  284 | 美国狙击手                           |
|  273 | 美国飞车党                           |
|  285 | 美国生活                             |
|  274 | 美国抢劫案                           |
|  287 | 美国甜心                             |
|  283 | 美国派重聚                           |
|  280 | 美国派                               |
|  281 | 美国派2                              |
|  271 | 美国梦想                             |
|  286 | 美国婚礼                             |
|  276 | 美国X历史                            |
|  268 | 美国仍是那个地方                     |
|  279 | 美国亡命之徒                         |
|  275 | 美国英雄                             |
|  278 | 美国忍者2:对抗                      |
|  270 | 美国德西                             |
+------+-------------------------------------+
20 rows in set (0.00 sec)

另一个我们应该考虑的是重复项。这更多地适用于我们想要在不唯一的字段上进行自动完成的情况。举例来说,让我们尝试按演员姓名进行自动完成:

MySQL [(none)]> SELECT actor_1_name FROM movies WHERE MATCH('@actor_1_name john* ');
+--------------------+
| actor_1_name       |
+--------------------+
| 约翰尼·德普        |
| 约翰尼·德普        |
| 约翰尼·德普        |
| 道恩·强森           |
| 约翰尼·德普        |
| 约翰尼·德普        |
| 唐·强森             |
| 道恩·强森           |
| 约翰尼·德普        |
| 约翰尼·德普        |
| 约翰尼·德普        |
| 约翰尼·德普        |
| 约翰尼·德普        |
| 道恩·强森           |
| 约翰尼·德普        |
| 约翰尼·德普        |
| R·布兰登·强森      |
| 道恩·强森           |
| 约翰尼·德普        |
| 约翰尼·德普        |
+--------------------+
20 rows in set (0.09 sec)

我们可以看到很多重复。通过对该字段进行分组可以解决这个问题 - 假设我们将其作为字符串属性:

MySQL [(none)]> SELECT actor_1_name FROM movies WHERE MATCH('@actor_1_name john* ') GROUP BY actor_1_name;
+------------------------+
| actor_1_name           |
+------------------------+
| 约翰尼·德普            |
| 道恩·强森             |
| 唐·强森               |
| R·布兰登·强森         |
| 约翰尼·帕卡尔         |
| 肯尼·约翰斯顿          |
| 约翰尼·卡尼扎罗        |
| 妮可·兰德尔·强森      |
| 约翰尼·刘易斯          |
| 理查德·强森            |
| 比尔·强森              |
| 埃里克·强森            |
| 约翰·贝卢西            |
| 约翰·科斯兰            |
| 约翰·拉森博格          |
| 约翰·卡梅隆·米切尔    |
| 约翰·萨克逊            |
| 约翰·盖金斯            |
| 约翰·博伊加            |
| 约翰·迈克尔·希金斯    |
+------------------------+
20 rows in set (0.10 sec)

高亮


自动完成查询可以返回包含高亮的结果。虽然也可以在应用程序端执行,但Manticore Search进行的高亮更强大,因为它将遵循搜索查询(相同的分词设置, 查询中的与、或和非等 )。以上一个示例为例,我们所需要做的就是使用’SNIPPET’函数:

MySQL [(none)]> SELECT SNIPPET(actor_1_name,' john*')  FROM movies WHERE MATCH('@actor_1_name john* ')  GROUP BY actor_1_name  ORDER BY WEIGHT() DESC, cast_total_facebook_likes DESC;
+--------------------------------+
| snippet(actor_1_name,' john*') |
+--------------------------------+
| <b>约翰尼</b>·德普             |
| 道恩·<b>强森</b>               |
| <b>约翰尼</b>·帕卡尔           |
| 唐·<b>强森</b>                 |
| <b>约翰尼</b>·卡尼扎罗         |
| <b>约翰尼</b>·刘易斯           |
| 埃里克·<b>强森</b>             |
| 妮可·兰德尔·<b>强森</b>        |
| 肯尼·<b>约翰斯顿</b>           |
| R·布兰登·<b>强森</b>           |
| 比尔·<b>强森</b>               |
| 理查德·<b>强森</b>             |
| <b>约翰</b>·拉森博格            |
| <b>约翰</b>·贝卢西              |
| <b>约翰</b>·卡梅隆·米切尔      |
| <b>约翰</b>·科斯兰              |
| 奥利维亚·牛顿-<b>约翰</b>       |
| <b>约翰</b>·迈克尔·希金斯      |
| <b>约翰</b>·威瑟斯彭            |
| <b>约翰</b>·阿莫斯              |
+--------------------------------+
20 rows in set (0.00 sec)

您可以在 这个课程 中找到更多关于高亮的信息。

Besides using the infixes and prefixes there are other ways to do Autocomplete in Manticore: using CALL KEYWORDS , with or without bigram_index turned on, using CALL QSUGGEST / CALL SUGGEST . But the way it’s shown in this article seems to be the easiest to get started with. If you want to play with Manticore Search try out our docker image which has a one-liner to run Manticore on any server in just few seconds.

Check out the entire Autocomplete demo online .

安装Manticore Search

安装Manticore Search