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,不推荐
相关推荐
youngerwang3 小时前
【字节跳动 AI 原生 IDE TRAE 】
ide·人工智能·trae
youngerwang3 小时前
AI 编程环境与主流 AI IDE 对比分析报告
ide·人工智能
猿小猴子3 小时前
主流 AI IDE 之一的 Google Antigravity IDE 介绍
ide·人工智能·google·antigravity
daidaidaiyu3 小时前
Spring IOC 源码学习一 基本姿势
java·spring
间彧3 小时前
Spring AOT + GraalVM Native Image:云原生Java的效能引擎
spring
LSL666_3 小时前
SpringBoot自动配置类
java·spring boot·后端·自动配置类
Teacher.chenchong3 小时前
GEE云端林业遥感:贯通森林分类、森林砍伐与退化监测、火灾评估、森林扰动监测、森林关键生理参数(树高/生物量/碳储量)反演等
人工智能·分类·数据挖掘
q***78374 小时前
Spring Boot 3.X:Unable to connect to Redis错误记录
spring boot·redis·后端
t***26594 小时前
SpringBoot + vue 管理系统
vue.js·spring boot·后端
qq_12498707535 小时前
基于springboot的疾病预防系统的设计与实现(源码+论文+部署+安装)
java·spring boot·后端·毕业设计