什么是倒排索引
倒排索引(Inverted Index)是一种数据结构,主要用于高效地存储和检索文档中的词项。与正排索引(Forward Index)不同,正排索引是将每个文档ID对应多个内容,而倒排索引则是将多个关键词对应到一个或多个文档ID。这种结构特别适合文本检索和搜索引擎的需求,因为它可以快速定位到包含特定词项的文档。
倒排索引的优势
对比其他索引, 倒排索引有以下几种优势
-
快速检索:可以在较短的时间内返回包含特定词项的所有文档,尤其适合大规模数据集。
-
支持复杂查询:能够处理多种类型的查询,包括短语查询、布尔查询等,满足用户的不同需求。
-
高效的空间使用:在许多应用中,倒排索引能比正排索引更高效地使用存储空间,特别是当文档中包含大量重复词项时。
-
扩展性:随着文档和词项数量的增加,倒排索引仍能保持良好的性能,适应大规模数据的需求。
倒排索引的应用场景
倒排索引广泛应用在需要根据文本查找的场景下, 如:
- 搜索引擎:如 Google、Bing 等利用倒排索引快速返回与用户查询相关的网页。
- 文档检索系统:如数字图书馆、电子邮件搜索等,帮助用户快速找到所需文档。
- 信息检索:在自然语言处理(NLP)应用中,倒排索引常用于信息提取和文本分析。
如何构建倒排索引
文档收集:首先,收集一组需要建立索引的文档,通常是一个文本库或数据库中的记录。
文本预处理:
- 分词:将文档中的文本分解成单个词项,通常会去除停用词(如“的”、“是”等)以及进行词形还原(如“running”转为“run”)。
- 规范化:可以将所有词项转换为小写,去除标点符号等。
构建索引:
- 创建一个空的倒排索引结构。
- 遍历每个文档,对于每个词项:
- 如果词项在索引中不存在,则添加该词项,并初始化其文档列表。
- 将当前文档ID添加到词项的文档列表中。
- 更新文档频率。
- 存储和优化:将最终的倒排索引存储在数据库或文件中,并进行压缩和优化以节省空间。例如,可以使用位图压缩等技术。
代码实现倒排索引
以下是用go语言的一个倒排索引, 存储索引用的是bitmap, bitmap有着快速查找, 集合快速求并交集的特点, 特别适合来实现倒排索引
-
结构体的定义, table负责存储每个关键词对应的bitmap, locks为每个关键词的读写锁, 保证并发读写安全, data存储文档的数据, base是 bitmap结构需要用到的字段, bitmap的实现可以看我的另一篇文章bitmap
// 倒排索引整体上是个map,map的value是一个bitmap // ConcurrentHashMap 是一个并发安全的map type BitmapReverseIndex struct { table *util.ConcurrentHashMap //分段map,并发安全 locks []sync.RWMutex //修改倒排索引时,相同的key需要去竞争同一把锁 Data *util.ConcurrentHashMap // 存储文档的特征值, key为IntId base int64 } // 返回的数据 // id 为存储的倒排索引id // bitsfeature 存储文档的一些特征选项, 每个bit表示一个变量 type BitmapValue struct { Id string BitsFeature uint64 } // DocNumEstimate是预估的doc数量 func NewBitmapReverseIndex(DocNumEstimate int) *BitmapReverseIndex { indexer := new(BitmapReverseIndex) indexer.table = util.NewConcurrentHashMap(runtime.NumCPU(), DocNumEstimate) indexer.locks = make([]sync.RWMutex, 1000) indexer.base = 64 return indexer }
-
获取key对应的锁
func (indexer BitmapReverseIndex) getLock(key string) *sync.RWMutex { n := int(farmhash.Hash32WithSeed([]byte(key), 0)) return &indexer.locks[n%len(indexer.locks)] }
-
添加文档
func (indexer *BitmapReverseIndex) Add(doc types2.Document) { for _, keyword := range doc.Keywords { key := keyword.ToString() lock := indexer.getLock(key) lock.Lock() bitmapValue := BitmapValue{doc.Id, doc.BitsFeature} if value, exists := indexer.table.Get(key); exists { bm := value.(*util.Bitmap) err := bm.Set(strconv.FormatUint(doc.IntId, 10)) if err != nil { return } indexer.Data.Set(strconv.FormatUint(doc.IntId, 10), bitmapValue) } else { bm := util.NewBitmap(indexer.base) bm.Set(strconv.FormatUint(doc.IntId, 10)) indexer.Data.Set(strconv.FormatUint(doc.IntId, 10), bitmapValue) indexer.table.Set(key, bm) } // util.Log.Printf("add key %s value %d to reverse index\n", key, DocId) lock.Unlock() } }
-
删除文档
func (indexr *BitmapReverseIndex) Delete(IntId uint64, keyword *types2.Keyword) { key := keyword.ToString() lock := indexr.getLock(key) lock.Lock() if value, exists := indexr.table.Get(key); exists { bm := value.(*util.Bitmap) bm.Remove(strconv.FormatUint(IntId, 10)) } lock.Unlock() }
-
对文档求交并集
// 求多个Bitmap的交集 func IntersectionOfBitmap(bitmaps ...*util.Bitmap) *util.Bitmap { if len(bitmaps) == 0 { return nil } if len(bitmaps) == 1 { return bitmaps[0] } res := bitmaps[0] for i := 1; i < len(bitmaps); i++ { res.Intersect(bitmaps[i]) } return res } // 求多个Bitmap的并集 func UnionsetOfBitmap(bitmaps ...*util.Bitmap) *util.Bitmap { if len(bitmaps) == 0 { return nil } if len(bitmaps) == 1 { return bitmaps[0] } res := bitmaps[0] for i := 1; i < len(bitmaps); i++ { res = res.Union(bitmaps[i]) } return res }
-
筛选条件
// 按照bits特征进行过滤 func (indexer BitmapReverseIndex) FilterByBits(bits uint64, onFlag uint64, offFlag uint64, orFlags []uint64) bool { //onFlag所有bit必须全部命中 if bits&onFlag != onFlag { return false } //offFlag所有bit必须全部不命中 if bits&offFlag != 0 { return false } //多个orFlags必须全部命中 for _, orFlag := range orFlags { if orFlag > 0 && bits&orFlag <= 0 { //单个orFlag只人有一个bit命中即可 return false } } return true }
-
搜索
// 搜索,返回Bitmap func (indexer BitmapReverseIndex) search(q *types2.TermQuery, onFlag uint64, offFlag uint64, orFlags []uint64) *util.Bitmap { if q.Keyword != nil { Keyword := q.Keyword.ToString() if value, exists := indexer.table.Get(Keyword); exists { result := util.NewBitmap(indexer.base) bm := value.(*util.Bitmap) // util.Log.Printf("retrive %d docs by key %s", list.Len(), Keyword) iterator := bm.CreateBitMapIterator() node := iterator.Next() for node != nil { intId, _ := strconv.ParseUint(node.Data, 10, 64) val, _ := indexer.Data.Get(node.Data) bmv := val.(BitmapValue) flag := bmv.BitsFeature if intId > 0 && indexer.FilterByBits(flag, onFlag, offFlag, orFlags) { //确保有效元素都大于0 result.Set(node.Data) } node = iterator.Next() } return result } } else if len(q.Must) > 0 { results := make([]*util.Bitmap, 0, len(q.Must)) for _, q := range q.Must { results = append(results, indexer.search(q, onFlag, offFlag, orFlags)) } return IntersectionOfBitmap(results...) } else if len(q.Should) > 0 { results := make([]*util.Bitmap, 0, len(q.Must)) for _, q := range q.Should { results = append(results, indexer.search(q, onFlag, offFlag, orFlags)) } return UnionsetOfBitmap(results...) } return nil } // 搜索,返回docId func (indexer BitmapReverseIndex) Search(query *types2.TermQuery, onFlag uint64, offFlag uint64, orFlags []uint64) []string { result := indexer.search(query, onFlag, offFlag, orFlags) if result == nil { return nil } arr := make([]string, 0) iterator := result.CreateBitMapIterator() node := iterator.Next() for node != nil { val, _ := indexer.Data.Get(node.Data) bmv := val.(BitmapValue) arr = append(arr, bmv.Id) node = iterator.Next() } return arr }
总结
倒排索引是一种高效的数据结构,用于快速存储和检索文档中的词项。它将多个关键词映射到包含这些词项的文档ID,特别适合文本检索和搜索引擎。相比于正排索引,倒排索引能快速响应复杂查询,利用空间更高效,并在文档和词项数量增加时保持良好的性能。广泛应用于搜索引擎、文档检索系统和自然语言处理等领域。其实现方式多样,使用位图等结构可以提高检索速度和效率。