对于一个网站来说,搜索引擎需要提前预备好很多很多的静态资源。当用户输入查询的关键词的时候根据这些关键词来模糊查询匹配对应的资源,然后将这些资源展示给用户即可。
互联网上主要是依赖于爬虫程序,它们可以极大效率的利用互联网获取到海量信息资源。本项目没有用到爬虫,而是根据索引这样的数据结构来实现关键词快速查询指定文档id
- 文档:就是项目中预备的静态资源
- 正排索引:根据文档id查询文档内容
- 倒排索引:根据关键词查询文档id列表
站内搜索到技术文档的关键API和简介
区分全站搜索和站内搜索
全站搜索:Google,Bing应,火狐,百度,QQ浏览器,360浏览器。。。等等出名的浏览器在使用过程中会发现可以搜索到任意用户任意输入的关键词
站内搜索:比如在CSDN博客站内搜索某个内容,就会只显示站内的静态资源,站外的资源由于没有存储,所以无法搜索到。也一定程度上实现资源的筛选,得到搜索更准确的目的。
把相关的网页文档获取到,这样才能制作正派索引和倒排索引
官网JDK8在线文档查看步骤
这里对比一下本地的查看效果
至此就已经一步步进入到官网的文档了
下载离线JDK8文档步骤
往下拉,就会有Java8文档
至此就可以下载离线文档
按住ctrl新打开标签页就会出现如下显示
在本地基于离线文档来制作索引,根据关键词实现搜索;当用户在搜索结果页点击具体的搜索内容的时候就自动跳转到在线文档的页面
为什么不用爬虫来准备好这些静态资源呢?
如果用爬虫的话相当于直接拿到了静态资源而不需要我们去实现中间这个过程,这并不是项目的核心技术。也不需要为了爬虫而去学习Python这门语言。只要编程语言能够访问网络,那么就可以实现爬虫。
针对JDK8文档来说,我们选择更简单的方案:直接从官网下载文档的压缩包放在我们的静态资源中。不必通过爬虫来实现了。
- 索引模块
- 扫描下载到的文档,分析文档内容,构建出正排索引+倒排索引。并把索引内容保存到文件中
- 加载制作好的索引文件,提供一些 API 实现查正排和查倒排这样的功能
- 搜索模块
- 调用搜索模块实现一个完整的搜索过程
输入:关键词
输出:完整的搜索结果【标题,URL并且点击能够跳转,内容】
- 调用搜索模块实现一个完整的搜索过程
- web模块
- 需要实现一个简单的 web 模块,能够通过网页的形式来和用户进行交互。包含前后端。
分词是为了后续构造倒排索引,根据关键词查文档id用的。英文分词很简单,但是中文分词就容易造成误解
- 每天膳食,无鸡鸭亦可,无鱼肉亦可,青菜一碟足矣 --> 每天膳食,无鸡,鸭亦可;无鱼,肉亦可;青菜一碟,足矣
- 下雨天留客天留我不留 --> 下雨天,留客天,留我不?留!
- 寄钱三百吊买柴烧孩子小心带和尚田租等我回去收 -->寄钱三百吊买柴烧,孩子小心带,和尚田租等我回去收
分词方法采用的分词第三方库
-
基于词库
尝试把所有的“的”都进行穷举~把这些穷举结果放到词典中。然后就可以一次的取句子中的内容,每隔一个字,再次电力查一下;每隔两个词,查一下。
但是由于互联网带来的一些新词或者说是年度最热的词句是一只变动的,那么就无法正确的分词
-
基于统计
收集到很多很多的“语料库”–>人工标注/直接统计,也就知道了哪些字在一起的概率比较大
分词的实现,就是属于“人工智能”典型的应用场景 -
基于第三方库
这里使用的 ansj
这里使用 jackson
- 根据指定的路径,加载并解析本地的html静态资源
- 根据加载的文件列表,读取文件内容解析后构建正排索引和倒排索引
- 制作时把内存中加载好的索引保存到文件中;使用时从文件中读取数据到内存中
- 递归枚举文件
通过ArrayList装填File类,来存储当前路径下的全部html文件,递归处理根目录
- 解析html
解析html就是把html文件的标题,URL,描述内容给展现在网页上
描述来源于正文,所以首先需要解析html文件的内容正文
整个 HTML 的解析通过 parseHTML 函数解决
parseHTML 再由 parseTitle,parseUrl,parseConten 四部分构成
- 解析标题:html文件名可以作为我们的搜索结果的标题
- 解析URL:选取本地资源路径 为基准
- 在线:https://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html
- 离线:file:///D:/documents/docs/api/java/util/ArrayList.html>
做到的效果就是用户点击搜索结果,就能够跳转到对应的线上文档的页面;根据线下文档的后半部分URL和线上文档的前半部分URL进行拼接即可
会发现这里的正斜杠和反斜杠都存在,会不会影响浏览器的正常访问呢?
把地址复制到浏览器中发现,被正常解析了且成功访问。
主流的浏览器都有这样的纠错能力,也就是所谓 “鲁棒性【robot】”
解析正文:一个完整的 HTML文件 由 HTML标签 + 内容(Java文档),接下来,进行的解析正文的核心操作就是去掉 HTML 标签获取到里边的内容【相当于 innerText】
利用正则会太麻烦,因此我们就利用标签的 左右尖括号< > 来判断是否为内容
<div>hello</div>
思路:遇到一个字符 ‘<’ 这个位置开始不进行数据拷贝,接下来读到 “div” 也都不拷贝。读到 ‘>’ 也不拷贝,但是不拷贝状态结束,后续读到 “hello” 就能进行拷贝。读到 ‘<’ 就有结束拷贝。
思考:万一html中有 < 该怎么办
其实这个不用担心,因为 html 中的 ‘<’ 等这些特殊符号是由 & l t 来构成的
Parser 类通过 run 方法启动。
- 先扫描指定路径的文件,放入到一个 ArrayList 文件列表中【√】
- 再完善添加文件过程:排除非html文件【√】
- 再通过一个 parseHTML 解析全部的 html文件【√】
有了这些离线文档之后,下一步就是根据解析结果构造索引
index类 主要用来在内存中构造索引结构。办5件事!
- 通过 docId 查 文档信息【这是正排索引做的事情】
- 给定一个 关键词 查 docId【这是倒排索引做的事情】
- 往索引中增加一个文档【要及时更新正排索引和倒排索引】
- 把内存中的索引结构保存在文件中
- 加载文件中的索引结构到内存中
Index索引类大致框架
使用 ArrarList 来创建正派索引;HashMap<String, ArrayList> 来创建倒排索引
注意点
- 正排索引就利用 ArrayList 取下标的方式获取文档,时间复杂度为 O(1)
- 倒排索引可以使用 HashMap<String, ArrayList> 来实现 O(1) 复杂度的获取相关文档 ID
倒排索引为何这样构建
如果使用 HashMap<String, Integer> 是可以实现每个文档的 关键词 与 文档ID 的匹配联系,但是我们需要一个 权重Weight 。类似于搜索引擎中的搜索结果的排名,我们还需要文档和关键词匹配的相关性进行计算权重,因此使用一个数组来装填全部的与此关键词匹配的文档ID
Weight权重类
具体的权重计算先把大致框架搭建好之后再详细设计算法【这里先埋个坑】
- 正排索引
这里就是简单的通过参数生成指定文档之后加入正排索引
- 构建倒排
倒排索引:关键词->文档id 之间的映射关系。正常搜索的时候我们会发现结果相关性高的排名会靠前。因此 HashMap 的 value 需要进行一定的排序,根据 key 来匹配文档分词,如果匹配,就把 DocId 加入到 vlue 中即可。
权重如何设计?
此处我们就通过简单的次数统计来计算。
相关性往往是由部门的算法团队来训练模型的,根据文档中提取的特征,训练模型最终借助机器学习的方式来衡量
这个倒排索引代码稍长。我们一步一步分析。
我们需要统计标题个数和正文个数,所以创建了一个内部类 WordCnt ,先对标题进行分词并统计,统计结果放在 wordCntHashMap 中。再对正文进行分词统计,统计结果也放在 wordCntHashMap 中。最后遍历整个 wordCntHashMap 将结果保存在类变量 invertedIndex 中。
为何保存加载索引
当前这些索引是保存在内存中的,构建索引过程又是很耗时。因此我们不应该在服务器启动的时候就够建索引(服务器的启动速度会被拖慢很多的)
通常的做法就是这些耗时的操作单独执行,然后让线上的服务器直接加载这个构造好的索引。因此就需要把内存中的 索引这样的数据结构序列化成字符串,然后进行写文件【序列化操作;对应的反序列化就是把字符串反向解析成一些结构化数据(类/对象/基础数据结构)】
对于序列化操作,jdk 自带了就有 Serializable。我们这里使用第三方库 Jackson
下一步操作就是给 index类 添加文件的保存路径变量和Jackson对象来进行文件的读写
Index 类通过 add(title, url, content) 方法制作索引
- 先构建正排索引,放入到一个 ArrayList【√】
- 再构建倒排索引【√】
有了这些内存中的数据之后,下一步就把内存中的索引序列化到文件中,当项目启动的时候也要自动读取文件数据到内存中【服务器制作索引速度太慢】
- 保存内存中的索引到文件中
- 加载索引文件数据到内存中
Index 类通过 save(), load() 方法写和读
- save()【√】
- load()【√】
- 首先查看功能是否正常
在 Parser类 中创建 index 实例,并在 parserHTML 函数中 调用 index.addDoc(title, url, content) 方法进行保存操作,run函数 中调用 index.save() 方法
制作效果如图所示
- 如何优化
在程序的方法入口 run 中加上一个前后的时间戳。一步一步查看程序的性能瓶颈
先查看整体的耗时,发现内部有很多的文件读写操作如下
2.1. 加载静态资源
2.2. 构造索引
2.3. 索引保存
-
整个流程耗时 16.5s
-
加载静态资源耗时 0.3s
在枚举的代码前后计算耗时,因为用的递归来获取目录中全部的html文件,已经无法优化
-
构造耗时 15.9s
-
序列化索引耗时 0.21s
发现解析文件和索引制作是很耗时的。这些程序都是单线程执行的,且都是 CPU密集型
- 递归加载静态资源【cpu密集型】
- 解析html文件用的是分词库,每个html文件for循环一次就会进行分词解析【cpu密集型】
- 序列化索引【IO密集型】
构造索引整个过程表面上耗时这么多,其实是这个16.8s包含了:枚举0.3s【1.8%】,解析15.9s【94.6%】,保存0.21s【1.3%】。通过大致的计算结果得出:整个过程和磁盘无关而是在CPU的处理上上出现了瓶颈
难道是CPU性能跟不上?我们查看一下CPU的占用率如下所示【截图已经是最高的占用率了,没超过50%以上】
发现这个程序完全没有发挥出CPU的全部性能,因此我们可以考虑一下发挥CPU全部性能
因此我们采用一下线程池的方案来提升速度,重点放在解析HTML文件上
实现多线程制作索引
需要注意的是这里的多线程执行问题:可能会在 for 循环执行完毕了但是线程中的 parseHTML 函数没有执行完毕就会执行到后续的 index.save() 就会有错误的
因此需要等待所有线程执行完毕才执行后续代码
CPU密集型:线程数=核心数+1
IO密集型:线程数=核心数/(1-阻塞系数)
阻塞系数:阻塞时间/(阻塞时间+cpu运行时间)
这只是网上建议的线程数计算方式,实际情况需要多多测试得出最佳运行效率的核心数才是正道。我的cpu是 4c8t,因此就给5【4+1】个线程
- 使用封装好的线程池ExecutorService
耗时结果三次求平均值得来的
综上固定线程池的线程数采取8,线程数过多造成的时间片轮转,上下文环境切换也会有开销,属于典型的负优化
到此为止,多线程的框架已经出来。我们主要看 是否会导致线程不安全问题即可
主要观察是否存在多个线程修改同一个对象即可
多线程安全问题
这俩函数的执行只是简单的读取操作,并没有进行一些修改操作。因此再往下看 。
这里涉及到文件的读取和拷贝,因为没有任何的修改操作,所以也不用担心多个线程修改同一个变量的问题。
看最后的
这里我们发现会出现多个线程同时修改同一个变量的问题,当有很多个线程的时候,对于全局变量的正排索引和倒排索引都会有影响,因此需要加锁。
锁的粒度越小,并发程度越高。
构建正排索引加锁
构建倒排索引加锁
因为需要对圈红的代码进行加锁,所以为了简化,直接对整个 for循环代码块 进行加锁
这样加锁对吗?
我们发现加的都是同样的锁 ,而构建正排不会影响构建倒排,所以如果是相同的锁,就会出现锁竞争的情况发生,所以我们需要不同的锁。
解决线程不退出的隐患和保证全部线程任务执行完毕
为什么呢?
这有需要理解两个概念:守护线程和非守护线程
- 守护线程【后台线程】:这个线程的运行状态不会影响进程结束。
- 非守护线程:这个线程的运行状态会影响进程结束
我们创建的默认都是 非守护线程 所以我们需要设置成守护线程,进而不影响进程的结束。
如何设置?
- 方案一:线程池的 方法直接毁掉线程
- 方案二:线程本身的 设置为守护线程
再次验证多线程效果,加锁对此影响的速度还是蛮小的
首次制作索引比较慢的问题
优化了索引制作速度,但还有一个速度是磁盘文件的生成也很慢。我们计算一下文件生成的时间【主要是 函数的执行】
对于标题和URL的速度很快,可以忽略不计,重点放在了html文件内容的读取分词上。
代码准备好之后,IDEA重启,运行run方法来模拟服务器的重启效果。
发现耗时更严重了,明显的第一次加载的时候速度会慢的不是一点半点儿。
这是因为 缓存:操作系统会对经常用的文件进行缓存在内存中方便后续操作的读取 。 IDEA重启后,操作系统里对之前的所有操作的缓存都会清空;而当 IDEA 第一次运行的时候会重新读磁盘、各种文件读取一个一个处理,并缓存一些数据,当第二次继续编译运行的时候就会将内存中的缓存拿出来使用而不是从磁盘中拿出来使用,从而提高IO效率。
为了方便统计更详细的时间外加由于是多线程环境下,所以需要使用原子类【操作都是独立的,不会被干扰】来计算。
为什么会出现33.7s?
33s是8个线程累加的构造索引时间,20s是8个线程累加的新增全部文档内时间
现在问题是优化文件的读取速度
优化文件读取速度
从上边的优化和测试后得知:速度的瓶颈在于文件处理的操作上
之前我们是一个一个字符的读取,我们可以利用 BufferedReader 设置一个缓存区来加快读取操作。
只更改了 读取字符的方式,新增文档内容速度快了11秒;增加索引速度不会改变
在 Index 类中进行打断点测试
正排索引
倒排索引
-
实现一个 Parser 类
- 通过递归枚举出全部的 html 文件
- 针对每个 html 文件进行解析
- 解析 Title
- 解析 URL
- 解析 Content:用一个开关控制字符是否拷贝
- 解析后添加正排索引和倒排索引
- 正排索引:使用 数组长度作为新增 的下标,搜索时间复杂度为 O(1)
- 倒排索引:使用 作为访问的数据结构后,时间复杂度也为 O(1)。这里先把要解析的 装填入 中,然后遍历取得的 key 在 倒排中获取,因为如果没有就设为1,如果有就+1。所以把标题*10 + 内容 的出现次数相加作为最后的 权重
- 保存一个解析后的新增文档到文件中
- 在启动的时候加载本地文件到内存中
-
实现了一个 Index 类
- 查正排:直接取下标 即可
- 查倒排:按照 key 获取 中 value 即可
- 添加文档,Parser 类 构建索引的时候调用该方法
- 构建正排:构造 对象,依据 长度作为 的 进行添加
- 构建倒排:先进行标题,内容的分词。利用一个内部类 统计一个 出现的标题和内容的词频,去更新倒排索引
- 整个构建过程中涉及到的 多个线程修改同一个变量是应该注意线程安全问题
-
优化过程
- 单线程 vs 多线程
- 线程的安全性需要保证
- 线程资源的正常关闭
- 文件读取优化
- 增加一个文件缓存区
- 单线程 vs 多线程
-
保存索引:把数据转换为 JSON 格式存储在文件中
-
加载索引:把 JSON 格式的文件读取出来,加载在内存中
核心部分对比【由于文件数量很大,仪器运行的话会造成GC内存报错只能单独运行对比】
效率大致提升43.8%【】
调用搜索模块,来完成搜索的核心过程
- 分词:针对用户输入的 查询词 进行分词【用输入的可能是一个句子,可能是一个字也可能是一个词】
- 触发:拿着每个分词结果去倒排索引中查询
- 排序:针对上面出发的结果,进行排序【按照相关性,降序排序】
- 包装结果:根据排序后的结果一次去查正排,获取每个文档的详细信息,包装成一定数据结构的返回给页面
创建一个搜索结果 Result类,用来包装搜索后的返回结果
注意分词结果全是小写,因此需要现在把 DocInfo 的 Content 全转换为 小写
取内容的中关键词第一次出现位置的前后各60个单词
先埋一个坑:List关键词能不能只查 List 而排除掉 ArrayList?
这就会导致搜索结果的不准确,类似的情况在查倒排的时候是否会存在呢?倒排索引中的 key 都是分词的结果,我们应该让 List 仅查询出 List,视 ArrayList 为一个单词【独立成词】
原因
因为 parseContent 仅仅是通过 ‘<>’ 标签进行读写数据的,遇到 js 之后仅仅是把 <script><script> 给去掉了,而 <script>xxx<script> 内容 xxx 则没被去掉
因此现在问题就是如何去掉 xxx 呢?
就是使用正则表达式来实现效果。
出现jsBUG——使用正则表达式
Java 中的 String 方法很多都是支持正则的【indexOf,replaceAll,replace,split…】
替换script标签及其内容
知道了正则表达式后。
去掉 script 标签和内容:
去掉普通标签(不去掉内容)<.*?>既能匹配到开始标签又能匹配到结束标签
可以提前用正则在线测试工具检测自己的正则语句
非贪婪匹配结果:只替换了标签,正文内容被保留下来
贪婪匹配结果:整个正文内容全被替换
知道了正则该如何写之后就可以对 进行替换
这里需要注意的是:一定要先替换 标签,再替换普通的 标签。如果顺序反了之后会导致先去掉 标签, 标签的东西还存在并且后续的 ,结局就是改了和没改一样。
检测一下 的去 标签效果
ok,对比之前的函数解析正文内容说明已经成功达到目标效果
合并多个空格
问题又来了,我们发现有太多的空格,我们不需要这么多空格,因此需要合并多个空格
还是继续使用正则
注意 java 正则规则需要转义 ,因此多加一个
效果已经完善。仔细观察后边的的,会发现还有 这样的 html 中的空白占位符,我们也需要替换掉。因此在合并多个空格之前去掉
代码完善之后就开始最后的替换掉之前使用的
在验证一下整个 方法能否执行顺利
使用正则之后的优化速度还快了几秒钟的时间【解析正文快了2秒,新增文档快了7秒】
在对 类打断点验证一下是否解析正确
到这里,我们已经实现好了搜索模块的需求。这里做一个总结。
- 需求是能够去倒排索引中查询出和关键词分词结果有关的文档Id【√】
- 查询过程中需要将返回结果进行降序排序【重难点√】
- 包装数据返回【√】
剩下的一些是在制作过程中发现的一些需要优化的地方
- 优化文件读取
- 使用正则表达式提升替换效率
- 需要根据独立成词进行查询【合并空格】
在以后的实际开发中,技术都是为了业务服务的,更重要的是也要学习产品的业务
现在后台的逻辑,数据都有了。现在需要最终以网页的形式把我们的程序呈现给用户
前端(HTML+CSS+JS)+后端(Java,Servlet/Spring)
现在我们需要名的描述出,服务器接受什么样的请求都能返回什么样的响应。此处,我们需要是一个接口,搜索接口即可
Java代码
验证一下,我们需要的数据都在,现在需要把它放在 html 页面上
到此为止,页面的大概布局已经完成,现在需要获取后端数据来填充网页了。
ajax 前后端交互的常用手段,当用户点击搜索按钮的时候,浏览器就会获取到搜获框内容,基于 ajax 构造 HTTP 请求并发送给服务器,浏览器获取到服务器响应结果后再根据结果的 json 数据 把页面给生成出来
JS 是原生的 ajax,是 XMLHttpRequest,就可以采取其它方式来使用 ajax(借助第三方库),JQuery(js的第三方库,这个库里功能很多,单单是使用 ajax 即可)
如何使用JQuery?
搜索 JQuery,找到 JQuery 的官方然后下载即可
这里选用的压缩版本
先验证代码是否获取到搜索框内容
如果不验证,后续代码如果拿不到搜索框的内容将会出现获取不到值也就无法搜索
验证成功后再构造一个 ajax 请求发给服务器
$:是一个变量名,这个是 JQuery 这个库提供的一个内置的对象的变量名,使用的 JQuery 中的函数/方法,其实就是这个 $ 对象提供的
有的语言允许使用 $ 作为变量名(Java/JS),有的不允许(C/C++)
先测试一下后端服务器是否正常相应前端 ajax 发送的 HTTP 请求
如果不验证,不知道这个 ajax 请求是否正常发送
这一步代码没有问题,前后端的数据交互已经初步完成
细心的朋友会看到 url 中出现的两个
其实这个只是浏览器显示的问题,当初我发现的时候以为前边的 写错了。检查半天也没觉得不妥的地方,后来往下继续完成的时候发现仅仅是浏览器转义字符的问题,数据方面是完全正确的
下一步就是我们利用 DOM API 把数据填充到 html 中
完善 函数
针对内容太多,超出屏幕问题
可以设置 CSS 的 属性让超过的部分隐藏
针对点击之后搜索结果停留在当前页的修改
利用 DOM API 设置 title的 即可
针对多次搜搜结果重叠在一起
每次点击按钮,都是宝所结果往 中进行累加,没有清理过,更合理的做法应该是在搜索前把之前的的搜搜结果清空掉
想把用户的关键词在搜索的页面中全部标红显示。需要前后端配合
- 修改后端代码,生成搜索结果的时候(描述)就需要把其中包含关键词的部分你给加上一个 标签
- 前端这里针对 标签设置样式进行标红
Java 代码
CSS 代码
测试一些稀奇古怪的查询词
发现报了 500 错误。查看源头是越界异常的问题。
原来的代码
因此忘记了越界的情况,修改如下
修改效果图,已经完善了。
![在这里插入图片描述](https://img-blog.csdnimg.cn/7ae680be308c47538587508aa5ad3439.png
我们发现搜索 Array List 为何也会出现 一个标红的也没有 呢?
其实这里的原因是因为 Array List 中间的空格导致的,如果以 空格 来查询的话,那查询的数据会很多了。
因为空格就相当于汉语中的 我,的,是,用,好,有… 等等这些常用的词,英语中对应的是 is,a,yes,yeah,ok… 这些常用词。因此就需要使用一个叫做 “暂停词”的词表
这里去搜索关键词 暂停词 就会出现很多。下载保存即可。然后再使用 把这些词存储起来,再针对分词结果在停用词表中进行筛选。如果某个词在词表中存在就直接干掉。
完整 Search 类代码
再搜索 Array List 则不会出现那样的情况
我们在搜索 ArrayList 会发现还是有:既没有 Array 也没有 List 的情况发生,那么这可能是我们的后端代码的BUG了
我们点标题链接进去查看一下是什么原因
查看网页源代码
发现关键词 Array 是包含在 之中的,但是却没有出现在描述中
再看描述的开头
再看线上文档的开头
现在应该知道问题在哪儿了
因为找到了 array 关键字。而在原文档中是
而代码中为了达到全词匹配的效果采用的是 所以就找不到,触发下面的代码
这也就是我们看的一个标红的也没有的原因,但是也查到了 关键词
因此还是需要使用 正则表达式 来去除
正则在线测试工具
使用 来代替空格实现全词匹配,实际效果更好
但是不能全部使用 来代替
因为 不支持 正则表达式
解决方案:未知问题转为已知问题
提前先把 关键词周围的标点,符号全部转为空格,在进行之前的全词查找即可【经过转化之后就可以使用了】
查看效果,已经纠正之前的bug了,出现的结果一定会被标红,标红的结果一定会出现。
正常搜索引擎都会有一个搜索结果的统计。这里我们也加上一个。
有两个方案
- 直接在服务器这边算好了个数,返回给浏览器【及需要修改前端有需要修改后端】
- 在浏览器这边根据收到的结果的数组的的长度自动地展示出个数【只需要修改前端】
因此我们选择方案2简单
效果如图所示
我们看一下查询个数
是否可能存在某个文档同时包含 array 和 list 呢?我们就用集合类的接口 来举例
前面计算权重的时候,都是对 query 进行了分词。举例:
则会被分为 ,, 三部分。经过暂停词的过滤之后只有 和 。由于 和 都实现了 接口,因此会出现两次相同的结果。
正常来说,对于同一个结果不应该出现两次分重复的搜索结果,像 这样意的文档味着权重更高,我们提高权重之后相关性也就会更高。
一个简单的办法就是把权重进行相加
要实现这样的效果就需要把触发结果进行合并,把多个分词结果触发的文档按照 进行去重,同时进行权重的合并。
数据结构中有一个经典的题目就是 合并两个有序链表 此处我们就可以模仿类似的思路进行合并两个相同数组:先把统计结果按照 升序排序,再合并的时候相同 的就可以进行权重相加。此时我们可能还需要和合并 N个 数组。
利用优先级队列,建立大根堆,那么就会存储的是最小的堆顶元素,进行 N 个合并了。
需要改动一下的代码
记得一定要验证权重合并:在此搜索 然后浏览器查询 关键字就只会有一个标题出现了
如果服务器还未购买的可以先看我的博客,介绍了 阿里云服务器的购买及搭建一个博客园的流程
有了 和 环境之后就把生成的 包放在服务器上即可自动解压
利用的 xshell 可以直接把 包拖入 目录即可
验证一下成功没有
然后我们再放 正倒排索引和暂停词 的文件【注意源代码中修改路径】
更改 代码路径
更改 代码路径
云服务器测试通过,至此为止已经完成项目了。
当启动 Spring 的时候却发现找不到路径
因此我们设置一个配置文件,在本地运行的时候就设置为本地路径;在服务器上运行的时候就设置为线上路径
先测试本地
发现了一丝不妙的情况
抓包之后发现是数据的格式对不上,如果是 则前端会被认为是一个 ,因此我们需要在后端的数据返回的时候设置为 格式或者前端代码把 data 转为 json格式【这里采用后端修改】
前端修改应该修改 data【可查看JSON.Stringfy()】
修改后段的结果,程序正常显示
修改配置文件,资源加载路径进行切换
利用 Maven 打包却发现没有通过测试,因为路径不存在
添加 代码如下就可以在测试出错的情况下也完成编译【忽略Test单元测试】
上传至服务器
再启动之前一定要记得,先确定对应的端口号(默认是8080是否已经被占用),一个主机上的一个端口通常情况下只能被一个进程来绑定。
解决方案
- 关闭现有的8080端口
- 修改本程序的启动端口
这里我们选择关闭之前的 8080 端口
kill命令,netstat命令
启动程序
验证效果
部署也已经完成了
但是也有瑕疵,还没完成。
当我们把 xShell 关闭之后就会发现数据无响应
这里涉及到一个概念:前台进程 vs 后台进程
这里和 Java 中的 守护线程 和 非守护线程【isDaemon】没有关系
直接输入一个命令来产生的进程都是前台进程,前台进程会随着终端的关闭而随之被杀死
ps命令
查看后发现 java 产生的进程运行时间为 0
因此需要把这个前台进程转换为后台进程
nohup命令
在这里,我们会多出一个日志文件
终端不显示,而是把内容输出到文件中。我们再关闭 xShell 看看能否成功
已经成功,如果发现上述操作失败的了。可以重新把前台进程转换为后台进程就可以了。可能是服务器卡的原因。
项目链接
- 索引模块
Parser 类完成制作索引的流程;Index类实现索引的数据结构 - 搜索模块
Search 类来完成搜索的整个过程,调用了 Index 类来查正排查倒排。同时也实现了生成描述,关键词标红,重复文档合并等功能
核心内容:通过一些数据结构来完成了一个搜索引擎最小功能的集合
- Web 模块
通过 Servlet/Spring Boot 实现了两个版本的服务器程序;通过 HTML/CSS/JS 做了一个搜索页面
已经设置免费下载,至此完整代码如下 下载