Closed10

Chromaでセマンティック検索を試す

kun432kun432

ゆえあっていろいろベクトルDBを試し中。以前LangChainやLlamaIndexをよく触っていた頃にチュートリアルなどでよく出てきた「Chroma」を今一度改めて。

公式サイト

https://www.trychroma.com/

GitHubレポジトリ

https://github.com/chroma-core/chroma

Chroma

Chroma - オープンソースの埋め込みデータベース。

メモリを使用して Python または JavaScript LLM アプリを構築する最速の方法です!

機能

  • シンプル: 完全に型指定され、完全にテストされ、完全に文書化されています == 幸せ
  • インテグレーション: 🦜️🔗 LangChain (python および js)、🦙 LlamaIndex など、さらに追加予定
  • 開発、テスト、本番: Python ノートブックで実行されるのと同じ API、クラスタにスケーリング可能
  • 豊富な機能: クエリ、フィルタリング、密度推定など
  • 無料 & オープンソース: Apache 2.0 ライセンス

ユースケース: ______ 用の ChatGPT

例: 「データとチャット」ユースケース:

  1. データベースにドキュメントを追加します。独自の埋め込みデータ、埋め込み関数を渡すか、Chroma に埋め込みを任せることもできます。
  2. 自然言語で関連ドキュメントをクエリします。
  3. GPT3 などの LLM のコンテキストウィンドウにドキュメントを組み合わせて、追加の要約や分析を行います。

埋め込み?

埋め込みとは何ですか?

  • OpenAI のガイドを読む
  • 文字通りの意味: 何かを埋め込むと、画像/テキスト/音声が数値のリストに変換されます。🖼️ または 📄 => [1.2, 2.1, ....]。このプロセスにより、ドキュメントは機械学習モデルが「理解できる」形式になります。
  • 類推的な意味: 埋め込みは文書のエッセンスを表します。これにより、同じエッセンスを持つ文書やクエリが「近く」に配置され、容易に検索可能になります。
  • 技術的な説明: 埋め込みは、深層神経ネットワークの層における文の潜在空間上の位置です。データ埋め込み用に特化して訓練されたモデルでは、これは最後の層です。
  • 小さな例: 写真の中から「サンフランシスコの有名な橋」を検索する場合。このクエリを埋め込み、写真とそのメタデータの埋め込みと比較することで、ゴールデンゲートブリッジの写真が返されるはずです。

埋め込みデータベース(ベクトルデータベースとも呼ばれます)は埋め込みを格納し、従来のデータベースのように部分文字列で検索するのではなく、最近傍検索が可能です。デフォルトでは、ChromaはSentence Transformersを使用して埋め込みを実行しますが、OpenAI embeddings、Cohere(多言語対応)embeddings、または独自の埋め込みを使用することもできます。

kun432kun432

Getting Started

Chromaの利用方法は複数ある

  1. ネイティブAPI
    • Pythonパッケージ
    • JSパッケージ
  2. クライアント・サーバモード
    • CLI
    • Chromaが提供するクラウドサービス
    • Docker / Kubernetes
    • 主要クラウド(AWS/GCP/Azure)

とりあえずGetting Startedに従うと、まずはネイティブAPIでアクセスするみたい。一旦これに従って進めて、あとでクライアント・サーバモードを試す。環境はローカルのM2 Mac+Python-3.12で。

https://docs.trychroma.com/docs/overview/getting-started

Python環境作成

uv init -p 3.12.9 chroma-work && cd chroma-work

パッケージインストール

uv add chromadb
出力
 + chromadb==1.0.13

EmbeddingモデルはChromaのデフォルトだと all-MiniLM-L6-v2 が使用される。あとで、OpenAI text-embedding-3-small を使うので、OpenAIパッケージも追加。

uv add openai
出力
 + openai==1.90.0

OpenAI APIキーをセット

export OPENAI_API_KEY=XXXXXXX

こんなコード

main.py
import chromadb

# クライアント初期化
chroma_client = chromadb.Client()

# コレクションの作成
collection = chroma_client.create_collection(name="my_collection")

# ドキュメントをコレクションに追加
collection.add(
    documents=[
        "This is a document about pineapple",
        "This is a document about oranges"        
    ],
    ids=["id1", "id2"]
)

# 検索
results = collection.query(
    query_texts=["This is a query document about hawaii"],  # Chromaが埋め込みを生成
    n_results=2  # 検索結果の数
)

# 検索結果を表示
print(results)

実行

uv run main.py

初回はモデルがダウンロードされる。

出力
/Users/kun432/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onnx.tar.gz:  10%| | 7.86M/79.3M [00:02<00:20, 3.69MiB

結果。見やすさのために改行を入れてある。

出力
{
    'ids': [[
        'id1',
        'id2'
    ]],
    'embeddings': None,
    'documents': [[
        'This is a document about pineapple',
        'This is a document about oranges'
    ]],
    'uris': None,
    'included': ['metadatas', 'documents', 'distances'],
    'data': None,
    'metadatas': [[None, None]],
    'distances': [[
        1.0404008626937866,
        1.2430801391601562
    ]]
}

クエリを変更してみる。

main.py
(snip)

results = collection.query(
    query_texts=["This is a document about florida"], 
    n_results=2
)

(snip)

結果。上とは検索結果が異なっているのがわかる。

出力
{
    'ids': [[
        'id2',
        'id1'
    ]],
    'embeddings': None,
    'documents': [[
        'This is a document about oranges',
        'This is a document about pineapple'
    ]],
    'uris': None,
    'included': ['metadatas', 'documents', 'distances'],
    'data': None,
    'metadatas': [[None, None]],
    'distances': [[1.1462141275405884, 1.3015384674072266]]
}

OpenAIを使う場合は、embedding生成用関数を作成して、コレクション初期化時にembedding_functionで渡す。主要なものはあらかじめ用意してある様子。

コレクション初期化時のオプションについては以下に記載がある

https://docs.trychroma.com/docs/collections/create-get-delete

OpenAIの場合は以下。

https://docs.trychroma.com/integrations/embedding-models/openai#openai

上のサンプルコードを置き換えてみた。

main.py
import chromadb
import chromadb.utils.embedding_functions as embedding_functions
import os

# クライアント初期化
chroma_client = chromadb.Client()

# OpenAI Embedding作成用関数
openai_ef = embedding_functions.OpenAIEmbeddingFunction(
    api_key=os.environ["OPENAI_API_KEY"],
    model_name="text-embedding-3-small"
)

# コレクションの作成
collection = chroma_client.create_collection(
    name="my_collection",
    embedding_function=openai_ef  # Embedding作成用関数を渡す
)

# ドキュメントの定義
documents = [
    "木の温もりあふれるブックカフェで、自家焙煎の深煎りコーヒーと季節のタルトを味わいながら、窓辺から路面電車をのんびり眺められるんだ。",
    "庭にハーブが茂るガーデンカフェでは、ハンドドリップの浅煎りとフレッシュハーブティーが選べて、小鳥のさえずりが BGM 代わりになるよ。",
    "港直送の鯖を炙りしめ鯖にしてくれる専門店、皮目の香ばしさと酢のきりりとした酸味が口いっぱいに広がるんだ。",
    "カウンター割烹の金目鯛の煮付けは、甘辛ダレが骨の隅々まで染みていて、白ご飯が思わずおかわり必至だよね。",
    "昔ながらの屋台ラーメンは鶏ガラの澄んだ醤油スープと細ちぢれ麺が相性抜群で、深夜の胃袋にしみるんだ。",
    "真っ白な豚骨スープに焦がしニンニク油をひと垂らしした濃厚ラーメン、替え玉が無料でつい無限ループしてしまうよ。",
    "スリランカ式の混ぜて食べるプレートカレーでは、15種類のスパイスが複雑に重なって食べ進めるほど香りが花開くんだ。",
    "野菜がごろごろ入った欧風ビーフカレーは、赤ワインとバターのコクが効いたシャバっとルウで後を引くよ。",
    "薪窯ナポリピッツァのマルゲリータは、モッツァレラがびよーんと伸びて焼き立てを頬張る瞬間がたまらない。",
    "4種のチーズをのせたクアトロフォルマッジに蜂蜜を垂らすスタイルが人気で、塩気と甘さのコントラストがクセになるんだ。",
    "しゅわっととろけるバスクチーズケーキ専門店、表面の香ばしい焦げと濃厚クリーミーな中身のギャップが病みつきになるよ。",
    "パリパリの薄皮たい焼きは羽根つきで端っこまで香ばしく、黒あんか白あんか毎回真剣に迷っちゃうんだよね。",
]

# ドキュメントをコレクションに追加
collection.add(
    documents=documents,
    ids=[f"doc_{i}" for i in range(1, len(documents) + 1)],
)

# 検索
results = collection.query(
    query_texts=["中華そばが食べたい"],
    n_results=5
)

# 検索結果を表示
print(results)

実行結果

出力
{
    'ids': [[
        'doc_5',
        'doc_6',
        'doc_8',
        'doc_4',
        'doc_3'
    ]],
    'embeddings': None,
    'documents': [[
        '昔ながらの屋台ラーメンは鶏ガラの澄んだ醤油スープと細ちぢれ麺が相性抜群で、深夜の胃袋にしみるんだ。',
        '真っ白な豚骨スープに焦がしニンニク油をひと垂らしした濃厚ラーメン、替え玉が無料でつい無限ループしてしまうよ。',
        '野菜がごろごろ入った欧風ビーフカレーは、赤ワインとバターのコクが効いたシャバっとルウで後を引くよ。',
        'カウンター割烹の金目鯛の煮付けは、甘辛ダレが骨の隅々まで染みていて、白ご飯が思わずおかわり必至だよね。',
        '港直送の鯖を炙りしめ鯖にしてくれる専門店、皮目の香ばしさと酢のきりりとした酸味が口いっぱいに広がるんだ。'
    ]],
    'uris': None,
    'included': ['metadatas', 'documents', 'distances'],
    'data': None,
    'metadatas': [[None, None, None, None, None]],
    'distances': [[
        1.25453782081604,
        1.279504656791687,
        1.3072352409362793,
        1.3351938724517822,
        1.3546748161315918
    ]]
}

自分は過去ChromaをLangChainやLlamaIndex経由でしか触ったことがなくて、ネイティブに触ったのは今回が初なんだけど、Embedding化のプロセスが自然に組み込まれてて、こんなに使いやすいとは知らなかった・・・

SentenceTransformersモデルでも試してみた。hotchpotch/static-embedding-japaneseを使う。

https://huggingface.co/hotchpotch/static-embedding-japanese

uv add "sentence-transformers>=3.3.1"
import chromadb
import chromadb.utils.embedding_functions as embedding_functions
import os

# クライアント初期化
chroma_client = chromadb.Client()

# SentenceTransformersモデルでのEmbedding作成用関数
sentence_transformer_ef = embedding_functions.SentenceTransformerEmbeddingFunction(
    model_name="hotchpotch/static-embedding-japanese"
)

# コレクションの作成
collection = chroma_client.create_collection(
    name="my_collection",
    embedding_function=sentence_transformer_ef
)

# ドキュメントの定義
documents = [
    "木の温もりあふれるブックカフェで、自家焙煎の深煎りコーヒーと季節のタルトを味わいながら、窓辺から路面電車をのんびり眺められるんだ。",
    "庭にハーブが茂るガーデンカフェでは、ハンドドリップの浅煎りとフレッシュハーブティーが選べて、小鳥のさえずりが BGM 代わりになるよ。",
    "港直送の鯖を炙りしめ鯖にしてくれる専門店、皮目の香ばしさと酢のきりりとした酸味が口いっぱいに広がるんだ。",
    "カウンター割烹の金目鯛の煮付けは、甘辛ダレが骨の隅々まで染みていて、白ご飯が思わずおかわり必至だよね。",
    "昔ながらの屋台ラーメンは鶏ガラの澄んだ醤油スープと細ちぢれ麺が相性抜群で、深夜の胃袋にしみるんだ。",
    "真っ白な豚骨スープに焦がしニンニク油をひと垂らしした濃厚ラーメン、替え玉が無料でつい無限ループしてしまうよ。",
    "スリランカ式の混ぜて食べるプレートカレーでは、15種類のスパイスが複雑に重なって食べ進めるほど香りが花開くんだ。",
    "野菜がごろごろ入った欧風ビーフカレーは、赤ワインとバターのコクが効いたシャバっとルウで後を引くよ。",
    "薪窯ナポリピッツァのマルゲリータは、モッツァレラがびよーんと伸びて焼き立てを頬張る瞬間がたまらない。",
    "4種のチーズをのせたクアトロフォルマッジに蜂蜜を垂らすスタイルが人気で、塩気と甘さのコントラストがクセになるんだ。",
    "しゅわっととろけるバスクチーズケーキ専門店、表面の香ばしい焦げと濃厚クリーミーな中身のギャップが病みつきになるよ。",
    "パリパリの薄皮たい焼きは羽根つきで端っこまで香ばしく、黒あんか白あんか毎回真剣に迷っちゃうんだよね。",
]

# ドキュメントをコレクションに追加
collection.add(
    documents=documents,
    ids=[f"doc_{i}" for i in range(1, len(documents) + 1)],
)

# 検索
results = collection.query(
    query_texts=["中華そばが食べたい"],
    n_results=5
)

# 検索結果を表示
print(results)

結果

出力
{
    'ids': [[
        'doc_5',
        'doc_6',
        'doc_4',
        'doc_7',
        'doc_3'
    ]],
    'embeddings': None,
    'documents': [[
        '昔ながらの屋台ラーメンは鶏ガラの澄んだ醤油スープと細ちぢれ麺が相性抜群で、深夜の胃袋にしみるんだ。',
        '真っ白な豚骨スープに焦がしニンニク油をひと垂らしした濃厚ラーメン、替え玉が無料でつい無限ループしてしまうよ。',
        'カウンター割烹の金目鯛の煮付けは、甘辛ダレが骨の隅々まで染みていて、白ご飯が思わずおかわり必至だよね。',
        'スリランカ式の混ぜて食べるプレートカレーでは、15種類のスパイスが複雑に重なって食べ進めるほど香りが花開くんだ。',
        '港直送の鯖を炙りしめ鯖にしてくれる専門店、皮目の香ばしさと酢のきりりとした酸味が口いっぱいに広がるんだ。'
    ]],
    'uris': None,
    'included': ['metadatas', 'documents', 'distances'],
    'data': None,
    'metadatas': [[None, None, None, None, None]],
    'distances': [[
        2535.78173828125,
        2739.612060546875,
        2983.771728515625,
        3007.8134765625,
        3046.419189453125
    ]]
}

距離の計算がだいぶ違ってるけど、一応ランキングとしてはまあ正しそう。このあたりはメトリクスなどのパラメータも調整する必要はありそう。

kun432kun432

で、Getting Startedの最後にも書いてあるけども、この状態だとChromaに登録したデータはスクリプト終了後にはもう残っていない。

このガイドでは、簡潔にするため Chroma のehemeral clientを使用しました。これはメモリ内に Chroma サーバーを起動するため、プログラムが終了すると取り込んだデータはすべて失われます。データの永続化が必要な場合は、persistent clientを使用するか、Chroma をクライアント・サーバーモードで実行してください。

データを永続的に残すために、Persistentクライアントとクライアントサーバモードを試していく。

kun432kun432

Persistent Client

https://docs.trychroma.com/docs/run-chroma/persistent-client

クライアント初期化時にPersistentClientを使用して、データベースファイルのディレクトリパスを指定する。パス指定がない場合は.chromaになる。上でやったOpenAI Embeddingsを使うコードを修正。

コレクション作成

create_index_persistent.py
import chromadb
import chromadb.utils.embedding_functions as embedding_functions
import os

# Persistent Clientでクライアント初期化
chroma_client = chromadb.PersistentClient("./vectordb")

openai_ef = embedding_functions.OpenAIEmbeddingFunction(
    api_key=os.environ["OPENAI_API_KEY"],
    model_name="text-embedding-3-small"
)

collection = chroma_client.create_collection(
    name="my_collection",
    embedding_function=openai_ef  # Embedding作成用関数を渡す
)

documents = [
    "木の温もりあふれるブックカフェで、自家焙煎の深煎りコーヒーと季節のタルトを味わいながら、窓辺から路面電車をのんびり眺められるんだ。",
    "庭にハーブが茂るガーデンカフェでは、ハンドドリップの浅煎りとフレッシュハーブティーが選べて、小鳥のさえずりが BGM 代わりになるよ。",
    "港直送の鯖を炙りしめ鯖にしてくれる専門店、皮目の香ばしさと酢のきりりとした酸味が口いっぱいに広がるんだ。",
    "カウンター割烹の金目鯛の煮付けは、甘辛ダレが骨の隅々まで染みていて、白ご飯が思わずおかわり必至だよね。",
    "昔ながらの屋台ラーメンは鶏ガラの澄んだ醤油スープと細ちぢれ麺が相性抜群で、深夜の胃袋にしみるんだ。",
    "真っ白な豚骨スープに焦がしニンニク油をひと垂らしした濃厚ラーメン、替え玉が無料でつい無限ループしてしまうよ。",
    "スリランカ式の混ぜて食べるプレートカレーでは、15種類のスパイスが複雑に重なって食べ進めるほど香りが花開くんだ。",
    "野菜がごろごろ入った欧風ビーフカレーは、赤ワインとバターのコクが効いたシャバっとルウで後を引くよ。",
    "薪窯ナポリピッツァのマルゲリータは、モッツァレラがびよーんと伸びて焼き立てを頬張る瞬間がたまらない。",
    "4種のチーズをのせたクアトロフォルマッジに蜂蜜を垂らすスタイルが人気で、塩気と甘さのコントラストがクセになるんだ。",
    "しゅわっととろけるバスクチーズケーキ専門店、表面の香ばしい焦げと濃厚クリーミーな中身のギャップが病みつきになるよ。",
    "パリパリの薄皮たい焼きは羽根つきで端っこまで香ばしく、黒あんか白あんか毎回真剣に迷っちゃうんだよね。",
]

collection.add(
    documents=documents,
    ids=[f"doc_{i}" for i in range(1, len(documents) + 1)],
)

print("コレクションを作成しました。")

実行

uv run create_index_persistent.py
出力
コレクションを作成しました。

こんな感じでデータベースファイルが作成されていた。

tree vectordb
出力
vectordb
├── 8b18d6e9-832e-45fd-94d6-75029077a8b4
│   ├── data_level0.bin
│   ├── header.bin
│   ├── length.bin
│   └── link_lists.bin
└── chroma.sqlite3

では検索。

query_from_persistent.py
import chromadb
import chromadb.utils.embedding_functions as embedding_functions
import os
import json

# Persistent Clientでクライアント初期化
chroma_client = chromadb.PersistentClient("./vectordb")

openai_ef = embedding_functions.OpenAIEmbeddingFunction(
    api_key=os.environ["OPENAI_API_KEY"],
    model_name="text-embedding-3-small"
)

# get_collectionで既存のコレクションにアクセス
collection = chroma_client.get_collection(
    name="my_collection",
    embedding_function=openai_ef
)

# 検索
results = collection.query(
    query_texts=["中華そばが食べたい"],
    n_results=5
)

# 検索結果を表示(ちょっと見やすくした)
print(json.dumps(results, indent=2, ensure_ascii=False))

実行

uv run query_from_persistent.py
出力
{
  "ids": [
    [
      "doc_5",
      "doc_6",
      "doc_8",
      "doc_4",
      "doc_3"
    ]
  ],
  "embeddings": null,
  "documents": [
    [
      "昔ながらの屋台ラーメンは鶏ガラの澄んだ醤油スープと細ちぢれ麺が相性抜群で、深夜の胃袋にしみるんだ。",
      "真っ白な豚骨スープに焦がしニンニク油をひと垂らしした濃厚ラーメン、替え玉が無料でつい無限ループしてしまうよ。",
      "野菜がごろごろ入った欧風ビーフカレーは、赤ワインとバターのコクが効いたシャバっとルウで後を引くよ。",
      "カウンター割烹の金目鯛の煮付けは、甘辛ダレが骨の隅々まで染みていて、白ご飯が思わずおかわり必至だよね。",
      "港直送の鯖を炙りしめ鯖にしてくれる専門店、皮目の香ばしさと酢のきりりとした酸味が口いっぱいに広がるんだ。"
    ]
  ],
  "uris": null,
  "included": [
    "metadatas",
    "documents",
    "distances"
  ],
  "data": null,
  "metadatas": [
    [
      null,
      null,
      null,
      null,
      null
    ]
  ],
  "distances": [
    [
      1.25453782081604,
      1.279504656791687,
      1.3072352409362793,
      1.3352349996566772,
      1.3546748161315918
    ]
  ]
}

作成したデータベースファイルから検索ができていることがわかる。

kun432kun432

クライアント・サーバモード

chromaコマンドが用意されているので、データベースファイルのディレクトリパスを指定して起動。新しいデータベースを作成することとする。

uv run chroma run --path ./vectordb2

8000番ポートで起動している。

出力


                (((((((((    (((((####
             ((((((((((((((((((((((#########
           ((((((((((((((((((((((((###########
         ((((((((((((((((((((((((((############
        (((((((((((((((((((((((((((#############
        (((((((((((((((((((((((((((#############
         (((((((((((((((((((((((((##############
         ((((((((((((((((((((((((##############
           (((((((((((((((((((((#############
             ((((((((((((((((##############
                (((((((((    #########

Saving data to: ./vectordb2
Connect to Chroma at: http://localhost:8000
Getting started guide: https://docs.trychroma.com/docs/overview/getting-started

OpenTelemetry is not enabled because it is missing from the config.
Listening on localhost:8000

クライアントサーバモードでは、HttpClientでサーバにアクセスする。ではデータを登録。

create_index_http.py
import chromadb
import chromadb.utils.embedding_functions as embedding_functions
import os

# HttpClientでクライアント初期化
chroma_client = chromadb.HttpClient(host='localhost', port=8000)

openai_ef = embedding_functions.OpenAIEmbeddingFunction(
    api_key=os.environ["OPENAI_API_KEY"],
    model_name="text-embedding-3-small"
)

collection = chroma_client.create_collection(
    name="my_collection",
    embedding_function=openai_ef
)

documents = [
    "木の温もりあふれるブックカフェで、自家焙煎の深煎りコーヒーと季節のタルトを味わいながら、窓辺から路面電車をのんびり眺められるんだ。",
    "庭にハーブが茂るガーデンカフェでは、ハンドドリップの浅煎りとフレッシュハーブティーが選べて、小鳥のさえずりが BGM 代わりになるよ。",
    "港直送の鯖を炙りしめ鯖にしてくれる専門店、皮目の香ばしさと酢のきりりとした酸味が口いっぱいに広がるんだ。",
    "カウンター割烹の金目鯛の煮付けは、甘辛ダレが骨の隅々まで染みていて、白ご飯が思わずおかわり必至だよね。",
    "昔ながらの屋台ラーメンは鶏ガラの澄んだ醤油スープと細ちぢれ麺が相性抜群で、深夜の胃袋にしみるんだ。",
    "真っ白な豚骨スープに焦がしニンニク油をひと垂らしした濃厚ラーメン、替え玉が無料でつい無限ループしてしまうよ。",
    "スリランカ式の混ぜて食べるプレートカレーでは、15種類のスパイスが複雑に重なって食べ進めるほど香りが花開くんだ。",
    "野菜がごろごろ入った欧風ビーフカレーは、赤ワインとバターのコクが効いたシャバっとルウで後を引くよ。",
    "薪窯ナポリピッツァのマルゲリータは、モッツァレラがびよーんと伸びて焼き立てを頬張る瞬間がたまらない。",
    "4種のチーズをのせたクアトロフォルマッジに蜂蜜を垂らすスタイルが人気で、塩気と甘さのコントラストがクセになるんだ。",
    "しゅわっととろけるバスクチーズケーキ専門店、表面の香ばしい焦げと濃厚クリーミーな中身のギャップが病みつきになるよ。",
    "パリパリの薄皮たい焼きは羽根つきで端っこまで香ばしく、黒あんか白あんか毎回真剣に迷っちゃうんだよね。",
]

collection.add(
    documents=documents,
    ids=[f"doc_{i}" for i in range(1, len(documents) + 1)],
)

print("コレクションを作成しました。")
uv run create_index_http.py

次に検索。

query_from_server.py
import chromadb
import chromadb.utils.embedding_functions as embedding_functions
import os
import json

# Http Clientでクライアントを初期化
chroma_client = chromadb.HttpClient(host='localhost', port=8000)

openai_ef = embedding_functions.OpenAIEmbeddingFunction(
    api_key=os.environ["OPENAI_API_KEY"],
    model_name="text-embedding-3-small"
)

collection = chroma_client.get_collection(
    name="my_collection",
    embedding_function=openai_ef
)

results = collection.query(
    query_texts=["中華そばが食べたい"],
    n_results=5
)

print(json.dumps(results, indent=2, ensure_ascii=False))
uv run query_from_server.py
出力
{
  "ids": [
    [
      "doc_5",
      "doc_6",
      "doc_8",
      "doc_4",
      "doc_3"
    ]
  ],
  "distances": [
    [
      1.2545246,
      1.2796125,
      1.3074384,
      1.335235,
      1.3546748
    ]
  ],
  "embeddings": null,
  "metadatas": [
    [
      null,
      null,
      null,
      null,
      null
    ]
  ],
  "documents": [
    [
      "昔ながらの屋台ラーメンは鶏ガラの澄んだ醤油スープと細ちぢれ麺が相性抜群で、深夜の胃袋にしみるんだ。",
      "真っ白な豚骨スープに焦がしニンニク油をひと垂らしした濃厚ラーメン、替え玉が無料でつい無限ループしてしまうよ。",
      "野菜がごろごろ入った欧風ビーフカレーは、赤ワインとバターのコクが効いたシャバっとルウで後を引くよ。",
      "カウンター割烹の金目鯛の煮付けは、甘辛ダレが骨の隅々まで染みていて、白ご飯が思わずおかわり必至だよね。",
      "港直送の鯖を炙りしめ鯖にしてくれる専門店、皮目の香ばしさと酢のきりりとした酸味が口いっぱいに広がるんだ。"
    ]
  ],
  "uris": null,
  "data": null,
  "included": [
    "metadatas",
    "documents",
    "distances"
  ]
}

その他

  • 非同期HTTPクライアントも利用できる(AsyncHttpClient
  • クライント専用のPythonパッケージもある(chromadb-client
    • ただし、chromadb-clientはChroma ライブラリのサブセットであり、すべての依存関係が含まれているわけではない
    • 特にデフォルトのEmbedding生成関数が含まれていないため、自分で定義する必要がある。

https://docs.trychroma.com/docs/run-chroma/python-http-client

kun432kun432

コレクションの管理と設定

コレクションの管理(作成・削除等)は以下を参照。
https://docs.trychroma.com/docs/collections/create-get-delete

コレクションの設定については以下。ここはHNSWインデックスの設定なども含まれるため、少し見ていく。

https://docs.trychroma.com/docs/collections/configure

まず、ドキュメントにあるサンプルのコレクション定義の例。この例ではCohereのEmbeddingsを使用している。

import os
assert os.environ.get("COHERE_API_KEY"), 

from chromadb.utils.embedding_functions.cohere_embedding_function import CohereEmbeddingFunction

cohere_ef = CohereEmbeddingFunction(model_name="embed-english-light-v2.0")

collection = client.create_collection(
    name="my_collection_complete",
    configuration={
        "hnsw": {
            "space": "cosine",
            "ef_search": 100,
            "ef_construction": 100,
            "max_neighbors": 16,
            "num_threads": 4
        },
        "embedding_function": cohere_ef
    }
)

embedding_functionについてはすでに触れているので、主にHNSWインデックスの各設定について。

パラメータ デフォルト値 説明
space l2 Embeddingの距離関数を指定。l2(L2ノルムの2乗)、cosine(コサイン類似度距離)、ip(内積)を指定できる。
ef_construction 100 インデックス作成時に近傍を選択するための候補リストのサイズ。高いと精度は向上するがメモリ・時間が犠牲になる。低いと精度は低下するがインデックス作成が高速になる。
ef_search 100 最近傍を検索する際に使用される動的候補リストのサイズ。 高いとより多くの近傍候補を探索し、リコール・精度が向上するが、クエリ時間・計算コストが増加。
max_neighbors 16 インデックス構築中にグラフ内の各ノードが持つことのできる隣接(接続)の最大数。 大きいとグラフが密になり検索時のリコール・精度が向上するがメモリ使用量・構築時間が増加。 低いとグラフが疎になりメモリ使用量・構築時間が減少するが検索精度・想起が低下。
num_threads multiprocessing.cpu_count()
(使用可能なCPUコア数)
インデックス構築時や検索操作時に使用するスレッド数
batch_size 100 インデックス操作中に各バッチで処理するベクタの数。
sync_threshold 1000 インデックスを永続ストレージと同期するタイミング。
resize_factor 1.2 インデックスのサイズ変更時に、インデックスがどれだけ大きくなるかを制御する。

とりあえず距離関数についてはユースケースに合わせて変更すると良い。他のパラメータについては、チューニングについても記載があるので、それを踏まえて必要ならば試行錯誤。

HNSW パラメータのチューニング

Chroma では HNSW(Hierarchical Navigable Small World)インデックスを用いて近似最近傍(ANN)検索を行います。ここでいう リコール とは、真の最近傍のうちどれだけを取得できたかを示します。

ef_search を増やすと通常リコールは向上しますが、クエリ時間が長くなります。同様に、ef_construction を増やすとリコールは向上しますが、インデックス構築時のメモリ使用量と実行時間が増加します。

適切な HNSW パラメータは、データの性質・埋め込み関数・求めるリコールと性能要件によって異なります。さまざまな値を試して、要件を満たす最適な組み合わせを見つけてください。

例として、50,000 個・2048 次元の埋め込みからなるデータセットを考えます。

Python

embeddings = np.random.randn(50000, 2048).astype(np.float32).tolist()

ここでは Chroma コレクションを 2 つ作成しました。

  • コレクション 1ef_search: 10 で設定。セット内の特定の埋め込み(id = 1)をクエリすると、所要時間は 0.00529 秒、得られた距離は次のとおりです。
[3629.019775390625, 3666.576904296875, 3684.57080078125]
  • コレクション 2ef_search: 100, ef_construction: 1000 で設定。同じクエリを発行したところ、所要時間は 0.00753 秒(約 42% 遅い)でしたが、距離はより小さく、結果が改善されました。
[0.0, 3620.593994140625, 3623.275390625]

本例では、テスト埋め込み(id = 1)で検索した際、コレクション 1 ではコレクション内に存在する自身の埋め込みを見つけられませんでした(距離 0.0 の結果が出ない)。一方、コレクション 2 ではわずかに遅いものの、0.0 距離で自身の埋め込みを正しく取得し、全体として近い近傍を返しました。これは精度向上と性能低下のトレードオフを示しています。

kun432kun432

商用環境

商用環境での利用については以下にトピックがまとまっている

https://docs.trychroma.com/production/deployment

  • Dockerでのデプロイ
  • AWS・Azure・GCPでのデプロイ。AWSの場合はCloudFormationテンプレートが用意されている様子。
  • 複数のEC2インスタンスタイプでの性能比較
  • OpenTelemetryを使ったメトリクスの監視

などが記載されている。

kun432kun432

まとめ

フレームワークはいろいろ隠蔽してくれて便利な面もあるけど、ネイティブに触ってみてわかることもあるし、一次情報は改めて大事だなと感じた。それはそれとして、とてもシンプルで使いやすいし、ベクトルDBの経験があまりない、とか、PoCや小規模な案件などで高機能なものが不要であれば、十分なものだと思う。

自分は過去にいろいろなベクトルDBを試してきて、細かな制御ができるWeaviateが一番気に入っているのだけども、スキーマの設計とかはそれなりに考えることも多いので、よりシンプルなユースケースではChromaを使ってみようと思った。

kun432kun432

ただ他に比べると、あんまり速くはないかなぁという気はする。非力な環境だと遅さが目に付くかもしれない。

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