こんにちは!
スタンバイアドベントカレンダー3日目は求人検索エンジンの開発を担当する小野がお届けします!
はじめに
スタンバイでは検索エンジンにVespaを利用しています。
Vespaではドキュメントで多次元配列を扱うTensor型が定義されています。
Tensorは柔軟性が高く、機械学習モデルの定義から連想配列のような使い方まで幅広い用途で使用できます。
本記事では、時刻ごとに変動する値をTensorで表現する方法を説明します。
Tensorについて
MappedとIndexed
Tensorの次元タイプは2種類あります。
-
Mapped dimensions
疎(Sparse)な構造で、任意の文字列ラベルでアドレス指定できます。カテゴリ別の売上スコアなど、複数の異なるキーを持つデータに適しています。 -
Indexed dimensions
密(Dense)な構造で、0から始まる数値インデックスを使用します。画像データやembeddingベクトルなど、連続的な数値データに用います。
Tensorの表記
VespaではTensor型を明示的に指定します。
-
ages tensor<float>(fname{},lname{}): {{fname:田中, lname:太郎}:15, {fname:Doe, lname:John}:18}- Mapped dimensionsで年齢を表現 -
images tensor<float>(x[256])- 256個の要素を持つIndexed dimensions(embedding) -
demogra tensor<float>(interest{}, location{}, topic{})- 複数のMapped dimensionsを持つ3次元テンソル
Tensorには柔軟な形式に加えて多くの関数が用意されています。
Vespaコミュニティが提供しているTensor playgroundで手軽に動作確認できるので、ぜひ試してみてください。
Feed時の注意点
Mapped dimensionsは特殊なJSON形式で表現するため、Feed時には以下のように記述します。
{
"age": {
"cells": [
{"address": {"fname": "田中", "lname": "太郎"}, "value": 15},
{"address": {"fname": "Doe", "lname": "John"}, "value": 18}
]
}
}
実践例: ダイナミックプライシング
Mapped dimensionsを用いて、1時間ごとに商品価格が変動する機能を実装します。
実際に動かせるサンプルコードは以下を参照してください。
要件定義
商品の推薦機能で、購入する日時によって価格が異なるダイナミックプライシングを導入しています。
価格はランキングにも用いられるため、1時間おきに自動でドキュメントの価格を変更したいです。
課題
単一フィールドで実現する場合、1時間おきに値を更新する必要があります。
更新頻度の増加やリードタイムが許容されるなら問題ありませんが、指定の時刻で自動的に切り替わる仕組みが望ましいです。
Tensor型での解決方法
Tensor型を使えば、1時間ごとの価格変更を自動で実現できます。
ドキュメントに日時と価格を紐づけたdynamic_pricesフィールドを追加します。
年、月、日、時間の4次元のTensorになっています。
field dynamic_prices type tensor<float>(year{}, month{}, day{}, hour{}) {
indexing: summary | attribute
}
2025-12-07 13:00のタイミングで100円になるサンプルドキュメントをフィードします。
{
"put": "id:mynamespace:product::tensor-sample",
"fields": {
"name": "Tensor Sample",
"default_price": 500,
"dynamic_prices": {
"cells": [
{
"address": {"year": "2025", "month": "12", "day": "7", "hour": "13"},
"value": 100
}
]
}
}
}
dynamic_pricesを利用するにはrank-profileを変更します。
inputsにクライアント側の日時情報を受け取るパラメータを定義します。
inputs {
query(q_year) float
query(q_month) float
query(q_day) float
query(q_hour) float
}
次に、指定日時に一致するdynamic_pricesの値を取得する関数と、一致しない場合にデフォルト価格を返すcurrent_priceを追加します。
function current_price() {
# MEMO: Tensor型で該当する値が存在しない場合に`0`が返却される
expression: if(dynamic_price() > 0, dynamic_price(), default_price)
}
function dynamic_price() {
expression: attribute(dynamic_prices){year: query(q_year), month: query(q_month), day: query(q_day), hour: query(q_hour)}
}
最後に、summary-featuresへ追加してレスポンスで価格を確認できるようにします。
summary-features: current_price() dynamic_price()
これでクライアントが指定した日時の価格を取得できます。
$ vespa query \
'select * from products where true' \
'ranking=default' \
'ranking.features.query(q_year)=2025' \
'ranking.features.query(q_month)=12' \
'ranking.features.query(q_day)=7' \
'ranking.features.query(q_hour)=13'
# 結果
"summaryfeatures": {
"current_price": 100.0,
"dynamic_price": 100.0,
"vespa.summaryFeatures.cached": 0.0
}
応用編
今回紹介したのは基本的な利用例です。
VespaのTensorは柔軟な型と豊富な組み込み関数により、さまざまな機能を実現できます。
Vespa公式のTensor Computation Examplesでは、期間指定での価格変動や機械学習での利用例が紹介されています。
まとめ
- Tensorは多次元構造を扱える
- 細かい時間粒度の価格・スコア調整を 更新なし で実現できる
- ランキングロジックに動的な値を織り込める
- Tensor は辞書・スパース行列・embedding など幅広く使える
スタンバイではVespaを活用して新しいチャレンジをしています。
興味を持った方はお気軽にコメントください。
明日の記事は@0ta2さんです。お楽しみに!