全文向量混合检索

更新时间:2025-02-26 02:10:04

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

前提条件

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

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

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

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

注意事项

本文所有示例代码中的JSON字符串均采用了文本块(Text Block),这是JDK15及以上版本支持的正式标准特性,即通过使用三对双引号 """ """ 来标识文本块的开始和结束。如果您的JDK版本过低,可以将文本块自行转回多行字符串拼接的样式。

准备工作

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

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

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

创建索引

以下示例使用hsnw算法。

重要

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

// 创建索引
String indexName = "vector_text_hybridSearch";
Request indexRequest = new Request("PUT", "/" + indexName);
String jsonString = """
{
 "settings" : {
    "index": {
      "number_of_shards": 2,
      "knn": true
    }
  },
  "mappings": {
    "_source": {
      "excludes": ["vector1"]
    },
    "properties": {
      "vector1": {
        "type": "knn_vector",
        "dimension": 3,
        "data_type": "float",
        "method": {
          "engine": "lvector",
          "name": "hnsw", 
          "space_type": "l2",
          "parameters": {
            "m": 24,
            "ef_construction": 500
         }
       }
      },
      "text_field": {
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "field1": {
        "type": "long"
      },
      "filed2": {
        "type": "keyword"
      }
    }
  }
}
""";
indexRequest.setJsonEntity(jsonString);
Response response = restClient.performRequest(indexRequest);
String responseBody = EntityUtils.toString(response.getEntity());
System.out.println("createIndex responseBody = " + responseBody);

数据写入

// 批量写入数据
Random random = new Random();
Request bulkRequest = new Request("POST", "/_bulk");
jsonString = """
  { "index" : { "_index" : "vector_text_hybridSearch", "_id" : "1" } }
  { "field1" : 1, "vector1": [2.5, 2.3, 2.4], "text_field": "hello test5"}
  { "index" : { "_index" : "vector_text_hybridSearch", "_id" : "2" } }
  { "field1" : 2, "vector1": [2.6, 2.3, 2.4], "text_field": "hello test6 test5"}
  { "index" : { "_index" : "vector_text_hybridSearch", "_id" : "3" } }
  { "field1" : 3, "vector1": [2.7, 2.3, 2.4], "text_field": "hello test7"}
  { "index" : { "_index" : "vector_text_hybridSearch", "_id" : "4" } }
  { "field1" : 4, "vector1": [2.8, 2.3, 2.4], "text_field": "hello test8 test7"}
  { "index" : { "_index" : "vector_text_hybridSearch", "_id" : "5" } }
  { "field1" : 5, "vector1": [2.9, 2.3, 2.4], "text_field": "hello test9"}
""";
bulkRequest.setJsonEntity(jsonString);
// 刷新已写入的数据
bulkRequest.addParameter("refresh", "wait_for");
Response response = restClient.performRequest(bulkRequest);
String responseBody = EntityUtils.toString(response.getEntity());
System.out.println("bulkWriteDoc responseBody = " + responseBody);

数据查询(融合查询)

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检索只包含全文检索和纯向量检索,同时向量检索中无需进行标量字段的过滤。

String indexName = "vector_text_hybridSearch";
Request searchRequest = new Request("GET", "/" + indexName + "/_search");
String jsonString = """
  {
    "size": 10,
    "_source": false,
    "query": {
      "knn": {
        "vector1": {
          "vector": [2.8, 2.3, 2.4],
          "filter": {
            "match": {
               "text_field": "test5 test6 test7 test8 test9"
            }
          },
          "k": 10
        }
      }
    },
    "ext": {"lvector": {
      "hybrid_search_type": "filter_rrf",
      "rrf_rank_constant": "60",
      "rrf_knn_weight_factor": "0.5"
    }}
  }
""";
searchRequest.setJsonEntity(jsonString);
Response response = restClient.performRequest(searchRequest);
String responseBody = EntityUtils.toString(response.getEntity());
System.out.println("search responseBody = " + responseBody);

优点:写法较清晰。

缺点:不兼容开源_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 = restClient.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 indexName = "vector_text_hybridSearch";
Request searchRequest = new Request("GET", "/" + indexName + "/_search");
String jsonString = """
  {
      "size": 10,
      "_source": false,
      "query": {
        "knn": {
          "vector1": {
            "vector": [2.8, 2.3, 2.4],
            "filter": {
              "bool": {
                 "must": [
                    {
                      "bool": {
                        "must":[{
                          "match": {
                            "text_field": {
                              "query": "test5 test6 test7 test8 test9"
                            }
                          }
                        }]
                      }
                    },
                    {
                      "bool": {
                        "filter": [{
                          "range": {
                            "field1": {
                              "gt": 2
                            }
                          }
                        }]
                      }
                    }
                  ]
              }
            },
            "k": 10
          }
        }
      },
      "ext": {"lvector": {
        "filter_type": "efficient_filter",
        "hybrid_search_type": "filter_rrf",
        "rrf_rank_constant": "60"
      }}
    } 
  """;
searchRequest.setJsonEntity(jsonString);
Response response = restClient.performRequest(searchRequest);
String responseBody = EntityUtils.toString(response.getEntity());
System.out.println("searchWithFIlter responseBody = " + responseBody);
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":{"bool":{"must":[{"match":{"text_field":"test5 test6 test7 test8 test9"}}],"filter":[{"range":{"field1":{"gt":2}}}]}}}
  {"index": "vector_text_hybridSearch"}
  {"size":10,"_source":false,"query":{"knn":{"vector1":{"vector":[2.8,2.3,2.4],"filter":{"range":{"field1":{"gt":2}}},"k":10}}},"ext":{"lvector":{"filter_type":"efficient_filter"}}}
""";
searchRequest.setJsonEntity(jsonString);
Response response = restClient.performRequest(searchRequest);
String responseBody = EntityUtils.toString(response.getEntity());
System.out.println("msearchRrfWithFilter responseBody = " + responseBody);

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

点击开启售前

在线咨询服务

你好,我是AI助理

可以解答问题、推荐解决方案等