🚀 免費嘗試 Zilliz Cloud,完全托管的 Milvus,體驗速度提升 10 倍!立即嘗試

milvus-logo
LFAI
主頁
  • 使用者指南
  • Home
  • Docs
  • 使用者指南

  • 搜尋與重新排名

  • 混合搜尋

混合搜尋

混合搜尋指的是一種同時進行多個 ANN 搜尋、從這些 ANN 搜尋中重新排序多組結果,並最終返回單一結果集的搜尋方法。使用 Hybrid Search 可以提高搜尋準確度。Zilliz 支援在具有多向量領域的集合上進行混合搜尋。

Hybrid Search 最常用於包括稀疏密集向量搜尋和多模式搜尋等情況。本指南將以特定範例說明如何在 Zilliz 中執行混合搜尋。

使用情境

混合搜尋適用於以下兩種情況。

不同類型的向量可以代表不同的資訊,使用不同的嵌入模型可以更全面地代表資料的不同特徵和面向。例如,對同一句子使用不同的嵌入模型,可以產生表示語義的密集向量和表示句子中詞彙頻率的稀疏向量。

  • 稀疏向量:稀疏向量的特點是向量維度高,而且只有很少的非零值存在。這種結構使它們特別適合傳統的資訊檢索應用。在大多數情況下,稀疏向量所使用的維數對應於一種或多種語言的不同詞彙。每個維度都會被指定一個值,表示該標記在文件中的相對重要性。對於涉及文字比對的任務而言,這種佈局很有優勢。

  • 密集向量:密集向量是來自於神經網路的嵌入。當排列成有序陣列時,這些向量可以捕捉輸入文字的語意精髓。請注意,密集向量並不限於文字處理;它們也被廣泛應用於電腦視覺,以表示視覺資料的語意。這些密集向量通常是由文字嵌入模型所產生,其特點是大部分或所有元素都非零。因此,密集向量對於語意搜尋應用特別有效,因為即使沒有精確的文字匹配,它們也能根據向量距離傳回最相似的結果。此功能可讓搜尋結果更細緻、更能感知上下文,通常可捕捉到基於關鍵字的方法可能遺漏的概念之間的關係。

如需詳細資訊,請參閱Sparse VectorDense Vector

多模態搜尋是指跨多種模態(如圖像、視訊、音訊、文字等)的非結構化資料相似性搜尋。例如,可以使用指紋、聲紋和臉部特徵等多種模式的資料來表示一個人。混合搜尋支援同時進行多重搜尋。例如,使用相似的指紋和聲紋搜尋一個人。

工作流程

進行混合搜尋的主要工作流程如下。

  1. 透過BERTTransformers 等嵌入模型產生密集向量。

  2. 透過BM25BGE-M3SPLADE 等嵌入模型產生稀疏向量。

  3. 在 Zilliz 中建立集合,並定義集合模式,其中包含密集與稀疏向量領域。

  4. 將稀疏密集向量插入上一步中剛建立的集合中。

  5. 進行混合搜尋:稠密向量上的 ANN Search 將會傳回一組 Top-K 最相似的結果,而稀疏向量上的文字匹配也會傳回一組 Top-K 結果。

  6. 歸一化:將兩組 Top-K 結果的分數歸一化,將分數轉換為 [0,1] 之間的範圍。

  7. 選擇合適的重排策略來合併兩組 Top-K 結果並重排,最後傳回一組 Top-K 結果。

Hybrid Search Workflow 混合搜尋工作流程

範例

本節將使用一個特定範例來說明如何在稀疏密集向量上進行 Hybrid Search,以提高文字搜尋的精確度。

建立具有多向量欄位的集合

建立集合的過程包括三個部分:定義集合模式、配置索引參數,以及建立集合。

定義模式

在本範例中,需要在集合模式中定義多個向量欄位。目前,每個集合預設最多可包含 4 個向量欄位。但您也可以修改 proxy.maxVectorFieldNum 的值,以便根據需要在一個集合中最多包含 10 個向量欄位。

以下範例定義了一個集合模式,其中densesparse 是兩個向量欄位。

  • id:這個欄位是儲存文字 ID 的主索引鍵。這個欄位的資料類型是 INT64。

  • text:這個欄位用來儲存文字內容。這個欄位的資料類型是 VARCHAR,最大長度為 1000 個字元。

  • dense:這個欄位用來儲存文字的密集向量。這個欄位的資料類型是 FLOAT_VECTOR,向量尺寸是 768。

  • sparse:這個欄位用來儲存文字的稀疏向量。這個欄位的資料類型是 SPARSE_FLOAT_VECTOR。

# Create a collection in customized setup mode
from pymilvus import (
    MilvusClient, DataType
)

client = MilvusClient(
    uri="http://localhost:19530",
    token="root:Milvus"
)

# Create schema
schema = MilvusClient.create_schema(
    auto_id=False,
    enable_dynamic_field=True,
)
# Add fields to schema
schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True)
schema.add_field(field_name="text", datatype=DataType.VARCHAR, max_length=1000)
schema.add_field(field_name="sparse", datatype=DataType.SPARSE_FLOAT_VECTOR)
schema.add_field(field_name="dense", datatype=DataType.FLOAT_VECTOR, dim=5)

import io.milvus.v2.client.ConnectConfig;
import io.milvus.v2.client.MilvusClientV2;
import io.milvus.v2.common.DataType;
import io.milvus.v2.service.collection.request.AddFieldReq;
import io.milvus.v2.service.collection.request.CreateCollectionReq;

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)
        .build());

schema.addField(AddFieldReq.builder()
        .fieldName("dense")
        .dataType(DataType.FloatVector)
        .dimension(768)
        .build());

schema.addField(AddFieldReq.builder()
        .fieldName("sparse")
        .dataType(DataType.SparseFloatVector)
        .build());

// WIP

import { MilvusClient, DataType } from "@zilliz/milvus2-sdk-node";

const address = "http://localhost:19530";
const token = "root:Milvus";
const client = new MilvusClient({address, token});

// Create a collection in customized setup mode
// 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
    },
    {
        name: "sparse",
        data_type: DataType.SPARSE_FLOAT_VECTOR
    },
    {
        name: "dense",
        data_type: DataType.FloatVector,
        dim: 768
    }
]

export schema='{
        "autoId": false,
        "enabledDynamicField": true,
        "fields": [
            {
                "fieldName": "id",
                "dataType": "Int64",
                "isPrimary": true
            },
            {
                "fieldName": "text",
                "dataType": "VarChar",
                "elementTypeParams": {
                    "max_length": 1000
                }
            },
            {
                "fieldName": "sparse",
                "dataType": "SparseFloatVector"
            },
            {
                "fieldName": "dense",
                "dataType": "FloatVector",
                "elementTypeParams": {
                    "dim": "768"
                }
            }
        ]
    }'

在稀疏向量搜尋過程中,您可以利用全文檢索 (Full Text Search) 功能來簡化產生稀疏嵌入向量的過程。如需詳細資訊,請參閱Full Text Search

建立索引

定義集合模式後,就必須設定向量索引和相似度指標。在本範例中,為密集向量欄位dense 建立 IVF_FLAT 索引,並為稀疏向量欄位sparse 建立 SPARSE_INVERTED_INDEX 索引。要瞭解支援的索引類型,請參閱Index Explained

from pymilvus import MilvusClient

# Prepare index parameters
index_params = client.prepare_index_params()

# Add indexes
index_params.add_index(
    field_name="dense",
    index_name="dense_index",
    index_type="IVF_FLAT",
    metric_type="IP",
    params={"nlist": 128},
)

index_params.add_index(
    field_name="sparse",
    index_name="sparse_index",
    index_type="SPARSE_INVERTED_INDEX",  # Index type for sparse vectors
    metric_type="IP",  # Currently, only IP (Inner Product) is supported for sparse vectors
    params={"inverted_index_algo": "DAAT_MAXSCORE"},  # The ratio of small vector values to be dropped during indexing
)

import io.milvus.v2.common.IndexParam;
import java.util.*;

Map<String, Object> denseParams = new HashMap<>();
denseParams.put("nlist", 128);
IndexParam indexParamForDenseField = IndexParam.builder()
        .fieldName("dense")
        .indexName("dense_index")
        .indexType(IndexParam.IndexType.IVF_FLAT)
        .metricType(IndexParam.MetricType.IP)
        .extraParams(denseParams)
        .build();

Map<String, Object> sparseParams = new HashMap<>();
sparseParams.put("inverted_index_algo": "DAAT_MAXSCORE");
IndexParam indexParamForSparseField = IndexParam.builder()
        .fieldName("sparse")
        .indexName("sparse_index")
        .indexType(IndexParam.IndexType.SPARSE_INVERTED_INDEX)
        .metricType(IndexParam.MetricType.IP)
        .extraParams(sparseParams)
        .build();

List<IndexParam> indexParams = new ArrayList<>();
indexParams.add(indexParamForDenseField);
indexParams.add(indexParamForSparseField);

const index_params = [{
    field_name: "dense",
    index_type: "IVF_FLAT",
    metric_type: "IP"
},{
    field_name: "sparse",
    index_type: "SPARSE_INVERTED_INDEX",
    metric_type: "IP"
}]

export indexParams='[
        {
            "fieldName": "dense",
            "metricType": "IP",
            "indexName": "dense_index",
            "indexType":"IVF_FLAT",
            "params":{"nlist":128}
        },
        {
            "fieldName": "sparse",
            "metricType": "IP",
            "indexName": "sparse_index",
            "indexType": "SPARSE_INVERTED_INDEX"
        }
    ]'

建立集合

使用前兩個步驟中設定的集合模式和索引,建立一個名為demo 的集合。

from pymilvus import MilvusClient

client.create_collection(
    collection_name="hybrid_search_collection",
    schema=schema,
    index_params=index_params
)

CreateCollectionReq createCollectionReq = CreateCollectionReq.builder()
        .collectionName("hybrid_search_collection")
        .collectionSchema(schema)
        .indexParams(indexParams)
        .build();
client.createCollection(createCollectionReq);

res = await client.createCollection({
    collection_name: "hybrid_search_collection",
    fields: fields,
    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" \
-d "{
    \"collectionName\": \"hybrid_search_collection\",
    \"schema\": $schema,
    \"indexParams\": $indexParams
}"

插入資料

將稀疏密集向量插入集合demo

from pymilvus import MilvusClient

data=[
    {"id": 0, "text": "Artificial intelligence was founded as an academic discipline in 1956.", "sparse":{9637: 0.30856525997853057, 4399: 0.19771651149001523, ...}, "dense": [0.3580376395471989, -0.6023495712049978, 0.18414012509913835, ...]},
    {"id": 1, "text": "Alan Turing was the first person to conduct substantial research in AI.", "sparse":{6959: 0.31025067641541815, 1729: 0.8265339135915016, ...}, "dense": [0.19886812562848388, 0.06023560599112088, 0.6976963061752597, ...]},
    {"id": 2, "text": "Born in Maida Vale, London, Turing was raised in southern England.", "sparse":{1220: 0.15303302147479103, 7335: 0.9436728846033107, ...}, "dense": [0.43742130801983836, -0.5597502546264526, 0.6457887650909682, ...]}

res = client.insert(
    collection_name="hybrid_search_collection",
    data=data
)


import com.google.gson.Gson;
import com.google.gson.JsonObject;
import io.milvus.v2.service.vector.request.InsertReq;

Gson gson = new Gson();
JsonObject row1 = new JsonObject();
row1.addProperty("id", 1);
row1.addProperty("text", "Artificial intelligence was founded as an academic discipline in 1956.");
row1.add("dense", gson.toJsonTree(dense1));
row1.add("sparse", gson.toJsonTree(sparse1));

JsonObject row2 = new JsonObject();
row2.addProperty("id", 2);
row2.addProperty("text", "Alan Turing was the first person to conduct substantial research in AI.");
row2.add("dense", gson.toJsonTree(dense2));
row2.add("sparse", gson.toJsonTree(sparse2));

JsonObject row3 = new JsonObject();
row3.addProperty("id", 3);
row3.addProperty("text", "Born in Maida Vale, London, Turing was raised in southern England.");
row3.add("dense", gson.toJsonTree(dense3));
row3.add("sparse", gson.toJsonTree(sparse3));

List<JsonObject> data = Arrays.asList(row1, row2, row3);
InsertReq insertReq = InsertReq.builder()
        .collectionName("hybrid_search_collection")
        .data(data)
        .build();

InsertResp insertResp = client.insert(insertReq);

const { MilvusClient, DataType } = require("@zilliz/milvus2-sdk-node")

var data = [
    {id: 0, text: "Artificial intelligence was founded as an academic discipline in 1956.", sparse:[9637: 0.30856525997853057, 4399: 0.19771651149001523, ...] , dense: [0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592]},
    {id: 1, text: "Alan Turing was the first person to conduct substantial research in AI.", sparse:[6959: 0.31025067641541815, 1729: 0.8265339135915016, ...] , dense: [0.19886812562848388, 0.06023560599112088, 0.6976963061752597, 0.2614474506242501, 0.838729485096104]},
    {id: 2, text: "Born in Maida Vale, London, Turing was raised in southern England." , sparse:[1220: 0.15303302147479103, 7335: 0.9436728846033107, ...] , dense: [0.43742130801983836, -0.5597502546264526, 0.6457887650909682, 0.7894058910881185, 0.20785793220625592]}       
]

var res = await client.insert({
    collection_name: "hybrid_search_collection",
    data: data,
})

curl --request POST \
--url "${CLUSTER_ENDPOINT}/v2/vectordb/entities/insert" \
--header "Authorization: Bearer ${TOKEN}" \
--header "Content-Type: application/json" \
-d '{
    "data": [
        {"id": 0, "text": "Artificial intelligence was founded as an academic discipline in 1956.", "sparse":{"9637": 0.30856525997853057, "4399": 0.19771651149001523}, "dense": [0.3580376395471989, -0.6023495712049978, 0.18414012509913835, ...]},
        {"id": 1, "text": "Alan Turing was the first person to conduct substantial research in AI.", "sparse":{"6959": 0.31025067641541815, "1729": 0.8265339135915016}, "dense": [0.19886812562848388, 0.06023560599112088, 0.6976963061752597, ...]},
        {"id": 2, "text": "Born in Maida Vale, London, Turing was raised in southern England.", "sparse":{"1220": 0.15303302147479103, "7335": 0.9436728846033107}, "dense": [0.43742130801983836, -0.5597502546264526, 0.6457887650909682, ...]}
    ],
    "collectionName": "hybrid_search_collection"
}'

建立多個 AnnSearchRequest 實體

Hybrid Search 是透過在hybrid_search() 函式中建立多個AnnSearchRequest 來實作,其中每個AnnSearchRequest 代表特定向量領域的基本 ANN 搜尋請求。因此,在進行 Hybrid Search 之前,必須為每個向量欄位建立一個AnnSearchRequest

在 Hybrid Search 中,每個AnnSearchRequest 只支援一個查詢向量。

假設查詢文字「Who started AI research?」已經轉換成稀疏向量和密集向量。在此基礎上,分別針對sparsedense 向量字段建立兩個AnnSearchRequest 搜尋請求,如以下範例所示。

from pymilvus import AnnSearchRequest

query_dense_vector = [0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592]

search_param_1 = {
    "data": [query_dense_vector],
    "anns_field": "dense",
    "param": {
        "metric_type": "IP",
        "params": {"nprobe": 10}
    },
    "limit": 2
}
request_1 = AnnSearchRequest(**search_param_1)

query_sparse_vector = {3573: 0.34701499565746674}, {5263: 0.2639375518635271}
search_param_2 = {
    "data": [query_sparse_vector],
    "anns_field": "sparse",
    "param": {
        "metric_type": "IP",
        "params": {}
    },
    "limit": 2
}
request_2 = AnnSearchRequest(**search_param_2)

reqs = [request_1, request_2]


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;

float[] dense = new float[]{-0.0475336798f,  0.0521207601f,  0.0904406682f, ...};
SortedMap<Long, Float> sparse = new TreeMap<Long, Float>() {{
    put(3573L, 0.34701499f);
    put(5263L, 0.263937551f);
    ...
}};


List<BaseVector> queryDenseVectors = Collections.singletonList(new FloatVec(dense));
List<BaseVector> querySparseVectors = Collections.singletonList(new SparseFloatVec(sparse));

List<AnnSearchReq> searchRequests = new ArrayList<>();
searchRequests.add(AnnSearchReq.builder()
        .vectorFieldName("dense")
        .vectors(queryDenseVectors)
        .metricType(IndexParam.MetricType.IP)
        .params("{\"nprobe\": 10}")
        .topK(2)
        .build());
searchRequests.add(AnnSearchReq.builder()
        .vectorFieldName("sparse")
        .vectors(querySparseVectors)
        .metricType(IndexParam.MetricType.IP)
        .params()
        .topK(2)
        .build());

const search_param_1 = {
    "data": query_vector, 
    "anns_field": "dense", 
    "param": {
        "metric_type": "IP", 
        "params": {"nprobe": 10}
    },
    "limit": 2 
}

const search_param_2 = {
    "data": query_sparse_vector, 
    "anns_field": "sparse", 
    "param": {
        "metric_type": "IP", 
        "params": {}
    },
    "limit": 2 
}

export req='[
    {
        "data": [[0.3580376395471989, -0.6023495712049978, 0.18414012509913835, -0.26286205330961354, 0.9029438446296592,....]],
        "annsField": "dense",
        "params": {
            "params": {
                "nprobe": 10
             }
        },
        "limit": 2
    },
    {
        "data": [{"3573": 0.34701499565746674}, {"5263": 0.2639375518635271}],
        "annsField": "sparse",
        "params": {
            "params": {}
        },
        "limit": 2
    }
 ]'

由於參數limit 設定為 2,因此每個AnnSearchRequest 會返回 2 個搜尋結果。在本範例中,會建立 2 個AnnSearchRequest ,因此總共會傳回 4 個搜尋結果。

設定重新排序策略

要合併兩組 ANN 搜尋結果並將其重新排序,必須選擇適當的重新排序策略。Zilliz 支援兩種 reranking 策略:WeightedRankerRRFRanker。在選擇重排策略時,需要考慮的一件事是,是否需要強調向量場上的一個或多個基本 ANN 搜尋。

  • WeightedRanker:如果您要求結果強調特定向量領域,建議使用此策略。WeightedRanker 允許您為某些向量領域指定較高的權重,使其更受重視。例如,在多模式搜尋中,圖片的文字描述可能會被認為比這張圖片的顏色更重要。

  • RRFRanker (Reciprocal Rank Fusion Ranker):當沒有特定的重點時,建議使用此策略。RRF 可以有效平衡每個向量場的重要性。

有關這兩種重排策略機制的詳細資訊,請參閱Reranking

以下兩個範例示範如何使用 WeightedRanker 和 RRFRanker 重排策略。

  1. 範例 1:使用 WeightedRanker

    使用 WeightedRanker 策略時,需要在WeightedRanker 函式中輸入權重值。混合搜尋中的基本 ANN 搜尋數量與需要輸入的值數量相對應。輸入的值應該在 [0,1] 的範圍內,值越接近 1 表示重要性越高。

    from pymilvus import WeightedRanker
    
    rerank= WeightedRanker(0.8, 0.3) 
    
    
    import io.milvus.v2.service.vector.request.ranker.BaseRanker;
    import io.milvus.v2.service.vector.request.ranker.WeightedRanker;
    
    BaseRanker reranker = new WeightedRanker(Arrays.asList(0.8f, 0.3f));
    
    
    import { MilvusClient, DataType } from "@zilliz/milvus2-sdk-node";
    
    const rerank = WeightedRanker(0.8, 0.3);
    
    
    export rerank='{
            "strategy": "ws",
            "params": {"weights": [0.8,0.3]}
        }'
    
    
  2. 範例 2:使用 RRFRanker

    使用 RRFRanker 策略時,您需要將參數值k 輸入 RRFRanker。k 的預設值是 60。此參數有助於決定如何結合來自不同 ANN 搜尋的排名,目的是平衡和混合所有搜尋的重要性。

    from pymilvus import RRFRanker
    
    ranker = RRFRanker(100)
    
    
    import io.milvus.v2.service.vector.request.ranker.BaseRanker;
    import io.milvus.v2.service.vector.request.ranker.RRFRanker;
    
    BaseRanker reranker = new RRFRanker(100);
    
    
    import { MilvusClient, DataType } from "@zilliz/milvus2-sdk-node";
    
    const rerank = RRFRanker("100");
    
    
    export rerank='{
            "strategy": "rrf",
            "params": { "k": 100}
        }'
    
    

在執行混合搜尋之前,必須先將集合載入記憶體。如果集合中的任何向量欄位沒有索引或未載入,在呼叫 Hybrid Search 方法時將會發生錯誤。

from pymilvus import MilvusClient

res = client.hybrid_search(
    collection_name="hybrid_search_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("hybrid_search_collection")
        .searchRequests(searchRequests)
        .ranker(reranker)
        .topK(2)
        .consistencyLevel(ConsistencyLevel.BOUNDED)
        .build();

SearchResp searchResp = client.hybridSearch(hybridSearchReq);

const { MilvusClient, DataType } = require("@zilliz/milvus2-sdk-node")

res = await client.loadCollection({
    collection_name: "hybrid_search_collection"
})

import { MilvusClient, RRFRanker, WeightedRanker } from '@zilliz/milvus2-sdk-node';

const search = await client.search({
  collection_name: "hybrid_search_collection",
  data: [search_param_1, search_param_2],
  limit: 2,
  rerank: RRFRanker(100)
});

curl --request POST \
--url "${CLUSTER_ENDPOINT}/v2/vectordb/entities/advanced_search" \
--header "Authorization: Bearer ${TOKEN}" \
--header "Content-Type: application/json" \
-d "{
    \"collectionName\": \"hybrid_search_collection\",
    \"search\": ${req},
    \"rerank\": {
        \"strategy\":\"rrf\",
        \"params\": {
            \"k\": 10
        }
    },
    \"limit\": 3,
    \"outputFields\": [
        \"user_id\",
        \"word_count\",
        \"book_describe\"
    ]
}"

輸出如下。

["['id: 844, distance: 0.006047376897186041, entity: {}', 'id: 876, distance: 0.006422005593776703, entity: {}']"]

由於在 Hybrid Search 中指定了limit=2 ,Zilliz 會將步驟 3 中的四個搜尋結果重新排序,最終只會返回前兩個最相似的搜尋結果。

免費嘗試托管的 Milvus

Zilliz Cloud 無縫接入,由 Milvus 提供動力,速度提升 10 倍。

開始使用
反饋

這個頁面有幫助嗎?