搜索引擎系统———引擎模块(ssm三剑客项目)

=@TOC
咋们如果用我们的小服务器去搞百度,搜狗那种引擎肯定是不行的,内属于全站搜索,我们这里做一个站内搜索。这个还是可以的,就类似于我们对网站里的资源进行搜索。

一.搜索引擎怎么搜索

搜索引擎就像一个小蜜蜂每天不停的采摘蜂蜜,就是去爬虫各个网页,然后通过爬取之后建立索引,以供于我们去搜索。
这里我们可以使用Python,或者下载文档压缩包。这里我们下包把,快多了。本来想搞一个英雄联盟的,实在找不见,要是后续有老铁找到可以分享一下。
建议大家别爬虫(要不然被告了,不过我们学校的官网倒是可以随便爬,我们当时就是拿这个练手的)
为什么要用索引呢?
因为爬的数据太多了,不索引,难道我去遍历吗?时间复杂度太大了。
这里我们需要建立索引,索引分别为正排索引,和倒排索引

拿LOL举个例子吧,正排就相当于,我们提到无极剑圣的技能就可以联想到
Q技能 阿尔法突袭
W技能 冥想
E技能 无双
R技能 高原血统
故根据名字选技能
在这里插入图片描述

倒排索引就是LOL里面谁有剑
1.蛮王
2.无极剑圣
3.剑姬
故根据特点选择英雄

二.模块划分(共三个)

1.索引模块

1)扫描下载到的文档,分析内容,构建出,正排索引和倒排索引。并且把索引内容保存到文件中。
2)加载制作好的索引。并提供一些API实现查正排和查倒排这样的功能。

2.搜索模块

1)调用索引模块,实现一个搜索的完整过程。
输入:用户的查询词
输出:完整的搜索结果

3.web模块

需要实现一个简单的web程序,能够通过网页的形式和用户进行交互。
包含了前端和后端。

三. 怎么实现分词

分词的原理
1.基于词库
尝试把所有的词都进行穷举,把这些结果放到词典文件中。
2.基于统计
收集到很多的语料库,进行人工标注,知道了那些字在一起的概率比较大~

java中能够实现分词的第三方工具也是有很多的
比如ansj(听说唱的兄弟可能听过ansj,哈哈)这个就是一个maven中央仓库的分词第三方库。
在这里插入图片描述
我们直接下载最新版本然后放入pom.xml里面
test包里直接操作:我们使用这个测试代码直接搞。试一下这个包咋用。
在这里插入图片描述

import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;
import java.util.List;
public class TastAnsj {
    public static void main(String[] args) {
        String str = "易大师是一个有超高机动性的刺客、战士型英雄,擅长利用快速的打击迅速击溃对手,易大师一般打野和走单人路,作为无极剑道的最后传人,易可以迅速砍出大量伤害,同时还能利用技能躲避猛烈的攻击,避开敌人的集火。";
        List<Term> terms = ToAnalysis.parse(str).getTerms();
        for (Term term : terms) {
            System.out.println(term.getName());
        }
    }
}

四.文件读取

把刚刚下载好的文档的路径复制到String中并且用常量标记。
这一步是为了用遍历的方法把所有html文件搞出来,我们这里用了一个递归,如果是绝对路径,就填加到文件链表,如果不是就递归,继续添加里面的值。

import java.io.File;
import java.util.ArrayList;


//读取刚刚文档
public class Parser {
     private static final  String INPUT_PATH="D:/test/docs/api";
      public  void run(){
          //整个Parser类的入口
          //1.根据路径,去枚举出所有的文件.(html);
          ArrayList<File> fileList=new ArrayList<>();
          enumFile(INPUT_PATH,fileList);
          System.out.println(fileList);
          System.out.println(fileList.size());
          //2.针对上面罗列出的文件,打开文件,读取文件内容,并进行解析
          //3.把在内存中构造好的索引数据结构,保定到指定的文件中。
      }
      //第一个参数表示从哪里开始遍历 //第二个表示结果。
      private void enumFile(String inputPath,ArrayList<File>fileList){
         File rootPath=new File(inputPath);
         //listFiles 能够获取到一层目录下的文件
        File[] files= rootPath.listFiles();
         for(File f:files){
             //根据当前f的类型判断是否递归。
             //如果f是一个普通文件,就把f加入到fileList里面
             //如果不是就调用递归
             if(f.isDirectory()){
                 enumFile(f.getAbsolutePath(),fileList);
             }else {
                 fileList.add(f);
             }
         }
      }
    public static void main(String[] args) {
        //通过main方法来实现整个制作索引的过程
        Parser parser=new Parser();
        parser.run();
    }
}

在这里插入图片描述
我们尝试运行一下,这里的文件也太多了吧,而且无论是什么都打印出来了。所以我们下一步就是把这些文件进行筛选,选择有用的。

else {
                 if(f.getAbsolutePath().endsWith(",html"))
                 fileList.add(f);
             }

这个代码就是只是针对末尾为html的文件。下图就是展示结果。
在这里插入图片描述

4.1 打开文件,解析内容。

这里分为三个分别是解析Title,解析Url,解析内容Content(因为搜索引擎是先会出来一个界面的)

4.1.1解析Title

f.getName()是直接读取文件名字的方法。

我们用的name.substring(0,f.getName().length()-5);为什么要用总的文件名字长度减去5呢,因为.HTML刚好就是五。

private  String parseTitle(File f) {
          String name= f.getName();
         return name.substring(0,f.getName().length()-5);

    }

4.1.2解析Url操作

这里的url就是我们平时去一个浏览器输入一个东西下面会有一个url,这个url就是我们的绝对路径经过截取获得出我们的相对的目录,然后与我们的http进行拼接,这样就可以直接得到一个页面。

private  String parseUrl(File f) {
      String part1="https://docs.oracle.com/javase/8/docs/api/";
      String part2=f.getAbsolutePath().substring(INPUT_PATH.length());
          return part1+part2;
    }

4.1.3解析内容

以<>为开关进行对数据的读取,以int类型读取,为什么要用int而不是char呢因为int类型读完之后就变成-1可以判断一下是否读取完毕。
具体代码如下很容易理解。

private  String parseContent(File f) throws IOException {
          //先按照一个一个字符来读取,以<>作为开关
        try(FileReader fileReader=new FileReader(f)) {
            //加上一个是否拷贝的开关.
            boolean isCopy=true;
            //还需要准备一个结果保存
            StringBuilder content=new StringBuilder();
            while (true){
                //此处的read的返回值是int,不是char
                //如果读到文件末尾,就会返回-1,这是用int的好处;
                int  ret = 0;
                try {
                    ret = fileReader.read();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                if(ret==-1) {
                        break;
                    }
                    char c=(char) ret;
                    if(isCopy){
                        if(c=='<'){
                            isCopy=false;
                            continue;
                        }
                        //其他字符直接拷贝
                        if(c=='\n'||c=='\r'){
                            c=' ';
                        }
                        content.append(c);
                    }else{
                        if(c=='>'){
                            isCopy=true;
                        }
                    }
            }

            return  content.toString();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        return "";
    }

这一模块总的代码块如下:

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;

//读取刚刚文档
public class Parser {
     private static final  String INPUT_PATH="D:/test/docs/api";
      public  void run(){
          //整个Parser类的入口
          //1.根据路径,去枚举出所有的文件.(html);
          ArrayList<File> fileList=new ArrayList<>();
          enumFile(INPUT_PATH,fileList);
          System.out.println(fileList);
          System.out.println(fileList.size());
          //2.针对上面罗列出的文件,打开文件,读取文件内容,并进行解析
          for (File f:fileList){
              System.out.println("开始解析"+f.getAbsolutePath());
              parseHTML(f);
          }
          //3.把在内存中构造好的索引数据结构,保定到指定的文件中。
      }


    private  String parseTitle(File f) {
          String name= f.getName();
         return name.substring(0,f.getName().length()-5);

    }
    private  String parseUrl(File f) {
      String part1="https://docs.oracle.com/javase/8/docs/api/";
         String part2=f.getAbsolutePath().substring(INPUT_PATH.length());
          return part1+part2;
    }
    private  String parseContent(File f) throws IOException {
          //先按照一个一个字符来读取,以<>作为开关
        try(FileReader fileReader=new FileReader(f)) {
            //加上一个是否拷贝的开关.
            boolean isCopy=true;
            //还需要准备一个结果保存
            StringBuilder content=new StringBuilder();
            while (true){
                //此处的read的返回值是int,不是char
                //如果读到文件末尾,就会返回-1,这是用int的好处;
                int  ret = 0;
                try {
                    ret = fileReader.read();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                if(ret==-1) {
                        break;
                    }
                    char c=(char) ret;
                    if(isCopy){
                        if(c=='<'){
                            isCopy=false;
                            continue;
                        }
                        //其他字符直接拷贝
                        if(c=='\n'||c=='\r'){
                            c=' ';
                        }
                        content.append(c);
                    }else{
                        if(c=='>'){
                            isCopy=true;
                        }
                    }
            }

            return  content.toString();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        return "";
    }
    private void parseHTML (File f){
        //解析出标题
          String title=parseTitle(f);
        //解析出对应的url
          String url=parseUrl(f);
        //解析出对应的正文
        try {
            String content=parseContent(f);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
      //第一个参数表示从哪里开始遍历 //第二个表示结果。
      private void enumFile(String inputPath,ArrayList<File>fileList){
         File rootPath=new File(inputPath);
         //listFiles 能够获取到一层目录下的文件
        File[] files= rootPath.listFiles();
         for(File f:files){
             //根据当前f的类型判断是否递归。
             //如果f是一个普通文件,就把f加入到fileList里面
             //如果不是就调用递归
             if(f.isDirectory()){
                 enumFile(f.getAbsolutePath(),fileList);
             }else {
                 if(f.getAbsolutePath().endsWith(".html"))
                 fileList.add(f);
             }
         }
      }
    public static void main(String[] args) {
        //通过main方法来实现整个制作索引的过程
        Parser parser=new Parser();
        parser.run();
    }
}

第二部分 索引

通过这个类去构造出索引但是这个类,和数据库里面的结构是不一样的,但是目的都是为了加快查找的速度。
所以有两个类
正排和倒排的时间复杂度都是1,在把时间复杂度为一的放入倒排哩

正排索引

1.给定一个docid,在正排索引中,查询文档的详细信息。使用数组来表示docId
private ArrayList forwardIndex = new ArrayList<>();
2.把刚刚处理过的数据添加到索引里面(title,URL,content);
将这三个打包成一个数组大的元素插入数组中。(数组下标刚好是doc id)
3.

倒排索引

1.使用哈希表来表示倒排索引key就是词,value就是一组和这个词相关的文章。
private HashMap<String,ArrayList> invertedIndex= new HashMap<>();
2倒排索引是词和文档的映射,因为权重不一样,所以要对标题和正文分别分词,最后遍历分词结果,统计词的出现次数;
我们这里使用一个HashMap 的key表示Å分词结果,value是和分词结果相关的id列表。
在这里插入图片描述
通过就可以处理我们的文本的权重了

把索引结构放入磁盘中

分别使用两个文件来分别保存我们的倒排索引和正排索引,然后把索引的文件路径放在我们,解析好的文件包的地址中。
第一步,先判断索引对应的目录是否存在,不存在就创建。

把索引加载到内存

我们需要把索引加载到内存中,需要读取刚刚我们存进去的数据并解析成<ArrayList>
Jackson专门提供了一个辅助的工具类TypeRerference<>;
我们直接写入以下操作即可把存进去的json数据解析成我们需要的 ArrayList和HashMap<Sting,ArrayList>
forwardindex = objectmapper.readValue(farwardIndexFile,new TypeRerference<ArrayList> () {});

多线程制作索引

为了能够通过多线程制作索引,直接引入线程池。
由于索引并不是直接就执行完的,这里要等线程把任务执行完,
这里需要用到 countDownLatch 做一个裁判,记录下我们跑到哪里,直到跑完,但是为了防止有的任务过慢,我们加一个时间,等时间到了再保存索引。
注意这里还会遇到线程安全的问题,在多线程情况会出现多个线程执行处理索引的问题。
此时就需要用到加锁的操作,这里就可以分别的去把正排索引,和倒排索引去加锁。

关闭进程

为什么有时候我们运行完毕,该程序还不退出呢,这里需要用到守护线程,我们创建出来的线程并不是守护线程,因此当main执行完了,这些线程还在工作。有两种操作,要么都设置成守护线程,要吗使用shutdown()把线程池中的线程杀死即可。

搜索模块

1.针对与用户输入的查询词进行分词、
2.触发。拿着每个分词结果去倒排索引中查,找到相关性的文档
3.排序。针对上面触发出来的结果,进行排序
4.包装结果,根据排序后的结果去,查正排,获取到每个文档的详细信息,包装成一定结构的数据返回出去。
这四步仅仅是把之前准备好的工作串起来

web模块

提供一个web接口,前端使用三剑客,后端这里就用sprig boot就可以了。
这里就需要约定前后端的接口,明确描述,服务器能接受啥接口,能返回啥响应。
这里的接口我们只需要实现搜索接口,即可,

响应http /1.1 200 ok
[
{
tiele :”标题“
url: “这是url1”
desc:“这是描述1”
}
]

这里交互要用到ajax前后端交互:
当用户点击搜索按钮的时候,浏览器会获取到搜索框内容,基于ajax构造http,请求,然后发给搜索服务器,浏览器获取到json数据把页面生成。
此处使用到jquery 第三方库js的
使用引入jquery

  • 26
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 25
    评论
评论 25
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卷的快乐人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值