gofound项目服务框架剖析(一) | 青训营笔记

565 阅读6分钟

这是我参与「第三届青训营-后端场」笔记创作活动的第4篇笔记

本系列笔记将对gofound项目的api源码进行分析,探究这个搜索引擎的架构。

0. 预备工作:配置vscode调试环境

为了方便后续对项目源码的跟踪和调试,首先学习利用vscode对go项目进行调试。这里主要参考

github.com/golang/vsco…

segmentfault.com/a/119000001…

首先尝试按下F5启动自动调试,这时vscode会提醒缺少golang调试器,按照vscode的提示安装golang调试器后利用vscode的“运行和调试”组件生成一个launch.json文件,在该文件中设置程序启动时的参数

点击vscode“运行和调试”UI上的运行箭头即可开始调试,下图展示了程序停止在main函数设置的断点处,UI中变量一栏显示了当前函数局部变量args的值,调用堆栈一栏显示了函数调用栈。

小结:为了方便后续对项目源码进行跟踪调试,首先配置好vscode的调试环境。

  1. 分析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提供的分词服务。

  1. 分析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存在且内容与原来一致,则不值得更新
  • 添加倒排索引到数据库,添加正排索引到数据库
  1. 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层提供的与数据库交互的服务