全文向量混合检索结合了全文检索和纯向量检索,相较于单纯的全文检索或向量检索,其检索结果通常更加精确,相似度也更高。本文介绍如何使用Lindorm向量引擎的全文向量混合检索功能。
前提条件
注意事项
本文所有示例代码中的JSON字符串均采用了文本块(Text Block),这是JDK15及以上版本支持的正式标准特性,即通过使用三对双引号 """ """
来标识文本块的开始和结束。如果您的JDK版本过低,可以将文本块自行转回多行字符串拼接的样式。
准备工作
在使用高级特性前,您需要先安装Java Low Level REST Client并连接搜索引擎,具体操作,请参见准备工作。
全文+向量双路召回(RRF融合检索)
在一些查询场景中,您需要综合考虑全文索引和向量索引的排序,根据一定的打分规则对各自返回的结果进一步进行加权计算,并得到最终的排名。
创建索引
以下示例使用hsnw算法。
// 创建索引
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接口,支持通过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
。
包含标量字段过滤场景
在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融合检索)
- 创建索引
- 数据写入
- 数据查询(融合查询)