写在前面:上篇讲 FastAPI 怎么写接口,这篇讲数据怎么搜。readme 一上来就给了一个非常具体的判断——"content 内容字段全文搜索,mysql 扛不住,性能很差。select like 建议不要用的,用 es。" 这话有点绝对,但方向没错。今天就顺着这个判断,搞清三件事:MySQL 的 LIKE 为什么慢、ES 的倒排索引为什么快、以及怎么用一份 docker-compose.yml 把整套检索设施(ES + Kibana + Milvus)一次拉起来。以下所有代码和配置均来自课堂真实文件。
一、LIKE 的困境:从第一个字符开始扫
readme 用一句话点破了场景:
"MYSQL 关系型数据库,字段查找,表关联 适合的,块。content 内容字段全文搜索,mysql 扛不住,性能很差。"
先说清楚——MySQL 不是不行,是不擅长。
MySQL 擅长什么?readme 说得很准确:
| 擅长 | 例子 |
|---|---|
| 字段查找 | WHERE id = 123 |
| 表关联 | JOIN 两张表 |
但"在一篇文章内容里找关键词"这件事,MySQL 的方法很笨——挨个扫。
SELECT * FROM articles WHERE content LIKE '%机器学习%';
问题在于开头那个 %:
LIKE '机器学习%' → 可以用索引(从头匹配)
LIKE '%机器学习%' → 索引失效!只能全表扫描
只要 % 在前面,索引就直接废掉。 因为索引是按"开头"排序的——B+ 树能快速定位"以某字符开头"的记录,但没法回答"哪条记录中间某处包含某词"。
于是数据库只能一条条读、一条条比对。100 万篇文章?扫 100 万次。 这就是 readme 说的"性能很差"。
二、问题的本质:正排 vs 倒排
要理解 ES 为什么快,得先理解"方向"这件事。
readme 用两张图对比了 MySQL 和 ES 的结构:
Mysql database -> table -> row -> column -> 文本
like 性能不好
ES 分词 -> 文本 -> row -> table -> database 倒过来的
这两个方向,就是正排索引和倒排索引的区别。
正排索引:从"书"找"内容"
MySQL 的组织方式是"正着"的:
库 → 表 → 行 → 列 → 文本内容
你想验证"第 5 行有没有'机器学习'这个词",必须先定位到第 5 行,再把 content 字段整段读出来,然后字符串匹配。
这是"从记录找词"的方向。
就像图书馆里没有检索系统——你想找哪本书讲了"机器学习",只能把每本书翻开、逐页读一遍。
倒排索引:从"词"找"书"
ES 的做法是把方向倒过来:
分词 → 文本 → row → table → database
先把所有文本切成词,然后建一张"词 → 出现在哪些文档"的索引表。
readme 给了一个极简的示例:
浏览器 索引 [1, 2] 倒排索引
机器学习
翻译成表格就是:
| 词 | 出现在哪些文档 |
|---|---|
| 浏览器 | [1, 2] |
| 机器学习 | [1, 3, 7, ...] |
这就是倒排索引。 搜索时不再扫全部内容——直接查这个表,拿到文档编号列表,一步到位。
回到图书馆的类比:倒排索引就是图书馆的"主题卡片柜"。 你找"机器学习",直接翻到写有"机器学习"的那张卡片,上面列着所有相关书籍的编号。不用翻书。
为什么叫"倒"排?
| 索引类型 | 映射方向 |
|---|---|
| 正排 | 文档 → 词(这篇文档里有哪些词) |
| 倒排 | 词 → 文档(这个词出现在哪些文档里) |
搜索需要的恰好是"倒"的这个方向。 所以 ES 把索引倒着建——这就是它快的根本原因。
三、分词:倒排索引的前置工序
倒排索引的前提是"先把文本切成词"。readme 的链条写得很清楚:
"把 text 分词 tokenzation"
没有分词,就没有"词 → 文档"这张表。
中文分词的难处
英文分词很简单——按空格切就行:
"machine learning is fun" → ["machine", "learning", "is", "fun"]
中文没有空格:
"可以在浏览器中运行机器学习模型"
怎么切?可以有多种切法:
| 切法 | 结果 | 问题 |
|---|---|---|
| 按字切 | 可/以/在/浏/览/器/中/运/行... | 太碎,"机器"和"学习"被拆散 |
| 按词切 | 可以/在/浏览器/中/运行/机器学习/模型 | 需要词典和算法 |
所以 ES 要装"中文分词器"。 这就是后面 docker-compose 里出现 IK 的原因——IK 是 Elasticsearch 最常用的中文分词插件,后面细说。
readme 里那段被索引的示例文本,正好是真机上找来的素材:
"可以在浏览器中运行机器学习模型,支持图像、文本和声音等多种应用场景。"
分完词大概是:
可以 / 浏览器 / 中 / 运行 / 机器学习 / 模型 / 支持 / 图像 / 文本 / 声音 / 场景
于是倒排索引里就有了:浏览器 → [1, 2]、机器学习 → [...]。
这也解释了 readme 里那句"浏览器 机器学习 ... 索引"——分词的结果,就是索引的原料。
四、Docker Compose:把整套检索设施一次拉起
索引原理搞懂了,接下来是"怎么装 ES"。readme 复习了一下 Docker 基础:
"compose:docker 容器化技术。image 镜像(代码和环境依赖)运行起来 → 容器 container。将多个容器编排到一起,docker-compose.yml 就是这个多镜像编排配置文件。"
启动命令:
docker compose up -d
readme 逐字解释:
- docker compose 寻找目录下的 docker-compose.yml
- up 启动起来
- -d 后台运行
一条命令,一整个技术栈。 这次的 docker-compose.yml 里住着五个服务,我们一个个看。
服务一:Elasticsearch(本地构建,自带中文分词)
services:
# Elasticsearch 8.17.0 + IK 中文分词(内置到镜像)
es:
build: ./elasticsearch # 从本地 Dockerfile 构建镜像(自带IK)
container_name: es-dev
ports:
- "9200:9200" # ES 访问端口
environment:
- discovery.type=single-node # 单节点运行(开发环境)
- xpack.security.enabled=false # 关闭安全认证,免密码访问
- xpack.security.http.ssl.enabled=false # 关闭 HTTPS 加密
- xpack.security.transport.ssl.enabled=false # 关闭节点传输加密
- ES_JAVA_OPTS=-Xms512m -Xmx512m # JVM 内存配置,避免占用过高
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/es/data:/usr/share/elasticsearch/data
restart: always
这段配置信息量很大,逐条拆:
| 配置 | 值 | 为什么 |
|---|---|---|
build: ./elasticsearch | 从本地 Dockerfile 构建 | 不是拉官方镜像,而是自己构建——为了内置 IK 中文分词 |
container_name | es-dev | 容器名叫 es-dev,方便识别 |
ports | "9200:9200" | ES 的默认访问端口 |
discovery.type | single-node | 单节点模式(开发环境,不需要集群) |
xpack.security.enabled | false | 关掉认证,免密码访问 |
ES_JAVA_OPTS | -Xms512m -Xmx512m | JVM 堆内存固定 512MB |
volumes | 挂载到 volumes/es/data | 数据持久化,容器删了数据还在 |
restart: always | 总是重启 | 开机自启 / 崩溃自动拉起 |
几个关键点值得展开:
1. build 而不是 image——为了中文分词
注意这里用的是 build: ./elasticsearch(从本地 Dockerfile 构建),而不是 image: elasticsearch:8.17.0(直接拉官方镜像)。
为什么费这个劲?因为官方镜像不带中文分词器。中文进了 ES 默认会按单字切,效果很差。所以要基于官方镜像写个 Dockerfile,把 IK 插件塞进去,再构建成自己的镜像。
注释写得很清楚:
"Elasticsearch 8.17.0 + IK 中文分词(内置到镜像)"
这就是"镜像即交付"的思路——把"ES + 中文分词"的环境固化成镜像,谁都能一键复现,不用每台机器手动装插件。(还记得前面学的 Docker 比喻吗——Dockerfile 是施工图纸。)
2. 三个安全开关全关了
- xpack.security.enabled=false
- xpack.security.http.ssl.enabled=false
- xpack.security.transport.ssl.enabled=false
ES 8.x 默认开启安全认证(要账号密码、要 HTTPS)。开发环境嫌麻烦,三个全关——注释也老实交代了"免密码访问"。
但这里必须提醒一句:这三行是开发环境专用。生产环境开着这配置,等于把数据库裸奔在公网上(9200 端口直接对外开放)。开发图省事,上线要命。
3. JVM 内存卡死在 512MB
- ES_JAVA_OPTS=-Xms512m -Xmx512m # JVM 内存配置,避免占用过高
ES 是 Java 写的,跑在 JVM 上。默认情况下 JVM 会"贪心"地占用大量内存。这行配置把初始堆(-Xms)和最大堆(-Xmx)都锁在 512MB——开发机不吃紧,也避免 ES 把内存吃光。
-Xms 和 -Xmx 设成一样值是常见做法——避免 JVM 运行时动态扩缩堆带来的性能抖动。
服务二:Kibana(ES 的图形界面)
# Kibana 最新稳定版:8.17.0(必须与 ES 版本完全一致)
kibana:
image: kibana:8.17.0
container_name: kibana-dev
ports:
- "5601:5601" # Kibana 网页控制台端口
environment:
- ELASTICSEARCH_HOSTS=http://es:9200 # 连接 ES 容器内部地址
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/kibana:/usr/share/kibana/data
restart: always
depends_on:
- es # 等待 ES 启动完成后再启动 Kibana
Kibana 是 ES 的可视化控制台——可以理解成"ES 的 Navicat"。浏览器打开 5601 端口,就能可视化地建索引、写查询、看数据。
两个关键点:
1. ELASTICSEARCH_HOSTS=http://es:9200——用服务名通信
注意这里写的是 es:9200,不是 localhost:9200。
为什么?因为 Kibana 跑在自己的容器里,对它来说 localhost 是它自己,不是 ES。而 es 是 compose 里定义的服务名——Docker 的网络会自动把服务名解析成对应容器的 IP。
这就是容器编排的便利:服务之间用"名字"互相找,不用关心 IP。 而且 ES 容器重启后 IP 变了也没关系,服务名始终有效。
2. depends_on: es——启动顺序
Kibana 依赖 ES,所以要先等 ES 起来。这里有个微妙之处(前面讲 Milvus 时也遇到过):depends_on 只保证"容器启动顺序",不保证"服务就绪"。 ES 容器起来了,但它可能还在初始化(加载插件、建集群状态)。Kibana 这时候连上去可能会报错。
生产环境通常要配合 healthcheck 用(下面 minio 和 milvus 的配置里就有)。
3. 版本号必须一致
注释特意强调了两次:
"Kibana 最新稳定版:8.17.0(必须与 ES 版本完全一致)"
这不是建议,是硬要求。Kibana 和 ES 通过内部协议通信,版本不匹配会直接拒绝连接。所以:
es: build: ./elasticsearch # 8.17.0(注释标明)
kibana: image: kibana:8.17.0 # 8.17.0 ✅ 对上了
装 ES 前先想好 Kibana 用哪个版本——升级必须一起升。
服务三到五:Milvus 三件套
# Milvus 最新稳定版:2.5.0(必须与 ES 版本完全一致)# Milvus
etcd:
container_name: etcd-dev
image: quay.io/coreos/etcd:v3.5.18
environment:
- ETCD_AUTO_COMPACTION_MODE=revision
- ETCD_AUTO_COMPACTION_RETENTION=1000
- ETCD_QUOTA_BACKEND_BYTES=4294967296
- ETCD_SNAPSHOT_COUNT=50000
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/etcd:/etcd
command: etcd -advertise-client-urls=http://etcd:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd
healthcheck:
test: ["CMD", "etcdctl", "endpoint", "health"]
interval: 30s
timeout: 20s
retries: 3
minio:
container_name: minio-dev
image: minio/minio:RELEASE.2024-05-28T17-19-04Z
environment:
MINIO_ACCESS_KEY: minioadmin
MINIO_SECRET_KEY: minioadmin
ports:
- "9001:9001"
- "9000:9000"
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/minio:/minio_data
command: minio server /minio_data --console-address ":9001"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3
standalone:
container_name: standalone-dev
image: milvusdb/milvus:v2.5.25
command: ["milvus", "run", "standalone"]
security_opt:
- seccomp:unconfined
environment:
MINIO_REGION: us-east-1
ETCD_ENDPOINTS: etcd:2379
MINIO_ADDRESS: minio:9000
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/milvus:/var/lib/milvus
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"]
interval: 30s
start_period: 90s
timeout: 20s
retries: 3
ports:
- "19530:19530"
- "9091:9091"
depends_on:
- "etcd"
- "minio"
networks:
default:
name: common-network
等等,为什么 ES 的文章里会出现 Milvus?
这正是这份文件最有意思的地方——它不是"ES 的配置",而是"整个 AI 检索设施的总图"。
Milvus 是向量数据库(前面专门讲过——管"语义检索"),ES 是全文检索(管"关键词检索")。两个一起上,就是 RAG 的完整检索层:
| 引擎 | 检索方式 | 适合什么 |
|---|---|---|
| ES | 倒排索引 + 关键词匹配 | 精确术语、专有名词、代码符号 |
| Milvus | 向量相似度 | 语义相近、换个说法也能找到 |
一个负责"准",一个负责"懂"。 这就是为什么它们出现在同一份 compose 里——现代 RAG 系统常见的"混合检索"配置。
三个 Milvus 服务的分工(前面讲过,这里简要回顾):
| 服务 | 版本 | 职责 |
|---|---|---|
etcd | v3.5.18 | 存元数据(档案室) |
minio | RELEASE.2024-05-28T17-19-04Z | 存向量数据(地下仓库) |
standalone | v2.5.25 | Milvus 主服务(主楼),端口 19530 |
网络:五个服务在同一个局域网
networks:
default:
name: common-network
最后这三行,把五个服务(es、kibana、etcd、minio、standalone)放进了同一个叫 common-network 的网络。
这是一个常被忽略但很重要的细节——正因为它们在同一网络里:
- Kibana 能用
http://es:9200找到 ES - Milvus 能用
etcd:2379找到 etcd、minio:9000找到 minio - 而
etcd、minio完全不需要暴露端口到宿主机(配置里确实没写ports)——只有同一网络内的服务能访问,外网碰不到
网络隔离本身就是一层安全防护。 数据库、对象存储这些"内部服务"没必要对外开放——这份配置里只有 es(9200)、kibana(5601)、minio(9000/9001)、milvus(19530)暴露了端口。
一个反复出现的语法
volumes:
- ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/es/data:/usr/share/elasticsearch/data
${DOCKER_VOLUME_DIRECTORY:-.} 是 shell 的默认值语法:
${变量:-默认值}
含义:如果环境变量 DOCKER_VOLUME_DIRECTORY 有值就用它,没有就用 .(当前目录)。
这样写的灵活性在于——开发时数据放当前目录(./volumes/es/data),服务器上可以设成 /data/docker-volumes,一份配置到处能用。
左侧挂载点(宿主机)与右侧(容器内)用冒号分隔,这是 Docker 数据卷的标准写法。
五、一条命令,五个服务
现在回头看那条命令:
docker compose up -d
它做了什么?按依赖关系依次拉起五个服务:
docker compose up -d
↓
[es](先构建镜像,自带 IK 中文分词)
↓ healthcheck 通过
[kibana] 依赖 es,后启动,端口 5601
↓
[etcd] + [minio] 负责存储
↓ healthcheck 通过
[standalone](Milvus)依赖 etcd + minio,端口 19530
↓
全部就绪,同在 common-network 网络
手动部署这一套要多久? 装 ES、装 IK 插件、配 JVM 参数、装 Kibana 并对齐版本、装 etcd、装 MinIO、装 Milvus……顺利的话半天,不顺利的话两天。
用 compose 呢? 一条命令,几十秒。
这就是 readme 说的"将多个容器编排到一起"的价值——把"环境搭建"从一次性劳动变成了可复现的配置文件。
六、完整的检索方案长什么样
最后把上篇和下篇串起来,看看这节课整个技术栈是怎么配合的:
┌─────────────────────────────────────────────────┐
│ FastAPI 后端服务(上篇) │
│ 用 Pydantic 校验入参、Swagger 自动出文档 │
│ ↓ │
│ 检索请求分发 │
│ ┌────────────┴────────────┐ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Elasticsearch│ │ Milvus │ │
│ │ 倒排索引 │ │ 向量检索 │ │
│ │ 关键词精确匹配│ │ 语义相似匹配 │ │
│ │ (IK 中文分词)│ │ (etcd+minio) │ │
│ └──────────────┘ └──────────────┘ │
│ └────────────┬────────────┘ │
│ ▼ │
│ 结果合并 → 喂给 LLM → 生成回答 │
│ │
│ 全部跑在一个 docker-compose 网络里 │
└─────────────────────────────────────────────────┘
| 层次 | 技术 | 这节课教的 |
|---|---|---|
| 接口层 | FastAPI | 类型注解校验 + 自动文档(上篇) |
| 检索层-关键词 | Elasticsearch | 倒排索引 + IK 中文分词 |
| 检索层-语义 | Milvus | 向量检索(前面课程) |
| 部署层 | Docker Compose | 五个服务一键编排 |
从"接口怎么写"到"数据怎么搜"再到"服务怎么部署"——一条完整的链路。
七、三个可以立刻用上的结论
第一,什么时候该上 ES?
不是所有搜索都需要 ES。判断标准很简单:
| 场景 | 用什么 |
|---|---|
WHERE id = 123 精确查 | MySQL 够了 |
JOIN 关联查询 | MySQL 够了 |
| 在长文本里找关键词 | 考虑 ES |
| 要按相关性排序、要高亮、要分词 | 必须 ES |
readme 的判断"select like 建议不要用的"——如果你的 LIKE 是 '%关键词%' 这种前置通配符,而且数据量大、还要求排序,那确实该换 ES。
第二,倒排索引的思想比 ES 本身更值钱。
"把方向倒过来索引"这个思路,不只用在 ES:
| 技术 | 倒排思想的体现 |
|---|---|
| 搜索引擎 | 词 → 网页 |
| 数据库索引 | 值 → 行位置 |
| 向量数据库 | 向量 → 文档(ANN 索引) |
| Git | 对象哈希 → 引用 |
理解了"为了查询方便,反过来建索引",你就理解了大半个数据系统。
第三,开发配置和生产配置是两回事。
这份 compose 里那些"图省事"的配置——关掉安全认证、单节点、minio 默认账号密码 minioadmin、数据库端口全开放——每一条都是给开发环境踩的油门,生产环境都得踩回来:
| 开发配置 | 生产该怎么做 |
|---|---|
xpack.security.enabled=false | 开启认证 + HTTPS |
| minioadmin / minioadmin | 强密码 + 密钥管理 |
single-node | 集群 + 副本 |
| 端口全部暴露 | 只开必须的,其余走内网 |
"能跑起来"和"能扛住"之间,隔着这一张表。
PS:这节课的两个判断我觉得都挺实在——FastAPI 用注解替代了手写校验(代码即文档),ES 用倒排索引替代了全表扫描(方向决定速度)。而下篇里那份 docker-compose.yml 更像一张"现代 AI 检索设施的总图":ES 管准、Milvus 管懂、Kibana 管看、五个服务一个网络。下次再有人问"全文搜索为什么不用 LIKE",你可以反问他一句——你知道你刚才那句话让索引失效了吗。