全部產品
Search
文件中心

PolarDB:使用OpenSearch協議進行向量檢索

更新時間:Nov 01, 2025

PolarSearch的向量檢索功能的支援通過REST API對文本、映像等非結構化資料進行高效的相似性搜尋,能夠在海量資料中快速而精準地找到與查詢目標最為相似的結果,從而有效提升應用的智能化水平

功能簡介

向量檢索,也稱為相似性搜尋,是一種通過比較向量之間的“距離”來尋找最相似資料的技術,它與依賴精確關鍵詞匹配的傳統搜尋有著本質區別。

它的核心思想是將現實世界中的文本、映像、音頻等非結構化資料,通過深度學習模型(如LLM)轉換為向量嵌入(Embedding) 的數值表示。這些多維向量能夠捕捉資料的深層語義資訊。

當您發起一次查詢時,PolarSearch會將您的查詢內容同樣轉換為向量,然後執行k-最近鄰(k-Nearest Neighbor, k-NN)搜尋。這是一種核心演算法,其目標是在海量資料中找到與您的查詢向量“距離”最近的k個向量。這裡的k是一個由您定義的數字(例如,當k=5時,即代表尋找最相似的5個結果)。最終,PolarSearch會返回這k個最相似的結果。

為實現高效檢索,PolarSearch依賴兩大核心組件:向量索引和向量儲存最佳化。

  • 向量索引:為了避免在海量資料中進行全量計算,需要預先建立向量索引。索引能夠根據向量資料的特徵構建一種為查詢而最佳化的資料結構,在查詢時能夠大幅縮小搜尋範圍,從而顯著提升檢索效能。PolarSearch內支援了多種類型的向量索引,以下為您主要介紹業界主流的HNSW和IVF索引:

    • HNSW (Hierarchical Navigable Small World):一種基於圖的索引,具有高效能和高召回率的優點,但記憶體開銷也相應較大。適用於對查詢延遲和精度要求極高,且資料集大小在記憶體容量範圍內的情境。

    • IVF (Inverted File):一種基於聚類的倒排索引,記憶體佔用較低,更適合需要處理超大規模資料集且記憶體受限的情境,但其搜尋精度通常略低於HNSW。

  • 向量儲存最佳化:向量資料,尤其是高維向量,會佔用大量記憶體和儲存空間。PolarSearch提供多種最佳化技術來降低資源消耗。

    • 向量量化:通過降低向量數值的精度來壓縮資料,顯著減少空間佔用,是一種在壓縮率和精度之間取得平衡的技術。PolarSearch支援乘積量化(PQ)、標量量化(SQ)和二值量化(BQ)。

    • 基於磁碟的儲存:對於低記憶體環境,允許將部分索引資料存放區在磁碟上,以較低的記憶體成本運行向量檢索服務,代價是會適當增加查詢延遲。

注意事項

在使用PolarSearch向量檢索功能時,請您注意以下幾點:

  • 索引訓練要求IVF索引和PQ(乘積量化)技術在使用前需要一個獨立的訓練步驟。您需要提供一部分具有代表性的向量資料來訓練模型,否則索引無法正常工作。

  • 記憶體開銷HNSW索引雖然效能優異,但其圖結構需要完全載入到記憶體中,會產生較高的記憶體開銷。請在選擇前評估您的叢集記憶體資源。

  • 效能與成本權衡基於磁碟的向量搜尋會適當增加查詢延遲,請根據您的業務情境進行評估。

  • 自動訓練:二值量化(BQ)的訓練過程在索引構建期間自動處理,您無需進行額外的訓練操作。

操作指南

準備工作

要使用REST API進行向量檢索,您需要先開啟智能搜尋(PolarSearch)功能。請參考PolarSearch使用說明為已有叢集或建立叢集開啟PolarSearch功能。

步驟一:建立向量索引

要儲存和搜尋向量,您需首先建立一個特定配置的索引。這主要包含兩個關鍵動作:

  1. 啟用k-NN並定義向量欄位:在索引的設定(settings)中將knn參數設為true。這是一個主開關,它告知PolarDB該索引將用於向量檢索。

    核心參數

    • engine:固定為faiss。

      說明

      Faiss (Facebook AI Similarity Search) 是一個由Meta AI開發的高效能開源庫,專門用於高效的相似性搜尋和海量向量資料聚類,PolarSearch使用Faiss作為其核心向量檢索引擎。

    • dimension:用於指定向量的維度,需要與您模型產出的向量維度完全一致。

    • data_type:定義向量的資料類型。預設為float,您也可以選擇bytebinary以最佳化儲存。

    • space_type:定義向量相似性的計算方式(距離度量)。支援的範圍如下:

      space_type

      距離度量

      說明

      l2

      L2(歐幾裡得距離)

      計算平方差和的平方根,對數值大小敏感。

      l1

      L1(曼哈頓距離)

      對向量各維度差值的絕對值求和。

      cosinesimil

      餘弦相似性

      測量向量間的夾角,更關注方向而非大小。

      innerproduct

      內積

      計算向量點積,常用於排序情境。

      hamming

      漢明距離

      計算二進位向量中不同元素的數量。

      chebyshev

      L∞(切比雪夫距離)

      僅考慮向量各維度差值絕對值的最大值。

  2. 定義向量欄位(HNSW或IVF):在索引的映射(mappings)中,需定義一個knn_vector類型的欄位。這個欄位專門用於儲存向量資料,並在此處配置向量的維度、相似性計算方式以及核心的索引方法。

    選型建議

    HNSW和IVF在效能、資源消耗和精度上各有側重,適用於不同的業務情境。您可以參考下表進行快速選型:

    對比維度

    HNSW

    IVF

    查詢延遲

    極低。通過層級化的圖結構快速定位,搜尋路徑短。

    較低。需要先定位到簇,再在簇內搜尋,路徑相對較長。

    召回率(精度)

    高。圖的串連性更好,不容易漏掉近鄰點。

    中到高。存在邊緣效應(查詢點在簇的邊界),可能損失一定精度,可通過調整nprobes參數緩解。

    記憶體佔用

    高。需要將完整的圖結構載入到記憶體中。

    低。主要儲存聚類中心和倒排列表,記憶體開銷遠低於HNSW。

    構建時間

    較長。構建高品質的圖結構需要複雜的計算。

    較快。但需要一個額外的訓練步驟來產生聚類中心。

    適用情境

    對查詢效能和精度有極致要求,且記憶體資源充足的情境。例如:即時語義搜尋、Face Service。

    資料集規模巨大,記憶體資源受限,且可以接受微小精度損失的成本敏感型情境。例如:海量商品推薦、大規模圖片庫檢索。

使用樣本

HNSW

HNSW通過IndexHNSWFlat實現,適用於對效能和召回率有高要求的情境。

核心參數

參數

取值範圍

說明

m

正整數。

圖中每個節點的最大鄰居(出度)數量。此值決定了圖的密度,且是影響索引品質和記憶體佔用的最關鍵參數。

  • 值越大:圖的串連性越好,搜尋路徑更優,召回率更高。但同時索引構建更慢,記憶體佔用也越大。

  • 值越小:構建速度快,記憶體佔用小。但可能導致搜尋過早陷入局部最優,影響召回率。

  • 實踐建議:通常建議取值為8到64之間。可以從16或32開始嘗試,根據召回率和記憶體佔用的測試結果進行調整。

ef_construction

正整數,且通常應大於m

構建索引時,動態鄰居列表的大小。它控制了構建圖期間的搜尋深度和廣度。此值主要影響索引的構建時間和最終品質。

  • 值越大:在插入新節點時能探索更多的潛在鄰居,構建出的圖品質更高(有利於召回率),但構建時間會顯著增加。

  • 實踐建議:通常建議設定為m的2倍或更高。如果對構建時間不敏感但追求高品質索引,可以設定為500或更高。

ef_search

正整數。

查詢時,動態鄰居列表的大小。它控制了查詢期間的搜尋深度。

說明

此參數不在建立索引時指定,而是在查詢時或在索引的settings中全域設定。它是影響查詢延遲和召回率的直接因素。

  • 值越大:查詢時會探索更多的節點,召回率更高,但查詢耗時也越長。

  • 實踐建議:此值沒有固定選型建議,需要通過業務壓測找到延遲和召回率的最佳平衡點。可以從一個較小的值(如50或100)開始,逐步增加並觀察效能變化。

說明

實際建立HNSW索引時,請將下述<my-index>替換為您的索引名稱,<my_vector_field>替換為您的欄位名稱。同時,其他核心參數dimensiondata_typespace_typem以及ef_construction等參數請根據實際業務需求配置。

REST API

// HNSW索引建立樣本,請將<my-index>替換為您的實際的索引名稱
PUT /<my-index>
{
  "settings": {
    "index": { 
      "knn": true 
    } 
  },
  "mappings": {
    "properties": {
      "<my_vector_field>": {//請將<my_vector_field>替換為您的實際的欄位名稱
        "type": "knn_vector",
        "dimension": 128,
        "data_type": "float",
        "method": {
          "name": "hnsw",
          "engine": "faiss",
          "space_type": "l2",
          "parameters": {
            "m": 16,
            "ef_construction": 256
          }
        }
      }
    }
  }
}

Java client

private static void createVectorIndex(OpenSearchClient client) throws IOException {
    Property vectorProperty = Property.of(p -> p.knnVector(
        KnnVectorProperty.of(kvp -> kvp
            .dimension(128)
            .dataType("float")
            .method(new KnnVectorMethod.Builder()
                .name("hnsw")
                .engine("faiss")
                .spaceType("l2")
                .parameters(Map.of(
                    "m", JsonData.of(16),
                    "ef_construction", JsonData.of(256)
                ))
                .build()
            )
        )
    ));
    
    TypeMapping mapping = TypeMapping.of(m -> m
        .properties("<my_vector_field>", vectorProperty)
        .properties("text", Property.of(p -> p.text(TextProperty.of(t -> t))))
        .properties("category", Property.of(p -> p.keyword(k -> k)))
    );
    
    CreateIndexRequest request = new CreateIndexRequest.Builder()
        .index(<my-index>)
        .settings(s -> s.knn(true))
        .mappings(mapping)
        .build();
        
    client.indices().create(request);
}

IVF

IVF通過IndexIVFFlat實現,適用於記憶體受限的超大規模資料集情境。

核心參數

參數

取值範圍

說明

nlist

正整數。

聚類中心的數量。索引會將整個向量空間劃分為nlist個地區(簇)。此值是影響IVF效能的基礎。

  • 值越大:劃分的地區越精細,每個簇包含的向量越少,查詢時需要掃描的資料量更少,速度更快。但可能增加邊緣效應,導致召回率下降,同時記憶體佔用也會增加。

  • 值越小:每個簇包含的向量多,搜尋速度慢。但召回率可能更高。

  • 實踐建議:一個常見的經驗法則是將nlist設定在4 * sqrt(N)16 * sqrt(N)之間,其中N是向量總數。例如,對於100萬個向量,sqrt(N) = 1000,那麼nlist可以考慮設定為4000到16000之間。通常從1024或4096開始是一個不錯的起點。

nprobes

正整數,且通常應小於nlist

查詢時,需要搜尋的聚類中心(簇)的數量。此值是在查詢速度和召回率之間進行權衡的最直接參數。

  • 值越大:查詢時會訪問更多的簇,搜尋範圍更廣,能有效緩解邊緣效應,召回率更高;但查詢速度會線性下降。

  • 值越小:查詢速度快,但如果查詢向量恰好落在多個簇的邊界,很可能因為搜尋範圍不夠大而找不到最近鄰,導致召回率低。

  • 實踐建議:通常從一個較小的值開始,如10或20,然後根據對召回率的要求逐步增加,直到找到可接受的效能平衡點。

說明

實際建立IVF索引時,請將下述<my-index>替換為您的索引名稱,<my_vector_field>替換為您的欄位名稱。同時,其他核心參數dimensiondata_typespace_typenlistnprobes等參數請根據實際業務需求配置。

// IVF索引建立樣本,請將<my-index>替換為您的實際的索引名稱
PUT /<my-index>
{
  "settings": {
    "index": { 
      "knn": true 
    } 
  },
  "mappings": {
    "properties": {
      "<my_vector_field>": {// 請將<my_vector_field>替換為您的實際的欄位名稱
        "type": "knn_vector",
        "dimension": 4,
        "data_type": "byte",
        "method": {
          "name": "ivf",
          "engine": "faiss",
          "space_type": "l2",
          "parameters": {
            "nlist": 1024,
            "nprobes": 10 // nprobes通常在查詢時指定,此處為樣本
          }
        }
      }
    }
  }
}

步驟二:索引向量資料

準備好您的文檔,包括向量資料及其他中繼資料,然後將其索引到您剛剛建立的索引中。

REST API

POST /_bulk
{ "index": { "_index": "my-index", "_id": "doc_1" } }
{ "my_vector_field": [5.2, 4.4] }
{ "index": { "_index": "my-index", "_id": "doc_2" } }
{ "my_vector_field": [5.2, 3.9] }
{ "index": { "_index": "my-index", "_id": "doc_3" } }
{ "my_vector_field": [4.9, 3.4] }
{ "index": { "_index": "my-index", "_id": "doc_4" } }
{ "my_vector_field": [4.2, 4.6] }
{ "index": { "_index": "my-index", "_id": "doc_5" } }
{ "my_vector_field": [3.3, 4.5] }

Java client

private static void indexSampleData(OpenSearchClient client) throws IOException {
    List<Map<String, Object>> documents = new ArrayList<>();
    documents.add(Map.of("text", "a book about data science", "category", "books", "<my_vector_field>", List.of(1.0f, 2.0f, 3.0f, 4.0f)));
    documents.add(Map.of("text", "an intelligent smartphone with a great camera", "category", "electronics", "<my_vector_field>", List.of(8.0f, 7.0f, 6.0f, 5.0f)));
    documents.add(Map.of("text", "a technical manual for a smart device", "category", "electronics", "<my_vector_field>", List.of(3.0f, 4.0f, 5.0f, 6.0f)));

    for (int i = 0; i < documents.size(); i++) {
        IndexRequest<Map<String, Object>> request = new IndexRequest.Builder<Map<String, Object>>()
            .index(<my-index>)
            .id("doc_" + i)
            .document(documents.get(i))
            .build();
        client.index(request);
    }
}

步驟三:執行向量檢索

現在您可以發起向量檢索請求,從海量資料中找出與查詢向量最相似的結果。

基本k-NN搜尋

這是最基礎的向量搜尋,它會在整個索引中找出與查詢向量距離最近的K個結果。

REST API

POST /<my-index>/_search
{
  "size": 3,
  "query": {
    "knn": {
      "<my_vector_field>": {
        "vector": [3.1, 4.1, 5.1, 6.1],
        "k": 3
      }
    }
  }
}

Java client

// 準備您的查詢向量
List<Float> queryVector = List.of(3.1f, 4.1f, 5.1f, 6.1f);

private static void performBasicKnnSearch(OpenSearchClient client, List<Float> queryVector) throws IOException {
    System.out.println("\n--- 1. Performing Basic k-NN Search ---");
    System.out.println("Querying for vectors most similar to: " + queryVector);
    // 尋找最相似的3個結果
    int k = 3;

    KnnQuery knnQuery = new KnnQuery.Builder()
        .field("<my_vector_field>")
        .vector(queryVector)
        .k(k)
        .build();

    SearchRequest searchRequest = new SearchRequest.Builder().index(<my-index>).query(new Query.Builder().knn(knnQuery).build()).size(k).build();
    SearchResponse<Map> response = client.search(searchRequest, Map.class);

    printResults(response);
}

帶過濾條件的k-NN搜尋(混合搜尋)

在許多情境下,您需要在執行向量搜尋前,先通過一個或多個條件縮小搜尋範圍。這正是混合搜尋的核心思想。您可以在KnnQuery中使用filter子句來實現這一點,過濾器本身可以是任何標準的OpenSearch查詢,如term(精確值匹配)或match(全文檢索索引)。

使用文本匹配過濾

這適用於經典的“關鍵字+向量”混合搜尋情境。例如,先搜尋所有描述中包含“新款手機”的文檔,然後根據向量相似性對它們進行排序。

REST API

POST /<my-index>/_search
{
  "size": 3,
  "query": {
    "knn": {
      "<my_vector_field>": {
        "vector": [3.1, 4.1, 5.1, 6.1],
        "k": 3,
        "filter": {
          "match": {
            "text": "book"
          }
        }
      }
    }
  }
}

Java client

// 準備您的查詢向量
List<Float> queryVector = List.of(3.1f, 4.1f, 5.1f, 6.1f);

private static void performHybridSearchWithText(OpenSearchClient client, List<Float> queryVector) throws IOException {
    System.out.println("\n--- 2. Performing Hybrid Search (k-NN + Text Match) ---");
    // 準備您的查詢關鍵字
    String textQuery = "book";
    System.out.println("Filtering for documents containing '" + textQuery + "', then finding most similar vectors.");
    // 尋找最相似的3個結果
    int k = 3;

    MatchQuery matchQuery = new MatchQuery.Builder().field("text").query(q -> q.stringValue(textQuery)).build();

    KnnQuery hybridKnnQuery = new KnnQuery.Builder()
        .field("<my_vector_field>")
        .vector(queryVector)
        .k(k)
        .filter(new Query.Builder().match(matchQuery).build())
        .build();
    
    SearchRequest searchRequest = new SearchRequest.Builder().index(<my-index>).query(new Query.Builder().knn(hybridKnnQuery).build()).size(k).build();
    SearchResponse<Map> response = client.search(searchRequest, Map.class);
    
    printResults(response);
}

使用精確值過濾(Term Filter)

這適用於根據明確的標籤、分類或ID進行過濾的情境。例如,只在“電子產品”類別中搜尋最相似的商品。

REST API

POST /<my-index>/_search
{
  "size": 3,
  "query": {
    "knn": {
      "<my_vector_field>": {
        "vector": [5, 4],
        "k": 3,
        "filter": {
          "term": {
            "category": "electronics"
          }
        }
      }
    }
  }
}

Java client

// 準備您的查詢向量
List<Float> queryVector = List.of(3.1f, 4.1f, 5.1f, 6.1f);

private static void performFilteredSearchWithTerm(OpenSearchClient client, List<Float> queryVector) throws IOException {
    System.out.println("\n--- 3. Performing Filtered Search (k-NN + Term Filter) ---");
    // 準備您的查詢分類
    String categoryFilter = "electronics";
    System.out.println("Filtering for documents in category '" + categoryFilter + "', then finding most similar vectors.");
    // 尋找最相似的3個結果
    int k = 3;

    TermQuery termQuery = new TermQuery.Builder().field("category").value(v -> v.stringValue(categoryFilter)).build();
    
    KnnQuery filteredKnnQuery = new KnnQuery.Builder()
        .field("<my_vector_field>")
        .vector(queryVector)
        .k(k)
        .filter(new Query.Builder().term(termQuery).build())
        .build();
    
    SearchRequest searchRequest = new SearchRequest.Builder().index(<my-index>).query(new Query.Builder().knn(filteredKnnQuery).build()).size(k).build();
    SearchResponse<Map> response = client.search(searchRequest, Map.class);

    printResults(response);
}

配置儲存最佳化

向量資料,尤其是高維浮點型向量,會佔用大量記憶體。PolarSearch提供多種儲存最佳化技術,通過對向量進行壓縮(量化)或改變儲存介質,在記憶體成本、查詢效能和搜尋精度之間取得平衡。

選型建議

在選擇具體的最佳化策略前,您可以參考下表,快速找到最適合您業務情境的方案。

最佳化策略

壓縮率

精度影響

訓練要求

CPU開銷

適用情境

標量量化 (SQ)

低 (固定2倍)

極小

無需訓練

對搜尋精度要求極高,希望在幾乎不損失精度的前提下,獲得適度記憶體最佳化的情境。

二值量化 (BQ)

高 (8-32倍)

較大

無需訓練

中等

對記憶體極度敏感,可以接受一定(甚至較大)精度損失,以換取最大程度記憶體節省的情境。

乘積量化 (PQ)

最高

中等

需要訓練

中等

資料集巨大,需要極致的壓縮率,且願意投入時間進行模型訓練以平衡精度和記憶體的情境。

基於磁碟的向量儲存

-

較大

無需訓練

較高

記憶體資源極其有限,寧願犧牲查詢延遲(因磁碟I/O),也要將記憶體佔用降至最低的成本敏感型情境。

操作說明

標量量化(SQ)

  • 工作原理:將標準的32位浮點(float)向量轉換為16位浮點(fp16)向量進行儲存,使記憶體佔用直接減半。在計算距離時,會解碼回32位進行,因此對精度的影響非常小。

  • 記憶體估算

    • 公式:記憶體 (GB) ≈ 1.1 * (2 * dimension + 8 * m) * num_vectors / 1024^3

    • 參數詳解:

      • dimension:向量的維度。

      • m:HNSW索引中的m參數,即每個節點的最大鄰居數。

      • num_vectors:向量總數。

      • 1.1:約10%的系統開銷係數。

    • 樣本:假設您有 100 萬個向量,每個向量的維度為 256,每個向量的維度m為 16。記憶體需求可以估算如下:1.1 * (2 * 256 + 8 * 16) * 1,000,000 ~= 0.656 GB

  • 使用樣本

    // HNSW + 標量量化(SQ)樣本
    PUT /<my-sq-index>
    {
      "settings": {
        "index": { 
          "knn": true 
        }
      },
      "mappings": {
        "properties": {
          "<my_vector_field>": {
            "type": "knn_vector",
            "dimension": 128,
            "method": {
              "name": "hnsw",
              "engine": "faiss",
              "parameters": {
                "m": 16,
                "ef_construction": 256,
                "encoder": {// 啟用SQ
                  "name": "fp16"
                 } 
              }
            }
          }
        }
      }
    }
    

二值量化(BQ)

  • 工作原理:將浮點向量的每個維度壓縮為二進位位(0和1)進行儲存,從而實現極高的壓縮率。訓練過程在索引構建時自動完成。

  • 記憶體估算

    • 公式:記憶體 (GB) ≈ 1.1 * ((dimension * bits / 8) + 8 * m) * num_vectors / 1024^3

    • 參數詳解:

      • dimension:向量的維度。

      • bits:每個維度用多少個二進位位表示,可選值為1,2,4。bits越小,壓縮率越高,但精度損失越大。

      • m:HNSW索引中的m參數。

      • num_vectors:向量總數。

    • 樣本:假設您有100萬個向量,每個向量的維度為256,每個向量的維度m為16。以下部分提供了各種壓縮值對記憶體需求的估算。

      • 1位量化(32倍壓縮):在1位量化中,每個維度用1位表示,相當於32倍壓縮係數。記憶體需求可以估算如下:1.1 * ((256 * 1 / 8) + 8 * 16) * 1,000,000 ~= 0.176 GB

      • 2位量化(16倍壓縮):在2位量化中,每個維度用2位表示,相當於16倍壓縮係數。記憶體需求可以估算如下:1.1 * ((256 * 2 / 8) + 8 * 16) * 1,000,000 ~= 0.211 GB

  • 使用樣本

    // HNSW + 二值量化(BQ)樣本
    PUT /<my-bq-index>
    {
      "settings" : { 
        "index": { 
          "knn": true 
        } 
      },
      "mappings": {
        "properties": {
          "<my_vector_field>": {
            "type": "knn_vector",
            "dimension": 128,
            "method": {
                "name": "hnsw",
                "engine": "faiss",
                "parameters": {
                  "m": 16,
                  "ef_construction": 512,
                  "encoder": {
                    "name": "binary",
                    "parameters": {// 啟用BQ,使用1位量化
                      "bits": 1 
                    }
                  }
                }
            }
          }
        }
      }
    }
    

乘積量化(PQ)

PQ是一種先進的向量壓縮技術,它能實現比SQ或BQ更高的壓縮率,但代價是需要一個獨立的訓練步驟來構建壓縮模型。

  • 工作原理

    1. 向量切分:首先,將一個原始的高維向量(例如256維)切分為m個等長的低維子向量。例如,將256維向量按m=32切分,會得到32個8維的子向量。

    2. 碼本訓練:接著,系統會為每一個子向量空間獨立學習一個“碼本”(Codebook)。這個碼本包含2^code_size個中心點(也稱質心)。這個訓練過程通常使用K-均值聚類演算法完成。

    3. 量化編碼:訓練完成後,在對新向量進行編碼時,其每個子向量不再儲存原始的浮點值,而是被替換為該子向量空間碼本中距離它最近的那個中心點的ID。如果code_size為8,則ID範圍是0-255,正好用1個位元組儲存。

    4. 最終結果:一個原始向量就被轉換成了一組中心點ID的序列,從而實現了極高的壓縮。

  • 訓練要求:PQ的效能嚴重依賴於訓練資料的品質。您必須提供一組與您最終要檢索的資料分布相似的向量來進行訓練。

    • 訓練資料來源:可以是您要索引的向量資料的子集。

    • 建議的訓練資料量:

      • 結合HNSW使用時:建議訓練向量數量為 2^code_size * 1000

      • 結合IVF使用時:建議訓練向量數量為 max(1000 * nlist, 2^code_size * 1000)

  • 記憶體估算:以HNSW+PQ為例。因為當HNSW與PQ結合使用時,其記憶體計算公式較為複雜,因為它包含了壓縮向量、HNSW圖結構和PQ碼本三部分的開銷。

    • 公式:記憶體 (位元組) ≈ 1.1 * ( (per_vector_cost) * num_vectors + (codebook_cost) )

      • per_vector_cost = (pq_code_size / 8 * pq_m) + 24 + (8 * hnsw_m)

      • codebook_cost = num_segments * (2^pq_code_size) * 4 * dimension

    • 參數詳解:

      • num_vectors:向量總數。

      • dimension:原始向量的維度。

      • pq_m:向量切分的段數。dimension必須能被pq_m整除。

      • pq_code_size:每個子向量碼本的大小,以位元為單位。通常為8。

      • hnsw_m:HNSW索引中的m參數,即每個節點的最大鄰居數。

      • num_segments:一個底層技術參數,代表索引被分成的段數。在估算時可以按叢集分區數或一個保守值(如100)來計算。

      • 1.1:約10%的系統開銷係數。

      • 248:HNSW圖結構中每個節點的固定開銷和指標開銷。

      • 4:代表碼本中的中心點座標使用32位浮點數(4位元組)儲存。

    • 樣本:假設您有100萬個向量(num_vectors),每個向量的維度(dimension)為256,每個向量切分段數(pq_m)為32,每個子向量碼本的大小(pq_code_size)為8,HNSW索引的m參數為16,num_segments為100。

      1. 計算單個向量的開銷(per_vector_cost):

        1. 壓縮後向量大小 = pq_code_size / 8 * pq_m = 8 / 8 * 32 = 32位元組。

        2. HNSW圖開銷 = 24 + 8 * hnsw_m = 24 + 8 * 16 = 152位元組。

        3. per_vector_cost = 32 + 152 = 184位元組

      2. 計算碼本的總開銷(codebook_cost):

        1. codebook_cost = num_segments * (2^pq_code_size) * 4 * dimension。

        2. codebook_cost = 100 * (2^8) * 4 * 256 = 100 * 256 * 4 * 256 = 26,214,400位元組。

      3. 計算總記憶體:

        1. 總記憶體 ≈ 1.1 * (per_vector_cost * num_vectors + codebook_cost)

        2. 總記憶體 ≈ 1.1 * (184 * 1,000,000 + 26,214,400) ≈ 231,235,840 位元組 ≈ 0.215 GB

  • 使用樣本

    // HNSW + 乘積量化(PQ)樣本
    PUT /<my-hnswpq-index>
    {
      "settings" : { 
        "index": { 
          "knn": true 
        } 
      },
      "mappings": {
        "properties": {
          "<my_vector_field>": {
            "type": "knn_vector",
            "dimension": 128, // 維度必須能被 m 整除
            "method": {
                "name": "hnsw",
                "engine": "faiss",
                "parameters": {
                  "m": 16, // HNSW的m參數
                  "ef_construction": 512,
                  "encoder": {
                    "name": "pq",
                    "parameters": {
                      "m": 4, // PQ的m參數:將128維切為4段32維
                      "code_size": 8
                    }
                  }
                }
            }
          }
        }
      }
    }

基於磁碟的向量儲存

  • 工作原理:基於磁碟的向量搜尋是利用內部的量化技術壓縮向量,並將主要的圖結構儲存在磁碟上,而不是堆記憶體中。這種記憶體最佳化可以大幅節省記憶體,但搜尋延遲會略有增加,同時仍能保持較高的召回率。

  • 記憶體估算:無固定公式。實際實體記憶體佔用由作業系統根據訪問模式動態管理。

  • 使用樣本

    // 基於磁碟儲存的樣本
    PUT /<my-ondisk-index>
    {
      "settings" : { 
        "index": {
           "knn": true 
        } 
      },
      "mappings": {
        "properties": {
          "<my_vector_field>": {
            "type": "knn_vector",
            "dimension": 128,
            "mode": "on_disk" // 啟用基於磁碟的模式
          }
        }
      }
    }
    

附錄:完整範例程式碼

下面是一個完整的Java client範例程式碼,示範了從建立向量索引到執行向量檢索的全過程。

依賴配置(pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>vector-search-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.opensearch.client</groupId>
            <artifactId>opensearch-java</artifactId>
            <version>3.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents.client5</groupId>
            <artifactId>httpclient5</artifactId>
            <version>5.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents.core5</groupId>
            <artifactId>httpcore5</artifactId>
            <version>5.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents.core5</groupId>
            <artifactId>httpcore5-h2</artifactId>
            <version>5.3</version>
        </dependency>
        <!-- Jackson databind is needed by opensearch-java -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.16.1</version>
        </dependency>
    </dependencies>
</project>

樣本程式(VectorSearchDemo.java)

package com.example;

import org.apache.hc.client5.http.auth.AuthScope;
import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder;
import org.apache.hc.core5.http.HttpHost;
import org.opensearch.client.opensearch.OpenSearchClient;
import org.opensearch.client.opensearch._types.mapping.KeywordProperty;
import org.opensearch.client.opensearch._types.mapping.KnnVectorMethod;
import org.opensearch.client.opensearch._types.mapping.KnnVectorProperty;
import org.opensearch.client.opensearch._types.mapping.Property;
import org.opensearch.client.opensearch._types.mapping.TextProperty;
import org.opensearch.client.opensearch._types.mapping.TypeMapping;
import org.opensearch.client.opensearch._types.query_dsl.KnnQuery;
import org.opensearch.client.opensearch._types.query_dsl.MatchQuery;
import org.opensearch.client.opensearch._types.query_dsl.Query;
import org.opensearch.client.opensearch._types.query_dsl.TermQuery;
import org.opensearch.client.opensearch.core.IndexRequest;
import org.opensearch.client.opensearch.core.SearchRequest;
import org.opensearch.client.opensearch.core.SearchResponse;
import org.opensearch.client.opensearch.core.search.Hit;
import org.opensearch.client.opensearch.indices.CreateIndexRequest;
import org.opensearch.client.opensearch.indices.DeleteIndexRequest;
import org.opensearch.client.transport.OpenSearchTransport;
import org.opensearch.client.transport.httpclient5.ApacheHttpClient5TransportBuilder;
import org.opensearch.client.json.JsonData;
import org.opensearch.client.json.jackson.JacksonJsonpMapper;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class VectorSearchDemo {

    private static final String INDEX_NAME = "test-java-demo-full-search";
    private static final String FIELD_NAME = "test-embedding";
    private static final int VECTOR_DIMENSION = 4;

    public static void main(String[] args) throws IOException {
        OpenSearchClient client = createClient("<polarsearch_host>", <polarsearch_port>, "<polarsearch_username>", "<polarsearch_password>");
        System.out.println("Client initialized.");

        deleteIndexIfExists(client);
        createVectorIndex(client);
        System.out.println("Index '" + INDEX_NAME + "' created.");

        indexSampleData(client);
        System.out.println("Sample data indexed.");
        
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        List<Float> queryVector = List.of(3.1f, 4.1f, 5.1f, 6.1f);

        // --- 依次執行三種搜尋 ---
        performBasicKnnSearch(client, queryVector);
        performHybridSearchWithText(client, queryVector);
        performFilteredSearchWithTerm(client, queryVector);
        
        client._transport().close();
        System.out.println("\nClient closed.");
    }

    // 初始化用戶端
    private static OpenSearchClient createClient(String hostName, int port, String username, String password) {
        final var host = new HttpHost("http", hostName, port);
        final var credentialsProvider = new BasicCredentialsProvider();
        credentialsProvider.setCredentials(new AuthScope(host), new UsernamePasswordCredentials(username, password.toCharArray()));
        final var connectionManager = PoolingAsyncClientConnectionManagerBuilder.create().build();
        OpenSearchTransport transport = ApacheHttpClient5TransportBuilder.builder(host)
            .setMapper(new JacksonJsonpMapper())
            .setHttpClientConfigCallback(httpClientBuilder ->
                httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider).setConnectionManager(connectionManager)
            ).build();
        return new OpenSearchClient(transport);
    }

    // 如果索引存在則刪除,便於測試效果。
    private static void deleteIndexIfExists(OpenSearchClient client) throws IOException {
        if (client.indices().exists(r -> r.index(INDEX_NAME)).value()) {
            client.indices().delete(new DeleteIndexRequest.Builder().index(INDEX_NAME).build());
            System.out.println("Index '" + INDEX_NAME + "' deleted.");
        }
    }

    // 建立向量索引
    private static void createVectorIndex(OpenSearchClient client) throws IOException {
        Property vectorProperty = Property.of(p -> p.knnVector(
            KnnVectorProperty.of(kvp -> kvp
                .dimension(VECTOR_DIMENSION)
                .dataType("float")
                .method(new KnnVectorMethod.Builder()
                    .name("hnsw")
                    .engine("faiss")
                    .spaceType("l2")
                    .parameters(Map.of(
                        "m", JsonData.of(16),
                        "ef_construction", JsonData.of(256)
                    ))
                    .build()
                )
            )
        ));
        
        TypeMapping mapping = TypeMapping.of(m -> m
            .properties(FIELD_NAME, vectorProperty)
            .properties("text", Property.of(p -> p.text(TextProperty.of(t -> t))))
            .properties("category", Property.of(p -> p.keyword(k -> k))) // 新增 category 欄位
        );
        
        CreateIndexRequest request = new CreateIndexRequest.Builder()
            .index(INDEX_NAME)
            .settings(s -> s.knn(true))
            .mappings(mapping)
            .build();
            
        client.indices().create(request);
    }

    // 索引向量資料
    private static void indexSampleData(OpenSearchClient client) throws IOException {
        List<Map<String, Object>> documents = new ArrayList<>();
        documents.add(Map.of("text", "a book about data science", "category", "books", FIELD_NAME, List.of(1.0f, 2.0f, 3.0f, 4.0f)));
        documents.add(Map.of("text", "an intelligent smartphone with a great camera", "category", "electronics", FIELD_NAME, List.of(8.0f, 7.0f, 6.0f, 5.0f)));
        documents.add(Map.of("text", "a technical manual for a smart device", "category", "electronics", FIELD_NAME, List.of(3.0f, 4.0f, 5.0f, 6.0f)));

        for (int i = 0; i < documents.size(); i++) {
            IndexRequest<Map<String, Object>> request = new IndexRequest.Builder<Map<String, Object>>()
                .index(INDEX_NAME)
                .id("doc_" + i)
                .document(documents.get(i))
                .build();
            client.index(request);
        }
    }

    /**
     * 樣本 1: 基礎 k-NN 搜尋
     */
    private static void performBasicKnnSearch(OpenSearchClient client, List<Float> queryVector) throws IOException {
        System.out.println("\n--- 1. Performing Basic k-NN Search ---");
        System.out.println("Querying for vectors most similar to: " + queryVector);
        int k = 3;

        KnnQuery knnQuery = new KnnQuery.Builder()
            .field(FIELD_NAME)
            .vector(queryVector)
            .k(k)
            .build();

        SearchRequest searchRequest = new SearchRequest.Builder().index(INDEX_NAME).query(new Query.Builder().knn(knnQuery).build()).size(k).build();
        SearchResponse<Map> response = client.search(searchRequest, Map.class);

        printResults(response);
    }

    /**
     * 樣本 2: 混合搜尋 (k-NN + 文本匹配)
     */
    private static void performHybridSearchWithText(OpenSearchClient client, List<Float> queryVector) throws IOException {
        System.out.println("\n--- 2. Performing Hybrid Search (k-NN + Text Match) ---");
        String textQuery = "book";
        System.out.println("Filtering for documents containing '" + textQuery + "', then finding most similar vectors.");
        int k = 3;

        MatchQuery matchQuery = new MatchQuery.Builder().field("text").query(q -> q.stringValue(textQuery)).build();

        KnnQuery hybridKnnQuery = new KnnQuery.Builder()
            .field(FIELD_NAME)
            .vector(queryVector)
            .k(k)
            .filter(new Query.Builder().match(matchQuery).build())
            .build();
        
        SearchRequest searchRequest = new SearchRequest.Builder().index(INDEX_NAME).query(new Query.Builder().knn(hybridKnnQuery).build()).size(k).build();
        SearchResponse<Map> response = client.search(searchRequest, Map.class);
        
        printResults(response);
    }

    /**
     * 樣本 3: 帶過濾條件的搜尋 (k-NN + 精確值匹配)
     */
    private static void performFilteredSearchWithTerm(OpenSearchClient client, List<Float> queryVector) throws IOException {
        System.out.println("\n--- 3. Performing Filtered Search (k-NN + Term Filter) ---");
        String categoryFilter = "electronics";
        System.out.println("Filtering for documents in category '" + categoryFilter + "', then finding most similar vectors.");
        int k = 3;

        TermQuery termQuery = new TermQuery.Builder().field("category").value(v -> v.stringValue(categoryFilter)).build();
        
        KnnQuery filteredKnnQuery = new KnnQuery.Builder()
            .field(FIELD_NAME)
            .vector(queryVector)
            .k(k)
            .filter(new Query.Builder().term(termQuery).build())
            .build();
        
        SearchRequest searchRequest = new SearchRequest.Builder().index(INDEX_NAME).query(new Query.Builder().knn(filteredKnnQuery).build()).size(k).build();
        SearchResponse<Map> response = client.search(searchRequest, Map.class);

        printResults(response);
    }

    private static void printResults(SearchResponse<Map> response) {
        System.out.println("Search Results:");
        for (Hit<Map> hit : response.hits().hits()) {
            System.out.printf(" - ID: %s, Score: %.4f, Source: %s%n", hit.id(), hit.score(), hit.source());
        }
        if (response.hits().hits().isEmpty()) {
            System.out.println(" - No results found.");
        }
    }
}

運行方式

mvn clean package
mvn exec:java -Dexec.mainClass="com.example.VectorSearchDemo"