前言
在上一篇文章ES基础信息(一)中,介绍了ES的背景、版本更新细则、建立索引所需要了解的基础概念以及常用的搜索关键字。本篇文章会继续补充一些全文索引相关的内容,分析器,相关性得分等等。
ES除了通过倒排索引实现全文检索之外,常用的功能还有聚合及排序,这是本篇文章的重点之一。这里需要大家提前知道一点:通过倒排索引的方式去实现聚合和排序,是非常不现实的,ES(其实是底层Lucene)底层将数据转成了另一个结构存储以实现这个逻辑,它就是DocValues,基于列式存储的数据格式。
除此之外,本文会介绍ES提供的一些比较好用的功能,索引别名、索引生命周期策略以及索引模版。这也是本系列文章中最后一篇关于功能点介绍的文章,ES的功能点远不如此,如果后续有Get到新的好用功能(欢迎评论或RTX讨论分享),会持续更新文章内容。后续文章会针对ES(Lucene)进行更深入的介绍。
ES基础使用介绍
分析器 Analyzer
在上一篇文章中提到了,针对全文索引类型,一定要选择合适的分析器,现在我们就来了解一下分析器~
Analyzer主要是对输入的文本类内容进行分析(通常是分词),将分析结果以 term
的形式进行存储。
Analyzer由三个部分组成:Character Filters、Tokenizer、Token Filters
- Character Filters Character Filters以characters流的方式接收原始数据,它可以支持characters的增、删、改,通常内置的分析器都没有设置默认的Character Filters。 ES内置的Character Filters:
- HTML Strip Character Filter:支持剔除html标签,解码
- Mapping Character Filter:支持根据定义的映射进行替换
- Pattern Replace Character Filter:支持根据正则进行替换
- Tokenizer Tokenizer接收一个字符流,分解成独立的tokens(通常就是指的分词),并且输出tokens。例如,一个 whitespace tokenizer(空格tokenizer),以空格作为分割词对输入内容进行分词。 例如:向whitespace tokenizer输入“Quick brown fox!”,将会输出“Quick”、 “brown”、“fox!” 3个token。
- Token Filters Token filters 接收Tokenizer输出的token序列,它可以根据配置进行token的增、删、改。 例如:指定synonyms增加token、指定remove stopwords进行token删除,抑或是使用lowercasing进行小写转换。
ES内置的分析器有Standard Analyzer、Simple Analyzer、Whitespace Analyzer、Stop Analyzer、Keyword Analyzer、Pattern Analyzer、Language Analyzers、Fingerprint Analyzer,并且支持定制化。 这里的内置分词器看起来都比较简单,这里简单介绍一下Standard Analyzer、Keyword Analyzer,其他的分词器大家感兴趣可以自行查阅。
text 类型默认analyzer:Standard Analyzer
Standard Analyzer的组成部分:
- Tokenizer Standard Tokenizer:基于Unicode文本分割算法-Unicode标准附件# 29,支持使用
max_token_length
参数指定token长度,默认为255。 - Token Filters
- Lower Case Token Filter
- Stop Token Filter :默认没有stop token/words,需通过参数
stopwords
或stopwords_path
进行指定。
如果text类型没有指定Analyzer,Standard Analyzer,前面我们已经了解了ES分析器的结构,理解它的分析器应该不在话下。Unicode文本分割算法依据的标准,给出了文本中词组、单词、句子的默认分割边界。该附件在notes中提到,像类似中文这种复杂的语言,并没有明确的分割边界,简而言之就是说,中文并不适用于这个标准。
通常我们的全文检索使用场景都是针对中文的,所以我们在创建我们的映射关系时,一定要指定合适的分析器。
keyword 类型默认analyzer:Keyword Analyzer
Keyword Analyzer本质上就是一个"noop" Analyzer,直接将输入的内容作为一整个token。
第三方中文分词器 ik
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分词器从单纯的词典分词向模拟语义分词衍化。
使用方式:
代码语言:javascript复制// 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
//结果包含了 “我”“爱”“中国”
相似性得分 similarity
classic:基于TF/IDF实现,V7已禁止使用,V8彻底废除(仅供了解)
TF/IDF介绍文章:https://zhuanlan.zhihu.com/p/31197209
TF/IDF使用逆文档频率作为权重,降低常见词汇带来的相似性得分。从公式中可以看出,这个相似性算法仅与文档词频相关,覆盖不够全面。例如:缺少文档长度带来的权重,当其他条件相同,“王者荣耀”这个查询关键字同时出现在短篇文档和长篇文档中时,短篇文档的相似性其实更高。
在ESV5之前,ES使用的是Lucene基于TF/IDF自实现的一套相关性得分算法,如下所示:
代码语言:javascript复制score(q,d) =
queryNorm(q)
· coord(q,d)
· ∑ (
tf(t in d)
· idf(t)²
· t.getBoost()
· norm(t,d)
) (t in q)
- queryNorm:query normalization factor 查询标准化因子,旨在让不同查询之间的相关性结果可以进行比较(实际上ES的tips中提到,并不推荐大家这样做,不同查询之间的决定性因素是不一样的)
- coord:coordination factor 协调因子,query经过分析得到的terms在文章中命中的数量越多,coord值越高。 例如:查询“王者荣耀五周年”,terms:“王者”、“荣耀”、“五周年”,同时包含这几个term的文档coord值越高
- tf:词频
- idf:文档逆频率
- boost:boost翻译过来是增长推动的意思,这里可以理解为一个支持可配的加权参数。
- norm:文档长度标准化,内容越长,值越小
Lucene已经针对TF/IDF做了尽可能的优化,但是有一个问题仍然无法避免:
- 词频饱和度问题,如下图所示,TF/IDF算法的相似性得分会随着词频不断上升。 在Lucene现有的算法中,如果一个词出现的频率过高,会直接忽略掉文档长度带来的权重影响。
另一条曲线是BM25算法相似性得分随词频的关系,它的结果随词频上升而趋于一个稳定值。
BM25:默认
BM25介绍文章:https://en.wikipedia.org/wiki/Okapi_BM25 ,对BM25的实现细节我们在这里不做过多阐述,主要了解一下BM25算法相较于之前的算法有哪些优点:
- 词频饱和 不同于TF/IDF,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
boolean相似性非常好理解,只能根据查询条件是否匹配,其最终值其实就是query boost值。
query and filter context
- filter Does this document match this query clause? filter只关心是/否,根据你过滤条件给你筛选出默认的文档
- query how well does this document match this query clause? query的关注点除了是否之外,还关注这些文档的匹配度有多高
他们本质上的区别是是否参与相关性得分。在查询过程中,官方建议可以根据实际使用情况配合使用 filter
和 query
。但是如果你的查询并不关心相关性得分,仅关心查询到的结果,其实两者差别不大。
题主本来以为使用filter可以节省计算相似性得分的耗时,但是使用filter同样会进行相似性得分,只是通过特殊的方式将其value置为了0。
代码语言:javascript复制//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": []
}
]
}
]
}
}
排序sort
在执行ES查询时,默认的排序规则是根据相关性得分倒排,针对非全文索引字段,可以指定排序方式,使用也非常简单。
代码语言:javascript复制//查询时先根据tab_id降序排列,若tab_id相同,则根究status升序排列
GET /[your index]/_search
{
"sort": [
{"tab_id": {"order": "desc"}},
{"status": {"order": "asc"}}
]
}
好坑啊:缺失数值类字段的默认值并不是0
事情的背景
题主使用的编程语言是golang,通常使用pb定义结构体,生成对应的go代码,默认情况下,结构体字段的json tag都会包含 omitempty
属性,也就是忽略空值,如果数字类型的value为0,进行json marshall时,不会生成对应字段。
事情的经过
刚好题主通过以上方式进行文档变更,所以实际上如果某个数值字段为0,它并没有被存储。
在题主的功能逻辑里,刚好需要对某个数值字段做升序排列,惊奇地发现我认为的字段值为0的文档,出现在了列表最末。
事情的调查结果
针对缺失数值类字段的默认值并不是0,ES默认会保证排序字段没有value的文档被放在最后,默认情况下:
- 降序排列,缺失字段默认值为该字段类型的最小值
- 升序排列,缺失字段默认值为该字段类型的最大值
好消息是,ES为我们提供了 missing
参数,我们可以指定缺失值填充,但是它太隐蔽了