搜索相关性详解

什么是相关性(Relevance)

搜索是用户和搜索引擎的对话,用户关心的是搜索结果的相关性

  • 是否可以找到所有相关的内容
  • 有多少不相关的内容被返回了
  • 文档的打分是否合理
  • 结合业务需求,平衡结果排名

相关性是指在搜索引擎中,描述一个文档与查询语句匹配程度的度量标准。这种相关性通过为每个匹配查询条件的文档计算一个相关性评分(_score)来实现,评分越高表示文档与查询语句的匹配程度越高

如下例子:显而易见,查询JAVA多线程设计模式,文档id为2,3的文档的算分更高

关键词

文档ID

JAVA

1,2,3

设计模式

1,2,3,4,5,6

多线程

2,3,7,9

Elasticsearch使用评分算法,根据查询条件与索引文档的匹配程度来确定每个文档的相关性。同时,为了满足各种特定的业务需求,Elasticsearch也充分允许用户自定义评分。

在下面示例中,_score就是Elasticsearch检索返回的评分,其值可以衡量每个文档与查询的匹配程度,即相关性。每个文档都有对应的评分,该得分由正浮点数表示。文档评分越高,则该文档的相关性越高。

计算相关性评分

Elasticsearch使用布尔模型查找匹配文档,并用一个名为“实用评分函数”的公式来计算相关性。这个公式借鉴了TF-IDF(词频-逆向文档频率)和向量空间模型,同时加入了一些现代的新特性,如协调因子、字段长度归一化以及词/查询语句权重提升。

Elasticsearch 5之前的版本,评分机制或者打分模型是基于TF-IDF实现的。从Elasticsearch 5之后,默认的打分机制改成了Okapi BM25。其中BM是Best Match的缩写,25是指经过25次迭代调整之后得出的算法,它是由TF-IDF机制进化来的。

传统TF-IDF和BM25都使用逆向文档频率来区分普通词(不重要)和非普通词(重要)​,使用词频来衡量某个词在文档中出现的频率。两种机制的逻辑相似:首先,文档里的某个词出现得越频繁,文档与这个词就越相关,得分越高;其次,某个词在集合中所有文档里出现的频次越高,则它的权重越低、得分越低。也就是说,某个词在集合中所有文档里越罕见,其得分越高。BM25在传统TF-IDF的基础上增加了几个可调节的参数,使得它在应用上更佳灵活和强大,具有较高的实用性。

TF-IDF评分公式:

  • TF是词频(Term Frequency)

检索词在文档中出现的频率越高,相关性也越高。

词频(TF) = 某个词在文档中出现的次数 / 文档的总词数

  • IDF是逆向文本频率(Inverse Document Frequency)

每个检索词在索引中出现的频率,频率越高,相关性越低。总文档中有些词比如“是”、“的” 、“在” 在所有文档中出现频率都很高,并不重要,可以减少多个文档中都频繁出现的词的权重。

逆向文本频率(IDF)= log (语料库的文档总数 / (包含该词的文档数+1))

  • 字段长度归一值( field-length norm)

检索词出现在一个内容短的 title 要比同样的词出现在一个内容长的 content 字段权重更大。

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

BM25 就是对 TF-IDF 算法的改进,对于 TF-IDF 算法,TF(t) 部分的值越大,整个公式返回的值就会越大。BM25 就针对这点进行来优化,随着TF(t) 的逐步加大,该算法的返回值会趋于一个数值。

  • BM 25的公式

示例:通过Explain API查看TF-IDF

Elasticsearch自定义评分

自定义评分是用来优化Elasticsearch默认评分算法的一种有效方法,可以更好地满足特定应用场景的需求。

自定义评分的核心是通过修改评分来修改文档相关性,在最前面的位置返回用户最期望的结果。

Elasticsearch自定义评分的主要作用如下:

1) 排序偏好:通过在搜索结果中给每个文档自定义评分,可以更好地满足搜索用户的排序偏好。

2) 特殊字段权重:通过给特定字段赋予更高的权重,可以让这些字段对搜索结果的影响更大。

3) 业务逻辑需求:根据业务需求,可以定义复杂的评分逻辑,使搜索结果更符合业务需求。

4) 自定义用户行为:可以使用用户行为数据(如点击率)作为评分因素,提高用户搜索体验。

搜索结果相关性与自定义评分的关系

搜索引擎本质是一个匹配过程,即从海量的数据中找到匹配用户需求的内容。判定内容与用户查询的相关性一直是搜索引擎领域的核心研究课题之一。如果搜索引擎不能准确地识别用户查询的意图并将相关结果排在前面的位置,那么搜索结果就不能满足用户的需求,从而影响用户对搜索引擎的满意度。

自定义评分的策略

然而,如何实现这样的自定义评分策略,以确保搜索结果能够最大限度地满足用户需求呢?我们可以从多个层面,包括索引层面、查询层面以及后处理阶段着手。

以下是几种主要的自定义评分策略:

  • Index Boost: 在索引层面修改相关性
  • boosting: 修改文档相关性
  • negative_boost: 降低相关性
  • function_score: 自定义评分
  • rescore_query:查询后二次打分

Index Boost: 在索引层面修改相关性

Index Boost这种方式能在跨多个索引搜索时为每个索引配置不同的级别。所以它适用于索引级别调整评分。

实战举例:一批数据里有不同的标签,数据结构一致,要将不同的标签存储到不同的索引(A、B、C),并严格按照标签来分类展示(先展示A类,然后展示B类,最后展示C类)​,应该用什么方式查询呢?

具体实现如下。借助indices_boost提升索引的权重,让A排在最前,其次是B,最后是C。

PUT /my_index_100b/_doc/1
{
    "subject":"subject 1"
}

PUT /my_index_100c/_doc/1
{
    "subject":"subject 1"
}

GET /my_index_100*/_search
{
    "query": {
        "term": {
          "subject.keyword": {
            "value": "subject 1"
          }
        }
    },
    "indices_boost": [
      {
        "my_index_100a": 1.5
      },
       {
        "my_index_100b": 1.2
      },
       {
        "my_index_100c": 1
      }

    ]
}

boosting: 修改文档相关性

boosting可在查询时修改文档的相关度。boosting值所在范围不同,含义也不同。

若boosting值为0~1,如0.2,代表降低评分;

若boosting值>1,如1.5,则代表提升评分。

适用于某些特定的查询场景,用户可以自定义修改满足某个查询条件的结果评分。

POST /51blog/_doc/1
{
    "title":"Apple iPad",
    "content":"Apple iPad,Apple iPad"
}

POST /51blog/_doc/2
{
    "title":"Apple iPad,Apple iPad",
    "content":"Apple iPad"
}

GET /51blog/_search
{
    "query":{
        "bool": {
            "should": [
              {
                "match": {
                  "title": {
                    "query": "apple,ipad"
                  }
                }
              },
              {
                "match": {
                  "content": {
                    "query": "apple,ipad"
                  }
                }
              }
            ]
        }

    }
}

GET /51blog/_search
{
    "query":{
        "bool": {
            "should": [
              {
                "match": {
                  "title": {
                    "query": "apple,ipad",
                    "boost": 4
                  }
                }
              },
              {
                "match": {
                  "content": {
                    "query": "apple,ipad"
                  }
                }
              }
            ]
        }

    }
}

negative_boost: 降低相关性

若对某些返回结果不满意,但又不想将其排除(must_not),则可以考虑采用negative_boost的方式。

原理说明如下:

  • negative_boost仅对查询中定义为negative的部分生效。
  • 计算评分时,不修改boosting部分评分,而negative部分的评分则乘以negative_boost的值。
  • negative_boost取值为0~1.0,如0.3。

案例:要求苹果公司的产品信息优先展示

POST /news/_doc/1
{
    "content":"Apple Mac"
}

POST /news/_doc/2
{
    "content":"Apple iPad"
}

POST /news/_doc/3
{
    "content":"Apple employee like Apple Pie and Apple Juice"
}

POST /news/_search
{
    "query": {
        "bool": {
            "must": [
              {"match":{
                "content":"apple"
              }}
            ],
            "must_not": [
              {"match":{
                "content":"pie"
              }}
            ]
        }
    }
}

POST /news/_search
{
    "query": {
        "boosting": {
            "positive": [
              {"match":{
                "content":"apple"
              }}
            ],
            "negative": [
              {"match":{
                "content":"pie"
              }}
            ],
            "negative_boost": 0.2
        }
    }
}

function_score: 自定义评分

该方式支持用户自定义一个或多个查询语句及脚本,达到精细化控制评分的目的,以对搜索结果进行高度个性化的排序设置。适用于需进行复杂查询的自定义评分业务场景。

案例1:商品信息如下,如何同时根据销量和浏览人数进行相关度提升?

商品

销量

浏览人数

A

10

10

B

20

20

想要提升相关度评分,则将每个文档的原始评分与其销量和浏览人数相结合,得到一个新的评分。例如,使用如下公式:

评分=原始评分×(销量+浏览人数)

这样,销量和浏览人数较高的文档就会有更高的评分,从而在搜索结果中排名更靠前。这种评分方式不仅考虑了文档与查询的匹配度(由_score表示)​,还考虑了文档的销量和浏览人数,非常适用于电子商务等场景。

该需求可以借助script_score实现,代码如下,其评分是基于原始评分和销量与浏览人数之和的乘积计算的结果。

POST /my_index_products/_doc/1
{
    "name":"A",
    "sales":10,
    "visitors":10
}

POST /my_index_products/_doc/2
{
    "name":"B",
    "sales":20,
    "visitors":20
}

POST /my_index_products/_doc/3
{
    "name":"C",
    "sales":30,
    "visitors":30
}

GET /my_index_products/_search

GET /my_index_products/_search
{
    "query": {
        "function_score": {
          "query": {
            "match_all": {}
          },
          "script_score":{
            "script": {
                "source": "_score*(doc['sales'].value+doc['visitors'].value)"
            }
          }
        }
    }
}

rescore_query:查询后二次打分

二次评分是指重新计算查询所返回的结果文档中指定文档的得分。

Elasticsearch会截取查询返回的前N条结果,并使用预定义的二次评分方法来重新计算其得分。但对全部有序的结果集进行重新排序的话,开销势必很大,使用rescore_query可以只对结果集的子集进行处理。该方式适用于对查询语句的结果不满意,需要重新打分的场景。

POST /my_index_books_demo/_doc/1
{
    "title":"ES实战",
    "content":"ES的实战操作,实战要领,实战经验"
}

POST /my_index_books_demo/_doc/2
{
    "title":"MySQL实战",
    "content":"MySQL的实战操作,实战要领,实战经验"
}

POST /my_index_books_demo/_doc/3
{
    "title":"MySQL",
    "content":"MySQL一定要会"
}

GET /my_index_books_demo/_search

GET /my_index_books_demo/_search
{
    "query": {
        "match": {
          "content": "实战"
        }
    },
    "rescore": {
      "query": {
        "rescore_query":{
            "match":{
                "title":"MySQL"
            }
        },
        "query_weight":0.7,
        "rescore_query_weight":1.2
      },
      "window_size": 50
    }
}

通过rescore_query我们可以对检索结果进行二次评分,增加自己更复杂的评分逻辑,提供更准确的结果排序,但是相应的也会增加查询的计算成本与响应时间。

多字段搜索场景优化

多字段搜索的三种场景:

  • 最佳字段(Best Fields) : 多个字段中返回评分最高的

当字段之间相互竞争,又相互关联。例如,对于博客的 title和 body这样的字段,评分来自最匹配字段

  • 多数字段(Most Fields):匹配多个字段,返回各个字段评分之和

处理英文内容时的一种常见的手段是,在主字段( English Analyzer),抽取词干,加入同义词,以匹配更多的文档。相同的文本,加入子字段(Standard Analyzer),以提供更加精确的匹配。其他字段作为匹配文档提高相关度的信号,匹配字段越多则越好。

  • 混合字段(Cross Fields): 跨字段匹配,待查询内容在多个字段中都显示

对于某些实体,例如人名,地址,图书信息。需要在多个字段中确定信息,单个字段只能作为整体的一部分。希望在任何这些列出的字段中找到尽可能多的词。

最佳字段搜索

将任何与任一查询匹配的文档作为结果返回,采用字段上最匹配的评分作为最终评分返回。

官方文档:Disjunction max query | Elasticsearch Guide [8.17] | Elastic

bool should的算法过程:

  • 查询should语句中的两个查询
  • 加和两个查询的评分
  • 乘以匹配语句的总数
  • 除以所有语句的总数
PUT /52blog/_doc/1
{
    "title":"Quick brown rabbits",
    "body":"Brown rabbits are commonly seen."
}

PUT /52blog/_doc/2
{
    "title":"Keeping pets healthy",
    "body":"My quick brown fox eats rabbits on a regular basis."
}

GET /52blog/_search

POST /52blog/_search
{
    "explain": true,
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "title": "Brown fox"
          }
        },
        {
          "match": {
            "body": "Brown fox"
          }
        }
      ]
    }
  }
}

上述例子中,title和body属于竞争关系,不应该将分数简单叠加,而是应该找到单个最佳匹配的字段的评分。

使用dis max query查询

POST /52blog/_search
{
    "explain": true,
  "query": {
    "dis_max": {
      "queries": [
        {
          "match": {
            "title": "Brown fox"
          }
        },
        {
          "match": {
            "body": "Brown fox"
          }
        }
      ]
    }
  }
}

可以通过tie_breaker参数调整

POST /52blog/_search
{
  "query": {
    "dis_max": {
      "tie_breaker": 0.1,
      "queries": [
        {
          "match": {
            "title": "quick pets"
          }
        },
        {
          "match": {
            "body": "quick pets"
          }
        }
      ]
    }
  }
}

POST /52blog/_search
{
  "query": {
    "multi_match": {
        "type": "best_fields",
      "query": "Brown fox",
      "fields": ["title", "body"],
      "tie_breaker": 0.2
    }
  }
}

Tie Breaker是一个介于0-1之间的浮点数。0代表使用最佳匹配;1代表所有语句同等重要。

  1. 获得最佳匹配语句的评分_score 。
  2. 将其他匹配语句的评分与tie_breaker相乘
  3. 对以上评分求和并规范化

最终得分=最佳匹配字段+其他匹配字段*tie_breaker

使用 best_fields 查询

best_fields策略获取最佳匹配字段的得分, final_score = max(其他匹配字段得分, 最佳匹配字段得分)

采用 best_fields 查询,并添加参数 tie_breaker=0.1,final_score = 其他匹配字段得分 * 0.1 + 最佳匹配字段得分

Best Fields是默认类型,可以不用指定,等价于dis_max查询方式

POST /52blog/_search
{
  "query": {
    "multi_match": {
        "type": "best_fields",
      "query": "Brown fox",
      "fields": ["title", "body"],
      "tie_breaker": 0.2
    }
  }
}

PUT /xiaomi_employee
{
    "settings": {
        "index":{
            "analysis.analyzer.default.type":"standard"
        }
    }
}

PUT /xiaomi_employee/_doc/1
{"empId":"1","name":"员工001","age":20,"sex":"男","mobile":"19000001111","salary" :23343 , "deptName":"技术部","address":"湖北省武汉市洪山区光谷大厦","content":"i like to write best elasticsearch article"}

PUT /xiaomi_employee/_doc/2
{"empId":"2","name":"员工002","age":25,"sex":"男","mobile":"19000002222","salary":15963,"deptName":"销售部","address":"湖北省武汉市江汉路","content":"i think java is the best programming language"}


PUT /xiaomi_employee/_doc/3
{"empId":"3","name":"员工003","age":30,"sex":"男","mobile":"19000003333","salary":20000,"deptName":"技术部","address":"湖北省武汉市经济开发区","content":"i am only an elasticseanch beginner"}

PUT /xiaomi_employee/_doc/4
{"empId":"4","name":"员工004","age":20,"sex":"女","mobile":"19000004444","salary":15600,"deptName":"销售部","address":"湖北省武汉市沌口开发区",
"content":"elasticseahch and hadoop are all very good solution, iam a beginner"}

PUT /xiaomi_employee/_doc/5
{"empId":"5","name":"员工005","age":20,"sex":"男","mobile":"19000005555","salary":19665,"deptName":"测试部","address":"湖北省武汉市东湖隧道",
"content":"spark is best big data solution based on scala, an programming language similar to java"}

PUT /xiaomi_employee/_doc/6
{"empId":"6","name":"员工006","age":30,"sex":"女","mobile":"19000006666","salary":30000,"deptName":"技术部","address":"湖北省武汉市江汉路","content":"i like java developer"}

PUT /xiaomi_employee/_doc/7
{"empId":"7","name":"员工007","age":60,"sex":"女","mobile":"19000007777","salary":52130,"deptName":"测试部","address":"湖北省黄冈市边城区","content":"i like elasticsearch developer"}

PUT /xiaomi_employee/_doc/8
{"empId":"8","name":"员工008","age":19,"sex":"女","mobile":"19000008888","salary":60000,"deptName":"技术部","address":"湖北省武汉市江汉大学","content":"i like spark language"}

PUT /xiaomi_employee/_doc/9
{"empId":"9","name":"员工009","age":40,"sex":"男","mobile":"19000009999","salary":23000,"deptName":"销售部","address":"河南省郑州市郑州大学"}

PUT /xiaomi_employee/_doc/10
{"empId":"10","name":"张湖北","age":35,"sex":"男","mobile":"19000001010","salary":18000,"deptName":"测试部","address":"湖北省武汉市东湖高新","content":"i like java developer, i also like elasticsearch"}


PUT /xiaomi_employee/_doc/11
{"empId":"11","name":"王河南","age":61,"sex":"男","mobile":"19000001011","salary":10000,"deptName":"销售部","address":"河南省开封市河南大学","content":"i am not like java"}

PUT /xiaomi_employee/_doc/12
{"empId":"12","name":"张大学","age":26,"sex":"女","mobile":"19000001012","salary":11321, "deptName":"测试部","address":"河南省开封市河南大学","content":"i am java developer, java is good"}

PUT /xiaomi_employee/_doc/13
{"empId":"13","name":"李江汉","age":36,"sex":"男","deptName":"销售部","address":"河南省郑州市二七区","content":"i like java and java is very best, i like it, do you like java"}

PUT /xiaomi_employee/_doc/14
{"empId":"14","name":"王技术","age":45,"sex":"男","deptName":"测试部","address":"河南省郑州市金水区","content":"i like c++"}


PUT /xiaomi_employee/_doc/15
{"empId":"15","name":"张测试","age":18,"sex":"男","deptName":"技术部","address":"河南省郑州市高新开发区","content":"i think spark is good"}

GET /xiaomi_employee/_search

GET /xiaomi_employee/_search
{
  "query": {
    "multi_match": {
      "query": "elasticseanch beginner 湖北省 开封市",
      "type": "best_fields",
      "fields": [
        "content","address"
      ],
      "tie_breaker": 0.1
    }
  },
  "size": 20
}

使用多数字段搜索

most_fields策略获取全部匹配字段的累计得分(综合全部匹配字段的得分),等价于bool should查询方式

GET /xiaomi_employee/_search
{
  "query": {
    "multi_match": {
      "query": "elasticseanch beginner 湖北省 开封市",
      "type": "most_fields",
      "fields": [
        "content","address"
      ]
    }
  },
  "size": 20
}
PUT /titles
{
  "mappings": {
    "properties": {
      "title":{
        "type": "text",
        "analyzer": "english",
        "fields": {
          "std":{
            "type":"text",
            "analyzer": "standard"
          }
        }
      }
    }
  }
}


POST _analyze
{
  "analyzer": "english",
  "text":"I see a lot of barking dogs on the road"
}

POST /titles/_doc/1
{"title":"My dog barks"}

POST /titles/_doc/2
{"title":"I see a lot of barking dogs on the road"}

GET /titles/_search

用广度匹配字段title包括尽可能多的文档——以提升召回率——同时又使用字段title.std 作为信号将相关度更高的文档置于结果顶部。

GET /titles/_search
{
  "query": {
    "match": {
      "title.std": "barking dogs"
    }
  }
}

每个字段对于最终评分的贡献可以通过自定义值boost 来控制。比如,使title 字段更为重要,这样同时也降低了其他信号字段的作用:

GET /titles/_search
{
  "query": {
    "multi_match": {
      "query": "barking dogs",
      "type": "most_fields",
      "fields": [
        "title^10","title.std"
      ]
    }
  }
}

跨字段搜索

搜索内容在多个字段中都显示,类似bool+dis_max组合

PUT /address
{
    "settings": {
        "index":{
            "analysis.analyzer.default.type":"standard"
        }
    }
}

PUT /address/_doc/1
{"province": "湖南","city":"长沙"}

PUT /address/_doc/2
{"province": "湖南","city":"常德"}

PUT /address/_doc/3
{"province": "广东","city":"广州"}

PUT /address/_doc/4
{"province": "湖南","city":"邵阳"}

GET /address/_search
{
  "query": {
    "multi_match": {
      "query": "湖南常德",
      "type": "most_fields",
      "fields": [
        "province","city"
      ]
    }
  }
}

可以使用cross_fields,支持operator

GET /address/_search
{
  "query": {
    "multi_match": {
      "query": "湖南常德",
      "type": "cross_fields",
      "operator": "and",
      "fields": [
        "province","city"
      ]
    }
  }
}

还可以用copy...to 解决,但是需要额外的存储空间

copy_to参数允许将多个字段的值复制到组字段中,然后可以将其作为单个字段进行查询

PUT /address
{
   "settings": {
        "index":{
            "analysis.analyzer.default.type":"standard"
        }
    },
  "mappings": {
    "properties": {
      "province":{
        "type": "keyword",
        "copy_to": "full_address"
      },
      "city":{
        "type": "keyword",
        "copy_to": "full_address"
      }
    }
  }
}
PUT /address/_doc/1
{"province": "湖南","city":"长沙"}

PUT /address/_doc/2
{"province": "湖南","city":"常德"}

PUT /address/_doc/3
{"province": "广东","city":"广州"}

PUT /address/_doc/4
{"province": "湖南","city":"邵阳"}


GET /address/_search
{
  "query": {
    "match": {
      "full_address": {
        "query": "湖南常德",
        "operator": "and"
      }
    }
  }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值