【项目日记(三)】搜索引擎-搜索模块

❣博主主页: 33的博客
▶️文章专栏分类:项目日记◀️
🚚我的代码仓库: 33的代码仓库🚚
🫵🫵🫵关注我带你了解更多项目内容

在这里插入图片描述

1.前言

在前面的文章中,我们已经完成了索引的制作,既然已经制作好了索引,我们该如何更具输入内容,匹配对应的结果呢?接下来我们就一起完成搜索模块。

2.项目回顾

到目前为止,我们已经实现了2个类,Parser和Index。
实现Parser类:
1.通过递归枚举出所有的HTML文件。
2.针对每一个HTML进行解析操作。
a)标题:直接使用文件名称
b)URL:基于文件路径进行简单的字符串拼接
c)正文:去掉script和html标签
3.把解析内容通过addDoc放入Index类中

实现Index类:
正排索引:ArrayList
倒排索引:HashMap<String,ArrayList>
1.查正排:直接按照下标来取ArrayList中的元素
2.查倒排:直接按照Key,来区HashMap中的元素
3.添加文档,供Parser类调用
a)构建正排索引,构造DocInfo对象,添加到索引末尾
b)构建倒排索引,先对标题,正文进行分词操作,统计词频,添加到Map中去
4.保存索引:基于json格式把索引数据保存到指定文件中。
5.加载索引:基于json格式对数据进行解析,存入内存。

3.搜索流程

  • 1.【分词】根据输入内容进行分词操作
  • 2.【触发】针对分词结果来查倒排
  • 3.【去重】针对相同的文档进行去重
  • 4.【排序】针对去重结果按照权重排序
  • 5.【包装】针对排序结果查正牌,包装为Result进行返回数据

3.1分词

在使用Ansj技术进行分词操作的时候,会把空格,以及一些高频词例如a,an,is 等词语都分出来,但这些词语和我们的查询内容关联性并不大,我们就单独罗列出来,进行排除。网上有许多暂停词表可以自行下载,例如:
在这里插入图片描述

private static  String STOP_WORD_PATH="D:/doc_searcher_index/stop_word.txt";
private HashSet<String> stopWords=new HashSet<>();
public DocSearcher(){
        index.load();
        loadStopWords();
    }
public void loadStopWords(){
        try (BufferedReader bufferedReader=new BufferedReader(new FileReader(STOP_WORD_PATH))){
           while (true){
               String line=bufferedReader.readLine();
               if (line==null){
                   break;
               }
               stopWords.add(line);
           }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }    
List<Term> oldTerms=ToAnalysis.parse(query).getTerms();
        List<Term> terms=new ArrayList<>();
        for (Term term:oldTerms){
            if (stopWords.contains(term.getName())){
                continue;
            }
            terms.add(term);
        }          

3.2触发

 List<List<Weight>> termResult=new ArrayList<>();
        for (Term term:terms){
            String word=term.getName();
            List<Weight> invertedList=index.getInverted(word);
            if (invertedList==null){
                continue;
            }
            termResult.add(invertedList);
        }

3.3去重

前面,我们对用户输入的结果进行触发操作的时候,一个词可能出现在多个文档中,同理,一个文档也可能存在多个分词结果,如果我们不对相同的文档进行去重,那么一个文档针对不同的分词结果就会出现多次,这样显然不合理的。索引我们需要对相同的文档进行去重。那么具体该如何操作呢?触发的结果是一个二维数组,可以利用两个有序数组排序的思想进行去重,只不过这里运用的是多个有序数组排序。

  • 1.针对每一行按照升序排序
  • 2.借助优先级队列,争对多个有序数组进行合并
  • 3.初始化队列,把每一行第一个元素放入队列
  • 4.循环的取每行首个元素,遇到相同的DocId,权重相加
 List<Weight> allTermResult=mergeResult(termResult);
  static class Pos{
        public int row;
        public int col;
        public Pos(int row, int col) {
            this.row = row;
            this.col = col;
        }
    }
    private List<Weight> mergeResult(List<List<Weight>> source) {
    //1.针对每一行按照升序排序
        for (List<Weight> curRow:source){
            curRow.sort(new Comparator<Weight>() {
                @Override
                public int compare(Weight o1, Weight o2) {
                    return o1.getDocId()-o2.getDocId();
                }
            });
        }
    //2.借助优先级队列,争对多个有序数组进行合并
        List<Weight> target=new ArrayList<>();
      PriorityQueue<Pos> queue=new PriorityQueue<>(new Comparator<Pos>() {
          @Override
          public int compare(Pos o1, Pos o2) {
              Weight w1=source.get(o1.row).get(o1.col);
              Weight w2=source.get(o2.row).get(o2.col);
              return w1.getDocId()-w2.getDocId();
          }
      });
      //3.初始化队列,把每一行第一个元素放入队列
        for (int row=0;row<source.size();row++){
            queue.offer(new Pos(row,0));
        }
      //循环的取每行首个元素
      while (!queue.isEmpty()){
          Pos minPos=queue.poll();
          Weight curWeight=source.get(minPos.row).get(minPos.col);
          if (target.size()>0){
              Weight lastWeight=target.get(target.size()-1);
              //遇到相同的文章,权重相加
              if (lastWeight.getDocId()==curWeight.getDocId()){
                  lastWeight.setWeight(lastWeight.getWeight()+curWeight.getWeight());
              }else {
                  target.add(curWeight);
              }
          }else {
              target.add(curWeight);
          }
          Pos newPos=new Pos(minPos.row,minPos.col+1);
          if (newPos.col>=source.get(newPos.row).size()){
              continue;
          }
          queue.offer(newPos);
      }
      return target;
    }

3.4排序

  allTermResult.sort(new Comparator<Weight>() {
            @Override
            public int compare(Weight o1, Weight o2) {
                //按照降序排序
                return o2.getWeight()-o1.getWeight();
            }
        });

3.5包装

需要注意的是返回的结果为标题,URL,描述,而描述不能直接把正文返回,而是返回一段包含用户分词结果的一小段描述。生成描述的思路:可以回去到所有分词结果,遍历分词结果,看哪个结果在正文中出现,那么直接截取分词的前10个字符和后160个字符来进行描述。

public class Result {
    private String title;
    private String url;
    private String desc;
    @Override
    public String toString() {
        return "Result{" +
                "title='" + title + '\'' +
                ", url='" + url + '\'' +
                ", desc='" + desc + '\'' +
                '}';
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getUrl() {
        return url;
    }
    public void setUrl(String url) {
        this.url = url;
    }
    public String getDesc() {
        return desc;
    }
    public void setDesc(String desc) {
        this.desc = desc;
    }
}

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());
            result.setDesc(GenDesc(docInfo.getContent(),terms));
            results.add(result);
        }
private String GenDesc(String content, List<Term> terms) {
        int firstPos=-1;
        for (Term term:terms){
            String word=term.getName();
            //避免出现word前后带有标点符号
            content=content.toLowerCase().replaceAll("\\b"+word+"\\b"," "+word+" ");
            firstPos=content.indexOf(" "+word+" ");
            if (firstPos>=0){
                break;
            }
        }
        if (firstPos==-1){
            if (content.length()>160){
                return content.substring(0,160)+"...";
            }
            return content;
        }
        String desc="";
        int descBeg=firstPos<60?0:firstPos-60;
        if (descBeg+160>content.length()){
            desc=content.substring(descBeg);
        }else {
            desc=content.substring(descBeg,descBeg+160)+"...";
        }
        //把描述中和分词结果相同的部分设置为斜体加上<i>标签,方便前端标红
        for (Term term:terms){
            String word=term.getName();
            //进行忽略大小写的全词匹配
            desc=desc.replaceAll("(?i) "+word+" ","<i> "+word+" </i>");
        }
        return desc;
    }

4.总结

这篇文章主要介绍了,搜索引擎的搜锁模块,这部分的难点主要是去重操作,去重的时候需要用到我们之前学过的数据结构,小根堆结合多个有序数组完成去重操作!

下期预告:搜索引擎(四)

  • 51
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 23
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值