
概要
- Rails + PostgreSQL の既存構成に pgvector を追加して、ベクトル検索を検証してみた
- 外部 API や Python を使わず、Ruby だけで Embedding 生成から検索まで完結する
ベクトル検索とは?
テキスト、画像、音声などのデータを Embedding(埋め込み) により数値ベクトルに変換し、その「意味」や「文脈」の近さを数値的に評価し、情報を検索する技術です。
単語の一致に依存せず、意味の類似性を考慮した検索が可能です。
Embedding(埋め込み)とは?
テキストを「数値の配列」に変換
Embedding モデルにテキスト(画像・音声も可)を渡すと、Float の配列(= ベクトル)に変換されます。 次元数はモデルによって異なります(今回の ruri-v3-30m は 256 次元、OpenAI text-embedding-3-large は 3,072 次元)。
model = Informers.pipeline('embedding', 'sirasagi62/ruri-v3-30m-ONNX') vector = model.call("リリースの進捗", model_output: 'sentence_embedding', pooling: 'none') #=> [-0.0297, 0.0384, 0.0042, -0.0710, 0.0958, ...] vector.size #=> 256
ローカルでも Embedding できる
Embedding モデルは「テキストを数値に変換する」だけの軽量なモデルです。 OpenAI などの Embedding API を使わずとも、ローカルで実行できます。
今回使った ruri-v3-30m は:
よくある構成: OpenAI API → Embedding(有料) 今回の構成: informers gem → ローカル ONNX モデル → Embedding(無料)
ONNX(Open Neural Network Exchange)とは?
ML モデルの共通フォーマットです。PyTorch や TensorFlow など特定のフレームワークに依存せず、ONNX Runtime さえあればどの言語からでも推論できます。つまり Python で学習したモデルを Ruby から動かせます。
類似度の計測
ベクトル間の類似度は、「ベクトル空間上での距離や角度」が指標として評価されます。 いくつかの指標がありますが、レコメンドシステムでは Cosine 類似度がよく使われます。
| 指標 | 説明 |
|---|---|
| ユークリッド距離 | 点間の直線距離 |
| Cosine 類似度 | ベクトル間の角度に基づく距離。方向性の相関を測る |
今回の検証では Cosine 類似度を使っています。
実装例
使用技術
| 技術 | 説明 |
|---|---|
| pgvector | PostgreSQL の拡張機能。ベクトル型・距離計算・HNSW インデックスなどが使える |
| neighbor | Rails で最近傍探索を行うための gem |
| informers | ONNX 形式のモデルを Ruby から直接実行するための gem |
| ruri-v3-30m | 日本語テキストに特化した軽量 Embedding モデル(30M パラメータ) |
全てローカルの Ruby + PostgreSQL で完結します。
簡易フロー

pgvector を PostgreSQL にインストール
pgvector 単体なら公式イメージが利用できます。
既存の PostgreSQL イメージに追加する場合は、以下のように Dockerfile を作成します:
FROM postgres:16 RUN set -eux; \ apt-get update; \ apt-get install -y --no-install-recommends \ build-essential \ git \ postgresql-server-dev-16 \ ca-certificates; \ rm -rf /var/lib/apt/lists/*; \ \ git clone --depth 1 https://github.com/pgvector/pgvector.git /tmp/pgvector; \ cd /tmp/pgvector; \ make; \ make install; \ rm -rf /tmp/pgvector; \ \ test -f /usr/share/postgresql/16/extension/vector.control
マイグレーション
pgvector 拡張を有効化
class InstallNeighborVector < ActiveRecord::Migration[8.0] def change enable_extension 'vector' end end
テーブル作成
今回の検証では問い合わせメッセージを保存するテーブルを作成しました。
class CreateInquiries < ActiveRecord::Migration[8.0] def change create_table :inquiries do |t| t.text :content, null: false t.datetime :posted_at, null: false t.timestamps end add_index :inquiries, :posted_at end end
embedding カラム + HNSW インデックス追加
class AddEmbeddingToInquiries < ActiveRecord::Migration[8.0] def change change_table :inquiries, bulk: true do |t| t.column :embedding, :vector, limit: 256 t.datetime :embedded_at end add_index :inquiries, :embedding, using: :hnsw, opclass: :vector_cosine_ops, where: 'embedding IS NOT NULL', name: 'index_inquiries_on_embedding_hnsw' end end
ポイント:
:vector型で 256 次元のベクトルカラムを追加limit: 256は Embedding モデルの出力次元数に合わせる必要がある
opclass: :vector_cosine_opsで Cosine 類似度を指定using: :hnswで HNSW(Hierarchical Navigable Small World)インデックスを使用- 高次元データに対する高速な類似検索アルゴリズム
- 参考: HNSWアルゴリズムの解説
モデル
class Inquiry < ApplicationRecord has_neighbors :embedding end
has_neighbors :embedding の 1 行で nearest_neighbors メソッドが使えるようになります。
Embedding の実行
namespace :inquiry do task embedding: :environment do model = Informers.pipeline( 'embedding', 'sirasagi62/ruri-v3-30m-ONNX' ) Inquiry .where(embedded_at: nil) .find_each do |inquiry| next if inquiry.content.blank? vector = model.call( inquiry.content, model_output: 'sentence_embedding', pooling: 'none' ) inquiry.update!( embedding: vector, embedded_at: Time.current ) puts "embedded inquiry_id=#{inquiry.id}" end end end
ポイント:
- informers が ONNX Runtime を使って Ruby プロセス内でモデルを実行
model_output: 'sentence_embedding'で文単位での embedding を取得token_embeddingsを指定すると token 毎に embedding される
pooling: 'none'は sentence_embedding の場合は不要(既に集約済み)
ベクトル検索の実装
namespace :inquiry do task :search, %i[query limit] => :environment do |_, args| query = args[:query].to_s limit = (args[:limit] || 5).to_i embed = Informers.pipeline('embedding', 'sirasagi62/ruri-v3-30m-ONNX') query_embedding = embed.call( query, model_output: 'sentence_embedding', pooling: 'none' ) inquiries = Inquiry .where.not(embedding: nil) .nearest_neighbors(:embedding, query_embedding, distance: 'cosine') .limit(limit) inquiries.each do |inquiry| puts "[dist=#{inquiry.neighbor_distance}] #{inquiry.content}" end end end
ポイント:
- 検索クエリもベクトルに変換し、類似度を求める
nearest_neighborsメソッドで Cosine 類似度によるベクトル検索を行うneighbor_distanceで類似度スコアを取得できる(0 に近いほど類似)
動作イメージ
社内の問い合わせデータをインポートし、ベクトル検索を行った例です。(公開用に内容を一部変更しています)
$ rails "inquiry:search[ログイン できない, 3]" [dist=0.079 posted_at=2025-09-03] インターン生の方でGoogleの権限など異なる可能性があり、 サービスにログインできません。ログインできるよう調整をお願いいたします。 -------------------------------------------------------------------------------- [dist=0.093 posted_at=2025-10-23] 登録用リンクをSMSで送付したところ、 ユーザー側でエラーメッセージが出て開けなかった。 -------------------------------------------------------------------------------- [dist=0.099 posted_at=2025-10-01] 登録リンク経由で登録してもらったが、 ユーザーIDが表示されず、トークルームにも表示されない。
「ログイン できない」という自然言語のクエリで、意味的に近い問い合わせが検索できています。
RAG への発展
今回実装したのは、実は RAG(Retrieval-Augmented Generation) の R の部分です。
RAG は 2 ステップで構成されます:
| ステップ | 説明 |
|---|---|
| Retrieval | クエリに関連するドキュメントをベクトル検索で取得する |
| Augmented Generation | 取得した検索結果を LLM に渡して、自然言語で回答を生成する |
例えば「サービスの通知が来ない」と質問すると、ベクトル検索で関連する過去の問い合わせを取得し、それを LLM に渡して対応方法の回答を生成できます。
やってみた所感 & まとめ
Ruby でも実装できた
- テキスト → Embedding → ベクトル保存 → 最近傍検索という流れさえ理解していれば、gem を組み合わせて簡単に実現できた
- 難しい部分は gem がやってくれる
- Python でなくてもできる
次元数が多いほど精度が良いわけではない
- 当初、384 次元のモデルを使っていたが、256 次元の日本語特化 Embedding モデルの方が精度が良かった
- 用途に合ったモデルを選ぶことが重要
実務での採用を検討できるレベル
- コストがかからず、精度も悪くない
- 1,000 件分のデータでしか試していないが、企業情報の紐付けやチケット検索くらいであれば、十分な精度で実現できそう
- 1,000 件分のデータ処理は約 1〜2 分程度で完了しており、処理時間の観点でも実運用に耐えうる可能性がある
おわりに
弊社では各事業部でエンジニアを募集しております! 気になるポジションあればお気軽にお問い合わせください。一緒に働きましょう。
SaaS
【業界特化SaaSを連続立ち上げ】2年で顧客数2,400社超え/シニアソフトウェアエンジニア(フルスタック) - 株式会社プレックス
【業界特化SaaSを連続立ち上げ】2年で顧客数2,400社超え/ソフトウェアエンジニア - 株式会社プレックス
PLEX JOB
エッセンシャルワーカー産業の人材課題を解決 | フロントエンドの技術を牽引するテックリード - 株式会社プレックス
エッセンシャルワーカー領域で日本を動かす仕組みをつくるスタートアップのエンジニア - 株式会社プレックス
コーポレート
オペレーションの効率化によって事業成長に貢献するコーポレートエンジニア - 株式会社プレックス
参考リンク
- pgvector - PostgreSQL のベクトル拡張
- neighbor - Rails 向け最近傍探索 gem
- informers - Ruby で ONNX モデルを実行
- ruri-v3-30m - 日本語特化 Embedding モデル