【Spring AI】基于SpringAI+Vue3+ElementPlus的QA系统实现(后端)

整理不易,请不要吝啬你的赞和收藏。

1. 前言

这篇文章将介绍如何基于 RAG 技术,使用 SpringAI + Vue3 + ElementPlus 实现一个 Q&A 系统。本文使用 deepseek 的 DeepSeek-V3 作为聊天模型,使用阿里百炼的 text-embedding-v3 作为向量模型,使用 redis 作为向量库。(PS:近期阿里百炼也上架了DeepSeek-V3  DeepSeek-R1 模型供开发者调用,如果觉得 DeepSeek 官方 API 比较慢的话,可以去试试)。

什么是 RAG

RAG(Retrieval-Augmented Generation,检索增强生成)技术是一种结合了检索(Retrieval)和生成(Generation)的自然语言处理方法。它通过检索相关的文档片段来增强语言模型的生成能力,从而提高生成文本的质量和相关性。RAG技术的核心思想是利用检索系统从大规模文档集合中找到与输入问题最相关的文档片段,然后将这些片段作为上下文信息输入到生成模型中,生成更加准确和详细的回答。

注:以下几行内容节选自 spring ai alibaba RAG 部分说明文档。

RAG 的工作流程分为两个阶段:Indexing pipeline 和 RAG 。

  • Indexing pipeline 阶段主要是将结构化或者非结构化的数据或文档进行加载和解析、chunk切分、文本向量化并保存到向量数据库。

  • RAG 阶段主要包括将 promp t文本内容转为向量、从向量数据库检索内容、对检索后的文档 chunk 进行重排和 prompt 重写、最后调用大模型进行结果的生成。

下面是大致流程:

2. 效果展示

让聊天模型基于我上传的文件,进行回答。

3. 设计思路

整个 Q&A 功能的时序图:

4. 前提条件

  • 已有自己的向量库,如 Redis、Faiss。想了解更多参考我的博客:

    【LLM】Redis 作为向量库入门指南_redis 向量数据库-CSDN博客文章浏览阅读1k次,点赞25次,收藏12次。这篇文章将介绍基于 RedisSearch 的Redis向量库实现。通过阅读本篇文章,你将学习到如何创建向量索引,如何存储和更新向量,如何进行向量搜索,如何使用阿里百炼 Embedding Model 文本向量化,如何集成到 spring boot 中并实现向量的存储和搜索等。_redis 向量数据库 https://blog.csdn.net/u013176571/article/details/145122380
  • 已安装 18.3 或更高版本的 Node.js ,未安装进入 官网下载 ,LTS 为长期支持版,Current 为最新功能版。

  • 使用过Spring AI,可参考我的这篇博客:

    【Spring AI】Spring AI Alibaba的简单使用_spring-ai-alibaba-starter-CSDN博客文章浏览阅读696次,点赞7次,收藏6次。项目中引入Spring AI、Spring AI Alibaba踩坑笔记,并实现简单的几种聊天模式。_spring-ai-alibaba-starter https://blog.csdn.net/u013176571/article/details/144488475

需要注意:写这篇文章的时候,我对 spring-ai-core 版本进行了升级,版本为:1.0.0-M5。并且舍弃了 spring-ai-alibaba-starter 包,改为引入使用更广泛的 spring-ai-openai 包,这些修改意味着之前写的文章中的部分代码要做相应的修改,下文将会介绍。

5. 引入 Spring AI

5.1 pom 文件配置

spring-ai 版本为 1.0.0-M5。

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-core</artifactId>
    <version>${spring-ai-core.version}</version>
</dependency>

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai</artifactId>
    <version>${spring-ai-core.version}</version>
</dependency>

由于 spring-ai 相关依赖包还没有发布到中央仓库,需要在项目的 pom.xml 依赖中加入如下仓库配置。

<repositories>
    <repository>
        <id>maven2</id>
        <name>maven2</name>
        <url>https://repo1.maven.org/maven2/</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

maven 的 setting.xml 中做出如下更改

<mirror>  
    <id>alimaven</id>  
    <name>aliyun maven</name>  
    <url>https://maven.aliyun.com/repository/public</url> 
    <!-- 表示除了spring-milestones、maven2其它都走阿里云镜像  --> 
    <mirrorOf>*,!spring-milestones,!maven2</mirrorOf>  
</mirror>

5.2 application.yml 配置

聊天模型基于 deepseek,‘your-api-key’ 为 deepseek 上申请的api-key,base-url 设置为 deepseek 的接口 url。

spring:
  ai:
    openai:
      api-key: your-api-key
      base-url: https://api.deepseek.com
      chat:
        model: deepseek-chat
    chat:
      client:
        enabled: false

6. 接口开发

由于我们使用的是 spring-ai-openai 依赖,依赖中的 ChatModel 、 EmbeddingModel 等 Bean 默认配置都是基于 OpenAI 的模型,所以我们都需要根据自己使用的模型重新注册这些 Bean。

6.1 注册 EmbeddingModel

从上文的 RAG 流程中我们知道,实现 Q&A 系统的第一步是将知识库的资源向量化(也就是 Embedding)。

我这里使用阿里百炼的嵌入模型,下图是其主要文本向量模型对比:

这篇文章使用 text-embedding-v3,需要新注册一个 EmbeddingModel Bean。

@Bean
public EmbeddingModel embeddingModel() {
    return new OpenAiEmbeddingModel(
            // embeddingBaseUrl 为阿里百炼的接口 url ,值为:https://dashscope.aliyuncs.com/compatible-mode
            // embeddingApiKey 为阿里百炼上申请的 api-key 。
            new OpenAiApi(embeddingBaseUrl, embeddingApiKey),
            MetadataMode.EMBED,
            OpenAiEmbeddingOptions.builder()
                    .model("text-embedding-v3")  // 选用的嵌入模型
                    .dimensions(1024)            // 向量维度
                    .build(),
            RetryUtils.DEFAULT_RETRY_TEMPLATE);
}

6.2 注册 ChatModel

我使用 DeepSeek 的 DeepSeek-V3 作为聊天模型,这里需要重新注入 ChatModel Bean。

@Bean
public ChatModel chatModel() {
    // baseUrl 为 DeepSeek 接口 url,值为:https://api.deepseek.com
    // apiKey 为申请的秘钥,defaultChatModel 为聊天模型。
    return new OpenAiChatModel(new OpenAiApi(baseUrl, apiKey), OpenAiChatOptions.builder()
            .model("deepseek-chat")    // 选用的聊天模型
            .temperature(0.7d)         // 
            .build());
}

6.3 注册 VectorStore

我的这篇文章

【LLM】RedisSearch 向量相似性搜索在 SpringBoot 中的实现_jedis ftsearch-CSDN博客文章浏览阅读762次,点赞11次,收藏11次。这篇文章将介绍两种实现方式,第一种为使用Jedis中的UnifiedJedis类实现,第二种为使用SpringAI中的VectorStore实现。通过这边文章你将收获,如何使用阿里百炼Embedding模型实现文本向量化,如何通过连接池获取UnifiedJedis对象,如何在SpringBoot中实现向量数据的存储以及使用fTSearch进行向量相似性搜索,如何使用SpringAI的VecotStore。_jedis ftsearch https://blog.csdn.net/u013176571/article/details/145235761

做过详细介绍,这里增加两个元数据配置 docId(文档Id) 和 docName(文档名),用于在回答的时候进行过滤。

@Bean
public VectorStore vectorStore() {
    return RedisVectorStore.builder(jedisPooled, embeddingModel)
            .indexName(indexName)
            .prefix(prefix)
            .embeddingFieldName("embedding")      // 向量字段名,默认 embedding
            .contentFieldName("content")          // 存储的原始文本内容
            .initializeSchema(initializeSchema)   // 是否初始化索引配置信息
            .batchingStrategy(batchingStrategy)
            .metadataFields(
                    RedisVectorStore.MetadataField.text(SpringAIConst.VectorStore.METADATA_DOC_ID),   // 存储的元数据信息
                    RedisVectorStore.MetadataField.text(SpringAIConst.VectorStore.METADATA_DOC_NAME)  // 存储的元数据信息
            ).build();
}

6.4 文档 Embedding

文本文档 Embedding 之前要先对文本文档进行分割,以满足大模型接口的输入字符数或者 Token 限制。

6.4.1 文档分割

文本分割的算法有:字符分割、递归字符文本分割、语义文档分割等等,如果有需要我将在后续文章中介绍这些算法。

本文使用较为常见的:递归字符文本分割。在递归字符文本分割中,通常会按照分隔符的优先级逐步分割文本。以英文常用的分隔符为例 ["\n\n", "\n", " "] ,其表示先用 "\n\n" 分割成段落,然后在每个段落中用 "\n" 分割成行,最后在每行中用 " " 分割成单词,这种逐层分割的方式可以将文本逐步细化为更小的单元。

public class RecursiveCharacterTextSplitter implements TextSplitterIntf {
    /**
     * 分隔符列表,用于拆分文本
     * 中文:"\n\n", "。", "!", "?", ",", ";"
     * 英文:"\n\n", "\n", " "
     */
    private List<String> separators = Arrays.asList("\n\n", "。", "!", "?", ",", ";");
    /**
     * 每个块的最大大小
     */
    private int chunkSize = 250;
    /**
     * 块之间的重叠大小,用来维持上下文关系
     */
    private int chunkOverlap = 30;

    public RecursiveCharacterTextSplitter() {
    }

    public RecursiveCharacterTextSplitter(int chunkSize, int chunkOverlap) {
        this.chunkSize = chunkSize;
        this.chunkOverlap = chunkOverlap;
        if (chunkOverlap >= chunkSize) {
            throw new IllegalArgumentException("chunkOverlap must be smaller than chunkSize");
        }
    }

    public RecursiveCharacterTextSplitter(List<String> separators, int chunkSize, int chunkOverlap) {
        this.separators = separators;
        this.chunkSize = chunkSize;
        this.chunkOverlap = chunkOverlap;
        if (chunkOverlap >= chunkSize) {
            throw new IllegalArgumentException("chunkOverlap must be smaller than chunkSize");
        }
    }

    /**
     * Spring ai Document 拆分
     *
     * @param documents
     * @param metaData  文档元数据信息
     * @return
     */
    @Override
    public List<Document> split(List<Document> documents, List<Map<String, Object>> metaData) {
        List<JSONObject> docList = this.beforeSplit(documents, metaData);
        List<Document> chunkedDocuments = new ArrayList<>();
        docList.forEach(item -> {
            Document document = item.getObject("document", Document.class);
            Map<String, Object> singleMetaData = item.getObject("metaData", Map.class);
            List<String> chunks = new ArrayList<>();
            splitRecursive(document.getContent(), chunks);
            chunks.forEach(chunk -> {
                chunkedDocuments.add(new Document(chunk, singleMetaData));
            });
        });
        return chunkedDocuments;
    }

    /**
     * 递归地拆分文本
     *
     * @param text
     * @param chunks
     */
    private void splitRecursive(String text, List<String> chunks) {
        if (text.length() <= chunkSize) {
            chunks.add(text);
            return;
        }

        String separator = findBestSeparator(text);
        if (separator == null) {
            chunks.add(text);
            return;
        }

        String[] parts = text.split(separator, -1);
        StringBuilder currentChunk = new StringBuilder();
        int currentLength = 0;

        for (String part : parts) {
            if (currentLength + part.length() > chunkSize) {
                if (currentChunk.length() > 0) {
                    chunks.add(currentChunk.toString());
                    // 通过回退chunkOverlap的距离创建来重叠部分
                    int overlapStart = Math.max(0, currentChunk.length() - chunkOverlap);
                    currentChunk = new StringBuilder(currentChunk.substring(overlapStart));
                    currentLength = currentChunk.length();
                }
            }
            currentChunk.append(part).append(separator);
            currentLength += part.length() + separator.length();
        }
        if (currentChunk.length() > 0) {
            chunks.add(currentChunk.toString());
        }
    }

    /**
     * 查找文本中第一个出现的分隔符
     *
     * @param text
     * @return
     */
    private String findBestSeparator(String text) {
        for (String separator : separators) {
            if (text.contains(separator)) {
                return separator;
            }
        }
        return null;
    }
}

6.4.2 BatchingStrategy 介绍

每个 Embedding 模型都有最大处理 token 数限制,对于一个大文档,我们不能够一次性发送给模型处理,为了解决这个问题,Spring AI 实现了一种批处理策略,这个方法将大量文档分解成更小的批次,不仅能够适配 Embedding Model 的最大上下文,还能够提高性能并更有效地利用API速率限制。

Spring AI 通过 BatchingStrategy 接口提供此功能,该接口允许根据文档的 token 数量将文档处理为子批次,这个接口有一个默认实现 TokenCountBatchingStrategy ,这里我们需要根据自己选择的模型重新注册该 Bean。

@Bean
public BatchingStrategy tokenCountBatchingStrategy() {
    return new TokenCountBatchingStrategy(
            EncodingType.CL100K_BASE,  // 指定用于分词的编码类型
            8000,                      // 最大 Token 数量
            0.1                        // 设置预留百分比(默认 10%)
    );
}

参数解释:

  • EncodingType.CL100K_BASE:指定用于分词的编码类型。这种编码类型被 JTokkitTokenCountEstimator 使用,以准确估计token数量。

  • 8000:最大 Token 数量,默认使用 OpenAI 的最大输入token数(8191)。

  • 0.1:设置预留百分比(默认 10%),从最大输入token数中预留的token百分比,这为处理过程中可能的token数量增加创建了一个缓冲区。

6.5 保存文档到向量库

6.5.1 分割后的文本入向量库

为了防止同一文档重复入库,在文档入库前先计算文档的 Hash 值,并在文档保存到向量库后,将文档的信息(文档名,文档大小,分割文本大小等)保存到 Redis 中(key 为文档 Hash 值),方便后续操作中的重复判断。

    /**
     * 文档入库
     *
     * @param inputStream 文档输入流
     * @param docHash     文档Hash
     * @param docName     文档名
     * @throws Exception
     */
    public void addDocument(InputStream inputStream, String docHash, String docName) throws Exception {
        // 校验文档是否已经存在
        if (redisService.hasKey(CommonConst.RAG_KEY_PREFIX + docHash)) {
            return;
        }
        int docSize = inputStream.available();
        // 文件流转为 Document
        TextReader textReader = new TextReader(new InputStreamResource(inputStream));
        List<Document> documents = textReader.get();
        // 文档拆分
        TextSplitterIntf textSplitter = new RecursiveCharacterTextSplitter(250, 30);
        List<Document> newDocuments = textSplitter.split(documents, List.of(Map.of(SpringAIConst.VectorStore.METADATA_DOC_ID, docHash,
                SpringAIConst.VectorStore.METADATA_DOC_NAME, docName)));
        // 向量入库
        vectorStore.add(newDocuments);
        // 将文档信息存入 Redis
        JSONObject docJson = new JSONObject();
        docJson.put(SpringAIConst.VectorStore.METADATA_DOC_NAME, docName);
        docJson.put("docSize", docSize);
        docJson.put("docChunks", newDocuments.size());
        redisService.setObject(CommonConst.RAG_KEY_PREFIX + docHash, docJson);
    }

6.5.2 计算文档的 Hash 值

支持的算法:MD5、SHA-1、SHA-256

    /**
     * 计算文件 Hash 值
     *
     * @param inputStream 输入流
     * @param algorithm   算法,可选,MD5、SHA-1、SHA-256
     * @return
     * @throws Exception
     */
    public static String calculateFileHash(InputStream inputStream, String algorithm) throws Exception {
        // 创建MessageDigest实例
        algorithm = StringUtils.isEmpty(algorithm) ? CommonConst.SHA_256 : algorithm;
        MessageDigest digest = MessageDigest.getInstance(algorithm);

        // 打开文件输入流
        try (inputStream) {
            byte[] buffer = new byte[1024];
            int numRead;
            // 读取文件内容并更新摘要
            while ((numRead = inputStream.read(buffer)) != -1) {
                digest.update(buffer, 0, numRead);
            }
            // 完成哈希计算
            byte[] hashBytes = digest.digest();
            // 将哈希值转换为十六进制字符串
            StringBuilder sb = new StringBuilder();
            for (byte b : hashBytes) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        }
    }

6.6 聊天接口

这是聊天接口,聊天记忆功能基于 Spring AI 提供的 InMemoryChatMemory 。

    /**
     * 带聊天记忆流式返回
     *
     * @param accessKey 聊天访问key,每次打开聊天页面重置
     * @param inputInfo 聊天输入内容
     * @param docIds    参考文档ID,多个用英文','号隔开
     * @return
     */
    @GetMapping("/memoryStreamWithApi")
    public Flux<ServerSentEvent<String>> memoryStreamWithApi(String accessKey, String inputInfo, String docIds) {
        // 接收并校验参数
        CommonUtil.checkAndThrow(accessKey, "您没有该功能访问权限!");
        CommonUtil.checkAndThrow(inputInfo, "请输入内容!");

        // 调用模型查询
        ChatClient.ChatClientRequestSpec chatClientRequestSpec = ChatClient.builder(chatModel)
                .defaultAdvisors(
                        new MessageChatMemoryAdvisor(new InMemoryChatMemory()))
                .build()
                .prompt()
                .user(inputInfo)
                .advisors(advisor -> advisor.param(CHAT_MEMORY_CONVERSATION_ID_KEY, accessKey).param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 5));
        // 先从向量库查询近似数据
        if (StringUtils.isNotEmpty(docIds)) {
            // 组装过滤条件,根据文档ID过滤,等价于 SQL 的 WHERE docId IN (docId1,docId2...)
            List<String> docIdList = Arrays.asList(docIds.split(","));
            Filter.Expression expression = new Filter.Expression(Filter.ExpressionType.IN,
                    new Filter.Key(SpringAIConst.VectorStore.METADATA_DOC_ID), new Filter.Value(docIdList));

            Advisor advisor = new QuestionAnswerAdvisor(vectorStore,
                    SearchRequest.builder().query(inputInfo)
                            .filterExpression(expression)
                            .topK(6)
                            .similarityThreshold(0.8d).build());
            chatClientRequestSpec.advisors(advisor);
        }
        return chatClientRequestSpec.stream()
                .chatResponse()
                .map(response -> ServerSentEvent.<String>builder()
                        .data(response.getResult().getOutput().getContent())
                        .build());

    }

7. 快速搭建 Elemenet-Plus 项目

写到这发现需要整理的文档太多,前端实现放到下一篇文章。

8. 参考文档

  • Spring AI RAG 文档
  • Spring AI Alibaba RAG 文档
  • Vue3 文档
  • Element Plus 文档
  • markdown-it 文档

9. 拓展

9.1 如何计算 Token ?

9.1.1 阿里通义

Token 是模型用来表示自然语言文本的基本单位,可以直观地理解为“字”或“词”。

  • 对于中文文本,1个 Token 通常对应一个汉字或词语。例如,“你好,我是通义千问”会被转换成['你好', ',', '我是', '通', '义', '千', '问']。

  • 对于英文文本,1个 Token 通常对应3至4个字母或1个单词。例如,"Nice to meet you."会被转换成['Nice', ' to', ' meet', ' you', '.']。

不同的大模型切分Token 的方法可能不同。可以本地运行 tokenizer 来估计文本的 Token 量。

9.1.2 DeepSeek

一般情况下模型中 token 和字数的换算比例大致如下:

  • 1 个英文字符 ≈ 0.3 个 token。

  • 1 个中文字符 ≈ 0.6 个 token。

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

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

相关文章

AI法理学与责任归属:技术演进下的法律重构与伦理挑战

文章目录 引言:智能时代的新型法律困境一、AI技术特性对传统法理的冲击1.1 算法黑箱与可解释性悖论1.2 动态学习系统的责任漂移1.3 多智能体协作的责任稀释二、AI法理学的核心争议点2.1 法律主体资格认定2.2 因果关系的技术解构2.3 过错标准的重新定义三、责任归属的实践案例分…

数值积分:通过复合梯形法计算

在物理学和工程学中&#xff0c;很多问题都可以通过数值积分来求解&#xff0c;特别是当我们无法得到解析解时。数值积分是通过计算积分区间内离散点的函数值来近似积分的结果。在这篇博客中&#xff0c;我将讨论如何使用 复合梯形法 来进行数值积分&#xff0c;并以一个简单的…

mybatis-plus逆向code generator pgsql实践

mybatis-plus逆向code generator pgsql实践 环境准备重要工具的版本供参考pom依赖待逆向的SQL 配置文件CodeGenerator配置类配置类说明 环境准备 重要工具的版本 jdk1.8.0_131springboot 2.7.6mybatis-plus 3.5.7pgsql 14.15 供参考pom依赖 <?xml version"1.0&quo…

RedHat8安装postgresql15和 postgis3.4.4记录及遇到的问题总结

安装包对照版本参考 UsersWikiPostgreSQLPostGIS – PostGIS 如果Red Hat系统上有旧版本的PostgreSQL需要卸载 在较新的Red Hat版本&#xff0c;使用dnf包管理器卸载&#xff1a;sudo dnf remove postgresql-server postgresql 旧版本&#xff0c;使用yum包管理器卸载 sudo y…

2024BaseCTF_week4_web上

继续&#xff01;冲冲冲 目录 圣钥之战1.0 nodejs 原型 原型链 原型链污染 回到题目 flag直接读取不就行了&#xff1f; 圣钥之战1.0 from flask import Flask,request import jsonapp Flask(__name__)def merge(src, dst):for k, v in src.items():if hasattr(dst, __geti…

leetcode:627. 变更性别(SQL解法)

难度&#xff1a;简单 SQL Schema > Pandas Schema > Salary 表&#xff1a; ----------------------- | Column Name | Type | ----------------------- | id | int | | name | varchar | | sex | ENUM | | salary | int …

【免费送书活动】《MySQL 9从入门到性能优化(视频教学版)》

本博主免费赠送读者3本书&#xff0c;书名为《MySQL 9从入门到性能优化&#xff08;视频教学版&#xff09;》。 《MySQL 9从入门到性能优化&#xff08;视频教学版&#xff09;&#xff08;数据库技术丛书&#xff09;》(王英英)【摘要 书评 试读】- 京东图书 这本书已经公开…

STM32、GD32驱动TM1640原理图、源码分享

一、原理图分享 二、源码分享 /************************************************* * copyright: * author:Xupeng * date:2024-07-18 * description: **************************************************/ #include "smg.h"#define DBG_TAG "smg&…

ElementUI 的组件 Switch(开关)如何让文字显示在按钮上

效果图&#xff1a; 一、引入switch组件 给组件自定义一个类&#xff1a;tableScopeSwitch&#xff0c;设置开关的值和对应展示的文字&#xff08;开为 1&#xff0c;并展示启用&#xff1b;关为 0&#xff0c;并展示禁用&#xff09;。 <div class"tableScopeSwitch…

我的新书《青少年Python趣学编程(微课视频版)》出版了!

&#x1f389; 激动人心的时刻来临啦&#xff01; &#x1f389; 小伙伴们久等了&#xff0c;我的第一本新书 《青少年Python趣学编程&#xff08;微课视频版&#xff09;》 正式出版啦&#xff01; &#x1f4da;✨ 在这个AI时代&#xff0c;市面上的Python书籍常常过于枯燥&…

CNN-BiLSTM卷积神经网络双向长短期记忆神经网络多变量多步预测,光伏功率预测

代码地址&#xff1a;CNN-BiLSTM卷积神经网络双向长短期记忆神经网络多变量多步预测&#xff0c;光伏功率预测 CNN-BiLSTM卷积神经网络双向长短期记忆神经网络多变量多步预测 一、引言 1.1、研究背景和意义 光伏功率预测在现代电力系统中占有至关重要的地位。随着可再生能源…

人工智能任务21-飞蛾火焰优化算法(MFO)在深度学习中的应用

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下人工智能任务21-飞蛾火焰优化算法(MFO)在深度学习中的应用。飞蛾火焰优化算法&#xff08;Moth-Flame Optimization, MFO&#xff09;是一种受自然界中飞蛾向光源趋近行为启发的新型群体智能优化算法。在自然界中&a…

处理项目中存在多个版本的jsqlparser依赖

异常提示 Correct the classpath of your application so that it contains a single, compatible version of net.sf.jsqlparser.statement.select.SelectExpressionIte实际问题 原因&#xff1a;项目中同时使用了 mybatis-plus 和 pagehelper&#xff0c;两者都用到了 jsqlpa…

Java数组二:数组的使用

for-each循环 打印数组所有元素 public class Demo04 {public static void main(String[] args) {int[] num {1,5,2,3,4};for (int num1:num) {System.out.println(num1);}} }多维数组 多维数组可以看成是数组的数组&#xff0c;比如二维数组就是一个特殊的一维数组&#x…

基于MATLAB的沥青试样孔隙率自动分析——原理详解与代码实现

摘要 在材料科学与土木工程领域&#xff0c;沥青孔隙率是评价其耐久性和稳定性的重要指标。本文提出一种基于图像处理的孔隙率自动计算方法&#xff0c;通过MATLAB实现灰度化、对比度增强、形态学处理等关键步骤&#xff0c;最终输出试样孔隙率。代码注释清晰&#xff0c;可直…

修改OnlyOffice编辑器默认字体

通过Docker修改OnlyOffice编辑器默认字体 问题描述详细方案1. 删除原生字体文件2. 创建字体目录3. 复制字体文件到容器中4. 执行字体更新脚本5. 重新启动容器 注意事项 问题描述 在OnlyOffice中&#xff0c;编辑器的默认字体可能不符合公司或个人的需求&#xff0c;通常会使用…

【天地图】绘制、删除点线面

使用天地图绘制、删除点线面 实现效果图地图组件完整代码使用地图组件完整代码 实现效果图 地图组件完整代码 // 天地图组件 <template><div class"map-container"><div id"mapCon"></div></div> </template><scri…

【MySQL】高频 SQL 50 题(基础版)

高频SQL50题&#xff08;基础版&#xff09; 1.查询 2.连接 MySQL多表查询&#xff08;联合查询、连接查询、子查询&#xff09; left join 左连接 我们首先执行LEFT JOIN操作&#xff0c;将两个表的数据基于 id 列进行组合。同样&#xff0c;我们使用 LEFT JOIN 来确保将所…

什么是网关?网关有什么作用?API网关的主要功能,SpringCloud可以选择有哪些API网关?什么是限流算法?网关如何实现限流?一篇文章读懂网关的前世今生

1、什么是网关&#xff1f; API网关&#xff08;API Gateway&#xff09;是一种中间层服务器&#xff0c;用于集中管理&#xff0c;保护和路由对后端服务的访问。它充当了客户端与后端服务之间的入口点&#xff0c;提供了一组统一的接口管理和控制API的访问。 2、网关示意图 3…

Jenkins 配置 Git Repository 五

Jenkins 配置 Git Repository 五 这里包含了 Freestyle project 任务类型 和 Pipeline 任务类型 关于 Git 仓库的配置&#xff0c;如下 不同的任务类型&#xff0c;只是在不同的模块找到 配置 Git 仓库 找到 Git 仓库配置位置之后&#xff0c;所有的任务类型配置都是一样的 …