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

简介:

转自: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)

 













本文转自张昺华-sky博客园博客,原文链接:http://www.cnblogs.com/bonelee/p/6341331.html,如需转载请自行联系原作者

相关文章
|
小程序
微信小程序App()方法与getApp()方法
微信小程序App()方法与getApp()方法
858 0
微信小程序App()方法与getApp()方法
|
C语言 索引 编译器
|
API 图形学
Unity 编辑器开发实战【AssetDatabase】- 获取资产的依赖项、引用项
Unity 编辑器开发实战【AssetDatabase】- 获取资产的依赖项、引用项
651 1
Unity 编辑器开发实战【AssetDatabase】- 获取资产的依赖项、引用项
|
存储 缓存 JavaScript
Umi 4 发布啦 🎈
Umi 4 发布啦 🎈
1941 0
Umi 4 发布啦 🎈
|
存储 传感器 机器学习/深度学习
树莓派踩坑备忘录 --使用Linux
树莓派踩坑备忘录 --使用Linux
363 0
树莓派踩坑备忘录 --使用Linux
|
JavaScript Android开发 开发者
【JavaScript-移动端常用事件】了解移动端touch触摸事件
【JavaScript-移动端常用事件】了解移动端touch触摸事件
428 0
【JavaScript-移动端常用事件】了解移动端touch触摸事件
|
小程序 JavaScript 前端开发
【微信小程序】WXML WXSS JS
🍓小程序代码的构成 - WXML 模板 1. 什么是 WXML 2. WXML 和 HTML 的区别 🍇小程序代码的构成 - WXSS 样式 1. 什么是 WXSS 2. WXSS 和 CSS 的区别 🍒小程序代码的构成 - JS 逻辑交互 1. 小程序中的 .js 文件 2. 小程序中 .js 文件的分类
【微信小程序】WXML WXSS JS
|
前端开发 测试技术 API
DingTalk「开发者说」— 钉钉应用开发前端工具实践
DingTalk「开发者说」是钉钉开发者最新上线的开发者栏目,联合阿里云ACE团队,分享钉应用开发解决方案、技术更新、实战技巧,致力于成为钉钉与开发者的桥梁与纽带,让更多的钉钉开发者传播技术、提升技能、分享观点。在数字化变革的时代,“云钉一体”“钉钉全面开放”战略之后,希望钉钉技术可以持续激发开发者的创造力,为组织数字化赋能。 本篇将主要介绍钉钉最新推出的前端应用开发工具DingStudio,及其特点、能力和使用案例,帮助钉钉应用开发提效。
DingTalk「开发者说」— 钉钉应用开发前端工具实践
|
存储 Kubernetes Devops
基于Jenkins+Argocd+Argo Rollouts的DevOps实现并用金丝雀发布(上)
基于Jenkins+Argocd+Argo Rollouts的DevOps实现并用金丝雀发布
基于Jenkins+Argocd+Argo Rollouts的DevOps实现并用金丝雀发布(上)