• 悟空分词的搜索和排序源码分析之——索引


    转自:http://blog.codeg.cn/2016/02/02/wukong-source-code-reading/

    索引过程分析

    下面我们来分析索引过程。

    // 将文档加入索引
    //
    // 输入参数:
    // 	docId	标识文档编号,必须唯一
    //	data	见DocumentIndexData注释
    //
    // 注意:
    //      1. 这个函数是线程安全的,请尽可能并发调用以提高索引速度
    // 	2. 这个函数调用是非同步的,也就是说在函数返回时有可能文档还没有加入索引中,因此
    //         如果立刻调用Search可能无法查询到这个文档。强制刷新索引请调用FlushIndex函数。
    func (engine *Engine) IndexDocument(docId uint64, data types.DocumentIndexData) {
    	engine.internalIndexDocument(docId, data)
    
    	hash := murmur.Murmur3([]byte(fmt.Sprint("%d", docId))) % uint32(engine.initOptions.PersistentStorageShards)
    	if engine.initOptions.UsePersistentStorage {
    		engine.persistentStorageIndexDocumentChannels[hash] <- persistentStorageIndexDocumentRequest{docId: docId, data: data}
    	}
    }
    
    func (engine *Engine) internalIndexDocument(docId uint64, data types.DocumentIndexData) {
    	if !engine.initialized {
    		log.Fatal("必须先初始化引擎")
    	}
    
    	atomic.AddUint64(&engine.numIndexingRequests, 1)
    	hash := murmur.Murmur3([]byte(fmt.Sprint("%d%s", docId, data.Content)))
    	engine.segmenterChannel <- segmenterRequest{
    		docId: docId, hash: hash, data: data}
    }
    

    这里需要注意的是,docId参数需要调用者从外部传入,而不是在内部自己创建,这给搜索引擎的实现者更大的自由。 将文档交给分词器处理,然后根据murmur3计算的hash值模PersistentStorageShards,选择合适的shard写入持久化存储中。

    索引过程分析:分词协程处理过程

    分词器协程的逻辑代码在这里:segmenter_worker.go:func (engine *Engine) segmenterWorker()

    分词器协程的逻辑是一个死循环,不停的从channel engine.segmenterChannel中读取数据,针对每一次读取的数据:

    1. 计算shard
    2. 将文档分词
    3. 根据分词结果,构造indexerAddDocumentRequest 和 rankerAddDocRequest
    4. indexerAddDocumentRequest投递到channel engine.indexerAddDocumentChannels[shard]
    5. rankerAddDocRequest投递到channel engine.rankerAddDocChannels[shard]

    补充一句:这里shard号的计算过程如下:

    // 从文本hash得到要分配到的shard
    func (engine *Engine) getShard(hash uint32) int {
    	return int(hash - hash/uint32(engine.initOptions.NumShards)*uint32(engine.initOptions.NumShards))
    }
    

    为什么不是直接取模呢?

    索引过程分析:索引器协程处理过程

    首先介绍一下倒排索引表,这是搜索引擎的核心数据结构。

    // 索引器
    type Indexer struct {
    	// 从搜索键到文档列表的反向索引
    	// 加了读写锁以保证读写安全
    	tableLock struct {
    		sync.RWMutex
    		table map[string]*KeywordIndices
    		docs  map[uint64]bool
    	}
    
    	initOptions types.IndexerInitOptions
    	initialized bool
    
    	// 这实际上是总文档数的一个近似
    	numDocuments uint64
    
    	// 所有被索引文本的总关键词数
    	totalTokenLength float32
    
    	// 每个文档的关键词长度
    	docTokenLengths map[uint64]float32
    }
    
    // 反向索引表的一行,收集了一个搜索键出现的所有文档,按照DocId从小到大排序。
    type KeywordIndices struct {
    	// 下面的切片是否为空,取决于初始化时IndexType的值
    	docIds      []uint64  // 全部类型都有
    	frequencies []float32 // IndexType == FrequenciesIndex
    	locations   [][]int   // IndexType == LocationsIndex
    }
    

    table map[string]*KeywordIndices这个是核心:一个关键词,对应一个KeywordIndices结构。该结构的docIds字段记录了所有包含这个关键词的文档id。 如果 IndexType == FrequenciesIndex ,则同时记录这个关键词在该文档中出现次数。 如果 IndexType == LocationsIndex ,则同时记录这个关键词在该文档中出现的所有位置的起始偏移。

    下面是索引的主函数代码:

    func (engine *Engine) indexerAddDocumentWorker(shard int) {
    	for {
    		request := <-engine.indexerAddDocumentChannels[shard]
    		engine.indexers[shard].AddDocument(request.document)
    		atomic.AddUint64(&engine.numTokenIndexAdded,
    			uint64(len(request.document.Keywords)))
    		atomic.AddUint64(&engine.numDocumentsIndexed, 1)
    	}
    }
    

    其主要逻辑又封装在func (indexer *Indexer) AddDocument(document *types.DocumentIndex)函数中实现。其逻辑如下:

    1. 将倒排索引表加锁
    2. 更新文档关键词的长度加在一起的总和
    3. 查找关键词在倒排索引表中是否存在
    4. 如果不存在,则直接加入到table map[string]*KeywordIndices
    5. 如果存在KeywordIndices,则使用二分查找该关键词对应的docId是否已经在KeywordIndices.docIds中存在。分两种情况: 1) docId存在,则更新原有的数据结构。 2) docId不存在,则插入到KeywordIndices.docIds数组中,同时保持升序排列。
    6. 更新索引过的文章总数

    索引过程分析:排序器协程处理过程

    在新索引文档的过程,排序器的主逻辑如下:

    func (engine *Engine) rankerAddDocWorker(shard int) {
    	for {
    		request := <-engine.rankerAddDocChannels[shard]
    		engine.rankers[shard].AddDoc(request.docId, request.fields)
    	}
    }
    

    进而调用下面的函数

    // 给某个文档添加评分字段
    func (ranker *Ranker) AddDoc(docId uint64, fields interface{}) {
    	if ranker.initialized == false {
    		log.Fatal("排序器尚未初始化")
    	}
    
    	ranker.lock.Lock()
    	ranker.lock.fields[docId] = fields
    	ranker.lock.docs[docId] = true
    	ranker.lock.Unlock()
    }
    

    上述函数非常简单,只是将应用层自定义的数据加入到ranker中。

    至此索引过程就完成了。简单来讲就是下面两个过程:

    1. 将文档分词,得到一堆关键词
    2. 将 关键词->docId 的对应关系加入到全局的map中(实际上是分了多个shard)

     

  • 相关阅读:
    Codeforces1499D The Number of Pairs
    Codeforces1493D GCD of an Array
    AtCoder Beginner Contest 192 F
    Codeforces 1485F Copy or Prefix Sum
    Miller_Rabin
    Codeforces Round 655 (Div. 2) E
    Codeforces Round 655 (Div. 2) D
    B
    A
    待更新笔记
  • 原文地址:https://www.cnblogs.com/bonelee/p/6341331.html
Copyright © 2020-2023  润新知