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

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

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)

    ┌──────────────────────────────────────────────────────────┐
    │ 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 未来展望

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

相关推荐
科技小花3 小时前
数据治理平台架构演进观察:AI原生设计如何重构企业数据管理范式
数据库·重构·架构·数据治理·ai-native·ai原生
一江寒逸3 小时前
零基础从入门到精通MySQL(中篇):进阶篇——吃透多表查询、事务核心与高级特性,搞定复杂业务SQL
数据库·sql·mysql
D4c-lovetrain3 小时前
linux个人心得22 (mysql)
数据库·mysql
阿里小阿希4 小时前
CentOS7 PostgreSQL 9.2 升级到 15 完整教程
数据库·postgresql
荒川之神4 小时前
Oracle 数据仓库雪花模型设计(完整实战方案)
数据库·数据仓库·oracle
做个文艺程序员4 小时前
MySQL安全加固十大硬核操作
数据库·mysql·安全
不吃香菜学java4 小时前
Redis简单应用
数据库·spring boot·tomcat·maven
一个天蝎座 白勺 程序猿4 小时前
Apache IoTDB(15):IoTDB查询写回(INTO子句)深度解析——从语法到实战的ETL全链路指南
数据库·apache·etl·iotdb
不知名的老吴4 小时前
Redis的延迟瓶颈:TCP栈开销无法避免
数据库·redis·缓存
YOU OU4 小时前
三大范式和E-R图
数据库