多向量混合搜索
在许多应用中,可以通过丰富的信息(如标题和描述)或多种模态(如文本、图像和音频)来搜索对象。例如,如果一条包含文本和图像的推文中,其文本或图像与搜索查询的语义匹配,则应将其检索出来。 混合搜索通过整合这些不同领域的搜索功能,从而提升搜索体验。Milvus 支持这一功能,允许在多个向量字段上进行搜索,并同时执行多次近似最近邻(ANN)搜索。 若您希望同时搜索文本和图片、描述同一对象的多个文本字段,或搜索稠密向量与稀疏向量以提升搜索质量,多向量混合搜索将特别有用。
混合搜索工作流
多向量混合搜索整合了不同的搜索方法,或涵盖了来自多种模态的Embeddings:
稀疏-稠密向量搜索:稠密向量非常适合捕捉语义关系,而稀疏向量则在精确的关键词匹配方面极为有效。 混合检索结合了这些方法,既能提供广泛的概念理解,又能确保术语的精确相关性,从而提升搜索结果质量。通过发挥每种方法的优势,混合检索克服了单一方法的局限性,在处理复杂查询时表现更佳。以下是关于将语义搜索与全文搜索相结合的混合检索的更详细指南。
多模态向量搜索:多模态向量搜索是一种强大的技术,允许您跨多种数据类型(包括文本、图像、音频及其他类型)进行搜索。该方法的主要优势在于能够将不同模态统一起来,提供无缝且连贯的搜索体验。 例如,在产品搜索中,用户可能输入文本查询来查找同时包含文本和图片描述的产品。通过混合搜索方法将这些模态结合起来,可以提高搜索准确性或丰富搜索结果。
示例
让我们考虑一个现实世界中的用例:每个产品都包含文字描述和图片。基于现有数据,我们可以进行三种类型的搜索:
语义文本搜索:这涉及使用稠密向量查询产品的文本描述。文本Embeddings可通过BERT和Transformers等模型,或OpenAI 等服务生成。
全文搜索:在此,我们使用稀疏向量通过关键词匹配来查询产品的文字描述。为此,可以利用BM25等算法,或BGE-M3、SPLADE等稀疏嵌入模型。
多模态图像搜索:该方法通过基于稠密向量的文本查询对图像进行检索。图像 Embeddings 可通过CLIP 等模型生成。
本指南将通过一个示例,带您了解如何结合上述搜索方法,基于产品的原始文本描述和图像Embeddings实现多模态混合搜索。我们将演示如何存储多向量数据,并采用重新排序策略进行混合搜索。
创建包含多个向量字段的Collection
创建Collection的过程包括三个关键步骤:定义Collection Schema、配置索引参数以及创建Collection。
定义Schema
对于多向量混合搜索,我们应在 Collection Schema 中定义多个向量字段。有关 Collection 中允许的向量字段数量限制的详细信息,请参阅Zilliz Cloud 限制。不过,如有必要,您可以调整 proxy.maxVectorFieldNum ,根据需要将 Collection 中的向量字段数量增加至最多 10 个。
本示例将以下字段纳入Schema:
id: 用作存储文本 ID 的主键。该字段的数据类型为INT64。text: 用于存储文本内容。该字段的数据类型为VARCHAR,最大长度为1000字节。enable_analyzer选项已设置为True,以支持全文搜索。text_dense: 用于存储文本的稠密向量。该字段的数据类型为FLOAT_VECTOR,向量维度为768。text_sparse: 用于存储文本的稠密向量。该字段的数据类型为SPARSE_FLOAT_VECTOR。image_dense: 用于存储产品图片的稠密向量。该字段的数据类型为FLOAT_VETOR,向量维度为512。
由于我们将使用内置的 BM25 算法对文本字段进行全文检索,因此必须将 Milvus 的Function 添加到 Schema 中。有关更多详细信息,请参阅全文检索。
from pymilvus import (
MilvusClient, DataType, Function, FunctionType
)
client = MilvusClient(
uri="http://localhost:19530",
token="root:Milvus"
)
# Init schema with auto_id disabled
schema = client.create_schema(auto_id=False)
# Add fields to schema
schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True, description="product id")
schema.add_field(field_name="text", datatype=DataType.VARCHAR, max_length=1000, enable_analyzer=True, description="raw text of product description")
schema.add_field(field_name="text_dense", datatype=DataType.FLOAT_VECTOR, dim=768, description="text dense embedding")
schema.add_field(field_name="text_sparse", datatype=DataType.SPARSE_FLOAT_VECTOR, description="text sparse embedding auto-generated by the built-in BM25 function")
schema.add_field(field_name="image_dense", datatype=DataType.FLOAT_VECTOR, dim=512, description="image dense embedding")
# Add function to schema
bm25_function = Function(
name="text_bm25_emb",
input_field_names=["text"],
output_field_names=["text_sparse"],
function_type=FunctionType.BM25,
)
schema.add_function(bm25_function)
import io.milvus.v2.client.ConnectConfig;
import io.milvus.v2.client.MilvusClientV2;
import io.milvus.v2.common.DataType;
import io.milvus.common.clientenum.FunctionType;
import io.milvus.v2.service.collection.request.AddFieldReq;
import io.milvus.v2.service.collection.request.CreateCollectionReq;
import io.milvus.v2.service.collection.request.CreateCollectionReq.Function;
import java.util.*;
MilvusClientV2 client = new MilvusClientV2(ConnectConfig.builder()
.uri("http://localhost:19530")
.token("root:Milvus")
.build());
CreateCollectionReq.CollectionSchema schema = client.createSchema();
schema.addField(AddFieldReq.builder()
.fieldName("id")
.dataType(DataType.Int64)
.isPrimaryKey(true)
.autoID(false)
.build());
schema.addField(AddFieldReq.builder()
.fieldName("text")
.dataType(DataType.VarChar)
.maxLength(1000)
.enableAnalyzer(true)
.build());
schema.addField(AddFieldReq.builder()
.fieldName("text_dense")
.dataType(DataType.FloatVector)
.dimension(768)
.build());
schema.addField(AddFieldReq.builder()
.fieldName("text_sparse")
.dataType(DataType.SparseFloatVector)
.build());
schema.addField(AddFieldReq.builder()
.fieldName("image_dense")
.dataType(DataType.FloatVector)
.dimension(512)
.build());
schema.addFunction(Function.builder()
.functionType(FunctionType.BM25)
.name("text_bm25_emb")
.inputFieldNames(Collections.singletonList("text"))
.outputFieldNames(Collections.singletonList("text_sparse"))
.build());
import (
"context"
"fmt"
"github.com/milvus-io/milvus/client/v2/column"
"github.com/milvus-io/milvus/client/v2/entity"
"github.com/milvus-io/milvus/client/v2/index"
"github.com/milvus-io/milvus/client/v2/milvusclient"
)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
milvusAddr := "localhost:19530"
client, err := milvusclient.New(ctx, &milvusclient.ClientConfig{
Address: milvusAddr,
})
if err != nil {
fmt.Println(err.Error())
// handle error
}
defer client.Close(ctx)
function := entity.NewFunction().
WithName("text_bm25_emb").
WithInputFields("text").
WithOutputFields("text_sparse").
WithType(entity.FunctionTypeBM25)
schema := entity.NewSchema()
schema.WithField(entity.NewField().
WithName("id").
WithDataType(entity.FieldTypeInt64).
WithIsPrimaryKey(true),
).WithField(entity.NewField().
WithName("text").
WithDataType(entity.FieldTypeVarChar).
WithEnableAnalyzer(true).
WithMaxLength(1000),
).WithField(entity.NewField().
WithName("text_dense").
WithDataType(entity.FieldTypeFloatVector).
WithDim(768),
).WithField(entity.NewField().
WithName("text_sparse").
WithDataType(entity.FieldTypeSparseVector),
).WithField(entity.NewField().
WithName("image_dense").
WithDataType(entity.FieldTypeFloatVector).
WithDim(512),
).WithFunction(function)
import { MilvusClient, DataType, FunctionType } from "@zilliz/milvus2-sdk-node";
const address = "http://localhost:19530";
const token = "root:Milvus";
const client = new MilvusClient({address, token});
// Define fields
const fields = [
{
name: "id",
data_type: DataType.Int64,
is_primary_key: true,
auto_id: false
},
{
name: "text",
data_type: DataType.VarChar,
max_length: 1000,
enable_analyzer: true
},
{
name: "text_dense",
data_type: DataType.FloatVector,
dim: 768
},
{
name: "text_sparse",
data_type: DataType.SparseFloatVector
},
{
name: "image_dense",
data_type: DataType.FloatVector,
dim: 512
}
];
// define function
const functions = [
{
name: "text_bm25_emb",
description: "text bm25 function",
type: FunctionType.BM25,
input_field_names: ["text"],
output_field_names: ["text_sparse"],
params: {},
},
];
export schema='{
"autoId": false,
"functions": [
{
"name": "text_bm25_emb",
"type": "BM25",
"inputFieldNames": ["text"],
"outputFieldNames": ["text_sparse"],
"params": {}
}
],
"fields": [
{
"fieldName": "id",
"dataType": "Int64",
"isPrimary": true
},
{
"fieldName": "text",
"dataType": "VarChar",
"elementTypeParams": {
"max_length": 1000,
"enable_analyzer": true
}
},
{
"fieldName": "text_dense",
"dataType": "FloatVector",
"elementTypeParams": {
"dim": "768"
}
},
{
"fieldName": "text_sparse",
"dataType": "SparseFloatVector"
},
{
"fieldName": "image_dense",
"dataType": "FloatVector",
"elementTypeParams": {
"dim": "512"
}
}
]
}'
#include "milvus/MilvusClientV2.h"
auto client = milvus::MilvusClientV2::Create();
milvus::ConnectParam connect_param{"http://localhost:19530", "root:Milvus"};
auto status = client->Connect(connect_param);
if (!status.IsOk()) {
std::cout << status.Message() << std::endl;
}
milvus::FunctionPtr function = std::make_shared<milvus::Function>("text_bm25_emb", milvus::FunctionType::BM25, "text bm25 function");
function->AddInputFieldName("text");
function->AddOutputFieldName("text_sparse");
milvus::CollectionSchemaPtr schema = std::make_shared<milvus::CollectionSchema>();
schema->AddField({"id", milvus::DataType::INT64, "", true, false});
schema->AddField(milvus::FieldSchema("text", milvus::DataType::VARCHAR).WithMaxLength(1000).EnableAnalyzer(true));
schema->AddField(milvus::FieldSchema("text_dense", milvus::DataType::FLOAT_VECTOR).WithDimension(768));
schema->AddField({"text_sparse", milvus::DataType::SPARSE_FLOAT_VECTOR});
schema->AddField(milvus::FieldSchema("image_dense", milvus::DataType::FLOAT_VECTOR).WithDimension(512));
schema->AddFunction(function);
创建索引
定义Collection Schema后,下一步是配置向量索引并指定相似度度量。在给定的示例中:
text_dense_index:为文本密集向量字段创建了一个类型为AUTOINDEX、度量类型为IP的索引。text_sparse_index:为文本稀疏向量字段创建了类型为SPARSE_INVERTED_INDEX、度量类型为BM25的索引。image_dense_index: 为图像密集向量场创建了一个类型为AUTOINDEX、度量类型为IP的索引。
您可以根据需要选择其他索引类型,以最好地满足您的需求和数据类型。有关支持的索引类型的更多信息,请参阅可用索引类型的文档。
# Prepare index parameters
index_params = client.prepare_index_params()
# Add indexes
index_params.add_index(
field_name="text_dense",
index_name="text_dense_index",
index_type="AUTOINDEX",
metric_type="IP"
)
index_params.add_index(
field_name="text_sparse",
index_name="text_sparse_index",
index_type="SPARSE_INVERTED_INDEX",
metric_type="BM25",
params={"inverted_index_algo": "DAAT_MAXSCORE"}, # or "DAAT_WAND" or "TAAT_NAIVE"
)
index_params.add_index(
field_name="image_dense",
index_name="image_dense_index",
index_type="AUTOINDEX",
metric_type="IP"
)
import io.milvus.v2.common.IndexParam;
import java.util.*;
Map<String, Object> denseParams = new HashMap<>();
IndexParam indexParamForTextDense = IndexParam.builder()
.fieldName("text_dense")
.indexName("text_dense_index")
.indexType(IndexParam.IndexType.AUTOINDEX)
.metricType(IndexParam.MetricType.IP)
.build();
Map<String, Object> sparseParams = new HashMap<>();
sparseParams.put("inverted_index_algo", "DAAT_MAXSCORE");
IndexParam indexParamForTextSparse = IndexParam.builder()
.fieldName("text_sparse")
.indexName("text_sparse_index")
.indexType(IndexParam.IndexType.SPARSE_INVERTED_INDEX)
.metricType(IndexParam.MetricType.BM25)
.extraParams(sparseParams)
.build();
IndexParam indexParamForImageDense = IndexParam.builder()
.fieldName("image_dense")
.indexName("image_dense_index")
.indexType(IndexParam.IndexType.AUTOINDEX)
.metricType(IndexParam.MetricType.IP)
.build();
List<IndexParam> indexParams = new ArrayList<>();
indexParams.add(indexParamForTextDense);
indexParams.add(indexParamForTextSparse);
indexParams.add(indexParamForImageDense);
indexOption1 := milvusclient.NewCreateIndexOption("my_collection", "text_dense",
index.NewAutoIndex(index.MetricType(entity.IP)))
indexOption2 := milvusclient.NewCreateIndexOption("my_collection", "text_sparse",
index.NewSparseInvertedIndex(entity.BM25, 0.2))
indexOption3 := milvusclient.NewCreateIndexOption("my_collection", "image_dense",
index.NewAutoIndex(index.MetricType(entity.IP)))
const index_params = [{
field_name: "text_dense",
index_name: "text_dense_index",
index_type: "AUTOINDEX",
metric_type: "IP"
},{
field_name: "text_sparse",
index_name: "text_sparse_index",
index_type: "SPARSE_INVERTED_INDEX",
metric_type: "BM25",
params: {
inverted_index_algo: "DAAT_MAXSCORE",
}
},{
field_name: "image_dense",
index_name: "image_dense_index",
index_type: "AUTOINDEX",
metric_type: "IP"
}]
export indexParams='[
{
"fieldName": "text_dense",
"metricType": "IP",
"indexName": "text_dense_index",
"indexType":"AUTOINDEX"
},
{
"fieldName": "text_sparse",
"metricType": "BM25",
"indexName": "text_sparse_index",
"indexType": "SPARSE_INVERTED_INDEX",
"params":{"inverted_index_algo": "DAAT_MAXSCORE"}
},
{
"fieldName": "image_dense",
"metricType": "IP",
"indexName": "image_dense_index",
"indexType":"AUTOINDEX"
}
]'
milvus::IndexDesc text_sparse_index("text_sparse", "text_sparse_index", milvus::IndexType::SPARSE_INVERTED_INDEX, milvus::MetricType::BM25);
text_sparse_index.AddExtraParam("inverted_index_algo", "DAAT_MAXSCORE");
std::vector<milvus::IndexDesc> indexes = {
milvus::IndexDesc("text_dense", "text_dense_index", milvus::IndexType::AUTOINDEX, milvus::MetricType::IP),
text_sparse_index,
milvus::IndexDesc("image_dense", "image_dense_index", milvus::IndexType::AUTOINDEX, milvus::MetricType::IP),
};
创建Collection
创建一个名为demo 的 Collection,其 Schema 和索引配置与前两个步骤中设置的一致。
client.create_collection(
collection_name="my_collection",
schema=schema,
index_params=index_params
)
CreateCollectionReq createCollectionReq = CreateCollectionReq.builder()
.collectionName("my_collection")
.collectionSchema(schema)
.indexParams(indexParams)
.build();
client.createCollection(createCollectionReq);
err = client.CreateCollection(ctx,
milvusclient.NewCreateCollectionOption("my_collection", schema).
WithIndexOptions(indexOption1, indexOption2, indexOption3))
if err != nil {
fmt.Println(err.Error())
// handle error
}
res = await client.createCollection({
collection_name: "my_collection",
fields: fields,
functions: functions,
index_params: index_params,
})
export CLUSTER_ENDPOINT="http://localhost:19530"
export TOKEN="root:Milvus"
curl --request POST \
--url "${CLUSTER_ENDPOINT}/v2/vectordb/collections/create" \
--header "Authorization: Bearer ${TOKEN}" \
--header "Content-Type: application/json" \
--header "Request-Timeout: 10" \
-d "{
\"collectionName\": \"my_collection\",
\"schema\": $schema,
\"indexParams\": $indexParams
}"
status = client->CreateCollection(milvus::CreateCollectionRequest()
.WithCollectionName("my_collection")
.WithCollectionSchema(schema)
.WithIndexes(std::move(indexes)));
if (!status.IsOk()) {
std::cout << status.Message() << std::endl;
}
插入数据
本节将根据之前定义的 Schema 向my_collection Collection 插入数据。插入时,请确保除具有自动生成值的字段外,所有字段均以正确格式提供数据。在此示例中:
id:表示产品 ID 的整数text: 包含产品描述的字符串text_dense: 包含768个浮点数值的列表,代表文本描述的密集嵌入image_dense: 由 512 个浮点数组成的列表,表示产品图片的稠密嵌入向量
您可以使用相同或不同的模型为每个字段生成稠密Embeddings。在此示例中,两个稠密Embeddings的维度不同,这表明它们是由不同的模型生成的。稍后在定义每个搜索时,请务必使用相应的模型来生成合适的查询Embedding。
由于本示例使用内置的 BM25 函数从文本字段生成稀疏 Embeddings,因此您无需手动提供稀疏向量。但是,如果您选择不使用 BM25,则必须自行预先计算并提供稀疏 Embeddings。
import random
# Generate example vectors
def generate_dense_vector(dim):
return [random.random() for _ in range(dim)]
data=[
{
"id": 0,
"text": "Red cotton t-shirt with round neck",
"text_dense": generate_dense_vector(768),
"image_dense": generate_dense_vector(512)
},
{
"id": 1,
"text": "Wireless noise-cancelling over-ear headphones",
"text_dense": generate_dense_vector(768),
"image_dense": generate_dense_vector(512)
},
{
"id": 2,
"text": "Stainless steel water bottle, 500ml",
"text_dense": generate_dense_vector(768),
"image_dense": generate_dense_vector(512)
}
]
res = client.insert(
collection_name="my_collection",
data=data
)
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import io.milvus.v2.service.vector.request.InsertReq;
import io.milvus.v2.service.vector.response.InsertResp;
Gson gson = new Gson();
JsonObject row1 = new JsonObject();
row1.addProperty("id", 0);
row1.addProperty("text", "Red cotton t-shirt with round neck");
row1.add("text_dense", gson.toJsonTree(new float[]{0.3580376395471989f, -0.6023495712049978f, 0.18414012509913835f, ...}));
row1.add("image_dense", gson.toJsonTree(new float[]{0.6366019600530924f, -0.09323198122475052f, ...}));
JsonObject row2 = new JsonObject();
row2.addProperty("id", 1);
row2.addProperty("text", "Wireless noise-cancelling over-ear headphones");
row2.add("text_dense", gson.toJsonTree(new float[]{0.19886812562848388f, 0.06023560599112088f, 0.6976963061752597f, ...}));
row2.add("image_dense", gson.toJsonTree(new float[]{0.6414180010301553f, 0.8976979978567611f, ...}));
JsonObject row3 = new JsonObject();
row3.addProperty("id", 2);
row3.addProperty("text", "Stainless steel water bottle, 500ml");
row3.add("text_dense", gson.toJsonTree(new float[]{0.43742130801983836f, -0.5597502546264526f, 0.6457887650909682f, ...}));
row3.add("image_dense", gson.toJsonTree(new float[]{-0.6901259768402174f, 0.6100500332193755f, ...}));
List<JsonObject> data = Arrays.asList(row1, row2, row3);
InsertReq insertReq = InsertReq.builder()
.collectionName("my_collection")
.data(data)
.build();
InsertResp insertResp = client.insert(insertReq);
_, err = client.Insert(ctx, milvusclient.NewColumnBasedInsertOption("my_collection").
WithInt64Column("id", []int64{0, 1, 2}).
WithVarcharColumn("text", []string{
"Red cotton t-shirt with round neck",
"Wireless noise-cancelling over-ear headphones",
"Stainless steel water bottle, 500ml",
}).
WithFloatVectorColumn("text_dense", 768, [][]float32{
{0.3580376395471989, -0.6023495712049978, 0.18414012509913835, ...},
{0.19886812562848388, 0.06023560599112088, 0.6976963061752597, ...},
{0.43742130801983836, -0.5597502546264526, 0.6457887650909682, ...},
}).
WithFloatVectorColumn("image_dense", 512, [][]float32{
{0.6366019600530924, -0.09323198122475052, ...},
{0.6414180010301553, 0.8976979978567611, ...},
{-0.6901259768402174, 0.6100500332193755, ...},
}))
if err != nil {
fmt.Println(err.Error())
// handle err
}
const { MilvusClient, DataType } = require("@zilliz/milvus2-sdk-node")
var data = [
{id: 0, text: "Red cotton t-shirt with round neck" , text_dense: [0.3580376395471989, -0.6023495712049978, 0.18414012509913835, ...], image_dense: [0.6366019600530924, -0.09323198122475052, ...]},
{id: 1, text: "Wireless noise-cancelling over-ear headphones" , text_dense: [0.19886812562848388, 0.06023560599112088, 0.6976963061752597, ...], image_dense: [0.6414180010301553, 0.8976979978567611, ...]},
{id: 2, text: "Stainless steel water bottle, 500ml" , text_dense: [0.43742130801983836, -0.5597502546264526, 0.6457887650909682, ...], image_dense: [-0.6901259768402174, 0.6100500332193755, ...]}
]
var res = await client.insert({
collection_name: "my_collection",
data: data,
})
curl --request POST \
--url "${CLUSTER_ENDPOINT}/v2/vectordb/entities/insert" \
--header "Authorization: Bearer ${TOKEN}" \
--header "Content-Type: application/json" \
--header "Request-Timeout: 10" \
-d '{
"data": [
{"id": 0, "text": "Red cotton t-shirt with round neck" , "text_dense": [0.3580376395471989, -0.6023495712049978, 0.18414012509913835, ...], "image_dense": [0.6366019600530924, -0.09323198122475052, ...]},
{"id": 1, "text": "Wireless noise-cancelling over-ear headphones" , "text_dense": [0.19886812562848388, 0.06023560599112088, 0.6976963061752597, ...], "image_dense": [0.6414180010301553, 0.8976979978567611, ...]},
{"id": 2, "text": "Stainless steel water bottle, 500ml" , "text_dense": [0.43742130801983836, -0.5597502546264526, 0.6457887650909682, ...], "image_dense": [-0.6901259768402174, 0.6100500332193755, ...]}
],
"collectionName": "my_collection"
}'
#include <random>
std::vector<float>
GenerateFloatVector(int dimension) {
std::random_device rd;
std::mt19937 ran(rd());
std::uniform_real_distribution<float> float_gen(0.0, 1.0);
std::vector<float> vector(dimension);
for (auto d = 0; d < dimension; ++d) {
vector[d] = float_gen(ran);
}
return vector;
}
milvus::EntityRows data = {
{{"id", 0}, {"text", "Red cotton t-shirt with round neck"}, {"text_dense", GenerateFloatVector(768)}, {"image_dense", GenerateFloatVector(512)}},
{{"id", 1}, {"text", "Wireless noise-cancelling over-ear headphones"}, {"text_dense", GenerateFloatVector(768)}, {"image_dense", GenerateFloatVector(512)}},
{{"id", 2}, {"text", "Stainless steel water bottle, 500ml"}, {"text_dense", GenerateFloatVector(768)}, {"image_dense", GenerateFloatVector(512)}}
};
milvus::InsertResponse response;
status = client->Insert(milvus::InsertRequest()
.WithCollectionName("my_collection")
.WithRowsData(std::move(data)),
response);
if (!status.IsOk()) {
std::cout << status.Message() << std::endl;
}
执行混合搜索
步骤 1:创建多个 AnnSearchRequest 实例
混合搜索的实现是在hybrid_search() 函数中创建多个AnnSearchRequest 实例,其中每个AnnSearchRequest 代表针对特定向量字段的基本ANN搜索请求。因此,在执行混合搜索之前,必须为每个向量字段创建一个AnnSearchRequest 。
此外,通过在AnnSearchRequest 中配置expr 参数,您可以为混合搜索设置过滤条件。请参阅《过滤搜索》和《过滤原理说明》。
在混合搜索中,每个AnnSearchRequest 仅支持一组查询数据。
为了演示各种搜索向量字段的功能,我们将使用一个示例查询构建三个AnnSearchRequest 搜索请求。在此过程中,我们还将使用其预计算的密集向量。这些搜索请求将针对以下向量字段:
text_dense用于语义文本搜索,支持上下文理解,并基于语义而非直接关键词匹配进行检索。text_sparse用于全文搜索或关键词匹配,侧重于文本中单词或短语的精确匹配。image_dense用于多模态文本到图像搜索,根据查询的语义内容检索相关的产品图像。
from pymilvus import AnnSearchRequest
query_text = "white headphones, quiet and comfortable"
query_dense_vector = generate_dense_vector(768)
query_multimodal_vector = generate_dense_vector(512)
# text semantic search (dense)
search_param_1 = {
"data": [query_dense_vector],
"anns_field": "text_dense",
"param": {"nprobe": 10},
"limit": 2
}
request_1 = AnnSearchRequest(**search_param_1)
# full-text search (sparse)
search_param_2 = {
"data": [query_text],
"anns_field": "text_sparse",
"param": {},
"limit": 2
}
request_2 = AnnSearchRequest(**search_param_2)
# text-to-image search (multimodal)
search_param_3 = {
"data": [query_multimodal_vector],
"anns_field": "image_dense",
"param": {"nprobe": 10},
"limit": 2
}
request_3 = AnnSearchRequest(**search_param_3)
reqs = [request_1, request_2, request_3]
import io.milvus.v2.service.vector.request.AnnSearchReq;
import io.milvus.v2.service.vector.request.data.BaseVector;
import io.milvus.v2.service.vector.request.data.FloatVec;
import io.milvus.v2.service.vector.request.data.SparseFloatVec;
import io.milvus.v2.service.vector.request.data.EmbeddedText;
float[] queryDense = new float[]{-0.0475336798f, 0.0521207601f, 0.0904406682f, ...};
float[] queryMultimodal = new float[]{0.0158298651f, 0.5264158340f, ...};
List<BaseVector> queryTexts = Collections.singletonList(new EmbeddedText("white headphones, quiet and comfortable"));
List<BaseVector> queryDenseVectors = Collections.singletonList(new FloatVec(queryDense));
List<BaseVector> queryMultimodalVectors = Collections.singletonList(new FloatVec(queryMultimodal));
List<AnnSearchReq> searchRequests = new ArrayList<>();
searchRequests.add(AnnSearchReq.builder()
.vectorFieldName("text_dense")
.vectors(queryDenseVectors)
.params("{\"nprobe\": 10}")
.topK(2)
.build());
searchRequests.add(AnnSearchReq.builder()
.vectorFieldName("text_sparse")
.vectors(queryTexts)
.topK(2)
.build());
searchRequests.add(AnnSearchReq.builder()
.vectorFieldName("image_dense")
.vectors(queryMultimodalVectors)
.params("{\"nprobe\": 10}")
.topK(2)
.build());
queryText := entity.Text("white headphones, quiet and comfortable")
queryVector := []float32{0.3580376395471989, -0.6023495712049978, 0.18414012509913835, ...}
queryMultimodalVector := []float32{0.015829865178701663, 0.5264158340734488, ...}
request1 := milvusclient.NewAnnRequest("text_dense", 2, entity.FloatVector(queryVector)).
WithAnnParam(index.NewIvfAnnParam(10))
annParam := index.NewSparseAnnParam()
annParam.WithDropRatio(0.2)
request2 := milvusclient.NewAnnRequest("text_sparse", 2, queryText).
WithAnnParam(annParam)
request3 := milvusclient.NewAnnRequest("image_dense", 2, entity.FloatVector(queryMultimodalVector)).
WithAnnParam(index.NewIvfAnnParam(10))
const query_text = "white headphones, quiet and comfortable"
const query_vector = [0.3580376395471989, -0.6023495712049978, 0.18414012509913835, ...]
const query_multimodal_vector = [0.015829865178701663, 0.5264158340734488, ...]
const search_param_1 = {
"data": query_vector,
"anns_field": "text_dense",
"params": {"nprobe": 10},
"limit": 2
}
const search_param_2 = {
"data": query_text,
"anns_field": "text_sparse",
"limit": 2
}
const search_param_3 = {
"data": query_multimodal_vector,
"anns_field": "image_dense",
"params": {"nprobe": 10},
"limit": 2
}
export req='[
{
"data": [[0.3580376395471989, -0.6023495712049978, 0.18414012509913835, ...]],
"annsField": "text_dense",
"params": {"nprobe": 10},
"limit": 2
},
{
"data": ["white headphones, quiet and comfortable"],
"annsField": "text_sparse",
"limit": 2
},
{
"data": [[0.015829865178701663, 0.5264158340734488, ...]],
"annsField": "image_dense",
"params": {"nprobe": 10},
"limit": 2
}
]'
auto query_text = "white headphones, quiet and comfortable";
auto query_dense_vector = GenerateFloatVector(768);
auto query_multimodal_vector = GenerateFloatVector(512);
// text semantic search (dense)
auto sub_req1 = milvus::SubSearchRequest()
.AddFloatVector(query_dense_vector)
.WithAnnsField("text_dense")
.WithLimit(2);
sub_req1.AddExtraParam("nprobe", "10");
// full-text search (sparse)
auto sub_req2 = milvus::SubSearchRequest()
.AddEmbeddedText(query_text)
.WithAnnsField("text_sparse")
.WithLimit(2);
// text-to-image search (multimodal)
auto sub_req3 = milvus::SubSearchRequest()
.AddFloatVector(query_multimodal_vector)
.WithAnnsField("image_dense")
.WithLimit(2);
sub_req3.AddExtraParam("nprobe", "10");
鉴于参数limit 设置为2,每次AnnSearchRequest 调用将返回2个搜索结果。在此示例中,共创建了3个AnnSearchRequest 实例,因此总共产生了6个搜索结果。
步骤 2:配置重新排序策略
要合并并重新排序各人工神经网络(ANN)搜索结果集,选择合适的重新排序策略至关重要。Milvus 提供了多种类型的重新排序策略。有关这些重新排序机制的更多详细信息,请参阅“加权排序器”或“RRF 排序器”。
在本示例中,由于没有特别强调特定的搜索查询,我们将采用 RRFRanker 策略。
ranker = Function(
name="rrf",
input_field_names=[], # Must be an empty list
function_type=FunctionType.RERANK,
params={
"reranker": "rrf",
"k": 100 # Optional
}
)
import io.milvus.common.clientenum.FunctionType;
import io.milvus.v2.service.collection.request.CreateCollectionReq.Function;
Function ranker = Function.builder()
.name("rrf")
.functionType(FunctionType.RERANK)
.param("reranker", "rrf")
.param("k", "100")
.build();
const rerank = {
name: 'rrf',
description: 'bm25 function',
type: FunctionType.RERANK,
input_field_names: [],
params: {
"reranker": "rrf",
"k": 100
},
};
reranker := milvusclient.NewRRFReranker().WithK(100)
# Restful
export rerank='{"k": 100}'
auto ranker = std::make_shared<milvus::RRFRerank>(100);
步骤 3:执行混合搜索
在启动混合搜索之前,请确保已加载Collection。如果Collection中的任何向量字段缺少索引或未加载到内存中,执行混合搜索方法时将引发错误。
res = client.hybrid_search(
collection_name="my_collection",
reqs=reqs,
ranker=ranker,
limit=2
)
for hits in res:
print("TopK results:")
for hit in hits:
print(hit)
import io.milvus.v2.common.ConsistencyLevel;
import io.milvus.v2.service.vector.request.HybridSearchReq;
import io.milvus.v2.service.vector.response.SearchResp;
HybridSearchReq hybridSearchReq = HybridSearchReq.builder()
.collectionName("my_collection")
.searchRequests(searchRequests)
.ranker(ranker)
.topK(2)
.build();
SearchResp searchResp = client.hybridSearch(hybridSearchReq);
resultSets, err := client.HybridSearch(ctx, milvusclient.NewHybridSearchOption(
"my_collection",
2,
request1,
request2,
request3,
).WithReranker(reranker))
if err != nil {
fmt.Println(err.Error())
// handle error
}
for _, resultSet := range resultSets {
fmt.Println("IDs: ", resultSet.IDs.FieldData().GetScalars())
fmt.Println("Scores: ", resultSet.Scores)
}
const { MilvusClient, DataType } = require("@zilliz/milvus2-sdk-node")
res = await client.loadCollection({
collection_name: "my_collection"
})
import { MilvusClient, RRFRanker, WeightedRanker } from '@zilliz/milvus2-sdk-node';
const search = await client.search({
collection_name: "my_collection",
data: [search_param_1, search_param_2, search_param_3],
limit: 2,
rerank: rerank
});
curl --request POST \
--url "${CLUSTER_ENDPOINT}/v2/vectordb/entities/hybrid_search" \
--header "Authorization: Bearer ${TOKEN}" \
--header "Content-Type: application/json" \
--header "Request-Timeout: 10" \
-d "{
\"collectionName\": \"my_collection\",
\"search\": ${req},
\"rerank\": {
\"strategy\":\"rrf\",
\"params\": ${rerank}
},
\"limit\": 2
}"
auto request = milvus::HybridSearchRequest()
.WithCollectionName("my_collection")
.AddSubRequest(std::make_shared<milvus::SubSearchRequest>(std::move(sub_req1)))
.AddSubRequest(std::make_shared<milvus::SubSearchRequest>(std::move(sub_req2)))
.AddSubRequest(std::make_shared<milvus::SubSearchRequest>(std::move(sub_req3)))
.WithRerank(ranker)
.WithLimit(2);
milvus::SearchResponse response;
status = client->HybridSearch(request, response);
if (!status.IsOk()) {
std::cout << status.Message() << std::endl;
}
for (auto& result : response.Results().Results()) {
std::cout << "TopK results:" << std::endl;
milvus::EntityRows output_rows;
status = result.OutputRows(output_rows);
for (const auto& row : output_rows) {
std::cout << "\t" << row << std::endl;
}
}
输出结果如下:
["['id: 1, distance: 0.006047376897186041, entity: {}', 'id: 2, distance: 0.006422005593776703, entity: {}']"]
当为混合搜索指定limit=2 参数时,Milvus将对从三次搜索中获得的六个结果进行重新排序。最终,系统将仅返回相似度最高的两个结果。
高级用法
为混合搜索临时设置时区
如果您的Collection包含TIMESTAMPTZ 字段,您可以通过在混合搜索调用中设置timezone 参数,针对单次操作临时覆盖数据库或Collection的默认时区。这将控制操作过程中TIMESTAMPTZ 值的显示和比较方式。
timezone 的值必须是有效的IANA 时区标识符(例如Asia/Shanghai、America/Chicago 或UTC)。有关如何使用TIMESTAMPTZ 字段的详细信息,请参阅TIMESTAMPTZ 字段。
以下示例演示了如何为混合搜索操作临时设置时区:
res = client.hybrid_search(
collection_name="my_collection",
reqs=reqs,
ranker=ranker,
limit=2,
timezone="America/Havana",
)
List<AnnSearchReq> tzRequests = new ArrayList<>();
tzRequests.add(AnnSearchReq.builder()
.vectorFieldName("text_dense")
.vectors(queryDenseVectors)
.params("{\"nprobe\": 10}")
.topK(2)
.timezone("America/Havana")
.build());
tzRequests.add(AnnSearchReq.builder()
.vectorFieldName("text_sparse")
.vectors(queryTexts)
.topK(2)
.timezone("America/Havana")
.build());
tzRequests.add(AnnSearchReq.builder()
.vectorFieldName("image_dense")
.vectors(queryMultimodalVectors)
.params("{\"nprobe\": 10}")
.topK(2)
.timezone("America/Havana")
.build());
HybridSearchReq tzHybridSearchReq = HybridSearchReq.builder()
.collectionName("my_collection")
.searchRequests(tzRequests)
.ranker(ranker)
.topK(2)
.build();
SearchResp tzSearchResp = client.hybridSearch(tzHybridSearchReq);
tzRequest1 := milvusclient.NewAnnRequest("text_dense", 2, entity.FloatVector(queryVector)).
WithAnnParam(index.NewIvfAnnParam(10)).
WithSearchParam("timezone", "America/Havana")
tzRequest2 := milvusclient.NewAnnRequest("text_sparse", 2, queryText).
WithAnnParam(annParam).
WithSearchParam("timezone", "America/Havana")
tzRequest3 := milvusclient.NewAnnRequest("image_dense", 2, entity.FloatVector(queryMultimodalVector)).
WithAnnParam(index.NewIvfAnnParam(10)).
WithSearchParam("timezone", "America/Havana")
resultSets, err = client.HybridSearch(ctx, milvusclient.NewHybridSearchOption(
"my_collection",
2,
tzRequest1,
tzRequest2,
tzRequest3,
).WithReranker(reranker))
res = await client.search({
collection_name: "my_collection",
data: [
{ ...search_param_1, params: { "nprobe": 10, timezone: "America/Havana" } },
{ ...search_param_2, params: { timezone: "America/Havana" } },
{ ...search_param_3, params: { "nprobe": 10, timezone: "America/Havana" } },
],
limit: 2,
rerank: rerank
});
# restful
curl --request POST \
--url "${CLUSTER_ENDPOINT}/v2/vectordb/entities/hybrid_search" \
--header "Authorization: Bearer ${TOKEN}" \
--header "Content-Type: application/json" \
-d '{
"collectionName": "my_collection",
"search": [
{
"data": [[0.3580376395471989, -0.6023495712049978, 0.18414012509913835, ...]],
"annsField": "text_dense",
"params": {"nprobe": 10, "timezone": "America/Havana"},
"limit": 2
},
{
"data": ["white headphones, quiet and comfortable"],
"annsField": "text_sparse",
"params": {"timezone": "America/Havana"},
"limit": 2
},
{
"data": [[0.015829865178701663, 0.5264158340734488, ...]],
"annsField": "image_dense",
"params": {"nprobe": 10, "timezone": "America/Havana"},
"limit": 2
}
],
"rerank": {
"strategy": "rrf",
"params": {"k": 100}
},
"limit": 2
}'
auto tz_req1 = milvus::SubSearchRequest()
.AddFloatVector(query_dense_vector)
.WithAnnsField("text_dense")
.WithTimezone("America/Havana")
.WithLimit(2);
tz_req1.AddExtraParam("nprobe", "10");
auto tz_req2 = milvus::SubSearchRequest()
.AddEmbeddedText(query_text)
.WithAnnsField("text_sparse")
.WithTimezone("America/Havana")
.WithLimit(2);
auto tz_req3 = milvus::SubSearchRequest()
.AddFloatVector(query_multimodal_vector)
.WithAnnsField("image_dense")
.WithTimezone("America/Havana")
.WithLimit(2);
tz_req3.AddExtraParam("nprobe", "10");
auto tz_request = milvus::HybridSearchRequest()
.WithCollectionName("my_collection")
.AddSubRequest(std::make_shared<milvus::SubSearchRequest>(std::move(tz_req1)))
.AddSubRequest(std::make_shared<milvus::SubSearchRequest>(std::move(tz_req2)))
.AddSubRequest(std::make_shared<milvus::SubSearchRequest>(std::move(tz_req3)))
.WithRerank(ranker)
.WithLimit(2);
milvus::SearchResponse tz_response;
status = client->HybridSearch(tz_request, tz_response);
if (!status.IsOk()) {
std::cout << status.Message() << std::endl;
}