ElasticSearch

简介

ElasticSearch(简称ES)

是一个开源的分布式搜索和数据分析引擎,是用Java开发并且是当前最流行的开源企业级搜索引擎,能够达到近实时搜索,专门设计用于处理大规模的文本数据和实现高性能的全文搜索

特点

  • 分布式架构:
    ElasticSearch是一个分布式系统,可以轻松的水平扩展,处理大规模的数据集和高并发的查询请求
  • 全文检索功能:
    提供了强大的全文检索功能,包括分词、词项查询、模糊匹配、多字段搜索等,并支持丰富的查询语法和过滤器
  • 多语言支持:
    支持多种语言的分词器和语言处理器,可以很好的处理不同语言的文本数据
  • 高性能:
    使用倒排索引和缓存等技术,具有快速的搜索速度和高效的查询性能
  • 实时性:
    支持实时索引和搜索,可以几乎实时地将文档添加到索引中,并立即可见
  • 易用性:
    提供了简单易用的Restful API,方便进行索引管理、查询操作和数据分析

什么是倒排索引?

正排索引和倒排索引是全文检索中常用的两种索引结构,他们在索引和搜索的过程中扮演不同的角色

  • 正排索引

    正排索引是将文档按顺序排列并进行编号的索引结构。每个文档都包含了完整的文本内容,以及其他相关的属性或元数据,如标题、作者、发布日期等。在正排索引中,可以根据文档编号或其他属性快速定位和访问文档的内容。正排索引适合用于需要对文档进行整体检索和展示的场景,但对于包含大量文本内容的数据集来说,正排索引的存储和查询效率可能会受到限制。

    在MySQL 中通过 ID 查找就是一种正排索引的应用。

  • 倒排索引

    倒排索引是根据单词或短语建立的索引结构。它将每个单词映射到包含该单词的文档列表中。倒排索引的建立过程是先对文档进行分词处理,然后记录每个单词在哪些文档中出现,以及出现的位置信息。通过倒排索引,可以根据关键词或短语快速找到包含这些词语的文档,并确定它们的相关性。倒排索引适用于在大规模文本数据中进行关键词搜索和相关性排序的场景,它能够快速定位文档,提高搜索效率。

    我们在创建文章的时候,建立一个关键词与文章的对应关系表,就可以称之为倒排索引。

Kibana

一个开源的数据分析和可视化平台,与ElasticSearch紧密集成

特性

  1. 数据可视化:通过各种图表如柱状图、折线图、饼图等)来展示 Elasticsearch 中的数据。例如,可以将网站访问日志数据在 Kibana 中以折线图的形式展示每天的访问量变化,或者以饼图展示不同来源的访问占比。
  2. 仪表板创建:用户可以创建自定义的仪表板,将多个相关的可视化图表组合在一起,以便更全面地监控和分析数据。比如,在监控服务器性能时,可以在一个仪表板中同时展示 CPU 使用率、内存使用率、磁盘 I/O 等多个指标的图表。
  3. 数据探索:方便用户对存储在 Elasticsearch 中的数据进行交互式的探索。用户可以通过简单的查询操作,快速查看和分析数据的分布、趋势等情况。例如,在一个包含用户行为数据的索引中,通过 Kibana 可以轻松地探索不同用户群体的行为模式。

环境搭建

  1. 下载ElasticSearch
    这里我是在官网下载的8.16.1版本

    官网下载连接

    1
    https://www.elastic.co/cn/downloads/elasticsearch
  2. 配置环境变量

  3. 关闭Security验证
    image-20241202174655971

  4. 启动脚本
    image-20241202174754521

  5. 访问默认端口:localhost:9200

  6. 安装Kibana

    1
    https://www.elastic.co/downloads/kibana

    下载安装Kibana - 同上解压
    修改配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    server.port: 5601

    server.host: "0.0.0.0"

    server.maxPayload: 1048576

    elasticsearch.hosts: ["http://127.0.0.1:9200"]

    elasticsearch.username: "kibana_system"
    elasticsearch.password: "vsrW2gnx+gcSLiGT9f8e"

    i18n.locale: "zh-CN"

  7. 访问http://localhost:5601/

原理

倒排索引

inverted index

结合一个例子来看

比如有一个需求从id为0,1,2的文本中找到有xiaobai的文本
image-20241203093639869

  1. 分词:对每个单词进行一个切分
    image-20241203094114078

    切分的操作叫做分词,分词后的每个部分称为一个词项(term)
    image-20241203094237151

  2. 如果文本较大的话词项就会有很多很多,如何优化时间?
    将词项按照字典序排序,使用二分查找的方法进行查找将时间复杂度优化为O(logn)
    此时,将排好序的词项称为Term Dictionary,词项对应的文档ID等信息的聚合称为Posting list
    image-20241203094558144

    Term DictionaryPosting list 共同构成了一个用于搜索的数据结构-倒排索引(inverted index)

Term Index

通过倒排索引发现一个问题,Term Dictionary数据量很大放进内存中并不现实,因此必须存放在磁盘中,但是查询磁盘是个比较缓慢的过程。想要优化就得有Term Index

  1. 我们发现一些词项的前缀是一致的,如果将部分term前缀提取出来复用,就能用更少的空间表达多个term

  2. 基于1的原理我们可以将term Dictionary的部分词项提取出来,用这些词项的前缀信息,构成一个精简的目录树。
    image-20241203095412009
    目录树的节点中存放这些词项在磁盘中的偏移量,也就是指向磁盘中的位置,这个目录树结构体积小适合放在内存中,就是我们讲的term index,用来加速搜索。

Stored Fields

前面提到的倒排索引搜索到的是文档ID,我们还需要拿着这个ID找到文档本身才能返回给用户,因此还需要有个地方存放完整的文档内容就是Stored Fields

  • 是一个行式存储结构
    image-20241203095816871

Doc Values

用户经常需要根据某个字段排序文档,比如时间、商品价格等等。而这些字段散落在文档里。
一般来说我们需要先获取stored fileds里面的文档,再提取出内部字段进行排序。下面我是Doc Values的解决办法

  1. 将散落在各个文档的某个字段集中存放
  2. 想对某个字段排序的时候,将这些几种存放的字段一次性读取出来。就能做到针对性的进行排序。
  3. 这是一个列式的存储结构,这个结构就叫做Doc Vlaues

image-20241203100213876

Segment

上面四种结构共同组成了一个复合文件就是Segment

是一个具备完整搜索功能的最小单元

Lucene

  • 我们用多个文档生成一个segment,如果新增文档时还是写入这个segment还是写入到这个segment,就得同事更新segment内部的多个数据结构,并发读写时,性能肯定会受影响。
  • 制定一个规矩,segment一旦生成则不能再被修改,如果还有新的文本就生成新的segment。这样老的segment只负责读,新的负责写同时保证读写性能
  • 但是这样segment就变多了,不知道数据在哪个segment里,虽然能并发读多个segment但segment过多总归要耗尽文件句柄
  • 可以不定期合并多个小segment,成为段合并(Segment Merge)。

通过以上的步骤生成的多个Segment就构成了一个单机文本检索库,一个很有名的开源基础搜索库Luence

image-20241203101203588

Lucene优化

高性能

多个调用方同时读写同一个Lucene,导致争抢计算资源,抢不到资源的进行等待-浪费时间。

image-20241203101336261

  1. 对写入Lucene的数据进行分类,将不同类数据写入到不同lucene中,根据需要搜索到不同的index name
    image-20241203101641922

    大大降低单个index name 的压力

  2. 但是单个index name数据仍然可能过多,可以将单个index name的同类数据拆分成好几份,m每份是一个Shard分片,每个shard分片本质上就是一个独立的Lucene库
    image-20241203101814167

  3. 将读写请求分到多个shard中,提升性能

高扩展

随着分片变多,如果这些分片都在同一个机器上就会导致单机CPU和内存过高,影响整体系统性能,我们就可以申请更多的机器,将分片分散部署在多态机器上

  1. 将分片分散部署在多台机器上,每台机器就是一个Node
  2. 增加Node,以此缓解cpu过高带来的性能问题

高可用

如果某个Node挂了,里面的所有分片就不能用了

  1. 给分片多加几个副本,将分片分为primary shard 和 replica shard (主分片和副本分片)
  2. 主分片将数据同步给副本分片,副本分片提供读操作的同时,在主分片挂了的时候升级成新主分片

Node角色分化

搜索架构需要支持的功能很多,既要负责管理集群又要储存管理数据,还要处理客户端的搜索请求,如果每个Node都支持这几个功能,当集群有数据压力需要扩容node时,就会顺带把其他能力一起扩容,如果其他能力完全够用不需要扩容就浪费了。

  1. 将功能拆开给集群里的Node赋予角色身份呢,不同角色负责不同的功能
  2. 负责管理集群的叫主节点(Master Node),负责存储管理数据的叫数据节点(Data Node),负责接受客户端搜索查询请求的叫协调节点(Coordinate Node)

去中心化

在Node中引入raft模块,在节点间互相同步数据,让所有Node看到的集群数据状态都是一致的

至此一个简陋的Lucene就成了一个高性能、高扩展性、高可用支持持久化的分布式搜索引擎—ES
image-20241203103354491

对外提供HTTP接口,任何语言的客户端都可以通过HTTP接口接入ES,实现对数据的增删改查。

写入流程

  1. 客户端发起写入请求
  2. 请求先发到Coordinate节点
  3. Coordinate根据Harsh路由判断数据应写到的数据节点和分片
  4. 住分片写入成功后,将数据写入到副本分片
  5. 副本分片写入完成后主分片会响应Coordinate一个ACK,意思是写入完成
  6. 最后协调节点响应客户端,些人完成

搜索流程

  • 查询阶段 query phase
    1. 客户端发起搜索请求
    2. 请求发到Coordinate节点
    3. Coordinate节点根据index name信息了解到index name被分为了几个分片,以及分片分散在哪个数据节点上
    4. 将请求转发到数据节点
    5. 数据节点并发搜索多个segment,获取到文档id和排序信息,将结果返回给协调节点
    6. 协调节点对拿到的数据进行排序聚合,舍弃大部分不需要的数据
  • 获取阶段 fetch phase
    1. 协调节点(Coordinate)再次拿着文档id请求数据节点里的分片
    2. 分片底层的Lucene库会从segment内的stored fileds读出完整文档内容返回给协调节点
    3. 协调节点返回给客户端

实操

ik分词器

安装

  1. 下载与elasticsearch和kibana同版本的ik分词器
  2. 放到elasticsearch的跟目录的./plugin/ik下面

使用

  1. 粗粒度切分

    1
    2
    3
    4
    5
    POST /_analyze
    {
    "text":"世界终归是你们的",
    "analyzer":"ik_smart"
    }

    image-20241203151010601

  2. 细粒度切分

    1
    2
    3
    4
    5
    POST /_analyze
    {
    "text":"世界终归是你们的",
    "analyzer":"ik_max_word"
    }

    image-20241203151101978

拓展词库

找到ik分词器目录中的config目录中的IKAnalyzer.cfg.xml文件

image-20241203151532356

  1. 里面写上要读取的文件名
    image-20241203151901081
  2. 如果文件不存在创建文件,在创建的文件中加上要停用或扩展的词

mapping属性

是对索引库中文档的约束

常见的mapping属性包括

  • type:字段数据类型,常见的类型有
    • 字符串:text(可分词的文本)、keybord(精确值,例如:品牌、国家、ip地址)
    • 数值:long、integer、short、byte、double、float
    • 布尔:boolean
    • 日期:date
    • 对象:object
  • index:是否创建索引,默认为true
  • analyzer:使用那种分词器
  • properties:该字段的子字段

索引库操作

创建索引库

ES中通过Restful请求操作索引库、文档。请求内容用DSL语句来表示。创建索引库和mapping的DSL语法如下:

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
#创建索引库
PUT /jyh
{
"mappings": {
"properties": {
"info": {
"type": "text",
"analyzer": "ik_smart"
},
"email": {
"type": "keyword",
"index": false
},
"name": {
"type": "object",
"properties": {
"firstName": {
"type": "keyword"
},
"lastName": {
"type": "keyword"
}
}
}
}
}
}

如果出现创建索引库失败,可能是磁盘上限了可以修改磁盘上限百分比

1
2
3
4
5
6
7
8
9
#磁盘上限
PUT /_cluster/settings
{
"transient": {
"cluster.routing.allocation.disk.watermark.low": "90%",
"cluster.routing.allocation.disk.watermark.high": "95%",
"cluster.info.update.interval": "1m"
}
}

查找索引库

1
GET /索引名

删除索引库

1
DELETE /索引名

修改索引库

添加了一个字段

1
2
3
4
5
6
7
8
PUT /jyh/_mapping
{
"properties": {
"age": {
"type": "integer"
}
}
}

文档操作

插入文档

1
2
3
4
5
6
7
8
9
POST /jyh/_doc/1
{
"info": "一个普通的大学生",
"email": "16937@xx.com",
"name": {
"firstName": "英浩",
"lastName": "景"
}
}

查询文档

1
GET /jyh/_doc/1

删除文档

1
DELETE /jyh/_doc/1

修改文档

  1. 全量修改,删除旧文档,添加新文档(如果id不存在直接变成一个新增操作)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    PUT /jyh/_doc/1
    {
    "info": "一个普通的大学生",
    "email": "46418@xx.com",
    "name": {
    "firstName": "英浩",
    "lastName": "景"
    }
    }
  2. 局部修改(对某个字段进行修改)

    1
    2
    3
    4
    5
    6
    POST /jyh/_update/1
    {
    "doc": {
    "email": "1@.com"
    }
    }