海量文档单词计数算法方案分析

海量文档单词计数算法方案分析

问题定义

  • 场景: 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
相关推荐
福大大架构师每日一题5 小时前
ollama v0.18.2 发布!OpenClaw 安装优化、Claude 加速、MLX 量化全面升级
golang·ollama
呆萌很8 小时前
【GO】if 语句练习题
golang
lars_lhuan9 小时前
Go Mutex
golang
人间打气筒(Ada)9 小时前
如何使用 Go 更好地开发并发程序?
开发语言·后端·golang
yuanlaile9 小时前
Go-Zero高性能Web+微服务全集解析
微服务·golang·go-zero·go微服务
呆萌很12 小时前
【GO】for 循环练习题
golang
F1FJJ12 小时前
开源实践:用 Go 实现浏览器直连内网 RDP/SSH/VNC
运维·网络·网络协议·网络安全·golang·ssh
呆萌很12 小时前
【GO】switch 练习题
golang
添尹1 天前
Go语言基础之变量和常量
golang