Spring AI + Redis 向量库实战

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

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 最佳实践总结

  1. 文档处理:合理的分块大小(500-1000 字符)和重叠(10-20%)
  2. 索引选择:小规模用 FLAT,大规模用 HNSW
  3. 性能优化:使用批量操作、缓存常见查询
  4. 监控告警:跟踪搜索延迟、缓存命中率
  5. 元数据设计:添加丰富的元数据以支持过滤

12.3 未来展望

  • 多模态支持(图像、音频向量)
  • 更智能的查询重写
  • 自动化参数调优
  • 分布式向量搜索

相关推荐
CC.GG1 小时前
【C++】面向对象三大特性之一——继承
java·数据库·c++
零匠学堂20251 小时前
woapi-server为Office Online Server文档在线预览提供文档加载地址
java·运维·服务器·oos·wopi
Hui Baby1 小时前
maven自动构建到镜像仓库
java·maven
czlczl200209251 小时前
SpringBoot手动配置:WebMvcConfigurer接口实现类的生效原理
java·spring boot·后端
野生技术架构师1 小时前
SpringBoot项目使用Redis对用户IP进行接口限流
spring boot·redis·bootstrap
程序员皮皮林1 小时前
SpringBoot + nmap4j 获取端口信息
java·spring boot·后端
小二·1 小时前
Spring框架入门:Spring 中注解支持详解
java·后端·spring
豐儀麟阁贵1 小时前
8.6运行时异常
java·开发语言
桃子叔叔1 小时前
Prompt Engineering完全指南:从基础到高阶技术实战
java·服务器·prompt