ベクトルデータベース Pinecone の概念を整理する

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

前回の記事ではじめてPineconeを使いました。Pineconeについて、もう少し詳しく知りたいと思ったので、公式ドキュメントを読んで内容をまとめました。基本的には分かりやすい概念が多いのですが、1つだけ難しいなと思った概念がでてきたので、サンプルとともに説明します。

ベクトルデータベースとは

機械学習では、文章、画像、音声、動画などのあらゆるデータを、特徴を抽出したベクトルに変換して扱うことが多いです。ベクトルは、数百から数千の次元の数値として表現されます。ベクトルデータベースは、このような特殊なデータ構造を持つデータを扱うために作られたデータベースです。

ベクトルデータベースを使うことで、ベクトル間の類似性を高速に検索することができます。これによって、文章のセマンティック検索、画像・音声・映像などの類似検索、ランキングやレコメンド、重複検出、異常検出、などに応用することができます。

参考: What is a Vector Database?

Pineconeの概要

Overview より引用させてもらいます。

Pineconeは、高性能なベクトル検索アプリケーションを簡単に構築することができます。シンプルなAPIとインフラストラクチャの煩わしさがない、マネージド型のクラウドネイティブなベクトルデータベースです。

Pineconeには、以下のような特徴があります。

  • Fast: 数十億のアイテムを扱う場合でも、あらゆるスケールで超低遅延のクエリレイテンシーを実現します。
  • Fresh: データの追加、編集、削除を行った際に、インデックスのライブアップデートを取得できます。
  • Filtered: ベクトル検索とメタデータフィルタを組み合わせて、より適切で迅速な結果を得ることができます。
  • Fully managed: 導入、使用、拡張を容易にし、スムーズで安全な運用を維持します。

Pineconeの概念

Pineconeの中で使われる概念をざっくりと説明します。

組織管理

  • Organization: 同じ課金を使用するProjectの集合です。Projectは必ず1つのOrganizationに属しています。Organizationは、属するProjectのメンバーやクオータを管理することができます。
  • Project: ProjectはPodとIndexを管理します。Projectの単位で、利用する環境(クラウドサービスやリージョン)が設定されます。APIキーもProject単位で発行されます。

インデックス管理

  • Pod: PodはIndexを管理するハードウェアの単位です。Indexは1つ以上のPodで管理されます。PodにはTypeとSizeがあります。
    • Type: ストレージ効率重視のs1, パフォーマンス効率重視のp1, p2があります。
    • Size: Podに格納できるデータ量です。容量は、Type × Size の組み合わせで決まります。
  • Index: Indexはベクトルデータを管理する単位です。Indexには、ベクトルの次元数、ベクトルの類似性検索に使用するMetric(コサイン類似度、ユークリッド距離、など)、ベクトルのMetadataの型、などが設定されます。
  • Collection: CollectionはIndexのある時点のダンプデータです。データのバックアップや、異なるPodにデータを移動するために使用できます。

データ管理

  • Vector: Indexに格納されるベクトルデータです。
  • Namespace: Index内のVectorの集合をNamespace(名前空間)で分けて管理することができます。Vectorは必ず1つのNamespaceに属しており、デフォルトは空文字です。queryを実行する時にNamespaceを指定することで、特定のNamespaceのVectorのみを検索することができます。
  • Metadata: Vectorに付随するメタ情報です。String, Number, Boolean, List of Stringなどがサポートされています。queryを実行する時にMetadataでフィルタリングすることで、Vectorを効率よく検索することができます。

APIインターフェイス

APIのエンドポイントも公開されていますが、基本的にはPythonかNode.jsのクライアントライブラリを使ってのアクセスになるかと思います。

  • fetch: ベクトルのidを指定して、ベクトルを取得します。
  • upsert: ベクトルを追加または更新します。upsertでの更新は、全体更新になります。
  • update: ベクトルを部分更新します。ベクトルとMedatadaをそれぞれ指定した値に更新できます。
  • delete: ベクトルを削除します。Namespace単位で削除したり、Metadataでフィルタリングして削除することもできます。
  • query: ベクトルを提供して類似するベクトルを取得します。Namespace単位で検索したり、Metadataでフィルタリングして検索することもできます。

他にも、インデックスの作成や削除、インデックスの情報取得などのAPIがありますが、ベクトルの操作に関してはこれくらいです。非常にシンプルであることが分かります。

Sparse-dense embeddings

日本語にすると「疎密埋め込み」です。疎と密とは何を指しているのでしょうか。

ドキュメントによると、「sparse-dense vectorsを使うと、キーワードを意識した意味検索ができます」とあります。

もっと読んでいくと、「Dense vectors(密なベクトル)は、セマンティック検索に適しているベクトルで、Sparse vectors(疎なベクトル)は、キーワード検索に適しているベクトルである」と説明されています。そして、Pineconeは、密な埋め込みと疎な埋め込みを1つのベクトルとして扱うことができると説明されています。

なんとなく、イメージが湧いてきました。これ以上の説明を読んでも難しいことが書かれているので、用意されているサンプルをやってみることにします。

やってみる

今回やってみるサンプルは、Ecommerce Hybrid Searchです。Colabが用意されているので、困ったことに、ここに書くことがあまりありません。PineconeにサインアップしてAPIキーを取得したら誰でも試すことができますので、やってみてください。

実際にやってみた流れを書いていきます。

  • pip installを実行します。(初回実行で依存関係のエラーが出ましたが、もう一度実行すると成功になりました)
  • Pineconeに接続します。PINECONE_API_KEYPINECONE_ENVIRONMENTを設定します。
  • Indexを作ります。Freeプランでは、Indexを1つしか作れないので、すでにある場合は削除しておきます。
  • データをロードします。実際に扱うデータはFashion Product Images (Small)です。
  • データを画像とメタデータに分け、メタデータはpandasのデータフレームにします。
  • メタデータから疎なベクトルを作り、画像から密なベクトルを作ります。
  • 疎なベクトルを作るために、pinecone-textのライブラリからBM25を使用します。
    • bm25.encode_documents で、疎なベクトルを作ります。
    • bm25.encode_queriesで、検索するためのベクトルを作ります。
  • 画像から密なベクトルを作るために、CLIPを使います。512次元のベクトルになります。
  • 2つのmodelの読み込みとチューニングが完了したので、PineconeにデータをUpsertします。データをbatch_size毎に分割し、bm25で作ったsparse_embedsをsparse_valuesに、CLIPで作ったdense_embedsをvaluesに、メタ情報をmetadataにセットします。

コードを抜粋します。

from tqdm.auto import tqdm

batch_size = 200

for i in tqdm(range(0, len(fashion), batch_size)):
    # find end of batch
    i_end = min(i+batch_size, len(fashion))
    # extract metadata batch
    meta_batch = metadata.iloc[i:i_end]
    meta_dict = meta_batch.to_dict(orient="records")
    # concatinate all metadata field except for id and year to form a single string
    meta_batch = [" ".join(x) for x in meta_batch.loc[:, ~meta_batch.columns.isin(['id', 'year'])].values.tolist()]
    # extract image batch
    img_batch = images[i:i_end]
    # create sparse BM25 vectors
    sparse_embeds = bm25.encode_documents()
    # create dense vectors
    dense_embeds = model.encode(img_batch).tolist()
    # create unique IDs
    ids = [str(x) for x in range(i, i_end)]

    upserts = []
    # loop through the data and create dictionaries for uploading documents to pinecone index
    for _id, sparse, dense, meta in zip(ids, sparse_embeds, dense_embeds, meta_dict):
        upserts.append({
            'id': _id,
            'values': dense, # 密なベクトル(筆者コメント)
            'sparse_values': sparse, # 疎なベクトル(筆者コメント)
            'metadata': meta
        })
    # upload the documents to the new hybrid index
    index.upsert(upserts)

# show index description after uploading the documents
index.describe_index_stats()
  • queryもupsertと同じように、密なベクトルと疎なベクトルを作り、2つのベクトルをqueryにセットします。

コードを抜粋します。

query = "dark blue french connection jeans for men"

# create sparse and dense vectors
sparse = bm25.encode_queries(query)
dense = model.encode(query).tolist()
# search
result = index.query(
    top_k=14,
    vector=dense, # 密なベクトル(筆者コメント)
    sparse_vector=sparse, # 疎なベクトル(筆者コメント)
    include_metadata=True
)
# used returned product ids to get images
imgs = [images[int(r["id"])] for r in result["matches"]]
imgs
  • さらに、密なベクトルと疎なベクトルの重みを変えて、検索することができます。密なベクトルの重みを上げると、queryに与えられた「問い」のセマンティックが重視された結果になり、疎なベクトルの重みを上げると、queryに与えられた「問い」のキーワードが重視された結果になることがわかります。

サンプルではさらに色々な条件のqueryを実行しています。本記事では全て取り上げませんので、興味があればぜひ見てみてください。

おわりに

今回は、ベクトルデータベースの Pinecone の概要や概念をまとめました。参考になれば幸いです。

また、「疎密埋め込み」はPineconeの独自機能のようで、これがハマるユースケースにおいてはかなり強力な機能となりそうですね!