一文带你了解elasticsearch
elasticsearch

es基本概念

es术语介绍

  • 文档Document
    • 用户存储在es中的数据文档
  • 索引Index
    • 由具有相同字段的文档列表组成
  • 节点node
    • 一个Elasticsearch的运行实例,是集群的构成单元
  • 集群Cluster
    • 由一个或多个节点组成,对外提供服务

document介绍

  • 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
  • 每个文档有唯一的id标识
    • 自行指定
    • es自动生成
  • 元数据,用于标注文档的相关信息
    • _index:文档所在的索引名
    • _type:文档所在的类型名
    • _id:文档唯一id
    • _uid:组合id,由_type和_id组成(6.x _type不再起作用,同_id一样)
    • _source:文档的原始json数据,可以从这里获取每个字段的内容
    • _all:整合所有字段内容到该字段,默认禁用

Index介绍

  • 索引中存储具有相同结构的文档(Document)
    • 每个索引都有自己的mapping定义,用于定义字段名和类型
  • 一个集群可以有多个索引,比如:
    • nginx日志存储的时候可以按照日期每天生成一个索引来存储

rest api

  • elasticsearch集群对外提供restful api
    • rest-representational state transfer
    • uri指定资源,如index、document等
    • http method指明资源操作类型,如GET、POST、PUT、DELETE等
  • 常用两种交互方式
    • curl命令行
    • Kibana Devtools

索引API

  • es有专门的index api,用于创建、更新、删除索引配置等
    • 创建索引api如下:
      PUT /test_index

文档api

  • es有专门的Document API
    • 创建文档
    • 查询文档
    • 更新文档
    • 删除文档
  • 创建文档
    • 指定id创建文档api如下:
      PUT /test_index/doc/1
      {
      "username":"alfred",
      "age":1
      }

      创建文档时,如果索引不存在,es会自动创建对应的index和type

    • 不指定id创建文档api如下:
      POST /test_index/doc
      {
      "username":"tom",
      "age":20
      }
  • 查询文档
    • 指定要查询的文档id
      GET /test_index/doc/1
    • 搜索所有文档,用到_search,如下:
GET /test_index/doc/_search
{
    "query":{
      "term":{
        "_id":"1"
      }
    }
}

查询语句,json格式,放在http body中发送到es

批量创建文档api

  • es允许一次创建多个文档,从而减少网络传输开销,提升写入速率
    • endpoint为_bulk,如下:
      POST _bulk
      {"index":{"_index":"test_index","_type":"doc","_id":"3"}}
      {"username":"alfred","age":10}
      {"delete":{"_index":"test_index","_type":"doc","_id":"1"}}

批量查询文档API

  • es允许一次查询多个文档
    • endpoint为_mget,如下:
      GET /_mget
      {
      "doc":[
      {
      "_index":"test_index",
      "_type":"doc",
      "_id":"1"
      },
      {
      "_index":"test_index",
      "_type":"doc",
      "_id":"2"
      }
      ]
      }

索引

正排索引与倒排索引

  • 正排索引
    • 文档id到文档内容、单词的关联关系
文档id 文档内容
1 elasticsearch是最流行的搜索引擎
2 PHP是世界上最好的语言
3 搜索引擎是如何诞生的
  • 倒排索引
    • 单词到文档id的关联关系
单词 文档ID列表
elasticsearch 1
流行 1
搜索引擎 1,3
世界 2

倒排索引-查询流程

  • 查询包含"搜索引擎"的文档
    • 通过倒排索引获得"搜索引擎"对应的文档ID有1和3
    • 通过正排索引查询1和3的完整内容
    • 返回用户最终结果

倒排索引详解

  • 倒排索引是搜索引擎的核心,主要包含两部分:
    • 单词字典(Term Dictionary)
    • 倒排列表(Posting List)
  • es存储的是一个json格式的文档,其中包含多个字段,每个字段会有自己的倒排索引

单词词典

  • 单词字典(Term Dictionary)是倒排索引的重要组成部分
    • 记录所有文档的单词,一般都比较大
    • 记录单词到倒排列表的关联信息
  • 单词字典的实现一般是用B+ Tree

倒排列表

  • 倒排列表记录了单词对应的文档集合,由倒排索引项组成
  • 倒排索引项主要包含如下信息:
    • 文档ID,用于获取原始信息
    • 单词频率,记录该单词在该文档中的出现次数,用于后续相关性算分
    • 位置,记录单词在该文档中的分词位置(多个),用于做词语搜索
    • 偏移,记录单词在文档的开始和结束位置,用于做高亮显示

分词

  • 分词是指将文本转换成一系列单词的过程,也叫作文本分析,在es里面称为analysis

分词器

  • 分词器是es中专门处理分词的组件,英文为analy,它的组成如下:
    • Character Filters
      • 针对原始文本进行处理,比如去除HTML特殊标记符
    • Tokenizer
      • 将原始文本按照一定规则切分为单词
    • Token Filters
      • 针对tokenizer处理的单词进行再加工,比如转小写、删除或新增等处理

        Analyize API

    • es提供了一个测试分词的api接口,方便验证分词效果,endpoint是_analyze
      • 可以直接指定analyzer进行测试
      • 可以直接指定索引中的字段进行测试
      • 可以自定义分词器进行测试
  • 直接指定analyzer进行测试,接口如下:
    POST _analyze
    {
    "analyzer":"standard", #分词器
    "text":"hello,world"  #测试文本
    }
  • 自定义分词器进行测试,接口如下:
    POST _analyze
    {
    "tokenizer":"standard",
    "filterf":["lowercase"],  #自定义analyzer
    "text":"hello world"
    }

预定义的分词器

  • es自带如下的分词器
    • Standard
    • Simple
    • Whitespace
    • Stop
    • Keyword
    • Pattern
    • Language
  • Standard Analyzer
    • 默认分词器
    • 其组成如下,特性为:
      • 按词切分,支持多语言
      • 小写处理
  • Simple Analyzer
    • 其组成如下,特性为:
      • 按照非字母切分
      • 小写处理
  • Whitespace Analyzer
    • 其组成如下,特性为:
      • 按照空格切分
  • Stop Analyzer
    • Stop Word指语气助词等修饰性的词语,比如the、an、的、这等等
    • 其组成如图,特性为:
      • 相比Simple Analyzer多了stop Word处理
  • Keyword Analyzer
    • 其组成如下,特性为:
      • 不分词,直接将输入作为一个单词输出
  • Pattern Analyze
    • 其组成如下,特性为:
      • 通过正则表达式自定义分隔符
      • 默认是\W+,即非字词的符号作为分隔符
  • language Analyze
    • 提供了30+常见语言的分词器
    • Arabic,Armenian,basque,bengali,Brazilian,Bulgarian,catAlan,cjk,Czech,Danish,Dutch,English...

中文分词

  • 难点
    • 中文分词指的是将一个汉字序列切分成一个一个单独的词。在英文中,单词之间是以空格作为自然分界符,汉语中没有一个形式上的分界符
    • 上下文不同,分词结果迥异,比如交叉歧义问题,比如下面两种分词都合理
      • 乒乓球拍/卖/完了
      • 乒乓球/拍卖/完了
  • 常用分词系统
  • 基于自然语言处理的分词系统

自定义分词

  • 当自带的分词无法满足需求时,可以自定义分词
    • 通过自定义Character Filters、Tokenizer Filter实现
  • Character Filters
    • 在Tokenizer之前对原始文本进行处理,比如增加、删除或替换字符等
    • 自带的如下:
      • HTML Strip去除html标签和转换html实体
      • Mapping进行字符替换操作
      • Pattern Replace进行正则匹配替换
    • 会影响后续Tokenizer解析的position和offset信息
  • Character Filters测试时可以采用如下api:
    POST _analyze
    {
    "tokenizer":"keyword", #keyword类型的Tokenizer可以直接看到输出结果
    "char_filter":["html_strip"], #指明要使用的char_filter
    "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按照文件路径进行分割
    • Tokenizer测试时可以采用如下api:
      POST _analyze
      {
      "tokenizer":"path_hierarchy",
      "text":"/one/two/three"
      }
  • Token Filters
    • 对于Tokenizer输出的单词(term)进行增加、删除、修改等操作
    • 自带的如下:
      • lowercase将所有term转换为小写
      • stop删除stop words
      • NGram和Edge NGram连词分割
      • Synonym添加近义词的term
    • Filter测试时可以采用如下api:
      POST _analyze
      {
      "text":"a hello world",
      "tokenizer":"standard",
      "filter":[
      "stop",
      "lowercase",
      {
      "type":"ngram",
      "min_gram":4,
      "max_gram":4
      }
      ]
      }

自定义分词的api

  • 自定义分词的api
    • 自定义分词需要在索引的配置中设定,如下所示:
      PUT test_index
      {
      "settings":{
      "analysis":{
      "char_filter":{},
      "tokenizer":{},
      "filter":{},
      "analyzer":{}
      }
      }
      }

分词使用说明

  • 分词会在如下两个时机使用:
    • 创建或更新文档时(Index Time),会对相应的文档进行分词处理
    • 查询时(Search Time),会对查询语句进行分词
  • 索引时分词是通过配置Index Mapping中每个字段的analyzer属性实现的,如下:
    • 不指定分词时,默认使用standard
      PUT test_index
      {
      "mappings":{
      "doc":{
      "properties":{
      "title":{
        "type":"text",
        "analyzer":"whitespace" #指定分词器
      }
      }
      }
      }
      }
  • 查询时分词的指定方式有如下几种:

    • 查询时通过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,查看文档的具体分词结果
  • 动手测试

Mapping

  • 类似数据库中的表结构定义,主要作用如下:
    • 定义Index下的字段名(Field Name)
    • 定义字段的类型,比如数值型、字符串型、布尔型等
    • 定义倒排索引相关的配置,比如是否索引、记录position等。

自定义mapping

  • 自定义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参数

  • copy_to
    • 将该字段的值复制到目标字段,实现类似_all的作用
    • 不会出现在_source中,只用来搜索
      PUT my_index
      {
      "mappings":{
      "doc":{
      "properties":{
      "first_name":{
        "type":""text",
        "copy_to":"full_name"
      },
      "last_name":{
        "type":"text",
        "copy_to":"full_name"
      },
      "full_name":{
        "type":"text"
      }
      }
      }
      }
      }

index参数

  • index
    • 控制当前字段是否索引,默认为true,即记录索引,false不记录,即不可搜索
      PUT my_index
      {
      "mappings":{
      "doc":{
      "properties":{
      "cookie":{
        "type":"text",
        "index":"false"
      }
      }
      }
      }
      }

index_options参数

  • 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
  • 记录内容越多,占用空间越大
  • indexoptions设定如下所示:
    PUT my_index
    {
    "mappings":{
    "doc":{
      "properties":{
        "cookies":{
          "type":"text",
          "index_options":"offsets"
        }
      }
    }
    }
    }
  • null_value
    • 当字段遇到null值时的处理策略,默认为null,即空值,此时es会忽略该值。可以通过设定该值设定字段的默认值
      PUT my_index
      {
      "mappings":{
      "my_type":{
      "properties":{
      "status_code":{
        "type":"keyword",
        "null_value":"NULL"
      }
      }
      }
      }
      }

数据类型

  • 核心数据类型
    • 字符串: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
  • 复杂数据类型
    • 数组类型array
    • 对象类型object
    • 嵌套类型nested object
  • 地理位置数据类型
    • geo_point
    • geo_shape
  • 专用类型
    • 记录ip地址ip
    • 实现自动补全completion
    • 记录分词数token_count
    • 记录字符串hash值murmur3
    • percolator
    • join
  • 多字段特性
    • 允许对同一个字段采用不同的配置,比如分词,常见例子如对人名实现拼音搜素,只需要在人名中新增一个子字段为pinyin即可
      {
      "test_index":{
      "mappings":{
      "doc":{
      "properties":{
        "username":{
          "type":"text",
          "fields":{
            "pinyin":{
              "type":"text",
              "analyzer":"pinyin"
            }
          }
        }
      }
      }
      }
      }
      }

Dynamic Mapping

  • es可以自动识别文档字段类型,从而降低用户使用成本
  • es是依靠json文档的字段类型来实现自动识别字段类型,支持的类型如下:
JSON类型 es类型
null 忽略
Boolean Boolean
浮点类型 float
整数 long
object object
array 有第一个非null值的类型决定
string 匹配为日期则设为date类型(默认开启),匹配为数字的话设为float或long类型(默认关闭),设为text类型,并附带keyword的子字段

dynamic日期与数字识别

  • 日期的自动识别可以自行配置日期格式,以满足各种需求
    • 默认是["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-20T15;30:50 +01:00)
    • dynamic_date_formats可以自定义日期类型
    • date_detection可以关闭日期自动识别的机制
      PUT mu_index
      {
      "mappings":{
      "my_type":{
      "dynamic_date_formats":["MM/dd/yyyy"],
      "date_detection":false
      }
      }
      }
  • 字符串是数字时,默认不会自动识别为整型,因为字符串中出现数字是完全合理的
    • numeric_detection可以开启字符串中数字的自动识别,如下所示:
      PUT my_index
      {
      "mappings":{
      "my_type":{
      "numeric_detection":true
      }
      }
      }

Dynamic Templates

  • 允许根据es自动识别的数据类型、字段名等来动态设定字段类型,可以实现如下效果:
    • 所有字符串类型都设定为keyword类型,即默认不分词
    • 所有以message开头的字段都设定为text类型,即分词
    • 所有以long_开头的字段都设定为long类型
    • 所有自动分配为double类型的都设定为float类型,以节省空间
  • API如下所示:
    PUT test_index
    {
    "mappings":{
    "doc":{
      "dynamic_templates":[#数组,可指定多个匹配规则
        {
          "strings":{ #模板名称
            "match_mapping_type":"string",#匹配规则
            "mapping":{
              "type":"keyword"
            }
          }
        }
      ]
    }
    }
    }
    • 匹配规则一般有如下几个参数:
      • match_mapping_type匹配es自动识别的字段类型,如Boolean,long,string等
      • match,unmatch匹配字段名
      • path_match,path_unmatch匹配路径

自定义mapping的建议

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

首先创建一个文档

PUT my_index/doc/1
{
  "referrer": "-",
  "response":"200",
  "remote_ip":"171.22.12.14",
  "method":"POST",
  "user_name":"-",
  "http_version":"1.1",
  "body_sent":{
    "bytes":"0"
  },
  "url":"/analyzeVideo"
}

es会根据创建的文档动态生成映射,可以直接将动态生成的映射直接复制到需要自定义的mapping中

PUT test_index
{
  "mappings": {
      "doc": {
        "properties": {
          "body_sent": {
            "properties": {
              "bytes": {
                "type": "long"

              }
            }
          },
          "http_version": {
            "type": "keyword"

          },
          "method": {
            "type": "keyword"
          },
          "referrer": {
            "type": "keyword"

          },
          "remote_ip": {
            "type": "keyword"

          },
          "response": {
            "type": "long"
          },
          "url": {
            "type": "text"
          },
          "user_name": {
            "type": "keyword"
          }
        }
      }
  }
}

这样定义的映射还是比较多余,可以利用动态模板将string类型直接替换成keyword

DELETE test_index
PUT test_index
{
  "mappings": {
      "doc": {
        "dynamic_templates":[
          {
            "strings":{
              "match_mapping_type":"string",
              "mapping":{
                "type":"keyword"
              }
            }
          }  
        ],
        "properties": {
          "body_sent": {
            "properties": {
              "bytes": {
                "type": "long"

              }
            }
          },
          "response": {
            "type": "long"
          },
          "url": {
            "type": "text"
          }
        }
      }
  }
}

索引模板

  • 索引模板,主要用于在新建索引时自动应用预先设定的配置,简化索引创建的操作步骤
    • 可以设定索引的配置和mapping
    • 可以有多个模板,根据order设置,order搭的覆盖小的配置

索引模板API如下所示:

PUT _template/test_template
{
  "index_patterns":["te*","bar*"],
  "order":0,
  "settings":{
   "number_of_shards":1,
   "number_of_replicas":0
  },
  "mappings":{
    "doc":{
      "_source":{
        "enabled":false
      },
      "properties":{
        "name":{
          "type":"keyword"
        }
      }
    }
  }
}
  • "index_paterns":匹配的索引名称
  • "order":匹配的优先级
  • "settings":索引的配置

search API

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

URI Search

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

URI Search - Query String Syntax

  • term与phrase
    • alfred way 等效于Alfred OR way
    • "alfred way" 词语查询,要求先后顺序
  • 泛查询
    • alfred 等效于在所有字段去匹配该term
  • 指定字段
    • 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) #返回一定包含lee,可以包含tom,一定不包含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差一个character的词,比如foam roams等
  • 近似度查询 proximity search
    • "fox quick" ~5
    • 以term为单位进行差异比较,比如"quick fox" "quick brown fox"都会被匹配

Query DSL

  • 基于json定义的查询语句,主要包含如下两种类型:
    • 字段类查询:
      • 如term,match,range等,只针对某一个字段进行查询
    • 复合查询
      • 如bool查询等,包含一个或多个字段类查询或者复合查询语句

Query DSL 字段类查询

  • 字段类查询主要包括以下两类:
    • 全文匹配
      • 针对text 类型的字段进行全文检索,会对查询语言先进行分词处理,如match,match_phrase等query类型
    • 单词匹配
      • 不会对查询语句做分词处理,直接去匹配字段的倒排索引,如term.terms,range等query类型
Match Query
  • 对字段作全文检索,最基本和常用的查询类型,API示例如下:
    GET test_index/_search
    {
    "query":{
    "match":{   #关键词
      "remote_ip":"171.22.12.14" #字段名
    }
    }
    }

    响应结果如下:

    {
    "took": 4, #查询总用时
    "timed_out": false, #是否超时
    "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
    },
    "hits": {
    "total": 1, #匹配文档总数
    "max_score": 0.2876821,
    "hits": [  #返回文档列表
      {
        "_index": "test_index_index",
        "_type": "doc",
        "_id": "1",
        "_score": 0.2876821,  #文档相关度得分
        "_source": { #文档原始内容
          "referrer": "-",
          "response": "200",
          "remote_ip": "171.22.12.14",
          "method": "POST",
          "user_name": "-",
          "http_version": "1.1",
          "body_sent": {
            "bytes": "0"
          },
          "url": "/analyzeVideo"
        }
      }
    ]
    }
    }
  • 通过operator参数可以控制单词间的匹配关系,可选项为or和and
  • 通过minimum_should_match参数可以控制需要匹配的单词数
Match Query -流程

首先对查询语句进行分词,分词后分别根据字段的倒排索引进行匹配算分,并会匹配到一个或多个文档,再将匹配到的文档进行汇总得分,根据得分排序返回多个文档

Match Phrase Query
  • 对字段做检索,有顺序要求,API示例如下:
    GET test_index_index/_search
    {
    "query":{
    "match_phrase":{
      "remote_ip":"171.22.12.14"
    }
    }
    }
  • 通过slop参数可以控制单词间的间隔
Query String Query
  • 类似于URI Search中的q参数查询

    GET test_index_index/_search
    {
    
    "query":{
    "query_string":{
      "default_field":"remote_ip",
      "query":"171.22.12.14"     
    }
    }
    }
Simple Query String Query
  • 类似Query String,但是会忽略错误的查询语法,并且仅支持部分查询语法
  • 其常用的逻辑符号如下,不能使用AND、OR、NOT等关键词:

    • +代指AND
    • |代指OR
    • -代指NOT
      常用API如下所示:
      
      GET test_index_index/_search
      {

    "query":{
    "simple_query_string":{
    "fields":["remote_ip"],
    "query":"alfred +way"
    }
    }
    }

Term Query
  • 将查询语句作为整个单词进行查询,即不对查询语句做分词处理,如下所示:

    GET test_index_index/_search
    {
    
    "query":{
    "term":{
      "remote_ip":"171.22.12.14"
    }
    }
    }
Range Query
  • 范围查询主要针对数值和日期类型,如下所示:

    GET test_index_index/_search
    {
    
    "query":{
    "range":{
      "response":{#找出响应状态码大于10,小于300的文档
        "gt": 10,
        "lte":300
      }
    }
    }
    }

    针对日期的查询如下所示:

    GET test_index_index/_search
    {
    "query":{
    "range":{
      "birth":{
        "gt": "1990-01-01",
        "lte":"now-2h",
        "gt":"2019-01-01||+1M/d"
      }
    }
    }
    }

相关性算分

  • 相关性算分是指文档与查询语句间的相关度,英文为relevance
    • 通过倒排索引可以获取与查询语句相匹配的文档列表,那么如何将最符合用户查询需求的文档放到前列呢?
    • 本质是一个排序问题,排序的依据是相关性算分
  • 相关性算分的几个重要概念如下:

    • Term Frequency(TF)词频,即单词在该文档中出现的次数。词频越高,相关度越高
    • Document Frequency(DF)文档频率,即单词出现的文档数
    • Inverse document Frequency(IDF)逆向文档频率,与文档频率相反,简单理解为1/DF。即单词出现的文档数越少,相关度越高
    • Field-length Norm 文档越短,相关性越高
  • ES目前主要有两个相关性算分模型,如下:
    • TF/IDF模型
    • BM25模型 5.x之后的默认模型
TF/IDF模型
  • 可以通过explain参数来查看具体的计算方法,但要注意:
    • es的算分是按照shard进行的,即shard的分数计算是相互独立的,所以在使用explain的时候注意分片数
    • 可以通过设置索引的分片数为1来避免这个问题
      GET test_index_index/_search
      {
      "explain": true, 
      "query":{
      "match":{
      "remote_ip":"171.22.12.14"
      }
      }
      }
BM25模型
  • BM25模型中BM指Best Match,25指迭代了25次才计算方法,是针对TF/IDF的一个优化。
  • BM25相比TF/IDF的一大优化是降低了tf在过大时的权重

Query DSL 复合查询

  • 复合查询是指包含字段类查询或符合查询的类型,主要包括以下几类:
    • constant_score query
    • bool query
    • dis_max query
    • function_score query
    • boosting query
      Constant Score Query
  • 该查询将其内部的查询结果文档得分都设定为1或者boost的值
    • 多用于结合bool查询实现自定义部分
      GET test_index_index/_search
      {
      "query":{
      "constant_score":{
      "filter":{
      "match": {
        "response":200
      }
      }
      }
      }
      }

      响应如下:

      {
      "took": 7,
      "timed_out": false,
      "_shards": {
      "total": 5,
      "successful": 5,
      "skipped": 0,
      "failed": 0
      },
      "hits": {
      "total": 1,
      "max_score": 1,
      "hits": [
      {
      "_index": "test_index_index",
      "_type": "doc",
      "_id": "1",
      "_score": 1,
      "_source": {
        "referrer": "-",
        "response": "200",
        "remote_ip": "171.22.12.14",
        "method": "POST",
        "user_name": "-",
        "http_version": "1.1",
        "body_sent": {
          "bytes": "0"
        },
        "url": "/analyzeVideo"
      }
      }
      ]
      }
      }
Bool Query
  • 布尔查询由一个或多个布尔子句组成,主要包含如下四个:
子句 内容
filter 只过滤符合条件的文档,不计算相关性得分
must 文档必须符合must中的所有条件,会影响相关性得分
must_not 文档必须不符合must_not中的所有条件
should 文档可以符合should中的条件,会影响相关性得分
  • Bool查询的API如下所示
    GET test_index_index/_search
    {
    "query":{
    "bool":{
      "filter":[
        {}
      ],
      "should":[
        {}  
      ],
      "must":[
        {}  
      ],
      "must_not": [
        {}
      ]
    }
    }
    }

    查询response为200,ip为"171.22.12.14"的值

    GET test_index_index/_search
    {
    "query":{
    "bool":{
      "must":[
        {
          "match":{
            "response":200
          }
        },
        {  
          "match":{
            "remote_ip": "171.22.12.14"
          } 
        }  
      ]
    }
    }
    }

查询上下文与过滤器上下文

  • 当一个查询语句位于Query或者Filter上下文时,es执行的结果会不同,对比如下:
上下文类型 执行类型 使用方式
Query 查询与查询语句最匹配的文档,对所有文档进行相关性算分并排序 1.query 2. bool中的must和should
Filter 查找与查询语句相匹配的文档,只过滤不算分,经常使用过滤器,ES会自动的缓存过滤器的内容,这对于查询来说,会提高很多性能 1.bool中的filter与must_not 2.constant_score中的filter
GET /_search
{
  "query": { 
    "bool": { 
      "must": [
        { "match": { "title":   "Search"        }}, 
        { "match": { "content": "Elasticsearch" }}  
      ],
      "filter": [ 
        { "term":  { "status": "published" }}, 
        { "range": { "publish_date": { "gte": "2015-01-01" }}} 
      ]
    }
  }
}

Count API

  • 只返回符合条件的文档数,endpoint为_count,不返回文档内容
    API示例如下:
    GET test_index_index/_count
    {
    "query":{
    "match":{
      "response":200   
    } 
    }
    }

    响应如下:

    {
    "count": 1,
    "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
    }
    }

Source Filter

  • 过滤返回结果中_source中的字段,主要有如下几种方式:
    不返回_source
    GET test_index/_search
    {
    "_source":false
    }

    返回部分字段

    GET test_index/_search
    {
    "_source":["response","name"]
    }
    GET test_index/_search
    {
    "_source":{
        "includes":"*i*",
        "excludes":"remote_ip"
    }
    }

es分布式

分布式特性

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

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

副本与分片

  • 如何将数据分布于所有节点上?
    • 引入分片解决问题
  • 分片是es支持PB级数据的基石
    • 分片存储了部分数据,可以分部于任意分片上
    • 分片数在索引创建时指定且后续不允许再更改,默认为5个
    • 分片有主分片和副本分片之分,以实现数据的高可用
    • 副本分片的数据由主分片同步,可以有多个,从而提高读取的吞吐量
  • 分片数的设定很重要,需要提前规划好
    • 过小会导致后续无法通过增加节点实现水平扩容
    • 过大会导致一个节点上分布过多过多分片,造成资源浪费,同时会影响查询性能

故障转移

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

文档分布式存储

  • document1是如何存储到分片P1的?选择P1的依据是什么?
    • 需要文档到分片的映射算法
  • 目的
    -使得文档均与分布在所有分片上,以充分利用资源
  • 算法
    • 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
  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发起批量创建文档的请求
  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,比如这里有两个shard需要参与
  3. R1、R2返回文档结果
  4. node3返回结果给client

搜索数据的流程

  1. 客户端发送请求到一个coordinate node
  2. 协调节点将搜索请求转发到所有的shard对应的primary shard或replica shard也可以
  3. query phase:每个shard将自己的搜索结果(其实就是一些doc id),返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果
  4. fetch phase:接着由协调节点,根据doc id去各个节点上拉取实际的document数据,最终返回给客户端

脑裂问题

  • 脑裂问题,英文为split-brain,是分布式系统中的经典网络问题
    • 当node1故障时,node2与node3会重新选举master,比如node2成为了新的master,此时会更新cluster state
    • node1自己组成集群后,也会更新cluster state
  • 同一个集群有两个master,而且维护不同的cluster state,网络恢复后无法选择正确的master
  • 解决方案为仅可在选举master-eligible节点数大于等于quorum时才可以进行master选举
    • quorum=master-eligible节点数/2+1,例如三个master-eligible节点时,quorum为2
    • 设定discovery.zen.minimum_master_nodes为quorum即可避免脑裂

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默认每一秒执行一次refresh,因此文档的实时性被提高到1秒,这也是es被称为近实时的原因
  • refresh发生的时机主要有如下几种情况:
    • 间隔时间达到时,通过index.settings.refresh_interval来设定,默认是一秒
    • index.buffer占满时,其大小通过indices.memory.index_buffer_size设置,默认为jvm heap的10%,所有shard共享
    • flush发生时也会发生refresh

文档搜索实时性-translog

  • 如果在内存中的segment还没有写入磁盘前发生了宕机,name其中的文档就无法恢复了。
    • es引入translog机制。写入文档到buffer时,同时将该操作写入translog
    • translog文件会即时写入磁盘(fsync),6.x默认每个请求都会落盘,可以修改为每五秒写一次,这样风险便是丢失5秒内的数据,相关配置为index.translog.*
    • es启动时会检查translog文件,并从中恢复数据

文档搜索实时性-flush

  • flush负责将内存中的segment写入磁盘,主要做如下的工作:
    • 将translog写入磁盘,形成translog文件
    • 将index buffer清空,其中的文档生成一个新的segment,相当于一个refresh操作
    • 更新commit point并写入磁盘
    • 执行fsync操作,将内存中的segment写入磁盘
    • 删除旧的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 运行机制

  • search 执行的时候实际分两个步骤运作的
    • Query阶段
    • fetch阶段
  • Query-Then-Fetch

Query阶段

  • node3在接收到用户的search请求后,会先进行Query阶段
  • node3在6个主副分片中随机选择三个分片,发送search request
  • 被选中的3个分片会分别进行查询并排序,返回from+size个文档Id和排序值
  • 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

排序

  • es默认会采用相关性算分排序,用户可以通过设定sorting参数来自行设定排序规则
    GET test_search_index/_search
    {
    "sort":[ #关键字
      {
        "birth":"desc"
      },
      {
        "_score":"desc"
      },
      {
        "_doc":"desc"
      }
    ]
    }
    • 按照字符串排序比较特殊,因为es有text和keyword两种类型,针对text类型排序,如下所示:
      GET test_search_index/_search
      {
      "sort":{
      "username":"desc"
      }   
      }
      #会产生报错
  • 针对keyword类型排序,可以返回预期结果
    GET test_search_index/_search
    {
    "sort":{
      "username.keyword":"desc"
    }
    }
  • 排序的过程实质是对字段原始内容排序的过程,这个过程中倒排索引无法发挥作用(term->document),需要用到正排索引,也就是通过文档id和字段可以快速得到字段原始内容。
  • es对此提供了两种实现方式:
    • fielddata默认禁用
    • doc values默认启用,除了text类型
文档ID 字段值
1 100
2 89
3 129

fielddata vs DocValues

对比 fielddata DocValues
创建时机 搜索时即时创建 索引时创建,与倒排索引创建时机一致
创建位置 JVM Heap 磁盘
优点 不会占用额外的磁盘空间 不会占用Heap内存
缺点 文档过多时,即时创建会花过多时间,占用过多Heap内存 减慢索引的速度,占用额外的磁盘资源

Fielddata

  • Fielddata默认是关闭的,可以通过如下api开启:
    • 此时字符串是按照分词后的term排序,往往结果很难符合预期
    • 一般是在对分词做聚合分析的时候开启
      PUT test_search_index/_mapping/doc
      {
      "properties":{
      "username":{
      "type":"text",
      "fileddata":"true"
      } 
      }
      }

Doc Values

  • Doc Values默认是启用的,可以在创建索引的时候关闭:
    • 如果后面要再开启doc values,需要做reindex操作
      PUT test_doc_value/
      {
      "mappings":{
      "doc":{
      "properties":
      }
      }
      }

docvalue_fields

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

分页与遍历

From/Size

  • 最常用的分页方案
    • from指明开始位置
    • size指明获取总数
  • 深度分页是一个经典问题:在数据分片存储的情况下如何获取前1000个文档?
    • 获取从990-1000的文档时,会在每个分片上都先获取1000个文档,然后再由coordinating node局和所有分片的结果后再排序选取前1000个文档
    • 页数越深,处理文档越多,占用内存越多,耗时越长。尽量避免深度分页,es通过index.max_result_window限定最多到10000条数据
      total_page=(total+page_size-1)/page_size
      total为文档总数

Scroll

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

search_after

  • 避免深度分页的性能问题,提供实时的下一页文档获取功能
    • 缺点是不能使用from参数,即不能指定页数
    • 只能下一页,不能上一页
    • 使用简单
  • 第一步为正常的搜索,但要指定sort值,并保证值唯一
  • 第二步为使用上一步最后一个文档的sort值进行查询
    GET test_search_index/_search
    {
    "size":1,
    "sort":{
      "age":"desc",
      "_id":"desc"
    }
    }
    GET test_search_index/_search
    {
    "size":1,
    "search_after":[28,"2"],
    "sort":{
      "age":"desc",
      "_id":"desc"
    }
    }
  • search_after如何避免深度分页问题
    • 通过唯一排序值定位将每次要处理的文档数都控制在size内

应用场景

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

聚合分析

  • 聚合分析,英文为aggregation,是es除搜索功能外提供的针对es数据做统计分析的功能
    • 功能丰富,提供bucket、metric。pipeline等多种分析方式,可以满足大部分的分析需求
    • 实时性高,所有的计算都是即时返回的,而Hadoop等大数据系统一般都是T+1级别的(today+1,第二天)
  • 聚合分析作为search的一部分,api如下所示:
    GET test_search_index/_search
    {
    "size":0,
    "aggs":{ #关键词,与query同级
      "<aggregation_name>":{#定义聚合名称
        "<aggregation_type>":{#定义聚合类型
          <aggregation_body>
        },
        [,"aggs":{[<sub_aggregation>]+}]? #子查询
      }
      [,"<aggregation_name_2>":{...}]*  #可以包含多个聚合分析
    }
    }

聚合分析分类

  • 为了便于理解,es将聚合分析主要分为如下四类
    • Bucket,分桶类型,类似sql中的group by语法
    • Metric,指标分析模型,如计算最大值、最小值、平均值等等
    • pipline,管道分析类型,基于上一级的聚合分析结果进行再分析
    • Matrix,矩阵分析类型

Metric聚合分析

  • 主要分如下两类:
    • 单值分析,只输出过一个分析结果
      • min,max,avg,sum
      • cardinality
    • 多值分析,输出多个分析结果
      • stats,extended stats
      • percentile,percentile rank
      • top hits
  • 返回数值类字段的最小值
    GET test_search_index/_search
    {
    "size":0,   #不需要返回文档列表
    "aggs":{
      "min_age":{
        "min":{  #关键词
          "field":"age"
        }
      }
    }
    }
  • 返回数值类字段的最大值
    GET test_search_index/_search
    {
    "size":0,
    "aggs":{
      "max_age":{
        "max":{
          "field":"age"
        }
      }
    }
    }
  • cardinality,意为集合的势,或者基数,是指不同数值的个数,类似sql中的distinct count概念
    GET test_search_index/_search
    {
    "size":0,
    "aggs":{
      "count_of_job":{
        "cardinality":{ #关键词
          "field":"job.keyword"
        }
      }
    }
    }
  • 返回一系列数值类型的统计值,包含min、max、avg、sum和count
    GET test_search_index/_search
    {
    "size":0,
    "aggs":{
      "stats_age":{
        "stats":{
            "field":"age"
        }
      }
    }
    }
  • 对stats的扩展,包含了更多的统计数据,如方差、标准差等
    GET test_search_index/_search
    {
    "size":0,
    "aggs":{
      "stats_age":{
        "extended_status":{
          "field":"age"
        }
      }
    }
    }
  • 百分位数统计percentile
    GET  test_search_index/_search
    {
    "size":0,
    "aggs":{
      "per_age":{
        "percentiles":{ #关键词
          "field":"salary"
        }
      }
    }
    }
  • top hits一般用于分桶后获取该桶内最匹配的顶部文档列表,即详情数据
    GET test_search_index/_search
    {
    "size":0,
    "aggs":{
      "jobs":{
        "terms":{
          "field":"job.keyword",
          "size":10
        },
        "aggs":{
          "top_employee":{
            "top_hits":{
              "size":10,
              "sort":[
                {
                  "age":{
                    "order":"desc"
                  }
                }
              ]
            }
          }
        }
      }
    }
    }

bucket聚合分析

  • Bucket,意为桶,即按照一定的规则将文档分配到不同的桶中,达到分类分析的目的
  • 按照Bucket的分桶策略,常见的Bucket聚合分析如下:
    • Terms
    • Range
    • Date Range
    • Histogram
    • Date Histogram

Terms

  • 该分桶策略最简单,直接按照term来分桶,如果是text类型,则按照分词后的结果分桶
    GET test_search_index/_search
    {
    "size":0,
    "aggs":{
      "jobs":{
        "terms":{  #关键词
          "field":"job.keyword",#指明term字段
          "size":5  #指定返回数目
        }
      }
    }
    }

Range

  • 通过指定数值的范围来设定分桶规则
    GET test_search_index/_search
    {
    "size":0,
    "aggs":{
      "salary_range":{
        "range":{ #关键词
          "field":"salary",\
          "ranges":[ #指定每个range的范围
            {
              "to":1000
            },
            {
              "from":1000,
              "to":2000
            }       
          ]
        }
      }
    }
    }

Date Range

  • 通过指定日期的范围来设定分桶规则
GET test_search_index/_search
{
  "size":0,
  "aggs":{
    "date_range":{
      "range":{ #关键词
        "field":"birth",
        "format":"yyyy",#指定返回结果的日期格式
        "ranges":[
          {
            "from":"1980",#指定日期,可以使用date math 
            "to":"1990"
          },
          {
            "from":"1990",
            "to":"2000"
          }
        ]
      }
    }
  }

}

historgram

  • 直方图,以固定间隔的策略来分隔数据
GET test_search_index/_search
{
    "size":0,
    "aggs":{
      "salary_hist":{
        "histogram":{ #关键词
          "field":"salary",
          "interval":5000, #指定间隔大小
          "extended_bounds":{ #指定数据范围
            "min":0,
            "max":40000
          }
        }
      }
    }
}

Date Historgram

  • 针对日期的直方图或者柱形图,是时序分析中常用的聚合分析类型
    Get test_search_index/_search
    {
    "size":0,
    "aggs":{
    "by_year":{
      "date_historgram":{ #关键词
        "field":"birth",
        "interval":"year", #指定间隔大小
        "format":"yyyy" #指定日期格式化
      }
    }
    }
    }

bucket和metric聚合分析

bucket+metric聚合分析

  • bucket聚合分析允许通过添加子分析来进一步进行分析,该子分析可以是bucket也可以是metric。这也使得es的聚合分析能力变得异常强大
  • 分桶后再分桶
    GET test_search_index/_search
    {
    "size":0,
    "aggs":{
      "jobs":{
        "terms":{
          "field":"job.keyword",
          "size":10
        },
        "aggs"
      }
    }
    }
  • 分桶后进行数据分析
    GET test_search_index/_search
    {
    "size":0,
    "aggs":{
    "jobs":{ #第一层聚合
      "terms":{
        "field":"job.keyword",
        "size":10
      },
      "aggs":{
        "salary":{ #第二层聚合
          "stats":{
            "field":"salary"
          }
        }
      }
    }
    }
    }

pipeline聚合分析

  • 针对聚合分析的结果再次进行聚合分析,而且支持链式调用,可以回答如下问题:
    • 订单月平均销售额是多少?
      POST order/_search
      {
      "size":0,
      "aggs":{
      "sales_per_month":{
      "date_histogram":{
      "field":"date",
      "interval":"month"
      },
      "aggs":{
      "sales":{
        "sum":{
          "field":"price"
        }
      }
      }
      },
      "avg_monthly_sales":{
      "avg_bucket":{
      "bucket_path":"sales_per_month>sales"
      }
      }
      }
      }
  • Pipeline的分析结果会输出到原结果中,根据输出位置的不同,分为以下两类:
    • Parent 结果内嵌到现有的聚合分析结果中
      • Derivative
      • Moving Average
      • Cumulative Sum
    • Sibling 结果与现有聚合分析结果同级
      • Max/Min/Avg/Sum Bucket
      • Stats/Extended Stats Bucket
      • Percentiles Bucket

Pipeline 聚合分析Sibling-Min Bucket

  • 找出所有Bucket中值最小的的Bucket名称和值
    GET test_search_index/_search
    {
    "size":0,
    "aggs":{
    "jobs":{
      "terms":{
        "field":"job.keyword",
        "size":10
      },
      "aggs":{
        "avg_salary":{
          "avg":{
            "field":"salary"
          }
        }
      }
    },
    "min_salary_by_job":{
      "min_bucket":{ #关键词 
        "buckets_path":"jobs>avg_salary"
      }
    }
    }
    }

Pipeline聚合分析 Sibling-Max Bucket

  • 找出所有Bucket中值最大的Bucket名称和值
    GET test_search_index/_search
    {
    "size":0,
    "aggs":{
    "jobs":{
      "terms":{
        "field":"job.keyword",
        "size":10
      },
      "aggs":{
        "avg_salary":{
          "avg":{
            "field":"salary"
          }
        }
      }
    },
    "max_salary_by_job":{
      "max_bucket":{
        "buckets_path":"jobs>avg_salary"
      }
    }
    }
    }

Pipeline 聚合分析Parent-Derivative

  • 计算Bucket值的导数
    GET test_search_index/_search
    {
    "size":0,
    "aggs":{
    "birth":{
      "date_histogram":{
        "field":"birth",
        "interval":"year",
        "min_doc_count":0
      },
      "aggs":{
        "avg_salary":{
          "avg":{
            "field":"salary"
          }
        },
        "derivative_avg_salary":{
          "derivative":{ #关键词
            "bucket_path":"avg_salary"
          }
        }
      }
    }
    }
    }

Pipeline 聚合分析Parent-Moving Average

  • 计算Bucket值的移动平均值
    GET test_search_index/_search
    {
    "size":,
    "aggs":{
    "birth":{
      "date_histogram":{
        "field":"birth",
        "interval":"year",
        "min_doc_count":0
      },
      "aggs":{
        "avg_salary":{
          "avg":{
            "field":"salary"
          }
        },
        "mavg_salary":{
          "moving_avg":{ #关键词
            "buckets_path":"avg_salary"
          }
        }
      }
    }
    }
    }

作用范围

  • es聚合分析默认作用范围是query的结果集,可以通过如下的方式改变其作用范围:
    • filter
    • post_filter
    • global
      GET test_search_index/_search
      {
      "size":0,
      "query":{
      "match":{#aggs作用域该query的结果集
      "username":"alfred"
      }
      },
      "aggs":{
      "jobs":{
      "terms":{
      "field":"job.keyword",
      "size":10
      }
      }
      }
      }

作用范围-filter

  • 为某个聚合分析设定过滤条件,从而在不更改整体语句的情况下修改了作用范围
    GET test_search_index/_search
    {
    "size":0,
    "aggs":{
    "jobs_salary_small":{
      "filter":{
        "range":{
          "salary":{
            "to":10000
          }
        }
      },
      "aggs":{
        "jobs":{
          "terms":{
            "field":"job.keyword"
          }
        }
      }
    },
    "jobs":{
      "terms":{
        "field":"job.keyword"
      }
    }
    }
    }

作用范围 - post-filter

  • 作用于文档过滤但在聚合分析后生效
    GET test_search_index/_search
    {
    "aggs":{
    "jobs":{
      "terms":{
        "field":"job.keyword"
      }
    }
    },
    "post_filter":{#过滤条件
    "match":{
      "job.keyword":"java engineer"
    }
    }
    }

作用范围-global

  • 无视query过滤条件,基于全部文档进行分析
    GET test_search_index/_search
    {
    "query":{
    "match":{
      "job.keyword":"java engineer"
    }
    },
    "aggs":{
    "java_avg_salary":{
      "avg":{
        "field":"salary"
      }
    },
    "all":{
      "global":{},
      "aggs":{
        "avg_salary":{#过滤条件
          "avg":{
            "field":"salary"
          }
        }
      }
    }
    }
    }

排序

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

数据精准度问题

terms不准确的原因

  • 数据分散在多shard上,coordinating node无法得悉数据全貌

terms不准确的解决方法

  • 设置shard数为1,消除数据分散的问题,但无法承载大数据量
  • 合理设置shard_size的大小,即每次从shard上额外多获取数据,以提升准确度
    GET test_search_index/_search
    {
    "size":0,
    "aggs":{
    "jobs":{
      "terms":{
        "field":"job.keyword",
        "size":1,
        "shard_size":10
      }
    }
    }
    }

shard_size大小的设定方法

  • terms聚合返回结果中有如下两个统计值:
    • doc_count_error_upper_bound被遗漏的term可能的最大值
    • sum_other_doc_count返回结果bucket的term外其他term的文档总数
  • 设定show_term_doc_count_error可以查看每个bucket误算的最大值
    GET test_search_index/_search
    {
    "size":0,
    "aggs":{
    "jobs":{
      "terms":{
        "field":"job.keyword",
        "size":2,
        "show_term_doc_count_error":true
      }
    }
    }
    }
  • shard_size默认大小如下:
    • shard_size=(sizex1.5)+10
  • 通过调整shard_size的大小降低doc_count_error_upper_bound来提升准确度
    • 增大了整体的计算量,从而降低了响应时间

近似统计算法

  • 追求海量数据的准确度,用作离线计算,失去了实时性
  • 追求海量数据的实时性,用作近似统计算法,失去了数据的准确度
  • 追求数据的准确度和实时性,只是作为有限的数据计算,分析的数据只是片面的
  • 在es的聚合分析中,cardinality和percentile分析使用的是近似统计算法
    • 结果是近似准确的,但不一定精准
    • 可以通过参数的调整使其结果精准,但同时也意味着更多的计算时间和更大的性能消耗

数据建模

  • 英文为data modeling,为创建数据模型的过程
  • 数据模型(Data Model)
    • 对现实世界进行抽象描述的一种工具和方法
    • 通过抽象的实体及实体之间联系的形式去描述业务规则,从而实现对现实世界的映射

数据建模的过程

  • 概念模型
    • 确定系统的核心需求和范围边界,设计实体和实体间的关系
  • 逻辑模型
    • 进一步梳理业务需求,确定每个实体的属性、关系和约束等
  • 物理模型
    • 结合具体的数据库产品,在满足业务读写性能等需求的前提下确定最终的定义
    • 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
    • c是否启用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字段属性的设定流程

  • 是何种类型
  • 是否需要检索
  • 是否需要排序和聚合分析
  • 是否需要另行存储

是何种类型

  • 字符串类型
    • 需要分词则设定为text类型,否则设定为keyword类型
  • 枚举类型
    • 基于性能考虑将其设定为keyword类型,即便该数据为整性
  • 数值类型
    • 尽量选择贴近的类型,比如byte即可表示所有数值时,即选用byte,不要用long
  • 其他类型
    • 比如布尔类型、日期、地理位置数据等

是否需要检索

  • 完全不需要检索、排序、聚合分析的字段
    • enabled设置为false
  • 不需要检索的字段
    • index设置为false
  • 需要检索的字段,可以通过如下配置设定需要的存储粒度
    • index_options结合需要设定
    • norms不需要归一化数据时关闭即可

是否需要排序和聚合分析

  • 不需要排序或者聚合分析功能
    • doc_values设定为false
    • fielddata设定为false

是否需要另行存储

  • 是否需要专门存储当前字段的数据?
    • store设定为true,即可存储该字段的原始内容(与_source中的不相关)
    • 一般结合_source的enabled设定为false时使用

关联关系处理

  • ES不擅长处理关系型数据库中的关联关系,比如文章表blog与评论表comment之间通过blog_id关联,在ES中可以通过如下两种手段变相解决
    • Nested Object
    • Parent/Child
  • ES 还提供了类似关系数据库中join的实现方式,使用join数据类型实现
    PUT blog_index_parrent_child
    {
    "mappings":{
    "doc":{
      "properties":{
        "join":{
          "type":"join",#指明类型
          "relations":{#指明父子关系
            "blog":"comment"
          }
        }
      }
    }
    }
    }

关联关系处理之Parent/Child

#创建父文档
PUT blog_index_parent_child/doc/1
{
  "title":"blog",
  "join":"blog"
}
#创建子文档
PUT blog_index_parent_child/doc/comment-1?routing=1 #指定routing值,确保父子文档在一个分片上,一般使用父文档id
{
  "comment":"comment world",
  "join":{
    "name":"comment", #指明子类型
    "parent":1 #指明父文档id
  }
}
  • 常见query语法包括如下几种:
    • parent_id返回某父文档的子文档
      GET blog_index_parent/_search
      {
      "query":{
      "parent_id":{#关键词
      "type":"comment",#指明子文档类型
      "id":"2" #指明父文档id
      }
      }
      }
    • has_child返回包含某子文档的父文档
      GET blog_index_parent/_search
      {
      "query":{
      "has_child":{#关键词
      "type":"comment",#指明子文档类型
      "query":{
      "match":{
        "comment":"world"
      }
      }
      }
      }
      }
    • has_parent返回某父文档的子文档
      GET blog_index_parent/_search
      {
      "query":{
      "has_parent":{
      "has_parent":"blog",#指定父文档类型
      "query":{#指明父文档查询条件
      "match":{
        "title":"blog"
      }
      }
      }  
      }
      }

Nested Object vs Parent/Child

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

Reindex

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

Reindex- _update_by_query

POST blog_index/_update_by_query?conflicts=proceed #如果遇到版本冲突,覆盖并继续执行
POST blog_index/_update_by_query
{
  "script":{ #更新文档的字段值
    "source":"ctx._source.likes++",
    "lang":"painless"
  },
  "query":{ #可以更新部分文档
    "term":{
      "user":"tom"
    } 
  }
}

Reindex - _reindex

POST _reindex
{
  "source":{
    "index":"blog_index"
  },
  "dest":{
    "index":"blog_new_index"
  }
}
POST _reindex
{
  "conflicts":"proceed",#冲突时覆盖并继续
  "source":{
    "index":"blog_index",
    "query":{
      "term":{
        "user":"tom"
      }
    }
  },
  "dest":{
    "index":"blog_new_index"
  }
}

Reindex - Task

  • 数据重建的时间受源索引文档规模的影响,当规模越大时,所需时间越多,此时需要通过设定url参数wait_for_completion为false来异步执行,ES以task来描述此类执行任务
  • ES提供了Task API来查看任务的执行进度和相关数据
    POST blog_index/_update_by_query?wait_for_completion=false #获取task ID
    GET _tasks/_qkdskglrfodsm(task ID)

数据建模的建议

数据模型版本管理

  • 对Mapping进行版本管理
    • 包含在代码或者以专门的文件进行管理,添加好注释,并加入Git等版本管理仓库中,方便回顾
    • 为每个增加一个metadata字段,在其中维护一些文档相关的元数据,方便对数据进行管理

防止字段过多

  • 一般字段过多的原因是由于没有高质量的数据建模导致的,比如dynamic设置为true
  • 考虑拆分多个索引来解决问题
  • 字段过多主要由以下坏处:
    • 难于维护,当字段成百上千时,基本很难有人能明确知道每个字段的含义
    • mapping的信息存储在cluster state里面,过多的字段会导致mapping过大,最终导致更新变慢
  • 通过设置index.mapping.total_fields.limit可以限定索引中最大字段数,默认是1000
  • 可以通过key/value的方式解决字段过多的问题,但并不完美
    Key/Value方式详解
  • 虽然通过这种方式可以极大地减少field数目,但也有一些明显的坏处
    • query语句复杂度飙升,且有一些可能无法实现,比如聚合分析相关的
    • 不利于在kibana中做可视化分析

集群调优建议

生产环境部署建议

  • 遵照官方建议设置所有的系统参数
  • 参见文档“setup Elasticsearch -> Important System Configuration"

    ES设置尽量简洁

  • elasticsearch.yml中尽量只写必备的参数,其他可以通过api动态设置的参数都通过api来设定
  • 随着ES版本的升级,很多网络流传的配置参数已经不再支持,因此不要随便复制别人的集群配置参数

    elasticsearch.yml中建议设定的基本参数

  • cluster.name
  • node.name
  • node.master/node.data/node.ingest
  • network.host建议显示指定为内网ip,不要偷懒直接设为0.0.0.0
  • discovery.zen.ping.unicast.hosts 设定集群其他节点地址
  • discovery.zen.minimum_master_nodes一般设定为2
  • path.data/path.log
  • 动态设定的参数有transient和persistent两种设置,前者在集群重启后会丢失,后者不会,但两种设定都会覆盖elasticsearch.yml中的配置
    PUT /_cluster/settings
    {
    "persistent":{
    "discovery.zen.minimum_master_nodes":2
    },
    "transient":{
    "indices.store.throttle.max_bytes_per_sec":"50mb"
    }
    }

jvm内存设定

  • 不要超过31GB
  • 预留一半内存给操作系统,用来做文件缓存
  • 具体大小根据该node要存储的数据量来估算,为了保证性能,在内存和数据量间有一个建议的比例
    • 搜索类项目的比例建议在1:16以内
    • 日志类项目的比例建议在1:48~1:96
  • 假设总数据量大小为1TB,3个node,1个副本,那么每个node要存储的数据量为2TB/3=666TB,即700GB左右,做20%的预留空间,每个node要存储大约850GB的数据
    • 如果是搜索类项目,每个node内存大小为850GB/16=53GB,大于31GB。31*16=496,即每个node最多存储496GB数据,所以需要至少5个node
    • 如果是日志类型项目,每个node内存大小为850GB/48=18GB,因此3个节点足够

写数据优化

写数据过程

  • refresh
  • translog
  • flush

ES写数据 - refresh

  • segment写入磁盘的过程依然很耗时,可以借助文件系统缓存的特性,先将segment在缓存中创建并开放查询来进一步提升实时性,该过程在es中称为refresh
  • 在refresh之前文档会先存储在一个buffer中,refresh时将buffer中的所有文档清空并生成segment
  • es默认每一秒执行一次refresh,因此文档的实时性被提高到一秒,这也是es被称为近实时的原因

ES写数据 - translog

  • 如果在内存中的segment还没有写入磁盘前发生了宕机,那么其中的文档就无法恢复了,如何解决这个问题?
    • es引入translog机制。写文档到buffer时,同时将该操作写入translog
    • translog文件会即时写入磁盘,6.x默认每个请求都会落盘,可以修改为每五秒写一次,这样风险便是丢失5秒内的数据,相关配置为index.translog.*
    • es启动时会检查translog文件,并从中恢复数据

ES写数据 - flush

  • flush负责将内存中的segment写入磁盘,主要做如下的工作:
    • 将translog写入磁盘
    • 将index buffer清空,其中的文档生成一个新的segment,相当于一个refresh操作
    • 更新commit point并写入磁盘
    • 执行fsync操作,将内存中的segment写入磁盘
    • 删除旧的translog文件

写性能优化

  • 目标是增大写吞吐量-EPS(Events Per Second)越高越好
  • 优化方案
    • 客户端:多线程写,批量写
    • ES:在高质量数据建模的前提下,主要是在refresh、translog和flush之间做文章

写性能优化 - refresh

  • 目标为降低refresh的频率
    • 增大refresh_interval,降低实时性,以增大一次refresh的文档数,默认是1秒,设置为-1直接禁止自动refresh
    • 增大index buffer size,参数为indices.memory.index_buffer_size(静态参数,需要设定在配置文件中),默认为10%

写性能优化 - translog

  • 目标是降低translog写磁盘的频率,从而提高写效率,但会降低容灾能力
    • index.tranlog.durability设置为async,index.translog.sync_interval设置需要的大小,比如120s,那么translog会改为每120s写一次磁盘
    • index.translog.flush_threshold_size默认为512mb,即translog超过该大小时会触发一次flush,那么调大该大小可以避免flush的发生

写性能优化 - flush

  • 目标为降低flush的次数,在6.x可优化的点不多,多为es自动完成

写性能优化 - 其他

  • 副本设置为0,写入完毕再增加
  • 合理地设计shard数,并保证shard均匀的分配在所有node上,充分利用所有node的资源
    • index.routing.allocation.total_shards_per_node限定每个索引在每个node上可分配的总主副分片数
    • 5个node,某索引有10个分片,1个副本,上述值应该设置为多少?
      • (10+10)/5=4
      • 实际要设置为5个,防止在某个node下线时,分片迁移失败的问题

读性能优化

  • 读性能主要受以下几方面影响:
    • 数据模型是否符合业务模型
    • 数据规模是否过大
    • 索引配置是否优化
    • 查询语句是否优化

读性能优化 - 数据建模

  • 高质量的数据建模是优化的基础
    • 将需要通过script脚本动态计算的值提前算好作为字段存到文档中
    • 尽量使得数据模型贴近业务模型
  • 根据不同的数据规模设定不同的sla
    • 上万条数据与上千万条数据性能肯定存在差异

读性能优化 - 索引配置调优

  • 索引配置优化主要包括如下:
    • 根据数据规模设置合理的主分片数,可以通过测试得到最适合的分片数
    • 设置合理的副本数目,不是越多越好

读性能优化 - 查询语句调优

  • 查询语句调优主要有以下几种常见手段:
    • 尽量使用filter上下文,减少算分的场景,由于filter有缓存机制,可以极大提升查询性能
    • 尽量不使用script进行字段计算或者算分排序等
    • 结合profile、explain api分析慢查询语句的症结所在,然后再去优化数据模型

如何设定shard数

  • ES的性能基本是线性扩展的,因此我们只要测出一个shard的性能指标,然后根据实际性能需求就算出需要的shard数。比如单shard写入eps是10000,而线上eps需求是50000,那么你需要五个shard(实际还要考虑副本的情况)
  • 测试一个shard的流程如下:
    • 搭建与生产环境相同配置的单节点集群
    • 设定一个单分片零副本的索引
    • 写入实际生产数据进行测试,获取写性能指标
    • 针对数据进行查询请求,获取读性能指标
  • 压测工具可以采用esrally
  • 压测的流程还是比较复杂,可以根据经验来设定。如果是搜索引擎场景,单shard大小不要超过15GB,如果是日志场景,单shard大小不要超过50GB(shard越大,查询性能越低)
  • 此时只要估算出你索引的总数据大小,然后再除以上面的单shard大小也可以得到分片数