这是我参与「第三届青训营 -后端场」笔记创作活动的第2篇笔记
搜索引擎模块设计与实现--网络与引擎层
一、设计目标
- 根据上传的文本文件进行分词 和 并构建对应的倒排和正排索引以供查询
- 对客户端提供查询接口,并根据关键词进行分词然后调用 不同的索引查询出结果,对倒排查询出的所有结果用正排进行过滤,并用TF-IDF算法对结果集排序后返回,使得越符合关键词的结果越靠前
- 提供屏蔽关键词的搜索操作,使得结果集返回不含屏蔽词的查询结果
- 对客户端提供删除文章的操作,使得删除的文章不会再被搜索到
二、设计方案
模块架构图;
在引擎层进行查询时,搜索方面,在查询的时候会使用布尔模型将搜索词文档、筛选条件文档取∩交集,对过滤词取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查询操作
-
对请求的参数进行解析
startTime := time.Now()//搜索的开始时间,以便计算搜索耗时 indexName, hasIndex := params["index"] //索引名称 pageSize, hasPageSize := params["pageSize"] //返回结果每页的大小 curPage, hasCurPage := params["curPage"] //当前请求的页数 -
根据索引名称获取对应索引
//获取索引 idx := gde.idxManager.GetIndex(indexName) if idx == nil { return NotFound, errors.New(IndexNotFound) } -
建立过滤条件和搜索条件
searchFilters, searchQueries, notSearchQueries := gde.parseParams(params, idx) docQueryNodes := make([]utils.DocIdNode, 0) docFilterIds := make([]uint64, 0) notDocQueryNodes := make([]utils.DocIdNode, 0) -
对searchQueries的每个关键词调用索引查询,并将结果添加到备选集
for _, query := range searchQueries { ids, ok := idx.SearchKeyDocIds(query) if ok { docQueryNodes = append(docQueryNodes, ids...) } } -
建立关键词过滤集合
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...) } } -
求带返回集对过滤集的补集 来达到过滤屏蔽词的效果
//对 docQueryNodes 和 docFilterIds求交集, 注意类型 []DocIdNode 和 []uint64 // 使用 bool模型汇总 docMergeFilter := boolea.DocMergeFilter(docQueryNodes, docFilterIds, notDocQueryNodes) -
计算起始和终止位置 和分页 然后返回
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{}
\