悟空(wukong)搜索引擎源代码阅读
最近为了了解搜索引擎的一些知识,阅读了wukong搜索的一些源码,在这里记录一下。项目地址在这里
整个的处理流程如下:
在项目中主要有一下几个目录:
目录名 | 作用 |
---|---|
core | |
data | |
docs | |
engines | |
examples | |
testdata | |
Types | |
utils |
示例代码如下:
package main
import (
"github.com/huichen/wukong/engine"
"github.com/huichen/wukong/types"
"log"
)
var (
// searcher是协程安全的
searcher = engine.Engine{}
)
func main() {
// 初始化
searcher.Init(types.EngineInitOptions{
SegmenterDictionaries: "./data/dictionary.txt"})
defer searcher.Close()
// 将文档加入索引
searcher.IndexDocument(0, types.DocumentIndexData{Content: "此次百度收购将成中国互联网最大并购"})
searcher.IndexDocument(1, types.DocumentIndexData{Content: "百度宣布拟全资收购91无线业务"})
searcher.IndexDocument(2, types.DocumentIndexData{Content: "百度是中国最大的搜索引擎"})
// 等待索引刷新完毕
searcher.FlushIndex()
// 搜索输出格式见types.SearchResponse结构体
res := searcher.Search(types.SearchRequest{Text:"百度中国"})
log.Printf("num=%d ", res.NumDocs)
for _, d := range res.Docs {
log.Printf("docId=%d", d.DocId)
log.Print("\tscore:", d.Scores)
log.Print("\tTokenLocations:", d.TokenLocations)
log.Print("\tTokenSnippetLocations:", d.TokenSnippetLocations)
}
}
可以看到整个处理流程为:
- [ ] 建立一个searcher
- [ ] 进行初始化
- [ ] 加入文档进行索引
- [ ] 搜索
下面按照该流程,结合代码目录里的代码介绍各个的作用。
初始化工作
初始化在engines中的engine.go中进行,包括:
初始化通道
索引器、排序器、分词器(启动NumShards个)初始化:
// 初始化索引器和排序器
for shard := 0; shard < options.NumShards; shard++ {
engine.indexers = append(engine.indexers, core.Indexer{})
engine.indexers[shard].Init(*options.IndexerInitOptions)
engine.rankers = append(engine.rankers, core.Ranker{})
engine.rankers[shard].Init()
}
// 载入分词器词典 engine.segmenter.LoadDictionary(options.SegmenterDictionaries)
// 初始化停用词
engine.stopTokens.Init(options.StopTokenFile)
索引器通道、排序器通道、分词器通道初始化:
分词器通道
/ 初始化分词器通道
engine.segmenterChannel = make(
chan segmenterRequest, options.NumSegmenterThreads)
启动分词器通道,传送的是segmenterRequest,位于engine.segementer_worker.go中。
索引器通道
// 初始化索引器通道
engine.indexerAddDocumentChannels = make(
[]chan indexerAddDocumentRequest, options.NumShards)
engine.indexerLookupChannels = make(
[]chan indexerLookupRequest, options.NumShards)
for shard := 0; shard < options.NumShards; shard++ {
engine.indexerAddDocumentChannels[shard] = make(
chan indexerAddDocumentRequest,
options.IndexerBufferLength)
engine.indexerLookupChannels[shard] = make(
chan indexerLookupRequest,
options.IndexerBufferLength)
}
启动了索引器的两个通道序列:indexerAddDocumentChannels、indexerLookupChannels
indexerAddDocumentChannels通道中传送的是indexerAddDocumentRequest用来发送增加索引文档的请求,indexerAddDocumentRequest在engine.indexer_worker.go里面定义。
indexerLookupChannels通道中传送的是indexerLookupRequest用来查找文档,也是在engine.indexer_worker.go中进行定义。
排序器通道
// 初始化排序器通道
engine.rankerAddScoringFieldsChannels = make(
[]chan rankerAddScoringFieldsRequest, options.NumShards)
engine.rankerRankChannels = make(
[]chan rankerRankRequest, options.NumShards)
engine.rankerRemoveScoringFieldsChannels = make(
[]chan rankerRemoveScoringFieldsRequest, options.NumShards)
for shard := 0; shard < options.NumShards; shard++ {
engine.rankerAddScoringFieldsChannels[shard] = make(
chan rankerAddScoringFieldsRequest,
options.RankerBufferLength)
engine.rankerRankChannels[shard] = make(
chan rankerRankRequest,
options.RankerBufferLength)
engine.rankerRemoveScoringFieldsChannels[shard] = make(
chan rankerRemoveScoringFieldsRequest,
options.RankerBufferLength)
}
启动了排序器的三个通道序列:rankerAddScoringFieldsChannels、rankerRankChannels、rankerRemoveScoringFieldsChannels
rankerAddScoringFieldsChannels用来传送rankerAddScoringFieldsRequest请求,该请求位于engine.ranker_worker.go中
rankerRankChannels用来传送rankerRankRequest请求,该请求位于engine.ranker_worker.go中
rankerRemoveScoringFieldsChannels用来传送rankerRemoveScoringFieldsRequest请求,该请求位于engine.ranker_worker.go中
然后就是启动持久化索引通道。
启动
// 启动分词器
for iThread := 0; iThread < options.NumSegmenterThreads; iThread++ {
go engine.segmenterWorker()
}
// 启动索引器和排序器
for shard := 0; shard < options.NumShards; shard++ {
go engine.indexerAddDocumentWorker(shard)
go engine.rankerAddScoringFieldsWorker(shard)
go engine.rankerRemoveScoringFieldsWorker(shard)
for i := 0; i < options.NumIndexerThreadsPerShard; i++ {
go engine.indexerLookupWorker(shard)
}
for i := 0; i < options.NumRankerThreadsPerShard; i++ {
go engine.rankerRankWorker(shard)
}
}
将初始化的一些通道进行启动。
加入文档索引
添加索引文档
在engine.engine.go中IndexDocument进行文档的索引。
IndexDocument主要调用internalIndexDocument,
/ 将文档加入索引
//
// 输入参数:
// docId 标识文档编号,必须唯一
// data 见DocumentIndexData注释
//
// 注意:
// 1. 这个函数是线程安全的,请尽可能并发调用以提高索引速度
// 2. 这个函数调用是非同步的,也就是说在函数返回时有可能文档还没有加入索引中,因此
// 如果立刻调用Search可能无法查询到这个文档。强制刷新索引请调用FlushIndex函数。
func (engine *Engine) IndexDocument(docId uint64, data types.DocumentIndexData) {
engine.internalIndexDocument(docId, 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 标识文档编号,必须唯一
// 注意:这个函数仅从排序器中删除文档的自定义评分字段,索引器不会发生变化。所以你的自定义评分字段必须能够区别评分字段为nil的情况,并将其从排序结果中删除。
for shard := 0; shard < engine.initOptions.NumShards; shard++ {
engine.rankerRemoveScoringFieldsChannels[shard] <- rankerRemoveScoringFieldsRequest{docId: docId}
}
删除文档中,实际上只是删除了排序器中文档的评分字段
等待索引完毕
// 阻塞等待直到所有索引添加完毕
func (engine *Engine) FlushIndex()
搜索
搜索函数的定义如下:
func (engine *Engine) Search(request types.SearchRequest) (output types.SearchResponse)
输入类型是types.searchRequest类型,该类型主要包括以下字段:
// 搜索的短语(必须是UTF-8格式),会被分词
// 当值为空字符串时关键词会从下面的Tokens读入
Text string
// 关键词(必须是UTF-8格式),当Text不为空时优先使用Text
// 通常你不需要自己指定关键词,除非你运行自己的分词程序
Tokens []string
// 文档标签(必须是UTF-8格式),标签不存在文档文本中,但也属于搜索键的一种
Labels []string
// 当不为空时,仅从这些文档中搜索
DocIds []uint64
// 排序选项
RankOptions *RankOptions
// 超时,单位毫秒(千分之一秒)。此值小于等于零时不设超时。
// 搜索超时的情况下仍有可能返回部分排序结果。
Timeout int
这些字段至少需要定义一个tokens
定义排序准则
在函数内部,首先需要定义排序的准则:
type RankOptions struct {
// 文档的评分规则,值为nil时使用Engine初始化时设定的规则
ScoringCriteria ScoringCriteria
// 默认情况下(ReverseOrder=false)按照分数从大到小排序,否则从小到大排序
ReverseOrder bool
// 从第几条结果开始输出
OutputOffset int
// 最大输出的搜索结果数,为0时无限制
MaxOutputs int
}
var rankOptions types.RankOptions
if request.RankOptions == nil {
rankOptions = *engine.initOptions.DefaultRankOptions
} else {
rankOptions = *request.RankOptions
}
if rankOptions.ScoringCriteria == nil {
rankOptions.ScoringCriteria = engine.initOptions.DefaultRankOptions.ScoringCriteria
}
排序的准则如果在request中定义了,就用其中的;要么由引擎的初始化选项的默认排序准则定义。
收集关键词
如果输入了文档,就对文档进行分词,并去掉其中的停用词;
如果没有,就用输入数据定义的关键词;
生成查找请求,发送给索引器,然后从排序器中取数据
排序,输出