相关性检索和组合查询

·  阅读 22

在全文检索中,检索结果与查询条件的相关性是一个极为重要的问题,优秀 的全文检索引擎应该将那些与查询条件相关性高的文档排在最前面。想象一下。 如果满足查询条件的文档成千上万,让用户在这些文档中再找出自己最满意的那 一条,这无异于再做一次人工检索。用户一般很少会有耐心在检索结果中翻到第 3 页,所以处理好检索结果的相关性对于检索引擎来说至关重要。Google 公司就 是因为发明了 Page Rank 算法,巧妙地解决了网页检索结果的相关性问题,才在 众多搜索公司中迅速崛起。

相关性问题有两方面问题要解决, 一是如何评价单个查询条件的相关性, 二是如何将多个查询条件的相关性组合起来。

相关性评分

全文检索与数据库查询的一个显著区别, 就是它并不一定会根据查询条件 做完全精确的匹配。除了模糊查询以外,全文检索还会根据查询条件给文档的相 关性打分并排序,将那些与查询条件相关性高的文档排在最前面。相关性 ( Relevance)或相似性(Similarity)是指两个事物间相互关联的程度,在检索领城特 指检索请求与检索结果之间的相关程度。在 Elaticsearch 返回的每条结果中都会 包含一个_ score 字段,这个字段的值就是当前文档匹配检索请求的相关性评分, 我们也可以称为相关度。

解决相关性问题的核心是计算相关度的算法和模型,相关度算法和模型是全 文检索引章最重要的技术之一。相关度算法和相关度模型并非完全相同的概念, 相关度模型可以认为是具有相同理论基础的算法集合。所以在实际应用时都是指 定到具体的相关度算法,相关度模型则是从理论层面对相关度算法的归类。

相关度模型

Elasticsearch 支持多种相关度算法,它们通过类型名称来标识,包括 boolean、 BM25、DFR 等等很多。这些算法分别归属于几种不同的理论模型,它们是布尔 模型、向量空间模型、概率模型、语言模型等

布尔模型

布尔模型( Boolean Model)是最简单的相关度模型,最终的相关度只有 1 或 0 两种。如果检索中包含多个查询条件,则查询条件之间的相关度组合方式取决它 们之间的逻辑运算符,即以逻辑运算中的与、或、非组合评分。文档的最终评分 为 1 时会被添加到检索结果中,而评分为 0 时则不会出现在检索结果中。这与使 用 SQL 语句查询数据库有些类似,完全根据查询条件决定结果,非此即彼。在 Elnticearch 支持的相关度算法中,boolean 算法即采用布尔模型,有些地方也部 分地采用了布尔模型。

向量空间模型

向量空间模型(Vector Space Mode)组合多个相关度时采用的是基于向量的算 法。在向量空间模型中,多个查询条件的相关度以向量的形式表示。向量实际上 就是包含多个数的一维数组,例如[1,2, 3,4, 5, 6]就是一个 6 维向量,其中每个 数字都代表一个查询条件的相关度。文档对于 n 个查询条件会形成一个 n 维的向 量空间,如果定义 1 个查询条件最佳匹配的 n 维向量,那么与这个向量越接近则 相关度越高。从向量的角度来看,就是两个向量之间的夹角越小相关度越高,所 以 n 个相关度的组合就转换为向量之间夹角的计算。

简单理解,可以只考虑一个二维向量,也就是查询条件只有两个,这样就可 以将两个相关度映射到二维坐标图的X轴和Y轴上。假设两个查询条件权重相同, 那么最佳匹配值就可以设置为[1, 1]。如果某文档匹配了第一个条件,部分地匹配 了第二个条件,则该文档的向量值为[1, 0.2]。将这两个向量绘制在二维坐标图中, 就得到了它们的夹角。

对于多维向量来说,线性代数提供了余弦近似度算法,专门用于计算两个多 维向量的夹角。

注:余弦相似性通过测量两个向量的夹角的余弦值来度量它们之间的相似性。 0 度角的余弦值是 1,而其他任何角度的余弦值都不大于 1;并且其最小值是-1。 从而两个向量之间的角度的余弦值确定两个向量是否大致指向相同的方向。两个 向量有相同的指向时,余弦相似度的值为 1;两个向量夹角为 90°时,余弦相似 度的值为 0;两个向量指向完全相反的方向时,余弦相似度的值为-1。这结果是 与向量的长度无关的,仅仅与向量的指向方向相关。余弦相似度通常用于正空间, 因此给出的值为-1 到 1 之间。

注意这上下界对任何维度的向量空间中都适用,而且余弦相似性最常用于高 维正空间。例如在信息检索中,每个词项被赋予不同的维度,而一个维度由一个 向量表示,其各个维度上的值对应于该词项在文档中出现的频率。余弦相似度因 此可以给出两篇文档在其主题方面的相似度。

概率模型

概率模型是基于概率论构建的模型,BM25、 DFR、DFI 都属于概率模型中 的一种实现算法, 背后有着非常严谨的概率理论依据。以其中最为流行的 BM25 为例,它背后的概率理论是贝叶斯定理,而这个定理在许多领城中都有广泛的应 用。

BM25 法将检索出来的文档(D)分为相关文档(R)和不相关文档(NR)两类,使用 P(R|D)代表文档属于相关文档的概率,而使用 P(NR|D) 表示文档属于不相关文档 的概率,则当 P(R|D)>P(NR|D)时认为这个文档与用户查询相关。根据贝叶斯公式 将 P(R|D)>P(NR|D)转换为对某个比值的计算。在此基础上再进行一此转换,就可 以得到不同的相关度算法

语言模型

语言模型最早并不是应用于全文检索领域,而是应用于语音识别、机器翻译、 拼写检查等领域。在全文检索中,语言模型为每个文档建立不同的计算模型,用 以判断由文档生成某一查询条件的概率是多少,而这个概率的值就可以认为是相 关度。可见,语言模型的与其他检索模型正好相反,其他检索模型都是从查询条 件查找满足条件的文档,而语言模型则是根据文档推断可能的查询条件。

TF/IDF

对于一篇几百字几千字的文章,如何生成足以准确表示该文章的特征向量 呢?就像论文一样,摘要、关键词毫无疑问就是全篇最核心的内容,因此,我们 要设法提取一篇文档的关键词,并对每个关键词计算其对应的特征权值,从而形 成特征向量。这里涉及一个非常简单但又相当强大的算法,即 TF-IDF 算法

TF/IDF 实际上两个影响相关度的因素,即 TF 和 IDF.其中,TF 是词项频率简 称词频,指一个词项在当前文档中出现的次数,而 IDF 则是逆向文档频率,指词 项在所有文档中出现的次数。

Elasticsearch 提供的几种算法中都或多或少有 TF/IDF 的思想,例如 BM25 算 法虽然是通过概率论推导而来,但最终的计算公式与 TF/IDF 在本质上也是一致 的。

TF/IDF 算法的核心思想是 TF 越高则相关度越高,而 IDF 越高相关度越低。 TF 对相关度的影响比较容易理解,但 IDF 为什么会在词项出现次数多的时候反 而相关度低呢?

举例来说,如果使用"elasticsearch 全文检索"两个词项做检索,文档中 “elasticsearch"出现次数高的文档比“全文检索”出现次数高的文档相关度要高。 这是因为“elasticsearch "是专业性比较强的词汇,更加专有,它在其他文档中出 现的次数会比较少,也就是 IDF 低,而“全文检索”虽然也是专业性词汇,但它 覆盖的面要比“elasticsearch " 更广泛,所以它在其他文档中出现的次数会比较 高,也就是 IDF 高。换句话说,介绍 elasticsearch 的文章大概率会提到全文检索。 但介绍全文检索的文章则不一定会提到 elasticsearch,比如一篇介绍 MongoDB 的文章大概率会提到全文检索,但是这样的文章与“elasticsearch 全文检索”的 相关度不高

可见,在使用 TF/IDF 计算评分时必须要用到词项在文档中出现的频率,即 词频。默认情况下文档 text 类型字段在编入索引时都会记录词频。

Elasticsearch 中的 classic 算法实际上是使用 Lucene 的实用评分函数( Practical Scoring Function) ,这个评分函数结合了布尔模型、TF/IDF 和向量空间模型来共 同计算分值。该算法是早期 Elatisearch 运算相关度的算法,现在已经改为 BM25 了

BM25

BM25 是 Best Match25 的简写,由于最早应用于一个名为 Okapi 的系统中, 所以很多文献中也称之为 Okapi BM25。BM25 算法被认为是当今最先进的相关 度算法之 ,Elasticsearch 文档字段的默认相关度算法就是采用 BM25,它属于概 率模型,依据贝叶斯公式,经过一系列的严格推导以后,得出了一个关于 IDF 的 公式

image.png

同时在这个基础上,最终的公式上加入了对 TF、当前文档的长度、词频饱 和度、长度归一化等因素的考虑

image.png

词频饱和度

所谓词频饱和度指的是当词频超过一定数量之后,它对相关度的影响将趋于 饱和。换句话说,词频 10 次的相关度比词频 1 次的分值要大很多,但 100 次与 10 次之间差距就不会那么明显了。在 BM25 算法中,控制词频饱和度的参数是 k1,默认值为 1.2。参数 k1 的值越小词频对相关度的影响就会越快趋于饱和,而 值越大词频饱和度变化越慢。

举例来说,如果将 k1,设置为 1,词频达到 10 时就会趋于饱和;而当 k1 设 置为 100 时词频在 100 时才会趋于饱和。一般来说 k1 的取值范围为[1.2, 2.0]

长度归一化

一般来说, 查询条件中的词项出现在较短的文本中,比出现在较长的文本 中对结果的相关性影响更大。

举例来说,如果一篇文章的标题中包含 elasticsearch, 那么这篇文章是专门 介绍elasticsearch的可能性比只在文章内容中出现elasticsearch的可能要高很多。 但这种比较其实是建立在两个不同的字段上,而在实际检索时往往是针对相同的 字段做比较。

比如在两篇文章的标题中都出现了 elasticsearch, 那么哪一篇文章的相关度 更高呢?

BM25 针对这种情况对文本长度做了所谓的归一化处理,即考虑当前文档字 段的文本长度与所有文档的字段平均长度的比值,而这个比值就是长度归一化因 素

为了控制长度归一化对相关度的影响,在长度归一化中加了一个控制参数 b。 这个值的取值范围为[0.0, 1.0],取值 0.0 时会禁用归一化,而取值 1.0 则会完全启 用归一化,默认值为 0. 75。

相关度解释

相关度算法可通过 text 或 keyword 类型字段的 similarity 参数修改,也就是 说相关度算法不针对整个文档而是针对单个字段,它的默认值是 BM25。

当然 Elasticsearch 相关度评分比这里介绍的内容要复杂得多,可以通过在查 询时添加 explain 参数查看评分解释。例如:

POST /kibana_sample_data_logs/_search
{
"query": {
"match":{
"message": "chrome"
}
},
"explain": true
}
复制代码

除此之外,通过_explain 接口也可以实现类似的功能,不同的是_explain 接 口查看的是单个文档与检索条件的相关度评分解释。例如:

POST /kibana_sample_data_logs/_explain/ni-ao3IBOcvNJ8V9P3JN 
{
    "query":{
        "match":{
            "message":"chrome"
        }
    }
}
复制代码

我们可以大致了解下 Elasticsearch 是如何进行评分的:

image.png

相关度权重

在一些情况下需要将某些字段的相关度权重提升,以增加这些字段对检索结 果相关性评分的影响。比如,同时使用对文章标题 tile 字段和文章内容 content 字段做检索,tile 字段化相关性评分中的权重应该比 content 字段高一些, 这时 就可以将 tile 字段的相关度评分权重提高。 所以相关度权重提升一般都是在多个查询条件时设置。提升相关度权重有多 种办法,下面分别来看一下

boost 参数

boost 参数可以在创建索引时直接设置给字段,也可以在执行检索时动态更 改。如果不做更改,boost 参数的默认值为 1。但并非所有类型的字段都可以设 置 boost,在创建索引时设置 boost 参数并不是一个好的方法,因为这个参数在 索引创建以后就不能再更改而降低了灵活性,所以在 Elasticsearch 版本 5 中就已 经被废止。

所以更好的方式是检索时提升查询条件的相关度权重,几乎前面介绍的所有 DSL 查询都支持通过 boost 参数设置查询条件的相关度权重。例如:

POST /kibana_sample_data_flights/_search
{
"query": {
"range":{
"AvgTicketPrice": {
"gte": 1000,
"lte": 1200,
"boost": 2
}
}
}
}
复制代码

通过boost参数设置查询条件的相关度权重主要用于多查询条件时调整每个 查询条件在相关度计算中的权重。

组合查询与相关度组合

相关性问题不仅要解决单个查询条件的相关度计算,还要考虑如何将多个查 询条件产生的相关度组合起来。而相关度组合问题主要出现在组合查询中,所以 接下来我们就要了解下组合查询及相关度组合问题。

组合查询可以将通过某种逻辑将子查询组合起来,实现对多个字段与多个查 询条件的任意组合。组合查询组合的子查询不仅可以是基于词项或基于全文的子 查询,也可以是另一个组合查询。

单纯从组合查询的使用上来看,组合查询并不复杂,复杂的是组合多个子查 询相关度的逻辑,这也是它们的核心区别之一。

bool 组合查询

bool 组合查询将一组布尔类型子句组合起来, 形成个大的布尔条件。 通过 SQL 语言查询数据时,如果一条数据不满足 where 子句的查询条件,这条记录 将不会作为结果返回。

但 Elasticsearch 的 bool 组合查询则不同,在它的子句中,一些子句的确会决 定文档是否会作为结果返回,而另一些子句则不决定文档是否可以作为结果,但 会影响到结果的相关度。

bool组合查询可用的布尔类型子句包括must、filter、should 和must _not 四 种,它们接收参数值的类型为数组。影响相关度会影响排序

must 查询结果中必须要包含的内容,影响相关度 
filter 查询结果中必须要包含的内容,不会影响相关度
should 查询结果非必须包含项,包含了会提高分数,影响相关度 
must_not 查询结果中不能包含的内容,不会影响相关度
复制代码

可见,filter 和 must _not 单纯只用于过滤文档,而它们对文档相关度没有 任何影响。换句话说,这两种子句对查询结果的排序没有作用。在这四种子句中, should 子句的情况有些复杂。首先它的执行结果影响相关度,但在是否过滤结 果上则取决于上下文。当 should 子句与 must 子句或 filter 子句同时出现在子句 中时,should 子句将不会过滤结果。也就是说,在这种情况下,即使 should 子 句不满足,结果也会返回。例如:

POST /kibana_sample_data_logs/_search
{
    "query":{
        "bool":{
            "must":[
                {
                    "match":{
                        "message":"firefox"
                    }
                }
            ],
            "should":[
                {
                    "term":{
                        "geo. src":"CN"
                    }
                },
                {
                    "term":{
                        "geo. dest":"CN"
                    }
                }
            ]
        }
    }
}
复制代码

只有 message 字段包含 firefox 词项的日志文档才会被返回,而 geo 的 src 字 段和 dest 字段是否为 CN 只影响相关度,当然相关度越高的肯定排在前面,可以 通过下面的例子观察到:

POST /kibana_sample_data_logs/_search

{
    "query":{
        "bool":{
            "must":[
                {
                    "match":{
                        "message":"firefox"
                    }
                }
            ],
            "should":[
                {
                    "term":{
                        "geo.src":"CN"
                    }
                },
                {
                    "term":{
                        "geo.dest":"CN"
                    }
                }
            ]
        }
    },
    "sort":[
        {
            "_score":{
                "order":"asc"
            }
        }
    ]
}
复制代码

但是如果在查询条件中将 must 子句删除,那么 should 子句就至少要满足有 一条。should 子句需要满足的个数由query的minimum_ should_match参数决定, 默认情况下它的值为 1。

布尔查询在计算相关性得分时,采取了匹配越多分值越高的策略。由于 filter 和 must_not 不参与分值运算,所以文档的最后得分是 must 和 should 子句的相 关性分值相加后返回给用户。同时还可以试试:

POST /kibana_sample_data_logs/_search 

{
    "query":{
        "bool":{
            "must":[
                {
                    "match":{
                        "message":"firefox"
                    }
                }
            ],
            "should":[
                {
                    "term":{
                        "geo. src":"CN"
                    }
                },
                {
                    "term":{
                        "geo. dest":"CN"
                    }
                }
            ],
            "filter":{
                "term":{
                    "extension":"zip"
                }
            }
        }
    }
}

复制代码

可以发现,最终返回的结果中又过滤了一次,extension 的值为 zip 的才会返 回

dis_max 组合查询

dis_max 查询也是种组合查询, 只是它在计算相关性度时与 bool 查询不同。 dis_max 查询在计算相关性分值时,会在子查询中取最大相关性分值为最终相关性分值结果,而忽略其他子查询的相关性得分。dis_max 查询通过 queries 参数 接收对象的数组。例如:

POST /kibana_sample_data_logs/_search
{
    "query":{
        "dis_max":{
            "queries":[
                {
                    "match":{
                        "message":"firefox"
                    }
                },
                {
                    "term":{
                        "geo. src":"CN"
                    }
                },
                {
                    "term":{
                        "geo. dest":"CN"
                    }
                }
            ],
            "tie_breaker":0.7
        }
    }
}
复制代码

在添加了 tie _breaker 参数后,相关度非最高值字段在参与最終相关度结果 时的权重就降低为 0.7。但它们对结果排序会产生影响,完全满足条件的文档将 排在结果最前面。

constant_score 查询

作用:在组合查询中调整子查询的权重。

constant_score 查询返回结果中文档的相关度为固定值,这个固定值由 boost 参数设置,默认值为 1.0。constant score 查询只有两个参数 filter 和 boost, 前者 与 bool 组合查询中的 filter 完全相同,仅用于过滤结果而不影响分值

POST /kibana_sample_data_logs/_search 
{
    "query":{
        "constant_score":{
            "filter":{
                "match":{
                    "geo.src":"CN"
                }
            },
            "boost":1.3
        }
    }
}
复制代码

由于示例中通过 boost 参数设置了相关度,所以满足查询条件文档的 score 值将都是 1.3,match_all 查询也可以当成是一种特殊类型的 constant_score 查询, 它会返回索引中所有文档,面每个文档的相关度都是 1.0。它的作用和 boost 参 数类似,组合查询时调整子查询在相关度计算中的权重。

boosting 查询

boosting 查询通过 positive 子句设置满足条件的文档,这类似于 bool 查询中 的 must 子句,只有满足 positive 条件的文档才会被返回。boosting 查询通过 negative 子句设置需要排除文档的条件, 这类似于 bool 查询中的 must _not 子 旬。但与 bool 查询不同的是,boosting 查询不会将满足 negative 条件的文档从 返回结果中排除,而只是会拉低它们的相关性

POST /kibana_sample_data_logs/_search 

{
    "query":{
        "boosting":{
            "positive":{
                "term":{
                    "geo.src":"CN"
                }
            },
            "negative":{
                "term":{
                    "geo. dest":"CN"
                }
            },
            "negative_boost":0.2
        }
    },
    "sort":[
        {
            "_score":"asc"
        }
    ]
}
复制代码

在示例中,参数 negative_ boost 设置了一个系数,当满足 negative 条件时 相关度会乘以这个系数作为最终分值,所以这个值应该小于 1 而大于等于 0。, 如果 geo_src 为 CN 的文档相关度为 1.6,那么 geo. Dest 字段也是 CN 的文档相关 度就需要再乘以 0.2,所以最终相关度为 0. 32。

function_score 查询

function_score 查询提供了一组计算查询结果相关度的不同函数,通过为查 询条件定义不同打分函数实现文档检索相关性的自定义打分机制。查询条件通过 function_score 的 query 参数设置,而使用的打分函数则使用 functions 参数设置

例如:


POST /kibana_sample_data_logs/_search 
{
    "query":{
        "function_score":{
            "query":{
                "query_string":{
                    "fields":[
                        "message"
                    ],
                    "query":"(firefox 6.0a1) OR (chrome 11.0.696.50)"
                }
            },
            "functions":[
                {
                    "weight":2
                },
                {
                    "random_score":{

                    }
                }
            ],
            "score_mode":"max",
            "boost_mode":"avg"
        }
    }
}
复制代码

function_score 查询在运算相关度时,首先会通过 functions 指定的打分函数 算出每份文档的得分。如果指定了多个打分函数,它们打分的结果会根据 score_mode 参数定义的模式组合起来。

以示例为例,functions 参数定义了两个打分函数,random_score 函数会在 0-1 之间产生一个随机数, 而 weight 函数则会以指定的值为相关性分值。由于 score_mode 参数设置的值为 max,即从所有评分函数运算结果中取最大值,而 weight 值为 2,它将永远大于 random_ score 产生的值,所以评分函数最终给出 的分值也将永远是 2。

score_mode 包括以下几个选项 multiply、sum、avg、first、 max、min, 通过 名称很容易判断它们的含义,分别是在所有评分函数的运算结果中取它们的乘积、 和、平均值、首个值、最大值和最小值。

打分函数运算的相关性评分会与 query 参数中查询条件的相关度组合起来, 组合的方式通过 boost_mode 参数指定,它的默认值与 score_mode 一样都是 multiply。boost_mode 参数的可选值与 score_mode 也基本一致,但没有 first 而 多了一个 replace,代表使用评分函数计算结果代替查询分值。

可见 function_score 是一种在运算相关度上非常灵活的组合查询,这种灵活 性主要体现在它提供了一组打分函数,以及组合这些打分函数的灵活方式。打分 函数包括 weight、 random_score、field_value_factor 以及衰减函数等等。在这 些函数中下面再来简单介绍一下其他打分函数

field_value_factor 函数

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改