海量文档单词计数算法方案分析
问题定义
- 场景: N 篇论文文档,每篇平均 M 个单词
- 查询: 统计特定单词 W 出现的总次数
- 约束: 数据量巨大,可能无法一次性加载到内存
方案对比
1. 暴力扫描 (Brute Force)
扫描所有文档,对每个单词逐一比对
| 复杂度 | 分析 |
|---|---|
| 时间 | O(N × M) - 必须遍历全部数据 |
| 空间 | O(1) - 只需常数额外空间 |
- 优点: 无需预处理,实现简单
- 缺点: 查询慢,不支持重复查询场景
2. 哈希表统计 (Hash Map)
预处理阶段: 遍历所有文档,HashMap[word]++
查询阶段: 直接返回 HashMap[word]
| 复杂度 | 分析 |
|---|---|
| 时间 | 预处理 O(N×M),查询 O(1) |
| 空间 | O(V) - V 为唯一单词数 |
- 优点: 查询极快 O(1)
- 缺点: 内存消耗大,字符串存储冗余
3. 字典树 (Trie)
构建: O(N×M×L) L=平均单词长度
查询: O(L)
| 复杂度 | 分析 |
|---|---|
| 时间 | 查询 O(L),L 为单词长度 |
| 空间 | O(26^L) 最坏,平均 O(N×M) |
- 优点: 前缀查询高效,支持联想输入
- 缺点: 空间开销大,不适合海量单语种
4. 倒排索引 (Inverted Index)
正排: DocID → [word1, word2, ...]
倒排: word → [(DocID, count), ...]
| 复杂度 | 分析 |
|---|---|
| 时间 | 构建 O(N×M),查询 O(1) 或 O(logN) |
| 空间 | O(V + N×平均词数) |
- 优点: 支持多文档快速检索,适合搜索引擎
- 缺点: 构建成本高
5. 后缀数组 (Suffix Array)
将所有文档拼接 → 构建后缀数组 → 二分查找
| 复杂度 | 分析 |
|---|---|
| 时间 | 构建 O(N log N),查询 O(L + log N) |
| 空间 | O(N) - 存储后缀数组 |
- 优点: 支持任意子串查询
- 缺点: 只适合单一大文本
6. 后缀树/后缀自动机 (Suffix Automaton)
线性时间构建,支持海量字符串模式匹配
| 复杂度 | 分析 |
|---|---|
| 时间 | 构建 O(N),查询 O(L) |
| 空间 | O(N) |
- 优点: 最优查询性能,O(L) 线性查询
- 缺点: 实现复杂
7. FM-Index (BWT + Wavelet Tree)
压缩存储 + 快速查询,适合大规模数据
| 复杂度 | 分析 |
|---|---|
| 时间 | 查询 O(L) |
| 空间 | O(N) - 可压缩至约原文本大小 |
- 优点: 内存友好,支持压缩查询
- 缺点: 实现复杂
综合排序
| 方案 | 查询时间 | 空间 | 适用场景 |
|---|---|---|---|
| 倒排索引 | O(1)~O(logN) | 中 | 多文档精确查询 (推荐) |
| HashMap | O(1) | 大 | 单机、内存充足 |
| 后缀自动机 | O(L) | O(N) | 子串/模式匹配 |
| FM-Index | O(L) | 压缩 | 磁盘/内存受限 |
| 暴力扫描 | O(N×M) | O(1) | 一次性查询 |
| Trie | O(L) | 大 | 前缀搜索场景 |
实际选型建议
数据规模:
├── < 100万单词 → HashMap 简单有效
├── 100万 ~ 10亿 → 倒排索引
├── 10亿+ / 磁盘存储 → FM-Index
└── 需要子串匹配 → 后缀自动机
倒排索引详解
1. 基本概念
倒排索引(Inverted Index)是搜索引擎的核心数据结构,与传统的"文档→词项"正排索引相反,它是"词项→文档"的映射。
正排索引: DocID → [word1, word2, word3, ...]
倒排索引: word → [(DocID, freq), (DocID, freq), ...]
2. 数据结构组成
┌─────────────────────────────────────────────────────────┐
│ 倒排索引结构 │
├───────────────────────┬─────────────────────────────────┤
│ 词典 (Lexicon) │ 倒排列表 (Posting List) │
├───────────────────────┼─────────────────────────────────┤
│ word1 ──────────────► │ [ (doc1, 3), (doc5, 1), ... ] │
│ word2 ──────────────► │ [ (doc2, 7), (doc3, 2), ... ] │
│ word3 ──────────────► │ [ (doc1, 5), (doc4, 2), ... ] │
│ ... │ ... │
└───────────────────────┴─────────────────────────────────┘
核心术语
| 术语 | 含义 |
|---|---|
| Term (词项) | 索引的最小单元,通常是单词 |
| Posting (倒排项) | 一个词项在某个文档中的出现记录 |
| Posting List | 同一词项的所有倒排项列表 |
| Dictionary | 所有词项的集合,通常用 Trie 或 Hash 存储 |
| TF (Term Frequency) | 词项在单个文档中的出现次数 |
| DF (Document Frequency) | 包含该词项的文档数量 |
| IDF (Inverse Document Frequency) | log(N/DF),衡量词项重要性的权重 |
3. 时间 & 空间复杂度
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 构建 | O(N × M) | O(V + P) |
| 单词查询 | O(1) ~ O(log D) | - |
| 布尔 AND | O(min(D₁, D₂, ...)) | - |
| 布尔 OR | O(D₁ + D₂ + ...) | - |
N = 文档数,M = 平均每文档词数,V = 唯一词数,D = 倒排列表长度,P = 总倒排项数
【空间占用估算】
假设:
- 1亿篇文档,每篇平均 1000 词
- 唯一词数约 100万 (英文) 或 500万 (中文)
- 平均词长 5 字符
空间估算:
├── 词典: ~50MB (100万词 × 50字节/词)
├── 倒排列表: ~2GB (10亿倒排项 × 20字节/项)
└── 文档ID存储: 可压缩后约 500MB
3.1 查询时间O(1) vs O(log N) 场景分析
O(1) - 常数时间复杂度
定义: 无论输入数据量如何增长,算法执行时间保持恒定。
存储结构 - 哈希表 (HashMap):
┌─────────────────────────────────────────────────────────────┐
│ 倒排索引 O(1) 存储结构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 词典表 (Dictionary) 倒排列表 (Posting List) │
│ ┌─────────────┬────────┐ ┌────────────────────────────┐ │
│ │ Key (word) │ Value │ │ Value ([]Posting) │ │
│ ├─────────────┼────────┤ ├────────────────────────────┤ │
│ │ "algorithm" │ ──────►│ [(doc1,3), (doc5,1), (doc9,2)│ │
│ │ "data" │ ──────►│ [(doc2,7), (doc3,2), ... ]│ │
│ │ "learning" │ ──────►│ [(doc1,5), (doc4,2), ... ]│ │
│ │ "network" │ ──────►│ [(doc7,1), (doc8,4), ... ]│ │
│ └─────────────┴────────┘ └────────────────────────────┘ │
│ │
│ [查询 "learning"] │
│ 1. Hash("learning") → 0x7b O(1) 定位 │
│ 2. 直接读取 bucket 0x7b O(1) 访问 │
│ 3. 遍历倒排列表 (k=DF) O(k) 计算总数 │
│ │
│ 总复杂度: O(1) Hash + O(k) 遍历 ≈ O(1) (短列表) │
└─────────────────────────────────────────────────────────────┘
实现方式:
- 哈希表 (Hash Table) 直接寻址
- 数组按下标随机访问
- 队列的 push/pop 操作
典型场景:
| 场景 | 数据结构 | 说明 |
|---|---|---|
| 单词精确查询 | HashMap / 哈希表 | 输入单词 → 直接返回计数 |
| 缓存命中 | LRU Cache | O(1) 的 get/put |
| 唯一性检查 | HashSet | 判断元素是否已存在 |
| 位运算操作 | BitSet | 设置/读取特定位 |
go
// PostingList结构
type PostingList struct{
DocId int
Freq int
}
// InvertedIndex结构
type InvertedIndex struct{
index map[string][]PostingList
}
// O(1) 查询 - 哈希表直接寻址
func (idx *InvertedIndex) QueryWordO1(word string) int {
word = strings.ToLower(word)
// 哈希计算 O(1) + 数组访问 O(1) = O(1)
postingList := idx.index[word]
total := 0
for _, p := range postingList {
total += p.Freq
}
return total
}
O(log N) - 对数时间复杂度
定义: 数据量翻倍,执行时间只增加一个常数增量。
存储结构 - 跳表 (Skip List):
┌─────────────────────────────────────────────────────────────┐
│ 倒排索引 O(log N) 存储结构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ [长倒排列表: 10000+ 文档] │
│ score:doc_ID │
│ Level 3 (稀疏层) │
│ ┌─────┐ ┌────────┐ ┌─────┐ │
│ │ doc1│───────►│ doc1000│──────►│doc10K│ │
│ └──┬──┘ └───┬────┘ └──┬──┘ │
│ │ │ │ │
│ Level 2 │ │ │
│ ┌──┴───┐ ┌──┴───┐ ┌──┴───┐ │
│ │doc1..│──────►│doc500 │──────►│doc8K │ │
│ │ doc50│ │ ..1000│ │..10K │ │
│ └──┬───┘ └──┬───┘ └──┬───┘ │
│ │ │ │ │
│ Level 1 (密集层) │
│ └──┬───┬───┬───┬───┴───┬───┬───┬───┴───┬───┬───┐ │
│ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ │
│ [doc1,doc5,doc10,doc20,doc50,...,doc999,doc1000,...] │
│ │
│ [查找 doc=600] │
│ Level 3: doc1 → doc1000 (600<1000, 向下) │
│ Level 2: doc500 → doc8000 (600<8000, 向下) │
│ Level 1: 逐个遍历直到 doc600 │
│ 总遍历: ~14 步 = log₂(10000) ≈ 14 │
│ │
└─────────────────────────────────────────────────────────────┘
存储结构 - 红黑树 (可选) :
不适合范围查找
┌─────────────────────────────────────────────────────────────┐
│ 倒排索引 红黑树存储结构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌───────┐ │
│ │ doc500│ (根) │
│ /├───┬───┤\ │
│ / │ │ \ │
│ ┌────┐┌┴──┐┌┴──┐┌────┐ │
│ │200 ││300 ││600 ││800 │ │
│ └──┬─┘└───┘└───┘└┬──┘ │
│ │ │ │
│ ┌──┴──┐ ┌──┴──┐ │
│ │子节点│ │子节点│ │
│ └─────┘ └─────┘ │
│ │
│ 特性: │
│ - 自平衡,查找/插入/删除 都是 O(log N) │
│ - 适合需要有序遍历的场景 │
│ - 内存占用比跳表略大 │
└─────────────────────────────────────────────────────────────┘
实现方式:
- 平衡二叉搜索树 (AVL、红黑树)
- 跳表 (Skip List)
选择策略
| 特性 | O(1) | O(log N) |
|---|---|---|
| 数据量敏感 | 不敏感 | 敏感(但增长慢) |
| 内存占用 | 较高(哈希表) | 较低(树/跳表结构) |
| 有序支持 | 不支持 | 支持 |
| 最坏情况 | 可能退化(哈希冲突) | 稳定 |
4. 构建过程
4.1 基本流程
Step 1: 文档预处理
├── 分词 (Tokenization)
├── 标准化 (Normalization: 小写化、词干提取)
├── 停用词过滤 (Stop words removal)
└── 过滤标点/数字
Step 2: 构建正排索引
Doc1: " algorithm is important "
Doc2: " data structure and algorithm "
Step 3: 转换为倒排索引
"algorithm" → [(Doc1, 1), (Doc2, 1)]
"data" → [(Doc2, 1)]
"structure" → [(Doc2, 1)]
"important" → [(Doc1, 1)]
4.2 增量构建伪代码
go
func NewInvertedIndex() *InvertedIndex {
return &InvertedIndex{
index: make(map[string][]Posting),
}
}
func (idx *InvertedIndex) AddDocument(docID int, text string) {
// 1. 分词
tokens := idx.tokenize(text)
// 2. 统计词频
freq := make(map[string]int)
for _, token := range tokens {
freq[token]++
}
// 3. 更新倒排列表
for word, f := range freq {
idx.index[word] = append(idx.index[word], Posting{
DocID: docID,
Freq: f,
})
}
idx.docCount++
}
func (idx *InvertedIndex) tokenize(text string) []string {
// 转小写,提取单词
text = strings.ToLower(text)
re := regexp.MustCompile(`[a-z0-9]+`)
return re.FindAllString(text, -1)
}
func (idx *InvertedIndex) Search(word string) []Posting {
word = strings.ToLower(word)
return idx.index[word]
}
5. 查询过程
5.1 单词查询
go
type QueryResult struct {
Word string
TotalCount int
DocFreq int
Documents []Posting
}
func (idx *InvertedIndex) QueryWord(word string) QueryResult {
word = strings.ToLower(word)
postingList := idx.index[word]
totalCount := 0
for _, p := range postingList {
totalCount += p.Freq
}
return QueryResult{
Word: word,
TotalCount: totalCount,
DocFreq: len(postingList),
Documents: postingList,
}
}
5.2 布尔查询 (AND/OR/NOT)
go
// QueryAND 查询同时包含所有词的文档
func (idx *InvertedIndex) QueryAND(words []string) []int {
if len(words) == 0 {
return nil
}
var docSets []map[int]bool
for _, word := range words {
word = strings.ToLower(word)
postingList := idx.index[word]
docSet := make(map[int]bool)
for _, p := range postingList {
docSet[p.DocID] = true
}
docSets = append(docSets, docSet)
}
// 取交集
result := make(map[int]bool)
first := docSets[0]
for docID := range first {
allMatch := true
for i := 1; i < len(docSets); i++ {
if !docSets[i][docID] {
allMatch = false
break
}
}
if allMatch {
result[docID] = true
}
}
// 转换为有序切片
ids := make([]int, 0, len(result))
for docID := range result {
ids = append(ids, docID)
}
sort.Ints(ids)
return ids
}
// QueryOR 查询包含任一词的文档
func (idx *InvertedIndex) QueryOR(words []string) []int {
if len(words) == 0 {
return nil
}
result := make(map[int]bool)
for _, word := range words {
word = strings.ToLower(word)
for _, p := range idx.index[word] {
result[p.DocID] = true
}
}
ids := make([]int, 0, len(result))
for docID := range result {
ids = append(ids, docID)
}
sort.Ints(ids)
return ids
}
// QueryNOT 查询不包含该词的文档
func (idx *InvertedIndex) QueryNOT(word string) []int {
word = strings.ToLower(word)
excluded := make(map[int]bool)
for _, p := range idx.index[word] {
excluded[p.DocID] = true
}
var result []int
for docID := 1; docID <= idx.docCount; docID++ {
if !excluded[docID] {
result = append(result, docID)
}
}
return result
}
6. 优化策略
6.1 索引分段
分片 + 并行
go
// ShardedIndex 分片索引,支持并行查询
type ShardedIndex struct {
shards []*InvertedIndex
numShards int
}
// NewShardedIndex 创建分片索引
func NewShardedIndex(numShards int) *ShardedIndex {
shards := make([]*InvertedIndex, numShards)
for i := 0; i < numShards; i++ {
shards[i] = NewInvertedIndex()
}
return &ShardedIndex{
shards: shards,
numShards: numShards,
}
}
// AddDocument 添加文档到对应分片
func (s *ShardedIndex) AddDocument(docID int, text string) {
// 使用 uint 类型避免负数取模导致越界
shardID := int(uint(docID) % uint(s.numShards))
s.shards[shardID].AddDocument(docID, text)
}
// Search 并行搜索所有分片
func (s *ShardedIndex) Search(word string) []Posting {
var results []Posting
var mu sync.Mutex
var wg sync.WaitGroup
for _, shard := range s.shards {
wg.Add(1)
go func(shard *InvertedIndex) {
defer wg.Done()
postings := shard.Search(word)
mu.Lock()
results = append(results, postings...)
mu.Unlock()
}(shard)
}
wg.Wait()
return results
}
7. 扩展: 位置信息索引
如果需要查询"单词在文档中的位置"或"相邻词查询":
"algorithm" → [(doc1, 1, [15]), (doc1, 1, [42]), (doc2, 1, [8])]
↑ ↑ ↑
文档ID TF频率 出现位置列表
# 可支持短语查询 "data structure"
# 找到 "data" 位置 p,"structure" 位置 p+2
8. 完整代码实现
go
package main
import (
"fmt"
"math"
"regexp"
"sort"
"strings"
"sync"
)
// Posting 倒排项
type Posting struct {
DocID int
Freq int
}
// Document 文档对象
type Document struct {
DocID int
Content string
Metadata map[string]interface{}
}
// InvertedIndex 倒排索引
type InvertedIndex struct {
index map[string][]Posting // 倒排索引
documents map[int]*Document // 正排索引
docCount int // 文档数量
enableStopWords bool // 是否启用停用词过滤
stopWords map[string]bool // 停用词集合
}
// NewInvertedIndex 创建倒排索引
func NewInvertedIndex(enableStopWords bool) *InvertedIndex {
stopWords := map[string]bool{
"a": true, "an": true, "and": true, "are": true, "as": true,
"at": true, "be": true, "by": true, "for": true, "from": true,
"has": true, "he": true, "in": true, "is": true, "it": true,
"its": true, "of": true, "on": true, "that": true, "the": true,
"to": true, "was": true, "will": true, "with": true,
}
return &InvertedIndex{
index: make(map[string][]Posting),
documents: make(map[int]*Document),
enableStopWords: enableStopWords,
stopWords: stopWords,
}
}
// AddDocument 添加文档到索引
func (idx *InvertedIndex) AddDocument(docID int, content string, metadata map[string]interface{}) {
// 保存文档
doc := &Document{
DocID: docID,
Content: content,
Metadata: metadata,
}
idx.documents[docID] = doc
// 分词
tokens := idx.tokenize(content)
// 统计词频
freq := make(map[string]int)
for _, token := range tokens {
freq[token]++
}
// 更新倒排索引
for word, tf := range freq {
idx.index[word] = append(idx.index[word], Posting{
DocID: docID,
Freq: tf,
})
}
idx.docCount++
}
// tokenize 分词处理
func (idx *InvertedIndex) tokenize(text string) []string {
// 转小写
text = strings.ToLower(text)
// 提取单词(只保留字母和数字)
re := regexp.MustCompile(`[a-z0-9]+`)
tokens := re.FindAllString(text, -1)
// 过滤停用词
if idx.enableStopWords {
result := make([]string, 0, len(tokens))
for _, token := range tokens {
if !idx.stopWords[token] {
result = append(result, token)
}
}
tokens = result
}
return tokens
}
// stem 简单的词干提取(简化版)
func (idx *InvertedIndex) stem(word string) string {
suffixes := []string{"ing", "ed", "ly", "ness", "ment", "tion", "sion"}
for _, suffix := range suffixes {
if strings.HasSuffix(word, suffix) && len(word) > len(suffix)+2 {
return word[:len(word)-len(suffix)]
}
}
return word
}
// QueryResult 查询结果
type QueryResult struct {
Word string
TotalCount int
DocFreq int
Documents []Posting
}
// QueryWord 查询单词出现次数
func (idx *InvertedIndex) QueryWord(word string) QueryResult {
word = strings.ToLower(word)
postingList := idx.index[word]
totalCount := 0
for _, p := range postingList {
totalCount += p.Freq
}
// 按频率降序排序
sortedDocs := make([]Posting, len(postingList))
copy(sortedDocs, postingList)
sort.Slice(sortedDocs, func(i, j int) bool {
return sortedDocs[i].Freq > sortedDocs[j].Freq
})
return QueryResult{
Word: word,
TotalCount: totalCount,
DocFreq: len(postingList),
Documents: sortedDocs,
}
}
// QueryAND 布尔 AND 查询
func (idx *InvertedIndex) QueryAND(words []string) []int {
if len(words) == 0 {
return nil
}
docSets := make([]map[int]bool, len(words))
for i, word := range words {
word = strings.ToLower(word)
docSet := make(map[int]bool)
for _, p := range idx.index[word] {
docSet[p.DocID] = true
}
docSets[i] = docSet
}
// 取交集
result := make(map[int]bool)
first := docSets[0]
for docID := range first {
allMatch := true
for i := 1; i < len(docSets); i++ {
if !docSets[i][docID] {
allMatch = false
break
}
}
if allMatch {
result[docID] = true
}
}
ids := make([]int, 0, len(result))
for docID := range result {
ids = append(ids, docID)
}
sort.Ints(ids)
return ids
}
// QueryOR 布尔 OR 查询
func (idx *InvertedIndex) QueryOR(words []string) []int {
if len(words) == 0 {
return nil
}
result := make(map[int]bool)
for _, word := range words {
word = strings.ToLower(word)
for _, p := range idx.index[word] {
result[p.DocID] = true
}
}
ids := make([]int, 0, len(result))
for docID := range result {
ids = append(ids, docID)
}
sort.Ints(ids)
return ids
}
// QueryNOT 查询不包含该词的文档
func (idx *InvertedIndex) QueryNOT(word string) []int {
word = strings.ToLower(word)
excluded := make(map[int]bool)
for _, p := range idx.index[word] {
excluded[p.DocID] = true
}
var result []int
for docID := range idx.documents {
if !excluded[docID] {
result = append(result, docID)
}
}
sort.Ints(result)
return result
}
// ScoreResult 评分结果
type ScoreResult struct {
DocID int
Score float64
}
// SearchWithTFIDF TF-IDF 搜索
func (idx *InvertedIndex) SearchWithTFIDF(query string, topK int) []ScoreResult {
queryTokens := idx.tokenize(query)
if len(queryTokens) == 0 {
return nil
}
scores := make(map[int]float64)
for _, token := range queryTokens {
postingList := idx.index[token]
if len(postingList) == 0 {
continue
}
// 计算 IDF
df := float64(len(postingList))
idf := math.Log(float64(idx.docCount) / df)
// 累加 TF-IDF 分数
for _, p := range postingList {
tfIDF := float64(p.Freq) * idf
scores[p.DocID] += tfIDF
}
}
// 转换为切片并排序
results := make([]ScoreResult, 0, len(scores))
for docID, score := range scores {
results = append(results, ScoreResult{DocID: docID, Score: score})
}
sort.Slice(results, func(i, j int) bool {
return results[i].Score > results[j].Score
})
if topK > 0 && len(results) > topK {
results = results[:topK]
}
return results
}
// IndexStats 索引统计
type IndexStats struct {
DocCount int
VocabSize int
TotalTerms int
AvgDocLength float64
}
// GetIndexStats 获取索引统计信息
func (idx *InvertedIndex) GetIndexStats() IndexStats {
totalTerms := 0
for _, postings := range idx.index {
totalTerms += len(postings)
}
avgDocLength := 0.0
if idx.docCount > 0 {
totalTokens := 0
for _, doc := range idx.documents {
totalTokens += len(idx.tokenize(doc.Content))
}
avgDocLength = float64(totalTokens) / float64(idx.docCount)
}
return IndexStats{
DocCount: idx.docCount,
VocabSize: len(idx.index),
TotalTerms: totalTerms,
AvgDocLength: avgDocLength,
}
}
// ShardedIndex 分片索引
type ShardedIndex struct {
shards []*InvertedIndex
numShards int
}
// NewShardedIndex 创建分片索引
func NewShardedIndex(numShards int, enableStopWords bool) *ShardedIndex {
shards := make([]*InvertedIndex, numShards)
for i := 0; i < numShards; i++ {
shards[i] = NewInvertedIndex(enableStopWords)
}
return &ShardedIndex{
shards: shards,
numShards: numShards,
}
}
// AddDocument 添加文档到对应分片
func (s *ShardedIndex) AddDocument(docID int, text string, metadata map[string]interface{}) {
// 使用 uint 类型避免负数取模导致越界
shardID := int(uint(docID) % uint(s.numShards))
s.shards[shardID].AddDocument(docID, text, metadata)
}
// Search 并行搜索所有分片
func (s *ShardedIndex) Search(word string) []Posting {
var results []Posting
var mu sync.Mutex
var wg sync.WaitGroup
for _, shard := range s.shards {
wg.Add(1)
go func(shard *InvertedIndex) {
defer wg.Done()
postings := shard.index[strings.ToLower(word)]
mu.Lock()
results = append(results, postings...)
mu.Unlock()
}(shard)
}
wg.Wait()
return results
}
// 使用示例
func main() {
// 创建索引
index := NewInvertedIndex(true)
// 添加文档
papers := []struct {
id int
content string
}{
{1, "Deep learning algorithms are widely used in natural language processing"},
{2, "Machine learning and data mining are important for data science"},
{3, "Neural networks and deep learning enable artificial intelligence"},
{4, "Data structure and algorithms are fundamental to computer science"},
}
for _, paper := range papers {
index.AddDocument(paper.id, paper.content, nil)
}
// 查询单词
result := index.QueryWord("learning")
fmt.Printf("查询 'learning':\n")
fmt.Printf(" 总出现次数: %d\n", result.TotalCount)
fmt.Printf(" 文档频率: %d\n", result.DocFreq)
fmt.Printf(" 详细: %v\n", result.Documents)
// AND 查询
andResult := index.QueryAND([]string{"deep", "learning"})
fmt.Printf("\n查询 'deep AND learning':\n")
fmt.Printf(" 结果文档: %v\n", andResult)
// TF-IDF 搜索
searchResults := index.SearchWithTFIDF("deep learning", 2)
fmt.Printf("\nTF-IDF 搜索 'deep learning':\n")
for _, r := range searchResults {
fmt.Printf(" Doc %d: %.4f\n", r.DocID, r.Score)
}
// 统计信息
stats := index.GetIndexStats()
fmt.Printf("\n索引统计: %+v\n", stats)
}
9. 实际应用场景
| 场景 | 应用 |
|---|---|
| 搜索引擎 | Google、Elasticsearch、Solr |
| 日志分析 | ELK Stack、Splunk |
| 代码搜索 | Sourcegraph、Livegrep |
| 论文检索 | 学术数据库 |
| 邮件搜索 | Gmail、Outlook |