はじめに
前回、Vertex AI Searchを触ってみました。
https://immersed-in-knowing.com/?p=601
お試しだったのでウェブサイト検索をやりましたが、今回はもう少し深く触っていこうと思います。
初期設定については前回のテックブログを参照してください。
全体の構成

Dialogflow CXからチャットで質問をしたら質問した内容をCloud Run経由でVertex AI SearchでJCBの情報を検索をかけるようなシステムを構築します。
Dialogflow CXとは、ウェブサイトやアプリのAIチャットボット作成ツールです。「JCBカードについて教えて」のような質問を理解し、自然な会話で答えます。複雑な質問にも対応可能でお店の受付や問い合わせ対応をAIで自動化し、より人間らしい会話体験を提供します。
Cloud Runは、アプリのコンテナを簡単に動かせる「おまかせサーバー」です。サーバー管理は不要で、アクセスが増えても自動でスケール可能です。ウェブサイトやアプリの裏側処理を、手間なく実行できます。
Vertex AI Searchとは、AIを使った賢い検索サービスです。例えば、会社のたくさんの資料の中から「新しいプロジェクトの進め方」について知りたい時、キーワードだけでなく「プロジェクトを始めるにはどうすればいい?」と質問しても、AIが内容を理解して関連する資料を見つけてくれます。
Vertex AI Searchアプリの作成

Google Cloudにログインします。
Google Cloudのサイドバーのメニューページから「Vertex AI Search」を選択します。
ウェブサイト検索の「作成」を押下します。
Enterprise エディションの機能:チェック入れます
高度なLLM 機能:チェック入れます
アプリ名:jcb-search-web(※任意)
会社名または組織名:immersed-in-knowing(※任意)
マルチリージョン:global
「続行」を押下します。

「データストアを作成」を押下します。
ウェブサイトのコンテンツの「SELECT」を押下します。

追加するサイトに「www.jcb.co.jp/*(※任意)」を入力して「続行」を押下します。
データストア名に「jcb-search-web(※任意)」を入力して「作成」を押下します。

作成したデータストアを選択して「作成」を押下します。

「システム開発」を押下します。
Cloud Shellの実行例の中身を確認します。
curl -X POST -H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "Content-Type: application/json" \
"https://discoveryengine.googleapis.com/v1alpha/projects/796429811784/locations/global/collections/default_collection/engines/jcb-search-web_1745900030546/servingConfigs/default_search:search" \
-d '{"query":"<QUERY>","pageSize":10,"queryExpansionSpec":{"condition":"AUTO"},"spellCorrectionSpec":{"mode":"AUTO"},"languageCode":"ja","userInfo":{"timeZone":"Etc/GMT-9"}}'
オレンジの箇所をメモしておきます。のちほどCloudRunで利用します。(★1)
CloudRunの構築
まずはCloud Functions APIを有効にしていきます。

Cloud Functions APIを検索して上記の画面で「有効にする」で有効化します。

Cloud Functionsを検索をかけます。
「CLOUD FUNCTIONS に移動」を押下して「サービスの作成」から「Cloud Run」を開きます。

「関数を作成」を押下します。
関数に選択が入っていることを確認します。
サービスの名前:jcb-rag-api(※任意)
(検証で作ってしまったのでサービスが既に存在してます)
リージョン:東京(※任意)
認証:未認証の呼び出しを許可
(セキュリティ的に問題ありますが検証なので未認証を許可します)
他の設定:デフォルトでOK
「作成」を押下します。

「jcb-rag-api」を押下します。
「ソース」を開き以下のソースコードを設定します。
package.json
{
"dependencies": {
"@google-cloud/functions-framework": "^3.0.0"
}
}
index.js
const functions = require('@google-cloud/functions-framework');
functions.http('helloHttp', async (req, res) => {
res.setHeader('Content-Type', 'application/json');
const query = req.body?.text;
console.log('ユーザーの入力:', query);
const responseText = await callVertexAISearch(query);
res.send({
fulfillmentResponse: {
messages: [
{
text: {
text: [responseText],
},
},
],
},
});
});
async function callVertexAISearch(query) {
const accessToken = await getAccessToken();
const endpoint = "★1を入力します";
const payload = {
query: query,
pageSize: 1,
queryExpansionSpec: { condition: "AUTO" },
spellCorrectionSpec: { mode: "AUTO" },
languageCode: "ja",
userInfo: { timeZone: "Etc/GMT-9" },
};
try {
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
console.error(`HTTP error! status: ${response.status}`);
const errorBody = await response.text();
console.error("Error body:", errorBody);
return "Vertex AI Searchとの通信でエラーが発生しました。";
}
const data = await response.json();
console.log("Vertex AI Search response:", data);
if (data && data.results && data.results.length > 0 && data.results[0].document && data.results[0].document.derivedStructData) {
const derivedData = data.results[0].document.derivedStructData;
let extractedText = "";
if (derivedData.title) {
extractedText += derivedData.title + "\n";
}
if (derivedData.snippets && derivedData.snippets.length > 0) {
derivedData.snippets.forEach(snippet => {
if (snippet.snippet) {
extractedText += snippet.snippet + "\n";
}
});
}
if (derivedData.pagemap && derivedData.pagemap.metatags && derivedData.pagemap.metatags.length > 0) {
derivedData.pagemap.metatags.forEach(tag => {
if (tag["og:description"]) {
extractedText += tag["og:description"] + "\n";
}
// 他にも必要なメタタグがあればここに追加 (例: og:title)
});
}
if (extractedText) {
console.log("抽出されたテキスト:", extractedText);
return extractedText;
} else {
console.log("JCBに関する情報が見つかりませんでした。");
return "JCBに関する情報は見つかりませんでした。";
}
} else {
console.log("検索結果の構造が予期せぬ形式です。");
return "検索結果の構造が予期せぬ形式です。";
}
} catch (error) {
console.error("Error calling Vertex AI Search:", error);
return "Vertex AI Searchの呼び出しでエラーが発生しました。";
}
}
async function getAccessToken() {
// Cloud Runの環境では、メタデータサーバーからアクセストークンを取得できます。
try {
const response = await fetch("http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token", {
headers: { "Metadata-Flavor": "Google" },
});
if (!response.ok) {
console.error(`Failed to fetch access token: ${response.status}`);
const errorBody = await response.text();
console.error("Error body:", errorBody);
return null; // エラー時は null を返す (上位の処理で適切にハンドリングされるべきですが、ここでは簡略化)
}
const data = await response.json();
return data.access_token;
} catch (error) {
console.error("Error fetching access token:", error);
return null; // エラー時は null を返す (上位の処理で適切にハンドリングされるべきですが、ここでは簡略化)
}
}
★1の設定を忘れずに設定します。
「保存して再デプロイ」を押下してデプロイします。

全部が完了していることを確認します。
またURLをコピーします。(★2)
Dialogflow CXの「Webhook」設定
Dialogflow CXからCloudRunにアクセスするためのwebhookの設定を行います。
Dialogflow CX コンソールに移動します。

プロジェクトを選択して、「Create agent」を選択します。

「jcb-agent」という名前で設定し、「asia-northeast1」、「GMT +9:00」、「ja – japanese」を設定します。
「Create」を押下します。

「Webhooks」を選択して「Create new」を押下します。

Display:「jcb-search-webhook」(※任意)
Webhook timeout:30(※最大値にしておきます)
webhook URL:★2のURLを入れます
上記を入力後、画面の上の方にある「Save」を押下します。
Dialogflow CXの「Intents」設定

メニューの「Intents」を選択して、「Create」を押下します。
Display Nameに「jcb-search(※任意)」を入力します。
Labels(ラベル)は、リソースの分類や管理をしやすくするための任意のメタ情報です。
設定しなくても動作に影響はないので今回は無視します。
Training phrases(トレーニングフレーズ)はユーザーがどんな言い方で話しかけてくるかをDialogflowに「学習させる」ためのものです。
いったん不要で問題ないです。
DTMF(Dual-Tone Multi-Frequency)設定とは、電話のプッシュボタンで送られる電話音声応答(IVR)システムなどで使われるので何もしなくてOKです。
画面上部の「Save」で保存します。
Dialogflow CXの「Flow」設定

「Build」を押下して「Start Page」を開きます。
「jcb-search」を選択して、Enable webhookをONにしてwebhookを選択します。
今回は「jcb-search-webhook」を選択します。
Tagは必須なので「jcb-search-tag」を入力して「Save」で保存します。

Event handlersのdefaultを選択してAgent responsesの内容を削除します。
余計な返信をさせないようにします。
2つとも「Save」で保存をします。
「Test Agent」による検証

右上のほうの「Test Agent」で任意の言葉で検索をかけてみます。
ちゃんと返却されています。