Elasticsearch高阶查询
相关性和相关性算分
搜索是用户和搜索引擎的对话,用户关心的是搜索结果的相关性
是否可以找到所有相关的内容
有多少不相关的内容被返回了
文档的打分是否合理
结合业务需求,平衡结果排名
如何衡量相关性:
- Precision(查准率)-尽可能返回较少的无关文档
- Recall(查全率)-尽量返回较多的相关文档
- Ranking-是否能够按照相关度进行排序
相关性 (Relevance)
搜索的相关性算分,描述了一个文档和查询语句匹配的程度。
ES 会对每个匹配查询条件的结果进行
算分 _score。打分的本质是排序,需要把最符合用户需求的文档排在前面。
ES 5之前,默认的相关性
算分采用TF-IDF,现在采用BM 25。
什么是TF-IDF
TF-IDF (term frequency-inverse document frequency) 是一种用于信息检索与数据挖掘的常用加权
技术。
- TF-IDF被公认为是信息检索领域最重要的发明,除了在信息检索,在文献分类和其他相关领
域有着非常广泛的应用。 - IDF的概念,最早是剑桥大学的“斯巴克.琼斯”提出
- 1972年一-“关键词特殊性的统计解释和它在文献检索中的应用”,但是没有从理论上解
释IDF应该是用log(全部文档数/检索词出现过的文档总数),而不是其他函数,也没有做进一
步的研究 - 1970,1980年代萨尔顿和罗宾逊,进行了进一步的证明和研究,并用香农信息论做了证明
- 现代搜索引擎,对TF-IDF进行了大量细微的优化
Lucene中的TF-IDF评分公式
- TF是词频(Term Frequency)
检索词在文档中出现的频率越高,相关性也越高
- IDF是逆向文本频率(Inverse Document Frequency)
每个检索词在索引中出现的频率,频率越高,相关性越低
比如java在100个文档中都有,而多线程只有一个文档有,那么java需要扫描100篇文档,而多线程只需要扫描一篇,所以java的相关性比多线程低。
java对于用户来说,搜索java,出现了100篇文档,那么对用户搜索的相关性是低的。
- 字段长度归一值(filed-length norm)
字段的长度是多少?字段越短,字段的权重越高。检索词出现在一个内容短的 title 要比同样的词出现在一个内容长的 content 字段权重更大。
说白了就是很长的content才找到那么一个搜索关键词,那么相关性肯定低撒。
BM25
BM25 就是对 TF-IDF 算法的改进,对于 TF-IDF 算法,TF(t)部分的值越大,整个公式返回的值就会越大。BM25 就针对这点进行来优化,随着TF(t) 的逐步加大,该算法的返回值会趋于一个数值。
explain关键字
在查询的时候加上explain:true可以返回相应的算分规则
POST hotel/_search
{
"explain": true,
"query": {
"match": {
"name": "北京"
}
}
}
返回具体的算分规则
"_explanation" : {
"value" : 1.6035968,
"description" : "weight(name:北京 in 163) [PerFieldSimilarity], result of:",
"details" : [
{
"value" : 1.6035968,
"description" : "score(freq=2.0), computed as boost * idf * tf from:",
"details" : [
{
"value" : 2.2,
"description" : "boost",
"details" : [ ]
},
{
"value" : 1.1731012,
"description" : "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:",
"details" : [
{
"value" : 62,
"description" : "n, number of documents containing term",
"details" : [ ]
},
{
"value" : 201,
"description" : "N, total number of documents with field",
"details" : [ ]
}
]
},
{
"value" : 0.621351,
"description" : "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:",
"details" : [
{
"value" : 2.0,
"description" : "freq, occurrences of term within document",
"details" : [ ]
},
{
"value" : 1.2,
"description" : "k1, term saturation parameter",
"details" : [ ]
},
{
"value" : 0.75,
"description" : "b, length normalization parameter",
"details" : [ ]
},
{
"value" : 9.0,
"description" : "dl, length of field",
"details" : [ ]
},
{
"value" : 8.815921,
"description" : "avgdl, average length of field",
"details" : [ ]
}
]
}
]
}
]
}
Boosting
Boosting是控制相关度的一个手段。
参数Boost的含义:
- 当Boost > 1 时,打分的权重相对性提升
- 当 0 < Boost < 1时,打分的权重相对性降低
- 当Boost < 0时,贡献负分
如何通过Boost控制想要的文档排在前面?
POST hotel/_search
{
"query": {
"match": {
"name": {
"query": "北京西直门",
"operator": "and"
}
}
}
}
如查询北京西直门的酒店
{
"_index" : "hotel2",
"_type" : "_doc",
"_id" : "1765008760",
"_score" : 13.104373,
"_source" : {
"id" : 1765008760,
"name" : "如家酒店(北京西直门北京北站店)",
"address" : "西直门北大街49号",
"price" : 356,
"score" : 44,
"brand" : "如家",
"city" : "北京",
"business" : "西直门/北京展览馆地区",
"location" : "39.945106,116.353827",
"pic" : "https://m.tuniucdn.com/fb3/s1/2n9c/4CLwbCE9346jYn7nFsJTQXuBExTJ_w200_h200_c1_t0.jpg"
}
},
{
"_index" : "hotel2",
"_type" : "_doc",
"_id" : "414168",
"_score" : 12.663941,
"_source" : {
"id" : 414168,
"name" : "7天连锁酒店(北京西直门店)",
"address" : "西城平安里西大街翠花街育幼胡同甲20-22号",
"price" : 419,
"score" : 37,
"brand" : "7天酒店",
"city" : "北京",
"business" : "西单、金融街地区",
"location" : "39.931338,116.364982",
"pic" : "https://m2.tuniucdn.com/filebroker/cdn/res/bc/66/bc666859edf4fc072a8006c66758058d_w200_h200_c1_t0.jpg"
}
}
可以看到如家评分在前面。
当我们将北京的条件boost相关性调低。
POST hotel/_search
{
"query": {
"boosting": {
"positive": {
"term": {
"name": {
"value": "西直门"
}
}
},
"negative": {
"term": {
"name": {
"value": "北京"
}
}
},
"negative_boost": 0.2
}
}
}
{
"_index" : "hotel2",
"_type" : "_doc",
"_id" : "414168",
"_score" : 0.87095565,
"_source" : {
"id" : 414168,
"name" : "7天连锁酒店(北京西直门店)",
"address" : "西城平安里西大街翠花街育幼胡同甲20-22号",
"price" : 419,
"score" : 37,
"brand" : "7天酒店",
"city" : "北京",
"business" : "西单、金融街地区",
"location" : "39.931338,116.364982",
"pic" : "https://m2.tuniucdn.com/filebroker/cdn/res/bc/66/bc666859edf4fc072a8006c66758058d_w200_h200_c1_t0.jpg"
}
},
{
"_index" : "hotel2",
"_type" : "_doc",
"_id" : "1765008760",
"_score" : 0.87095565,
"_source" : {
"id" : 1765008760,
"name" : "如家酒店(北京西直门北京北站店)",
"address" : "西直门北大街49号",
"price" : 356,
"score" : 44,
"brand" : "如家",
"city" : "北京",
"business" : "西直门/北京展览馆地区",
"location" : "39.945106,116.353827",
"pic" : "https://m.tuniucdn.com/fb3/s1/2n9c/4CLwbCE9346jYn7nFsJTQXuBExTJ_w200_h200_c1_t0.jpg"
}
}
可以看到7天酒店的评分高了
这种业务场景在于**收费**的要排在前面,即使它的相关性没那么高。
布尔查询(bool Query)
一个bool查询,是一个或者多个查询子句的组合
,总共包括4种子句,其中2种会影响算分,2种不影响算分。
- must: 相当于&&,必须匹配,贡献算分
- should: 相当于or,选择性匹配,贡献算分
- must not: 相当于!,必须不能匹配,不贡献算分
- filter: 必须匹配,不贡献算分
在Elasticsearch中,有Query和 Filter两种不同的Context
-
Query Context: 相关性算分
-
Filter Context: 不需要算分,可以利用Cache,获得更好的性能
相关性并不只是全文本检索的专利,也适用于yes|no 的子句,匹配的子句越多,相关性评分越高。如果多条查询子句被合并为一条复合查询语句,比如 bool查询,则每个查询子句计算得出的评分会被合并到总的相关性评分中。
查询语法
- 子查询可以任意顺序出现
- 可以嵌套多个子查询
- 如果bool查询中,没有must条件,should必须至少满足一个查询
语法格式
POST hotel/_search
{
"query": {
"bool": {
"must": [
{},{},{}
],
"should": [
{},{},{}
],
"must_not": [
{},{},{}
],
"filter": [
{},{},{}
]
}
}
}
如查询北京市的如家酒店
POST hotel/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"brand": "如家"
}
},
{
"match": {
"city": "北京"
}
}
]
}
}
}
查询北京市的如家酒店,并且不要国贸地区的
POST hotel/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"brand": "如家"
}
},
{
"match": {
"city": "北京"
}
}
],
"must_not": [
{
"match": {
"business.keyword": "国贸地区"
}
}
]
}
}
}
北京市的如家酒店,并且不要国贸地区的,并且价格在200到300之间,并且价格不重要
POST hotel/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"brand": "如家"
}
},
{
"match": {
"city": "北京"
}
}
],
"must_not": [
{
"match": {
"business.keyword": "国贸地区"
}
}
],
"filter": [
{
"range": {
"price": {
"gte": 200,
"lte": 300
}
}
}
]
}
}
}
北京市的如家酒店,并且不要国贸地区的,并且价格在200到300之间,并且价格不重要,而且地址最好在莲花池的
POST hotel/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"brand": "如家"
}
},
{
"match": {
"city": "北京"
}
}
],
"must_not": [
{
"match": {
"business.keyword": "国贸地区"
}
}
],
"filter": [
{
"range": {
"price": {
"gte": 200,
"lte": 300
}
}
}
],
"should": [
{
"match": {
"address": "莲花池"
}
}
]
}
}
}
单字符串多字段查询
三种场景
最佳字段(Best Fields)
当字段之间相互竞争,又相互关联。例如,对于博客的 title和 body这样的字段,评分来自最匹配字段
多数字段(Most Fields)
处理英文内容时的一种常见的手段是,在主字段( English Analyzer),抽取词干,加入同义词,以匹配更多的文档。相同的文本,加入子字段 (Standard Analyzer),以提供更加精确的匹配。其他字段作为匹配文档提高相关度的信号,匹配字段越多则越好。
混合字段(Cross Field)
对于某些实体,例如人名,地址,图书信息。需要在多个字段中确定信息,单个字段只能作为整体的部分。希望在任何这些列出的字段中找到尽可能多的词
最佳字段查询(Dis Max Query)
将任何与任一查询匹配的文档作为结果返回
,采用字段上最匹配的评分最终评分返回
第一种写法:
POST hotel/_search
{
"query": {
"multi_match": {
"type": "best_fields",
"query": "莲花",
"fields": ["address","business"],
"tie_breaker": 0.2
}
}
}
type: best_fields默认
第二种写法:
POST hotel/_search
{
"query": {
"dis_max": {
"tie_breaker": 0.7,
"queries": [
{
"match": {
"address": "莲花"
}
},
{
"match": {
"business": "莲花"
}
}
]
}
}
}
可以通过tie_breaker参数调整
Tier Breaker是一个介于0-1之间的浮点数。0代表使用最佳匹配;1代表所有语句同等重要
- 获得最佳匹配语句的评分 score。
- 将其他匹配语句的评分与tie breaker相乘
- 对以上评分求和并规范化
混合字段
混合字段支持and符,可以在多字段中精确查询每个字段。如四川成都,在province中匹配四川,在city字段中匹配成都
聚合查询
聚合主要用做统计分析功能,常见的聚合操作有求和,平均,分组
语法:
"aggs": {
"aggs_name": { //自定义聚合查询的名字
"aggs_type" : { //聚合的定义,如求和还是平均什么的
"aggs_body"
},
"aggs" : {} //子查询,如某些先分组,再平均的情况
}
}
聚合的分类
数学运算,如最大,最小,求和,平均等
- 查询酒店价格最大,最小,和平均值,es默认会返回查询的数据结果,使用size:0可以不需要返回数据
POST hotel/_search
{
"size": 0,
"aggs": {
"max_price": {
"max": {
"field": "price"
}
},
"min_price" : {
"min" : {
"field": "price"
}
},
"avg_price" : {
"avg": {
"field": "price"
}
}
}
}
{
"took" : 3,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 201,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"max_price" : {
"value" : 3299.0
},
"min_price" : {
"value" : 127.0
},
"avg_price" : {
"value" : 660.6019900497513
}
}
}
- 利用
stats
类型,计算所有的数学运算结果
POST hotel/_search
{
"size": 0,
"aggs": {
"stats_price": {
"stats": {
"field": "price"
}
}
}
}
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 201,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"stats_price" : {
"count" : 201,
"min" : 127.0,
"max" : 3299.0,
"avg" : 660.6019900497513,
"sum" : 132781.0
}
}
}
- 去重查询 cardinality,主要是针对keyword进行去重,如果是text类型,不允许去重。
POST hotel/_search
{
"size": 0,
"aggs": {
"cardinate_city": {
"cardinality": {
"field": "city"
}
}
}
}
Bucket aggregation:类似于分组,就是分桶的概念
常见的分桶聚合:
- terms: 需要字段支持fielddata
- keyword默认支持fielddata
- text需要在Mapping中开启fielddata,会按照分词后的结果进行分桶
- 数字类型
- Range/Data Range
- Histogram(直方图)/Data Histogram
- 支持嵌套:也就在桶里再坐分桶
- 按照城市分组
POST hotel/_search
{
"aggs": {
"by_size": {
"terms": {
"field": "city"
}
}
}
}
- 按照价格区间分组
POST hotel/_search
{
"size": 0,
"aggs": {
"range_price": {
"range": {
"field": "price",
"ranges": [
{
"key": "小于200",
"to": 200
},
{
"key": "500以内",
"from": 200,
"to": 500
},
{
"key": "1000以内",
"from": 500,
"to": 1000
},
{
"key": "大于1000",
"from": 1000
}
]
}
}
}
}
"aggregations" : {
"range_price" : {
"buckets" : [
{
"key" : "小于200",
"to" : 200.0,
"doc_count" : 17
},
{
"key" : "500以内",
"from" : 200.0,
"to" : 500.0,
"doc_count" : 78
},
{
"key" : "1000以内",
"from" : 500.0,
"to" : 1000.0,
"doc_count" : 77
},
{
"key" : "大于1000",
"from" : 1000.0,
"doc_count" : 29
}
]
}
}
- 按照价格区间进行直方图查询
POST hotel/_search
{
"size": 0,
"aggs": {
"histogram_price": {
"histogram": {
"field": "price",
"interval": 1000,
"extended_bounds": {
"min": 0,
"max": 3000
}
}
}
}
}
- 嵌套查询,如先按照城市进行分组,再计算各城市酒店的平均价格
# 先按照城市进行分桶,再求价格的平均值
POST hotel/_search
{
"size": 0,
"aggs": {
"bucket_city": {
"terms": {
"field": "city"
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
Pipeline Aggregation: 对其他的聚合结果进行二次聚合
如求取各品牌下酒店平均价格的最小值
- 根据brand进行分桶
- 求取分桶下price的平均值
- 根据avg_price求最小值
POST hotel/_search
{
"size": 0,
"aggs": {
"terms_brand": {
"terms": {
"field": "brand"
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
},
"min_price_by_brand" : {
"min_bucket": {
"buckets_path": "terms_brand>avg_price"
}
}
}
}
buckets_path下的> 符号为路径符号,意思是最小值通过terms_brand下的avg_price取