• 从0基础开发搜索引擎(一)-----实战项目


    项目目标:实现一个JavaAPI文档的站内搜索引擎

    我们有一个网页,上面带有一个搜索框,用户输入查询词之后进行搜索,将会在服务器上面搜索出与查询词相关的文档,并把这些信息返回到页面上面,用户点击搜索结果,就可以跳转到具体的详细页面

    1.认识搜索引擎:

    1)搜索引擎主页有一个搜索框,我们在搜索框里面进行输入的内容,我们称之为查询词(可以是一个词,两个词,还可以是一段话)

    2)还有搜索结果页,里面包含了若干个搜索结果

    3)针对每一个搜索结果,带有颜色的,它一般都会包含查询词或者查询词的一部分或者和查询词具有一部分的相关性

    下面是搜索结果的一部分: 在浏览器上面输入土巴兔

    4)标红的部分都是查询词或者是查询词的一部分,或者是带有一部分的相关性(搜索土巴免)

    5)每一个搜索结果都包含了好几个部分:

    5.1标题(必有)(带有官网的那一部分文字)

    5.2描述(通常是页面的摘要信息)(必有),对于查询词的解析信息,(介绍土巴兔的详细信息)

    5.3子链接:像上面图片中的装修,一站式装修,这些都是一个a标签,点击之后都会有反应

    像什么北京,登录

    5.4展示URL(必有):点击不跳转,上面的www.to8to.com - 品牌广告

    5.5图片(可爱的小兔子)

    5.6点击URL(在标题下面里面),进行点击URL,浏览器将会跳转到落地页,就是搜索结果对应的一个页面(必有)-----标题下面有一个带颜色的横线;

    但是我们知道,一定是有标题,描述,展示url还有点击url

     2.搜索引擎的功能:

    1)搜索引擎的功能,就是搜索,也叫做查找,查找用户的输入的查询词在哪些网页中出现过,或者说出现过一部分,我们就可以把结果展示到网页上面,我们进行点击结果就可以跳转到该页面,也就是详情页;

    2)但是我们浏览器来如何获取到网页数据信息呢?

    2.1)自然搜索结构:网页数据通常是通过爬虫

    2.2)广告搜索结构:广告主把物料提供给广告平台,比如说我们在浏览器上面输入的不孕不育,就会在网页开头有大量的广告会显示出来,这里面的广告主就是医院,这里面的广告平台就是搜狗,对于搜索广告来说,每一次用户点击就会计费,广告主(医院)会给广告平台(搜狗,也称之为制作搜索引擎的人)

    1)像百度,搜狗这样的搜索引擎,进行全网搜索(整个互联网上所有的页面他都可以获取到),那么进行处理的数据量集时十分庞大的,现在咱们是不具有搞全网搜索的条件的,因为我们没有那么多服务器,我们可以搞一个数据量比较小的站内搜索,咱们现在所做的搜索引擎,叫做Java API文档

    2)但是我们为什么要用JavaAPI文档,因为官方文档上面没有一个好用的搜索框,况且JavaAPI文档数量比较少,当前有限的硬件资源足够进行处理,况且文档内容不需要使用爬虫来进行获取,我们可以直接在官网上面进行下载(例如说我想要找一下String类的用法,我就必须先要找java.lang这个包,再去找String类,就显得十分的麻烦

    1)官方文档上面没有一个好的搜索框

    2)Java API文档数量比较少,也就只有几万个页面,当前的硬件资源已经足够处理;

    3)文档内容不需要使用爬虫来进行获取,我们只需要在官方文档上面进行下载即可

    3.搜索引擎是如何进行工作的?

    当我们有了倒排索引之后,我们就可以根据用户进行输入的查询词就可以找到相关联的文档

    1)搜索引擎绝对不是一个简单的小网页,我们需要在很多的网页数据中查询到你进行输入的查询词(部分查询词)

    2)搜索引擎后台此时已经获取到了很多很多的网页数据,每一个网页数据又是一个个的HTML,都是一个一个文档,每一个文档都是一个页面,我们要进行的工作就是想知道查询词在那些文档里面出现过

    解决方案:

    1)暴力搜索,依次遍历每一个文件,来进行查看当前文件中是否包含查询词,因为文档数目是非常多的,依次进行遍历的话效率就会非常的低效,因为搜索引擎针对时间效率上面的要求是非常高的(在几秒内或者几毫秒内返回大量的数据)

    2)倒排索引:这是一种特殊的数据结构(Key是字符串,Value就是List)

    1. 假设现在有两个网页,也就是有两个文档:
    2. docID:1这个docID对应的文档内容是:乔布斯发布了苹果手机
    3. docID:2这个docID对应的文档内容是:乔布斯卖了四斤苹果
    4. 我们所说的正排索引,就是说根据docID来进行查询到我们的文档内容

    我们想要根据正排索引来进行制作倒排索引,首先我们就要根据文档内容进行分词操作

    分词操作就是在这个文档中都有哪些词,我们来进行分开,在文档为docID=1的文档中文档内容是,乔布斯发布了苹果手机,我们就根据这个文档来进行分词操作:

    1. 词语 倒排索引
    2. 乔布斯 1
    3. 发布 1
    4. 1
    5. 苹果 1
    6. 手机 1

    在上面的语句模块中,我们又重新的建立了一个映射关系,这个映射关系是从词语到文档ID建立了一个映射关系,正排索引是根据文档ID来进行查找到文档的内容,而倒排索引就是根据这个词的内容信息来找到在这个词哪些文档ID中存在过

    针对文档一和文档二来进行综合分析,刚才我们针对文档一进行分词了,现在我们来进行针对以下文档二来进行分词:

    1. 词语 倒排索引
    2. 乔布斯 1 2 //乔布斯在文档1和文档2中都出现过
    3. 发布 1
    4. 了 1 2
    5. 苹果 1
    6. 手机 1
    7. 买 2
    8. 四斤 2

    搜索引擎的处理过程:

    1.根据所有文档内容构建倒排索引

    一:根据我们的正排索引来进行遍历(遍历docID来查找到所有文档内容),对查询到的每一个文档内容来进行分词,放到一个HashMap里面:

    比如说取出了文档1,分成了若干个词,我们遍历我们创建一个HashMap,Key对应String,value对应List,发现我们的词和String中的值相同,我们就把这个词对应的docID给追加到list里面(如果此时文档中的这一个词在我们指定的HashMap中没有出现过,那么我们就把这个put("这个词",这个词出现的第一次的docID)----这是第一次进行插入

    此时如果说我们在搜索框里面输入了乔布斯,先去查找倒排索引,查询到文档ID,那么我们需要最终给前端返回的文档就包括文档1和文档2,我们此时再根据文档ID来进行查询到对应的文档信息

    2.整个搜索过程:当我们有了倒排索引之后,根据用户的查询词就可以快速查询到相关联的文档

    1)比如说我们在输入框里面输入小明爱吃苹果,此时我们先对查询词进行分词,小明/爱吃/苹果,然后我们根据我们的分词结果依次在倒排索引(搜索之前已经制作好了)当中进行查找

    这时就查找苹果这个词就在我们的哈希表里面,我们根据"苹果“这个词就可以根据Key查找到对应的文档ID是1,这个时候我们就成功地根据了文档内容来进行查询到了对应的docID;

    2)因为我们此时返回给前端的是一个文档,我们再根据刚才的docID来成功的进行查询到了文档内容,我们就可以把这个文档直接返回给前端了;

    3)创建倒排索引就需要先遍历正排索引

    4)我们可以把文档ID抽象成数组下标,一个文档就抽象成数组里面的元素,正排索引就是根据数组下标来进行查询数组里面的值,倒排索引就是根据数组里面的值来进行查询数组下标;倒排索引:key:String,value:list

    比如说String="乔布斯",value=list(1,2)-----说明乔布斯在文档1和文档2中都出现过

    3.实现分词的基本思路:这个搜索框里面分成几个有意义的词

    1)基于词典,计算机把所有可能成词的词语放到一个词典里面,大约有几十万个词,后面我们来进行分词的时候,计算机就看看这两个字是否可以组合出现在词典里面,进行查询词典

    2)基于概率,我们要统计很多的文本信息,哪两个,三个,四个汉字出现在一起组成的词语的概率比较高,大概率就说明是一个词

    当前商业公司分词库基本都能达到99%以上的准确率,分词算法一般都是公司的商业机密,我们可以使用现成的第三方库,虽然不会太准确,但是也够用;

    4.我们此次的项目目标:实现一个JavaAPI文档的站内搜索引擎,JavaAPI有一个线上版本,还有一个可以进行离线下载的版本

    下载好线下API文档,分析里面的内容,我们在搜索结果中填写上若干线上文档结果的链接地址,相当于我们搜索关键词之后,一进行点击,我们就可以找到若干个搜索结果,我们点击其中一个搜索结果,我们就可以进行跳转到线上文档的链接里面了(直接跳转到线上文档)

    1)线上文档链接:https://www.oracle.com/cn/java/technologies/javase-jdk8-doc-downloads.html
    2)在我们的线下文档链接里面:我们直接点击docs里面的api里面的index.html,就可以和线上文档一模一样了;
    在我们的线下文档里面:我们点击api里面的java里面的util里面就有很多的API接口
    3)我们的线上文档的链接地址是:https://docs.oracle.com/javase/8/docs/api/index.html

    线下文档的生成路径:

    1)我们假设想要从线下文档里面查询Collection的用法:

    C:\Users\18947\Downloads\jdk-8u341-docs-all \docs\api\java\util\Collection.html

    2)我们要想从线上文档里面进行获取到Collection的用法就下面:

    https:\\docs.oracle.com\javase\                         \docs\api\java\util\Collection.html

    3)我们发现这两个目录有一部分是相同的,后面我们进行分析线下文档的时候,我们就可以根据当前文档的路径来进行拼接出对应的线上文档的URL(我在D盘又进行复制了一份,叫做docs)

    5.项目的模块划分:

    1)预处理模块,我们把下载好的html文档(JavaAPI)进行一次初步的处理,简单分析结构并干掉这里面的html标签,最后成为一个行文本格式的文件,每一行对应着一个文档,使用\3来进行分割,包含了标题,URL和正文,并把这个文件转化成一个文档对象

    上面指的JavaAPI文档是指我们的docus目录下面api目录下面的所有文件,他们都要被转换成行文本的格式

    2)索引模块:我们将预处理得到的结果(行文本数据),构建正排+倒排索引

    3)搜索模块:我们进行完成一次搜索过程,从用户输入关键词,并得到最终的查询结果

    4)前端页面:有一个页面,展示结果并让用户输入数据

    6.预处理模块:

    1)我们把API里面的目录中的所有HTML进行处理,这样就可以得到一个单个的文件,我们使用行文本的方式来进行组织,为什么要得到一个单个的行文本呢呢?主要是为了我们在后续的时候做索引方便,直接按行读取就可以了;------>后续只要按行读取文件就可以了,制作倒排索引的时候不需要遍历文件就可以,直接读取一个文件

    2)我们得到的这个临时文件里面,每一行对应着一个HTML文档,每一行中又包含三列,第一列表示这个文档的标题,第二列表示这个文档的URL(对应的线上文档版本的url,根据本地的URL来进行拼接),第三列表示这个文档的正文(去掉HTML标签),因为每一个文档文件都是一个HTML,所以我们要先得到api目录下面的每一个HTML文件,然后再进行处理;

    3)当前我们的下载好的JavaAPI文档中的每一个文件用记事本打开都是一个HTML文件,包含着html标签,所以我们要把所有html标签都过滤掉,我们去除里面的html标签是为了让我们的搜索结果只集中到文档的正文上面,就不用关心什么body标签,head标签里面的内容,因为这些标签并不是网页正文的内容,所以我们把他们过滤掉;

    代码模块:

    0)下面开始进行创建maven项目写代码,我们在java目录下进行创建一个包叫做common包,里面创建一个类,叫做Document,这个Common包是存放各个功能模块可能用到的公共信息

    1)这一个Document类表示JavaAPI的一个文档对象表示一个HTML文档,里面包含了文档ID,文档的标题,对应线上文档的URL,还有文档正文;

    2)所以我们要根据线上文档,提取属性放到这个对象里面

    1. package common;
    2. //这个包底下存放一些各个功能模块的一些公共属性
    3. //这个类表示一个文档对象,只有有了这些内容我们才可以制作索引,完成搜索内容
    4. public class Document {
    5. private int DocID;//表示HTML文档的唯一身份标识,这是不可以重复的
    6. private String title;//该文档的标题,可以使用文件名来进行使用,Collection
    7. private String URL;//该文档对应的线上文档的URL,根据本地文件路径
    8. //可以拼接成先上文档的URL
    9. private String content;//表示该文档的正文,表示该文档的正文
    10. //把HTML文件里面的html标签去掉,从而留下的内容
    11. public int getDocID() {
    12. return DocID;
    13. }
    14. public void setDocID(int docID) {
    15. DocID = docID;
    16. }
    17. public String getTitle() {
    18. return title;
    19. }
    20. public void setTitle(String title) {
    21. this.title = title;
    22. }
    23. public String getURL() {
    24. return URL;
    25. }
    26. public void setURL(String URL) {
    27. this.URL = URL;
    28. }
    29. public String getContent() {
    30. return content;
    31. }
    32. public void setContent(String content) {
    33. this.content = content;
    34. }
    35. @Override
    36. public String toString() {
    37. return "Document{" +
    38. "DocID=" + DocID +
    39. ", title='" + title + '\'' +
    40. ", URL='" + URL + '\'' +
    41. ", content='" + content + '\'' +
    42. '}';
    43. }
    44. }

    3)我们在从java包底下创建一个包,叫做Pre,创建一个类叫做PreProcess类,我们用这个类遍历文档目录,读取所有的HTML文档内容,把结果解析成文本文件,每一行都包含着文档标题,文档的URL和文档的内容;

    1)但是此时我们发现,在JavaAPI目录下有很多级目录,每一个目录底下都包含着html文件,我们就要写方法实现来进行遍历这些HTML文件.

    1. public static List GetAllFile(File file,List list)
    2. {
    3. if(file==null||file.equals(""))
    4. {
    5. return;
    6. }
    7. File[] files=file.listFiles();//相当于是linux里面的命令ls,这就是把我们当前目录中的所有的文件都罗列出来了
    8. for(File f:files)
    9. {
    10. if(f.isDirecty()){
    11. GetAllFile(f, list);
    12. }else {
    13. //如果当前我们的f不是一个目录,而是一个文件,我们就查看当前文件的后缀名是不是.html,如果是,就把他加入到list里面
    14. if (f.getAbsolutePath().endsWith(".html")) {
    15. list.add(f);
    16. }
    17. }
    18. }
    19. return list;
    20. }

    我们在写循环递归的时候别忘了写终止条件

    当我们执行完这段代码之后,当前api目录下面的所有目录和文件就会全部被放到list里面了

    2)我们再进行遍历list中的每一个文件,把他进行设置成行文本的方式,给每一个文件设置成几个属性,标题+线上URL+文档内容,再把这里面的每一个部分放到一个文件里面的一行

    进行设置标题的时候:直接把文件名当作标题就行了

    进行设置线上URL的时候,本地目录和线上文档目录进行字符串拼接就可以了

    进行设置文档的时候,我们要去掉里面的html标签还有\n

    思路:我们应该如何进行去掉这里面的HTML标签呢?

    1)我们进行遍历这个文档,创建一个索引,一个字符一个字符的进行读取数据,我们自己写一个Boolean值叫做IsContent,如果说他的值是true,说明当前进行读取的html是正文,不是标签,如果说当前的值是false,那么说明当前读取的就是标签

    2)如果说当前字符是<,我们就把IsContent的值设置成false,这时候就把读到的字符全部进行忽略掉

    3)如果说当前的字符是>的时候,我们就把IsContent的值设置成true,我们就把读到的字符放到StringBuilder里面即可,先进行读取再进行判断

    \n换行:另起一行,光标放到行首,\r回车,不会另起新行,光标直接会放到行首

    4)我们在这里面用的是字符流,在我们所有的步骤都完成之后,我们要打开输出文件,发现很慢,我们就想到了一个linux上面的一个常见命令:less;

    less就是说打开大文件的速度很快,很多文本编辑器都是尝试把所有文件内容都加载到内存里面,less只加载一小块,显示那一部分就进行加载哪一部分,下面是加载每一个文件的时候,把他转化成行文本的数据,并把它们写入到输出文件里面;

    1. 此处的类中的URL是先上文档的URL
    2. 本地文档的URL类似于:D:/docs/api/java/util/Collection.html
    3. 线上文档的URL类似于:https://docs.oracle.com/javase/8/docs/api/java/util/Collection.html
    4. 线下路径是线上文件的子集

    整个代码框架:

    1. public static void main(String[] args) throws IOException {
    2. JavaAPI文档的具体路径:
    3. 1)private static final String inputpath="C:\Users\18947\Downloads\jdk-8u341-docs-all (1)\docs\jdk\api"
    4. 预处理模块所生成的行文本路径在哪里:
    5. 2)private static final String outputpath="d:\张钟俊.txt";
    6. //通过main方法完整整个预处理的过程
    7. //1我们进行枚举出JavaAPI里面下的所有html文件,通过文件递归的方式
    8. FileWriter fileWriter=new FileWriter(new File(outputpath));
    9. File file=new File(inputpath);
    10. List list=new ArrayList<>();
    11. GetAllFile(file,list);
    12. System.out.println(list);
    13. System.out.println(list.size());
    14. //2.我们进行根据枚举出来的html文件进行遍历,依次打开每一个文件,并读取里面的内容
    15. //再把内容转化成需要的结构化的数据,并转化成Document对象
    16. for(File f:list)
    17. {
    18. System.out.println("现在这个文件正在转换中"+f.getAbsolutePath());
    19. //注意:我们最后输出的文件应该是一个行文本文件,每一行对应着HTML文件,这个line就包含了每一个HTML文档应该包含的信息
    20. line这个对象就是一个行文本文件里面的内容
    21. String line=Convent(f);
    22. fileWriter.write(line);
    23. }
    24. //3.我们把最终的File对象放到最终的输出文件里面,写成行文本的方式
    25. fileWriter.close();
    26. }
    1. private static String Convent(File f) throws IOException {
    2. //1根据f转换成标题
    3. String Title=ConventTitle(f);
    4. // System.out.println(Title);
    5. //2根据f转换成线上版本的url;
    6. String Url=ConventUrl(f);
    7. System.out.println(Url);
    8. //3.根据f转换成文章正文,去掉这里面的html标签和换行符,避免转换成行文本的时候出现问题
    9. String Content=ConventContent(f);
    10. System.out.println(Content);
    11. //4.把这三行拼成一个文本
    12. return Title+"\3"+Url+"\3"+Content+"\3"+"/n";//我们使用\3来进行分割三个同一行数据的效果,是ASCILL码为三的字符
    13. //我们不能拿分号进行分割,你能保证你的正文里面没有分号吗?使用/t中,你能保证你的正文没有空格吗?但是正文是不应该出现\3
    14. }
    15. private static String ConventContent(File f) throws IOException {
    16. StringBuilder stringBuilder=new StringBuilder();
    17. InputStream inputStream=new FileInputStream(new File(f.getAbsolutePath()));
    18. boolean flag=false;
    19. while(true){
    20. int len=inputStream.read();
    21. char ch=(char)len;
    22. if(len==-1){
    23. break;
    24. }
    25. if(flag==false){
    26. if(ch=='>'){
    27. flag=true;
    28. }
    29. }else{
    30. if(ch=='<'){
    31. flag=false;
    32. continue;
    33. }
    34. if(ch=='\n'||ch=='r'){
    35. ch=' ';
    36. }
    37. stringBuilder.append(ch);
    38. }
    39. }
    40. return stringBuilder.toString();
    41. }
    42. private static String ConventUrl(File f) {
    43. //形如线下文档的html就类似于:D:/jdk-8u341-docs-all%20(1)/docs/api/java/util/Collection.html
    44. //形如线上文档的html就类似于:https://docs.oracle.com/javase/8/docs/api/java/util/Collection.html
    45. //我们可以看出这个本地文件的路径由两部分组成
    46. //part1:D:/jdk-8u341-docs-all%20(1)/docs/api/---->这一个就是咱们刚才进行递归文件处理行文本处理的过程中所有.html的父亲目录,是固定的
    47. //part2:java/util/Collection.html
    48. //而咱们先上文档的路径,无论是什么样的.html文件,它的父亲目录都是固定的:https://docs.oracle.com/javase/8/docs/api
    49. String path="https://docs.oracle.com/javase/8/docs/api";
    50. String resultpath=f.getAbsolutePath().substring(inputPath.length());
    51. return path+resultpath;
    52. }
    53. private static String ConventTitle(File f) {
    54. //我们在这里面一定要分别出文件名和全路径的区别,文件名是:"collection.html"
    55. //全路径是指:"D://Java100//API/collection.html",相当于是这个文件的父亲目录+这个文件名
    56. //我们去掉最终的.html后缀最为文件名就可以了
    57. return f.getName().substring(0,f.getName().length()-".html".length());
    58. }

     

    针对预处理模块进行设置测试用例:

    1)验证文件整体格式是否是行文本格式

    2)验证每一行是否是对应一个html文件

    3)验证每一行是否只包含三个字段,是否用\3来进行分割

    4)验证标题是否和html文件名一致

    5)验证url是否正确,是否可以快速跳转到文档页面

    6)验证正文格式是否正确,是否可以把html标签和\n去掉

    模块思路:

    1)通过递归的方式,把我们当前的api目录下面的所有.html文件放到一个List里面

    2)遍历这个list中的文件,解析成行文本的格式,一个文件对应着一个行文本,我们把若干个行文本文件写到一个文件里面。方便后续进行构建正排+倒排索引,在行文本处理过程中,干掉所有的html标签和换行符,况且文件名和全路径不要搞混了

  • 相关阅读:
    SSO 和 OAuth2.0 的关系是什么?
    力扣shell刷题
    c小白勇闯结构体!!!!
    【Java21天挑战赛】多线程
    数据结构之LinkedList与链表(上)
    量子电动力学和量子场论,多体系统的量子场论
    关于我用iVX沉浸式体验了一把0代码创建飞机大战这件事
    Android核心组件:Activity
    竞赛 基于深度学习的人脸性别年龄识别 - 图像识别 opencv
    SpringBoot - @ConditionalOnProperty注解使用详解
  • 原文地址:https://blog.csdn.net/weixin_61518137/article/details/126041426