介绍:日常pc使用或者app使用或者可供开发者参考的文档都会用到的一个常见的功能就是搜索,此项目就是如何去实现一个简单的搜索引擎。
技术要点:spring boot、mybatis、mysql
主要实现的模块包括四个:文档的准备(这里下载了一个文档到本地),构建索引,索引保存,搜索模块。
文档准备
1.这里常规的情况下应该是通过网络爬虫或者把用户提交过来的信息存成一个文档实时动态更新,但这里用的是本地下载的文档。
构建索引以及索引保存
正排索引: 由文档指向关键词。倒排索引: 由关键词指向文档。
项目中只针对html文档进行搜索,刚开始将文档以文件的形式进行存储,但查看打开文件的时候卡顿严重,在indexer目录下。
流程:
1.扫描文档目录的所有文档
通过FileScanner工具类对本地进行扫描得到文件列表
2.得到所需信息比如文档标题,最终url以及文档内容
根据文件列表构建文档列表,这里针对每个 html 文件,得到其 标题、URL、正文信息,把这些信息封装成一个对象(文档 Document)
3.构建索引
正排索引正常尾插,倒排索引对文档进行了分词操作(引入第三方分词工具),正排索引是一个IndexDocument类型的集合,倒排索引是一个map,key中存的是词,value就是一个DocWeight的集合,docweight包含的信息就是词,文档id和权重(这就是倒排的表现)
分词
调用工具类(Toanalysis)根据标题title分词得到一个list,然后根据list统计标题中词出现的次数用map来统计key是string,value是Integer,同理对正文进行分词计算词出现的次数,然后统计标题文章的词进行权重的计算,这里标题中的词*10正文中的词按原值进行计算.
4.保存索引信息
以json的格式存进去
// forward.json
File forwardFile = new File(properties.getIndexRootPath(), "forward.json");
objectMapper.writeValue(forwardFile, forwardIndex);
log.debug("正排索引已保存。{}", forwardFile.getCanonicalPath());
// inverted.json
File invertedFile = new File(properties.getIndexRootPath(), "inverted.json");
objectMapper.writeValue(invertedFile, invertedIndex);
log.debug("倒排索引已保存。{}", invertedFile.getCanonicalPath());
构建完成发现查看文件困难,决定保存成表记录。
正排索引表大概1W条数据,一次插入10条数据,倒排索引表虽然大部分都是词id什么的但是整体量级较大,到了百万级别,决定每次插入1W条。
正排索引之前的文章id是由尾插递增生成的,在数据库中可自增生成,把保存索引数据的过程变成批量插入,用到了mybatis。
搜索
访问接口要求进行传参,就是你想要查询的文字信息要手动传入
1.前端传词
2.根据拿到的词在正排索引表和倒排索引表查找拿到一个包含传入参数信息的结果集,这里值得一提的是多词搜索的时候如何给出文档的列表。
大体思路是这样的:
比如查三个词,会得出来三个结果集。每个结果集的docid是有可能重复的,把相同的docid的权重进行聚合然后进行重排序,但是这样对多词搜索的处理虽然有变化,但是查询的一个结果并不是特别的符合预期,也就是对于查询这一块通过一些策略肯定可以更好的进行查询具体代码如下
// 针对所有文档列表,做权重聚合工作
// 维护:
// docId -> document 的 map
Map<Integer, DocumentWithWeight> documentMap = new HashMap<>();
for (DocumentWithWeight documentWithWeight : totalList) {
int docId = documentWithWeight.getDocId();
if (documentMap.containsKey(docId)) {
DocumentWithWeight item = documentMap.get(docId);
item.weight += documentWithWeight.weight;
continue;
}
DocumentWithWeight item = new DocumentWithWeight(documentWithWeight);
documentMap.put(docId, item);
}
Collection<DocumentWithWeight> values = documentMap.values();
// Collection 没有排序这个概念(只有线性结构才有排序的概念),所以我们需要一个 List
List<DocumentWithWeight> list = new ArrayList<>(values);
// 按照 weight 的从大到小排序了
Collections.sort(list, (item1, item2) -> {
return item2.weight - item1.weight;
});
3.结果集完成之后去构建一个显示的描述,类似于百度中你查一个词,出来的条目文章下面会有一些简单信息的展示,这里构建的描述就是拿到词的索引,他前面截取一些词,后面截取一些词当做描述,然后写进document的desc属性,这样从前端进行获取的时候,描述信息就有了。