すべてのプロダクト
Search
ドキュメントセンター

PolarDB:OpenSearch プロトコルを使用したベクトル検索の実行

最終更新日:Nov 10, 2025

PolarSearch のベクトル検索機能を使用すると、REST API を使用して、テキストや画像などの非構造化データに対して効率的な類似検索を実行できます。この機能は、大規模なデータセット内で最も類似した結果を迅速かつ正確に検出し、アプリケーションのインテリジェンスを効果的に向上させます。

特徴

ベクトル検索は、類似検索とも呼ばれ、ベクトル間の距離を比較することで最も類似したデータを見つける技術です。これは、キーワードの完全一致に依存する従来の検索とは根本的に異なります。

中心となる考え方は、テキスト、画像、音声などの非構造化データを、大規模言語モデル (LLM) などのディープラーニングモデルを使用して、ベクトル埋め込みと呼ばれる数値表現に変換することです。これらの多次元ベクトルは、データの深い意味情報を捉えます。

クエリを開始すると、PolarSearch はクエリ内容をベクトルに変換し、k-最近傍 (k-NN) 検索を実行します。このコアアルゴリズムは、データセット内でクエリベクトルに最も距離が近い k 個のベクトルを見つけます。k の値はユーザーが定義します。たとえば、k=5 の場合、PolarSearch は最も類似した 5 つの結果を見つけます。その後、PolarSearch はこれらの k 個の最も類似した結果を返します。

効率的な取得を実現するために、PolarSearch はベクトルインデックスとベクトルストレージの最適化という 2 つのコアコンポーネントに依存しています。

  • ベクトルインデックス: 大規模なデータセットでのフルスケール計算を避けるために、事前にベクトルインデックスを構築する必要があります。インデックスは、ベクトルデータの特徴に基づいてクエリに最適化されたデータ構造を構築します。クエリ中に、インデックスは検索範囲を大幅に絞り込むことができ、これにより取得パフォーマンスが大幅に向上します。PolarSearch は複数のタイプのベクトルインデックスをサポートしています。次のセクションでは、業界で主流の HNSW および IVF インデックスを紹介します。

    • 階層的航行可能小世界 (HNSW): 高いパフォーマンスと高い取得率を提供するグラフベースのインデックスですが、それに応じてメモリオーバーヘッドも高くなります。クエリのレイテンシーが極めて低く、高い精度が要求され、データセットのサイズがメモリ容量内に収まるシナリオに適しています。

    • 転置ファイル (IVF): メモリ使用量が少ないクラスタリングベースの転置インデックスです。メモリが限られている超大規模なデータセットを扱う必要があるシナリオに適していますが、検索精度は通常 HNSW よりもわずかに低くなります。

  • ベクトルストレージの最適化: ベクトルデータ、特に高次元ベクトルは、大量のメモリとストレージスペースを消費します。PolarSearch は、リソース消費を削減するための複数の最適化技術を提供します。

    • ベクトル量子化: この技術は、データ精度を低下させてデータを圧縮し、スペース使用量を大幅に削減します。圧縮率と精度のバランスを取ります。PolarSearch は、積量子化 (PQ)、スカラー量子化 (SQ)、およびバイナリ量子化 (BQ) をサポートしています。

    • ディスクベースのストレージ: 低メモリ環境向けに、一部のインデックスデータをディスクに保存することができます。これにより、ベクトル検索サービスをより低いメモリコストで実行できますが、クエリのレイテンシーはわずかに増加します。

注意

PolarSearch のベクトル検索機能を使用する際は、次の点に注意してください。

  • インデックスのトレーニング要件: IVF インデックスと PQ (積量子化) 技術は、使用前に個別のトレーニングステップが必要です。モデルをトレーニングするために、代表的なベクトルデータのサンプルを提供する必要があります。そうしないと、インデックスは正しく機能しません。

  • メモリオーバーヘッド: HNSW インデックスは優れたパフォーマンスを提供しますが、そのグラフ構造は完全にメモリにロードする必要があり、高いメモリオーバーヘッドが発生します。このオプションを選択する前に、クラスターのメモリリソースを評価する必要があります。

  • パフォーマンスとコストのトレードオフ: ディスクベースのベクトル検索は、クエリのレイテンシーをわずかに増加させます。ビジネスシナリオに基づいて、このトレードオフを評価する必要があります。

  • 自動トレーニング: バイナリ量子化 (BQ) のトレーニングプロセスは、インデックス構築中に自動的に処理されます。追加のトレーニング操作を実行する必要はありません。

ユーザーガイド

準備

ベクトル検索に REST API を使用するには、まずインテリジェント検索 (PolarSearch) 機能を有効にする必要があります。新規または既存のクラスターで PolarSearch 機能を有効にする方法の詳細については、「PolarSearch ユーザーガイド」をご参照ください。

ステップ 1: ベクトルインデックスの作成

ベクトルを保存および検索するには、まず特定の構成でインデックスを作成する必要があります。これには 2 つの主要な操作が含まれます。

  1. k-NN の有効化とベクトルフィールドの定義: インデックスの settings で、knn パラメーターを true に設定します。これは、インデックスがベクトル検索に使用されることを PolarDB に伝えるメインスイッチです。

    コアパラメーター

    • engine: このパラメーターは `faiss` に設定する必要があります。

      説明

      Faiss (Facebook AI Similarity Search) は、Meta AI によって開発された高性能なオープンソースライブラリです。大規模なベクトルデータの効率的な類似検索とクラスタリングのために設計されています。PolarSearch は、Faiss をコアベクトル検索エンジンとして使用しています。

    • dimension: ベクトルのディメンションを指定します。この値は、モデルによって生成されるベクトルのディメンションと完全に一致する必要があります。

    • data_type: ベクトルのデータ型を定義します。デフォルト値は float です。byte または binary を選択してストレージを最適化することもできます。

    • space_type: ベクトル類似度の計算方法を定義します。これは距離メジャーとも呼ばれます。サポートされているオプションは次のとおりです。

      space_type

      距離メジャー

      説明

      l2

      L2 (ユークリッド距離)

      差の二乗和の平方根を計算します。値の大きさに敏感です。

      l1

      L1 (マンハッタン距離)

      ベクトルディメンション間の差の絶対値の合計を計算します。

      cosinesimil

      コサイン類似度

      大きさではなく方向性に焦点を当て、ベクトル間の角度を測定します。

      innerproduct

      内積

      ベクトルのドット積を計算します。ソートによく使用されます。

      hamming

      ハミング距離

      バイナリベクトル内の異なる要素の数を計算します。

      chebyshev

      L∞ (チェビシェフ距離)

      ベクトルディメンション間の差の最大絶対値のみを考慮します。

  2. ベクトルフィールドの定義 (HNSW または IVF): インデックスの mappings で、knn_vector 型のフィールドを定義します。このフィールドは、ベクトルデータを保存するために特別に使用されます。このフィールドでは、ベクトルのディメンション、類似度計算方法、およびコアインデックス方法を構成できます。

    選択ガイド

    HNSW と IVF は、パフォーマンス、リソース消費、および精度において異なる強みを持ち、それぞれ異なるビジネスシナリオに適しています。迅速な選択のために、次の表を参照できます。

    比較

    HNSW

    IVF

    クエリレイテンシー

    極めて低い。HNSW は、短い検索パスを持つ階層的なグラフ構造を通じて結果を迅速に特定します。

    低い。IVF は、まずクラスターを特定し、その中で検索する必要があるため、比較的長いパスになります。

    取得率 (精度)

    高い。グラフの接続性が優れているため、最近傍を見逃す可能性が低くなります。

    中から高。クエリポイントがクラスターの境界にあるエッジ効果により、精度が若干失われる可能性があります。これは nprobes パラメーターを調整することで軽減できます。

    メモリ使用量

    高い。完全なグラフ構造をメモリにロードする必要があります。

    低い。IVF は主に重心と転置インデックスを保存します。メモリオーバーヘッドは HNSW よりもはるかに低いです。

    構築時間

    より長い。高品質なグラフ構造を構築するには、複雑な計算が必要です。

    より速い。ただし、IVF は重心を生成するために追加のトレーニングステップが必要です。

    シナリオ

    クエリパフォーマンスと精度に極端な要件があり、十分なメモリリソースがあるシナリオ。例としては、リアルタイムのセマンティック検索や顔認識などがあります。

    大規模なデータセット、限られたメモリリソース、およびわずかな精度低下を許容できるコスト重視のシナリオ。例としては、大規模な製品推奨や大規模な画像ギャラリーの取得などがあります。

使用例

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_typemef_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 クライアント

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 のパフォーマンスの基本となります。

  • 大きい値: 領域がより細かく分割され、各クラスターに含まれるベクトルが少なくなります。これにより、スキャンするデータが少なくなるため、クエリが高速になります。ただし、エッジ効果が増加し、取得率が低下する可能性があり、メモリ使用量も増加します。

  • 小さい値: 各クラスターに含まれるベクトルが多くなり、検索が遅くなります。ただし、取得率は高くなる可能性があります。

  • 実践的なアドバイス: 一般的な経験則として、nlist4 × sqrt(N) から 16 × sqrt(N) の間に設定します。ここで N はベクトルの総数です。たとえば、100 万個のベクトルの場合、sqrt(N) = 1000 なので、nlist は 4,000 から 16,000 の間に設定できます。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 は通常クエリ時に指定されます。これは単なる例です。
          }
        }
      }
    }
  }
}

ステップ 2: ベクトルデータのインデックス作成

ベクトルデータやその他のメタデータを含むドキュメントを準備し、作成したインデックスにそれらをインデックスします。

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 クライアント

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);
    }
}

ステップ 3: ベクトル検索の実行

これで、ベクトル検索リクエストを送信して、大規模なデータセットからクエリベクトルに最も類似した結果を見つけることができます。

基本的な 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 クライアント

// クエリベクトルを準備します。
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 検索 (ハイブリッド検索)

多くのシナリオでは、ベクトル検索を実行する前に、1 つ以上の条件で検索範囲を絞り込む必要がある場合があります。これがハイブリッド検索の中心的な考え方です。これを行うには、KnnQueryfilter 句を使用できます。フィルター自体は、完全一致のための term やフルテキストインデックスのための match など、任意の標準 OpenSearch クエリにすることができます。

テキスト一致を使用したフィルター

これは、古典的な「キーワード + ベクトル」のハイブリッド検索シナリオに適しています。たとえば、まず説明に「新しいスマートフォン」を含むすべてのドキュメントを検索し、次にそれらをベクトル類似度でソートすることができます。

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 クライアント

// クエリベクトルを準備します。
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 フィルター)

これは、特定のラベル、カテゴリ、または ID でフィルターをかけるシナリオに適しています。たとえば、「electronics」カテゴリ内でのみ最も類似した製品を検索することができます。

REST API

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

Java クライアント

// クエリベクトルを準備します。
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 ビットにデコードして戻されるため、精度への影響は最小限です。

  • メモリ推定:

    • 数式: Memory (GB) ≈ 1.1 × (2 × dimension + 8 × m) × num_vectors / 1024³

    • パラメーターの説明:

      • dimension: ベクトルのディメンション。

      • m: Hierarchical Navigable Small World (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) に圧縮してストレージに保存し、非常に高い圧縮率を実現します。トレーニングプロセスは、インデックスの構築時に自動的に完了します。

  • メモリ推定:

    • 数式: Memory (GB) ≈ 1.1 × ((dimension × bits / 8) + 8 × m) × num_vectors / 1024³

    • パラメーターの説明:

      • 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. コードブックのトレーニング: 次に、システムは各サブベクトル空間に対して個別のコードブックを学習します。このコードブックには 2^code_size 個の重心が含まれます。このトレーニングプロセスは通常、k-means クラスタリングアルゴリズムを使用して実行されます。

    3. 量子化エンコーディング: トレーニング後、新しいベクトルがエンコードされると、その各サブベクトルは置き換えられます。元の浮動小数点値を格納する代わりに、システムはサブベクトルのコードブック内で最も近い重心の ID を格納します。code_size が 8 の場合、ID の範囲は 0 から 255 であり、これは正確に 1 バイトで格納できます。

    4. 最終結果: 元のベクトルは重心 ID のシーケンスに変換されます。このメソッドは非常に高い圧縮率を実現します。

  • トレーニング要件: PQ のパフォーマンスは、トレーニングデータの品質に大きく依存します。最終的に取得するデータと類似したデータ分布を持つトレーニング用のベクトルセットを提供する必要があります。

    • トレーニングデータソース: トレーニングデータは、インデックスを作成する予定のベクトルデータのサブセットにすることができます。

    • 推奨されるトレーニングデータ量:

      • HNSW と併用する場合: 推奨されるトレーニングベクトル数は 2^code_size × 1,000 です。

      • 転置ファイル (IVF) と併用する場合: 推奨されるトレーニングベクトル数は max(1,000 × nlist, 2^code_size × 1,000) です。

  • メモリ推定: HNSW+PQ を例にとります。HNSW と PQ を組み合わせると、圧縮されたベクトル、HNSW グラフ構造、および PQ コードブックのオーバーヘッドが含まれるため、メモリ計算式は複雑になります。

    • 数式: Memory (bytes) ≈ 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: ベクトルがチャンク化されるセグメントの数。dimensionpq_m で割り切れる必要があります。

      • pq_code_size: 各サブベクトルコードブックのサイズ (ビット単位)。一般的な値は 8 です。

      • hnsw_m: HNSW インデックスの m パラメーター。各ノードの最大近傍数です。

      • num_segments: インデックスが分割されるセグメントの数を表す低レベルの技術的パラメーター。推定には、クラスターシャードの数や、100 などの保守的な値を使用できます。

      • 1.1: 約 10% のシステムオーバーヘッドの係数。

      • 248: HNSW グラフ構造における各ノードの固定オーバーヘッドとポインターオーバーヘッド。

      • 4: コードブック内の重心座標が 4 バイトの 32 ビット浮動小数点数として格納されることを表します。

    • 例: 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 ディメンションのベクトルを 32 ディメンションの 4 つのセグメントにチャンク化
                      "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 クライアント の完全なサンプルコードを提供します。このコードは、ベクトルインデックスの作成からベクトル検索の実行までの全手順を示しています。

依存関係の構成 (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 は 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("クライアントが初期化されました。");

        deleteIndexIfExists(client);
        createVectorIndex(client);
        System.out.println("インデックス '" + INDEX_NAME + "' が作成されました。");

        indexSampleData(client);
        System.out.println("サンプルデータがインデックス化されました。");
        
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

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

        // 3 種類の検索を順番に実行します。
        performBasicKnnSearch(client, queryVector);
        performHybridSearchWithText(client, queryVector);
        performFilteredSearchWithTerm(client, queryVector);
        
        client._transport().close();
        System.out.println("\nクライアントがクローズされました。");
    }

    // クライアントを初期化します。
    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_NAME + "' が削除されました。");
        }
    }

    // ベクトルインデックスを作成します。
    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))) // カテゴリフィールドを追加します。
        );
        
        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. 基本的な k-NN 検索を実行 ---");
        System.out.println("最も類似したベクトルをクエリしています: " + 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. ハイブリッド検索 (k-NN + テキスト一致) を実行 ---");
        String textQuery = "book";
        System.out.println("'" + textQuery + "' を含むドキュメントをフィルタリングし、最も類似したベクトルを検索します。");
        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 + term フィルター)
     */
    private static void performFilteredSearchWithTerm(OpenSearchClient client, List<Float> queryVector) throws IOException {
        System.out.println("\n--- 3. フィルター検索 (k-NN + Term フィルター) を実行 ---");
        String categoryFilter = "electronics";
        System.out.println("カテゴリ '" + categoryFilter + "' のドキュメントをフィルタリングし、最も類似したベクトルを検索します。");
        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("検索結果:");
        for (Hit<Map> hit : response.hits().hits()) {
            System.out.printf(" - ID: %s, スコア: %.4f, ソース: %s%n", hit.id(), hit.score(), hit.source());
        }
        if (response.hits().hits().isEmpty()) {
            System.out.println(" - 結果が見つかりませんでした。");
        }
    }
}

仕組み

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