前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【你真的会用ES吗】ES基础介绍(二)

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

原创
作者头像
Lynalmost
修改2022-07-01 13:16:13
修改2022-07-01 13:16:13
1.5K1
举报
文章被收录于专栏:小李的文章专栏

前言

在上一篇文章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:
  • 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 AnalyzerSimple AnalyzerWhitespace AnalyzerStop AnalyzerKeyword AnalyzerPattern AnalyzerLanguage AnalyzersFingerprint Analyzer,并且支持定制化。 这里的内置分词器看起来都比较简单,这里简单介绍一下Standard Analyzer、Keyword Analyzer,其他的分词器大家感兴趣可以自行查阅。

text 类型默认analyzer:Standard Analyzer

Standard Analyzer的组成部分:

如果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 参数,我们可以指定缺失值填充,但是它太隐蔽了?,其默认值为 _last

代码语言:javascript
复制
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]}
    ]
}

使用技巧:用function score实现自定义排序

不知道大家是否遇到过类似的场景:期望查询结果按照某个类型进行排序,或者查询结果顺序由多个字段的权重组合决定。

具体解决方案需要根据业务具体情况而定,这里给出一种基于ES查询的解决方案。ES为我们提供了 function score ,支持自定义相关性得分score的生成方式,部分参数介绍:

  • weight:权重值
  • boost:加权值
  • boost_mode:加权值计算方式(默认为multiple)
  • score_mode:得分计算方式(默认为multiple)

举点实际的栗子,假设咱们有一个存放水果的Index:

  • 简单一点的case:查询结果根据水果类型苹果,梨优先 苹果的优先级高于梨的优先级,梨的优先级高于其他水果的优先级。我们可以定义梨的权重为1,苹果的权重为2
代码语言:javascript
复制
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"
    }
  }
}
  • 复杂一点的case(别问我是怎么想到的):
    • 优先级一:根据水果是否有货排序,有货的排前面,无货的过滤掉
    • 优先级二:根据水果是否预售排序,非预售优先展示
    • 优先级三:根据水果类型苹果,梨优先展示
    • 优先级四:根据水果颜色红色,绿色优先展示
    • 优先级五:根据价格升序排序 我们根据优先级顺序定义每个条件的权重,指定自定义相关性得分规则后,在 sort 中指定先根据 _score 降序排列,再根据价格升序排列。
    • 优先级四:绿色权重 1 、红色权重 2
    • 优先级三:梨权重 3 、苹果权重 4
    • 优先级二:预售权重 7(优先级四max + 优先级三max = 6,优先级二的权重必须大于这个值)
    • 优先级一:直接将无货水果过滤
代码语言:javascript
复制
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"}
    }
  ]
}

聚合aggs

聚合操作可以帮助我们将查询数据按照指定的方式进行归类。常见的聚合方式,诸如:max、min、avg、range、根据term聚合等等,这些都比较好理解,功能使用上也没有太多疑惑,下面主要介绍题主在使用过程中遇到的坑点以及指标聚合嵌套查询。

ES还支持pipline aggs,主要针对的对象不是文档集,而是其他聚合的结果,感兴趣的同学可以自行了解。

好坑啊:ES默认的时间格式为毫秒级时间

如果你有诉求,需要针对秒级时间戳进行时间聚合,例如:某销售场景下,我们期望按小时/天/月/进行销售单数统计。

那么有以下两种常见错误使用方式需要规避:

  • 如果在创建 date 类型字段,但是没有指定时间format格式,并且以秒级时间戳赋值(直接以年月日赋值没有问题) 根据时间聚合将无法解析出正确的数据,时间会被解析为1970年
  • 如果直接使用 numberic 类型,例如 integer 存储时间戳 不管是秒级还是毫秒级,都无法被正确识别

正确的做法:创建mapping,明确指定时间的格式为秒级时间戳。

代码语言:javascript
复制
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
        }
      ]
    }
  }
}

聚合嵌套查询

上面介绍了根据时间聚合,还是以刚刚的例子来说,某销售场景下,我们期望在根据时间统计销售单数的同时,统计出时间区间内的销售总金额。

代码语言:javascript
复制
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
          }
        }
      ]
    }
  }
}

使用技巧:自实现distinct

ES默认并不支持distinct,可以尝试使用 terms 聚合,解析结果中的key

代码语言:javascript
复制
{
"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}
      ]
    }
  }
}

索引别名、索引生命周期策略、索引模版

  • Aliases 索引别名 索引别名,顾名思义,定义了别名之后,可以通过别名对index进行查询 PUT /[your index]/_alias/[your alias name]
  • Index Lifecycle Policies 索引生命周期策略 索引生命周期策略支持我们根据天、存储量级等信息去自动管理我们的索引。 创建方式可以通过RESTful API,也可以直接在kibana上创建,题主使用的是后者,可视化界面看起来比较清晰~ 支持配置满足一定规则后索引自动变化:
    • 自动滚动索引(hot)
    • 保留索引仅供检索(warm)
    • 保留索引仅供检索同时减少磁盘存储(cold)
    • 删除索引
  • Template 索引模板 通过 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

https://code.google.com/archive/p/ik-analyzer/

https://zhuanlan.zhihu.com/p/79202151

原创声明:本文系作者授权云服务器哪家好开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权云服务器哪家好开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • ES基础使用介绍
    • 分析器 Analyzer
      • text 类型默认analyzer:Standard Analyzer
      • keyword 类型默认analyzer:Keyword Analyzer
      • 第三方中文分词器 ik
    • 相似性得分 similarity
      • classic:基于TF/IDF实现,V7已禁止使用,V8彻底废除(仅供了解)
      • BM25:默认
      • boolean
    • query and filter context
      • 排序sort
        • 好坑啊:缺失数值类字段的默认值并不是0
        • 使用技巧:用function score实现自定义排序
      • 聚合aggs
        • 好坑啊:ES默认的时间格式为毫秒级时间
        • 聚合嵌套查询
        • 使用技巧:自实现distinct
      • 索引别名、索引生命周期策略、索引模版
      • 参考
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
      http://www.vxiaotou.com