(1.3)elasticsearch查询基础

【1】概念性知识

数据类型

字符串#

  • text:用于全文索引,该类型的字段将通过分词器进行分词
  • keyword:不分词,只能搜索该字段的完整的值

数值型#

  • long、integer、short、byte、double、float、half_float、scaled_float

布尔#

  • boolean

二进制#

  • binary:该类型的字段把值当做经过base64编码的字符串,默认不存储,且不可搜索

范围类型#

  1. 范围类型表示值是一个范围,而不是一个具体的值
  2. integer_range、float_range、long_range、double_range、date_range
  3. 比如age类型是integer_range,那么值可以是{"gte":20,"lte":40};搜索"term":{"age":21}可以搜索该值

日期-date#

  由于json类型没有date类型,所以es通过识别字符串是否符合format定义的格式来判断是否为date类型

  format默认为:strict_date_optional_time || epoch_millis

  格式

    "2022-01-01" "2022/01/01 12:10:30" 这种字符串格式

  从开始纪元(1970年1月1日0点)开始的毫秒数

Search API 概述

  • URI Search
    • 在URL中使用查询参数
  • Requests Body Search
    • 使用ES提供的,基于JSON格式的更加完备的 query Domain Specific Language(DSL)

指定查询的索引

image-20210416141432505

URI查询

image-20210416141557248

Request body

image-20210416141623606

查询返回结果解析

image

衡量相关性(Precision,Recall,Ranking)

information retrieval

  • Precision(查准率):尽可能的返回较少的无关文档
  • Recall(查全率):尽量返回较多的相关文档
  • Ranking:是否能够按照相关度进行排序?

image-20210416142906110

URI Search 详解

image-20210416143106395

  • q:指定查询语句,使用 Query String Syntax
  • df: 默认字段,不指定是,会对所有字段进程查询
  • Sort:排序 / from 和 size 用于分页
  • Profile 可以查看查询是如何被执行的

(1)指定字段与泛查询

​ q=title:2012 / q=2012

GET /movies/_search?q=2012&df=title  #泛查询,但默认只搜索 title字段
GET /movies/_search?q=2012  		#泛查询,对应_all,搜索所有字段
GET /movies/_search?q=title:2012 	#指定字段
GET /users4/_search?q=user:"lisi"&q=age:30 # 多字段查询
GET /movies/_search?q=title:2012
{
	"profile":"true"
}
GET /movies/_search?q=title:beautiful Mind #查找美丽心灵,在title中查询beautiful,Mind为泛查询

(2)分词与词组查询 (Term、phrase)

# 双引号括起来,就相当于PhraseQuery,整个标题这2个单词都出现过,且还要求前后顺序保持一致
	GET /movies/_search?q=title:"Beautiful Mind"  
# 查找美丽心灵,在title中查询beautiful,Mind为泛查询(每个字段都查)
	GET /movies/_search?q=title:beautiful Mind 
# 分组,Bool查询,两个term在括号中默认是 or的关系,查询 title中包含 Beautiful分词 或者 Mind 分词
	GET /movies/_search?q=title:(Beautiful Mind)

(3)分组()与引号""

  • title:(Beautiful Mind):表示查询 title 中的出现 beautiful Mind 或者 Mind 的 =》 Beautiful and Mind
  • title="Beautiful Mind":表示 "Beautiful Mind" 是一个整体,查询 title 中这2个词都出现过的,并且还要求前后顺序保持一致

具体案例见上面(2)中的代码;

(4)Bool 布尔操作

布尔操作:

​ AND / OR / NOT 或者 && / || / !

  • b必须大写
  • title:(matrix NOT reloaded)
  • 默认为 OR,如:GET /movies/_search?q=title:(Beautiful Mind) #查询 title中包含 Beautiful 或者 Mind

分组

  • +表示 must
  • -表示 must_not
  • titile:(+matrix - reloaded)

(5)范围查询 [] 与 {} (闭区间,开区间)

区间表示:[] 闭区间,{} 开区间

  • year:{2019 TO 2018}
  • year:[* TO 2018]

算数符号:

  • year:>2012
  • year:(>2010 && <=2018)
  • year(+>2012 +<=2018)

(6)通配符查询、正则、模糊匹配与近似查询

通配符查询:不推荐,很费性能

​ ?代表1个字符,*表示0个或者多个字符

  • title:mi?d
  • title:be*

正则

  • title:[bt]oy

模糊匹配与近似查询

  • title:beautifl~1 :比如 本来应该是 beautiful,但是我们少打了一个 ful 打成了 fl;用模糊查询,可以自动识别出 beautiful
  • title:"lord rings"~2:比如 lord of the rings 就会被搜索出来

Request body search(Query DSL)

将查询语句通过 http Request body 发送给 ES

(1)一般形式

image-20210416153127193

(2)分页

image-20210416153244645

  • From 默认从0开始,默认返回10个结果;
  • size 表示获取多少个结果;
  • 上图中的分页表示,从10开始,获取后面的20个结果
  • 注意,获取靠后的翻页成本较高

(3)排序

image-20210416153508949

如上图,对搜索结果进行了排序

  1. 最好在 数字类型、日期类型 字段上排序
  2. 因为对于多值类型或分析过的字段排序,系统会选一个值,无法得知该值

(4)_source filtering

_source 里面包含了该文档所有内容

image-20210416162356144

过滤:

  • 如果_source 没有存储,那么就只返回匹配的文档的元数据
  • 是 _source 支持使用通配符,如 "_source":["name*","desc*"]

(5)脚本字段(拼接、计算)

image-20210419160357020

7.11测试

image-20210419164435303

(6)query match 与 match_phrase

(1)query match

image-20210419160600582

  • 如果是直接写,如上图中的上半区,那么会是两个term 都以 or 的形式出来;即包含 Last 或者 包含 Christmas 的;

  • 下半区,可以加上操作符 operator:and 就表示是两个分词是 and ,要同时包含才行;

(2)query match_phraseimage-20210419160930367

  • 如果我们直接查,不加slop 参数,则默认是2个词要连在一起才出来,比如 aaa one love;
  • 如果我们加上 slop:1 参数,则 如上图,One I Love 也会被检索出来 (类似于 title:beautifl~1 )

(7)query string query / simple query string

(1)query string query

image-20210419161254260

  • 这里面的 AND 是逻辑符
  • 在右图中,也可以使用分组

(2)simple query string

image-20210419161345483

  • 类似 Query String,但是会忽略错误的语法,同时只支持部分语法
  • 不支持在 query 中直接使用 AMD OR NOT ,会当做字符串处理
  • Term之间的默认关系是 OR,可以指定 Operator
  • 支持 部分逻辑
    • + 替代 AND
    • | 替代 OR
    • - 替代 NOT

(8)query(exist,prefix,wildcard,regexp,ids)

  • Term query 精准匹配查询(查找号码为23的球员)
  • Exsit Query 在特定的字段中查找空值的文档(查找队名空的球员)
  • Prefix Query 查找包含带有指定前缀term的?档(查找队名以Rock开头的球员)
  • Wildcard Query 支持通配符查询,*表示任意字符,?表示任意单个字符(查找?箭队的球员)
  • Regexp Query 正则表达式查询(查找?箭队的球员)
  • Ids Query(查找id为1和2的球员),这个id为 _id 元数据

Dynamic-Mapping

(1)什么是 Mapping

Mapping 类似于数据库中的schema的定义,作用如下

  • 定义索引中的字段的名称
  • 定义字段的数据类型,例如字符串,数字,布尔....
  • 字段,倒排索引的相关配置,(Analyzed or Not Analyzed,analyzer)

Mapping会把 JSON文档映射成 Lucene 所需要的扁平格式

一个Mapping 属于一个索引的 Type

  • 每个文档都属于一个Type
  • 一个Type有一个 Mapping 定义
  • 7.0开始不需要在 Mapping 定义中指定 Type,因为有且只有一个,那就是_doc

(2)字段的数据类型

image-20210419162610662

(3)Dynamic-Mapping

  • 就是说当写入文档时,如果索引不存在,会自动创建索引;
  • 无需手动定义,ES会自动根据文档信息,推算出字段类型
  • 但有时候不一定对,比如地理位置
  • 当类型如果设置的不对,会导致一些功能无法使用,比如无法对字符串类型使用 range 查询

可以通过 GET /users/_mapping

image-20210419165724929

能否更改 Mapping 字段类型?

  • 情况1:新增加字字段
    1. Dynamic 默认为 True:新增字段可增加,数据可被索引,Mapping 也更新
    2. Dynamic 如果为 False:新增字段无法被检索,文档可被搜索,_source中也有,但Mapping 无法被更新
    3. Dynamic 设置为 Strict:不符合当前Mapping 的新文档无法被写入
  • 对易游字段,一旦已经有数据写入,就不再支持修改字段定义
    1. Lucene 实现的倒排索引,一旦生成后,就不允许修改
  • 如果希望改变字段类型,必须Reindex API 重建索引
  • 原因
    1. 如果修改了字段的数据类型,会导致已被索引的术语无法被搜索
    2. 但如果是增加新的字段,就不会有这样的影响

自定义 Mapping

(1)建议

  • 可以参考API手册,纯手写
  • 为了减少出错率,提高效率,可以按照下列步骤
    1. 创建一个临时的 index,写入一些样本数据
    2. 通过访问 GET /indexname/_mapping 获取该临时索引的动态 Mapping定义
    3. 自定义修改,达到自己想要的效果并创建自己的索引
    4. 删除临时索引

(2)控制当前字段是否被索引

index - 控制当前字段是否被索引。默认为 ture。如果设置为 false,则该字段不可被搜索,如下面代码中的 mobile 字段

PUT users5
{
	"mappings":{
		"properties":{
			"firstName":{"type":"text"},
			"lastName":{"type":"text","index_options:"offsets"},
			"mobile":{"type":"text","index":false}
		}
	}
}

(3)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 offects

Text类型,默认记录 positions,其他默认为 docs

记录的内容越多,占用的存储空间越大

配置:如(2)中的 "lastName":{"type":"text","index_options:"offsets"}

(3)null_value

如果有需要对 NULL 值实现搜索,那就要使用 null_value:"null",且只有 keyword 类型支持设置 Null_value

在mapping中设置如下:

​ "messages":{"type":"keyword","null_value":"NULL"}

(4)ES7中 copy_to 替代 _all

  • _all 在 7 中被 copy_to 所替代
  • 满足一些特定的搜索需求
  • copy_to 将字段的数值拷贝到目标字段,实现类似 _all 的作用
  • copy_to 的目标字段不出现在 _source 中
  • 如下列代码,就可以用 fullname 来搜索 这两个字段;
PUT users5
{
	"mappings":{
		"properties":{
			"firstName":{"type":"text","copy_to":"fullName"},
			"lastName":{"type":"text","copy_to":"fullName"}
		}
	}
}

(5)数组

POST users5/_doc/
{
  "firstName":["tom","tony"],
  "lastName": "jack"
}

发现结果,数组字段,依旧是 text 类型

(6)多字段类型

image-20210420173122596

(7)keyword 与 text 区别

Excat values VS Full Text

  • Exact Value: 包括数字 / 日期 / 具体一个字符串(例如 "app Store")
    • 其实就是 ES 中的 Keyword 类型,不需要分词,全内容为索引内容
  • 全文本,非结构化的全文本数据
    • ES 中的 text ,一般针对该类字段做 分词

Index-Template 和 Dynamic-Template

(1)Index-Template 介绍

  • Index Templates:帮助你设定 Mappings 和 Settings ,并且按照一定的规则,自动匹配到新创建的索引之上

    1. 模板仅在一个索引被新创建时,才会起作用。修改模板不会影响已创建的索引
    2. 你可以设定多个索引模板,这些设置会被 " merge " 在一起
    3. 你可以指定 " order " 的数值,控制 "merging " 的过程
  • 演示

PUT _template/template_default
{
  "index_patterns": ["*"],
  "order": 0
  , "version": 1
  , "settings": {
    "number_of_replicas": 1
    , "number_of_shards": 1
  }
}

PUT _template/template_test
{
  "index_patterns": ["test*"]
  , "order": 1
  , "settings": {
    "number_of_shards": 1
    , "number_of_replicas": 2
  }
  , "mappings": {
    "date_detection": false
    , "numeric_detection": true
  }
}
  • date_detection:默认情况下,是否在字符串中的日期,自动转换为日期数据类型
  • numeric_detection:默认情况下,字符串如果是纯数字字符串,是否自动转换成数字类型

(2)Index Template 的工作方式

  • 当一个索引被新建时
    1. 应用 ES 默认的 settings 和 mappings
    2. 应用 order 数值低的 Index Template 中的设定
    3. 应用 order 高的 Index Template中的设定,之前的设定会被覆盖
    4. 应用创建索引时,用户显示指定的 Settings 和 Mappings,并覆盖之前模板中的设定

(3)Index Template 演示案例

image-20210421104553185

如上图,我们发现真的应用了 template_test 模板的 mapping

如下图,我们发现真的应用了 template_test 模板的 setting

image-20210421104653792

疑惑:为什么不应用 template_default 模板 呢?

  1. 因为我们之前上面(2)中说了,会先应用 order 低的,再应用 order 高的,且高的配置会覆盖低的
  2. 所以,template_test 的 order 是 1 ,比 template_default 的 order 高,所以 test 开头的索引,会应用 template_test的配置,覆盖 template_default 上的配置;

(4)Dynamic Template 介绍

  • 根据 ES 识别的数据类型,结合字段名称,来动态设定字段类型

  • 比如说:

    1. 所有的字符串类型都设置成 Keyword,或者关闭 Keyword 字段
    2. is 开头的字段都设置成 boolean
    3. long_开头的都设置成 long 类型
  • 基本形式参考如下:

  • PUT test4
    {
      "mappings": {
        "dynamic_templates":[
          {
            "string_as_boolean":{
              "match_mapping_type":"string",
              "match":"is*",
              "mapping":{
                "type":"boolean"
              }
            }
          },
          {
            "string_as_keyword":{
              "match_mapping_type":"string",
              "mapping":{
                "type":"keyword"
              }
            }
          }
          ]
      }
    }
    
    PUT test4/_doc/1
    {
      "user":"zhangsan",
      "isVip":"true"
    }
    
    GET test4/_mapping
    

(5)Dynamic Template 演示案例

image-20210421111104546

如上图,证明我们配置成功!

其他案例:

DELETE test4
PUT test4
{
  "mappings": {
    "dynamic_templates":[
      {
        "full_name":{
          "path_match":"name.*",
          "path_unmatch":"*.middle",
          "mapping":{
            "type":"text",
            "copy_to":"full_name"
          }
        }
      }
      ]
  }
}

PUT test4/_doc/1
{
  "name":{
    "first":"john",
    "middle":"winston",
    "last":"lennon"
  }
}

GET test4/_search?q=full_name:lennon

【2】搜索、结构化搜索

聚合(Aggregation)的简介

(1)聚合的介绍

  • ES 除搜索外,提供的针对 ES 数据进行统计分析的功能
    1. 实时性高
    2. Hadoop(T+1):也就是说如果是 Hadoop 来分析,怕是要一整天
  • 通过聚合,我们会得到一个数据的概览,是分析和总结全套的数据,而不是寻找单个文档
    1. 尖沙咀和香港岛的可烦数量
    2. 不同的价格区间,可预订的经济型酒店和五星级酒店的数量
  • 高性能,只需要一条语句,就可以从 ES 得到分析结果
    1. 无需在客户端自己去实现分析逻辑

(2)聚合的分类

  • Bucket Aggregation:一些列满足特定条件的文档的集合
  • Metric Aggregation:一些数据运算,可以对文档字段进行统计分析
  • Pipeline Aggregation:对其他的聚合结果进行二次聚合
  • Matrix Aggregration:支持对多个字段的操作,并提供一个结果矩阵

Bucket:一组满足条件的文档:可以初步理解是关系型数据库 group by 后面的

Metric:一些系统的统计方法:可以理解成是关系型数据库中的聚合运算,如 count() max(),stats 包含 count,min,max,avg,sum

(3)Bucket 演示案例

这个就相当于 select * from group by column ,Bucket 就相当于是 group by 操作

Bucket的例子,关键词是 aggs

  • 如果使用的聚合字段是 text 类型,那么它的聚合分组是 text 分词后的数据
  • 如果使用的聚合字段是 text 类型,那么它的 mapping 设置必须开启 fielddata 参数
  • 如:
  • image-20210429093915571
  • 常规的形式如下:
GET users4/_search
{
  "size":0,
  "aggs": {
    "user_group": {
      "terms": {
        "field": "user"
        "size":3 #这里面也可以写 size,如果是3,那就去分组后,前三行
      }
    }
  }

image-20210421153305429

如下图:查询年龄大于20的 根据Job字段分组查询

image-20210429151131405

如果是text类型:

  • 如下图,第一个查询中 job 字段为 text 类型,查出的结果会根据 job 内容分词后聚合查询
  • 如下图,第二个查询中,写的是 job.keyword,这样的话,就把整个 text 类型字段的文本做为一个单位来分组聚合,就不会分词了;

image-20210429094140477

不同工作类别中,查看年纪最大的3个员工的具体信息

image-20210429095906927

(4)优化 Term 聚合查询

我们上面的信息,都可以称之为 Term 聚合查询

image-20210429100427575

  • 可以通过在 keyword 字段上把 eager_global_ordinals 参数打开
  • 这样在有新数据写入时,会把新数据的 term 加入到缓存中来,这样再做 term aggs 的时候,性能会得到提升
  • 什么时候需要打开该参数?
    1. 聚合查询非常频繁
    2. 对聚合查询操作的性能要求较高
    3. 持续不断的高频文档写入

(5)Metric 计算类聚合(max,min等)

GET users4/_search
{
  "size":0,
  "aggs": {
    "user_group": {
      "terms": {
        "field": "user"
      }
      , "aggs": {
          "max_age": {
            "max": {
              "field": "age"
            }
          },
          "avg_age":{
            "avg":{
              "field": "age"
            }
          }
        }
    }
  }
}

image-20210421162137347

还可以相互嵌套!!

(6)range 范围查询分桶

image-20210429101431032

(7)Histogram

image-20210429150140818

相当于SQL中的 where salary>=0 and salary <=100000 group by salary/5000

(8)嵌套聚合

image-20210429150350162

相当于 select max,min,avg,sum,count from tab group by job

image-20210429150421149

相当于: select max,min,avg,sum,count from tab group by job,gender

(9)Pipline: min_bucket

主要是用来在一个聚合的结果集上 再次聚合

image-20210429150809028

(10)聚合分析的原理,精准度分析

image-20210429151533872

如下的 top 操作,就结果不一定准确,因为可能某一个节点包含了

image-20210429151630466

基于Term与text的搜索

(1)基于 Term 的查询

Term 是表达语意的最小单位

特点:

  • Term Level Query / Term Query / Range Query / Exists Query / Prefix Query / Wildcard Query
  • 在ES 中,Term 查询,对输入不做分词。会将输入作为一个整体,在倒排索引中查找准确的词项,并且使用相关度算分公式,为每个包含该词项的文档进项相关度算分;
  • 可以通过 Constant Score 将查询转换成一个 Filtering ,避免算分,并利用缓存,提高性能

基本形式:

POST users4/_search
{
  "query":{
    "term": {
      "user": {
        "value": "lisi"
      }
    }
  }
}
  • 注意,大多数分词器会自动把分词信息转换为小写,如果这里写大写的 lisi ,就检索不到内容;
  • 而且,这里的 term 中的 value 已经是最小单位的一个整体,不会再拆分;

完全匹配,当做 Keyword来匹配,如下:

POST users4/_search
{
  "query":{
    "term": {
      "user.keyword": {
        "value": "lisi"
      }
    }
  }
}
  • 而且,针对与数组类型的多值字段,比如 a:["q","w"] 的字段;
  • term查询的值,是包含该值,而不是完全匹配
  • 解决方案:增加一个genre_count字段进行计数。会在组合 bool query 给出解决方案
    • image-20210425194059357

(2)基于全文的查找(match query)

其实包含:

  • Match Query / Match Phrase Query / Query String Query

特点:

  • 索引和搜索时都会进行分词,需要查询的字符串会先传递到一个合适的分词器,然后生成一个供查询的词项列表
  • 查询的时候,会先对输入的查询进行分词,然后每个词项逐个进行底层的查询,最终将结果进行合并。并为每个文档生成一个算分; - 例如查 "hello world" ,会查到 包含 hellp 或者 world 的所有结果

一般形式

GET users4/_search
{
  "query":{
    "match":{
      "user":{
        "query":"lisi wangwu"
        "operator":"OR"
        "mininum_should_match":2
      }
    }
  }
}

#多字段  multi_match
GET /customer/doc/_search/
{
  "query": {
    "multi_match": {
      "query" : "blog",
      "fields":  ["name","title"]   #只要里面一个字段包含值 blog 既可以
    }
  }
}

"mininum_should_match":2

image-20210422153017516

image-20210422153038000

(3)Match query原理

image-20210422152854916

(4)复合查询 -- Constant Score 转为 Filter

  • 将 Query 转成 Filter ,忽略 TF-IDF 计算,避免相关性 score算分的开销

  • Filter 可以有效利用缓存

    image-20210425184300092

(5)总结

  • 基于词项的查找 VS 基于全文的查找
  • 通过字段 Mapping 控制字段的分词
    • "Text" VS "Keyword"
  • 通过参数控制查询的 Precision & Recall
  • 复合查询 -- Constant Score 查询
    • 即使是对 Keyword 进行 Term 查询,同样会进行算分
    • 可以将查询转为 Filtering,取消相关性算分环节,提升性能

相关性算分

  • 相关性 - Relevance
  • 搜索的相关性算分,描述了一个文档的查询语句匹配的程序。ES 会对每个匹配查询条件的结果进行算分 _score
  • 打分的本质是排序,需要把最符合用户需求的文档排在前面。ES 5 之前,默认的相关性算分采用 TF-IDF,之后采用BM25;

(1)TF-IDF

比如这么一段文本:区块链的应用

被分为: 区块链 、 的 、 应用 ,三个 Term

  • TF(词频):Term Frequency:检索词在一篇文档中出现的评率 -- 检索词出现的次数 / 文档的总字数
    1. 度量一条查询和结果文档相关性的简单办法:简单讲搜索中每一个词的 TF 进行相加
      • TF(区块链) + TF(的) + TF(应用)
    2. Stop word
      • "的" 在文档中出现了很多次,但是对于贡献相关度几乎没有用处,所以不应该考虑他们的 TF
  • -----------------分割线 -------------------------
  • DF:检索词在所有文档中出现的频率

image-20210425190518471

评分公式:

image-20210425192028414

(2)BM 25

  • 从ES 5 开始,默认算法改为 BM 25
  • 和经典的 TF - IDF 相比,当 TF 无限增长时,BM 25 算分会趋于一个数值
  • 可以通过 explain api 查看 TF-IDF 的算分过程
    • PUT users/_search{ "explan":true , query:..... }

(3)Boosting & Bootsting query 控制查询打分 image-20210425192920402

image-20210426100950441

(4)Bool 查询

  • 一个 bool 查询,是一个或者多个查询自居的组合
    • 总共包括4种自居,其中2种会影响算分,2种不影响算分
    • 可以嵌套查询,不同层次算分情况不同,越高层次算分越高
  • 相关性算分不只是全文检索的专利。也适用于 yes|no 的自居,匹配的自居越多,相关性评分越高;
  • 如果多条查询自居被合并为一条符合查询语句,比如 bool 查询,则每个查询自居计算得出的评分会被合并到总的相关性评分中;
  1. must :必须匹配,贡献算分

  2. should:选择性匹配,贡献算分

  3. must_not:Filer Context,查询子句,必须不能匹配

  4. filter:Filter Context,必须匹配,但不算贡献分

    image-20210425193742897

【分布式】配置跨集群搜索

(1)水平扩展的痛点

  • 单集群 - 当水平扩展石,节点数不能无限增加
    • 当集群的 meta 信息(元数据信息,节点、索引、集群状态)过多,会导致更新压力变大,单个 Active Master 会成为性能瓶颈,导致整个集群无法正常工作
  • 早期版本:通过Tribe Node 可以实现多集群访问的需求,但还是存在一定的问题
    • Tribe Node 会以 Client Node 的方式加入每个集群。集群中的 Master 节点的任务变更需要 Tribe Node 的回应才能继续
    • Tribe Node 不保存 Cluster state 的信息,一旦重启,初始化很慢
    • 当多个集群存在索引重名的情况,只能设置一种 Prefer 规则
  • ES 5.3 引入了跨集群搜索的功能(Cross Cluster Search),推荐使用
    • 允许任何节点扮演 federated 节点,以轻量的方式,将搜索请求进行代理
    • 不需要以 client Node 的形式加入其它集群
  • 集群配置如下图:

image-20210427144305607

  1. 左边的是设置单个集群的主机发现信息(每个集群都需要操作设置),就是写集群包含了哪些节点,会搜索哪些节点
  2. 右边第一个是查询 cluster_one集群下的 tmdb,movies 索引
  3. 右边第二个是配置,当某个远程集群不可用了,就跳过搜索这个集群

(3)CURL 发送ES 请求

测试数据构造:

  • 在本机启动了 3个单实例 构造成 3个集群
  • 每个节点搜索

image-20210427144755407

(4)测试跨集群搜索

image-20210427164343819

【分布式】文档的分布式存储

(1)文档在集群上的存储概述

  • 单个文档直接会存在(记住是作为一个整体单位) 某一个主分片和副本分片上
  • 文档到分片的映射算法
    1. 确保文档能均匀的分部在所用的分片,充分利用硬件资源,避免部分机器空闲,部分机器繁忙
    2. 潜在的算法
      • 随机 / Round Robin。当查询文档1,分片数很多的时候,需要多次查询才可能查到文档1(因为要扫描各个分片直到找到文档1所在的分片,和全表扫描似得)
      • 维护文档到分片的映射关系,当文档数量大的时候,维护成本高
      • 实时计算,通过文档1,自动算出需要去哪个分片上获取文档

(2)文档到分片的路由算法(_routing)

  • shard = hash(_routing) % number_of_primary_shards

    1. hash 算法宝珠文档均匀分散到分片中

    2. 默认的 _routing 是文档的 id 值

    3. 可以自行制定 routing数值,例如用相同国家的商品,都分配到相同指定的 shard,例如下图:

      image-20210428145206964

    4. 设置 index Setting 后,primary 片数 不能随意修改,因为这个算法是得出的 hash值数字,%主分片数;一旦打乱则会找不到数据所在的正确分片,从而导致问题;

(3)更新、删除一个文档的过程

《1》更新一个文档 image-20210428145315898

《2》删除一个文档

image-20210428145345304

【分布式】分片原理及其生命周期

(1)分片的内部原理

  • 什么是 ES 分片?

    • ES 中最小的工作单元 / 是一个lucene 的 index
  • 一些问题:

    1. 为什么 ES 的搜索是近实时的(1S 后被搜到)
    2. ES 如何保证断电时数据也不会丢失
    3. 为什么删除文档,并不会立刻释放空间

(2)倒排索引不可变性

  • 倒排索引采用 Immutabe Design , 一旦生成,不可更改
  • 不可变性,带来的好处如下:
    1. 无需考虑并发写文件的问题,避免了锁机制带来的性能问题
    2. 一旦读入内核的文件系统缓存,便留在那里。只要文件系统存有足够的空间,大部分请求就会直接请求内存,不会命中磁盘,提升了很大的性能
    3. 缓存容易生成和维护 / 数据可以被压缩
  • 不可变更性引起的问题:如果需要让一个新的文档可以被搜索,需要重建整个索引

(3)Lucene Index(删除文档不会立即释放空间)

  • 在 Lucene 中,单个倒排索引文件被称为 Segment. Segment 是自包含的,不可变更的,多个 Segments 汇总一起,称为 Lucene 的 Index,其实对应的就是 ES 中的 Shard;
  • 当有新文档写入时,会生成新的 Segment。查询时会同时查询所有的 Segments,并且对结果汇总。Lucene 中有一个文件,用来记录所有的 Segments 信息,叫做 Commit Point
  • 删除文档的信息,保存在 .del 文件中,检索的都是会过滤掉 .del 文件中记录的 Segment
  • Segment 会定期 Merge,合并成一个,同时删除已删除文档

image-20210428152738987

(4)ES 的 Refresh(近实时)

image-20210428153003892

  • 将index buffer 写入 Segment buffer 的过程叫做 Refresh. Refresh 不执行 fsync 操作
  • Refresh 频率:默认1s 发生一次,可以通过 index.refresh_interval配置。Refresh 后,数据就可以被搜索到了;这也是为什么ES 被称为近实时搜索;
  • 如果系统有大量的数据写入,那就会产生很多的 Segment
  • Index Buffer 被占满时,会触发 Refresh 默认值是 JVM 的 10%

(5)ES 的 TransLog(断电不丢失)

image-20210428154920514

  • Segment 写入磁盘的过程相对耗时,借助文件系统缓存, Refresh 时,先将Document信息 写入Segment 缓存,以开放查询
  • 为了保证数据不会丢失,所以在 index/操作 文档时,同时写 Tranaction Log,高版本开始,Transaction Log 默认落盘,每个分片有一个 Transaction Log
  • translog 是实时 fsync 的,既写入 es 的数据,其对应的 translog 内容是实时写入磁盘的,并且是以顺序 append 文件的方式,所以写磁盘的性能很高。只要数据写入 translog 了,就能保证其原始信息已经落盘,进一步就保证了数据的可靠性。
  • 在 ES Refresh 时, Index Buffer 被清空, Transaction log 不会清空;
  • 如果断电, Segment buffer 会被清空,这个时候就根据 Transaction log 来进行恢复
  • 操作参考
index.translog.flush_threshold_ops,执行多少次操作后执行一次flush,默认无限制
index.translog.flush_threshold_size,translog的大小超过这个参数后flush,默认512mb
index.translog.flush_threshold_period,多长时间强制flush一次,默认30m
index.translog.interval,es多久去检测一次translog是否满足flush条件

index.translog.sync_interval 控制translog多久fsync到磁盘,最小为100ms
index.translog.durability translog是每5秒钟刷新一次还是每次请求都fsync,这个参数有2个取值:request(每次请求都执行fsync,es要等translog fsync到磁盘后才会返回成功)和async(默认值,translog每隔5秒钟fsync一次)

(7)ES 的 Flush

image-20210428160740138

  • ES Flush & Lucene Commit
    1. 调用 Refresh,Index Buffer 清空并且 Refresh
    2. 调用 fsync,缓存中的 Segments 写入磁盘,且写入commit point 信息
    3. 清空(删除)旧的 Transaction Log
    4. 默认30分钟调用一次
    5. Transaction Log 满(默认 512MB)
    index.translog.flush_threshold_ops,执行多少次操作后执行一次flush,默认无限制
    index.translog.flush_threshold_size,translog的大小超过这个参数后flush,默认512mb
    index.translog.flush_threshold_period,多长时间强制flush一次,默认30m
    index.translog.interval,es多久去检测一次translog是否满足flush条件
    
  • 上面的参数是es多久执行一次flush操作,在系统恢复过程中es会比较translog和segments中的数据来保证数据的完整性,为了数据安全es默认每隔5秒钟会把translog刷新(fsync)到磁盘中,也就是说系统掉电的情况下es最多会丢失5秒钟的数据,如果你对数据安全比较敏感,可以把这个间隔减小或者改为每次请求之后都把translog fsync到磁盘,但是会占用更多资源;这个间隔是通过下面2个参数来控制的:
index.translog.sync_interval 控制translog多久fsync到磁盘,最小为100ms
index.translog.durability translog是每5秒钟刷新一次还是每次请求都fsync,这个参数有2个取值:request(每次请求都执行fsync,es要等translog fsync到磁盘后才会返回成功)和async(默认值,translog每隔5秒钟fsync一次)

(8)ES 的 Merge

  • Segment 很多,需要被定期合并
    • 减少 Segments / 删除已经删除的文档
  • ES 和 Lucene 会自动进行 Merge 操作
    • 手动操作:POST my_index/_forcemerge

【分布式】分布式查询

(1)Query 阶段

  1. 用户发出搜索请求到 ES 节点(假设一共6个分配,3个 M 3个 S )
  2. ES 节点收到请求后,会以 coordinating 节点的身份,在6个中的其中3个分配,发送查询请求
  3. 被选中的分片执行查询,进行排序(算分排序)
  4. 然后每个分配都会返回 From + Size 个排序后的文档 ID 和排序值给 Coordinating 节点

image-20210428174752724

(2)Fetch

  1. Coordinating 节点将会从 Query阶段产生的数据;从每个分配获取的排序后的文档 id 列表、排序值列表,根据排序值重新排序。选取 From 到 From + Size 个文档的 Id
  2. 以 multi get 请求的方式,到想要的分片获取详细的文档数据

image-20210428175031412

(3)Query then Fetch 潜在的问题

  1. 性能问题
    • 每个分配上需要查的文档个数 = From + Size
    • 最终协调节点需要处理: number_of_shard * ( from + size )
    • 深度分页 引起的性能问题
  2. 相关性算分
    • 每个分配都 基于自己的分片上的数据 进行相关度计算。这回导致打分偏离的情况,特别是数据量很少的时候。
    • 相关性算分在分片之间是相互独立的。当文档总数很少的情况下,如果主分片大于1,主分片越多,相关性算分越不准

(4)解决算分不准的问题

  1. 数据量不大的时候,可以将主分片设置为 1
    • 当数据量足够大的时候,只要保证文档均匀的分散在各个分片上,结果一般就不会出现偏差
  2. 使用 DFS Query Then Fetch
    • 搜索的 URL 中指定参数 "_search?search_type=dfs_query_then_fetch"
    • 到每个分配,把各个分片的磁盘和文档频率进行搜集,然后网站的进行一次相关性算分,耗费很多的CPU、内存 资源,执行性能低下,一般不建议使用

sort 排序及 Doc-Values与Fielddata

(1)排序的过程

  • 排序是针对字段原始内容进行的。倒排索引无法发挥作用
  • 需要用到正牌索引。通过文档 ID 和字段快速得到字段原始内容
  • ES 有两种实现方式(排序、聚合分析等都是依靠它)
    • Fielddata (一般用于开启text类型)
      1. 默认启用,可以通过 Mapping 设置关闭(增加索引的速度 / 减少磁盘空间)
      2. 如果重新打开,需要重建索引
      3. 明确不需要做排序、聚合分析等操作时,才手动关闭
      4. 手动关闭代码: 在 _mapping 下 properties 下 字段名下 "doc_values":"false"
      5. image-20210429093859094
    • Doc Values(默认开启,列式存储,对 Text 类型无效),ES2.X之后,默认使用它;
  • 关闭 Doc values

(2)两种排序方式的对比

image-20210428190653480

分页与遍历:From-Size-Search-After-Scroll

(1)基本分页形式

image-20210428190916593

  • 默认情况下,按照相关度算分排序,返回前10条记录
  • 容易理解的分页关键词方案:
    • From :开始位置
    • Size:期望获取文档的数量

(2)分布式系统中深度分页问题

image-20210428191118715

  • 如上图,当一个查询: From = 990 , Size = 10

    1. 会在每个分配上都先获取1000个文档。

      然后在通过 Coordinating Node 聚合所有结果。最后在通过排序选取前1000和文档

    2. 页数越深,占用内存越多。为了避免深度分页带来的内存开销。ES 有一个设定,默认限定到 10000 个文档

      即 Index.max_result_window 参数

(3)Search After 避免深度分页

image-20210428192126789

  • 避免深度分页的性能问题,可以实时获取下一页的文档信息
    • 不支持指定页数( From )
    • 只能往下翻(局限性)
  • 第一步收缩需要指定 Sort,并且保证 Sort字段值是唯一的(可以如上图加上 _id 保证唯一性 )
  • 然后使用上一次,最后一个文档查询出结果的 Sort 值进行查询

如何执行?

  1. 第一次运行,不需要加入 Search_after 参数,因为不知道其 sort值是多少

image-20210428192455231

第二次执行,就要把上次查询出来的 sort值,放入 search_after 关键字中;如上图

Search After 是如何解决深度分页的问题的?

  1. 假设 Size 是 10
  2. 当查询 990 - 1000 时
  3. 通过 唯一排序值,快速利用正排索引定位文档读取位置,然后读取该位置下面的 10个文档,这样就将每次要处理的文档都控制在10

(4)Scroll 查全索引

image-20210428194211285

  • 这个时间其实指的是es把本次快照的结果缓存起来的有效时间。
    scroll 参数相当于告诉了 ES我们的search context要保持多久,后面每个 scroll 请求都会设置一个新的过期时间,以确保我们可以一直进行下一页操作。
  • 快照只能生成当前的,当你利用这个 scroll_id 一直往下滚动搜索的话,那么只能获取到使用 _search?scroll=5 的时候的快照数据,之后的数据是看不到的

(5)总结:搜索类型和使用场景

  1. 默认的 Regular:
    • 需要试试获取顶部的部分文档。例如查询最新的订单
    • 默认 from = 0 , size = 10
  2. 滚动的 Scroll:
    • 需要全部文档,例如导出全部数据
    • 使用 Scroll = 5m ,快照生成
  3. 页码的 Pagination:
    • From 和 Size
    • 如果需要深度分页,则选用 Search After

并发控制

(1)并发控制的必要性

例子:

image-20210428195854348

  • 两个程序同时更新某个文档,如上图,两个程序同时修改某个文档的字段,ES是没有锁的,所以如果不做并发控制,会导致更新丢失问题
  • 悲观并发控制
    • 嘉定有变更冲突的可能,会对资源加锁,防止冲突。例如数据库行锁
    • 但我们知道 ES 是没有锁的,所以不会使用这个
  • 乐观并发控制
    • 嘉定冲突是不会发生的,不会阻塞正在尝试的操作。如果数据在读写中被修改,更新将会失败。应用程序决定如何解决冲突,例如重试更新,使用新的数据,或将错误报告给用户
    • ES 采用的是乐观并发控制

(2)ES 的乐观并发控制

image-20210428200307013

  • ES 中的文档是不可变更的。如果你更新一个文档,会将旧文档标记为删除,同时增加一个全新的文档。同时文档的 Version 字段加1
  • 内部版本控制
    • 使用: if_seq_no + if_primary_term
    • 如:PUT products/doc/1?if_seq_no=1&if_primary_term=1
  • 使用外部版本(使用其他数据库作为主要数据存储,如从mysql同步数据到 ES )
    • version + version_type = external
    • 如:PUT products/_doc/1?version=30000&version_type=external

内部版本控制案例:

image-20210428200540238

  • 如上图,不管是查询、还是更新,还是索引操作,都会显示 _seq_no 以及 _primary_term 元数据信息
  • 然后我们根据这个值,用 PUT products/doc/1?if_seq_no=1&if_primary_term=1 格式来判断该值,在本会话查询到提交更新之后是否有其他会话并发更新、索引;

外部版本控制案例:

image-20210428201226159

  • 如上图,是指定版本的,如果版本号已经存在,则报错
  • 所以我们可以先查询,然后以查询出的 version+1 作为更新参数,如果有并发在本回话之前更新了
  • 版本号是只能更高不能更低的
    • 如:
      • session 1:GET /products/doc/1 ,获取到 version=100
      • session 2:GET /products/doc/1 ,获取到 version=100 并发操作也获取到100
      • session 2:修改 count:为 1100,提交的版本号为 version+1 即 101,操作成功
      • session 1:修改 count:为 1001,提交的版本号为 version+1 即 101,更新操作发现当前版本号>=我们本次提交的版本号 101的了,所以会报错;
    • 最终这样实现了 乐观并发控制

【总结】

(1)【match,match_phrase,query_string,term,bool查询的区别】

参考:https://blog.csdn.net/weixin_46792649/article/details/108055763
参考:http://blog.majiameng.com/article/2819.html
参考:https://www.pianshen.com/article/66431547985/
首先,我们要明白 keyword 和 text类型的区别;
《1》keyword:不参与分词 《2》text:参与分词
所以:

  1. term:某个字段,完全匹配分词

    • 精确查询,搜索前不会再对搜索词进行分词
    • 如:"term":{ "foo": "hello world" }
    • 那么只有在字段中存储了“hello world”的数据才会被返回,如果在存储时,使用了分词,原有的文本“I say hello world”会被分词进行存储,不会存在“hello world”这整个词,那么不会返回任何值。
    • 但是如果使用“hello”作为查询条件,则只要数据中包含“hello”的数据都会被返回,分词对这个查询影响较大。
  2. match_phase:完全、精准匹配

    • 查询确切的phrase,keyword需要完全匹配,text需要完全匹配(多个分词均在且顺序相同)
    • 如果换个顺序,例如:有字符串 "深圳鹏开信息技术有限公司",分词为 深圳[0] , 鹏开[1] , 信息技术[2] , 信息[3] , 技术[4] , 有限公司[5] , 有限[6] , 公司[7];
    • 1.es会先过滤掉不符合的query条件的doc:
      • 在搜索"鹏开技术信息"时,被分词成 鹏开[0] , 技术信息[1] , 技术[2] , 信息[3]
      • 很明显,技术信息这个分词在文档的倒排索引中不存在,所以被过滤掉了
    • 2.es会根据分词的position对分词进行过滤和评分,这个是就slop参数,默认是0,意思是查询分词只需要经过距离为0的转换就可以变成跟doc一样的文档数据
      • 在搜索"鹏开信息"时,如果不加上slop参数,那么在原文档的索引中,"鹏开"和"信息"这两个分词的索引分别为1和3,并不是紧邻的,中间还存在一个"信息技术"分词,很显然还需要经过1的距离,才能与搜索词相同。所以会被过滤。
      • 那么我们加上slop参数就好了 {"query":"鹏开信息","slop":1}
  3. match:某个字段,出现一个或多个分词的模糊匹配

    • 先对输入值进行分词,对分词后的结果进行查询,文档只要包含match查询条件的一部分就会被返回。
  4. query_string:多个分词逻辑操作时使用,比如 与或非

    • 语法查询,同match_phase的相同点在于,输入的查询条件会被分词,但是不同之处在与文档中的数据可以不用和query_string中的查询条件有相同的顺序。
    • 可以使用
  5. bool:多字段多条件查询

    • 参数1:must 必须匹配
    • 参数2:must_not 必须不匹配
    • 参数3:should 默认情况下,should语句一个都不要求匹配,只有一个特例:如果查询中没有must语句,那么至少要匹配一个should语句

【参考文档】

本文参考学习笔记自:阮一鸣 极客时间教程

原文地址:https://www.cnblogs.com/gered/p/14735410.html