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

news2024/12/23 5:04:16

❣博主主页: 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.总结

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

独一无二的设计模式——单例模式(python实现)

1. 引言 大家好&#xff0c;今天我们来聊聊设计模式中的“独一无二”——单例模式。想象一下&#xff0c;我们在开发一个复杂的软件系统&#xff0c;需要一个全局唯一的配置管理器&#xff0c;或者一个统一的日志记录器&#xff1b;如果每次使用这些功能都要创建新的实例&…

java基于ssm+jsp 多用户博客个人网站

1管理员功能模块 管理员登录&#xff0c;管理员通过输入用户名、密码等信息进行系统登录&#xff0c;如图1所示。 图1管理员登录界面图 管理员登录进入个人网站可以查看&#xff1b;个人中心、博文类型管理、学生博客管理、学生管理、论坛信息、管理员管理、我的收藏管理、留…

【Android面试八股文】请描述一下Service的生命周期是什么样的?

文章目录 一、Service的生命周期是什么样的?1.1 通过 `startService` 启动的 Service 生命周期:1.1.1 相关方法说明1.1.2 流程1.1.3 总结1.2 通过 bindService 启动的 Service 生命周期1.2.1 相关方法说明1.2.2 流程1.3 生命周期调用1.4 总结一、Service的生命周期是什么样的…

算法:链表题目练习

目录 链表的技巧和操作总结 常用技巧&#xff1a; 链表中的常用操作 题目一&#xff1a;反转一个单链表 题目二&#xff1a;链表的中间结点 题目三&#xff1a;返回倒数第k个结点 题目四&#xff1a;合并两个有序链表 题目五&#xff1a;移除链表元素 题目六&#xff…

Linux常用命令大全(超详细!!!)

文章目录 1.Linux是什么1.1 关于Linux我们主要学习什么1.1 学习Linux常见命令的前置知识 2. Linux常见命令2.1 ls命令2.2 cd命令2.3 pwd命令2.4 touch命令2.5 cat命令2.6 echo命令2.7 vim命令2.8 mkdir 命令2.9 rm命令2.10 cp命令2.11 mv命令2.12 grep命令2.13 ps命令2.14 nets…

影响LED显示屏质量的关键因素

LED电子显示屏以其环保节能的特点&#xff0c;成为现代显示技术的重要选择。然而&#xff0c;确保显示屏的质量和安全使用&#xff0c;需要考虑多个方面。本文将探讨影响LED电子显示屏质量的关键因素&#xff0c;以及在不同环境下如何预防失火现象。 材质因素 显示屏的质量首先…

Vue3使用jsbarcode生成条形码,以及循环生成条形码

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;我是前端菜鸟的自我修养&#xff01;今天给大家分享Vue3使用jsbarcode生成条形码&#xff0c;以及循环生成条形码&#xff0c;介绍了JsBarcode插件的详细使用方法&#xff0c;并提供具体代码帮助大家深入理解&#xff0c;彻…

Day6: 344.反转字符串 541. 反转字符串II 卡码网:54.替换数字

题目344. 反转字符串 - 力扣&#xff08;LeetCode&#xff09; void reverseString(vector<char>& s) {int len s.size();int left 0;int right len - 1;while (left < right){swap(s[left], s[right--]);}return;} 题目541. 反转字符串 II - 力扣&#xff0…

基于SSM+Jsp的疫情居家办公OA系统

开发语言&#xff1a;Java框架&#xff1a;ssm技术&#xff1a;JSPJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包…

DP:解决路径问题

文章目录 二维DP模型如何解决路径问题有关路径问题的几个问题1.不同路径2.不同路径Ⅱ3.下降路径最小和4.珠宝的最高价值5.地下城游戏 总结 二维DP模型 二维动态规划&#xff08;DP&#xff09;模型是一种通过引入两个维度的状态和转移方程来解决复杂问题的技术。它在许多优化和…

SpringBoot: Eureka入门

1. IP列表 公司发展到一定的规模之后&#xff0c;应用拆分是无可避免的。假设我们有2个服务(服务A、服务B)&#xff0c;如果服务A要调用服务B&#xff0c;我们能怎么做呢&#xff1f;最简单的方法是让服务A配置服务B的所有节点的IP&#xff0c;在服务A内部做负载均衡调用服务B…

跟《经济学人》学英文:2024年6月22日这期 India’s electronics industry is surging

India’s electronics industry is surging Foreign and domestic firms are investing in local manufacturing surge:激增&#xff1b;急剧上升&#xff1b; 原文&#xff1a; To witness India’s growing role as a manufacturing hub, dodge Bangalore’s notorious t…

FreeBSD虚拟化解决之道:高效、安全、灵活的虚拟解决方案全览

FreeBSD下的虚拟化技术 虚拟化软件可让一台计算机同时运行多个操作系统。这种用于个人电脑的系统软件通常涉及一个运行虚拟化软件的宿主机&#xff08;host&#xff09;操作系统&#xff0c;并支持任何数量的客户机&#xff08;guest&#xff09;操作系统。 FreeBSD下的虚拟解…

惠海H6392 2.6v升5V 3.7V升9V 4.2V升12V 升压恒压芯片 小家电IC

惠海H6392升压恒压芯片是一款小家电、移动设备以及其他需要升压恒压电源的电子设备设计的DC-DC转换器。这款芯片以其独特的产品特性和广泛的应用场景&#xff0c;为电子产品设计者提供了高效、稳定的电源解决方案。 产品描述&#xff1a; H6392采用了简单的电流模式升压技术&a…

数据质量管理-时效性管理

前情提要 根据GB/T 36344-2018《信息技术 数据质量评价指标》的标准文档&#xff0c;当前数据质量评价指标框架中包含6评价指标&#xff0c;在实际的数据治理过程中&#xff0c;存在一个关联性指标。7个指标中存在4个定性指标&#xff0c;3个定量指标&#xff1b; 定性指标&am…

【漏洞复现】科立讯通信有限公司指挥调度管理平台uploadgps.php存在SQL注入

0x01 产品简介 科立讯通信指挥调度管理平台是一个专门针对通信行业的管理平台。该产品旨在提供高效的指挥调度和管理解决方案&#xff0c;以帮助通信运营商或相关机构实现更好的运营效率和服务质量。该平台提供强大的指挥调度功能&#xff0c;可以实时监控和管理通信网络设备、…

文件加密|电脑文件夹怎么设置密码?5个文件加密软件,新手必看!

电脑文件夹怎么设置密码&#xff1f;您是否希望更好地在电脑上保护您的个人或敏感文件&#xff1f;设置电脑文件夹密码是一种简单而有效的方式来确保你的隐私不被侵犯。通过使用文件加密软件&#xff0c;您可以轻松地为您的文件和文件夹设置密码保护。因此&#xff0c;本文将介…

4.x86游戏实战-人物状态标志位

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 本次游戏没法给 内容参考于&#xff1a;微尘网络安全 上一个内容&#xff1a;3.x86游戏实战-寄存器 人物状态标志位&#xff1a; 什么叫人物状态标志位&…

【机器学习】大模型训练的深入探讨——Fine-tuning技术阐述与Dify平台介绍

目录 引言 Fine-tuning技术的原理阐 预训练模型 迁移学习 模型初始化 模型微调 超参数调整 任务设计 数学模型公式 Dify平台介绍 Dify部署 创建AI 接入大模型api 选择知识库 个人主页链接&#xff1a;东洛的克莱斯韦克-CSDN博客 引言 Fine-tuning技术允许用户根…

pytorch-01

加载mnist数据集 one-hot编码实现 import numpy as np import torch x_train np.load("../dataset/mnist/x_train.npy") # 从网站提前下载数据集&#xff0c;并解压缩 y_train_label np.load("../dataset/mnist/y_train_label.npy") x torch.tensor(y…