Spring AI + Redis 向量库实战:构建高性能 RAG 应用

摘要

Spring AI 是 Spring 生态系统中的 AI 应用开发框架,而 Redis 作为高性能内存数据库,其向量搜索能力(Redis Stack)为 RAG(检索增强生成)应用提供了强大支持。本文将深入探讨如何使用 Spring AI 最新版本结合 Redis 向量库,构建生产级的智能问答系统。

目录

  1. Spring AI 与 Redis 向量库简介
  2. 核心架构与技术选型
  3. 环境准备与依赖配置
  4. Redis 向量库核心概念
  5. 快速开始
  6. 核心功能实现
  7. 高级特性
  8. 生产环境实践
  9. 性能优化与调优
  10. 实战案例
  11. 常见问题与解决方案
  12. 总结与展望

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 整体架构图

┌──────────────────────────────────────────────────────────────┐ │ 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 工作流程

┌──────────────────────────────────────────────────────────────┐ │ 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 方式(推荐):


# 拉取 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 方式:


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 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:


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)

┌──────────────────────────────────────────────────────────┐ │ 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 启动类

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 基础配置类

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 第一个示例

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 文档加载器服务

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 文档处理服务

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 向量搜索服务

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 问答服务

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 响应

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 元数据过滤与混合查询

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 文档管理服务

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

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

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

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 配置优化

# 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 批量处理优化

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 缓存策略

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 完整示例

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 性能问题

问题:搜索速度慢


// 解决方案 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 内存占用过高


# 查看内存使用 redis-cli INFO memory # 设置最大内存 redis-cli CONFIG SET maxmemory 2gb redis-cli CONFIG SET maxmemory-policy allkeys-lru

11.3 召回率问题

问题:搜索结果不相关


// 解决方案 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 最佳实践总结
  1. 文档处理:合理的分块大小(500-1000 字符)和重叠(10-20%)
  2. 索引选择:小规模用 FLAT,大规模用 HNSW
  3. 性能优化:使用批量操作、缓存常见查询
  4. 监控告警:跟踪搜索延迟、缓存命中率
  5. 元数据设计:添加丰富的元数据以支持过滤
12.3 未来展望
  • 多模态支持(图像、音频向量)
  • 更智能的查询重写
  • 自动化参数调优
  • 分布式向量搜索
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐