Tech Waves

produced by Hakuhodo DY ONE

本ブログは、株式会社Hakuhodo DY ONEの開発チームによるエンジニアブログです。
それぞれのメンバーが業務を通して得た技術情報や、各種セミナーの参加レポート、またその他トピックについて情報発信を行っています。

Zvecを触りながらベクトルDBについて調べてみる

こんにちは!佐藤です🕶

GitHub Trendingを調べていたら、「Zvec」というリポジトリが上位に来ており、気になってどんなサービスなのか自分なりにまとめましたので共有します。

調べていくと、ベクトルデータベース(以下 ベクトルDB)やマルチモーダルなど色々わからない単語出てきて苦戦したのでひとつずつ自分なりの解釈をしながらZvecを利用してみたいと思います。

「Zvec」ってなんぞや

リポジトリのREADMEのリード文を確認すると、下記のようになっています。

Zvec is an open-source, in-process vector database — lightweight, lightning-fast, and designed to embed directly into applications. Built on Proxima (Alibaba's battle-tested vector search engine), it delivers production-grade, low-latency, scalable similarity search with minimal setup.

ざっとDeepLを使って日本語訳すると、下記になります。

Zvecは、オープンソースのインプロセス型ベクトルデータベースです。軽量で超高速、そしてアプリケーションに直接組み込めるよう設計されています。Alibabaの実績あるベクトル検索エンジンであるProximaをベースに構築されており、最小限のセットアップで本番環境レベルの低遅延でスケーラブルな類似検索を実現します。

うーん…よくわからない…。

リード文中の用語で、気になったものを一つずつ確認します。

用語

オープンソース

  • コンピュータプログラムの著作権の一部を放棄し、ソースコードの自由な利用および頒布を万人に許可するソフトウェア開発モデル (Wikipediaより引用)
  • 有名なものだと、プログラミング言語のPythonやPHPやOSであるLinuxなど

インプロセス型

  • ZvecのFeatureの項目に以下のような文面が記載されています。
    • Runs Anywhere: As an in-process library, Zvec runs wherever your code runs — notebooks, servers, CLI tools, or even edge devices.
    • 日本語訳)どこでも実行可能:Zvecはコードが実行されるあらゆる環境で動作します。ノートブック、サーバー、CLIツール、さらにはエッジデバイスでも利用可能です。

ベクトル検索エンジン

ここがZvecを知る上で一番大切な部分のようなので、噛み砕いで説明します。

ベクトル(ベクトル化)

ざっくり言うと、画像・文章・音声などの情報を共通のアルゴリズムで数値化して配列にしたものです。

  • 例)犬の画像→ [0.12, -0.03, ・・]みたいな数値の並びにして保存

ベクトルとして登録する数字は適当に数値を並べたりするわけではなく、人間にとっての「意味が近いほど、数値的にも近くなる」ように作られているのがポイントです。

(実際のベクトル化はZvec上で行わず、CLIPなどのモデルを利用してデータを変換) マルチデータの数値化については以下の記事を参考↓

qiita.com

ベクトル検索

普通のDBを考えると、検索する場合、ほとんどがSQLを使うでしょう。

例えば以下のような検索が代表的ではないでしょうか?

  • WHERE name = “cat” (完全一致)
  • WHERE date BETWEEN ‘2025-01-01’ AND ‘2026-01-01’ (範囲検索)

しかしながら、画像や文章をこの方法で検索するのは難しそうです。

そこで行うのがベクトル検索になります。

やっていることは非常にシンプルで、

  1. 検索したい対象をベクトルにする(例:「青い車の写真」→[0.01, -0.02, ….])
  2. すでにベクトルDBに格納しているベクトルデータと検索したい対象のベクトル値を比較して一番近いものを計算する
  3. 近い順に返す

Zvecはこのうちの2,3の工程を専門としていて、なおかつ高速に処理できるのが売り、ということが概要からわかります。

ここまでのまとめ:Zvecを一言で言うと

ここまでの内容を整理すると、Zvecは「どのようなプラットフォームにも組み込むことができる、軽量なベクトル検索用のローカルデータベース」と言えそうです。

利用の流れとしては以下の流れになります。

  1. 外部のツールを利用して画像や文章をベクトル化
  2. 1.でベクトル化したデータをZvecに保存
  3. 自然言語で検索が可能!

次のセクションでは実際に画像データを入れて、テキスト検索・画像検索ができるところまで動かしてみます。

検索してみる

以下のリンクにQuickstart専用のページがあるので、そちらを利用し実際に画像検索をしてみます。

zvec.org

データソース・プログラムの用意

Quickstart専用ページをみるとすでにサンプル用のデータソースが用意されているのでそちらを利用します。

ファイルの中身を確認すると以下のようなデータが格納されています。

data.jsonl

データ構造

{
  "id": "ILSVRC2012_val_00000003", -- 画像のID
  "image_encoding": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD...", -- base64にエンコードされた元画像データ
  "image_embedding": [0.0627, 0.0060, -0.0525, ...], -- ベクトル化された画像データ
  "category": "n01443537" -- ImageNetカテゴリID
}

ポイント

  • image_encoding: JPEG画像をbase64エンコードしたもの。デコードすれば元の画像に戻せます。
  • image_embedding: 機械学習モデルで生成した1024次元の特徴ベクトル
    • このベクトルはすでに計算済みの物で、Zvecに格納して検索をします
  • これらのデータセットは以下のものがベクトル化され格納されている
    • ImageNet
      • ImageNetの検証セット(Validation Set)から抽出された5,000枚の画像データセット
      • ライセンス: Apache 2.0

text_quries.jsonl (テキスト検索クエリ)

データ構造

{
  "text": "Cute dog",
  "embedding": [0.0214, 0.0673, -0.1168, ...]
}

ポイント

  • テキストから画像を検索するためのクエリ情報が格納されています。サンプルコードではクエリ情報の中から一つテキストを選択して、data.jsonlに登録されているベクトルデータから近い画像をZvecが抽出するために利用します。
  • 例えば上記の例だと”cute dog”という検索したいテキストクエリに対して、embeddingに”cute dog”をベクトル化したものが対となって格納されています。(data.jsonlと同じく1024次元)
  • 勘違いしてはいけないのは、「画像データとテキストデータは同じアルゴリズムでベクトル化されている」と言う点です。例えば”dog”と言う文字列と犬の画像では、数値が近いようになるようにベクトル化されるはずです。Zvecはあくまでベクトルデータを検索するのに特化しているツールで、ベクトル化に特化しているツールではないことに注意です。

image_queries.jsonl (画像検索クエリ)

データ構造

{
  "encoding": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD...",
  "embedding": [0.0122, -0.0702, 0.0308, ...]
}

ポイント

  • text_queries.jsonlとは違い、画像から似た画像を検索するために利用するデータです。(逆画像検索)
  • 例) アヒルに似た画像データ(ベクトル化はする)をzvec上に検索して、アヒルに似た画像を抽出してもらう。
    • 逆検索のデータセットについてもdata.jsonlと同じデータセットの画像が格納されている

walkthrough_utils.py

概要

  • jsonlファイルを読み込んでpythonのリストに変換(load_jsonl)
  • base64にエンコードされた画像を画面に表示する(display_image_from_base64)

コード

import base64
import json
from io import BytesIO

import matplotlib.pyplot as plt
from PIL import Image

def load_jsonl(file_path: str) -> list[dict]:
    with open(file_path, "r", encoding="utf-8") as f:
        return [json.loads(line) for line in f if line.strip()]

def display_image_from_base64(base64_string: str):
    # Remove the data URI prefix if present
    if base64_string.startswith("data:image"):
        # Extract the actual base64 data
        base64_data = base64_string.split(",")[1]
    else:
        base64_data = base64_string

    # Decode the base64 string
    image_data = base64.b64decode(base64_data)

    # Create a BytesIO object from the decoded data
    image_buffer = BytesIO(image_data)

    # Open the image using Pillow
    image = Image.open(image_buffer)

    # For Jupyter notebooks, use matplotlib to display the image inline
    plt.imshow(image)
    plt.axis("off")  # Hide axes
    plt.show()
    plt.show()

workthrough.py (メインプログラム)

概要

  • ダウンロード元はipynb形式だったが、py形式で実行できるように変更
  • コードのコメントに関してはAIを利用

コード

"""
Zvec Walkthrough - 画像検索システムのデモ(日本語版)
================================================

このスクリプトは以下の流れで画像検索システムを構築します:
1. Zvecの初期化とコレクション(データベース)作成
2. 130枚の画像データを読み込んでコレクションに挿入
3. テキストクエリで画像を検索
4. 画像クエリで類似画像を検索
5. コレクションの削除(クリーンアップ)
"""

import zvec

# ==========================================
# 1. Zvecの初期化
# ==========================================

print("Zvec version: ", zvec.__version__)

# Zvecの初期化(最初に1回だけ実行)
# ログをコンソールに出力し、警告レベル以上のみ表示
zvec.init(log_type=zvec.LogType.CONSOLE, log_level=zvec.LogLevel.WARN)

# ==========================================
# 2. コレクションスキーマの定義
# ==========================================

# スカラーフィールド: 画像データそのもの(base64エンコード)
image_encoding = zvec.FieldSchema(
    name="base64_image",
    data_type=zvec.DataType.STRING,  # 文字列型
)

# ベクトルフィールド: 画像の特徴ベクトル(埋め込み)
image_embedding = zvec.VectorSchema(
    name="embedding",
    data_type=zvec.DataType.VECTOR_FP32,  # 32ビット浮動小数点数のベクトル
    dimension=1024,  # 1024次元のベクトル
    # HNSWインデックスを使用してコサイン類似度で検索
    # HNSW = Hierarchical Navigable Small World(高速な近似最近傍探索アルゴリズム)
    index_param=zvec.HnswIndexParam(metric_type=zvec.MetricType.COSINE),
)

# コレクションスキーマ: データベースの設計図
collection_schema = zvec.CollectionSchema(
    name="image_search",
    fields=[image_encoding],  # スカラーフィールドのリスト
    vectors=[image_embedding],  # ベクトルフィールドのリスト
)

# ==========================================
# 3. コレクションの作成
# ==========================================

# コレクションを作成して開く
# path: データが保存されるディレクトリ
collection = zvec.create_and_open(
    path="./image_search",  # カレントディレクトリのimage_searchフォルダに保存
    schema=collection_schema,
)

# コレクションのスキーマを表示
print(collection.schema)

# コレクションの統計情報を表示
print(collection.stats)
# 今は空なので doc_count は 0 のはず

# ==========================================
# 4. データの読み込みと確認
# ==========================================

import walkthrough_utils

# JSONLファイルから画像データを読み込む
# data.jsonl には130件の画像データが含まれる
data = walkthrough_utils.load_jsonl("./data.jsonl")

# データの構造を確認
print("Structure of the first document: ", data[0].keys())
print(f"Total number of documents loaded: {len(data)}")

# サンプルデータを表示
example_index = 2  # 3番目の画像を表示(0から始まるので2)

raw_doc = data[example_index]
print(f"\nDisplaying image[{example_index}] from the dataset:")

# base64エンコードされた画像を表示
walkthrough_utils.display_image_from_base64(raw_doc["image_encoding"])

# 画像の情報を表示
print(f"ID: {raw_doc['id']}")
print(f"Dimension of the image embedding: {len(raw_doc['image_embedding'])}")
print("Image embedding: ", raw_doc["image_embedding"])
if "category" in raw_doc:
    print("Category: ", raw_doc["category"])

# ==========================================
# 5. データの挿入
# ==========================================

# 130件の画像データを1件ずつコレクションに挿入
for raw_doc in data:
    # 各フィールドを取り出す
    id: str = raw_doc["id"]  # 画像の一意識別子(例: "ILSVRC2012_val_00000003")
    image: str = raw_doc["image_encoding"]  # base64エンコードされた画像
    vector: list[float] = raw_doc["image_embedding"]  # 1024次元の特徴ベクトル
    
    # Zvecのドキュメント形式に変換
    doc = zvec.Doc(
        id=id,
        fields={
            "base64_image": image,  # フィールド名: データ
        },
        vectors={
            "embedding": vector,  # ベクトル名: ベクトルデータ
        },
    )
    
    # コレクションに挿入
    result = collection.insert(doc)
    
    # 挿入が成功したか確認
    if not result.ok():
        print(result)
        break

# 挿入後の統計情報を表示
# doc_count が 130 になっているはず
print(collection.stats)

# ==========================================
# 6. テキストクエリによる画像検索
# ==========================================

# テキストクエリを読み込む
# text_queries.jsonl には17件のテキストクエリが含まれる
text_queries = walkthrough_utils.load_jsonl("./text_queries.jsonl")

# テキストクエリの構造を確認
print(f"Total number of text queries loaded: {len(text_queries)}")
print("Structure of the first text query: ", text_queries[0].keys())

# 最初のテキストクエリを確認
query_index = 0  # "Cute dog" のクエリ
print(f"\nThe text query at index {query_index} is:")
print(f"Text: {text_queries[query_index]['text']}")
print("Embedding: ", text_queries[query_index]["embedding"])

# 最初のテキストクエリ("Cute dog")で検索
print(f"The first text query: {text_queries[0]['text']}")

# ベクトル検索を実行
result = collection.query(
    zvec.VectorQuery(
        field_name="embedding",  # 検索対象のベクトルフィールド名
        vector=text_queries[0]["embedding"],  # "Cute dog" のベクトル
    ),
    topk=3,  # 最も類似した上位3件を取得
    include_vector=False,  # ベクトルデータは返さない(軽量化)
)

print("\nQuery result:\n")
print(result)

# 検索結果の画像を表示
for idx, doc in enumerate(result):
    print(f"\nDisplaying image #{idx + 1} with ID: {doc.id}")
    walkthrough_utils.display_image_from_base64(doc.fields["base64_image"])

# ==========================================
# 7. テキスト検索の関数化
# ==========================================

def display_similar_images_by_text_query(query_index: int):
    """
    テキストクエリで類似画像を検索して表示する
    
    引数:
        query_index (int): text_queries のインデックス番号
    """
    text_query = text_queries[query_index]
    
    # 使用するテキストクエリを表示
    print(f"The text query[{query_index}]: {text_query['text']}")
    
    # ベクトル検索を実行
    result = collection.query(
        zvec.VectorQuery(
            field_name="embedding",
            vector=text_query["embedding"],  # テキストのベクトル
        ),
        topk=3,  # 上位3件
        include_vector=False,
    )
    
    # 検索結果の画像を表示
    for idx, doc in enumerate(result):
        print(f"\nDisplaying image #{idx + 1} with ID: {doc.id}")
        walkthrough_utils.display_image_from_base64(doc.fields["base64_image"])

# 2番目のテキストクエリで検索
# "Find images that have both a dog and a car."
display_similar_images_by_text_query(query_index=1)

# ==========================================
# 8. 画像クエリによる類似画像検索
# ==========================================

# 画像クエリを読み込む
# image_queries.jsonl には6件の画像クエリが含まれる
image_queries: list[dict] = walkthrough_utils.load_jsonl("./image_queries.jsonl")

print(f"Loaded {len(image_queries)} image queries.")

def display_similar_images_by_image_query(query_index: int):
    """
    画像クエリで類似画像を検索して表示する(逆画像検索)
    
    引数:
        query_index (int): image_queries のインデックス番号
    """
    image_query = image_queries[query_index]
    
    # クエリ画像を表示
    print(f"The image query[{query_index}]:")
    walkthrough_utils.display_image_from_base64(image_query["encoding"])
    
    # ベクトル検索を実行
    result = collection.query(
        zvec.VectorQuery(
            field_name="embedding",
            vector=image_query["embedding"],  # 画像のベクトル
        ),
        topk=3,  # 上位3件
        include_vector=False,
    )
    
    # 検索結果を表示
    print(f"\nRetrieved {len(result)} similar images:")
    for idx, doc in enumerate(result):
        print(f"\nDisplaying image #{idx + 1} with ID: {doc.id}")
        walkthrough_utils.display_image_from_base64(doc.fields["base64_image"])

# 最初の画像クエリで検索
display_similar_images_by_image_query(0)

# ==========================================
# 9. コレクションの削除(クリーンアップ)
# ==========================================

# コレクションとそのデータを完全に削除
collection.destroy()

print("✅ Collection deleted successfully.")

実際に試す

準備

データソース・プログラムの用意で説明したファイルを以下のように配置

walkthrough
    ├── data.jsonl
    ├── image_queries.jsonl
    ├── text_queries.jsonl
    ├── walkthrough.py
    └── walkthrough_utils.py

実行前にzvecモジュールをpipする

pip install zvec

メインプログラム実行

メインプログラムはコメントアウトの通り以下の手順を沿って実行されます。

  1. ベクトルDBスキーマの作成(データソースを格納する場所の作成)
  2. data.jsonlを1.で作成したDBに挿入
  3. テキストクエリを利用して”cute dog”を検索
  4. 画像クエリを利用して類似画像を検索
  5. コレクション(DB)の削除

それではworkthrough.pyを実行してみましょう。

実行結果

テキストクエリ結果

“cute dog”の検索結果

“Find images that have both a dog and a car.”の検索結果

イメージクエリ結果(画像を入力として入力画像と違い画像を検索)

インプット

検索結果

まとめ:感想

今回はZvecの概要の調査から実際にコードを実行するまで試してみました。

検索も非常に早く、結果を見る限り、想定通りに検索できているようです。(少なくとも”cute dog”すべて可愛かったです!)

Zvecの日本語の情報は少ないですが、実際にコードを動かしてさまざまな場面で活用していただけると幸いです。

ベクトルDBについてはまだ調査したばかりなので、もし解釈が異なっているところなどあればご連絡いただけると幸いです。

ここまで読んでいただきありがとうございました。