直接用 map[string][]int 虽能跑通,但不处理词归一化、id顺序、重复插入会导致结果错误或性能极差;须用整数id、预清洗分词、双指针归并实现高效and查询。

直接用 map[string][]int 就能跑通核心流程,但不处理词归一化、ID 顺序、重复插入这三件事,查出来的结果大概率是错的或者慢得离谱。
为什么不能直接 strings.Split + map[string][]string
常见错误是把文件路径当文档 ID 存成 map[string][]string,比如 idx["go"] = []string{"./a.md", "./b.md"}。这会导致两个问题:一是字符串比较和哈希开销大,GC 压力明显;二是后续做 AND 查询时无法用双指针归并,只能嵌套遍历,1000 个文档命中 200 个词,比较次数轻松破万。
正确做法是用连续整数 ID:
- 构建索引前先统一分配 ID(如按文件读取顺序递增)
- 路径单独缓存为 docsByID := []string{"./a.md", "./b.md"}
- 所有索引结构只操作 int,不碰 string
分词和词项标准化必须一步到位
搜 "Go" 找不到 "go",不是 bug 是设计缺陷。大小写、标点、停用词必须在插入前清洗干净,否则索引从第一步就不可靠。
- 用
strings.FieldsFunc(text, func(r rune) bool { return !unicode.IsLetter(r) && !unicode.IsNumber(r) })切分,比strings.Split或正则更稳 - 统一转小写用
strings.ToLower即可,非 ASCII 场景才需strings.EqualFold - 停用词过滤要显式做:
if !stopWords[w] && len(w) > 1,单字符(如 “a”、“i”)和停用词直接丢弃 - 中文必须上
github.com/go-ego/gse,别用字切分;启用seg.WithFrequency(false)省内存
AND 查询必须用双指针归并,别写嵌套 for
用户搜 "golang index",你要返回同时包含这两个词的文档 ID。如果对每个词的结果列表都嵌套遍历,性能直接退化成 O(n²)。
intersect 函数必须满足:
- 输入的两个 []int 都是严格递增且无重复(构建时就得 sort.Ints 或插入前去重)
- 指针移动逻辑只依赖大小比较,不依赖 map 查找
- 返回结果也保持递增,方便后续再与其他词求交
示例关键片段:
func intersect(a, b []int) []int {
i, j := 0, 0
var res []int
for i <h3>构建阶段最容易被忽略的三个细节</h3><p>很多实现跑起来“好像能用”,但一加数据就漏结果或卡死,问题基本出在这三点:</p>
- 文档 ID 不是连续整数——哪怕只是哈希路径得到的
int,只要跳变或无序,intersect就会漏匹配 - 同一文档中重复出现的词,没保留多次 ID——比如文档 123 中 “go” 出现 3 次,就要
append3 次123,否则 TF 计算失真,后续扩展排序打分直接崩 - 插入时反复
make([]int, 0)再append——应该直接idx[word] = append(idx[word], docID),让 Go 自动扩容;若预估平均词频为 4,可初始化为make([]int, 0, 4)
真正难的不是写完代码,而是确保每条文档进索引前,ID 已分配、词已清洗、切片已预分配、顺序已排好——这些事全在构建阶段,查询时只是查表+归并,没得商量。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











