Valkeyでセマンティック検索を試す
Valkeyいろいろ触っていたが、
ベクトルインデックスは作成できる、ということで。ただ、公式ドキュメントでQuick Start的なものが見つけにくいのよな・・・
一応説明はここ。
コマンドリファレンス
で、valkey-searchはモジュールなので、そちらのレポジトリを見てみる。
READMEにはビルド&Valkey本体への組み込み手順がある
Quick Startがあった。これで良さそう。
あと、valkey-pyでPython経由で使う場合もnotebookが用意されている、ありがたい。
これで迷わずに進めれそう。
今回はProxmox上に立てたUbuntu-22.04 VM上で。なお、VMにはメモリを8GB割り当てている、というのも一度4GBでvalkey-searchのビルドを試してみたらOOM-Killerでkillされてしまったため。
まずvalkey本体。Ubuntu-22.04ではパッケージはないけど、Valkey公式からバイナリが配布されているのでそれを取得。なお、Ubuntu-24だとパッケージでインストールできる。
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のユニットファイルが含まれているので、それを流用させていただく。
ダウンロード
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ユニットファイルを修正
(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'.
valkey本体をソースからビルドするのも試してみたけど、同じだった。
うーん、、、このあたりと絡んでるのかな
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側の何かしら設定が必要とかなんだろうか?
とりあえず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
本来は、設定ファイルを指定して起動するほうがいいし、認証も設定しておく必要があるけど、とりあえず。やっと次に進める。
とりあえず軽く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で。
ここからはJupyterLabでPythonでやる。以下を参考に。
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=新鮮な魚介を提供する店です。地元の漁師から直接仕入れているので鮮度は抜群ですし、料理人の腕も確かです。
まとめ
valkeyやredisをKVSやpubsubで使うことは多いと思うけど、ベクトル検索もまとめれると管理すべきものが減るという意味では良さそう。
ただちょっとビルドは大変なので、拡張モジュールをバンドルしたDockerイメージを使うのが良さそう。
ちょっと前まではvalkey-extentionsというイメージになっていたみたいだけど、それらはアーカイブになっていて、今後はこちらになっていくのかなという感じ。ただ、まだ出来立てホヤホヤっぽくて、今後整備されていくような印象。
2025/06/25
イメージが用意されていた。