随着人工智能技术的迅猛发展,大模型应用已广泛落地于各类场景。然而,所有智能应用的核心基础依然是“数据”。尤其在 RAG(Retrieval-Augmented Generation,检索增强生成)类任务中,既涉及结构化的关系数据,也包含大量非结构化的向量数据,因此如何选择合适的数据库成为构建智能系统的关键一环。
为了降低运维成本和开发复杂度,我们更倾向于选择一种既支持关系型数据,又具备原生向量检索能力的数据库。TiDB 自 v8.5 起引入了向量检索功能,为我们提供了统一的数据平台解决方案。
然而,在实际使用中我们常常会遇到这样一些问题:
- 如何存储和使用向量类型数据?
- 如何在 Java 项目中优雅地集成 TiDB 的向量能力?
- 面对 Spring 技术栈,如何进行良好的架构拓展?
- 常见 ORM 框架(如 MyBatis)中如何高效支持向量检索?
为了解答这些问题,我构建了一个基于 RAG 技术的示例项目,从实战角度出发,展示如何在 Java + Spring 环境下集成 TiDB 向量库,并实现端到端的智能问答流程。
接下来,我将围绕以下三个部分展开说明:
- RAG 执行流程解析
- 项目所采用的技术栈说明
- TiDB 向量库的使用方法与集成经验分享
希望本项目能够为你在构建智能应用、使用 TiDB 向量能力的过程中提供有价值的参考。
RAG执行流程
检索增强生成(RAG)是一种优化大型语言模型(LLM)输出的架构。通过使用向量搜索,RAG 应用程序可以在数据库中存储向量嵌入,并在 LLM 生成回复时检索相关文档作为附加上下文,从而提高回复的质量和相关性。
上图分为两个流程:
- 知识库维护:用户上传文档 --> 文档分块 --> 文本向量化 --> 向量存储到tidb
- 用户问答:用户提出问题 --> 问题向量化 --> 向量检索 --> 检索内容rerank重排序 --> 重新组织上下文 --> 大语言模型生成回答 --> 给用户响应。
项目技术栈
为了更直观地演示 TiDB 在 RAG 场景中的应用,我基于 Java 构建了一个简单的 Spring Boot 项目,实现了从文档解析、向量化、存储到问答交互的完整流程。项目中集成了主流的技术组件,包括 ORM 框架(mybatis)、模型服务、向量存储等,下面是本项目的技术栈及其作用说明:
- AI框架:springAI
- ORM框架:mybatis
- 模型部署工具:ollama
- 大语言模型:qwen2.5:14b
- embedding模型:bge-large-zh-v1.5:f32
- 文档分块服务:unstructured-api
- 关系数据存储和向量存储:tidb
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信息
- 定义的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:是根据向量和其他条件进行查询
- 定义的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>
逻辑处理
- 在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());
}
- 在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;
}
使用过程展示
-
准备一个docx的文档:
-
请求接口上传文档,作为知识库内容:
- 知识库问答接口请求
可以看到回答内容是根据文档中的信息生成:
总结与展望
在实际使用 TiDB 向量库的过程中,整体操作非常简洁易用。向量的存储方面,只需将 float[]
类型的向量通过 JSON 格式进行转换后存入数据库即可。查询时,需要特别注意 SQL 写法,主要使用了内置的 VEC_COSINE_DISTANCE()
函数进行向量相似度计算。除此之外,其余操作几乎与普通关系型查询一致,上手非常快速,学习成本低。
以上内容是我在实战中总结的一些配置方式和代码示例,希望能够为广大 TiDBer 在构建 RAG 系统或其他 AI 应用时,提供一些实际参考和技术借鉴。
我们期待 TiDB 的向量功能能够尽快实现 GA,同时希望其向量查询的 SQL 语法更加简洁友好,提升开发体验。
随着国产化数据库的发展和大模型应用的加速落地,TiDB 在融合结构化与非结构化数据处理方面具备巨大潜力。我们相信,TiDB 有望成为国产数据库与人工智能技术融合的中坚力量,持续推动智能时代的技术革新。