文章目录
ElasticSearch搜索技术深入与查询
相关性和相关性算分
搜索的相关性算分,描述了一个文档和查询语句匹配的程度。ES 会对每个匹配查询条件的 结果进行算分_score。打分的本质是排序,需要把最符合用户需求的文档排在前面。ES 5之 前,默认的相关性算分采用TF-IDF,现在采用BM 25。
如下例子:显而易见,查询JAVA多线程设计模式,文档id为2,3的文档的算分更高
关键词 | 文档ID |
---|---|
JAVA | 1,2,3 |
设计模式 | 1,2,3,4,5,6 |
多线程 | 2,3,7,9 |
TF-IDF
TF-IDF(term frequency–inverse document frequency)是一种用于信息检索与数据挖掘的常用加权技术。
对于TF-IDF的算法和公式不做过多解释,主要与下面几个参数有关
- TF是词频(Term Frequency)
检索词在文档中出现的频率越高,相关性也越高。 - IDF是逆向文本频率(Inverse Document Frequency)
每个检索词在索引中出现的频率,频率越高,相关性越低。 - 字段长度归一值( field-length norm)
字段的长度是多少?字段越短,字段的权重越高。检索词出现在一个内容短的 title 要比同样的词出现在一个内容长的 content 字段权重更大。
以上三个因素——词频(term frequency)、逆向文档频率(inverse document frequency)和字段长度归一值(field-length norm)——是在索引时计算并存储的,最后将它们结合在一起计算单个词在特定文档中的权重。
BM25
BM25 就是对 TF-IDF 算法的改进,对于 TF-IDF 算法,TF(t) 部分的值越大,整个公式返回的值就会越大。BM25 就针对这点进行来优化,随着TF(t) 的逐步加大,该算法的返回值会趋于一个数值。
通过Explain API查看TF-IDF
PUT /test_score/_bulk
{"index":{"_id":1}}
{"content":"we use Elasticsearch to power the search"}
{"index":{"_id":2}}
{"content":"we like elasticsearch"}
{"index":{"_id":3}}
{"content":"Thre scoring of documents is caculated by the scoring formula"}
{"index":{"_id":4}}
{"content":"you know,for search"}
GET /test_score/_search
{
"explain": true,
"query": {
"match": {
"content": "elasticsearch"
}
}
}
Boosting算分控制
Boosting是控制相关度的一种手段。
参数boost的含义:
当boost > 1时,打分的权重相对性提升
当0 < boost <1时,打分的权重相对性降低
当boost <0时,负分
返回匹配positive查询的文档并降低匹配negative查询的文档相似度分。这样就可以在不排除某些文档的前提下对文档进行查询,搜索结果中存在只不过相似度分数相比正常匹配的要低
从上面的测试数据中,比如搜索带有elasticsearch的文档,并且包含like 的文档要放在最后
如果通过
GET /test_score/_search
{
"query": {
"term": {
"content": "elasticsearch"
}
}
}
方式获取到的数据
包含有like的文档算分就会高,排在前面。所以我们可以通过控制算分达到需求
GET /test_score/_search
{
"query": {
"boosting": {
"positive": {
"term": {
"content": "elasticsearch"
}
},
"negative": {
"term": {
"content": "like"
}
},
"negative_boost": 0.2
}
}
}
应用场景:希望包含了某项内容的结果不是不出现,而是排序靠后。
布尔查询bool Query
一个bool查询,是一个或者多个查询子句的组合,总共包括4种子句,其中2种会影响算分,2种不影响算分。
- must: 相当于&& ,必须匹配,贡献算分
- should: 相当于|| ,选择性匹配,贡献算分
- must_not: 相当于! ,必须不能匹配,不贡献算分
- filter: 必须匹配,不贡献算法
在Elasticsearch中,有Query和 Filter两种不同的Context
- Query Context: 相关性算分
- Filter Context: 不需要算分 ,可以利用Cache,获得更好的性能
如果多条查询子句被合并为一条复合查询语句,比如 bool查询,则每个查询子句计算得出的评分会被合并到总的相关性评分中
bool查询语法:
- 子查询可以任意顺序出现
- 可以嵌套多个查询
- 如果你的bool查询中,没有must条件,should中必须至少满足一条查询
使用上一篇的测试数据es_db。
GET es_db/_search
{
"query": {
"bool": {
"must": [
{
"match": { "sex": 1}
},
{
"match": {"address": "广州公园"}
}
]
}
}
}
# filter 等价于 must
GET es_db/_search
{
"query": {
"bool": {
"filter": [
{
"match": { "sex": 1}
},
{
"match": {"address": "广州公园"}
}
]
}
}
}
综合例子:
GET /es_db/_search
{
"query": {
"bool": {
"must": {
"match": {
"remark": "java developer"
}
},
"filter": {
"term": {
"sex": "1"
}
},
"must_not": {
"range": {
"age": {
"gte": 30
}
}
},
"should": [
{
"term": {
"address.keyword": {
"value": "广州天河公园"
}
}
},
{
"term": {
"address.keyword": {
"value": "广州白云山公园"
}
}
}
],
"minimum_should_match": 1
}
}
}
Boosting Query
测试数据
POST /blogs/_bulk
{"index":{"_id":1}}
{"title":"Apple iPad","content":"Apple iPad,Apple iPad"}
{"index":{"_id":2}}
{"title":"Apple iPad,Apple iPad","content":"Apple iPad"}
GET blogs/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"title": {
"query": "apple,ipad",
"boost": 0.5
}
}
},
{
"match": {
"content": {
"query": "apple,ipad",
"boost": 1
}
}
}
]
}
}
}
单字符串多字段查询
三种场景
- 最佳字段(Best Fields)
当字段之间相互竞争,又相互关联。例如,对于博客的 title和 body这样的字段,评分来自最匹配字段 - 多数字段(Most Fields)
处理英文内容时的一种常见的手段是,在主字段( English Analyzer),抽取词干,加入同义词,以
匹配更多的文档。相同的文本,加入子字段(Standard Analyzer),以提供更加精确的匹配。其他字段作为匹配文档提高相关度的信号,匹配字段越多则越好。 - 混合字段(Cross Field)
对于某些实体,例如人名,地址,图书信息。需要在多个字段中确定信息,单个字段只能作为整体的一部分。希望在任何这些列出的字段中找到尽可能多的词
最佳字段查询
PUT /blogs/_doc/1
{
"title": "Quick brown rabbits",
"body": "Brown rabbits are commonly seen."
}
PUT /blogs/_doc/2
{
"title": "Keeping pets healthy",
"body": "My quick brown fox eats rabbits on a regular basis."
}
GET /blogs/_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
]
}
}
}
得到的结果id=1的算分跟高,但是我们知道id=2的文档中包含了Brown fox。
可以通过tie_breaker参数调整
Tier Breaker是一个介于0-1之间的浮点数。0代表使用最佳匹配;1代表所有语句同等重要。
- 获得最佳匹配语句的评分_score 。
- 将其他匹配语句的评分与tie_breaker相乘
- 对以上评分求和并规范化
GET blogs/_search
{
"query": {
"multi_match": {
"query": "Brown fox",
"fields": ["title","body"],
"tie_breaker": 0
}
}
}
跨字段(Cross Field)搜索
测试数据:
DELETE /address
PUT /address
{
"settings" : {
"index" : {
"analysis.analyzer.default.type": "ik_max_word"
}
}
}
PUT /address/_bulk
{ "index": { "_id": "1"} }
{"province": "湖南","city": "长沙"}
{ "index": { "_id": "2"} }
{"province": "湖南","city": "常德"}
{ "index": { "_id": "3"} }
{"province": "广东","city": "广州"}
{ "index": { "_id": "4"} }
{"province": "湖南","city": "邵阳"}
要从province和city中查询湖南常德 通常的multi_match 并不能符合要求,可以使用cross_fields,支持operator。
GET /address/_search
{
"query": {
"multi_match": {
"query": "湖南常德",
"type": "cross_fields",
"operator": "and",
"fields": ["province","city"]
}
}
}
聚合查询
Elasticsearch除搜索以外,提供了针对ES 数据进行统计分析的功能。聚合(aggregations)可以让我们极其方便的实现对数据的统计、分析、运算。
语法:
"aggs" : { #和query同级的关键词
"<aggregation_name>" : { #自定义的聚合名字
"<aggregation_type>" : { #聚合的定义: 不同的type+body
<aggregation_body>
}
[,"meta" : { [<meta_data_body>] } ]?
[,"aggregations" : { [<sub_aggregation>]+ } ]? #子聚合查询
}
[,"<aggregation_name_2>" : { ... } ]* #可以包含多个同级的聚合查询
}
聚合的分类
Metric Aggregation:—些数学运算,可以对文档字段进行统计分析,类比Mysql中的 min(), max(), sum() 操作。
Bucket Aggregation: 一些满足特定条件的文档的集合放置到一个桶里,每一个桶关联一个key,类比Mysql中的group by操作。
Pipeline Aggregation:对其他的聚合结果进行二次聚合
测试数据:
DELETE /employees
#创建索引库
PUT /employees
{
"mappings": {
"properties": {
"age":{
"type": "integer"
},
"gender":{
"type": "keyword"
},
"job":{
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 50
}
}
},
"name":{
"type": "keyword"
},
"salary":{
"type": "integer"
}
}
}
}
PUT /employees/_bulk
{ "index" : { "_id" : "1" } }
{ "name" : "Emma","age":32,"job":"Product Manager","gender":"female","salary":35000 }
{ "index" : { "_id" : "2" } }
{ "name" : "Underwood","age":41,"job":"Dev Manager","gender":"male","salary": 50000}
{ "index" : { "_id" : "3" } }
{ "name" : "Tran","age":25,"job":"Web Designer","gender":"male","salary":18000 }
{ "index" : { "_id" : "4" } }
{ "name" : "Rivera","age":26,"job":"Web Designer","gender":"female","salary": 22000}
{ "index" : { "_id" : "5" } }
{ "name" : "Rose","age":25,"job":"QA","gender":"female","salary":18000 }
{ "index" : { "_id" : "6" } }
{ "name" : "Lucy","age":31,"job":"QA","gender":"female","salary": 25000}
{ "index" : { "_id" : "7" } }
{ "name" : "Byrd","age":27,"job":"QA","gender":"male","salary":20000 }
{ "index" : { "_id" : "8" } }
{ "name" : "Foster","age":27,"job":"Java Programmer","gender":"male","salary": 20000}
{ "index" : { "_id" : "9" } }
{ "name" : "Gregory","age":32,"job":"Java Programmer","gender":"male","salary":22000 }
{ "index" : { "_id" : "10" } }
{ "name" : "Bryant","age":20,"job":"Java Programmer","gender":"male","salary": 9000}
{ "index" : { "_id" : "11" } }
{ "name" : "Jenny","age":36,"job":"Java Programmer","gender":"female","salary":38000 }
{ "index" : { "_id" : "12" } }
{ "name" : "Mcdonald","age":31,"job":"Java Programmer","gender":"male","salary": 32000}
{ "index" : { "_id" : "13" } }
{ "name" : "Jonthna","age":30,"job":"Java Programmer","gender":"female","salary":30000 }
{ "index" : { "_id" : "14" } }
{ "name" : "Marshall","age":32,"job":"Javascript Programmer","gender":"male","salary": 25000}
{ "index" : { "_id" : "15" } }
{ "name" : "King","age":33,"job":"Java Programmer","gender":"male","salary":28000 }
{ "index" : { "_id" : "16" } }
{ "name" : "Mccarthy","age":21,"job":"Javascript Programmer","gender":"male","salary": 16000}
{ "index" : { "_id" : "17" } }
{ "name" : "Goodwin","age":25,"job":"Javascript Programmer","gender":"male","salary": 16000}
{ "index" : { "_id" : "18" } }
{ "name" : "Catherine","age":29,"job":"Javascript Programmer","gender":"female","salary": 20000}
{ "index" : { "_id" : "19" } }
{ "name" : "Boone","age":30,"job":"DBA","gender":"male","salary": 30000}
{ "index" : { "_id" : "20" } }
{ "name" : "Kathy","age":29,"job":"DBA","gender":"female","salary": 20000}
Metric Aggregation
单值分析︰只输出一个分析结果
- min, max, avg, sum
- Cardinality(类似distinct Count)
多值分析:输出多个分析结果 - stats(统计), extended stats
- percentile (百分位), percentile rank
- top hits(排在前面的示例)
#单值分析
GET employees/_search
{
"aggs": {
"max_privae": {
"max": {
"field": "salary"
}
}
}
}
#去重
GET employees/_search
{
"aggs": {
"aaaa":{
"cardinality": {
"field": "job.keyword"
}
}
}
}
以上语法都是带有文档信息返回,如果只需要统计信息而不需要文档信息
#对salary进行统计 size 表示输出文档信息的条数
GET employees/_search
{
"size": 0,
"aggs": {
"aaa": {
"stats": {
"field": "salary"
}
}
}
}
#并行查询最大最小平均值
GET employees/_search
{
"size": 0,
"aggs": {
"aaa": {
"max": {
"field": "salary"
}
},
"bbb":{
"min": {
"field": "salary"
}
},
"ccc":{
"avg": {
"field": "salary"
}
}
}
}
Bucket Aggregation
按照一定的规则,将文档分配到不同的桶中,从而达到分类的目的。ES提供的一些常见的 Bucket Aggregation。
Terms,需要字段支持filedata
- keyword 默认支持fielddata
- text需要在Mapping 中开启fielddata,会按照分词后的结果进行分桶
数字类型
- Range / Data Range
- Histogram(直方图) / Date Histogram
支持嵌套: 也就在桶里再做分桶
聚合可配置属性有:
field:指定聚合字段
size:指定聚合结果数量
order:指定聚合结果排序方式
默认情况下,Bucket聚合会统计Bucket内的文档数量,记为_count,并且按照_count降序排序。我们可以指定order属性,自定义聚合的排序方式:
# 对keyword 进行分桶
GET /employees/_search
{
"size": 0,
"aggs": {
"jobs": {
"terms": {
"field":"job.keyword",
"size": 3,
"order": {
"_count": "desc"
}
}
}
}
}
限定聚合范围
GET employees/_search
{
"size": 0,
"query": {
"range": {
"salary": {
"gte": 10000
}
}
},
"aggs": {
"jobs": {
"terms": {
"field": "job.keyword",
"size": 10
}
}
}
}
注意:对 Text 字段进行 terms 聚合查询,会失败抛出异常,需要对 Text 字段打开 fielddata,支持terms aggregation
Range & Histogram聚合
#Salary Range分桶,可以自己定义 key
GET employees/_search
{
"size": 0,
"aggs": {
"aaa": {
"range": {
"field": "salary",
"ranges": [
{
"to": 10000
},
{
"from": 10000,
"to": 20000
},
{
"key":">20000",
"from":20000
}
]
}
}
}
}
#按照工资的间隔分桶 工资0到10万,以工资间隔5000一个区间进行分桶
GET employees/_search
{
"size": 0,
"aggs": {
"aaaa":{
"histogram": {
"field": "salary",
"interval": 5000,
"extended_bounds": {
"min": 0,
"max": 100000
}
}
}
}
}
嵌套聚合示例
# 多次嵌套。根据工作类型分桶,然后按照性别分桶,计算工资的统计信息
GET employees/_search
{
"size": 0,
"aggs": {
"aaaa":{
"terms": {
"field": "job.keyword"
},
"aggs": {
"bbb": {
"terms": {
"field": "gender"
},
"aggs": {
"ccc": {
"stats": {
"field": "salary"
}
}
}
}
}
}
}
}
Pipeline Aggregation
支持对聚合分析的结果,再次进行聚合分析。Pipeline 的分析结果会输出到原结果中,根据位置的不同,分为两类:
Sibling - 结果和现有分析结果同级
- Max,min,Avg & Sum Bucket
- Stats,Extended Status Bucket
- Percentiles Bucket
Parent -结果内嵌到现有的聚合分析结果之中
- Derivative(求导)
- Cumultive Sum(累计求和)
- Moving Function(移动平均值 )
#在各个工种里,找出平均工资最低的工种
GET employees/_search
{
"size": 0,
"aggs": {
"jobs": {
"terms": {
"field": "job.keyword",
"size": 3
},
"aggs": {
"avg_salary": {
"avg": {
"field": "salary"
}
}
}
},
"aaa":{
"min_bucket": {
"buckets_path": "jobs>avg_salary"
}
}
}
}
min_salary_by_job结果和jobs的聚合同级
min_bucket求之前结果的最小值
通过bucket_path关键字指定路径
ES聚合分析不精准原因分析
ElasticSearch在对海量数据进行聚合分析的时候会损失搜索的精准度来满足实时性的需求
不精准的原因: 数据分散到多个分片,聚合是每个分片的取 Top X,导致结果不精准。ES 可以不每个分片Top X,而是全量聚合,但势必这会有很大的性能问题。
如何提高聚合精确度?
方案1:设置主分片为1
注意7.x版本已经默认为1。
适用场景:数据量小的小集群规模业务场景。
方案2:调大 shard_size 值
设置 shard_size 为比较大的值,官方推荐:size*1.5+10。shard_size 值越大,结果越趋近于精准聚合结果值。此外,还可以通过show_term_doc_count_error参数显示最差情况下的错误值,用于辅助确定 shard_size 大小。
- size:是聚合结果的返回值,客户期望返回聚合排名前三,size值就是 3。
- shard_size: 每个分片上聚合的数据条数。shard_size 原则上要大于等于 size
适用场景:数据量大、分片数多的集群业务场景。
方案3:将size设置为全量值,来解决精度问题
将size设置为2的32次方减去1也就是分片支持的最大值,来解决精度问题。
原因:1.x版本,size等于 0 代表全部,高版本取消 0 值,所以设置了最大值(大于业务的全量值)。
全量带来的弊端就是:如果分片数据量极大,这样做会耗费巨大的CPU 资源来排序,而且可能会阻塞网络。
适用场景:对聚合精准度要求极高的业务场景,由于性能问题,不推荐使用。
方案4:使用Clickhouse/ Spark 进行精准聚合
适用场景:数据量非常大、聚合精度要求高、响应速度快的业务场景。