提高准确率的方法有很多,但是要在提高准确率的同时保证召回率往往比较困难,本文只介绍一种比较常见的情况。
我们经常搜索内容,往往不止针对某个字段进行搜索,比如:标题、内容,往往都是一起搜索的。
index结构如下:
{
"settings": {
"number_of_shards": "1",
"number_of_replicas": "0"
},
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_smart"
},
"content": {
"type": "text",
"analyzer": "ik_smart"
}
}
}
}
样例数据如下:
{"index":{"_id":1}}
{"title":"我喜欢的一种水果","content":"我喜欢的苹果是红色的,含有铜、碘、锰、锌、钾等元素"}
{"index":{"_id":2}}
{"title":"红色的番茄","content":"番茄是一种红色的水果,含有各种维生素以及糖分"}
{"index":{"_id":3}}
{"title":"樱桃的介绍","content":"樱桃是红色的,含有丰富的糖分、铁、维生素C、蛋白质、维生素E、维生素B族和胡萝卜素"}
{"index":{"_id":4}}
{"title":"不知名介绍","content":"我爱吃红色,红色,红色,红色,红色,红色,红色,红色,红色,红色,红色,红色,红色,红色,红色,红色的水果"}
现在我要搜索【红色的苹果】,我们人眼看下来,id=1的文档肯定是最佳匹配的。但我们真实搜索结果会怎么样呢?
搜索语句假设如下:
{
"query": {
"bool": {
"should": [
{
"match": {
"title": "红色的苹果"
}
},
{
"match": {
"content": "红色的苹果"
}
}
]
}
}
}
上面搜索语句dsl语句看着略微复杂,我们换个写法,效果一样
{
"query": {
"multi_match": {
"query": "红色的苹果",
"type": "most_fields",
"fields": [
"title",
"content"
]
}
}
}
结果:
[
{
"_index":"dong_analyzer_test2",
"_type":"_doc",
"_id":"2",
"_score":1.9675379,
"_source":{
"title":"红色的番茄",
"content":"番茄是一种红色的水果,含有各种维生素以及糖分"
}
},
{
"_index":"dong_analyzer_test2",
"_type":"_doc",
"_id":"1",
"_score":1.9362588,
"_source":{
"title":"我喜欢的一种水果",
"content":"我喜欢的苹果是红色的,含有铜、碘、锰、锌、钾等元素"
}
},
{
"_index":"dong_analyzer_test2",
"_type":"_doc",
"_id":"3",
"_score":0.63812846,
"_source":{
"title":"樱桃的介绍",
"content":"樱桃是红色的,含有丰富的糖分、铁、维生素C、蛋白质、维生素E、维生素B族和胡萝卜素"
}
},
{
"_index":"dong_analyzer_test2",
"_type":"_doc",
"_id":"4",
"_score":0.2719918,
"_source":{
"title":"不知名介绍",
"content":"我爱吃红色,红色,红色,红色,红色,红色,红色,红色,红色,红色,红色,红色,红色,红色,红色,红色的水果"
}
}
]
很明显和我们人眼评分肯定是不一样的
我们先看下切词
{
"tokens": [
{
"token": "红色",
"start_offset": 0,
"end_offset": 2,
"type": "CN_WORD",
"position": 0
},
{
"token": "的",
"start_offset": 2,
"end_offset": 3,
"type": "CN_CHAR",
"position": 1
},
{
"token": "苹果",
"start_offset": 3,
"end_offset": 5,
"type": "CN_WORD",
"position": 2
}
]
}
因为番茄中title中有【红色】【的】,content中有【红色】【的】,title和content同时都命中了,所以匹配到了它。
因为id=1的title种没有【红色】【的】,尽管id=1的content的匹配度 大于 id=2的content,但是title匹配度不及id=2
一般来说title分低,只要content分高,照样总分可以超过其他文档。那这个样例的问题出在哪了呢?
我们再看下样例:
{"title":"我喜欢的一种水果","content":"我喜欢的苹果是红色的,含有铜、碘、锰、锌、钾等元素"}
{"title":"红色的番茄","content":"番茄是一种红色的水果,含有各种维生素以及糖分"}
{"title":"樱桃的介绍","content":"樱桃是红色的,含有丰富的糖分、铁、维生素C、蛋白质、维生素E、维生素B族和胡萝卜素"}
{"title":"不知名介绍","content":"我爱吃红色,红色,红色,红色,红色,红色,红色,红色,红色,红色,红色,红色,红色,红色,红色,红色的水果"}
我们发现content中【红色】这个词出现频率非常高。
我们可以想到es的搜索算法中有一个逆向文档频率,它描述的是某个词在所有文档中出现的频率越高,它的权重越低。
回到问题2,content分高一点不能把整体评分拉齐?答案是可以的,但是问题出在了content的分虽然高,但是高的不多,比起title差的远,上面样例中title出现【红色】只有一个而已。
能不能给title权重降低一点?这样就能弥补【红色】权重低的问题了。
上面有个问题就是,id=2的文档中,根本没有【苹果】也被匹配出来了,那么我精确匹配是不是就可以了
{
"query": {
"multi_match": {
"query": "红色的苹果",
"type": "most_fields",
"operator": "and",
"fields": [
"title",
"content"
]
}
}
}
查询结果:
[
{
"_index":"dong_analyzer_test2",
"_type":"_doc",
"_id":"1",
"_score":1.499949,
"_source":{
"title":"我喜欢的一种水果",
"content":"我喜欢的苹果是红色的,含有铜、碘、锰、锌、钾等元素"
}
}
]
确实我们最希望匹配出来的结果被匹配出来了,并且排在了第一,但是其他不是很相关的文档却没有匹配出来,这降低了召回率。所以这种方案不是特别推荐
备注:这种解决问题的思路是没有问题的,往往这种精确匹配要搭配其他查询条件一起使用,但和本文想讨论的问题不相关,放到其他文章中去介绍。
上面的问题关键在哪呢?
仔细分析可以发现,我们的需求是希望搜索一个query进行多个字段(title、content)的搜索。换句话说,我们其实是希望title和content是一个字段(他们共享TF/IDF),我们并不希望因为某些词在content中出现很频繁,但在title中出现不频繁导致最终评分不符合预期。
根据上面思路,我们是不是可以建一个新字段,把title和content拼接在一起就行了?
{
"settings":{
"number_of_shards":"1",
"number_of_replicas":"0"
},
"mappings":{
"properties":{
"title":{
"type":"text",
"analyzer":"ik_smart"
},
"content":{
"type":"text",
"analyzer":"ik_smart"
},
"title_content":{
"type":"text",
"analyzer":"ik_smart"
}
}
}
}
这样做是可以的,但是有两个弊端
怎么解决方案3的这个问题呢?
可以利用copy_to
{
"settings":{
"number_of_shards":"1",
"number_of_replicas":"0"
},
"mappings":{
"person":{
"properties":{
"title":{
"type":"text",
"analyzer":"ik_smart",
"copy_to":"title_content"
},
"content":{
"type":"text",
"analyzer":"ik_smart",
"copy_to":"title_content"
},
"title_content":{
"type":"string",
"analyzer":"ik_smart"
}
}
}
}
}
这样es帮我们在插入数据的时候自动把映射的索引copy到了title_content中去。
注意:这里所有的分词器要保持一致
但它同样还有弊端:
其实解决办法除了重新刷一遍数据以外,还有别的更加优雅的方式,可以不用在建索引的时候把所有情况考虑到位。
利用cross_fields词中心式的方式来解决
{
"query": {
"multi_match": {
"query": "红色的苹果",
"type": "cross_fields",
"fields": [
"title",
"content"
]
}
}
}
搜索结果:
[
{
"_index":"dong_analyzer_test2",
"_type":"_doc",
"_id":"1",
"_score":1.499949,
"_source":{
"title":"我喜欢的一种水果",
"content":"我喜欢的苹果是红色的,含有铜、碘、锰、锌、钾等元素"
}
},
{
"_index":"dong_analyzer_test2",
"_type":"_doc",
"_id":"4",
"_score":0.30932084,
"_source":{
"title":"不知名介绍",
"content":"我爱吃红色,红色,红色,红色,红色,红色,红色,红色,红色,红色,红色,红色,红色,红色,红色,红色的水果"
}
},
{
"_index":"dong_analyzer_test2",
"_type":"_doc",
"_id":"2",
"_score":0.2428131,
"_source":{
"title":"红色的番茄",
"content":"番茄是一种红色的水果,含有各种维生素以及糖分"
}
},
{
"_index":"dong_analyzer_test2",
"_type":"_doc",
"_id":"3",
"_score":0.23682731,
"_source":{
"title":"樱桃的介绍",
"content":"樱桃是红色的,含有丰富的糖分、铁、维生素C、蛋白质、维生素E、维生素B族和胡萝卜素"
}
}
]
他的原理就是把所有字段当成一个大字段,并在每个字段中查找每个词。
看下es的对cross_fields的分析过程
blended(terms:[title:红色, content:红色])
blended(terms:[title:的, content:的])
blended(terms:[title:苹果, content:苹果])
可以发现es进行三次大搜索,每次大搜索下面有两次小搜索,每次大搜索都是把切词的结果词进行匹配,每次小搜索都是把当前的切词对title和content进行terms匹配,最后把里层和外层搜索评分相加,得到最终结果。
本文探讨了多字段搜索的时候,每个字段的词频和逆向文档频率不同带来的搜索准确率问题。
问题的根本原因在于:搜索的时候大多数都是针对字段进行搜索,但本文中的情况是希望对词进行搜索。
解决思路也是很简单,就是把多个字段的词频和逆向文档频率整合到一起,当然可以在建立索引的时候整合,也可以搜索的时候进行整合查询。