阿尔戈利亚怎么这么快?

2020-11-29 14:27:03

大多数应用程序和网站都建立在数据库之上。它可以是传统的关系数据库(如MySQL)或NoSQL数据库(如MongoDB)。问题在于这些数据库都没有提供令人满意的全文搜索功能。尽管它们通常具有相似的功能(在MySQL中使用LIKE操作数,在MongoDB中使用文本索引),但正如所有开发人员所经历的那样,这些都是较差的选择。

建立Algolia是为了解决数据库全文搜索的缺点。它是一种SaaS API,致力于解决应用程序和网站开发人员在为最终用户提供快速,可靠且相关的搜索功能方面的难题。

到目前为止,Elasticsearch一直是开发人员的后备解决方案。尽管它是用于大数据分析或文档搜索的漂亮产品,但它并不是为对象搜索而设计的。阿尔戈利亚有。这篇博文的目的是回答我们一个经常被问到的问题:当Elasticsearch提供广泛的工具集时,如果Algolia提供了一个特定的答案,那么它们如何与数据库搜索进行比较?

我们决定对两者进行测试。我们使用40万演员和2M电影/电视连续剧的IMDB数据库,决定建立并衡量这两种搜索服务的性能,并保持其他一切不变。我们的测试并不仅限于粗略的关键字搜索,而是旨在建立一流的用户体验,在每次击键后返回即时结果,在排名中考虑流行度,并宽容用户的错误。

在第一部分中,我们总结了我们的发现。有关测试的技术细节在本文的第二部分中进行了描述。

在下面的基准测试中,对于我们执行的每个搜索查询,您都可以看到Algolia的性能始终比Elasticsearch快12到200倍。

但是,我们了解到,这不仅仅涉及性能,这是为什么:我们是要在尽可能短的时间内为用户提供搜索结果,还是想在尽可能短的时间内为用户提供所需的信息?我们决定为后者。

许多事情会在“幕后”发生,导致用户迅速找到他们想要的东西:

即时搜索。用户已经习惯于搜索引擎自动完成查询,而不是建议实际结果。在这里,我们想更进一步:每次输入键时,都会实时显示并更新索引范围的结果。输入字母并立即获得最佳效果。

平衡相关性和受欢迎度。在IMDB示例中,如果我们搜索“ geor”,那么我们想要演员姓名为“ George”(即相关性)的所有结果,并且希望“ George Clooney”成为该列表的顶部,因为他是最受欢迎的(由用户访问他的页面/在IMDB上查找他的次数定义)。尽管在Algolia方面很简单,但在Elasticsearch中将相关性和受欢迎程度混合在一起并非是不可能的。您要么根据相关性进行排序,要么使用人气属性进行排序,则不能将两者混用。

智能处理Typos。用户在输入搜索查询时通常会键入错误。良好的用户体验是,例如在搜索“峡谷克隆”时找到“乔治克隆尼”。 Algolia提供了开箱即用的错字容错功能,可同时处理单词和前缀,并智能地突出显示结果。这使最终用户即使有错别字也能理解搜索结果。不幸的是,Elasticsearch模糊匹配无法立即使用,定制起来很复杂,并且不能提供突出显示前缀的功能。

通过出色地完成数据库全文搜索,具有直观的关联性和匹配性配置以及开箱即用的强大的错字容错功能,Algolia可以帮助用户在最快的时间内获得所需的内容。在台式机上,尤其是在移动设备上,这对执行搜索所花费的时间产生了巨大的影响。

Elasticsearch是一个出色的工具箱,可用于构建Intranet搜索和大数据分析。但是,解决这种多样化的用例会导致集成困难。一种尺寸并不适合所有尺寸。 Algolia专注于对象搜索,因此在此方面做得更好:更快的集成,更好的性能,出色的用户体验。并且不要忘记,您还需要在某个地方托管Elasticsearch。

一切都很好,但是您当中技术最强的人可能想更多地了解该基准的实际实施情况。开始了。

为了提供参考,我们使用了相同的硬件:Xeon E3 1245v2(四核3.4Ghz),具有32GB的RAM和240GB的SSD(RAID 0中有2个SSD)。我们使用1个分片进行了测试,使用5个分片进行了另一个测试,并使用了基于Lucene 4.3.1的Elasticsearch版本0.90.2。请注意,分片的数量未在Algolia API中公开,因为分片的数量是在后端自动确定的,以实现可伸缩性,并且不会为小型数据集触发。

2013年7月底,我们从IMDB中提取了40万名演员/女演员和2M电影/电视剧的清单。为了在相关性中考虑受欢迎程度,我们使用以下方法为每个对象计算了一个整数等级:

演员:我们使用7月底对演员进行的IMDB每周排名。结果是,最佳男演员或女演员的排名为1,然后根据IMDB每周排名的增加值。

电影:我们根据以下公式对电影进行了排序:log(nb_voters)*评分。然后,我们选择使用电影在已排序向量中的位置作为等级(最佳电影为1)。

在此步骤中,我们从IMDB中获得了一组240万个条目,其中包含演员和电影,并带有相关性。在JSON中检出包含所有对象和等级的数据集。

{ “ name”:“肖申克的救赎”, “ url”:“ / title / tt0111161 /”, “评分”:9.3, “ year”:“(1994)”, “ nb_voters”:1010572, “等级”:1 }

{ “ name”:“权力的游戏”, “ url”:“ / title / tt2178784 /”, “评分”:9.8, “ year”:“(2011电视连续剧)”, “ nb_voters”:13312, “ episode”:“ Castamere的雨”, “等级”:330 }

要进行相关搜索,我们想先在“名称”中搜索,然后在“年份”中搜索,最后在“ episode”属性中搜索。我们还希望查询能够匹配这三个属性,以便能够回答“ Shawshank Redemption 1994”或“ Games of Thrones rains of castamere”这样的搜索

Algolia和Elasticsearch的架构较少,直接支持我们对象的索引。为了在Algolia上建立索引,我们将其ruby客户端与以下代码配合使用:

对于Elasticsearch,我们以批量索引格式转换了对象。我们编写了一个小的ruby脚本,将数据拆分为多个文件,然后使用CURL导入它们。

需要'json' 计数= 0 output_count = 1 输出= File.open(“ final-es-bulk-1.txt”,“ w”) File.open(“ imdb.json”,“ r:utf-8”)做|输入| imdb = JSON.parse(input.read); imdb.each做|进入| 计数+ = 1 如果((count%200000)== 0)然后 output.close output_count + = 1 输出= File.open(“ final-es-bulk-” + output_count.to_s +“ .txt”,“ w”) 结束 meta = {} meta [“ index”] = {} meta [“ index”] [“ _ index”] =“ imdb” meta [“ index”] [“ _ type”] =“ imdb” meta [“ index”] [“ _ id”] = count.to_s output.write(meta.to_json) output.write(“ n”) output.write(entry.to_json) output.write(“ n”) 结束 结束 output.close

为了评估性能,我们直接在索引主机上本地导入了数据。这是索引时间:

旁注:为什么我们不对Elasticsearch使用建议插件。在Elasticsearch中提供即时搜索的捷径是使用自动完成功能或建议的插件。这些模块的速度比标准搜索快得多,已在SoundCloud等多个站点中使用。但是,它们不支持多属性搜索,因此无法在我们的情况下使用。而且,它们可能会阻碍用户体验。例如,在SoundCloud中,如果以正确的顺序输入文本,您将获得有关``粉红色的月亮的黑暗面''的建议,但是如果您输入``月亮的黑暗的侧面,弗洛伊德·弗洛伊德'',则不会得到建议任何结果。烦死了

现在我们已经索引了所有数据,我们想要指定要搜索的属性。如前所述,我们想搜索以下三个属性:“名称”,然后是“年”,最后是“情节”。其他属性不应用于搜索。

使用Algolia,我们可以通过«attributeToIndex»参数在索引设置中指定字段列表,它们按照重要性的降序进行排序,因此您无需进行任何设置。只需一行代码即可更改它们:

借助Elasticsearch,我们可以直接在查询中指定字段,并对每个字段使用增强。升压值的选择很重要,因为它会直接影响“ _score”值。但是,它是一个不透明的值,需要反复试验才能正确。

s = Tire.search“ imdb”做 查询做 字符串“ batman”,:fields => [“ name ^ 5”,“ year ^ 2”,“ episode”] 结束 结束

在这里,第一个困难开始了。提醒一下,我们希望考虑演员和电影的受欢迎程度,因此查询“ geo”将首先返回“ George Clooney”,因为它是最著名的演员,以“ geo”开头。为此,我们使用了计算出的等级值。

为了在ElasticSearch的查询中添加排序条件,我们直接修改了查询:

s = Tire.search“ imdb”做 查询做 字符串“下雨”, :fields => [“ name ^ 5”,“ year ^ 2”,“ episode”], :default_operator =>“与” 结束 排序 通过:rank,“ asc” 由:_score 结束 结束

这种排序配置看似很明确,但实际上与与字段提升相冲突,因此非常危险。为了更好地理解问题,让我们看一下查询“下雨”:

“点击数”:[ { “ _index”:“ imdb”, “ _type”:“ imdb”, “ _id”:“ 330”, “ _score”:1.5647705, “_资源”: { “ name”:“权力的游戏”, “ url”:“ / title / tt2178784 /”, “评分”:9.8, “ year”:“(2011电视连续剧)”, “ nb_voters”:13312, “ episode”:“ Castamere的雨”, “等级”:330 }, “排序”:[ 330, 1.5647705 ] }, { “ _index”:“ imdb”, “ _type”:“ imdb”, “ _id”:“ 21986”, “ _score”:7.3712673, “_资源”: { “ name”:“在下雨之前”, “ url”:“ / title / tt0870195 /”, “评分”:6.6, “ year”:“(2007)”, “ nb_voters”:1299, 排名:15188 }, “排序”:[ 15188, 7.3712673 ] }, { “ _index”:“ imdb”, “ _type”:“ imdb”, “ _id”:“ 24324”, “ _score”:7.371266, “_资源”: { “ name”:“小雨来了”, “ url”:“ / title / tt0031835 /”, “评分”:6.8, “ year”:“(1939)”, “ nb_voters”:881, “等级”:16232 }, “排序”:[ 16232, 7.371266 ] }, ...

与“ episode”属性匹配的结果早于在“ name”属性中找到的结果而未考虑提升的情况可能是违反直觉的。提升会影响“ _score”浮点值,当它匹配情节属性而不是名称时,浮点值会较小。这里的问题是将用户定义的“等级”与浮动值“ _score”合并是很复杂的:

如果排序条件中的“ _score”位于“ rank”之前,则不会使用“ rank”,因为每个匹配项的“ _score”浮点值都不同。

如果在排序标准中“等级”在“ _score”之前,则不考虑属性顺序。

在阿尔戈利亚,排名的处理方式有所不同。我们没有一个唯一的浮点值来进行排名,而是计算一组明确且易于理解的整数值。您可以在查询“下雨”的前三个结果中看到它们:

“点击数”:[ { “ name”:“小雨来了”, “ url”:“ / title / tt0031835 /”, “评分”:6.8, “ year”:“(1939)”, “ nb_voters”:881, “等级”:16232, “ objectID”:“ 24324”, “ _highlightResult”:{ “名称”: { “ value”:“ The 雨来了”, “ matchLevel”:“完整” }, “年”: { “ value”:“(1939)”, “ matchLevel”:“无” } }, “ _rankingInfo”:{ “ nbTypos”:0, “ firstMatchedWord”:0, “ proximityDistance”:1 “ userScore”:2379657, “ geoDistance”:0, “ geoPrecision”:1 “ nbExactWords”:2 } }, { “名称”:“兰奇普尔的雨”, “ url”:“ / title / tt0048538 /”, “评分”:5.7, “ year”:“(1955)”, “ nb_voters”:495, “等级”:25569, “ objectID”:“ 62175”, “ _highlightResult”:{ “名称”: { “ value”:“兰奇普尔的 The 雨”, “ matchLevel”:“完整” }, “年”: { “ value”:“(1955)”, “ matchLevel”:“无” } }, “ _rankingInfo”:{ “ nbTypos”:0, “ firstMatchedWord”:0, “ proximityDistance”:1 “ userScore”:2323136, “ geoDistance”:0, “ geoPrecision”:1 “ nbExactWords”:2 } }, { “ name”:“在下雨之前”, “ url”:“ / title / tt0870195 /”, “评分”:6.6, “ year”:“(2007)”, “ nb_voters”:1299, “等级”:15188, “ objectID”:“ 21986”, “ _highlightResult”:{ “名称”: { “ value”:“在 the 雨之前”, “ matchLevel”:“完整” }, “年”: { “ value”:“(2007)”, “ matchLevel”:“无” } }, “ _rankingInfo”:{ “ nbTypos”:0, “ firstMatchedWord”:1 “ proximityDistance”:1 “ userScore”:2384081, “ geoDistance”:0, “ geoPrecision”:1 “ nbExactWords”:2 } }, ...

严格遵守属性的顺序。除非我们没有另外指定,否则结果将按照执行的默认顺序进行排名:

然后,按地理距离排序。仅当查询在给定的地理区域内完成时才使用,并且在这种情况下适用。

然后,按对象中匹配查询词之间的接近度进行排序(由接近度定义,以递增顺序)。如果查询词彼此相邻,则值为1,两个查询词之间有一个单词,值为2,依此类推。

然后,根据匹配的属性和单词在属性中的位置(由firstMatchedWord定义,以递增顺序)进行排序。这是遵循“ attributesToIndex”设置中定义的属性顺序的标准。

然后,对匹配的确切单词数进行排序(由nbExactWords定义,按降序排列)。这通常很重要,因为默认情况下将最后一个查询词解释为前缀。

最后,按用户排序提供了自定义排名(由customScore属性定义,按降序排列)。

这种明确的结果排名方式使您完全了解结果的排名,而反对使用很难理解的浮点值。例如,在前两个结果中,除“ userScore”外,所有整数均相同,这表示使用自定义排名来区分这两个对象。

关于Algolia排名最重要的是,您可以轻松自定义它而无需放弃其他条件。在这种情况下,我们在电影/演员上引入了人气排名,同时仍认为匹配属性在总体排名规则中更为重要。如果我们想考虑自定义排名得分比匹配属性更重要的话,当然可以更改条件的顺序。

更进一步,我们想提供即时搜索,也称为按类型搜索。为此,我们需要将最后一个查询词解释为前缀。

在ElasticSearch中,您可以启用通配符,并在最后一个查询词的末尾添加一个,这是查询“ word w”的示例:

s = Tire.search“ imdb”做 查询做 字符串“ world w *”, :default_operator =>“ AND”, :analyze_wildcard =>是, :fields => [“ name ^ 5”,“ year ^ 2”,“ episode”] 结束 排序 通过:rank,“ asc” 由:_score 结束 结束

ElasticSearch中的通配符并不精确,并且是以近似方式执行的(前缀查询可以扩展到的单词数有限制)。

在阿尔戈利亚,前缀查询是精确的,不执行任何近似。默认情况下,最后一个查询词被视为前缀,但是您可以轻松地更改此行为,以将所有词在索引设置中解释为前缀:

在这两种产品中,错字容忍度(或模糊搜索)由查询词和命中词之间的Levenshtein距离定义。对于亚洲语言(中文,日文,韩文),已知Levenshtein距离效率低下,Algolia采用了不同的策略,例如将简体中文的转换视为繁体中文的错字,反之亦然。

在Elasticsearch中,您可以在每个查询词上使用Lucene模糊运算符'〜'来应用Levenshtein距离和您要容许的错别字数量(例如'george〜1 clooney〜1'意味着您可以容忍一个错字(乔治)和1个错字(克隆人))。

不幸的是,您不能将模糊运算符与通配符运算符结合使用,因此您不能对前缀应用模糊搜索,从而在最后一个查询词上具有拼写错误。为了保留即时搜索功能,我们对最后一个查询词进行了通配符搜索,并对其他查询词进行了模糊搜索。

在Algolia中,错字公差是即开即用的,您可以使用以下两个索引设置定义允许一两个错字所需的单词大小:

minWordSizefor1Typo:一个单词/前缀中可忍受一种错字的最小字母数(默认值为3)

minWordSizefor2Typos:一个单词/前缀中允许两个错字的最小字母数(默认为7)

例如,这是ElasticSearch中查询'alexandre〜1 b *'的第一个结果:

{ “ _index”:“ imdb”, “ _type”:“ imdb”, “ _id”:“ 2152873”, “ _score”:1.8949691, “_资源”: { “ name”:“ Alexandra Breckenridge”, “ url”:“ /名称/ nm1020036 /”, “等级”:1015 }, “突出显示”:{ “名称”: [ “ Alexandra 布雷肯里奇” ] }, “排序”:[ 1015, 1.8949691 ] }

{ “ name”:“ Alexandre Bustillo”, “ url”:“ /名称/ nm2376614 /”, “等级”:19110, “ objectID”:“ 2070968”, “ _highlightResult”:{ “名称”: { “ value”:“ Alexandre B ustillo”, “ matchLevel&#