搜索引擎(基于java在线文档)

背景:

基于java文档的搜索引擎,可以输入搜索词,然后就可以查询出与搜索词相关的文档。该项目的最主要的工作是要构建索引,就是正排和倒排索引。正排索引:根据文档id获取到文档;倒排索引:根据搜索词分词结果获取到相关文档。

此项目主要有3个板块:解析板块(Parser),索引板块(Index),服务板块(DocSearchService)

完整代码

1.解析板块

此版块的主要作用就是从本地文档(java在线文档)中加载文件到内存中,然后解析加载到内存中的文件(主要就是解析标题,Url,正文),解析完成后,构造索引结构,最后在进行硬盘的保存(以便于下一次启动时更快,不需要重新构建索引结构)

1.1 加载文件(enumFile)

此处是采用了一个递归的写法,首先找到本地文件路径,然后打开文件,文件中肯定不止一个文件,肯定会有很多子文件,所以需要递归来找出后缀名为html的所有文件,最终放入一个list集合。

1.2 解析文件(pathFile)

此处主要的完成工作是解析标题,解析Url,解析文章内容,然后添加文档(这一步就是构建索引结构,这一步放在索引模块来仔细说明)

1.2.1 解析标题(pathTitle)

搜索结果标题可以从文件名上获取,一般文件名都是文件内存最相关的内容,所以可以直接用字符串的截取获取即可。

1.2.2 解析Url(pathUrl)

解析Url就相当于拼接字符串,在线路径+本地路径的拼接,java在线文档的路径,对于前面来说都是固定的路径:,而后面就是就是本地文档路径了。

1.2.3 解析content(pathContent)

解析内容主要完成的是去掉标签,这里去标签,一是去script标签(标签和内容都不需要),二是去普通标签 。

去标签这里使用了正则表达式进行匹配,去之前还得将文件中的内容读到字符串中。

ps:去script标签必须在去普通标签之前。如果先取普通标签的话,那么就会将script标签给去掉,但是script标签中的内容还在,那后进行去script标签的时候就找不到script,从而script标签中的内容就去不掉了。

1.2.4 添加文档(addDoc)

主要包括构建正排和倒排索引,这部分放到后面索引板块来讲。

1.3 硬盘保存(save)

上述步骤完成之后,正排,倒排索引已经构建完毕了,用ObjectMapper类进行Json字符串的转化然后写入硬盘文件中,这部分放到索引板块细讲。

上述的所有方法都是在run方法中实现的,Parser是一个启动类入口,对于run方法固然有可以用单线程的方式来写,但是这里还实现了多线程的版本:

首先定义一个线程池(newFixedThreadPool:需要设置线程数量,队列是无限大的),队列无限大意味着任务可以一直添加,也就是任务添加完毕过后,但是任务(指的是解析文件:pathFile这一步)可能还没处理完就直接保存了,所以还需要一个计数器(CountDownLatch)来进行计数。设置当前CountDownLatch的大小为文件的数量,每当一个文件被解析完毕后,就减1,直到减为0以后才进行硬盘上的保存。

既然已经使用了多线程,那么在操作集合的时候就需要注意,否则会出现线程安全问题,这部分在后面的索引板块会体现出来。

2.索引板块

此模块的主要作用是创建正排,倒排索引,硬盘保存索引结构,从硬盘上加载索引数据。

2.1 创建正排索引(buildForward)

正排索引就是根据文档id获取文档,那么可以构造一个数组,数组的下标就是文档id,根据文档id就能获取对应文档。

由于上面在进行构造正排索引的时候使用了多线程,所以这里在操作集合的时候,要格外注意,所以在进行修改的时候,应该加锁。

2.2 创建倒排索引(buildInverted)

倒排索引就是根据搜索词寻找出相关文档,相关二字就涉及到权重问题(当前搜索词和文档的关联程度),由于是根据词来搜索文档,所以可以使用一个Map来进行当亲索引的创建。

private void buildInverted(DocInfo docInfo) {class WordCnt {public int titleCount;public int contentCount;}synchronized (object2) {//当前map用来统计分词结果HashMap<String,WordCnt> wordCntHashMap = new HashMap<>();//1.针对文档标题进行分词//已经对大写进行转换成小写//仔细观察,HEllo和hello搜索出来的结果一样,所以遇到这两个单词,应该记两次List<Term> titleTerms = ToAnalysis.parse(docInfo.getTitle()).getTerms();//2.遍历分词结果,统计出每个单词出现的次数for(Term term : titleTerms) {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;wordCntHashMap.put(word,wordCnt);}}//3.针对正文页进行分词List<Term> contentTerms = ToAnalysis.parse(docInfo.getContent()).getTerms();//4.遍历分词结果,统计每个词出现的次数for(Term term : contentTerms) {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;wordCntHashMap.put(word,wordCnt);}}//5.把上面的结果汇总到一个hashMap中,最终文档的权重,就设定成标题中出现的次数 * 10 + 正文中出现的次数//6.遍历刚才的hashMap,以此来更新倒排索引中的结构了for(Map.Entry<String,WordCnt> entry : wordCntHashMap.entrySet()) {//先根据这里的词去倒排索引中查一查ArrayList<Weight> invertedList = invertedIndex.get(entry.getKey());if(invertedList == null) {//表示倒排索引中没有相关词的文档//要进行构造ArrayList<Weight> newInvertedList = new ArrayList<>();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());weight.setWeight(entry.getValue().titleCount*10 + entry.getValue().contentCount);invertedList.add(weight);}}}
}

注意点:

(1)这个方法中有一个内部类WordCnt,有两个属性(一个用来统计在标题中出现的次数,一个用来统计在文档中出现的次数),并且用标题中出现的次数*10+文档中出现的次数(这里的权重计算比较简单,如果有更好的自行更换)。

(2)在操作map集合的时候,如果是在多线程环境下,没有进行线程安全问题的考虑的话是可能会出现问题的,所以这里也进行了加锁,保障了线程安全。可以看见构建正排和倒排的索引的锁是不相同的,如果使用同一把锁,由于是多线程的,那么正排和倒排索引是不能同时进行的,但如果是使用不同的锁,是可以同时进行的。

2.3 保存索引(save)

这一步主要是使用ObjectMapper类,进行对象到json字符串的转化,然后进行写硬盘。

2.4 加载硬盘(load)

也是使用ObjectMapper类进行,json字符串到对象的转换,然后进行读硬盘。

2.5 新增文档(addDoc)

主要完成的就是构建正排和倒排索引,也就是由buildForward和buildInverted组成。

 3.服务板块

该板块主要完成分词,去停用词,多个分词触发相同文档进行权重合并,结果构造。

3.1 分词

主要使用分词类ToAnalysis对搜索词进行分词,然后去掉停用词。

去停用词的必要性: 

进行去分词结果中的停用词,是为了避免用户输入带有空格,换行符等比较通用的词语,一旦这样的搜索词在被分词后,就当相于没有搜索了,因为只要是一篇比较长的文档都会带有这些符号,那不是全都会被查询出来,所以在进行搜索之前应该要这些停用词(网络上有文档可以自行下载)给去掉。

3.2 触发

针对分词结果进行倒排索引的查询。

3.3 合并

该步是很重要的一步,如果不行合并,那么对于一个文档中出现了多个分词结果,那么在最终返回的结果里面是会有重复的文档的,所以要进行权重的合并。

这里的合并就是多个List的合并,下面设计一个算法:

private List<Weight> mergeResult(List<List<Weight>> source) {//在进行合并的时候,是把多个行合并成一行了//合并的过程中势必是要操作这个二维数组里面的每个元素//操作元素就涉及到“行”“列”这样的概念,要想确定二维数组中的一个元素,就需要明确知道行和列//1.先针对每一行进行排序(按照id进行升序排序),因为必须是有序的,才可以进行下面的权重合并for(List<Weight> weights : source) {weights.sort((o1,o2)->{return o1.getDocId().compareTo(o2.getDocId());});}//2.借助一个优先级队列,进行合并//target表示合并的结果List<Weight> target = new ArrayList<>();//创建优先级队列,按照docId创建小根堆PriorityQueue<Pos> posPriorityQueue = new PriorityQueue<>((o1,o2)->{Weight w1 = source.get(o1.row).get(o1.col);Weight w2 = source.get(o2.row).get(o2.col);return w1.getDocId().compareTo(w2.getDocId());});//初始化队列,把每一行的第一个元素放到优先级队列中for(int i = 0; i < source.size(); i++) {posPriorityQueue.offer(new Pos(i,0));}//循环的取队首元素(也就是相当于这若干行中的最小元素)while (!posPriorityQueue.isEmpty()) {Pos minPos = posPriorityQueue.poll();Weight minWeight = source.get(minPos.row).get(minPos.col);//看看这个取到的weight是否和前一个插入到target中的结果是相同的docId//如果是就合并if(target.size() > 0) {Weight lastWeight = target.get(target.size()-1);if(lastWeight.getDocId().equals(minWeight.getDocId())) {//此时代表是同一个文档,应该进行合并lastWeight.setWeight(lastWeight.getWeight()+minWeight.getWeight());}else {target.add(minWeight);}}else {//当前结果集中没有对应的文档,直接插入即可target.add(minWeight);}//把当前元素处理完成之后,就需要进行把这个元素的对应光标位置往后移,去取当前位置(当前行)的下一个元素Pos newPos = new Pos(minPos.row,minPos.col+1);if(newPos.col >= source.get(newPos.row).size()) {//如果光标移动到超出了这一行的列数的话,就说明到末尾了//到达末尾之后说明当前行已经处理完毕continue;}posPriorityQueue.offer(newPos);}return target;
}

解析:

(1)先对每一个list进行文档id降序排序。

(2)创建一个优先级队列,按照文档id构建小根堆。

(3)把每一个list的第一个weight添加进队列中,然后取出队首的,与结果集合中的最后一个元素进行比较(因为里面结果集合中的每一个元素都是按照文档id进行排序的,所以只需要和结果集合中的最后一个元素比较即可),如果结果集合为空或者是最后一个元素的文档id与当前文档不同的话,说明不同合并直接添加进去即可,否则如果相同就进行权重的合并。

ps:这里的Pos类是为了更好的操作为List<List<Weight>>,这就相当于一个二维数组,并且也方便后面进行下一个元素的比较。

3.4 包装结果

根据查询出来的Weight(带有docId,weight两个属性)权重集合去查正排索引,查询出现的结果保存在最终返回的结果集合中。

可以看到这里还有一个方法GenDesc(生存描述),对于描述可以参考一些网站的搜索结果,搜索发现这些结果的描述中都是包含有关键词的,所以我们可以在正文中先找到关键词,从当前词向后150字符,向前60字符。如果当前位置是小于150字符直接返回所有文章内容即可,否则则按上述约定规则来进行生成描述。

private String GenDesc(String content, List<Term> terms) {int firstPos = 0;//拿分词结果去正文中查看是否存在当前分词for(Term term : terms) {String word = term.getName();//这里的匹配如果只是word的话,进行匹配list的话,会将ArrayList也进行匹配出来,而我们只是想要匹配出list//所以加上空格在两边//注意此处要进行小写转换,否则匹配会出现问题//因为分词的时候就已经进行了小写转换//这里只进行了了单个单词的匹配,如果现在有单词list),如果还是想搜索list就会搜索失败//所以可以采用如下方法进行替换://法一:由于indexOf不支持正则匹配的,所以可以采用第三方库进行正则匹配,但是比较复杂//法二:使用正则表达式将上述形式替换成旁边带有两个空格的形式content = content.replaceAll("\\b" + word +"\\b"," " + word + " ");firstPos= content.toLowerCase().indexOf(" " + word + " ");if(firstPos > 0) {//找到了break;}}if(firstPos == -1) {//代表当前没有找到//此时就直接返回一个空的描述或者也可以取正文的前150字符返回if(content.length() <= 150) {return content;}return content.substring(0,150) + "...";}//找到的第一个字符往前60字符,然后从当前位置往后150个字符为描述int begIndex = firstPos < 50 ? 0 : firstPos - 50;int endIndex = begIndex + 150 < content.length() ? (begIndex + 150) : content.length();String desc = content.substring(begIndex,endIndex) + "...";for (Term term : terms) {String word = term.getName();//当查询词为list的时候,不能把arraylist标红,所以加上空格//(?i)代表不区分大小写进行匹配desc = desc.replaceAll("(?i) " + word + " ","<i>" + word + "</i>");}return desc;
}

这里我们首先使用\b+word+\b进行正则匹配成 空格+word+空格的形式,是防止一旦出现word,的形式,使用空格+word+空格的形式就匹配不到了;使用空格+word+空格的时候,是为了避免想要匹配出list而匹配出Arraylist的情况。

最后还进行了word两边的替换成了i标签,是为了让前面能够选中i标签,从而给这个关键词设置一些形式,比如:标红。

前端略,重点在后端的描述,可以自行前往gitee查看前端代码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/897092.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【每日学点HarmonyOS Next知识】web滚动、事件回调、selectable属性、监听H5内部router、Grid嵌套时高度设置

【每日学点HarmonyOS Next知识】web滚动、事件回调、selectable属性、监听H5内部router、Grid嵌套时高度设置 1、HarmonyOS WebView加载url无法滚动&#xff1f; scroll 里面嵌套webView&#xff0c;demo参考&#xff1a; // xxx.ets import web_webview from ohos.web.webv…

Flink性能指标详解MetricsAnalysis

文章目录 Flink 组成1.JobManager2.TaskManager3.ResourceManager4.Dispatcher5.Client6. Env JobManager MetricsTaskManager Metrics Flink 组成 1.JobManager 管理任务 作业调度&#xff1a;负责接收和调度作业&#xff0c;分配任务到 TaskManager。资源管理&#xff1a;…

Flutter底层实现

1. Dart 语言 Dart 是 Flutter 的主要编程语言。Dart 设计之初就是为了与 JavaScript 兼容&#xff0c;并且可以编译为机器代码运行。Dart 提供了一些特性&#xff0c;如异步支持&#xff08;通过 async 和 await&#xff09;&#xff0c;这使得编写高效的网络请求和复杂动画变…

< 自用文儿 > CertBot 申请 SSL 证书 使用 challenge 模式 避开防火墙的阻挡

环境&#xff1a; 腾讯 VPS 腾讯会向你销售 SSL &#xff0c; 这个本是免费的。CertBot 默认申请证书要用到 80 端口&#xff0c;会蹭边什么什么条款&#xff0c;备案法律来阻止80端口的通讯&#xff0c;没有网站也一样被阻拦。 通过腾讯买的域名&#xff1a; bestherbs.cn …

【AI】【Unity】关于Unity接入DeepseekAPI遇到的坑

前言 由于deepseek网页端在白天日常抽风&#xff0c;无法正常的使用&#xff0c;所以调用API就成了目前最好的选择&#xff0c;尤其是Deepseek的API价格低得可怕&#xff0c;这不是和白送的一样吗&#xff01;然后使用过很多本地部署接入API的方式&#xff0c;例如Chatbox、Pa…

【微知】Mellanox驱动中to是什么?有哪些超时时间?(time out,心跳2s,reset 1分钟)

to是tout缩写&#xff0c;tout是time out 单位是毫秒。 static const u32 tout_def_sw_val[MAX_TIMEOUT_TYPES] {[MLX5_TO_FW_PRE_INIT_TIMEOUT_MS] 120000, # 2min。预初始化的总超时时间[MLX5_TO_FW_PRE_INIT_ON_RECOVERY_TIMEOUT_MS] 7200000, #设备恢复过程中的固件预初…

linux | Vim 命令快捷操作

注&#xff1a;本文为过去的 “vim 使用笔记”。 跳转命令 跳转命令 #&#xff1a;向前查找光标当前所在单词&#xff0c;并跳转到该单词的上一个出现位置。*&#xff1a;向后查找光标当前所在单词&#xff0c;并跳转到该单词的下一个出现位置。 行内跳转 0&#xff1a;跳转…

树莓派3B+的初步使用

树莓派3B的初步使用 一、安装使用树莓派系统1.将系统写入SD卡2.登录树莓派系统3.用C和Python编译运行hello world 一、安装使用树莓派系统 1.将系统写入SD卡 首先&#xff0c;准备至少16GB大小的SD卡以便装入树莓派系统&#xff0c;将SD卡插入读卡器后连接电脑准备给SD卡写入…

基于Windows11的DockerDesktop安装和布署方法简介

基于Windows11的DockerDesktop安装和布署方法简介 一、下载安装Docker docker 下载地址 https://www.docker.com/ Download Docker Desktop 选择Download for Winodws AMD64下载Docker Desktop Installer.exe 双点击 Docker Desktop Installer.exe 进行安装 测试Docker安装是…

文档处理控件Aspose.Total教程:使用 C# 将 Obsidian Markdown 转换为 OneNote

Obsidian 是一款广泛使用的基于 Markdown 的笔记应用程序。它提供了一种强大而有效的方式来构建和组织想法。用户可以无缝地连接他们的想法&#xff0c;提高清晰度和工作效率。另一方面&#xff0c;OneNote 是 Microsoft 的一款功能强大的笔记应用程序。它还可以帮助用户组织他…

第5章:vuex

第5章&#xff1a;vuex 1 求和案例 纯vue版2 vuex工作原理图3 vuex案例3.1 搭建vuex环境错误写法正确写法 3.2 求和案例vuex版细节分析源代码 4 getters配置项4.1 细节4.2 源代码 5 mapState与mapGetters5.1 总结5.2 细节分析5.3 源代码 6 mapActions与mapMutations6.1 总结6.2…

迷你世界脚本对象库接口:ObjectLib

对象库接口&#xff1a;ObjectLib 迷你世界 更新时间: 2023-04-26 20:21:09 具体函数名及描述如下: 序号 函数名 函数描述 1 getAreaData(...) 获取区域数据 2 getPositionData(...) 获取位置数据 3 getLivingData(...) 获取生物数据 4 getItemDat…

测试是如何跟进和管理 bug

测试在跟进和管理 Bug定位精确、问题反馈及时、修复闭环高效 三大关键环节中起到了至关重要的作用。Bug定位精确 是整个流程的基础&#xff0c;通过详细记录和复现问题&#xff0c;可以帮助开发团队迅速找出缺陷根源&#xff1b;而及时有效的反馈机制则确保问题不会被遗漏&…

运动控制卡--固高实用

目录 组件 配置参数 编程控制 组件 我手头有固高卡&#xff0c;记录使用。 用运动控制卡 伺服&#xff08;步进&#xff09;电机搭建一个运动控制系统&#xff0c;主要包括&#xff1a;1、控制器 2、端子板 1、控制器 2、端子板 3、伺服&#xff08;步进&#xff09;…

2025年能源工作指导意见

2025年是“十四五”规划收官之年&#xff0c;做好全年能源工作意义重大。为深入贯彻落实党中央、国务院决策部署&#xff0c;以能源高质量发展和高水平安全助力我国经济持续回升向好&#xff0c;满足人民群众日益增长的美好生活用能需求&#xff0c;制定本意见。 一、总体要求…

键值对(C++实现)

目录 键值对的定义 键值对的底层实现 键值对的作用 键值对的使用 对键值对中的值的搜索 一、键值对的定义 键值对&#xff08;Key-Value Pair&#xff09;是一种数据结构&#xff0c;用于存储和表示两个相关联的值。在键值对中&#xff0c;一个值被关联到一个唯一的键上&…

使用 Docker 部署 RabbitMQ 并实现数据持久化

非常好&#xff01;以下是一份完整的 Docker 部署 RabbitMQ 的博客文档&#xff0c;包含从安装到问题排查的详细步骤。你可以直接将其发布到博客中。 使用 Docker 部署 RabbitMQ 并实现数据持久化 RabbitMQ 是一个开源的消息队列系统&#xff0c;广泛应用于分布式系统中。使用…

springboot429-基于springboot的教务管理系统(源码+数据库+纯前后端分离+部署讲解等)

&#x1f495;&#x1f495;作者&#xff1a; 爱笑学姐 &#x1f495;&#x1f495;个人简介&#xff1a;十年Java&#xff0c;Python美女程序员一枚&#xff0c;精通计算机专业前后端各类框架。 &#x1f495;&#x1f495;各类成品Java毕设 。javaweb&#xff0c;ssm&#xf…

my学习网址

文章目录 1.软件版本管控GIT学习网站大全1官方文档类2在线教程类3互动学习类4问答社区类 Linux学习网址1、 Linux命令行与shell脚本编程大全 1.软件版本管控 GIT学习网站大全 廖雪峰网站 以下为你推荐不同类型的学习Git的网站&#xff1a; 1官方文档类 Git官方文档 网址&am…

Best practice-生产环境中加锁的最佳实践

什么是死锁&#xff1f; 场景&#xff1a;图书馆有两个相邻的储物柜&#xff08;柜子A和柜子B&#xff09;&#xff0c;小明和小红需要同时使用这两个柜子才能完成借书流程。 互斥资源 每个柜子只有一把钥匙&#xff0c;且一次只能被一人使用&#xff08;资源不可共享&#x…