Pineconeのデータ管理についてのプラクティス

前回の記事はで、Pineconeの一通りの概念を紹介しました。実際にPineconeでアプリケーションを運用する場合、バッチ処理やイベント処理でデータの管理が必要になるケースが多いと思います。今回は、Pineconeのデータを管理する方法について確認していきたいと思います。

Google Colabの準備

本記事のサンプルコードはGoogle Colabで実行しています。お手元で実行する場合は、以下の準備を行ってください。

!pip install -U pinecone-client
import pinecone

pinecone.init(
    api_key="API_KEY",
    environment="ENVIRONMENT"
)

if 'sample' not in pinecone.list_indexes():
    pinecone.create_index('sample', dimension=1536)

index = pinecone.Index('sample')

idを使ってデータを更新する

ベクトルデータにはテキストや画像など何らかの元になるデータがあると思います。元データとベクトルデータが1:1の関係になる場合、idを使ってデータを更新することができます。

まずは適当なベクトルデータを登録します。

res = index.upsert(
    vectors=[{
        'id': '100',
        'values': [0.0] * 1536,
        'metadata': {
            'content': 'テストです'
        }
    }],
    namespace='blog_example'
)
res
---
{'upserted_count': 1}
res = index.fetch(
    ids=['100'],
    namespace='blog_example'
)
res["vectors"]['100']['metadata']
---
{'content': 'テストです'}

データを更新するには、updateやupsertのAPIにidを指定してデータを更新します。

res = index.upsert(
    vectors=[{
        'id': '100',
        'values': [0.0] * 1536,
        'metadata': {
            'content': '更新します'
        }
    }],
    namespace='blog_example'
)
res
---
{'upserted_count': 1}
res = index.fetch(
    ids=['100'],
    namespace='blog_example'
)
res['vectors']['100']['metadata']
---
{'content': '更新します'}

データを削除するには、delete APIにidを指定してデータを削除します。

res = index.delete(
    ids=['100'],
    namespace='blog_example'
)
res
---
{}
res = index.fetch(
    ids=['100'],
    namespace='blog_example'
)
res
---
{'namespace': 'blog_example', 'vectors': {}}

1:1の関係の場合、特に問題になる点はありません。

metadataを使ってデータを更新する

元データに対して、ベクトルデータが複数個になる場合があります。例えば、テキストデータの場合、文章を一定の長さで分割して、それぞれの分割した文章をベクトル化することがあります。その場合、idはid_N(Nは整数)のような形式にすることが考えられます。

例えば、元データのid: 100に対して、ベクトルデータが3個あるので、100_0, 100_1, 100_2という具合です。

res = index.upsert(
    vectors=[{
        'id': '100_0',
        'values': [0.0] * 1536,
        'metadata': {
            'id': '100',
            'content': '1個目のテキスト'
        }
    },
    {
        'id': '100_1',
        'values': [0.0] * 1536,
        'metadata': {
            'id': '100',
            'content': '2個目のテキスト'
        }
    },
    {
        'id': '100_2',
        'values': [0.0] * 1536,
        'metadata': {
            'id': '100',
            'content': '3個目のテキスト'
        }
    }],
    namespace='blog_example'
)
res
---
{'upserted_count': 3}

このとき、元データ(id: 100)のテキストが更新され、ベクトルデータが2個になった場合を考えてみます。

idベースで更新を行った場合、id: 100_0, 100_1のベクトルデータはupdateやupsertで更新されますが、100_2のベクトルデータは残ってしまうので削除する必要があります。しかしアプリケーションは、元データ(id: 100)に対して何個のベクトルデータがあるか知りません。

そこで、元データのidをベクトルデータのmetadataにセットしておく方法が考えられます。

まずdelete APIを使って、filterオプションにmetadataのidを指定して実行することで、そのidに対応するベクトルデータをすべて削除することができます。

res = index.delete(
    filter={
        "id": {"$eq": "100"}
    },
    namespace='blog_example'
)
res
---
{}

その後、upsert APIでベクトルデータを登録すれば、古いデータを残すことなく、新しいデータを登録することができます。

res = index.upsert(
    vectors=[{
        'id': '100_0',
        'values': [0.0] * 1536,
        'metadata': {
            'id': '100',
            'content': '1個目のテキスト(更新)'
        }
    },
    {
        'id': '100_1',
        'values': [0.0] * 1536,
        'metadata': {
            'id': '100',
            'content': '2個目のテキスト(更新)'
        }
    }],
    namespace='blog_example'
)
res
---
{'upserted_count': 2}
res = index.fetch(
    ids=['100_0', '100_1', '100_2'],
    namespace='blog_example'
)
len(res['vectors'])
---
2

元データが削除されたときも、同様にmetadataにidを指定してdelete APIを実行すれば良いです。

ちなみに、2023-04-27現在、delete APIはfilterオプションのみで実行できますが、query APIはvectorまたはidの指定が必須でfilterオプションのみでの実行ができません。fetch APIもidsの指定のみです。これができると運用する中でデータの確認がしやすくなると思うので、サポートされてほしいなと思いました。

全部更新と部分更新を使い分ける

ベクトルデータを更新するためには、updateやupsertの2つのAPIがあります。updateはベクトルの更新またはmetadataの部分更新、upsertはベクトルとmetadataの全部を上書きします。

ドキュメントを読んでもパフォーマンスの観点ではどちらが良いという記載はありませんでしたが、ベクトルを作るのにコンピューティングリソースがかかるので、ベクトルの元データに変更がない場合は、updateでmetadataのみの更新をするのが良いかと思います。

res = index.update(
    id='100',
    setMetadata={
        'content': 'metadataだけ更新します'
    },
    namespace='blog_example'
)
res
---
{}
res = index.fetch(
    ids=['100'],
    namespace='blog_example'
)
res['vectors']['100']['metadata']
---
{'content': 'metadataだけ更新します'}

バックアップを取る

これは他のデータベースと同様ですが、オペレーションをする前や定期的にバックアップを取ることをおすすめします。バックアップは、IndexをCollectionにすることで、エクスポート・インポートすることができます。

pinecone.create_collection('backup-2023-04-26', 'sample')
pinecone.list_collections()
---
['backup-2023-04-26']

リストアもやってみます。Pineconeのフリープランでは作成できるIndexが1つだけのため、既存のIndexを削除して、CollectionからIndexをリストアしてみます。

pinecone.delete_index('sample')
pinecone.create_index('sample',
                      dimension=1536,
                      source_collection='backup-2023-04-26'
)

Indexがリストアされたことを確認します。

index.describe_index_stats()
---
{'dimension': 1536,
 'index_fullness': 0.0,
 'namespaces': {'blog_example': {'vector_count': 3}},
 'total_vector_count': 3}

おわりに

実際にPineconeのデータ管理を試してみました。一番の趣旨として、データの運用ができるようにmetadataの設計をしましょうということです。参考になれば幸いです。