首先获取很多网页(爬虫->一个http客户端,发送http请求获取http响应结果(就是网站))(批量化的获取很多的页面)
再根据用户输入的查询词,在网页中进行查找
用户输入查询词之后,如何让查询词和当前这些网页进行匹配
->使用倒排索引
倒排索引
1.文档: 每个待搜索的网页(被爬虫技术,把各个网站的各个网页搜集在一起,然后保存到服务器上)
2. 正排索引: 文档Id(身份标识)-> 文档内容(包含很多段落,句子,词..)
3. 倒排索引: 词-> 文档id列表(包含词)
key和value不一样
项目目标: 实现一个针对Java文档的搜索引擎
百度是属于"全站搜索",搜索震哥哥互联网上的网站
B站是属于"站内搜索".只针对某个网站的内容进行搜索
把相关的文档获取到,才能制作正排索引和倒排索引
我们直接从官网上下载文档的压缩包即可
离线文档和在线文档的路径都是对应的, 我们可以根据离线文档的路径直接构造出在线文档的路径
模块划分
1> 索引模块
2> 搜索模块
3> web模块
分词的概念
把一个完整的句子,切分为多个词
基于现成的第三方库来实现分词
分词的实现原理
1. 基于词库
2. 基于统计: 某俩个字在一起使用的概率很大,此刻我们就可以把它们标注为一个词
单身狗这种在词库里基本找不到,因为是网络造的词
实现分词的第三方库: ansj
<dependency>
<groupId>org.ansj</groupId>
<artifactId>ansj_seg</artifactId>
<version>5.1.6</version>
</dependency>
分词的使用
实现索引模块
Parser: 读取下好的文档,然后解析文档内容,完成索引的制作
一个文件的扩展名,都是随便起的,一般代表文件的格式
解析获取到的html
1> 解析html标题
我们要想办法把html后缀名给去除
去除
完成
java计算长度
2> 获取url
广告结果会根据点击计费
自然搜索结果,需要根据点击来优化用户体验
Java API 文档,有俩份
1. 线上文档
2. 线下文档
我们希望用户点击线下文档的搜索结果,就能够跳转到线上的文档页面
具体代码实现
有/和\,但是因为浏览器的容错能力很强 ,因此是每关系的
3> 解析正文
我们只关心内容,不要html标签
我们需要去掉标签
此时我们可以用正则表达式来去标签,正则表达式是一种计算机中进行字符串处理/匹配 常用的字段
核心就是通过一些特殊符号来描述字符串的特征, 然后看某个字符串是否符合这些特征
此时我们使用更加简单的方式来实现
html都是<>包裹起来的
如果找到<直到>为止,就不把它放在内容里面(使用StringBuilder)
关于读取字符的操作
针对这个,我们发现有很多的换行操作,目的是为了生成描述信息,不应该有很多的换行(内容本身不应该有换行,此时要去除换行
解决方法: 多加一个判断语句
小总结:
构建正排序索引和倒排索引
索引大小就是设置的DocId
正排索引实现: id ->doc对象
倒排索引: 词->文档id
需要进行分词
权重值: 描述词和文档的相关性,我们此时通过词出现的次数表示相关性
计算权重的公式: 标题1次抵文章出现10次
迭代这些公式的方法
"小流量实验"
是否忽略大小
梳理倒排索引的步骤
什么时候使用for each
构建文档出现的问题
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.0</version>
</dependency>
json格式
反序列化: 把串转为对象
创建匿名内部类的实例的目的
计算保存和加载的时间
paeser和Index的关系
生成文件的数据
衡量加载时间
性能优化
找到性能瓶颈(那个部位耗时最高)
说明遍历的耗时最多
把刚刚单线程的代码改成多线程
单线程: 18秒
提交任务操作很快,保存所有可能会有线程安全的问题
分析过程
保证在save之前把任务都处理完成
考虑线程安全: 多个线程是否同时使用公共的东西,尝试修改同一个对象, add里面有操作公共的对象
索引大小....
此处使用加锁的方式来解决线程安全问题
修改版本
我们分别操作正排和倒排是不会产生锁竞争的,因此我们不需要对整个index对象进行枷锁,我们对各种的正排和倒排进行加锁即可,让并发最大化
最终结果:使用八个线程来执行,只要7秒
放多少个线程合适?
细节问题
1> 守护线程
通过线程池创建的线程, 不是守护线程
当main方法执行完了,这些线程任然在工作(任然在等待)
程序依然在执行(进程没有退出)
进程顺利结束
2> 第一次制作索引的时候,速度很慢,第二次很快.关机后再第一次制作, 又会很慢..
主要是parsContent里面有读文件的操作,开销很大
猜想,是否开机之后,首次运行读取文件速度特别慢?
我们第一次运行
后面再运行
差距很大,主要是缓存的问题
解决从磁盘读文件速度慢的问题,
提前使用缓存区, 把数据进行缓存
验证索引加载逻辑
小结:
实现Parse类
实现Index类
核心方法
实现搜索模块
调用搜索模块,来搜索的核心操作
1. 分词
2. 触发
3. 排序
4. 包装结果
构造摘要: 包含正文的查询词或者查询词的一部分
生成的思路: 获取所有的查询词的分词结果, 遍历分词结果, 看哪个结果在正文中出现
正文转小写
还有个问题
因此在生成描述的时候,要找到整个词都是匹配的,而不是一个词的一部分,保证word在内容中是独立的一个词
实现
我们的HTML里面js代码,因此去标签后,js也在倒排索引里面了
去除js里面的内容,使用正则表达式, 特殊字符串 描述了一些匹配规则
. 表示匹配一个非换行字符
*表示前面的字符可以出现若干次
去掉script标签和内容,去除普通的标签(不去掉内容)? 表示非贪婪匹配: 匹配到符合条件的最短结果
结果空格太多 了
我们依然使用正则,把多个空格合并为一个空格
然后进行搜索测试,发现没有js的代码了
小结:
实现了search方法
实现Web模块
提供一个web接口, 最终以网页的形式,把我们的程序呈现给用户
前端+后端
约定前后端的接口
此处我们只需要实现搜索接口即可
首先基于sevlet来进行实现
引入依赖
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
search里面是当前搜索引擎的核心代码逻辑, 构造索引, 实现搜索逻辑
api里面实现的是sevlet的代码
实现前端页面,实现搜索页
使用ajax前后交互的常用手段
拼接传过来的数据
对应进行拼接
出现的一些问题
针对内容太多,超出了屏幕
点击后, 搜索结果页被目标页面替换问题:在a标签那里设置_blank属性
俩次搜索结果混合在一起的问题
把内容的分词进行标红
1. 修改后端代码, 把查询词加上标记,套上<i>标签
2. 针对<i>设置样式,比如给<i>字体颜色设置红色
500异常
当 array list的时候,分词结果还加上了空格,因此会搜出很多其他没有用的东西
此处使用暂停词: 高频,但是没有意义的内容
停用词是哪些?
把暂停词表加载到内存里面
然后读取到hashset里面,然后遍历我们的分词,然后使用contains方法,判断在不在暂停词的表里面
此刻其实还有问题, 就是边界问题,我们之前是 " "查询词" "来保证分词的独立性,但是 查询词. 并没有排除,此时还是使用正则表达式\b
indexOf 不支持正则,但是replaceAll支持, 我们把所有的\\b都替换成空格,然后我们再进行查找
然后package-use就成功把array)给标红了
显示搜索结果的条数
我们修改前端
然后我们发现,同一个文档可能会出现俩次(一个文档既有array又有list)
这个就要和之前算权重的公式有关
去重,权重的合并步骤
分析俩路归并
分析多路归并
建立堆: 完全二叉树,小根堆
权重合并
先从若干行拿到最小的值,然后和上次插入的值一样,然后判断是不是一个文档,是一个文档就进行权重的合并,不是就进行插入
最后搜索结果
总结