はじめに:なぜオンプレを検討するのか
現在、AWSのサーバーレス構成(DynamoDB + AppSync + GraphQL)でWebサービスを運用している。しかし、類似検索やEmbedding生成を本格的に導入しようとすると、AWSのOpenSearch等はコストが高い。
そこで浮上したのが「オンプレサーバーでGPUを使ったEmbedding生成・再ランキング、そして全文検索エンジンを動かす」という選択肢だ。本記事では、この構成を検討する際のポイントを整理する。
検討の前提条件
- データ件数:約1万件
- 検索対象:タイトル・短文・タグ
- 要件:多言語対応、リアルタイム検索
- 現行構成:AWS DynamoDB + AppSync + GraphQL
- 目的:コスト削減とGPU活用
1. Ollamaで使えるEmbeddingモデルの選び方
ローカルでEmbeddingを生成する場合、Ollamaは手軽な選択肢だ。Embeddingモデルを選ぶ基準を整理する。
選定基準
| 基準 | 説明 | 推奨 |
|---|---|---|
| 多言語対応 | 日本語・英語など複数言語を扱う場合は必須 | qwen3-embedding, bge-m3, granite-embedding |
| 次元数 | 高次元ほど精度が上がるがストレージ・計算コスト増 | 384〜1024次元が実用的 |
| モデルサイズ | GPUメモリとのトレードオフ | 8GB VRAM なら 4B以下のモデル |
| 検索タスク適性 | 検索・類似度計算に特化したモデルを選ぶ | MTEB Retrievalスコアを参考に |
Ollamaで利用可能な多言語Embeddingモデル
2025年以降、Ollamaで利用可能な多言語対応Embeddingモデルが充実してきた。以下は日本語対応で実績のあるモデル。
| モデル | サイズ | 言語 | 特徴 |
|---|---|---|---|
| qwen3-embedding | 0.6B / 4B / 8B | 100+言語 | MTEB多言語1位(スコア70.58)、次元数32-4096で柔軟 |
| bge-m3 | 567M | 100+言語 | dense/sparse/multi-vector対応、8Kトークン |
| granite-embedding:278m | 278M | 12言語(日本語明記) | IBM製、軽量で高速、日本語サポート明記 |
| nomic-embed-text-v2-moe | 475M(305M active) | 100言語(日本語含む) | MoE構成、MIRACL 65.80、mC4で学習 |
| nomic-embed-text | 137M | 英語のみ | 768次元、8Kコンテキスト、軽量(v1/v1.5は英語専用) |
| mxbai-embed-large | 335M | 英語中心 | 1024次元、検索精度が高い |
# 多言語対応モデルのダウンロード例
ollama pull qwen3-embedding:4b # バランス型(推奨)
ollama pull bge-m3 # 多機能型
ollama pull granite-embedding:278m # 軽量型(日本語明記)
# Embedding生成の例
curl http://localhost:11434/api/embed -d '{
"model": "qwen3-embedding:4b",
"input": "日本語のテキストをベクトル化"
}'
本件の推奨:タイトル・短文・タグの類似検索で多言語対応が必要な場合、qwen3-embedding:4b(精度重視)またはgranite-embedding:278m(軽量・日本語明記)がおすすめ。
2. キーワード検索との併用:ハイブリッド検索の実装
Embedding(ベクトル検索)だけでなく、キーワード検索を併用するハイブリッド検索が効果的だ。特に短いクエリや専門用語の検索ではキーワード検索が有利な場合がある。
ハイブリッド検索の基本構成
スコア統合の方法
# Reciprocal Rank Fusion (RRF) の実装例
def rrf_score(keyword_rank: int, vector_rank: int, k: int = 60) -> float:
"""RRFスコアを計算"""
return 1 / (k + keyword_rank) + 1 / (k + vector_rank)
# 加重平均の例
def weighted_score(keyword_score: float, vector_score: float,
alpha: float = 0.5) -> float:
"""キーワード検索とベクトルスコアの加重平均"""
return alpha * keyword_score + (1 - alpha) * vector_score
3. 全文検索エンジンの比較
オンプレで使える全文検索エンジンを比較する。
| エンジン | 特徴 | メモリ使用量 | 運用難易度 | 推奨ケース |
|---|---|---|---|---|
| Meilisearch | 高速、タイポ耐性、日本語対応、独自ランキング | 低〜中 | ★☆☆(簡単) | 1万件程度の軽量用途 |
| OpenSearch | Elasticsearch互換、BM25、k-NN対応 | 高 | ★★★(複雑) | 大規模・多機能が必要 |
| Elasticsearch | 業界標準、BM25、豊富なエコシステム | 高 | ★★★(複雑) | 既存資産がある場合 |
| Tantivy | Rust製、BM25、軽量高速 | 低 | ★★☆(中程度) | カスタム実装したい場合 |
補足:MeilisearchはBM25ではなく独自のランキングアルゴリズムを使用している。厳密なBM25が必要な場合はOpenSearch/Elasticsearch/Tantivyを検討すること。
1万件でリアルタイム検索なら
Meilisearchがおすすめ。理由は以下の通り。
- インストール・設定が簡単(Docker一発)
- 1万件程度なら十分高速(数ミリ秒で応答)
- 日本語トークナイザー内蔵
- REST APIでAWSからも呼び出しやすい
# Meilisearchのインストール(Docker)
docker run -d --name meilisearch \
-p 7700:7700 \
-v $(pwd)/meili_data:/meili_data \
getmeili/meilisearch:latest
4. AWS サーバーレスとオンプレの連携構成
AWSのサーバーレス構成とオンプレ検索サーバーを連携させる方法を検討する。
構成案
連携パターン
パターン1:Lambda から直接呼び出し
- VPNやDirect Connect経由でオンプレにアクセス
- Lambdaがオンプレの検索APIを呼び出す
- レイテンシ:VPN経由で50〜100ms程度追加
パターン2:API Gateway + オンプレ
- オンプレにAPIサーバーを立て、パブリック公開
- API Gateway経由でルーティング
- 認証はAPI Keyまたは署名検証
# Lambda から Meilisearch を呼び出す例
import os
import requests
def lambda_handler(event, context):
query = event['arguments']['query']
# オンプレの Meilisearch にリクエスト
response = requests.post(
f"{os.environ['MEILI_HOST']}/indexes/articles/search",
json={"q": query, "limit": 20},
headers={"Authorization": f"Bearer {os.environ['MEILI_KEY']}"}
)
return response.json()['hits']
5. データ同期:DynamoDB → オンプレ
DynamoDBのデータをオンプレの検索インデックスに同期する方法。
方法1:DynamoDB Streams + Lambda
# DynamoDB Streams のイベントを処理してオンプレに同期
import os
import requests
MEILI_HOST = os.environ.get('MEILI_HOST', 'http://localhost:7700')
def extract_tags(tags_attr):
"""DynamoDB AttributeValue の List を文字列配列に変換"""
if not tags_attr or 'L' not in tags_attr:
return []
return [tag['S'] for tag in tags_attr['L'] if 'S' in tag]
def sync_to_onprem(event, context):
for record in event['Records']:
if record['eventName'] in ('INSERT', 'MODIFY'):
item = record['dynamodb']['NewImage']
doc = {
'id': item['id']['S'],
'title': item['title']['S'],
'body': item['body']['S'],
'tags': extract_tags(item.get('tags'))
}
# Meilisearch に追加/更新
requests.put(
f"{MEILI_HOST}/indexes/articles/documents",
json=[doc]
)
elif record['eventName'] == 'REMOVE':
doc_id = record['dynamodb']['Keys']['id']['S']
requests.delete(
f"{MEILI_HOST}/indexes/articles/documents/{doc_id}"
)
方法2:定期バッチ同期
- EventBridge で定期実行(5分〜1時間間隔)
- DynamoDB Scan で全件取得し、差分更新
- 1万件なら数秒で完了
6. オンプレ障害時の対応(フォールバック)
オンプレサーバーがダウンした場合の対策は重要だ。
フォールバック戦略
| 戦略 | 実装方法 | コスト |
|---|---|---|
| DynamoDB直接検索 | オンプレ障害時はDynamoDBのScan/Queryで代替 | 低 |
| CloudSearchへフォールバック | AWS CloudSearchを待機系として維持 | 中 |
| キャッシュ活用 | 検索結果をElastiCacheにキャッシュ | 低〜中 |
| オンプレ冗長化 | オンプレサーバーを2台構成 | 中 |
実装例:サーキットブレーカーパターン
# サーキットブレーカーでオンプレ障害を検知
import os
import requests
import boto3
from circuitbreaker import circuit
MEILI_HOST = os.environ.get('MEILI_HOST', 'http://localhost:7700')
@circuit(failure_threshold=3, recovery_timeout=60)
def search_onprem(query: str):
"""オンプレ検索(3回失敗で遮断、60秒後に再試行)"""
return requests.post(
f"{MEILI_HOST}/indexes/articles/search",
json={"q": query}
)
def search_handler(event, context):
query = event['arguments']['query']
try:
# まずオンプレで検索
return search_onprem(query).json()
except Exception:
# フォールバック:DynamoDBで検索
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('articles')
response = table.scan(
FilterExpression='contains(title, :q)',
ExpressionAttributeValues={':q': query}
)
return response['Items']
7. コスト比較
AWS OpenSearch vs オンプレの概算コスト比較(1万件、リアルタイム検索の場合)。
| 項目 | AWS OpenSearch | オンプレ構成 |
|---|---|---|
| 初期費用 | なし | GPU搭載サーバー 15〜30万円 |
| 月額費用 | 約$150〜300/月(t3.small.search×2) | 電気代 約3,000〜5,000円/月 |
| 年間コスト | 約25〜40万円 | 初年度:20〜35万円、2年目以降:4〜6万円 |
| スケーラビリティ | 高(自動スケーリング) | 低(手動増設) |
| 運用負荷 | 低(マネージド) | 高(自己管理) |
結論:1万件程度で長期運用するなら、オンプレは2年で元が取れる計算。ただし運用負荷とのトレードオフ。
8. 推奨構成まとめ
今回の要件(1万件、多言語、リアルタイム、コスト重視)に対する推奨構成。
オンプレサーバー構成
- CPU:Ryzen 5以上
- GPU:RTX 3060 12GB または RTX 4060 Ti 16GB
- メモリ:32GB以上
- ストレージ:NVMe SSD 512GB以上
ソフトウェア構成
- Embedding:Ollama + qwen3-embedding:4b(精度重視)または granite-embedding:278m(軽量)
- 全文検索:Meilisearch(独自ランキング)
- ベクトルDB:pgvector(PostgreSQL)
- API:FastAPI でラップ
AWS連携
- 同期:DynamoDB Streams + Lambda
- 通信:Site-to-Site VPN
- フォールバック:DynamoDB直接検索 + サーキットブレーカー
次のステップ
今後、以下を検証予定。
- Meilisearchの日本語検索精度の実測
- qwen3-embedding / granite-embedding の日本語ベンチマーク
- VPN経由のレイテンシ測定
- フォールバック時のユーザー体験検証
参考リンク
- Ollama - ローカルLLM実行環境
- Ollama Embedding Models
- qwen3-embedding - Ollama
- bge-m3 - Ollama
- granite-embedding - Ollama
- nomic-embed-text-v2-moe - Ollama
- Nomic Embed Text V2 公式ブログ
- Meilisearch Documentation
- Meilisearch Relevancy(ランキングルール解説)
- OpenSearch Documentation
- pgvector - PostgreSQLベクトル拡張
- Tantivy - Rust製全文検索ライブラリ
- Amazon OpenSearch Service 料金
- DynamoDB Streams
- MTEB Leaderboard - Embeddingモデル比較
コメント