项目描述
我们要实现什么样的搜索引擎呢?
该项目主要针对Java API 文档设计出一款文档搜索引擎,当用户在页面上输入查询词后,能够快速的匹配出相关API在线文档,补充了Java在线文档中没有搜索的功能。 每个搜索结果中包含了标题, 描述, 展示 url 和点击 url等信息,便于用户浏览。
Java API 文档线上版本参见: https://docs.oracle.com/javase/8/docs/api/index.html
项目流程
1.对离线版本的HTML文档进行解析,将解析的结果整理为一个行文本文件;
2.读取处理好的行文本文件进行Ansj分词,权重计算等操作,在内存中构造正排索引和倒排索引;
3.根据输入查询词进行分词、触发依据相似性分析以及倒排索引结构对结果进行检索排序,并以Json格式进行包装后序列化为字符串返回;
4.编写简单页面,通过HTTP服务器搭载搜索页面,点击搜索结果可跳转至对应API在线文档。
开发环境:IDEA、Tomcat、Maven、JDK1.8
相关技术栈:Ansj分词、倒排索引、过滤器、HTML、Servlet、Json、Ajax
项目代码:https://github.com/xiaoting-hub/Projects-DocSearcher
基础知识
什么是倒排索引?
文档(DOC):经过预处理,用户输入关键字要被检索的页面
正排索引:“一个文档包含了哪些词”。描述一个文档的基本信息, 包括文档标题、 正文,以及文档标题和正文的分词/断句结果。
倒排索引:“一个词被哪些文档引用了”。 描述一个词的基本信息, 包括这个词都被哪些文档引用, 这个词在该文档中的重要程度以及这个词的出现位置等信息。
问题:为什么要用倒排索引?暴力搜索行不行啊?
每次处理搜索请求的时候, 拿着查询词去所有的网页中搜索一遍,检查每个网页是否包含查询词字符串。显然,暴力搜索这种方式随着文档数量的增加开销会线性增加,一般我们对搜索引擎的效率还是比较看重的。所以尽可能的高效才是重点(^ . ^)。
为什么要进行分词?分词的原理,在该项目中分词如何来实现?
用户输入的关键字有时候是多个词/一句话,要搜索准确就必须进行分词,分词原理有两个方面:一种是基于词库,尝试把这些词进行穷举,放到字典文件中,我们可以依次取句子中的内容,每隔一个词进行查找。第二种是基于统计,会有很多官方的语料库进行人工标注/统计,分词技术在NLP中也比较常见。我们在该项目中使用Maven中的第三方库Ansj分词技术。
附带依赖链接:https://mvnrepository.com/artifact/org.ansj/ansj_seg/5.1.6
基本实现
模块划分
项目总共划分为三个模块:
索引模块:扫描下载到的文档,分析数据内容构建正排+倒排索引,并保存到文件中。
搜索模块:加载索引。根据输入的查询词, 基于正排+倒排索引进行检索,得到检索结果。
Web模块:编写一个简单的页面,展示搜索结果。点击其中的搜索结果能跳转到对应的 Java API 文档页面。
创建项目
使用IDEA创建一个SpringBoot项目,具体细节不在详细赘述。项目的目录结结构大致是这样:
引入分词依赖
使用Ansj分词第三方库,可以看一些简单的示例:java分词-ansj的初次使用
在pom.xml中注入依赖:
<dependency>
<groupId>org.ansj</groupId>
<artifactId>ansj_seg</artifactId>
<version>5.1.6</version>
</dependency>
注意:当 ansj 对英文分词时,会自动把单词大写转为小写。
实现索引模块
我们要实现的是在本地基于离线文档制作索引,实现检索,当用户在搜素结果页点击具体的搜索结果时,就自动跳转到在线文档的页面。跳转过去的目标页面也称为落地页面。
1. 实现Parse类
Parse类构建一个可执行程序。
① 根据指定路径,枚举出该路径中的所有文件(html),这个过程中需要把所有子目录的文件获取到;
② 根据文件罗列出的文件路径,打开文件,读取文件内容,解析并构建索引;
a) 标题:直接使用解析操作
b) URL:基于文件路径进行了简单拼接(离线文档和线上文档路径的关系)
c) 正文:核心操作,去标签~简单粗暴的方式实现的。使用<>作为“是否考虑要拷贝数据”的开关
③ 把在文件中构建好的索引数据结构,保存在指定的文件,使用Index类中的addDoc()方法。
Parse类最主要的事情是辅助Index类完成索引的制作过程。详细代码见上述github链接。
public class Parser {
private static final String INPUT_PATH = "E:/IdeaProjects/doc_searcher_index/jdk-8u231-docs-all/docs/api";
//TODO补充上索引实例
public static void main(String[] args) throws InterruptedException {
Parser parser = new Parser();
parser.run();
}
public void run() {
System.out.println("开始解析!");
long beg = System.currentTimeMillis();
// 1. 枚举出这个目录下的所有文件
ArrayList<File> fileList = new ArrayList<>();
enumFile(INPUT_PATH, fileList);
for (File f : fileList) {
System.out.println("解析 " + f.getAbsolutePath());
// 2. 针对每个文件, 打开, 并读取内容, 进行转换
parseHTML(f);
}
System.out.println("解析完成! 开始保存索引!");
long end = System.currentTimeMillis();
System.out.println("保存索引完成! 时间: " + (end - beg));
}
// 递归完成目录枚举过程
private void enumFile(String rootPath, ArrayList<File> fileList) {
File rootFile = new File(rootPath);
File[] files = rootFile.listFiles();
for (File f : files) {
if (f.isDirectory()) {
enumFile(f.getAbsolutePath(), fileList);
} else if (f.getAbsolutePath().endsWith(".html")) {
fileList.add(f);
}
}
}
private void parseHTML(File f) {
// 1. 转换出标题
String title = parseTitle(f); //得到的文件名字-“.html”
// 2. 转换出 url
String url = parseUrl(f); //网络URL和本地URL进行拼接
// 3. 转换出正文(正文需要去除 html 标签)
String content = parseContent(f); // 先按照一个字符一个字符的方式来读取,以 < 和 > 来控制拷贝数据的开关
// 4. TODO 添加到索引中
}
private String parseTitle(File f) {
// 直接使用文件名作为标题
String name = f.getName();
return name.substring(0, name.length() - ".html".length());
}
private String parseUrl(File f) {
// 这个 url 是指在线文档对应的链接.
// url 由两个部分构成.
// 第一部分是 https://docs.oracle.com/javase/8/docs/api
// 第二部分是 文件路径中 api 之后的部分.
String part1 = "https://docs.oracle.com/javase/8/docs/api";
String part2 = f.getAbsolutePath().substring(INPUT_PATH.length());
return part1 + part2;
}
private String parseContent(File f) {
// 读取文件内容, 并去除其中的 html 标签和换行
try {
FileReader fileReader = new FileReader(f);
// 是否当前读的字符是正文
boolean isContent = true;
StringBuilder output = new StringBuilder();
while (true) {
int ret = fileReader.read();
if (ret == -1) {
break;
}
char c = (char)ret;
if (isContent) {
if (c == '<') {
isContent = false;
continue;
}
if (c == '\n' || c == '\r') {
c = ' ';
}
output.append(c);
} else {
if (c == '>') {
isContent = true;
}
}
}
fileReader.close();
return output.toString();
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
}
2. 实现Index
Index 负责构建索引数据结构。主要提供以下方法:
- getDocInfo():根据 docId 查正排,返回类型是DocInfo类,包含文档中的详细信息(docId, title, url, content),直接按照下标来取元素
- getInverted():根据关键词查倒排,返回值类型为List列表;Weight包含了docId和weight权重;按照key取HashMap<String, ArrayList>的value即可
- addDoc(): 往索引中新增一个文档,包括构建正排索引和构建倒排索引。①构建正排,构造DocInfo对象,添加到正排索引末尾。②构建倒排,先进行标题和正文分词,统计词频。遍历分词结果,去更新倒排索引中对应的倒排拉链即可,同时注意线程安全问题。
- save():往磁盘中写索引数据,使用ObjectMapper类保存成Json格式;ObjectMapper类是Jackson库的主要类。它能够提供writeValue()和readValue()方法将Java对象和JSON结构相互转换(序列化和反序列化),基于JSON格式把索引数据保存到指定文件中
- load():从磁盘加载索引数据,基于JSON格式对数据进行解析,将硬盘中的文件读出来,解析到内存中
创建Index类
首先,我们要了解Parse类和Index类的关系,Parse类相当于制作索引的入口,Index类相当于实现了索引的数据结构,可以提供一些API。
所用到的类结构:
文章来源:https://www.toymoban.com/news/detail-414856.html
public class Index {
public static final String INDEX_PATH = "E:/IdeaProjects/doc_searcher_index/";
// 正排索引, 下标对应 docId
private ArrayList<DocInfo> forwardIndex = new ArrayList<>();
// 倒排索引, key 是分词结果, value 是这个分词 term 对应的倒排拉链(包含一堆 docid)
private HashMap<String, ArrayList<Weight>> invertedIndex = new HashMap<>();
// 根据 docId 查正排
public DocInfo getDocInfo(int docId) {
}
// 根据 分词结果 查倒排
public ArrayList<Weight> getInverted(String term) {
}
// 向索引中新增一条文档
public void addDoc(String title, String url, String content) {
}
// 加载索引文件
public void load() {
}
// 保存索引文件
public void save() {
}
}
创建Weight类
Weight类表示一个文档的权重信息,其中 weight 的值通过词出现的频率来构造。
在代码中使用的是:weight = 10×标题中出现的次数 + 1×正文中出现的次数。文章来源地址https://www.toymoban.com/news/detail-414856.html
class Weight {
private int docId;
private int weight;
public int getDocId() {
return docId;}
public void setDocId(int docId) {
this.docId = docId;}
public int getWeight() {
return weight;}
public void setWeight(int weight) {
this.weight = weight;}
}
实现 getDocInfo 和 getInverted
// 根据 docId 查正排
public DocInfo getDocInfo(int docId) {
return forwardIndex.get(docId);
}
// 根据 分词结果 查倒排
public ArrayList<Weight> getInverted(String term) {
return invertedIndex.get(term);
}
实现addDoc
DocInfo docInfo = buildForward
到了这里,关于DocSearcher:文档搜索引擎的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!