Elasticsearch——为什么 Like 查询总是那么慢 / Elasticsearch for Full-Text Search at Scale
📅 创建时间:2026-05-08 🏷️ 标签:#Elasticsearch #倒排索引 #ES分词 #全文搜索 #搜索建议 📚 前置知识:[[00-backend-overview]] [[09-mysql-optimization]](索引原理) 📚 相关知识:[[/03-web/06-databases-and-data-access/09-olap-databases]](列式存储) [[11-architecture-patterns]](CQRS)
场景:搜索"小米手机"却找不到"小米 13"
┌─────────────────────────────────────────────────────────────┐
│ │
│ 用户在商品搜索框输入:"小米手机" │
│ │
│ SQL 查询: │
│ SELECT * FROM products │
│ WHERE name LIKE '%小米手机%' │
│ │
│ 结果:找到 0 条(因为商品名是"小米 13 手机") │
│ │
│ 用户困惑:我明明搜的就是小米,为什么搜不到? │
│ │
│ 产品经理:搜索体验太差了,转化率掉了 30%。 │
│ │
└─────────────────────────────────────────────────────────────┘MySQL 的 LIKE 是最左匹配,ES 的倒排索引是语义匹配。这一章,我们理解 ES 为什么能让搜索变快变准。
第1节:倒排索引——从"找文档"到"找词"
问题:MySQL LIKE 为什么慢
┌─────────────────────────────────────────────────────────────┐
│ MySQL LIKE 的工作原理 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 索引:B+ 树,按字段值排序 │
│ │
│ name = '小米手机' → 走索引 ✅ │
│ name LIKE '小米%' → 走索引(前缀匹配)✅ │
│ name LIKE '%小米%' → 全表扫描!❌ │
│ │
│ 原因:'%' 在前面,无法利用 B+ 树的有序性 │
│ │
│ 100 万条数据: │
│ 全表扫描 → 100 万次磁盘 IO → 5-10 秒 │
│ │
└─────────────────────────────────────────────────────────────┘倒排索引的核心思想
┌─────────────────────────────────────────────────────────────┐
│ 倒排索引 vs 正排索引 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 正排索引(MySQL):文档 → 包含哪些词 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 文档 1: 小米 13 手机,高性价比 │ │
│ │ 文档 2: 华为 Mate60,拍照神器 │ │
│ │ 文档 3: iPhone 15,体验流畅 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 倒排索引(ES):词 → 在哪些文档中 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 小米 → [文档 1] │ │
│ │ 13 → [文档 1] │ │
│ │ 手机 → [文档 1] │ │
│ │ 华为 → [文档 2] │ │
│ │ iPhone → [文档 3] │ │
│ │ 拍照 → [文档 2] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 搜索"小米":直接查"小米"这个词,O(1) 找到文档列表 │
│ │
└─────────────────────────────────────────────────────────────┘倒排索引的详细结构
┌─────────────────────────────────────────────────────────────┐
│ 倒排索引详细结构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 词(Term) → 倒排列表(Posting List) │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 词项词典(Term Dictionary) │ │
│ │ 小米 → 偏移量 100 │ │
│ │ 手机 → 偏移量 200 │ │
│ │ 华为 → 偏移量 300 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 倒排文件(Postings File) │ │
│ │ [偏移量 100] → docID=1, freq=1, positions=[0] │ │
│ │ [偏移量 200] → docID=1, freq=1, positions=[2] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ docID:文档 ID │
│ freq:词在文档中出现次数 │
│ positions:词在文档中的位置(用于短语搜索) │
│ │
└─────────────────────────────────────────────────────────────┘第2节:分词器——为什么"苹果"能搜到"apple"
问题:中文怎么分词
┌─────────────────────────────────────────────────────────────┐
│ 分词的歧义 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 句子:"中华人民共和国成立" │
│ │
│ 最小切分: │
│ 中华 / 人民 / 共和国 / 成立 │
│ │
│ 最大切分: │
│ 中华人民共和国 / 成立 │
│ │
│ 搜索引擎需要: │
│ → 粒度适中(太细语义丢失,太粗匹配不准) │
│ → 歧义消解("行"是多音字) │
│ → 未知词识别(新词、热词) │
│ │
└─────────────────────────────────────────────────────────────┘ES 分词器对比
┌─────────────────────────────────────────────────────────────┐
│ 分词器对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ standard(默认): │
│ → 英文:按空格 + 标点切分,lowercase │
│ → 中文:单字切分 │
│ → "小米手机" → "小", "米", "手", "机" │
│ → 缺点:失去语义,相邻单字无法关联 │
│ │
│ ik_max_word(最细粒度): │
│ → 中文:最大词数切分 │
│ → "小米手机" → "小米", "手机" │
│ → 优点:召回率高 │
│ → 缺点:precision 可能下降 │
│ │
│ ik_smart(最粗粒度): │
│ → 中文:最大语义切分 │
│ → "中华人民共和国" → "中华人民共和国" │
│ → 优点:精确率高 │
│ │
│ 推荐策略: │
│ 索引时:ik_max_word(最大程度索引) │
│ 搜索时:ik_smart(精准匹配) │
│ │
└─────────────────────────────────────────────────────────────┘自定义分词器配置
json
PUT /products
{
"settings": {
"analysis": {
"analyzer": {
"my_analyzer": {
"type": "custom",
"tokenizer": "ik_max_word",
"filter": ["lowercase", "synonym_filter"]
}
},
"filter": {
"synonym_filter": {
"type": "synonym",
"synonyms": [
"手机,移动电话,智能手机",
"电脑,计算机,笔记本",
"小米,MI,红米"
]
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "my_analyzer",
"search_analyzer": "ik_smart"
},
"description": {
"type": "text",
"analyzer": "ik_max_word"
},
"price": {
"type": "float"
},
"category": {
"type": "keyword"
}
}
}
}第3节:ES 搜索实战
基础查询
python
from elasticsearch import Elasticsearch
es = Elasticsearch(["http://localhost:9200"])
# 全文搜索
result = es.search(index="products", body={
"query": {
"match": {
"name": "小米手机" # 会分词搜索
}
}
})
# 短语搜索(保持词的相邻关系)
result = es.search(index="products", body={
"query": {
"match_phrase": {
"name": {
"query": "小米手机",
"slop": 1 # 允许 1 个词的距离
}
}
}
})
# 多字段搜索 + 权重
result = es.search(index="products", body={
"query": {
"multi_match": {
"query": "小米手机",
"fields": ["name^3", "description"], # name 权重更高
"type": "best_fields"
}
}
})过滤 + 聚合 + 高亮
python
# 复杂搜索:过滤 + 排序 + 高亮
result = es.search(index="products", body={
"query": {
"bool": {
"must": {
"match": {"name": "手机"}
},
"filter": [
{"range": {"price": {"gte": 2000, "lte": 5000}}},
{"term": {"status": "active"}},
{"terms": {"category": ["手机", "数码"]}}
]
}
},
"sort": [
{"sales": "desc"}, # 销量优先
{"price": "asc"} # 价格其次
],
"from": 0,
"size": 20,
"highlight": {
"fields": {
"name": {
"pre_tags": ["<em>"],
"post_tags": ["</em>"]
}
}
}
})
# 聚合统计
result = es.search(index="products", body={
"size": 0, # 不返回文档,只返回聚合结果
"query": {"match": {"name": "手机"}},
"aggs": {
"avg_price": {"avg": {"field": "price"}},
"price_ranges": {
"range": {
"field": "price",
"ranges": [
{"to": 1000},
{"from": 1000, "to": 3000},
{"from": 3000}
]
}
},
"top_brands": {
"terms": {"field": "brand", "size": 5}
}
}
})第4节:搜索建议——为什么输入框能自动补全
场景:输入"小"就出现"小米"
┌─────────────────────────────────────────────────────────────┐
│ 搜索建议的三种实现 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 方案 1:Prefix Query(最简单) │
│ → 用前缀匹配实现即时补全 │
│ → 搜索速度快,但无法处理同音字/拼写错误 │
│ │
│ 方案 2:Completion Suggester(推荐) │
│ → 使用 FST(有限状态转换器) │
│ → 专为补全设计,性能极高 │
│ → 支持权重排序 │
│ │
│ 方案 3:Phrase Suggester(智能纠错) │
│ → 处理拼写错误、同音字 │
│ → 搜索"iphon"能返回"iPhone 15" │
│ │
└─────────────────────────────────────────────────────────────┘Completion Suggester 实现
json
PUT /products/_mapping
{
"properties": {
"suggest": {
"type": "completion",
"analyzer": "simple",
"preserve_separators": true,
"preserve_position_increments": true,
"max_input_length": 50
},
"name": {"type": "text"},
"price": {"type": "float"}
}
}
PUT /products/_doc/1
{
"name": "小米 13 手机",
"price": 3999,
"suggest": {
"input": ["小米13", "小米手机", "MI 13", "XiaoMi 13"],
"weight": 100
}
}
POST /products/_search
{
"suggest": {
"product-suggest": {
"prefix": "小",
"completion": {
"field": "suggest",
"size": 5,
"skip_duplicates": true
}
}
}
}第5节:ES 在搜索推荐中的应用
多维度排序实战
python
# 搜索 + 多维度综合排序
result = es.search(index="products", body={
"query": {
"function_score": {
"query": {
"bool": {
"must": {"match": {"name": "手机"}},
"filter": {"range": {"price": {"lte": 5000}}}
}
},
"functions": [
{
"field_value_factor": {
"field": "sales",
"factor": 1.2,
"modifier": "log1p",
"missing": 1
}
},
{
"gauss": {
"price": {
"origin": 3000,
"scale": 1000,
"decay": 0.5
}
}
},
{
"filter": {"term": {"is_hot": true}},
"weight": 2
}
],
"score_mode": "sum",
"boost_mode": "multiply"
}
}
})升华:MySQL vs ES 的选择
┌─────────────────────────────────────────────────────────────┐
│ MySQL vs ES 选择指南 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 选 MySQL(全文搜索): │
│ ✅ 数据量小(<100 万) │
│ ✅ 查询简单(关键词匹配) │
│ ✅ 一致性要求高(ES 是最终一致) │
│ ✅ 已有 MySQL,不想引入新组件 │
│ │
│ 选 ES(全文搜索): │
│ ✅ 搜索体验要求高(纠错/同义词/高亮) │
│ ✅ 数据量大(>100 万) │
│ ✅ 需要复杂聚合(facet search) │
│ ✅ 搜索和 OLAP 混合场景 │
│ │
│ 最佳实践:MySQL + ES 双写 │
│ → MySQL 作为主库(事务、一致性) │
│ → ES 作为搜索库(全文搜索、聚合) │
│ → 数据同步:Canal / Debezium 监听 binlog │
│ │
└─────────────────────────────────────────────────────────────┘"AI 可查 vs 必须理解"清单
AI 可查:
✅ ES 的各种 Query DSL 语法
✅ 分词器的具体配置参数
✅ FST 的数据结构细节
必须理解:
🔴 倒排索引和正排索引的核心区别
🔴 为什么 ES 搜索比 MySQL LIKE 快
🔴 索引时分词和搜索时分词的区别
🔴 ik_max_word vs ik_smart 的适用场景
🔴 Completion Suggester 的应用场景学习状态:🟡 开始学习