基于正倒排索引的 Boost 搜索引擎
从零手写的 C++ 站内搜索引擎:把 8563 篇 Boost 官方文档离线清洗成分词语料,建正排 + 倒排双索引,再配一个网页界面直接搜索。从预处理到排序全流程自己实现。
项目简介
这是一个从零手写的全文搜索引擎:给它一堆网页,它能建好索引,然后你输入关键词,它按相关度给你返回标题、摘要和原文链接。
语料用的是 Boost 1.82 官方文档的离线版本:56 个目录、9022 个文件,其中 8563 篇 HTML,合计 92.7 MB。整个搜索引擎的核心逻辑(去标签、分词、建索引、算权重、排序、生成摘要)没有用任何现成的检索库,自写 C++ 一共 734 行。
效果与数据规模
| 环节 | 规模 |
|---|---|
| 原始语料 | 8563 篇 HTML,92.7 MB |
清洗后的语料 raw.txt | 16.6 MB / 8563 行(体积压到原来的 17.9%) |
| 索引结构 | 正排索引(8563 条文档)+ 倒排索引(词 → 文档列表) |
| 检索响应 | 一次查询走完”分词 → 召回 → 排序 → 生成摘要”,返回 JSON |
整体流程
离线(跑一次) 在线(服务常驻)┌──────────────────────┐ ┌──────────────────────────────┐│ parser 预处理 │ │ http_server(cpp-httplib) ││ HTML → 去标签 │ │ GET /s?word=xxx ││ 提取标题 / 拼原文 URL │ │ │ ││ 写成 raw.txt │ │ ▼ │└──────────┬───────────┘ │ searcher 检索 │ │ │ 分词 → 查倒排 → 排序 → 摘要 │ ▼ │ │ │ data/raw_html/raw.txt ─────────►│ index 正排 + 倒排(内存) │ └──────────┬───────────────────┘ ▼ wwwroot 网页结果列表一、预处理:把网页洗成结构化文本
parser 负责离线那一段:递归遍历语料目录取出所有 HTML,用状态机去掉 HTML 标签只留正文,单独截出 <title> 作为标题,再根据本地路径反推出这篇文档在 Boost 官网的地址,方便结果里直接点回原文。
清洗时有几个细节:原始 HTML 的换行和连续空白会被压掉(换行要留给自己当文档分隔符用),每篇文档按 标题 \3 正文 \3 原文链接 的格式写成一行 —— 用不可见字符 \3 当分隔符,是因为它几乎不可能出现在正文里,比用换行更安全。8563 篇 HTML 洗出 8563 行,一篇没丢。
二、建索引:正排 + 倒排各司其职
这是搜索引擎最核心的一步,也是我花时间最多的地方:
std::vector<DocInfo> forward_index; // 正排:数组下标就是文档 IDstd::unordered_map<std::string, InvertedList> inverted_index; // 倒排:词 → 文档列表- 正排索引回答”第 N 篇文档的标题、正文、链接是什么”。用
vector存、下标天然就是文档 ID,取一篇文档是 O(1)。 - 倒排索引回答”哪些文档包含这个词”。每个词对应一条”倒排拉链”,链上挂着
{文档 ID, 权重}。 - 两者缺一不可:倒排负责找到,正排负责讲清楚(结果里的标题、摘要、链接都是靠文档 ID 回正排取的)。
打分用的是最简单的相关性信号 —— 词出现在标题里比出现在正文里重要:
// 权重系数:标题词频权重远高于正文词频,体现标题关键词的重要性item.weight = 10 * title_cnt + 1 * content_cnt;建索引时对每篇文档的标题和正文分别统计词频,再按上面的公式合成权重写进倒排拉链。
三、检索:分词 → 召回 → 排序 → 摘要
一次搜索走四步:
- 分词:把查询词交给 cppjieba 切分(跟建索引用的是同一套词典和停用词表,保证”怎么建的就怎么查”);
- 召回:对每个词取出倒排拉链,把命中的文档汇总到一张
文档 ID → 权重的表里; - 排序:按权重降序排列;
- 生成结果:回正排索引取出标题和原文链接,再按关键词开窗生成摘要 —— 找到命中的词在正文里第一次出现的位置,往前取 50 字节、往后取 100 字节,拼成一段预览。
最后用 jsoncpp 序列化成 JSON 返回,转义交给库处理,不用手写字符串拼接。
四、对外服务与前端
服务端只有 28 行:用 cpp-httplib 起一个 HTTP 服务,把 wwwroot 设为静态目录,再注册一条 GET /s:
svr.set_base_dir("./wwwroot");svr.Get("/s", [&search](const httplib::Request &req, httplib::Response &rsp) { std::string word = req.get_param_value("word"); std::string json_string; search.Search(word, &json_string); rsp.set_content(json_string, "application/json");});svr.listen("0.0.0.0", 8081);前端是一个纯静态页面:一个搜索框 + 结果列表,用 ajax 请求 /s?word=关键词,把返回的 JSON 渲染成”标题 / 摘要 / 原文链接”。
怎么跑起来
make # 编译出 parser、debug、http_server 三个可执行文件./parser # 离线预处理:扫 data/input → 生成 data/raw_html/raw.txt./http_server # 启动服务(8081 端口,启动时建索引)依赖:boost_system / boost_filesystem(预处理用)、jsoncpp、cpp-httplib,编译器 g++ -std=c++11。data/input/ 里的语料已经随仓库提供,所以 make 完直接跑就行。
小结
这个项目让我真正搞明白了”索引到底是怎么把文档变成可检索结构的”:正排负责”知道 ID 能拿到原文”,倒排负责”知道词能拿到 ID 列表”,两者配合,才有”输入一个词、几百毫秒内按相关度列出一屏结果”。同时也第一次完整地走了一遍”语料 → 清洗 → 建索引 → 检索 → 排序 → 展示”的链路,中间每一环的问题都很具体:标签怎么去干净、中文怎么切词、停用词怎么过滤、摘要从哪里开始截。

