搜索引擎知识体系 / Search Engine Knowledge System
📅 创建时间:2026-07-28 🏷️ 标签:#搜索引擎 #Elasticsearch #Meilisearch #倒排索引 #全文搜索 #向量搜索 📚 前置知识:[[../../00-overview]](中间件全景) [[../../06-databases-and-data-access/00-overview]](数据库基础)
📋 本章目标
- 理解搜索的三种范式演变:数据库 LIKE → 全文索引 → 向量搜索
- 掌握倒排索引的核心原理:分词、倒排表、跳表、词典
- 理解搜索引擎的五大核心组件及其协作方式
- 能够对比 Elasticsearch、Meilisearch、OpenSearch、Typesense 的差异与适用场景
- 掌握搜索架构设计:应用层 → 搜索服务 → 索引 → 数据源同步
- 理解索引策略的核心决策:Mapping 设计、分词器选择、Field 类型
第1部分:搜索的三种范式
1.1 搜索问题的本质
搜索不是一个技术问题,而是一个匹配问题:用户脑子里有一个"意图",系统里有一堆"数据",搜索就是把意图和数据匹配起来。
┌─────────────────────────────────────────────────────────────┐
│ 搜索的本质:意图匹配 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 用户侧 系统侧 │
│ ┌──────────────────┐ ┌──────────────────────────────┐ │
│ │ 脑子里的"意图" │ │ 数据库里的"文档" │ │
│ │ │ │ │ │
│ │ "我要买个 │ │ {name: "小米 13 Pro"} │ │
│ │ 小米手机" ─────┼───→│ {name: "红米 Note 12"} │ │
│ │ │ │ {name: "iPhone 15"} │ │
│ │ 实际上想找: │ │ {name: "Xiaomi 13"} │ │
│ │ "小米13系列" │ │ │ │
│ └──────────────────┘ └──────────────────────────────┘ │
│ │
│ 矛盾:用户说的 ≠ 用户想的 ≠ 系统存的 │
│ │
│ 搜索就是在"说的"和"存的"之间找到最匹配的那条。 │
│ │
└─────────────────────────────────────────────────────────────┘1.2 范式一:数据库 LIKE —— 字符级别的暴力匹配
┌─────────────────────────────────────────────────────────────┐
│ 范式一:数据库 LIKE │
├─────────────────────────────────────────────────────────────┤
│ │
│ 实现方式: │
│ SELECT * FROM products │
│ WHERE name LIKE '%小米手机%' │
│ │
│ 匹配逻辑:字符级别的子串匹配 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ "小米 13 手机" 包含 "小米手机" 吗? │ │
│ │ 逐字符比对:小=小 ✓ 米=米 ✓ 手机≠13 ✗ │ │
│ │ 结果:不匹配! │ │
│ │ │ │
│ │ 用户搜"小米手机"找不到"小米 13 手机" │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 致命缺陷: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. 无法处理词汇变体 │ │
│ │ "跑" ≠ "跑步" ≠ "跑了" ≠ "跑着" │ │
│ │ │ │
│ │ 2. 无法处理顺序差异 │ │
│ │ "小米手机" ≠ "手机小米" │ │
│ │ │ │
│ │ 3. 全表扫描,不可扩展 │ │
│ │ 100 万行 LIKE '%x%' → 100 万次磁盘 IO │ │
│ │ │ │
│ │ 4. 无相关性排序 │ │
│ │ 所有匹配结果一视同仁,无法知道哪个更相关 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘1.3 范式二:全文索引 —— 词汇级别的倒排匹配
┌─────────────────────────────────────────────────────────────┐
│ 范式二:全文索引 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 核心思路:不再按字符匹配,而是按"词"匹配 │
│ │
│ 索引构建过程: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 文档: "小米 13 手机,性价比很高" │ │
│ │ ↓ 分词 │ │
│ │ 词汇: [小米, 13, 手机, 性价比, 很高] │ │
│ │ ↓ 建倒排索引 │ │
│ │ 小米 → [文档1] │ │
│ │ 13 → [文档1] │ │
│ │ 手机 → [文档1] │ │
│ │ 性价比 → [文档1] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 搜索过程: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 用户输入: "小米手机" │ │
│ │ ↓ 分词 │ │
│ │ 查询词: [小米, 手机] │ │
│ │ ↓ 查倒排索引 │ │
│ │ 小米 → [文档1, 文档5, 文档9] │ │
│ │ 手机 → [文档1, 文档3, 文档7] │ │
│ │ ↓ 取交集 │ │
│ │ 结果: [文档1] → "小米 13 手机" ← 找到了! │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 关键突破: │
│ • 分词后,"小米手机"和"小米 13 手机"都能匹配 │
│ • 倒排索引让查找从 O(n) 降到 O(1) │
│ • 可以计算相关性得分(TF-IDF / BM25) │
│ │
└─────────────────────────────────────────────────────────────┘1.4 范式三:向量搜索 —— 语义级别的相似度匹配
┌─────────────────────────────────────────────────────────────┐
│ 范式三:向量搜索 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 核心思路:把文字映射到高维向量空间,用距离衡量语义相似度 │
│ │
│ 索引构建过程: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 文档: "小米 13 手机" │ │
│ │ ↓ Embedding 模型(如 text-embedding-3-small) │ │
│ │ 向量: [0.23, -0.15, 0.78, ..., 0.42] (1536维) │ │
│ │ ↓ 存入向量数据库 │ │
│ │ ANN 索引(近似最近邻) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 搜索过程: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 用户输入: "性价比高的安卓机" │ │
│ │ ↓ Embedding │ │
│ │ 查询向量: [0.21, -0.18, 0.75, ..., 0.40] │ │
│ │ ↓ 向量相似度计算(余弦相似度) │ │
│ │ 结果: "小米 13 手机" ← 即使没有相同词汇! │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 三种范式的适用边界: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ LIKE │ 全文索引 │ 向量搜索 │ │
│ │ ─────────────────┼─────────┼─────────┼────────── │ │
│ │ 精确匹配 │ ✅ │ ✅ │ ❌ │ │
│ │ 词汇变体 │ ❌ │ ✅* │ ✅ │ │
│ │ 语义理解 │ ❌ │ ❌ │ ✅ │ │
│ │ 多语言 │ ❌ │ ✅* │ ✅ │ │
│ │ 可解释性 │ ✅ │ ✅ │ ❌ │ │
│ │ 速度 │ 慢 │ 快 │ 中 │ │
│ │ 资源消耗 │ 低 │ 中 │ 高 │ │
│ │ *依赖分词器质量 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘1.5 三种范式的演进关系
┌─────────────────────────────────────────────────────────────┐
│ 搜索范式的演进路线 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 2000s 2010s 2020s 未来 │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ LIKE │ ──→ │ 倒排 │ ──→ │ 向量 │ ──→ │ 混合 │ │
│ │ │ │ 索引 │ │ 搜索 │ │ 搜索 │ │
│ └──────┘ └──────┘ └──────┘ └──────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ 字符匹配 词汇匹配 语义匹配 多路融合 │
│ 精确但死板 灵活但依赖 智能但黑盒 兼收并蓄 │
│ 分词质量 │
│ │
│ 今天的主流:全文索引 + 向量搜索 = 混合搜索(Hybrid Search)│
│ • Elasticsearch 8.x+ 内置向量搜索 │
│ • 先用倒排索引做关键词召回,再用向量做语义精排 │
│ • 结合两者优势:精确 + 语义 │
│ │
└─────────────────────────────────────────────────────────────┘第2部分:倒排索引原理
2.1 从文档到倒排表
倒排索引是全文搜索引擎的基石。理解它不需要任何 ES 知识——它就是一本"词汇 → 页码"的字典。
┌─────────────────────────────────────────────────────────────┐
│ 倒排索引的构建过程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 原始文档集合: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Doc1: "Elasticsearch is a search engine" │ │
│ │ Doc2: "Meilisearch is fast" │ │
│ │ Doc3: "Elasticsearch engine is powerful" │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Step 1: 分词(Tokenization) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Doc1 → [elasticsearch, is, a, search, engine] │ │
│ │ Doc2 → [meilisearch, is, fast] │ │
│ │ Doc3 → [elasticsearch, engine, is, powerful] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Step 2: 规范化(Normalization)—— 小写、词干提取 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Elasticsearch → elasticsearch │ │
│ │ running → run(词干提取 stemming) │ │
│ │ better → good(词形还原 lemmatization) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Step 3: 构建倒排表(Inverted Index) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Term │ Posting List (文档ID列表) │ │
│ │ ────────────────┼────────────────────────────────── │ │
│ │ elasticsearch │ [1, 3] │ │
│ │ search │ [1] │ │
│ │ engine │ [1, 3] │ │
│ │ meilisearch │ [2] │ │
│ │ fast │ [2] │ │
│ │ powerful │ [3] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Step 4: 排序(Sort)—— 按 Term 字典序排列以便二分查找 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ elasticsearch → [1, 3] │ │
│ │ engine → [1, 3] │ │
│ │ fast → [2] │ │
│ │ meilisearch → [2] │ │
│ │ powerful → [3] │ │
│ │ search → [1] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘2.2 Posting List 的数据结构
Posting List(倒排列表)不只存文档 ID,还存位置、词频等元信息:
┌─────────────────────────────────────────────────────────────┐
│ Posting List 完整结构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Term: "elasticsearch" │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ DocID │ TF │ Positions │ Fields │ │
│ │ ──────┼─────┼────────────┼────────────────────── │ │
│ │ 1 │ 1 │ [0] │ title │ │
│ │ 3 │ 1 │ [0] │ title │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ DocID: 文档唯一 ID │
│ TF (Term Frequency): 该词在该文档中出现的次数 │
│ Positions: 该词在文档中的位置(用于短语查询) │
│ Fields: 该词出现在文档的哪些字段中 │
│ │
│ 这些元信息支撑了: │
│ • 短语查询:"search engine" 要求两个词相邻 │
│ • 字段权重:title 匹配比 body 匹配更重要 │
│ • 相关性评分:TF-IDF / BM25 依赖词频数据 │
│ │
└─────────────────────────────────────────────────────────────┘2.3 跳表(Skip List)—— 加速多词查询
当查询包含多个词时,需要对多个 Posting List 取交集。跳表避免了对短列表的完整扫描。
┌─────────────────────────────────────────────────────────────┐
│ 跳表加速取交集 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 查询: "elasticsearch engine" │
│ │
│ elasticsearch → [1, 3, 7, 12, 15, 20, 25, 30, ...100] │
│ engine → [3, 8, 15, 22, 30, ...80] │
│ │
│ 无跳表时:O(len(A) + len(B))—— 需要扫描每个元素 │
│ │
│ 有跳表时: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ elasticsearch posting list (带跳表): │ │
│ │ [1] →→→→→→ [15] →→→→→→ [30] →→→ ... │ │
│ │ ↓ ↗ ↓ ↗ ↓ │ │
│ │ [3,7,12] [20,25] [35,40,...] │ │
│ │ │ │
│ │ 查询 engine=3: │ │
│ │ 1) 检查 elasticsearch[0]=1,1 < 3,跳! │ │
│ │ 2) 跳到 15,15 > 3,回退到区间 [3,7,12] │ │
│ │ 3) 找到 3,匹配! │ │
│ │ │ │
│ │ 跳过了大量中间元素,大幅减少比较次数 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ES 使用跳表 + BitSet 两种策略: │
│ • 跳表:适合长列表且分布稀疏的场景 │
│ • BitSet:适合稠密列表,用位运算取交集(极快) │
│ │
└─────────────────────────────────────────────────────────────┘2.4 词典(Term Dictionary)与 Term Index
┌─────────────────────────────────────────────────────────────┐
│ 词典与 Term Index │
├─────────────────────────────────────────────────────────────┤
│ │
│ 查一个词"elasticsearch"在不在索引里,不能线性扫描所有词 │
│ │
│ 解决方案:FST (Finite State Transducer) —— ES 的核心魔法 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Term Index (FST, 内存) │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ 前缀: "ela" → 指向磁盘上某个 block │ │ │
│ │ │ 前缀: "eng" → 指向磁盘上某个 block │ │ │
│ │ │ 前缀: "mei" → 指向磁盘上某个 block │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ │ ↓ 定位到磁盘 block │ │
│ │ Term Dictionary (磁盘,按 block 组织) │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ elasticsearch → 指向 Posting List 的指针 │ │ │
│ │ │ engine → 指向 Posting List 的指针 │ │ │
│ │ │ fast → 指向 Posting List 的指针 │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ │ ↓ 通过指针读取 │ │
│ │ Posting Lists (磁盘) │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ elasticsearch: [1, 3, 7, 12, ...] │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ FST 的优势: │
│ • 极高的压缩率:共享前缀,比 HashMap 小 10-20 倍 │
│ • 支持前缀查询:给出"ela"即可定位到 elasticsearch 附近 │
│ • 常驻内存:虽然索引数据在磁盘,但词典可以全放内存 │
│ │
│ 为什么 Lucene/ES 能在 TB 级数据上毫秒级响应? │
│ 答案:FST 词典 + 跳表/BitSet + OS Page Cache │
│ │
└─────────────────────────────────────────────────────────────┘第3部分:搜索引擎核心组件
3.1 五大核心组件全景
┌─────────────────────────────────────────────────────────────┐
│ 搜索引擎核心组件架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 用户查询 │
│ ↓ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ① 分词器 (Tokenizer/Analyzer) │ │
│ │ 把"小米13手机"拆成 → [小米, 13, 手机] │ │
│ └─────────────────────────┬───────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ② 查询解析器 (Query Parser) │ │
│ │ 把 "price:[100 TO 500] AND brand:小米" │ │
│ │ 解析成 AST 语法树 │ │
│ └─────────────────────────┬───────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ③ 相关性评分 (Scoring) │ │
│ │ BM25 / TF-IDF 计算每个文档与查询的匹配度 │ │
│ └─────────────────────────┬───────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ④ 排序与过滤 (Sorting & Filtering) │ │
│ │ 按 score 排序 + 应用 filter 条件 + 分页 │ │
│ └─────────────────────────┬───────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ⑤ 结果高亮与聚合 (Highlighting & Aggregation) │ │
│ │ 高亮匹配片段 + Facet 统计 + 聚合分析 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ 返回给用户 │
│ │
└─────────────────────────────────────────────────────────────┘3.2 分词器(Analyzer)的组成
┌─────────────────────────────────────────────────────────────┐
│ 分词器三件套 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Analyzer = Character Filter → Tokenizer → Token Filter │
│ │
│ 示例:处理 HTML 文本 "<p>Hello World!</p>" │
│ │
│ Step 1: Character Filter(字符过滤器) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 输入: "<p>Hello World!</p>" │ │
│ │ html_strip: 去掉 HTML 标签 │ │
│ │ 输出: "Hello World!" │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ Step 2: Tokenizer(分词器) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 输入: "Hello World!" │ │
│ │ standard: 按空格和标点拆分 │ │
│ │ 输出: [Hello, World] │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ Step 3: Token Filter(词元过滤器) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 输入: [Hello, World] │ │
│ │ lowercase: 转小写 → [hello, world] │ │
│ │ stop: 去掉停用词(is, the, a...) │ │
│ │ 输出: [hello, world] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 中文分词器特别重要(中文没有空格分隔): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ik_max_word (IK 分词器,细粒度): │ │
│ │ "中华人民共和国" → [中华人民共和国, 中华人民, │ │
│ │ 中华, 华人, 人民共和国, │ │
│ │ 人民, 共和国, 共和, 国] │ │
│ │ │ │
│ │ ik_smart (IK 分词器,粗粒度): │ │
│ │ "中华人民共和国" → [中华人民共和国] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘3.3 查询解析器(Query Parser)
┌─────────────────────────────────────────────────────────────┐
│ 查询解析流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 用户输入(可能是自然语言、关键词、或 DSL): │
│ "小米手机 AND 价格:<5000 NOT 二手" │
│ ↓ │
│ 词法分析:拆成 Token │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [小米手机, AND, 价格:<5000, NOT, 二手] │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ 语法分析:构建 AST │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ AND │ │
│ │ / \ │ │
│ │ TermQuery NOT │ │
│ │ "小米手机" \ │ │
│ │ RangeQuery │ │
│ │ 价格<5000 │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ 生成 Lucene Query 对象 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ BooleanQuery(must=[TermQuery("小米手机"), │ │
│ │ RangeQuery(0-5000)], │ │
│ │ must_not=[TermQuery("二手")]) │ │
│ └─────────────────────────────────────────────────────┘ │
│ ↓ │
│ 执行查询 → 倒排索引取交集 → 评分 → 排序 → 返回 │
│ │
└─────────────────────────────────────────────────────────────┘3.4 相关性评分(BM25)
┌─────────────────────────────────────────────────────────────┐
│ 从 TF-IDF 到 BM25 │
├─────────────────────────────────────────────────────────────┤
│ │
│ TF-IDF(老方案,ES 5.x 前): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Score = TF × IDF │ │
│ │ │ │
│ │ TF (词频): 词在文档中出现的次数 │ │
│ │ → 问题:出现 100 次 ≠ 100 倍相关 │ │
│ │ │ │
│ │ IDF (逆文档频率): log(总文档数 / 包含词的文档数) │ │
│ │ → 出现越少的词越重要("elasticsearch" vs "is") │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ BM25(当前标准,ES 5.x+): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Score = IDF × (TF × (k1 + 1)) / (TF + k1 × (...)) │ │
│ │ │ │
│ │ 改进点: │ │
│ │ 1. 词频饱和:TF 增加到一定程度后不再增加分数 │ │
│ │ → 一篇文档写 100 个"手机",不会比写 5 个高20倍 │ │
│ │ │ │
│ │ 2. 文档长度归一化:长文档不会被"惩罚"过度 │ │
│ │ → 比 TF-IDF 更公平地对待不同长度的文档 │ │
│ │ │ │
│ │ 3. 可调参数:k1(词频饱和度)和 b(长度归一化程度) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘第4部分:搜索引擎全景对比
4.1 四大搜索引擎定位
┌─────────────────────────────────────────────────────────────┐
│ 搜索引擎选型全景对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Elasticsearch OpenSearch Meilisearch Typesense
│ ──────────┼──────────────────────────────────────────────
│ 起源 │ 2010, Shay 2021, AWS 2018, 法国 2015, 印度
│ │ Banon Fork ES Rust 编写 C++ 编写
│ ──────────┼──────────────────────────────────────────────
│ 许可证 │ SSPL/Elastic Apache 2.0 MIT GPLv3
│ ──────────┼──────────────────────────────────────────────
│ 定位 │ 全文搜索 + ES 的开源 轻量即时 轻量即时
│ │ 分析引擎 替代品 搜索 搜索
│ ──────────┼──────────────────────────────────────────────
│ 适用规模 │ PB 级 PB 级 TB 级 TB 级
│ ──────────┼──────────────────────────────────────────────
│ 部署难度 │ 高 高 极低 低
│ ──────────┼──────────────────────────────────────────────
│ 内存需求 │ 2GB+ (heap) 2GB+ (heap) 512MB+ 512MB+
│ ──────────┼──────────────────────────────────────────────
│ 搜索延迟 │ <100ms <100ms <50ms <50ms
│ ──────────┼──────────────────────────────────────────────
│ 聚合分析 │ 极强 极强 有限 有限
│ ──────────┼──────────────────────────────────────────────
│ 中文支持 │ IK 分词器 IK 分词器 内置 Jieba 内置
│ ──────────┼──────────────────────────────────────────────
│ 向量搜索 │ ✓ (8.x+) ✓ (2.x+) ✗ (实验性) ✗ (实验性)
│ ──────────┼──────────────────────────────────────────────
│ SQL 支持 │ ✓ ✓ ✗ ✗
│ ──────────┼──────────────────────────────────────────────
│ 适用场景 │ 日志分析 ES 替代 站内搜索 SaaS 搜索
│ │ 企业搜索 合规要求 即时搜索 电商搜索
│ │ 指标聚合 成本敏感 文档搜索 文档搜索
│ │
└─────────────────────────────────────────────────────────────┘4.2 核心差异:使用场景矩阵
┌─────────────────────────────────────────────────────────────┐
│ 场景 → 引擎映射 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 场景 推荐引擎 原因 │
│ ───────────────────────────────────────────────────── │
│ 电商商品搜索(百万 SKU) ES / OpenSearch 聚合+过滤强 │
│ 博客/文档站内搜索 Meilisearch 部署简单 │
│ 日志分析(ELK Stack) ES 生态完整 │
│ SaaS 产品内置搜索 Typesense 易于嵌入 │
│ 合规要求(Apache 2.0) OpenSearch 许可证友好 │
│ 小项目/个人项目 Meilisearch 零配置 │
│ RAG 应用的知识库搜索 ES/OpenSearch 向量搜索成熟 │
│ 实时 Dashboard 搜索 Typesense 速度极快 │
│ │
│ 一句话总结: │
│ • 需要分析能力?→ ES / OpenSearch │
│ • 只需要搜索?→ Meilisearch / Typesense │
│ • 担心许可证?→ OpenSearch / Meilisearch │
│ • 追求开发体验?→ Meilisearch │
│ │
└─────────────────────────────────────────────────────────────┘第5部分:搜索架构设计
5.1 经典搜索架构
┌─────────────────────────────────────────────────────────────┐
│ 搜索系统整体架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 应用层 (App) │ │
│ │ ┌────────────────┐ ┌────────────────────────┐ │ │
│ │ │ 搜索 UI │ │ 管理后台 │ │ │
│ │ │ (搜索框+结果) │ │ (索引管理+同义词) │ │ │
│ │ └───────┬────────┘ └───────────┬────────────┘ │ │
│ └──────────┼──────────────────────┼───────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 搜索服务层 (Search Service) │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ │
│ │ │ 查询构建 │ │ 结果处理 │ │ 索引管理 │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ • 分词 │ │ • 高亮 │ │ • bulk 写入 │ │ │
│ │ │ • 纠错 │ │ • 排序 │ │ • reindex │ │ │
│ │ │ • 改写 │ │ • 分页 │ │ • alias 切换 │ │ │
│ │ │ • 过滤 │ │ • 聚合 │ │ • mapping 更新 │ │ │
│ │ └─────┬────┘ └────┬─────┘ └────────┬─────────┘ │ │
│ └────────┼───────────┼───────────────┼───────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 搜索引擎集群 (ES / Meilisearch) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Node 1 │ │ Node 2 │ │ Node 3 │ │ │
│ │ │ Shard 0 │ │ Shard 1 │ │ Shard 2 │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ └────────────────┬────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 数据源同步 │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │
│ │ │ CDC │ │ 定时全量 │ │ 应用双写 │ │ │
│ │ │ (Canal, │ │ 同步 │ │ │ │ │
│ │ │ Debezium│ │ (cron) │ │ │ │ │
│ │ └─────┬────┘ └────┬─────┘ └──────┬───────┘ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ 主数据库 (MySQL / PostgreSQL) │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘5.2 数据同步策略对比
┌─────────────────────────────────────────────────────────────┐
│ 三种同步策略对比 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 策略1:应用双写 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 应用代码: │ │
│ │ db.save(product); // 写数据库 │ │
│ │ es.index(product); // 同时写 ES │ │
│ │ │ │
│ │ 优点:实时性最高 │ │
│ │ 缺点:数据一致性难以保证,ES 写入失败需要补偿 │ │
│ │ 适用:对实时性要求极高的小规模场景 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 策略2:CDC (Change Data Capture) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ MySQL binlog → Canal/Debezium → Kafka → ES Sink │ │
│ │ │ │
│ │ 优点:解耦、可靠、可回溯、不侵入业务代码 │ │
│ │ 缺点:引入 Kafka + Canal 等额外组件,运维复杂 │ │
│ │ 适用:中大型项目,有运维能力的团队 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 策略3:定时全量/增量同步 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ cron job → SELECT * FROM products │ │
│ │ WHERE updated_at > :last_sync │ │
│ │ → bulk index to ES │ │
│ │ │ │
│ │ 优点:实现简单,无需额外组件 │ │
│ │ 缺点:有延迟(分钟级),全量同步资源消耗大 │ │
│ │ 适用:对实时性要求不高(分钟级延迟可接受)的场景 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘第6部分:索引策略
6.1 Mapping 设计核心原则
┌─────────────────────────────────────────────────────────────┐
│ Mapping 设计决策树 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 这个字段用来做什么? │ │
│ │ ├── 全文搜索 → text + 配置分词器 │ │
│ │ ├── 精确匹配 → keyword │ │
│ │ ├── 范围查询 → integer / long / date │ │
│ │ ├── 排序 → keyword / numeric │ │
│ │ ├── 聚合 → keyword / numeric │ │
│ │ └── 不需要搜索 → enabled: false (节省空间) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Mapping 示例(商品搜索): │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ { │ │
│ │ "properties": { │ │
│ │ "title": { │ │
│ │ "type": "text", // 全文搜索 │ │
│ │ "analyzer": "ik_max_word", // 中文分词 │ │
│ │ "fields": { │ │
│ │ "keyword": { // 精确匹配 + 排序 │ │
│ │ "type": "keyword" │ │
│ │ } │ │
│ │ } │ │
│ │ }, │ │
│ │ "price": {"type": "float"}, // 范围查询 │ │
│ │ "category": {"type": "keyword"}, // 精确匹配 │ │
│ │ "created_at": {"type": "date"}, // 范围查询 │ │
│ │ "description": { │ │
│ │ "type": "text", │ │
│ │ "analyzer": "ik_max_word" │ │
│ │ }, │ │
│ │ "stock": { │ │
│ │ "type": "integer" // 范围 + 排序 │ │
│ │ } │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘6.2 常见字段类型速查
┌─────────────────────────────────────────────────────────────┐
│ ES 字段类型速查表 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 数据类型 字段用途 搜索能力 │
│ ──────────────────────────────────────────────────── │
│ text 全文索引 分词、模糊、短语 │
│ keyword 标签、ID、枚举 精确匹配、排序、聚合 │
│ integer/long 数量、ID 范围、排序、聚合 │
│ float/double 价格、评分 范围、排序、聚合 │
│ date 时间戳 范围、date_histogram │
│ boolean 布尔标记 精确过滤 │
│ geo_point 经纬度 距离排序、范围 │
│ nested 对象数组 独立查询子对象 │
│ join 父子文档 跨文档关联查询 │
│ dense_vector 向量嵌入 相似度搜索 (kNN) │
│ completion 自动补全 前缀匹配 (suggest) │
│ │
└─────────────────────────────────────────────────────────────┘6.3 分词器选择决策
┌─────────────────────────────────────────────────────────────┐
│ 分词器选择指南 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 中文场景: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ik_max_word:最大化分词,召回率最高 │ │
│ │ 例:"北京大学" → [北京, 大学, 北京大学, 京大] │ │
│ │ 适用:商品搜索、内容搜索(宁可多召回也不能漏) │ │
│ │ │ │
│ │ ik_smart:最粗粒度分词,精确率最高 │ │
│ │ 例:"北京大学" → [北京大学] │ │
│ │ 适用:特定领域搜索(法律、医学等专业术语) │ │
│ │ │ │
│ │ jieba:Python 社区流行,ES 有插件 │ │
│ │ 适用:已有 jieba 积累自定义词典的团队 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 英文场景: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ standard:默认分词器,按单词边界拆分 │ │
│ │ "running quickly" → [running, quickly] │ │
│ │ │ │
│ │ + stemmer:词干提取 │ │
│ │ "running" → "run", "jumped" → "jump" │ │
│ │ 适用:英文内容搜索(提高召回率) │ │
│ │ │ │
│ │ + stop words:停用词过滤 │ │
│ │ "the quick brown fox" → [quick, brown, fox] │ │
│ │ 适用:减少索引大小,加速搜索 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 多语言场景:Meilisearch 表现优于 ES(内置语言检测) │
│ │
└─────────────────────────────────────────────────────────────┘核心总结
总结1:三种搜索范式
范式一(LIKE)→ 范式二(全文索引)→ 范式三(向量搜索)
字符匹配 词汇匹配 语义匹配
简单但脆弱 成熟可靠 智能但黑盒当前生产环境的主流是全文索引为主 + 向量搜索为辅的混合模式。
总结2:倒排索引核心数据结构
Term Dictionary (FST, 内存)
→ Term → Posting List 指针
→ Posting List: [DocID, TF, Positions, Fields]
→ 跳表 / BitSet 加速取交集总结3:搜索引擎选型决策
| 条件 | 推荐引擎 |
|---|---|
| 需要聚合/日志分析 | Elasticsearch |
| 合规要求(Apache 2.0) | OpenSearch |
| 只需搜索、追求简单 | Meilisearch |
| SaaS 产品搜索 | Typesense |
| 小型项目 | PostgreSQL 全文搜索 |
总结4:搜索架构核心原则
1. ES 不是数据库——它是搜索服务的索引,数据源永远是主数据库
2. 数据同步是搜索系统最脆弱的一环——选对同步策略比调索引更重要
3. Mapping 设计决定搜索质量——花时间设计 mapping,不要用 dynamic mapping
4. 分词器是中文搜索的灵魂——IK 分词器的词典决定召回率章节测试
测试1:范式理解
LIKE '%手机%' 为什么无法找到"小米 13 手机"?倒排索引是如何解决这个问题的?
测试2:倒排索引
倒排索引中的 Posting List 包含哪些关键信息?每种信息支撑了什么搜索功能?
测试3:数据结构
FST (Finite State Transducer) 在倒排索引中扮演什么角色?它比 HashMap 好在哪里?
测试4:分词器
Analyzer 由哪三部分组成?每部分负责什么?
测试5:选型
一个 10 人团队的电商项目,需要商品搜索和简单的 facet 聚合,你会选 ES 还是 Meilisearch?为什么?
测试6:同步策略
什么是 CDC 同步?它比应用双写好在哪里?引入了什么代价?
测试7:Mapping
一个商品名称字段既是搜索的主体(需要分词),又要做 A-Z 排序(需要精确值),Mapping 应该怎么设计?
参考答案
测试1答案
答案:LIKE 是字符级别的子串匹配。"小米 13 手机"不包含连续子串"小米手机",所以 LIKE 找不到。倒排索引先把文档分词成 [小米, 13, 手机],再把用户输入分词成 [小米, 手机],然后对两个词分别查倒排表取交集,文档同时包含这两个词就能匹配。
测试2答案
答案:
- DocID:定位到具体文档(多词查询取交集)
- TF (Term Frequency):词频,支撑 BM25 相关性评分
- Positions:词在文档中的位置,支撑短语查询("search engine" 要求两个词连续)
- Fields:词出现在哪些字段,支撑字段权重(title 比 body 重要)
测试3答案
答案:FST 是 Term Dictionary 的底层数据结构,作用是将 term 映射到 Posting List 的磁盘位置。比 HashMap 好的原因:共享前缀(极高压缩率,比 HashMap 小 10-20 倍);支持前缀查询;可常驻内存。FST 是 ES 能在海量数据上毫秒级响应的关键。
测试4答案
答案:
- Character Filter(字符过滤器):预处理原始文本(去 HTML 标签、转换特殊字符)
- Tokenizer(分词器):将文本拆分成词元(按空格、标点、或语言规则拆分)
- Token Filter(词元过滤器):对词元做后处理(小写转换、停用词过滤、词干提取)
测试5答案
答案:需要 facet 聚合 → 推荐 ES(或 OpenSearch)。Meilisearch 的 facet 能力有限,适合简单筛选但不适合复杂的聚合分析。10 人团队有能力运维 ES 集群(至少比 ES 调优简单)。
测试6答案
答案:CDC (Change Data Capture) 通过监听数据库 binlog/WAL 实现数据变更的实时捕获,经 Kafka 等通道写入搜索引擎。比应用双写的优势:解耦(业务代码不需要感知搜索);可靠(binlog 天然有序且不丢);可回溯(重放 binlog 重建索引)。代价:引入 Kafka、Canal/Debezium 等额外组件,增加运维复杂度;有秒级延迟。
测试7答案
答案:使用 multi-field 设计:
{
"title": {
"type": "text",
"analyzer": "ik_max_word",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}搜索时用 title 字段(分词匹配),排序时用 title.keyword 字段(精确值排序)。
相关笔记
- [[/03-web/07-middleware/00-overview]] - 中间件全景
- [[/03-web/07-middleware/06-elasticsearch]] - ES 倒排索引深入
- [[./02-elasticsearch-queries]] - ES 查询 DSL 深入
- [[./03-elasticsearch-cluster]] - ES 集群规划与运维
- [[./04-meilisearch-and-alternatives]] - 轻量搜索替代方案
下一步学习
- [ ] 阅读 02 - Elasticsearch 查询 DSL
- [ ] 阅读 03 - Elasticsearch 集群
- [ ] 阅读 04 - Meilisearch 与替代方案
学习状态:🟡 开始学习