ElasticSearch

2,087 阅读50分钟

本文大部分笔记来自某网站ElasticSearch实战学习,其中少部分总结来自Elastic Search性能优化,也有一些自己在项目中实践的内容以及公司大佬的分享。如有不正确,请指明,在下进行更改。

ElasticSearch

ElasticSearch入门

文档Document

用户存储在es中的数据文档,是es存储数据的最小的一个单元。类似于关系型数据表中的一行数据。

每一个文档都有一个唯一的id标识:自行指定、es自动生成

Json Object,由字段(Field)组成,常见的数据类型如下:

  • 字符串:text(分词)、keyword(不分词)
  • 数字型:long、integer、short、byte、double、float、half_float、scaled_float
  • 布尔:boolean
  • 日期:date
  • 二进制:binary
  • 范围类型:integer_range、float_range、long_range、double_range、date_range

元数据

用于标注文档的相关信息

  • _index:文档所在的索引名
  • _type:文档所在的类型名
  • _id:文档唯一id
  • _uid:组合id,由_type和_id组成(6.x_type不再起作用,同_id一样)
  • _source:文档的原始Json数据,可以从这个获取每一个字段的内容
  • _all:整合所有字段的内容到该字段,默认禁用

索引Index

由具有相同字段的文档列表组成。类似于关系型数据库中的表,6.0版本。

索引中存储具有相同结构的文档(Document)。每个索引都有自己的mapping定义,用于定义字段名和类型

一个集群可以有多个索引。比如:

Nginx日志存储的时候可以按照日期每天生成一个索引存储

  • nginx-log-2017-01-01
  • nginx-log-2017-01-02
  • nginx-log-2017-01-03

节点Node

一个ElasticSearch的运行实例,是集群的构成单元。

集群Cluster

由一个或多个节点组成,对外提供服务

Rest API

ElasticSearch集群对外提供RESTful API

  • REST REpresentational State Transfer
  • URI指定资源,如Index、Document
  • Http Method指明资源操作类型,如GET、POST、PUT、DELTE等

交互方式:

  1. Curl命令行
  2. Kibana DevTools

索引API

es有专门的Index API,用户创建、更新、删除索引配置等

  • PUT /test_index --创建索引
  • GET _cat/indices --查看索引
  • DELETE /test_index --删除索引
  • POST /test_index/doc/1/_update --更新索引

文档Document API

es有专门的Document API /index/type/id

  • 创建文档
    • 指定ID:PUT /test_index/doc/1 {"username":"alfred","age":1} 创建文档时,如果索引不存在,es会自动创建index和type
    • 不指定ID:POST /test_index/doc {"username":"tom","age":20}
  • 查询文档
    • 指定ID:GET /test_index/doc/1
    • 搜索所有文档:GET /test_indes/doc/_search {"query":{"term":{"_id":1}}}
  • 批量创建
    • endpoint为_bulk POST _pulk action_tye:index、update、create、delete {"index":{"_index":"test_index","_type":"doc","_id":"3"}} {"username":"LZH","age":45} {"delete":{"_index":"test_index","_type":"doc","_id":"gg0mF2UBL7MW6CurOJYJ"}} {"update":{"_index":"test_index","_type":"doc","_id":1}} {"doc":{"age":11}}
  • 批量查询
    • endpoint为_mget GET _mget {"docs":[{"_index":"test_index","_type":"doc","_id":1},{"_index":"test_index","_type":"doc","_id":2},]}

倒排索引和分词

举例:在书中,目录页对应正排索引,索引页对应倒排索引

正排索引:可以通过文档的id到文档内容、单词(需要做分词处理)的关联关系

倒排索引:单词到文档id的关联关系

正排索引

文档ID到文档内容、单词的关联关系,类似于书的目录,类似如下表格,根据文档ID获取内容

文档ID 文档内容
1 ElasticSearch是最流行的搜索引擎
2 php是世界上最好的语言
3 ElasticSearch搜索引擎是如何诞生的

倒排索引

单词到文档ID的关联关系,类似书的索引页,类似如下表格,分词是倒排索引的一个前提条件,只有将文档内容进行分词以后才能进行倒排索引

单词 文档ID列表
ElasticSearch 1、3
流行 1
搜索引擎 1、3
php 2
世界 2
最好 2
语言 2
如何 3
诞生 3

查询实例步骤

  1. 通过倒排索引查询获得“搜索引擎”的对应的内容的ID有1和3
  2. 通过正排索引查询1和3,获取完整的内容
  3. 返回用户最终结果

详解

倒排索引是搜索引擎的核心,主要包含有两个部分

  1. 单词词表(Term Dictionary):记录所有文档的单词,一般比较大;记录单词到倒排列表的关联信息。采用的数据结构是B+ Tree
  2. 倒排列表(Posting List):记录单词对应的文档集合,由倒排索引项(Posting)组成。倒排索引项(Posting)主要包含的信息有:
    1. 文档ID,用于获取原始信息
    2. 单词频率(TF,Term Frequency),记录该单词在该文档中出现的次数,用于后续相关性算分
    3. 位置(Position),记录单词在文档中的分词位置(多个),用于做词语搜索(Phrase Query)
    4. 偏移(Offset),记录单词在文档的开始和结束位置,用于做高亮显示

在es中,存储的是一个Json格式的文档,其中包含多个字段,每个字段都有自己的倒排索引

以“搜索引擎”为例

文档ID 文档内容
1 ElasticSearch是最流行的搜索引擎
2 php是世界上最好的语言
3 搜索引擎是如何诞生的

倒排列表

DocId TF Position Offset
1 1 2 <18,22>
3 1 0 <0,4>

分词

分词是指将文本转换为一系列单词(term or token)的过程,也可以叫做文本分析,在es里面称为Analysis

分词器

分词器是es中专门处理分词的组件,英文为Analyzer,它的组成如下:

  • Character Filters
    • 针对原始文本进行处理,比如去除HTML特殊标记
  • Tokenizer
    • 将原始文本按照一定规则分为单词
  • Token Filters
    • 针对tokenizer处理的单词再加工,比如转小写、删除或新增等处理

预定义的分词器

  • Standard:Standard Analyzer
    • 默认分词器
    • 其组成由{Tokenizer:Standard,Token Filters:Standard、Lower case、Stop(disabled by default)}。特性为:
      • 按词切分,支持多语言
      • 小写处理
    • 示例:The 2 QUICK Brown-Foxes jumed over the lazy dog's bone 分词后为[the,2,quick,brown,foxes,jumped,over,the,lazy,dog's,bone]
  • Simple:Simple Analyzer
    • 其组成由{Tokenizer:Lower Case}。特性为:
      • 按照非字母切分
      • 小写处理
    • 示例:The 2 QUICK Brown-Foxes jumed over the lazy dog's bone 分词后为[the,quick,brown,foxes,jumped,over,the,lazy,dog,s,bone]
  • Whitespace:Whitespace Analyzer
    • 其组成由{Tokenizer:Whitespace}。特性为:
      • 按照空格切分
    • 示例:The 2 QUICK Brown-Foxes jumed over the lazy dog's bone 分词后为[The,2,QUICK,Brown-Foxes,jumped,over,the,lazy,dog's,bone]
  • Stop:Stop Analyzer
    • Stop Word指语气助词等修饰性的词语,比如the、an、的、这等等
    • 其组成由{Tokenizer:Lower Case,Token Filter:Stop}。特性为
      • 相比Simple Analyzer多了Stop Word处理
    • 示例:The 2 QUICK Brown-Foxes jumed over the lazy dog's bone 分词后为[quick,brown,foxes,jumped,over,lazy,dog,s,bone]
  • Keyword:Keyword Analyzer
    • 其组成由[Tokenizer:Keyword]。特性为:
      • 不分词,直接将输入作为一个单词输出
    • 示例:The 2 QUICK Brown-Foxes jumed over the lazy dog's bone 分词后为 [The 2 QUICK Brown-Foxes jumed over the lazy dog's bone]
  • Pattern:Pattern Analyzer
    • 其组成由{Tokenizer:Pattern,Token Filters:Lower case、Stop(disabled by default)}。特性为:
      • 通过正则表达式自定义分隔符
      • 默认是\W+,即非字词的符号作为分隔符
    • 示例:The 2 QUICK Brown-Foxes jumed over the lazy dog's bone 分词后为 [the,2,quick,brown,foxes,jumped,over,the,lazy,god,s,bone]
  • Language:Language Analyzer
    • 提供了30+常见语言的分词器

中文分词

中文分词指的是将一个汉字序列切分成一个一个的单独的词。在英文中,单词之间是以空格作为自然分界符,汉语中词没有一个形式上的分界符

  • IK
    • 实现中英文单词的切分,支持ik_smart、ik_maxword等模式
    • 可自定义词库,支持热更新分词词典
    • https://github.com/medcl/elasticsearch-analysis-ik
  • jieba
    • python中最流行的分词系统,支持分词和词性标注
    • 支持繁体分词、自定义词典、并行分词等
    • http://github.com/singlee/elasticsearch-jieba-plugin
  • 基于自然语言处理的分词系统
    • Hanlp
      • 由一些列模型与算法组成的Java工具包,目标是普及自然语言处理在生产环境中的应用
      • https://github.com/hankcs/HanLP
    • THULAC
      • THU Lexical Analyzer for Chinese,由清华大学自然语言处理与社会人文计算实验室研制推出的一套中文词法分析工具包,具有中文分词和词性标注功能
      • https://github.com/microbun/elasticsearch-thulac-plugin

自定义分词

当自带的分词无法满足需求时,可以自定义分词。通过自定义Character Filter、Tokenizer和Token Filter实现

Character Filter
  • 在Tokenizer之前对原始文本进行处理,比如增加、删除或替换字符等
  • 自带的如下:
    • HTML Strip去除html标签和转换html实体
    • Mapping进行字符替换操作
    • Pattern Replace进行正则匹配替换
  • 会影响后续tokenizer解析的position和offset信息
  • 示例:POST _analyze {"tokenizer":"keyword","char_filter":["html_strip"],"text":"<p>I'm so <b>happy</b>!</p>"}
Tokenizer
  • 将原始文本按照一定规则切分为单词(term or token)
  • 自带的如下:
    • standard 按照单词进行分割
    • letter 按照非字符类进行分割
    • whitespace 按照空格进行分割
    • UAX URL Email 按照standard分割,但不会分割邮箱和url
    • NGram和Edge NGram连词分割
    • Path Hierarchy 按照文件路径进行切割
  • 示例:POST _analyze {"tokenizer":"path_hierarchy","text":"/one/two/three"} #按照文件路径分割
Token Filter
  • 对于tokenizer输出的单词(term)进行增加、删除、修改等操作
  • 自带的如下:
    • lowercase 将所有term转换为小写
    • stop 删除stop words
    • NGram和Edge NGram连词分割
    • Synonym添加近义词term
  • 示例:POST _analyze {"text":"a Hello,World!","tokenizer":"standard","filter":["stop","lowercase",{"type":"ngram","min_gram":2,"max_gram":4}]} #min_gram 最小连词,max_gram最大连词
API操作
  • 自定义分词需要在索引的配置中设定 需要在index没有创建之前配置分词器
    • PUT test_index {"settings":{"analysis":{"char_filter":{},"tokenizer":{},"filter":{},"analyzer":{}}}}
  • 示例:PUT test_index_1 {"settings":{"analysis":{"analyzer":{"my_custome_analyzer":{"type":"custom","tokenizer":"standard","char_filter":["html_strip"],"filter":["lowercase","asciifolding"]}}}}}
  • 示例:PUT test_index_2 {"settings":{"analysis":{"analyzer":{"my_custom_analyzer":{"type":"custom","char_filter":["emoticons"],"tokenizer":"punctuation","filter":["lowercase","englist_stop"]}},"tokenizer":{"punctuation":{"type":"pattern","pattern":"[ .,!?]"}},"char_filter":{"emoticons":{"type":"mapping","mappings":[":)=>_happy_",":(=>_sad_"]}},"filter":{"englist_stop":{"type":"stop","stopwords":"_english"}}}}}

分词使用说明

  • 创建或更新文档时(Index Time),会对相应的文档进行分词处理
    • 索引时分词是通过配置Index Mapping中每个字段的analyzer属性实现的,示例:PUT test_index {"mappings":{"doc":{"properties":{"title":{"type":"text","analyzer":"whitespace"}}}}} #指定分词器
    • 不指定分词器时,使用默认standard
  • 查询时(Search Time),会对查询语句进行分词
    • 查询的时候通过analyzer指定分词器
      • 示例:POST test_index/_search {"query":{"match":{"message":{"query":"hello","analyzer":"standard"}}}}
    • 通过index mapping设置search_analyzer实现
      • 示例:PUT test_index {"mappings":{"doc":{"properties":{"title":{"type":"text","analyzer":"whitespace","search_analyzer":"standard"}}}}}
    • 一般不需要特别指定查询时分词器,直接使用索引时分词器即可,否则会出现无法匹配的情况
  • 分词使用建议:
    • 明确字段是否需要分词,不需要分词的字段就将type设置为keyword,可以节省空间和提高写性能
    • 善用_analyze API,查看文档的具体分词结果
    • 动手测试

调用顺序

st=>start: 
e=>end: 
op1=>operation: Character Filters
op2=>operation: Tokenizer
op3=>operation: Token Filters

st->op1->op2
op2->op3->e

Analyze API

es提供了一个测试分词的api接口,方便验证分词效果,endpoint是_analyze

  • 可以直接指定analyzer进行测试
  • 可以直接指定索引中的字段进行测试
  • 可以自定义分词器进行测试

输入关键词,结果不是理想中结果是,可以使用这个进行排查

  • 直接指定analyzer进行分词
    • GET _analyze {"analyzer":"standard","text":"hello world"} 结果中有分词结果、起始偏移、结束偏移、分词位置信息
  • 直接指定索引中的字段进行测试
    • GET test_index/_analyze {"field":"username", "text":"hello world"}
  • 自定义分词器进行测试
    • POST _analyze {"tokenizer":"standard","filter":["lowercase"],"text":"Hello World!"}

Mapping

类似数据库中的表结果定义,主要作用如下:

  • 定义Index下的字段名(Field Name)
  • 定义字段类型,比如数值型、字符串型、布尔型等
  • 定义倒排索引相关的配置,比如是否索引、记录position等

自定义Mapping

API:PUT my_index {"mappings":{"doc":{"properties":{"title":{"type":"text"},"name":{"type":"keyword"},"age":{"type":"integer"}}}}}

  • Mapping中的字段类型一旦设定后,禁止直接修改
    • Lucene实现的倒排索引生成后不允许修改
  • 重新建立新的索引,然后做reindex操作
  • 允许新增字段
  • 通过dynamic参数来控制字段的新增
    • true(默认)允许新增字段
    • false不允许自动新增字段,但是文档可以正常写入,但无法对字段进行查询等操作
    • strict文档不能写入,报错。要求非常严格

参数说明

  • copy_to
    • 将该字段的值复制到目标字段,实现类似_all的作用
    • 不会出现在_source中,只用来搜索
  • index:字段不需要查询,比如敏感信息设置为false,不能被搜索
    • 控制当前字段是否索引,默认为true,即记录索引,false不记录,即不可搜索
  • index_options:用于控制倒排索引记录的内容,有如下4中配置
    • docs只记录doc id
    • freqs记录doc id和term frequencies
    • positions记录doc id、term frequencies和term position
    • offsets记录doc id、term frequencies、term position和character offsets
    • text类型默认配置为positions,其他默认为docs
    • 记录内容越多,占用空间越大
  • null_value
    • 当字段遇到null值时的处理策略,默认为null,即空值,此时es会忽略该值。可以通过设定该值设定字段的默认值

建议

自定义Mapping的操作步骤如下:

  1. 写入一条文档到es的临时索引中,获取es自动生成的mapping
  2. 修改步骤1得到的mapping,自定义相关配置
  3. 使用步骤2的mapping创建实际所需索引

数据类型

  • 核心数据类型
    • 字符串型text(分词)、keyword(不分词)
    • 数值型long、integer、short、byte、double、float、half_float、scaled_float
    • 日期类型date
    • 布尔类型boolean
    • 二进制类型binary
    • 范围类型integer_range、float_range、long_range、double_range、date_range
  • 复杂数据类型
    • 数据类型array
    • 对象类型json object
    • 嵌套类型nested object
  • 地理位置数据类型
    • geo_option
    • geo_shape
  • 专用类型
    • 记录IP地址
    • 实现自动补全completion
    • 记录分词数token_count
    • 记录字符串hash值
    • percolator
    • join

多字段特性multi-fields

允许对同一个字段采用不同的配置,比如分词,常见的例子如对人名实现拼音搜索,只需要在人名中新增一个子字段为pinyin即可

Dynamic Mapping

es可以自动识别文档字段类型,从而降低用户使用成本

es是依靠JSON文档的字段类型来实现自动识别字段类型,支持的类型如下:

JSON类型 es类型
null 忽略
boolean boolean
浮点类型 float
整数 long
object object
array 由第一个非null值的类型决定
string 匹配为日期则设定为date类型(默认开启);匹配为数字的话设为float或long类型(默认关闭);设为text类型,并且附带keyword的子字段

示例: PUT /my_index/doc/1 {"username":"alfred","age":14,"birth":"1988-10-10","married":false,"year":"18","tags":["boy","fashion"],"money":"100.1"}

日期与数字识别

  • 日期的自动识别可以自行配置日期格式,以满足各种需求
    • 默认是["strict_date_optional_time","yyyy/MM/dd HH:mm:ss Z||yyyy/MM/dd Z"]
    • strict_date_optional_time 是ISO datetime的格式,完整格式类型类似下面:
      • YYYY-MM-DDThh:mm:ssTZD (eg 1997-07-16T19:20:30+01:00)
    • dynamic_date_formats 可以自定义日期类型 PUT my_index {"mappings":{"my_type":{"dynamic_date_formats":["yyyy-MM-dd"]}}}
    • date_detection可以关闭日期自动识别的机制 PUT my_index {"mappings":{"my_type":{"date_detection":false}}}
  • 字符串是数字时,默认不会自动识别为整型,因为字符串中出现数字时完全合理的
    • numeric_detection 可以开启字符串中数字的自动识别 PUT my_index {"mappings":{"my_type":{"numeric_detection ":true}}}

Dynamic Templates

  • 允许根据es自动识别的数据类型、字段名等来动态设定字段类型,可以实现如下效果
    • 所有字符串类型都设定为keyword类型,即默认不分词。如果分词,比较占用内存空间
    • 所有以message开头的字段都设定为text类型, 即分词
    • 所有以long_开头的字段都设定为long类型
    • 所有自动匹配为double类型的都设定为float类型,以节省空间
  • Dynamic Templates API
    • 匹配规则一般有如下几个参数:
      • match_mapping_type匹配es自动识别的字段类型,如boolean、long、string等
      • match,unmatch匹配字段名 以什么开头的进行匹配
      • path_match、path_unmatch匹配路径 对象内部的字段
    • 示例:PUT my_index {"mappings":{"doc":{"dynamic_templates":[{"strings":{"match_mapping_type":"string","mapping":{"type":"keyword"}}}]}}}

索引模版

索引模版,英文为Index Template,主要用于在新建索引时自动应用预先设定的配置,简化索引创建的操作步骤

  • 可以设定索引的配置和mapping
  • 可以有多个模版,根据order设置,order大的覆盖小的配置

索引模版API,endpoint为_temlate

示例:PUT _template/test_template {"index_patterns":["te*","bar"],"order":0,"settings":{"number_of_shards":1},"mappings":{"doc":{"_source":{"enabled":false},"properties":{"name":{"type":"keyword"}}}}}

Search API

实现对es中存储的数据进行查询分析,endpoint为_search

  • 查询主要有两种形式
    • URI Search
      • 操作简便,方便通过命令行测试
      • 仅包含部分查询语法
    • Request Body Search
      • es提供的完备查询语法Query DSL(Domain Specific Language)

URI Search

  • 通过uri query参数来实现搜索,常用的参数如下:
    • q指定查询的语句,语法为Query String Syntax
    • df q参数中不指定字段时默认查询的字段, 如果不指定,es会查询所有字段
    • sort排序
    • timeout指定超时时间,默认不超时
    • from、size用户分页

Query String Syntax

  • term(单词)与phrase(词语)
    • alfred way 等效于alfred OR way
    • “alfred way”词语查询,要求先后顺序
  • 泛查询(不指定字段的查询)
    • alfred等效于在所有字段中去匹配term(前提条件,没有指定df 参数)
  • 指定字段
    • name:alfred
  • Group 分组设定,使用括号指定匹配的规则,
    • (quick OR brown) AND fox 括号匹配优先级
    • status:(active OR pending) title:(full text search) 关键词作为一个整体
  • 布尔操作符
    • AND(&&),OR(||),NOT(!)
      • name:(tom NOT lee)
      • 注意大写,不能小写
    • + - 分别对应must和must_not
      • name:(tom +lee -alfred)
      • name:((lee && !alfred) || (tom && lee && !alfred))
      • +在URL中会被解析为空格,要是用encode后结果才可以,为%2B
  • 范围查询,支持数值和日期
    • 区间写法,闭区间用[],开区间用{}
      • age:[1 TO 10] 1<=age<=10
      • age:[1 TO 10} 1<=age<10
      • age:[1 TO ] age>=1
      • age:[* TO 10] age<=10
    • 算数符号写法
      • age:>=1
      • age:(>= 1 && <= 10) 或者 age:(+>=1 +<=10)
  • 通配符查询
    • ? 代表1个字符,*代表0或多个字符
      • name:t?m
      • name:tom*
      • name:t*m
    • 通配符匹配执行效率低,且占用较多内存,不建议使用
    • 如无特殊需求,不要将?/*放在最前面
  • 正则表达式匹配
    • name:/[mb]oat/
  • 模糊匹配 fuzzy query
    • name:roam~1
    • 匹配与roam差1个character的词,比如foam、roams等
  • 近似度查询proximity search
    • "fox quick"~5
    • 以term为单位进行差异比较,比如"quick fox","quick brown fox" 都会被匹配

Request Body Search

将查询语句通过http request body 发送到es,主要包含如下参数

  • query 符合Query DSL语法的查询语句
  • from、size
  • timeout
  • sort
  • ....

Query DSL

基于JSON定义的查询语言,主要包含如下两种类型:

  • 字段类查询
    • 如term,match,range等,只针对某一个字段进行查询
  • 复合查询
    • 如bool查询等,包含一个或多个字段类查询或者复合查询语句

字段类查询

字段类查询主要包括以下两类:

  • 全文匹配
    • 针对text类型的字段进行全文检索,会对查询语句先进行分词处理,如match,match_phrase等query类型
  • 单词匹配
    • 不会对查询语句做分词处理,直接去匹配字段的倒排索引,如term,terms,range等query类型

Match Query

  • 对字段做全文检索,最基本和常用的查询类型

    • API示例: GET test_search_index/_search {"query":{"match":{"username":"alfred way"}}} #关键词:match,字段名:username,待查询的语句:alfred way
  • 通过operator参数可以控制单词间的匹配关系,可选项为or和and

    • API示例: GET test_search_index/_search {"query":{"match":{"username":{"query":"alfred way","operator":"and"}}}} #字段名:username,待查询的语句:alfred way,关键字:operator
  • 通过minimun_should_match参数可以控制需要匹配的单词数

    • API示例:GET test_search_index/_search {"query":{"match":{"username":{"query":"alfred way","minimum_should_match":2}}}} #字段名:username,待查询的语句:alfred way,关键字:minimum_should_match

Match Query流程

st=>start: 
e=>end: 
op1=>operation: 对查询的语句分词,如alfred way
op2=>operation: 分词后为alfed和way
op3=>operation: 针对每一个词,然后根据username的倒排索引进行匹配算分。比如alfred文档列表为1,2。way的文档ID为1 算分模型TF/IDF、BM25
op4=>operation: 汇总得分
op5=>operation: 根据得分排序,返回匹配文档

st->op1->op2->op3->op4->e
相关性算分
  • 相关性算分是指文档与查询语句间的相关度,英文为relevance
    • 通过倒排索引可以获取与查询语句相匹配的文档列表
    • 本质是一个排序问题,排序的依据是相关性算分

倒排索引

单词 文档ID列表
alfred 1,2
way 1
概念
  • Term Frequency(TF) 词频,即单词在该文档中出现的次数。词频越高,相关度越高
  • Document Frequency(DF) 文档频率,即单词出现的文档数
  • lnverse Document Frequency(IDF) 逆向文档频率,与文档频率相反,简单理解为1/DF。即单词出现的文档数越少,相关度越高
  • Field-length Norm 文档越短,相关性越高

模型

  • TF/IDF模型

    • score(q,d)=coord(q,d)*queryNorm(q)*\sum_{t\ in\ q}(tf(t\ in\ d)*idf(t)^2*t.getBoost()*norm(t,d))
    • socre(q,d):相关性得分,q为查询语句,d为匹配的文档。例如:q="alfred" d=1,2

    • coord(q,d)和queryNorm(q):对query进行正则化处理

    • 在求和公式中,范围:t in q 查询语句分词之后的每一个term进行求和计算

    • tf(t in d):词频计算

    • idf(t)^2:逆向文档频率计算

    • t.getBoost():是否加权处理

    • norm(t,d):Filed-length Norm计算

    • 可以通过explain参数来查看具体的计算方法,但是要注意:

      • es的算分是按照shard进行的,即shard的分数计算是相互独立的,所以在使用explain的时候注意分片数
      • 可以通过设置索引的分片数为1来避免
  • BM25模型 5.x之后的默认模型 对TF/IDF的优化

Match Phrase Query
  • 对字段做检索,有顺序要求
    • API示例: GET test_search_index/_search {"query":{"match_phrase":{"job":"java engineer"}}}
  • 通过slop参数可以控制单词间的间隔
    • API示例:GET test_search_index/_search {"query":{"match_phrase":{"job":{"query":"java engineer","slop":1}}}} #运行java和engineer之间有一个距离的差距
Query String Query
  • 类似于URI Search中q参数查询
    • API示例:GET test_search_index/_search {"query":{"query_string":{"default_field":"username","query":"alfred AND way"}}}
    • API示例:GET test_search_index/_search {"query":{"query_string":{"fields":["username","job"],"query":"alfred OR (java AND ruby)"}}}
Simple Query String Query
  • 类似Query String,但是会忽略错误的查询语法,并且仅支持部分查询语法
  • 其常用的逻辑符号如下,不能使用AND、OR、NOT等关键词:
    • +代指AND
    • |代指OR
    • - 代指NOT
      • 示例:GET test_search_index/_search {"query":{"simple_query_string":{"query":"alfred +way","fields":["username"]}}}
Term Query
  • 将查询语句作为整个单词进行查询,即不对查询语句做分词处理。可以用来处理数字、布尔值、日期和文本。是一个精确值的查询方式
    • API 示例:GET test_search_index/_search {"query":{"term":{"username":"alfred"}}}
Terms Query
  • 一次传入多个单词进行查询,查找多个精确值。Term和Terms其实是包含的一个操作,并不是等于某个值
    • API示例: GET test_search_index/_search {"query":{"terms":{"username":["alfred","way"]}}}
Range Query
  • 范围查询主要针对数值和日期类型
    • 示例: GET test_search_index/_search {"query":{"range":{"age":{"gte":10,"lte":20}}}}
gt >
lt <
gte >=
lte <=

复合查询

  • 复合查询是指包含字段类查询或复合查询的类型,主要包括以下几类:
    • constant_score query:该查询将其内部的查询结果文档得分都设定为1或者boost的值
      • 多用于结合bool查询实现自定义得分
      • 示例:GET test_search_index/_search {"query":{"constant_score":{"filter":{"match":{"username":"alfred"}}}}}
    • bool query
    • dis_max query
    • function_score query
    • boosting query
Bool query
  • 布尔查询由一个或多个布尔子句组成,主要包含如下四个。是一个组合过滤器
filter 只过滤符合条件的文档,不计算相关性得分
must 文档必须符合must中的所有条件,会影响相关性得分,相当于and。必须匹配
must_not 文档必须不符合must_not中的所有条件,相当于not。不能匹配
should 文档可以符合should中的条件,会影响相关性得分,相当于or。至少有一个语句匹配
  • API示例: GET test_search_index/_search {"query":{"bool":{"must":[{}],"must_not":[{}],"should":[{}],"filter":[{}]}}}
Filter
  • Filter查询只过滤符合条件的文档,不会进行相关性算分
    • es针对filter会有智能缓存,因此其执行效率很高
    • 做简单匹配查询且不考虑算分时,推荐使用filter代替query等
    • API示例: GET test_search_index/_search {"query":{"bool":{"filter":[{"term":{"username":"alfred"}}]}}}
Must
  • 指包含的值
  • API示例: GET test_search_index/_search {"query":{"bool":{"must":[{"match":{"username":"alfred"}},{"match":{"job":"specialist"}}]}}}
Must not
  • 不包含的值
  • API示例: GET test_search_index/_search {"query":{"bool":{"must":[{"match":{"job":"java"}}],"must_not":[{"match":{"job":"ruby"}}]}}}
Should
  • should使用分两种情况
    • bool查询中只包含should,不包含must查询
      • 文档必须满足至少一个条件
        • minimum_should_match可以控制满足条件的个数或者百分比
        • API 示例:GET test_search_index/_search {"query":{"bool":{"should":[{"term":{"job":"java"}},{"term":{"job":"ruby"}},{"term":{"job":"specialist"}}],"minimum_should_match":2}}}
    • bool查询中同时包含should和must查询
      • 同时包含should和must时,文档不必满足should中的条件,但是如果满足条件,会增加相关性得分
        • API示例:GET test_search_index/_search {"query":{"bool":{"must":[{"term":{"username":"alfred"}}],"should":[{"term":{"job":"ruby"}}]}}}
Query Context VS Filter Context
  • 当衣蛾查询语句位于Query或者Filter上下文时,es执行的结果会不同
上下文类型 执行类型 使用方式
Query 查找与查询语句最匹配的文档,对所有文档进行相关性算分并排序 query、bool中must和should
Filter 查找与查询语句相匹配的文档 bool中的filter与must_not、const_score中的filter

Count API

  • 获取符合条件的文档数,endpoint为_count
    • API 示例: GET /test_search_index/_count {"query":{"match":{"username":"alfred"}}}

Source Filtering

  • 过滤返回结果中_source中的字段,返回部分字段主要有如下几种方式:
    • URI
      • 示例:GET test_search_index/_search?_source=username
    • Request Body Search
      • 示例:GET test_search_index/_search {"_source":["username","age"]}

分布式特性介绍

  • es支持集群模式,是一个分布式系统,其好处主要有两个:
    • 增大系统容量,如内存、磁盘,使得es集群可以支持PB级的数据
    • 提高系统可用性,即使部分节点停止服务,整个集群依然可以正常服务
  • es集群由多个es实例组成
    • 不同集群通过集群名字来区分,可通过cluster.name进行修改,默认为elasticsearch
    • 每个es实例本质上是一个JVM进程,且有自己的名字,通过node.name进行修改

cerebro安装与运行

  • 地址:https://github.com/lmenezes/cerebro
  • bin/cerebro文件执行即可

构建集群

  • 构建一个节点,node,cluster.name相同,node.name不同,端口不同
  • es集群相关的数据为cluster state,主要记录如下信息:
    • 节点信息,比如节点名称、连接地址等
    • 索引信息,比如索引名称、配置等

Master Node

  • 可以修改cluster state的节点称为master节点,一个节点只能有一个
  • cluster state存储在每个节点上,master维护最新版本并同步给其他节点
  • master节点是通过集群中所有节点选举产生的,可以被选举的节点称为master-eligible节点。配置如下:
    • node.master: true

Coordinating Node

  • 处理请求的节点即为coordinating节点,该节点为所有节点的默认角色,不能取消
    • 路由请求到正确的节点处理,比如创建索引的请求到master节点

Data Node

  • 存储数据的节点即为data节点,默认节点都是data类型,相关配置如下:
    • node.data:true

单点问题

  • 如果一个集群中只有一个节点,这个节点停止,那么整个集群服务就停止。只能通过新增节点解决

副本与分片

提高系统可用性

  • 服务可用性
    • 2个节点的情况下,允许其中1个节点停止服务
  • 数据可用性
    • 引入副本(Replication)解决
    • 每个节点上都有完备的数据

增大系统容量

  • 将数据分布于所有的节点上,使用分片(Shard)
  • 分片是es支持PB级数据的基石
    • 分片存储了部分数据,可以分布于任意节点上
    • 分片数在索引创建时指定且后续不允许再更改,默认为5个
    • 分片有主分片和副本分片之分,以实现数据的高可用
    • 副本分片的数据由主分片同步,可以有多个,从而提高读取的吞吐量

分片问题

  • 通过新增节点不能提高已经分片的索引的数据容量,新增节点无法利用
  • 通过新增副本不能提高已经有副本的索引的读取吞吐量
  • 分片数的设定很重要,需要提前规划好
    • 过小会导致后续无法通过增加节点实现水平扩容
    • 过大会导致一个节点上分布过多分片,造成资源浪费,同时会影响查询性能

集群状态

  • 通过如下api可以查看集群健康状态,包括以下三种:
    • green健康状态,指所有主副分片都正常分配
    • yellow指所有主分片都正常分配,但是副本分片未正常分配
    • red有主分片未分片
    • API示例:GET _cluster/health

故障转移

  • 集群如有3个节点,此时集群状态是green
  • node1所在机器宕机导致服务终止
    • 1、node2和node3发现node1无法响应一段时间后会发起一个master选举,比如这里选择node2为master节点。此时由于主分片P0下线,集群状态变为red
    • 2、node2发现主分片P0未分配,将R0提升为主分片,此时由于所有主分片都正常分配,集群状态变为yellow
    • 3、node2为P0和P1生成新的副本,集群状态变为绿色

文档分布式存储

  • 文档会最终存储到分片上
    • 例如:document1存储到node2节点的分片P1上
    • 需要文档到分片的映射算法
    • 目的是使得文档均匀分布到所有分片上,以充分利用资源
    • 算法:随机选择或者round-robin算法,不可取,因为需要维护文档到分片的映射关系,成本巨大
    • 根据文档值实时计算对应的分片
  • 文档到分片的映射算法
    • shard = hash(routing) % number_of_primary_shards
    • hash算法保证可以将数据均匀地分散在分片中
    • routing是一个关键参数,默认是文档ID,也可以自行指定
    • number_of_primary_shards主分片数
    • 该算法与主分片数相关,这也是分片数一点确定后便不能更改的原因
  • 文档创建流程
    1. Client向node3发起请求创建文档的请求
    2. node3通过routing计算该文档应该存储在Shard1上,查询cluster state后确认主分片P1在node2上,然后转发创建文档的请求到node2
    3. P1接收并执行创建文档的请求后,将同样的请求发送到副本分片R1上(R1在node1上)
    4. R1接收并执行创建文档的请求后,通知P1成功的结果
    5. P1接收副本分片结果后,通知node3创建成功
    6. node3返回结果到Client
  • 文档读取流程
    1. Client向node3发起获取文档1的请求
    2. node3通过routing计算该文档在Shard1上,查询cluster state后获取Shard1的主副分片列表,然后以轮询的机制获取一个shard,比如这是R1,然后转发读取文档的请求到node1
    3. R1接收并执行读取文档请求后,将结果返回node3
    4. node3返回结果给Client
  • 文档批量创建流程
    1. Client向node3发起批量创建文档的请求(bulk)
    2. node3通过routing计算所有文档对应的shard,然后按照主shard分配对应执行的操作,同时发送请求到涉及的主shard,比如这个3个主shard都需要参与
    3. 主shard接收并执行请求后,将同样的请求同步到对应的副本shard
    4. 副本shard执行结果后返回结果到主shard,主shard再返回node3
    5. node3整合结果后返回Client
  • 文档批量读取流程
    1. Client向node3发起批量获取文档的请求(mget)
    2. node3通过routing计算所有文档对应的shard,然后以轮询的机制获取要参与shard,按照shard构建mget请求,同时发送请求到涉及的shard,比如这里有2个shard需要参与
    3. R1、R2返回文档结果
    4. node3返回结果给Client

脑裂问题

  • 脑裂问题,英文为split-brain,是分布式系统中的经典网络问题。
    • 示例:3个节点组成的集群,突然node1的网络和其他两个节点中断
    • node2与node3会重新选举master,比如node2成为了新的master,此时会更新cluster state
    • node1自己组成集群后,也会更新cluster state
  • 同一个集群有两个master,而且维护不同的cluster master,网络恢复后无法选择正确的master
  • 解决方案为仅在可选举master-eligible节点数大于等于quorum时才进行master选举
    • quorum = master - eligible节点数/2+1,例如3个master-eligible节点时,quorum为2
    • 设定discovery.zen.minimun_master_nodes为quorum即可避免脑裂
    • 在示例中,node1达不到选举的要求,则不参与选择;node2和node3可以进行选举,待网络恢复后,node1会加入到集群中

Shard详解

  • 倒排索引的不可变更:倒排索引一旦生成,不能更改
  • 其好处如下:
    • 不用考虑并发写文件的问题,杜绝了锁机制带来的性能问题
    • 由于文件不再更改,可以充分利用文件系统缓存,只需载入一次,只要内存足够,对该文件的读取都会从内存读取,性能高
    • 利于生成缓存数据
    • 利于对文件进行压缩存储,节省磁盘和内存存储空间
  • 坏处:
    • 需要写入新文档时,必须重新构建倒排索引文件,然后替换老文件后,新文档才能被检索,导致文档实时性差
  • 文档搜索实时性
    • 解决方案是新文档直接生成新的倒排索引文件,查询的同时查询所有的倒排文件,然后做结果的汇总计算即可
  • Lucene便是采用了这种方案,它构建的单个倒排索引称为segment,合在一起称为Index,与ES中的Index概念不同。ES中的一个Shard对应一个Lucene Index
  • Lucene会有一个专门的文件来记录所有的segment信息,称为commit point
  • 文档搜索实时性-refresh
    • segment写入磁盘的过程依然很耗时,可以借助文件系统缓存的特性,先将segment在缓存中创建并开放查询来进一步提升实时性,该过程在es中被称为refresh
    • 在refresh之前文档会先存储在一个buffer(缓冲队列)中,refresh时将buffer中的所有文档清空并生成segment
    • es默认每1秒执行一次refresh,因此文档的实时性被提高到1秒,这也是es被称为近实时(Near Real TIme)的原因
    • refresh发生的时机主要有如下几种情况:
      • 间隔时间达到时,通过index.settings.refresh_interval来设定,默认是1秒
      • index.buffer占满时,其大小通过indices.memory.index_buffer_size设置,默认为jvm head的10%,所有shard共享
      • flush发生时也会发生reflush
  • 文档搜索实时性-translog
    • 如果在内存中的segment还没有写入磁盘前发生了宕机,那么其中的文档就无法恢复了
    • es引入translog机制。写入文档到buffer时,同时将该操作写入translog。
    • translog文件会即时写入磁盘(fsync),6.x默认每个请求都会落盘,可以修改为每5秒写一次,这样风险便是5秒内的数据,相关配置index.translog.*
    • es启动时会检查translog文件,并从中恢复数据
  • 文档搜索实时性-flush
    • flush负责将内存中的segment写入到磁盘,主要做如下的工作:
      • 将translog写入磁盘
      • 将index buffer清空,其中的文档生成一个新的segment,相当于一个refresh操作
      • 执行fsync操作,将内存中的segment写入磁盘
      • 更新commit point并写入磁盘
      • 删除旧的translog文件
    • flush发生的时机主要有如下几种情况:
      • 间隔时间达到时,默认是30分钟,5.x之前可以通过index.translog.flush_threshold_period修改,之后无法修改
      • translog占满时,其大小可以通过index.translog.flush_threshold_size控制,默认是512mb,每个index有自己的translog
  • 文档搜索实时性-删除与更新文档
    • segment一旦生成,无法修改
    • 删除操作:
      • Lucene专门维护一个.del的文件,记录所有已经删除的文档,注意.del上记录的是文档在Lucene内部的ID
      • 在查询结果返回前会过滤掉.del中的所有文档
    • 更新操作:
      • 首先删除文档,然后在创建新文档
  • Segment Merging
    • 随着segment的增多,由于一次查询的segment数增多,查询速度会变慢
    • es会定时在后台进行segment merge的操作,减少segment的数量
    • 通过force_merge_api可以手动强制做segment merge的操作

Search的运行机制

Query Then Fetch

  • Search执行的时候实际分为两个步骤运行:
    • Query阶段:node3在接收到用户的search请求后,会先进行Query阶段(此时是Coordinating Node角色)
      1. node3在6个主副分片中随机选择3个分片,发送search request
      2. 被选中的3个分片会分别执行查询并排序,返回from+size个文档Id和排序值
      3. node3整合3个分片返回的from+size个文档Id,根据排序值排序后选取from到from+size的文档Id
    • Fetch阶段:node3根据Query阶段获取的文档Id列表去对应的shard上获取文档详情数据
      1. node3向相关的分片发送multi_get请求
      2. 3个分片返回文档详细数据
      3. node3拼接返回的结果并返回给客户

相关性算分

  • 相关性算分在shard与shard间是相互独立的,也就意味着同一个Term的IDF等值在不同shard上是不同的。文档的相关性算分和它所处的shard相关
  • 在文档数量不多时,会导致相关性算分严重不准的情况发生
  • 解决思路有两个:
    • 一是设置分片数为1个,从根本上解决问题,在文档数量不多的情况下可以考虑使用该方案,比如百万到千万级别的文档数量
    • 二是使用DFS Query-then-Fetch查询方式
      • DFS Query-then-Fetch是在拿到所有文档后再重新完整的计算一次相关性算分,耗费更多的CPU和内存,执行性能也比较低下,一般不建议使用。使用方式如下:
      • GET test_index/_search?search_type=dfs_query_then_fetch {"query":{"match":{"name":"hello"}}}

排序

  • es默认会采用相关性算分排序,用户可以通过设定sorting参数来自行设定排序规则
  • API示例:
    • GET test_index/_search {"sort":{"birth":"desc"}}
    • GET test_index/_search {"sort":[{"birth":"desc"},{"_score":"desc"},{"_doc":"desc"}]}
  • 按照字符串排序比较特殊,因为es有text和keyword两种类型,针对text类型排序,按照如下API执行会报错:
    • GET test_index/_search {"sort": {"username": "desc"}} 因为text类型是可以进行分词的
    • 解决方案是API如下示例
    • GET test_index/_search {"sort":{"username.keyword":"desc"}} 即可,keyword是username的子字段的类型,是不分词的
  • 排序的过程实质是对字段原始内容排序的过程,这个过程中是倒排索引并不能发挥作用,需要使用到正排索引,也就是通过文档Id和字段可以快速得到字段原始内容
  • es对此提供了两种实现方式:
    • fielddata默认禁止
    • doc values默认启用,除了text类型
对比 FieldData DocValues
创建时机 搜索时即时创建 索引时创建,与倒排索引创建时机一致
创建位置 JVM Head 磁盘
优点 不会占用额外的磁盘资源 不会占用Heap内存
确定 文档过多时,即时创建会花过多时间,占用过多Heap内存 减慢索引速度,占用额外的磁盘资源

Fielddata

  • Fielddata默认是关闭的,只是针对text类型的字段,可以通过如下API开启:
    • 此时字符串是按照分词后的Term排序,往往结果很难符合预期
    • 一般是在分词进行聚合分析的时候开启。可以随时开启和关闭
    • API示例: PUT test_index/_mapping/doc {"properties":{"username":{"type":"text","fielddata":true}}} 可以随时开启和关闭

Doc Values

  • Doc Values默认是启动的,可以在创建索引的时候关闭:
    • 如果后面要再开启doc values,需要做reindex操作。当明确知道不需要使用这个字段进行排序和聚合分析,就可以把这个字段的doc_values设置为false。

docvalues_fields

  • 可以通过该字段获取fielddata或者doc values中存储的内容
    • API示例:GET test_index/_search {"docvalue_fields": ["username","username.keyword","age"]}

分页和遍历

from/size

  • 最常用的分页方案
    • from 指明开始位置
    • size 指明获取总数
    • total_page=(total + page_size - 1) / page_size
  • 深度分页:在数据分片存储的情况下如何获取前1000个文档?
    • 获取从990~1000的文档时,会在每个分片上都先获取1000个文档,然后再由Coordinating Node聚合所有分片的结果后再排序获取前1000个文档
    • 页数越深,处理文档越多,占用内存越多,耗时越长。尽量避免深度分页,es通过index.max_result_window限定最多到10000条数据
    • 在每个节点上获取前1000条数据,然后再由Coordinating Node把(节点数 * 1000)条数据进行排序后返回990~1000的文档

Scroll

  • 遍历文档集的api,以快照的方式来避免深度分页的问题
    • 不能用来做实时搜索,因为数据不是实时的
    • 尽量不要使用复杂的sort条件,使用_doc最高效
    • 使用稍嫌复杂
  • 使用步骤:
    1. 需要发起1个scroll search
      • es在收到该请求后会根据查询条件创建文档Id合集的快照
      • API示例:GET test_search_index/_search?scroll=5m {"size":1} 该scroll快照的有效时间为5分钟,size指的是每一次scroll返回的文档数,返回有_scroll_id下次调用使用
    2. 调用scroll search的api,获取文档集合
      • 不断迭代调用直到返回hits.hits数组为空时停止,我在使用时发现可以不进行迭代调用,每次返回的scroll_id都是一样的
      • API示例:POST _search/scroll {"scroll":"5m","scroll_id":"....."} 返回有_scroll_id下次调用使用
  • 过多的scroll调用会占用大量内存,可以通过clear api删除过多的scroll快照
    • API 示例:DELETE /_search/scroll {"scroll_id":["....","...."]}

Search_after

  • 避免深度分页的性能问题,提供实时的下一页文档获取功能
    • 缺点是不能使用from参数,即不能指定页数
    • 只能下一页,不能上一页
    • 使用简单
  • 使用步骤:
    1. 正常的搜索,但是要指定sort值,并保证值唯一
    2. 为使用上一步最后一个文档的sort值进行排序
  • 原理:通过唯一排序值定位将每次要处理的文档数都控制的size内
    • 先到每一个节点中获取排序的size个
    • 再由Coordinating Node将总的文档进行排序后返回前size个文档

总结

类型 场景
From/Size 需要实时获取顶部的部分文档,且需要自由翻页
Scroll 需要全部文档,如导出所有数据的功能
Search_after 需要全部文档,不需要自由翻页

聚合分析

  • 聚合分析,英文为Aggregation,是es除了搜索功能外提供的针对es数据做统计分析的功能
    • 功能丰富,提供Bucket、Metric、Pipeline等多种分析方式,可以满足大部分的分析需求
    • 实时性高,所有的计算结果都时即时返回的
  • 分类
    • Bucket,分桶类型,类似SQL中的GROUP BY语法
    • Metric,指标分析类型,如计算最大值、最小值、平均值等
    • Pipeline,管道分析类型,基于上一级的聚合分析结果进行再分析
    • Matrix,矩阵分析类型

Metric聚合分析

  • 指标分析类型,如计算最大值、最小值、平均值等
  • 分类:
    • 单值分析,只输出一个分析结果
      • min、max、avg、sum
        • API 示例:GET test_search_index/_search {"size":0,"aggs":{"min_age":{"min":{"field":"age"}}}}
        • API 示例:GET test_search_index/_search {"size":0,"aggs":{"max_age":{"max":{"field":"age"}}}}
        • API 示例:GET test_search_index/_search {"size":0,"aggs":{"avg_age":{"avg":{"field":"age"}}}}
        • API 示例:GET test_search_index/_search {"size":0,"aggs":{"sum_age":{"sum":{"field":"age"}}}}
        • API 示例:GET test_search_index/_search {"size":0,"aggs":{"max_age":{"max":{"field":"age"}},"min_age":{"min":{"field":"age"}},"avg_age":{"avg":{"field":"age"}},"sum_age":{"sum":{"field":"age"}}}}
      • cardinality:意为集合的势,或者基数,是指不同数值的个数,类似SQL中distinct count概念
        • API 示例:GET test_search_index/_search {"size":0,"aggs":{"count_of_job":{"cardinality":{"field":"job.keyword"}}}}
    • 多值分析,输出多个分析结果
      • stats、extended stats
        • stats:返回一系列数值类型的统计值,包含min、max、avg、sum和count
          • API 示例:GET test_search_index/_search {"size":0,"aggs":{"stats_age":{"stats":{"field":"age"}}}}
        • extended stats:对stats的扩展,包含了更多的统计数据,如方差、标准差等
          • API 示例:GET test_search_index/_search {"size":0,"aggs":{"stats_age":{"extended_stats":{"field":"age"}}}}
      • percentile、percentile rank
        • percentile:百分位数统计,查看百分比之内的统计
          • API 示例: GET test_search_index/_search {"size":0,"aggs":{"per_age":{"percentiles":{"field":"age","percents":[1,20,25,50,75,95,99]}}}}
        • percentile rank:百分位数排名统计
          • API 示例: GET test_search_index/_search {"size":0,"aggs":{"per_age":{"percentile_ranks":{"field":"age","values":[20,15,25,28]}}}}
      • top hits
        • 一般用于分桶后获取该桶内最匹配的顶部文档列表,即详情数据
        • API 示例:GET test_search_index/_search {"size":0,"aggs":{"ages":{"terms":{"field":"age","size":2},"aggs":{"top_age":{"top_hits":{"size":2,"sort":[{"age":{"order":"desc"}}]}}}}}}

Bucket聚合分析

  • Bucket,意为桶,即按照一定的规则将文档分配到不同的桶中,达到分类分析的目的
  • 按照Bucket的分桶策略,常见的Bucket聚合分析如下:
    • Terms:该分桶策略最简单,直接按照term来分桶,如果是text类型,则按照分词后的结果分桶
      • API 示例: GET test_search_index/_search {"size":0,"aggs":{"jobs":{"terms":{"field":"job.keyword","size":4}}}}
    • Range:通过指定数值范围来设定分桶规则
      • API 示例:GET test_search_index/_search {"size":0,"aggs":{"age_rang":{"range":{"field":"age","ranges":[{"to":15},{"from":15,"to":20},{"from":20,"to":25},{"from":25,"to":30},{"from":30}]}}}}
    • Date Range:通过指定日期的范围来设定分桶规则
      • API 示例:GET test_search_index/_search {"size":0,"aggs":{"date_range":{"range":{"field":"birth","format":"yyyy","ranges":[{"to":"1980"},{"from":"1980","to":"1990"},{"from":"1990"}]}}}}
    • Histogram:直方图,以固定间隔的策略来分割数据
      • API 示例: GET test_search_index/_search {"size":0,"aggs":{"age_hist":{"histogram":{"field":"age","interval":5,"extended_bounds":{"min":15,"max":30}}}}}
    • Date Histogram:日期直方图,以固定日期间隔的策略来分割数据
      • API 示例: GET test_search_index/_search {"size":0,"aggs":{"birth_hist":{"date_histogram":{"field":"birth","interval":"year","format":"yyyy"}}}}

Bucket + Metric聚合分析

  • Bucket聚合分析允许通过添加子分析来进一步进行分析,该子分析可以是Bucket也可以是Metric。
  • 分桶后再分桶
    • API 示例:GET test_search_index/_search {"size":0,"aggs":{"jobs":{"terms":{"field":"job.keyword","size":10},"aggs":{"age_range":{"range":{"field":"age","ranges":[{"to":15},{"from":15,"to":20},{"from":20,"to":25},{"from":25,"to":30},{"from":30}]}}}}}}
  • 分桶后进行数据分析
    • API 示例:GET test_search_index/_search {"size":0,"aggs":{"jobs":{"terms":{"field":"job.keyword","size":10},"aggs":{"age":{"stats":{"field":"age"}}}}}}

Pipeline聚合分析

  • 针对聚合分析的结果再次进行聚合分析,而且支持链式调用
  • Pipeline的分析结果会输出到原结果中,根据输出位置的不同,分为两类:
    • Parent结构内嵌到现有的聚合分析结果中
      • Derivative 求导
      • Moving Average 移动平均值
      • Cumulative Sum 累加和
    • Sibling结果与现有聚合分析结果同级
      • Max/Min/Avg/Sum Bucket
        • API 示例:GET test_search_index/_search {"size":0,"aggs":{"jobs":{"terms":{"field":"job.keyword","size":10},"aggs":{"min_age":{"min":{"field":"age"}}}},"job_min_age":{"min_bucket":{"buckets_path":"jobs>min_age"}}}} 查询年龄最小的那个人是什么工作
      • Stats/Extended Stats Bucket
      • Percentiles Bucket

作用范围

  • es聚合分析默认作用范围是query的结果集,可以通过如下方式改变其作用范围:
    • filter:只为某一个聚合分析设定过滤条件,从而在不更改整体query语句情况下修改了作用范围
    • post_filter:作用于文档过滤,但在聚合分析后生效
      • API 示例: GET test_search_index/_search {"aggs":{"jobs":{"terms":{"field":"job.keyword"}}},"post_filter":{"match":{"job.keyword":"java engineer"}}}
    • global:无视query过滤条件,基于全部文档进行分析
      • API 示例: GET test_search_index/_search {"query":{"match":{"job":"java"}},"aggs":{"java_avg_age":{"avg":{"field":"age"}},"all":{"global":{},"aggs":{"avg_age":{"avg":{"field":"age"}}}}}}

排序

  • 可以使用自带的关键数据进行排序,比如:_count文档数、_key按照key值排序
  • API 示例: GET test_search_index/_search {"size":0,"aggs":{"jobs":{"terms":{"field":"job.keyword","size":10,"order":[{"stats_age.sum":"asc"}]},"aggs":{"stats_age":{"stats":{"field":"age"}}}}}} 按照stats_age.sum排序

原理和精准度问题

  • 原理:在每一个节点中,根据聚合分析算出每个节点上的聚合结果;每一个节点上的聚合结果返回到Coordinating Node中,Coordinating Node再把每一个节点聚合返回的结果进行再次的聚合分析,得到结果;最后返回到Client。但是Terms并不永远准确
  • Terms不准确的原因:由于数据分散在多个Shard上,Coordinating Node无法得悉数据全貌
  • Terms不准确的解决方法:
    • 设置Shard数为1,消除数据分散的问题
    • 合理设置Shard_Size大小,即每次从Shard上额外多获取数据,以提升准确度
      • 设定方法:terms聚合返回结果中有如下两个统计值
        • doc_count_error_upper_bound:被遗漏的term可能的最大值
        • sum_other_doc_count:返回结果bucket的term外其他term的文档总数
        • 设定show_term_doc_count_error:true可以查看每个bucket误算的最大值
      • Shard_Size默认大小:shard_size = (size * 1.5) + 10
      • 通过调整Shard_Size的大小降低doc_count_error_upper_bound来提升精度,减少了响应时间

近似统计算法

  • 在ES的聚合分析中,Cardinality和Percentile分析使用的是近似统计算法
    • 结果是近似准确的,但不一定精准
    • 可以通过参数的调整使其结果精准,但同时也意味着更多的计算的时间和更大的性能消耗

数据建模

  • 简介:英文为Data Modeling,为创建数据模型的过程
  • 数据模型
    • 对现实世界进行抽象描述的一种工具和方法
    • 通过抽象的实体以及实体之间的联系的形式去描述业务规则,从而实现对现实世界的映射
  • 数据建模过程:
    • 概念模型
      • 确定系统的核心需求和范围边界,设计实体与实体间的关系
    • 逻辑模型
      • 进一步梳理业务需求,确定每个实体的属性、关系和约束等
    • 物理模型
      • 结合具体的数据产品,在满足业务读写性能等需求的前提下确定最终的定义
      • MySQL、MongoDB、ElasticSearch等
      • 第三范式

ES中的数据建模

  • ES是基于Lucene以倒排索引为基础实现的存储体系,不遵循关系型数据库中的范式约定
  • Mapping字段相关设置
    • enabled
      • true|false
      • 仅存储,不做搜索或聚合分析
    • index
      • true|false
      • 是否构建倒排索引
    • index_options
      • docs|freqs|positions|offsets
      • 存储倒排索引的哪些信息
    • norms
      • true|false
      • 是否存储归一化相关参数,如果字段仅用于过滤和局和分析,可关闭
    • doc_values
      • true|false
      • 是否启动doc_values,用户排序和聚合分析
    • field_data
      • false|true
      • 是否为text类型启动fielddata,实现排序和聚合分析
    • store
      • false|true
      • 是否存储该字段值
    • coerce
      • true|false
      • 是否开启自动数据类型转换功能,比如字符串转为数字、浮点转为整型等
    • multifields多字段
      • 灵活使用多字段特性来解决多样的业务需求
    • dynamic
      • true|false|strict
      • 控制mapping自动更新
    • date_detection
      • true|false
      • 是否自动识别日期类型
  • Mapping字段属性的设定流程
    1. 是何种类型?
      • 字符串类型
        • 需要分词则设定为text类型,否则设置为keyword类型
      • 枚举类型
        • 基于性能考虑将其设定为keyword类型, 即便该数据为整型
      • 数值类型
        • 尽量选择贴近的类型,比如byte即可表示所有数值时,即选用byte,不要用long
      • 其他类型
        • 比如布尔类型、日期、地理位置数据等
    2. 是否需要检索?
      • 完全不需要检索、排序、聚合分析的字段
        • enabled设置为false
      • 不需要检索的字段
        • index设置为false
      • 需要检索的字段,可以通过如下配置设定需要的存储粒度
        • index_options结合需要设定
        • norms不需要归一化数据时关闭即可
    3. 是否需要排序和聚合分析?
      • 不需要排序或者聚合分析
        • doc_values设置为false
        • fielddata设置为false
    4. 是否需要另行存储?
      • 是否需要专门存储当前字段的数据?
        • store设定为true,即可存储该字段的原始内容(与_source中的不相关)
        • 一般结合_source的enabled设定为false时使用

关联关系处理

  • ES不擅长处理关系型数据库中的关联关系,在ES中可以通过如下两种手段变相解决
    • Nested Object
    • Parent/Child

Nested Object

在设置mapping的时候,type的值为nested

API示例: PUT blog_index {"mappings":{"doc":{"_source":{"enabled":true},"properties":{"title":{"type":"text","fields":{"keyword":{"type":"keyword"}},"store":true},"publish_date":{"type":"date","store":true},"author":{"type":"keyword","store":true},"abstract":{"type":"keyword","store":true},"content":{"type":"text","store":true},"url":{"type":"keyword","doc_values":false,"norms":false,"ignore_above":100,"store":true},"comments":{"type":"nested","properties":{"username":{"type":"keyword","ignore_above":100},"date":{"type":"date"},"content":{"type":"text"}}}}}}}

Parent/Child

ES还提供了类似关系型数据库中join的实现方式,使用join数据类型实现,6.x版本。指明父子关系类型

API示例:PUT blog_index_parent_child {"mappings":{"doc":{"properties":{"join":{"type":"join","relations":{"blog":"comments"}}}}}}

类型为join,关系relations中,左边为父类型名称,右边为子类型名称

在保存的时候,必须使得父子类型的数据在同一个shard上面

  • 常见query关键词
    • parent_id:返回某父文档的子文档
    • has_child:返回包含某子文档的父文档
    • has_parent:返回包含某父文档的子文档

Nested Object vs Parent/Child

对比 Nested Object Parent/Child
优点 文档存储在一起,因此可读性能高 父子文档可以独立更新,互不影响
更新父或子文档时需要更新整个文档 为了维护join的关系,需要占用部分内存,读取性能较差
场景 子文档偶尔更新,查询频繁 子文档更新频繁

尽量选择Nested Object解决问题

reindex

  • 指重建所有数据的过程,一般发生在如下情况:
    • mapping设置变更,比如字段类型变化、分词器字典更新等
    • index设置变更,比如分片更改等
    • 迁移数据
  • ES提供了现成的API用于完成该工作
    • _update_by_query在现有索引上重建
    • _reindex在其他索引上重建
    • POST _reindex {"source": {"index": "spider" }, "dest": {"index": "new_spider","op_type": "create"}}

集群调优建议

批量提交

  • 在数据读取方面,使用mget接口
  • 在数据变更方面,使用bulk接口

bulk size

在配置bulk数据时,一般需要注意请求体大小(bulk size)

  • 要确保 bulk 数据不要超过 http.max_content_length 设置
  • 建议bulk请求体的大小,在15M左右,可以通过实际测试继续上调合适的位置

实例:

以 logstash 默认的 bulk_size => 5000 为例,假设单条数据平均大小 200B ,一次 bulk 请求体的大小就是 1.5MB。那么我们可以尝试 bulk_size => 50000;而如果单条数据平均大小是 20KB,一次 bulk 大小就是 100MB,显然超标了,需要尝试下调至 bulk_size => 500

gateway

gateway是ES设计用来长期存储索引数据的接口。一般来说,大家都是使用本地磁盘来存储索引数据,即

gateway.type为local

  • gateway.recover_after_nodes:该参数控制集群在达到多少个节点的规模后,才开始数据恢复任务。可以避免集群自动发现的初期,分片不全的问题
  • gateway.revover_after_time:该参数控制集群在达到上条配置设置的节点规模后,再等待多久才开始数据恢复任务
  • gateway.expected_nodes:该参数设置集群的预期节点总数。在达到这个总数后,即认为集群节点已经完全加载,即可开始数据恢复,不用再等待上条设置的时间。

注意:gateway 中说的节点,仅包括主节点和数据节点,纯粹的 client 节点是不算在内的。如果你有更明确的选择,也可以按需求写:

  • gateway.recover_after_data_nodes
  • gateway.recover_after_master_nodes
  • gateway.expected_data_nodes
  • gateway.expected_master_nodes

缓存

在ES中针对不同的阶段,设计有不同的缓存,以此提升数据检索时的响应性能。主要包括节点层面的filter cache和分片层面的request cache

filter cache

在使用过程中傻傻分不清filter和query,在filter和query中有上下文的区别,ES依靠这个上下文判断,来决定是否启用filter。

query和filter上下文的区别:

  • query是要相关性评分的,filter不需要
  • query结果无法缓存,filter可以

则:

  • 全文检索、评分排序,使用query
  • 是非过滤、精确匹配,使用filter

需要注意的是,filter cache 是节点层面的缓存设置,每个节点上所有数据在响应请求时,是共用一个缓存空间的。当空间用满,按照 LRU 策略淘汰掉最冷的数据。

可以用 indices.cache.filter.size 配置来设置这个缓存空间的大小,默认是 JVM 堆的 10%,也可以设置一个绝对值。注意这是一个静态值,必须在 elasticsearch.yml 中提前配置。

shard request cache

ES 还有另一个分片层面的缓存,叫 shard request cache。5.0 之前的版本中,request cache 的用途并不大,因为 query cache 要起作用,还有几个先决条件:

  1. 分片数据不再变动,也就是对当天的索引是无效的(如果 refresh_interval 很大,那么在这个间隔内倒也算有效);
  2. 使用了 "now" 语法的请求无法被缓存,因为这个是要即时计算的;
  3. 缓存的键是请求的整个 JSON 字符串,整个字符串发生任何字节变动,缓存都无效。

生成环境部署建议

  • 遵照官方建议设置所有的系统参数
  • 参见文档Setup ElasticSearch -> Important System Configuration
  • 容量预估 磁盘总大小 = 源数据 * (1 + 副本数量) * (1 + 索引开销) / (1 - Linux预留空间) / (1 - ES开销) / (1 - 安全阈值) = 源数据 * (1 + 副本数量) * 1.7
  • 大文件参数设置
  • 别名设置

服务器优化

  • Java环境
  • * - nofile 20480, 调整文件打开数
  • swap off,关闭swap
  • * - memlock unlimited, 调整memlock
  • ulimit -n 204800, 调整每个进程可打开文件数

JVM参数

vi /elasticsearch/bin/elasticsearch.sh set ES_HEAP_SIZE=31G

HEAP_SIZE 设置为物理内存的50%左右,其余剩下的内容留给操作系统做文件系统分页缓存,不要超过32G

索引优化

  • client端减少频繁建立连接
    • 使用TCP长连接,而非HTTP
  • client减少请求次数,合并索引操作
    • 使用bulk接口
    • 合理增加bulk对列长度
  • 尽量减少索引大小,索引按日期滚动
  • 写入数据不指定_id,让ES自动产生

Java API

如果使用的是spring-boot-starter-data-elasticsearch,则可以直接使用创建接口继承ElasticSearchRepository<T, ID>即可,不过高版本会有分页查询不准确的问题,同时分词需要另外配置,其中的增删改查直接使用接口提供的即可

如果使用的是原生的ElasticSearch Java API操作,可以使用注入ElasticsearchTemplate,直接操作