这是我参与「第三届青训营-后端场」笔记创作活动的第4篇笔记
本系列笔记将对gofound项目的api源码进行分析,探究这个搜索引擎的架构。
0. 预备工作:配置vscode调试环境
为了方便后续对项目源码的跟踪和调试,首先学习利用vscode对go项目进行调试。这里主要参考
首先尝试按下F5启动自动调试,这时vscode会提醒缺少golang调试器,按照vscode的提示安装golang调试器后利用vscode的“运行和调试”组件生成一个launch.json文件,在该文件中设置程序启动时的参数
点击vscode“运行和调试”UI上的运行箭头即可开始调试,下图展示了程序停止在main函数设置的断点处,UI中变量一栏显示了当前函数局部变量args的值,调用堆栈一栏显示了函数调用栈。
小结:为了方便后续对项目源码进行跟踪调试,首先配置好vscode的调试环境。
-
分析gofound主流程
以下是gofound的主函数,可以看到main函数做的事情就是根据参数args做一些初始化操作,得到一个提供分词服务和数据存储服务的container对象,之后调用initGin函数对Gin引擎进行初始化。
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("panic: %s\n", r)
}
}()
//解析参数
args := parseArgs()
//线程数=cpu数
runtime.GOMAXPROCS(args.GOMAXPROCS)
//初始化分词器,args.DictionaryPath的默认值是"./data/dictionary.txt"
tokenizer := initTokenizer(args.DictionaryPath)
container := initContainer(args, tokenizer)
//初始化gin
initGin(args, container)
fmt.Printf("Done!")
}
根据以上这个代码结构我们可以推断出该服务器通过Gin框架实现http通信,通过container提供检索服务,container又利用了tokenizer提供的分词服务。
-
分析searcher.Container提供的检索服务
Container结构体定义
type Container struct {
Dir string //文件夹
engines map[string]*Engine //引擎
Debug bool //调试
Tokenizer *words.Tokenizer //分词器
Shard int //分片
}
上面是Container结构体的定义,显然engines是检索引擎,Engine结构体的定义如下:
type Engine struct {
IndexPath string //索引文件存储目录
Option *Option //配置
invertedIndexStorages []*storage.LeveldbStorage //关键字和Id映射,倒排索引,key=id,value=[]words
positiveIndexStorages []*storage.LeveldbStorage //ID和key映射,用于计算相关度,一个id 对应多个key,正排索引
docStorages []*storage.LeveldbStorage //文档仓
sync.Mutex //锁
sync.WaitGroup //等待
addDocumentWorkerChan []chan *model.IndexDoc //添加索引的通道
IsDebug bool //是否调试模式
Tokenizer *words.Tokenizer //分词器
DatabaseName string //数据库名
Shard int //分片数
}
type Option struct {
InvertedIndexName string //倒排索引
PositiveIndexName string //正排索引
DocIndexName string //文档存储
}
根据注释我们可以看出Engine使用了3组数据库分别是invertedIndexStorages,positiveIndexStorages和docStorages。(源代码注释中的关键字可以理解为利用分词器得到的关键词,每条索引可以有多个关键字,但是只能有一个id)
- 倒排索引(invertedIndexStorages)存储关键词和id数组之间的映射
- 正排索引(positiveIndexStorages)存储id和关键词之间的映射
- 文档仓(docStorages)存储id和(索引,关键词)之间的映射
下面以添加一条gofound官方给出的索引为例,说明上面三种存储的差异
{
"id": 88888,
"text": "深圳北站",
"document": {
"title": "阿森松岛所445",
"number": 223
}
}
假设分词器为此索引生成的关键词为 “深圳”、“北站”
那么倒排索引(invertedIndexStorages)中会添加两个条目分别为:
{"深圳",[88888]}, {"北站",[88888]}
正排索引(positiveIndexStorages)中会添加一个条目为:
{88888,[“深圳”,“北站”]}
文档仓(docStorages)中会添加一个条目为:
{88888, { {"id": 88888, "text": "深圳北站","document": {"title": "阿森松岛所445","number": 223}}, [“深圳”,“北站”]}}
Container api分析
上文已经提到gofound通过Contain结构体中的Engine提供数据检索相关服务,下面对查询索引和添加索引对应两个api对应的源码进行分析
查询索引api
api定义:func (e *Engine) MultiSearch(request *model.SearchRequest) *model.SearchResult
请求、响应结构体定义:
// SearchRequest 搜索请求
type SearchRequest struct {
Query string `json:"query,omitempty"` // 搜索关键词
Order string `json:"order,omitempty"` // 排序类型
Page int `json:"page,omitempty"` // 页码
Limit int `json:"limit,omitempty"` // 每页大小,最大1000,超过报错
Highlight *Highlight `json:"highlight,omitempty"` // 关键词高亮
}
// SearchResult 搜索响应
type SearchResult struct {
Time float64 `json:"time,omitempty"` //查询用时
Total int `json:"total"` //总数
PageCount int `json:"pageCount"` //总页数
Page int `json:"page,omitempty"` //页码
Limit int `json:"limit,omitempty"` //页大小
Documents []ResponseDoc `json:"documents,omitempty"` //文档
Words []string `json:"words,omitempty"` //搜索关键词
}
type ResponseDoc struct {
IndexDoc
OriginalText string `json:"originalText,omitempty"`
Score int `json:"score,omitempty"` //得分
Keys []string `json:"keys,omitempty"`
}
主要逻辑流程:
- 调用分析器服务得到搜索关键词
- 多协程并行对关键词进行遍历
- 利用倒排索引找到每个关键词对应的索引id
- 计算每条索引id的得分,并按照得分从高到低排序
- 根据页号选出对应索引id的窗口,再根据id窗口去读取索引内容
增加/修改索引api
api定义:func (e *Engine) AddDocument(index *model.IndexDoc)
请求结构体定义:
// IndexDoc 索引实体
type IndexDoc struct {
Id uint32 `json:"id,omitempty"`
Text string `json:"text,omitempty"`
Document map[string]interface{} `json:"document,omitempty"`
}
主要逻辑流程:
- 调用分词器得到关键词
- 判断当前索引是否值得更新
- 如果当前id存在且内容与原来一致,则不值得更新
- 添加倒排索引到数据库,添加正排索引到数据库
-
Web api分析
上文已经介绍了container中的engine提供了索引查找和索引添加的服务,Web层Api可以直接调用这些服务实现,下面展示了Web层的query 和 addIndex两个api的实现
查询索引API
func (a *Api) query(c *gin.Context) {
var request = &model.SearchRequest{}
err := c.BindJSON(&request)
if err != nil {
c.JSON(200, Error(err.Error()))
return
}
//调用Container提供的搜索服务
r := a.Container.GetDataBase(c.Query("database")).MultiSearch(request)
c.JSON(200, Success(r))
}
请求、响应结构体定义:
增加/修改索引API
func (a *Api) addIndex(c *gin.Context) {
document := &model.IndexDoc{}
err := c.BindJSON(&document)
if err != nil {
c.JSON(200, Error(err.Error()))
return
}
//调用Container提供的添加索引服务
//这些协程(生产者)实际上将索引缓存到了Container中chan缓存队列中
//Container内部的消费者协程(go e.DocumentWorkerExec(worker))
//负责将索引数据取出并保存到数据库
go a.Container.GetDataBase(c.Query("database")).IndexDocument(document)
c.JSON(200, Success(nil))
}
请求、响应结构体定义:
响应结构体示例:
{
"state": true,
"message": "success"
}
总结
本文对gofound项目Web层和搜索引擎层核心API进行了分析,总结出gofound服务框架的基本轮廓:
暂时无法在文档外展示此内容
- Web层利用Gin框架处理与客户端之间的数据传输,利用Container提供的服务实现索引的查询和添加
- Container层中的Engine实现了查询索引和添加索引的具体逻辑,调用Tokenizer提供的分词服务,
调用LeveldbStorage层提供的与数据库交互的服务