全文向量融合查询

更新时间:2025-02-26 02:11:53

全文向量混合检索结合了全文检索和纯向量检索,相较于单纯的全文检索或向量检索,其检索结果通常更加精确,相似度也更高。本文介绍如何使用Lindorm向量引擎的全文向量混合检索功能。

前提条件

  • 已安装Java环境,要求安装JDK 1.8及以上版本。

  • 已开通向量引擎。如何开通,请参见开通向量引擎

  • 已开通搜索引擎。如何开通,请参见开通指南

  • 已将客户端IP地址添加至Lindorm白名单,具体操作请参见设置白名单

准备工作

在使用高级特性前,您需要先安装Java High Level REST Client并连接搜索引擎。具体操作,请参见准备工作

全文+向量双路召回(RRF融合检索)

在一些查询场景中,您需要综合考虑全文索引和向量索引的排序,根据一定的打分规则对各自返回的结果进一步进行加权计算,并得到最终的排名。

创建索引

以下示例使用hsnw算法。

重要

如果使用ivfpq算法,需要先将knn.offline.construction设置为true,导入离线数据后发起索引构建,构建成功后方可进行查询,详细说明请参见创建向量索引索引构建

String indexName = "vector_text_hybridSearch";
int dim = 3;
CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName);

createIndexRequest.settings(Map.of(
  "index", Map.of(
    "number_of_shards", 2, 
    "knn", true)));

createIndexRequest.mapping(Map.of(
  "_source", Map.of(
    "excludes", new String[] { "vector1" }
  ),
  "properties", Map.of(
    "vector1", Map.of(
      "type", "knn_vector",
      "dimension", dim,
      "data_type", "float",
      "method", Map.of(
        "engine", "lvector",
        "name", "hnsw",
        "space_type", "l2",
        "parameters", Map.of(
          "m", 24,
          "ef_construction", 500
        )
      )
    ),
    "text_field", Map.of(
      "type", "text",
      "analyzer", "ik_max_word"
    ),
    "field1", Map.of(
      "type", "long"
    )
  )
));


  CreateIndexResponse createIndexResponse = client.indices().create(createIndexRequest, RequestOptions.DEFAULT);
System.out.println("createIndexResponse: " + createIndexResponse.index());

数据写入

// 准备批量请求
String index = "vector_text_hybridSearch";
BulkRequest bulkRequest = new BulkRequest();

// 添加多个 IndexRequest,每个 IndexRequest 表示一个文档
bulkRequest.add(new IndexRequest(index).id("1").source(Map.of(
  "field1", 1, "field2", "flag1", "vector1", new float[]{2.5f, 2.3f, 2.4f}, "text_field", "hello test5")));
bulkRequest.add(new IndexRequest(index).id("2").source(Map.of(
  "field1", 2, "field2", "flag1", "vector1", new float[]{2.6f, 2.3f, 2.4f}, "text_field", "hello test6 test5")));
bulkRequest.add(new IndexRequest(index).id("3").source(Map.of(
  "field1", 3, "field2", "flag1", "vector1", new float[]{2.7f, 2.3f, 2.4f}, "text_field", "hello test7")));
bulkRequest.add(new IndexRequest(index).id("4").source(Map.of(
  "field1", 4, "field2", "flag2", "vector1", new float[]{2.8f, 2.3f, 2.4f}, "text_field", "hello test8 test7")));
bulkRequest.add(new IndexRequest(index).id("5").source(Map.of(
  "field1", 5, "field2", "flag2", "vector1", new float[]{2.9f, 2.3f, 2.4f}, "text_field", "hello test9")));

bulkRequest.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE);
BulkResponse bulkResponse = client.bulk(bulkRequest, RequestOptions.DEFAULT);

if (bulkResponse.hasFailures()) {
  // 处理可能的失败情况
  System.out.println("Bulk operation had failures:");
  System.out.println(bulkResponse.buildFailureMessage());
} else {
  System.out.println("Bulk operation completed successfully.");
}

数据查询(融合查询)

RRF计算方式如下:

进行查询时系统会根据传入的rrf_rank_constant参数,对全文检索和向量检索分别获得的topK结果进行处理。对于每个返回的文档_id,使用公式1/(rrf_rank_constant + rank(i))计算得分,其中rank(i)表示该文档在结果中的排名。

如果某个文档_id同时出现在全文检索和向量检索的topK结果中,其最终得分为两种检索方法计算得分之和。而仅出现在其中一种检索结果中的文档,则只保留该检索方法的得分。

rrf_rank_constant = 1为例,计算结果如下:

# doc   | queryA     | queryB         | score
_id: 1 =  1.0/(1+1)  + 0              = 0.5
_id: 2 =  1.0/(1+2)  + 0              = 0.33
_id: 4 =    0        + 1.0/(1+2)      = 0.33
_id: 5 =    0        + 1.0/(1+1)      = 0.5

支持通过_search接口或_msearch_rrf接口进行融合查询,两种接口的对比如下:

接口

开源性

易读性

是否支持全文、向量检索比例调整

接口

开源性

易读性

是否支持全文、向量检索比例调整

_search

兼容

不易读

支持

_msearch_rrf

自研接口

易读

不支持

以下是两种场景下使用_search接口或_msearch_rrf接口的具体写法:

无标量字段过滤的场景

使用开源_search接口
使用自研_msearch_rrf接口

优点:兼容开源_search接口,支持通过rrf_knn_weight_factor参数调整全文检索与纯向量检索的比例。

缺点:写法较为复杂。

ext.lvector扩展字段中,不设置filter_type,则表示该RRF检索只包含全文检索和纯向量检索,同时向量检索中无需进行标量字段的过滤。

SearchRequest searchRequest = new SearchRequest();
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
Map<String, Object> queryBody = Map.of(
  "knn", Map.of(
    "vector1", Map.of(
      "vector", new float[]{2.8f, 2.3f, 2.4f},
      "filter", Map.of(
        "match", Map.of(
          "text_field", "test5 test6 test7 test8 test9"
        )
      ),
      "k", 10
    )
));

searchSourceBuilder.query(QueryBuilders.wrapperQuery(new Gson().toJson(queryBody)));
Map<String, String> ext = Map.of(
  "hybrid_search_type", "filter_rrf",
  "rrf_rank_constant", "60",
  "rrf_knn_weight_factor", "0.5");
searchSourceBuilder.ext(Collections.singletonList(new LVectorExtBuilder("lvector", ext)));
// _source接口可以进行配置,使返回值携带属性
searchSourceBuilder.fetchSource(new FetchSourceContext(true));
searchRequest.source(searchSourceBuilder);
searchRequest.indices(index);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
System.out.println(searchResponse);

LVectorExtBuilder的定义代码,请参见LVectorExtBuilder

优点:写法较清晰。

缺点:不兼容开源_search接口,不支持调整全文检索与纯向量检索的比例。

Request searchRequest = new Request("GET", "/_msearch_rrf?re_score=true&rrf_rank_constant=60&pretty");
String jsonString = """
  {"index": "vector_text_hybridSearch"}
  {"size":10,"_source":false, "query":{"match":{"text_field":"test5 test6 test7 test8 test9"}}}
  {"index": "vector_text_hybridSearch"}
  {"size":10,"_source":false,"query":{"knn":{"vector1":{"vector":[2.8,2.3,2.4],"k":10}}}}
""";
searchRequest.setJsonEntity(jsonString);
Response response = client.getLowLevelClient().performRequest(searchRequest);
String responseBody = EntityUtils.toString(response.getEntity());
System.out.println("msearchRrf responseBody = " + responseBody);
说明

连接参数中必须添加re_score=true

包含标量字段过滤场景

使用开源_search接口
使用自研_msearch_rrf接口

ext.lvector扩展字段中设置filter_type参数,则表示该RRF检索中的向量检索还需进行标量字段的过滤。

说明

RRF融合检索时,如果希望携带filter过滤条件,需要将全文检索的query条件和用于过滤的filter条件分别设置到两个bool表达式中,通过bool.must进行连接。must中的第一个bool表达式将用于全文检索,计算全文匹配度得分。must中第二个bool filter表达式将用于knn检索的过滤条件。

String index = "vector_text_hybridSearch";
SearchRequest searchRequest = new SearchRequest();
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
Map<String, Object> queryBody = Map.of(
  "knn", Map.of(
    "vector1", Map.of(
      "vector", List.of(2.8, 2.3, 2.4),
      "filter", Map.of(
        "bool", Map.of(
          "must", List.of(
            Map.of(
              "bool", Map.of(
                "must", List.of(
                  Map.of(
                    "match", Map.of(
                      "text_field", Map.of(
                        "query", "test5 test6 test7 test8 test9"
                      )
                    )
                  )
                )
              )
            ),
            Map.of(
              "bool", Map.of(
                "filter", List.of(
                  Map.of(
                    "range", Map.of(
                      "field1", Map.of(
                        "gt", 2
                      )
                    )
                  )
                )
              )
            )
          )
        )
      ),
      "k", 10
    )
  )
);

searchSourceBuilder.query(QueryBuilders.wrapperQuery(new Gson().toJson(queryBody)));
Map<String, String> ext = Map.of(
  "filter_type", "efficient_filter",
  "hybrid_search_type", "filter_rrf",
  "rrf_rank_constant", "60",
  "rrf_knn_weight_factor", "0.5");
searchSourceBuilder.ext(Collections.singletonList(new LVectorExtBuilder("lvector", ext)));
// _source接口可以进行配置,使返回值携带属性
searchSourceBuilder.fetchSource(new FetchSourceContext(true));
searchRequest.source(searchSourceBuilder);
searchRequest.indices(index);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
System.out.println(searchResponse);

LVectorExtBuilder的定义代码,请参见LVectorExtBuilder

自研_msearch_rrf接口仅支持通过Java Low Level REST Client调用,具体操作,请参见包含标量字段过滤场景

  • 本页导读 (1)
  • 前提条件
  • 准备工作
  • 全文+向量双路召回(RRF融合检索)
  • 创建索引
  • 数据写入
  • 数据查询(融合查询)