wukong引擎源码分析之索引——part 1 倒排列表本质是有序数组存储

时间:2022-09-12 19:59:29

searcher.IndexDocument(0, types.DocumentIndexData{Content: "此次百度收购将成中国互联网最大并购"})

engine.go中的源码实现:

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

而其中:

engine.segmenterChannel <- segmenterRequest{
docId: docId, shard: shard, data: data}

将请求发送给segmenterChannel,其定义:

    // 建立分词器使用的通信通道
segmenterChannel chan segmenterRequest

而接受请求处理的代码在segmenter_worker.go里:

func (engine *Engine) segmenterWorker() {
for {
request := <-engine.segmenterChannel //关键 tokensMap := make(map[string][]int)
numTokens :=
if !engine.initOptions.NotUsingSegmenter && request.data.Content != "" {
// 当文档正文不为空时,优先从内容分词中得到关键词
segments := engine.segmenter.Segment([]byte(request.data.Content))
for _, segment := range segments {
token := segment.Token().Text()
if !engine.stopTokens.IsStopToken(token) {
tokensMap[token] = append(tokensMap[token], segment.Start())
}
}
numTokens = len(segments)
} else {
// 否则载入用户输入的关键词
for _, t := range request.data.Tokens {
if !engine.stopTokens.IsStopToken(t.Text) {
tokensMap[t.Text] = t.Locations
}
}
numTokens = len(request.data.Tokens)
} // 加入非分词的文档标签
for _, label := range request.data.Labels {
if !engine.initOptions.NotUsingSegmenter {
if !engine.stopTokens.IsStopToken(label) {
tokensMap[label] = []int{}
}
} else {
tokensMap[label] = []int{}
}
} indexerRequest := indexerAddDocumentRequest{
document: &types.DocumentIndex{
DocId: request.docId,
TokenLength: float32(numTokens),
Keywords: make([]types.KeywordIndex, len(tokensMap)),
},
}
iTokens :=
for k, v := range tokensMap {
indexerRequest.document.Keywords[iTokens] = types.KeywordIndex{
Text: k,
// 非分词标注的词频设置为0,不参与tf-idf计算
Frequency: float32(len(v)),
Starts: v}
iTokens++
} var dealDocInfoChan = make(chan bool, ) indexerRequest.dealDocInfoChan = dealDocInfoChan
engine.indexerAddDocumentChannels[request.shard] <- indexerRequest rankerRequest := rankerAddDocRequest{
docId: request.docId,
fields: request.data.Fields,
dealDocInfoChan: dealDocInfoChan,
}
engine.rankerAddDocChannels[request.shard] <- rankerRequest
}
}

上面代码的作用就是在统计词频和单词位置(注意:tag也是作为搜索的单词,不过其词频是0,而无法参与tf-idf计算),并封装为indexerRequest,发送给engine.indexerAddDocumentChannels[request.shard]

------------------------------------------------

补充一点,上述代码之所以得以执行是因为在:

searcher = engine.Engine{}
// 初始化
searcher.Init(types.EngineInitOptions{SegmenterDictionaries: "../data/dictionary.txt"})

searcher的初始化代码里有这么一段:

    // 启动分词器
for iThread := ; iThread < options.NumSegmenterThreads; iThread++ {
go engine.segmenterWorker()
}

------------------------------------------------

接收indexerRequest的代码在index_worker.go里:

func (engine *Engine) indexerAddDocumentWorker(shard int) {
for {
request := <-engine.indexerAddDocumentChannels[shard] //关键
addInvertedIndex := engine.indexers[shard].AddDocument(request.document, request.dealDocInfoChan)
// save
if engine.initOptions.UsePersistentStorage {
for k, v := range addInvertedIndex {
engine.persistentStorageIndexDocumentChannels[shard] <- persistentStorageIndexDocumentRequest{
typ: "index",
keyword: k,
keywordIndices: v,
}
}
} atomic.AddUint64(&engine.numTokenIndexAdded,
uint64(len(request.document.Keywords)))
atomic.AddUint64(&engine.numDocumentsIndexed, )
}
}

-----------------------------------------------

而上述函数之所以得以执行,还是因为在searcher的初始化函数里有这么一句:

        // 启动索引器和排序器
for shard := ; shard < options.NumShards; shard++ {
go engine.indexerAddDocumentWorker(shard) //关键
go engine.indexerRemoveDocWorker(shard)
go engine.rankerAddDocWorker(shard)
go engine.rankerRemoveDocWorker(shard) for i := ; i < options.NumIndexerThreadsPerShard; i++ {
go engine.indexerLookupWorker(shard)
}
for i := ; i < options.NumRankerThreadsPerShard; i++ {
go engine.rankerRankWorker(shard)
}
}

------------------------------------------------

其中,engine.indexers[shard].AddDocument(request.document, request.dealDocInfoChan)的核心代码在indexer.go里:

// 向反向索引表中加入一个文档
func (indexer *Indexer) AddDocument(document *types.DocumentIndex, dealDocInfoChan chan<- bool) (addInvertedIndex map[string]*types.KeywordIndices) {
if indexer.initialized == false {
log.Fatal("索引器尚未初始化")
} indexer.InvertedIndexShard.Lock()
defer indexer.InvertedIndexShard.Unlock() // 更新文档总数及关键词总长度
indexer.DocInfosShard.Lock()
if _, found := indexer.DocInfosShard.DocInfos[document.DocId]; !found {
indexer.DocInfosShard.DocInfos[document.DocId] = new(types.DocInfo)
indexer.DocInfosShard.NumDocuments++
}
if document.TokenLength != {
originalLength := indexer.DocInfosShard.DocInfos[document.DocId].TokenLengths
indexer.DocInfosShard.DocInfos[document.DocId].TokenLengths = float32(document.TokenLength)
indexer.InvertedIndexShard.TotalTokenLength += document.TokenLength - originalLength
}
indexer.DocInfosShard.Unlock()
close(dealDocInfoChan) // docIdIsNew := true
foundKeyword := false
addInvertedIndex = make(map[string]*types.KeywordIndices)
for _, keyword := range document.Keywords {
addInvertedIndex[keyword.Text], foundKeyword = indexer.InvertedIndexShard.InvertedIndex[keyword.Text]
if !foundKeyword {
addInvertedIndex[keyword.Text] = new(types.KeywordIndices)
}
indices := addInvertedIndex[keyword.Text] if !foundKeyword {
// 如果没找到该搜索键则加入
switch indexer.initOptions.IndexType {
case types.LocationsIndex:
indices.Locations = [][]int{keyword.Starts}
case types.FrequenciesIndex:
indices.Frequencies = []float32{keyword.Frequency}
}
indices.DocIds = []uint64{document.DocId}
indexer.InvertedIndexShard.InvertedIndex[keyword.Text] = indices
continue
} // 查找应该插入的位置
position, found := indexer.searchIndex(
indices, , indexer.getIndexLength(indices)-, document.DocId)
if found {
// docIdIsNew = false // 覆盖已有的索引项
switch indexer.initOptions.IndexType {
case types.LocationsIndex:
indices.Locations[position] = keyword.Starts
case types.FrequenciesIndex:
indices.Frequencies[position] = keyword.Frequency
}
continue
} // 当索引不存在时,插入新索引项
switch indexer.initOptions.IndexType {
case types.LocationsIndex:
indices.Locations = append(indices.Locations, []int{})
copy(indices.Locations[position+:], indices.Locations[position:])
indices.Locations[position] = keyword.Starts
case types.FrequenciesIndex:
indices.Frequencies = append(indices.Frequencies, float32())
copy(indices.Frequencies[position+:], indices.Frequencies[position:])
indices.Frequencies[position] = keyword.Frequency
}
indices.DocIds = append(indices.DocIds, )
copy(indices.DocIds[position+:], indices.DocIds[position:])
indices.DocIds[position] = document.DocId
}
return
}

查找docID是否存在于倒排列表的时候是二分:

// 二分法查找indices中某文档的索引项
// 第一个返回参数为找到的位置或需要插入的位置
// 第二个返回参数标明是否找到
func (indexer *Indexer) searchIndex(
indices *types.KeywordIndices, start int, end int, docId uint64) (int, bool) {
// 特殊情况
if indexer.getIndexLength(indices) == start {
return start, false
}
if docId < indexer.getDocId(indices, start) {
return start, false
} else if docId == indexer.getDocId(indices, start) {
return start, true
}
if docId > indexer.getDocId(indices, end) {
return end + , false
} else if docId == indexer.getDocId(indices, end) {
return end, true
} // 二分
var middle int
for end-start > {
middle = (start + end) /
if docId == indexer.getDocId(indices, middle) {
return middle, true
} else if docId > indexer.getDocId(indices, middle) {
start = middle
} else {
end = middle
}
}
return end, false
}

TODO,待分析:indexer里索引的细节,以及评分相关的逻辑:


        rankerRequest := rankerAddDocRequest{
docId: request.docId,
fields: request.data.Fields,
dealDocInfoChan: dealDocInfoChan,
}
engine.rankerAddDocChannels[request.shard] <- rankerRequest

wukong引擎源码分析之索引——part 1 倒排列表本质是有序数组存储的更多相关文章

  1. wukong引擎源码分析之索引——part 2 持久化 直接set(key,docID数组)在kv存储里

    前面说过,接收indexerRequest的代码在index_worker.go里: func (engine *Engine) indexerAddDocumentWorker(shard int) ...

  2. wukong引擎源码分析之索引——part 3 文档评分 无非就是将docid对应的fields信息存储起来,为搜索结果rank评分用

    之前的文章分析过,接受索引请求处理的代码在segmenter_worker.go里: func (engine *Engine) segmenterWorker() { for { request : ...

  3. wukong引擎源码分析之搜索——docid有序的数组里二分归并求交集,如果用跳表的话,在插入索引时会更快

    searcher.Search(types.SearchRequest{Text: "百度中国"}) // 查找满足搜索条件的文档,此函数线程安全 func (engine *En ...

  4. Spark源码分析 &ndash&semi; 汇总索引

    http://jerryshao.me/categories.html#architecture-ref http://blog.csdn.net/pelick/article/details/172 ...

  5. bleve搜索引擎源码分析之索引——mapping和lucene一样,也有&lowbar;all

    例子: package main import ( "fmt" "github.com/blevesearch/bleve" ) func main() { / ...

  6. bleve搜索引擎源码分析之索引——mapping真复杂啊

    接下来看看下面index部分的源码实现: data := struct { Name string Des string }{ Name: "hello world this is bone ...

  7. 转&colon;Irrlicht 0&period;1引擎源码分析与研究&lpar;一&rpar;

    目录(?)[-] 主要技术特性 引擎概览 Irrlicht的窗口管理   Irrlicht引擎主要是由一个名叫Nikolaus Gebhardt奥地利人所设计,是sourceforge上的一个开源项目 ...

  8. lua源码分析 伪索引

    Lua 提供了一个 注册表, 这是一个预定义出来的表, 可以用来保存任何 C 代码想保存的 Lua 值. 这个表可以用有效伪索引 LUA_REGISTRYINDEX 来定位. 任何 C 库都可以在这张 ...

  9. Java集合系列:-----------03ArrayList源码分析

    上一章,我们学习了Collection的架构.这一章开始,我们对Collection的具体实现类进行讲解:首先,讲解List,而List中ArrayList又最为常用.因此,本章我们讲解ArrayLi ...

随机推荐

  1. WPF 容器的Z顺序操作

    当需要动态添加.修改.删除控件时,如果要达到最好的效果,肯定不只是把需要的控件添加到容器中,并且还需要把容器中的已有控件进行排序操作(置顶.置底.前移.后移操作).由于初次接触到wpf,所以对很多知识 ...

  2. jQuery选择器引擎和Sizzle介绍

    一.前言 Sizzle原来是jQuery里面的选择器引擎,后来逐渐独立出来,成为一个独立的模块,可以*地引入到其他类库中.我曾经将其作为YUI3里面的一个module,用起来畅通无阻,没有任何障碍. ...

  3. vs2013 RTM 激活码

    BWG7X-J98B3-W34RT-33B3R-JVYW9

  4. java web 学习九(通过servlet生成验证码图片)

    一.BufferedImage类介绍 生成验证码图片主要用到了一个BufferedImage类,如下:

  5. Flex弹性布局以及box-sizing

    (本篇内容代表本人理解,如有错误请指出!) box-sizing box-sizing 属性用于更改用于计算元素宽度和高度的默认的 CSS 盒子模型.可以使用此属性来模拟不正确支持CSS盒子模型规范的 ...

  6. 第十七篇 make的路径搜索综合实践

     本节我们编写路径搜索相关的makefile,具体需求如下:  1.工程项目中不希望源码文件夹在编译时被改动(只读文件夹).  2.在编译时自动创建文件夹(build)用于存放编译结果.  3.编译过 ...

  7. CRNN中英文字符识别

    代码地址如下:http://www.demodashi.com/demo/13870.html 参考GitHub源码:https://github.com/YoungMiao/crnn 应demo大师 ...

  8. jQuery监控文本框事件并作相应处理的方法

    本文实例讲述了jQuery监控文本框事件并作相应处理的方法.分享给大家供大家参考.具体如下: //事情委托 $(document)  .on('input propertychange', '#que ...

  9. ElasticSearch6&period;0 Java API 使用 排序,分组 ,创建索引,添加索引数据,打分等(一)

    ElasticSearch6.0  Java API  使用     排序,分组 ,创建索引,添加索引数据,打分等 如果此文章对你有帮助,请关注一下哦 1.1 搭建maven 工程  创建web工程 ...

  10. 01&lowbar;Redis基础

    [Redis定义(参考了百度百科)] Redis是一个key-value存储系统.与Memchached类似,Redis支持的value类型更多,包括String.list.set.zset(有序集合 ...