前言

最近在做企业知识库问答系统时,采用了 Spring AI + Redis Stack 实现向量检索。

整体架构非常简单:

FAQ
 ↓
Embedding
 ↓
Redis Vector Store
 ↓
Similarity Search
 ↓
LLM生成答案

本以为是一个非常标准的实现,结果遇到了一个非常诡异的问题:

FAQ已经成功写入Redis,Embedding也生成成功,但向量检索始终返回空结果。

经过几个小时排查,最终发现问题竟然出在一个 Metadata 字段类型上。

本文记录完整排查过程,希望能帮助后来者少踩坑。


问题现象

RedisVectorStore 配置如下:

@Bean
public VectorStore vectorStore(
        JedisPooled jedisPooled,
        EmbeddingModel embeddingModel) {

    return RedisVectorStore.builder(jedisPooled, embeddingModel)
            .indexName("faq_index")
            .prefix("faq:v2:")
            .contentFieldName("answer")
            .vectorAlgorithm(RedisVectorStore.Algorithm.HNSW)
            .metadataFields(
                    RedisVectorStore.MetadataField.tag("faqId"),
                    RedisVectorStore.MetadataField.text("question"),
                    RedisVectorStore.MetadataField.tag("category")
            )
            .initializeSchema(true)
            .build();
}

FAQ入库代码:

vectorStore.add(List.of(toDocument(faq)));

查询代码:

SearchRequest request = SearchRequest.builder()
        .query(query)
        .topK(5)
        .build();

List<Document> docs =
        vectorStore.similaritySearch(request);

结果:

FAQ成功导入
Redis中有数据
向量生成成功

但是:

similaritySearch() 返回 []

第一轮排查:怀疑向量没有写进去

首先查看 Redis 索引。

FT.INFO faq_index

结果:

Number of docs: 0
Number of records: 0

看到这里第一反应:

难道向量根本没写进去?

于是开始检查写入逻辑。

增加日志:

Boolean exists =
        jedisPooled.exists("faq:v2:" + faq.getId());

log.info("exists={}", exists);

结果:

exists=true
exists=true
exists=true
...

说明文档确实已经写入Redis。


第二轮排查:验证索引状态

增加校验代码:

var result = jedisPooled.ftSearch(
        "faq_index",
        "*",
        new FTSearchParams().limit(0, 0));

log.info(
        "totalResults={}",
        result.getTotalResults());

结果:

totalResults=10

进一步查看:

Map<String,Object> info =
        jedisPooled.ftInfo("faq_index");

log.info(
        "numDocs={}",
        info.get("num_docs"));

输出:

numDocs=10

这说明:

  • Redis索引正常
  • 文档已被索引
  • Vector字段存在

问题并不在写入阶段。


第三轮排查:验证Embedding维度

检查Redis中的向量维度:

Map<String,Object> doc =
        (Map<String,Object>)
                jedisPooled.jsonGet(key);

Object embedding =
        doc.get("embedding");

输出:

dim=1024

再查看索引:

VECTOR HNSW FLOAT32 DIM 1024

维度完全一致。

排除维度不匹配问题。


第四轮排查:怀疑相似度阈值

尝试修改查询:

SearchRequest request =
        SearchRequest.builder()
                .query(query)
                .topK(5)
                .similarityThreshold(0.0)
                .build();

结果:

依然为空

说明并非阈值过滤问题。


最终发现的问题

问题出在 Document Metadata。

最初代码:

private Document toDocument(Faq faq) {

    Map<String,Object> metadata =
            new HashMap<>();

    metadata.put(
            "faqId",
            faq.getId());

    metadata.put(
            "question",
            faq.getQuestion());

    metadata.put(
            "category",
            faq.getCategory());

    return new Document(
            String.valueOf(faq.getId()),
            faq.getAnswer(),
            metadata);
}

注意这里:

metadata.put(
        "faqId",
        faq.getId());

faqId 是:

Long

而Redis Schema定义的是:

MetadataField.tag("faqId")

即:

faqId TAG

Redis TAG字段的要求

Redis Search中的TAG字段本质上是字符串类型。

正确的数据应该是:

{
  "faqId": "1"
}

而不是:

{
  "faqId": 1
}

虽然Redis JSON可以存储数字:

{
  "faqId": 1
}

但是对于TAG索引字段来说:

TAG → String
TEXT → String
NUMERIC → Number

类型不一致可能导致:

  • 索引异常
  • 检索异常
  • Metadata过滤异常
  • Spring AI查询结果为空

修复方案

修改为:

metadata.put(
        "faqId",
        String.valueOf(faq.getId()));

完整代码:

private Document toDocument(Faq faq) {

    Map<String,Object> metadata =
            new HashMap<>();

    metadata.put(
            "faqId",
            String.valueOf(faq.getId()));

    metadata.put(
            "question",
            faq.getQuestion());

    metadata.put(
            "category",
            faq.getCategory());

    return new Document(
            String.valueOf(faq.getId()),
            "【" + faq.getQuestion() + "】\n"
                    + faq.getAnswer(),
            metadata);
}

重新构建索引后:

numDocs=10
totalResults=10

similaritySearch()
返回正常结果

问题解决。


经验总结

在 Spring AI + Redis Vector Store 场景下,Metadata 字段类型必须与 Redis Schema 保持一致。

推荐遵循下面的映射关系:

Redis类型 Java类型
TAG String
TEXT String
NUMERIC Integer / Long / Double
VECTOR float[]

例如:

MetadataField.tag("faqId")

对应:

metadata.put(
        "faqId",
        String.valueOf(id));

而不是:

metadata.put(
        "faqId",
        id);

Spring AI + Redis 排查Checklist

以后遇到检索无结果时,可以按下面顺序排查:

1. 查看索引

FT.INFO faq_index

关注:

num_docs

2. 查看文档

KEYS faq:v2:*

确认数据存在。


3. 查看向量维度

DIM

是否与Embedding模型一致。


4. 查看Metadata类型

确认:

TAG -> String
TEXT -> String
NUMERIC -> Number

5. 降低相似度阈值

.similarityThreshold(0.0)

排除过滤问题。


结语

这次问题最大的特点是:

写入成功、索引成功、Embedding成功,但检索结果为空。

由于错误并不会直接抛出异常,因此排查起来非常耗时。

如果你也在使用 Spring AI + Redis Vector Store,并且遇到“明明有数据却搜不到”的情况,不妨先检查一下 Metadata 字段类型是否与 Redis Schema 完全一致。

有时候,一个 Long 和一个 String 的区别,就足以让整个向量检索失效。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐