【文档搜索引擎】在内存中构造出索引结构(下)

news2024/12/21 11:28:42

文章目录

  • 4.保存到磁盘中
    • 为什么要保存在磁盘中
    • 怎么保存
    • 操作步骤
      • 1. 前期准备
      • 2. 主要操作
  • 5. 将磁盘中的数据加载到内存中
  • Parser 类完整源码
  • Index 类完整源码

4.保存到磁盘中

为什么要保存在磁盘中

索引本来是存储在内存中的,为什么要将其保存在硬盘中?

  • 因为创建索引是比较耗时的

因此我们不应该在服务器启动的时候,才构建索引(启动服务器就可能会拖慢很多很多)

  • 通常的做法是:把这些耗时的操作,单独去进行执行
  • 单独执行完了之后,再让线上服务器直接加载这个构造好的索引

怎么保存

文本实质上就是字符串,我们就可以把字符串直接保存在文件中。我们就需要把内存中的索引结构变成一个“字符串”,然后写文件即可

  • 变成字符串的过程就是——序列化
  • 对应的特定结构的字符串,反向解析成一些结构化数据(类/对象/基础数据结构)——反序列化

序列化和反序列化有很多现成的通用方法,此处咱们就直接使用 JSON 格式来进行序列化/反序列化——jackson

  • 通过 Maven 仓库,引入依赖
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.18.2</version>
</dependency>

操作步骤

1. 前期准备

引入一个 jackson 里面会用到的核心对象

private ObjectMapper objectMapper = new ObjectMapper();
  • 之后就通过这个对象,完成后续的序列化和反序列化操作

创建一个文件指定存放的目录

private static final String INDEX_PATH =
"/Users/yechiel/Desktop/Byte/code_world/Gitee/java_doc_searcher";

2. 主要操作

使用两个文件,分别保存正排和倒排

  1. 先判定一下索引对应的目录是否存在,不存在就创建
  2. 然后在索引中分别创建两个文件:forwardIndexFile (正排文件)、invertedIndexFile (倒排文件)
  3. 使用 writeValue 方法,将文件进行写入
public void save(){  
    // 使用两个文件,分别保存正排和倒排  
    // 1. 先判断一下,索引对应的目录是否存在,不存在就创建  
    File indexPathFile = new File(INDEX_PATH);  
    if(!indexPathFile.exists()){  
        indexPathFile.mkdirs();  
    }  
    File forwardIndexFile = new File(INDEX_PATH + "fordword.txt");  
    File invertedIndexFile = new File(INDEX_PATH + "inverted.txt");  
    try {  
        // 第一个参数:写到哪个文件里    第二个:对哪个对象进行写入  
        objectMapper.writeValue(forwardIndexFile, forwardIndex);  
        objectMapper.writeValue(invertedIndexFile, invertedIndex);  
    }catch (IOException e) {  
        e.printStackTrace();  
    }  
}
  • mkdirs() 可以一次嵌套创建多级目录
  • writeValue 方法会报错,要在两个操作外面加上 try-catch。这里调用这个方法就不用我们再将文件变成字符串,然后再写入文件,这里直接进行写入就方便了很多

5. 将磁盘中的数据加载到内存中

public void load(){  
    System.out.println("加载索引开始!");  
    // 1. 设置加载索引的路径(和前面保存的路径一样)  
    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();  
    }  
    System.out.println("加载索引结束!");  
}
  • readValue 就会直接读取到文件内容,并且把文件内容按照这里指定的类型进行解析
    1. 看见这个类型是 ArrayList<>,然后就预期文件里面的 jason 也是代大括号的数组
    2. 然后看到每一个元素又是 DocInfo,我们的 readValue 就期望,我们的数据里面的大括号里面的每一个字段都得和 DocInfo 是相对应的
      • 这个对应关系我们是可以保证的,因为前面存入磁盘的时候,就是用 objectMapperwriteValue() 来去把对象生成 JSON 然后保存的
      • 生成的时候就是按照每一个属性名为 key 来去存的,所以下面解析的时候也是和上面相对应的,根据得到的 JSON 中的每一个 key 的值,来去找到对应对象中的属性,然后给其赋值

这里需要将这个这个结构的字符串,转换成一个 ArrayList<DocInfo> 类型的对象,jakson 专门提供了一个辅助工具类—— TypeReference<>

  • 这是一个带有泛型参数的类,我们通过这个类的泛型参数,来指定我们实际要转换的类型
forwardIndex = objectMapper.readValue
(forwardIndexFile, new TypeReference<ArrayList<DocInfo>>() {});
  • 这里相当于创建了一个匿名内部类的实例(后面 new 的部分)
    • 创建一个匿名内部类,这个类实现了 TypeReference
    • 同时再创建一个这个匿名内部类的实例
    • 创建这个实例的最主要目的,就是为了把 ArrayList<DocInfo> 这个类型信息,告诉 readValue 方法

java 中,并不能直接把一个类型作为方法的参数,而是必须得传一个具体的对象,正因为这个语法限制,我们就必须得绕一个弯。通过一个专门的泛型类,再搭配泛型参数,才能完成这个过程

Parser 类完整源码

package com.glg.javadoc_searcher;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;

public class Parser {
    // 先指定一个加载文档的路径

    private static final String INPUT_PATH = "/Users/yechiel/Desktop/Byte/code_world/docs";

    // 创建一个 Index 实例
    private Index index = new Index();

    public void run(){
        // 整个 Parser 的入口
        // 1. 根据指定的路径,枚举出该路径中所有的文件(HTML),这个过程需要把所有子目录中的文件都获取到
        ArrayList<File> fileList = new ArrayList<>();
        enumFile(INPUT_PATH, fileList);
        /*for(File file : fileList){
            System.out.println(file);
        }
        System.out.println(fileList.size());
*/

        // 2. 针对上面罗列出的文件路径,打开路径,读取文件内容,进行解析,并构建索引
        for(File f : fileList) {
            // 通过这个方法来解析单个 HTML 文件
            System.out.println("开始解析: "+ f.getAbsolutePath());
            parseHTML(f);
        }

        // 3. 把在内存中构造好的索引数据结构,保存到指定的文件中
        index.save();
    }


    private void parseHTML(File f) {
        // 1. 解析出 HTML 的标题
        String title = parseTitle(f);
        // 2. 解析出 HTML 对应的 URL
        String url = parseUrl(f);
        // 3. 解析出 HTML 对应的正文(有了正文才有后续的描述)
        String content = parseContent(f);
        // 4. 将解析出来的这些信息,加入到索引当中
        index.addDoc(title,url,content);
    }

    // 用来解析 HTML 里面的标题信息
    private String parseTitle(File f) {
        String name = f.getName();
        return name.substring(0, name.length() - ".html".length());
    }

    // 用来解析 HTML 里面的 URL 信息
    private String parseUrl(File f) {
        String part1 = "https://docs.oracle.com/javase/8/docs/";
        String part2 = f.getAbsolutePath().substring(INPUT_PATH.length());
        return part1 + part2;
    }

    // 用来解析 HTML 里面的正文信息
    public String parseContent(File f) {
        //先按照一个字符一个字符的方式来读取,以 < 和 > 来控制拷贝数据的开关
        StringBuilder content = new StringBuilder();
        try {
            FileReader fileReader = new FileReader(f);
            // 加上一个是否要进行拷贝的开关
            boolean isCopy = true;
            // 还得准备一个保存结果的 StringBuilder
            //StringBuilder content = new StringBuilder();
            while (true) {
                // 注意:此处的 read() 返回值是 int,不是 char
                // 按理说,应该是依次读一个字符,返回 char 就够了呀?
                // 此处使用 int 作为返回值,主要是为了表示一些非法情况
                // 比如说读到了文件末尾,继续读,就会返回 -1
                // 我们就可以根据返回的 -1 判断读完了
                int ret= fileReader.read();
                if(ret == -1) {
                    // 表示文件读完了
                    break;
                }
                // 这个结果不是 -1,那么就是一个合法的字符了
                char c = (char)ret;
                if(isCopy){
                    // 开关打开的状态,遇到普通字符就应该拷贝到 StringBuilder 中
                    if(c == '<'){
                        // 关闭开关
                        isCopy = false;
                        continue;
                    }
                    if(c == '\n' || c == '\r'){
                        // 为了去掉换行,把换行/回车替换成空格
                        c = ' ';
                    }
                    // 其他字符,直接进行拷贝即可,把结果拷贝到最终的 StringBuilder 中
                    content.append(c);
                }else {
                    // 开关关闭的状态,暂时不拷贝,直到遇到 >
                    if(c == '>'){
                        isCopy = true;
                    }
                }
            }
            fileReader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return content.toString();
    }


    // 第一个参数表示我们从哪个参数开始进行递归遍历
    // 第二个参数表示递归得到的结果
    private void enumFile(String inputPath, ArrayList<File> fileList) {
        File rootPath = new File(inputPath);
        // 把当前目录中,所包含的目录名全部获取到
        // listFiles 能够获取到 rootPath 当前目录下所包含的文件/目录(一层目录,不会进入子文件)
        File[] files = rootPath.listFiles();
        for(File f : files) {
            // 此时我们就根据当前 f 的类型,来决定是否要进行递归
            // 若 f 是一个普通文件,就把 f 加入到 fileList 结果中
            // 若 f 是一个目录,就递归调用 enumFile 方法,来进一步地获取子目录中的内容
            if(f.isDirectory()) {
                enumFile(f.getAbsolutePath(),fileList);
            }else {
                if (f.getAbsolutePath().endsWith(".html"))
                    fileList.add(f);
            }
        }

    }

    public static void main(String[] args) {
        // 通过 main 方法,来实现整个制作索引的过程
        Parser parser = new Parser();
        parser.run();
    }
}

Index 类完整源码

package com.glg.javadoc_searcher;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

// 通过这个类,在内存中构造索引结构
public class Index {

    private static final String INDEX_PATH = "/Users/yechiel/Desktop/Byte/code_world/Gitee/java_doc_searcher/";
    private ObjectMapper objectMapper = new ObjectMapper();

    // 使用数组下标表示 docId
    private ArrayList<DocInfo> forwardIndex = new ArrayList<>();

    // 使用一个 哈希表 来表示倒排索引
    // key 就是词     value 就是一簇和这个词相关的文章
    private HashMap<String, ArrayList<Weight>> invertedIndex = new HashMap<>();

    // 这个类要提供的方法
    // 1. 给定一个 docId,在正排索引中,查询文档的详细信息
    public DocInfo getDocInfo(int docId){
        return forwardIndex.get(docId);
    }


    // 2. 给定一个词,在倒排索引中,查询哪些文档和这个词关联
    // 仔细思考这里的返回值,单纯的返回一个整数的 List 是否可行呢?这样不太好(返回整数是因为 List 里面存的是文档 id)
    // 词和文档之间是存在一定的“相关性”的(文档和词的相关性有强有弱),不是单一的依次排列
    // 所以我们再创建一个 Weight 类来处理 文档id 和 文档与词 的相关性权重
    public List<Weight> getInverted(String term){
        return invertedIndex.get(term);
    }


    // 3. 往索引中新增一个文档
    public void addDoc(String title, String url, String content){
        // 新增文档操作,需要同时给正排索引和倒排索引新增信息
        // 构建正排索引
        DocInfo docInfo = buildForward(title, url, content);
        // 构建倒排索引
        buildInverted(docInfo);
    }

    // 实现倒排索引
    private void buildInverted(DocInfo docInfo) {
        // 直接使用内部类,词频统计
        class WordCnt {
            public int titleCount;
            public int contentCount;
        }
        // 通过一个内部类,将两个数据装到一起了,变成一个 HashMap,更方便遍历
        // 这个数据结构用来统计词频
        HashMap<String, WordCnt> wordCntHashMap = new HashMap<>();

        // 3.1 针对文档标题进行分词
        List<Term> terms = ToAnalysis.parse(docInfo.getTitle()).getTerms();

        // 3.2 遍历分词结果,统计每个词出现的次数
        for(Term term : terms){
            // 先判断一下 term 是否存在
            String word = term.getName();
            WordCnt wordCnt = wordCntHashMap.get(word);
            if(wordCnt == null) {
                // 如果不存在,就创建一个新的键值对,插入进去,titleCount 设为 1
                WordCnt newWordCnt = new WordCnt();
                newWordCnt.titleCount = 1;
                newWordCnt.contentCount = 0;
                wordCntHashMap.put(word, newWordCnt);
            }
                // 如果存在,就找到之前的值,然后把对应的 titleCount + 1
            wordCnt.titleCount++;
        }

        // 3.3 针对正文页进行分词
        terms = ToAnalysis.parse(docInfo.getContent()).getTerms();

        // 3.4 遍历分词结果,统计每个词出现的次数
        for(Term term : terms) {
            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++;
            }
        }

        // 3.5 把上面的结果汇总到一个 HashMap 里面
        //    最终文档的权重,就设定成标题中出现的次数 * 10 + 正文中出现的次数
        // 3.6 遍历刚才这个 HashMap,依次来更新倒排索引中的结构
        // 将 Map 转换成 Set 进行遍历(Map 不能直接进行遍历)
        for(Map.Entry<String, WordCnt> entry : wordCntHashMap.entrySet()) {
            // 先根据这里的词,去倒排索引中查一查
            // 倒排索引中的一个值——倒排拉链
            List<Weight> invertedList = invertedIndex.get(entry.getKey());
            // 判断是不是存在的(空的)
            if(invertedList == null) {
                // 如果为空,就插入一个新的键值对
                ArrayList<Weight> newInvertedList = new ArrayList<>();
                // 把新的文档(当前的 DocInfo)构造成 Weight 对象,插入进来
                Weight weight = new Weight();
                weight.setDocId(docInfo.getDocId());
                // 权重计算公式:标题中出现的次数 * 10 + 正文中出现的次数
                weight.setWeight(entry.getValue().titleCount * 10 + entry.getValue().contentCount);
                newInvertedList.add(weight);
                invertedIndex.put(entry.getKey(), newInvertedList);
            }else{
                // 如果非空,就把当前这个文档,构造出一个 Weight 对象,插入到倒排拉链的后面
                Weight weight = new Weight();
                weight.setDocId(docInfo.getDocId());
                // 权重计算公式:标题中出现的次数 * 10 + 正文中出现的次数
                weight.setWeight(entry.getValue().titleCount * 10 + entry.getValue().contentCount);
                invertedList.add(weight);
            }
        }
    }

    private DocInfo buildForward(String title, String url, String content) {
        DocInfo docInfo = new DocInfo();
        docInfo.setDocId(forwardIndex.size());
        docInfo.setTitle(title);
        docInfo.setUrl(url);
        docInfo.setContent(content);
        forwardIndex.add(docInfo);
        return docInfo;
    }

    // 4. 把内存中的索引结构保存到磁盘中
    public void save(){
        long beg = System.currentTimeMillis();
        // 使用两个文件,分贝保存正排和倒排
        System.out.println("保存索引开始!");
        // 先判断一下,索引对应的目录是否存在,不存在就创建
        File indexPathFile = new File(INDEX_PATH);
        if(!indexPathFile.exists()){
            indexPathFile.mkdirs();
        }
        File forwardIndexFile = new File(INDEX_PATH + "fordword.txt");
        File invertedIndexFile = new File(INDEX_PATH + "inverted.txt");
        try {
            // 第一个参数:写到哪个文件里    第二个:对哪个对象进行写入
            objectMapper.writeValue(forwardIndexFile, forwardIndex);
            objectMapper.writeValue(invertedIndexFile, invertedIndex);
        }catch (IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("保存索引完成!消耗时间为:" + (end - beg) + "ms");
    }

    // 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");
    }

}

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

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

相关文章

Flash语音芯片相比OTP语音芯片的优势

Flash语音芯片和OTP语音芯片是两种常见的语音解决方案&#xff0c;在各自的应用领域中发挥着重要作用。本文‌将介绍Flash语音芯片相比OTP(One-Time Programmable)语音芯片的显著优势‌。 1‌.可重复擦写‌&#xff1a;Flash语音芯片的最大特点是支持多次编程和擦除&#xff0c…

门店全域推广,线下商家营销布局的增量新高地

门店是商业中最古老的经营业态之一。很早就有行商坐贾的说法&#xff0c;坐贾指的就是门店商家&#xff0c;与经常做商品流通的「行商」相对应。 现在的门店经营&#xff0c;早已不是坐等客来&#xff0c;依靠自然流量吸引顾客上门&#xff0c;大部分的门店经营与推广都已经开…

NX系列-使用 `nmcli` 命令创建 Wi-Fi 热点并设置固定 IP 地址

使用 nmcli 命令创建 Wi-Fi 热点并设置固定 IP 地址 一、前言 在一些场景下&#xff0c;我们需要将计算机或嵌入式设备&#xff08;例如 NVIDIA Orin NX&#xff09;转换为 Wi-Fi 热点&#xff0c;以便其他设备&#xff08;如手机、笔记本等&#xff09;能够连接并使用该设备…

[react] <NavLink>自带激活属性

NavLink v6.28.0 | React Router 点谁谁就带上类名 当然类名也是可以自定义 <NavLinkto{item.link}className{({ isActive }) > (isActive ? 测试 : )}>{item.title}</NavLink> 有什么用?他会监听你的路由,刷新的话也会带上激活效果

【LC】100. 相同的树

题目描述&#xff1a; 给你两棵二叉树的根节点 p 和 q &#xff0c;编写一个函数来检验这两棵树是否相同。 如果两个树在结构上相同&#xff0c;并且节点具有相同的值&#xff0c;则认为它们是相同的。 示例 1&#xff1a; 输入&#xff1a;p [1,2,3], q [1,2,3] 输出&…

代码随想录day24 | leetcode 93.复原IP地址 90.子集 90.子集II

93.复原IP地址 Java class Solution {List<String> result new ArrayList<String>();StringBuilder stringBuilder new StringBuilder();public List<String> restoreIpAddresses(String s) {backtracking(s, 0, 0);return result;}// number表示stringb…

Hive是什么,Hive介绍

官方网站&#xff1a;Apache Hive Hive是一个基于Hadoop的数据仓库工具&#xff0c;主要用于处理和查询存储在HDSF上的大规模数据‌。Hive通过将结构化的数据文件映射为数据库表&#xff0c;并提供类SQL的查询功能&#xff0c;使得用户可以使用SQL语句来执行复杂的​MapReduce任…

AI智能决策赋能服装零售 实现精准商品计划与供需平衡

在服装这个典型的散对散供需模型中&#xff0c;库存问题一直是零售商面临的重大挑战。如何精准预测市场需求&#xff0c;实现供需平衡&#xff0c;成为摆在零售商面前的一道难题。然而&#xff0c;随着智能决策系统的应用&#xff0c;这一切正在悄然改变。 在这个信息爆炸的时代…

RadiAnt DICOM - 基本主题 :从 PACS 服务器打开研究

正版序列号获取&#xff1a;https://r-g.io/42ZopE RadiAnt DICOM Viewer PACS 客户端功能允许您从 PACS 主机&#xff08;图片存档和通信系统&#xff09;搜索和下载研究。 在开始之前&#xff0c;您需要确保您的 PACS 服务器和 RadiAnt 已正确配置。有关配置说明&#xff0c…

VR虚拟展馆如何平衡用户隐私保护与数据收集?

在虚拟现实&#xff08;VR&#xff09;虚拟展馆的设计和运营中&#xff0c;用户隐私保护与数据收集之间的平衡是一个至关重要的议题。 接下来&#xff0c;由专业从事VR虚拟展馆制作的圆桌3D云展厅平台为大家介绍一些策略&#xff0c;可以帮助VR虚拟展馆在收集有用数据的同时&a…

46.全排列 python

全排列 题目题目描述示例 1&#xff1a;示例 2&#xff1a;示例 3&#xff1a;提示&#xff1a; 题解解决方案&#xff1a;回溯算法思路&#xff1a;Python 实现&#xff1a;复杂度分析&#xff1a; 提交结果 题目 题目描述 给定一个不含重复数字的数组 nums &#xff0c;返回…

在Win11系统上安装Android Studio

诸神缄默不语-个人CSDN博文目录 下载地址&#xff1a;https://developer.android.google.cn/studio?hlzh-cn 官方安装教程&#xff1a;https://developer.android.google.cn/studio/install?hlzh-cn 点击Next&#xff0c;默认会同时安装Android Studio和Android虚拟机&#…

基于字节大模型的论文翻译(含免费源码)

基于字节大模型的论文翻译 源代码&#xff1a; &#x1f44f; star ✨ https://github.com/boots-coder/LLM-application 展示 项目简介 本项目是一个基于大语言模型&#xff08;Large Language Model, LLM&#xff09;的论文阅读与翻译辅助工具。它通过用户界面&#xff08…

密钥.id文件连接SSH

不用设置密码&#xff0c;直接连接

run postinstall error, please remove node_modules before retry!

下载 node_modules 报错&#xff1a;run postinstall error, please remove node_modules before retry! 原因&#xff1a;node 版本出现错误&#xff0c;我的项目之前是在 12 下运行的。解决方法&#xff1a; 先卸载node_modules清除缓存将node版本切换到12重新下载即可

【ETCD】【实操篇(二)】如何从源码编译并在window上搭建etcd集群?

要在 Windows 上编译 etcd 及 etcdctl 工具&#xff0c;并使用 bat 脚本启动 etcd 集群&#xff0c;首先需要准备好开发环境并确保依赖项正确安装。下面是从 etcd 3.5 源码开始编译和启动 etcd 集群的详细步骤&#xff1a; 目录 1. 安装 Go 环境2. 获取 etcd 源码3. 编译 etcd…

双指针---和为s的两个数字

这里写自定义目录标题 题目链接问题分析代码解决执行用时 题目链接 购物车内的商品价格按照升序记录于数组 price。请在购物车中找到两个商品的价格总和刚好是 target。若存在多种情况&#xff0c;返回任一结果即可。 问题分析 暴⼒解法&#xff0c;会超时 &#xff08;两层…

处理 Audio PCM 数据24位偏移问题

在音频处理过程中&#xff0c;我们有时会遇到特殊的问题&#xff0c;例如某些WAV文件的PCM数据发生了位移&#xff0c;导致声音播放异常。最近&#xff0c;我遇到了一个具体的问题&#xff0c;48000&#xff0c;32bit&#xff0c;8ch的PCM数据每32位&#xff08;4字节&#xff…

【大模型】GraphRAG技术原理

核心概念 GraphRAG 的核心在于用大模型构建知识图谱知识图谱聚类社区化RAG RAG就是输入&#xff08;问题知识&#xff09;到大模型 1-大模型自动从海量数据中构建知识图谱&#xff08;提取合并实体关系&#xff09; 2-聚类算法从知识图谱中聚类社区并生成社区摘要 3-输入问题…

Vue与React:前端框架的巅峰对决

文章目录 一、引言&#xff08;一&#xff09;前端框架发展现状简述 二、Vue 与 React 框架概述&#xff08;一&#xff09;Vue.js 简介&#xff08;二&#xff09;React.js 简介 三、开发效率对比&#xff08;一&#xff09;Vue 开发效率分析&#xff08;二&#xff09;React …