Closed8

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

kun432kun432

Valkeyいろいろ触っていたが、

https://zenn.dev/kun432/scraps/1e7ed4789bd93c

ベクトルインデックスは作成できる、ということで。ただ、公式ドキュメントでQuick Start的なものが見つけにくいのよな・・・

一応説明はここ。

https://valkey.io/topics/search/

コマンドリファレンス

https://valkey.io/commands/ft.create/

https://valkey.io/commands/ft.dropindex/

https://valkey.io/commands/ft.info/

https://valkey.io/commands/ft.search/

https://valkey.io/commands/ft._list/

で、valkey-searchはモジュールなので、そちらのレポジトリを見てみる。

READMEにはビルド&Valkey本体への組み込み手順がある

https://github.com/valkey-io/valkey-search

Quick Startがあった。これで良さそう。
https://github.com/valkey-io/valkey-search/tree/main/QUICK_START.md

あと、valkey-pyでPython経由で使う場合もnotebookが用意されている、ありがたい。

https://github.com/valkey-io/valkey-py/tree/495bf7e53bbc523c393336ea78d58090c5fddcfc/docs/examples/search_vector_similarity_examples.ipynb#L8

これで迷わずに進めれそう。

kun432kun432

今回はProxmox上に立てたUbuntu-22.04 VM上で。なお、VMにはメモリを8GB割り当てている、というのも一度4GBでvalkey-searchのビルドを試してみたらOOM-Killerでkillされてしまったため。

まずvalkey本体。Ubuntu-22.04ではパッケージはないけど、Valkey公式からバイナリが配布されているのでそれを取得。なお、Ubuntu-24だとパッケージでインストールできる。

https://valkey.io/download/

wget https://download.valkey.io/releases/valkey-8.1.2-jammy-x86_64.tar.gz

展開。自分は/opt/valkey以下にした。

sudo mkdir /opt/valkey
sudo tar zxvf valkey-8.1.2-jammy-x86_64.tar.gz --strip-components 1 -C /opt/valkey

GitHubレポジトリにsystemdのユニットファイルが含まれているので、それを流用させていただく。

https://github.com/valkey-io/valkey/blob/unstable/utils/systemd-valkey_server.service

ダウンロード

wget https://raw.githubusercontent.com/valkey-io/valkey/refs/heads/unstable/utils/systemd-valkey_server.service

パスだけ書き換え

sed -i -e 's/\/usr\/local/\/opt\/valkey/g' systemd-valkey_server.service

/etc/systemd/system配下にコピーして、読み込ませる

sudo cp systemd-valkey_server.service /etc/systemd/system/valkey-server.service
sudo systemctl daemon-reload

有効にして起動

sudo systemctl enable --now valkey-server

接続もOK

/opt/valkey/bin/valkey-cli
出力
127.0.0.1:6379>

ではvalkey-searchのGitHubレポジトリのREADMEに従って、モジュールをビルドしていく。

ビルドに必要なパッケージをインストール

sudo apt update
sudo apt install -y clangd          \
                    build-essential \
                    g++             \
                    cmake           \
                    libgtest-dev    \
                    ninja-build     \
                    libssl-dev      \
                    clang-tidy      \
                    clang-format    \
                    libsystemd-dev

gcc/g++のバージョンは12以上、Clangのバージョンは16以上が必要みたいだが、インストールされていたのはgcc-11。

gcc --version
出力
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0

ということで、gcc-12/g++-12を追加インストールして、そちらをデフォルトにする。

sudo apt install -y gcc-12 g++-12
sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 1000
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 1000

valkey-searchのレポジトリをクローン

git clone https://github.com/valkey-io/valkey-search && cd valkey-search

ビルド。ちょっと今回はVMが非力なせいもあってか、めちゃめちゃ時間がかかる。気長に待つ。

./build.sh

以下のように表示されればOK、というかほんとに時間めちゃめちゃかかった・・・

出力
(snip)
Building
[227/227] Linking CXX executable tests/response_generator_test

Build Successful!

Module path: /home/kun432/valkey-search/.build-release/libsearch.so

You may want to run the unit tests by executing:
    ./build.sh  --run-tests

To load the module, execute the following command:
    valkey-server --loadmodule /home/kun432/valkey-search/.build-release/libsearch.so

ということで、モジュールを読み込ませてみる。モジュール用ディレクトリを作成して配置。

sudo mkdir -p /opt/valkey/modules
sudo cp .build-release/libsearch.so /opt/valkey/modules/

systemdユニットファイルを修正

/etc/systemd/system/valkey-server.service
(snip)

[Service]
ExecStart=/opt/valkey/bin/valkey-server \
    --supervised systemd \
    --daemonize no \
    --loadmodule /opt/valkey/modules/libsearch.so
(snip)

反映して再起動

sudo systemctl daemon-reload
sudo systemctl restart valkey-server
出力
Job for valkey-server.service failed because a fatal signal was delivered causing the control process to dump core.
See "systemctl status valkey-server.service" and "journalctl -xeu valkey-server.service" for details.

ログ見たらコア吐いて死んでた・・・むー

出力
Jun 18 21:16:14 ubuntu-2205-base systemd[1]: valkey-server.service: Failed with result 'core-dump'.
kun432kun432

ProxmoxのVMじゃなくて、ベアメタルなUbuntu-22.04サーバでビルドしてvalkey-serverでロードしてみたら問題なく起動した。それでもそこそこ時間はかかるけど。

valkey-server --loadmodule ../.build-release/libsearch.so
出力
(snip)
1715455:M 19 Jun 2025 08:08:06.726 # <search> [notice], tid: 125871342740096, /XXXXXXXXX/valkey-search/vmsdk/src/module.cc:129: search module was successfully loaded!
1715455:M 19 Jun 2025 08:08:06.726 * Module 'search' loaded from ../.build-release/libsearch.so
1715455:M 19 Jun 2025 08:08:06.726 * Server initialized
1715455:M 19 Jun 2025 08:08:06.726 * Ready to accept connections tcp
valkey-cli
出力
127.0.0.1:6379> MODULE LIST
1) 1) "name"
   2) "search"
   3) "ver"
   4) (integer) 10000
   5) "path"
   6) "../.build-release/libsearch.so"
   7) "args"
   8) (empty array)

うーん、ProxmoxのVMで動かなかったのはVM側の何かしら設定が必要とかなんだろうか?

kun432kun432

とりあえずProxmox上でビルドすることが目的ではないので、ベアメタルなUbuntu-22.04サーバのほうで進める。まず構築手順だけおさらい。

Valkey本体

wget https://download.valkey.io/releases/valkey-8.1.2-jammy-x86_64.tar.gz

sudo mkdir /opt/valkey
sudo tar zxvf valkey-8.1.2-jammy-x86_64.tar.gz --strip-components 1 -C /opt/valkey

wget https://raw.githubusercontent.com/valkey-io/valkey/refs/heads/unstable/utils/systemd-valkey_server.service

sed -i -e 's/\/usr\/local/\/opt\/valkey/g' systemd-valkey_server.service

sudo cp systemd-valkey_server.service /etc/systemd/system/valkey-server.service
sudo systemctl daemon-reload

sudo systemctl enable --now valkey-server

valkey-search

sudo apt update
sudo apt install -y clangd          \
                    build-essential \
                    g++             \
                    cmake           \
                    libgtest-dev    \
                    ninja-build     \
                    libssl-dev      \
                    clang-tidy      \
                    clang-format    \
                    libsystemd-dev

sudo apt install -y gcc-12 g++-12
sudo update-alternatives \
    --install /usr/bin/gcc gcc /usr/bin/gcc-11  100 \
    --slave   /usr/bin/g++ g++ /usr/bin/g++-11
sudo update-alternatives \
    --install /usr/bin/gcc gcc /usr/bin/gcc-12  110 \
    --slave   /usr/bin/g++ g++ /usr/bin/g++-12

git clone https://github.com/valkey-io/valkey-search && cd valkey-search
./build.sh

valkey-serverへのモジュール反映

sudo mkdir -p /opt/valkey/modules
sudo cp .build-release/libsearch.so /opt/valkey/modules/

sudo sed -i -e '/ExecStart.*valkey-server/ s/$/ --loadmodule \/opt\/valkey\/modules\/libsearch.so/' /etc/systemd/system/valkey-server.service

sudo systemctl daemon-reload
sudo systemctl restart valkey-server

確認

export PATH=/opt/valkey/bin:$PATH
valkey-cli
出力
127.0.0.1:6379> MODULE LIST
1) 1) "name"
   2) "search"
   3) "ver"
   4) (integer) 10000
   5) "path"
   6) "/opt/valkey/modules/libsearch.so"
   7) "args"
   8) (empty array)

あと、デフォルトだとprotected-modeが有効になっていて、外部からの接続はすべて拒否されるので、これを無効化しておく。

CONFIG SET protected-mode no

本来は、設定ファイルを指定して起動するほうがいいし、認証も設定しておく必要があるけど、とりあえず。やっと次に進める。

kun432kun432

とりあえず軽くCLIで。

valkey-cliで接続して、まずインデックスの作成。こういうインデックスを作るものとする。

  • インデックス名:myIndex
  • データ: ハッシュ(ON HASH
  • キー名プレフィックス:doc:。このプレフィックスで始まるキーがインデックス対象になる。
  • ベクトルフィールド名:vector
  • ベクトル次元数: 32
  • 検索アルゴリズム:HNSW
  • 距離計量:COSINE

あとはアルゴリズムによって異なるパラメータがあるけど、おいおい。

FT.CREATE myIndex ON HASH PREFIX 1 doc: SCHEMA vector VECTOR HNSW 6 TYPE FLOAT32 DIM 32 DISTANCE_METRIC COSINE
出力
OK

インデックスの参照

FT._LIST
出力
1) myIndex

インデックスの詳細

FT.INFO myIndex
出力
 1) index_name
 2) myIndex
 3) index_options
 4) (empty array)
 5) index_definition
 6) 1) key_type
    2) HASH
    3) prefixes
    4) 1) doc:
    5) default_score
    6) "1"
 7) attributes
 8) 1) 1) identifier
       2) vector
       3) attribute
       4) vector
       5) type
       6) VECTOR
       7) index
       8)  1) capacity
           2) (integer) 10240
           3) dimensions
           4) (integer) 32
           5) distance_metric
           6) COSINE
           7) size
           8) "0"
           9) data_type
          10) FLOAT32
          11) algorithm
          12) 1) name
              2) HNSW
              3) m
              4) (integer) 16
              5) ef_construction
              6) (integer) 200
              7) ef_runtime
              8) (integer) 10
 9) num_docs
10) "0"
11) num_terms
12) "0"
13) num_records
14) "0"
15) hash_indexing_failures
16) "0"
17) backfill_in_progress
18) "0"
19) backfill_complete_percent
20) "1.000000"
21) mutation_queue_size
22) "0"
23) recent_mutations_queue_delay
24) "0 sec"
25) state
26) ready

インデックスの削除

FT.DROPINDEX myIndex

まあこのへんまではCLIでいいのだけど、流石にデータの登録や検索は厳しいので、ここからはPythonで。

kun432kun432

ここからはJupyterLabでPythonでやる。以下を参考に。

https://github.com/valkey-io/valkey-py/tree/495bf7e53bbc523c393336ea78d58090c5fddcfc/docs/examples/search_vector_similarity_examples.ipynb

JypyterLabのコンテナを起動

mkdir valkey-search-work && cd valkey-search-work
docker run --rm \
    -p 8888:8888 \
    -u root \
    -e GRANT_SUDO=yes \
    -v .:/home/jovyan/work \
    quay.io/jupyter/minimal-notebook:latest

以後はJupyterLab上で。

valkey-pyのパッケージインストール

!pip install "valkey[libvalkey]"

インデックスを作成する関数を定義

import valkey
from valkey.commands.search.field import TagField, VectorField
from valkey.commands.search.indexDefinition import IndexDefinition, IndexType
from valkey.commands.search.query import Query

r = valkey.Valkey(host="[valkey-serverのIP]", port=6379)

INDEX_NAME = "my_index"   # ベクトルインデックス名
DOC_PREFIX = "doc:"       # インデックス対象のキーのプレフィクス

def create_index(vector_dimensions: int):
    """インデックス作成陽関数"""
    try:
        # check to see if index exists
        r.ft(INDEX_NAME).info()
        print("インデックスがすでに存在します!")
    except:
        # schema
        schema = (
            VectorField("vector",                  # ベクトルデータのフィールド名
                "FLAT", {                          # ベクトルインデックスの種類: FLAT or HNSW
                    "TYPE": "FLOAT32",             # FLOAT32 or FLOAT64
                    "DIM": vector_dimensions,      # ベクトル次元数
                    "DISTANCE_METRIC": "COSINE",   # ベクトル検索の距離メトリック: COSINE or L2 or IP
                    # HNSW 専用オプションを追加したい場合は追加
                }
            ),
        )

        # インデックス定義
        definition = IndexDefinition(
            prefix=[DOC_PREFIX],       # プレフィックスの指定
            index_type=IndexType.HASH  # ON HASH
        )

        # インデックス作成
        r.ft(INDEX_NAME).create_index(fields=schema, definition=definition)

インデックス作成。今回はStatic Embedding Japaneseを使おうと思うので、次元数1024で。

VECTOR_DIMENSIONS = 1024
create_index(vector_dimensions=VECTOR_DIMENSIONS)

Static Embedding Japaneseをインストールしてベクトルデータを作成・valkey-serverに登録

!pip install "sentence-transformers>=3.3.1"
from sentence_transformers import SentenceTransformer
import numpy as np

model_name = "hotchpotch/static-embedding-japanese"
model = SentenceTransformer(model_name, device="cpu")

docs = [
    "素敵なカフェが近所にあるよ。落ち着いた雰囲気でゆっくりできるし、窓際の席からは公園の景色も見えるんだ。",
    "新鮮な魚介を提供する店です。地元の漁師から直接仕入れているので鮮度は抜群ですし、料理人の腕も確かです。",
    "あそこは行きにくいけど、隠れた豚骨の名店だよ。スープが最高だし、麺の硬さも好み。",
    "おすすめの中華そばの店を教えてあげる。とりわけチャーシューが手作りで柔らかくてジューシーなんだ。",
]

embeddings: np.ndarray = model.encode(docs)

VECTOR_FIELD = "vector"
TEXT_FIELD = "text"

# pipeline を使うと高速
pipe = r.pipeline()
for i, vec in enumerate(embeddings, start=1):
    key = f"{DOC_PREFIX}{i}"
    # float32 に変換してバイト列化
    b = np.asarray(vec, dtype=np.float32).tobytes()
    pipe.hset(key, mapping={
        VECTOR_FIELD: b,
        TEXT_FIELD: docs[i-1]
    })
pipe.execute()

print(f"{len(docs)} 件のドキュメントを登録しました。")
出力
4 件のドキュメントを登録しました。

では検索

query = "美味しいラーメン屋に行きたい"
query_vec: np.ndarray = model.encode([query])[0]  

# float32 → バイト列化
q_bytes = np.asarray(query_vec, dtype=np.float32).tobytes()

TOP_K = 4 # 取得したい上位件数
res = r.ft(INDEX_NAME).search(
    query=f"*=>[KNN {TOP_K} @{VECTOR_FIELD} $vec AS score]", 
    query_params={"vec": q_bytes}
)

print(f"Top {TOP_K} results for query: “{query}”")
for i, doc in enumerate(res.docs, start=1):
    text = getattr(doc, TEXT_FIELD, "")
    # score を float に変換してからフォーマット
    score = float(doc.score)
    print(f"{i}. id={doc.id}, score={score:.4f}, text={text}")

メトリクスはコサイン「距離」なので、数値の低いほうがより類似度が高いことになる。

出力
Top 4 results for query: “美味しいラーメン屋に行きたい”
1. id=doc:3, score=0.5165, text=あそこは行きにくいけど、隠れた豚骨の名店だよ。スープが最高だし、麺の硬さも好み。
2. id=doc:4, score=0.6801, text=おすすめの中華そばの店を教えてあげる。とりわけチャーシューが手作りで柔らかくてジューシーなんだ。
3. id=doc:2, score=0.7479, text=新鮮な魚介を提供する店です。地元の漁師から直接仕入れているので鮮度は抜群ですし、料理人の腕も確かです。
4. id=doc:1, score=0.8960, text=素敵なカフェが近所にあるよ。落ち着いた雰囲気でゆっくりできるし、窓際の席からは公園の景色も見えるんだ。

別のクエリでも。

query = "コーヒー飲みたい。"

結果

出力
Top 4 results for query: “コーヒー飲みたい。”
1. id=doc:1, score=0.8507, text=素敵なカフェが近所にあるよ。落ち着いた雰囲気でゆっくりできるし、窓際の席からは公園の景色も見えるんだ。
2. id=doc:3, score=0.9208, text=あそこは行きにくいけど、隠れた豚骨の名店だよ。スープが最高だし、麺の硬さも好み。
3. id=doc:4, score=0.9842, text=おすすめの中華そばの店を教えてあげる。とりわけチャーシューが手作りで柔らかくてジューシーなんだ。
4. id=doc:2, score=1.0220, text=新鮮な魚介を提供する店です。地元の漁師から直接仕入れているので鮮度は抜群ですし、料理人の腕も確かです。
kun432kun432

まとめ

valkeyやredisをKVSやpubsubで使うことは多いと思うけど、ベクトル検索もまとめれると管理すべきものが減るという意味では良さそう。

ただちょっとビルドは大変なので、拡張モジュールをバンドルしたDockerイメージを使うのが良さそう。

https://github.com/valkey-io/valkey-bundle

https://hub.docker.com/r/valkey/valkey-bundle

ちょっと前まではvalkey-extentionsというイメージになっていたみたいだけど、それらはアーカイブになっていて、今後はこちらになっていくのかなという感じ。ただ、まだ出来立てホヤホヤっぽくて、今後整備されていくような印象。

2025/06/25

イメージが用意されていた。
https://hub.docker.com/r/valkey/valkey-bundle

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