lasticearch权威指南:深入搜索(下)腾讯云开发者社区

敏锐的读者会注意,目前为止本书介绍的所有查询都是针对整个词的操作。为了能匹配,只能查找倒排索引中存在的词,最小的单元为单个词。

但如果想匹配部分而不是全部的词该怎么办? 部分匹配 允许用户指定查找词的一部分并找出所有包含这部分片段的词。

与想象的不太一样,对词进行部分匹配的需求在全文搜索引擎领域并不常见,但是如果读者有 SQL 方面的背景,可能会在某个时候实现一个 低效的全文搜索 用下面的 SQL 语句对全文进行搜索:

当然, Elasticsearch 提供分析过程,倒排索引让我们不需要使用这种粗笨的技术。为了能应对同时匹配 “fox” 和 “foxes” 的情况,只需简单的将它们的词干作为索引形式,没有必要做部分匹配。

也就是说,在某些情况下部分匹配会比较有用, 常见的应用如下:

本章始于检验 not_analyzed 精确值字段的前缀匹配。

我们会使用美国目前使用的邮编形式(United Kingdom postcodes 标准)来说明如何用部分匹配查询结构化数据。 这种邮编形式有很好的结构定义。例如,邮编 W1V 3DG 可以分解成如下形式:

假设将邮编作为 not_analyzed 的精确值字段索引,所以可以为其创建索引,如下:

然后索引一些邮编:

现在这些数据已可查询。

为了找到所有以 W1 开始的邮编,可以使用简单的 prefix 查询:

prefix 查询是一个词级别的底层的查询,它不会在搜索之前分析查询字符串,它假定传入前缀就正是要查找的前缀。

默认状态下, prefix 查询不做相关度评分计算,它只是将所有匹配的文档返回,并为每条结果赋予评分值 1 。它的行为更像是过滤器而不是查询。 prefix 查询和 prefix 过滤器这两者实际的区别就是过滤器是可以被缓存的,而查询不行。

之前已经提过:“只能在倒排索引中找到存在的词”,但我们并没有对这些邮编的索引进行特殊处理,每个邮编还是以它们精确值的方式存在于每个文档的索引中,那么 prefix 查询是如何工作的呢?

回想倒排索引包含了一个有序的唯一词列表(本例是邮编)。 对于每个词,倒排索引都会将包含词的文档 ID 列入 倒排表(postings list) 。与示例对应的倒排索引是:

为了支持前缀匹配,查询会做以下事情:

这对于小的例子当然可以正常工作,但是如果倒排索引中有数以百万的邮编都是以 W1 开头时,前缀查询则需要访问每个词然后计算结果!

前缀越短所需访问的词越多。如果我们要以 W 作为前缀而不是 W1 ,那么就可能需要做千万次的匹配。

prefix查询或过滤对于一些特定的匹配是有效的,但使用方式还是应当注意。 当字段中词的集合很小时,可以放心使用,但是它的伸缩性并不好,会对我们的集群带来很多压力。可以使用较长的前缀来限制这种影响,减少需要访问的量。

本章后面会介绍另一个索引时的解决方案,这个方案能使前缀匹配更高效,不过在此之前,需要先看看两个相关的查询: wildcard 和 regexp (模糊和正则)。

与 prefix 前缀查询的特性类似, wildcard 通配符查询也是一种底层基于词的查询, 与前缀查询不同的是它允许指定匹配的正则式。它使用标准的 shell 通配符查询: ? 匹配任意字符, * 匹配 0 或多个字符。

这个查询会匹配包含 W1F 7HW 和 W2F 8HW 的文档:

设想如果现在只想匹配 W 区域的所有邮编,前缀匹配也会包括以 WC 开头的所有邮编,与通配符匹配碰到的问题类似,如果想匹配只以 W 开始并跟随一个数字的所有邮编, regexp 正则式查询允许写出这样更复杂的模式:

wildcard 和 regexp 查询的工作方式与 prefix 查询完全一样,它们也需要扫描倒排索引中的词列表才能找到所有匹配的词,然后依次获取每个词相关的文档 ID ,与 prefix 查询的唯一不同是:它们能支持更为复杂的匹配模式。

这也意味着需要同样注意前缀查询存在性能问题,对有很多唯一词的字段执行这些查询可能会消耗非常多的资源,所以要避免使用左通配这样的模式匹配(如: *foo 或 .*foo 这样的正则式)。

数据在索引时的预处理有助于提高前缀匹配的效率,而通配符和正则表达式查询只能在查询时完成,尽管这些查询有其应用场景,但使用仍需谨慎。

prefix 、 wildcard 和 regexp 查询是基于词操作的,如果用它们来查询 analyzed 字段,它们会检查字段里面的每个词,而不是将字段作为整体来处理。比方说包含 “Quick brown fox” (快速的棕色狐狸)的 title 字段会生成词: quick 、 brown 和 fox 。

把邮编的事情先放一边,让我们先看看前缀查询是如何在全文查询中起作用的。 用户已经渐渐习惯在输完查询内容之前,就能为他们展现搜索结果,这就是所谓的 即时搜索(instant search) 或 输入即搜索(search-as-you-type) 。不仅用户能在更短的时间内得到搜索结果,我们也能引导用户搜索索引中真实存在的结果。

例如,如果用户输入 johnnie walker bl ,我们希望在它们完成输入搜索条件前就能得到:Johnnie Walker Black Label 和 Johnnie Walker Blue Label 。

生活总是这样,就像猫的花色远不只一种!我们希望能找到一种最简单的实现方式。并不需要对数据做任何准备,在查询时就能对任意的全文字段实现 输入即搜索(search-as-you-type) 的查询。

这种查询的行为与 match_phrase 查询一致,不同的是它将查询字符串的最后一个词作为前缀使用,换句话说,可以将之前的例子看成如下这样:

如果通过 validate-query API 运行这个查询查询,explanation 的解释结果为:

但是只有查询字符串的最后一个词才能当作前缀使用。

可以通过设置 max_expansions 参数来限制前缀扩展的影响, 一个合理的值是可能是 50 :

参数 max_expansions 控制着可以与前缀匹配的词的数量,它会先查找第一个与前缀 bl 匹配的词,然后依次查找搜集与之匹配的词(按字母顺序),直到没有更多可匹配的词或当数量超过 max_expansions 时结束。max_expansions是作用在分片级别(shard level)的,这意味着即使设置为1,依然有可能匹配到多个词,这些词来自不同的分片(shards)。这种行为使得结果看起来跟max_expansions没生效一样,因此谨记计算返回搜索结果的关键词数量不能作为检验max_expansions是否生效的方法。

不要忘记,当用户每多输入一个字符时,这个查询又会执行一遍,所以查询需要快,如果第一个结果集不是用户想要的,他们会继续输入直到能搜出满意的结果为止。

到目前为止,所有谈论过的解决方案都是在 查询时(query time) 实现的。 这样做并不需要特殊的映射或特殊的索引模式,只是简单使用已经索引的数据。

查询时的灵活性通常会以牺牲搜索性能为代价,有时候将这些消耗从查询过程中转移到别的地方是有意义的。在实时 web 应用中, 100 毫秒可能是一个难以忍受的巨大延迟。

可以通过在索引时处理数据提高搜索的灵活性以及提升系统性能。为此仍然需要付出应有的代价:增加的索引空间与变慢的索引能力,但这与每次查询都需要付出代价不同,索引时的代价只用付出一次。用户会感谢我们。

之前提到:“只能在倒排索引中找到存在的词。” 尽管 prefix 、 wildcard 、 regexp 查询告诉我们这种说法并不完全正确,但单个词的查找 确实 要比在词列表中盲目挨个查找的效率要高得多。 在搜索之前准备好供部分匹配的数据可以提高搜索的性能。

在索引时准备数据意味着要选择合适的分析链,这里部分匹配使用的工具是 n-gram 。可以将 n-gram 看成一个在词语上 滑动窗口 , n 代表这个 “窗口” 的长度。如果我们要 n-gram quick 这个词 —— 它的结果取决于 n 的选择长度:

可能会注意到这与用户在搜索时输入 “quick” 的字母次序是一致的,换句话说,这种方式正好满足即时搜索(instant search)!

Analyzer包含两个核心组件,Tokenizer以及TokenFilter。两者的区别在于,前者在字符级别处理流,而后者则在词语级别处理流。Tokenizer是Analyzer的第一步,其构造函数接收一个Reader作为参数,而TokenFilter则是一个类似的拦截器,其参数可以是TokenStream、Tokenizer。

第一步需要配置一个自定义的 edge_ngram token 过滤器,称为 autocomplete_filter :

这个配置的意思是:对于这个 token 过滤器接收的任意词项,过滤器会为之生成一个最小固定值为 1 ,最大为 20 的 n-gram 。然后会在一个自定义分析器 autocomplete 中使用上面这个 token 过滤器:

这个分析器使用 standard 分词器将字符串拆分为独立的词,并且将它们都变成小写形式,然后为每个词生成一个边界 n-gram,这要感谢 autocomplete_filter 起的作用。

创建索引、实例化 token 过滤器和分析器的完整示例如下:

可以拿 analyze API 测试这个新的分析器确保它行为正确:

结果表明分析器能正确工作,并返回以下词:

可以用 update-mapping API 将这个分析器应用到具体字段:

现在创建一些测试文档:

如果使用简单 match 查询测试查询 “brown fo” :

可以看到两个文档同时 都能 匹配,尽管 Yellow furballs 这个文档并不包含 brown 和 fo :

如往常一样, validate-query API 总能提供一些线索:

explanation 表明查询会查找边界 n-grams 里的每个词:

name:f 条件可以满足第二个文档,因为 furballs 是以 f 、 fu 、 fur 形式索引的。回过头看这并不令人惊讶,相同的 autocomplete 分析器同时被应用于索引时和搜索时,这在大多数情况下是正确的,只有在少数场景下才需要改变这种行为。

我们需要保证倒排索引表中包含边界 n-grams 的每个词,但是我们只想匹配用户输入的完整词组( brown 和 fo ), 可以通过在索引时使用 autocomplete 分析器,并在搜索时使用 standard 标准分析器来实现这种想法,只要改变查询使用的搜索分析器 analyzer 参数即可:

换种方式,我们可以在映射中,为 name 字段分别指定 index_analyzer 和 search_analyzer 。因为我们只想改变 search_analyzer ,这里只要更新现有的映射而不用对数据重新创建索引:

如果再次请求 validate-query API ,当前的解释为:

再次执行查询就能正确返回 Brown foxes 这个文档。

因为大多数工作是在索引时完成的,所有的查询只要查找brown和fo这两个词,这比使用match_phrase_prefix 查找所有以fo开始的词的方式要高效许多。

使用边界 n-grams 进行输入即搜索(search-as-you-type)的查询设置简单、灵活且快速,但有时候它并不够快,特别是当试图立刻获得反馈时,延迟的问题就会凸显,很多时候不搜索才是最快的搜索方式。

Elasticsearch 里的 completion suggester 采用与上面完全不同的方式,需要为搜索条件生成一个所有可能完成的词列表,然后将它们置入一个 有限状态机(finite state transducer) 内,这是个经优化的图结构。为了搜索建议提示,Elasticsearch 从图的开始处顺着匹配路径一个字符一个字符地进行匹配,一旦它处于用户输入的末尾,Elasticsearch 就会查找所有可能结束的当前路径,然后生成一个建议列表。

本数据结构存于内存中,能使前缀查找非常快,比任何一种基于词的查询都要快很多,这对名字或品牌的自动补全非常适用,因为这些词通常是以普通顺序组织的:用 “Johnny Rotten” 而不是 “Rotten Johnny” 。

当词序不是那么容易被预见时,边界 n-grams 比完成建议者(Completion Suggester)更合适。即使说不是所有猫都是一个花色,那这只猫的花色也是相当特殊的。

keyword 分词器是一个非操作型分词器,这个分词器不做任何事情,它接收的任何字符串都会被原样发出,因此它可以用来处理 not_analyzed 的字段值,但这也需要其他的一些分析转换,如将字母转换成小写。

下面示例使用 keyword 分词器将邮编转换成 token 流,这样就能使用边界 n-gram token 过滤器:

最后,来看看 n-gram 是如何应用于搜索复合词的语言中的。 德语的特点是它可以将许多小词组合成一个庞大的复合词以表达它准确或复杂的意义。例如:

有些人希望在搜索 “Wörterbuch”(字典)的时候,能在结果中看到 “Aussprachewörtebuch”(发音字典)。同样,搜索 “Adler”(鹰)的时候,能将 “Weißkopfseeadler”(秃鹰)包括在结果中。

假设某个 n-gram 是一个词上的滑动窗口,那么任何长度的 n-gram 都可以遍历这个词。我们既希望选择足够长的值让拆分的词项具有意义,又不至于因为太长而生成过多的唯一词。一个长度为 3 的 trigram 可能是一个不错的开始:

使用 analyze API 测试 trigram 分析器:

返回以下词项:

索引前述示例中的复合词来测试:

“Adler”(鹰)的搜索转化为查询三个词 adl 、 dle 和 ler :

正好与 “Weißkopfsee-adler” 相匹配:

类似查询 “Gesundheit”(健康)可以与 “Welt-gesundheit-sorganisation” 匹配,同时也能与 “Militär-ges-chichte” 和 “Rindfleischetikettierungsüberwachungsaufgabenübertragungs-ges-etz” 匹配,因为它们同时都有 trigram 生成的 ges :

使用合适的 minimum_should_match 可以将这些奇怪的结果排除,只有当 trigram 最少匹配数满足要求时,文档才能被认为是匹配的:

这有点像全文搜索中霰弹枪式的策略,可能会导致倒排索引内容变多,尽管如此,在索引具有很多复合词的语言,或词之间没有空格的语言(如:泰语)时,它仍不失为一种通用且有效的方法。

全文相关的公式或 相似算法(similarity algorithms) 会将多个因素合并起来,为每个文档生成一个相关度评分 _score 。本章中,我们会验证各种可变部分,然后讨论如何来控制它们。

当然,相关度不只与全文查询有关,也需要将结构化的数据考虑其中。可能我们正在找一个度假屋,需要一些的详细特征(空调、海景、免费 WiFi ),匹配的特征越多相关度越高。可能我们还希望有一些其他的考虑因素,如回头率、价格、受欢迎度或距离,当然也同时考虑全文查询的相关度。

所有的这些都可以通过 Elasticsearch 强大的评分基础来实现。

布尔模型(Boolean Model) 只是在查询中使用 AND 、 OR 和 NOT (与、或和非)这样的条件来查找匹配的文档,以下查询:

会将所有包括词 full 、 text 和 search ,以及 elasticsearch 或 lucene 的文档作为结果集。

这个过程简单且快速,它将所有可能不匹配的文档排除在外。

当匹配到一组文档后,需要根据相关度排序这些文档,不是所有的文档都包含所有词,有些词比其他的词更重要。一个文档的相关度评分部分取决于每个查询词在文档中的 权重 。

词频:词在文档中出现的频度是多少? 频度越高,权重 越高 。 5 次提到同一词的字段比只提到 1 次的更相关。词频的计算方式如下:

如果不在意词在某个字段中出现的频次,而只在意是否出现过,则可以在字段映射中禁用词频统计:

反向文档频:词在集合所有文档里出现的频率是多少?频次越高,权重 越低 。 常用词如 and 或 the 对相关度贡献很少,因为它们在多数文档中都会出现,一些不常见词如 elastic 或 hippopotamus 可以帮助我们快速缩小范围找到感兴趣的文档。逆向文档频率的计算公式如下:

字段长度归一值:字段的长度是多少? 字段越短,字段的权重 越高 。如果词出现在类似标题 title 这样的字段,要比它出现在内容 body 这样的字段中的相关度更高。字段长度的归一值公式如下:

字段长度的归一值对全文搜索非常重要, 许多其他字段不需要有归一值。无论文档是否包括这个字段,索引中每个文档的每个 string 字段都大约占用 1 个 byte 的空间。对于 not_analyzed 字符串字段的归一值默认是禁用的,而对于 analyzed 字段也可以通过修改字段映射禁用归一值:

对于有些应用场景如日志,归一值不是很有用,要关心的只是字段是否包含特殊的错误码或者特定的浏览器唯一标识符。字段的长度对结果没有影响,禁用归一值可以节省大量内存空间。

以下三个因素——词频(term frequency)、逆向文档频率(inverse document frequency)和字段长度归一值(field-length norm)——是在索引时计算并存储的。 最后将它们结合在一起计算单个词在特定文档中的 权重 。

前面公式中提到的 文档 实际上是指文档里的某个字段,每个字段都有它自己的倒排索引,因此字段的 TF/IDF 值就是文档的 TF/IDF 值。

以上请求(简化)的 explanation 解释如下:

当然,查询通常不止一个词,所以需要一种合并多词权重的方式——向量空间模型(vector space model)。

向量空间模型(vector space model) 提供一种比较多词查询的方式,单个评分代表文档与查询的匹配程度,为了做到这点,这个模型将文档和查询都以 向量(vectors) 的形式表示:

向量实际上就是包含多个数的一维数组,例如:

尽管 TF/IDF 是向量空间模型计算词权重的默认方式,但不是唯一方式。Elasticsearch 还有其他模型如 Okapi-BM25 。TF/IDF 是默认的因为它是个经检验过的简单又高效的算法,可以提供高质量的搜索结果。

现在,设想我们有三个文档:

向量之间是可以比较的,只要测量查询向量和文档向量之间的角度就可以得到每个文档的相关度,文档 1 与查询之间的角度最大,所以相关度低;文档 2 与查询间的角度较小,所以更相关;文档 3 与查询的角度正好吻合,完全匹配。

现在已经讲完评分计算的基本理论,我们可以继续了解 Lucene 是如何实现评分计算的。

一个多词查询

会在内部被重写为:

bool 查询实现了布尔模型,在这个例子中,它会将包括词 quick 和 fox 或两者兼有的文档作为查询结果。

只要一个文档与查询匹配,Lucene 就会为查询计算评分,然后合并每个匹配词的评分结果。这里使用的评分计算公式叫做 实用评分函数(practical scoring function) 。看似很高大上,但是别被吓到——多数的组件都已经介绍过,下一步会讨论它引入的一些新元素。

查询归一因子 ( queryNorm )试图将查询 归一化 , 这样就能将两个不同的查询结果相比较。

尽管查询归一值的目的是为了使查询结果之间能够相互比较,但是它并不十分有效,因为相关度评分 _score 的目的是为了将当前查询的结果进行排序,比较不同查询结果的相关度评分没有太大意义。

这个因子是在查询过程的最前面计算的,具体的计算依赖于具体查询,一个典型的实现如下:

相同查询归一化因子会被应用到每个文档,不能被更改,总而言之,可以被忽略。

协调因子 ( coord ) 可以为那些查询词包含度高的文档提供奖励,文档里出现的查询词越多,它越有机会成为好的匹配结果。

设想查询 quick brown fox ,每个词的权重都是 1.5 。如果没有协调因子,最终评分会是文档里所有词权重的总和。例如:

协调因子将评分与文档里匹配词的数量相乘,然后除以查询里所有词的数量,如果使用协调因子,评分会变成:

协调因子能使包含所有三个词的文档比只包含两个词的文档评分要高出很多。回想将查询 quick brown fox 重写成 bool 查询的形式:

bool 查询默认会对所有 should 语句使用协调功能,不过也可以将其禁用。为什么要这样做?通常的回答是——无须这样。查询协调通常是件好事,当使用 bool 查询将多个高级查询如 match 查询包裹的时候,让协调功能开启是有意义的,匹配的语句越多,查询请求与返回文档间的重叠度就越高。

但在某些高级应用中,将协调功能关闭可能更好。设想正在查找同义词 jump 、 leap 和 hop 时,并不关心会出现多少个同义词,因为它们都表示相同的意思,实际上,只有其中一个同义词会出现,这是不使用协调因子的一个好例子:

我们不建议在建立索引时对字段提升权重,有以下原因:

查询时赋予权重 是更为简单、清楚、灵活的选择。

查询时的权重提升 是可以用来影响相关度的主要工具,任意类型的查询都能接受 boost 参数。 将 boost 设置为 2 ,并不代表最终的评分 _score 是原值的两倍;实际的权重值会经过归一化和一些其他内部优化过程。尽管如此,它确实想要表明一个提升值为 2 的句子的重要性是提升值为 1 语句的两倍。

当在多个索引中搜索时, 可以使用参数 indices_boost 来提升整个索引的权重。在下面例子中,当要为最近索引的文档分配更高权重时,可以这么做:

Elasticsearch 的查询表达式相当灵活, 可以通过调整查询结构中查询语句的所处层次,从而或多或少改变其重要性。比如,设想下面这个查询:

可以将所有词都放在 bool 查询的同一层中:

这个查询可能最终给包含 quick 、 red 和 brown 的文档评分与包含 quick 、 red 、 fox 文档的评分相同,这里 Red 和 brown 是同义词,可能只需要保留其中一个,而我们真正要表达的意思是想做以下查询:

上述查询有个更好的方式:

现在, red 和 brown 处于相互竞争的层次, quick 、 fox 以及 red OR brown 则是处于顶层且相互竞争的词。

在互联网上搜索 “Apple”,返回的结果很可能是一个公司、水果和各种食谱。 我们可以在 bool 查询中用 must_not 语句来排除像 pie 、 tart 、 crumble 和 tree 这样的词,从而将查询结果的范围缩小至只返回与 “Apple” (苹果)公司相关的结果:

谁又敢保证在排除 tree 或 crumble 这种词后,不会错失一个与苹果公司特别相关的文档呢?有时, must_not 条件会过于严格。

它接受 positive 和 negative 查询。只有那些匹配 positive 查询的文档罗列出来,对于那些同时还匹配 negative 查询的文档将通过文档的原始 _score 与 negative_boost 相乘的方式降级后的结果。

为了达到效果, negative_boost 的值必须小于 1.0 。在这个示例中,所有包含负向词的文档评分 _score 都会减半。

有时候我们根本不关心 TF/IDF , 只想知道一个词是否在某个字段中出现过。可能搜索一个度假屋并希望它能尽可能有以下设施:

这个度假屋的文档如下:

可以用简单的 match 查询进行匹配:

但这并不是真正的 全文搜索 ,此种情况下,TF/IDF 并无用处。我们既不关心 wifi 是否为一个普通词,也不关心它在文档中出现是否频繁,关心的只是它是否曾出现过。实际上,我们希望根据房屋不同设施的数量对其排名——设施越多越好。如果设施出现,则记 1 分,不出现记 0 分。

或许不是所有的设施都同等重要——对某些用户来说有些设施更有价值。如果最重要的设施是游泳池,那我们可以为更重要的设施增加权重:

我们可以给 features 字段加上 not_analyzed 类型来提升度假屋文档的匹配能力:

可以采用与之前相同的方法 constant_score 查询来解决这个问题:

实际上,每个设施都应该看成一个过滤器,对于度假屋来说要么具有某个设施要么没有——过滤器因为其性质天然合适。而且,如果使用过滤器,我们还可以利用缓存。

这里的问题是:过滤器无法计算评分。这样就需要寻求一种方式将过滤器和查询间的差异抹平。 function_score 查询不仅正好可以扮演这个角色,而且有更强大的功能。

实际上,也能用过滤器对结果的 子集 应用不同的函数,这样一箭双雕:既能高效评分,又能利用过滤器缓存。

Elasticsearch 预定义了一些函数:

为每个文档应用一个简单而不被规范化的权重提升值:当 weight 为 2 时,最终结果为 2 * _score 。

使用这个值来修改 _score ,如将 popularity 或 votes (受欢迎或赞)作为考虑因素。

为每个用户都使用一个不同的随机评分对结果排序,但对某一具体用户来说,看到的顺序始终是一致的。

将浮动值结合到评分 _score 中。例如结合 publish_date 获得最近发布的文档,结合 geo_location 获得更接近某个具体经纬度(lat/lon)地点的文档,结合 price 获得更接近某个特定价格的文档。

如果需求超出以上范围时,用自定义脚本可以完全控制评分计算,实现所需逻辑。

如果没有 function_score 查询,就不能将全文查询与最新发生这种因子结合在一起评分,而不得不根据评分 _score 或时间 date 进行排序;这会相互影响抵消两种排序各自的效果。这个查询可以使两个效果融合:可以仍然根据全文相关度进行排序,但也会同时考虑最新发布文档、流行文档、或接近用户希望价格的产品。正如所设想的,查询要考虑所有这些因素会非常复杂,让我们先从简单的例子开始,然后顺着梯子慢慢向上爬,增加复杂度。

设想有个网站供用户发布博客并且可以让他们为自己喜欢的博客点赞, 我们希望将更受欢迎的博客放在搜索结果列表中相对较上的位置,同时全文搜索的评分仍然作为相关度的主要排序依据,可以简单的通过存储每个博客的点赞数来实现它:

在搜索时,可以将 function_score 查询与 field_value_factor 结合使用, 即将点赞数与全文相关度评分结合:

在前面示例中,每个文档的最终评分 _score 都做了如下修改:

图29受欢迎度的线性关系基于 _score 的原始值 2.0

一种融入受欢迎度更好方式是用 modifier 平滑 votes 的值。 换句话说,我们希望最开始的一些赞更重要,但是其重要性会随着数字的增加而降低。 0 个赞与 1 个赞的区别应该比 10 个赞与 11 个赞的区别大很多。

对于上述情况,典型的 modifier 应用是使用 log1p 参数值,公式如下:

图 30. 受欢迎度的对数关系基于 _score 的原始值 2.0

带 modifier 参数的请求如下:

可以通过将 votes 字段与 factor 的积来调节受欢迎程度效果的高低。

添加了 factor 会使公式变成这样:

图 31. 受欢迎度的对数关系基于多个不同因子

multiply:评分 _score 与函数值的积(默认)

sum:评分 _score 与函数值的和

min:评分 _score 与函数值间的较小值

max:评分 _score 与函数值间的较大值

replace:函数值替代评分 _score

与使用乘积的方式相比,使用评分 _score 与函数值求和的方式可以弱化最终效果,特别是使用一个较小 factor 因子时。

图 32. 使用 sum 结合受欢迎程度

max_boost 只对函数的结果进行限制,不会对最终评分 _score 产生直接影响。

到目前为止,我们展现的都是为所有文档应用单个函数的使用方式,现在会用过滤器将结果划分为多个子集(每个特性一个过滤器),并为每个子集使用不同的函数。

在下面例子中,我们会使用 weight 函数,它与 boost 参数类似可以用于任何查询。有一点区别是 weight 没有被 Luence 归一化成难以理解的浮点数,而是直接被应用。

查询的结构需要做相应变更以整合多个函数:

这个新特性需要注意的地方会在以下小节介绍。

首先要注意的是filter过滤器代替了 query 查询, 在本例中,我们无须使用全文搜索,只想找到city 字段中包含Barcelona的所有文档,逻辑用过滤比用查询表达更清晰。过滤器返回的所有文档的评分_score 的值为 1 。function_score查询接受query 或filter ,如果没有特别指定,则默认使用 match_all 查询。

functions 关键字保持着一个将要被使用的函数列表。 可以为列表里的每个函数都指定一个 filter 过滤器,在这种情况下,函数只会被应用到那些与过滤器匹配的文档。例子中,我们为与过滤器匹配的文档指定权重值 weight 为 1 (为与 pool 匹配的文档指定权重值为 2 )。

每个函数返回一个结果,所以需要一种将多个结果缩减到单个值的方式,然后才能将其与原始评分 _score 合并。评分模式 score_mode 参数正好扮演这样的角色, 它接受以下值:

在本例中,我们将每个过滤器匹配结果的权重weight 求和,并将其作为最终评分结果,所以会使用 sum 评分模式。不与任何过滤器匹配的文档会保有其原始评分, _score 值的为 1 。

你可能会想知道 一致随机评分(consistently random scoring) 是什么,又为什么会使用它。 之前的例子是个很好的应用场景,前例中所有的结果都会返回 1 、 2 、 3 、 4 或 5 这样的最终评分 _score ,可能只有少数房子的评分是 5 分,而有大量房子的评分是 2 或 3 。

作为网站的所有者,总会希望让广告有更高的展现率。在当前查询下,有相同评分 _score 的文档会每次都以相同次序出现,为了提高展现率,在此引入一些随机性可能会是个好主意,这能保证有相同评分的文档都能有均等相似的展现机率。

我们想让每个用户看到不同的随机次序,但也同时希望如果是同一用户翻页浏览时,结果的相对次序能始终保持一致。这种行为被称为 一致随机(consistently random) 。

random_score 函数会输出一个 0 到 1 之间的数, 当种子 seed 值相同时,生成的随机结果是一致的。例如,将用户的会话 ID 作为 seed :

当然,如果增加了与查询匹配的新文档,无论是否使用一致随机,其结果顺序都会发生变化。

很多变量都可以影响用户对于度假屋的选择, 也许用户希望离市中心近点,但如果价格足够便宜,也有可能选择一个更远的住处,也有可能反过来是正确的:愿意为最好的位置付更多的价钱。

如果我们添加过滤器排除所有市中心方圆 1 千米以外的度假屋,或排除所有每晚价格超过 £100 英镑的,我们可能会将用户愿意考虑妥协的那些选择排除在外。

function_score 查询会提供一组 衰减函数(decay functions) , 让我们有能力在两个滑动标准,如地点和价格,之间权衡。

有三种衰减函数—— linear 、 exp 和 gauss (线性、指数和高斯函数),它们可以操作数值、时间以及经纬度地理坐标点这样的字段。所有三个函数都能接受以下参数:

图 33. 衰减函数曲线

在此范围之外,评分开始衰减,衰减率由 scale 值(此例中的值为 5 )和 衰减值 decay (此例中为默认值 0.5 )共同决定。结果是所有三个曲线在 origin +/- (offset + scale) 处的评分都是 0.5 ,即点 30 和 50 处。

linear 、 exp 和 gauss (线性、指数和高斯)函数三者之间的区别在于范围( origin +/- (offset + scale) )之外的曲线形状:

选择曲线的依据完全由期望评分 _score 的衰减速率来决定,即距原点 origin 的值。

回到我们的例子:用户希望租一个离伦敦市中心近( { "lat": 51.50, "lon": 0.12} )且每晚不超过 £100 英镑的度假屋,而且与距离相比, 我们的用户对价格更为敏感,这样查询可以写成:

location 语句可以简单理解为:

price 语句使用了一个小技巧:用户希望选择 £100 英镑以下的度假屋,但是例子中的原点被设置成 £50 英镑,价格不能为负,但肯定是越低越好,所以 £0 到 £100 英镑内的所有价格都认为是比较好的。

如果我们将原点 origin 被设置成 £100 英镑,那么低于 £100 英镑的度假屋的评分会变低,与其这样不如将原点 origin 和偏移量 offset 同时设置成 £50 英镑,这样就能使只有在价格高于 £100 英镑( origin + offset )时评分才会变低。

weight 参数可以被用来调整每个语句的贡献度,权重 weight 的默认值是 1.0 。这个值会先与每个句子的评分相乘,然后再通过 score_mode 的设置方式合并。

最后,如果所有 function_score 内置的函数都无法满足应用场景,可以使用 script_score 函数自行实现逻辑。

计算每个度假屋利润的算法如下:

我们很可能不想用绝对利润作为评分,这会弱化其他如地点、受欢迎度和特性等因子的作用,而是将利润用目标利润 target 的百分比来表示,高于 目标的利润空间会有一个正向评分(大于 1.0 ),低于目标的利润空间会有一个负向分数(小于 1.0 ):

Elasticsearch 里使用 Groovy 作为默认的脚本语言,它与JavaScript很像, 上面这个算法用 Groovy 脚本表示如下:

最终我们将 script_score 函数与其他函数一起使用:

这个查询根据用户对地点和价格的需求,返回用户最满意的文档,同时也考虑到我们对于盈利的要求。

BM25 同样使用词频、逆向文档频率以及字段长归一化,但是每个因子的定义都有细微区别。与其详细解释 BM25 公式,倒不如将关注点放在 BM25 所能带来的实际好处上。

2. 词频饱和度

不幸的是,普通词随处可见, 实际上一个普通词在同一个文档中大量出现的作用会由于该词在 所有 文档中的大量出现而被抵消掉。

Elasticsearch 的 standard 标准分析器( string 字段默认使用)不会移除停用词,因为尽管这些词的重要性很低,但也不是毫无用处。这导致:在一个相当长的文档中,像 the 和 and 这样词出现的数量会高得离谱,以致它们的权重被人为放大。

这就是 非线性词频饱和度(nonlinear term-frequency saturation) 。

图 34. TF/IDF 与 BM25 的词频饱和度

3. 字段长度归一化

BM25 当然也认为较短字段应该有更多的权重,但是它会分别考虑每个字段内容的平均长度,这样就能区分短 title 字段和 长 title 字段。

4. BM25调优

不像 TF/IDF ,BM25 有一个比较好的特性就是它提供了两个可调参数:

在实践中,调试 BM25 是另外一回事, k1 和 b 的默认值适用于绝大多数文档集合,但最优值还是会因为文档集不同而有所区别,为了找到文档集合的最优值,就必须对参数进行反复修改验证。

相似度算法可以按字段指定, 只需在映射中为不同字段选定即可。

目前,Elasticsearch 不支持更改已有字段的相似度算法 similarity 映射,只能通过为数据重新建立索引来达到目的。

配置相似度算法和配置分析器很相似, 自定义相似度算法可以在创建索引时指定。例如:

自定义的相似度算法可以通过关闭索引,更新索引设置,开启索引这个过程进行更新。这样可以无须重建索引又能试验不同的相似度算法配置。

本章介绍了 Lucene 是如何基于 TF/IDF 生成评分的。理解评分过程是非常重要的, 这样就可以根据具体的业务对评分结果进行调试、调节、减弱和定制。

实践中,简单的查询组合就能提供很好的搜索结果,但是为了获得 具有成效 的搜索结果,就必须反复推敲修改前面介绍的这些调试方法。

通常,经过对策略字段应用权重提升,或通过对查询语句结构的调整来强调某个句子的重要性这些方法,就足以获得良好的结果。有时,如果 Lucene 基于词的 TF/IDF 模型不再满足评分需求(例如希望基于时间或距离来评分),则需要更具侵略性的调整。

除此之外,相关度的调试就有如兔子洞,一旦跳进去就很难再出来。 最相关 这个概念是一个难以触及的模糊目标,通常不同人对文档排序又有着不同的想法,这很容易使人陷入持续反复调整而没有明显进展的怪圈。

我们强烈建议不要陷入这种怪圈,而要监控测量搜索结果。监控用户点击最顶端结果的频次,这可以是前 10 个文档,也可以是第一页的;用户不查看首次搜索的结果而直接执行第二次查询的频次;用户来回点击并查看搜索结果的频次,等等诸如此类的信息。

一旦有了这些监控手段,想要调试查询就并不复杂,稍作调整,监控用户的行为改变并做适当反复尝试。本章介绍的一些工具就只是工具而已,要想物尽其用并将搜索结果提高到 极高的 水平,唯一途径就是需要具备能评价度量用户行为的强大能力。

THE END
0.人工智能套件8.3.0使用指南(for华为云Stack8.3.0)01表4-59 Query 参数 参数 是否必选 参数类型 描述 speaker_id 是 string 说话人id。长度为1-256的字符串,只能 包含字母、数字、下划线。文档版本 01 (2023-09-30) 版权所有 © 华为云计算技术有限公司 45 人工智能套件API 参考 4 SIS 接口请求参数jvzquC41uwvqq{y0jwgxgr3eqo5fp}jtrtotg8j1fud1NIQE3712<793:<@unhvkqt>m953
1.teens6pron青少年英语发音指南,掌握地道口语技巧,快速提升流利度核心:建立集中化的元数据仓库,采集并管理业务术语、技术表结构、字段含义、数据血缘、数据沿革等信息。 价值:提升数据可发现性、可理解性、可信任度;支撑影响分析(上游变更对下游影响)、根因分析(数据问题溯源)。 PM关注点:推动工具落地;确保关键业务系统元数据被采集;设计易用的数据目录供业务人员查询;推广血缘分析jvzq<84o0nqg{s3ep1kolx~133711?;6;64ivv
2.盘点深圳泰语培训班榜单发布,建议查看泰语好学吗 学习泰语的难易程度因人而异,但总体来看,泰语作为一种声调语言,具有其独特的挑战性。首先,泰语有五个声调,这使得同一个单词的发音不同可能意味着完全不同的意思,这对于初学者来说可能较为困难。其次,泰语的字母表与拉丁字母大相径庭,学员需要适应新的书写系统,这也增加了学习的难度。然而,泰语的语法jvzquC41o0=78ry0eqs0pn|u15613970jvsm
3.AIkit通用XTTS合成LinuxSDK文档|讯飞开放平台文档中心reg 英文发音方式 int 0:引擎自动判断, 1:按字母发音, 2:按单词发音 否 0 rdn 数字发音方式 int 0:引擎自动判断, 1:按数字发音, 2:按字符串发音 否 0 textEncoding 文本编码 string GBK:GBK编码, UTF-8:UTF-8编码, Unicode:Unicode编码 是 AIKIT_Start 方法说明:参数类型必填jvzquC41yy}/zo~wp0io1mte1vzt1JNmkvephoqkpgeuv|4Nkp{y/\IM0jznn
4.Forvo——发音指南的语言列表和代码选择你的语言并快速发音 评价或讨论其他用户的发音 跟踪你添加和发音的词语 下载mp3格式的音频文件 免费加入 找不到你的语言吗? 请告诉我们选择你的语言: Deutsch English Español Français Italiano 日本語 Nederlands Polski Português Русский Türkçe 汉语 其他语言 博客 App Forvo Kids 视频 APIjvzquC41|j4gq{{q0eun1ufpiwghg|2eqfkt1