30. 技术专题-‌应用框架组件-搜索引擎-ES

文章目录

  • 前言
  • [一、LIKE + 索引 的局限](#一、LIKE + 索引 的局限)
  • 二、倒排索引:把组织数据的方向反过来
    • [2.1 从"文档找词"到"词找文档"](#2.1 从"文档找词"到"词找文档")
    • [2.2 光有倒排不够,快在这三个设计](#2.2 光有倒排不够,快在这三个设计)
  • 三、Elasticsearch:开箱即用的倒排索引
  • [四、上手:30 行命令建一个能用的搜索](#四、上手:30 行命令建一个能用的搜索)
    • [4.1 建索引:最容易踩坑的一步](#4.1 建索引:最容易踩坑的一步)
    • [4.2 写入和查询](#4.2 写入和查询)
    • [4.3 那个坑,现场演示一遍](#4.3 那个坑,现场演示一遍)
  • 五、一条数据写进去,到底发生了什么
  • 六、高频面试题精讲
    • [6.1 倒排索引由什么组成?](#6.1 倒排索引由什么组成?)
    • [6.2 为什么说 ES 是"近实时",而不是实时?](#6.2 为什么说 ES 是"近实时",而不是实时?)
    • [6.3 深分页怎么处理?](#6.3 深分页怎么处理?)
    • [6.4 `text` 和 `keyword` 怎么选?](#6.4 text 和 keyword 怎么选?)
    • [6.5 主分片数为什么创建后不能改?](#6.5 主分片数为什么创建后不能改?)
    • [6.6 MySQL 的数据怎么同步到 ES?](#6.6 MySQL 的数据怎么同步到 ES?)
    • [6.7 集群脑裂怎么防?](#6.7 集群脑裂怎么防?)
  • 写在最后

前言

在图书馆找一本书,有两种办法。

一种是挨个书架、逐本翻页,看哪本书里出现过"倒排索引"这四个字------这是 LIKE '%倒排索引%'。另一种是走到检索台,抽出一张卡片,上面写着"倒排索引 → 3 楼 A 区 12 架第 4 本"。前者靠体力,后者靠一张提前做好的卡片。

很多团队的全文搜索,至今还停在第一种办法上:给字段加上索引,然后写 LIKE '%关键词%',以为加了索引就万事大吉。但是当数据量变得庞大、搜索场景变得复杂时,单纯的数据库操作就无法满足需求。

接下来让我们一起看一看:LIKE + 索引有什么局限?倒排索引凭什么快?ES 怎么用?面试的时候都有哪些常见的高频题?


一、LIKE + 索引 的局限

先看一张 book 表,title 上有普通的 B+ 树索引。

-- 能走上索引:范围扫描 SELECT * FROM book WHERE title LIKE 'Java%'; -- 走不上索引:全表扫描 SELECT * FROM book WHERE title LIKE '%Java%'; -- 走不上索引:全表扫描 SELECT * FROM book WHERE title LIKE '%Java';

区别只有一个符号的位置,结果天差地别。

写法 能否用上 B+ 树索引 原因
title = 'Java' 能,精确定位 按完整值二分查找
title LIKE 'Java%' 能,范围扫描 前缀确定,能找到起点
title LIKE '%Java%' 不能,全表扫描 开头不确定,没有起点
title LIKE '%Java' 不能,全表扫描 同上

原因藏在 B+ 树本身:它按完整值排好序,只能从左往右比。 以某个前缀开头的值一定连成一段------能找到起点,就能范围扫描;起点一旦不确定,二分查找就无从下手,只能把叶子节点挨个读一遍------索引还在,但一点忙都帮不上,退化成了全表扫描。

更麻烦的是,就算全表扫描能扫出结果,它也回答不了全文搜索真正要回答的问题:

    1. 分词:搜"Elasticsearch 教程",内容是"Elasticsearch 入门指南"算不算命中?
    1. 相关度:1000 条结果里,哪条最该排第一?
    1. 多词匹配:搜"Java 并发",要的是同时包含这两个词,而不是包含任意一个。

这三个需求,B+ 树一个都接不住。不是 MySQL 不行,是它压根没打算干这件事。

说句公道话

MySQL 8.0 也有全文索引(FULLTEXT),InnoDB 从 5.6 起就支持了,底层同样是倒排思想。但它的中文分词、相关度排序、横向扩展能力都远不及专用搜索引擎。小规模、纯英文、对相关度没要求的场景可以凑合;一旦要中文分词,或者文档量上千万,就该换工具了。


二、倒排索引:把组织数据的方向反过来

知道了 LIKE 慢在哪,就能看懂 ES 快在哪------它没有让"搜索"这件事变快,而是把搜索时要做的遍历,提前搬到了写入时。

2.1 从"文档找词"到"词找文档"

正排索引是"文档 → 词",也是我们最习惯的方向:

文档 ID 内容
1 Java 并发编程
2 Java 虚拟机
3 并发与性能

要找含"并发"的文档,只能把三条内容都读一遍------这正是 LIKE '%并发%' 在做的事。

倒排索引把它翻过来,变成"词 → 文档":

词项(Term) 文档 ID 列表(Posting List)
Java 1, 2
并发 1, 3
虚拟机 2
编程 1

现在搜"并发",直接查表拿到 1, 3,一次定位,不用碰任何一个文档的原文。

2.2 光有倒排不够,快在这三个设计

词典和倒排表加起来,就是一份"词项到文档"的巨型字典。但一个上千万文档的索引,词项可能有几百万个,倒排表可能上百 GB------凭什么还能毫秒级返回?

1. FST 让词典索引常驻内存

如果整个词典都放内存,放不下;全放磁盘,每次查询都要多次随机读。ES 的做法是折中:把词典的前缀压缩成一份索引,用 FST(有限状态转换器)存储并常驻内存,完整的词典和倒排表留在磁盘上。查询时先在内存的 FST 里定位到词项,再精准地去磁盘读一次。

2. 压缩让倒排表变小

倒排表里存的是递增的文档 ID。ES 用差值编码(Frame of Reference)只存相邻 ID 的差,再按块做位打包,常见词项的倒排表能压掉一个数量级;越稀疏的词项收益越小------块内的位宽由最大的那个间隔决定。数据小到一定程度,磁盘读就不再是瓶颈。

3. 跳表让多条件求交变快

搜"Java 并发",拿到 1, 2 和 1, 3,需要求交集。逐条比对是 O(n),ES 在倒排表上建了跳表(Skip List):每隔一段插一个跳转指针,比对时可以一次跳过一大片。

倒排索引不是魔法

它只是把代价从"每次搜索"转移到了"每次写入"------索引要建、要维护、要占空间,更新还要付出额外成本。这笔账怎么还,第五节会讲清楚。


三、Elasticsearch:开箱即用的倒排索引

Elasticsearch 是基于 Lucene 构建的分布式搜索引擎,对外只暴露 REST 接口和 JSON 文档模型,不依赖任何特定语言。前面说的倒排索引、分词、相关度排序,它都打包成了一个开箱即用的服务------数据以 JSON 文档写进去,全文检索、聚合分析、高亮结果直接查出来,单机扛不住就加节点横向扩展。

它要解决的,正是 LIKE 答不了的那三件事:中文分词、相关度排序、多条件组合。

常见的落地场景有两类:站内搜索 (商品、文章、用户)和日志分析(也就是常说的 ELK 技术栈,ES 负责存和查,Logstash 负责采集,Kibana 负责可视化)。

一句话定位

ES 不是 MySQL 的替代品,它是"搜索"这件事的专用工具------事务、关联查询、强一致这些活,还是交给数据库。


四、上手:30 行命令建一个能用的搜索

原理明白,接下来动手。先启动一个单机实例。

docker 复制代码
docker network create elastic
docker run --name es01 --net elastic -p 9200:9200 \
  -e "discovery.type=single-node" \
  docker.elastic.co/elasticsearch/elasticsearch:8.18.0

9300 是节点间通信端口,单机实验用不到,集群部署时在内网放开即可。

ES 8.0 起安全功能默认开启 ,这一步和早期版本不一样:启动时会自动生成 TLS 证书、设置 elastic 超级用户的密码、生成 Kibana 的注册 token。密码只在终端打印一次,先存下来。

docker 复制代码
# 取出自动生成的 CA 证书,之后用 HTTPS + 密码访问
docker cp es01:/usr/share/elasticsearch/config/certs/http_ca.crt .
curl --cacert http_ca.crt -u elastic:<密码> https://localhost:9200

关掉认证的 ES,是数据泄露的经典入口

本地实验常加 -e "xpack.security.enabled=false" 关掉认证图省事,但绝不能用在生产环境。历史上被勒索、被拖库的案例,大多不是被攻破,而是压根没设防。

4.1 建索引:最容易踩坑的一步

sql 复制代码
PUT /book
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 1
  },
  "mappings": {
    "properties": {
      "title":       { "type": "text", "analyzer": "ik_max_word" },
      "author":      { "type": "keyword" },
      "description": { "type": "text", "analyzer": "ik_max_word" },
      "price":       { "type": "double" },
      "publishDate": { "type": "date" }
    }
  }
}

单机实验的一个小提醒

上面 number_of_replicas 写 1 是为了讲清概念,但单节点实例里副本分片无处分配,_cluster/health 会一直显示 yellow------这不是故障,单机练手时改成 0 即可。

ik_max_word 是 IK 中文分词器的细粒度模式(需要单独装插件),会把"Java 并发编程实战"尽量多切几个词出来。

text 和 keyword 的区别,是新手第一个大坑:

类型 是否分词 用于 典型坑
text 是,切成词项后入索引 全文检索 拿整句话去精确查,查不到
keyword 否,整串原样保留 精确匹配、聚合、排序 拿它做全文检索,只有完全相同才命中

Tip

一句话记法:要"搜得到"用 text,要"对得上"用 keyword。

4.2 写入和查询

sql 复制代码
POST /book/_doc/1
{
  "title": "Java 并发编程实战",
  "author": "张三",
  "description": "从内存模型讲到线程池",
  "price": 89.0,
  "publishDate": "2024-06-01"
}
sql 复制代码
GET /book/_search
{
  "query": {
    "bool": {
      "must":   [ { "match": { "title": "Java 并发" } } ],
      "filter": [ { "term":  { "author": "张三" } } ]
    }
  }
}

must 里的 match 会参与相关度打分,filter 里的 term 不打分、还能走缓存。

查询优化里性价比最高的一条

能用 filter 就别用 must。 缓存的实现正是 Roaring Bitmap:把命中的文档 ID 集合按块存成数组或位图,求交并只做位运算,不必再去碰倒排表的原文。

Java 侧接入(8.x 起推荐官方 elasticsearch-java 客户端,旧的 RestHighLevelClient 从 7.15 起已标记弃用):

java 复制代码
ElasticsearchClient client = new ElasticsearchClient(transport);

SearchResponse<Book> resp = client.search(s -> s
        .index("book")
        .query(q -> q.bool(b -> b
                .must(m -> m.match(mm -> mm.field("title").query("Java 并发")))
                .filter(f -> f.term(t -> t.field("author").value("张三"))))),
        Book.class);

4.3 那个坑,现场演示一遍

数据刚写进去,换一种查法:

sql 复制代码
GET /book/_search
{ "query": { "term": { "title": "Java 并发编程实战" } } }

没有结果。文档明明在,为什么查不到?

因为 title 是 text 类型,写入时已经被切成 java, 并发, 编程, 实战 这些词项存进倒排表(这里只列了主干词,ik_max_word 是细粒度模式,实际还会切出重叠词项,下一段的 _analyze 一看便知)------倒排表里根本没有"Java 并发编程实战"这整个串 。而 term 查询不会对查询词做分词,它拿整句话原文去词典里找,自然找不到。

这就是 term 和 match 的分工:

  • match :会走分析器,把查询词也分词,用于 text 字段

  • term :不做任何处理,拿原文精确匹配,用于 keyword 字段

顺带一提,分析器除了分词还会做归一化处理(比如把英文转成小写),所以同一个词的大小写写法不同,也可能出现"能查到"和"查不到"两种结果。

排查这类问题,第一步永远是

用 _analyze 看看你的词到底被切成了什么。

sql 复制代码
GET /book/_analyze
{ "analyzer": "ik_max_word", "text": "Java 并发编程实战" }

五、一条数据写进去,到底发生了什么

会用之后,问题就变成:为什么刚写完查不到?为什么删了磁盘还不释放?答案在同一条链路里。

1. 先进缓冲区和事务日志。 文档先写进内存缓冲区(Memory Buffer),同时追加到 translog。此刻数据在内存里,搜不到。

2. refresh 让它可见。 默认每 1 秒执行一次 refresh:把缓冲区里的数据生成一个新的 segment。segment 一旦生成,文档就可被搜索了------这就是"近实时"(NRT)的来源。

Note

ES 不是实时的,它默认有大约 1 秒的可见延迟。

3. translog 是保命符。 数据还在缓冲区时断电,靠 translog 恢复。默认 index.translog.durability: request,每个写请求都会 fsync 一次------这条是"写入性能换数据安全"的默认选择,批量导入时可以调,但要知道自己在放弃什么。

4. segment 不可变。 这是 ES 的基石,也是理解一切"怪脾气"的钥匙。不可变意味着不用加锁、可以放心缓存、方便压缩。代价是:"更新"一条文档,实际是把旧文档标记删除、再写一条新的;"删除"也只是打标记,文档依然占着空间。 一条文档改十次,索引里就躺着 10 份被标记删除的旧版本,直到段合并才真正释放。

5. flush 和段合并。 flush 把 segment 落盘 fsync 并清空 translog;段合并(merge)把一堆小 segment 合成大 segment,这时才真正物理删除那些被标记的文档,磁盘空间才释放。

批量导入的标准操作

先关掉自动 refresh,导完再打开。导入期间新写入的数据不会生成新的小 segment,段合并的压力和查询抖动都会小很多。

sql 复制代码
PUT /book/_settings
{ "index.refresh_interval": "-1" }

导入完成后再改回来:

sql 复制代码
PUT /book/_settings
{ "index.refresh_interval": "1s" }

六、高频面试题精讲

原理捋顺了,面试问的就都在射程内。下面七道是 ES 面试里出现频率最高的,每题标注考点在哪,并给出可以直接说出口的话术。

Tip

下面每个绿色框里的内容都是口语回答模板,面试被问到可以直接照着说。

6.1 倒排索引由什么组成?

考点 :不是考你记不记得"倒排索引"这四个字,而是考你知不知道它由词典 + 倒排表两部分组成,以及为什么这么设计。

Tip

倒排索引把"文档 → 词"反过来组织成"词 → 文档 ID 列表",由词典(Term Dictionary)和倒排表(Posting List)两部分组成。词典存所有词项,倒排表存每个词项对应的文档 ID 列表。为了性能,词典的词项索引用 FST 压缩后常驻内存,完整词典和倒排表放在磁盘;倒排表用差值编码加位打包压缩,多个词项求交并时用跳表加速。而 filter 查询命中的文档集合会缓存成 Roaring Bitmap,位运算求交并很快,这也是 filter 比 must 高效的原因之一。

6.2 为什么说 ES 是"近实时",而不是实时?

考点:refresh 机制。

Tip

文档写进 ES 后先落在内存缓冲区和 translog 里,此时不可搜索。默认每 1 秒执行一次 refresh,把缓冲区数据生成新的 segment,文档才可见,所以默认有约 1 秒延迟,叫近实时(NRT)。需要立刻可见可以手动 refresh,但频繁 refresh 会产生大量小 segment,反而拖慢查询,批量导入时通常是先关掉 refresh、导完再打开。

6.3 深分页怎么处理?

考点 :from + size 的执行方式------协调节点要向每个分片 都要 from + size 条数据,再汇总排序。翻到第 100 页时,每个分片都得吐 1000 条出来,协调节点的内存和 CPU 都是灾难。默认上限 10000,超了直接报 Result window is too large。

Tip

三种方案按场景选。from + size 只适合浅分页,默认上限 10000;search_after 用上一页最后一条的排序值当游标,每次只取 size 条,适合深分页和滚动加载,缺点是不能跳页;导出全量数据用 PIT + search_after,PIT 提供一致性快照,是官方现在推荐的方案,旧的 scroll 已经不再推荐。生产上一般翻页用 search_after,导出用 PIT。

6.4 text 和 keyword 怎么选?

考点:会不会在聚合和排序上踩坑。

Tip

text 会分词,用于全文检索;keyword 不分词、整串保留,用于精确匹配、聚合和排序。用 text 字段做聚合会直接报 Fielddata is disabled。常见做法是一个字段同时建两套:"title": {"type": "text", "fields": {"keyword": {"type": "keyword"}}},检索走 title,聚合排序走 title.keyword。

6.5 主分片数为什么创建后不能改?

考点:路由公式。

Tip

文档落在哪个分片由 shard = hash(_routing) % number_of_primary_shards 决定,_routing 默认取文档 ID。一旦改了主分片数,取模结果全变,已有数据就找不到了。所以主分片数必须在建索引时确定,要改只能新建索引再 _reindex。副本分片数可以随时调整。ES 7.0 起默认主分片数是 1(之前是 5),现在官方更推荐先设少量分片、不够再 reindex,而不是一上来就开一堆。

6.6 MySQL 的数据怎么同步到 ES?

考点:分布式场景下的数据一致性意识。

Tip

三种主流方案。同步双写最简单,但业务代码耦合,两次写入不在一个事务里,失败就不一致;异步 MQ 解耦、可重试、能削峰,是主流做法;binlog 订阅(Canal、Debezium)对业务零侵入,存量加增量一次搞定,适合已有系统改造。无论哪种,ES 和 MySQL 之间都只能是最终一致,必须配补偿和对账,不能指望强一致。

6.7 集群脑裂怎么防?

考点:候选 master 节点的投票机制。

Tip

脑裂是集群被网络分区切成两半,两边各自选出 master,同时写数据导致冲突。ES 7.0 之前靠 discovery.zen.minimum_master_nodes 参数防------设为「候选 master 节点数 ÷ 2 + 1」,候选节点必须拿到过半票数才能当选。7.0 起换成了新的集群协调层(Zen2),投票逻辑内置,这个参数已经取消 ,只需要正确配置 cluster.initial_master_nodes(仅首次引导集群时使用)和 discovery.seed_hosts。节点数上,三个专用候选 master 是最常见的配置------Zen2 靠多数派投票,奇数是个好习惯,但不是硬性要求。


写在最后

回到开头那张检索卡片。全文搜索还靠 LIKE 硬扛,问题从来不是"索引加没加",而是拿 B+ 树去解一道它不擅长的题。

倒排索引把方向反了过来:写入时建好"词 → 文档"的映射,搜索时一次定位。代价是写入链路更长、数据有约 1 秒才可见、segment 不可变导致删除只是打标记------你在生产上遇到的每一个"ES 的怪脾气",几乎都能追溯到这条写入链路。

面试也是如此:问倒排索引,其实是在问你知不知道它的时间和空间代价;问深分页,其实是在问你对分片模型的理解。把这条链路捋顺,这些面试题不过是换个说法再问一遍。


本文的引用仅限自我学习如有侵权,请联系作者删除。

参考知识

全文搜索还靠 LIKE 硬扛?Elasticsearch 从入门到精通与高频面试题精讲


相关推荐
Elasticsearch36 分钟前
两条规则,一次故障,一条告警:变点检测如何减少告警噪声
elasticsearch
天河归来10 小时前
第三方接口不稳定,如何让核心任务继续完成
java·大数据
GlobalInfo11 小时前
2026年全球机器人用超平执行器市场研究报告:市场规模达7.51亿美元,人形机器人商业化与轻量化运动控制需求推动行业增长
大数据·人工智能·数码相机·microsoft·机器人
IT古董13 小时前
《Agent开发工程师成长指南》- 第4章 第2节:Prompt设计五要素——任务、角色、上下文、约束与输出格式
大数据·人工智能·prompt
kingx314 小时前
BugKu PRA 渗透测试 2(Harry‘s Blog):9 Flag 全链实战复盘
中间件·渗透测试·log4j2·内网渗透·反序列化
8Qi814 小时前
DDD架构演进
java·分布式·mvc·ddd
方方洛15 小时前
ray教程-23-附录C-术语表
人工智能·分布式·机器学习
Allen_LVyingbo15 小时前
NVIDIA CUDA Tile 在医疗计算中的算子形式化、算法重构与内核实现
大数据·python·算法·重构·知识图谱·健康医疗·数据库架构
全栈陈序员16 小时前
【Git】从入门到精通(一)—— Git简介:什么是版本控制系统
大数据·git·elasticsearch