一、ES的简单了解
(一)直面Elasticsearch
Elasticsearch(通常简写为ES)是一个开源的分布式搜索和分析引擎,它被设计用于快速、实时地搜索和分析大规模数据。以下是对Elasticsearch的简单了解:
分布式搜索引擎:Elasticsearch是一个分布式系统,它可以在多个节点上运行,允许你存储和搜索大量的数据。这使得它非常适合处理日志、文档、地理空间数据等各种类型的信息。
文本搜索:Elasticsearch是一个强大的文本搜索引擎,它可以高效地搜索和匹配文本数据。它支持全文搜索、模糊搜索、多字段搜索等功能,使得用户可以轻松地构建搜索引擎、推荐系统和数据分析应用。
实时性:Elasticsearch支持实时搜索,这意味着当你添加、更新或删除文档时,你可以立即在搜索结果中看到变化,这对于监控、日志分析和实时报告非常有用。
多种数据类型支持:Elasticsearch不仅支持文本数据搜索,还支持地理空间数据、数值数据、日期和时间数据等多种数据类型的搜索和分析。
弹性和可扩展性:Elasticsearch是弹性的,你可以根据需要添加或删除节点,以适应不断增长的数据和负载。它还具有自动分片和复制机制,以确保数据的高可用性和可扩展性。
RESTful API:Elasticsearch提供了一个易于使用的RESTful API,使得与其交互变得简单。你可以使用HTTP请求来执行各种操作,例如索引文档、执行搜索查询、管理索引和节点等。
生态系统:Elasticsearch是Elastic公司的一个产品,它是ELK(Elasticsearch、Logstash和Kibana)堆栈的一部分,用于日志收集、分析和可视化。此外,有丰富的插件和工具,可以扩展Elasticsearch的功能。
二、基本概念回顾
当使用Elasticsearch时,有一些基本概念是很重要的,下面是一些ES基本概念的回顾:
索引(Index)
概念:索引是Elasticsearch中的一个逻辑容器,用于组织和存储相关的文档数据。它类似于关系数据库中的表,但更灵活。
理解:可以将索引看作是一个大型数据集的容器,用于存储相似类型的数据。例如,你可以创建一个名为"products"的索引,用于存储产品信息。
文档(Document)
概念:文档是存储在索引中的基本数据单元,通常以JSON格式表示。每个文档代表了一个独立的数据记录。
理解:文档类似于关系数据库中的一行记录。例如,在"products"索引中,每个文档可以代表一个产品,包括产品的名称、描述、价格等信息。
字段(Field)
概念:字段是文档中的数据项,它们包含了文档的具体信息。每个字段都有一个名称和一个对应的值。
理解:在一个文档中,每个字段代表了一个属性或特征,例如,一个产品文档可以包含字段如“productName”、“productDescription”、“price”等,每个字段存储相关信息。
综合起来,可以将这些概念视为组织和存储数据的层次结构:
一个索引可以包含多个文档,这些文档代表了不同类型的数据。
每个文档包含多个字段,每个字段存储文档的具体数据。
这种层次结构的弹性和灵活性使得Elasticsearch非常适合处理大规模、异构数据,并且具有强大的搜索和分析功能。通过理解这些概念,你可以更好地组织、检索和分析你的数据
(二)映射
Mapping 定义了一个索引中每个字段的数据类型、索引方式以及查询规则,相当于 Elasticsearch 的"表结构(Schema)"。
如果你有 MySQL 基础,可以这样类比:
| MySQL | Elasticsearch |
|---|---|
| Database | Index |
| Table | (ES 7+ 已取消 Type,可理解为 Index) |
| Column | Field |
| 表结构(DDL) | Mapping |
| Row | Document |
例如,MySQL 中建表:
CREATE TABLE user ( id BIGINT, name VARCHAR(100), age INT, birthday DATE );对应到 ES,大概就是:
PUT user_index { "mappings": { "properties": { "id": { "type": "long" }, "name": { "type": "text" }, "age": { "type": "integer" }, "birthday": { "type": "date" } } } }这里的Mapping就是在告诉 ES:
- id 是 long
- name 是 text
- age 是 integer
- birthday 是 date
Mapping 最容易考的几个点
① text 和 keyword 有什么区别?
text会分词,keyword是整体
一个是全文搜索一个是精确查询
② Mapping 可以自动生成吗?
可以。
ES 默认:
Dynamic Mapping。
例如:
第一次:
{ "age":18 }ES 自动:
"age":"long"第二次:
{ "name":"Tom" }自动:
"name":"text"但是不建议这么做,因为如果由text->long 会冲突报错。
③ Mapping 能修改吗?
text 改 keywork是不行的,因为底层的倒排索引已经建好了,改意味着Segment重建,一般都是重建索引
Mapping 是 Elasticsearch 中对索引结构的定义,类似于关系型数据库中的表结构(Schema)。它不仅定义字段的数据类型,如 text、keyword、integer、date 等,还决定字段是否建立索引、采用什么分词器、是否支持排序和聚合等。不同的 Mapping 会决定底层采用不同的数据结构,例如 text 字段建立倒排索引,数值和日期字段采用 BKD Tree,排序和聚合依赖 Doc Values。因此 Mapping 不仅影响数据存储方式,也直接影响查询性能和功能。由于底层索引结构已经生成,大部分字段类型创建后不能直接修改,通常需要重新创建索引并进行 Reindex。
映射(Mapping)是Elasticsearch中的一个重要概念,它用于定义索引中文档的结构和字段的属性。映射指定了每个字段的数据类型、如何分析文本、是否存储原始数据等信息。以下是关于映射的详细介绍:
字段的数据类型:映射确定了每个字段的数据类型,例如文本、数值、日期、布尔值等。指定正确的数据类型有助于Elasticsearch正确地索引和搜索数据。
分析器(Analyzer):对于文本字段,映射可以指定使用哪种分析器来处理文本数据。分析器决定了如何将文本拆分成词条,以及如何处理这些词条,例如小写化、删除停用词等。
存储选项:映射可以指定是否将字段的原始值存储在索引中。存储原始值可以提高检索性能,但会占用更多的存储空间。
多值字段:映射允许你指定字段是否可以包含多个值,这对于数组或多选字段非常有用。
日期格式:对于日期字段,映射可以定义日期的格式,以确保正确的日期解析和排序。
自定义字段属性:你还可以在映射中定义自定义字段属性,例如字段的权重、是否可搜索、是否可排序等。
嵌套对象:映射允许你在文档中包含嵌套对象,这些对象可以具有自己的字段和映射。
映射的正确定义对于Elasticsearch的性能和数据质量非常重要。它确保了索引中的文档被正确地存储和检索,并允许执行高级的搜索和分析操作。通常,映射可以自动创建,但在需要更精细的控制时,也可以手动定义映射。
Lucene 负责“单机搜索能力”,ES 负责“把多个 Lucene 组成一个可用的分布式搜索服务”。
可以按照职责拆分:
Elasticsearch | -------------------------------- | | 分布式系统能力 搜索引擎能力 | | | | Cluster管理 Lucene Shard分配 倒排索引 副本复制 Segment 节点通信 BKD Tree REST API FST DSL解析 BM25 权限 translog 查询执行一、Lucene 负责什么?
Lucene 是搜索核心。
1. 索引构建
比如:
文档:
{ "title":"Java技术", "price":100 }Lucene负责:
text字段
分词:
Java 技术建立:
倒排索引:
Java | doc1 技术 | doc12. 倒排索引
这是 Lucene 最核心能力。
例如:
10亿篇文章。
搜索:
Java不是扫描。
而是:
Java | Posting List | docId列表快速找到文档。
3. 数值索引
例如:
{ "price":100 }Lucene:
使用:
BKD Tree支持:
- range查询
- 数值term查询
- geo查询
例如:
price 100~2004. 相关性计算
例如:
搜索:
iphone哪个文档排前面?
Lucene:
计算:
- TF-IDF(老)
- BM25(现在)
得到:
doc1 score=10.2 doc2 score=8.55. Segment管理
Lucene:
索引不是一个大文件。
而是:
Index | +-- Segment1 | +-- Segment2 | +-- Segment3每个 Segment:
类似一个不可变的小索引。
Lucene负责:
- 创建 Segment
- 查询 Segment
- Merge Segment
6. 文件存储
Lucene定义:
自己的文件格式:
例如:
.tim .tip .doc .pos .dvd .dvm管理:
- 写入
- 压缩
- 读取
ES的查询链路
你前面已经把 ES 搜索链路基本串起来了:
Mapping | Analyzer 完整的一个分词 | Tokenizer 负责把词语分开 | Token Filter 负责把分开的词语做后处理,比如转小写、去掉无意义词,同义词等等 | 倒排索引 | 召回 | BM25 | 排序同义词就在Token Filter 这一层。你现在问的已经不是“会不会用 ES”,而是在理解搜索引擎内部机制了。
Segment(段)是 Lucene 最核心的数据组织单位。
简单理解:
Segment 是 Lucene 生成的一份不可变的小型索引。一个 Lucene Index 不是一个完整的大文件,而是由多个 Segment 组成。
你可以把它理解成:
- MySQL:一个表里面有很多页(Page)
- Lucene:一个索引里面有很多 Segment
Segment(段)是 Lucene 最核心的数据组织单位。
简单理解:
Segment 是 Lucene 生成的一份不可变的小型索引。一个 Lucene Index 不是一个完整的大文件,而是由多个 Segment 组成。
你可以把它理解成:
- MySQL:一个表里面有很多页(Page)
- Lucene:一个索引里面有很多 Segment
1. 为什么需要 Segment?
因为搜索引擎面对两个矛盾:
写入要求
希望:
- 快速写入
- 顺序写磁盘
- 不频繁修改已有数据结构
查询要求
希望:
- 快速查找
- 有完整倒排索引
- 有BKD Tree
如果每写一条数据就修改整个索引:
新增一条数据 | 修改倒排索引 | 修改BKD Tree | 磁盘随机写性能会很差。
所以 Lucene 设计:
数据先积累,然后批量生成一个新的 Segment。
2. Segment生成过程
例如写入数据:
{ "id":1, "title":"Java开发" }流程:
写入 | v 内存缓冲区 | refresh | v 生成Segment生成:
Segment_1 倒排索引 BKD Tree DocValues Stored Fields继续写:
Segment_1 已有 新增数据 ↓ Segment_2于是:
Index +-------------+ | Segment_1 | +-------------+ | Segment_2 | +-------------+ | Segment_3 | +-------------+3. Segment里面有什么?
一个 Segment 里面包含很多 Lucene 数据结构:
① 倒排索引
负责文本搜索:
例如:
文档:
doc1: Java Spring doc2: Java Elasticsearch倒排:
Java doc1 doc2 Spring doc1 Elasticsearch doc2② BKD Tree
负责:
数值、地理查询。
例如:
price > 100 location within 5km③ Stored Fields
保存原始文档。
例如:
搜索命中:
doc1
返回:
{ "title":"Java Spring" }这个数据来自 Stored Fields。
④ DocValues
用于:
- 排序
- 聚合
例如:
按照价格排序 统计品牌数量4. Segment为什么叫不可变?
这是 Lucene 一个非常重要的设计。
Segment生成后:
不能修改。
例如:
已有:
Segment_1 doc1 doc2 doc3现在:
修改doc2:
doc2: Java 改成 Python不会直接改:
Segment_1。
而是:
Segment_1 doc2 标记删除 + Segment_2 新的doc2为什么?
因为:
如果允许修改:
需要维护:
- 倒排索引
- BKD Tree
- DocValues
代价巨大。
5. 那Segment越来越多怎么办?
比如:
持续写入:
Segment_1 Segment_2 ... Segment_1000查询:
需要查:
1000个索引。
性能下降。
所以 Lucene 有:
Segment Merge
后台合并:
Segment_1 Segment_2 Segment_3 | v Segment_big合并过程:
重新:
- 构建倒排索引
- 构建BKD Tree
- 清理删除数据
Segment Merge 是 Lucene 自动执行的,不需要业务代码手动触发。
但是它不是“每生成一个 Segment 马上合并”,而是由 Lucene 的Merge Policy(合并策略)和后台Merge Scheduler(合并调度器)控制。
1. Segment Merge 的整体流程
假设持续写入:
写入数据 | v Segment_1 继续写 | v Segment_2 继续写 | v Segment_3此时:
Lucene Index Segment_1 Segment_2 Segment_3后台线程发现:
这些 Segment 太多了,需要合并。
于是:
Segment_1 Segment_2 Segment_3 | v Segment_4生成新的大 Segment。
旧 Segment 删除。
2. 谁决定什么时候合并?
两个核心组件:
① Merge Policy(合并策略)
负责决定:
哪些 Segment 应该合并?
Lucene 默认:
TieredMergePolicy
它主要考虑:
- Segment数量
- Segment大小
- Segment之间大小差异
例如:
现在:
Segment A 10MB Segment B 12MB Segment C 15MB Segment D 500MB可能:
优先:
A+B+C而不是:
C+D因为小 Segment 合并收益更高。
② Merge Scheduler
负责:
什么时候执行合并?用几个线程执行?
例如:
后台:
Merge Thread | | 读取多个Segment | | 生成新Segment3. Merge 是不是实时发生?
不是。
例如:
大量写入:
Segment_1 Segment_2 Segment_3 ... Segment_100可能短时间存在。
原因:
Merge 本身也消耗资源:
- CPU
- IO
- 磁盘空间
如果疯狂merge:
写入性能会下降。
所以 Lucene 会控制节奏。
4. Merge 会影响查询吗?
会,但是影响可控。
例如:
正在查询:
Segment_1 Segment_2 Segment_3后台开始merge:
Segment_1 + Segment_2 生成 Segment_4查询不会被阻塞。
因为:
Segment是不可变的。
旧查询继续读:
Segment_1 Segment_2新查询:
可能读:
Segment_4 Segment_35. Merge过程会不会丢数据?
不会。
因为:
流程:
旧Segment 读取数据 ↓ 生成新Segment ↓ 校验成功 ↓ 替换引用 ↓ 删除旧Segment不是:
先删除再生成。
6. ES里面可以控制吗?
可以。
例如:
查看merge情况
GET _cat/segments查看:
- segment数量
- 大小
手动触发
ES提供:
POST index/_forcemerge例如:
POST my_index/_forcemerge?max_num_segments=1强制合并。
但是:
生产环境慎用。
因为:
merge非常耗资源。
KD Tree(K-Dimensional Tree,K维树)是一种用于多维空间数据检索的数据结构,主要解决:
在多个维度的数据中,如何快速查找附近的点、范围内的点。
它和你前面问的BKD Tree有直接关系:BKD Tree 可以理解为 KD Tree 面向磁盘、大数据场景的工程化改进版本。
1. KD Tree解决什么问题?
假设有二维数据:
城市坐标: 北京 (116,39) 上海 (121,31) 广州 (113,23) 深圳 (114,22)如果我要查询:
距离点 (115,30) 最近的城市?
暴力:
遍历所有点:
北京 上海 广州 深圳计算距离。
复杂度:
O(N)
数据多了很慢。
KD Tree:
把空间组织起来。
查询:
不用访问所有点。
2. KD Tree的核心思想
一句话:
不断选择一个维度,把空间一分为二。
例如二维:
维度:
X轴 Y轴假设数据:
y | A | | -----------x | |第一次:
按照 X 轴切:
x < 100 x >=100分左右。
然后下一层:
按照 Y 轴切。
再下一层:
继续 X。
所以:
维度轮流切。
3. KD Tree结构长什么样?
例如二维:
数据:
(3,6) (17,15) (13,15) (6,12) (9,1)构建:
第一层:
按 X:
(9,1) / \ x<9 x>9第二层:
按 Y:
(9,1) / \ (6,12) (17,15)类似二叉树。
4. KD Tree查询怎么做?
假设:
找:
距离(10,10)最近的点先走:
因为:
当前节点:
(9,1)比较:
x:
10 > 9所以:
优先走右边。
但是:
不能只走右边。
为什么?
因为:
左边可能也有更近的点。
所以:
需要判断:
另一边有没有可能更优。
这就是:
剪枝(Pruning)
核心:
如果另一边空间距离已经不可能更近:
直接舍弃。
否则:
继续搜索。
5. KD Tree为什么适合多维?
因为它利用:
空间划分。
例如二维:
全部空间 ---------------- 左半区 | 右半区 继续切: 上 下最终:
每个节点代表:
一个空间区域。
查询时:
排除大量区域。
6. KD Tree的问题
KD Tree 理论很好。
但是:
实际大规模数据有问题。
BKD树 就是非点状的,而是一个点存很多数据
为什么KD树要做成二叉的,这样查询效率应该比多叉的慢吧
这是一个非常好的问题。你的直觉是对的:
如果只从“树的高度”来看,二叉树确实比多叉树更深,单次查找路径更长。
但是 KD Tree 选择二叉,不是因为二叉一定最快,而是因为它解决的是多维空间划分问题,二叉划分更符合它的数学和工程需求。
ES 查询快 ≠ BKD Tree 比 B+Tree 快。
实际上,ES 快的原因绝大部分不是 BKD Tree,而是倒排索引(Inverted Index)。BKD Tree 只是负责数值、日期、地理位置等字段的查询。
如果按照对 ES 查询性能的贡献排序,大概可以理解成:
倒排索引(70%~80%) ↓ Lucene Segment 不可变设计 ↓ 缓存(Query Cache、Request Cache、OS Page Cache) ↓ 分布式并行查询(多个 Shard) ↓ BKD Tree(数值/范围查询)所以很多人学 ES,一直在研究 BKD Tree,其实真正让 ES 牛的是倒排索引。
全文检索能力真正强的是 Lucene,而不是 Elasticsearch。ES 的全文检索能力,本质上就是 Lucene 提供的。
Lucene 解决的是"如何搜索",ES 解决的是"如何在生产环境大规模使用搜索"。
举个例子
假设你只有一台机器。
100 万篇文档。
用 Lucene:
IndexWriter ↓ 建立倒排索引 ↓ IndexSearcher ↓ 查询查询速度:
和 ES几乎没有区别。
为什么?
因为:
ES 底层:
最后也是:
IndexSearcher.search(...)也是 Lucene 在查。
那为什么大家不用 Lucene,而用 ES?
因为 Lucene 太底层了。
Lucene 更像:
JDBCES 更像:
MyBatis或者:
Netty和:
Spring Boot不是一个层级。
Lucene 只有搜索库
你自己要负责:
建立索引 打开索引 关闭索引 更新索引 Merge Commit Searcher 缓存 线程池全部自己写。
例如:
Directory directory = FSDirectory.open(...); IndexWriter writer = ... writer.addDocument(...) writer.commit() DirectoryReader.open(...)都是代码。