苏州专业建设网站,网站开发的在淘宝上是什么类目,网页设计与网站开发素材,电子工程网络维护记录导航小助手
一、认识搜索引擎
二、项目目标
三、模块划分
四、创建项目
五、关于分词
六、实现索引模块
6.1 实现 Parser类
6.2 实现 Index类
6.2.1 创建 Index类
6.2.2 创建DocInfo类
6.2.3 创建 Weight类
6.2.4 实现 getDocInfo 和 getInverted方法
6.2.5 实现 …导航小助手
一、认识搜索引擎
二、项目目标
三、模块划分
四、创建项目
五、关于分词
六、实现索引模块
6.1 实现 Parser类
6.2 实现 Index类
6.2.1 创建 Index类
6.2.2 创建DocInfo类
6.2.3 创建 Weight类
6.2.4 实现 getDocInfo 和 getInverted方法
6.2.5 实现 addDoc方法
6.2.6 实现save方法
6.2.7 实现 load方法
6.3 在 Parser 中调用 Index
6.3.1 新增 Index实例
6.3.2 修改 parseHTML方法
6.3.3 新增save方法的调用
6.4 验证索引模块
6.5 优化制作索引速度
七、实现搜索模块
7.1 创建 DocSearcher类
7.1.1 创建 Result类
7.1.2 实现 search方法
7.1.3 验证 DocSearcher类
7.1.4 修改 Parser类
八、实现Web模块 一、认识搜索引擎
关于搜索引擎平时的时候都是用的比较多如百度、搜狗、谷歌等搜索引擎但是并没有仔细的观察过~
以搜狗搜索为例我们打开搜狗搜索就会发现有一个搜索主页并且有一个搜索框我们可以在搜索框中搜索 想要知道的内容点击 搜索就可以查找搜索结果的页面~ 搜索引擎 的核心功能就是查找到 一组和用户输入的查询词或者是一句话所相关联的网页~
通过观察可以发现对于每一条搜索结果都会显示该搜索结果的 标题、描述、展示url等属性虽然有的结果样式上可能会更复杂但是还是会有 标题、描述、展示url属性当我们点击一条搜索记录的时候就跳转到相应的页面称之为 落地页~ 搜索引擎的核心实现思路
搜索引擎的核心目的是在很多很多的网页中 找到和查询词相关联的内容~
那么对于一个搜索引擎来说首先需要获取到很多的网页然后再根据用户输入的查询词在这些网页中进行查找~ 这就涉及到了几个问题 搜索引擎的网页是怎么获取到的此处主要是涉及到 爬虫 程序此处不过多介绍用户输入查询词之后如何让查询词和当前的这些网页进行匹配呢这个是重点 假设 当前已经爬取到了 1亿 个网页(html文件)用户输入了一个 蛋糕查询词此时如果使用的是 暴力搜索的话那么就需要把 蛋糕这个字符串 在这1亿个网页里面进行查找那肯定是效率很低的特别是对于搜索引擎来说我们都希望 再进行搜索之后会立刻出现我们所要查找的结果~ 因此为了更加高效的解决这个问题大佬们专门设计了一种特殊的数据结构 —— 倒排索引~ 需要知道的知识 举个例子 在日常的生活中我们也会有所体会就比如说在 玩王者荣耀的时候 二、项目目标
像搜索引擎这样的程序是一个很复杂的程序很难做出像百度、搜狗一样的规模庞大的搜索引擎所以可以简化一下目标可以做一个针对 Java API文档的搜索引擎~ 通过观察可以发现官方文档上面并没有一个搜索框所以想要去查找某个类但又不知道这个类在哪个包里就会显得很麻烦~ 因此就可以实现这样一个站内搜索引擎让用户输入一些查询词之后就可以找到相关的文档页面~ 获取Java文档
要想实现这个项目首先就需要把相关的网页文档给获取到然后才可以继续接下来的过程~
相关网页在 oracle官方网站上面想要把上面所有的页面获取到并不是一件很容易的事情~
虽然说可以通过 爬虫技术把这些文档获取到但是 爬虫技术 还没有学到听说 实现爬虫程序 会存在法律风险的所以就暂时放弃这个方法~
针对于 Java API文档 来说还有更加简单的方法 —— 可以直接从官网上直接下载因此就不必通过爬虫来实现~ Java API文档 线上版本https://docs.oracle.com/javase/8/docs/api/index.html Java API文档 线下版本 下载地址https://www.oracle.com/java/technologies/downloads/ 三、模块划分
1索引模块
扫描下载到的文档分析文档内容构建出 正排索引倒排索引并且把索引内容保存到文件中~加载制作好的索引并提供一些API实现查正排和查倒排这样的功能~
2搜索模块
调用索引模块实现一个搜索的完整过程~输入用户的查询词输出完整的搜索结果包含了很多条记录每个记录有 标题、描述、展示url并且点击可以跳转~
3Web模块
需要实现一个简单的 web程序能够通过网页的形式和用户进行交互~ 四、创建项目 五、关于分词
用户在搜索引擎中输入的查询词不一定真的只是一个词也有可能是一句话 由上面的搜索结果我们可以知道先针对完整的句子进行分词然后根据每个词的分词结果分别找到相关的文档~
针对 分词操作人是非常容易完成的但是对于机器来说这就困难很多英文的话还好有空格就可以了但是中文博大精深额就非常的哇塞了~
比如说我一把把车把把住小龙女也想过过过儿过过的生活下雨天留客天留我不留 ~
好在我们要想实现这个分词效果就可以基于一些现成的第三方库从而实现分词效果~ 分词实现原理
虽然说我们现在使用的是第三方库并不是自己实现分词逻辑但是还是需要了解一下 分词的原理~ 第一种情况基于词库 以汉语为例虽然说汉字很多但是仍然是可以尝试着把所有的词都穷举在一本词典文件中然后就可以依次的取句子中的内容如每隔一个字就在词典里查一下每隔两个字查一下...... 但是这并不能把所有的词都穷举出来而且 随着时间的推移也会产生越来越多的新词......关是基于词库并不能分出所有的词~ 第二种情况基于统计 利用这种方法收集很多很多的 语料库如现有的一些文章、资料等等接着就可以进一步进行人工标注/或者是直接统计接着就进一步的知道了哪些词 成词的情况比较高就类似于根据用户的习惯~ 分词的实现就是属于 人工智能 典型应用的场景 ~ 使用第三方库
在Java中可以实现分词的第三方库 也是有很多的在这里我们可以使用 ansj分词库~ !-- 引入分词依赖下载安装ansj --!-- https://mvnrepository.com/artifact/org.ansj/ansj_seg --dependencygroupIdorg.ansj/groupIdartifactIdansj_seg/artifactIdversion5.1.6/version/dependency
示例 public static void main(String[] args) {//准备一个比较长的话用来分词(中文分词)String str1 人终将被少年不可得之物困其一生但也终会因一事一景解开一生困惑;ListTerm terms1 ToAnalysis.parse(str1).getTerms();for (Term term1 : terms1) {System.out.print(term1.getName() /);}System.out.println();//英文分词(会把大写字母转换成小写字母)String str2 I have a dream!;ListTerm terms2 ToAnalysis.parse(str2).getTerms();for (Term term2 : terms2) {System.out.print(term2.getName() );}}
结果 六、实现索引模块
6.1 实现 Parser类
创建一个Parser类通过这个类来完成制作索引的过程~ Parser读取之前下载好的离线文档去解析文档的内容并完成索引的制作 import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;public class Parser {//指定加载文档的路径private static final String INPUT_PATH E:/doc_searcher_index/jdk-8u361-docs-all/docs/api/;private void run() {//整个Parser类的入口//1、根据上面指定的路径枚举出该路径中所有的 html文件(包括子目录中的文件)//2、针对上面罗列出文件的路径打开文件读取文件内容并进行解析并构建索引//3、todo 把在内存中构造好的索引数据结构保存到指定的文件中ArrayListFile fileList new ArrayList();enumFile(INPUT_PATH,fileList);/*** System.out.println(fileList);* System.out.println(fileList.size());*/for (File f:fileList) {//通过parseHTML()方法解析 .html文件System.out.println(开始解析 f.getAbsolutePath());parseHTML(f);}}/*** 解析 .html文件 的方法* param f*/private void parseHTML(File f) {//解析 标题String title parseTitle(f);//解析 urlString url parseUrl(f);//解析 对应的正文(描述 是正文的一小部分先解析出来正文然后才可以获取到 描述)String content parseContent(f);//todo 把解析出来的这些信息加入到索引当中}/*** 通过观察可以发现在 .html文件中title标签 里面的就是标题* 进一步可以发现.html文件名 就是该文件的标题*/private String parseTitle(File f) {return f.getName().substring(0,f.getName().length() - .html.length());}/*** url 是在线文档对应的链接*/private String parseUrl(File f) {//先获取到固定的前缀String part1 https://docs.oracle.com/javase/8/docs/api/;//后获取 线下文档api后面的部分String part2 f.getAbsolutePath().substring(INPUT_PATH.length());return part1 part2;}/*** 一个完整的 .html文件里面有的是 html标签 内容* 所以 解析正文的核心工作是去掉 html标签,只剩下 内容*/public String parseContent(File f) {//先按照一个字符一个字符的方式来读取( 和 来控制拷贝数据的开关)try( FileReader fileReader new FileReader(f)) {//加上一个开关:true拷贝false不拷贝boolean isCopy true;//加上一个保存结果的stringBuilderStringBuilder stringBuilder new StringBuilder();while (true) {//read()返回值是 int而不是 char//主要是为了表示一些非法情况文件读完了返回-1int ret fileReader.read();if (ret -1) {break;}//如果结果不是-1那就是合法的字符char c (char) ret;if (isCopy) {//开关打开遇到普通字符就拷贝到 StringBuilder中if (c ) {//关闭开关isCopy false;continue;}//去掉 .html文件当中的换行不然实在是不好看if (c \n || c \r) {//把换行替换成空格c ;}//其他字符直接拷贝stringBuilder.append(c);}else {//开关不拷贝直到遇到 if (c ) {isCopy true;}}}return stringBuilder.toString();} catch (IOException e) {e.printStackTrace();}return ;}/**** param inputPath 表示从哪个目录开始进行递归遍历* param fileList 表示递归所得到的结果*/private void enumFile(String inputPath, ArrayListFile fileList) {File rootPath new File(inputPath);File[] files rootPath.listFiles(); //listFiles()方法获取到rootPath目录下的路径名(仅仅只能看到一层内容)//使用递归看见所有子目录下面的内容for (File f:files) {//根据f类型来决定是否需要递归if (f.isDirectory()) {//f 是一个目录递归调用enumFile()方法进一步获取子目录的内容enumFile(f.getAbsolutePath(),fileList);}else {//f 是一个普通文件加入到 fileList中//在此处排除非.html文件(以.html的文件才添加上去)if (f.getAbsolutePath().endsWith(.html)) {fileList.add(f);}}}}public static void main(String[] args) {//通过 main方法实现整个制作索引的过程Parser parser new Parser();parser.run();}
}在Parser类当中主要做的是枚举出当前指定目录中的所有的 .html文档通过递归函数 enumFile(INPUT_PATH,fileList) 把所获取到的文档存储在 fileList里面接下来就可以循环遍历这里的每一个文件再针对文件进行解析每一个文件都是一个 .html解析 .html文件的 标题、描述、展示url ~
在解析好了这些信息就可以把它们加入到索引数据结构当中于是就可以专门创建一个 Index类来负责构建索引数据结构~ 6.2 实现 Index类
6.2.1 创建 Index类
Index类 主要实现的功能有
正排索引倒排索引新增一个文档保存索引文件加载索引文件
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;/*** 通过这个类在内存中构造出索引结构*/
public class Index {//使用数组下标表示 docId(正排索引)private ArrayListDocInfo forwardIndex new ArrayList();//使用 哈希表 来表示倒排索引(key是词value是一组和这个词相关联的文章)private HashMapString,ArrayListWeight invertedIndex new HashMap();//这个类所提供的方法//1.正排索引给定一个 docId在正排索引中可以查询到文档的详细信息//2.倒排索引给定一个词在倒排索引中查找那些文档和这个词有关联//3.将前面所解析出来的信息添加进去向索引当中新增一个文档//4.把内存中的索引结构保存到磁盘中//5.把磁盘中的索引数据加载到内存//1.public DocInfo getDocInfo(int docId) {//todoreturn null;}//2.public ListWeight getInverted(String term) {//todoreturn null;}//3.public void addDoc(String title,String url,String content) {//todo}//4.public void save() {//todo}//5.public void load() {//todo}
}6.2.2 创建DocInfo类
DocInfo类主要表示的是在正排索引中根据 docId 所查询的文档的相关细节~
/*** 该类当中可以表示一个文档的相关细节*/
public class DocInfo {private int docId;private String title;private String url;private String content;public int getDocId() {return docId;}public void setDocId(int docId) {this.docId docId;}public String getTitle() {return title;}public void setTitle(String title) {this.title title;}public String getUrl() {return url;}public void setUrl(String url) {this.url url;}public String getContent() {return content;}public void setContent(String content) {this.content content;}
}6.2.3 创建 Weight类
Weight类主要表示的是在倒排索引中文档id 和 文档与词的相关性 的权重~
/*** 这个类是把 文档id 和 文档与词的相关性 的权重 进行一个包裹*/
public class Weight {private int docId;//weight表示 文档和词之间的相关性值越大就说明 这个词和这个文档越有关的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;}
}6.2.4 实现 getDocInfo 和 getInverted方法 //1.public DocInfo getDocInfo(int docId) {return forwardIndex.get(docId);}//2.public ListWeight getInverted(String term) {return invertedIndex.get(term);} 6.2.5 实现 addDoc方法
//3.public void addDoc(String title,String url,String content) {//新增文档操作需要给正排索引和倒排索引都新增//构建正排索引DocInfo docInfo buildForward(title,url,content);//构建倒排索引buildInverted(docInfo);}private void buildInverted(DocInfo docInfo) {//倒排索引是词与文档id的映射关系//就需要知道当前的文档里面存在有哪些词就需要实现分词//就需要针对 标题、正文 进行分词之后就可以根据分词的结果知道当前的文档id 应该要加入到哪一个倒排索引的key里//1.针对标题进行分词遍历分词结果统计每个词出现的次数//2.针对正文进行分词遍历分词结果统计每个词出现的个数//3.汇总到HashMap中简单设置权重权重标题出现的次数*10 正文出现的次数//4.遍历HashMap依次更新倒排索引中的结构class WordCount {public int titleCount; //标题次数public int contentCount;//正文次数}//用来统计词频的数据结构HashMapString,WordCount wordCountHashMap new HashMap();ListTerm terms ToAnalysis.parse(docInfo.getTitle()).getTerms();for (Term term:terms) {//先判定term 是否存在String word term.getName();WordCount wordCount wordCountHashMap.get(word);if (wordCount null) {//不存在创建一个新的键值对插入进去titleCount设为1WordCount newWordCount new WordCount();newWordCount.titleCount 1;newWordCount.contentCount 0;wordCountHashMap.put(word,newWordCount);}else {//存在找到之前的值对应的 titleCount1wordCount.titleCount 1;}}terms ToAnalysis.parse(docInfo.getContent()).getTerms();for (Term term:terms) {String word term.getName();WordCount wordCount wordCountHashMap.get(word);if (wordCount null) {WordCount newWordCount new WordCount();newWordCount.titleCount 0;newWordCount.contentCount 1;wordCountHashMap.put(word,newWordCount);}else {wordCount.contentCount 1;}}for (Map.EntryString,WordCount entry : wordCountHashMap.entrySet()) {//先根据这里的词去倒排索引中查一查ListWeight invertedList invertedIndex.get(entry.getKey()); //倒排拉链if (invertedList null) {ArrayListWeight newInvertedList new ArrayList();//把新的文档(当前 DocInfo),构造成 Weight对象插入进来Weight weight new Weight();weight.setDocId(docInfo.getDocId());//权重计算公式标题中出现的次数*10正文中出现的次数weight.setWeight(entry.getValue().titleCount * 10 entry.getValue().contentCount);newInvertedList.add(weight);//如果为空就插入一个新的键值对invertedIndex.put(entry.getKey(),newInvertedList);}else {//如果非空就把这个文档构造一个 Weight对象插入到倒排拉链的后面Weight weight new Weight();weight.setDocId(docInfo.getDocId());//权重计算公式标题中出现的次数*10正文中出现的次数weight.setWeight(entry.getValue().titleCount * 10 entry.getValue().contentCount);invertedList.add(weight);}}}
6.2.6 实现save方法
至于为什么要实现save方法呢其实原因也是很简单的当前索引是存储在 内存 中而构建索引的过程(即上面实现的addDoc方法)实际上是非常耗时间的仅仅一个API就有上万份文档了更别说其他的了~
因此我们不应该服务器启动的时候才构建索引(启动服务器就可能会被拖慢很多了)万一服务器在运行过程中又崩溃了那又要重启服务器那又要浪费更长的时间了~
通常的操作可以是先把这些耗时的操作单独的去执行执行好了之后再让线上服务器直接加载这个构造好的索引此时加载的速度就仅在读磁盘上面花费时间了~
至于具体该如何去保存到文件中呢我们都知道文件里保存的不是 二进制数据就是文本数据这就需要把内存中的索引结构 变成一个字符串这个过程叫做 序列化然后就可以直接写文件就可以了~ 类似的把特定结构的字符串 按照一定的结构解析回来类/对象/基础数据结构我们就称之为 反序列化~ 其实序列化和反序列化 都有许多现成的方法此处可以使用JSON的格式来完成 序列化和反序列化就可以很轻松的完成保存和加载索引的操作了~ 此处使用JSON的格式可以从中央仓库中引入 相关依赖 !-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind --
dependencygroupIdcom.fasterxml.jackson.core/groupIdartifactIdjackson-databind/artifactIdversion2.15.2/version
/dependency//引入 jackson 里面的核心对象
private ObjectMapper objectMapper new ObjectMapper();//保存的地址
private static final String INDEX_PATH E:\\doc_searcher_index\\;
由于涉及到两种索引就可以分别使用两个文件 来保存正排索引和倒排索引~ //4.public void save() {//序列化//使用两个文件分别保存正排和倒排System.out.println(保存所以开始);//1.先判断索引对应的目录是否存在不存在就创建File indexPathFile new File(INDEX_PATH);if (!indexPathFile.exists()) {indexPathFile.mkdirs();}File forwardIndexFile new File(INDEX_PATH forward.txt);File invertedIndexFile new File(INDEX_PATH inverted.txt);try {objectMapper.writeValue(forwardIndexFile,forwardIndex);objectMapper.writeValue(invertedIndexFile,invertedIndex);}catch (IOException e) {e.printStackTrace();}System.out.println(保存索引完成);}6.2.7 实现 load方法 //5.public void load() {//反序列化System.out.println(加载索引开始);File forwardIndexFile new File(INDEX_PATH forward.txt);File invertedIndexFile new File(INDEX_PATH inverted.txt);try {forwardIndex objectMapper.readValue(forwardIndexFile, new TypeReferenceArrayListDocInfo() {});invertedIndex objectMapper.readValue(invertedIndexFile, new TypeReferenceHashMapString, ArrayListWeight() {});} catch (IOException e) {e.printStackTrace();}System.out.println(加载索引结束);} 当然由于索引中的文件可能会有很多构造出来的索引数据也是比较大的那么保存和加载到底会消耗多长时间呢我们就可以修改一下 load和save方法来衡量出它们的运行时间 //4.public void save() {//序列化long beg System.currentTimeMillis();//使用两个文件分别保存正排和倒排System.out.println(保存索引开始);//1.先判断索引对应的目录是否存在不存在就创建File indexPathFile new File(INDEX_PATH);if (!indexPathFile.exists()) {indexPathFile.mkdirs();}File forwardIndexFile new File(INDEX_PATH forward.txt);File invertedIndexFile new File(INDEX_PATH inverted.txt);try {objectMapper.writeValue(forwardIndexFile,forwardIndex);objectMapper.writeValue(invertedIndexFile,invertedIndex);}catch (IOException e) {e.printStackTrace();}long end System.currentTimeMillis();System.out.println(保存索引完成所消耗的时间是: ( end - beg ) ms);}//5.public void load() {//反序列化long beg System.currentTimeMillis();System.out.println(加载索引开始);File forwardIndexFile new File(INDEX_PATH forward.txt);File invertedIndexFile new File(INDEX_PATH inverted.txt);try {forwardIndex objectMapper.readValue(forwardIndexFile, new TypeReferenceArrayListDocInfo() {});invertedIndex objectMapper.readValue(invertedIndexFile, new TypeReferenceHashMapString, ArrayListWeight() {});} catch (IOException e) {e.printStackTrace();}long end System.currentTimeMillis();System.out.println(加载索引结束消耗时间 ( end - beg ) ms);} 6.3 在 Parser 中调用 Index
当然关于 Parser类 和 Index类 之间的关系 我们需要清楚Parser类 就相当于是 制作索引的入口通过 Parser类 中的 main方法就可以把整个流程给跑起来Index类 就相当于是实现了 索引的数据结构并且提供了一些API供他人调用~
因此Index类 需要给 Parser类 调用才可以完成整个制作索引的过程~
这也是上面的曾经留下了 两个todo 需要做的事情 6.3.1 新增 Index实例
public class Parser {......//创建一个 Index实例private Index index new Index();......
} 6.3.2 修改 parseHTML方法
新增 addDoc方法的调用
private void parseHTML(File f) {//解析 标题String title parseTitle(f);//解析 urlString url parseUrl(f);//解析 对应的正文(描述 是正文的一小部分先解析出来正文然后才可以获取到 描述)String content parseContent(f);//todo 把解析出来的这些信息加入到索引当中[新增]index.addDoc(title,url,content);} 6.3.3 新增save方法的调用 private void run() {long beg System.currentTimeMillis();System.out.println(索引制作开始);//整个Parser类的入口//1、根据上面指定的路径枚举出该路径中所有的 html文件(包括子目录中的文件)//2、针对上面罗列出文件的路径打开文件读取文件内容并进行解析并构建索引//3、todo 把在内存中构造好的索引数据结构保存到指定的文件中long begEnumFile System.currentTimeMillis();ArrayListFile fileList new ArrayList();enumFile(INPUT_PATH,fileList);long endEnumFile System.currentTimeMillis();System.out.println(枚举文件完毕消耗时间 ( endEnumFile - begEnumFile ) ms);/*** System.out.println(fileList);* System.out.println(fileList.size());*/long begFor System.currentTimeMillis();for (File f:fileList) {//通过parseHTML()方法解析 .html文件System.out.println(开始解析 f.getAbsolutePath());parseHTML(f);}long endFor System.currentTimeMillis();System.out.println(循环遍历文件完毕消耗时间 ( endFor - begFor ) ms);//新增save方法的调用(好吧其实也新增了一些时间戳)index.save();long end System.currentTimeMillis();System.out.println(索引制作完毕消耗时间 ( end - beg ) ms);} 6.4 验证索引模块 运行Parser类观察运行结果 6.5 优化制作索引速度
关于性能优化首先就需要通过 测试的手段咳咳通过一些时间戳 知道某一段程序运行所花费的时间找到其中的 性能瓶颈~
通过结果发现主要花费的时间性能瓶颈 就是在循环遍历文件 上面每次遍历一个 .html文件都需要进行 读文件分词解析内容 等操作主要就是花费在CPU运算上面~
在单个线程的情况下这些任务都是串行执行的因此可以引进多线程并发执行优化速度当然需要通过加锁来完成线程安全~
//通过这个方法实现多线程制作索引public void runByThread() throws InterruptedException {long beg System.currentTimeMillis();System.out.println(索引制作开始);//1.枚举所有文件ArrayListFile files new ArrayList();enumFile(INPUT_PATH,files);//2.循环遍历文件(引入线程池)CountDownLatch latch new CountDownLatch(files.size());ExecutorService executorService Executors.newFixedThreadPool(4);for (File f:files) {executorService.submit(new Runnable() {Overridepublic void run() {System.out.println(解析 f.getAbsolutePath());parseHTML(f);latch.countDown();}});}//await方法会阻塞直到所有选手都调用countDown撞线才会阻塞结束latch.await();executorService.shutdown(); //手动杀死线程池里面的线程//3.保存索引index.save();long end System.currentTimeMillis();System.out.println(索引制作完毕(多线程)消耗时间 ( end - beg ) ms);}
解决线程安全问题 //创建两个锁对象private Object locker1 new Object();private Object locker2 new Object();
修改 buildForward方法
private DocInfo buildForward(String title, String url, String content) {DocInfo docInfo new DocInfo();docInfo.setTitle(title);docInfo.setUrl(url);docInfo.setContent(content);synchronized (locker1) {docInfo.setDocId(forwardIndex.size());forwardIndex.add(docInfo);}return docInfo;}
修改 buildInverted方法
private void buildInverted(DocInfo docInfo) {......for (Map.EntryString,WordCount entry : wordCountHashMap.entrySet()) {//先根据这里的词去倒排索引中查一查synchronized (locker2) {......}}}在调用多线程的方法之后我们就可以发现 制作索引的速度就快了很多 一些其他的小问题
比如说第一次制作索引的时候它的制作速度会特别慢在之后制作索引的时候速度又会快很多但把电脑关掉再一次重启电脑第一次制作索引的时候又变得慢很多后面的时候速度又会快很多这是为什么呢 分析在代码中 核心操作还是在 循环遍历上面循环遍历上面 又调用了 parseHTML方法parseHTML方法中又调用了 parseContent方法进行了读取文件的操作~ 在计算机读取文件的时候是一个开销比较大的操作~ 我们就可以猜想是不是开机之后首次读取文件的时候 速度特别慢呢~ 实际上是因为缓存的问题~ 首次运行的时候由于当前的这些 Java API 文档没有在内存中进行缓存因此读取的时候就只能从硬盘上进行读取这是及其耗时的后面再运行的时候由于前面已经读取过文档了即 在操作系统中其实已经有了一个缓存在内存中所以这次读取的就不是直接读硬盘而是直接读内存的缓存速度就会快很多~ 当然这也是可以进行优化的~ BufferedReader 和 FileReader 搭配着来使用BufferedReader 内部会内置一个缓冲区就可以自动的把 FileReader 中的一些内容预读到内存中从而减少直接访问磁盘的次数~ 虽然说后面的运行程序 的时候操作系统已经做了缓存此时运行速度也提高不了太多但也好歹是优化了一点点~ 七、实现搜索模块
搜索模块 主要是调用 索引模块来完成搜索的核心过程
其中是可以包括
分词针对用户输入的查询词进行分词触发拿着每一个分词结果去倒排索引当中找出具有相关性的文章调用 Index类 里面查倒排的方法即可 排序根据上面所触发出来的结果根据相关性降序进行排序包装结果根据排序后的结果依次去查正排获取到每个文档的详细信息包装成一定结构的数据返回出去 7.1 创建 DocSearcher类
import java.util.List;//通过这个类 来完成整个搜索模块的过程
public class DocSearcher {//此处需要加上索引对象的实例,同时需要完成索引加载的工作private Index index new Index();public DocSearcher() {index.load();}/*** 完成整个搜索过程的方法* param query 查询词* return 搜索结果的集合*/public ListResult searcher(String query) {//1.分词//2.触发//3.排序//4.包装结果return null;}
}7.1.1 创建 Result类
//这个类来表示一个搜索结果
public class Result {private String title;private String url;private String desc; //描述(正文的一段摘要)public String getTitle() {return title;}public void setTitle(String title) {this.title title;}public String getUrl() {return url;}public void setUrl(String url) {this.url url;}public String getDesc() {return desc;}public void setDesc(String desc) {this.desc desc;}Overridepublic String toString() {return Result{ title title \ , url url \ , desc desc \ };}
}7.1.2 实现 search方法
//通过这个类 来完成整个搜索模块的过程
public class DocSearcher {//此处需要加上索引对象的实例,同时需要完成索引加载的工作private Index index new Index();public DocSearcher() {index.load();}/*** 完成整个搜索过程的方法* param query 查询词* return 搜索结果的集合*/public ListResult searcher(String query) {//1.分词ListTerm terms ToAnalysis.parse(query).getTerms();//2.触发(/针对分词结果进行倒排查找)ListWeight allTermResult new ArrayList();for (Term term:terms) {String word term.getName();ListWeight invertedList index.getInverted(word);if (invertedList null) {//说明这个词在所有文档中不存在continue;}allTermResult.addAll(invertedList);}//3.排序allTermResult.sort(new ComparatorWeight() {Overridepublic int compare(Weight o1, Weight o2) {//如果是升序排序return o1.getWeight() - o2.getWeight()//如果是降序排序return o2.getWeight() - o1.getWeight()return o2.getWeight() - o1.getWeight();}});//4.包装结果ListResult results new ArrayList();for (Weight weight:allTermResult) {DocInfo docInfo index.getDocInfo(weight.getDocId());Result result new Result();result.setTitle(docInfo.getTitle());result.setUrl(docInfo.getUrl());result.setDesc(GenDesc(docInfo.getContent(),terms)); //描述results.add(result);}return results;}//生成描述信息private String GenDesc(String content, ListTerm terms) {//先遍历分词结果看看结果是否在 content 中存在//正文需要先转成小写(重要)int firstPos -1;for (Term term:terms) {String word term.getName();firstPos content.toLowerCase().indexOf( word );if (firstPos 0) {//找到了位置break;}}if (firstPos -1) {//所有的分词结果不在正文中存在//取正文的前 160 个字符作为描述return content.substring(0,160) ...;}//从 firstPos 作为基准位置往前找60个字符往后找100个字符String desc ;int descBeg firstPos 60 ? 0 : firstPos-60;if (descBeg 160 content.length()) {//descBeg 位置很靠后面了desc content.substring(descBeg);} else {desc content.substring(descBeg,descBeg160) ...;}return desc;}
}在包装结果生成描述的时候需要注意的是 前面使用第三方库分词的结果 都是小写形式的所以不可以直接对着内容进行比较需要先转换成小写才可以其实需要根据实际的情况类似于 买了一份老婆饼并不可以送一个老婆我们搜索的时候不能把两个单词由于差不多就归结于相关性较强所以在查找单词的时候可以在前面和后面加上 虽然可能会查询不到在最前面或者最后面但这毕竟还是少数情况当然也可以使用正则表达式去解决这个问题 7.1.3 验证 DocSearcher类
验证 DocSearcher类 public static void main(String[] args) {DocSearcher docSearcher new DocSearcher();Scanner scanner new Scanner(System.in);while (true) {System.out.print(- );String query scanner.next();ListResult results docSearcher.searcher(query);for (Result result : results) {System.out.println();System.out.println(result);}}} 我们观察结果可以发现在有的查询结果中出现了下面的内容 其实这个内容就是 JavaScript 的代码由于在处理文档的时候只是针对正文 进行了 去标签而有的 HTML 里面包含了 script标签就导致了在去标签过后JS的代码也被整理到索引里面了所以查的时候就出现了这个情况~
而这种情况并不科学因此还需要在后面去掉可以使用正则表达式~ 7.1.4 修改 Parser类 修改 Parser类换成使用正则表达式来解决 /*** 通过这个方法内部基于正则表达式实现去除标签以及 script的效果*/public String parseContentByRegex(File f) {//1.先把这个文件都读到 String 里面String content readFile(f);//2.替换script标签content content.replaceAll(script.*?(.*?)/script, );//3.替换普通的 .html标签content content.replaceAll(.*?, );return content;}private String readFile(File f) {try(BufferedReader bufferedReader new BufferedReader(new FileReader(f))) {StringBuilder content new StringBuilder();while (true) {int ret bufferedReader.read();if (ret -1) {//读完了break;}char c (char) ret;if (c \n || c \r) {c ;}content.append(c);}return content.toString();}catch (IOException e) {e.printStackTrace();}return ;}
此时就可以验证一下效果如何 可是新的看起来也不是很好看原因是因为空格太多了我们就可以把多个空格合并成一个即可同样是使用正则表达式 //4.使用正则表达式把多个空格合并成一个空格content content.replaceAll(\\s, ); 所以在解析正文的时候就可以使用正则的方式了~ 八、实现Web模块
此时搜索的核心功能都已经实现出来了但是此时仍然需要提供一个Web接口以网页的形式把程序呈现给用户~
因为 此时的效果只能是在控制台中进行操作此时对于普通用户来说 并不友好此时如果想真正的搜索引擎一样有一个搜索框有一个搜索按钮......想必这样的界面对于用户来说更加的友好~ 要想实现这样的Web模块需要约定一下前后端通信的接口需要明确的描述出服务器都能接受什么样的请求 都能返回什么样的响应的~ 前端HTMLCSSJS 后端Java Servlet/Spring相关~ 针对于这个项目我们只需要实现一个 搜索接口 即可~ //约定
//请求
GET/searcher?query[查询词] HTTP/1.1//响应
HTTP/1.1[{title:这是标题1,url:这是描述1,desc:这是描述1}
]如果是使用 Servlet 来实现后端的接口的话第一步需要引入相关的依赖的jar包~ !-- 引入 Servlet依赖 --!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api --dependencygroupIdjavax.servlet/groupIdartifactIdjavax.servlet-api/artifactIdversion3.1.0/versionscopeprovided/scope/dependency
创建 DocSearchServlet类基于Servlet来实现后端
//指定当前的路径 和哪个Servlet类对应
WebServlet(/searcher)
public class DocSearchServlet extends HttpServlet {//引入 DocSearcher实例调用 searcher方法private static DocSearcher docSearcher new DocSearcher();private ObjectMapper objectMapper new ObjectMapper();Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//1.先解析请求拿到用户所提交的查询词String query req.getParameter(query);if (query null || query.equals( )) {//用户的参数是不科学的一个没有key一个有key没有valueString msg 您的参数非法没有获得query的值;System.out.println(msg);System.out.println();resp.sendError(404,msg);return;}//2.打印 记录 query的值System.out.println(query query);//3.调用搜索模块来进行一下搜索ListResult results docSearcher.searcher(query);//4.把当前的搜索结果进行打包resp.setContentType(application/json;charsetutf-8);objectMapper.writeValue(resp.getWriter(),results);}
}嗯最后剩下的就是实现前端页面的部分了关于这一部分内容就不细细展开了~ 而如果是使用 Spring Boot 来实现后端接口的话那么可以这样做
在 controller包下面创建 DocSearcherController 类
RestController
public class DocSearcherController {private static DocSearcher new DocSearcher();private ObjectMapper objectMapper new ObjectMapper();RequestMapping(/searcher)public String search(RequestParam(query) String query) throws JsonProcessingException {//参数是查询词返回值是响应内容//参数来自于请求query string中的query这个key的值ListResult results searcher.search(query);return objectMapper.writeValueAsString(results);}
}对了由于在运行的时候路径会发生错误因为有两条路径一条是云服务器上的路径一条是本地路径~
那么想要在哪里运行的话就需要去运行哪条路径~
所以想要更加方便的运行可以制作一个开关来切换路径~
虽然说在实际开发中不是这样子的但是这个却是一个更加简单粗暴的方法:
//创建一个Config类
public class Config {// 这个变量为 true表示在云服务器上运行为 false表示在本地运行public static boolean isOnline false;
}
//Index类底下public class Index {private static String INDEX_PATH null;static {if (Config.isOnline) {INDEX_PATH /root/java/doc_searcher_index/;}else {INDEX_PATH E:\\doc_searcher_index\\;}}......
}
//DocSearcher类底下public class DocSearcher {//停用词表的路径private static String STOP_WORD_PATH null;static {if (Config.isOnline) {STOP_WORD_PATH /root/java/doc_searcher_index/stop_word.txt;}else {STOP_WORD_PATH E:\\doc_searcher_index\\stop_word.txt;}}......
}