悟空(wukong)搜索引擎源代码阅读(待续)

本文介绍了悟空搜索引擎的源代码阅读过程,详细解析了初始化、文档索引、搜索流程,涉及分词器、索引器和排序器的工作原理。通过分析各个组件的通道和请求类型,展示了搜索引擎的基本操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

悟空(wukong)搜索引擎源代码阅读

最近为了了解搜索引擎的一些知识,阅读了wukong搜索的一些源码,在这里记录一下。项目地址在这里

整个的处理流程如下:

wukong-framework

在项目中主要有一下几个目录:

目录名作用
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中定义了,就用其中的;要么由引擎的初始化选项的默认排序准则定义。

收集关键词

如果输入了文档,就对文档进行分词,并去掉其中的停用词;

如果没有,就用输入数据定义的关键词;

生成查找请求,发送给索引器,然后从排序器中取数据

排序,输出

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值