在上一篇文章ES基础信息(一)中,介绍了ES的背景、版本更新细则、建立索引所需要了解的基础概念以及常用的搜索关键字。本篇文章会继续补充一些全文索引相关的内容,分析器,相关性得分等等。
ES除了通过倒排索引实现全文检索之外,常用的功能还有聚合及排序,这是本篇文章的重点之一。这里需要大家提前知道一点:通过倒排索引的方式去实现聚合和排序,是非常不现实的,ES(其实是底层Lucene)底层将数据转成了另一个结构存储以实现这个逻辑,它就是DocValues,基于列式存储的数据格式。
除此之外,本文会介绍ES提供的一些比较好用的功能,索引别名、索引生命周期策略以及索引模版。这也是本系列文章中最后一篇关于功能点介绍的文章,ES的功能点远不如此,如果后续有Get到新的好用功能(欢迎评论或RTX讨论分享),会持续更新文章内容。后续文章会针对ES(Lucene)进行更深入的介绍。
在上一篇文章中提到了,针对全文索引类型,一定要选择合适的分析器,现在我们就来了解一下分析器~
Analyzer主要是对输入的文本类内容进行分析(通常是分词),将分析结果以 term
的形式进行存储。
Analyzer由三个部分组成:Character Filters、Tokenizer、Token Filters
ES内置的分析器有Standard Analyzer、Simple Analyzer、Whitespace Analyzer、Stop Analyzer、Keyword Analyzer、Pattern Analyzer、Language Analyzers、Fingerprint Analyzer,并且支持定制化。 这里的内置分词器看起来都比较简单,这里简单介绍一下Standard Analyzer、Keyword Analyzer,其他的分词器大家感兴趣可以自行查阅。
Standard Analyzer的组成部分:
max_token_length
参数指定token长度,默认为255。stopwords
或 stopwords_path
进行指定。如果text类型没有指定Analyzer,Standard Analyzer,前面我们已经了解了ES分析器的结构,理解它的分析器应该不在话下。Unicode文本分割算法依据的标准,给出了文本中词组、单词、句子的默认分割边界。该附件在notes中提到,像类似中文这种复杂的语言,并没有明确的分割边界,简而言之就是说,中文并不适用于这个标准。
通常我们的全文检索使用场景都是针对中文的,所以我们在创建我们的映射关系时,一定要指定合适的分析器。
Keyword Analyzer本质上就是一个"noop" Analyzer,直接将输入的内容作为一整个token。
github地址:https://github.com/medcl/elasticsearch-analysis-ik
IK Analyzer是一个开源的,基于java语言开发的轻量级的中文分词工具包。从2006年12月推出1.0版开始, IKAnalyzer已经推出了4个大版本。最初,它是以开源项目Luence为应用主体的,结合词典分词和文法分析算法的中文分词组件。从3.0版本开始,IK发展为面向Java的公用分词组件,独立于Lucene项目,同时提供了对Lucene的默认优化实现。在2012版本中,IK实现了简单的分词歧义排除算法,标志着IK分词器从单纯的词典分词向模拟语义分词衍化。
使用方式:
// mapping创建
PUT /[your index]
{
"mappings": {
"properties": {
"text_test":{
"type": "text",
"analyzer": "ik_smart"
}
}
}
}
?
// 新建document
POST /[your index]/_doc
{
"text_test":"我爱中国"
}
?
//查看term vector
GET /[your index]/_termvectors/ste3HYABZRKvoZUCe2oH?fields=text_test
//结果包含了 “我”“爱”“中国”
TF/IDF介绍文章:https://zhuanlan.zhihu.com/p/31197209
TF/IDF使用逆文档频率作为权重,降低常见词汇带来的相似性得分。从公式中可以看出,这个相似性算法仅与文档词频相关,覆盖不够全面。例如:缺少文档长度带来的权重,当其他条件相同,“王者荣耀”这个查询关键字同时出现在短篇文档和长篇文档中时,短篇文档的相似性其实更高。
在ESV5之前,ES使用的是Lucene基于TF/IDF自实现的一套相关性得分算法,如下所示:
score(q,d) =
queryNorm(q)
· coord(q,d)
· ∑ (
tf(t in d)
· idf(t)?
· t.getBoost()
· norm(t,d)
) (t in q)
Lucene已经针对TF/IDF做了尽可能的优化,但是有一个问题仍然无法避免:
另一条曲线是BM25算法相似性得分随词频的关系,它的结果随词频上升而趋于一个稳定值。
BM25介绍文章:https://en.wikipedia.org/wiki/Okapi_BM25 ,对BM25的实现细节我们在这里不做过多阐述,主要了解一下BM25算法相较于之前的算法有哪些优点:
我们在查询过程可以通过设置 "explain":true
查看相似性得分的具体情况
GET /[your index]/_search
{
"explain": true,
"query": {
"match": {
"describe": "测试"
}
}
}
//简化版查询结果
{
"_explanation": {
"value": 0.21110919,
"description": "weight(describe:测试 in 1) [PerFieldSimilarity], result of:",
"details": [
{
"value": 0.21110919,
"description": "score(freq=1.0), computed as boost * idf * tf from:",
"details": [
{
"value": 2.2,
"description": "boost",
"details": []
},
{
"value": 0.18232156,
"description": "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:",
"details": [...]
},
{
"value": 0.5263158,
"description": "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:",
"details": [...]
}
]
}
]
}
}
boolean相似性非常好理解,只能根据查询条件是否匹配,其最终值其实就是query boost值。
他们本质上的区别是是否参与相关性得分。在查询过程中,官方建议可以根据实际使用情况配合使用 filter
和 query
。但是如果你的查询并不关心相关性得分,仅关心查询到的结果,其实两者差别不大。
题主本来以为使用filter可以节省计算相似性得分的耗时,但是使用filter同样会进行相似性得分,只是通过特殊的方式将其value置为了0。
//only query
GET /[your index]/_search
{
"explain": true,
"query": {
"bool": {
"must": [
{"match": {"describe": "测试"}},
{"term": {"tab_id": 5}}
]
}
}
}
//简化_explanation结果
{
"_explanation": {
"value": 1.2111092,
"description": "sum of:",
"details": [
{
"value": 0.21110919,
"description": "weight(describe:测试 in 1) [PerFieldSimilarity], result of:"
},
{
"value": 1,
"description": "tab_id:[5 TO 5]",
"details": []
}
]
}
}
?
//query+filter
GET /[your index]/_search
{
"explain": true,
"query": {
"bool": {
"filter": [
{"term": {"tab_id": "5"}}
],
"must": [
{"match": {"describe": "测试"}}
]
}
}
}
//简化_explanation结果
{
"_explanation": {
"value": 0.21110919,
"description": "sum of:",
"details": [
{
"value": 0.21110919,
"description": "weight(describe:测试 in 1) [PerFieldSimilarity], result of:"
},
{
"value": 0,
"description": "match on required clause, product of:",
"details": [
{
"value": 0,
"description": "# clause",
"details": []
},
{
"value": 1,
"description": "tab_id:[5 TO 5]",
"details": []
}
]
}
]
}
}
在执行ES查询时,默认的排序规则是根据相关性得分倒排,针对非全文索引字段,可以指定排序方式,使用也非常简单。
//查询时先根据tab_id降序排列,若tab_id相同,则根究status升序排列
GET /[your index]/_search
{
"sort": [
{"tab_id": {"order": "desc"}},
{"status": {"order": "asc"}}
]
}
事情的背景
题主使用的编程语言是golang,通常使用pb定义结构体,生成对应的go代码,默认情况下,结构体字段的json tag都会包含 omitempty
属性,也就是忽略空值,如果数字类型的value为0,进行json marshall时,不会生成对应字段。
事情的经过
刚好题主通过以上方式进行文档变更,所以实际上如果某个数值字段为0,它并没有被存储。
在题主的功能逻辑里,刚好需要对某个数值字段做升序排列,惊奇地发现我认为的字段值为0的文档,出现在了列表最末。
事情的调查结果
针对缺失数值类字段的默认值并不是0,ES默认会保证排序字段没有value的文档被放在最后,默认情况下:
好消息是,ES为我们提供了 missing
参数,我们可以指定缺失值填充,但是它太隐蔽了?,其默认值为 _last
。
GET /[your index]/_search
{
"sort": [
{"num": {"order": "asc"}}
]
}
//简化结果
{
"hits": [
{"sort": [1]},
{"sort": [9223372036854775807]},
{"sort": [9223372036854775807]}
]
}
?
GET /your_index/_search
{
"sort": [
{"num": {"order": "desc"}}
]
}
?
//简化结果
{
"hits": [
{"sort": [1]},
{"sort": [-9223372036854775808]},
{"sort": [-9223372036854775808]}
]
}
?
// with missing
GET /[your index]/_search
{
"sort": [
{
"num": {
"order": "asc",
"missing": "0"
}
}
]
}
?
//简化结果
{
"hits": [
{"sort": [0]},
{"sort": [0]},
{"sort": [1]}
]
}
不知道大家是否遇到过类似的场景:期望查询结果按照某个类型进行排序,或者查询结果顺序由多个字段的权重组合决定。
具体解决方案需要根据业务具体情况而定,这里给出一种基于ES查询的解决方案。ES为我们提供了 function score
,支持自定义相关性得分score的生成方式,部分参数介绍:
举点实际的栗子,假设咱们有一个存放水果的Index:
GET /fruit_test/_search
{
"explain": true,
"query": {
"function_score": {
"functions": [
{
"filter": {"term": {"type": "pear"}},
"weight": 1
},
{
"filter": {"term": {"type": "apple"}},
"weight": 2
}
],
"boost": 1,
"score_mode": "sum"
}
}
}
sort
中指定先根据 _score
降序排列,再根据价格升序排列。GET /fruit_test/_search
{
"query": {
"function_score": {
"query": {"range": {"stock": {"gt": 0}}
},
"functions": [
{
"filter": {"term": {"color": "green"}},
"weight": 1
},
{
"filter": {"term": {"color": "red"}},
"weight": 2
},
{
"filter": {"term": {"type": "pear"}},
"weight": 3
},
{
"filter": {"term": {"type": "apple"}},
"weight": 4
},
{
"filter": {"term": {"pre_sale": false}},
"weight": 7
}
],
"boost": 1,
"boost_mode": "sum",
"score_mode": "sum"
}
},
"sort": [
{"_score": {"order": "desc"}},
{"price_per_kg": {"order": "asc"}
}
]
}
聚合操作可以帮助我们将查询数据按照指定的方式进行归类。常见的聚合方式,诸如:max、min、avg、range、根据term聚合等等,这些都比较好理解,功能使用上也没有太多疑惑,下面主要介绍题主在使用过程中遇到的坑点以及指标聚合嵌套查询。
ES还支持pipline aggs,主要针对的对象不是文档集,而是其他聚合的结果,感兴趣的同学可以自行了解。
如果你有诉求,需要针对秒级时间戳进行时间聚合,例如:某销售场景下,我们期望按小时/天/月/进行销售单数统计。
那么有以下两种常见错误使用方式需要规避:
date
类型字段,但是没有指定时间format格式,并且以秒级时间戳赋值(直接以年月日赋值没有问题) 根据时间聚合将无法解析出正确的数据,时间会被解析为1970年numberic
类型,例如 integer
存储时间戳 不管是秒级还是毫秒级,都无法被正确识别正确的做法:创建mapping,明确指定时间的格式为秒级时间戳。
PUT /date_test/_mapping
{
"properties":{
"create_time":{
"type":"date",
"format" : "epoch_second"
}
}
}
?
//以年为时间间隔 进行统计
GET /date_test/_search
{
"size": 0,
"aggs": {
"test": {
"date_histogram": {
"field": "create_time",
"format": "yyyy",
"interval": "year"
}
}
}
}
//从查询结果可以看出来,实际计算时ES会帮我们把秒级时间戳转成毫秒级时间戳
{
"aggregations" : {
"test" : {
"buckets" : [
{
"key_as_string" : "2018",
"key" : 1514764800000,
"doc_count" : 2
},
{
"key_as_string" : "2019",
"key" : 1546300800000,
"doc_count" : 0
},
{
"key_as_string" : "2020",
"key" : 1577836800000,
"doc_count" : 3
}
]
}
}
}
上面介绍了根据时间聚合,还是以刚刚的例子来说,某销售场景下,我们期望在根据时间统计销售单数的同时,统计出时间区间内的销售总金额。
GET /date_test/_search
{
"size": 0,
"aggs": {
"test": {
"date_histogram": {
"field": "create_time",
"format": "yyyy",
"interval": "year"
},
"aggs": {
"sum_profit": {
"sum": {
"field": "profit"
}
}
}
}
}
}
?
{
"aggregations" : {
"test" : {
"buckets" : [
{
"key_as_string" : "2018",
"key" : 1514764800000,
"doc_count" : 2,
"sum_profit" : {
"value" : 200.0
}
},
{
"key_as_string" : "2019",
"key" : 1546300800000,
"doc_count" : 0,
"sum_profit" : {
"value" : 0.0
}
},
{
"key_as_string" : "2020",
"key" : 1577836800000,
"doc_count" : 3,
"sum_profit" : {
"value" : 3000.0
}
}
]
}
}
}
ES默认并不支持distinct,可以尝试使用 terms
聚合,解析结果中的key
{
"aggregations" : {
"test" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{"key" : "1","doc_count" : 2},
{"key" : "10","doc_count" : 2},
{"key" : "16","doc_count" : 2}
]
}
}
}
PUT /[your index]/_alias/[your alias name]
index_patterns
参数设置索引名正则匹配规则,向一个不存在的索引POST数据,命中索引名规则后即会根据索引模版创建索引,不会进行动态映射。ES的一个比较常见的应用场景是存储日志流,自实现一套这样的系统就可以结合上述3个功能。
https://www.elastic.co/guide/en/elasticsearch/reference/7.7/index.html
https://blog.csdn.net/laoyang360/article/details/80468757
https://www.elastic.co/guide/en/elasticsearch/reference/7.0/query-filter-context.html
https://zhuanlan.zhihu.com/p/31197209
原创声明:本文系作者授权云服务器哪家好开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权云服务器哪家好开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。