Elasticsearch 原理与用法
一、倒排索引
是什么?
Term(词项)→ 包含该 Term 的文档 ID 列表。写入时对文档分词,建立「词 → 文档」的关联,查询时直接按词定位文档。
创建流程
- 建立文档列表,每个文档有唯一 ID
- 分词器对文档分词,生成
<词语, 文档 ID>数据 - 以词语为索引关键字,记录词与文档的对应关系(可附带位置、词频,用于高亮和排序)
检索流程
- 根据分词查找对应的文档 ID
- 根据文档 ID 找到文档
由什么组成?
- 单词字典(Term Dictionary):存储单词列表,一般用 B+Tree 或 Hash 拉链法,提高查询效率
- 倒排列表(Posting List) :
DocID:文档 IDTF:词频Position:词在文档中的位置,用于检索Offset:字符偏移量,用于高亮显示
倒排索引 vs 正排索引
| 正排索引 | 倒排索引 | |
|---|---|---|
| 关联方向 | 文档 ID → 分词 | 分词 → 文档 ID |
| 优点 | 维护成本低,新增数据只需在正排表末尾加 ID->内容 | 查询效率高 |
| 缺点 | 查询要扫描所有词逐一比较,效率低 | 建立成本高;文档每次更新都要重建;有精度问题(dogs 和 dog 想匹配一致需要合适的分词器) |
可以针对某些字段不做索引吗?
可以。设 index: false 后该字段不能被检索(但结果中仍会返回),能节省存储空间。
二、写入流程(NRT)
1. 核心概念
- refresh :默认每 1 秒执行一次,将内存 Buffer 中的数据写入 Lucene 段,使数据可被搜索。这就是 ES 被称为 Near Real-Time(NRT) 的原因。
- translog:预写日志(WAL),每次写入同时记录 translog,防止宕机丢数据。
- flush:将 Lucene 段持久化到磁盘,并清空 translog。默认每 30 分钟或 translog 达到阈值时触发。
2. 完整写入流程(了解)
- 客户端发送写请求到协调节点。
- 协调节点根据路由规则找到对应的主分片。
- 主分片将数据写入内存 Buffer,同时写入 translog。
- 主分片将请求同步到副本分片。
- 默认每 1 秒执行 refresh,将 Buffer 中的数据生成新的 Lucene 段,此时数据可被搜索。
- 定期执行 flush,将段落盘,清空 translog。
3. 为什么是 NRT
- 数据写入后不会立即被搜索到,需要等待 refresh(默认 1 秒)。
- 所以 ES 是"近实时",不是实时。
4. 一句话
"ES 写入时先写内存 Buffer 和 translog,然后默认每秒 refresh 一次生成 Lucene 段,数据才能被搜索。translog 保证宕机不丢数据,flush 负责落盘。所以 ES 是近实时搜索。"
三、查询流程(Query Then Fetch)
1. 两阶段流程
ES 查询分为两个阶段:
第一阶段:Query
- 客户端发送查询请求到协调节点。
- 协调节点将查询广播到索引的所有分片(或相关分片)。
- 每个分片在本地执行查询,返回文档 ID 和排序值(不返回文档内容)。
- 协调节点收集所有分片的结果,进行全局排序,选出需要返回的文档 ID。
第二阶段:Fetch
- 协调节点根据选出的文档 ID,向对应的分片发送获取请求。
- 各分片返回完整的文档内容。
- 协调节点将结果返回给客户端。
2. 为什么分两阶段
- Query 阶段只返回 docId 和排序值,数据量小,减少网络传输。
- Fetch 阶段才取回完整文档,避免不必要的 IO。
- 这就是"Query Then Fetch"的由来。
3. 一句话
"ES 查询分两阶段:Query 阶段协调节点广播查询,各分片返回 docId 和排序值,协调节点合并排序;Fetch 阶段再根据 docId 去各分片取完整文档。这样设计是为了减少网络传输,先拿到 ID,再取内容。"
四、附:query 与 filter 的区别(高频)
| 维度 | query | filter |
|---|---|---|
| 是否计算评分 | 是 | 否 |
| 是否可缓存 | 否 | 是 |
| 适用场景 | 全文检索、相关性排序 | 精确匹配、范围过滤 |
| 性能 | 较低 | 较高 |
面试话术:
"filter 不计算评分,可以被缓存,所以性能更好。精确匹配、范围过滤尽量用 filter,全文检索和需要相关性排序的用 query。"
五、分词器(Analyzer)
由什么组成?
Character Filters(0 个或多个,处理原始文本,如去 HTML 标签)→ Tokenizer(1 个,按规则切词)→ Token Filters(0 个或多个,转小写、去停用词、加近义词)
IK Analyzer
- 为什么中文要装插件? ES 内置分词器按空格 / 字符切分,中文没空格,默认的 Standard 会退化成单字切分,效果很差
ik_max_word(细粒度)模式:尽可能多切。以「数据库索引可以大幅提高查询速度」为例 → 数据库 / 数据 / 索引 / 可以 / 大幅 / 提高 / 查询 / 速度ik_smart(粗粒度)模式:已切出的词不再被占用 → 数据库 / 索引 / 可以 / 大幅 / 提高 / 查询 / 速度- 最佳实践 :建索引用
ik_max_word(召回优先),查询用ik_smart(精准优先) - 搜不到 / 搜不准怎么排查? 先用
_analyze接口看分词结果,再考虑加自定义词典(扩展词 / 停用词)、同义词 - 拼音搜索 :装
elasticsearch-analysis-pinyin,做汉字与拼音互转,一般配合 IK 一起用
其余分词器知道名字即可(了解)
- ES 内置:
Standard(默认,英文转小写,中文单字切分)、Simple、Stop、Whitespace、Keyword、Fingerprint - 中文第三方:
Ansj、ICU、THULAC、Jcseg - 这些实际项目几乎不单独用
六、数据类型与 Mapping
常见数据类型
- 关键词:
keyword、constant_keyword、wildcard - 数值:
long、integer、short、byte、double、float、half_float、scaled_float - 其他:
boolean、date、date_nanos、binary、ip、version - 文本:
text、annotated-text、completion - 对象与关系:
object、flattened、nested、join - 范围:
integer_range、float_range、long_range、double_range、date_range - 空间:
geo_point、geo_shape
你说项目中都是用keyword,为什么不用text
keyword 和 text 的区别?
keyword 不走分词器,查询效率高,用于精确匹配、排序、聚合;text 会走分词器,用于全文检索。常见做法是在 fields 里给 text 字段挂一个 keyword 子字段,兼顾两种用途。
有专门的数组类型吗?
没有。默认任何字段都可以包含零个或多个值,但数组内所有值必须是同一数据类型。
字段类型能改吗?
不能。Mapping 的字段类型只能增加不能修改,要改只能 reindex 重建索引,或重新建模导入数据。
Nested 类型是什么?有什么用?
object 类型的特殊版本,允许对象数组被独立查询 。
默认情况下数组会被扁平化,多个数组字段之间会做笛卡尔积,导致查出实际不存在的数据;改用 nested 可以避免。
怎么把多个字段合并成一个?
用 copy_to。例如把 first_name 和 last_name 合并为 full_name(full_name 不会出现在查询结果里)。
什么是 Mapping?动态和显式有什么区别?
Mapping 定义字段名称、数据类型、是否索引、分词器等,一个 Index 对应一个 Mapping。
- 动态 Mapping:写入时自动推断、自动建立映射
- 显式 Mapping:手动控制字段的存储和索引方式
实际项目应尽量手动定义映射。
为什么插入数据不用指定 Mapping?
因为写入文档时如果索引不存在,ES 会根据数据类型自动推断(Dynamic Mapping),但推断结果经常不准确。
自定义 Mapping 的实操步骤
- 创建临时 Index,插入一批临时数据
- 调用 Mapping API 拿到自动生成的映射定义
- 在此基础上修改(补 keyword、nested 等)
- 删除临时 Index
动态 Mapping 有几种属性配置?
4 种,默认 true:
true:新字段被加入映射runtime:新字段作为运行时字段加入,不索引,查询时从_source加载false:忽略新字段,不索引、不可搜索strict:检测到新字段直接抛异常拒绝文档,新字段必须显式加入映射
怎么防止字段无限膨胀(映射爆炸)?
用映射限制设置,常用的几个:
index.mapping.total_fields.limit:字段总数上限,默认 1000index.mapping.depth.limit:字段最大深度,默认 20index.mapping.nested_fields.limit:nested 字段数上限,默认 50index.mapping.nested_objects.limit:单文档 nested 对象数上限,默认 10000index.mapping.field_name_length.limit:字段名最大长度,默认无限制
想让某个字段不被索引怎么做?
Mapping 中设 index: false。
相关属性 index_options 控制倒排索引记录的内容:docs(只记 DocID)→ freqs(+词频)→ positions(默认,+位置)→ offsets(+字符偏移量)。记录越多,占用空间越大,检索越精确。
七、查询 DSL
查询语句分几类?
- 请求体查询(最常用) :即 Query DSL,分叶子查询(
term、match、match_phrase)和复合查询(bool、dis_max) - 请求 URI:条件放 URL 里,不常用
- 类 SQL 检索:功能不完备,不推荐
term 查询和全文检索的区别?
term:不做分词处理,查询词与文档中的词精确匹配才命中,用于非文本字段- 全文检索:走对应分词器,流程为「分词 → 逐词查询 → 汇总得分」,用于文本字段
如何实现范围查询?
用 range,支持 gt / gte / lt / lte,可作用于数值、日期、字符串字段。只能查询单个字段,不能作用于多个字段。
match 和 match_phrase 的区别?
match:多个检索词之间默认是 or 关系,可用operator改为 andmatch_phrase:多个检索词之间默认是 and ,且词的位置关系会影响搜索结果(短语匹配)
multi_match(单条件多字段匹配) 有几种匹配策略?
best_fields(默认):包含任一条件即可,最终得分取最佳匹配字段的得分most_fields:包含任一条件即可,最终得分合并所有匹配字段,条件之间默认 orcross_fields:跨字段匹配,解决了 most_fields 查询词无法用 and 连接的问题,相当于把多个字段整合成一个字段,但不像copy_to那样占用存储
bool 有哪几种查询子句?
| 子句 | 含义 |
|---|---|
must |
必须匹配 |
should |
应该匹配一个或多个 |
must_not |
必须不能匹配 |
filter |
必须匹配,可走缓存,效率更高 |
八、总结速查表
| 概念 | 要点 |
|---|---|
| 倒排索引 | Term → DocId,全文检索快 |
| 写入流程 | Buffer + translog → refresh(1s)→ 可搜索 → flush 落盘 |
| 查询流程 | Query(取 docId)→ Fetch(取文档) |
| NRT | 近实时,默认 1 秒可见 |
| translog | 防宕机丢数据 |
| filter | 不评分、可缓存,性能优于 query |