3
2
2
1
专栏/.../

tidb+springAI:一个基于国产化分布式数据库的RAG项目实战

 du拉松  发表于  2025-06-11

随着人工智能技术的迅猛发展,大模型应用已广泛落地于各类场景。然而,所有智能应用的核心基础依然是“数据”。尤其在 RAG(Retrieval-Augmented Generation,检索增强生成)类任务中,既涉及结构化的关系数据,也包含大量非结构化的向量数据,因此如何选择合适的数据库成为构建智能系统的关键一环。

为了降低运维成本和开发复杂度,我们更倾向于选择一种既支持关系型数据,又具备原生向量检索能力的数据库。TiDB 自 v8.5 起引入了向量检索功能,为我们提供了统一的数据平台解决方案。

然而,在实际使用中我们常常会遇到这样一些问题:

  • 如何存储和使用向量类型数据?
  • 如何在 Java 项目中优雅地集成 TiDB 的向量能力?
  • 面对 Spring 技术栈,如何进行良好的架构拓展?
  • 常见 ORM 框架(如 MyBatis)中如何高效支持向量检索?

为了解答这些问题,我构建了一个基于 RAG 技术的示例项目,从实战角度出发,展示如何在 Java + Spring 环境下集成 TiDB 向量库,并实现端到端的智能问答流程。

接下来,我将围绕以下三个部分展开说明:

  1. RAG 执行流程解析
  2. 项目所采用的技术栈说明
  3. TiDB 向量库的使用方法与集成经验分享

希望本项目能够为你在构建智能应用、使用 TiDB 向量能力的过程中提供有价值的参考。

RAG执行流程

检索增强生成(RAG)是一种优化大型语言模型(LLM)输出的架构。通过使用向量搜索,RAG 应用程序可以在数据库中存储向量嵌入,并在 LLM 生成回复时检索相关文档作为附加上下文,从而提高回复的质量和相关性。

未命名绘图.png

上图分为两个流程:

  • 知识库维护:用户上传文档 --> 文档分块 --> 文本向量化 --> 向量存储到tidb
  • 用户问答:用户提出问题 --> 问题向量化 --> 向量检索 --> 检索内容rerank重排序 --> 重新组织上下文 --> 大语言模型生成回答 --> 给用户响应。

项目技术栈

为了更直观地演示 TiDB 在 RAG 场景中的应用,我基于 Java 构建了一个简单的 Spring Boot 项目,实现了从文档解析、向量化、存储到问答交互的完整流程。项目中集成了主流的技术组件,包括 ORM 框架(mybatis)、模型服务、向量存储等,下面是本项目的技术栈及其作用说明:

springAI:Spring 官方推出的 AI 框架,简化了集成人工智能能力的开发流程。通过统一的抽象层支持多种模型和服务,避免繁琐的接入配置。这里不过多介绍,详情请看官网相关内容。

mybatis:一个流行的持久层框架,用于操作关系数据库。通过 MyBatis,我们可以无缝地访问 TiDB,包括向量存储的相关功能,无需引入额外插件。。

其他工具,比如ollama是用来本地化部署大语言模型和embedding模型的框架,文档分块服务:unstructured-api可以帮我们快速的给文本分块,并且支持多种文本格式。

tidb:TiDB 是本项目的核心存储组件。它既支持传统关系数据,也原生支持向量索引与检索功能,简化了系统架构,避免依赖多种数据库,大大提升了开发效率和系统一致性。

技术细节请参考github:https://github.com/geeklc/rag-tidb

库表准备

数据库部署

想要使用tidb中的向量索引,在部署tidb时需要部署tiflash组件,我在本地的测试中使用单机部署了一个简易集群,用来集成验证相关功能。

具体部署方式请参考官方文档,我这里只列出部署需要的拓扑文件:

global:
 user: "root"
 ssh_port: 22
 deploy_dir: "/home/tidb-deploy"
 data_dir: "/home/tidb-data"
​
monitored:
 node_exporter_port: 9100
 blackbox_exporter_port: 9115
​
server_configs:
 tidb:
   instance.tidb_slow_log_threshold: 300
 tikv:
   readpool.storage.use-unified-pool: false
   readpool.coprocessor.use-unified-pool: true
 pd:
   replication.enable-placement-rules: true
   replication.location-labels: ["host"]
 tiflash:
   logger.level: "info"
​
pd_servers:
 - host: 192.168.0.116
​
tidb_servers:
 - host: 192.168.0.116
​
tikv_servers:
 - host: 192.168.0.116
   port: 20160
   status_port: 20180
   config:
     server.labels: { host: "logic-host-1" }
​
 - host: 192.168.0.116
   port: 20161
   status_port: 20181
   config:
     server.labels: { host: "logic-host-2" }
​
 - host: 192.168.0.116
   port: 20162
   status_port: 20182
   config:
     server.labels: { host: "logic-host-3" }
​
tiflash_servers:
 - host: 192.168.0.116
​
monitoring_servers:
 - host: 192.168.0.116
​
grafana_servers:
 - host: 192.168.0.116

表创建

比如在项目中使用的文本分块表:

-- 创建文本分块表
CREATE TABLE `doc_chunk` (
     `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
     `kb_id` bigint DEFAULT NULL COMMENT '知识库id',
     `doc_id` bigint DEFAULT NULL COMMENT '文档id',
     `content` varchar(1000) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '分段内容',
     `embedding` vector(1024) COMMENT '分段内容向量',
     `sort` int DEFAULT NULL COMMENT '分段排序',
     `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     PRIMARY KEY (`id`)
) AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='文档分段';

其中embedding字段就是向量类型字段,字段的维度为1024,因为我这里使用的embedding模型就是1024维度。

创建索引:

-- 首先设置表doc_chunk的tiflash副本数为1
ALTER TABLE doc_chunk SET TIFLASH REPLICA 1;
​
-- 创建向量索引,索引类型为HNSW
CREATE VECTOR INDEX idx_doc_chunk_embedding ON doc_chunk ((VEC_COSINE_DISTANCE(embedding))) USING HNSW;

向量的使用

添加依赖

连接tidb需要添加mysql的驱动,并添加mybatis的相关依赖:

注意本项目使用的是springboot3.4.6,具体的版本请查阅springboot官网。

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.4</version>
</dependency>
​
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

配置信息

在 application.properties配置文件中增加如下配置:

# 配置tidb的连接信息
spring.datasource.url=jdbc:mysql://xxx.xxx.xxx.xxx:4000/rag_test?useUnicode=true&characterEncoding=UTF-8&useSSL=false&allowMultiQueries=true&serverTimezone=Hongkong
spring.datasource.username=xxxx
spring.datasource.password=xxxx
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
​
# 配置mybatis
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.df.rag.**.domain
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.jdbc-type-for-null=null
mybatis.configuration.call-setters-on-nulls=true
mybatis.configuration.lazy-loading-enabled=true
mybatis.configuration.aggressive-lazy-loading=false
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
​

定义文本分块内容的实体

定义实体时,注意向量类型的字段类型为String

@Getter
@Setter
public class DocChunk {
​
    private Long id;
​
    /**
     * 知识库id
     */
    private Long kbId;
​
    /**
     * 文档id
     */
    private Long docId;
​
    /**
     * 文本内容
     */
    private String content;
​
    /**
     * 向量内容,注意这里的向量类型是String类型
     */
    private String embedding;
​
    /**
     * 分块顺序
     */
    private Integer sort;
​
    /**
     * 创建时间
     */
    private Date createTime;
​
    /**
     * 语义距离
     */
    private Float distance;
}

定义对应的mapper信息

  1. 定义的DocChunkMapper如下:
/**
 *  这里定义的文档chunkMapper
 *
 */
public interface DocChunkMapper {
​
    /**
     * 批量保存
     * @param infos
     * @return
     */
    int batchInsert(@Param("infos") List<DocChunk> infos);
​
    /**
     * 根据向量,知识库id和文档id检索数据
     *
     * @param embedding 向量值
     * @param kbId      知识库id
     * @param docId     文档id
     * @return
     */
    List<DocChunk> queryByVector(@Param("embedding") String embedding,
                                 @Param("kbId") Long kbId,
                                 @Param("docId") Long docId,
                                 @Param("topK") Integer topK);
}

batchInsert:是批量保存文档分块信息:

queryByVector:是根据向量和其他条件进行查询

  1. 定义的DocChunkMapper.xml如下:
<insert id="batchInsert" keyColumn="id" keyProperty="id" useGeneratedKeys="true" parameterType="com.df.rag.domain.DocChunk">
        insert into doc_chunk (kb_id, doc_id, content, embedding, sort)values
            <foreach collection="infos" item="info" separator=",">
                (#{info.kbId}, #{info.docId}, #{info.content}, #{info.embedding}, #{info.sort})
            </foreach>
    </insert>
​
    <select id="queryByVector" resultType="com.df.rag.domain.DocChunk">
        select * from (
            select id, kb_id, doc_id, content, sort, create_time, VEC_COSINE_DISTANCE(embedding, #{embedding}) as distance from doc_chunk
            ORDER BY distance asc
            limit ${topK}
        ) t
        <where>
            <if test="docId != null">
                and doc_id = #{docId}
            </if>
            <if test="kbId != null">
                and kb_id = #{kbId}
            </if>
        </where>
    </select>

逻辑处理

  1. 在service中保存分块信息的处理
/**
     * 处理分段数据
     *
     * @param chunks
     * @param docId
     * @param kbId
     */
    private void dealChunk(List<UnstructuredData> chunks, Long docId, Long kbId) {
        //对分块数据没50个数据一批处理
        ListSplitter<UnstructuredData> chunkSplitter = new ListSplitter<>(50, chunks);
        int sort = 0;
        while (chunkSplitter.hasNext()) {
            List<UnstructuredData> chunksInfos = chunkSplitter.next();
            List<String> embed = getEmbed(chunksInfos);
            List<DocChunk> chunkList = new ArrayList<>();
            for (int i = 0; i < chunksInfos.size(); i++) {
                DocChunk docChunk = new DocChunk();
                docChunk.setDocId(docId);
                docChunk.setKbId(kbId);
                sort++;
                docChunk.setSort(sort);
                docChunk.setContent(chunksInfos.get(i).getText());
                docChunk.setEmbedding(embed.get(i));
                chunkList.add(docChunk);
            }
            //保存数据到tidb
            docChunkMapper.batchInsert(chunkList);
        }
    }
​
    /**
     * 获取向量数据
     *
     * @param chunksInfos
     * @return
     */
    private List<String> getEmbed(List<UnstructuredData> chunksInfos) {
        //获取分块的文本内容
        List<String> texts = chunksInfos.stream().map(UnstructuredData::getText).collect(Collectors.toList());
        //定义embedding 模型的请求参数
        EmbeddingRequest embeddingRequest = new EmbeddingRequest(texts, OllamaOptions.builder()
                .model(appProperties.getOllama().getEmbedModel()).truncate(false)
                .build());
        // 请求embedding模型
        EmbeddingResponse call = ollamaEmbeddingModel.call(embeddingRequest);
        // 获取embedding结果
        List<Embedding> results = call.getResults();
        // 把float[] 类型json化 string
        return results.stream().map(info -> JSON.toJSONString(info.getOutput())).collect(Collectors.toList());
    }
  1. 在service中查询分块信息的处理
/**
     * 向量搜索
     *
     * @param content 搜索内容
     * @param kbId    知识库id
     * @param docId   文档id
     * @param topK    topK
     * @return
     */
    @Override
    public List<DocChunk> vectorSearch(String content, Long kbId, Long docId, Integer topK) {
        if (StringUtils.isEmpty(content)) {
            return new ArrayList<>();
        }
        //先进行文本向量化
        EmbeddingRequest embeddingRequest = new EmbeddingRequest(Lists.newArrayList(content), OllamaOptions.builder()
                .model(appProperties.getOllama().getEmbedModel()).truncate(false)
                .build());
        //请求embedding模型
        EmbeddingResponse call = ollamaEmbeddingModel.call(embeddingRequest);
        List<Embedding> results = call.getResults();
        // 把 float[]使用JSON转化为字符串
        String embedding = JSON.toJSONString(results.get(0).getOutput());
        // 从tidb中查询相关信息
        List<DocChunk> docChunks = docChunkMapper.queryByVector(embedding, kbId, docId, topK);
        return docChunks;
    }

使用过程展示

  1. 准备一个docx的文档:

image-20250611104310963.png

  1. 请求接口上传文档,作为知识库内容:

image-20250611103223665.png

  1. 知识库问答接口请求

可以看到回答内容是根据文档中的信息生成:

image-20250611104225310.png

总结与展望

在实际使用 TiDB 向量库的过程中,整体操作非常简洁易用。向量的存储方面,只需将 float[] 类型的向量通过 JSON 格式进行转换后存入数据库即可。查询时,需要特别注意 SQL 写法,主要使用了内置的 VEC_COSINE_DISTANCE() 函数进行向量相似度计算。除此之外,其余操作几乎与普通关系型查询一致,上手非常快速,学习成本低。

以上内容是我在实战中总结的一些配置方式和代码示例,希望能够为广大 TiDBer 在构建 RAG 系统或其他 AI 应用时,提供一些实际参考和技术借鉴。

我们期待 TiDB 的向量功能能够尽快实现 GA,同时希望其向量查询的 SQL 语法更加简洁友好,提升开发体验。

随着国产化数据库的发展和大模型应用的加速落地,TiDB 在融合结构化与非结构化数据处理方面具备巨大潜力。我们相信,TiDB 有望成为国产数据库与人工智能技术融合的中坚力量,持续推动智能时代的技术革新。

3
2
2
1

版权声明:本文为 TiDB 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。

评论
暂无评论