要不是遇到这个坑我不会去了解这个参数。
——《论通读文档的重要性》
问题
起因是搜索结果排序的时候遇到一个奇怪的问题,一个在我理解应该排第一的结果被放在了后面,而且评分相差接近两倍之多。
分析
通过explain发现结果中第一的那个文档与我认为应该排第一的文档的idf竟然相差甚远。大家知道,idf的定义如下(摘自维基百科):
逆向文件频率(inverse document frequency,idf)是一个词语普遍重要性的度量。某一特定词语的idf,可以由总文件数目除以包含该词语之文件的数目,再将得到的商取以10为底的对数得到。
从定义可知,idf仅仅与搜索关键词有关,与文档无关。所以同一输入来说,所有的文档应该是共享同一idf的。但事实上并非如此。原因就在elasticsearch的分布式机制。elasticsearch的索引(index)会被分片(shard),而每一个分片相当于一个独立的搜索引擎。每一次搜索任务会被分配到不同的shard去执行,然后将各个shard的结果汇总起来得到最终我们看到的结果。而评分的过程会在shard完成,因此不同分片下,会得到不同的idf。这里需要有个前提假设是文档数量足够多的时候各个分片的词频会趋近,因此idf的差异也就不大。但是如果文档数量不够多的时候启用分片,可能词频在不同分片会有较大的差异,我遇到的情况就是这样的。这时候就需要我们了解一下今天故事的主角search_type。
解决
这个参数的定义大家自行查阅文档,我这里简单介绍它的两个取值:query_then_fetch 和 dfs_query_then_fetch。
query_then_fetch 是默认值,它对词频的计算方式和所在的问题如上文所述。
dfs_query_then_fetch 就是为了解决我们今天遇到的问题的,当search_type设置为它的时候,词频的计算方法是整个索引(index)而不是单个分片(shard),这样会得到更准确的tf-idf评分。
不过文档上面比较讨厌的是没有说明这个参数是加在哪里的,答案就是URL的query string,与pretty、explain等参数用法相同。
后记
想必大家可以想到另一个解决方案那就是在创建索引的时候设置只有一个分片,这样也不需要search_type了。其实如果数据的确不多的话,用一个分片足矣。
实验
为了更直白理解上文的叙述,下面设计一个实验:
DELETE twitter PUT twitter/tweet/1 { "title" : "b c d d d" } PUT twitter/tweet/2 { "title" : "b c d d" } PUT twitter/tweet/3 { "title" : "b c d" } PUT twitter/tweet/4 { "title" : "b c" } POST /twitter/tweet/_search { "explain": true, "query": { "term" : { "title" : "d" } } } POST /twitter/tweet/_search?search_type=dfs_query_then_fetch { "explain": true, "query": { "term" : { "title" : "d" } } }
因为elasticsearch默认是5个分片,实验中第一个查询得到idf算法如下
"value": 0.6931472, "description": "idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:", "details": [ { "value": 1, "description": "docFreq", "details": [] }, { "value": 2, "description": "docCount", "details": [] } ]
其中两个重要参数文档数(docCount)和文档频率(docFreq)分别是2和1。我们可以很容易发现我们这个索引的文档数是4,而文档频率,也就是当前查询包含d的文档数有3条。
而第二个查询得到idf算法如下,我们可以看到文档频率和文档数与我们人工估算是一致的,也就是说idf的计算是针对整个索引的。
"value": 0.35667494, "description": "idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:", "details": [ { "value": 3, "description": "docFreq", "details": [] }, { "value": 4, "description": "docCount", "details": [] } ]