在我们的学习过程中,会阅读很多的文档,例如jdk的API文档,但是在这样的大型文档中,如果没有搜索功能,我们是很难找到我们想查阅的内容的,于是我们可以实现一个搜索引擎来帮助我们阅读文档。
1. 实现思路
1.1 获取文档
第一点,要搜索指定内容,首先要先获取到内容,我们以实现Java API文档搜索引擎来说,我们要先获取到Java的API文档,我们可以在Oracle的官网找到:Overview (Java SE 17 & JDK 17) (oracle.com)
Oracle官网提供了在线和离线两种文档,我们可以下载离线文档,通过离线文档来实现。
离线文档下载地址:Java 开发工具包 17 文档 (oracle.com)
下载好后解压缩,在 jdk-17.0.11_doc-all\docs\api 目录和子目录下的所有html文件就是所有的api文档
1.2 通过关键词查询
获取到了文档,我们还需要能够通过关键词定位到相关的文档,这里需要用到索引
- 正排索引: 给每个文档引入一个文档id,文档id是每个文档的身份标识,不能重复,通过文档id快速获取到对应文档就叫正排索引。
- 倒排索引:通过一个或几个关键词查询到与之有关的所有文档的文档id,这种方式就叫到排索引。
于是要实现关键词查询,我们只需要给下载好的Java API文档实现一个正排索引和倒排索引,通过到排索引查询到相关的文档的id,要查看某个文档时再用查询到的id使用正排索引查询到对应文档。
1.3 如何返回查询到的结果
查询到对应的api文档之后,如何返回给用户,这里我的想法是返回一个在线文档的url,当用户想要查看某个文档时,返回Oracle官方的在线文档对应的页面的url。
那么此种方式就需要我们把在线文档的url和离线文档联系起来:
我们观察某个文档的url和在线文档的本地路径:
在线文档:
离线文档:
我们发现相同api文档的在线版本的url和离线版本路径,它们的后半部分是相同的,所有我们只需要通过一些字符串拼接操作,就可以通过离线文档的文件路径得到在线文档的url。
1.4 模块划分
通过上面的叙述,我们可以对我们的程序进行一个模块划分:
- 索引模块:扫描并解析所有的本地文档并构建出索引;提供一些API实现查正排/到排的功能
- 搜索模块:调用索引模块通过关键词查询到相关文档信息,并处理后返回
- Web模块:实现一个简单的Web程序,能通过网页的形式和用户交互
2. 索引模块实现
创建一个Spring项目
2.1 实现Parser类
实现一个Parser类用于扫描并解析本地的离线文档:
package org.example.docsearcher.config;
@Configuration
public class Parser {
//指定文档文件的路径
private static final String FILE_PATH = "D:/桌面/jdk-17.0.11_doc-all/docs/api";
//解析文档
private void parser() {
//1.找出所有html文件
List<File> fileList = new ArrayList<>();
enumFile(FILE_PATH, fileList);
//2.对每个HTML文件进行解析
for(File f : fileList) {
parserHTML(f);
}
}
//枚举出所有的html文件
private void enumFile(String filePath, List<File> fileList) {
}
//解析出html文件的内容
private void parserHTML(File file) {
}
}
实现enumFile方法:
private void enumFile(String filePath, List<File> fileList) {
File file = new File(filePath);
//获取目录下的文件列表
File[] files = file.listFiles();
for(File f : files) {
if(f.isDirectory()) {
//如果f是目录则递归添加文件
enumFile(f.getAbsolutePath(), fileList);
}else if(f.getName().endsWith(".html")){
//如果f是html文件则添加到fileList中
fileList.add(f);
}
}
}
实现parserHTML方法:
要实现parserHTML方法我们要先理清楚,html文件中有什么和我们需要什么:
- 标题:返回查询结果时,可以展示给用户以供选择
- 正文:用于提取关键词构建倒排索引
- url:用户点击时通过url跳转到对应页面
private void parserHTML(File file) {
//a.解析出标题
String title = parserTitle(file);
//b.解析出url
String url = parserUrl(file);
//c.解析出正文
String content = parserContent(file);
}
private String parserContent(File file) {
StringBuilder content = new StringBuilder();
//按字节读,这里使用BufferedReader 提高速度
try(BufferedReader bufferedReader = new BufferedReader(new FileReader(file), 1024 * 1024)) {
while(true) {
int ch = bufferedReader.read();
if(ch == -1) {
//文件读完了
break;
}
content.append((char)ch);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
String ret = content.toString();
//使用正则表达式替换掉js代码
ret = ret.replaceAll("<script.*?>.*?</script>", "");
//使用正则表达式替换html标签
ret = ret.replaceAll("<.*?>", "");
//把换行符和连续的多个空格替换为一个空格使内容更美观
ret = ret.replaceAll("\\s+", " ");
}
private String parserUrl(File file) {
//拼接出在线文档对应的url
String s1 = "https://docs.oracle.com/en/java/javase/17/docs/api";
String s2 = file.getAbsolutePath().substring(FILE_PATH.length()).replace("\\", "/");
return s1 + s2;
}
private String parserTitle(File file) {
String fileName = file.getName();
return fileName.substring(0, fileName.length() - ".html".length());
}
注意:FileReader的read方法是每次从磁盘里读取一个字符到内存中,BuferedReader 内部带有一个缓存区,会一次把多个字符加载到缓存区中,调用read方法时会从缓存区中读取字符,减少直接访问磁盘的次数提高了速度,构造方法中的第二个参数就是设置缓冲区的大小,单位是字节
2.2 实现Index类
实现Index类用于创建索引和通过关键词和索引查询相关文档:
前排索引由文档id和文档组成,要求能够通过文档id快速查询到文档,索引我们可以使用一个List来储存前排索引,即通过数组下标当作文档id,数组的内容即为文档的信息,于是我们创建一个DocInfo类用于存储文档信息:
package org.example.docsearcher.model;
import lombok.Data;
@Data
public class DocInfo {
//储存一个文档的相关信息
private int docId;
private String title;