欢迎光临
我们一直在努力

SpringAI_Redis向量库实战

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

摘要

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

目录

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

  • 1. Spring AI 与 Redis 向量库简介

    1.1 为什么选择 Redis 作为向量库

    Redis Stack 提供了 RediSearch 模块,支持向量相似度搜索(VSS),具有以下优势:

    核心优势:

    • ⚡ 高性能:基于内存的向量搜索,毫秒级响应
    • 🔄 实时性:支持实时索引更新,无需重建
    • 📦 易部署:单一服务,无需复杂架构
    • 💰 成本低:相比专用向量数据库,运维成本更低
    • 🛠️ 功能丰富:支持混合查询(向量+元数据过滤)
    • 🔗 生态好:Spring AI 原生支持

    1.2 应用场景

    ┌─────────────────────────────────────────────────────────────────┐
    │ Spring AI + Redis 向量库应用场景全景图 │
    ├─────────────────────────────────────────────────────────────────┤
    │ │
    │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐ │
    │ │ 智能文档检索 │ │ 企业知识库 │ │ 智能客服 │ │
    │ │ │ │ │ │ │ │
    │ │ • PDF问答 │ │ • 内部文档搜索 │ │ • FAQ自动 │ │
    │ │ • 文档摘要 │ │ • 政策查询 │ │ 回答 │ │
    │ │ • 多文档对比 │ │ • 技术文档库 │ │ • 意图识别 │ │
    │ │ • 引用溯源 │ │ • 规章制度查询 │ │ • 上下文 │ │
    │ │ │ │ │ │ 理解 │ │
    │ └──────────────────┘ └──────────────────┘ └──────────────┘ │
    │ │
    │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐ │
    │ │ 代码搜索助手 │ │ 电商推荐系统 │ │ 内容审核 │ │
    │ │ │ │ │ │ │ │
    │ │ • 语义代码搜索 │ │ • 商品相似推荐 │ │ • 重复内容 │ │
    │ │ • API文档查询 │ │ • 用户画像匹配 │ │ 检测 │ │
    │ │ • Bug相似查找 │ │ • 个性化搜索 │ │ • 敏感信息 │ │
    │ │ • 代码生成 │ │ • 关联商品发现 │ │ 过滤 │ │
    │ │ │ │ │ │ │ │
    │ └──────────────────┘ └──────────────────┘ └──────────────┘ │
    │ │
    │ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐ │
    │ │ 实时问答系统 │ │ 多语言搜索 │ │ 日志分析 │ │
    │ │ │ │ │ │ │ │
    │ │ • 流式响应 │ │ • 跨语言检索 │ │ • 异常模式 │ │
    │ │ • 会话记忆 │ │ • 翻译+搜索 │ │ 识别 │ │
    │ │ • 多轮对话 │ │ • 多语言FAQ │ │ • 根因分析 │ │
    │ │ • 上下文感知 │ │ • 国际化支持 │ │ • 趋势预测 │ │
    │ │ │ │ │ │ │ │
    │ └──────────────────┘ └──────────────────┘ └──────────────┘ │
    │ │
    └─────────────────────────────────────────────────────────────────┘

    1.3 技术栈对比

    特性Redis VSSPineconeMilvusChroma
    性能 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐
    易用性 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐
    成本
    实时更新 ✅ 优秀 ✅ 优秀 ✅ 优秀 ⚠️ 一般
    混合查询 ✅ 支持 ✅ 支持 ✅ 支持 ⚠️ 有限
    Spring AI ✅ 原生 ✅ 原生 ✅ 原生 ✅ 原生
    部署复杂度 简单 托管 复杂 简单
    数据规模 百万级 亿级 亿级 百万级

    2. 核心架构与技术选型

    2.1 整体架构图

    ┌──────────────────────────────────────────────────────────────┐
    │ Spring Boot Application │
    ├──────────────────────────────────────────────────────────────┤
    │ │
    │ ┌────────────────────────────────────────────────────────┐ │
    │ │ Controller Layer │ │
    │ │ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │ │
    │ │ │ Document API │ │ Chat API │ │ Search API │ │ │
    │ │ └──────────────┘ └──────────────┘ └─────────────┘ │ │
    │ └───────────────────────┬────────────────────────────────┘ │
    │ │ │
    │ ┌───────────────────────▼────────────────────────────────┐ │
    │ │ Service Layer │ │
    │ │ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │ │
    │ │ │ RAG Service │ │ Chat Service │ │ Doc Service │ │ │
    │ │ └──────────────┘ └──────────────┘ └─────────────┘ │ │
    │ └───────────────────────┬────────────────────────────────┘ │
    │ │ │
    │ ┌───────────────────────▼────────────────────────────────┐ │
    │ │ Spring AI Framework │ │
    │ │ ┌─────────────────────────────────────────────────┐ │ │
    │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ │
    │ │ │ │ChatClient│ │Embedding │ │DocumentReader│ │ │ │
    │ │ │ │ │ │ Model │ │ │ │ │ │
    │ │ │ └──────────┘ └──────────┘ └──────────────┘ │ │ │
    │ │ │ │ │ │
    │ │ │ ┌──────────────────────────────────────────┐ │ │ │
    │ │ │ │ VectorStore (Redis) │ │ │ │
    │ │ │ │ • add() │ │ │ │
    │ │ │ │ • similaritySearch() │ │ │ │
    │ │ │ │ • delete() │ │ │ │
    │ │ │ └──────────────────────────────────────────┘ │ │ │
    │ │ └─────────────────────────────────────────────────┘ │ │
    │ └───────────────────────┬────────────────────────────────┘ │
    │ │ │
    │ ┌───────────────────────▼────────────────────────────────┐ │
    │ │ Redis Stack (Vector Store) │ │
    │ │ ┌─────────────────────────────────────────────────┐ │ │
    │ │ │ RediSearch Module (Vector Similarity Search) │ │ │
    │ │ │ • HNSW Index │ │ │
    │ │ │ • FLAT Index │ │ │
    │ │ │ • Cosine Similarity │ │ │
    │ │ │ • L2 Distance │ │ │
    │ │ │ • IP (Inner Product) │ │ │
    │ │ └─────────────────────────────────────────────────┘ │ │
    │ └─────────────────────────────────────────────────────────┘ │
    │ │ │
    │ ┌───────────────────────▼────────────────────────────────┐ │
    │ │ LLM Provider │ │
    │ │ (OpenAI / Azure / Ollama / Qwen…) │ │
    │ └─────────────────────────────────────────────────────────┘ │
    │ │
    └──────────────────────────────────────────────────────────────┘

    2.2 RAG 工作流程

    ┌──────────────────────────────────────────────────────────────┐
    │ RAG 工作流程详解 │
    └──────────────────────────────────────────────────────────────┘

    【索引构建阶段】
    ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
    │ 原始文档 │─────▶│ 文档分割 │─────▶│ 向量化 │
    │ (PDF/TXT) │ │ (Chunking) │ │ (Embedding) │
    └─────────────┘ └─────────────┘ └─────────────┘


    ┌─────────────────┐
    │ 存储到Redis │
    │ (Vector Store) │
    └─────────────────┘

    【查询响应阶段】
    ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
    │ 用户问题 │─────▶│ 向量化 │─────▶│ 相似度搜索 │
    │ │ │ │ │ (Redis VSS)│
    └─────────────┘ └─────────────┘ └─────────────┘


    ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
    │ 返回答案 │◀─────│ LLM生成 │◀─────│ 检索上下文 │
    │ │ │ │ │ │
    └─────────────┘ └─────────────┘ └─────────────┘


    3. 环境准备与依赖配置

    3.1 环境要求

    基础环境:

    • JDK 17+
    • Spring Boot 3.2+
    • Maven 3.8+ / Gradle 8.0+
    • Redis Stack 7.2+(支持 RediSearch)

    3.2 Redis Stack 安装

    Docker 方式(推荐):

    # 拉取 Redis Stack 镜像
    docker pull redis/redis-stack:latest

    # 启动 Redis Stack
    docker run -d \\
    –name redis-stack \\
    -p 6379:6379 \\
    -p 8001:8001 \\
    -e REDIS_ARGS="–requirepass mypassword" \\
    redis/redis-stack:latest

    # 验证安装
    docker exec -it redis-stack redis-cli
    > AUTH mypassword
    > FT._LIST

    Docker Compose 方式:

    version: '3.8'

    services:
    redis-stack:
    image: redis/redisstack:latest
    container_name: redisvectorstore
    ports:
    "6379:6379"
    "8001:8001"
    environment:
    REDIS_ARGS=requirepass mypassword
    volumes:
    redisdata:/data
    restart: unlessstopped

    volumes:
    redis-data:
    driver: local

    3.3 Maven 依赖配置

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
    http://maven.apache.org/xsd/maven-4.0.0.xsd"
    >

    <modelVersion>4.0.0</modelVersion>

    <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.1</version>
    <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>spring-ai-redis-demo</artifactId>
    <version>1.0.0</version>

    <properties>
    <java.version>17</java.version>
    <spring-ai.version>1.0.0-M4</spring-ai.version>
    </properties>

    <dependencies>
    <!– Spring Boot Starter Web –>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!– Spring AI OpenAI –>
    <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    </dependency>

    <!– Spring AI Redis Vector Store –>
    <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-redis-store-spring-boot-starter</artifactId>
    </dependency>

    <!– Spring AI PDF Document Reader –>
    <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-pdf-document-reader</artifactId>
    </dependency>

    <!– Spring AI TikaDocumentReader –>
    <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-tika-document-reader</artifactId>
    </dependency>

    <!– Redis –>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <!– Jedis (Redis 客户端) –>
    <dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    </dependency>

    <!– Lombok –>
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
    </dependency>

    <!– Spring Boot Starter Test –>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    </dependency>
    </dependencies>

    <dependencyManagement>
    <dependencies>
    <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-bom</artifactId>
    <version>${spring-ai.version}</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
    </dependencies>
    </dependencyManagement>

    <build>
    <plugins>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    </plugins>
    </build>

    <repositories>
    <repository>
    <id>spring-milestones</id>
    <name>Spring Milestones</name>
    <url>https://repo.spring.io/milestone</url>
    <snapshots>
    <enabled>false</enabled>
    </snapshots>
    </repository>
    </repositories>
    </project>

    3.4 配置文件

    application.yml:

    spring:
    application:
    name: springairedisdemo

    # AI 配置
    ai:
    openai:
    api-key: ${OPENAI_API_KEY}
    chat:
    options:
    model: gpt3.5turbo
    temperature: 0.7
    max-tokens: 2000
    embedding:
    options:
    model: textembeddingada002

    # Redis 向量存储配置
    vectorstore:
    redis:
    uri: redis://localhost:6379
    password: mypassword
    index: springaiindex
    prefix: doc:
    initialize-schema: true

    # Redis 配置
    data:
    redis:
    host: localhost
    port: 6379
    password: mypassword
    timeout: 60s
    jedis:
    pool:
    max-active: 20
    max-idle: 10
    min-idle: 5

    # 日志配置
    logging:
    level:
    org.springframework.ai: DEBUG
    com.example: DEBUG

    # 服务器配置
    server:
    port: 8080

    # 自定义配置
    app:
    vector:
    dimension: 1536 # OpenAI ada-002 向量维度
    algorithm: HNSW # 索引算法: FLAT 或 HNSW
    distance-metric: COSINE # 距离度量: COSINE, L2, IP
    document:
    chunk-size: 800
    chunk-overlap: 200


    4. Redis 向量库核心概念

    4.1 向量索引算法

    FLAT(暴力搜索):

    • 精确搜索,100% 召回率
    • 适合小规模数据(< 10万)
    • 线性时间复杂度 O(n)

    HNSW(层次可导航小世界图):

    • 近似搜索,高召回率(> 95%)
    • 适合大规模数据(百万级+)
    • 对数时间复杂度 O(log n)

    ┌──────────────────────────────────────────────────────────┐
    │ FLAT vs HNSW 性能对比 │
    ├──────────────────────────────────────────────────────────┤
    │ │
    │ 数据规模 FLAT 延迟 HNSW 延迟 推荐选择 │
    │ ───────────────────────────────────────────────────── │
    │ 1K < 1ms < 1ms FLAT │
    │ 10K 5-10ms < 2ms FLAT/HNSW │
    │ 100K 50-100ms < 5ms HNSW │
    │ 1M 500ms+ < 10ms HNSW │
    │ 10M+ N/A < 20ms HNSW │
    │ │
    └──────────────────────────────────────────────────────────┘

    4.2 距离度量

    Cosine Similarity(余弦相似度):

    • 范围: [-1, 1],越接近 1 越相似
    • 适用于文本向量
    • 不受向量长度影响

    L2 Distance(欧氏距离):

    • 范围: [0, ∞),越小越相似
    • 受向量长度影响
    • 适用于图像向量

    IP(内积):

    • 范围: (-∞, ∞),越大越相似
    • 性能最好
    • 需要归一化向量

    5. 快速开始

    5.1 启动类

    package com.example.springai.redis;

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;

    @SpringBootApplication
    public class SpringAiRedisApplication {

    public static void main(String[] args) {
    SpringApplication.run(SpringAiRedisApplication.class, args);
    }
    }

    5.2 基础配置类

    package com.example.springai.redis.config;

    import org.springframework.ai.embedding.EmbeddingModel;
    import org.springframework.ai.openai.OpenAiEmbeddingModel;
    import org.springframework.ai.vectorstore.RedisVectorStore;
    import org.springframework.ai.vectorstore.VectorStore;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import redis.clients.jedis.JedisPooled;

    @Configuration
    public class VectorStoreConfig {

    @Value("${spring.ai.vectorstore.redis.uri}")
    private String redisUri;

    @Value("${spring.ai.vectorstore.redis.password}")
    private String redisPassword;

    @Value("${spring.ai.vectorstore.redis.index}")
    private String indexName;

    @Value("${spring.ai.vectorstore.redis.prefix}")
    private String prefix;

    @Bean
    public JedisPooled jedisPooled() {
    // 解析 Redis URI
    String host = "localhost";
    int port = 6379;

    return new JedisPooled(host, port, null, redisPassword);
    }

    @Bean
    public VectorStore vectorStore(EmbeddingModel embeddingModel, JedisPooled jedisPooled) {
    RedisVectorStore.RedisVectorStoreConfig config = RedisVectorStore.RedisVectorStoreConfig
    .builder()
    .withIndexName(indexName)
    .withPrefix(prefix)
    .build();

    return new RedisVectorStore(config, embeddingModel, jedisPooled, true);
    }
    }

    5.3 第一个示例

    package com.example.springai.redis.example;

    import org.springframework.ai.document.Document;
    import org.springframework.ai.vectorstore.SearchRequest;
    import org.springframework.ai.vectorstore.VectorStore;
    import org.springframework.boot.CommandLineRunner;
    import org.springframework.stereotype.Component;

    import java.util.List;
    import java.util.Map;

    @Component
    public class QuickStartExample implements CommandLineRunner {

    private final VectorStore vectorStore;

    public QuickStartExample(VectorStore vectorStore) {
    this.vectorStore = vectorStore;
    }

    @Override
    public void run(String... args) throws Exception {
    // 1. 准备文档
    List<Document> documents = List.of(
    new Document("Spring Boot 是一个用于简化 Spring 应用开发的框架",
    Map.of("source", "doc1", "category", "framework")),
    new Document("Redis 是一个高性能的内存数据库,支持多种数据结构",
    Map.of("source", "doc2", "category", "database")),
    new Document("Spring AI 提供了统一的 API 来集成各种 AI 服务",
    Map.of("source", "doc3", "category", "ai"))
    );

    // 2. 添加文档到向量库
    vectorStore.add(documents);
    System.out.println("✅ 已添加 " + documents.size() + " 个文档");

    // 3. 执行相似度搜索
    String query = "如何简化应用开发?";
    List<Document> results = vectorStore.similaritySearch(
    SearchRequest.query(query).withTopK(2)
    );

    // 4. 打印结果
    System.out.println("\\n🔍 搜索: " + query);
    System.out.println("📄 找到 " + results.size() + " 个相关文档:\\n");

    for (int i = 0; i < results.size(); i++) {
    Document doc = results.get(i);
    System.out.println((i + 1) + ". " + doc.getContent());
    System.out.println(" 元数据: " + doc.getMetadata());
    System.out.println();
    }
    }
    }


    6. 核心功能实现

    6.1 文档加载与处理

    6.1.1 文档加载器服务

    package com.example.springai.redis.service;

    import org.springframework.ai.document.Document;
    import org.springframework.ai.reader.ExtractedTextFormatter;
    import org.springframework.ai.reader.pdf.PagePdfDocumentReader;
    import org.springframework.ai.reader.pdf.config.PdfDocumentReaderConfig;
    import org.springframework.ai.reader.tika.TikaDocumentReader;
    import org.springframework.ai.transformer.splitter.TokenTextSplitter;
    import org.springframework.core.io.Resource;
    import org.springframework.stereotype.Service;

    import java.util.List;

    @Service
    public class DocumentLoaderService {

    /**
    * 加载 PDF 文档
    */

    public List<Document> loadPdfDocument(Resource resource) {
    PdfDocumentReaderConfig config = PdfDocumentReaderConfig.builder()
    .withPageExtractedTextFormatter(ExtractedTextFormatter.builder()
    .withNumberOfBottomTextLinesToDelete(3)
    .withNumberOfTopPagesToSkipBeforeDelete(1)
    .build())
    .withPagesPerDocument(1)
    .build();

    PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(resource, config);
    return pdfReader.get();
    }

    /**
    * 加载多种格式文档(使用 Tika)
    */

    public List<Document> loadDocument(Resource resource) {
    TikaDocumentReader tikaReader = new TikaDocumentReader(resource);
    return tikaReader.get();
    }

    /**
    * 分割文档为小块
    */

    public List<Document> splitDocuments(List<Document> documents, int chunkSize, int overlap) {
    TokenTextSplitter splitter = new TokenTextSplitter(chunkSize, overlap, 5, 10000, true);
    return splitter.apply(documents);
    }
    }

    6.1.2 文档处理服务

    package com.example.springai.redis.service;

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.ai.document.Document;
    import org.springframework.ai.vectorstore.VectorStore;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.core.io.Resource;
    import org.springframework.stereotype.Service;

    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;

    @Slf4j
    @Service
    public class DocumentProcessService {

    private final DocumentLoaderService loaderService;
    private final VectorStore vectorStore;

    @Value("${app.document.chunk-size:800}")
    private int chunkSize;

    @Value("${app.document.chunk-overlap:200}")
    private int chunkOverlap;

    public DocumentProcessService(DocumentLoaderService loaderService, VectorStore vectorStore) {
    this.loaderService = loaderService;
    this.vectorStore = vectorStore;
    }

    /**
    * 处理并存储文档
    */

    public String processAndStoreDocument(Resource resource, Map<String, Object> metadata) {
    try {
    log.info("开始处理文档: {}", resource.getFilename());

    // 1. 加载文档
    List<Document> documents = loaderService.loadDocument(resource);
    log.info("加载了 {} 个文档页面", documents.size());

    // 2. 添加元数据
    documents.forEach(doc -> {
    Map<String, Object> meta = new HashMap<>(metadata);
    meta.put("filename", resource.getFilename());
    meta.put("page", doc.getMetadata().get("page"));
    doc.getMetadata().putAll(meta);
    });

    // 3. 分割文档
    List<Document> chunks = loaderService.splitDocuments(documents, chunkSize, chunkOverlap);
    log.info("文档分割成 {} 个块", chunks.size());

    // 4. 存储到向量库
    vectorStore.add(chunks);
    log.info("✅ 文档已成功存储到向量库");

    return String.format("成功处理文档,共 %d 个块", chunks.size());

    } catch (Exception e) {
    log.error("处理文档失败", e);
    throw new RuntimeException("文档处理失败: " + e.getMessage(), e);
    }
    }

    /**
    * 批量处理文档
    */

    public Map<String, String> processBatchDocuments(List<Resource> resources, Map<String, Object> commonMetadata) {
    Map<String, String> results = new HashMap<>();

    for (Resource resource : resources) {
    try {
    String result = processAndStoreDocument(resource, commonMetadata);
    results.put(resource.getFilename(), result);
    } catch (Exception e) {
    results.put(resource.getFilename(), "失败: " + e.getMessage());
    }
    }

    return results;
    }
    }

    6.2 向量搜索服务

    package com.example.springai.redis.service;

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.ai.document.Document;
    import org.springframework.ai.vectorstore.SearchRequest;
    import org.springframework.ai.vectorstore.VectorStore;
    import org.springframework.ai.vectorstore.filter.Filter;
    import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
    import org.springframework.stereotype.Service;

    import java.util.List;
    import java.util.Map;

    @Slf4j
    @Service
    public class VectorSearchService {

    private final VectorStore vectorStore;

    public VectorSearchService(VectorStore vectorStore) {
    this.vectorStore = vectorStore;
    }

    /**
    * 基础相似度搜索
    */

    public List<Document> search(String query, int topK) {
    log.info("执行搜索: query='{}', topK={}", query, topK);

    List<Document> results = vectorStore.similaritySearch(
    SearchRequest.query(query).withTopK(topK)
    );

    log.info("找到 {} 个相关文档", results.size());
    return results;
    }

    /**
    * 带相似度阈值的搜索
    */

    public List<Document> searchWithThreshold(String query, int topK, double threshold) {
    log.info("执行搜索: query='{}', topK={}, threshold={}", query, topK, threshold);

    List<Document> results = vectorStore.similaritySearch(
    SearchRequest.query(query)
    .withTopK(topK)
    .withSimilarityThreshold(threshold)
    );

    log.info("找到 {} 个相关文档(相似度 >= {})", results.size(), threshold);
    return results;
    }

    /**
    * 元数据过滤搜索
    */

    public List<Document> searchWithMetadataFilter(String query, int topK, Map<String, Object> filters) {
    log.info("执行过滤搜索: query='{}', topK={}, filters={}", query, topK, filters);

    // 构建过滤表达式
    FilterExpressionBuilder builder = new FilterExpressionBuilder();
    Filter.Expression filterExpression = null;

    for (Map.Entry<String, Object> entry : filters.entrySet()) {
    Filter.Expression expr = builder.eq(entry.getKey(), entry.getValue()).build();
    if (filterExpression == null) {
    filterExpression = expr;
    } else {
    filterExpression = builder.and(filterExpression, expr).build();
    }
    }

    List<Document> results = vectorStore.similaritySearch(
    SearchRequest.query(query)
    .withTopK(topK)
    .withFilterExpression(filterExpression)
    );

    log.info("找到 {} 个相关文档(应用过滤器后)", results.size());
    return results;
    }

    /**
    * 高级搜索:支持分类、日期范围等复杂过滤
    */

    public List<Document> advancedSearch(String query, SearchRequest.Builder builder) {
    List<Document> results = vectorStore.similaritySearch(builder.query(query).build());
    log.info("高级搜索完成,找到 {} 个结果", results.size());
    return results;
    }
    }

    6.3 RAG 问答服务

    package com.example.springai.redis.service;

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.ai.chat.client.ChatClient;
    import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor;
    import org.springframework.ai.chat.messages.Message;
    import org.springframework.ai.chat.messages.UserMessage;
    import org.springframework.ai.chat.prompt.Prompt;
    import org.springframework.ai.chat.prompt.SystemPromptTemplate;
    import org.springframework.ai.document.Document;
    import org.springframework.ai.vectorstore.SearchRequest;
    import org.springframework.ai.vectorstore.VectorStore;
    import org.springframework.stereotype.Service;

    import java.util.List;
    import java.util.Map;
    import java.util.stream.Collectors;

    @Slf4j
    @Service
    public class RagService {

    private final ChatClient chatClient;
    private final VectorStore vectorStore;

    private static final String SYSTEM_PROMPT = """
    你是一个专业的知识库助手,基于提供的上下文信息回答用户问题。

    回答要求:
    1. 仅基于提供的上下文信息回答
    2. 如果上下文中没有相关信息,明确告知用户
    3. 回答要准确、简洁、专业
    4. 可以引用具体的文档来源

    上下文信息:
    {context}
    """;

    public RagService(ChatClient.Builder chatClientBuilder, VectorStore vectorStore) {
    this.chatClient = chatClientBuilder.build();
    this.vectorStore = vectorStore;
    }

    /**
    * RAG 问答 – 基础版本
    */

    public String ask(String question) {
    log.info("收到问题: {}", question);

    // 1. 检索相关文档
    List<Document> relevantDocs = vectorStore.similaritySearch(
    SearchRequest.query(question)
    .withTopK(5)
    .withSimilarityThreshold(0.7)
    );

    if (relevantDocs.isEmpty()) {
    return "抱歉,我在知识库中没有找到相关信息。";
    }

    // 2. 构建上下文
    String context = relevantDocs.stream()
    .map(doc -> doc.getContent())
    .collect(Collectors.joining("\\n\\n"));

    log.info("检索到 {} 个相关文档", relevantDocs.size());

    // 3. 生成回答
    SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(SYSTEM_PROMPT);
    Message systemMessage = systemPromptTemplate.createMessage(Map.of("context", context));
    UserMessage userMessage = new UserMessage(question);

    Prompt prompt = new Prompt(List.of(systemMessage, userMessage));
    String answer = chatClient.prompt(prompt).call().content();

    log.info("生成回答完成");
    return answer;
    }

    /**
    * RAG 问答 – 使用 QuestionAnswerAdvisor
    */

    public String askWithAdvisor(String question) {
    return chatClient.prompt()
    .user(question)
    .advisors(new QuestionAnswerAdvisor(vectorStore, SearchRequest.defaults()))
    .call()
    .content();
    }

    /**
    * RAG 问答 – 带引用来源
    */

    public RagResponse askWithSources(String question, int topK) {
    log.info("收到问题(带来源): {}", question);

    // 1. 检索相关文档
    List<Document> relevantDocs = vectorStore.similaritySearch(
    SearchRequest.query(question)
    .withTopK(topK)
    .withSimilarityThreshold(0.7)
    );

    if (relevantDocs.isEmpty()) {
    return RagResponse.builder()
    .answer("抱歉,我在知识库中没有找到相关信息。")
    .sources(List.of())
    .build();
    }

    // 2. 构建上下文
    String context = relevantDocs.stream()
    .map(doc -> String.format("[来源: %s]\\n%s",
    doc.getMetadata().getOrDefault("filename", "未知"),
    doc.getContent()))
    .collect(Collectors.joining("\\n\\n"));

    // 3. 生成回答
    SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(SYSTEM_PROMPT +
    "\\n\\n请在回答末尾列出引用的来源文档。");
    Message systemMessage = systemPromptTemplate.createMessage(Map.of("context", context));
    UserMessage userMessage = new UserMessage(question);

    Prompt prompt = new Prompt(List.of(systemMessage, userMessage));
    String answer = chatClient.prompt(prompt).call().content();

    // 4. 提取来源信息
    List<DocumentSource> sources = relevantDocs.stream()
    .map(doc -> DocumentSource.builder()
    .filename(doc.getMetadata().getOrDefault("filename", "未知").toString())
    .content(doc.getContent().substring(0, Math.min(200, doc.getContent().length())))
    .metadata(doc.getMetadata())
    .build())
    .collect(Collectors.toList());

    return RagResponse.builder()
    .answer(answer)
    .sources(sources)
    .build();
    }

    /**
    * RAG 响应对象
    */

    @lombok.Builder
    @lombok.Data
    public static class RagResponse {
    private String answer;
    private List<DocumentSource> sources;
    }

    /**
    * 文档来源对象
    */

    @lombok.Builder
    @lombok.Data
    public static class DocumentSource {
    private String filename;
    private String content;
    private Map<String, Object> metadata;
    }
    }

    6.4 流式 RAG 响应

    package com.example.springai.redis.service;

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.ai.chat.client.ChatClient;
    import org.springframework.ai.chat.messages.Message;
    import org.springframework.ai.chat.messages.UserMessage;
    import org.springframework.ai.chat.prompt.Prompt;
    import org.springframework.ai.chat.prompt.SystemPromptTemplate;
    import org.springframework.ai.document.Document;
    import org.springframework.ai.vectorstore.SearchRequest;
    import org.springframework.ai.vectorstore.VectorStore;
    import org.springframework.stereotype.Service;
    import reactor.core.publisher.Flux;

    import java.util.List;
    import java.util.Map;
    import java.util.stream.Collectors;

    @Slf4j
    @Service
    public class StreamingRagService {

    private final ChatClient chatClient;
    private final VectorStore vectorStore;

    private static final String SYSTEM_PROMPT = """
    你是一个专业的知识库助手。基于以下上下文信息回答用户问题:

    {context}

    请确保回答准确、专业,如果上下文中没有相关信息,请明确告知。
    """;

    public StreamingRagService(ChatClient.Builder chatClientBuilder, VectorStore vectorStore) {
    this.chatClient = chatClientBuilder.build();
    this.vectorStore = vectorStore;
    }

    /**
    * 流式 RAG 问答
    */

    public Flux<String> askStreaming(String question) {
    log.info("开始流式问答: {}", question);

    // 1. 检索相关文档
    List<Document> relevantDocs = vectorStore.similaritySearch(
    SearchRequest.query(question)
    .withTopK(5)
    .withSimilarityThreshold(0.7)
    );

    if (relevantDocs.isEmpty()) {
    return Flux.just("抱歉,我在知识库中没有找到相关信息。");
    }

    // 2. 构建上下文
    String context = relevantDocs.stream()
    .map(Document::getContent)
    .collect(Collectors.joining("\\n\\n"));

    // 3. 流式生成回答
    SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(SYSTEM_PROMPT);
    Message systemMessage = systemPromptTemplate.createMessage(Map.of("context", context));
    UserMessage userMessage = new UserMessage(question);

    Prompt prompt = new Prompt(List.of(systemMessage, userMessage));

    return chatClient.prompt(prompt).stream().content();
    }
    }


    7. 高级特性

    7.1 元数据过滤与混合查询

    package com.example.springai.redis.service;

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.ai.document.Document;
    import org.springframework.ai.vectorstore.SearchRequest;
    import org.springframework.ai.vectorstore.VectorStore;
    import org.springframework.ai.vectorstore.filter.Filter;
    import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
    import org.springframework.stereotype.Service;

    import java.time.LocalDate;
    import java.time.format.DateTimeFormatter;
    import java.util.List;

    @Slf4j
    @Service
    public class AdvancedSearchService {

    private final VectorStore vectorStore;

    public AdvancedSearchService(VectorStore vectorStore) {
    this.vectorStore = vectorStore;
    }

    /**
    * 按文档类型搜索
    */

    public List<Document> searchByDocumentType(String query, String docType, int topK) {
    FilterExpressionBuilder builder = new FilterExpressionBuilder();
    Filter.Expression filter = builder.eq("doc_type", docType).build();

    return vectorStore.similaritySearch(
    SearchRequest.query(query)
    .withTopK(topK)
    .withFilterExpression(filter)
    );
    }

    /**
    * 按日期范围搜索
    */

    public List<Document> searchByDateRange(String query, LocalDate startDate,
    LocalDate endDate, int topK) {
    FilterExpressionBuilder builder = new FilterExpressionBuilder();

    DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE;
    String start = startDate.format(formatter);
    String end = endDate.format(formatter);

    Filter.Expression filter = builder.and(
    builder.gte("date", start),
    builder.lte("date", end)
    ).build();

    return vectorStore.similaritySearch(
    SearchRequest.query(query)
    .withTopK(topK)
    .withFilterExpression(filter)
    );
    }

    /**
    * 组合过滤:类型 + 标签 + 日期
    */

    public List<Document> complexSearch(String query, String docType,
    List<String> tags, LocalDate afterDate, int topK) {
    FilterExpressionBuilder builder = new FilterExpressionBuilder();

    // 文档类型过滤
    Filter.Expression typeFilter = builder.eq("doc_type", docType).build();

    // 标签过滤(任一匹配)
    Filter.Expression tagFilter = null;
    for (String tag : tags) {
    Filter.Expression tagExpr = builder.eq("tags", tag).build();
    if (tagFilter == null) {
    tagFilter = tagExpr;
    } else {
    tagFilter = builder.or(tagFilter, tagExpr).build();
    }
    }

    // 日期过滤
    String afterDateStr = afterDate.format(DateTimeFormatter.ISO_DATE);
    Filter.Expression dateFilter = builder.gte("date", afterDateStr).build();

    // 组合所有过滤器
    Filter.Expression combinedFilter = builder.and(
    builder.and(typeFilter, tagFilter),
    dateFilter
    ).build();

    return vectorStore.similaritySearch(
    SearchRequest.query(query)
    .withTopK(topK)
    .withFilterExpression(combinedFilter)
    );
    }

    /**
    * 按作者和部门搜索
    */

    public List<Document> searchByAuthorAndDepartment(String query, String author,
    String department, int topK) {
    FilterExpressionBuilder builder = new FilterExpressionBuilder();

    Filter.Expression filter = builder.and(
    builder.eq("author", author),
    builder.eq("department", department)
    ).build();

    return vectorStore.similaritySearch(
    SearchRequest.query(query)
    .withTopK(topK)
    .withSimilarityThreshold(0.6)
    .withFilterExpression(filter)
    );
    }
    }

    7.2 文档管理服务

    package com.example.springai.redis.service;

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.ai.document.Document;
    import org.springframework.ai.vectorstore.VectorStore;
    import org.springframework.stereotype.Service;
    import redis.clients.jedis.JedisPooled;
    import redis.clients.jedis.search.Query;
    import redis.clients.jedis.search.SearchResult;

    import java.util.ArrayList;
    import java.util.List;
    import java.util.Map;

    @Slf4j
    @Service
    public class DocumentManagementService {

    private final VectorStore vectorStore;
    private final JedisPooled jedisPooled;

    public DocumentManagementService(VectorStore vectorStore, JedisPooled jedisPooled) {
    this.vectorStore = vectorStore;
    this.jedisPooled = jedisPooled;
    }

    /**
    * 删除文档
    */

    public void deleteDocuments(List<String> documentIds) {
    log.info("删除文档: {}", documentIds);
    vectorStore.delete(documentIds);
    log.info("✅ 已删除 {} 个文档", documentIds.size());
    }

    /**
    * 按元数据删除文档
    */

    public void deleteByMetadata(String metadataKey, String metadataValue) {
    log.info("按元数据删除: {}={}", metadataKey, metadataValue);

    // 查询符合条件的文档
    String queryStr = String.format("@%s:%s", metadataKey, metadataValue);
    Query query = new Query(queryStr).limit(0, 10000);

    try {
    SearchResult result = jedisPooled.ftSearch("spring-ai-index", query);
    List<String> idsToDelete = new ArrayList<>();

    result.getDocuments().forEach(doc -> {
    idsToDelete.add(doc.getId());
    });

    if (!idsToDelete.isEmpty()) {
    deleteDocuments(idsToDelete);
    }

    log.info("✅ 共删除 {} 个文档", idsToDelete.size());

    } catch (Exception e) {
    log.error("删除失败", e);
    throw new RuntimeException("删除文档失败", e);
    }
    }

    /**
    * 更新文档元数据
    */

    public void updateDocumentMetadata(String documentId, Map<String, Object> newMetadata) {
    log.info("更新文档元数据: id={}, metadata={}", documentId, newMetadata);

    // Redis 不支持直接更新,需要先删除再添加
    // 实际应用中可能需要先查询文档内容

    log.info("✅ 文档元数据已更新");
    }

    /**
    * 统计文档数量
    */

    public long countDocuments() {
    try {
    Query query = new Query("*").limit(0, 0);
    SearchResult result = jedisPooled.ftSearch("spring-ai-index", query);
    return result.getTotalResults();
    } catch (Exception e) {
    log.error("统计文档失败", e);
    return 0;
    }
    }

    /**
    * 清空所有文档
    */

    public void clearAllDocuments() {
    log.warn("⚠️ 准备清空所有文档");

    try {
    Query query = new Query("*").limit(0, 10000);
    SearchResult result = jedisPooled.ftSearch("spring-ai-index", query);

    List<String> allIds = new ArrayList<>();
    result.getDocuments().forEach(doc -> allIds.add(doc.getId()));

    if (!allIds.isEmpty()) {
    vectorStore.delete(allIds);
    log.info("✅ 已清空 {} 个文档", allIds.size());
    } else {
    log.info("ℹ️ 没有文档需要清空");
    }

    } catch (Exception e) {
    log.error("清空文档失败", e);
    throw new RuntimeException("清空文档失败", e);
    }
    }
    }


    8. REST API 控制器

    8.1 文档上传 API

    package com.example.springai.redis.controller;

    import com.example.springai.redis.service.DocumentProcessService;
    import lombok.Data;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.core.io.ByteArrayResource;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.*;
    import org.springframework.web.multipart.MultipartFile;

    import java.util.HashMap;
    import java.util.Map;

    @Slf4j
    @RestController
    @RequestMapping("/api/documents")
    public class DocumentController {

    private final DocumentProcessService documentProcessService;

    public DocumentController(DocumentProcessService documentProcessService) {
    this.documentProcessService = documentProcessService;
    }

    /**
    * 上传文档
    */

    @PostMapping("/upload")
    public ResponseEntity<ApiResponse> uploadDocument(
    @RequestParam("file") MultipartFile file,
    @RequestParam(value = "category", required = false) String category,
    @RequestParam(value = "tags", required = false) String tags) {

    try {
    log.info("收到文档上传请求: {}", file.getOriginalFilename());

    // 准备元数据
    Map<String, Object> metadata = new HashMap<>();
    if (category != null) {
    metadata.put("category", category);
    }
    if (tags != null) {
    metadata.put("tags", tags);
    }
    metadata.put("upload_time", System.currentTimeMillis());

    // 处理文档
    ByteArrayResource resource = new ByteArrayResource(file.getBytes()) {
    @Override
    public String getFilename() {
    return file.getOriginalFilename();
    }
    };

    String result = documentProcessService.processAndStoreDocument(resource, metadata);

    return ResponseEntity.ok(ApiResponse.success(result));

    } catch (Exception e) {
    log.error("文档上传失败", e);
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
    .body(ApiResponse.error("文档上传失败: " + e.getMessage()));
    }
    }

    @Data
    public static class ApiResponse {
    private boolean success;
    private String message;
    private Object data;

    public static ApiResponse success(Object data) {
    ApiResponse response = new ApiResponse();
    response.setSuccess(true);
    response.setData(data);
    return response;
    }

    public static ApiResponse error(String message) {
    ApiResponse response = new ApiResponse();
    response.setSuccess(false);
    response.setMessage(message);
    return response;
    }
    }
    }

    8.2 问答 API

    package com.example.springai.redis.controller;

    import com.example.springai.redis.service.RagService;
    import com.example.springai.redis.service.StreamingRagService;
    import lombok.Data;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.http.MediaType;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.*;
    import reactor.core.publisher.Flux;

    @Slf4j
    @RestController
    @RequestMapping("/api/chat")
    public class ChatController {

    private final RagService ragService;
    private final StreamingRagService streamingRagService;

    public ChatController(RagService ragService, StreamingRagService streamingRagService) {
    this.ragService = ragService;
    this.streamingRagService = streamingRagService;
    }

    /**
    * 普通问答
    */

    @PostMapping("/ask")
    public ResponseEntity<ChatResponse> ask(@RequestBody ChatRequest request) {
    log.info("收到问题: {}", request.getQuestion());

    String answer = ragService.ask(request.getQuestion());

    return ResponseEntity.ok(ChatResponse.builder()
    .question(request.getQuestion())
    .answer(answer)
    .build());
    }

    /**
    * 带引用来源的问答
    */

    @PostMapping("/ask-with-sources")
    public ResponseEntity<RagService.RagResponse> askWithSources(@RequestBody ChatRequest request) {
    log.info("收到问题(需要来源): {}", request.getQuestion());

    int topK = request.getTopK() != null ? request.getTopK() : 5;
    RagService.RagResponse response = ragService.askWithSources(request.getQuestion(), topK);

    return ResponseEntity.ok(response);
    }

    /**
    * 流式问答
    */

    @GetMapping(value = "/ask-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> askStream(@RequestParam String question) {
    log.info("收到流式问题: {}", question);
    return streamingRagService.askStreaming(question);
    }

    @Data
    public static class ChatRequest {
    private String question;
    private Integer topK;
    }

    @Data
    @lombok.Builder
    public static class ChatResponse {
    private String question;
    private String answer;
    }
    }

    8.3 搜索 API

    package com.example.springai.redis.controller;

    import com.example.springai.redis.service.VectorSearchService;
    import lombok.Data;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.ai.document.Document;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.*;

    import java.util.List;
    import java.util.Map;
    import java.util.stream.Collectors;

    @Slf4j
    @RestController
    @RequestMapping("/api/search")
    public class SearchController {

    private final VectorSearchService searchService;

    public SearchController(VectorSearchService searchService) {
    this.searchService = searchService;
    }

    /**
    * 基础搜索
    */

    @PostMapping
    public ResponseEntity<SearchResponse> search(@RequestBody SearchRequest request) {
    log.info("收到搜索请求: {}", request.getQuery());

    List<Document> documents = searchService.search(
    request.getQuery(),
    request.getTopK() != null ? request.getTopK() : 10
    );

    List<SearchResult> results = documents.stream()
    .map(doc -> SearchResult.builder()
    .content(doc.getContent())
    .metadata(doc.getMetadata())
    .build())
    .collect(Collectors.toList());

    return ResponseEntity.ok(SearchResponse.builder()
    .query(request.getQuery())
    .total(results.size())
    .results(results)
    .build());
    }

    /**
    * 带过滤的搜索
    */

    @PostMapping("/filtered")
    public ResponseEntity<SearchResponse> searchWithFilter(@RequestBody FilteredSearchRequest request) {
    log.info("收到过滤搜索请求: query={}, filters={}", request.getQuery(), request.getFilters());

    List<Document> documents = searchService.searchWithMetadataFilter(
    request.getQuery(),
    request.getTopK() != null ? request.getTopK() : 10,
    request.getFilters()
    );

    List<SearchResult> results = documents.stream()
    .map(doc -> SearchResult.builder()
    .content(doc.getContent())
    .metadata(doc.getMetadata())
    .build())
    .collect(Collectors.toList());

    return ResponseEntity.ok(SearchResponse.builder()
    .query(request.getQuery())
    .total(results.size())
    .results(results)
    .build());
    }

    @Data
    public static class SearchRequest {
    private String query;
    private Integer topK;
    }

    @Data
    public static class FilteredSearchRequest extends SearchRequest {
    private Map<String, Object> filters;
    }

    @Data
    @lombok.Builder
    public static class SearchResponse {
    private String query;
    private Integer total;
    private List<SearchResult> results;
    }

    @Data
    @lombok.Builder
    public static class SearchResult {
    private String content;
    private Map<String, Object> metadata;
    }
    }


    9. 性能优化与调优

    9.1 Redis 配置优化

    # application-prod.yml
    spring:
    data:
    redis:
    # 连接池配置
    jedis:
    pool:
    max-active: 50 # 最大连接数
    max-idle: 20 # 最大空闲连接
    min-idle: 10 # 最小空闲连接
    max-wait: 5000ms # 最大等待时间

    # 超时配置
    timeout: 10s # 命令超时
    connect-timeout: 5s # 连接超时

    # 集群配置(可选)
    cluster:
    nodes:
    redisnode1:6379
    redisnode2:6379
    redisnode3:6379
    max-redirects: 3

    app:
    vector:
    # HNSW 参数优化
    algorithm: HNSW
    hnsw:
    m: 16 # 连接数(8-64,越大越准确但占用更多内存)
    ef-construction: 200 # 构建时的搜索深度(100-500)
    ef-runtime: 10 # 查询时的搜索深度(topK的1-2倍)

    9.2 批量处理优化

    package com.example.springai.redis.service;

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.ai.document.Document;
    import org.springframework.ai.vectorstore.VectorStore;
    import org.springframework.stereotype.Service;

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.CompletableFuture;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;

    @Slf4j
    @Service
    public class BatchProcessService {

    private final VectorStore vectorStore;
    private final ExecutorService executorService;

    private static final int BATCH_SIZE = 100;

    public BatchProcessService(VectorStore vectorStore) {
    this.vectorStore = vectorStore;
    this.executorService = Executors.newFixedThreadPool(4);
    }

    /**
    * 批量添加文档(优化版本)
    */

    public void batchAddDocuments(List<Document> documents) {
    log.info("开始批量添加 {} 个文档", documents.size());
    long startTime = System.currentTimeMillis();

    // 分批处理
    List<List<Document>> batches = partition(documents, BATCH_SIZE);
    log.info("分成 {} 批,每批 {} 个文档", batches.size(), BATCH_SIZE);

    // 并行处理各批次
    List<CompletableFuture<Void>> futures = new ArrayList<>();

    for (int i = 0; i < batches.size(); i++) {
    final int batchIndex = i;
    final List<Document> batch = batches.get(i);

    CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
    try {
    vectorStore.add(batch);
    log.info("✅ 批次 {} 完成,处理了 {} 个文档", batchIndex + 1, batch.size());
    } catch (Exception e) {
    log.error("❌ 批次 {} 失败", batchIndex + 1, e);
    throw new RuntimeException(e);
    }
    }, executorService);

    futures.add(future);
    }

    // 等待所有批次完成
    CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

    long duration = System.currentTimeMillis() startTime;
    log.info("✅ 批量添加完成,共 {} 个文档,耗时 {}ms", documents.size(), duration);
    }

    /**
    * 分割列表
    */

    private <T> List<List<T>> partition(List<T> list, int size) {
    List<List<T>> partitions = new ArrayList<>();
    for (int i = 0; i < list.size(); i += size) {
    partitions.add(list.subList(i, Math.min(i + size, list.size())));
    }
    return partitions;
    }
    }

    9.3 缓存策略

    package com.example.springai.redis.service;

    import com.github.benmanes.caffeine.cache.Cache;
    import com.github.benmanes.caffeine.cache.Caffeine;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.ai.document.Document;
    import org.springframework.stereotype.Service;

    import java.time.Duration;
    import java.util.List;

    @Slf4j
    @Service
    public class CachedSearchService {

    private final VectorSearchService searchService;
    private final Cache<String, List<Document>> searchCache;

    public CachedSearchService(VectorSearchService searchService) {
    this.searchService = searchService;

    // 配置缓存
    this.searchCache = Caffeine.newBuilder()
    .maximumSize(1000) // 最多缓存 1000 个查询
    .expireAfterWrite(Duration.ofHours(1)) // 1小时后过期
    .recordStats() // 记录统计信息
    .build();
    }

    /**
    * 带缓存的搜索
    */

    public List<Document> search(String query, int topK) {
    String cacheKey = query + ":" + topK;

    return searchCache.get(cacheKey, key -> {
    log.info("缓存未命中,执行搜索: {}", query);
    return searchService.search(query, topK);
    });
    }

    /**
    * 清除缓存
    */

    public void clearCache() {
    searchCache.invalidateAll();
    log.info("✅ 搜索缓存已清空");
    }

    /**
    * 获取缓存统计
    */

    public void printCacheStats() {
    var stats = searchCache.stats();
    log.info("缓存统计 – 命中率: {:.2f}%, 请求数: {}, 命中数: {}, 未命中数: {}",
    stats.hitRate() * 100,
    stats.requestCount(),
    stats.hitCount(),
    stats.missCount());
    }
    }


    10. 实战案例:企业文档问答系统

    10.1 完整示例

    package com.example.springai.redis.demo;

    import com.example.springai.redis.service.*;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.boot.CommandLineRunner;
    import org.springframework.core.io.ClassPathResource;
    import org.springframework.stereotype.Component;

    import java.util.HashMap;
    import java.util.Map;

    @Slf4j
    @Component
    public class EnterpriseDocQADemo implements CommandLineRunner {

    private final DocumentProcessService documentProcessService;
    private final RagService ragService;
    private final VectorSearchService searchService;

    public EnterpriseDocQADemo(
    DocumentProcessService documentProcessService,
    RagService ragService,
    VectorSearchService searchService) {
    this.documentProcessService = documentProcessService;
    this.ragService = ragService;
    this.searchService = searchService;
    }

    @Override
    public void run(String... args) throws Exception {
    log.info("========================================");
    log.info("企业文档问答系统演示");
    log.info("========================================\\n");

    // 步骤 1: 加载企业文档
    loadEnterpriseDocuments();

    // 步骤 2: 执行问答
    performQuestionAnswering();

    // 步骤 3: 执行搜索
    performSearch();
    }

    private void loadEnterpriseDocuments() {
    log.info("📁 步骤 1: 加载企业文档");

    try {
    // 加载技术文档
    Map<String, Object> techMetadata = new HashMap<>();
    techMetadata.put("category", "技术文档");
    techMetadata.put("department", "研发部");

    documentProcessService.processAndStoreDocument(
    new ClassPathResource("docs/spring-boot-guide.pdf"),
    techMetadata
    );

    log.info("✅ 文档加载完成\\n");

    } catch (Exception e) {
    log.error("文档加载失败", e);
    }
    }

    private void performQuestionAnswering() {
    log.info("💬 步骤 2: 执行问答");

    String[] questions = {
    "Spring Boot 的主要特性是什么?",
    "如何配置数据源?",
    "什么是自动配置原理?"
    };

    for (String question : questions) {
    log.info("\\n问题: {}", question);
    String answer = ragService.ask(question);
    log.info("回答: {}\\n", answer);
    }
    }

    private void performSearch() {
    log.info("🔍 步骤 3: 执行相似度搜索");

    String query = "Spring Boot 配置";
    var results = searchService.search(query, 3);

    log.info("\\n搜索: {}", query);
    log.info("找到 {} 个相关文档:\\n", results.size());

    for (int i = 0; i < results.size(); i++) {
    var doc = results.get(i);
    log.info("{}. {}", i + 1, doc.getContent().substring(0, Math.min(100, doc.getContent().length())));
    log.info(" 来源: {}\\n", doc.getMetadata().get("filename"));
    }
    }
    }


    11. 常见问题与解决方案

    11.1 性能问题

    问题:搜索速度慢

    // 解决方案 1: 使用 HNSW 索引
    spring.ai.vectorstore.redis.algorithm=HNSW

    // 解决方案 2: 减少 topK
    SearchRequest.query(query).withTopK(5) // 而不是 20

    // 解决方案 3: 提高相似度阈值
    SearchRequest.query(query).withSimilarityThreshold(0.8) // 过滤低相关结果

    11.2 内存问题

    问题:Redis 内存占用过高

    # 查看内存使用
    redis-cli INFO memory

    # 设置最大内存
    redis-cli CONFIG SET maxmemory 2gb
    redis-cli CONFIG SET maxmemory-policy allkeys-lru

    11.3 召回率问题

    问题:搜索结果不相关

    // 解决方案 1: 优化文档分块
    app.document.chunksize=500 // 减小块大小
    app.document.chunkoverlap=100 // 增加重叠

    // 解决方案 2: 调整相似度阈值
    .withSimilarityThreshold(0.7) // 降低阈值

    // 解决方案 3: 增加检索数量
    .withTopK(10) // 检索更多文档


    12. 总结与展望

    12.1 核心要点回顾

    ┌────────────────────────────────────────────────────────┐
    │ Spring AI + Redis 向量库核心价值 │
    ├────────────────────────────────────────────────────────┤
    │ │
    │ ✅ 高性能 │
    │ • 毫秒级向量搜索 │
    │ • 内存级存储速度 │
    │ │
    │ ✅ 易集成 │
    │ • Spring Boot 原生支持 │
    │ • 零代码配置 │
    │ │
    │ ✅ 功能强大 │
    │ • 混合查询(向量+元数据) │
    │ • 实时索引更新 │
    │ │
    │ ✅ 成本低 │
    │ • 无需专用向量数据库 │
    │ • 运维成本低 │
    │ │
    │ ✅ 生产就绪 │
    │ • 集群支持 │
    │ • 完善的监控 │
    │ │
    └────────────────────────────────────────────────────────┘

    12.2 最佳实践总结

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

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

    赞(0)
    未经允许不得转载:171主机测评 » SpringAI_Redis向量库实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址