站内搜索引擎

article/2025/9/9 15:00:41

1.什么是搜索引擎?

如图所示:

我用的是谷歌浏览器,但是我的搜索引擎可以跟换 。切换到bing主页

在搜索框中我们输入一段话,跳到一个带有搜索结果的页面如下:

搜索引擎的核心功能:查找用户输入的词/一句话 相关联的网页。 

搜索结果页一条记录包含的信息如下:

 当我们点击标题,会跳转到落地页,如下图:

相信大家对搜索引擎都有一定的了解了。

2.实现搜索引擎的核心思路:

  • 首先我需要很多网页。
  • 再根据用户输入的词/一句话,在这些网页中进行查找。

搜索引擎的网页又该如何获取呢?

像百度,搜狗,这样的大型搜索引擎,每天会有很多爬虫去爬取大量网页,在存储起来。

我们这里先直接去官网下载jdk文档(之后抽取时间改进利用爬虫获取网页)。

用户输入查询词之后,如何去让查询词语和当前的这些网页进行匹配呢?

搜索引擎工作原理

利用这些网页(.html文件解析过后,生成的文档DocInfo)构建正排索引和倒排索引。

3.正排索引和倒排索引原理:

正排,倒排原理理解

1.正排索引:文档 id        ->        文档内容                        (1个id对应1个文档内容)

2.倒排索引:词                ->        和词相关联的文档id        (1个词对应一个/多个文档id)

举个例子:

//正排索引

1                        我上街买了一份炸鸡

2                        我晚饭吃了一份牛肉

//倒排索引(先分词)

我                                1,2

上街                            1

买                                1

了                                1,2

一份                            1,2

炸鸡                            1 

晚饭                            2

吃                                2

牛肉                            2

4.我要把搜索引擎做到什么程度?

 我们还是先做一个“站内搜索”,类似于哔哩哔哩里的搜索框,搜索站内的东西。我们要做一个java,jdk8文档的搜索引擎,比如输入一个ArrayList,会在页面显示一系列关于ArrayList文档的信息,通过点击标题或者utl跳转到官网jdk8文档。

5.获取文档(网页)

Java Downloads | Oracle

先看看在线文档的内容以及url

 https://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html

 在对比看一下离线后的文档路径

 file:///C:/Users/Administrator/Desktop/online_search_doc/jdk-8u341-docs-all/docs/api/java/util/ArrayList.html

本地链接和在线网站链接相比得出结论:

我们可以基于离线文档来制作索引,实现搜索 ,当用户在搜索结果页面点击具体的搜索结果的时候,就自动跳转到在线文档的页面。

6.模块粗略划分:

1.索引模块:

  • 扫描下载的文档,分析文档内容,构造出正排索引+倒排索引。并且把索引内容保存到文件中
  • 加载制作好的索引,并提供一些API实现查正排和查倒排这样的功能。

2.搜索模块:

  • 调用索引模块,实现一个搜索的完整过程
  • 输入:用户输入词/一句话
  • 输出:完整的搜索结果(包含了很多条记录,每个记录都有标题,描述,展示url,并且点击能够跳转)

3.web模块:

  • 需要实现一个简单的web程序,能够通过网页的形式来和用户交互~

7.创建项目

用到的工具:

idea专业版2022.2.1

webstorm专业版2022.2.1

8.介绍一下什么是分词

用户在使用时侯,往往会输入一句话,比如:

会出现一下结果页:

用户输入的话被分成好多个词,每个词又对应很多文档id,所以结果页面会有很多条记录,每条记录都包括标题,url,描述这三个基本信息 。

我上街买了一份炸鸡        可分词为:

上街

一份

炸鸡

我们要使用第三方库ansj来进行分词。ansj地址

说一下分词的原理,有助于编写代码。

1.基于词库(跟不上时代发展,每隔一段时间会出现新词)

  • 尝试把所有词都进行穷举~把这些穷举结果放到词典文件中。
  • 然后就可以依次的取句子中的内容,每隔一个字,在词典里查一下,每隔俩个字,查一下.....

2.基于统计(该方法更加科学,用的多)

  • 收集到很多很多的“语料库” => "人工标注/直接统计  => "也就知道了哪些字放在一起的概率比较大~";
  • 分词的实现,就是属于“人工智能”典型应用场景~

分词原理

写个测试类用一下ansj这个第三方库

说一下爆红的原因:

在分词的时候,会加载一些词典文件,通过这些词典文件能够加快分词速度和准确率,但是没有这些词典文件ansj仍然能快速分出词。 (可配置词典文件)

我们在来看一下分词结果:

9.实现索引模块Parser类的整体流程 

  • 指定本地jdk8文档的路径INPUT_PATH
  • Parser类作为入口,main函数调用run方法
  • run函数包含三个功能:
  • 1.根据指定路径,罗列出该文件夹下所有的.html文件,包括子文件夹中的.html的路径
  • 2.根据上边罗列的所有html文件路径,打开文件,读取文件,解析文件,构建索引(在内存中)
  • 3.把在内存中构造好的索引数据结构,保存到指定的文件中(即序列化,反序列化)

10.递归枚举文件

import java.io.File;
import java.util.ArrayList;public class Parser {public static final String INPUT_PATH = "C:/Users/Administrator/Desktop/online_search_doc/jdk-8u341-docs-all/docs/api/";public void run() {//1.枚举出所有的.html文件,包括子目录中的文件ArrayList<File> filelist = new ArrayList<>();enumFile(INPUT_PATH, filelist);
//        System.out.println(filelist);
//        System.out.println(filelist.size());//2.针对上面罗列出的文档路径,打开文件,读取文件内容,进行解析,构建索引//3.把在内存中构造好的索引数据结构,保存到指定的文件中}private void enumFile(String inputPath, ArrayList<File> filelist) {File rootPath = new File(inputPath);File[] files = rootPath.listFiles();for (File f : files) {if(f.isDirectory()){enumFile(f.getAbsolutePath(),filelist);}else {filelist.add(f);}}}public static void main(String[] args) {Parser parser = new Parser();parser.run();}
}

现在有一个问题,我要的是html文件,其他文件例如css,js等其他文件是我不想要它出现在结果里边,用户也看不懂,所以filelist里边要去掉这些前端文件。

11.除去不是html的文件 

我怎么实现?

我的思路是得到所有文件的目录,它的后缀不是html的就不加到那个filelist里边

private void enumFile(String inputPath, ArrayList<File> filelist) {File rootPath = new File(inputPath);File[] files = rootPath.listFiles();for (File f : files) {if(f.isDirectory()){enumFile(f.getAbsolutePath(),filelist);}else {if(f.getAbsolutePath().endsWith(".html")){filelist.add(f);}}}}

这里我想说的是,后续的开发中会遇到各种各样的问题,但是只要思想不滑坡,办法总比困难多。

浩大的工程,都是由一个一个小小的函数堆积起来的。好了,现在我们把所有的html文件都加载到filelist里边了,现在的问题是,我怎么根据上边的路径打开文件去解析里边的文件呢?解析,我要解析什么?(1.标题,2.url,3.描述(描述是根据正文来的,所以因该是解析正文)),我为什么要这三个东西?我要把这三个东西用一个弄一个实体类来表示,即一个DocInfo,里边有id,title,url,content,然后通过一个ArrayList<DocInfo>构建出一个正排索引,然后根据这个正排索引去构建倒排索引,目前先考虑这么多。

12.解析html

我们打开文件发现html的名字和文件中的title里的名字相似,但是html的名字更加获取,代码实现上更加容易,我们就以html前边的单词作为标题 。

 

这样的话,我们解析html的标题的功能完成了。我们继续往下做。

13.解析url

我们所期望的结果就是,用户点击搜索结果,就能够跳转到对应的线上文档的页面。

思路:

把本地路径的后半段提取出来作为后缀         java/util/ArrayList.html

以在线路径前半部分为前缀             https://docs.oracle.com/javase/8/docs/api/

拼接后就是完整的url         https://docs.oracle.com/javase/8/ docs/api/java/util/ArrayList.html

写一个测试代码:

import java.io.File;public class TestUrl {public static final String INPUT_PATH = "C:/Users/Administrator/Desktop/online_search_doc/jdk-8u341-docs-all/docs/api/";public static void main(String[] args) {File f = new File("C:/Users/Administrator/Desktop/online_search_doc/jdk-8u341-docs-all/docs/api/java/util/ArrayList.html");String part1 = "https://docs.oracle.com/javase/8/docs/api/";String part2 = f.getAbsolutePath().substring(INPUT_PATH.length());System.out.println(part1+part2);}
}

这里虽然是反斜杠,但是粘贴在浏览器上还能打开,体现出浏览器的鲁棒性。

我们优化一下吧,把\都替换成/.

具体方法:

注意:

 虽然没看懂,但是能用!接下来我们开始解析正文,先讲讲解析正文的思路:

14.解析正文

我们先打开一个html文件观察观察

这里边有script的代码,还有css的代码style,还有html的标签

这里我们使用正则表达式去除一下这些标签

具体代码:

public String parseContent(File f) {try (FileReader fileReader = new FileReader(f)) {StringBuilder content = new StringBuilder();while (true) {int ret = fileReader.read();if (ret == -1) {break;}char c = (char) ret;content.append(c);}String scriptRegex = "<script[^>]*?>[\\s\\S]*?<\\/script>";//定义style的正则表达式,去除style样式,防止css代码过多时只截取到css样式代码String styleRegex = "<style[^>]*?>[\\s\\S]*?<\\/style>";//定义HTML标签的正则表达式,去除标签,只提取文字内容String htmlRegex = "<[^>]+>";//定义回车,换行符,制表符String spaceRegex = "\t|\r|\n";String content_final = content.toString();content_final = content_final.replaceAll(scriptRegex, "");content_final = content_final.replaceAll(styleRegex, "");content_final = content_final.replaceAll(htmlRegex, "");content_final = content_final.replaceAll(spaceRegex,"");return content_final;} catch (IOException e) {e.printStackTrace();}return null;}

效果展示:

 嗯不错不错~(蜜汁自信)

现在的问题是,我们已经把东西解析出来了,我们现在需要一个索引类去把解析出来的东西加入到索引,然后将制作好的索引保存到指定文件中去。

15.实现索引模块

先创建一个Index类,这个类的主要实现方法:

//给定一个docId,在正排索引中,查询文档的详细信息
//给定一个词,在倒排索引中,查哪些文档和这个词相关联
//往索引中新增一个文档
//把内存中的索引结构保存到磁盘中
//把磁盘中的索引数据加载到内存中

然后创建一个DocInfo类,这个实体类来描述docId,title,url,content.

这里有一个问题,返回值是List<Integer>行不行?用户输入一个词/一句话,我返回了文章的id难道不对吗?如过这样做的话,就体现不出文章的相关性了,排在前边的永远是docId小的文章。为了解决这样的问题,修改一下返回值。

创建一个类Weight,权重的意思,Weight里边包含docId,和weight这俩个属性,weight值越大,我们就表示,用户输入的值和文章的相关性越强,在浏览器显示的越靠前。这个Weight类就是把文档id和文档与词的相关性进行一个包裹。

具体实现代码:

import java.util.List;//通过这个类在内存中构造出索引
public class Index {//给定一个docId,在正排索引中,查询文档的详细信息public DocInfo getDocInfo(int docId) {return null;}//给定一个词,在倒排索引中,查哪些文档和这个词相关联public List<Weight> getInverted(String term) {return null;}//往索引中新增一个文档public void addDoc(String title, String url, String content) {}//把内存中的索引结构保存到磁盘中public void save(){}//把磁盘中的索引数据加载到内存中public void load(){}
}

我们现在还需要俩个结构表示正排索引和倒排索引:

现在在想一下,正排索引是通过一个docId返回一个DocInfo,而倒排索引可以通过一个词返回一组docId,为了让词和docId有相关性,所以进行如下编码:

private ArrayList<DocInfo> forwardIndex = new ArrayList<>();private HashMap<String,ArrayList<Weight>> invertedIndex = new HashMap<>();

现在我有一个问题,查正排,和查倒排效率高吗?时间复杂度是多少?

查正排,我们用的是ArrayList,时间复杂度是O(1)

查倒排,我们用的是HashMap,时间复杂度是O(1)

所以说,查询的很快,我们用这样的结构来保证查询的够快,够高效。

还有就是,我们现在的数据都是放在内存上的,这是查询快的第二个原因。

目前,实现的代码如下:

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;//通过这个类在内存中构造出索引
public class Index {private ArrayList<DocInfo> forwardIndex = new ArrayList<>();private HashMap<String,ArrayList<Weight>> invertedIndex = new HashMap<>();//给定一个docId,在正排索引中,查询文档的详细信息public DocInfo getDocInfo(int docId) {return forwardIndex.get(docId);}//给定一个词,在倒排索引中,查哪些文档和这个词相关联public List<Weight> getInverted(String term) {return invertedIndex.get(term);}//往索引中新增一个文档public void addDoc(String title, String url, String content) {//构建正排索引DocInfo docInfo = buildForward(title,url,content);//构建倒排索引buildInverted(docInfo);}private void buildInverted(DocInfo docInfo) {}private DocInfo buildForward(String title, String url, String content) {DocInfo docInfo = new DocInfo();docInfo.setDocId(forwardIndex.size());docInfo.setTitle(title);docInfo.setUrl(url);docInfo.setContent(content);forwardIndex.add(docInfo);return docInfo;}//把内存中的索引结构保存到磁盘中public void save(){}//把磁盘中的索引数据加载到内存中public void load(){}
}

目前,我们新增文档的功能还没实现完,正排索引构建好了,现在实现构建倒排索引,怎么构建?

怎么做?倒排索引就是词与docId的映射,我准备,把传入的docInfo,把它的title,和content进行分词,我倒排索引用的是HashMap<String,ArrayList<Weight>>,也就是词和权重的键值对。权重里边包括docId,weight,我们又如何确定这个权重的值, (权重这个值,描述了词个文档之间的相关性,权重值越大,我们认为词和文章id相关性越强,在网页上显示的也就越靠前)。

在真实的搜索引擎中,这里的相关性,是一个非常复杂的逻辑,往往是一个专门的算法团队来进行负责的。根据文档中提取的特征,训练模型,最终借助机器学习的方式来衡量相关性。

现在我对构建倒排索引做如下规划:

1.针对文档标题进行分词

2.遍历分词结果,统计每个词出现的次数

3.针对正文进行分词

4.遍历分词结果,统计每个词出现的次数

5.把上面的结果汇总到一个HashMap里面。

6.遍历刚才这个HashMap,依次来更新倒排索引中的结构。

现在,我们想一想,标题中出现的词,和正文出现的词相比,标题出现的词是不是权重更大一些?

标题的词少,但是这里的词更能表达文章的中心思想,

 正文的词多,但是这里的词更不能表达文章的中心思想。

一句话:最终文档的权重,就设定成 : 标题中出现的次数 * 10 + 正文中出现的次数。

这里的权重公式可以改进。如何改进?

点击率 = 点击次数/展示次数

在实际开发中,比如我们的服务器,每天有一亿访问量,然后可以把这一亿访问量拆成若干份,

30%        使用A公式

30%        使用B公式

30%        使用C公式

10%        使用D公式

分别统计,这些情况的点击率如何

公式会越来越复杂,同时也会让点击效果提升

最终我们看看这几个公式哪个更好,就会留下哪个公式继续迭代。

实际的搜索引擎,这里的计算公式非常复杂,并且要持续调整,反复迭代。

画个图理解一下倒排索引的数据结构

 具体代码实现:

private void buildInverted(DocInfo docInfo) {class WordCnt {public int titleCount;public int contentCount;}HashMap<String, WordCnt> wordCntHashMap = new HashMap<>();List<Term> terms = ToAnalysis.parse(docInfo.getTitle()).getTerms();for (Term term : terms) {String word = term.getName();WordCnt wordCnt = wordCntHashMap.get(word);if (wordCnt == null) {WordCnt newWordCnt = new WordCnt();newWordCnt.titleCount = 1;wordCntHashMap.put(word, newWordCnt);} else {wordCnt.titleCount += 1;}}terms = ToAnalysis.parse(docInfo.getContent()).getTerms();for (Term term : terms) {String word = term.getName();WordCnt wordCnt = wordCntHashMap.get(word);if (wordCnt == null) {WordCnt newWordCnt = new WordCnt();newWordCnt.contentCount = 1;wordCntHashMap.put(word, newWordCnt);} else {wordCnt.contentCount += 1;}}for (Map.Entry<String, WordCnt> entry : wordCntHashMap.entrySet()) {//倒排拉链 invertedListArrayList<Weight> invertedList = invertedIndex.get(entry.getKey());if (invertedList == null) {ArrayList<Weight> newInvertedList = new ArrayList<>();Weight weight = new Weight();weight.setDocId(docInfo.getDocId());weight.setWeight(entry.getValue().titleCount * 10 + entry.getValue().contentCount);newInvertedList.add(weight);invertedIndex.put(entry.getKey(), newInvertedList);} else {Weight weight = new Weight();weight.setDocId(docInfo.getDocId());weight.setWeight(entry.getValue().titleCount * 10 + entry.getValue().contentCount);invertedList.add(weight);}}}

        

目前倒排索引构建好了,但这些索引都保存在内存中。而且构建索引的过程是比较耗时间的。我们的文档大概也有一万多条,addDoc这个比较耗时间。

因此,我们不因该在服务器启动的时候才构建索引(启动服务器会被拖慢很多很多)

我的想法是:

把这些耗时间的操作,单独去执行,单独执行完了之后,然后让线上服务器直接加载构造好的索引。

因此才要实现save和load的操作。

我们接下来就去把内存中的索引结构,变成一个字符串,然后写入文件,也就是序列化的过程,当我们加载文档也就是反序列化的过程。

 序列化与反序列化也有很多方法,此处我们就用json格式来进行序列化与反序列化

我们先把jackson库引入

具体实现代码:

public void save() {System.out.println("保存索引开始!");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("保存索引完成!");}

现在说一下json的格式:

 当我们实现加载功能的时候,jackson库提供了TypeReference这样的类来帮助我们解决,json格式的数据转换成什么类型的数据,由于forwardIndex 的返回值类型是ArrayList<DocInfo>,所以进行以下编码:

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 TypeReference<ArrayList<DocInfo>>() {});invertedIndex = objectMapper.readValue(invertedIndexFile, new TypeReference<HashMap<String, ArrayList<Weight>>>() {});} catch (IOException e) {e.printStackTrace();}System.out.println("加载索引结束!");}

现在我们的索引模块基本完成了,我们看看效果,在看看能否进行一下优化。

16.优化运行时间

先运行一下项目:保存索引大概是560ms这个样子,也就是save的运行时间

 

文件大小第一个大约是70MB,第二个文件大小大约是64MB 

我们前边的解析html也是比较消耗时间的,我们先写个日志打印一下消耗的时间。

 public void run() {long begin = System.currentTimeMillis();System.out.println("索引制作开始!");//1.枚举出所有的.html文件,包括子目录中的文件ArrayList<File> filelist = new ArrayList<>();enumFile(INPUT_PATH, filelist);long endEnumFile = System.currentTimeMillis();System.out.println("枚举文件完成!消耗时间: " + (endEnumFile - begin) + "ms");
//        System.out.println(filelist);
//        System.out.println(filelist.size());//2.针对上面罗列出的文档路径,打开文件,读取文件内容,进行解析,构建索引for (File f : filelist) {System.out.println("开始解析:" + f.getAbsolutePath());parseHtml(f);}long endFor = System.currentTimeMillis();System.out.println("遍历文件完成!消耗时间:" + (endFor - endEnumFile) + "ms");//3.把在内存中构造好的索引数据结构,保存到指定的文件中index.save();long end = System.currentTimeMillis();System.out.println("索引制作完成!消耗时间: " + (end - begin) + " ms");}

 可以清楚的看到制作索引需要消耗18s的时间,还是挺长的。我们该怎么办?

说一下性能优化:

要想优化一段程序的性能,就需要先通过测试的手段找到其中的“性能瓶颈”。现在问题主要出现在

 for (File f : filelist) {System.out.println("开始解析:" + f.getAbsolutePath());parseHtml(f);}

这一段代码上,也就是

每次循环都要针对一个文件进行解析,读文件+分词+解析内容(这里面主要还是卡在cpu运算上)

单个线程的情况下,这些任务,都是串行执行的,多个线程,这些任务就可以并发执行了。

我们的速度就会有较大提升。

现在我们修改一下代码,改成多线程制作索引。

什么时候会有线程安全问题?

多个线程同时修改一个对象,就会出现线程安全问题。

目前碰到一个问题,如何保证线程安全? 

线程安不安全主要在parseHtml(f);这个函数

parseTitle不涉及多个线程同时修改一个对象,

parseUrl也同样不涉及多个线程同时修改一个对象,

parseContent也不涉及多个线程同时修改一个对象,

上边三个函数都是多个线程玩各自的对象。

问题出现在addDoc这个函数,画个图理解一下。

八个线程同时玩forwardIndex,invertedIndex这俩个对象,势必会出现线程不安全问题。

那你为啥不给addDoc加个锁,这样不就行了吗?

我们引入线程的目的是为了让串行变成并发,你给addDoc加上锁不就又成了串行执行了吗?

通俗点来说,脱裤子放屁,多费手续。

我们的目的是,串行的让它并发,实在并发不了的让它串行。让加锁的粒度在小一些。

所以如果直接把synchronized加到parseHTML或者addDoc上,这样做锁的粒度太粗了,并发程度不高,就是提高的效率有限。

现在发现,buildForward 和 buildInverted其实是在操作不同的对象。也就不存在锁竞争。

新建俩个锁对象,针对俩个锁对象进行加锁。

多线程的效果:

 多线程的使用极大的提高了,索引制作时间。

那么线程池中的线程个数设置成几合适呢?我们只能通过实验来进行确定。

不同的代码并发程度是不一样的。

并不是线程数目越多越好,线程多确实可以提升效率,但是太多的话,提升效果就不明显了,在多加线程,白白浪费计算机资源。

目前又出现一个问题:

 程序执行完了,进程没有关闭。

这个问题的主要原因就是:

如果要是一个线程是守护线程,此时这个线程的运行状态,不会影响到进程结束。

如果要是一个线程不是守护线程,这个线程的运行状态,就会影响到进程结束。

默认创建出来的都是非守护线程,需要通过setDaemon方法手动设置,才能成为守护线程。

通过线程池创建的线程,并不是守护线程,

当main方法执行完了,这些线程仍然在工作。(仍然在等待新任务的到来)

我们可以使用线程池提供的方法,让线程停止。

目前又碰到一个问题,首次制作索引比较慢,尤其是开机之后,首次制作索引特别慢,后面就会快了,重启机器,第一次制作就会特别慢。

通过排查parseHtml函数定位到,parseContent这个函数有问题,

计算机读取文件,是一个开销比较大的操作。

我为什么选择atomiclong来计时间?

多线程环境下,可以简单使用AtomicXXX 使代码变得线程安全。 

atomiclong相当于加了synchronized的long。

开机第一次运行:

第二次运行:

可以明显看到,t1的时间变化,    

    

注意此处是八个线程累加的时间。 

(第一次开机运行,t2用了51s ,第二次运行t2用了25s,搞不懂为啥出现这种情况)

这种情况的话,我猜测是因为addDoc之前,你就得把title,url,content解析出来,parsecontent没解析完,adddoc就得等着,t2的时间前后也是2倍关系。

接下来我们可以优化一下parsecontent这个函数。

    public String parseContent(File f) {try (FileReader fileReader = new FileReader(f)) {StringBuilder content = new StringBuilder();while (true) {int ret = fileReader.read();if (ret == -1) {break;}char c = (char) ret;content.append(c);}

我们读取文件用的是FileReader这个方法,fileReader.read()每次从文件中读取一个字符,每次read都是在读磁盘,它的速度就会很慢。

思路:我们可以先让filereader提前把文件读取到内存里面,然后每次调用一次bufferedReader.read() 就可以从内存中读取.

BufferedReader 可以搭配FileReader来使用。

BufferedReader内部就会内置一个缓冲区。就能够自动的把FileReader中的一些内容预读到内存中,从而减少直接访问磁盘的次数。

其实FileReader也有缓冲区,只是BufferedReader对缓冲支持更好。

此刻的心情:优化了个寂寞。

但是,开机第一次运行的效果肯定比没优化前好,我们开机重新试一下。

此时用了19秒 比前开机少用了10来秒,只能说还行。

17.实现搜索模块

首先,搜索模块就是调用索引模块,来完成搜索的核心过程~

1.分词。针对用户输入的词/句进行分词

2.触发,拿着每个分词结果,去倒排索引中查,找到具有相关性的文档,

3.排序,针对上边触发出来的结果,进行排序(按照相关性,降序排序)

4.包装结果。根据排序后的结果,依次去查正排,获取到每个文档的详细信息,包装成一定的数据结构返回出去。

目前遇到一个问题,返回结果中的描述该如何返回?

我们只是得到了正文。

说一下描述包含什么?描述是正文的一段摘要,这个摘要来源于正文,同时要包含查询词或者查询词的一部分。

思路:

目前我们可以获取所有的查询词的分词结果

遍历分词结果,看看哪个结果在正文中出现

for (Weight weight : allTermTesult) {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);}

这里抽象出了一个方法GenDesc,根据正文和分词结果来生成描述。

针对当前文档来说,不一定包含所有的分词结果

就针对这个被包含的分词结果,去正文中查找,找到对应的位置

就以这个词的位置为中心,往前截取60个字符,作为描述的开始。

然后再从这个描述开始,截取160个字符,作为整个描述。

如果用户查询的时候输入了Hello,我们的分词结果都是小写的,所以用户不管输入什么,都会被转换成小写字母,那么正文中的大写字母也得转换成小写字母,不然就查不到。我们这里统一弄成小写的。

private String GenDesc(String content, List<Term> terms) {int firstPos = -1;for (Term term : terms) {String word = term.getName();firstPos = content.toLowerCase().indexOf(word);if (firstPos >= 0) {//找到了位置break;}}}

还有一个问题,如果用户输入的查询词是List,而正文中出现的是ArrayList,在生成描述的时候,此处拿着这个List去正文中indexOf,此时是否会把ArrayList当作结果呢?

这就会导致生成的描述,里面就是带ArrayList的,而不是带List的了

我要查List结果描述里边是ArrayList 和 List,这样设计就不科学。

类似这样的情况,在查倒排的时候,是否会存在呢?

倒排索引中的key都是分词结果,ArrayList不会被分成Array + List ,仍然把ArrayList视为是一个单词。List和ArrayList并不能匹配,使用List这个词不能查出包含ArrayList的结果。这是科学的

因此我们希望,在生成描述的过程中能够找到整个词都匹配的结果,才算是找到了。而不是只找到词的一部分。

如何让word独立成词?而不是只作为正文词的一部分。

我的想法是直接在word的前后加个空格,让这个词独立出来。

 仅仅是这么一处代码,却需要思考这么多的东西。

验证一下docsearcher类

 

结果符合我们的预期要求。 

18.实现web模块

提供一个web接口,最终以网页的形式,把我们的程序呈现给用户。

前端(html+css+js)后端(servlet/spring)先用servlet实现,在升级成spring

现在约定一下前后端的通信接口。

请求:

GET/searcher?query=[查询词] HTTP/1.1

响应:(json格式的数据)

Http/1.1 200 ok

[

        {

                title:"这是标题1",

                url:"这是url1",

                desc:"这是描述1",

        },

        {     

                title:"这是标题2",

                url:"这是url2",

                desc:"这是描述2",

        },

]

具体实现:

package api;import com.fasterxml.jackson.databind.ObjectMapper;
import searcher.DocSearcher;
import searcher.Result;import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;public class DocSearcherServlet extends HttpServlet {//全局唯一,用static修饰一下private static DocSearcher docSearcher = new DocSearcher();private ObjectMapper objectMapper = new ObjectMapper();@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//拿到用户提交的查询词String query = req.getParameter("query");if (query == null || query.equals("")) {String msg = "您的参数非法,没有获取到query的值!";System.out.println(msg);resp.sendError(404, msg);return;}System.out.println("query=" + query);List<Result> results = docSearcher.search(query);//把结果进行打包resp.setContentType("application/json;charset=utf-8");objectMapper.writeValue(resp.getWriter(),results);}
}

验证一下效果:

后端这边我们已经做好了,我们接下来做一个好看点的前端页面。

19.前端页面的制作 

我们利用ajax进行前后端交互,

当用户点击搜索按钮的时候,浏览器就会获取到搜索框的内容,基于ajax构造HTTP请求,然后发给搜索服务器。浏览器获取到搜索结果之后,在根据结果的json数据,把页面生成出来。

目前遇到一个巨坑,呜呜呜~调试了2个小时终于让我给逮住了。

ajax构造请求的路径没有理清楚,造成时间上的浪费。

我把index. html放在了html这个文件夹中,所以ajax构造请求url的路径得修改一下,

 如果我放在webapp下,就不用加../了

我是看了这个博客解决的:

ajax关于url路径问题

目前又遇到一个问题:

内容太多,超出了一个屏幕,该怎么解决?

可以通过修改css代码,

你可以定位到代码问题出现在哪里吗?

最大的div类名是container.,就让在这个div内部滚动。

又遇到一个问题:

第一次搜索和第二次搜索结果会累加到一起

 

出现这个问题的原因是啥?

 每次点击按钮,都是把结果往.result里面进行追加,没有清理过内容

更科学的方法,应该是在每次搜索之前,都把之前的旧的结果给清空掉。

现在我们在实现一下标红逻辑。

1.修改后端代码,生成搜索结果的时候,就需要把其中包含查询词的部分,给加上一个标记。

例如,给这个部分套上一层<i>标签~

2.然后在前端这里针对<i>标签设置样式,然后浏览器就可以根据<i>标签的样式来进行显示了,

(比如给<i>标签的字体颜色设置为红色即可)

具体实现:

private String GenDesc(String content, List<Term> terms) {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) + "...";}//String desc = "";int descBeg = firstPos < 60 ? 0 : firstPos - 60;if (descBeg + 160 > content.length()) {desc = content.substring(descBeg);//从起始位置截到末尾} else {desc = content.substring(descBeg, descBeg + 160) + "...";}for (Term term : terms) {String word = term.getName();//此处进行全字匹配,也就是当查询词为list的时候不能把Arraylist中的List给单独标红。(?i) 不区分大小写替换desc = desc.replaceAll("(?i) " + word + " ", "<i> " + word + " </i>");}return desc;}

现在测试一下更加复杂的情况:

 俩次的搜索结果一样,

服务器500,原因是代码执行过程中抛出异常了。

就需要找到 

 

 数组越界异常:

排查发现如果正文没有160个字符,还要截取160个字符必然会导致错误。

改正:

又出现一个问题:

下边的itemDiv都没有标红的字 

 我想是因为分词把空格也看成一个词了,导致代码会拿空格去查找倒排索引,我们想个办法把空格去掉。

这里用停用词表,修改一下代码。

思路:

让搜索程序加载这个停用词表,到内存中,

使用一个HashSet把这些词都存起来,

在针对分词结果,在停用词中进行筛选,

如果某个结果在词表中存在,就直接去掉。

验证结果:

符合预期要求。 

又发现一个bug

还是有不标红的情况,出现这种情况的原因

我点进去一个不标红的文档,搜索关键字,结果发现该文档确实存在这个关键字,哪里出现了问题?

前边后边都有空格才去匹配这个firstPos,那要是没有空格就匹配不到了,关键是全文就只有这一个字 ,所以firstPos直接返回的是-1,我们要解决这里的问题,就得使用正则表达式。

成功解决bug。

目前又发现一个问题:

1576 + 1350  = 2926 = 2926

同一个文档即出现了array 又出现了list,也就是说,ArrayList出现了俩次,重复了。

前面算权重,针对分词结果依次进行促发,

array => 触发一组docId

list => 触发一组docId 

同时包含多个分词结果,意味着这个文档的相关性更高,

像这种情况,就应该提高这个文档的权重,既然相关性更高,提高权重之后就能排的更靠前。

把权重进行相加。把多个分词结果触发的文档,按照docId进行去重,同时进行权重合并。

去重思路: 

合并俩个有序列表,合并俩个数组。

先把分词结果进行一个排序处理(按照docId升序排序)在进行合并,合并的时候就可以根据docId值相同的情况,做权重相加。

可是用户输入的仅仅就是俩个词吗?

所以应该是N个数组合并。

对比多个数组值的大小关系,找到小的,插入到结果中。

谁对应的值越小,就把这个值取出来,插入到结果数组中 ,同时针对这个下标进行++。

我们这里使用堆/优先级队列 

end.扩散一下思维,那么一些小型网站的搜索引擎我们是否就会做了呢?

我们是否还能通过es来做一下?

爬虫能否实现一下?


http://chatgpt.dhexx.cn/article/c2OkVOlM.shtml

相关文章

SecureCRT 软件完全汉化

在正常汉化包的基础上&#xff0c;删除一个菜单文件&#xff0c;然后再运行"SecureCRT_CHS.exe" 程序即可 注意&#xff1a;如果以前是英文版本&#xff0c;则需要删除菜单配置文件后才可以正常汉化。直接删除SecureCRT配置目录下的"...\Config\SCRTMenuToolbar…

secureCRT安装、破解

SecureCRT是最常用的终端仿真程序&#xff0c;简单的说就是Windows下登录UNIX或Liunx服务器主机的软件。本文讲解SecureCRT的安装、破解与使用。 下载&安装 1.从https://pan.baidu.com/s/1dGMKJF7下载 2.解压缩下载的文件&#xff0c;双击scrt736-x64.exe执行安装。 破解…

【工具】SecureCR-8.5下载、安装激活和使用教程(包含常用设置)

目录 一、安装包下载 二、安装教程 三、激活操作 四、使用教程 五、常用设置 一、安装包下载 SecureCRT8.5安装包&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1yy677I99ln_3evoHc5dMXg 提取码&#xff1a;9tyj 二、安装教程 1. 解压、双击进行安装 2. 安装进…

SecureCRT的下载、安装( 过程非常详细!!值得查看)

SecureCRT的下载、安装&#xff08; 过程非常详细&#xff01;&#xff01;值得查看&#xff09; 简单介绍下SecureCRT一、SecureCRT的下载二、SecureCRT的安装三、SecureCRT的使用 简单介绍下SecureCRT SecureCRT是一款支持SSH&#xff08;SSH1和SSH2&#xff09;的终端仿真程…

SecureCRT和SecureFX的下载和安装2022

小编在新电脑上安装SecureCRT和SecureFX&#xff0c;按照之前写的文章步骤SecureCRT和SecureFX的下载和安装2021在使用注册机SecureFX.-kg.exe安装SecureFX的时候报错了&#xff0c;提示mfc100u.dll和msvcp100.dll两个文件找不到。&#xff08;注册机keygen.exe安装SecureCRT可…

SecureCRT 64位 破解版v8.1.4及安装

securecrt 破解版是一款支持SSH1和SSH2的终端仿真程序&#xff0c;这个程序能够在windows系统中登陆UNIX或Linux的服务器主机并且还能进行管理设置&#xff0c;是一款非常强大的ssh传输软件&#xff0c;是用于连接运行包括Windows、UNIX和VMS的理想工具。并且它还有打印功能、可…

SecureCRT下载和破解

一. secureCRT下载 secureCRT是一款支持SSH的终端仿真程序&#xff0c;简单的说是Windows下登录Linux服务器的工具。下载时&#xff0c;需要下载scrt安装包和Keygen注册机包。下载链接secureCRT安装包。 1. 下载文件 2. 安装SCRT 按照指定步骤安装即可 二. 破解 安装完成之…

SecureCRT 8.5下载安装破解

SecureCRT 8.5下载安装破解 不用关闭防火墙&#xff0c;不用下载注册机&#xff0c;不用csdn下载积分 不用关闭防火墙&#xff0c;不用下载注册机&#xff0c;不用csdn下载积分 公司领导让我下载SecureCRT&#xff0c;网上找了很多教程&#xff0c;注册机也下载了&#xff0c;…

【工具使用】SecureCRT的下载、安装图文详细过程介绍

本文目录 一、SecureCRT的介绍 二、SecureCRT的下载 三、SecureCRT的安装 四、SecureCRT的使用 一、SecureCRT的介绍 SecureCRT是一款支持SSH&#xff08;SSH1和SSH2&#xff09;的终端仿真程序&#xff0c;简单地说是Windows下登录UNIX或Linux服务器主机的软件。 Secure…

2020最详细下载安装注册最新版 Secure CRT 8.7

文章目录 一.下载SecureCRT 8.7二.安装SecureCRT 8.7三.注册SecureCRT 8.7 一.下载SecureCRT 8.7 官网下载 网址:https://www.vandyke.com/products/securecrt/ 2.选择系统 3.选择64位或者32位 4.输入邮箱密码 邮箱 adminmail.bccto.me 密码 wheretogo8888 5.点击下载,下…

SecureCRT的下载安装

亲测成功了&#xff0c;按照下面的步骤完成即可&#xff01; 下载安工具包包地址连接&#xff1a;网盘地址点击即可 提取码&#xff1a;0lp7 1、下载SecureCRT 2、从百度网盘下载SecureCRT&#xff0c;页面如下 3、安装SecureCRT 4、激活SecureCRT 第一步&#xff1a;打开安装…

SecureCRT免安装版免费下载

https://yunpan.360.cn/surl_y2qdTx4jfhq &#xff08;提取码&#xff1a;4993&#xff09; 360安全云盘虽然需要充值&#xff0c;但上传下载不限速&#xff0c;个人特别推荐360安全云盘

securecrt破解版64位

securecrt 破解版是一款支持SSH1和SSH2的终端仿真程序&#xff0c;这个程序能够在windows系统中登陆UNIX或Linux的服务器主机并且还能进行管理设置&#xff0c;是一款非常强大的ssh传输软件&#xff0c;是用于连接运行包括Windows、UNIX和VMS的理想工具。并且它还有打印功能、可…

SecureCRT 免安装、绿色版、免费版本

SecureCRT远程连接Linux服务器 文章目录 1. 下载软件2. 解压软件3. 双击运行4. 选择SecureCRT5. 图标点击&#xff0c;输入用户名6. 添加密码&#xff0c;点击确定7. 连接成功 1. 下载软件 链接https://pan.baidu.com/s/1vv40uI_0uwIagPGm7FS9_g 提取码t32h 2. 解压软件 3. 双…

SecureCRT绿色版的下载和安装

SecureCRT绿色版的下载、安装 一、SecureCRT绿色版的下载 链接&#xff1a;https://pan.baidu.com/s/1ZMeWbJnatNTrBwUPkcjNOg 提取码&#xff1a;bbwt 1. 下载完成后将压缩包解压缩到自己电脑的想要压缩的安装目录下 解压后 双击运行 默认登录模式是SSH2&#xff0c;根据自…

18.CRT的绿色版安装和使用。

一、找到SecureCRT.exe点击启动。 二、在centos虚拟机中输入命令ifconfig&#xff0c;查询IP地址&#xff08;192.168.0.104&#xff09;。 三、在主机名中输入IP地址和用户名&#xff0c;点击连接。&#xff08;虚拟机查询的IP地址&#xff0c;使用的用户名&#xff09; 主机…

CentOS7 安装CloudStack4.14.0

https://blog.csdn.net/dandanfengyun/article/details/107220199#comments_13468183 按照4.13.1设置首先进行基本设置及基础软件安装完成。然后将4.13.1的安装包改成4.14.0的基本就可以了。 管理节点安装cloudsatck-management 更改cloudstack安装源&#xff0c;尽管是将安装…

CentOS7下搭建CloudStack4.10.0云平台

一篇学习性质的文章&#xff0c;关于CloudStack怎么实现这些功能一概不了解。只是跟着他人博客学着搭建一下。如有侵权&#xff0c;立删。 建议&#xff08;使用4.11.0 更好&#xff0c;4.10.0 添加主机经常失败&#xff0c;不知是不是个人问题&#xff09; http://docs.clouds…

cloudstack java api_CloudStack核心类ApiServlet、ApiServer、ApiDispatcher、GenericDaoBase源码分析...

ApiServlet 首先从整体上看下ApiServlet&#xff0c;Outline视图如下&#xff0c; 一、注意Inject依赖的是javax.inject.jar&#xff0c;它和spring的Autowired的区别在于使用它时变量不用生成相应的set方法。 二、CloudStack所有的请求都会被ApiSerlet拦截处理&#xff0c;进入…

cloudstack之basic network

本章则主要尝试basic network模式的使用。 基础环境搭建见cloudstack测试环境搭建 1、概念介绍 cloudstack的网络模式主要分为两种&#xff1a; basic network&#xff1a;一个zone中仅有一个guest network来承载客户虚拟机的流量&#xff0c;可以理解为一个简单的二层网络。…