Closed6

ベクトルデータベース「Weaviate」を試す 3:スキーマ

kun432kun432

スキーマとは?

スキーマ≒コレクションの定義になる。前回までやった限り、コレクションはオブジェクトを入れておく「箱」という印象。では「コレクションの定義」とはなにか?

データベーススキーマは、Weaviate におけるデータの保存、整理、検索の方法を定義するものです。

データをインポートする前に、スキーマを定義する必要があります。通常、スキーマはできるだけ手動で定義することをお勧めしますが、自動スキーマ機能が有効になっていれば、Weaviateはインポート時にスキーマを推測することもできます。

この時点ではまだピンとこない。実際にコレクションを作って確認してみる。

スキーマ作成の基本

"FAQ"というcollectionを作ってみる。前回・前々回とほぼ同じだけどすこしだけ変えてある。

  • collectionの名前は"FAQ"
  • 4つのプロパティを持つ
    • faq_id
    • question
    • answer
    • category
  • ベクトル化モジュールにtext2vec-openai、テキスト生成モジュールにgenerative-openaiを使う

データセット

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)

faq_objs = df.to_dict(orient='records')
print(len(faq_objs))
print(faq_objs[0])

データセットの中身は、ID・質問・回答・カテゴリの各文字列となっている。

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

コレクションを作成する。

import weaviate
import weaviate.classes as wvc
import os
import requests
import json
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 = client.collections.create(
    name="FAQ",
    vectorizer_config=wvc.config.Configure.Vectorizer.text2vec_openai(),
    generative_config=wvc.config.Configure.Generative.openai(),
)

上記ではコレクション作成時に設定しているのは、

  • コレクション名
  • ベクトル化モジュールの有効化
  • 生成モジュールの有効化

だけ。

まずこの状態でスキーマを見てみる。

from pprint import pprint 

schema = faq.config.get(simple=False)
pprint(schema.to_dict())
{
    'class': 'FAQ',
    'invertedIndexConfig': {
        'bm25': {
            'b': 0.75,
            'k1': 1.2
        },
        'cleanupIntervalSeconds': 60,
        'indexNullState': False,
        'indexPropertyLength': False,
        'indexTimestamps': False,
        'stopwords': {
            'preset': 'en'
        }
    },
    'moduleConfig': {
        'generative-openai': {
        },
        'text2vec-openai': {
            'baseURL': 'https://api.openai.com',
            'model': 'ada',
            'vectorizeClassName': True
        }
    },
    'multiTenancyConfig': {
        'enabled': False
    },
    'properties': [
    ],
    'replicationConfig': {
        'factor': 1
    },
    'shardingConfig': {
        'actualCount': 1,
        'actualVirtualCount': 128,
        'desiredCount': 1,
        'desiredVirtualCount': 128,
        'function': 'murmur3',
        'key': '_id',
        'strategy': 'hash',
        'virtualPerPhysical': 128
    },
    'vectorIndexConfig': {
        'cleanupIntervalSeconds': 300,
        'distanceMetric': 'cosine',
        'dynamicEfFactor': 8,
        'dynamicEfMax': 500,
        'dynamicEfMin': 100,
        'ef': -1,
        'efConstruction': 128,
        'flatSearchCutoff': 40000,
        'maxConnections': 64,
        'skip': False,
        'vectorCacheMaxObjects': 1000000000000
    },
    'vectorIndexType': 'hnsw',
    'vectorizer': 'text2vec-openai'
}

ざっくり以下のような設定が行われているのがわかる。

  • コレクション(クラス)名
  • 転置インデックスの設定
  • ベクトルインデックスの設定
  • モジュールの設定
  • レプリケーションの設定
  • シャーディングの設定
  • マルチテナントの設定

では次にデータを登録する。

faq.data.insert_many(faq_objs)     # オブジェクトをcollectionに追加

再度スキーマを見てみる。

from pprint import pprint 

schema = faq.config.get(simple=False)
pprint(schema.to_dict())
{
    'class': 'FAQ',
    'invertedIndexConfig': {
        'bm25': {
            'b': 0.75,
            'k1': 1.2
        },
        'cleanupIntervalSeconds': 60,
        'indexNullState': False,
        'indexPropertyLength': False,
        'indexTimestamps': False,
        'stopwords': {
            'preset': 'en'
        }
    },
    'moduleConfig': {
        'generative-openai': {
        },
        'text2vec-openai': {
            'baseURL': 'https://api.openai.com',
            'model': 'ada',
            'vectorizeClassName': True
        }
    },
    'multiTenancyConfig': {
        'enabled': False
    },
    'properties': [
        {
            'dataType': [
                'text'
            ],
            'description': "This property was generated by Weaviate's "
                           'auto-schema feature on Sat Mar  9 09:09:59 '
                           '2024',
            'indexFilterable': True,
            'indexVector': True,
            'moduleConfig': {
                'text2vec-openai': {
                    'skip': False,
                    'vectorizePropertyName': False
                }
            },
            'name': 'category',
            'tokenizer': 'word'
        },
        {
            'dataType': [
                'text'
            ],
            'description': "This property was generated by Weaviate's "
                           'auto-schema feature on Sat Mar  9 09:09:59 '
                           '2024',
            'indexFilterable': True,
            'indexVector': True,
            'moduleConfig': {
                'text2vec-openai': {
                    'skip': False,
                    'vectorizePropertyName': False
                }
            },
            'name': 'question',
            'tokenizer': 'word'
        },
        {
            'dataType': [
                'number'
            ],
            'description': "This property was generated by Weaviate's "
                           'auto-schema feature on Sat Mar  9 09:09:59 '
                           '2024',
            'indexFilterable': True,
            'indexVector': False,
            'moduleConfig': {
                'text2vec-openai': {
                    'skip': False,
                    'vectorizePropertyName': False
                }
            },
            'name': 'faq_id',
            'tokenizer': None
        },
        {
            'dataType': [
                'text'
            ],
            'description': "This property was generated by Weaviate's "
                           'auto-schema feature on Sat Mar  9 09:09:59 '
                           '2024',
            'indexFilterable': True,
            'indexVector': True,
            'moduleConfig': {
                'text2vec-openai': {
                    'skip': False,
                    'vectorizePropertyName': False
                }
            },
            'name': 'answer',
            'tokenizer': 'word'
        }
    ],
    'replicationConfig': {
        'factor': 1
    },
    'shardingConfig': {
        'actualCount': 1,
        'actualVirtualCount': 128,
        'desiredCount': 1,
        'desiredVirtualCount': 128,
        'function': 'murmur3',
        'key': '_id',
        'strategy': 'hash',
        'virtualPerPhysical': 128
    },
    'vectorIndexConfig': {
        'cleanupIntervalSeconds': 300,
        'distanceMetric': 'cosine',
        'dynamicEfFactor': 8,
        'dynamicEfMax': 500,
        'dynamicEfMin': 100,
        'ef': -1,
        'efConstruction': 128,
        'flatSearchCutoff': 40000,
        'maxConnections': 64,
        'skip': False,
        'vectorCacheMaxObjects': 1000000000000
    },
    'vectorIndexType': 'hnsw',
    'vectorizer': 'text2vec-openai'
}

先ほどの設定に加えてpropertiesというプロパティに関する設定が追加されているのがわかる。

また、このプロパティは、登録したデータの内容に基づいて作成されており、question/answer/categoryなどのテキストデータは'dataType': ['text']、数値であるfaq_id'dataType': ['number']となっているなど、データの型なども自動で設定されている。

これは、WeaviateのAuto-schemaという機能によるもので、登録したデータの内容に基づいて、プロパティの設定が自動で行われているらしい。

https://weaviate.io/developers/weaviate/config-refs/schema#auto-schema

ただし、ドキュメントには以下の記載がある。

プロダクションでの使用時のコレクションの手動定義

一般的に、本番環境ではauto-schemaを無効にすることをお勧めします。

  • 手動でコレクションを定義すると、より正確な制御が可能になります。
  • インポート時にデータ構造を推測することに関連するパフォーマンス・ペナルティがあります。これは、複雑なネストされたプロパティなど、場合によってはコストのかかる操作になることがあります。

とはいえ、現時点ではまだスキーマのメリットが全然わかっていない、もっと具体的に言うと、どのようなベクトル化が行われているのかを知りたい。

これについては以下のような記載がある。

セマンティックインデックスの設定

Weaviate は、(個々のプロパティではなく)オブジェクトレベルでベクトル埋め込みを生成します。例えば text2vec-* モジュールはテキストオブジェクトからベクトルを生成することができます。各オブジェクトからベクトル化する文字列を生成するために、Weaviate は関連するクラスのスキーマ設定に従います。

スキーマで特に指定されていない限り、デフォルトの動作は次のようになります:

  • textデータ型を使用するプロパティのみをベクトル化(スキップ設定しない限り)
  • 値を連結する前に、プロパティをアルファベット順 (a-z) にソート
  • vectorizePropertyNametrue の場合(デフォルトではfalse)、 プロパティ名を各プロパティ の値の先頭に追加。
  • (プロパティ名が先頭に追加された)プロパティの値をスペースで連結
  • vectorizeClassNamefalseでない限り)クラス名を先頭に追加。
  • 生成された文字列を小文字に変換

例えば、以下のデータオブジェクトの場合は、

Article = {
 summary: "Cows lose their jobs as milk prices drop"、
text: "As his 100 diary cows lumbered over for their Monday..."

以下としてベクトル化されます:

article cows lose their jobs as milk prices drop as his diary cows lumbered over for their monday...

デフォルトでは、collection_nameとすべてのプロパティのvalueが計算に含まれますが、プロパティのnameはインデックスされません。

コレクション単位でベクトル化の動作を設定するには、vectorizeClassName を使用します。

プロパティ単位でベクトル化を設定するには、skipvectorizePropertyName を使用します。

なるほど、ではこれを今回作成したコレクションのデータに当てはめてみる。

collection_name = "FAQ".lower()
properties_str = " ".join([faq_objs[0][key] for key in sorted(faq_objs[0]) if isinstance(faq_objs[0][key], str)]).lower()

query = f"{collection_name} {properties_str}"
print(query)
faq 窓口で妊娠届をご記入いただき、母子手帳をお渡しします。
住民票の世帯が別の方が代理で窓口に来られる場合は、委任状が必要になります。

▼詳しくはこちら
(自治体hp内関連ページのurl) 妊娠・出産 母子手帳を受け取りたいのですが、手続きを教えてください。

これで検索してみる。

response = faq.query.near_text(
    query=query,
    limit=5,
    return_metadata=wvc.query.MetadataQuery(distance=True)  # 類似度の距離をレスポンスに含める
)

for r in response.objects:
    print(r.metadata.distance)
    print(r.properties)
    print()

結果

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

0.036223530769348145
{'answer': '妊娠したら妊娠届を○○課窓口(または支所・出張所窓口)に提出し、母子手帳を受け取ってください。\n\n▼詳しくはこちら\n(自治体HP内関連ページのURL)', 'faq_id': 450.0, 'question': '妊娠したので、必要な手続きを教えてください。', 'category': '妊娠・出産'}

0.05426454544067383
{'answer': '母子手帳は、妊娠届の内容を確認させていただき、その場でお渡しします。\n\n▼詳しくはこちら\n(自治体HP内関連ページのURL)', 'faq_id': 3.0, 'question': '母子手帳はすぐに発行してもらえますか?', 'category': '妊娠・出産'}

0.06013476848602295
{'answer': '母子手帳は、○○市役所本庁舎△△階××課窓口、◎◎出張所、………(その他の受け取り場所を適宜記載)………で受け取れます。\n\n▼詳しくはこちら\n(自治体HP内関連ページのURL)', 'faq_id': 2.0, 'question': '母子手帳の受け取り場所はどこですか?', 'category': '妊娠・出産'}

0.06259500980377197
{'answer': '妊娠した人は、(市役所○○課、保健所、保健相談所等の妊娠届を出せる場所を記載してください。)で妊娠届を出してください。(母子手帳や妊婦健診の受診票など、妊娠した人にお渡しするもの)をさしあげます。なるべく早めの届出をお願いします。\n\n▼詳しくはこちら\n(自治体HP内関連ページのURL)', 'faq_id': 360.0, 'question': '妊娠届について教えてください。', 'category': '妊娠・出産'}

distanceは0に近いほど類似性が高いらしいのだけど、なんかマイナスになってるな・・・イコールならば0になってほしいんだけど、まあ少なくとも上記のクエリはめちゃめちゃ類似度が高いように思える。

ちなみに、質問・回答・カテゴリ、それぞれでクエリしてみた結果も以下に記載しておく。

for key in faq_objs[0]:
    if isinstance(faq_objs[0][key], str):
        print(f"===== {key} =====\n")
        query = faq_objs[0][key]
        print(f"クエリ: {query}")
        print()
        response = faq.query.near_text(
            query=query,
            limit=5,
            return_metadata=wvc.query.MetadataQuery(distance=True)  # 類似度の距離をレスポンスに含める
        )
        for r in response.objects:
            print(r.metadata.distance)
            print(r.properties)
            print()
===== question =====

クエリ: 母子手帳を受け取りたいのですが、手続きを教えてください。

0.10973608493804932
{'answer': '母子手帳は、妊娠届の内容を確認させていただき、その場でお渡しします。\n\n▼詳しくはこちら\n(自治体HP内関連ページのURL)', 'faq_id': 3.0, 'question': '母子手帳はすぐに発行してもらえますか?', 'category': '妊娠・出産'}

0.11005508899688721
{'answer': '母子手帳の申請には診断書はいりませんが、妊娠届に診断を受けた病院名・医師名を記入していただきます。', 'faq_id': 37.0, 'question': '母子手帳の申請には医師の診断書が必要ですか?', 'category': '妊娠・出産'}

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

0.1140519380569458
{'answer': '産前は母子手帳以外の手続きは特にありません。\n産後に、出生の届出や出生通知書の提出、(自治体が行う出産助成等)の申請をお願いします。', 'faq_id': 36.0, 'question': '母子手帳の他に産前に市役所でやるべき手続きはありますか?', 'category': '妊娠・出産'}

0.11453193426132202
{'answer': '母子手帳をなくしたときは、再交付を受けてください。\nお子さんが出生前の母子手帳については、(再交付を受けられる場所)で再交付を受けられます。\nお子さんが出生後の母子手帳については、(再交付を受けられる場所)で受けられます。\n申請の際はご本人確認できるものをお持ちください。\n\n◆お問い合わせ\n(自治体の担当課等の名称)\n(電話番号)/(開庁時間)', 'faq_id': 108.0, 'question': '母子手帳をなくした場合は再発行できますか?', 'category': '妊娠・出産'}

===== answer =====

クエリ: 窓口で妊娠届をご記入いただき、母子手帳をお渡しします。
住民票の世帯が別の方が代理で窓口に来られる場合は、委任状が必要になります。

▼詳しくはこちら
(自治体HP内関連ページのURL)

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

0.052805423736572266
{'answer': '妊娠したら妊娠届を○○課窓口(または支所・出張所窓口)に提出し、母子手帳を受け取ってください。\n\n▼詳しくはこちら\n(自治体HP内関連ページのURL)', 'faq_id': 450.0, 'question': '妊娠したので、必要な手続きを教えてください。', 'category': '妊娠・出産'}

0.07569718360900879
{'answer': '夜間・休日窓口の場合、母子手帳の証明や届書の受理証明書などの発行、 子どもに関する手当・助成の受付はしていませんので、通常窓口で手続き・申請してください。\n\n▼詳しくはこちら\n(自治体HP内関連ページのURL)', 'faq_id': 252.0, 'question': '出生届は夜間でも届出できますか。', 'category': '妊娠・出産'}

0.0770578384399414
{'answer': '妊娠した人は、(市役所○○課、保健所、保健相談所等の妊娠届を出せる場所を記載してください。)で妊娠届を出してください。(母子手帳や妊婦健診の受診票など、妊娠した人にお渡しするもの)をさしあげます。なるべく早めの届出をお願いします。\n\n▼詳しくはこちら\n(自治体HP内関連ページのURL)', 'faq_id': 360.0, 'question': '妊娠届について教えてください。', 'category': '妊娠・出産'}

0.07807260751724243
{'answer': '母子手帳は、妊娠届の内容を確認させていただき、その場でお渡しします。\n\n▼詳しくはこちら\n(自治体HP内関連ページのURL)', 'faq_id': 3.0, 'question': '母子手帳はすぐに発行してもらえますか?', 'category': '妊娠・出産'}

===== category =====

クエリ: 妊娠・出産

0.12727570533752441
{'answer': '○○市では、妊娠した人とパートナーを対象に、これからの子育てや、地域で子育てをするための仲間づくりに役立つように、(母親学級等のイベント)を開催しています。(イベントの内容等を記載してください。)\n\n▼詳しくはこちら\n(自治体HP内関連ページのURL)', 'faq_id': 113.0, 'question': '出産の不安を相談できるところはありますか?', 'category': '妊娠・出産'}

0.12849712371826172
{'answer': '○○市の産前産後のサービスは、(東京都における「母子保健バッグ」等、自治体が妊婦等に対して交付しているもの)をご覧ください。\n出産休暇についてはお勤め先にお問い合わせください。\n出産一時金については、加入している健康保険組合等にお問い合わせください。', 'faq_id': 35.0, 'question': '産前・産後のことについて教えてください。', 'category': '妊娠・出産'}

0.13298261165618896
{'answer': '妊娠した人は、(市役所○○課、保健所、保健相談所等の妊娠届を出せる場所を記載してください。)で妊娠届を出してください。(母子手帳や妊婦健診の受診票など、妊娠した人にお渡しするもの)をさしあげます。なるべく早めの届出をお願いします。\n\n▼詳しくはこちら\n(自治体HP内関連ページのURL)', 'faq_id': 360.0, 'question': '妊娠届について教えてください。', 'category': '妊娠・出産'}

0.13639229536056519
{'answer': '出生通知票とは赤ちゃんが生まれたときに提出いただくものです。\nこんにちは赤ちゃん訪問や乳幼児健診の参考にさせていただきますので、妊娠中や生まれたときに気になったことなど、何か心配なことがあればご記入ください。\n\n◆お問い合わせ\n(自治体の担当課等の名称)\n(電話番号)/(開庁時間)', 'faq_id': 566.0, 'question': '出生通知票について教えてください。', 'category': '妊娠・出産'}

0.13681483268737793
{'answer': '妊娠したら妊娠届を○○課窓口(または支所・出張所窓口)に提出し、母子手帳を受け取ってください。\n\n▼詳しくはこちら\n(自治体HP内関連ページのURL)', 'faq_id': 450.0, 'question': '妊娠したので、必要な手続きを教えてください。', 'category': '妊娠・出産'}

シンプルにクエリのボリューム量が多い「回答」が一番類似度が高いとなっているのがわかるし、distanceみても先ほどのクエリとは距離の差が全然違うのがわかる。

確かにドキュメントの通り、デフォルトだとテキスト型の全プロパティを使ってベクトルデータが作成されている様子。

kun432kun432

ここで改めて、プロパティのスキーマのリファレンスを見てみる。

https://weaviate.io/developers/weaviate/config-refs/schema#properties

{
    "name": "title",                       // プロパティ名。
    "description": "title of the article", // プロパティの説明
    "dataType": [                          // プロパティのデータの「型」。オブジェクトの相互参照を行う場合には、1つのプロパティが複数の型を持つことはありうる。
      "text"
    ],
    "tokenization": "word",                // 転置インデックスの場合、フィールド内のコンテンツを単語単位のトークンに分割する。詳しくは「プロパティのトークン化」を参照。
    "moduleConfig": {                      // モジュール固有の設定。
      "text2vec-contextionary": {
          "skip": true,                    // trueの場合、プロパティ全体がベクトル化に含まれない。デフォルトは false でオブジェクトはスキップされない。
          "vectorizePropertyName": true    // データオブジェクトのベクトル位置の計算にプロパティ名を使用する。デフォルトはfalse。
      }
    },
    "indexFilterable": true,               // オプション。デフォルトはtrue。デフォルトでは、各プロパティは効率的なフィルタリングのために、roaling bitmapインデックスが利用可能な場合はそれでインデックス化される。
    "indexSearchable": true                // オプション。デフォルトはtrue。デフォルトでは、各プロパティは BM25 用の検索可能なインデックスでインデックスされる。
}

改めて見てみると、

  • プロパティに限った話ではないけども、ベクトル化設定だけでなく、キーワード検索設定も含まれているように見える。
  • モジュール設定にベクトル化するかしないかを設定する項目がある。

というように見える。

一般的なベクトル専門データベースの場合だと、

  • 直接検索に使うベクトルデータを(自分でベクトル化して)登録する
  • 検索検索から取得したいパラメータ(LLMのプロンプトに渡すコンテキストの文字列)をメタデータとして登録する

というもたせ方をするやり方が多いと思う。例えばQdrantだとこう(ちょっと説明が足りないかもだけど元記事を参照)

https://zenn.dev/link/comments/02daf6b15eac85

from qdrant_client.http.models import Batch
from pprint import pprint 
from qdrant_client.http.models import Filter, FieldCondition, MatchValue

# pandasのデータフレームにあるデータを順次登録する
for index, row in df.iterrows():
    arr = row["embedding"]
    text = row["text"]
    client.upsert(
        collection_name="manga_info",
        points=Batch(
            ids=[index + 1],
            vectors=[row["embedding"]],     # テキストのembedding済データを渡す
            payloads=[{"text": row["text"], "topic": row["topic"]}]      # payload≒メタデータとして、元のテキストやそれ以外の情報を渡す
        )
    )
query = "主人公の名前は"
query_embed = get_embedding(query, embedding_model)

search_result = client.search(
    collection_name="manga_info",
    query_vector=query_embed,
    limit=4,
)
pprint(search_result)
[ScoredPoint(id=15, version=0, score=0.7953094823332205, payload={'text': 'サイヤ人編\nピッコロ(マジュニア)との闘いから約5年後、息子の孫悟飯を儲けて平和な日々を過ごしていた悟空のもとに、実兄・ラディッツが宇宙より来襲し、自分が惑星ベジータの戦闘民族・サイヤ人であることを知らされる。・・・', 'topic': 'dragonball'}, vector=None),
 ScoredPoint(id=13, version=0, score=0.7934801424501989, payload={'text': '孫悟空少年編\n地球の人里離れた山奥に住む尻尾の生えた少年・孫悟空はある日、西の都からやって来た少女・ブルマと出会う。そこで7つ集めると神龍(シェンロン)が現れ、どんな願いでも一つだけ叶えてくれるというドラゴンボールの・・・', 'topic': 'dragonball'}, vector=None),
 ScoredPoint(id=1, version=0, score=0.7925997563399234, payload={'text': '第一訓 - 第八十八訓\n江戸時代末期、地球は「天人(あまんと)」と呼ばれる宇宙人の襲来を受ける。まもなく地球人と天人との間に十数年にも及ぶ攘夷戦争が勃発、数多くの侍たちが攘夷志士として天人との戦争に参加したが、天人の強大な・・・', 'topic': 'gintama'}, vector=None),
 ScoredPoint(id=17, version=0, score=0.7902940685119297, payload={'text': '人造人間・セル編\nナメック星での闘いから約1年後、密かに生き延びていたフリーザとその一味が地球を襲撃するが、謎の超サイヤ人によって撃退される。トランクスと名乗るその青年は、自分は未来からやってきたブルマとベジータの息子・・・', 'topic': 'dragonball'}, vector=None)]

Pineconeの場合

https://zenn.dev/kun432/scraps/927885cd23d085

for index, row in tqdm(df.iterrows(), total=df.shape[0]):
    pinecone_index.upsert(
        vectors = [
            {
                'id': str(row["ID"]),
                'values': row["A_embeddings"],  # テキストのembedding済データを渡す
                'metadata': {"Answer": row["A"], "Category": row["Cat1"]}  #メタデータとして、元のテキストやそれ以外の情報を渡す
            }
        ]
    )
query = "母子手帳を受け取りたいのですが、手続きを教えてください。"#@param {type:"string"}
query_vector = get_embedding(query)

pinecone_index.query(
    vector=query_vector,
    top_k=10,
    include_metadata=True
)
{'matches': [{'id': '37',
              'metadata': {'Answer': '母子手帳の申請には診断書はいりませんが、妊娠届に診断を受けた病院名・医師名を記入していただきます。',
                           'Category': '妊娠・出産'},
              'score': 0.904983163,
              'values': []},
             {'id': '3',
              'metadata': {'Answer': '母子手帳は、妊娠届の内容を確認させていただき、その場でお渡しします。\n'
                                     '\n'
                                     '▼詳しくはこちら\n'
                                     '(自治体HP内関連ページのURL)',
                           'Category': '妊娠・出産'},
              'score': 0.899657249,
              'values': []},
             {'id': '1024',
              'metadata': {'Answer': '母子手帳をなくしたときは、再交付を受けてください。\n'
                                     'お子さんが出生前の母子手帳については、(再交付を受けられる場所)で再交付を受けられます。\n'
                                     'お子さんが出生後の母子手帳については、(再交付を受けられる場所)で受けられます。\n'
                                     '申請の際はご本人確認できるものをお持ちください。\n'
                                     '\n'
                                     '◆お問い合わせ\n'
                                     '(自治体の担当課等の名称)\n'
                                     '(電話番号)/(開庁時間)',
                           'Category': '妊娠・出産'},
              'score': 0.891071,
              'values': []},
(snip)

調べてみた限り、Weaviateにもメタデータの概念はあるんだけども、あくまでも決まったパラメータを参照できる「のみ」で、自分でメタデータを追加することはできない。

https://weaviate.io/developers/weaviate/api/graphql/additional-properties#overview

というか、これをやりたいのであれば「プロパティ」として登録することになる。ただ、上にも記載した通り、

デフォルトでは、collection_nameとすべてのプロパティのvalueが計算に含まれますが、プロパティのnameはインデックスされません。

コレクション単位でベクトル化の動作を設定するには、vectorizeClassName を使用します。

プロパティ単位でベクトル化を設定するには、skip と vectorizePropertyName を使用します。

となるので、プロパティのどれをベクトル化するか?もしくはどのプロパティに対してベクトル検索するか?を設定してやる必要があることになる。

前者の場合は以下の方法がありそう。

  1. プロパティのベクトル化モジュールの設定で"Skip": true"にする
  2. "dataType": ["object"]を使ってnestedPropertiesを使う。ここを参照

後者については前者との複合っぽい使い方になると思うのだけど、named vector/multiple vectorというのがある。

https://weaviate.io/developers/weaviate/config-refs/schema/multi-vector#syntax

ここではそれぞれを深く追わずに、とりあえずシンプルに1の方法を試してみようと思う。

以下で「質問」だけをベクトル化の対象としてみた。

client.collections.delete(name="FAQ")

faq = client.collections.create(
    name="FAQ",
    vectorizer_config=wvc.config.Configure.Vectorizer.text2vec_openai(vectorize_collection_name=False),  # collection/class名をベクトル化煮含めない
    generative_config=wvc.config.Configure.Generative.openai(),
    properties=[
        wvc.config.Property(
            name="question",
            data_type=wvc.config.DataType.TEXT,
            vectorize_property_name=False,  # プロパティ「名」(この例だと「question」)をベクトル化に含めるか⇒しない
            skip_vectorization=False  # ベクトル化する
        ),
        wvc.config.Property(
            name="faq_id",
            data_type=wvc.config.DataType.NUMBER,
            vectorize_property_name=False,  # プロパティ「名」(この例だと「faq_id)をベクトル化に含めるか⇒しない
            skip_vectorization=True  # ベクトル化をスキップする
        ),
        wvc.config.Property(
            name="answer",
            data_type=wvc.config.DataType.TEXT,
            vectorize_property_name=False,  # プロパティ「名」(この例だと「answer」)をベクトル化に含めるか⇒しない
            skip_vectorization=True  # ベクトル化をスキップする
        ),
        wvc.config.Property(
            name="category",
            data_type=wvc.config.DataType.TEXT,
            vectorize_property_name=False,  # プロパティ「名」(この例だと「category」)をベクトル化に含めるか⇒しない
            skip_vectorization=True  # ベクトル化をスキップする
        ),
    ]
)

faq.data.insert_many(faq_objs)

質問・回答・カテゴリのそれぞれでクエリしてみる。

for key in faq_objs[0]:
    if isinstance(faq_objs[0][key], str):
        print(f"===== {key} =====\n")
        query = faq_objs[0][key]
        print(f"クエリ: {query}")
        print()
        response = faq.query.near_text(
            query=query,
            limit=5,
            return_metadata=wvc.query.MetadataQuery(distance=True)  # 類似度の距離をレスポンスに含める
        )
        for r in response.objects:
            print(r.metadata.distance)
            print(r.properties)
            print()
===== question =====

クエリ: 母子手帳を受け取りたいのですが、手続きを教えてください。

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

0.06805276870727539
{'answer': '母子手帳は、○○市役所本庁舎△△階××課窓口、◎◎出張所、………(その他の受け取り場所を適宜記載)………で受け取れます。\n\n▼詳しくはこちら\n(自治体HP内関連ページのURL)', 'faq_id': 2.0, 'question': '母子手帳の受け取り場所はどこですか?', 'category': '妊娠・出産'}

0.0812482237815857
{'answer': '母子手帳は、妊娠届の内容を確認させていただき、その場でお渡しします。\n\n▼詳しくはこちら\n(自治体HP内関連ページのURL)', 'faq_id': 3.0, 'question': '母子手帳はすぐに発行してもらえますか?', 'category': '妊娠・出産'}

0.08628928661346436
{'answer': '母子手帳の申請には診断書はいりませんが、妊娠届に診断を受けた病院名・医師名を記入していただきます。', 'faq_id': 37.0, 'question': '母子手帳の申請には医師の診断書が必要ですか?', 'category': '妊娠・出産'}

0.08671343326568604
{'answer': '産前は母子手帳以外の手続きは特にありません。\n産後に、出生の届出や出生通知書の提出、(自治体が行う出産助成等)の申請をお願いします。', 'faq_id': 36.0, 'question': '母子手帳の他に産前に市役所でやるべき手続きはありますか?', 'category': '妊娠・出産'}

===== answer =====

クエリ: 窓口で妊娠届をご記入いただき、母子手帳をお渡しします。
住民票の世帯が別の方が代理で窓口に来られる場合は、委任状が必要になります。

▼詳しくはこちら
(自治体HP内関連ページのURL)

0.1078183650970459
{'answer': '産前は母子手帳以外の手続きは特にありません。\n産後に、出生の届出や出生通知書の提出、(自治体が行う出産助成等)の申請をお願いします。', 'faq_id': 36.0, 'question': '母子手帳の他に産前に市役所でやるべき手続きはありますか?', 'category': '妊娠・出産'}

0.1114574670791626
{'answer': '(混雑状況等のお知らせシステムがある場合は、説明を記載してください。)\n例「(住民票・戸籍担当課)窓口の混雑状況はこちらをご確認ください。\n(自治体HP内関連ページのURL)」\n', 'faq_id': 133.0, 'question': '(住民票・戸籍担当課)の窓口の混雑状況を教えてください', 'category': '住民票・戸籍・印鑑証明'}

0.11577862501144409
{'answer': '受診票の妊婦歯科健診は、○○市内のみでのご利用になります。\nそれ以外の受診票は、●●県内共通になりますので、そのままお使いいただけます。\n各自治体独自のサービスが受けられる場合があるので、引っ越し先の自治体にお問い合わせください。\n\n▼詳しくはこちら\n(自治体HP内関連ページのURL)', 'faq_id': 8.0, 'question': '●●県内で引っ越したら、妊婦健診の受診票は?', 'category': '妊娠・出産'}

0.1158173680305481
{'answer': '児童扶養手当(ひとり親手当)受給者が転入された場合は、(自治体の担当課等の名称)にて住所変更の手続きが必要です。', 'faq_id': 210.0, 'question': '児童扶養手当(ひとり親手当)受給者が市外から転入した際の手続きを教えてください。', 'category': '子どもの手当・助成'}

0.11591964960098267
{'answer': '児童扶養手当(ひとり親手当)受給者が市内で転居された場合は、(自治体の担当課等の名称)にて住所変更の手続きが必要です。', 'faq_id': 208.0, 'question': '児童扶養手当(ひとり親手当)受給者が市内で住所が変わった時の手続きを教えてください。', 'category': '子どもの手当・助成'}

===== category =====

クエリ: 妊娠・出産

0.11723536252975464
{'answer': '(相談できる機会・場所等について記載してください。)\n例「○○市では、妊娠している人やパートナーを対象に、これからの子育てや、地域で子育てをするための仲間づくりに役立つように、パパ・ママ入門学級を開催してますよ。\n\n▼詳しくはこちら\n(自治体HP内関連ページのURL)」', 'faq_id': 426.0, 'question': '出産について相談したいのですが。', 'category': '妊娠・出産'}

0.12292230129241943
{'answer': '妊娠した人は、(市役所○○課、保健所、保健相談所等の妊娠届を出せる場所を記載してください。)で妊娠届を出してください。(母子手帳や妊婦健診の受診票など、妊娠した人にお渡しするもの)をさしあげます。なるべく早めの届出をお願いします。\n\n▼詳しくはこちら\n(自治体HP内関連ページのURL)', 'faq_id': 360.0, 'question': '妊娠届について教えてください。', 'category': '妊娠・出産'}

0.12933039665222168
{'answer': '○○市の産前産後のサービスは、(東京都における「母子保健バッグ」等、自治体が妊婦等に対して交付しているもの)をご覧ください。\n出産休暇についてはお勤め先にお問い合わせください。\n出産一時金については、加入している健康保険組合等にお問い合わせください。', 'faq_id': 35.0, 'question': '産前・産後のことについて教えてください。', 'category': '妊娠・出産'}

0.1340796947479248
{'answer': '妊娠届(母子手帳交付申請含む)は代理でも届出することができます。ただし、妊婦ご本人と同一世帯の方以外が代理申請する場合は、委任状が必要になります。\nまた、妊娠届には妊娠週数、分娩予定日、性病に関する健康診断(血液検査)の有無、結核に関する健康診断(レントゲン検査)の有無及び診断を受けた医療機関の名前・所在地・診断者氏名を記入していただく必要がありますので、予め妊婦(委任者)ご本人にご確認の上お越しください。\n\n▼詳しくはこちら\n(自治体HP内関連ページのURL)', 'faq_id': 149.0, 'question': '妊娠届を代理でも届出できますか?', 'category': '妊娠・出産'}

0.1348932981491089
{'answer': '○○市では、妊娠した人とパートナーを対象に、これからの子育てや、地域で子育てをするための仲間づくりに役立つように、(母親学級等のイベント)を開催しています。(イベントの内容等を記載してください。)\n\n▼詳しくはこちら\n(自治体HP内関連ページのURL)', 'faq_id': 113.0, 'question': '出産の不安を相談できるところはありますか?', 'category': '妊娠・出産'}

質問の1位のdistanceだけが他と異なっていて、質問だけがベクトル化されているのがわかる。

kun432kun432

Weaviateのスキーマがわかりにくかった理由がなんとなくわかった気がする。ただしあくまでも自分の認識。

上にも書いたけども、

一般的なベクトル専門データベースの場合だと、

  • 直接検索に使うベクトルデータを(自分でベクトル化して)登録する
  • 検索検索から取得したいパラメータ(LLMのプロンプトに渡すコンテキストの文字列)をメタデータとして登録する

というもたせ方をするやり方が多いと思う。

自分がこれまで触ってきたようなベクトルデータベースの多くは、

  • テキストのベクトル化プロセスは自分で行う
  • そのベクトルデータをベクトルデータベースに登録する
  • よって、どういったデータをベクトル化の対象にするか?は自分で考えて決める

というシンプルなものが多くて(細かいところまで使いこなせてない可能性はあるけれども)、そして、検索そのものは、クエリのベクトルとドキュメントのベクトルで行うのだけど、RAGで必要になるのはベクトル化前のテキストになる。通常これはメタデータに入れて結果とともに取得することになる。

図にするとこうなる。

これはとてもシンプルで、メタデータはほんとにメタデータとしての役割しかない代わりに何でもいれることができるし、ベクトル化するデータも単一になるのでとてもわかりやすい。

ただし、当然ながらデータ構造の設計は、検索時等のことも踏まえて考えないといけないし、例えば、複数ベクトルを持たせた多段ベクトル検索みたいなものが構築したくなった場合、シンプルな1オブジェクト≒1ベクトルデータの構成だと、何かしらの枠組み(データベースとかコレクションとか名前空間とか)を跨ぐようなアプローチになり、その部分の管理を自分で行わないといけなくなる。

これに対して、Weaviateの場合は、上記のメタデータとして扱っていたような必要なデータも含めて、すべてWeaviateに登録し、モジュールを使う場合にはWeaviate側でベクトル化も含めて行う。

デフォルトではすべてのプロパティを使ってベクトルデータが作成されるので、事前のデータ構造の設計とかを考えなくても良い。ただ、これだと検索精度が上がらない場合も考えられる。その場合は、データの「どれ」をベクトル検索で使うのか?を設定で渡すことになる。

この場合、最初にスキーマの設計が必要になるのは一般的なベクトルデータベースと同様にはなるものの、複数のベクトルデータを1つのオブジェクトに紐づけてユースケースに応じて検索するベクトルデータを選択する(named vector/multiple vectorとか)とか、より複雑な構造のオブジェクト(nestedPropertiesとか)といった柔軟性に持ちつつ、管理上も1つのオブジェクトとして管理ができる。

また、Weaviateは、BM25などを使ったキーワード検索(単体でも使える)や、それをベクトル検索とも組み合わせたハイブリッド検索にも対応しており、この設定を見ていても、Pineconeあたりのハイブリッド検索が、あくまでもdense vectorにsparse vectorが追加されただけって感じのシンプルなアプローチなのに対して、Weaviateの場合はElasticSearch/OpenSearchあたりの全文検索エンジンで各プロパティのマッピングを設定するイメージに近いと思う。

自分の場合は、PineconeとかQdrantあたりの先入観があってからWeaviateを触ったので、多分そのギャップでわかりにくいと感じたのだと思う。特にQuickstartの流れだと、プロパティ全部でベクトルデータが生成されるとか所見だと絶対わからないし。

めっちゃ気持ちがわかるw

https://forum.weaviate.io/t/i-dont-understand-the-weaviate-schema-structure/896

やっとスッキリした気がする。

kun432kun432

スキーマには他にも気になる設定がある。

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

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

また上ですこし触れたnested propertiesやmulti-vectorなんかも気になる。

https://weaviate.io/blog/weaviate-1-22-release#nested-object-storage

https://weaviate.io/developers/weaviate/config-refs/schema/multi-vector#syntax

が、スキーマは一旦ここで終了。

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