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

❣博主主页: 33的博客

▶️文章专栏分类:项目日记◀️

🚚我的代码仓库: 33的代码仓库🚚

🫵🫵🫵关注我带你了解更多项目内容

目录

1.前言

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

2.索引结构

创建index类,通过这个类来构建索引结构

基本步骤:

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

2.1创捷索引

正排索引

java 复制代码
private ArrayList<DocInfo> forwardIndex=new ArrayList<>();

倒排索引

java 复制代码
private HashMap<String,ArrayList<Weight>> invertedIndex=new HashMap<>();

DocInfo类:

java 复制代码
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类:

java 复制代码
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根据索引查询

java 复制代码
//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新增文档

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

在正排索引中添加文档:

java 复制代码
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+正文次数

java 复制代码
 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来完成此操作。

java 复制代码
 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把磁盘索引加载到内存

java 复制代码
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文件时,应添加到索引中

java 复制代码
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);         
    }

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

java 复制代码
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对象放入阻塞队列中)。

把代码改进成多线程的版本,线程池中的线程数目具体设置成多少才合适呢?最好通过实验来确定。

java 复制代码
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()操作。

java 复制代码
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.总结

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

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

相关推荐
Swift社区1 小时前
在 Swift 中实现字符串分割问题:以字典中的单词构造句子
开发语言·ios·swift
没头脑的ht1 小时前
Swift内存访问冲突
开发语言·ios·swift
没头脑的ht1 小时前
Swift闭包的本质
开发语言·ios·swift
wjs20241 小时前
Swift 数组
开发语言
吾日三省吾码2 小时前
JVM 性能调优
java
stm 学习ing2 小时前
FPGA 第十讲 避免latch的产生
c语言·开发语言·单片机·嵌入式硬件·fpga开发·fpga
湫ccc3 小时前
《Python基础》之字符串格式化输出
开发语言·python
弗拉唐3 小时前
springBoot,mp,ssm整合案例
java·spring boot·mybatis
oi774 小时前
使用itextpdf进行pdf模版填充中文文本时部分字不显示问题
java·服务器