如今,大大小小的网站以及软件都可能会涉及到搜索这一功能,因此,计划写出一个基于web的搜索相关文档的搜索引擎项目。以锻炼自己的能力,加强对技术的应用,能够更好的掌握相关的技术,了解互联网的发展。众所周知,搜索本身看起来貌似很简单,很多方法其实都可以做出来,比如简简单单的sql语句的模糊匹配也就可以实现这一功能,为什么非要做一个项目来实现搜索引擎呢?最主要的原因还是相关的性能问题,试想一下,若在平时生活中搜索一个词的时间长达几分钟甚至更长,这是人们所接受不了的,因此,设计一个搜索引擎就显现的非常的重要,能够加快搜索的性能,能够实现很快出结果,在性能上能够进行日常的应用
该项目一共分为两大模块,一大模块是建立索引模块,另一大模块是搜索模块。
建立索引模块分为扫描文档,构建文档,构建保存正排索引和构建保存倒排索引。
扫描文档是将本地文档进行扫描,并将其保存到内存中以供其之后的操作,构建文档相当于
正排索引是把相关的文档进行编号,每篇文档有自己的编号id,以及自己的标题,自己的url,自己的正文等相关的信息,目的是通过倒排索引相关的单词找到正排索引并且进行展示相关的文档,能够查到到相关的文档。
倒排索引相当于把文档中包括标题的每一个进行分割开来,并且每篇进行词的统计,保存相应词语的文章id跟权重(为了显示的时候的排序问题),权重的计算方法为文章标题出现的次数* 10 + 文章正文出现的次数,根据单词进行查询,找到相关文档的id,然后根据id显示。
该项目的软件层次结构图如下:
软件层次图
该设计的数据库主要作用是进行正排索引和倒排索引的保存。
正排索引的字段主要有:
docId --文章的id
title --文章的标题
url --文章对应的url
content --文章的正文
表的结构如图所示。
正排索引表
建表语句如下
CREATE TABLE `forward_indexes`
( `docid` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(100) NOT NULL,
`url` varchar(200) NOT NULL,
`content` longtext NOT NULL,
PRIMARY KEY (`docid`))
COMMENT=\'存放正排索引\\ndocid -> 文档的完整信息\''
id --对应单词的id
word --对应的单词
docid --对应的文章id
weight --该单词在该文章对应的权重
表的结构如下图所示。
倒排索引表
值得注意的是,在后续的查询过程中,为了提高查询的效率,在该表建立了相关word和weight的联合索引,以达到性能提升的目的。
CREATE TABLE `inverted_indexes`
( `id` int(11) NOT NULL AUTO_INCREMENT,
`word` varchar(100) NOT NULL,
`docid` int(11) NOT NULL,
`weight` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `INDEX_word_weight` (`word`,`weight`))
ENGINE=InnoDB AUTO_INCREMENT=2155795 DEFAULT CHARSET=utf8mb4
COMMENT=\'倒排索引\\n通过 word -> [ { docid + weight }, { docid + weight }, ... ]'
--建立索引--
ALTER TABLE `searcher_refactor`.`inverted_indexes`
ADD INDEX `word_weight_Indexs` (`word` ASC, `weight` DESC);
(1)正排索引的插入
再插入正排索引时,采用了mybatis进行动态sql的技术进行插入,相关语句如下:
insert into forward_indexes (title,url,content) values
(#{doc.title},#{doc.url},#{doc.content})
(2)倒排索引的插入
在进行倒排索引插入时,也采用了Mybatis动态sql继续插入,相关语句如下:
insert into inverted_indexes(word,docid,weight) values
(#{record.word},#{record.docId},#{record.weight})
(3)查询一个单词如下语句如下:
(4)查询单词带权重如下
扫描文档的主要任务是将本地的文档进行扫描,本次设计以JDK1.8文档为主,将本地JDK1.8文档进行扫描,加载其中的.html文档,获取文档的路径内容标题等信息。
构建文档是将加载进来的模块进行构建,构建出相应的标题,url,路径等信息,用List列表进行保存,这里采用一种JDK1.8中stream流的特性进行处理。
File rootFile = new File(properties.getDocRootPath());
List documentList = htmlFileList.stream()
.parallel() //使用说stream用法,可以使用。pararllel()。使得整个操作并称并行,利用多核增加运行速度
.map(file -> new Document(file, properties.getUrlPrefix(), rootFile))
.collect(Collectors.toList());
log.debug("构建文档完毕,一共{}篇文档", documentList.size());
构建保存正排索引就是将刚刚建立好的文章插入到数据库中以持久化保存,构建正排索引最开始采用单线程一条一条进行插入,最后发现性不理想,采取优化措施,再进行sql的批量插入的基础上,然后进行多线程插入,以提高该项目的性能。
public void saveForwardIndexesConcurrent(List documentList){
//批量插入每次插入多少条数据
int batchSize = 10;
//一共需要执行多少次sql
int listSize = documentList.size();
// 向上取整
int times =(int) Math.ceil(1.0 * listSize / batchSize);
log.debug("一共需要{} 批任务",times);
//同ing及每个线程的完成情况,初始值是times 一共多少批
CountDownLatch latch = new CountDownLatch(times);
//开始插入
for (int i = 0; i < listSize; i += batchSize) {
int from = i;
int to = Integer.min(from + batchSize, listSize);
//Lamba表达式用外部变量必须为隐式final对象
Runnable task = () ->{
List subList = documentList.subList(from, to);
//针对此subList做插入
mapper.batchInsertForwardIndexes(subList);
latch.countDown(); // 执行完后 ,让countDown 让latch--;
};
//主线程只负责把一批批任务提交到线程池,具体工作由其他线程完成
executorService.submit(task);
}
//4.循环结束后,只是意味着主线程把任务提交完成了
latch.await();
}
在构建倒排索引的过程中,由于也考虑到性能的问题,也采用了相关的批量sql插入并且加入多线程的操作,对于倒排索引来说,需要先将文章和标题的内容进行分词管理,这里采用分词库ansj进行分词管理,在进行分词的过程中对于分出的词进行统计并计算权重。
分词库依赖
org.ansj
ansj_seg
5.1.6
public void saveInvertedIndexesConcurrent(List documentList) {
//批量插入最多10000条
int batchSize = 10000;
int gorupSize = 50;
int listSize = documentList.size();
int times = (int) Math.ceil(listSize * 1.0 / gorupSize);
CountDownLatch latch = new CountDownLatch(times);
//放本次插入的数据
for (int i = 0; i < listSize; i += gorupSize) {
int from = i;
int to = Integer.min(from + gorupSize, listSize);
List subList = documentList.subList(from, to);
Runnable task = new InvertedInsertTask(subList, mapper, batchSize,latch);
executorService.submit(task);
}
latch.await();
}
public void run() {
List recordList = new ArrayList<>();
for(Document document: documentList){
Map wordToWeight = document.segWordAndCalcWeight();
for (Map.Entry entry : wordToWeight.entrySet()){
String word = entry.getKey();
int weight = entry.getValue();
int docId = document.getDocId();
InvertedRecord record = new InvertedRecord(word,docId,weight);
recordList.add(record);
if(recordList.size() == batchSize){
mapper.batchInsertInvertedIndexes(recordList);
// 清空List
recordList.clear();
}
}
}
//还剩下一些
mapper.batchInsertInvertedIndexes(recordList);
recordList.clear();
latch.countDown();
}
这样就能够进行批量的插入和多线程进行插入。性能成倍的提升。
最后,再利用spring的特性aop进行切片的及时操作,对于构建保存正排索引构建保存倒排索引进行aop切片的设计进行了统计时间。运行效果如下:
通过解析url进行参数的获取,获取到相关的参数后进行参数的合法性检验等等。
首先通过关键字查询倒排索引获取相关的文章id,通过文章id查询相关的文章信息。
通过关键字进行文档综合权重的计算,并且进行相关的排序,通过stream流的操作进行。
List totalList = new ArrayList<>();
for (String s : queryList) {
List documentList = searchMapper.queryWithWeight(s, limit, offset);
totalList.addAll(documentList);
}
//针对所有的文档列表,做聚合
//维护:docId -》 document 的map
//{docId,weight} 的list
Map documentMap = new HashMap<>();
for (DocumentWightWeight documentWightWeight : totalList) {
int docId = documentWightWeight.getDocId();
if (documentMap.containsKey(docId)) {
DocumentWightWeight item = documentMap.get(docId);
item.weight += documentWightWeight.weight;
continue;
}
DocumentWightWeight item = new DocumentWightWeight(documentWightWeight);
documentMap.put(docId, item);
}
Collection values = documentMap.values();
//Collection 没有排序的概念
ArrayList list = new ArrayList<>(values);
//按照weight从大到小排序
Collections.sort(list, (item1, item2) -> {
return item2.weight - item1.weight;
});
int from = (page - 1) * 20;
int to = Integer.min(from + 20,list.size());
List subList = list.subList(from, to);
List documentList = subList.stream()
.map(dww -> dww.toDocument())
.collect(Collectors.toList());
//lanmda不能使用final变量
String word = query;
List wordList = queryList;
documentList = documentList.stream()
.map(doc -> descBuilder.build(wordList, doc))
.collect(Collectors.toList());
前端采用相关的thymeleaf模版技术来进行的渲染操作。
model.addAttribute("query", query);
model.addAttribute("docList", documentList);
model.addAttribute("page", page);
前端主要是应用html和css样式的设计以及js对于DOM树的修改以及与后端进行数据交互。
1.项目首页
2.项目搜索页
项目搜索页
1.为什么要做这个项目?
首先最近刚刚入门spring ,为了巩固自己所学的技术,想找一个项目锻炼一下自己的技术,同时由于搜索引擎的应用非常的广泛,为了了解这个项目背后的原理以及对于spring的熟悉阶段,做了一个基于springboot的搜索引擎项目来锻炼自己。同时,也是为了在以后的生活以及工作中能够应用到这个项目中的一些知识点等。
2.本项目的难点是什么?
本项目的难点在于正排索引以及倒排索引的设计,首先需要记录每一篇文章的标题以及各个文章之间的分词,文章利用正则表达式进行单词的挑选,在设计正则表达式时是挺不容易的,最后通过查询搞清了正则表达式的一些相关用法,其次就是后来的调优阶段,在首先插入的时候需要半个小时之久,插入两百多万条数据确实不是一个小的数目,最后通过批量的插入以及相关的线程池的多线程解决了此问题
3.本次项目对你最大的提升是什么?
通过本次项目,最大的提升莫过于技术方面的提升,使得自己对于springboot框架更加清楚基本流程是什么,以及对于每一个数据他背后所存在的意义是什么,每个注解的意义是什么,该怎么使用,以及对于spring框架的熟悉,在中间碰到了许许多多的错误,通过一点点的摸索,解决相关的问题,同时,对于自己处理能力的方法以及手段有了进一步的提升。