评分基础

评分基础 #

处理结构化数据(比如:时间、数字、字符串、枚举)的数据库,只需检查文档(或关系数据库里的行)是否与查询匹配。

布尔的是/非匹配是全文搜索的基础,但不止如此,我们还要知道每个文档与查询的相关度,在全文搜索引擎中不仅需要找到匹配的文档,还需根据它们相关度的高低进行排序。

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

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

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

相关度评分背后的理论 #

Easysearch 使用布尔模型(Boolean Model)查找匹配文档,并用一个名为实用评分函数(practical scoring function)的公式来计算相关度。这个公式借鉴了词频/逆向文档频率(term frequency/inverse document frequency)向量空间模型(vector space model),同时也加入了一些现代的新特性,如字段长度归一化(field length normalization),以及词或查询语句权重提升。

注意:Easysearch 默认使用 BM25 相似度算法,而非经典的 TF/IDF。BM25 基于概率信息检索模型,在大多数场景下效果更好。下面先介绍 TF/IDF 的基本概念(有助于理解评分原理),然后说明 BM25 的改进。

不要紧张!这些概念并没有像它们字面看起来那么复杂,尽管本小节提到了算法、公式和数学模型,但内容还是让人容易理解的,与理解算法本身相比,了解这些因素如何影响结果更为重要。

布尔模型 #

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

full AND text AND search AND (elasticsearch OR lucene)

会将所有包括词 fulltextsearch,以及 elasticsearchlucene 的文档作为结果集。

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

词频/逆向文档频率(TF/IDF) #

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

词的权重由三个因素决定:

词频(Term Frequency) #

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

tf(t in d) = √frequency

t 在文档 d 的词频(tf)是该词在文档中出现次数的平方根。

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

PUT /my_index
{
  "mappings": {
    "properties": {
      "text": {
        "type": "text",
        "index_options": "docs"
      }
    }
  }
}

将参数 index_options 设置为 docs 可以禁用词频统计及词频位置,这个映射的字段不会计算词的出现次数,对于短语或近似查询也不可用。

逆向文档频率(Inverse Document Frequency) #

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

idf(t) = 1 + log ( numDocs / (docFreq + 1))

t 的逆向文档频率(idf)是:索引中文档数量除以所有包含该词的文档数,然后求其对数。

字段长度归一值(Field-Length Norm) #

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

norm(d) = 1 / √numTerms

字段长度归一值(norm)是字段中词数平方根的倒数。

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

PUT /my_index
{
  "mappings": {
    "properties": {
      "text": {
        "type": "text",
        "norms": { "enabled": false }
      }
    }
  }
}

这个字段不会将字段长度归一值考虑在内,长字段和短字段会以相同长度计算评分。

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

结合使用 #

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

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

当用 explain 查看一个简单的 term 查询时,可以发现与计算相关度评分的因子就是前面章节介绍的这些:

PUT /my_index/_doc/1
{ "text" : "quick brown fox" }

GET /my_index/_search?explain
{
  "query": {
    "term": {
      "text": "fox"
    }
  }
}

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

weight(text:fox in 0) [PerFieldSimilarity]:  0.15342641
result of:
    fieldWeight in 0                         0.15342641
    product of:
        tf(freq=1.0), with freq of 1:        1.0
        idf(docFreq=1, maxDocs=1):           0.30685282
        fieldNorm(doc=0):                    0.5
  • fox 在文档的内部 Lucene doc ID 为 0,字段是 text 里的最终评分
  • fox 在该文档 text 字段中只出现了一次
  • fox 在所有文档 text 字段索引的逆向文档频率
  • 该字段的字段长度归一值

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

向量空间模型 #

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

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

[1,2,5,22,3,8]

在向量空间模型里,向量空间模型里的每个数字都代表一个词的权重,与词频/逆向文档频率(term frequency/inverse document frequency)计算方式类似。

提示:尽管 TF/IDF 是向量空间模型计算词权重的默认方式,但不是唯一方式。Easysearch 还有其他模型如 Okapi-BM25。BM25 是 Easysearch 的默认相似度算法,它在 TF/IDF 基础上做了重要改进:词频具有饱和效应(一个词出现 10 次不会比出现 5 次有明显更高的权重),并且通过参数 k1b 可以调节词频饱和度和字段长度归一化的影响。

设想如果查询 “happy hippopotamus”,常见词 happy 的权重较低,不常见词 hippopotamus 权重较高,假设 happy 的权重是 2,hippopotamus 的权重是 5,可以将这个二维向量——[2,5]——在坐标系下作条直线,线的起点是 (0,0) 终点是 (2,5)。

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

  1. I am happy in summer。
  2. After Christmas I’m a hippopotamus
  3. The happy hippopotamus helped Harry。

可以为每个文档都创建包括每个查询词——happyhippopotamus——权重的向量,然后将这些向量置入同一个坐标系中:

  • 文档 1:(happy,____________) —— [2,0]
  • 文档 2:( ___ ,hippopotamus) —— [0,5]
  • 文档 3:(happy,hippopotamus) —— [2,5]

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

提示:在实际中,只有二维向量(两个词的查询)可以在平面上表示,幸运的是,线性代数——作为数学中处理向量的一个分支——为我们提供了计算两个多维向量间角度工具,这意味着可以使用如上同样的方式来解释多个词的查询。关于比较两个向量的更多信息可以参考余弦近似度(cosine similarity)。

Lucene 的实用评分函数 #

对于多词查询,Lucene 使用布尔模型(Boolean model)、TF/IDF 以及向量空间模型(vector space model),然后将它们组合到单个高效的包里以收集匹配文档并进行评分计算。

BM25 vs 经典 TF/IDF:Easysearch 默认使用 BM25 而非经典 TF/IDF。BM25 的实际评分公式为:

score(q,d) = ∑ ( idf(t) · tf_bm25(t in d) · t.getBoost() ) (t in q)

其中 BM25 的词频计算引入了饱和函数:

tf_bm25 = (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * dl / avgdl))
  • k1:控制词频饱和度,默认 1.2。值越大,词频对评分的影响越大
  • b:控制字段长度归一化的影响,默认 0.75。值为 0 时完全忽略字段长度,值为 1 时完全按字段长度归一化
  • dl:当前文档的字段长度(词项数)
  • avgdl:所有文档该字段的平均长度

这比经典 TF/IDF 的 √frequency 更合理——一个词出现 20 次不会比出现 10 次获得显著更高的评分。

一个多词查询:

GET /my_index/_search
{
  "query": {
    "match": {
      "text": "quick fox"
    }
  }
}

会在内部被重写为:

GET /my_index/_search
{
  "query": {
    "bool": {
      "should": [
        {"term": { "text": "quick" }},
        {"term": { "text": "fox"   }}
      ]
    }
  }
}

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

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

在 BM25 下,简化的评分公式为:

score(q,d)  =  
            ∑ (           
                idf(t)²      
              · tf_bm25(t in d)
              · t.getBoost() 
              · norm(t,d)    
            ) (t in q)    
  • score(q,d) 是文档 d 与查询 q 的相关度评分
  • 查询 q 中每个词 t 对于文档 d 的权重和
  • idf(t) 是词 t 的逆向文档频率
  • tf_bm25(t in d) 是词 t 在文档 d 中的 BM25 词频(带饱和效应)
  • t.getBoost() 是查询中使用的 boost
  • norm(t,d) 是字段长度归一值(在 BM25 中已集成到 tf_bm25 公式中)

历史说明:经典 TF/IDF 评分公式中还包含 queryNorm(查询归一化因子)和 coord(协调因子),但这两个因子在 Easysearch(基于 Lucene 8+)中已被移除。如果在 explain 输出中看到相关术语,那是旧版本的遗留信息。

BM25 参数调优 #

Easysearch 默认使用 BM25 相似度,可以在字段映射中自定义其参数:

PUT /my_index
{
  "settings": {
    "index": {
      "similarity": {
        "custom_bm25": {
          "type": "BM25",
          "k1": 1.2,
          "b": 0.75
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "content": {
        "type": "text",
        "similarity": "custom_bm25"
      }
    }
  }
}
参数默认值说明
k11.2控制词频饱和速度。值越大词频影响越大;设为 0 则完全忽略词频
b0.75控制字段长度归一化。0 = 忽略长度;1 = 完全按长度归一化

调优建议

  • 短字段(标题):可降低 b(如 0.3),减少长度惩罚
  • 长字段(正文):保持默认值通常效果最好
  • 如果词频很重要:增大 k1(如 2.0)

其他相似度算法 #

除 BM25 外,Easysearch 还支持以下相似度算法:

算法说明
BM25默认。基于概率模型,带词频饱和
boolean不计算评分,所有匹配文档得分相同
DFR基于随机性偏离(Divergence from Randomness)框架
IB基于信息模型(Information Based)
LMDirichletDirichlet 平滑的语言模型
LMJelinekMercerJelinek-Mercer 平滑的语言模型
DFI基于独立性偏离(Divergence from Independence)
scripted自定义脚本评分

小结 #

  • Easysearch 默认使用 BM25 相似度算法(而非经典 TF/IDF)
  • BM25 考虑词频(带饱和效应)、逆向文档频率和字段长度归一化
  • 可通过 k1b 参数调优 BM25 的行为
  • 布尔模型用于查找匹配文档,BM25 用于计算相关度评分
  • 向量空间模型用于比较多词查询
  • 经典 TF/IDF 中的协调因子和查询归一化因子已被移除

下一步可以继续阅读: