手把手教你用springboot实现jdk文档搜索引擎

news2024/9/27 5:45:09

目录

项目背景

项目描述

项目整体架构

 项目流程

构建索引


项目背景

        搜索引擎是我们经常会用到的工具,例如我们熟知的百度,谷歌等搜索引擎。除了网络搜索引擎,还有很多地方也有搜索引擎的身影,例如视频网站的搜索框,手机的应用搜索功能。搜索引擎是一个很有用的工具,在数据量很大的时候,使用搜索引擎搜索能极大的提高效率,因此我想到了 JAVA 开发者们经常会用到的 JDK 文档。文档的内容很多,数量高达上万篇,因此当我们想查找一个东西的时候想找到对应的文档很难,因此我们可以写一个搜索引擎来快速的查找到我们想要的文档。

项目描述

        打开浏览器,在搜索框中输入我们想要查找的关键词,点击搜索就能查找到 JDK 文档中所有与关键词有关的文档。但是我们无法搜索到 JDK 文档以外的信息,因为我们只针对 JDK 文档建立的搜索功能。

项目整体架构

 项目流程

构建索引

1.首先创建一个 springboot 项目

 

 

2.配置数据库

找到 resource 下的 application.application (也可以改成 application.yml) 配置文件配置数据库

3.扫描所有的jdk文档

在 indexer 下新建 util 包用来存放工具类,再在 util 包下新建 FileScanner 类,实现扫描 JDK 文档的功能。我们需要在配置文件中定义文档的根目录,FileScanner 就能扫描出根目录中的所有 html 文档。

package com.yukuanyan.indexer.util;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

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

@Service
@Slf4j
public class FileScanner {
    public List<File> scanFile(String rootPath){
        List<File> finalFileList = new ArrayList<>();

        File rootFile = new File(rootPath);
        //说明对应路径的的文件不存在
        if (rootFile == null) {
            return finalFileList;
        }
        //进行遍历
        traversal(rootFile,finalFileList);
        return finalFileList;
    }

    private void traversal(File rootFile,List<File> fileList) {
        // 获取所有的文件和文件夹,得到一个 FileList
        File[] files = rootFile.listFiles();
        // 一般是权限问题,一般不会碰到
        if (files == null) {
            return ;
        }

        // 遍历 FileList,如果是 html 文件就保存到list中,如果是文件夹就继续递归遍历
        for (File file : files) {
            if (file.isFile() && file.getName().endsWith(".html")) {
                fileList.add(file);
            } else {
                traversal(file,fileList);
            }
        }
    }
}

4.生成正排索引和倒排索引

正排索引的结构:key-value key是文档id,value是标题,url和内容。

倒排索引的结构:key-value key 是一个单词,value是一个list,list里面是一个 倒排记录对象,对象中有 单词,文档id和权重,表示在id为……的文档中,某某单词的权重是多少

因此,我只要们拿到所有的 files 就能获得正排索引,docId就是数据库自增id

而倒排索引是一个自定义对象(及在id为……的文档中,某某单词的权重是多少)的集合,在这里一条倒排索引称之为InvertedRecord(倒排记录)

创建Document类:在indexer 包下新建 model 包,在model 包下新建 Document 类。当前类是我们对 html 文档的抽象,用于将磁盘中的文件加载到内存中并且提取出构建索引需要的内容

因为每个文档对象都需要被分词和计算权重,所以每一个 Document 对象都需要有分词和计算权重的方法(segWordsAndCalcWeight。我们先分别分词和统计标题单词的出现次数正文单词的出现次数。

再根据权重计算公式计算出当前 document 每个单词对应的权重(权重 = 标题权重 + 正文权重),至此当前 document 的所有单词的权重已经生成好了,将他们保存在 map 中。

完整代码:

Document类:

package com.yukuanyan.indexer.model;

import lombok.Data;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.ansj.domain.Result;
import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;

import java.io.*;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Data
public class Document {
    private Integer docId;
    //文档的标题
    private String title;
    //文档对应的url
    private String url;
    //文档的正文部分
    private String content;

    //由于分词结果中会出现这些没有意义的字符,忽略分词结果中的这些字符
    private final static HashSet<String> ignoredWordSet = new HashSet<>();
    static {
        ignoredWordSet.add(" ");
        ignoredWordSet.add("\t");
        ignoredWordSet.add("。");
        ignoredWordSet.add(".");
        ignoredWordSet.add(",");
        ignoredWordSet.add("(");
        ignoredWordSet.add(")");
        ignoredWordSet.add("/");
        ignoredWordSet.add("-");
        ignoredWordSet.add(";");
    }

    public Document(File file,String urlPrefix,File rootFile){
        this.title = parseTitle(file);
        this.url = parseUrl(file,urlPrefix,rootFile);
        this.content = parseContent(file);
    }

    // 解析正文
    @SneakyThrows
    private String parseContent(File file) {
        StringBuilder contentBuilder = new StringBuilder();

        try (InputStream is = new FileInputStream(file)) {
            try (Scanner scanner = new Scanner(is, "ISO-8859-1")) {
                while (scanner.hasNextLine()) {
                    String line = scanner.nextLine();
                    contentBuilder.append(line).append(" ");
                }

                // 利用正则表达式去除正文中的 html 标签
                return contentBuilder.toString()
                        .replaceAll("<script.*?>.*?</script>", " ")
                        .replaceAll("<.*?>", " ")
                        .replaceAll("\\s+", " ")
                        .trim();
            }
        }
    }

    @SneakyThrows
    private String parseUrl(File file, String urlPrefix, File rootFile) {
        // 需要得到一个相对路径,file 相对于 rootFile 的相对路径
        // 比如:rootFile 是 C:\Users\秋叶雨\Downloads\docs\api\
        //      file 是     C:\Users\秋叶雨\Downloads\docs\api\java\ util\TreeSet.html
        // 则相对路径就是:java\ util\TreeSet.html
        // 把所有反斜杠(\) 变成正斜杠(/)
        // 最终得到 java/sql/DataSource.html

        String rootPath = rootFile.getCanonicalPath();
        rootPath = rootPath.replace("/", "\\");
        if (!rootPath.endsWith("\\")) {
            rootPath = rootPath + "\\";
        }

        String filePath = file.getCanonicalPath();
        String relativePath = filePath.substring(rootPath.length());
        relativePath = relativePath.replace("\\", "/");

        return urlPrefix + relativePath;
    }

    private String parseTitle(File file) {
        // 从文件名中,将 .html 后缀去掉,剩余的看作标题
        String name = file.getName();
        String suffix = ".html";
        return name.substring(0, name.length() - suffix.length());
    }

    //对当前 document 进行 分词 和 计算权重
    public Map<String,Integer> segWordsAndCalcWeight() {
        //首先对标题进行分词,得到一个 titleWordlist
        Result parseResultOfTitle = ToAnalysis.parse(title);
        List<String> titleWordList = parseResultOfTitle
                .getTerms()
                .stream()
                .parallel()
                .map(Term::getName)
                .filter(s -> !ignoredWordSet.contains(s))
                .collect(Collectors.toList());

        //统计标题中每个 word 出现的次数,并且保存在 titleWordCountMap 中
        HashMap<String,Integer> titleWordCountMap = new HashMap<>();
        for (String word : titleWordList) {
            int count = titleWordCountMap.getOrDefault(word,0);
            titleWordCountMap.put(word,count + 1);
        }

        //对正文进行分词,的到一个 contentWordList
        Result parseResultOfContent = ToAnalysis.parse(content);
        List<String> contentWordList = parseResultOfContent
                .getTerms()
                .stream()
                .parallel()
                .map(Term::getName)
                .filter(s -> !ignoredWordSet.contains(s))
                .collect(Collectors.toList());
        // 统计正文中每个 word 出现的次数,并且保存在 contentWordCountMap 中
        HashMap<String,Integer> contentWordCountMap = new HashMap<>();
        for (String word : contentWordList) {
            int count = contentWordCountMap.getOrDefault(word,0);
            contentWordCountMap.put(word,count + 1);
        }

        // 这里我们已经拿到了标题和正文中所有 word 分别出现的次数
        // 计算所有 word 的权重,将结果保存到一个 map 中
        HashMap<String,Integer> wordWeight = new HashMap<>();
        // document 中所有 word 的一个集合
        HashSet<String> documentWordSet = new HashSet<>();
        // 我们已经拿到了 title 和 context 的 wordlist ,接下来只需要全部放入一个set容器中进行去重
        documentWordSet.addAll(titleWordList);
        documentWordSet.addAll(contentWordList);

        for (String word : documentWordSet) {
            // 标题部分的权重
            int titleWeight = titleWordCountMap.getOrDefault(word,0) * 10;
            // 正文部分的权重
            int contentWeight = contentWordCountMap.getOrDefault(word,0);
            // 这个 word 在整个部分的权重
            int weight = titleWeight + contentWeight;
            // 将结果加入集合中
            wordWeight.put(word,weight);
        }

        return wordWeight;
    }
}

InvertedRecord类:

package com.yukuanyan.indexer.model;

import lombok.Data;

@Data
public class InvertedRecord {
    //表示 word 在文章号为 docId 的文章中权重为 weight
    private String word;
    private Integer docId;
    private Integer weight;

    public InvertedRecord(String word,Integer docId,Integer weight) {
        this.word = word;
        this.docId = docId;
        this.weight = weight;
    }
}

5.保存正排索引和倒排索引

由于文档的数量较多,正排索引的数量 在1w左右,倒排索引的数量在百万级比,因此在插入数据库的时候不加任何优化会很慢,因此采用了多线程+批量插入数据库的优化。

我们首先需要创建一个线程池,在indexer 包下新建config 包,在config 包下创建 AppConfig 类。AppConfig 类是 spring 容器中的 生产者,因此我们给类加上 @Configuration ,给方法加上@Bean,我们需要线程池,因此返回类型是 ExecutorService

 我们还需要将索引保存到数据库中。使用mybatis框架进行数据库操作需要 Mapper 接口和 对应的 Mapper.xml,因此在 indexer 包下新建 mapper 包,在mapper 包下新建 IndexerMapper 接口,在接口内定义插入正排索引和倒排索引的方法

package com.yukuanyan.indexer.mapper;

import com.yukuanyan.indexer.model.Document;
import com.yukuanyan.indexer.model.InvertedRecord;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

@Mapper
@Repository
public interface IndexMapper {
    //批量插入正排索引
    public void batchInsertForwardIndexes(@Param("list") List<Document> documentList);

    //批量插入倒排索引
    public void batchInsertInvertedIndexes(@Param("list") List<InvertedRecord> recordsList);
}

 在 resource 下新建 mapper ,在mapper 下新建Mapper.xml。同时在application.yml 中配置 Mapper.xml 的 路径

 之后我们就可以在Mapper.xml 内写 sql 语句了

一个 mapper.xml 对应一个 接口,因此需要在 mapper.xml 中配置对应接口

 批量插入正排索引的标签。由于需要获取docid因此我们需要自增id。

 批量插入倒排索引的标签,和正排索引类似

 在indexer 包下 新建 core 包,在core包下新建 IndexerManager 类,这个类用来批量保存索引,会用到线程池 和 数据库

 主要功能:保存正排索引和倒排索引。由于插入数据库不需要返回值,所以我们  继承 Runnable 接口 再提交到线程池中执行。

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

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

相关文章

Windows系统下使用mingw32编译curl-7.87.0办法

使用工具&#xff1a;Windows10QT5.14.2CMake (cmake-gui)curl-7.87.0 编译办法&#xff1a; 1、下载CURL源码&#xff1a;curl - Download&#xff0c;解压缩zip文件到指定路径下&#xff08;如&#xff1a;D:\QTCode\curl-7.87.0&#xff09; 2、新增环境变量&#xff0c;打…

为什么说IO密集型业务,线程数是CPU数的2倍?

I/O密集型业务&#xff0c;线程数量要设置成 CPU 的 2 倍&#xff01; 也不知道这是哪本书的坑爹理论&#xff0c;现在总有一些小青年老拿着这样的定理来说教。说的信誓旦旦&#xff0c;毋庸置疑&#xff0c;仿佛是权威的化身。讨论时把这样的理论当作前提&#xff0c;真的是受…

MySQL复制底层技术——单线程复制、DATABASE并行复制

1. 单线程复制 单线程复制是MySQL最早出现的主从复制技术&#xff0c;本节我们将对单线程复制做进一步说明。 在MySQL5.6之前的版本中&#xff0c;从库复制不支持多线程&#xff0c;所以当主库写压力稍微大一点时&#xff0c;从库就会出现复制延迟。当然&#xff0c;目前的最…

网络音频广播RtpCast软件

RtpCast是一款基于Windows平台运行的网络音频广播软件。这款RTPCast软件可以以目标分组的方式播放电脑系统声卡&#xff08;麦克风、喇叭和音频混合器&#xff09;、MP3文件列表和网络Rtp音频流等音源到终端设备。此外&#xff0c;RtpCast网络音频广播软件支持方案调度&#xf…

【区块链 | EVM】深入理解学习EVM - 深入Solidity数据位置:Calldata

深入了解Solidity数据位置 - Calldata 原文链接: https://betterprogramming.pub/solidity-tutorial-all-about-calldata-aebbe998a5fc理解Solidity中以太坊交易的 "data" 字段 这是 深入Solidity数据存储位置 系列的第三篇 今天,我们将学习 calldata 的特殊性,以…

springboot项目使用SchedulingConfigurer实现多个定时任务

目录一、引入依赖二、配置文件属性配置三、代码目录结构四、示例代码4.1、定义 定时任务基础接口4.2、定义 定时任务一&#xff08;每天几点几分执行&#xff09;4.3、定义 定时任务二&#xff08;每几分钟执行一次&#xff09;4.4、定义 定时任务注册器4.5、运行springboot项目…

欧拉系统部署NextCloud与常见部署问题解决

欧拉系统部署NextCloud与常见部署问题解决一、欧拉系统安装二、openEuler安装图形界面Ukui三、yum安装的npm包进行本地保存设置&#xff08;个人任务需要&#xff09;四、部署nextCloud4.1构建LAMP环境基础4.1.1开启httpd,防火墙端口号4.1.2开启MariaDB服务4.1.3安装并测试php4…

2023/1/4总结

今天AC了三个题目&#xff1a; 第一个题目&#xff1a;P4913 【深基16.例3】二叉树深度 (1条消息) P4913 【深基16.例3】二叉树深度_lxh0113的博客-CSDN博客 第二个题目&#xff1a;P1229 遍历问题 (1条消息) P1229 遍历问题_lxh0113的博客-CSDN博客 第三个题目&#xff1…

药品市场信息查询-药品数据库(全面)

药品市场信息包含了药品招标、药品投标、药品集采、药品销售数据&#xff08;医院、零售&#xff09;、药品海关进出口数据、药品交易&#xff08;药品license in/out&#xff09;、价格、一致性评价、政策法规、药品公司等多个方面的数据信息&#xff0c;是医药行业市场信息工…

双向循环链表的讲解及实现(图解+代码/C语言)

本次为大家分享的是双向循环链表的增删查改等系列操作。 目录 一、图解双向循环链表结构 二、分步实现 &#xff08;1&#xff09;创建并初始化 &#xff08;2&#xff09;链表元素打印 &#xff08;3&#xff09;头插和尾插 &#xff08;4&#xff09;判断链表为空 &a…

MySQL调优-MySQL索引优化实战一

目录 MySQL调优-MySQL索引优化实战一 插入数据&#xff1a; 举一个大家不容易理解的综合例子&#xff1a; 1、联合索引第一个字段用范围不会走索引 2、强制走索引 什么是回表&#xff1f;为什么要回表&#xff1f;如何进行回表&#xff1f; 但是回表具有很大的弊端&#…

NetInside网络分析帮您解决系统性能问题(二)

前言 某大学信息中心负责人表示&#xff0c;有用户反馈&#xff0c;在通过VPN访问某一IP的80端口时连接时断时续。同时信息中心给到的信息是通过VPN&#xff1a;XXX.XXX.253.5访问IP地址XXX.XXX.130.200的80端口出现访问时断时续问题。 前一文章我们分析了系统整体性能分析&a…

学编程有哪些误区吗?避坑指南拿去不谢!

学习编程时信心满满&#xff0c;但反而效率不高&#xff0c;从“入门”到“放弃”&#xff0c;你肯定猜中了这些误区&#xff01; 今天就专门写了一篇避坑指南&#xff0c;提前避开误区&#xff0c;有助于更好学习编程。 误区1&#xff1a;忽略基础&#xff0c;好高骛远 现在…

@Column写在属性和写在get方法上面的区别

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是「奇点」&#xff0c;江湖人称 singularity。刚工作几年&#xff0c;想和大家一同进步&#x1f91d;&#x1f91d; 一位上进心十足的【Java ToB端大厂…

【YOLOv7/YOLOv5系列改进NO.51】融入多分支空洞卷积结构RFB-Bottleneck改进PANet构成新特征融合网络

文章目录前言一、解决问题二、基本原理三、​添加方法四、总结前言 作为当前先进的深度学习目标检测算法YOLOv7&#xff0c;已经集合了大量的trick&#xff0c;但是还是有提高和改进的空间&#xff0c;针对具体应用场景下的检测难点&#xff0c;可以不同的改进方法。此后的系列…

axios中get、post请求传参区别及使用

axios 发送请求时 params 和 data 的区别 params 中的参数是通过地址栏传参&#xff0c;一般用于get请求data 是添加到请求体&#xff08;body&#xff09;中的&#xff0c; 一般用于post请求get请求只能传query参数&#xff0c;query参数都是拼在请求地址上的post可以传body和…

qt使用qxlsx实现xlsx、xls表格文件快速写入和读取

一、前言 本片文章主要记录和分享一下qt使用qxlsx开源文件读写xlsx表格文件用法。 目录一、前言二、环境三、正文1.读取指定xlsx文件2.保存xlsx文件3.保存xlsx文件内容过大崩溃解决方案一4.保存xlsx文件内容过大崩溃解决方案二四、结语二、环境 windows linux qt5.7 三、正文…

【财务】FMS财务管理系统---对账平台

人工进行对账工作是非常繁杂的&#xff0c;此时&#xff0c;就非常有必要建设一个对账平台。笔者在本文介绍了对账平台的相关内容&#xff0c;分享给大家。 前面介绍过应收对账、财务应付结算两部分内容&#xff1b;应收对账主要是调用第三方支付的接口获取支付流水信息与我司的…

C++设计模式:三种工厂模式详解(简单工厂,工厂模式,抽象工厂)

文章目录简单工厂模式简单工厂实现步骤简单工厂优缺点工厂模式工厂模式和简单工厂模式有什么不同&#xff1f;工厂模式实现步骤实现代码工厂模式优缺点抽象工厂模式抽象工厂模式实现步骤实现代码抽象工厂模式优缺点简单工厂模式 简单工厂模式属于类的创建型模式,又叫做静态工厂…

【算法】算法分析技术(第一章习题解答)

1 算法分析技术 1.1 假设 fff 和 ggg 是定义在自然数集合上的函数, 若对某个其他函数 hhh 有 fO(h)f O(h)fO(h)和 gO(h)g O(h)gO(h) 成立, 那么证明 fgO(h)f g O(h)fgO(h) 证明&#xff1a; 根据已知条件 fO(h)f O(h)fO(h)&#xff0c;存在 c1>0c_{1}>0c1​>0 …