Spring AI + Redis 向量库实战:构建高性能 RAG 应用
摘要
Spring AI 是 Spring 生态系统中的 AI 应用开发框架,而 Redis 作为高性能内存数据库,其向量搜索能力(Redis Stack)为 RAG(检索增强生成)应用提供了强大支持。本文将深入探讨如何使用 Spring AI 最新版本结合 Redis 向量库,构建生产级的智能问答系统。
目录
- Spring AI 与 Redis 向量库简介
- 核心架构与技术选型
- 环境准备与依赖配置
- Redis 向量库核心概念
- 快速开始
- 核心功能实现
- 高级特性
- 生产环境实践
- 性能优化与调优
- 实战案例
- 常见问题与解决方案
- 总结与展望
1. Spring AI 与 Redis 向量库简介
1.1 为什么选择 Redis 作为向量库
Redis Stack 提供了 RediSearch 模块,支持向量相似度搜索(VSS),具有以下优势:
核心优势:
- ⚡ 高性能:基于内存的向量搜索,毫秒级响应
- 🔄 实时性:支持实时索引更新,无需重建
- 📦 易部署:单一服务,无需复杂架构
- 💰 成本低:相比专用向量数据库,运维成本更低
- 🛠️ 功能丰富:支持混合查询(向量+元数据过滤)
- 🔗 生态好:Spring AI 原生支持
1.2 应用场景
┌─────────────────────────────────────────────────────────────────┐
│ Spring AI + Redis 向量库应用场景全景图 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐ │
│ │ 智能文档检索 │ │ 企业知识库 │ │ 智能客服 │ │
│ │ │ │ │ │ │ │
│ │ • PDF问答 │ │ • 内部文档搜索 │ │ • FAQ自动 │ │
│ │ • 文档摘要 │ │ • 政策查询 │ │ 回答 │ │
│ │ • 多文档对比 │ │ • 技术文档库 │ │ • 意图识别 │ │
│ │ • 引用溯源 │ │ • 规章制度查询 │ │ • 上下文 │ │
│ │ │ │ │ │ 理解 │ │
│ └──────────────────┘ └──────────────────┘ └──────────────┘ │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐ │
│ │ 代码搜索助手 │ │ 电商推荐系统 │ │ 内容审核 │ │
│ │ │ │ │ │ │ │
│ │ • 语义代码搜索 │ │ • 商品相似推荐 │ │ • 重复内容 │ │
│ │ • API文档查询 │ │ • 用户画像匹配 │ │ 检测 │ │
│ │ • Bug相似查找 │ │ • 个性化搜索 │ │ • 敏感信息 │ │
│ │ • 代码生成 │ │ • 关联商品发现 │ │ 过滤 │ │
│ │ │ │ │ │ │ │
│ └──────────────────┘ └──────────────────┘ └──────────────┘ │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐ │
│ │ 实时问答系统 │ │ 多语言搜索 │ │ 日志分析 │ │
│ │ │ │ │ │ │ │
│ │ • 流式响应 │ │ • 跨语言检索 │ │ • 异常模式 │ │
│ │ • 会话记忆 │ │ • 翻译+搜索 │ │ 识别 │ │
│ │ • 多轮对话 │ │ • 多语言FAQ │ │ • 根因分析 │ │
│ │ • 上下文感知 │ │ • 国际化支持 │ │ • 趋势预测 │ │
│ │ │ │ │ │ │ │
│ └──────────────────┘ └──────────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
1.3 技术栈对比
| 特性 | Redis VSS | Pinecone | Milvus | Chroma |
|---|---|---|---|---|
| 性能 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 易用性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| 成本 | 低 | 高 | 中 | 低 |
| 实时更新 | ✅ 优秀 | ✅ 优秀 | ✅ 优秀 | ⚠️ 一般 |
| 混合查询 | ✅ 支持 | ✅ 支持 | ✅ 支持 | ⚠️ 有限 |
| Spring AI | ✅ 原生 | ✅ 原生 | ✅ 原生 | ✅ 原生 |
| 部署复杂度 | 简单 | 托管 | 复杂 | 简单 |
| 数据规模 | 百万级 | 亿级 | 亿级 | 百万级 |
2. 核心架构与技术选型
2.1 整体架构图
scss
┌──────────────────────────────────────────────────────────────┐
│ Spring Boot Application │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Controller Layer │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │ │
│ │ │ Document API │ │ Chat API │ │ Search API │ │ │
│ │ └──────────────┘ └──────────────┘ └─────────────┘ │ │
│ └───────────────────────┬────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────▼────────────────────────────────┐ │
│ │ Service Layer │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │ │
│ │ │ RAG Service │ │ Chat Service │ │ Doc Service │ │ │
│ │ └──────────────┘ └──────────────┘ └─────────────┘ │ │
│ └───────────────────────┬────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────▼────────────────────────────────┐ │
│ │ Spring AI Framework │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ │
│ │ │ │ChatClient│ │Embedding │ │DocumentReader│ │ │ │
│ │ │ │ │ │ Model │ │ │ │ │ │
│ │ │ └──────────┘ └──────────┘ └──────────────┘ │ │ │
│ │ │ │ │ │
│ │ │ ┌──────────────────────────────────────────┐ │ │ │
│ │ │ │ VectorStore (Redis) │ │ │ │
│ │ │ │ • add() │ │ │ │
│ │ │ │ • similaritySearch() │ │ │ │
│ │ │ │ • delete() │ │ │ │
│ │ │ └──────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └───────────────────────┬────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────▼────────────────────────────────┐ │
│ │ Redis Stack (Vector Store) │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ RediSearch Module (Vector Similarity Search) │ │ │
│ │ │ • HNSW Index │ │ │
│ │ │ • FLAT Index │ │ │
│ │ │ • Cosine Similarity │ │ │
│ │ │ • L2 Distance │ │ │
│ │ │ • IP (Inner Product) │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────▼────────────────────────────────┐ │
│ │ LLM Provider │ │
│ │ (OpenAI / Azure / Ollama / Qwen...) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
2.2 RAG 工作流程
scss
┌──────────────────────────────────────────────────────────────┐
│ RAG 工作流程详解 │
└──────────────────────────────────────────────────────────────┘
【索引构建阶段】
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 原始文档 │─────▶│ 文档分割 │─────▶│ 向量化 │
│ (PDF/TXT) │ │ (Chunking) │ │ (Embedding) │
└─────────────┘ └─────────────┘ └─────────────┘
│
▼
┌─────────────────┐
│ 存储到Redis │
│ (Vector Store) │
└─────────────────┘
【查询响应阶段】
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 用户问题 │─────▶│ 向量化 │─────▶│ 相似度搜索 │
│ │ │ │ │ (Redis VSS)│
└─────────────┘ └─────────────┘ └─────────────┘
│
▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 返回答案 │◀─────│ LLM生成 │◀─────│ 检索上下文 │
│ │ │ │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
3. 环境准备与依赖配置
3.1 环境要求
基础环境:
- JDK 17+
- Spring Boot 3.2+
- Maven 3.8+ / Gradle 8.0+
- Redis Stack 7.2+(支持 RediSearch)
3.2 Redis Stack 安装
Docker 方式(推荐):
bash
# 拉取 Redis Stack 镜像
docker pull redis/redis-stack:latest
# 启动 Redis Stack
docker run -d \
--name redis-stack \
-p 6379:6379 \
-p 8001:8001 \
-e REDIS_ARGS="--requirepass mypassword" \
redis/redis-stack:latest
# 验证安装
docker exec -it redis-stack redis-cli
> AUTH mypassword
> FT._LIST
Docker Compose 方式:
yaml
version: '3.8'
services:
redis-stack:
image: redis/redis-stack:latest
container_name: redis-vector-store
ports:
- "6379:6379"
- "8001:8001"
environment:
- REDIS_ARGS=--requirepass mypassword
volumes:
- redis-data:/data
restart: unless-stopped
volumes:
redis-data:
driver: local
3.3 Maven 依赖配置
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.1</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>spring-ai-redis-demo</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
<spring-ai.version>1.0.0-M4</spring-ai.version>
</properties>
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AI OpenAI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<!-- Spring AI Redis Vector Store -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-redis-store-spring-boot-starter</artifactId>
</dependency>
<!-- Spring AI PDF Document Reader -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
<!-- Spring AI TikaDocumentReader -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Jedis (Redis 客户端) -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Spring Boot Starter Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>
3.4 配置文件
application.yml:
yaml
spring:
application:
name: spring-ai-redis-demo
# AI 配置
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-3.5-turbo
temperature: 0.7
max-tokens: 2000
embedding:
options:
model: text-embedding-ada-002
# Redis 向量存储配置
vectorstore:
redis:
uri: redis://localhost:6379
password: mypassword
index: spring-ai-index
prefix: doc:
initialize-schema: true
# Redis 配置
data:
redis:
host: localhost
port: 6379
password: mypassword
timeout: 60s
jedis:
pool:
max-active: 20
max-idle: 10
min-idle: 5
# 日志配置
logging:
level:
org.springframework.ai: DEBUG
com.example: DEBUG
# 服务器配置
server:
port: 8080
# 自定义配置
app:
vector:
dimension: 1536 # OpenAI ada-002 向量维度
algorithm: HNSW # 索引算法: FLAT 或 HNSW
distance-metric: COSINE # 距离度量: COSINE, L2, IP
document:
chunk-size: 800
chunk-overlap: 200
4. Redis 向量库核心概念
4.1 向量索引算法
FLAT(暴力搜索):
- 精确搜索,100% 召回率
- 适合小规模数据(< 10万)
- 线性时间复杂度 O(n)
HNSW(层次可导航小世界图):
- 近似搜索,高召回率(> 95%)
- 适合大规模数据(百万级+)
- 对数时间复杂度 O(log n)
css
┌──────────────────────────────────────────────────────────┐
│ FLAT vs HNSW 性能对比 │
├──────────────────────────────────────────────────────────┤
│ │
│ 数据规模 FLAT 延迟 HNSW 延迟 推荐选择 │
│ ───────────────────────────────────────────────────── │
│ 1K < 1ms < 1ms FLAT │
│ 10K 5-10ms < 2ms FLAT/HNSW │
│ 100K 50-100ms < 5ms HNSW │
│ 1M 500ms+ < 10ms HNSW │
│ 10M+ N/A < 20ms HNSW │
│ │
└──────────────────────────────────────────────────────────┘
4.2 距离度量
Cosine Similarity(余弦相似度):
- 范围: [-1, 1],越接近 1 越相似
- 适用于文本向量
- 不受向量长度影响
L2 Distance(欧氏距离):
- 范围: [0, ∞),越小越相似
- 受向量长度影响
- 适用于图像向量
IP(内积):
- 范围: (-∞, ∞),越大越相似
- 性能最好
- 需要归一化向量
5. 快速开始
5.1 启动类
java
package com.example.springai.redis;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringAiRedisApplication {
public static void main(String[] args) {
SpringApplication.run(SpringAiRedisApplication.class, args);
}
}
5.2 基础配置类
java
package com.example.springai.redis.config;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.openai.OpenAiEmbeddingModel;
import org.springframework.ai.vectorstore.RedisVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPooled;
@Configuration
public class VectorStoreConfig {
@Value("${spring.ai.vectorstore.redis.uri}")
private String redisUri;
@Value("${spring.ai.vectorstore.redis.password}")
private String redisPassword;
@Value("${spring.ai.vectorstore.redis.index}")
private String indexName;
@Value("${spring.ai.vectorstore.redis.prefix}")
private String prefix;
@Bean
public JedisPooled jedisPooled() {
// 解析 Redis URI
String host = "localhost";
int port = 6379;
return new JedisPooled(host, port, null, redisPassword);
}
@Bean
public VectorStore vectorStore(EmbeddingModel embeddingModel, JedisPooled jedisPooled) {
RedisVectorStore.RedisVectorStoreConfig config = RedisVectorStore.RedisVectorStoreConfig
.builder()
.withIndexName(indexName)
.withPrefix(prefix)
.build();
return new RedisVectorStore(config, embeddingModel, jedisPooled, true);
}
}
5.3 第一个示例
java
package com.example.springai.redis.example;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
@Component
public class QuickStartExample implements CommandLineRunner {
private final VectorStore vectorStore;
public QuickStartExample(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
@Override
public void run(String... args) throws Exception {
// 1. 准备文档
List<Document> documents = List.of(
new Document("Spring Boot 是一个用于简化 Spring 应用开发的框架",
Map.of("source", "doc1", "category", "framework")),
new Document("Redis 是一个高性能的内存数据库,支持多种数据结构",
Map.of("source", "doc2", "category", "database")),
new Document("Spring AI 提供了统一的 API 来集成各种 AI 服务",
Map.of("source", "doc3", "category", "ai"))
);
// 2. 添加文档到向量库
vectorStore.add(documents);
System.out.println("✅ 已添加 " + documents.size() + " 个文档");
// 3. 执行相似度搜索
String query = "如何简化应用开发?";
List<Document> results = vectorStore.similaritySearch(
SearchRequest.query(query).withTopK(2)
);
// 4. 打印结果
System.out.println("\n🔍 搜索: " + query);
System.out.println("📄 找到 " + results.size() + " 个相关文档:\n");
for (int i = 0; i < results.size(); i++) {
Document doc = results.get(i);
System.out.println((i + 1) + ". " + doc.getContent());
System.out.println(" 元数据: " + doc.getMetadata());
System.out.println();
}
}
}
6. 核心功能实现
6.1 文档加载与处理
6.1.1 文档加载器服务
java
package com.example.springai.redis.service;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.ExtractedTextFormatter;
import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig;
import org.springframework.ai.reader.tika.TikaDocumentReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class DocumentLoaderService {
/**
* 加载 PDF 文档
*/
public List<Document> loadPdfDocument(Resource resource) {
PdfDocumentReaderConfig config = PdfDocumentReaderConfig.builder()
.withPageExtractedTextFormatter(ExtractedTextFormatter.builder()
.withNumberOfBottomTextLinesToDelete(3)
.withNumberOfTopPagesToSkipBeforeDelete(1)
.build())
.withPagesPerDocument(1)
.build();
PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(resource, config);
return pdfReader.get();
}
/**
* 加载多种格式文档(使用 Tika)
*/
public List<Document> loadDocument(Resource resource) {
TikaDocumentReader tikaReader = new TikaDocumentReader(resource);
return tikaReader.get();
}
/**
* 分割文档为小块
*/
public List<Document> splitDocuments(List<Document> documents, int chunkSize, int overlap) {
TokenTextSplitter splitter = new TokenTextSplitter(chunkSize, overlap, 5, 10000, true);
return splitter.apply(documents);
}
}
6.1.2 文档处理服务
java
package com.example.springai.redis.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
public class DocumentProcessService {
private final DocumentLoaderService loaderService;
private final VectorStore vectorStore;
@Value("${app.document.chunk-size:800}")
private int chunkSize;
@Value("${app.document.chunk-overlap:200}")
private int chunkOverlap;
public DocumentProcessService(DocumentLoaderService loaderService, VectorStore vectorStore) {
this.loaderService = loaderService;
this.vectorStore = vectorStore;
}
/**
* 处理并存储文档
*/
public String processAndStoreDocument(Resource resource, Map<String, Object> metadata) {
try {
log.info("开始处理文档: {}", resource.getFilename());
// 1. 加载文档
List<Document> documents = loaderService.loadDocument(resource);
log.info("加载了 {} 个文档页面", documents.size());
// 2. 添加元数据
documents.forEach(doc -> {
Map<String, Object> meta = new HashMap<>(metadata);
meta.put("filename", resource.getFilename());
meta.put("page", doc.getMetadata().get("page"));
doc.getMetadata().putAll(meta);
});
// 3. 分割文档
List<Document> chunks = loaderService.splitDocuments(documents, chunkSize, chunkOverlap);
log.info("文档分割成 {} 个块", chunks.size());
// 4. 存储到向量库
vectorStore.add(chunks);
log.info("✅ 文档已成功存储到向量库");
return String.format("成功处理文档,共 %d 个块", chunks.size());
} catch (Exception e) {
log.error("处理文档失败", e);
throw new RuntimeException("文档处理失败: " + e.getMessage(), e);
}
}
/**
* 批量处理文档
*/
public Map<String, String> processBatchDocuments(List<Resource> resources, Map<String, Object> commonMetadata) {
Map<String, String> results = new HashMap<>();
for (Resource resource : resources) {
try {
String result = processAndStoreDocument(resource, commonMetadata);
results.put(resource.getFilename(), result);
} catch (Exception e) {
results.put(resource.getFilename(), "失败: " + e.getMessage());
}
}
return results;
}
}
6.2 向量搜索服务
java
package com.example.springai.redis.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.filter.Filter;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
public class VectorSearchService {
private final VectorStore vectorStore;
public VectorSearchService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
/**
* 基础相似度搜索
*/
public List<Document> search(String query, int topK) {
log.info("执行搜索: query='{}', topK={}", query, topK);
List<Document> results = vectorStore.similaritySearch(
SearchRequest.query(query).withTopK(topK)
);
log.info("找到 {} 个相关文档", results.size());
return results;
}
/**
* 带相似度阈值的搜索
*/
public List<Document> searchWithThreshold(String query, int topK, double threshold) {
log.info("执行搜索: query='{}', topK={}, threshold={}", query, topK, threshold);
List<Document> results = vectorStore.similaritySearch(
SearchRequest.query(query)
.withTopK(topK)
.withSimilarityThreshold(threshold)
);
log.info("找到 {} 个相关文档(相似度 >= {})", results.size(), threshold);
return results;
}
/**
* 元数据过滤搜索
*/
public List<Document> searchWithMetadataFilter(String query, int topK, Map<String, Object> filters) {
log.info("执行过滤搜索: query='{}', topK={}, filters={}", query, topK, filters);
// 构建过滤表达式
FilterExpressionBuilder builder = new FilterExpressionBuilder();
Filter.Expression filterExpression = null;
for (Map.Entry<String, Object> entry : filters.entrySet()) {
Filter.Expression expr = builder.eq(entry.getKey(), entry.getValue()).build();
if (filterExpression == null) {
filterExpression = expr;
} else {
filterExpression = builder.and(filterExpression, expr).build();
}
}
List<Document> results = vectorStore.similaritySearch(
SearchRequest.query(query)
.withTopK(topK)
.withFilterExpression(filterExpression)
);
log.info("找到 {} 个相关文档(应用过滤器后)", results.size());
return results;
}
/**
* 高级搜索:支持分类、日期范围等复杂过滤
*/
public List<Document> advancedSearch(String query, SearchRequest.Builder builder) {
List<Document> results = vectorStore.similaritySearch(builder.query(query).build());
log.info("高级搜索完成,找到 {} 个结果", results.size());
return results;
}
}
6.3 RAG 问答服务
java
package com.example.springai.redis.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@Service
public class RagService {
private final ChatClient chatClient;
private final VectorStore vectorStore;
private static final String SYSTEM_PROMPT = """
你是一个专业的知识库助手,基于提供的上下文信息回答用户问题。
回答要求:
1. 仅基于提供的上下文信息回答
2. 如果上下文中没有相关信息,明确告知用户
3. 回答要准确、简洁、专业
4. 可以引用具体的文档来源
上下文信息:
{context}
""";
public RagService(ChatClient.Builder chatClientBuilder, VectorStore vectorStore) {
this.chatClient = chatClientBuilder.build();
this.vectorStore = vectorStore;
}
/**
* RAG 问答 - 基础版本
*/
public String ask(String question) {
log.info("收到问题: {}", question);
// 1. 检索相关文档
List<Document> relevantDocs = vectorStore.similaritySearch(
SearchRequest.query(question)
.withTopK(5)
.withSimilarityThreshold(0.7)
);
if (relevantDocs.isEmpty()) {
return "抱歉,我在知识库中没有找到相关信息。";
}
// 2. 构建上下文
String context = relevantDocs.stream()
.map(doc -> doc.getContent())
.collect(Collectors.joining("\n\n"));
log.info("检索到 {} 个相关文档", relevantDocs.size());
// 3. 生成回答
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(SYSTEM_PROMPT);
Message systemMessage = systemPromptTemplate.createMessage(Map.of("context", context));
UserMessage userMessage = new UserMessage(question);
Prompt prompt = new Prompt(List.of(systemMessage, userMessage));
String answer = chatClient.prompt(prompt).call().content();
log.info("生成回答完成");
return answer;
}
/**
* RAG 问答 - 使用 QuestionAnswerAdvisor
*/
public String askWithAdvisor(String question) {
return chatClient.prompt()
.user(question)
.advisors(new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults()))
.call()
.content();
}
/**
* RAG 问答 - 带引用来源
*/
public RagResponse askWithSources(String question, int topK) {
log.info("收到问题(带来源): {}", question);
// 1. 检索相关文档
List<Document> relevantDocs = vectorStore.similaritySearch(
SearchRequest.query(question)
.withTopK(topK)
.withSimilarityThreshold(0.7)
);
if (relevantDocs.isEmpty()) {
return RagResponse.builder()
.answer("抱歉,我在知识库中没有找到相关信息。")
.sources(List.of())
.build();
}
// 2. 构建上下文
String context = relevantDocs.stream()
.map(doc -> String.format("[来源: %s]\n%s",
doc.getMetadata().getOrDefault("filename", "未知"),
doc.getContent()))
.collect(Collectors.joining("\n\n"));
// 3. 生成回答
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(SYSTEM_PROMPT +
"\n\n请在回答末尾列出引用的来源文档。");
Message systemMessage = systemPromptTemplate.createMessage(Map.of("context", context));
UserMessage userMessage = new UserMessage(question);
Prompt prompt = new Prompt(List.of(systemMessage, userMessage));
String answer = chatClient.prompt(prompt).call().content();
// 4. 提取来源信息
List<DocumentSource> sources = relevantDocs.stream()
.map(doc -> DocumentSource.builder()
.filename(doc.getMetadata().getOrDefault("filename", "未知").toString())
.content(doc.getContent().substring(0, Math.min(200, doc.getContent().length())))
.metadata(doc.getMetadata())
.build())
.collect(Collectors.toList());
return RagResponse.builder()
.answer(answer)
.sources(sources)
.build();
}
/**
* RAG 响应对象
*/
@lombok.Builder
@lombok.Data
public static class RagResponse {
private String answer;
private List<DocumentSource> sources;
}
/**
* 文档来源对象
*/
@lombok.Builder
@lombok.Data
public static class DocumentSource {
private String filename;
private String content;
private Map<String, Object> metadata;
}
}
6.4 流式 RAG 响应
java
package com.example.springai.redis.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@Service
public class StreamingRagService {
private final ChatClient chatClient;
private final VectorStore vectorStore;
private static final String SYSTEM_PROMPT = """
你是一个专业的知识库助手。基于以下上下文信息回答用户问题:
{context}
请确保回答准确、专业,如果上下文中没有相关信息,请明确告知。
""";
public StreamingRagService(ChatClient.Builder chatClientBuilder, VectorStore vectorStore) {
this.chatClient = chatClientBuilder.build();
this.vectorStore = vectorStore;
}
/**
* 流式 RAG 问答
*/
public Flux<String> askStreaming(String question) {
log.info("开始流式问答: {}", question);
// 1. 检索相关文档
List<Document> relevantDocs = vectorStore.similaritySearch(
SearchRequest.query(question)
.withTopK(5)
.withSimilarityThreshold(0.7)
);
if (relevantDocs.isEmpty()) {
return Flux.just("抱歉,我在知识库中没有找到相关信息。");
}
// 2. 构建上下文
String context = relevantDocs.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n\n"));
// 3. 流式生成回答
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(SYSTEM_PROMPT);
Message systemMessage = systemPromptTemplate.createMessage(Map.of("context", context));
UserMessage userMessage = new UserMessage(question);
Prompt prompt = new Prompt(List.of(systemMessage, userMessage));
return chatClient.prompt(prompt).stream().content();
}
}
7. 高级特性
7.1 元数据过滤与混合查询
java
package com.example.springai.redis.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.filter.Filter;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Slf4j
@Service
public class AdvancedSearchService {
private final VectorStore vectorStore;
public AdvancedSearchService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}
/**
* 按文档类型搜索
*/
public List<Document> searchByDocumentType(String query, String docType, int topK) {
FilterExpressionBuilder builder = new FilterExpressionBuilder();
Filter.Expression filter = builder.eq("doc_type", docType).build();
return vectorStore.similaritySearch(
SearchRequest.query(query)
.withTopK(topK)
.withFilterExpression(filter)
);
}
/**
* 按日期范围搜索
*/
public List<Document> searchByDateRange(String query, LocalDate startDate,
LocalDate endDate, int topK) {
FilterExpressionBuilder builder = new FilterExpressionBuilder();
DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE;
String start = startDate.format(formatter);
String end = endDate.format(formatter);
Filter.Expression filter = builder.and(
builder.gte("date", start),
builder.lte("date", end)
).build();
return vectorStore.similaritySearch(
SearchRequest.query(query)
.withTopK(topK)
.withFilterExpression(filter)
);
}
/**
* 组合过滤:类型 + 标签 + 日期
*/
public List<Document> complexSearch(String query, String docType,
List<String> tags, LocalDate afterDate, int topK) {
FilterExpressionBuilder builder = new FilterExpressionBuilder();
// 文档类型过滤
Filter.Expression typeFilter = builder.eq("doc_type", docType).build();
// 标签过滤(任一匹配)
Filter.Expression tagFilter = null;
for (String tag : tags) {
Filter.Expression tagExpr = builder.eq("tags", tag).build();
if (tagFilter == null) {
tagFilter = tagExpr;
} else {
tagFilter = builder.or(tagFilter, tagExpr).build();
}
}
// 日期过滤
String afterDateStr = afterDate.format(DateTimeFormatter.ISO_DATE);
Filter.Expression dateFilter = builder.gte("date", afterDateStr).build();
// 组合所有过滤器
Filter.Expression combinedFilter = builder.and(
builder.and(typeFilter, tagFilter),
dateFilter
).build();
return vectorStore.similaritySearch(
SearchRequest.query(query)
.withTopK(topK)
.withFilterExpression(combinedFilter)
);
}
/**
* 按作者和部门搜索
*/
public List<Document> searchByAuthorAndDepartment(String query, String author,
String department, int topK) {
FilterExpressionBuilder builder = new FilterExpressionBuilder();
Filter.Expression filter = builder.and(
builder.eq("author", author),
builder.eq("department", department)
).build();
return vectorStore.similaritySearch(
SearchRequest.query(query)
.withTopK(topK)
.withSimilarityThreshold(0.6)
.withFilterExpression(filter)
);
}
}
7.2 文档管理服务
java
package com.example.springai.redis.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
import redis.clients.jedis.JedisPooled;
import redis.clients.jedis.search.Query;
import redis.clients.jedis.search.SearchResult;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
public class DocumentManagementService {
private final VectorStore vectorStore;
private final JedisPooled jedisPooled;
public DocumentManagementService(VectorStore vectorStore, JedisPooled jedisPooled) {
this.vectorStore = vectorStore;
this.jedisPooled = jedisPooled;
}
/**
* 删除文档
*/
public void deleteDocuments(List<String> documentIds) {
log.info("删除文档: {}", documentIds);
vectorStore.delete(documentIds);
log.info("✅ 已删除 {} 个文档", documentIds.size());
}
/**
* 按元数据删除文档
*/
public void deleteByMetadata(String metadataKey, String metadataValue) {
log.info("按元数据删除: {}={}", metadataKey, metadataValue);
// 查询符合条件的文档
String queryStr = String.format("@%s:%s", metadataKey, metadataValue);
Query query = new Query(queryStr).limit(0, 10000);
try {
SearchResult result = jedisPooled.ftSearch("spring-ai-index", query);
List<String> idsToDelete = new ArrayList<>();
result.getDocuments().forEach(doc -> {
idsToDelete.add(doc.getId());
});
if (!idsToDelete.isEmpty()) {
deleteDocuments(idsToDelete);
}
log.info("✅ 共删除 {} 个文档", idsToDelete.size());
} catch (Exception e) {
log.error("删除失败", e);
throw new RuntimeException("删除文档失败", e);
}
}
/**
* 更新文档元数据
*/
public void updateDocumentMetadata(String documentId, Map<String, Object> newMetadata) {
log.info("更新文档元数据: id={}, metadata={}", documentId, newMetadata);
// Redis 不支持直接更新,需要先删除再添加
// 实际应用中可能需要先查询文档内容
log.info("✅ 文档元数据已更新");
}
/**
* 统计文档数量
*/
public long countDocuments() {
try {
Query query = new Query("*").limit(0, 0);
SearchResult result = jedisPooled.ftSearch("spring-ai-index", query);
return result.getTotalResults();
} catch (Exception e) {
log.error("统计文档失败", e);
return 0;
}
}
/**
* 清空所有文档
*/
public void clearAllDocuments() {
log.warn("⚠️ 准备清空所有文档");
try {
Query query = new Query("*").limit(0, 10000);
SearchResult result = jedisPooled.ftSearch("spring-ai-index", query);
List<String> allIds = new ArrayList<>();
result.getDocuments().forEach(doc -> allIds.add(doc.getId()));
if (!allIds.isEmpty()) {
vectorStore.delete(allIds);
log.info("✅ 已清空 {} 个文档", allIds.size());
} else {
log.info("ℹ️ 没有文档需要清空");
}
} catch (Exception e) {
log.error("清空文档失败", e);
throw new RuntimeException("清空文档失败", e);
}
}
}
8. REST API 控制器
8.1 文档上传 API
java
package com.example.springai.redis.controller;
import com.example.springai.redis.service.DocumentProcessService;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/documents")
public class DocumentController {
private final DocumentProcessService documentProcessService;
public DocumentController(DocumentProcessService documentProcessService) {
this.documentProcessService = documentProcessService;
}
/**
* 上传文档
*/
@PostMapping("/upload")
public ResponseEntity<ApiResponse> uploadDocument(
@RequestParam("file") MultipartFile file,
@RequestParam(value = "category", required = false) String category,
@RequestParam(value = "tags", required = false) String tags) {
try {
log.info("收到文档上传请求: {}", file.getOriginalFilename());
// 准备元数据
Map<String, Object> metadata = new HashMap<>();
if (category != null) {
metadata.put("category", category);
}
if (tags != null) {
metadata.put("tags", tags);
}
metadata.put("upload_time", System.currentTimeMillis());
// 处理文档
ByteArrayResource resource = new ByteArrayResource(file.getBytes()) {
@Override
public String getFilename() {
return file.getOriginalFilename();
}
};
String result = documentProcessService.processAndStoreDocument(resource, metadata);
return ResponseEntity.ok(ApiResponse.success(result));
} catch (Exception e) {
log.error("文档上传失败", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("文档上传失败: " + e.getMessage()));
}
}
@Data
public static class ApiResponse {
private boolean success;
private String message;
private Object data;
public static ApiResponse success(Object data) {
ApiResponse response = new ApiResponse();
response.setSuccess(true);
response.setData(data);
return response;
}
public static ApiResponse error(String message) {
ApiResponse response = new ApiResponse();
response.setSuccess(false);
response.setMessage(message);
return response;
}
}
}
8.2 问答 API
java
package com.example.springai.redis.controller;
import com.example.springai.redis.service.RagService;
import com.example.springai.redis.service.StreamingRagService;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
@Slf4j
@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final RagService ragService;
private final StreamingRagService streamingRagService;
public ChatController(RagService ragService, StreamingRagService streamingRagService) {
this.ragService = ragService;
this.streamingRagService = streamingRagService;
}
/**
* 普通问答
*/
@PostMapping("/ask")
public ResponseEntity<ChatResponse> ask(@RequestBody ChatRequest request) {
log.info("收到问题: {}", request.getQuestion());
String answer = ragService.ask(request.getQuestion());
return ResponseEntity.ok(ChatResponse.builder()
.question(request.getQuestion())
.answer(answer)
.build());
}
/**
* 带引用来源的问答
*/
@PostMapping("/ask-with-sources")
public ResponseEntity<RagService.RagResponse> askWithSources(@RequestBody ChatRequest request) {
log.info("收到问题(需要来源): {}", request.getQuestion());
int topK = request.getTopK() != null ? request.getTopK() : 5;
RagService.RagResponse response = ragService.askWithSources(request.getQuestion(), topK);
return ResponseEntity.ok(response);
}
/**
* 流式问答
*/
@GetMapping(value = "/ask-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> askStream(@RequestParam String question) {
log.info("收到流式问题: {}", question);
return streamingRagService.askStreaming(question);
}
@Data
public static class ChatRequest {
private String question;
private Integer topK;
}
@Data
@lombok.Builder
public static class ChatResponse {
private String question;
private String answer;
}
}
8.3 搜索 API
java
package com.example.springai.redis.controller;
import com.example.springai.redis.service.VectorSearchService;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@RestController
@RequestMapping("/api/search")
public class SearchController {
private final VectorSearchService searchService;
public SearchController(VectorSearchService searchService) {
this.searchService = searchService;
}
/**
* 基础搜索
*/
@PostMapping
public ResponseEntity<SearchResponse> search(@RequestBody SearchRequest request) {
log.info("收到搜索请求: {}", request.getQuery());
List<Document> documents = searchService.search(
request.getQuery(),
request.getTopK() != null ? request.getTopK() : 10
);
List<SearchResult> results = documents.stream()
.map(doc -> SearchResult.builder()
.content(doc.getContent())
.metadata(doc.getMetadata())
.build())
.collect(Collectors.toList());
return ResponseEntity.ok(SearchResponse.builder()
.query(request.getQuery())
.total(results.size())
.results(results)
.build());
}
/**
* 带过滤的搜索
*/
@PostMapping("/filtered")
public ResponseEntity<SearchResponse> searchWithFilter(@RequestBody FilteredSearchRequest request) {
log.info("收到过滤搜索请求: query={}, filters={}", request.getQuery(), request.getFilters());
List<Document> documents = searchService.searchWithMetadataFilter(
request.getQuery(),
request.getTopK() != null ? request.getTopK() : 10,
request.getFilters()
);
List<SearchResult> results = documents.stream()
.map(doc -> SearchResult.builder()
.content(doc.getContent())
.metadata(doc.getMetadata())
.build())
.collect(Collectors.toList());
return ResponseEntity.ok(SearchResponse.builder()
.query(request.getQuery())
.total(results.size())
.results(results)
.build());
}
@Data
public static class SearchRequest {
private String query;
private Integer topK;
}
@Data
public static class FilteredSearchRequest extends SearchRequest {
private Map<String, Object> filters;
}
@Data
@lombok.Builder
public static class SearchResponse {
private String query;
private Integer total;
private List<SearchResult> results;
}
@Data
@lombok.Builder
public static class SearchResult {
private String content;
private Map<String, Object> metadata;
}
}
9. 性能优化与调优
9.1 Redis 配置优化
yaml
# application-prod.yml
spring:
data:
redis:
# 连接池配置
jedis:
pool:
max-active: 50 # 最大连接数
max-idle: 20 # 最大空闲连接
min-idle: 10 # 最小空闲连接
max-wait: 5000ms # 最大等待时间
# 超时配置
timeout: 10s # 命令超时
connect-timeout: 5s # 连接超时
# 集群配置(可选)
cluster:
nodes:
- redis-node1:6379
- redis-node2:6379
- redis-node3:6379
max-redirects: 3
app:
vector:
# HNSW 参数优化
algorithm: HNSW
hnsw:
m: 16 # 连接数(8-64,越大越准确但占用更多内存)
ef-construction: 200 # 构建时的搜索深度(100-500)
ef-runtime: 10 # 查询时的搜索深度(topK的1-2倍)
9.2 批量处理优化
java
package com.example.springai.redis.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Slf4j
@Service
public class BatchProcessService {
private final VectorStore vectorStore;
private final ExecutorService executorService;
private static final int BATCH_SIZE = 100;
public BatchProcessService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
this.executorService = Executors.newFixedThreadPool(4);
}
/**
* 批量添加文档(优化版本)
*/
public void batchAddDocuments(List<Document> documents) {
log.info("开始批量添加 {} 个文档", documents.size());
long startTime = System.currentTimeMillis();
// 分批处理
List<List<Document>> batches = partition(documents, BATCH_SIZE);
log.info("分成 {} 批,每批 {} 个文档", batches.size(), BATCH_SIZE);
// 并行处理各批次
List<CompletableFuture<Void>> futures = new ArrayList<>();
for (int i = 0; i < batches.size(); i++) {
final int batchIndex = i;
final List<Document> batch = batches.get(i);
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
vectorStore.add(batch);
log.info("✅ 批次 {} 完成,处理了 {} 个文档", batchIndex + 1, batch.size());
} catch (Exception e) {
log.error("❌ 批次 {} 失败", batchIndex + 1, e);
throw new RuntimeException(e);
}
}, executorService);
futures.add(future);
}
// 等待所有批次完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
long duration = System.currentTimeMillis() - startTime;
log.info("✅ 批量添加完成,共 {} 个文档,耗时 {}ms", documents.size(), duration);
}
/**
* 分割列表
*/
private <T> List<List<T>> partition(List<T> list, int size) {
List<List<T>> partitions = new ArrayList<>();
for (int i = 0; i < list.size(); i += size) {
partitions.add(list.subList(i, Math.min(i + size, list.size())));
}
return partitions;
}
}
9.3 缓存策略
java
package com.example.springai.redis.service;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.List;
@Slf4j
@Service
public class CachedSearchService {
private final VectorSearchService searchService;
private final Cache<String, List<Document>> searchCache;
public CachedSearchService(VectorSearchService searchService) {
this.searchService = searchService;
// 配置缓存
this.searchCache = Caffeine.newBuilder()
.maximumSize(1000) // 最多缓存 1000 个查询
.expireAfterWrite(Duration.ofHours(1)) // 1小时后过期
.recordStats() // 记录统计信息
.build();
}
/**
* 带缓存的搜索
*/
public List<Document> search(String query, int topK) {
String cacheKey = query + ":" + topK;
return searchCache.get(cacheKey, key -> {
log.info("缓存未命中,执行搜索: {}", query);
return searchService.search(query, topK);
});
}
/**
* 清除缓存
*/
public void clearCache() {
searchCache.invalidateAll();
log.info("✅ 搜索缓存已清空");
}
/**
* 获取缓存统计
*/
public void printCacheStats() {
var stats = searchCache.stats();
log.info("缓存统计 - 命中率: {:.2f}%, 请求数: {}, 命中数: {}, 未命中数: {}",
stats.hitRate() * 100,
stats.requestCount(),
stats.hitCount(),
stats.missCount());
}
}
10. 实战案例:企业文档问答系统
10.1 完整示例
java
package com.example.springai.redis.demo;
import com.example.springai.redis.service.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component
public class EnterpriseDocQADemo implements CommandLineRunner {
private final DocumentProcessService documentProcessService;
private final RagService ragService;
private final VectorSearchService searchService;
public EnterpriseDocQADemo(
DocumentProcessService documentProcessService,
RagService ragService,
VectorSearchService searchService) {
this.documentProcessService = documentProcessService;
this.ragService = ragService;
this.searchService = searchService;
}
@Override
public void run(String... args) throws Exception {
log.info("========================================");
log.info("企业文档问答系统演示");
log.info("========================================\n");
// 步骤 1: 加载企业文档
loadEnterpriseDocuments();
// 步骤 2: 执行问答
performQuestionAnswering();
// 步骤 3: 执行搜索
performSearch();
}
private void loadEnterpriseDocuments() {
log.info("📁 步骤 1: 加载企业文档");
try {
// 加载技术文档
Map<String, Object> techMetadata = new HashMap<>();
techMetadata.put("category", "技术文档");
techMetadata.put("department", "研发部");
documentProcessService.processAndStoreDocument(
new ClassPathResource("docs/spring-boot-guide.pdf"),
techMetadata
);
log.info("✅ 文档加载完成\n");
} catch (Exception e) {
log.error("文档加载失败", e);
}
}
private void performQuestionAnswering() {
log.info("💬 步骤 2: 执行问答");
String[] questions = {
"Spring Boot 的主要特性是什么?",
"如何配置数据源?",
"什么是自动配置原理?"
};
for (String question : questions) {
log.info("\n问题: {}", question);
String answer = ragService.ask(question);
log.info("回答: {}\n", answer);
}
}
private void performSearch() {
log.info("🔍 步骤 3: 执行相似度搜索");
String query = "Spring Boot 配置";
var results = searchService.search(query, 3);
log.info("\n搜索: {}", query);
log.info("找到 {} 个相关文档:\n", results.size());
for (int i = 0; i < results.size(); i++) {
var doc = results.get(i);
log.info("{}. {}", i + 1, doc.getContent().substring(0, Math.min(100, doc.getContent().length())));
log.info(" 来源: {}\n", doc.getMetadata().get("filename"));
}
}
}
11. 常见问题与解决方案
11.1 性能问题
问题:搜索速度慢
java
// 解决方案 1: 使用 HNSW 索引
spring.ai.vectorstore.redis.algorithm=HNSW
// 解决方案 2: 减少 topK
SearchRequest.query(query).withTopK(5) // 而不是 20
// 解决方案 3: 提高相似度阈值
SearchRequest.query(query).withSimilarityThreshold(0.8) // 过滤低相关结果
11.2 内存问题
问题:Redis 内存占用过高
bash
# 查看内存使用
redis-cli INFO memory
# 设置最大内存
redis-cli CONFIG SET maxmemory 2gb
redis-cli CONFIG SET maxmemory-policy allkeys-lru
11.3 召回率问题
问题:搜索结果不相关
java
// 解决方案 1: 优化文档分块
app.document.chunk-size=500 // 减小块大小
app.document.chunk-overlap=100 // 增加重叠
// 解决方案 2: 调整相似度阈值
.withSimilarityThreshold(0.7) // 降低阈值
// 解决方案 3: 增加检索数量
.withTopK(10) // 检索更多文档
12. 总结与展望
12.1 核心要点回顾
┌────────────────────────────────────────────────────────┐
│ Spring AI + Redis 向量库核心价值 │
├────────────────────────────────────────────────────────┤
│ │
│ ✅ 高性能 │
│ • 毫秒级向量搜索 │
│ • 内存级存储速度 │
│ │
│ ✅ 易集成 │
│ • Spring Boot 原生支持 │
│ • 零代码配置 │
│ │
│ ✅ 功能强大 │
│ • 混合查询(向量+元数据) │
│ • 实时索引更新 │
│ │
│ ✅ 成本低 │
│ • 无需专用向量数据库 │
│ • 运维成本低 │
│ │
│ ✅ 生产就绪 │
│ • 集群支持 │
│ • 完善的监控 │
│ │
└────────────────────────────────────────────────────────┘
12.2 最佳实践总结
- 文档处理:合理的分块大小(500-1000 字符)和重叠(10-20%)
- 索引选择:小规模用 FLAT,大规模用 HNSW
- 性能优化:使用批量操作、缓存常见查询
- 监控告警:跟踪搜索延迟、缓存命中率
- 元数据设计:添加丰富的元数据以支持过滤
12.3 未来展望
- 多模态支持(图像、音频向量)
- 更智能的查询重写
- 自动化参数调优
- 分布式向量搜索