文章目录
一、项目简介
1.项目背景
目前搜索引擎技术已经是非常成熟了,很多网站,应用等都有属于自己搜索引擎。但是哪一个的性能好,哪一个用户用着舒服,就说不定了。搜索引擎虽然只是做搜索的,但是在各个地方都有用到,是许多系统必不可少的功能。而且搜索时间短,匹配度高,满足用户心意的搜索引擎才是最重要的。鉴于此,我也想做一个搜索引擎,锻炼自己的业务能力,加深自己对这方面技术的掌握程度。
2.项目描述
本项目主要实现了在前端输入框内输入需要搜索的Java API文档的关键字,对后端发出请求,后端将处理后的结果返回给前端,按照一定的权重排序展示若干个搜索结果,每个搜索结果包含了标题,描述,展示URL,可点击标题跳转,查看文档的详细内容。
3.项目条件
- 开发环境:IDEA、Tomcat 9、Maven、JDK1.8
- 相关技术:正排索引、倒排索引、分词技术、过滤器、Servlet、Json、Ajax
- 文档资源:我用的是jdk源码文件包解压之后的Java API文档,下载地址:点击这里
二、项目设计
1.数据库设计
(1)创建数据库“searcher”,在该数据库下创建正排索引表,包括文档id(docid)、标题(title)、url、文档内容(content),用于保存项目构建的正排索引;
CREATE TABLE `searcher`.`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='正排索引';
(2)创建倒排索引表,包括字段id、关键词(word)、文档id(docid)、单词权重(weight),用于保存构建的倒排索引;
CREATE TABLE `searcher`.`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`)
) COMMENT='倒排索引';
2.保存数据的实体类
(1)Document:
每一个 api 文档的 html 文件都对应一个该类,在该类中主要有四个属性字段,分别是:
docId:类似于数据库的主键可以对应单独一个文档
title:文档的文件名
url:Oracle 官网上的 api 文档下 html 的 url 地址
content:文档的正文部分
public class Document {
@Getter @Setter
private Integer docId;
@Getter
private final String title;
@Getter
private final String url;
@Getter
private final String content;
(2)InvertedRecord:
该类表示的是某个关键词在某个文档中的权值,在该类中主要有三个属性字段,分别是:
word:关键词
docId:该关键词对应的文档的id
weight:该关键词在该文档中的权值
public class InvertedRecord {
private final String word;
private final int docId;
private final int weight;
}
(3)Result:
该类表示的是将搜索内容进行分词后,会得到多个关键词,每个关键词会对应多个文档,而其中不乏出现重复的文档,这时就需要对重复文档进行合并,用文档 ID 作为唯一标识,将 ID 相同的文档的权值根据关键字先后顺序不同进行加权操作,最终所有会匹配到的文档都是唯一的,根据权值对其进行排序后返回前端展示。在该类中主要有三个属性字段,分别是:
title:该文档的标题
url:该文档的 url
decs:该文档的描述
public class Result {
private final String title;
private final String url;
private final String desc;
}
3.构建索引
索引构建程序原则上只执行一次即可,所以我们单独创建一个项目包indexer,存放构建索引的业务代码。下面是具体实现:
(1)遍历 api 文档存储的目录,对每个 html 文件进行读取解析,去掉多余的标签,并且将需要的信息提取出来并且封装到实体类 Document中,然后将所有提取到的信息持久化到本地的 文件中。具体实现:
public class DocumentBuilder {
private static final String SUFFIX = ".html";
private final IndexProperties properties;
@Autowired
public DocumentBuilder(IndexProperties properties) {
this.properties = properties;
}
public Document build(File rootFile, File docFile) {
String title = parseTitle(docFile);
String url = parseUrl(rootFile, docFile);
String content = parseContent(docFile);
return new Document(title, url, content);
}
@SneakyThrows
private String parseTitle(File file) {
String name = file.getName();
return name.substring(0, name.length() - SUFFIX.length());
}
@SneakyThrows
private String parseUrl(File rootFile, File docFile) {
String rootPath = rootFile.getCanonicalPath().replace('\\', '/');
String docPath = docFile.getCanonicalPath().replace('\\', '/');
String relativePath = docPath.substring(rootPath.length());
if (properties.getUrlPrefix().endsWith("/")) {
return properties.getUrlPrefix() + relativePath.substring(1);
} else {
return properties.getUrlPrefix() + relativePath;
}
}
@SneakyThrows
private String parseContent(File file) {
StringBuilder contentBuilder = new StringBuilder();
try (InputStream is = new FileInputStream(file)) {
try (Scanner scanner = new Scanner(is, "ISO-8859-1")) {
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
contentBuilder.append(line).append(" ");
}
}
}
return contentBuilder.toString()
.replaceAll("<script.*?>.*?</script>", " ") // 去掉 <script ...>...</script>
.replaceAll("<[^>]*>", " ") // 去掉所有标签 <...>
.replaceAll("&.*?;", " ") // 去掉 HTML 转义符
.replaceAll("\\s+", " ") // 合并空白字符
.trim(); // 去掉首尾空白字符
}
}
(2)首先加载本地文件内容,加载到正排索引的集合中,根据正排索引构建倒排索引(标题权重10,内容权重1),具体实现如下:
首先有一个 Map<String,Integer> 集合表示一个关键词对应多个 api 文档,然后遍历存储所有 DocInfo 类的 List,对于每一个 Doc都分别对标题和内容进行分词。
所以这里引入分词技术,分词技术使用的是一个开源的分词工具 Ansj,可以很高效的将句子进行分词处理。我们将分词之后的关键词加入 Map 集合,关键词作为键,Weight类作为值,用来保存每个关键词在对应的每个 api 文档中的权值。对于权值的计算,我们自定义的认为如果出现在标题中那么权值 乘10,如果出现在文章中,那么权值 +1,从而构建好倒排索引。具体代码实现如下:
public class docunment{
@Getter @Setter
private Integer docId;
@Getter
private final String title;
@Getter
private final String url