【你真的会用ES吗】ES基础介绍(二)

2022-07-01 13:16:13 浏览数 (1)

前言

在上一篇文章ES基础信息(一)中,介绍了ES的背景、版本更新细则、建立索引所需要了解的基础概念以及常用的搜索关键字。本篇文章会继续补充一些全文索引相关的内容,分析器,相关性得分等等。

ES除了通过倒排索引实现全文检索之外,常用的功能还有聚合及排序,这是本篇文章的重点之一。这里需要大家提前知道一点:通过倒排索引的方式去实现聚合和排序,是非常不现实的,ES(其实是底层Lucene)底层将数据转成了另一个结构存储以实现这个逻辑,它就是DocValues,基于列式存储的数据格式。

除此之外,本文会介绍ES提供的一些比较好用的功能,索引别名、索引生命周期策略以及索引模版。这也是本系列文章中最后一篇关于功能点介绍的文章,ES的功能点远不如此,如果后续有Get到新的好用功能(欢迎评论或RTX讨论分享),会持续更新文章内容。后续文章会针对ES(Lucene)进行更深入的介绍。

ES基础使用介绍

分析器 Analyzer

在上一篇文章中提到了,针对全文索引类型,一定要选择合适的分析器,现在我们就来了解一下分析器~

Analyzer主要是对输入的文本类内容进行分析(通常是分词),将分析结果以 term 的形式进行存储。

Analyzer由三个部分组成:Character FiltersTokenizerToken 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,需通过参数 stopwordsstopwords_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 查看相似性得分的具体情况

代码语言:javascript复制
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的关注点除了是否之外,还关注这些文档的匹配度有多高

他们本质上的区别是是否参与相关性得分。在查询过程中,官方建议可以根据实际使用情况配合使用 filterquery 。但是如果你的查询并不关心相关性得分,仅关心查询到的结果,其实两者差别不大

题主本来以为使用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 参数,我们可以指定缺失值填充,但是它太隐蔽了

0 人点赞