Closed6

ベクトルデータベース「Weaviate」を試す 10:CRUD操作

kun432kun432

ベクトルDBは、ベクトルデータ+メタデータを保存するというとてもシンプルなもので、「データベース」という言葉どおりにRDBと比較した場合、明らかに機能的に劣ると感じる。

例えば一般的なデータベースが持っている以下のような機能、

  • ビジネスロジックに合わせた細かいスキーマ設計には対応できない。
  • データソース(テーブル)間でリレーションを張ったりということもできない。
  • アトミックな更新処理などもできない
  • SQLのようなCRUDに長けた言語もない
  • CRUDができるような管理GUI

これらを、ほとんどのベクトルDBは有していないと思う。

ただ、現状のベクトルDBは、RAGのデータソースとして使うケースがほとんどだと思うし、そもそもRDBが必要になるようなユースケースが存在していないという可能性はあると思う。(個人的には使い捨てぐらいでもいいぐらいの気持ちはある。物量にもよるけれどもembeddingの再変換コストは大した金額にならないと思うし。)

とはいえ、例えば、お客様向けRAGサービスとして提供するようなケースで、お客様にデータ操作を委ねる管理画面も提供するような場合を想像すると、多くのケースでは以下のようにベクトルDB以外に管理用のRDBを用意することになるのではないかと思っている。

この構成の課題は2つある。

  • データの所在が、2箇所に分かれてしまうため、エラーが発生した場合でも差分が発生しないような何らかの同期管理が必要になる
  • ベクトルDBへの登録には、ベクトル化プロセスが間に必要になるため、RDBへの登録よりプロセスが増える

Weeaviateの以下のような機能がある。

  • オブジェクトのCRUD操作に対応している
    • これ自体は珍しいことではなく、普通の機能。他のベクトルDBでも当然できる。
  • オブジェクトのフィルタに対応している
    • 柔軟な事前フィルタ機能
    • マルチテナントを使えば、toB等での利用も可能
  • Weaviateはモジュールに対応している
    • 例えばベクトル化モジュールを使うと、データを登録するだけでベクトル化の処理をWeaviate側でやってくれる。
  • Weaviateには、スキーマの概念があり、データ構造を柔軟に管理できる
    • RDBのような管理ができる可能性がある

これらの機能があれば、上に書いたような課題を(全部とは言わないまでも)解決できるのではないか?という期待がある。


色々書いたけど、個人的に重要だと思うのは、

  • ベクトル化モジュールが使えるので、開発者側でデータ登録時にベクトル化を意識する必要がない
  • では更新時は?

というところ。ここがちゃんと動くのであれば、RDBほどではないにせよ、データを一元管理できるのではないかと考えている。

このあたりを念頭に確認していく。

kun432kun432

https://weaviate.io/developers/weaviate/manage-data

https://weaviate.io/developers/weaviate/client-libraries/python

WCSで試す。まずコレクションのスキーマ作成まで。

パッケージインストール

!pip install weaviate-client

クライアント初期化、ベクトル化モジュールを使うのでOpenAI APIキーもセット。

import weaviate
import weaviate.classes as wvc
from google.colab import userdata

client = weaviate.connect_to_wcs(
    cluster_url=userdata.get('WEAVIATE_CLUSTER_URL'),
    auth_credentials=weaviate.auth.AuthApiKey(userdata.get('WEAVIATE_API_KEY')),
    headers={
        "X-OpenAI-Api-Key": userdata.get('OPENAI_API_KEY')
    }
)

スキーマを作成。今回はあとで使うFAQのデータに合わせてスキーマを作成した。ベクトル化モジュールも有効化。

faq = client.collections.create(
    name="FAQ",
    vectorizer_config=wvc.config.Configure.Vectorizer.text2vec_openai(
        vectorize_collection_name=False,
    ),
    properties=[
        wvc.config.Property(
            name="faq_id",
            data_type=wvc.config.DataType.INT,
        ),
        wvc.config.Property(
            name="question",
            data_type=wvc.config.DataType.TEXT,
            skip_vectorization=False,
            vectorize_property_name=False,
            index_searchable=True,
            index_filterable=True,
            tokenization=wvc.config.Tokenization.TRIGRAM
        ),
        wvc.config.Property(
            name="answer",
            data_type=wvc.config.DataType.TEXT,
            skip_vectorization=False,
            vectorize_property_name=False,
            index_searchable=True,
            index_filterable=True,
            tokenization=wvc.config.Tokenization.TRIGRAM
        ),
        wvc.config.Property(
            name="category",
            data_type=wvc.config.DataType.TEXT,
            skip_vectorization=True,
            index_searchable=True,
            index_filterable=True,             
            tokenization=wvc.config.Tokenization.TRIGRAM
        ),
    ]
)

FAQのデータは以下を流用させていただく。

https://linecorp.com/ja/csr/newslist/ja/2020/260

!wget https://d.line-scdn.net/stf/linecorp/ja/csr/dataset_.zip
!!unzip dataset_.zip
import pandas as pd

df = pd.read_excel("dataset_.xlsx")
df.drop(columns=["ID", "カテゴリ2", "出典", "<参考>UMカテゴリタグ", "<参考>UMサービスメニュー\n(標準的な行政サービス名称)"], inplace=True)
df.rename(columns={
    'サンプルID': 'faq_id',
    'サンプル 問い合わせ文': 'question',
    'サンプル 応答文': 'answer',
    'カテゴリ1': 'category',
}, inplace=True)

display(df)

これを辞書データに変換

faq_objs = df.to_dict(orient='records')
print(len(faq_objs))
print(faq_objs[0])
662
{
    'faq_id': 1,
    'question': '母子手帳を受け取りたいのですが、手続きを教えてください。',
    'answer': '窓口で妊娠届をご記入いただき、母子手帳をお渡しします。\n住民票の世帯が別の方が代理で窓口に来られる場合は、委任状が必要になります。\n\n▼詳しくはこちら\n(自治体HP内関連ページのURL)',
    'category': '妊娠・出産'
}

Create

1つのオブジェクトの作成

collection.data.insertで1つのオブジェクトを辞書で渡す。上で作成したデータの1つを流用して登録してみる。

response = faq.data.insert(
    {
        'faq_id': 1,
        'question': '母子手帳を受け取りたいのですが、手続きを教えてください。',
        'answer': '窓口で妊娠届をご記入いただき、母子手帳をお渡しします。\n住民票の世帯が別の方が代理で窓口に来られる場合は、委任状が必要になります。\n\n▼詳しくはこちら\n(自治体HP内関連ページのURL)',
        'category': '妊娠・出産'
    }
)
print(response)

オブジェクトのUUIDが返される。

61d51672-af58-4103-82d5-ffc9f42a2186

複数のオブジェクトの作成

collection.data.insert_manyで複数のオブジェクトを辞書で渡す。上で作成したデータをいくつか流用する。

response = faq.data.insert_many(
    faq_objs[1:5]
)

print(response.all_responses)  # すべてのUUID
print()
print(response.uuids)   # 成功したUUID
print()
print(response.errors)  # 失敗したUUID
print()
print(response.has_errors)  # エラー有無
[UUID('188487de-e45d-43dd-b937-b1067a9e337d'), UUID('a5d9f9b5-af46-4f9c-9d43-4bd038459472'), UUID('4d86a6d9-5bb3-4518-a1e3-59bb9ef58f44'), UUID('ec2914da-9930-40db-8f59-d2c63d120afb')]

{0: UUID('188487de-e45d-43dd-b937-b1067a9e337d'), 1: UUID('a5d9f9b5-af46-4f9c-9d43-4bd038459472'), 2: UUID('4d86a6d9-5bb3-4518-a1e3-59bb9ef58f44'), 3: UUID('ec2914da-9930-40db-8f59-d2c63d120afb')}

{}

False

Read

1つのオブジェクトの読み込み

collection.query.fetch_object_by_idで、指定のオブジェクトIDのデータを1つ取得

response = faq.query.fetch_object_by_id(
    "61d51672-af58-4103-82d5-ffc9f42a2186",
    include_vector=True
)

print(response.uuid)
print(response.properties)
print(response.vector['default'][:3])
61d51672-af58-4103-82d5-ffc9f42a2186
{'answer': '窓口で妊娠届をご記入いただき、母子手帳をお渡しします。\n住民票の世帯が別の方が代理で窓口に来られる場合は、委任状が必要になります。\n\n▼詳しくはこちら\n(自治体HP内関連ページのURL)', 'faq_id': 1, 'question': '母子手帳を受け取りたいのですが、手続きを教えてください。', 'category': '妊娠・出産'}
[-0.004106943495571613, -0.005064225755631924, -0.00622903136536479]

複数のオブジェクトの読み込み

collection.query.fetch_objectsで、複数のオブジェクトを取得。以下のようにソートすることもできる。

from weaviate.collections.classes.grpc import Sort

response = faq.query.fetch_objects(
    include_vector=True,
    sort=Sort.by_property(name="faq_id", ascending=True),
)

for robj in response.objects:
    print(robj.uuid)
    print(robj.properties)
    print(robj.vector['default'][:3])
    print()

前回やったような条件フィルタも使える。

from weaviate.collections.classes.grpc import Sort
import weaviate.classes as wvc

response = faq.query.fetch_objects(
    include_vector=True,
    filters=wvc.query.Filter.by_property("faq_id").equal(2),
)

for robj in response.objects:
    print(robj.uuid)
    print(robj.properties)
    print(robj.vector['default'][:3])
    print()
188487de-e45d-43dd-b937-b1067a9e337d
{'answer': '母子手帳は、○○市役所本庁舎△△階××課窓口、◎◎出張所、………(その他の受け取り場所を適宜記載)………で受け取れます。\n\n▼詳しくはこちら\n(自治体HP内関連ページのURL)', 'faq_id': 2, 'question': '母子手帳の受け取り場所はどこですか?', 'category': '妊娠・出産'}
[-0.004151968751102686, -0.010158279910683632, -0.004003079608082771]

Update

今回一番確認したかったのはこれ。

まず事前に少しベクトル検索を確認してみる。

query="母子手帳の手続きについて教えて"

response = faq.query.near_text(
    query=query,
    limit=5,
    return_metadata=wvc.query.MetadataQuery(certainty=True)
)

for p in response.objects:
    print(f"==== {p.uuid} ====")
    print(f"certainty: {p.metadata.certainty}")
    print(f"faq_id: {p.properties['faq_id']}")
    print(f"question: {p.properties['question']}")
    print("answer: {}...".format(p.properties['answer'].replace('\n','')[:100]))
    print(f"category: {p.properties['category']}")
    print()
==== a5d9f9b5-af46-4f9c-9d43-4bd038459472 ====
certainty: 0.9501070380210876
faq_id: 3
question: 母子手帳はすぐに発行してもらえますか?
answer: 母子手帳は、妊娠届の内容を確認させていただき、その場でお渡しします。▼詳しくはこちら(自治体HP内関連ページのURL)...
category: 妊娠・出産

==== 61d51672-af58-4103-82d5-ffc9f42a2186 ====
certainty: 0.9422510266304016
faq_id: 1
question: 母子手帳を受け取りたいのですが、手続きを教えてください。
answer: 窓口で妊娠届をご記入いただき、母子手帳をお渡しします。住民票の世帯が別の方が代理で窓口に来られる場合は、委任状が必要になります。▼詳しくはこちら(自治体HP内関連ページのURL)...
category: 妊娠・出産

==== 188487de-e45d-43dd-b937-b1067a9e337d ====
certainty: 0.9376183748245239
faq_id: 2
question: 母子手帳の受け取り場所はどこですか?
answer: 母子手帳は、○○市役所本庁舎△△階××課窓口、◎◎出張所、………(その他の受け取り場所を適宜記載)………で受け取れます。▼詳しくはこちら(自治体HP内関連ページのURL)...
category: 妊娠・出産

==== 4d86a6d9-5bb3-4518-a1e3-59bb9ef58f44 ====
certainty: 0.9025362730026245
faq_id: 4
question: 妊婦健診受診票はいつの分から使えますか?
answer: 妊婦健診の受診票は、受診票を受け取った日より後で、病院が妊婦健診と規定した日に利用できます。▼詳しくはこちら(自治体HP内関連ページのURL)...
category: 妊娠・出産

==== ec2914da-9930-40db-8f59-d2c63d120afb ====
certainty: 0.9016791582107544
faq_id: 5
question: 妊婦健診受診票は○○市外で使えますか?
answer: 妊婦健診の受診票は、●●県内の契約医療機関でお使いいただけます。受診希望の病院にお問い合わせください。▼詳しくはこちら(自治体HP内関連ページのURL)...
category: 妊娠・出産

一番上のFAQ ID: 3のデータを見てみる。

object_before = faq.query.fetch_object_by_id(
    "a5d9f9b5-af46-4f9c-9d43-4bd038459472",
    include_vector=True
)

print(f"==== {object_before.uuid} ====")
print(f"faq_id: {object_before.properties['faq_id']}")
print(f"question: {object_before.properties['question']}")
print("answer: {}...".format(object_before.properties['answer'].replace('\n','')[:100]))
print(f"category: {object_before.properties['category']}")
print(f"vector: {object_before.vector['default'][:3]}")
==== a5d9f9b5-af46-4f9c-9d43-4bd038459472 ====
faq_id: 3
question: 母子手帳はすぐに発行してもらえますか?
answer: 母子手帳は、妊娠届の内容を確認させていただき、その場でお渡しします。▼詳しくはこちら(自治体HP内関連ページのURL)...
category: 妊娠・出産
vector: [-0.00463054608553648, -0.0068620541132986546, -0.008939433842897415]

collection.data.updateで質問を少し書き換えてみる。なお、複数のプロパティを同時に変更することもできる。

response = faq.data.update(
    uuid="a5d9f9b5-af46-4f9c-9d43-4bd038459472",
    properties={
        "question": "母子手帳の発行にはどれぐらいの時間がかかりますか?"
    }
)
print(response)

特にレスポンスになにか返ってくるわけではないみたい。

None

再度オブジェクトを見てみる。

object_after = faq.query.fetch_object_by_id(
    "a5d9f9b5-af46-4f9c-9d43-4bd038459472",
    include_vector=True
)

print(f"==== {object_after.uuid} ====")
print(f"faq_id: {object_after.properties['faq_id']}")
print(f"question: {object_after.properties['question']}")
print("answer: {}...".format(object_after.properties['answer'].replace('\n','')[:100]))
print(f"category: {object_after.properties['category']}")
print(f"vector: {object_after.vector['default'][:3]}")

質問が書き換わっていると同時に、ベクトルデータも書き換わっているのがわかる。

==== a5d9f9b5-af46-4f9c-9d43-4bd038459472 ====
faq_id: 3
question: 母子手帳の発行にはどれぐらいの時間がかかりますか?
answer: 母子手帳は、妊娠届の内容を確認させていただき、その場でお渡しします。▼詳しくはこちら(自治体HP内関連ページのURL)...
category: 妊娠・出産
vector: [0.0004760493466164917, -0.005097903776913881, -0.004366291221231222]

再度ベクトル検索してみる。

query="母子手帳の手続きについて教えて"

response = faq.query.near_text(
    query=query,
    limit=5,
    return_metadata=wvc.query.MetadataQuery(certainty=True)
)

for p in response.objects:
    print(f"==== {p.uuid} ====")
    print(f"certainty: {p.metadata.certainty}")
    print(f"faq_id: {p.properties['faq_id']}")
    print(f"question: {p.properties['question']}")
    print("answer: {}...".format(p.properties['answer'].replace('\n','')[:100]))
    print(f"category: {p.properties['category']}")
    print()

今回の場合はランキングには変化ないけども、スコアが少し変わっているのがわかる。

==== a5d9f9b5-af46-4f9c-9d43-4bd038459472 ====
certainty: 0.9473534822463989
faq_id: 3
question: 母子手帳の発行にはどれぐらいの時間がかかりますか?
answer: 母子手帳は、妊娠届の内容を確認させていただき、その場でお渡しします。▼詳しくはこちら(自治体HP内関連ページのURL)...
category: 妊娠・出産

==== 61d51672-af58-4103-82d5-ffc9f42a2186 ====
certainty: 0.9422510266304016
faq_id: 1
question: 母子手帳を受け取りたいのですが、手続きを教えてください。
answer: 窓口で妊娠届をご記入いただき、母子手帳をお渡しします。住民票の世帯が別の方が代理で窓口に来られる場合は、委任状が必要になります。▼詳しくはこちら(自治体HP内関連ページのURL)...
category: 妊娠・出産

==== 188487de-e45d-43dd-b937-b1067a9e337d ====
certainty: 0.9376183748245239
faq_id: 2
question: 母子手帳の受け取り場所はどこですか?
answer: 母子手帳は、○○市役所本庁舎△△階××課窓口、◎◎出張所、………(その他の受け取り場所を適宜記載)………で受け取れます。▼詳しくはこちら(自治体HP内関連ページのURL)...
category: 妊娠・出産

==== 4d86a6d9-5bb3-4518-a1e3-59bb9ef58f44 ====
certainty: 0.9025362730026245
faq_id: 4
question: 妊婦健診受診票はいつの分から使えますか?
answer: 妊婦健診の受診票は、受診票を受け取った日より後で、病院が妊婦健診と規定した日に利用できます。▼詳しくはこちら(自治体HP内関連ページのURL)...
category: 妊娠・出産

==== ec2914da-9930-40db-8f59-d2c63d120afb ====
certainty: 0.9016791582107544
faq_id: 5
question: 妊婦健診受診票は○○市外で使えますか?
answer: 妊婦健診の受診票は、●●県内の契約医療機関でお使いいただけます。受診希望の病院にお問い合わせください。▼詳しくはこちら(自治体HP内関連ページのURL)...
category: 妊娠・出産

複数件数のデータを更新する、というのはちょっと見当たらなかった。

Delete

1件のデータをUUIDで指定して削除

response = faq.data.delete_by_id(
    uuid="a5d9f9b5-af46-4f9c-9d43-4bd038459472",
)
print(response)
True

ベクトル検索してみる。

query="母子手帳の手続きについて教えて"

response = faq.query.near_text(
    query=query,
    limit=5,
    return_metadata=wvc.query.MetadataQuery(certainty=True)
)

for p in response.objects:
    print(f"==== {p.uuid} ====")
    print(f"certainty: {p.metadata.certainty}")
    print(f"faq_id: {p.properties['faq_id']}")
    print(f"question: {p.properties['question']}")
    print("answer: {}...".format(p.properties['answer'].replace('\n','')[:100]))
    print(f"category: {p.properties['category']}")
    print()

当然ながら出てこなくなった。

==== 61d51672-af58-4103-82d5-ffc9f42a2186 ====
certainty: 0.9422510266304016
faq_id: 1
question: 母子手帳を受け取りたいのですが、手続きを教えてください。
answer: 窓口で妊娠届をご記入いただき、母子手帳をお渡しします。住民票の世帯が別の方が代理で窓口に来られる場合は、委任状が必要になります。▼詳しくはこちら(自治体HP内関連ページのURL)...
category: 妊娠・出産

==== 188487de-e45d-43dd-b937-b1067a9e337d ====
certainty: 0.9376183748245239
faq_id: 2
question: 母子手帳の受け取り場所はどこですか?
answer: 母子手帳は、○○市役所本庁舎△△階××課窓口、◎◎出張所、………(その他の受け取り場所を適宜記載)………で受け取れます。▼詳しくはこちら(自治体HP内関連ページのURL)...
category: 妊娠・出産

==== 4d86a6d9-5bb3-4518-a1e3-59bb9ef58f44 ====
certainty: 0.9025362730026245
faq_id: 4
question: 妊婦健診受診票はいつの分から使えますか?
answer: 妊婦健診の受診票は、受診票を受け取った日より後で、病院が妊婦健診と規定した日に利用できます。▼詳しくはこちら(自治体HP内関連ページのURL)...
category: 妊娠・出産

==== ec2914da-9930-40db-8f59-d2c63d120afb ====
certainty: 0.9016791582107544
faq_id: 5
question: 妊婦健診受診票は○○市外で使えますか?
answer: 妊婦健診の受診票は、●●県内の契約医療機関でお使いいただけます。受診希望の病院にお問い合わせください。▼詳しくはこちら(自治体HP内関連ページのURL)...
category: 妊娠・出産

複数同時に削除する場合はcollection.data.delete_manyを使う。条件を指定する場合はここだけwhereになるみたい。

response = faq.data.delete_many(
    where=wvc.query.Filter.by_property("faq_id").less_than(3)
)
print(response)

対象件数と実施件数が返ってくる。

DeleteManyReturn(failed=0, matches=2, objects=None, successful=2)

再度ベクトル検索してみると、削除されているのがわかる。

==== 4d86a6d9-5bb3-4518-a1e3-59bb9ef58f44 ====
certainty: 0.9025362730026245
faq_id: 4
question: 妊婦健診受診票はいつの分から使えますか?
answer: 妊婦健診の受診票は、受診票を受け取った日より後で、病院が妊婦健診と規定した日に利用できます。▼詳しくはこちら(自治体HP内関連ページのURL)...
category: 妊娠・出産

==== ec2914da-9930-40db-8f59-d2c63d120afb ====
certainty: 0.9016791582107544
faq_id: 5
question: 妊婦健診受診票は○○市外で使えますか?
answer: 妊婦健診の受診票は、●●県内の契約医療機関でお使いいただけます。受診希望の病院にお問い合わせください。▼詳しくはこちら(自治体HP内関連ページのURL)...
category: 妊娠・出産
kun432kun432
  • ベクトル化モジュールが使えるので、開発者側でデータ登録時にベクトル化を意識する必要がない
  • では更新時は?

一番確認したかった点が確認できた。RDBほどの機能はないとしても、ベクトル化プロセスを意識する必要なく、WeaviateへのCRUDを行えば良い、というのは、結構な強みではないだろうかと思う。

なお、CRUDできるようなGUIはないので(WCSのGUIでのデータ操作はgraphqlコンソールのみっぽい)、必要な場合は実装することにはなる。

kun432kun432

まとめ

Weaviateについて全10回に分けて色々やってみた。

日本では一番使われているベクトルDBはおそらくPineconeあたりではないかと推測するが、それに比べればWeaviateはマイナーなところかと思う。

これは、スキーマというWeaviate固有の概念が、

  • これによってどうベクトルデータが生成されているのか?
  • スキーマを使えることでどういったメリットがあるのか?

というところがQuickstartだけではわかりにくいのが要因のように思える。

実際、自分も以前ベクトルデータベース選定の際にWeaviateを軽く触っただけではよくわからなくて、今回、日本語トークナイザー対応を機に、ある程度ドキュメントを見て・実際に触ってみて、でやっと少しわかったような感じ。

ただ、いろいろ触ってみると、このスキーマをうまく設計すれば、大きな柔軟性を生みそうな気がするし、

  • モジュール機能を活用してアプリケーションコードをシンプルに
  • 柔軟フィルタとマルチテナント機能
  • モジュールと連動したCRUDで管理の簡素化
  • Multiple vectors/Named vectorsによる、より複雑なベクトルデータ管理
  • 日本語に対応したキーワード検索/ハイブリッド検索による精度改善
  • OSSによる活発な開発と複数の導入形態

といった豊富な機能には期待を持てるのではないかと感じた。

このスクラップは1ヶ月前にクローズされました