搜索引擎
- 标题
- 描述
- 展示url
跳转结果称为 **落地页
- 获取无数网页 > 爬虫程序 发送无数Http请求,相当于一个HTTP的客户端
- 为了提高查询速度 :查询词和当前网页进行匹配 则需要 倒排索引 \color{red}{倒排索引} 倒排索引这个特殊的数据结构
- 正排索引:文档id -> 文档内容
- 倒排索引:词 -> 文档id列表
一个针对java文档的搜索引擎
- 全站搜索 对整个互联网上所有的网站
- 站内搜索 只针对某给网站内部内容进行搜索
基于本地的离线文档制作索引,用户在搜索结果页面点击具体的搜索结果的时候,自动跳转到在线文档上
模块划分:
- 索引模块
- 扫描下载的文档,并分析,构建正排索引+倒排索引,并把索引保存到文件中
- 加载索引,提供API
- 搜索模块
实现搜索的完整过程 - 实现简单的web程序,用网页的形式和用户进行交互
对于用户输入的内容,往往是一句话。但在搜索时,可以看到现有的搜索引擎是会根据句子中的词语进行网页匹配的,所以此软件也需要实现分词功能。 但分词是较为复杂的,尤其是像中文这样的语言,所以为了简便起见,也为了是该软件具有一定的可扩展性,采用第三方库实现分词。(这也更接近实际的开发)
分词的原理
- 基于词库
把所有“词”进行穷举,放入一个词典中,之后根据句子进行对应查询即可 - 基于统计
收集海量的“语料库” 比如已有的文章、资料。 > 人工标注分词结果/直接统计 > 哪些字频繁连接使用
ansj库可以实现分词* 支持中英文
ansj会加载一些词典文件,通过词典文件能够加快分词速度和准确率 (词典中可以包含最新的词语)
没这些词典文件,依然可以顺利运行
Parser类###
实现制作索引的过程:
读取下载的文档,并进行解析,完成索引的制作
最后展示的效果是标题、描述、URL
-
其中描述可认为是正文的一个摘要,直接得到描述是困难的,先得到正文,再进后续过程
完整的HTML文件=HTML标签+内容,所以去掉标签,就是内容。
去标签:即去除<……>,在HTML里,如果内容中出现<>,则分别转义为< less than it
> greater than it -
标题在实际的搜索引擎中,也是一个超链接,即是一个a标签,在每个搜索结果的底部又会显示 一个URL,这两个是不一样的,但在此项目中为了方便起见,设置为相同的。
期望的结果:用户点击搜索结果,就能跳转到对应的线上文档的页面。为此修改本地的URL为线上文档的URL,即进行字符串拼接
在线文档的URL为 https://docs.oracle.com/javase/17/docs/api/
因为搜索引擎会统计网页被点击的次数,所以标题对应的URL一般会与搜索引擎有关
Index类
为了实现快速搜索,就需要有相应的索引
索引
常用的就是正排索引和倒排索引,设计的数据结构在被查询时最好复杂度只有O(1)
所以正排索引用ArrayList,倒排索引用哈希
倒排索引的构建的步骤:
首先得知道 某个文档中有哪些词,即分词。
1. 对标题进行分词
2. 对内容(即正文)进行分词
根据分词的结果,将文档加入相应的倒排索引的value中 ,其中weight 简单处理为词出现的次数 标题中出现的词的权重理应高于正文中出现的词。实际中搜索引擎通过点击率衡量weight,点击率为 点击次数/展示次数
索引的构造过程是十分耗时的,因为设计了很多的运算,单个索引构造的时间虽然不长,但索引本身的数量是十分庞大的。所以同样的索引不可能反复构造。应该构造好了存入磁盘中。也就是是说,服务器每次启动时,不同时构建索引,而是直接从硬盘中加载已经构造好的索引。否则服务器启动是极慢的。构造的过程是单独执行的。
把索引文件以字符串的形式进行保存,即序列化
将字符串解析为特定结构的数据,即反序列化
借助JSON格式实现
在加载的时候,借助了Jackson中的ObjectMapper里的readValue方法。
forwardIndex = objectMapper.readValue(forwardIndexFile, new TypeReference<ArrayList<DocInfo>>() {});
forwardIndexFile 指定带读取的文件,
new TypeReference<ArrayList<DocInfo>>() {} 表明读到的数据按照什么类型进行解析
要将字符串转换为ArrayList<DocInfo>类型的对象 ,Jackson提供了一个辅助的工具类 TypeReference< ……>,这个类的泛型参数就是转换的目标类型。因为Java的语法规定,参数不能类型,只能是一个实例。 创建一个匿名内部类的实例,是为了传递类型信息。
衡量代码的执行时间的简单方式:
开始执行前,获得一个时间戳
执行结束后,获取一个时间戳
大数据的量级至少也在TB级
几十MB的文本文件,可以用记事本打开,但是容易卡死。按理说这么点数据全部加入内存是很快的,但记事本打开的效果极差。用VScode很好
通过实际运行,发现运行时间较长,所以要性能优化。
首先就要确定性能瓶颈是什么,也就是通过前后加时间戳这种简单的方式确定瓶颈段
此程序的性能瓶颈为循环遍历文件上,
每循环一次,就对一个文件进行处理:读文件 + 分词 + 内容解析 主要还是CPU的运算占据很多时间。
单个线程的情况,这些任务都是串行执行的,所以要考虑并发编程
IDEA在控制台展示的内容是来自内从中的一个缓冲区的,有一定大小,要显示的太多,位于前面的内容就会丢弃
搜索模块###
调用索引模块,完成搜索
- 分词,针对用户输入的查询词进行分词
- 触发,对每个分词结果,在倒排索引中查询,找到相关文档
- 排序,针对上述触发的结果,按照相关性降序排序
- 包装结果,根据排序结果,依次去查正排,获取每个文档的详细信息,并按照一定的数据结构进行返回
描述 是正文的一段摘要,需要包含查询词/查询词的一部分
对于某个文档来说,不一定包含所有的分词结果
在文档中找到分词结果的位置,以这个为中心,向前30字符,向后30字符,作为整个描述
Web模块
约定前后端的的结构
请求
GET /searcher?query=[查询词] HTTP/1.1
响应
HTTP/1.1 200 OK
[
{
title: “标题1”,
url: “url1”,
desc:“描述1”,
}
]
为了实现前端展示上,查询词会被标红,则在后端对于查询词加上标记,套上<i>标签,在前端对<i>设置样式即可
之前的分词导致暂停词也成为了倒排索引的查询对象
暂停词:停用词 即高频 但 不具备查询意义的词
a is have 等
之后是搜索程序加载停用表,针对分词结果,在停用词表中进行筛选,如果在就干掉
多个分词结果被同一个文档包含,则表明该文档的相关性更高,则Weight权重更大
采用merge-sort ,按照docID升序排序,之后合并
为了找打最小值,使用堆排序
正排索引,倒排索引,暂停词三个文件
总结#
1. 索引模块###
- Pareser 类:制作索引的流程
- Index 类 : 实现索引的树结构
2. 搜索模块###
Sercher 类 : 完成搜索的流程
1. 调用Index 类 ,实现正排倒排的查询
2. 生成描述、标红、文档合并(去重,合理利用权重)等功能,使得展示更合理
3. Web模块###
Servlet/Spring Boot 两个后端版本的服务器程序
HTML/CSS/JS 搜索界面