map[string][]int是最直接轻量级倒排索引结构,key为term(如“go”“内存”),value为文档ID切片;需用整数ID、append追加、预估容量初始化,并保留重复词的多次ID以保障TF计算准确。
用 map[string][]int 实现倒排索引最直接
轻量级场景下,map[string][]int 是最可控、最低开销的倒排索引结构。key 是分词后的 term(如 "go"、"内存"),value 是该 term 出现过的文档 id 列表([]int)。它不依赖外部库,无 gc 风险放大,构建和查询逻辑都可完全掌控。
注意三点:
- 文档 ID 必须是整数——推荐用文件路径哈希或递增序号,避免用
string做 key 导致频繁字符串分配与 GC - 插入时始终用
append,不要每次make([]int, 0)后重分配;若预估平均词频为 4,可初始化为make([]int, 0, 4) - 同一文档中重复出现的词,必须保留多次 ID(如文档 123 中 “Go” 出现 3 次,就 append 3 次 123),否则后续 TF 计算失真
为什么不用 TreeMap 或 GoDS 的 TreeMap
TreeMap(比如 GoDS 提供的)能自动排序 key,但倒排索引中 term 本身无需按字典序遍历;真正需要排序的是 value 中的文档 ID 列表——而这个排序应在构建阶段完成(插入后调用 sort.Ints),而非靠 tree 结构维持。
用 TreeMap 反而带来额外负担:
- 每次
Put都触发红黑树旋转,写入性能低于原生map - 内存占用更高(节点指针、颜色标记等),对嵌入式或 CLI 工具不友好
- GoDS 的
TreeMap不支持并发安全,多 goroutine 写入需额外加锁,而原生map+sync.Map组合更轻量
中文分词后如何过滤无效 term
用 gse 分词后,原始输出常含大量单字(如 "的"、"了")和停用词,直接塞进 map 会膨胀索引、拖慢查询。
实操建议在分词后立即过滤:
- 跳过长度 ≤ 1 的
seg.Segment()结果(len(token.Token) ) - 显式调用
seg.RemoveStopWord(true),并加载自定义停用词表(内置表对技术文档效果差) - 避免用
strings.Fields处理英文——它无法切开"hello,world"这类标点粘连,会把整个当一个 token
查询时合并 posting list 必须用双指针归并
用户搜 "Go 内存",要取 index["go"] 和 index["内存"] 两个 slice 求交集(AND)或并集(OR)。此时千万别转成 map[int]bool 再遍历——内存翻倍且缓存不友好。
正确做法是利用文档 ID 天然有序(插入时已排序),用双指针归并在 O(m+n) 时间完成:
- AND 查询:两指针同向推进,相等时记录,小者前进
- OR 查询:取较小值,相等时只记一次,小者前进
- IDF 权重部分可预缓存为
map[string]float64,避免每次查词都遍历全部文档统计 DF - top-K 截断必须放在归并完成之后,不能提前在每个 term 的 posting list 上 limit,否则高权重组可能被漏掉
真正难的从来不是怎么存 ID,而是分词边界是否合理、query rewrite 是否到位——比如用户输 "redis cluster",索引层没法判断他想查集群配置还是某个叫 cluster 的模块。这得靠 query log 分析或点击反馈闭环,纯数据结构解决不了。



















