一、搜索系统概述:高并发搜索的挑战、搜索引擎工作原理、倒排索引基础

1.1 高并发搜索到底难在哪?

做搜索系统这么多年,我经常被问到同一个问题:「不就是个搜索框吗?能有多难?」

嗯,这话听着简单。但你想想看,当用户敲下回车的那一瞬间,系统要在几毫秒内从几十亿条数据里找到最相关的结果。而且,同一秒可能有几万个人同时在敲回车。

这就是高并发搜索的挑战。说白了,三个核心矛盾:

  • 数据量大——动不动几十亿条,全量扫描是不可能的
  • 响应要快——用户等不了,超过500ms用户就流失了
  • 并发高——双十一、大促期间,QPS轻松破万

核心矛盾: 数据量越大,查询越慢;并发越高,资源越吃紧。搜索引擎要在这三者之间找到平衡点。

我在项目中遇到过最极端的情况:某电商大促期间,搜索QPS飙到8万,结果倒排索引的合并操作直接把CPU打满,响应时间从20ms暴涨到2秒。嗯,那天晚上我基本没睡。

1.2 搜索引擎工作原理——别把它想得太神秘

搜索引擎到底是怎么工作的?我习惯把它拆成两个阶段:建索引查索引

1.2.1 建索引阶段

你想想看,如果让你从一本没目录的书里找一句话,你得一页页翻。搜索引擎也一样,它不能每次查询都去翻原始数据。所以它提前建好一个「目录」——这就是索引。

建索引的流程大致如下:

  1. 爬取/采集——拿到原始文档(网页、商品、文章等)
  2. 分词——把文档拆成一个个词条
  3. 去停用词——去掉「的」「了」「是」这类高频无意义词
  4. 构建倒排索引——记录每个词出现在哪些文档里
  5. 写入磁盘——持久化,供查询使用

我的经验: 分词这一步特别关键。中文分词比英文复杂得多。我曾经因为分词词典没更新,导致「苹果手机」被分成了「苹果」和「手机」,结果用户搜「iPhone」完全搜不到。后来我加了一个同义词映射表,才解决这个问题。

1.2.2 查询阶段

用户输入关键词后,系统做这几件事:

  • 对查询词做同样的分词处理
  • 去倒排索引里找到对应的文档ID列表
  • 合并多个词的查询结果(比如搜「高并发 搜索」,要取交集)
  • 按相关性打分排序
  • 返回Top N结果

整个过程必须在几十毫秒内完成。你想想看,这背后有多少优化要做。

1.3 倒排索引基础——搜索引擎的「灵魂」

倒排索引,说白了就是「词 → 文档」的映射。跟书的目录一个道理。

举个例子,假设有三篇文档:

文档ID 内容
1 高并发搜索系统设计
2 搜索引擎工作原理
3 倒排索引实战

经过分词后,倒排索引长这样:

词条 文档ID列表
高并发 [1]
搜索 [1, 2]
搜索引擎 [2]
倒排索引 [3]
实战 [3]

当用户搜「搜索」时,系统直接去倒排索引里查,发现文档1和文档2都包含这个词。返回这两个文档即可。不需要扫描全部数据。

关键点: 倒排索引把「文档→词」的正向关系,变成了「词→文档」的反向关系。查询时直接定位,时间复杂度从O(n)降到了O(1)。这就是搜索引擎能秒级响应的根本原因。

1.3.1 倒排索引的存储结构

实际工程中,倒排索引不是简单存个列表就完事了。我习惯把它分成两部分:

  • 词典(Term Dictionary)——所有词条的集合,通常用跳表或哈希表存储
  • 倒排列表(Posting List)——每个词对应的文档ID列表,用压缩算法存储

为什么要压缩?你想想看,几十亿条数据,每个词对应的文档ID可能上千万。不压缩的话,内存根本扛不住。

避坑指南: 我曾经在项目里直接用int存文档ID,结果索引文件比原始数据还大。后来改用差分编码+变长编码,压缩率达到了1:5。嗯,内存一下子就省下来了。

1.3.2 倒排索引的合并操作

多词查询时,需要合并多个倒排列表。比如搜「高并发 搜索」,要取两个列表的交集。

最简单的做法是双指针遍历:

// 伪代码:两个有序列表求交集
List<Integer> intersect(List<Integer> list1, List<Integer> list2) {
    int i = 0, j = 0;
    List<Integer> result = new ArrayList<>();
    while (i < list1.size() && j < list2.size()) {
        if (list1.get(i) == list2.get(j)) {
            result.add(list1.get(i));
            i++; j++;
        } else if (list1.get(i) < list2.get(j)) {
            i++;
        } else {
            j++;
        }
    }
    return result;
}

这个算法的时间复杂度是O(m+n)。但实际场景中,两个列表可能一个很大一个很小。我习惯先拿小的列表去大的列表里做二分查找,效率更高。

注意: 倒排列表的合并是高并发场景下的性能瓶颈。如果合并策略选不好,QPI一上来就崩。我建议用跳表(Skip List)来加速合并,实测性能提升3倍以上。

1.4 高并发搜索的常见坑

做搜索系统这些年,我踩过的坑不少。挑几个典型的说说:

  • 缓存穿透——热门词大家都在搜,缓存扛不住。我后来加了布隆过滤器,先过滤掉不存在的词。
  • 索引更新延迟——新数据写进去了,但索引还没更新,用户搜不到。我建议用近实时索引,秒级可见。
  • 排序计算太慢——相关性打分涉及TF-IDF、BM25等算法,计算量很大。我习惯把打分结果提前算好,存到索引里。

嗯,这些坑后面章节会详细讲。今天先有个概念就行。

1.5 小结

这一章我们聊了高并发搜索的三大挑战、搜索引擎的建索引和查询流程,以及倒排索引的核心原理。说白了,搜索引擎就是「空间换时间」的典型代表——提前建好索引,查询时直接定位。

下一章,我会带你手写一个迷你搜索引擎,把今天讲的理论落地成代码。到时候你就知道,倒排索引到底是怎么工作的了。

一句话总结: 倒排索引是搜索引擎的基石。理解了它,你就理解了搜索系统的一半。