Elasticsearch学习笔记-基于7.x

Elasticsearch学习笔记-基于7.x

什么是Elasticsearch

​ Elasticsearch 是一个开源的分布式 RESTful 搜索和分析引擎、可扩展的数据存储和向量数据库,能够解决不断涌现出的各种用例。作为 Elastic Stack (Elasticsearch + Kibana + Beats + Logstash)的核心,Elasticsearch 会集中存储您的数据,让您飞快完成搜索,微调相关性,进行强大的分析,并轻松缩放规模。

  • 分布式:Elasticsearch 是一个分布式文档存储。Elasticsearch将复杂的数据结构存储为 JSON 文档。当集群中有多个 Elasticsearch 节点时,存储的文档会分布在整个集群中,并且可以从任何节点立即访问。
  • 搜索和分析引擎:存储文档后,它会被编入索引,并可近乎实时地进行全面搜索(1 秒内)。Elasticsearch 使用一种称为倒排索引的数据结构,支持非常快速的全文搜索。倒排索引列出了任何文档中出现的每个唯一单词,并标识了每个单词出现的所有文档。
  • RESTful:Elasticsearch提供简单、一致的 REST API,用于管理集群以及索引和搜索数据。由于采用REST API,所以天生就支持多种客户端语言(Java、Python等)。

倒排索引

​ 倒排索引(Inverted index),也常被称为反向索引,是一种索引方法,被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射。它是文档检索系统中最常用的数据结构。

​ 倒排索引中有两个非常重要的概念:

  • 文档:用来搜索的数据,其中的每一条数据(例如MySQL中的一条记录)就是一个文档。
  • 词条:对文档数据利用某种分词算法得到的词语就是词条。例如:居居懿的学习笔记,就可以分为:居居懿、学习笔记、学习、笔记几个词条。

​ 正向索引与倒排索引两者形式如下:

​ 正向索引

id CONTENT 其他字段
1 居居懿的学习笔记
2 Elasticsearch学习笔记

​ 倒排索引

词条 文档id
居居懿 1
学习 1,2
学习笔记 1, 2
笔记 1, 2
Elasticsearch 2

Elasticsearch基本概念

  • 索引(Index):类型相同的文档的集合,类似于数据库中的表。

    默认情况下,Elasticsearch 会索引每个字段中的所有数据,并且每个索引字段都有专用的优化数据结构。例如,文本字段存储在倒排索引中,数字和地理字段存储在 BKD 树中。

  • 文档(Document):一条条的数据,类似数据库中的行。

    文档数据会被序列化为JSON 格式后存储在Elasticsearch中。

  • 字段(Field):JSON文档中的字段,类似数据库中的列。

  • 映射(Mapping):索引中文档的约束,例如字段类型约束,类似数据库的表结构。

  • DSL:Elasticsearch提供的JSON风格的请求语句,用来操作Elasticsearch,实现CRUD。

环境安装

安装Elasticsearch

​ 其中9300端口为Elasticsearch集群间通信的端口,9200 端口为提供REST API的端口。

1
2
3
4
5
6
7
8
9
10
11
docker run -d \
--name es \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \
-v es-data:/usr/share/elasticsearch/data \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network es-net \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:7.12.1

安装Kibana

1
2
3
4
5
6
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network es-net \
-p 5601:5601 \
kibana:7.12.1

安装分词器

​ 在前面有提到:对文档数据利用某种分词算法得到的词语就是词条,Elasticsearch自带的分词器对中文的支持不是很好,处理中文一般使用ik分词器

​ 该插件包括分析器:ik_smart, ik_max_word和标记器:ik_smart, ik_max_word。

​ 访问https://release.infinilabs.com/analysis-ik/stable/下载对应版本的分词器。

image-20241209144143705

​ 解压名后上传到数据卷位置(文件夹可随便命名)

image-20241209144854841

​ 最后重启容器即可

1
docker restart es

​ ik分词器依赖于词典进行分词,显然,旧版本的分词器自带的词典是不包含新型词汇的,ik分词器提供了词典扩展功能,可在配置文件中配置,扩展字典为新增词,停止字典为禁用词(ban)。

image-20241209150829678

image-20241209151017842

​ 在IK分词器的config目录新建一个 myext.dic

image-20241209151223868

​ 重启Elasticsearch即可实现对扩展的词进行分词。

同样方式安装拼音分词器,后续可通过该分词器实现中文自动补全。

​ 该插件包括分析器:pinyin、标记器:pinyin和标记过滤器:pinyin。

测试

​ 访问安装机器5601端口,选择Explore on my own之后,进入主页面,点击Dev tools进行测试。

image-20241209145702240

image-20241209145750391

索引(Index)操作

​ Index类似数据库表,Mapping映射类似表的结构。我们要创建Index得先学会Mapping。

​ Mapping是对索引库中文档的约束,常见的Mapping属性包括:

  • type:字段数据类型,常见的简单类型有:
    • 字符串:text(可分词的文本)、keyword(关键字,不会进行分词)
    • 数值:longintegershortbytedoublefloat
    • 布尔:boolean
    • 日期:date
    • 对象:object
  • index:是否创建索引,默认为true
  • analyzer:使用哪种分词器
  • properties:该字段的子字段

创建索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
PUT /test1
{
"mappings": {
"properties": {
"age": {
"type": "integer"
},
"email": {
"type": "keyword"
},
"name": {
"type": "object",
"properties": {
"firstName": {
"type": "keyword"
},
"lastName": {
"type": "text",
"analyzer": "ik_smart"
}
}
}
}
}
}

删除索引

1
DELETE /索引名称

增加索引字段(索引库一旦创建,无法修改mapping,只能新增)

1
2
3
4
5
6
7
8
9
PUT /索引名称/_mapping
{
"properties": {
"id": {// 新增id字段
"type": "keyword",
"index": false
}
}
}

查看索引字段

1
2
3
4
5
获取所有
GET /索引名称/_mapping

获取指定
GET /索引名称/_mapping/field/字段名

查看所有索引

1
GET _cat/indices?v

文档(Document)操作

新增文档

1
2
3
4
5
6
7
8
9
10
POST /索引名称/_doc/文档id
{
"age": 18,
"name": {
"firstName": "firstName",
"lastName" : "lastName"
},
"email": "jujuyi121925@163.com",
"id": "1"
}

删除文档

1
DELETE /索引名称/_doc/文档id

修改文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 全量修改,会先根据指定的id删除文档,然后再新增一个id一样的文档
# 如果根据id删除时,id不存在,第二步的新增也会执行,也就从修改变成了新增操作
PUT /索引名称/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
// ...
}

#局部修改,修改指定id匹配的文档中的部分字段
POST /索引名称/_update/文档id
{
"doc": {
"字段名": "新的值",
// ...
}
}

查询文档

1
GET /索引名称/_doc/文档id

DSL查询

​ Elasticsearch的查询可以分为两大类:

  • 叶子查询(Leaf query clauses):一般是在特定的字段里查询特定值,属于简单查询,很少单独使用。
  • 复合查询(Compound query clauses):以逻辑方式组合多个叶子查询或者更改叶子查询的行为方式。

​ 详细请见官方文档:

image-20241210131424792

叶子查询(部分)

​ 全部的查询

  • 全文检索查询(Full Text Queries):利用分词器对用户输入搜索条件先分词,得到词条,然后再利用倒排索引搜索词条。例如:

    • match
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    GET /索引名称/_search
    {
    "query": {
    "match": {
    "字段名": "搜索条件"
    }
    }
    }


    GET /test1/_search
    {
    "query": {
    "match": {
    "age": 18
    }
    }
    }
    • multi_match
    1
    2
    3
    4
    5
    6
    7
    8
    9
    GET /索引名称/_search
    {
    "query": {
    "multi_match": {
    "query": "搜索条件",
    "fields": ["字段名1", "字段名2"]
    }
    }
    }
  • 精确查询(Term-level queries):不对用户输入搜索条件分词,根据字段内容精确值匹配。但只能查找keyword、数值、日期、boolean类型的字段。例如:

    • ids
    • term
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    GET /索引名称/_search
    {
    "query": {
    "term": {
    "字段名": {
    "value": "搜索条件"
    }
    }
    }
    }
    • range
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    GET /索引名称/_search
    {
    "query": {
    "range": {
    "字段名": {
    "gte": 最小值,
    "lte": 最大值
    }
    }
    }
    }

    //gte:大于等于;gt:大于;lte:小于等于;lt:小于
  • 地理坐标查询:用于搜索地理位置,搜索方式很多,例如:

    • geo_bounding_box:按矩形搜索
    • geo_distance:按点和半径搜索

复合查询(部分)

​ 复合查询大致可以分为两类:

  • 第一类:基于逻辑运算组合叶子查询,实现组合条件,例如
    • bool
  • 第二类:基于某种算法修改查询时的文档相关性算分,从而改变文档排名。例如:
    • function_score
    • dis_max

bool

​ bool查询,即布尔查询。就是利用逻辑运算来组合一个或多个查询子句的组合。bool查询支持的逻辑运算有:

  • must:必须匹配每个子查询,类似“与”
  • should:选择性匹配子查询,类似“或”
  • must_not:必须不匹配,不参与算分,类似“非”
  • filter:必须匹配,不参与算分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GET /索引名称/_search
{
"query": {
"bool": {
"must": [
{"match": {"name": "手机"}}
],
"should": [
{"term": {"brand": { "value": "vivo" }}},
{"term": {"brand": { "value": "小米" }}}
],
"must_not": [
{"range": {"price": {"gte": 2500}}}
],
"filter": [
{"range": {"price": {"lte": 1000}}}
]
}
}
}

function_score

​ function score 查询中包含四部分内容:

  • 原始查询条件:query部分,基于这个条件搜索文档,并且基于BM25算法给文档打分,原始算分(query score)
  • 过滤条件:filter部分,符合该条件的文档才会重新算分
  • 算分函数:符合filter条件的文档要根据这个函数做运算,得到的函数算分(function score),有四种函数
    • weight:函数结果是常量
    • field_value_factor:以文档中的某个字段值作为函数结果
    • random_score:以随机数作为函数结果
    • script_score:自定义算分函数算法
  • 运算模式:算分函数的结果、原始查询的相关性算分,两者之间的运算方式,包括:
    • multiply:相乘
    • replace:用function score替换query score
    • 其它,例如:sum、avg、max、min

​ function score的运行流程如下:

  • 1)根据原始条件查询搜索文档,并且计算相关性算分,称为原始算分
  • 2)根据过滤条件,过滤文档
  • 3)符合过滤条件的文档,基于算分函数运算,得到函数算分,不满足的文档继续使用原始算分,并且会在结果集中
  • 4)将原始算分和函数算分基于运算模式做运算,得到最终结果,作为相关性算分

​ 因此,其中的关键点是:

  • 过滤条件:决定哪些文档的算分被修改
  • 算分函数:决定函数算分的算法
  • 运算模式:决定最终算分结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
GET /索引名称/_search
{
"query": {
"function_score": {
"query": { .... }, // 原始查询,可以是任意条件
"functions": [ // 算分函数,可以有多个
{
"filter": { // 过滤条件,满足的条件
"term": {
"age": "18"
}
},
"weight": 10 // 算分函数,满足filter的将会赋予该权值
}
],
"boost_mode": "replace" // 运算模式
}
}
}


GET /test1/_search
{
"query": {
"function_score": {
"query": {
"term": {
"age": {
"value": 18
}
}
},
"functions": [
{
"filter": {
"match": {
"email": "jujuyi121925"
}
},
"weight": 10
},
{
"filter": {
"match": {
"name.firstName": "Juju"
}
},
"random_score": {}
}
],
"boost_mode": "multiply"
}
}
}

结果集排序

​ Elasticsearch默认是根据相关度算分(_score)来排序,但是也支持自定义方式对搜索结果排序。不过分词字段无法排序,能参与排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
GET /索引名称/_search
{
"query": {
"match_all": {}
},
"sort": [
//keyword类型、数值类型、日期类型等。
{
"字段名": {
"order": "desc"
},
//地理坐标类型
"_geo_distance": {
"字段名": {
"lat": 40,
"lon": -70
},
"order": "asc"
}
}
]
}

结果集分页

​ Elasticsearch中通过修改fromsize参数来控制要返回的分页结果:

  • from:从第几个文档开始
  • size:总共查询几个文档

​ 类似于mysql中的limit ?, ?,Elasticsearch会禁止 from + size 超过10000的请求,因为在集群模式下需要聚合每个节点中的数据再排序后返回,分页越深对性能影响越大。

1
2
3
4
5
6
7
8
GET /索引名称/_search
{
"query": {
"match_all": {}
},
"from": 0, // 分页开始的位置,默认为0
"size": 10 // 每页文档数量,默认10
}

结果集高亮

​ 给搜索关键字加标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GET /索引名称/_search
{
"query": {// 必须有查询条件,而且是全文检索类型的查询条件,例如match
"match": {
"搜索字段": "搜索关键字"
}
},
"highlight": {
"fields": {
"高亮字段名称": {// 参与高亮的字段必须是text类型的字段
"pre_tags": "<em>",
"post_tags": "</em>",
required_field_match=false// 默认情况下参与高亮的字段要与搜索字段一致,可以不一致的需要设置
}
}
}
}

数据聚合

​ 聚合(aggregations)可以让我们极其方便的实现对数据的统计、分析、运算,类似于MySQL的Group By但又有所区别。例如:

  • 什么品牌的手机最受欢迎?
  • 这些手机的平均价格、最高价格、最低价格?
  • 这些手机每月的销售情况如何?

​ 实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现近实时搜索效果,各种聚合对于数据类型会有对应的要求

​ 聚合常见的有三类:

  • 桶(Bucket)聚合:用来对文档做分组,默认情况下会按照数量降序排序

  • Term:按照文档字段值分组,例如按照品牌分组、按照国家分组

  • Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组

  • 度量(Metric)聚合:用以计算一些值,比如:最大值、最小值、平均值等

  • Avg:求平均值

  • Max:求最大值

  • Min:求最小值

  • Stats:同时求max、min、avg、sum等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
GET /索引名称/_search
{
"query": {// 原始的查询,满足原始条件的文档才会进行聚合
"bool": {
"filter": [
{
"term": {
"category": "手机"
}
},
{
"range": {
"price": {
"gte": 300000
}
}
}
]
}
},
"size": 0, // 返回原始文档个数
"aggs": {// 定义聚合
"brand_agg": {// 自定义聚合名
"terms": {// 聚合类型
"field": "brand",// 聚合字段
"size": 20// 返回结果数量
},
"aggs": {// 子聚合
"stats_meric": {// 自定义聚合名
"stats": {// 聚合类型
"field": "price"
}
}
}
}
}
}
  • 管道(pipeline)聚合:其它聚合的结果为基础做进一步运算

自动补全

​ 在我们使用搜索引擎时,搜索引擎能够通过我们输入的内容给予提示,这个就叫做自动补全功能。

image-20241211143121371

​ 要想实现中文的自动补全,需要使用到之前安装过的拼音分词器,并且需要进行自定义配置(详细的各种配置请参照GitHub)。

Elasticsearch 分词器的组成部分

​ 在 Elasticsearch 中,分析文本时的流程大致如下:

  1. 字符过滤(Character Filters):首先会应用字符过滤器,对输入文本进行初步处理,可能是修正字符、删除不需要的字符、替换字符等。
  2. 分词器(Tokenizer):字符过滤器处理后,分词器会将文本拆分成一系列的词条(tokens)。
  3. 分词过滤器(Token Filters):分词器产生的词条会进一步通过分词过滤器进行处理,如去除停用词、词干提取、同义词替换等。

image-20241211150235928

​ 详细介绍如下,通过合理地结合使用字符过滤器、分词器和分词过滤器,可以根据不同的需求,优化文本的分析过程,以达到更准确的搜索效果

  1. 字符过滤器(Character Filters)

    • 功能:主要用于在文本进入分词器之前对原始文本进行预处理。它们可以对字符进行替换、删除或转换等操作。
    • 常用类型
      • HTML字符过滤器(html_strip):用来去除 HTML 标签,常用于处理包含 HTML 标签的文本数据。
      • Mapping字符过滤器(mapping):使用字符映射规则替换文本中的字符。
      • Pattern字符过滤器(pattern_replace):使用正则表达式替换文本中的字符。
  2. 分词器(Tokenizer)

    • 功能:分词器将输入文本按一定规则切分为一系列词条(tokens),这些词条是后续分析处理的基础。
    • 常用类型
      • 标准分词器(Standard Tokenizer):将文本按空格和标点符号进行拆分。
      • 字母分词器(Letter Tokenizer):按字母(a-zA-Z)划分。
      • 空格分词器(Whitespace Tokenizer):仅按空格进行拆分。
      • 关键字分词器(Keyword Tokenizer):整个输入文本作为一个单一的token,通常用于对不需要拆分的内容进行处理。
      • 正则表达式分词器(Pattern Tokenizer):根据用户提供的正则表达式对输入文本进行拆分。
      • 中文分词器(IK Analyzer):专为中文设计的分词器,使用字典、规则等方式对中文文本进行拆分。
  3. 分词过滤器(Token Filter)

    • 功能:分词过滤器用于处理分词器产生的词条,进一步优化和调整词条。常见的操作包括去除停用词、词干提取、大小写转换、同义词替换等。
    • 常用类型
      • 停用词过滤器(Stop Token Filter):去除常见的无意义的词语(如“的”、“是”、“在”)。
      • 词干过滤器(Stemmer Token Filter):将词语转换为其词干形式,如将“running”转换为“run”。
      • 小写转换过滤器(Lowercase Token Filter):将所有词条转化为小写。
      • 同义词过滤器(Synonym Token Filter):替换词条为其同义词,增强检索的匹配度。
      • 字符映射过滤器(Mapping Token Filter):根据用户提供的映射规则,替换词条中的字符。
      • 长度过滤器(Length Token Filter):根据指定的长度范围过滤词条。

自定义分词器配置

​ 在创建索引时,可以通过settings进行自定义配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
PUT /test2
{
"mappings": {// mapping部分
"properties": {
"name":{
"type": "text",
// 创建倒排索引时使用自定义分词器
"analyzer": "my_custom_analyzer",
// 检索时使用的分词器
"search_analyzer": "ik_smart"
}
}
},
"settings": {// 自定义设置
"analysis": {
"analyzer": {// 分词器设置
"my_custom_analyzer": {// 自定义分词器名称
"tokenizer": "ik_max_word",
"filter": "my_costom_pinyin"// 过滤器使用自定义过滤器
}
},
"filter": {// 过滤器设置
"my_costom_pinyin": {// 自定义过滤器名称
// 设置参考GitHub
"type": "pinyin",// 过滤类型
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true,
"none_chinese_pinyin_tokenize": false
}
}
}
}
}

// 测试自定义分词器
POST /test2/_analyze
{
"analyzer": "my_custom_analyzer",
"text": ["居居懿的学习笔记"]
}

// 检索
GET /test2/_search
{
"query": {
"match": {
"name": "jjy"
}
}
}

Completion Suggester查询

Completion Suggester提供了自动补全/按类型搜索功能。这是一个导航功能,可以在用户输入时引导用户找到相关结果,提高搜索精度。要使用此功能,补全查询的字段必须type是completion类型。

​ 该字段在文档中的值可以是一个字符串数组,也可以只是一个字符串。

​ 查询语法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
GET /索引名称/_search
{
"suggest": {
"name-suggest": {// 自定义补全名
"text": "jujuyi",// 关键字
"completion": {
"field": "name",// 字段,该字段需要是completion类型
"size": 20,
"skip_duplicates": true// 跳过重复的
}
}
}
}

​ 案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
PUT /test2
{
"mappings": {
"properties": {
"name":{
"type": "completion",
"analyzer": "my_custom_analyzer",
"search_analyzer": "ik_smart"
}
}
},
"settings": {
"analysis": {
"analyzer": {
"my_custom_analyzer": {
// 会进行分词,如果不需要分词,存入的是关键字可以使用keyword
"tokenizer": "ik_max_word",
"filter": "my_costom_pinyin"
}
},
"filter": {
"my_costom_pinyin": {
"type": "pinyin",
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true,
"none_chinese_pinyin_tokenize": false
}
}
}
}
}

POST /test2/_doc/1
{
"name":"居居懿的学习笔记"
}

GET /test2/_search
{
"suggest": {
"song-suggest": {
"text": "居居懿",
"completion": {
"field": "name",
"size": 20,
"skip_duplicates": true
}
}
}
}

数据同步(双写一致)

​ 当数据库中的数据进行改变时,Elasticsearch中存储的数据也需要进行改变,否则无法通过Elasticsearch检索到正确的结果,即需要实现Elasticsearch与数据库的双写一致,总体上可为同步双写和异步双写。

同步双写

  • 方案1:在应用程序层面,每次对数据库进行写操作时,同步地将相同的数据写入Elasticsearch。

    优点:

    • 数据一致性高,几乎可以做到实时同步。

    • 实现相对简单,逻辑清晰。

    缺点:

    • 性能开销大,因为每次写操作都需要等待Elasticsearch的响应。

    • 可能会导致单点故障,如果Elasticsearch不可用,数据库写操作也会失败。

  • 方案2:使用CDC(Change Data Capture)工具,如Canal、Debezium、Maxwell捕获数据库的变化,并将这些变化实时地推送到Elasticsearch。

    优点:

    • 实现自动化,无需手动编写同步逻辑。

    • 支持复杂的变更捕获,包括插入、更新和删除操作。

    缺点:

    • 需要额外的CDC工具和配置,增加了系统的复杂性。

    • 可能存在一定的延迟,但通常比异步双写更小。

异步双写

  • 方案1:将变更记录放入消息队列(如Kafka、RabbitMQ),然后由消费者异步地将这些变更写入Elasticsearch。

    优点:

    • 减少了对数据库写操作的影响,提高了性能。

    • 解耦了数据库和Elasticsearch,增强了系统的稳定性。

    缺点:

    • 数据可能存在短暂的不一致,直到消息被消费并写入Elasticsearch。

    • 需要额外的消息队列系统,增加了系统的复杂性。

    • 可能还需要解决消息有序性的问题

  • 方案2:定期批量同步

    优点:

    • 实现简单,不需要复杂的架构。

    • 对数据库和Elasticsearch的性能影响较小。

    缺点:

    • 数据一致性较差,可能存在较大的延迟。

    • 难以处理实时性要求较高的场景。

集群

​ 在生产环境中,Elasticsearch肯定是需要集群部署的,节点之间会通过投票选举出主节点,防止单点故障导致服务不可用,同时也为了能够存储海量数据。Elasticsearch的集群架构与Kafka类似,Elasticsearch的索引(index)会拆分为多个逻辑分片(shard),存储到多个节点,同时分片还会在不同的节点上进行备份(replica)。

​ Elasticsearch集群有一个唯一的名字标识(cluster.name),集群中的节点也是由一个名字来标识的(node.name)。可以通过配置来加入一个指定的集群。

​ 在创建索引时,可在settings中设置分片和分片副本的数量,Elasticsearch集群会自动进行故障转移和负载均衡。

1
2
3
4
5
6
7
8
9
10
PUT /索引名称
{
"mappings": {// mapping部分

},
"settings": {// 自定义设置
"number_of_shards": 3,// 分片数量,创建后无法修改
"number_of_replicas" 1// 副本数量,可修改
}
}

​ 文档在进行插入时,会根据hash算法对_routing进行哈希运算,然后再对分片数量取余,来计算文档应当插入到哪个分片当中,_routing默认为文档id,也可在插入时指定。

集群节点类型

​ Elasticsearch集群中的节点,有着类型的概念,不同类型的节点的职责不同,对于硬件资源的需求也不同。节点类型如下:

类型 配置参数 默认值 职责
master eligible node.master true 能够成为集群的master,master会管理和记录集群状态、决定分片在哪个节点、处理创建和删除索引库的请求
data node.data true 数据节点,能够存储和搜索数据
ingest node.ingest true 存储数据前进行预处理
coordinating 非上面三种类型的节点 路由请求到其他节点,并对其他节点的返回结果进行合并,然后返回

​ 文档插入流程

image-20241213135058767

​ 文档查询流程

image-20241213135208563

集群脑裂

​ 当发生网路故障时,集群中的节点无法相互通信,导致集群分裂成两个或多个独立的部分,每个部分都认为自己是主节点,从而引发数据不一致和其他问题被称为集群脑裂。为了避免发生集群脑裂,需要配置discovery.zen.minimum_master_nodes参数,设置为(master eligible节点数量 + 1)/ 2,含义是选票超过多少数量才能当选为主节点master。在Elasticsearch7.0以后,默认为该配置,因此一般不会发生脑裂问题。

写入一致性

​ Elasticsearch的一个索引可以有多个分片,每个分片可以有多个副本,在向Elasticsearch中插入数据时,会根据_routing计算数据存放在哪个分片中,副本分片是主分片的备份,用于实现容错和负载均衡,主分片与副本分片之间的数据同步会直接影响集群的性能、可用性以及一致性,需要根据业务场景进行合适的配置。

SpringBoot整合

​ Elasticsearch8.x版本改动较7.x较大,所以spring-boot-starter-data-elasticsearch的版本不能太新,否则无法连接,我图方便直接用的SpringBoot2,对应的spring-boot-starter-data-elasticsearch的版本为2.7.14,使用SpringBoot3需要进行其他配置。

​ yaml配置

1
2
3
4
spring:
elasticsearch:
# 版本更低的话在spring.elasticsearch.rest.uris
uris: http://ip地址:9200

​ 操作基于以下索引定义进行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
PUT /test1
{
"mappings": {
"properties": {
"id": {
"type": "keyword",
"index": false
},
"age": {
"type": "integer"
},
"email": {
"type": "keyword"
},
"name": {
"type": "object",
"properties": {
"firstName": {
"type": "keyword"
},
"lastName": {
"type": "text",
"analyzer": "ik_smart"
}
}
}
}
}
}

POST /test1/_doc/1
{
"id": "1",
"age": 18,
"name": {
"firstName": "firstName",
"lastName" : "lastName"
},
"email": "jujuyi121925@163.com"
}

​ User.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import lombok.Data;
import lombok.ToString;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

/**
* @Author 居居懿
* @Create 2024/12/9 18:46
* @Version 1.0
* @Description
*/
@Data
@Document(indexName = "test1", createIndex = false)// 指定索引名
@ToString
public class User {

@Id
private String id;

@Field(type = FieldType.Integer)
private Integer age;

@Field(type = FieldType.Keyword)
private String email;

@Field(type = FieldType.Object)
private Name name;
}

​ Name.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import lombok.Data;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

/**
* @Author 居居懿
* @Create 2024/12/9 18:49
* @Version 1.0
* @Description
*/
@Data
public class Name {

@Field(type = FieldType.Keyword)
private String firstName;

@Field(type = FieldType.Text, analyzer = "ik_smart")
private String lastName;

}

​ 测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import com.jujuyi.elasticsearchdemo.dao.es.User;
import com.jujuyi.elasticsearchdemo.dao.es.Name;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.elasticsearch.core.*;
import org.springframework.data.elasticsearch.core.query.Criteria;
import org.springframework.data.elasticsearch.core.query.CriteriaQuery;

@SpringBootTest
class ElasticsearchDemoApplicationTests {

@Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;

@Test
void testGet() {
User user = elasticsearchRestTemplate.get("1", User.class);

System.out.println(user);
}

@Test
void testSearch() {
Criteria criteria = new Criteria();
criteria = criteria.and("email").is("jujuyi121925@163.com");

CriteriaQuery query = CriteriaQuery.builder(criteria).build();
SearchHits<User> search = elasticsearchRestTemplate.search(query, User.class);
for (SearchHit<User> userSearchHit : search) {
System.out.println(userSearchHit.getContent());
}
}

@Test
void testSave() {

User user = new User();

user.setId("2");
user.setAge(18);
user.setEmail("jujuyi121925@163.com");
Name name = new Name();
name.setFirstName("Juju");
name.setLastName("Yi");
user.setName(name);
elasticsearchRestTemplate.save(user);

User user2 = elasticsearchRestTemplate.get("2", User.class);

System.out.println(user2);
}

}

Elasticsearch8.x变化

​ Elasticsearch 8 相较于 7.x 版本,主要的变化体现在以下几个方面:

  • 安全性:默认启用安全功能。

    ​ 在 Elasticsearch 8 中,安全功能(如身份验证、授权、加密等)被默认启用,而在 7.x 版本中这些功能默认是禁用的,需要手动配置。

    • TLS 加密:Elasticsearch 8 默认启用节点间通信加密(TLS),这意味着集群内部的通信默认是加密的。
    • 默认用户认证:Elasticsearch 8 内置了默认的 elastic 用户,并要求用户在初次启动时配置密码。
    • 内置角色和用户管理:更强大的基于角色的访问控制(RBAC)和内置角色管理。
  • 性能优化:改进集群性能、资源管理和查询执行效率。

  • 架构改进:移除类型支持和部分功能,同时简化索引结构。

    ​ 在Elasticsearch 7.15版本中就放弃对旧版本中的Java REST Client(High Level Rest Client (HLRC))的支持,替换为推荐的Java API Client。

  • 新特性:增加了更智能的索引管理、SQL 支持和 ML 功能。

  • 企业级功能:增强与 Elastic Stack 其他组件的集成,进一步强化商业支持。