为什么 MySQL 的 LIKE 查询这么慢?Elasticsearch 倒排索引完全解析

为什么 MySQL 的 LIKE 查询这么慢?Elasticsearch 倒排索引完全解析

摘要:在 MySQL 里用 LIKE '%关键词%' 搜 1000 万条记录,数据库只能逐行扫描,慢得让人崩溃。Elasticsearch 用倒排索引把"文档→词条"翻转成"词条→文档",实现海量文本下毫秒级全文检索。本文从倒排索引原理出发,拆解 ES 的索引设计、分词机制、DSL 查询,以及它在 RAG 混合检索中的角色。

📑 目录

  • MySQL 的困境:为什么 LIKE 全文检索这么慢?
  • 倒排索引:把"找书"变成"查目录"
  • ES 核心概念:索引、文档、字段类型
  • 分词机制:text vs keyword
  • 中文分词:standard 不够用,IK 来救场
  • ES 的 CRUD 操作:和 MySQL 有什么不同?
  • 检索 API:全文检索、精确检索、分页排序
  • ES 与 MySQL 的协作:存查分离
  • ES 在 RAG 中的角色:关键词检索 + 语义检索的混合方案
  • 互动讨论

MySQL 的困境:为什么 LIKE 全文检索这么慢?

MySQL 是关系型数据库,擅长字段查找和表关联。但当你需要在 content 这样的长文本字段里做全文检索时,MySQL 就力不从心了。

sql

sql 复制代码
-- ❌ 不推荐:全文检索用 LIKE
SELECT * FROM articles WHERE content LIKE '%高血压%';

这条 SQL 的问题在于:

问题 原因
无法使用索引 LIKE '%关键词%' 前置通配符导致 B+树索引失效
全表扫描 数据库只能逐行读取 content 字段,逐字匹配
数据量大就崩溃 1000 万条记录,每条都要读一遍,耗时不可接受

根本原因 :MySQL 使用的是正排索引------通过主键 ID 找到对应的行,再去字段里匹配内容。检索方向是"文档 → 关键词"。

而全文检索的需求是:给我一个关键词,快速找到所有包含它的文档。这个方向恰好是反的。

倒排索引:把"找书"变成"查目录"

倒排索引(Inverted Index) 是 ES 的核心底层机制,它把检索方向翻转了:

索引类型 方向 类比
正排索引(MySQL) 文档 → 关键词 从第一页翻到最后一页,逐页找关键词
倒排索引(ES) 关键词 → 文档 先查书末的"关键词索引表",直接跳到对应页码

物理实现细节

ES 的倒排索引并不是简单的"关键词 → 文档 ID",它有一套精密的存储结构:

  1. DocID:每个文档分配一个内部整数 ID(段内唯一)
  2. 倒排列表 :存储 词条 → [DocID...] 的映射
  3. 词典 key :格式为 字段名:词条,区分不同字段(如 title:高血压content:高血压
  4. _source:存储原始 JSON 文档,通过 DocID 可以取回完整数据

关键认知 :ES 自己存了完整的文档副本(_source),不需要"回表"到 MySQL 取原始数据。数据是通过同步机制写入 ES 的。

为什么倒排索引快?

text

arduino 复制代码
MySQL 正排索引:
用户输入"高血压" → 遍历 1000 万行 → 逐行匹配 content 字段 → 找到 500 条

ES 倒排索引:
用户输入"高血压" → 查词典 "content:高血压" → 直接拿到 [DocID_1, DocID_2, ...] → 毫秒级返回

时间复杂度从 O(n) (全表扫描)降到 O(1) (词典查找)。

ES 核心概念:索引、文档、字段类型

ES 是面向文档的搜索引擎,它的概念和 MySQL 有对应关系:

ES 概念 MySQL 对应 说明
索引(Index) 表(Table) 一类文档的集合,如 article 索引
文档(Document) 行(Row) 一条 JSON 数据
字段(Field) 列(Column) 文档中的一个属性
映射(Mapping) 表结构(Schema) 定义字段类型和分词方式
DSL SQL ES 的查询语言,基于 JSON

ES 存储的是索引,运行在 9200 端口。Kibana 是 ES 的可视化工具,类似于 phpMyAdmin 之于 MySQL。

创建索引与映射

在 Kibana 中创建 article 索引,定义各字段的类型:

text

css 复制代码
PUT /article
{
    "mappings": {
        "properties": {
            "title": {
                "type": "text"
            },
            "content": {
                "type": "text"
            },
            "author": {
                "type": "keyword"
            },
            "createTime": {
                "type": "date"
            },
            "viewCount": {
                "type": "integer"
            }
        }
    }
}

字段类型说明

字段 类型 分词 用途
title text ✅ 分词 全文检索
content text ✅ 分词 全文检索
author keyword ❌ 不分词 精确匹配、聚合
createTime date --- 时间范围查询
viewCount integer --- 数值排序、范围查询

查看索引信息

text

bash 复制代码
GET /article/_mapping      # 查看映射
GET /article/_settings     # 查看设置
GET /_cat/indices?v&h=health,status,index,docs.count   # 查看所有索引

分词机制:text vs keyword

这是 ES 中最重要的设计决策。

类型 分词 适用场景 查询方式
text ✅ 自动分词,拆成多个词条建倒排索引 全文检索(标题、正文) match 查询
keyword ❌ 整值存储,不分词 精确匹配(作者、标签、状态码) term 查询

为什么 text 要分词?

假设 content 字段的值是 "Elasticsearch RAG 混合检索知识库",ES 会把它拆成多个词条:

text

arduino 复制代码
"Elasticsearch RAG 混合检索知识库"
    ↓ 分词(standard)
["elasticsearch", "rag", "混", "合", "检", "索", "知", "识", "库"]

然后为每个词条建立倒排索引。这样用户搜"检索"时,就能匹配到包含这个词的文档。

为什么 keyword 不分词?

如果 author 字段的值是 "AI开发者",使用 keyword 类型后,整个字符串作为一个词条 存储。搜 "AI""开发" 都匹配不到,必须搜完整的 "AI开发者" 才能命中。

keyword 的典型用途:作者名、标签、状态码、枚举值------这些字段需要"完全相等"的匹配,不需要分词。

中文分词:standard 不够用,IK 来救场

ES 内置的 standard 分词器对英文很友好,但对中文几乎是把每个字拆开:

text

bash 复制代码
POST /_analyze
{
    "analyzer": "standard",
    "text": "Elasticsearch RAG 混合检索知识库"
}

输出结果中,英文被合理分词,但中文被拆成了单字:。这样搜"检索"时无法精确匹配,因为倒排索引里存的是"检"和"索"两个独立的字。

IK 分词器

生产环境通常安装 IK 分词器,它提供两种中文分词模式:

分词器 粒度 特点 适用场景
ik_max_word 细粒度 尽可能多地拆分词汇 写入时建立索引(召回优先)
ik_smart 粗粒度 按语义拆分,性能更好 查询时使用(精度优先)

使用 ik_smart 对同一段文本分词:

text

bash 复制代码
POST /_analyze
{
    "analyzer": "ik_smart",
    "text": "Elasticsearch RAG 混合检索知识库"
}

输出结果会把中文按语义拆成 混合检索知识 等词条,而不是单字。

最佳实践 :写入时用 ik_max_word(建立更多索引,提高召回率),查询时用 ik_smart(精度更高,性能更好)。

ES 的 CRUD 操作:和 MySQL 有什么不同?

ES 的 API 看起来和 MySQL 很像,但底层操作完全不同------ES 会自动分词、建立倒排索引。

新增文档

text

bash 复制代码
# POST 不指定 ID,自动生成
POST /article/_doc
{
    "title": "Elasticsearch 全文检索入门",
    "content": "ES 基于倒排索引与 BM25 实现全文索引,适用于文本检索场景",
    "author": "后端开发者",
    "createTime": "2026-09-14",
    "viewCount": 120
}

# PUT 指定 ID(全量替换,ID 不存在则新增)
PUT /article/_doc/1001
{
    "title": "RAG 混合检索实战",
    "content": "ES 负责关键词检索,Milvus 负责向量语义检索,结合使用更佳",
    "author": "AI开发者",
    "createTime": "2026-09-14",
    "viewCount": 236
}

注意:PUT 是全量替换------如果只传了部分字段,未传的字段会被覆盖为 null。局部修改要用 POST。

局部更新

text

bash 复制代码
POST /article/_update/1001
{
    "doc": {
        "viewCount": 999
    }
}

删除文档

text

bash 复制代码
# 条件删除
POST /article/_delete_by_query
{
    "query": {
        "match": {
            "author": "后端开发"
        }
    }
}

检索 API:全文检索、精确检索、分页排序

全文检索:match

match 查询会对查询文本进行分词,然后去倒排索引中匹配:

text

css 复制代码
GET /article/_search
{
    "query": {
        "match": {
            "content": "RAG 向量 检索"
        }
    }
}

查询文本 "RAG 向量 检索" 会被分词为 ["rag", "向量", "检索"],然后查找包含这些词条的文档。大小写不敏感,这在现实搜索中很实用。

多字段检索:multi_match

同时在多个字段中检索:

text

bash 复制代码
GET /article/_search
{
    "query": {
        "multi_match": {
            "query": "RAG",
            "fields": ["title", "content"]
        }
    }
}

精确检索:term

term 查询不会对查询文本分词,直接拿整段去匹配倒排索引中的词条:

text

bash 复制代码
GET /article/_search
{
    "query": {
        "term": {
            "author": "AI开发者"
        }
    }
}

关键区别term 是索引的最小单位,意味着不分词。它只能匹配 keyword 类型字段,或者 text 字段分词后的单个词条(不能匹配整个原始文本)。

限制输出字段与分页排序

text

bash 复制代码
GET /article/_search
{
    "_source": ["title", "author"],
    "from": 0,
    "size": 10,
    "sort": [
        { "viewCount": "desc" }
    ],
    "query": {
        "match_all": {}
    }
}
参数 作用
_source 限制返回的字段,减少网络传输
from + size 分页(先排序再分页)
sort 按指定字段排序

DSL:领域特定语言

ES 的查询语言叫 DSL(Domain Specific Language) ------它不是通用编程语言,而是专门用于搜索领域的 JSON 格式查询语言。

技术 查询语言
MySQL SQL
Milvus Embedding 向量检索
Elasticsearch DSL(JSON 格式)

ES 与 MySQL 的协作:存查分离

核心思想:MySQL 负责"存",ES 负责"查"。

维度 MySQL Elasticsearch
定位 业务数据存储,强事务 搜索加速层,全文检索
数据 完整业务数据 需要被搜索的字段副本
事务 ✅ ACID ❌ 不支持
写入可见性 实时 近实时(默认 1 秒)
适用场景 CRUD、关联查询 全文检索、日志分析

数据同步方案

ES 不会自动从 MySQL 同步数据,必须通过以下机制之一:

方案 实时性 侵入性 复杂度 适用场景
同步双写 简单项目
异步双写(MQ) 较高 企业级推荐
CDC 监听 Binlog 工业界主流
定时批量同步 低(分钟级) 报表分析

易错:以为 ES 会自动从 MySQL 同步数据。ES 不会主动拉取,必须通过双写、Canal、Logstash 等机制手动同步。

ES 在 RAG 中的角色:关键词检索 + 语义检索的混合方案

在 RAG(检索增强生成)系统中,ES 扮演着关键词检索 的角色,与 Milvus 的向量语义检索形成互补。

为什么需要混合检索?

向量数据库(Milvus)有一个问题:有时两个语义上十分接近的专业术语,所表达的意思几乎完全相反------比如"高血糖"和"低血糖"。纯语义检索容易导致匹配失准。

而关键词检索(ES)恰好能解决这个问题------它是精确匹配,不会把"高血糖"和"低血糖"混为一谈。

混合检索 = ES 关键词检索 + Milvus 相似度检索。二者并不冲突,而是相互合作才能实现最准确的检索。

ES vs Milvus

维度 Elasticsearch Milvus
核心定位 全文搜索与日志分析 向量数据库,AI 相似度搜索
检索方式 关键词匹配(BM25) 语义相似度(ANN)
数据模型 JSON 文档 高维向量
典型场景 商品搜索、日志分析 RAG、图像检索、推荐
混合检索 8.x 原生支持(RRF) 2.5+ 支持(需外接文本引擎)

混合检索的完整链路

text

css 复制代码
用户 query
  → 关键词检索(ES)+ 向量检索(Milvus)(召回优先,宁滥勿缺)
  → 融合排序(RRF / 加权)
  → Rerank 精排(精度优先,宁缺毋滥)
  → 最终 top 3~5 送给 LLM

RRF(Reciprocal Rank Fusion) 是常用的融合算法:

text

arduino 复制代码
得分 = 1 / (k + 排名)    # k 通常取 60

两路得分相加,按综合得分重新排序。融合是并集,不是交集------单路强推的文档也会被召回。

Rerank:精排阶段

Rerank 是对初步检索结果再进行精排:

类型 方法 成本 精度
规则重排 关键词计数、字段权重 极低
Cross-Encoder 专用重排模型(如 BGE-reranker)
LLM 打分 通用大模型逐篇打分 最高但不稳定

Rerank 是省 token 的主力:用 Cross-Encoder 把候选从 top 100 压到 top 5,不消耗 LLM token。如果让 LLM 对大量文档逐篇打分,token 成本会爆炸。

JavaScript 集成示例

typescript

php 复制代码
import { Client } from '@elastic/elasticsearch';

const esClient = new Client({ node: 'http://localhost:9200' });

// 混合检索(BM25 + kNN + RRF 融合)
const response = await esClient.search({
  index: 'medical_articles',
  body: {
    query: { match: { content: queryText } },
    knn: {
      field: 'content_vector',
      query_vector: queryVector,
      k: 10,
      num_candidates: 100,
    },
    rank: { rrf: {} },
    size: 5,
  },
});

互动讨论

💬 ES 能完全替代 MySQL 吗?

不能。ES 不保证 ACID,写入是近实时(默认 1 秒可见),适合搜索但不适合强事务场景。生产环境用 MySQL 存业务数据,ES 存需要被搜索的字段副本。

💬 为什么 LIKE '%关键词%' 在 MySQL 中这么慢?

前置通配符导致 B+树索引失效,数据库只能全表扫描、逐行匹配。ES 的倒排索引把"文档→词条"翻转成"词条→文档",查关键词直接定位文档 ID 列表,时间复杂度从 O(n) 降到 O(1)。

💬 textkeyword 应该怎么选?

需要全文检索的字段(标题、正文)用 text(会分词,支持 match 查询);需要精确匹配的字段(作者、标签、状态码)用 keyword(不分词,支持 term 查询)。

💬 ES 的中文分词为什么需要 IK?

ES 内置的 standard 分词器对中文几乎按单字拆分,导致"检索"被拆成"检"和"索"两个独立词条,搜索时无法精确匹配。IK 分词器按语义拆词,ik_max_word 用于写入(召回优先),ik_smart 用于查询(精度优先)。

💬 混合检索中 RRF 融合是取交集吗?

不是。RRF 是并集------两路各自召回的结果都会被保留,然后按排名加权融合重新排序。单路强推的文档也会被召回,确保不会漏掉任何一路认为相关的内容。

相关推荐
YDS8291 小时前
AI Agent 脚手架 —— 脚手架工程化和Maven私服
ai·agent·spring ai
ba_pi2 小时前
springAI2.0接入mcp读取mysql
java·agent·spring ai
七牛云行业应用2 小时前
WorkBuddy自定义模型失败怎么办?从接口鉴权到协议兼容的完整排查
人工智能·agent·ai编程
深圳市爱派派智能科技有限公司3 小时前
告别部署繁琐:LlamaPi 一键搭建本地 Agent 推理底座
rk3588·agent·openai api·本地部署·边缘ai·端侧大模型·llamapi
Elastic 中国社区官方博客14 小时前
列式存储并不等同于列式数据库。Columnar 模式为 Elasticsearch 带来了什么
大数据·运维·数据库·elasticsearch·搜索引擎
65岁退休Coder16 小时前
PI Agent 开发一个生产级 Harness
后端·node.js·agent
贾伟康16 小时前
【HarmonyOS 7新能力|026】Agent Framework Kit工程封装:把接入逻辑放进可维护的分层结构
agent·harmonyos·arkts·a2a·harmonyos 7