ElasticSearch
阅读提示
建议先看题目目录,再按“概念 -> 原理 -> 场景 -> 优化”顺序复习。
每题先讲结论,再补关键机制和项目实践,回答会更稳。
本文以 Elasticsearch 8.19(自建集群) 为基准;MySQL 对比以 MySQL 8.0 为准。示例采用 Kibana Dev Tools 请求格式,未在实际集群执行。
1、ES是什么,为什么要用ES
ES是一个开源的分布式搜索和分析引擎,它被设计用于快速、灵活地搜索和分析大量数据。
- Elasticsearch提供出色的全文搜索能力,适用于需要对大量数据进行高效搜索和查询的应用场景,使用倒排索引技术,能够快速高效地执行文本搜索,支持模糊搜索等。可以用来解决使用数据库进行模糊搜索时存在的性能问题;
- Elasticsearch能够提供近实时的搜索和分析结果(写入成功不代表立即可被搜索到),适用于对数据的及时性有要求的场景;
- 支持多种数据类型,适用于处理不同结构和格式的数据,使其更灵活和通用;
和Mysql相比:
- MySQL 的查询性能取决于索引与查询方式,不能笼统认为大数据量下一定慢。
LIKE '%关键词%'通常无法通过普通 B-tree 索引进行有效的范围定位;全文检索则应比较各自的全文索引能力。 - MySQL 8.0 的全文索引支持
CHAR、VARCHAR、TEXT,也支持全文解析器:例如内置的 ngram 解析器可处理中文、日文和韩文。ES 的优势在于更丰富的分析器、相关性查询与分布式搜索能力,而不是 MySQL“不支持分词”。参见 MySQL 8.0 官方全文搜索说明 和 ngram 解析器。
2、ES索引的构建
Elasticsearch采用Rest风格API,因此其API就是一次http请求,你可以用任何工具发起http请求。
可以通过RESTful API或相关的客户端库来操作创建索引。
创建索引的请求格式:
- 请求方式:PUT
- 请求路径:/索引库名
- 请求参数:json格式:
PUT /articles
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 2
},
"mappings": {
"properties": {
"title": { "type": "text", "analyzer": "standard" },
"status": { "type": "keyword" }
}
}
}number_of_shards 是主分片数量;number_of_replicas 是每个主分片的副本数,因此这里共需分配 9 个分片副本(3 主 + 6 副)。同一主分片及其副本不能放在同一节点,至少需要 3 个符合分配条件的数据节点才能全部分配;单节点使用这组设置会留下未分配副本,集群呈 yellow。主分片数通常在创建时确定,副本数可以动态调整,不能把副本数误当作集群节点数。
text 会经过分析器生成词项,适合全文检索;keyword 适合状态等精确匹配字段。standard 并不等于面向中文的专用分词器,应按语言和检索需求选择分析器。参见 Create index API。
3、ES为啥查询这么快
- 倒排索引:Elasticsearch使用倒排索引结构,这是一种能够快速定位文档的数据结构。倒排索引通过记录每个词项(terms)出现在哪些文档中来加速搜索。相比于传统的行式数据库存储结构,倒排索引在文本搜索场景中的性能更好。
- 分布式搜索引擎:Elasticsearch是一个分布式系统,可以在多个节点上运行。它将数据分成多个分片,每个分片可以独立运行在不同的节点上。这种分布式架构允许Elasticsearch并行处理查询请求,提高了查询的速度和扩展性。
- 缓存机制:节点查询缓存可复用符合条件的 filter 上下文查询结果;分片请求缓存默认主要缓存
size: 0的请求结果,例如聚合。缓存有准入、失效和容量限制,不是所有查询都会命中。操作系统文件缓存也会影响读取速度。 - 按访问方式组织数据:JSON 是请求和文档的表示形式,本身不保证检索快。全文检索主要利用倒排索引;排序、聚合通常利用
doc_values,它在索引时构建,按文档组织字段值。默认不应为了对text排序而直接启用耗堆内存的 fielddata,可使用keyword子字段。
近实时性描述的是数据何时对搜索可见,不是查询执行速度。refresh 会使新 segment 可被搜索读取,不要求先完成一次完整的 Lucene commit;它不能等同于持久化保证。8.19 自建集群默认每秒 refresh,但默认只对最近 30 秒内收到过搜索请求的索引周期刷新,这也不是严格的一秒延迟承诺。需要写后搜索可见时,可使用 refresh=wait_for 等待刷新,避免每次写入都强制 refresh=true 产生大量小 segment。
PUT /articles/_doc/1?refresh=wait_for
{ "title": "distributed search", "status": "published" }
GET /articles/_search
{ "query": { "match": { "title": "search" } } }上例在写入成功返回后再执行搜索;wait_for 等待的是搜索可见性,不是跨 MySQL 与 ES 的事务提交。分片并行也有网络与结果归并成本,分片越多不一定越快。
官方资料:近实时搜索、refresh 参数、节点查询缓存、分片请求缓存、doc values。
4、倒排索引
传统的我们的检索是通过文章,逐个遍历找到对应关键词的位置。 而倒排索引,恰恰相反,它是从词出发,记载这个词在哪些文章中存在。
倒排索引不只是 FST,而是包含词项词典、词项定位结构和倒排列表(postings)等部分。倒排列表记录包含某词项的文档 ID,还可按字段配置记录词频、位置等信息,用于评分和短语查询。
以 Lucene 9.12 的 Lucene90 BlockTree 格式为例,词典按块存储,FST 用来索引词项前缀并定位词典块;随后从词典找到对应的 postings。FST 通过共享路径压缩词项索引,帮助快速定位,但它本身不是完整的文档倒排列表。具体文件格式随 Lucene codec 和版本变化,不能把这一实现概括为所有版本的唯一结构。
文档 1: distributed search
文档 2: search engine
↓ 分析后建立词项与文档的关系
词项 distributed → [1]
词项 search → [1, 2]
词项 engine → [2]查询 search 时先定位词项,再读取其倒排列表;多词查询可对列表求交或求并,而不必逐篇扫描全文。match 会先分析查询文本,term 则直接匹配索引中的词项,不能把两者当作相同的全文搜索方式。
参见 Lucene 9.12 BlockTree 官方说明。
5、ES中的数据和数据库同步的问题
- 同步双写:写 MySQL 后再写 ES,任意一步失败都可能产生不一致;单纯顺序调用不会获得跨系统原子性,需要补偿、重试和对账。
- 异步双写:使用 MQ 异步写 ES,削峰并降低请求链路耦合,但数据库提交与消息发送之间仍有失败窗口。可采用事务 outbox:在同一数据库事务内写业务数据和待发送事件,再由独立任务投递 MQ;消费者需支持重复消息和失败重试。
- 定时扫表:通过
updated_at等变更标记扫描增量,时效性受调度间隔影响。只用时间戳水位可能漏掉同时间记录或晚提交事务,需要重叠扫描、去重与对账;物理删除后的行无法再被扫到,需软删除标记或独立删除事件。 - 基于 Binlog 的 CDC 同步:读取已提交的数据库变更,可直接写 ES,也可经 MQ 消费后写入 ES。它仍有传输、消费和 refresh 延迟,属于最终一致性方案,需处理初始全量与增量衔接、消费位点和日志保留期。
通常以 MySQL 主键作为 ES _id,重复投递同一记录时覆盖同一文档;但固定 ID 只能避免重复文档,不能阻止旧事件覆盖新状态。还需按同一业务键保证顺序,或使用可单调比较的业务版本和 ES 外部版本控制;删除事件也应纳入顺序设计。批量写入时逐项检查 Bulk 响应的失败并重试,不能仅凭 HTTP 200 判定整批成功。
这些是同步架构的设计建议,ES API 本身不提供与 MySQL 的分布式事务。官方 API 依据:Index API 的 ID 与版本控制、Bulk API 响应与单项错误。
