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

news2024/12/25 2:29:11

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

主要步骤

简化版本的逻辑

  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.coloradmin.cn/o/2265027.html

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

相关文章

帝国cms灵动标签调用相关文章

帝国cms相关文章调用的方法很多&#xff0c;官方默认调用方法是使用标签 [!--other.link--] 那么内容页调用相关文章&#xff0c;什么时候用到灵动标签呢 答案是调用同类型的文章&#xff0c;比如有相同关键词或者相同tags标签的文章 使用灵动标签是如何调用的呢&#xff0…

低空经济的地理信息支撑:构建安全、高效的飞行管理体系

随着无人机等低空飞行器的广泛应用&#xff0c;低空空域管理的重要性日益凸显。地理信息技术作为低空空域管理的重要支撑&#xff0c;对于保障低空经济的健康发展具有不可替代的作用。 地理信息技术在低空空域管理中的作用 地理信息技术在低空空域管理中扮演着关键角色&#x…

一体式IO模块:打印机加工产线国产化降本增效的新利器

在当今全球化的市场竞争中&#xff0c;打印机制造行业面临着前所未有的挑战与机遇。为了提升生产效率、降低成本&#xff0c;并加速国产化进程&#xff0c;各大打印机制造商纷纷寻求技术创新与升级。明达技术自研推出的MR20一体式IO模块作为工业自动化领域的核心组件&#xff0…

「下载」智慧产业园区-数字孪生建设解决方案:重构产业全景图,打造虚实结合的园区数字化底座

数字孪生技术作为一种创新的管理工具&#xff0c;正逐步展现出其在智慧园区建设中的重要意义。以下将从几个方面详细阐述数字孪生在智慧园区建设中的关键作用。 一、提升园区运营管理的智能化水平 数字孪生技术通过构建园区的虚拟镜像&#xff0c;实现了对园区物理世界的全面…

ICASSP 2025 中了 ,不去印度参会就撤稿?

近日&#xff0c;ICASSP 2025正式放榜&#xff0c;身边很多同学都收到了好消息&#xff0c;虽然目前官方还没有给出正式的数据&#xff0c;但据知情人士透露&#xff0c;今年的投稿编号超过8000&#xff0c;可以说是创造了历史新高&#xff0c;预测整体录取率在46%左右。 根据…

深度学习之目标检测——RCNN

Selective Search 背景:事先不知道需要检测哪个类别,且候选目标存在层级关系与尺度关系 常规解决方法&#xff1a;穷举法&#xff0c;在原始图片上进行不同尺度不同大小的滑窗&#xff0c;获取每个可能的位置 弊端&#xff1a;计算量大&#xff0c;且尺度不能兼顾 Selective …

LOS/NLOS环境建模与三维TOA定位,MATLAB仿真程序,可自定义锚点数量和轨迹点长度

本代码的主要功能是建模 LOS&#xff08;视距&#xff09;和 NLOS&#xff08;非视距&#xff09;环境下的定位系统&#xff0c;估计目标的动态位置&#xff0c;三维空间 文章目录 运行结果源代码代码介绍 总结 运行结果 10个点的轨迹定位&#xff1a; 50个点的轨迹定位&#…

企业内训|高智能数据构建、Agent研发及AI测评技术内训-吉林省某汽车厂商

吉林省某汽车厂商为提升员工在AI大模型技术方面的知识和实践能力&#xff0c;举办本次为期8天的综合培训课程。本课程分为两大部分&#xff1a;面向全体团队成员的AI大模型技术结构与行业应用&#xff0c;以及针对技术团队的高智能数据构建与Agent研发。课程内容涵盖非结构化数…

专业的内外网数据交换方案 可解决安全、效率、便捷3大问题

内外网数据交换是很多企业和行业都会面临的场景&#xff0c;既然隔离了内外网&#xff0c;重中之重就是要确保数据的安全性&#xff0c;其次在数据流转交换过程中&#xff0c;不能太繁琐复杂&#xff0c;需要让用户快速、便捷的进行数据交换。首先我们来看看&#xff0c;在进行…

【linux】NFS实验

NFS服务 NFS是Network File System&#xff08;网络文件系统&#xff09;的缩写。它是一种用于在计算机网络中共享文件和目录的协议。NFS允许计算机上的用户访问和操作远程计算机上的文件&#xff0c;就像访问本地文件一样。它使用客户端-服务端模型&#xff0c;其中客户端机器…

2024年Python最新下载安装教程,附详细图文,持续更新

大家好&#xff0c;我是Python老安&#xff0c;今天为大家带来的是Windows Python3下载、安装教程&#xff0c;适用于 Python3 所有版本&#xff0c;包括 Python3.7,Python33.8,Python33.10 等版本。希望对大家有所帮助 Python目前已支持所有主流操作系统&#xff0c;在Linux,…

职业技能赛赛后心得

这是一位粉丝所要求的&#xff0c;也感谢这位粉丝对我的支持。 那么本篇文章我也是分成四个部分&#xff0c;来总结一下这次赛后心得。 赛中问题 那么这里的赛中问题不会只包含我所遇到的问题&#xff0c;也会包含赛中其他选手出现的问题。 那么首先我先说一下我在赛中遇到的…

WordPress File Upload 插件 任意文件读取漏洞复现(CVE-2024-9047)

0x01 产品简介 WordPress File Upload插件是一款功能强大的WordPress站点文件上传插件,它允许用户在WordPress站点中的文章、页面、侧边栏或表单中轻松上传文件到wp-contents目录中的任何位置。该插件使用最新的HTML5技术,确保在现代浏览器和移动设备上都能流畅运行,同时也…

没想到互联网大厂都喜欢问MySQL中的数据类型?

&#x1f3a5; 作者简介&#xff1a; CSDN\阿里云\腾讯云\华为云开发社区优质创作者&#xff0c;专注分享大数据、Python、数据库、人工智能等领域的优质内容 &#x1f338;个人主页&#xff1a; 长风清留杨的博客 &#x1f343;形式准则&#xff1a; 无论成就大小&#xff0c;…

YOLOv8 引入高效的可变形卷积网络 DCNv4 | 重新思考用于视觉应用的动态和稀疏算子

我们介绍了可变形卷积v4(DCNv4),这是一种为广泛的视觉应用设计的高效且有效的算子。DCNv4通过以下两项关键改进解决了其前身DCNv3的局限性: 在空间聚合中移除softmax归一化,以增强其动态特性和表达能力。优化内存访问,减少冗余操作以提高速度。这些改进使得DCNv4相比DCNv…

入侵他人电脑,实现远程控制(待补充)

待补充 在获取他人无线网网络密码后&#xff0c;进一步的操作是实现入侵他人电脑&#xff0c;这一步需要获取对方的IP地址并需要制作自己的代码工具自动化的开启或者打开对方的远程访问权限。 1、获取IP地址&#xff08;通过伪造的网页、伪造的Windows窗口、hook&#xff0c;信…

C++中的字符串实现

短字符串优化(SSO) 实现1 实现2 写时复制 #define _CRT_SECURE_NO_WARNINGS #include<iostream> #include<cstdio> #include<cstring> #include<cstring> using std::cout; using std::endl;// 引用计数存放的位置 // 1. 存放在栈上 --- 不行 // 2. 存…

clickhouse解决suspiciously many的异常

1. 问题背景 clickhouse安装在虚拟机上&#xff0c;持续写入日志时&#xff0c;突然关机&#xff0c;然后重启&#xff0c;会出现clickhouse可以正常启动&#xff0c;但是查询sql语句&#xff0c;提示suspiciously many异常&#xff0c;如图所示 2. 问题修复 touch /data/cl…

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

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

Y3编辑器教程7:界面编辑器

文章目录 一、简介1.1 导航栏1.2 画板1.3 场景界面1.4 控件1.4.1 空节点1.4.2 按钮1.4.3 图片1.4.4 模型1.4.5 文本1.4.6 输入框1.4.7 进度条1.4.8 列表 1.5 元件1.5.1 简介1.5.2 差异说明1.5.3 元件实例的覆盖、还原与禁止操作1.5.4 迷雾控件 1.6 属性1.7 事件&#xff08;动画…