【SpringAI】6.向量检索(redis)

发布于:2025-07-12 ⋅ 阅读:(17) ⋅ 点赞:(0)

基于redis的向量检索

向量数据库可以使用milvus,redis,Elasticsearch等,本文以redis为例:

docker启动redis-stack-server

docker run -d --name redis-stack-server -p 6380:6379  -v /home/redis/data:/data -e REDIS_ARGS="--requirepass mima --bind 0.0.0.0 --protected-mode no"  redis/redis-stack-server:latest

1,pom引入依赖

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-vector-store-redis</artifactId>
</dependency> 

2,yml配置

这里以硅基流动的免费量化模型测试

spring:
  data:
    redis:
      host: xx.xx.xx.xx
      port: 6380
      password: xxx
  ai:
    openai:
      api-key: sk-xxxxxxxxxxxxxxxxxxxx
      embedding:
        base-url: https://api.siliconflow.cn
        options:
          model: BAAI/bge-m3

    vectorstore:
      redis:
        ## 是否初始化所需的 schema
        initialize-schema: true
        ## 用于存储向量的索引的名称
        index-name: knowledgeId
        ## Redis 键的前缀
        prefix: glmapper_

3,文本向量化和文本检索


import lombok.RequiredArgsConstructor;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.ai.document.Document;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.ArrayList;
import java.util.Set;

@Component
@RequiredArgsConstructor
public class VectorService {

    private final VectorStore vectorStore;


    // 将上传的文本文件向量化并保存到向量数据库
    public void embedFile(MultipartFile file,String knowledgeId) {
        try {
            // 读取上传文件内容
            String content = new String(file.getBytes(), StandardCharsets.UTF_8);
            // 切分为小块
            List<Document> docs = splitTextToDocuments(content,knowledgeId); // 每500字符为一块
            // 写入向量库
            vectorStore.add(docs);
        } catch (Exception e) {
            throw new RuntimeException("文件向量化失败: " + e.getMessage(), e);
        }
    }

    // 按固定长度分割文本为 Document 列表
    private List<Document> splitTextToDocuments(String text,String knowledgeId) {
        List<Document> docs = new ArrayList<>();
        int length = text.length();
        for (int i = 0; i < length; i += 500) {
            int end = Math.min(length, i + 500);
            String chunk = text.substring(i, end);
            Document document = new Document(chunk);
            //指定向量数据的知识库Id
//            document.getMetadata().put("knowledgeId",knowledgeId);
            docs.add(document);
        }
        return docs;
    }

    public void store(List<Document> documents) {
        if (documents == null || documents.isEmpty()) {
            return;
        }
        vectorStore.add(documents);
    }

    public List<Document> search(String query,String knowledgeId,Double threshold) {
        FilterExpressionBuilder b = new FilterExpressionBuilder();
        return vectorStore.similaritySearch(SearchRequest.builder()
                .query(query)
                .topK(5)   //返回条数
                .similarityThreshold(threshold)   //相似度,阈值范围0~1,值越大匹配越严格‌
//                .filterExpression(b.eq("knowledgeId", knowledgeId).build())
                .build());
    }

    public void delete(Set<String> ids) {
        vectorStore.delete(new ArrayList<>(ids));
    }

}

原本想将量化数据按照知识库分组和过滤,实际并没有效果,即使按照官方文档手动定义Bean指定knowledgeId的tag也无效,官方提供的手动定义如下:

@Bean
public VectorStore vectorStore(JedisPooled jedisPooled, EmbeddingModel embeddingModel) {
    return RedisVectorStore.builder(jedisPooled, embeddingModel)
        .indexName("custom-index")                // Optional: defaults to "spring-ai-index"
        .prefix("custom-prefix")                  // Optional: defaults to "embedding:"
        .metadataFields(                         // Optional: define metadata fields for filtering
            MetadataField.tag("country"),
            MetadataField.numeric("year"))
        .initializeSchema(true)                   // Optional: defaults to false
        .batchingStrategy(new TokenCountBatchingStrategy()) // Optional: defaults to TokenCountBatchingStrategy
        .build();
}

// This can be any EmbeddingModel implementation
@Bean
public EmbeddingModel embeddingModel() {
    return new OpenAiEmbeddingModel(new OpenAiApi(System.getenv("OPENAI_API_KEY")));
}

搜索时代码如下:

vectorStore.similaritySearch(SearchRequest.builder()
        .query("The World")
        .topK(TOP_K)
        .similarityThreshold(SIMILARITY_THRESHOLD)
        .filterExpression(b.and(
                b.in("country", "UK", "NL"),
                b.gte("year", 2020)).build()).build());

4,测试接口

@Tag(name = "向量检索", description = "向量检索")
@RestController
@RequestMapping("/vector")
public class VectorController {

    @Autowired
    private VectorService vectorService;

    @Operation(summary = "文本文件向量化", description = "文本文件向量化")
    @PostMapping("/uploadFile")
    public RestVO<Map<String, Object>> uploadFile(@RequestPart MultipartFile file, @RequestParam String knowledgeId) {
        vectorService.embedFile(file, knowledgeId);
        return RestVO.success(Map.of("success", true, "message", "文件已向量化"));
    }

    @Operation(summary = "向量检索", description = "向量检索")
    @GetMapping("/query")
    public RestVO<List<Document>> uploadFile(@RequestParam String query, @RequestParam Double threshold, @RequestParam(required = false) String knowledgeId) {
        List<Document> documentList = vectorService.search(query, knowledgeId,threshold);
        return RestVO.success(documentList);
    }
}

查询结果
在这里插入图片描述

5, 检索结果运用

        List<Message> messages = new ArrayList<>();
        //省略历史会话
        UserMessage userMessage;
        //TODO 可以先对用户问题做关键词提取再去检索
        List<Document> documentList = vectorStore.similaritySearch(body.getMessage());
        System.out.println("检索结果" + documentList.size());
        if (documentList != null && !documentList.isEmpty()) {
                String context = documentList.stream()
                        .map(Document::getText)
                        .collect(Collectors.joining(""));
                userMessage = new UserMessage("参考内容:【\n" + context + "】\n\n回答:【" + body.getMessage() + "】");
        } else {
                userMessage = new UserMessage(body.getMessage());
        }
        messages.add(userMessage);
        Prompt prompt = new Prompt(messages);

预览效果
在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到