こんにちは、アドプラットフォーム事業を担当する部署でアプリケーションエンジニアとして働く、おおしまと申します!
4月から2年目に突入しましたが、優秀な後輩がたくさん入ってきて、頑張らないとなという気持ちになっています😂
およそ半年前にいわゆる新卒エントリーを執筆したので、興味のある方はぜひご一読いただければ幸いです。
本記事では、グラフDBを使って新しい広告配信システムのプロトタイプを作った話をしたいと思います!
なぜ作ることになった?
そもそもなぜこのプロトタイプを作ることになったかについて、簡単に説明したいと思います。
話は入社前にまで遡ります。まずはグラフDBに興味を持ったきっかけから。
私は農学系出身なのですが、グラフを使って遺伝子やタンパク質同士の働きを表現する場面が授業の中で登場し、これがきっかけでグラフ構造に興味を抱くようになりました。
そして自分でグラフについて調べていく中で、グラフDBというものがあることを知り、さらに興味を持つようになっていました。
ただ研究ではこれらを触ることはなく、趣味でグラフ表示用のライブラリを使って遊んでいる程度でした。(参考:pyvis)
入社後、研修の一環として、技術職(エンジニア・デザイナー)として入社したメンバーはスライドを用いた自己紹介を行うこととなりました。
毎年恒例となっているこの発表会では、自己紹介と共に3年後の目標を発表するのですが、グラフDBへの熱が冷めていなかった私は「グラフDBを使った広告配信システムを作る」という目標を発表しました。
それからも、部署内でグラフDBを題材としたLTの発表を行ったり、関連記事を読むなどして、あの時に発表したものを実際に作ってみたいという気持ちが高まっていきました。
そんな状況の中、弊社では四半期ごとに個人OKRという目標を各個人で立てて通常業務と共に取り組んでいるのですが、2023年最初の四半期は部署の方針として技術研鑽につながるような目標を立てることとなりました。
これはチャンスだ!と思い、グラフDBを使った広告配信システムのプロトタイプ作成を、四半期の目標として立てました💪
グラフDBとは
そもそもグラフDBってなんじゃそりゃという方に向けて、簡単に説明したいと思います。
グラフDBとは、データをプロパティ(属性)を持ったノード(点)とエッジ(線)の集合として表現するデータベースのことです。
行列では表しにくい、繋がりの表現が重視される場面(SNSでのフォロー関係など)でよく使われます。(参考:グラフデータベースとは?)
少し前になりますがパナマ文章の分析に使われたことで有名となり、つながりのある膨大な情報を処理するのに有効であることが示されています。(参考:パナマ文書で注目されたNeo4jとグラフデータベースの未来とは?--CEOに聞く)
このような、つながりを表現することに長けているという特性を広告表示に活かせないかと考え、プロトタイプを作成することにしました!
今回作成したアプリケーション
概要
今回作ったアプリケーションでは、ユーザーが見ている記事の内容によって表示される広告が変わるという仕組みを検討しました。
現在インターネット広告は、ユーザーの閲覧履歴や検索履歴などから表示されるものが決定されるターゲティング広告が主流となっています。
しかし昨今のプライバシー保護意識の高まりや関連法の改正によって、このようなユーザーの個人情報を用いた手法は困難になりつつあります。
そのため、コンテキストマッチ広告というユーザーが見ているコンテンツの内容に合わせた広告を表示する手法が注目されています。(参考:コンテキストマッチ広告)
今回はデータのつながりを表現するグラフ構造が、そうしたコンテンツと広告の結び付けを表現するのにマッチするのではないかと考え、グラフDBを用いた広告表示システムのプロトタイプを作成することにしました。
構成
開発では、まずローカル環境を構築し、その後AWSの各リソースを用いてリモート環境を構築しました。
ローカル
ローカル環境では、以下のような構成で開発を行いました。
大きくは、フロントエンドをVue.jsで、バックエンドをExpressを用いて構築しています。
またデータベースには、グラフDB界隈で有名なNeo4jを使用しました。(参考:オープンソースのデータベース/Neo4jとは)
後ほど示しますが、Neo4jには標準でGUI(ブラウザを通して利用)が付属しているため、とても開発がしやすいです!
ExpressからNeo4jにクエリを投げる際には、openCypherという言語を用いました。(参考:Cypherクエリの基礎 2020 #neo4j)
こちらはMySQLに対するSQLのようなものですが、文法としてSQLと近しい部分が多く、初心者でもとっつきやすいものになっています。(他に有名なものとしてGremlin、SPARQLなどがあります)(参考:グラフデータベースを選ぶ際に考慮すべき16のこと(前編))
- フロントエンド
- 言語:TypeScript
- ライブラリ:Vue.js
- ルーティング:Vue Router
- 状態管理:Pinia
- ビルドツール:Vite
- 単体テスト:Vitest
- E2Eテスト:Cypress
- モック:Mock Service Worker(MSW)
- バックエンド
- 言語:TypeScript
- フレームワーク:Express
- データベース
- DB:Neo4j
- クエリ言語:openCypher
リモート
基本的にはローカル環境をそのままAWS上に移植した形になりますが、データベースにはNeo4jと互換性のあるAmazon Neptuneを使用しました。
NeptuneとはAWSが提供するマネージドのグラフDBサービスであり、MySQLに対するAmazon Auroraのようなものです。
構成は雑ですので、温かい目で見ていただければ幸いです😂
- フロントエンド
Viteで出力したものをS3に配置しました。 - バックエンド
ExpressがEC2上で動作するようにしました。 - データベース
Neptuneを使用し、バックエンドから投げられるクエリを受け付けるようにしました。
openCypher
今回使用したopenCypherの文法について、実際に使ったクエリを例に説明したいと思います。
まずは、キーワードと広告の関係を表現するデータを作成するクエリを見てみましょう。
[1] キーワード(広告を検索する際に使う単語)を表現するノードを定義する
# 以下の構造を持つデータを作成できるようにする(SQLのCREATEに相当) # keyword_idがプライマリな、Keywordプロパティを持つノードnを定義 CREATE CONSTRAINT keyword_id FOR (n:Keyword) # keyword_idにunique属性を付与 REQUIRE n.keyword_id IS UNIQUE
[2] キーワードのノードを作成する
# nameプロパティが"computer"、keyword_idが"1"であるノードnを作成(SQLのINSERTに相当) CREATE (n:Keyword {name: "computer", keyword_id: "1"})
[3] 広告を表現するノードを定義する
# Adプロパティを持つノードmを定義 CREATE CONSTRAINT ad_id FOR (m:Ad) REQUIRE m.ad_id IS UNIQUE
[4] 広告のノードを作成する
# titleが"computer"、hrefに広告クリック時に遷移させるページURL及びsrcに広告画像のURLを持つノードnを作成 CREATE (n:Ad {title: "computer", href: "https://ja.wikipedia.org/wiki/%E3%83%91%E3%83%BC%E3%82%BD%E3%83%8A%E3%83%AB%E3%82%B3%E3%83%B3%E3%83%94%E3%83%A5%E3%83%BC%E3%82%BF", src: "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a6/Portable_eteint.png/231px-Portable_eteint.png", ad_id: "1"})
[5] 特定のキーワードのノードと広告のノードを結びつけるエッジを作成する
# 以下の操作で、キーワードに関連する広告を取得できるようにする(ノード同士を結びつける) # Keywordプロパティを持つノードaとAdプロパティを持つノードbを紐付けるエッジを作成 # デカルト結合に関するエラーを回避するため、それぞれでMATCHを行う MATCH (a:Keyword{keyword_id:"1"}) MATCH (b:Ad{ad_id:"1"}) CREATE (a)-[:Relation]->(b)
このようにSQLに近い構文で、データを作成することができるようになっています。
実際にデータが追加されているかを下記コマンドで確認してみましょう。
# 全てのデータを取得する
MATCH (n) RETURN n
無事追加されていることがわかりました!
次に、キーワードに紐づいた広告を取得するクエリを見てみましょう。
# 以下の構造を持つデータを返す(SQLのSELECTに相当) # (Keywordというプロパティを持つノードn)→(Relationというプロパティを持つエッジ)→(Adというプロパティを持つノードm) MATCH (n:Keyword)-[:Relation]->(m:Ad) # nameプロパティが"computer"であるノードnを取得 WHERE n.name = "computer" # Adとしてノードmを返す RETURN m AS Ad
このように、キーワードに紐づいた広告を取得することができました🙌
Neo4jでは各クエリの実行結果をグラフィカルな状態で表示してくれるので、とてもわかりやすいですね!
広告表示の仕組み
クエリを用いてキーワードに紐づいた広告を取得することはできるようなったので、続いてはアプリケーション側の動きも見ておきましょう。
今回のアプリケーションでは、各ページのmetaタグに記載されているキーワードに沿った広告を表示することを目指しました。
以下が広告表示をするまでの大きな流れとなります。
[1] 各ページのmetaタグにキーワード(keywords
プロパティの値)を設定する
// Vue Routerの設定に広告キーワードが設定されている const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/best_computer', name: 'best_computer', component: () => import('../views/BestComputer.vue'), // keywordsプロパティに表示してほしい広告のキーワードが設定されている meta: { title: 'おすすめPC', keywords: 'computer' } }, ... ] });
[2] ユーザーがページを閲覧した際に、metaタグに設定されているキーワードを送信する
export const fetchAd = async (data: Record<string, string>): Promise<AdData> => { const parameters = { keyword: data.keywords }; const origin = '${サーバーのOrigin}'; const queryParameters = new URLSearchParams(parameters); // エンドポイント/adにkeywordパラメータ付きでGETリクエストを送信 const response = await fetch(origin + '/ad?' + queryParameters, { method: 'GET' }); const json = await response.json(); const result = json._fields[0].properties; return result; };
[3] フロントエンドから送られたキーワードをもとにopenCypherのクエリを発行し、グラフDBから広告を取得する
export const getAd = async (keyword: string) => { const session = driver.session(); // 広告取得クエリを発行 const query = `MATCH (n:Keyword)-[:Relation]->(m:Ad) WHERE n.name = '${keyword}' RETURN m AS Ad`; // クエリを実行 const result = await session.run(query).finally(async () => await session.close()); // 1つ目の結果を返す const record = result.records[0]; return record; };
[4] 取得した広告を表示する!
実際には以下のような形で表示されます🙌
まとめ
今回は、グラフDBであるNeo4jを用いて、キーワードに紐づいた広告を表示するアプリケーションを作成しました。
本例では、グラフDBの特性を活かせるほどのデータ量ではなかったため、同様のアプリケーションをリレーショナルDBで作成することも可能だと思います。
ただし紐付けが大量になったり複雑になればなるほど、活かせるようになるのではないかと思います。
また今回は、metaタグのキーワードをもとに広告を表示するという単純な仕組みでしたが、記事から自然言語処理によってキーワードを生成し、これをもとに広告を表示するという仕組みも作成できるのではないかと思います。
そんなサービスを立ち上げることになった際には、今回の知見を活かすことができればと密かに思っています😁
ではまたどこかで!