搜索引擎模块设计与实现--网络与引擎层 | 青训营笔记

253 阅读4分钟

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

搜索引擎模块设计与实现--网络与引擎层

一、设计目标

  • 根据上传的文本文件进行分词 和 并构建对应的倒排和正排索引以供查询
  • 对客户端提供查询接口,并根据关键词进行分词然后调用 不同的索引查询出结果,对倒排查询出的所有结果用正排进行过滤,并用TF-IDF算法对结果集排序后返回,使得越符合关键词的结果越靠前
  • 提供屏蔽关键词的搜索操作,使得结果集返回不含屏蔽词的查询结果
  • 对客户端提供删除文章的操作,使得删除的文章不会再被搜索到

二、设计方案

模块架构图;

笔记.png

image.png

在引擎层进行查询时,搜索方面,在查询的时候会使用布尔模型将搜索词文档、筛选条件文档取∩交集,对过滤词取NOT补集,查找出所有相关文档。使用相关性评分函数使用TF-IDF算法、字段长度归一值BOOST、向量空间模型、协调因子实现对所有相关文档进行权重排序。 项目实时搜索与相关搜索用字典树(trie)实现,按照搜索词频次、长短依次排序。实时搜索是根据用户输入实时提示相关内容下拉框,内容<=10个。

1、网络层

功能分析

对客户端提供发布文章的接口

对客户端提供关键词的查询接口

根据客户端的URL请求,解析请求的参数,并根据URL的参数解析后 调用引擎层的方法查询结果,对结果用算法排序后返回给客户端

2、搜索引擎层

  • 实现文章的上传和文章分析和分词,并对文章的标题,正文..等字段构建倒排和正排索引
  • 业务逻辑层:实现文档的上传功能,文档的删除功能,关键词的搜索功能。实现对具体的Term的查询操作,对多个term的结果取交集并集进行合并
  • 对关键词进行过滤操作
  • 调用算法TF-IDF等算法对结果集进行排序,将结果返回给客户端

3、索引管理层

模块功能:

负责与数据层对接,调用数据层提供的关于索引的接口,对索引进行增删改查操作

三、具体实现

引擎层:

1.相关搜索

每次查询时先利用Trie 树的结构存储本次查询的关键字,

每次查询时根据搜索的关键字返回Trie树里相关的10个历史搜索内容

func (gde *GoDanceEngine) RelatedSearch(key string) (string, error) {
    search := gde.trie.Search(key, false)
    results, err := json.Marshal(search)
    if err == nil {
        return string(results), err
    }
    return "", nil
}

2查询操作

  1. 对请求的参数进行解析

    startTime := time.Now()//搜索的开始时间,以便计算搜索耗时
    indexName, hasIndex := params["index"] //索引名称
    pageSize, hasPageSize := params["pageSize"] //返回结果每页的大小
    curPage, hasCurPage := params["curPage"] //当前请求的页数
    
  2. 根据索引名称获取对应索引

    //获取索引
    idx := gde.idxManager.GetIndex(indexName)
    if idx == nil {
        return NotFound, errors.New(IndexNotFound)
    }
    
  3. 建立过滤条件和搜索条件

      searchFilters, searchQueries, notSearchQueries := gde.parseParams(params, idx)
      docQueryNodes := make([]utils.DocIdNode, 0)
      docFilterIds := make([]uint64, 0)
      notDocQueryNodes := make([]utils.DocIdNode, 0)
    
  4. 对searchQueries的每个关键词调用索引查询,并将结果添加到备选集

    for _, query := range searchQueries {
            ids, ok := idx.SearchKeyDocIds(query)
            if ok {
                docQueryNodes = append(docQueryNodes, ids...)
            }
        }
    
  5. 建立关键词过滤集合

    for _, filter := range searchFilters {
            ids, ok := idx.SearchFilterDocIds(filter)
            if ok {
                docFilterIds = append(docFilterIds, ids...)
            }
        }
    for _, query := range notSearchQueries {
        ids, ok := idx.SearchKeyDocIds(query)
        if ok {
            notDocQueryNodes = append(notDocQueryNodes, ids...)
        }
    }
    ​
    
  6. 求带返回集对过滤集的补集 来达到过滤屏蔽词的效果

    //对 docQueryNodes 和 docFilterIds求交集, 注意类型 []DocIdNode 和 []uint64
    // 使用 bool模型汇总
    docMergeFilter := boolea.DocMergeFilter(docQueryNodes, docFilterIds, notDocQueryNodes)
    
  7. 计算起始和终止位置 和分页 然后返回

resultSet.Results = make([]map[string]string, 0)
    for _, docId := range docMergeFilter[start:end] {
        doc, ok := idx.GetDocument(docId)
        doc["id"] = fmt.Sprintf("%v", docId)
        if ok {
            resultSet.Results = append(resultSet.Results, doc)
        }
    }
​
    resultSet.From = start + 1
    resultSet.To = end
    resultSet.Status = "OK"
    resultSet.TotalCount = lens
    endTime := time.Now()
    resultSet.CostTime = fmt.Sprintf("%v", endTime.Sub(startTime))
​
    r, err := json.Marshal(resultSet)
    if err != nil {
        return NotFound, err
    }
​
    return string(r), nil

索引管理层:

结构体定义

type IndexInfo struct {
    Name string `json:"name"`
    Path string `json:"path"`
}
​
type IndexManager struct {
    indexers       map[string]*gdindex.Index
    indexMapLocker *sync.RWMutex
    IndexInfos     map[string]IndexInfo `json:"indexinfos"`
    Logger         *utils.Log4FE        `json:"-"`
}

对引擎层提供构造方法

func newIndexManager(logger *utils.Log4FE) *IndexManager {
    idm := &IndexManager{
        indexers:       make(map[string]*gdindex.Index),
        indexMapLocker: new(sync.RWMutex),
        IndexInfos:     make(map[string]IndexInfo),
        Logger:         logger,
    }
    // 如果之前有记录则进行反序列化
    if utils.Exist(fmt.Sprintf("%v%v.idm.meta", utils.IDX_ROOT_PATH, utils.GODANCEENGINE)) {
    ....
    }
    //检查是否要段合并
    
    .....
    
    idm.Logger.Info("[INFO]  New Index Manager ")
    return idm
}

对数据层的操作

//根据索引名称获取对应的索引管理对象
func (idm *IndexManager) GetIndex(indexName string) *gdindex.Index {
}
​
//创建索引
func (idm *IndexManager) CreateIndex(indexName string, fields []segment.SimpleFieldInfo) error{
    //加锁
    idm.indexMapLocker.Lock()
    defer idm.indexMapLocker.Unlock()
}
​
//把Term添加到对应的索引上
func (idm *IndexManager) AddField(indexName string, field segment.SimpleFieldInfo) error{   
}
​
//删除对应索引上的Term
func (idm *IndexManager) DeleteField(indexName string, fieldName string) error {
    
}
​
//对文章提供添加,删除,更新 操作
func (idm *IndexManager) addDocument(indexName string, document map[string]string) (string, error) {}
func (idm *IndexManager) deleteDocument(indexName string, pk string) (string, error) {}
func (idm *IndexManager) updateDocument(indexName string, document map[string]string) (string, error) 
​
//将索引以json的格式持久化到磁盘上
func (idm *IndexManager) storeIndexManager() error {
    
}
​
func (idm *IndexManager) sync(indexName string) error{}

\