【项目日记(二)】搜索引擎-索引制作

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

在这里插入图片描述

目录

  • 1.前言
  • 2.索引结构
    • 2.1创捷索引
    • 2.2根据索引查询
    • 2.3新增文档
    • 2.4内存索引保存到磁盘
    • 2.5把磁盘索引加载到内存
  • 3.性能优化
    • 3.1多线程
    • 3.2线程安全
    • 3.3CountDownLatch类
  • 4.总结

1.前言

在上一篇文章中,我们已经介绍了索引解析,那么接下来我们继续完善我们的项目,既然已经有了解析好的索引,那么我们就需要把解析的内容添加到倒排索引和正排索引中。

2.索引结构

创建index类,通过这个类来构建索引结构
基本步骤:

  • 用ArrayList创建正排索引
  • 用HashMap创建倒排索引
  • 1.给定docid在正排索引中,查询详细信息
  • 2.给定一个词,在倒排索引中查与这个词的关联文档
  • 3.往索引中新增文档
  • 4.把内存的索引保存到磁盘
  • 5.把磁盘的索引结构保存到内存

2.1创捷索引

正排索引

private ArrayList<DocInfo> forwardIndex=new ArrayList<>();

倒排索引

private HashMap<String,ArrayList<Weight>> invertedIndex=new HashMap<>();

DocInfo类:

public class DocInfo {
    private int docID;
    private String title;
    private String url;
    private String content;
    public int getDocID() {
        return docID;
    }
    public void setDocID(int docID) {
        this.docID = docID;
    }
    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 getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }
}

Weight类:

public class Weight {
    private int docId;
    private int weight;
    public int getDocId() {
        return docId;
    }
    public void setDocId(int docId) {
        this.docId = docId;
    }
    public int getWeight() {
        return weight;
    }
    public void setWeight(int weight) {
        this.weight = weight;
    }
}

2.2根据索引查询

//1.根据docId查询文档详情,数组小标就是文档id
    public DocInfo getDocInfo(int docId){
        return forwardIndex.get(docId);
    }
//2.给定一个词,查在哪些文档中
    public List<Weight> getInverted(String term){
        return invertedIndex.get(term);
    }   

2.3新增文档

 public void addDoc(String title,String url,String content){
        //给正排索引新增和倒排索引都新增信息
        //构建正排索引
        DocInfo docInfo=buildForward(title,url,content);
        //创建倒排索引
        buildInverted(docInfo);
    }

在正排索引中添加文档:

private DocInfo buildForward(String title, String url, String content) {
        DocInfo docInfo=new DocInfo();
        docInfo.setTitle(title);
        docInfo.setUrl(url);
        docInfo.setContent(content);
        //巧妙设计:docInfoId的下标和数组下标一一对应
        docInfo.setDocID(forwardIndex.size());
        forwardIndex.add(docInfo);
        return docInfo;
    }

在倒排索引中新增文档
1.需要统计每一个词在文档中的出现次数,在根据次数算出权重
2.首先进行分词操作,统计每一个不同的词在标题中出现的次数
3.再进行分词操作,统计每一个词在正文出现的次数
4.设置权重为标题次数*10+正文次数

 private void buildInverted(DocInfo docInfo) {
        class WordCnt{
        public int titleCount;
        public int contentCount;
        }
        HashMap<String,WordCnt> wordCntHashMap=new HashMap<>();
        //1.针对标题进行分词操作
        List<Term> terms= ToAnalysis.parse(docInfo.getTitle()).getTerms();
        //2.针对分词结果,统计每个词出现的次数
        for (Term term:terms){
            String word=term.getName();
            WordCnt wordCnt=wordCntHashMap.get(word);
            if (wordCnt==null){
                WordCnt newwordCnt=new WordCnt();
                newwordCnt.titleCount=1;
                newwordCnt.contentCount=0;
                wordCntHashMap.put(word,newwordCnt);
            }else {
                wordCnt.titleCount+=1;
            }
        }
        //3.针对正文进行分词操作
        List<Term> terms2=ToAnalysis.parse(docInfo.getContent()).getTerms();
        //4.遍历分词结果,统计每个词出现的次数
        for (Term term:terms2){
            String word=term.getName();
            WordCnt wordCnt=wordCntHashMap.get(word);
            if (wordCnt==null){
                WordCnt newWordCnt=new WordCnt();
                newWordCnt.titleCount=0;
                newWordCnt.contentCount=1;
                wordCntHashMap.put(word,newWordCnt);
            }else {
                wordCnt.contentCount+=1;
            }
        }
        //5.设置权重为:标题*10+正文
        //一个对象必须实现了Iterable接口才能使用for each进行遍历,而Map并没有实现该接口,但Set实现了,所以就把Map转换为Set
        for(Map.Entry<String,WordCnt> entry:wordCntHashMap.entrySet()) {            
     List<Weight> invertedList=invertedIndex.get(entry.getKey());
                if (invertedList==null){
                    ArrayList<Weight> newInvertedList=new ArrayList<>();
                    Weight weight=new Weight();
                    weight.setWeight(entry.getValue().titleCount*10+entry.getValue().contentCount);
                    weight.setDocId(docInfo.getDocID());
                    newInvertedList.add(weight);
                    invertedIndex.put(entry.getKey(),newInvertedList);
                }else {
                    Weight weight=new Weight();
                    weight.setDocId(docInfo.getDocID());
                    weight.setWeight(entry.getValue().titleCount*10+entry.getValue().contentCount);
                    invertedList.add(weight);
                }        
        }
    }

2.4内存索引保存到磁盘

索引当前是存储在内存中的,构造索引的过程是非常耗时的,因此我们就不应该再服务器启动时才去构造索引,通常就把这些耗时的操作,单独执行完成之后,然后再让线上的服务器加载构造好的索引。
我们就把内存中构造的索引结构,给变成一个字符串,然后写入文件即可,这个操作就叫序列化。适应Jackson中的ObjectMapper来完成此操作。

 private static  String INDEX_PATH="D:/doc_searcher_index/";
 public void save(){
        long beg=System.currentTimeMillis();
        System.out.println("保存索引开始!");
        File indexPathFile=new File(INDEX_PATH);
        if(!indexPathFile.exists()){
            indexPathFile.mkdir();
        }
        File forwardIndexFile=new File(INDEX_PATH+"forward.txt");
        File invertedIndexFile=new File(INDEX_PATH+"inverted.txt");
        try {
        //利用ObjectMapperJava对象转换为JSON格式
        //从内存中读取forwardIndex保存到forwardIndexFile
            objectMapper.writeValue(forwardIndexFile,forwardIndex);
        //从内存中读取nvertedIndex保存到invertedIndexFile
            forwardIndexFileobjectMapper.writeValue(invertedIndexFile,invertedIndex);
        }catch (IOException e) {
            e.printStackTrace();
        }
        long end=System.currentTimeMillis();
        System.out.println("保存索引完成!消耗时间:"+(end-beg)+"ms");
    }

2.5把磁盘索引加载到内存

public void load(){
        long beg=System.currentTimeMillis();
        System.out.println("加载索引开始!");
        File forwardIndexFile=new File(INDEX_PATH+"forward.txt");
        File invertedIndexFile=new File(INDEX_PATH+"inverted.txt");
        try {
            forwardIndex=objectMapper.readValue(forwardIndexFile, new TypeReference<ArrayList<DocInfo>>() {});
            invertedIndex=objectMapper.readValue(invertedIndexFile, new TypeReference<HashMap<String, ArrayList<Weight>>>() {});
        }catch (IOException e){
            e.printStackTrace();
        }
        long end=System.currentTimeMillis();
        System.out.println("加载引擎结束消耗时间:"+(end-beg)+"ms");
    }

parser相当于制作索引的入口,对应到一个可执行的程序
index相当于实现了索引的数据结构,提供一些Api
接下来我们就在parser里面调用对应的api
在parser类中解析完Html文件时,应添加到索引中

private  void parseHTML(File f) {
        //1.解析HTML标题
        String title=parseTitle(f);
        //2.解析HTML的URL
        String url=parseUrl(f);
        //3.解析HTML的正文
        long beg=System.nanoTime();
        //String content=parseContent(f);
        String content=parseContentByRegex(f);
        long mid=System.nanoTime();
        //把解析出来的信息加载到索引
        index.addDoc(title,url,content);         
    }

在添加完索引之后,应该把索引保存到磁盘

public void run()  {
        long beg=System.currentTimeMillis();
        System.out.println("索引制作开始");
        //1.枚举出INPUT_PATH下的所有html文件
        ArrayList<File> fileList=new ArrayList<>();
        enumFile(INPUT_PATH,fileList);
        //2.解析文档内容
        for (File f:fileList){
            System.out.println("开始解析"+f.getAbsolutePath()+"....");
            parseHTML(f);
        }
        //3.把内存构造的索引保存到磁盘
        index.save();
        long end=System.currentTimeMillis();
        System.out.println("索引制作结束!消耗时间:"+(end-beg)+"ms");
    }

3.性能优化

此时我们已经完成了文档解析和索引制作模块,那么我们进行验证
在这里插入图片描述
文档内容正确生成:
在这里插入图片描述
在这里插入图片描述
但我们观察索引制作的时间:一个消耗了19973ms就是19s,花费的时间是比较长的,那么有什么办法提高效率呢?方法当然是有的,首先我们得清楚具体是哪一个步骤拖慢了执行效率,我们来分析代码:
在这里插入图片描述
可以看到解析文档的时候从磁盘读文件,循环遍历文件操作,那么显然效率是非常慢的,既然一个线程串行执行效率非常慢,那么我们就采用多线程并发执行来提高效率。

3.1多线程

我们可以使用创建一个线程池来实现并发操作。通过submit往线程池中提价任务,操作极快(只是把Runnable对象放入阻塞队列中)。
把代码改进成多线程的版本,线程池中的线程数目具体设置成多少才合适呢?最好通过实验来确定。

public void run()  {
        long beg=System.currentTimeMillis();
        System.out.println("索引制作开始");
        //1.枚举出INPUT_PATH下的所有html文件
        ArrayList<File> fileList=new ArrayList<>();
        enumFile(INPUT_PATH,fileList);
         ExecutorService executorService= Executors.newFixedThreadPool(6);
        //2.解析文档内容
        for (File f:files){
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("解析:"+f.getAbsolutePath());
                    parseHTML(f);                    
                }
            });
        }
        //3.把内存构造的索引保存到磁盘
        index.save();
        long end=System.currentTimeMillis();
        System.out.println("索引制作结束!消耗时间:"+(end-beg)+"ms");
    }

3.2线程安全

我们既然引入了多线程就要考虑线程安全问题,要注意修改操作读写操作。当多个线程同时尝试修改同一个共享数据时,需要确保数据的一致性,避免出现竞态条件。读写操作:如果一个线程在读取共享数据的同时另一个线程在修改该数据,可能导致读取到不一致或无效的数据。
那么我们就需要对程序进行加锁操作:
在这里插入图片描述
在这里插入图片描述

3.3CountDownLatch类

添加锁虽然解决了线程安全问题,依然有新的问题,那就是在所有文件提交完成后就会立即执行save()操作,但是可能文件解析还没有完成。为了解决这样的问题,我们就引入 CountDownLatch类。
CountDownLatch类类似于跑步比赛的裁判,只有所有的选手都撞线了,就认为这场比赛结束了。再构造 CountDownLatch的时候指定一下比赛选手的个数,每个选手撞线都要通知一下countDown(),通过await来等待所有的选手都撞线完毕才执行save()操作。

public void runByThread(){
        long beg=System.currentTimeMillis();
        System.out.println("索引开始制作");
        //1.枚举出INPUT_PATH下的所有html文件
        ArrayList<File> files=new ArrayList<>();
        enumFile(INPUT_PATH,files);
        //2.解析文档内容
        CountDownLatch latch=new CountDownLatch(files.size());
        ExecutorService executorService= Executors.newFixedThreadPool(6);
        for (File f:files){
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("解析:"+f.getAbsolutePath());
                    parseHTML(f);
                    latch.countDown();
                }
            });
        }
        try {
            //await会阻塞,把所有选手都调用countDown以后才会继续执行
            latch.await();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //手动的把线程池里面的线程杀掉
        executorService.shutdown();
        //3.把内存构造的索引保存到磁盘
        index.save();
        long end=System.currentTimeMillis();
        System.out.println("索引制作结束!消耗时间:"+(end-beg)+"ms");
        System.out.println("t1:"+t1+"t2"+t2);
    }

4.总结

这篇文章主要完成了索引制作模块,以及进行了性能优化,在下一篇文章中将进行搜索模块的制作。

下期预告:项目日记(三)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/755249.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

VUE的快速使用

使用步骤 代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title> </head&…

数据结构历年考研真题对应知识点(串的模式匹配)

目录 4.2串的模式匹配 4.2.2串的模式匹配算法——KMP算法 【KMP 匹配过程中指针变化的分析(2015)】 【KMP 匹配过程中比较次数的分析(2019)】 4.2串的模式匹配 4.2.2串的模式匹配算法——KMP算法 【KMP 匹配过程中指针变化的分析(2015)】 最终得到子串指针变化公式 jnex…

Dahlia Hart: Stylized Casual Character(休闲角色模型)

此包包含两个发型和两个服装&#xff0c;每个都有多种颜色选择。每个发型都适合与物理资源一起使用&#xff0c;并包含各种表情和音素混合形状。 下载&#xff1a;​​Unity资源商店链接资源下载链接 效果图&#xff1a;

OBD诊断(ISO15031) 02服务

文章目录 功能简介请求和响应1、read-supported PIDs1.1、请求1.2、肯定响应 2、read PID value1.1、请求1.2、肯定响应 3、同时请求多个PID4、同时读取多个PID数据 Parameter definition报文示例1、单个PID请求和读取2、多个PID请求和读取 功能简介 02服务&#xff0c;即 Req…

基于CNN卷积神经网络的步态识别matlab仿真,数据库采用CASIA库

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1步态识别系统框架 4.2 CNN原理及数学表述 4.3 CASIA步态数据库 5.算法完整程序工程 1.算法运行效果图预览 (完整程序运行后无水印) 1.训练过程 2.样本库 3.提取的步态能量图 4.步态识…

李商隐,情丝绕指的朦胧诗人

李商隐&#xff0c;字义山&#xff0c;号玉谿生、樊南生&#xff0c;约生于唐宪宗元和八年&#xff08;公元813年&#xff09;&#xff0c;卒于唐宣宗大中十二年&#xff08;公元858年&#xff09;&#xff0c;享年45岁。李商隐生活在晚唐时期&#xff0c;与杜牧合称“小李杜”…

【51单片机入门】矩阵键盘

文章目录 前言矩阵键盘介绍与检测原理原理图代码讲解总结 前言 在嵌入式系统设计中&#xff0c;键盘输入是一种常见的人机交互方式。其中&#xff0c;矩阵键盘因其简单、方便和易于扩展的特性&#xff0c;被广泛应用于各种设备中。本文将介绍如何使用51单片机来实现矩阵键盘的…

微机原理与接口技术:重点内容|计算机系统|学习笔记

系列目录 前言 只将最重要的知识点考点列出来方便学习复习 目录 系列目录前言第1章 微型计算机概述第2章 16位和32位微处理机&#x1f31f;16位微处理器 8086 第3章 Pentium 的指令系统常用指令 第4章 存储器、存储管理和高速缓存技术第5章 微型计算机和外设的数据传输第6章 串…

Java学习 - 布隆过滤器

前置需求 需求 已经有50亿个电话号码&#xff0c;现在给出10万个电话号码&#xff0c;如何快速准确地判断这些电话号码是否已经存在&#xff1f; 参考方案 通过数据库查询&#xff1a;比如MySQL&#xff0c;性能不行&#xff0c;速度太慢将数据先放进内存&#xff1a;50亿*8字…

vue3-openlayers 图标闪烁、icon闪烁、marker闪烁

本篇介绍一下使用vue3-openlayers 图标闪烁、icon闪烁、marker闪烁 1 需求 图标闪烁、icon闪烁、marker闪烁 2 分析 图标闪烁、icon闪烁、marker闪烁使用ol-animation-fade组件 3 实现 <template><ol-map:loadTilesWhileAnimating"true":loadTilesWh…

龙迅#LT6911GXC支持HDMI2.1转MIPI/4PORT LVDS应用功能,分辨率高达8K30HZ/4K120HZ压缩格式。

1. 描述 该LT6911GXC是一款高性能HD-DVI2.1转MIPI或LVDS芯片&#xff0c;适用于VR/显示应用。 HDCP RX作为HDCP中继器的上游&#xff0c;可以与其他芯片的HDCP TX配合实现中继器功能。 对于 HD-DVI2.1 输入&#xff0c;LT6911GXC可以配置为 3/4 通道。 对于MIPI输出&#xff0c…

推荐一款免费的GIF编辑器——【ScreenToGif编辑器】

读者大大们好呀&#xff01;&#xff01;!☀️☀️☀️ &#x1f440;期待大大的关注哦❗️❗️❗️ &#x1f680;欢迎收看我的主页文章➡️木道寻的主页 文章目录 &#x1f525;前言&#x1f680;素材准备&#x1f680;逐帧制作&#x1f680;保存图片⭐️⭐️⭐️总结 &#…

PPT中的文字跟随Excel动态变化,且保留文字格式

今天协助客户解决了一个有趣的问题&#xff0c;这里记录一下&#xff0c;以此共勉。 目录 1. 提出问题2. 此功能的应用场景3. 开始制作4. 注意事项5. 若遇到任何问题 1. 提出问题 PPT的图表是可以引用Excel的&#xff0c;那PPT的文本是否可以引用Excel实现动态更新呢&#xff…

农业新质生产力数据(2012-2022年)原始+dofile+测算数据集

数据简介&#xff1a;农业新质生产力是指在现代农业发展中&#xff0c;通过融合尖端科技、信息技术与创新管理模式&#xff0c;实现农业生产效率飞跃、产品质量显著提升及生产可持续性增强的一种革新性生产能力&#xff0c;农业新质生产力代表了从依赖传统资源转向依靠科技创新…

中科驭数CEO鄢贵海:从计算系统的三个视角重新审视DPU的核心价值

在信息技术日新月异的浪潮中&#xff0c;DPU正逐渐崭露头角。当前&#xff0c;DPU发展的核心驱动力来自于什么&#xff1f;DPU技术是否已经足够成熟到广泛应用&#xff1f;市场上头部玩家参与到这一创新技术的市场角逐之中&#xff1f;在算力时代&#xff0c;DPU应该如何找准价…

k8s流控平台apiserver详解

一、简单理解认识apiserver 1.主要功能 认证 鉴权 准入 mutating validating admission 限流 2.概念 apiserver保护etcd&#xff0c;缓存机制&#xff0c;有缓存直接返回&#xff0c;没缓存再去查看etcd,apiserver是担任和其他平台同信并认证 3.访问控制概览…

[linux]sed命令基础入门详解

sed是一种流编辑器&#xff0c;它一次处理一行内容。处理时&#xff0c;把当前处理的行存储在临时缓冲区中&#xff0c;称为“模式空间”&#xff0c;接着用sed命令处理缓冲区中的内容&#xff0c;处理完成后&#xff0c;把缓冲区的内容送往屏幕。接着处理下一行&#xff0c;这…

ROS2开发机器人移动

.创建功能包和节点 这里我们设计两个节点 example_interfaces_robot_01&#xff0c;机器人节点&#xff0c;对外提供控制机器人移动服务并发布机器人的状态。 example_interfaces_control_01&#xff0c;控制节点&#xff0c;发送机器人移动请求&#xff0c;订阅机器人状态话题…

PHP电商系统开发指南数据库管理

回答&#xff1a;数据库管理是电商系统开发的关键&#xff0c;涉及数据的存储、管理和检索。选择合适的数据库引擎&#xff0c;如mysql或 postgresql。创建数据库架构&#xff0c;定义数据的组织方式&#xff08;如产品表、订单表&#xff09;。进行数据建模&#xff0c;考虑实…

Vue-cli项目及Element UI 环境搭建 保姆级教程

一、Vue-cli介绍及其作用 什么是Vue-cli手脚架 vue-cli 官方提供的一个脚手架&#xff0c;用于快速生成一个 vue 的项目模板&#xff1b;预先定义 好的目录结构及基础代码&#xff0c;就好比咱们在创建 Maven 项目时可以选择创建一个 骨架项目&#xff0c;这个骨架项目就是脚…