CC 咖啡猫的工作空间 Coding Space

搜索引擎

原始素材:4-服务器与后端/4.16-search-engines.md

前言

你在淘宝搜"红色连衣裙",0.1 秒内从几十亿商品中找到了最相关的结果——这背后是怎么做到的? 搜索引擎是互联网最核心的基础设施之一,从 Google 到电商站内搜索,它的核心原理都是一样的:倒排索引 + 相关性排序。

这篇文章会带你学什么?

学完这章后,你将获得:

  • 倒排索引 :理解搜索引擎最核心的数据结构
  • 分词技术 :了解中文分词的挑战和常见方案
  • 相关性排序 :掌握 TF-IDF 和 BM25 的基本原理
  • Elasticsearch :了解最流行的搜索引擎的架构和使用场景
  • 搜索优化 :掌握同义词、纠错、高亮等实用搜索功能
章节 内容 核心概念
第 1 章 倒排索引 正排索引 vs 倒排索引
第 2 章 分词与分析 中文分词、停用词、词干提取
第 3 章 相关性排序 TF-IDF、BM25
第 4 章 Elasticsearch 分布式架构、分片、副本
第 5 章 搜索优化 同义词、纠错、自动补全

0. 全景图:搜索的本质是什么?

搜索的本质是一个信息检索(Information Retrieval) 问题:给定一个查询,从海量文档中找到最相关的结果,并按相关性排序返回。

这个过程分为两个阶段:

  • 索引阶段(离线) :提前把所有文档处理好,建立高效的查找结构
  • 查询阶段(在线) :用户输入关键词时,快速找到匹配的文档并排序

为什么不能用数据库 LIKE 查询?

SELECT * FROM products WHERE name LIKE '%红色连衣裙%' 看起来能搜索,但它需要全表扫描 ——逐行检查每条记录。当数据量达到百万级时,这种查询会慢到不可用。倒排索引把这个 O(n) 的操作变成了 O(1) 的查找。

1. 倒排索引:搜索引擎的"心脏"

传统数据库用的是正排索引 :从文档 ID 找到文档内容。而搜索引擎用的是倒排索引 :从关键词找到包含它的文档列表。


1.1 倒排索引的结构

原始文档集合

文档 ID 内容
Doc 1 苹果是一种常见的水果
Doc 2 苹果公司发布了新款手机
Doc 3 我喜欢吃水果和蔬菜
Doc 4 这款手机的价格很实惠
Doc 5 水果店里有苹果和香蕉

倒排索引表(Inverted Index)

词语 出现的文档 出现频率 描述
苹果 [Doc 1, Doc 2, Doc 5] 3 在 3 个文档中出现
水果 [Doc 1, Doc 3, Doc 5] 3 在 3 个文档中出现
手机 [Doc 2, Doc 4] 2 在 2 个文档中出现
公司 [Doc 2] 1 在 1 个文档中出现
发布 [Doc 2] 1 在 1 个文档中出现
喜欢 [Doc 3] 1 在 1 个文档中出现
蔬菜 [Doc 3] 1 在 1 个文档中出现
价格 [Doc 4] 1 在 1 个文档中出现
实惠 [Doc 4] 1 在 1 个文档中出现
香蕉 [Doc 5] 1 在 1 个文档中出现
常见 [Doc 1] 1 在 1 个文档中出现

1.2 正排索引 vs 倒排索引

使用场景对比

索引类型 方向 查找方式 时间复杂度 适用场景 例子
正排索引 文档 → 内容 知道文档 ID,查询文档内容 O(1) 快速获取完整文档 数据库主键查询、文档详情页
倒排索引 关键词 → 文档列表 知道关键词,查找包含该词的所有文档 O(1) 查找词,O(k) 获取文档列表 全文搜索、模糊查询 搜索"苹果"找到 [Doc 1, 2, 5]

查询效率对比

场景:从 1000 万条商品中搜索包含"苹果"的商品

❌ 使用正排索引(LIKE 查询):
SELECT * FROM products WHERE name LIKE '%苹果%'
└─ 需要扫描全表 1000 万行
└─ 每行都要检查是否包含"苹果"
└─ 时间复杂度 O(n*m),其中 n=1000万,m=平均字符数
└─ 需要 10-100 秒 ❌ 不可用

✅ 使用倒排索引(ES 查询):
GET /products/_search?q=苹果
└─ 直接在索引中查找"苹果"
└─ 立即返回 [Doc1, Doc2, Doc5, ...]
└─ 时间复杂度 O(log n)
└─ 需要 0.01-0.1 秒 ✅ 秒级响应

1.3 倒排索引的构建过程

完整的构建流程

第 1 步:文档收集
  ↓
从数据库、爬虫、API 等获取原始文档
例如:获取电商系统中的 1000 万个商品信息

第 2 步:文本预处理
  ↓
删除 HTML 标签、特殊字符、进行大小写统一
示例:
  输入:<p>iPhone 15 Pro <b>Max</b></p>
  输出:iPhone 15 Pro Max

第 3 步:分词(Tokenization)
  ↓
将文本拆分为一个个词语(Token)
示例:
  输入:搜索引擎的基本原理
  输出:[搜索, 引擎, 的, 基本, 原理]

第 4 步:停用词过滤
  ↓
删除"的"、"了"、"是"等无意义的高频词
示例:
  输入:[搜索, 引擎, 的, 基本, 原理]
  输出:[搜索, 引擎, 基本, 原理]  ("的"被删除)

第 5 步:建立映射
  ↓
为每个词建立倒排表:记录词出现在哪些文档中、出现位置、频率等
结果:
  搜索 → {doc_id: [1, 3, 5, ...], positions: [12, 45, 78, ...], freq: 3}
  引擎 → {doc_id: [1, 2, 4, ...], positions: [18, 23, 56, ...], freq: 3}

第 6 步:持久化存储
  ↓
将索引写入磁盘,支持快速查找和高并发访问
存储格式:
  - B+ 树索引(快速查找)
  - 压缩存储(节省空间)
  - 分片存储(分布式扩展)

建立倒排索引的伪代码

// 简化版的倒排索引构建

class InvertedIndex {
  constructor() {
    this.index = {};  // {词语: [文档列表]}
  }

  // 添加文档
  addDocument(docId, content) {
    // 第 2 步:文本预处理
    const cleaned = this.preprocess(content);
    
    // 第 3 步:分词
    const tokens = this.tokenize(cleaned);
    
    // 第 4 步:停用词过滤
    const filtered = this.removeStopwords(tokens);
    
    // 第 5 步:建立倒排表
    for (const token of filtered) {
      if (!this.index[token]) {
        this.index[token] = [];
      }
      if (!this.index[token].includes(docId)) {
        this.index[token].push(docId);
      }
    }
  }

  // 查询
  search(queryTerms) {
    // 获取包含所有查询词的文档
    let results = null;
    
    for (const term of queryTerms) {
      const docIds = this.index[term] || [];
      
      if (results === null) {
        results = new Set(docIds);
      } else {
        // 求交集(AND 逻辑)
        results = new Set(docIds.filter(id => results.has(id)));
      }
    }
    
    return Array.from(results || []);
  }

  preprocess(text) {
    // 转小写、删除特殊字符
    return text.toLowerCase().replace(/[^\w\s]/g, '');
  }

  tokenize(text) {
    // 按空格分词(简化版,实际需要使用专业分词工具)
    return text.split(/\s+/).filter(t => t.length > 0);
  }

  removeStopwords(tokens) {
    const stopwords = new Set(['的', '了', '是', 'a', 'the']);
    return tokens.filter(t => !stopwords.has(t));
  }
}

// 使用示例
const index = new InvertedIndex();

// 添加文档
index.addDocument('doc1', '苹果是一种常见的水果');
index.addDocument('doc2', '苹果公司发布了新款手机');
index.addDocument('doc3', '我喜欢吃水果和蔬菜');
index.addDocument('doc4', '这款手机的价格很实惠');
index.addDocument('doc5', '水果店里有苹果和香蕉');

// 查询:找包含"苹果"的文档
console.log(index.search(['苹果']));  // 输出:['doc1', 'doc2', 'doc5']

// 查询:找既包含"苹果"又包含"水果"的文档
console.log(index.search(['苹果', '水果']));  // 输出:['doc1', 'doc5']

1.4 倒排索引在搜索中的应用

场景 1:单词搜索

用户查询:苹果
  ↓
在倒排索引中查找"苹果"
  ↓
立即返回 [Doc1, Doc2, Doc5]
  ↓
再按相关性排序,返回给用户

场景 2:多词搜索(AND 逻辑)

用户查询:苹果 水果
  ↓
查找"苹果"→ [Doc1, Doc2, Doc5]
查找"水果"→ [Doc1, Doc3, Doc5]
  ↓
求交集 → [Doc1, Doc5](既有"苹果"又有"水果")
  ↓
返回 [Doc1, Doc5] 给用户

场景 3:短语搜索

用户查询:"苹果公司"(带引号表示短语搜索)
  ↓
找到包含"苹果"的文档:[Doc1, Doc2, Doc5]
找到包含"公司"的文档:[Doc2]
  ↓
交集 → [Doc2]
  ↓
检查"苹果"和"公司"在 Doc2 中是否相邻 ✅
  ↓
返回 [Doc2]

1.5 倒排索引的优化

优化方向 1:压缩存储

原始倒排表占用空间大,常用的优化技术:

技术 原理 效果
变长编码 (VInt) 小数字用 1 字节,大数字用多字节 节省 60-70% 空间
差值编码 (Delta Encoding) 只存储与前一个文档 ID 的差值,而不是绝对值 节省 70-80% 空间
二进制位图 (Bitmap) 用位图表示哪些文档包含该词 适合高频词,节省 90% 空间

优化方向 2:并行构建

普通方式(串行):
文档1 → 分词 → 倒排表 → 文档2 → 分词 → 倒排表 → ...
需要时间:T1 + T2 + T3 + ...

并行方式(MapReduce):
文档1 → 分词 → 倒排表 ┐
文档2 → 分词 → 倒排表 ├─ 合并 → 最终索引
文档3 → 分词 → 倒排表 ┘
需要时间:max(T1, T2, T3)  (快得多!)

优化方向 3:增量索引

初始索引:包含前 1000 万个文档

新文档持续增加:

❌ 每次都全量重建索引
  └─ 需要 1-2 小时
  └─ 期间新文档不可搜

✅ 增量更新索引
  └─ 只处理新增文档
  └─ 需要 10-30 秒
  └─ 新文档立即可搜

2. 分词与文本分析

分词是搜索引擎的第一步,也是中文搜索的最大挑战。英文天然以空格分词,但中文没有分隔符——"乒乓球拍卖了"可以分成"乒乓球/拍卖/了"或"乒乓/球拍/卖/了"。

分词方式 说明 示例
标准分词 按空格和标点切分(英文) "hello world" → ["hello", "world"]
中文分词 基于词典或模型切分 "搜索引擎" → ["搜索", "引擎"]
N-gram 按固定长度滑动窗口切分 "搜索" → ["搜索", "索引"]
自定义词典 添加业务专有词汇 "iPhone16ProMax" 作为一个词

文本分析管道

分词只是文本分析的一步,完整的管道包括:

  1. 字符过滤 :去除 HTML 标签、特殊字符
  2. 分词 :将文本拆分为词语(Token)
  3. 停用词过滤 :去除"的"、"了"、"是"等无意义的高频词
  4. 同义词扩展 :将"手机"扩展为"手机、电话、移动电话"
  5. 词干提取 :将 "running" 还原为 "run"(英文)

3. 相关性排序:哪个结果最"相关"?

找到匹配的文档只是第一步,更重要的是排序 ——把最相关的结果排在最前面。

算法 原理 特点
TF-IDF 词频(TF) × 逆文档频率(IDF) 经典算法,简单有效
BM25 TF-IDF 的改进版,加入文档长度归一化 Elasticsearch 默认算法
向量检索 将文档和查询转为向量,计算余弦相似度 支持语义搜索

TF-IDF 直觉理解

  • TF(词频) :一个词在文档中出现越多次,这个文档越可能与该词相关
  • IDF(逆文档频率) :一个词在越少的文档中出现,它的区分度越高
  • "的"在所有文档中都出现(IDF 低),所以搜索"的"没有意义
  • "Elasticsearch"只在少数文档中出现(IDF 高),搜索它能精确定位

4. Elasticsearch:最流行的搜索引擎

Elasticsearch 是目前最流行的开源搜索引擎,基于 Apache Lucene 构建,提供分布式、RESTful API 的全文搜索能力。

概念 说明
Index 类似数据库的"表",存储同类文档
Document 一条记录,JSON 格式
Shard 分片,将索引拆分到多个节点
Replica 副本,提供高可用和读扩展
Mapping 字段类型定义,类似数据库 Schema
Analyzer 文本分析器,定义分词规则

ES vs 数据库

Elasticsearch 不是用来替代数据库的,而是作为搜索层与数据库配合使用。典型架构:数据写入数据库 → 同步到 ES → 搜索请求走 ES → 详情请求走数据库。

5. 搜索优化:让搜索更"聪明"

优化手段 说明 效果
同义词 "手机"也能搜到"电话" 提高召回率
拼写纠错 "iphoen" 自动纠正为 "iphone" 容错性
自动补全 输入"苹"提示"苹果手机" 提升体验
高亮 搜索结果中标红匹配词 直观展示
权重调整 标题匹配权重 > 内容匹配 提高精确度
过滤与聚合 按价格区间、品牌筛选 缩小范围

总结

搜索引擎是互联网应用的核心基础设施。理解倒排索引、分词、相关性排序这三个核心概念,就掌握了搜索引擎的本质。

回顾本章的关键要点:

  1. 倒排索引 :从关键词到文档的反向映射,是搜索引擎的核心数据结构
  2. 分词是基础 :中文分词是搜索质量的关键,需要选择合适的分词器
  3. BM25 排序 :基于词频和文档频率的相关性评分,是 ES 的默认算法
  4. ES 架构 :分片 + 副本实现分布式和高可用
  5. 搜索优化 :同义词、纠错、补全让搜索更智能

延伸阅读