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

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

主要步骤

简化版本的逻辑

  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.rhkb.cn/news/495052.html

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

相关文章

如何在centos系统上挂载U盘

在CentOS上挂载NTFS格式的U盘,需要执行一系列步骤,包括识别U盘设备、安装必要的软件、创建挂载点,并最终挂载U盘。以下是在CentOS上挂载NTFS格式U盘的详细步骤: 一、准备工作 确认CentOS版本: 确保你的CentOS系统已经安装并正常运行。不同版本的CentOS在命令和工具方面可能…

pytest自动化测试数据驱动yaml/excel/csv/json

数据驱动 数据的改变从而驱动自动化测试用例的执行&#xff0c;最终引起测试结果的改变。简单说就是参数化的应用。 测试驱动在自动化测试中的应用场景&#xff1a; 测试步骤的数据驱动&#xff1b;测试数据的数据驱动&#xff1b;配置的数据驱动&#xff1b; 1、pytest结合数据…

ECharts散点图-气泡图,附视频讲解与代码下载

引言&#xff1a; ECharts散点图是一种常见的数据可视化图表类型&#xff0c;它通过在二维坐标系或其它坐标系中绘制散乱的点来展示数据之间的关系。本文将详细介绍如何使用ECharts库实现一个散点图&#xff0c;包括图表效果预览、视频讲解及代码下载&#xff0c;让你轻松掌握…

高强度螺栓等级划分

高强度螺栓的等级划分主要依据其性能等级&#xff0c;常见的等级有8.8级和10.9级。这些等级标号由两部分数字组成&#xff0c;分别表示螺栓材料的公称抗拉强度值和屈强比值。 8.8级高强度螺栓&#xff1a;表示螺栓杆的抗拉强度不小于800MPa&#xff0c;屈强比&#xff08;屈服强…

WEB UI 创建视图

1 视图名称 (点第1创建视图) 2 模型节点 可以空 3 上下文节点 4 新增节点下的属性 &#xff0c;参考结构(先建好的结构) 5 选择视图类型&#xff1a;&#xff08;表单&#xff0c; 列表&#xff09; 表单 &#xff1a;单条数据 列表 &#xff1a;多条数据&#xff08;表格…

linux-19 根文件系统(一)

之前提到过&#xff0c;linux的目录是一个倒置的树&#xff0c;它通过层次性的方式来组织&#xff0c;管理整个系统的文件&#xff0c;而这本身实际上是通过文件系统。文件系统&#xff0c;大家记得文件系统是内核的主要功能之一&#xff0c; 它的主要目的就是实现本机上的某一…

四种电子杂志制作软件

​大家好&#xff0c;今天给大家种草四种超级实用的电子杂志制作软件。无论你是专业出版人士&#xff0c;还是业余爱好者&#xff0c;这四款软件都能帮助你轻松制作出精美的电子杂志。让我们一起来看看吧&#xff01; 1.FLBOOK FLBOOK是一款在线仿真翻页制作H5电子画册&#x…

webserver log日志系统的实现

参考博客&#xff1a;https://blog.csdn.net/weixin_51322383/article/details/130474753 https://zhuanlan.zhihu.com/p/721880618 阻塞队列blockqueue 1、阻塞队列的设计流程是什么样的 它的底层是用deque进行管理的 阻塞队列主要是围绕着生产者消费者模式进行多线程的同步和…

kkfileview代理配置,Vue对接kkfileview实现图片word、excel、pdf预览

kkfileview部署 官网&#xff1a;https://kkfileview.keking.cn/zh-cn/docs/production.html 这个是官网部署网址&#xff0c;这里推荐大家使用docker镜像部署&#xff0c;因为我是直接找运维部署的&#xff0c;所以这里我就不多说明了&#xff0c;主要说下nginx代理配置&am…

《Vue3实战教程》5:响应式基础

如果您有疑问&#xff0c;请观看视频教程《Vue3实战教程》 响应式基础​ API 参考 本页和后面很多页面中都分别包含了选项式 API 和组合式 API 的示例代码。现在你选择的是 组合式 API。你可以使用左侧侧边栏顶部的“API 风格偏好”开关在 API 风格之间切换。 声明响应式状态…

EasyExcel停更,FastExcel接力

11月6日消息&#xff0c;阿里巴巴旗下的Java Excel工具库EasyExcel近日宣布&#xff0c;将停止更新&#xff0c;未来将逐步进入维护模式&#xff0c;将继续修复Bug&#xff0c;但不再主动新增功能。 EasyExcel以其快速、简洁和解决大文件内存溢出的能力而著称&#xff0c;官方…

深入解读数据资产化实践指南(2024年)

本指南主要介绍了数据资产化的概念、目标和意义&#xff0c;以及实施数据资产化的过程。指南详细阐述了数据资产化的内涵&#xff0c;包括数据资产的定义、数据资产化的目标与意义&#xff0c;并介绍了数据资产化的过程包括业务数据化、数据资源化、数据产品化和数据资本化。 …

广州大学计算机组成原理课程设计

一.课设性质&#xff0c;目的&#xff0c;任务 《计算机组成与系统结构课程设计》是计算机学院各专业集中实践性环节之一&#xff0c;是学习完《计算机组成与系统结构》课程后进行的一次全面的综合练习。其目的是综合运用所学计算机原理知识&#xff0c;设计并实现一台模型计算…

MimicBrush:智能图像编辑新宠,能否革新你的创意设计?

一、介绍 MimicBrush 是一款由阿里巴巴和香港大学联合研发的图像编辑工具&#xff0c;它通过模仿参考图像&#xff0c;对目标图像选定区域进行自动局部编辑。以下是关于 MimicBrush 的详细介绍&#xff1a; 1.技术特点 智能匹配 &#xff1a;利用尖端 AI 技术&#xff0c;Mi…

QT用Enigmavb 打包成单独exe

QT用这个工具打包成单个exe&#xff0c;然后再用winrar打包成zip可以发给别人 在之前需要用QT的release打包 之前的文章QTrelease打包【非单个exe】 Enigmavb 打包流程&#xff1a; 安装过程&#xff1a; next-》i accept -》选择安装位置 -》next -》Create a desktop ic…

f(f(x))=x^2 -11x+36, 求f(6)的值,

偶然看到的一个题目&#xff0c;一时兴起&#xff0c;做了一下。题目如下 简单粗暴的思路是待定系数法&#xff0c;盲猜f(x)是个2次函数&#xff0c;令f(x)ax^2bxc ,带入原式&#xff0c;发现矛盾&#xff08;计算略&#xff09;就想放弃了。 忽然看到如果带入6 的话&#xf…

华为浏览器(HuaweiBrowser),简约高效上网更轻松

华为浏览器是一款由华为公司自主研发的网页浏览工具&#xff0c;凭借其独特的设计理念和优质的用户体验&#xff0c;正在吸引越来越多的用户关注。这款基于Chromium技术打造的浏览器不仅继承了Chrome的高性能特质&#xff0c;更融入了华为自身的创新元素&#xff0c;为用户打造…

Ubuntu下ESP32-IDF开发环境搭建

Ubuntu下ESP32-IDF开发环境搭建 文章目录 Ubuntu下ESP32-IDF开发环境搭建一、前言二、软件安装三、开发环境搭建3.1 ESP-IDF安装&#xff1a;3.2 安装编译工具&#xff1a; 四、编译并烧录代码五、ESP32代码编辑工具 一、前言 ​ 开发ESP32&#xff0c;我们首先就要安装开发环…

修炼内功之函数栈帧的创建与销毁

修炼内功之函数栈帧的创建与销毁 一 前置知识&#xff08;1&#xff09;栈&#xff08;2&#xff09;相关寄存器和汇编指令 二 函数栈帧三 代码演示函数栈帧的创建&#xff08;1&#xff09;代码演示&#xff08;2&#xff09;函数栈帧逐帧分析 四 对开篇问题的解答 相信来CSDN…

学习threejs,THREE.PlaneGeometry 二维平面几何体

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;threejs gis工程师 文章目录 一、&#x1f340;前言1.1 ☘️HREE.PlaneGeometry 二维平…