この記事は TVer Advent Calendar 2025 18日目の記事です。
はじめに
Backend Enabling Team の小林 (@k0bya4) です。
TVer のサービスユーザー向けメインバックエンドシステムは AWS で構築していますが、サブシステムとして全文検索・ベクトル検索・グラフクエリなどの機能を、バックエンドエンジニアが RDB の知識を活かしながら運用できないか検証しています。Spanner はこれらの機能を 1 つのデータベースで提供しており、今回は Google Cloud が提供するサンプルデータセットを使って、30 分ほどでクイックにその使用感を確認してみました。
Spanner Graph が 2025年1月に GA となりました。Spanner は従来からリレーショナルデータベースとしての機能を持っていますが、グラフクエリ(GQL)、全文検索、ベクトル検索といった機能が追加され、1つのデータベースで多様なワークロードに対応できるようになっています。
本記事では、Retail サンプルデータセットを使って、全文検索、ベクトル検索、グラフクエリによるレコメンドを実際に動かしてみます。サンプルデータセットを使えば、スキーマ設計やデータ投入なしですぐに試すことができます。
環境構築
本記事では Google Cloud が提供する Retail サンプルデータセットを使用します。これは EC サイトを模したデータで、ユーザー、商品、注文などのテーブルがあらかじめ用意されています。
Retail サンプルには Spanner Graph、全文検索、ベクトル検索を実行するための構文とサンプルクエリが含まれているため、Enterprise エディション以上のインスタンスが必要です。
インスタンスの作成からサンプルデータセットの投入まで、こちらの記事が参考になります。構築を試す際には参照してください。
本記事では retail データベースが作成済みの前提で進めます。
Retail データセットの概要
Retail サンプルは EC サイトを模したデータセットで、以下のテーブルで構成されています。
| テーブル名 | 役割 | グラフでの扱い |
|---|---|---|
| Users | ユーザー情報 | Node |
| Products | 商品情報 | Node |
| Orders | 注文情報 | Node |
| Payments | 支払い情報 | Node |
| Addresses | 住所情報 | Node |
| OrderItems | 注文と商品の関連 | Edge(Orders → Products) |
| ShoppingCarts | カートと商品の関連 | Edge(Users → Products) |
全文検索の設定
Spanner の全文検索には複数の種類があります。Retail サンプルでは TOKENIZE_FULLTEXT を使った基本的な全文検索が設定されています。TOKENIZE_FULLTEXT は自然言語テキストを単語単位でトークン化し、キーワード検索に適しています。
Products テーブルには商品名をトークン化した列が定義されています。
Name_Tokens TOKENLIST AS (TOKENIZE_FULLTEXT(Name)) HIDDEN
この列に対して Search Index が作成されており、SEARCH() 関数でキーワード検索ができます。全文検索の詳細は公式ドキュメントを参照してください。
ベクトル類似度検索の設定
Products テーブルには商品の埋め込み表現のベクトルが格納されています。埋め込み表現は Vertex AI の Embedding API などで生成でき、テキストや画像の意味的な特徴を数値ベクトルとして表現したものです。
ProductEmbedding ARRAY<FLOAT64>(vector_length=>768)
大量のベクトル同士の類似度計算を高速化するため、Vector Index が設定されています。
CREATE VECTOR INDEX ProductVectorIndex ON Products(ProductEmbedding) WHERE ProductEmbedding IS NOT NULL OPTIONS (distance_type = 'COSINE');
Property Graph の定義
グラフクエリを実行するには、テーブルを Node や Edge として定義した Property Graph が必要です。Retail サンプルでは以下のように定義されています。
CREATE PROPERTY GRAPH ECommerceGraph
NODE TABLES (
Users,
Products,
Orders,
Payments,
Addresses
)
EDGE TABLES (
OrderItems
SOURCE KEY (OrderID) REFERENCES Orders
DESTINATION KEY (ProductID) REFERENCES Products,
ShoppingCarts
SOURCE KEY (UserID) REFERENCES Users
DESTINATION KEY (ProductID) REFERENCES Products
);
Node は既存のテーブルをそのまま指定します。Edge では SOURCE KEY と DESTINATION KEY で接続元・接続先の Node を指定します。
OrderItems Edge は「どの注文にどの商品が含まれるか」、ShoppingCarts Edge は「どのユーザーがどの商品をカートに入れているか」という関係を表現しています。
Property Graph の詳細は公式ドキュメントを参照してください。
全文検索を試す
全文検索を使って商品名からキーワード検索してみましょう。Retail サンプルには以下のようなクエリが用意されています。
SELECT ProductID, Name, PriceUSD FROM Products WHERE SEARCH(Name_Tokens, 'phone') ORDER BY PriceUSD DESC LIMIT 5;
SEARCH() 関数の第一引数にはトークン化された列(Name_Tokens)、第二引数には検索キーワードを指定します。このクエリは「phone」を含む商品を価格の高い順に 5 件取得します。
実行結果:
| ProductID | Name | PriceUSD |
|---|---|---|
| 201414 | Smith-Allen Mobile Phone | 1086.15 |
| 197251 | Lutz-Howard Phone | 974.94 |
| 301629 | Garza-Rogers Cell Phone Plus 69 | 908.03 |
| 581741 | Brock, Phone | 830.75 |
| 377370 | Adkins, Cell Phone Plus 58 | 734.93 |
AND / OR 検索
複数キーワードでの検索も可能です。スペース区切りで AND 検索、OR 演算子で OR 検索になります。
AND 検索(両方を含む):
SELECT ProductID, Name, PriceUSD FROM Products WHERE SEARCH(Name_Tokens, 'cell phone') ORDER BY PriceUSD DESC LIMIT 5;
| ProductID | Name | PriceUSD |
|---|---|---|
| 301629 | Garza-Rogers Cell Phone Plus 69 | 908.03 |
| 377370 | Adkins, Cell Phone Plus 58 | 734.93 |
| 865179 | Jones, Cell Phone Pro 43 | 709.21 |
| 407419 | Farley, Cell Phone | 625.71 |
| 988662 | Johnson Cell Phone Plus 28 | 395.94 |
OR 検索(いずれかを含む):
SELECT ProductID, Name, PriceUSD FROM Products WHERE SEARCH(Name_Tokens, 'cell OR phone') ORDER BY PriceUSD DESC LIMIT 5;
| ProductID | Name | PriceUSD |
|---|---|---|
| 201414 | Smith-Allen Mobile Phone | 1086.15 |
| 197251 | Lutz-Howard Phone | 974.94 |
| 301629 | Garza-Rogers Cell Phone Plus 69 | 908.03 |
| 581741 | Brock, Phone | 830.75 |
| 377370 | Adkins, Cell Phone Plus 58 | 734.93 |
AND 検索では「Cell Phone」を両方含む商品のみ、OR 検索では「Mobile Phone」のように片方のみ含む商品も結果に含まれています。
日本語テキストの検索
全文検索は日本語にも対応しています。テストデータで確認してみましょう。
INSERT INTO Products (ProductID, Name, Description, PriceUSD) VALUES (99999901, 'スマートフォン Pro Max', 'Latest smartphone', 999.99); SELECT ProductID, Name FROM Products WHERE SEARCH(Name_Tokens, 'スマートフォン');
| ProductID | Name |
|---|---|
| 99999901 | スマートフォン Pro Max |
「スマートフォン」ではヒットしますが、「スマート」や「フォン」ではヒットしませんでした。公式ドキュメントによると、日本語は自動でセグメンテーションされます。分割の粒度は内部の辞書に依存するため、検索対象のテキストに応じてどのようなキーワードでヒットするかを事前に確認しておくと良いでしょう。
部分文字列での検索が必要な場合は、TOKENIZE_NGRAMS や TOKENIZE_SUBSTRING の使用を検討してください。TOKENIZE_NGRAMS は文字列を N 文字単位で分割してトークン化するため、「スマート」のような部分文字列でもヒットするようになります。
トークンの分割結果は DEBUG_TOKENLIST() 関数で確認できます。
SELECT Name, DEBUG_TOKENLIST(Name_Tokens) AS tokens FROM Products WHERE ProductID = 99999901;
| Name | tokens |
|---|---|
| スマートフォン Pro Max | スマートフォン(boundary), pro, max(end_boundary) |
「スマートフォン」が1つのトークンとして扱われているため、部分文字列の「スマート」や「フォン」ではヒットしません。
ベクトル類似度検索を試す
ベクトル類似度検索を使って、ある商品に似た商品を検索してみましょう。
まず、埋め込み表現のベクトルを持つ商品を確認します。
SELECT ProductID, Name, ARRAY_LENGTH(ProductEmbedding) AS embedding_dim FROM Products WHERE ProductID = 123;
| ProductID | Name | embedding_dim |
|---|---|---|
| 123 | High-end Smartphone Model X | 768 |
ProductID 123 は「High-end Smartphone Model X」で、768次元の埋め込みベクトルを持っています。この商品を基準に、他の商品とのコサイン距離を計算します。COSINE_DISTANCE() はコサイン距離を返し、値が小さいほど類似度が高いことを示します。
WITH ReferenceProduct AS ( SELECT ProductEmbedding FROM Products WHERE ProductID = 123 ) SELECT p.ProductID, p.Name, COSINE_DISTANCE(rp.ProductEmbedding, p.ProductEmbedding) AS distance FROM Products p, ReferenceProduct rp WHERE p.ProductID != 123 AND p.ProductEmbedding IS NOT NULL ORDER BY distance LIMIT 5;
実行結果:
| ProductID | Name | distance |
|---|---|---|
| 748564 | Gonzales, Phone | 0.160337 |
| 733052 | Mathews Smartphone Pro 16 | 0.160598 |
| 789 | Premium Wireless Headphones | 0.167190 |
| 476417 | Dixon, Mobile Phone Pro 37 | 0.179702 |
| 865179 | Jones, Cell Phone Pro 43 | 0.185011 |
基準の商品「High-end Smartphone Model X」に対して、Phone や Smartphone といった類似カテゴリの商品が上位に並んでいます。
グラフクエリでレコメンドを試す
グラフクエリを使って、よく一緒に購入される商品を見つけてみましょう。「この商品を買った人はこんな商品も買っています」のようなレコメンド機能の基礎となる分析です。
SQL での分析
まず、公式サンプルに含まれる SQL 版のクエリを実行します。
SELECT p1.ProductID AS product1_id, p1.Name AS product1_name, p2.ProductID AS product2_id, p2.Name AS product2_name, COUNT(*) AS frequency FROM OrderItems oi1 JOIN OrderItems oi2 ON oi1.OrderID = oi2.OrderID AND oi1.ProductID < oi2.ProductID JOIN Products p1 ON oi1.ProductID = p1.ProductID JOIN Products p2 ON oi2.ProductID = p2.ProductID GROUP BY p1.ProductID, p1.Name, p2.ProductID, p2.Name ORDER BY frequency DESC LIMIT 5;
| product1_id | product1_name | product2_id | product2_name | frequency |
|---|---|---|---|---|
| 270555 | Roy, Camping Chair | 733052 | Mathews Smartphone Pro 16 | 6 |
| 205907 | Silva-Navarro Smart Yoga Mat | 974628 | Quinn-Burton Deluxe Sleeping Bag | 5 |
| 182627 | Peterson, mobile device | 838797 | Conley Smartphone | 5 |
| 662275 | Wood-Oneal Smartphone Max 79 | 759176 | Moreno mobile device Max 10 | 5 |
| 789 | Premium Wireless Headphones | 733052 | Mathews Smartphone Pro 16 | 5 |
同じ注文に含まれる商品ペアを集計しています。自己結合と複数の JOIN が必要で、クエリがやや複雑です。
GQL での分析
同じ分析を GQL で書くと、関係をパターンとして直感的に表現できます。
GRAPH ECommerceGraph MATCH (p1:Products)<-[:OrderItems]-(o:Orders)-[:OrderItems]->(p2:Products) WHERE p1.ProductID < p2.ProductID RETURN p1.ProductID AS ProductID1, p1.Name AS Name1, p2.ProductID AS ProductID2, p2.Name AS Name2, COUNT(*) AS frequency GROUP BY ProductID1, Name1, ProductID2, Name2 ORDER BY frequency DESC LIMIT 5;
| ProductID1 | Name1 | ProductID2 | Name2 | frequency |
|---|---|---|---|---|
| 270555 | Roy, Camping Chair | 733052 | Mathews Smartphone Pro 16 | 6 |
| 205907 | Silva-Navarro Smart Yoga Mat | 974628 | Quinn-Burton Deluxe Sleeping Bag | 5 |
| 182627 | Peterson, mobile device | 838797 | Conley Smartphone | 5 |
| 662275 | Wood-Oneal Smartphone Max 79 | 759176 | Moreno mobile device Max 10 | 5 |
| 789 | Premium Wireless Headphones | 733052 | Mathews Smartphone Pro 16 | 5 |
MATCH 句の (p1:Products)<-[:OrderItems]-(o:Orders)-[:OrderItems]->(p2:Products) で「注文を介して繋がる2つの商品」という関係を1行で表現しています。SQL 版で必要だった自己結合や複数の JOIN が不要になり、データの関係性が読み取りやすくなります。
まとめ
Retail サンプルデータセットを使って、Spanner の 3 つの機能を試しました。
| 機能 | 用途 | 使用例 |
|---|---|---|
| 全文検索 | キーワードによる検索 | 商品名から「phone」を含む商品を検索 |
| ベクトル類似度検索 | 意味的に似たアイテムの検索 | ある商品に似た商品を検索 |
| グラフクエリ | 関係性をたどる分析 | よく一緒に購入される商品を検索 |
これらの機能は 1 つのデータベースに統合されており、ユースケースに応じて使い分けたり組み合わせたりできます。
Retail サンプルには本記事で紹介した以外にもクエリが用意されています。ぜひ他のクエリも試してみてください。
TVer では今後、実際のサービスデータを想定した検証を進めていく予定です。