【文档搜索引擎】搜索模块的完整实现

调用索引模块,来完成搜索的核心过程

主要步骤

简化版本的逻辑

  1. 分词:针对用户输入的查询词进行分词(用户输入的查询词,可能不是一个词,而是一句话)
  2. 触发:拿着每个分词结果,去倒排索引中查,找到具有相关性的文档(调用 Index 类里面查倒排的方法)
  3. 排序:针对上面触发出来的结果,进行排序(按照相关性,降序排序)
  4. 包装结果:根据排序后的结果,依次去查正排,获取到每个文档的详细信息,包装成一定结构的数据,返回出去

DocSearcher 类

public List<Result> search(String query){}

这个方法用来完成整个搜索的过程。

  • 参数就是用户给出的查询词
  • 返回值就是搜索结果的集合
// 通过这个类,来完成整个的搜索过程  
public class DocSearcher {  // 此处要加上索引对象的实例  // 同时要完成索引加载的工作(这样才能将文件里面的索引加到内存中,不然内存中没有东西查)  private Index index = new Index();  public DocSearcher() {  index.load();  }  // 完成整个搜索过程的方法  // 参数(输入部分)就是用户给出的查询词  // 返回值(输出部分)就是搜索结果的集合  public List<Result> search(String query){  // 1. [分词] 针对 query 这个查询词进行分词  // 2. [触发] 针对分词结果来查倒排  // 3. [排序] 针对触发的结果按照权重降序排序  // 4. [包装结果] 针对排序的结果,去查正排,构造出要返回的数据  return null;  }  
}
  • 这里要加上索引,并且要将索引加载到内存中,不然搜索没有原数据
  • 我们这里直接使用一个构造方法,将 index 加载到内存中即可

1. 分词操作

针对 query 这个查询词进行分词

List<Term> terms = ToAnalysis.parse(query).getTerms();
  • 直接使用第三方库,进行分词
  • 用 List 接收每一个分词结果

2. 触发

针对分词结果来查倒排

List<Weight> allTermResult = new ArrayList<>();   
for(Term term : terms) {  String word = term.getName();  // 虽然倒排索引中有很多的词,但是这里的词一定都是之前的文档中存在的  List<Weight> invertedList = index.getInverted(word);  if(invertedList == null) {  // 说明这个词在所有文档中都不存在  continue;  }  // 对我们的每一个倒排拉链进行汇总  allTermResult.addAll(invertedList);  
}
  • 循环遍历 Terms,提出每一个词的名字,然后去查倒排
    1. 首先取到名字
    2. 然后直接使用 index 里面的查倒排的方法 getInverted 方法即可(这里是直接返回 term 所对应的 value,若不存在,就返回 null
  • 最后将所有的倒排拉链都加入到 allTermResult 中,进行汇总

3. 排序

针对触发的结果按照权重降序排序。此时待排序的对象是 alltermResult

  • 此处我们直接使用线程的排序方法
allTermResult.sort(new Comparator<Weight>() {  @Override  public int compare(Weight o1, Weight o2) {  return o2.getWeight() - o1.getWeight();  }  
});
  • 这里进行 sort 比较时,由于比较对象不清楚,比较规则不知道,所以我们需要制定一个比较规则
  • 创建一个实现 Comparator 接口的类,再去重写里面的方法,最后再去 new 出实例
    • 如果是升序排序:return o1.getWeight() - o2.getWeight();
    • 如果是降序排序:return o2.getWeight() - o1.getWeight();

4. 包装结果

针对排序的结果,去查正排,构造出要返回的数据

List<Result> 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());  results.add(result);  
}
  1. 取到每一个结果 weight
  2. 拿着 weight 里面的 DocId,去查找文档 docInfo
  3. docInfo 里面的 titleurl 信息都设置到 result 里面(content 部分我们只需要一部分,所以不能直接通过 getContent 获得)
  4. 将描述添加到 result
  5. result 添加到链表 results 中即可
生成描述

构造结果的时候,需要生成“描述”

  • 描述就是正文的一段摘要,这个摘要源自于正文,同时要包含查询词或者查询词的一部分

生成描述的思路
我们可以获取到所有的查询词的分词结果。

  1. 遍历分词结果,看哪个结果在正文中出现
  2. 针对这个被包含的分词结果,去正文中查找,找到对应的位置
  3. 以这个词的位置为中心,往前截取 60 个字符,作为描述的开始
  4. 再从描述开始,一股脑地截取 160 个字符,作为整个描述

针对当前这个文档来说,不一定会包含所有分词结果。只要包含其中一个就能被触发出来

小写转换查找
// 此处需要的是 “全字匹配”,让 word 能够独立成词,才要查找出来,
// 而不是只作为词的一部分(左右加空格)  
firstPos = content.toLowerCase().indexOf(" " + word + " ");
  • word 是通过分词结果得来的,在进行分词的时候,分词库就自动地将 word 转换成小写了
  • 正因如此,我们需要先把正文部分也转换成小写
独立成词
int firstPos = -1;  
// 先遍历分词结果,看看哪个结果是在 content 中存在  
for(Term term : terms) {  // 分词库直接针对词进行转小写了  // 正因如此,就必须把正文也先转成小写,然后再查询  String word = term.getName();  // 此处需要的是 “全字匹配”,让 word 能够独立成词,才要查找出来,而不是只作为词的一部分(左右加空格)  firstPos = content.toLowerCase().indexOf(" " + word + " ");  if(firstPos >= 0){  // 找到了位置  break;  }  
}
  • 假设现在的查询词是 List
  • 文档正文中包含一个这样的单词—— ArrayList
  • 在生成描述的时候,此处拿着这个 List 去正文中 indexOf,此时是否会把 ArrayList 当做结果呢?(肯定会
    • 这就会导致生成的描述,里面就是带 ArrayList 的,而不是带 List 的(不科学的
  • 类似的情况,在查倒排的时候,是否会存在呢?
    • 不会的;倒排索引中的 key 都是分词结果,ArrayList 不会被分成 Array + List,就仍然会吧 ArrayList 视为是一个单词,所以 ListArrayList 不能匹配,因此 List 这个词不能查出包含 ArrayList 的结果(科学的

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

截取字符
// 所有的分词结果都不在正文中存在(标题中触发)  
if(firstPos == -1) {  // 此时就直接返回一个空的描述,或者也可以直接取正文的前 160 个字符  // return null;  return content.substring(0, 160) + "...";  
}  
// 从 firstPos 作为基准位置,往前找 60 个字符,作为描述的起始位置  
String desc = "";  
int descBeg = firstPos < 60 ? 0 : firstPos - 60;  // 不足 60 个字符,就直接从 0 开始读  
if(descBeg + 160 > content.length()) {  desc = content.substring(descBeg); // 从 descBeg 截取到末尾  
}else {  desc = content.substring(descBeg, descBeg + 160) + "...";  
}  
return desc;
  • firstPos 还是 -1 的时候,就是分词结果未找到,我们可以直接返回 null 或者正文前 160 个字符
  • firstPos 不是 -1 的时候,就是找到分词了
    • firstPos < 60,则 descBeg 置为 0;若 firstPos > 60,则 descBeg 置为 firstPos - 60
    • descBeg 的长度大于正文的长度了,则直接在正文中从 descBeg 的位置截取到文末;若没有,则从 descBeg 的位置往后截取 160 个字符
  • 最后直接返回 desc 即可
完整代码
private String GenDesc(String content, List<Term> terms) {  int firstPos = -1;  // 先遍历分词结果,看看哪个结果是在 content 中存在  for(Term term : terms) {  // 分词库直接针对词进行转小写了  // 正因如此,就必须把正文也先转成小写,然后再查询  String word = term.getName();  // 此处需要的是 “全字匹配”,让 word 能够独立成词,才要查找出来,而不是只作为词的一部分(左右加空格)  firstPos = content.toLowerCase().indexOf(" " + word + " ");  if(firstPos >= 0){  // 找到了位置  break;  }  }  // 所有的分词结果都不在正文中存在(标题中触发)  if(firstPos == -1) {  // 此时就直接返回一个空的描述,或者也可以直接取正文的前 160 个字符  // return null;  return content.substring(0, 160) + "...";  }  // 从 firstPos 作为基准位置,往前找 60 个字符,作为描述的起始位置  String desc = "";  int descBeg = firstPos < 60 ? 0 : firstPos - 60;  // 不足 60 个字符,就直接从 0 开始读  if(descBeg + 160 > content.length()) {  desc = content.substring(descBeg); // 从 descBeg 截取到末尾  }else {  desc = content.substring(descBeg, descBeg + 160) + "...";  }  return desc;  
}

测试

public static void main(String[] args) {  DocSearcher docSearcher = new DocSearcher();  Scanner scanner = new Scanner(System.in);  while(true) {  System.out.println("-> ");  String query = scanner.next();  List<Result> results = docSearcher.search(query);  for(Result result : results) {  System.out.println("======================");  System.out.println(result);  }  }
}

image.png

  • 在验证过程中,发现描述中出现了这种内容,这个内容就是 JavaScript 的代码
  • 我们在处理文档的时候,只对正文进行了“去标签”,有的 HTML 里面还包含了 script 标签
  • 因此就导致去了标签之后,JS 的代码也被整理到索引里面了
  • 这个情况显然是并不科学的,我们需要处理一下

去掉 JS 标签和内容

正则表达式

  • 通过一些特殊的字符串,描述了一些匹配的规则
  • JavaString 里面的很多方法,都是直接支持正则的(indexOfreplacereplaceAllspilt…)

这里我们主要用到的主要有:

  • .:表示匹配一个非换行字符(不是 \n 或者不是 \r
  • *:表示前面的字符可以出现若干次
  • .*:匹配非换行字符出现若干次

去掉 script 的标签和内容,正则就可以写成这样:<script.*?>(.*?)</script>

  • 先去匹配一下有没有 <script>, 里面可能会包含各种属性, 有的话我们都当成任意字符来匹配
    去掉普通的标签(不去掉内容):<.*?>
  • 既能匹配到开始标签,也能匹配到结束标签
  • ? 表示“非贪婪匹配”:尽可能短的去匹配,匹配一个符合条件的最短结果
  • 不带 ? 表示“贪婪匹配”:尽可能长的去匹配,匹配一个符合条件的最长结果

假设有一个 content<div>aaa</div> <div>bbb</div>

  • 如果使用贪婪匹配,.* 此时就把整个正文都匹配到了。进行替换,自然就把整个正文内容都给替换没了
  • 如果使用非贪婪匹配,.*? 此时就是会匹配到四个标签。如果进行替换,也只是替换标签,不会替换内容

代码实现

此时我们就需要重新对 Parser 类的 parserContent 方法进行修改,让其能够去掉 JS 标签和内容

此时我们在 Parser 类中重新写一个方法,实现一个让正文能够去掉 JS 标签和内容的逻辑。

  • 这个方法内部就基于正则表达式,实现去标签,以及去除 script
  1. 先把整个文件都读到 String 里面(然后才好使用正则进行匹配)

这里我们实现一个 readFile 方法,用来读取文件

private String readFile(File f) {  // BufferedReader 设置缓冲区,将 f 中的内容预读到内存中  try(BufferedReader bufferedReader = new BufferedReader(new FileReader(f))){  StringBuilder content = new StringBuilder();  while(true) {  int ret = bufferedReader.read();  if(ret == -1) {  // 读完了  break;  }else {  char c = (char)ret;  if(c == '\n' || c == '\r'){  c = ' ';  }  content.append(c);  }  return content.toString();  }  }catch (IOException e){  e.printStackTrace();  }  return "";  // 抛了异常,就直接返回一个空字符串  
}
  1. 替换掉 script 标签
content = content.replaceAll("<script.*?>(.*?)</script>", " ");
  1. 替换掉普通的 HTML 标签
content = content.replaceAll("<.*?>", " ");

注意标签替换顺序不能变

  1. 使用正则,把多个空格合并成一个
content = content.replaceAll("\\s+", " ");
  • 正则表达式的空格是 \s\\s 是转义字符
  • + 也是表示这个符号会出现多次,还表示这个符号至少要出现一次
  • * 只表示这个符号会出现多次,但也可以一次都不出现

完整代码

// 这个方法内部就基于正则表达式,实现去标签,以及去除 script
public String parseContentByRegex(File f) {  //1. 先把整个文件都读到 String 里面  String content = readFile(f);  // 2. 替换掉 script 标签  content = content.replaceAll("<script.*?>(.*?)</script>", " ");  // 3. 替换掉普通的 HTML 标签  content = content.replaceAll("<.*?>", " ");  // 4. 使用正则把多个空格,合并成一个空格  content = content.replaceAll("\\s+", " ");return content;  
}

再次运行 DocSearcher,可以发现描述中的内容变规范了:image.png|592

搜索模块总结

实现了 Searcher 类里面的 search 方法

  1. 分词
  2. 触发
  3. 排序
  4. 包装结果
    这里面的很多脏活累活都交给了第三方库和前面模块已经封装好的方法,这里仅仅只是将之前准备好的工作给串起来

这里的搜索模块实现比较简单,主要还是因为当前没有什么“业务逻辑

  • 有的搜索结果要展示不同的搜索样式(图片、子版块、视频…)
  • 有的搜索结果会受到地域和时间的影响
  • 在实际开发中,技术都是为了业务服务的
  • 在公司中除了学习技术之外,也要学习产品的业务

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

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

相关文章

MySQL 数据库优化详解【Java数据库调优】

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c; 忍不住分享一下给大家。点击跳转到网站 学习总结 1、掌握 JAVA入门到进阶知识(持续写作中……&#xff09; 2、学会Oracle数据库入门到入土用法(创作中……&#xff09; 3、手把…

ensp 关于acl的运用和讲解

ACL&#xff08;Access Control List&#xff0c;访问控制列表&#xff09;是一种常用于网络设备&#xff08;如路由器、交换机&#xff09;上的安全机制&#xff0c;用于控制数据包的流动与访问权限。ACL 可以指定哪些数据包允许进入或离开某个网络接口&#xff0c;基于不同的…

突围边缘:OpenAI开源实时嵌入式API,AI触角延伸至微观世界

当OpenAI宣布开源其名为openai-realtime-embedded-sdk的实时嵌入式API时&#xff0c;整个科技界都为之震惊。这一举动意味着&#xff0c;曾经遥不可及的强大AI能力&#xff0c;如今可以被嵌入到像ESP32这样的微型控制器中&#xff0c;真正地将AI的触角延伸到了物联网和边缘计算…

知识中台与人工智能:融合赋能企业智能化知识服务与决策

在数字化、智能化的时代背景下&#xff0c;企业面临着前所未有的机遇与挑战。为了提升知识管理与服务的能力&#xff0c;推动企业的智能化转型与发展&#xff0c;知识中台与人工智能的融合应用正成为新的趋势。知识中台作为连接数据、知识与业务的核心平台&#xff0c;能够为企…

《Web 项目开发之旅》

一、项目简介 介绍项目的背景与目标&#xff0c;说明为什么要开展这个 Web 项目。展示项目最终完成后的整体页面截图&#xff0c;让读者对项目外观有初步印象。 二、技术选型 阐述在项目中使用的前端技术&#xff08;如 HTML、CSS、JavaScript 框架等&#xff09;、后端技术…

VSCode搭建Java开发环境 2024保姆级安装教程(Java环境搭建+VSCode安装+运行测试+背景图设置)

名人说&#xff1a;一点浩然气&#xff0c;千里快哉风。—— 苏轼《水调歌头》 创作者&#xff1a;Code_流苏(CSDN) 目录 一、Java开发环境搭建二、VScode下载及安装三、VSCode配置Java环境四、运行测试五、背景图设置 很高兴你打开了这篇博客&#xff0c;更多详细的安装教程&…

【GIS教程】使用GDAL实现栅格转矢量(GeoJSON、Shapefile)- 附完整代码

文章目录 一、 应用场景1、GeoJSON2、ESRI Shapefile3、GDAL 二、基本思路1、数据准备2、重投影&#xff08;可选&#xff09;3、创建空的矢量图层4、栅格转矢量 三、完整代码四、总结五、拓展&#xff08;使用ArcGIS工具进行栅格转矢量&#xff09; 一、 应用场景 TIFF格式的…

计算机毕业设计PySpark+Hadoop中国城市交通分析与预测 Python交通预测 Python交通可视化 客流量预测 交通大数据 机器学习 深度学习

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…

JVM系列(十三) -常用调优工具介绍

最近对 JVM 技术知识进行了重新整理&#xff0c;再次献上 JVM系列文章合集索引&#xff0c;感兴趣的小伙伴可以直接点击如下地址快速阅读。 JVM系列(一) -什么是虚拟机JVM系列(二) -类的加载过程JVM系列(三) -内存布局详解JVM系列(四) -对象的创建过程JVM系列(五) -对象的内存分…

Hive其四,Hive的数据导出,案例展示,表类型介绍

目录 一、Hive的数据导出 1&#xff09;导出数据到本地目录 2&#xff09;导出到hdfs的目录下 3&#xff09;直接将结果导出到本地文件中 二、一个案例 三、表类型 1、表类型介绍 2、内部表和外部表转换 3、两种表的区别 4、练习 一、Hive的数据导出 数据导出的分类&…

使用RKNN进行YOLOv8人体姿态估计的实战教程:yolov8-pose.onnx转yolov8-pose.rknn+推理全流程

之前文章有提到“YOLOv8的原生模型包含了后处理步骤,其中一些形状超出了RK3588的矩阵计算限制,因此需要对输出层进行一些裁剪”,通过裁剪后得到的onnx能够顺利的进行rknn转换,本文将对转rnkk过程,以及相应的后处理进行阐述。并在文末附上全部源码、数据、模型的百度云盘链…

Pytorch | 利用NI-FGSM针对CIFAR10上的ResNet分类器进行对抗攻击

Pytorch | 利用NI-FGSM针对CIFAR10上的ResNet分类器进行对抗攻击 CIFAR数据集NI-FGSM介绍背景算法流程 NI-FGSM代码实现NI-FGSM算法实现攻击效果 代码汇总nifgsm.pytrain.pyadvtest.py 之前已经针对CIFAR10训练了多种分类器&#xff1a; Pytorch | 从零构建AlexNet对CIFAR10进行…

继承详细总结

一.继承的定义与概念 1.定义&#xff1a;继承是一种is-a的关系&#xff0c;例如&#xff0c;哺乳动物是动物&#xff0c;狗是哺乳动物&#xff0c;因此&#xff0c;狗是动物&#xff0c;等等。 2.允许我们在保持原有类特性的基础上进行扩展&#xff0c;增加方法(成员函数) 和 属…

批量多线程给TXT文档插入相关腾讯AI【高质量无水印无版权】原创图片

给蜘蛛访问的网站文章插入相关图片&#xff0c;可以带来以下好处&#xff1a; ‌1、提升用户体验‌&#xff1a;图片能够直观地展示文章内容&#xff0c;帮助用户更好地理解和消化信息。对于阅读者来说&#xff0c;图文并茂的内容往往更具吸引力&#xff0c;也能提高他们的阅读…

牵手红娘:牵手App红娘助力“牵手”,脱单精准更便捷

随着互联网的普及&#xff0c;现代青年的社交圈层加速扩大&#xff0c;他们的恋爱观也正经历着前所未有的转变。在繁忙的工作之余&#xff0c;人们希望能够找到一种既高效又真诚的交友方式。于是&#xff0c;线上交友平台成为了他们寻找爱情的新选择。让不同文化背景、不同工作…

STM32-笔记12-实现SysTick模拟多线程流水灯

1、前言 正常STM32实现多线程&#xff0c;需要移植一个操作系统FreeRTOS。但是在这里不移植FreeRTOS怎么实现多线程呢&#xff1f;使用SysTick&#xff0c;那么怎么使用SysTick来模拟多线程呢&#xff1f;前面我们知道SysTick就是一个定时器&#xff0c;它不是在主函数的while循…

麒麟信安受邀出席开放原子园区行暨供需对接活动,分享基于关基领域打造的行业解决方案

12月17日&#xff0c;武汉市经信局和开放原子开源基金会共同主办开放原子园区行暨供需对接活动&#xff0c;旨在推进武汉市开源体系建设&#xff0c;推动开源技术赋能千行百业。活动吸引了武汉市委金融办、市卫健委、疾控中心、医保局、信息中心(大数据中心)、市红十字会、银行…

golang 并发--goroutine(四)

golang 语言最大的特点之一就是语法上支持并发&#xff0c;通过简单的语法很容易就能创建一个 go 程&#xff0c;这就使得 golang 天生适合写高并发的程序。这一章节我们就主要介绍 go 程&#xff0c;但是要想完全理解 go 程我们需要深入研究 GPM 模型&#xff0c;关于 GPM 模型…

SpringAI人工智能开发框架006---SpringAI多模态接口_编程测试springai多模态接口支持

可以看到springai对多模态的支持. 同样去创建一个项目 也是跟之前的项目一样,修改版本1.0.0 这里 然后修改仓库地址,为springai的地址 然后开始写代码

JSON 系列之1:将 JSON 数据存储在 Oracle 数据库中

本文为Oracle数据库JSON学习系列的第一篇&#xff0c;讲述如何将JSON文档存储到数据库中&#xff0c;包括了版本为19c和23ai的情形。 19c中的JSON 先来看一下数据库版本为19c时的情形。 创建表colortab&#xff0c;其中color列的长度设为4000。若color的长度需要设为32767&a…