Spring AI集成Elasticsearch向量检索时filter过滤失效问题排查与解决方案

使用vectorStore.similaritySearch遇到问题

最近需要做一个功能,用到了es做向量数据库。在使用vectorStore.similaritySearch查询的时候,发现filterExpression中加的条件并没有完全生效,导致查询出来的数据不准确,出现了不符合metadata筛选条件的数据。然后研究了一下,发现了问题所在。

先说结论,Spring AI调用eselasticsearchClient.search方法查询的时候,使用的是filter过滤,用的是queryString。导致出现特殊字符的时候,没有转义的话,会出现歧义调用或者报错。
org.springframework.ai.vectorstore.elasticsearch.ElasticsearchVectorStore#doSimilaritySearch

插入数据

下面是添加数据到es的部分代码,实际代码是批量处理,这里改了一些。text做完向量化之后,会存到embedding字段。而metadata部分会存到metadata字段,是一个对象类型。这一部分没有遇到问题,数据都正常插入了。

java 复制代码
Document document = Document.builder()
                                .id(entity.bizid())
                                .text(entity.description());
                                .metadata("a1", entity.a1())
                                .metadata("a2", entity.a2())
                                .build();
// 使用vectorStore.add的时候,会自动调用embedding模型
vectorStore.add(documentList);


查询数据

我现在的需求是metadata里面的数据,都需要精确查询(完全匹配),就好比数据库中的where a1 = 'xxx'。当我a1加上了某08_1表啥≠"2"(调)或 "7"(叠加)时条件时,发现查询出来的数据,出现了a1为其他值的情况,这明显不符合项目要求。
查询数据的代码,做了部分修改:

java 复制代码
public List<Document> query(@RequestBody QueryDTO query) {
        SearchRequest.Builder searchBuilder = SearchRequest.builder()
                .query(query.description())
                .similarityThreshold(0.7);
        FilterExpressionBuilder b = new FilterExpressionBuilder();
        FilterExpressionBuilder.Op finalOp = null;
        // 构建过滤表达式
        // 如果a1有值,就加上a1条件,key实际上会被处理成metadata.a1.keyword
        if (query.a1() != null) {
            finalOp = b.eq("a1.keyword", query.a1());
        }
        // 同上,但是可能会存在a1也有值的情况,所以下面要做个判断
        if (query.a2() != null && !query.a2().isEmpty()) {
            finalOp = (finalOp != null) ? b.and(finalOp, b.eq("a2.keyword", query.a2())) : b.eq("a2.keyword", query.a2());
        }
        // 最后传入过滤表达式
        if (finalOp != null) {
            searchBuilder.filterExpression(finalOp.build());
        }
        return vectorStore.similaritySearch(searchBuilder.build());
    }

定位问题

最后源码定位到org.springframework.ai.vectorstore.elasticsearch.ElasticsearchVectorStore#doSimilaritySearch方法,里面使用了filter过滤,用的是queryString

最后请求的body体,query_vector太长做了删减,原本是1024维

json 复制代码
{
    "knn":[ {
        "field":"embedding",
        "query_vector":[-0.043929047882556915, 0.015229480341076851],
        "k":4,
        "num_candidates":6,
        "filter":[ {
            "query_string": {
                "query": "metadata.errorMessage.keyword:某08_1表啥≠"2"(调)或 "7"(叠加)时"
            }
        }
        ],
        "similarity":0.699999988079071
    }
    ],
    "size":4
}

co.elastic.clients.transport.rest_client.RestClientHttpClient#performRequest处打个断点,执行new String(restRequest.getEntity().getContent().readAllBytes())就可以拿到请求体内容

不管是代码还是最后发送的请求体来看,都确定了使用的是query_string,而query_string对特殊字符是有要求的,这就是前面查询出其他数据的原因。

query_string和term区别

问了AI,AI的答复:

特点 query_string term
用途 搜一句话、一段话,支持复杂搜索(像百度搜索) 精确查找一个完全一样的词、数字或状态
怎么用 写一个"搜索命令":字段:要搜的内容 直接告诉它值:字段: 完全一样的值
搜什么 text 类型的长文本(如文章内容、错误信息) keyword 类型的短词、数字、状态(如状态码、ID)
是否分词 会把"要搜的内容"拆开(分词)再找 不分词,必须完全一样才能找到
性能 较慢(要分析、计算相关度) 很快(直接匹配,结果可缓存)
对特殊字符 @, #, !, *, (, ) 等的处理 非常麻烦! 这些符号有特殊含义(如 AND, OR)。 如果当普通字用,必须: 1. 用 双引号 " " 把整个词或句子括起来,或者 2. 用 反斜杠 \ 一个个转义(在JSON里要写 \\)。 否则会报错! 完全不用管! 直接把包含特殊字符的完整字符串写进去就行。 因为它不分词,也不解析语法,就把整个值当普通文本比对。
例子 找包含 user@abc 的文档: "query_string": { "query": "email:\"user@abc.com\"" } (必须加引号) 找邮箱是 user@abc.com 的文档: "term": { "email.keyword": "user@abc.com" } (直接写,无需处理)

一句话总结:

  • query_string :用来全文搜索 ,功能强但复杂,遇到特殊字符容易出错,必须小心处理
  • term :用来精确匹配 ,简单、快速、可靠,特殊字符不是问题,直接用就行

es官网query-string-syntax中也有相关介绍,遇到这些特殊字符,都要进行处理。注意官网的NOTE ,我这边还没有试这种情况。

也就是说,符合我要求的,实际上是term,使用query_string的话,还要转义,就算不用转义,速度也更慢。

解决办法

  1. 转义
    所有可能出现的特殊字符,就是官网提到的那些,都加反斜杠转义

  2. 双引号包裹
    某08_1表啥≠"2"(调)或 "7"(叠加)时改成"某08_1表啥≠"2"(调)或 "7"(叠加)时"

  3. 改源码
    复制ElasticsearchVectorStore代码,建一个全类名一样的类,拷贝过去。query_string改成term。这种有个缺点,就是限制死了term查询,不友好。更倾向于其他的方式。
    改源码的话,需要从getFilterExpression里面拿到过滤表达式,自行用term重新拼装,处理起来比较复杂,这种不推荐

  4. 不使用vectorStore.similaritySearch,自行调用es代码查询
    注入EmbeddingModelElasticsearchClient,然后自己实现这个调用过程,这种是最灵活的,推荐使用,因为有些场景就是需要使用termmetadata.别忘了加

    java 复制代码
    	// 先做向量搜索
        float[] vectors = embeddingModel.embed(query.description());
        // 下面三个参数是配置的,ElasticsearchVectorStore的options属性对象里面可以拿到,但是是private的
        String index = "jap-index";
        Integer topK = 4;
        String embeddingFieldName = "embedding";
        // 查询es
        SearchResponse<Document> res = this.elasticsearchClient.search(sr -> sr.index(index)
                .knn(knn -> knn.queryVector(EmbeddingUtils.toList(vectors))
                        .similarity(query.similarityThreshold())
                        .k(topK)
                        .field(embeddingFieldName)
                        .numCandidates((int) (1.5 * topK))
                        .filter(fl -> fl.term(t ->
                                // metadata.别忘了加
                                t.field("metadata.a1.keyword"
                                ).value(query.errorMessage()))))
                .size(topK), Document.class);
        // 拿结果
        List<Hit<Document>> hits = res.hits().hits();

    需要注意的一点是,index等参数因为optionsprivate的,所以需要通过其他方式拿到。

    • 配置文件拿,这种前提是通过application配置文件方式配置的向量数据库(我不是这种)

    • 自行创建bean方式,可以把这个配置类存放到某个地方或者注入到容器(我是这种)

      java 复制代码
      	    @Bean
          public VectorStore vectorStore(RestClient restClient, EmbeddingModel embeddingModel) {
          	// 可以把这个类也存起来,或者注册成bean
              ElasticsearchVectorStoreOptions options = new ElasticsearchVectorStoreOptions();
              options.setIndexName("jap-index");    // Optional: defaults to "spring-ai-document-index"
              options.setSimilarity(cosine);           // Optional: defaults to COSINE
              options.setDimensions(1024);             // Optional: defaults to model dimensions or 1536
      
              return ElasticsearchVectorStore.builder(restClient, embeddingModel)
                      .options(options)                     // Optional: use custom options
                      .initializeSchema(true)               // Optional: defaults to false
                      .batchingStrategy(new TokenCountBatchingStrategy()) // Optional: defaults to TokenCountBatchingStrategy
                      .build();
          }		
    • 反射方式拿ElasticsearchVectorStore(也就是注入的VectorStore)的options属性,不推荐
    • 复制类,全类名一样的,拷贝代码,改成options改成public,不推荐
相关推荐
光锥智能几秒前
AI+金融,如何跨越大模型和场景鸿沟?
人工智能
找不到、了9 分钟前
Kafka在Springboot项目中的实践
spring boot·分布式·kafka
慧星云12 分钟前
魔多 AI 支持 Flux.1 Krea 在线训练:感受超自然细节
人工智能·云计算·aigc
Rysxt_14 分钟前
免费语音识别(ASR)服务深度指南
人工智能·语音识别
张成AI30 分钟前
Qwen3-30B-A3B-Thinking-2507 推理模型深度评测
人工智能·qwen
神经星星37 分钟前
在线教程丨全球首个 MoE 视频生成模型!阿里 Wan2.2 开源,消费级显卡也能跑出电影级 AI 视频
人工智能·机器学习·开源
a cool fish(无名)44 分钟前
8.1-使用向量存储值列表
人工智能·python·算法
LONGZETECH1 小时前
【龙泽科技】汽车维护与底盘拆装检修仿真教学软件【风光580】
人工智能·科技·汽车·汽车仿真教学软件·汽车教学软件
美团技术团队1 小时前
ACL 2025 | 美团技术团队论文精选
人工智能·算法
AI视觉网奇1 小时前
OWSM v4 语音识别学习笔记
人工智能·语音识别