倒排索引构建核心是map[string][]int,将词映射到其出现的文档id升序列表;需小写归一化、去标点、空白分词,文档id用连续整数;and搜索用双指针归并求交,避免嵌套遍历。

倒排索引怎么建:从文档切词到 map[string][]int
核心就一句话:把每个词映射到它出现过的文档 ID 列表。不是存全文,是存「这个词在哪几篇里出现过」。
常见错误是直接用 map[string]string 存原始文本,结果搜“go”时匹配不到“Golang”,也做不到按文档 ID 快速取交集。必须先做基础归一化——小写、去标点、简单分词(比如按空白和标点切),再构建 map[string][]int。
- 文档 ID 建议用连续整数(
0, 1, 2...),别用文件名或 UUID,后续求交/并集时下标运算快且无哈希开销 - 切词不用上 full-text 分词器,
strings.FieldsFunc(text, func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsNumber(r) })足够起步 - 停用词可后期加,初期跳过,避免过早引入维护成本
搜索逻辑怎么写:AND 检索的交集计算不能靠遍历
用户输“golang index”,你要找同时包含这两个词的文档 ID。如果对每个词的结果列表都用 for 嵌套遍历求交,复杂度是 O(n×m),1000 个文档、每个词命中 200 篇时就卡住。
正确做法是双指针归并——两个升序 ID 列表,各一个游标,相等则记录,不等则移动小的那个。Go 标准库没现成函数,但几行就能写完。
立即学习“go语言免费学习笔记(深入)”;
- 确保倒排索引中每个
[]int都是严格递增且无重复(插入时去重 + 排序,或用sort.Ints预处理) - 多个关键词 AND 检索,不要递归两两合并,用迭代:从第一个词结果开始,逐个与下一个结果求交
- 如果某次交集结果为空(
len(intersection) == 0),立刻返回空,别继续算
func intersect(a, b []int) []int {
i, j := 0, 0
var res []int
for i < len(a) && j < len(b) {
if a[i] == b[j] {
res = append(res, a[i])
i++
j++
} else if a[i] < b[j] {
i++
} else {
j++
}
}
return res
}
内存和性能怎么控:别让 map 把进程吃光
倒排索引本质是内存数据结构,文档量一上来,map[string][]int 的 key 字符串和 slice 头都会占不少内存。10 万文档、平均 50 个词,key 就可能上百万个。
容易被忽略的是:Go 的 map 不会自动缩容,删掉大量 key 后内存不释放;而且字符串 key 默认是独立分配的,重复词(比如“the”)哪怕出现一万次,也存一万份地址。
- 用
sync.Map不解决内存问题,只解决并发写,读多写少场景反而更慢,优先用普通map+ 读写锁 - 词典去重:把所有词先塞进
map[string]struct{},再转成 slice 排序,用sort.SearchStrings查词是否存在,能省掉大量重复字符串内存 - 文档量超 5 万后,考虑把倒排索引拆成两级:一级是词 → block ID,二级是 block 内部偏移,减少单个 slice 长度
为什么搜不到“Go”却能搜到“go”:大小写和归一化必须统一做
这是最常踩的坑:索引时用 strings.ToLower,但搜索时忘了对 query 做同样处理,导致“Go”查不到任何结果。或者反过来,索引没转小写,query 却转了,也匹配不上。
归一化动作必须在唯一入口完成,且对文档和 query 完全一致。别指望前端传来的 query 已经标准化——它可能来自 URL、表单、API body,来源不可控。
- 归一化函数要封装成独立函数,比如
normalizeText,索引构建和查询解析都调它,别复制粘贴逻辑 - 别用正则替换 Unicode 字母大小写(
regexp.MustCompile(`[A-Z]`)),用strings.Map(unicode.ToLower, text)更稳妥 - 测试用例至少覆盖:空格前后、标点夹着、首字母大写、全大写、带数字混合(如 “Go2”)
倒排索引本身很简单,难的是边界——词怎么切、ID 怎么编、内存怎么扛、大小写怎么对齐。这些地方一松动,搜出来的东西就不可信。










