はじめに
SAP HANA CloudでEmbedding(テキストをベクトルに変換したもの)を生成できる機能が2024 Q4にリリースされました。これにより、SAP HANA Cloudに格納した情報を自然言語のクエリで検索するというようなことができます。取得した情報をもとに回答を成形するには外部のLLMが必要になりますが、「関連する情報を取ってくる」ところまではSAP HANA Cloudで完結します。
この記事では、以下のSAPブログを参考にCAPでEmbeddingを作成し、Similarity Search(類似性検索)を実行する方法について説明します。
前提
HANA Cloudの設定でNatural Language Processing (NLP)を有効化します。
CAP
コードは以下のリポジトリに格納しています。
データモデル
以下のデータモデルを定義します。質問のカテゴリ(category)と質問内容(questionText)を結合したものをベクトル化したものをquestionEmbeddingとして格納します。最新のモデルであるSAP_GXY.20250407(※)は日本語をサポートしています。
questionEmbeddingは計算項目なので、storedというキーワードをつけてDBに保存されるようにします。
※利用可能なモデルは以下を参照
Available Models and Versions
namespace questions;
using {managed} from '@sap/cds/common';
entity Questions : managed {
key ID : UUID;
category : String(100) enum {
BTP_GENERAL;
CLOUD_FOUNDRY;
CAP;
FIORI_ELEMENTS;
RAP;
UI5;
OTHER;
};
questionText : String(1000);
answerText : String(1000);
embeddingSource : String = (
'category: ' || category || ', question: ' || questionText
);
questionEmbedding : Vector = VECTOR_EMBEDDING(
embeddingSource, 'DOCUMENT', 'SAP_GXY.20250407'
) stored;
}
サービスモデル
データモデルで定義したQuestionsをサービスに公開します。Vector型はOData V4でサポートされないため、questionEmbeddingは非公開にしています。
アクションgetSimilarQuestionsはカテゴリと質問、取得件数を指定して類似検索を実行します。アクションdeleteAllQuestionsはすべての質問を削除するユーティリティ機能です。
using {questions as db} from '../db/schema';
service FAQService {
entity Questions as
projection on db.Questions
excluding {
questionEmbedding
};
action getSimilarQuestions(category: String @mandatory,
question: String @mandatory,
topN: Integer) returns array of {
ID : UUID;
category : String(100);
questionText : String(1000);
answerText : String(1000);
similarityScore : String;
}
action deleteAllQuestions() returns String;
}
イベントハンドラ
イベントハンドラを以下のように実装します。アクションgetSimilarQuestionsでは、cosine_similarityというファンクションを使って質問をベクトルに変換したものと、DBに格納されているquestionEmbedding,を比較しています。
const cds = require('@sap/cds')
const { SELECT } = require('@sap/cds/lib/ql/cds-ql')
module.exports = class FAQService extends cds.ApplicationService {
async init() {
const db = await cds.connect.to('db')
const { Questions } = db.entities
this.on('getSimilarQuestions', async (req) => {
const { question, category, topN = 3 } = req.data
console.log(req.data)
const questionString = `category: ${category}, question: ${question}`
const results = await SELECT.from(Questions)
.columns `ID,
category,
questionText,
answerText,
cosine_similarity(
questionEmbedding, to_real_vector(
vector_embedding(
${questionString}, 'QUERY', 'SAP_GXY.20250407'
)
)
)as similarityScore`
.orderBy`similarityScore desc`
.limit(topN)
return results
})
this.on('deleteAllQuestions', async (req) => {
await db.run(DELETE.from(Questions))
return 'All questions have been deleted.'
})
return super.init()
}
}
サンプル質問
以下のサンプル質問を用意しました。
ID,category,questionText,answerText
1,BTP_GENERAL,"SAP BTP(Business Technology Platform)は何に使われますか?","SAP BTPは、クラウド上でビジネスアプリケーションを開発、統合、拡張するための統合プラットフォームです。データベース、分析、アプリ開発、統合、AIなどの機能を組み合わせて提供します。"
2,CLOUD_FOUNDRY,"CAPアプリケーションをCloud Foundry環境にデプロイするにはどうすればよいですか?","CAPアプリは、`mbt build`でMTAをビルドし、`cf push`でCloud Foundryにデプロイします。`mta.yaml`または`manifest.yml`でモジュール・リソースを定義し、必要なサービスをバインドしてください。"
3,CAP,"CAPプロジェクトの`srv/`フォルダには何を配置しますか?","`srv/`フォルダにはサービス定義(`.cds`)と実装ロジック(Node.jsなら`.js`/`.ts`、Javaならハンドラクラス)を配置します。ビジネスロジックやイベントハンドラを記述する場所です。"
4,CAP,"CAP Node.jsで独自のバリデーションロジックを追加するには?","`before`ハンドラで検証します。例:`this.before('CREATE', 'Books', req => { if(!req.data.title) req.error('Title is required'); });`"
5,FIORI_ELEMENTS,"Fiori Elementsアプリで、ユーザー権限に応じてアクションの表示・非表示を切り替えるには?","CAPのサービスで`@requires`やロールベースの認可を定義します。認可されないアクションはODataで公開されず、Fiori Elements側でも自動的に非表示になります。"
6,RAP,"SAP開発におけるCAPとRAPの違いは何ですか?","CAPはBTP上でNode.js/Javaによりクラウドネイティブアプリを開発するモデル、RAPはABAP CloudやS/4HANA上でABAPによりODataサービスを構築するモデルです。"
7,UI5,"SAPUI5アプリケーションでCAPのODataサービスを利用するには?","`manifest.json`の`sap.app.dataSources`でサービスURLを定義し、`models`でモデルを宣言します。コントロールは`{path: '/Books'}`のようにバインディングしてデータを表示します。"
8,CAP,"CAP Javaでドラフト対応エンティティを扱うには?","CAP JavaはFioriドラフトを標準サポートします。`@Before`/`@After`ハンドラでREADやSAVEイベントに処理を追加できます。ドラフトには`IsActiveEntity`フラグが含まれます。"
9,CLOUD_FOUNDRY,"Cloud FoundryでCAPサービスをSAP HANA Cloudインスタンスにバインドするには?","`cf create-service`でHANA Cloudインスタンスを作成し、`cf bind-service`でアプリにバインドします。CAP側では`cds.requires.db`がバインドしたサービス名と一致している必要があります。"
10,OTHER,"CAPアプリケーションをクラウドにデプロイせずにローカルでテストするには?","`cds watch`でローカル開発サーバーを起動します。CAPはデフォルトでSQLiteをサポートしており、HANAへ切り替える前にローカルDBで動作確認できます。"
CAPアプリケーションをCloud Foundryにデプロイすると、questionEmbeddingが自動的に計算されて設定されます。

テスト
cds bind 2 <HDIコンテナ>によりHDIコンテナのサービスインスタンスをバインドした後、cds watch --profile hybridでハイブリッドモードによる実行をします。
###
POST {{server}}/odata/v4/faq/getSimilarQuestions
Content-Type: application/json
{
"question": "CAPをローカル環境でテストする方法は?",
"category": "CAP",
"topN": 3
}
以下の結果が返ってきました。
{
"@odata.context": "$metadata#Collection(FAQService.return_FAQService_getSimilarQuestions)",
"value": [
{
"ID": "10",
"answerText": "`cds watch`でローカル開発サーバーを起動します。CAPはデフォルトでSQLiteをサポートしており、HANAへ切り替える前にローカルDBで動作確認できます。",
"category": "OTHER",
"questionText": "CAPアプリケーションをクラウドにデプロイせずにローカルでテストするには?",
"similarityScore": 0.7638473254244234
},
{
"ID": "4",
"answerText": "`before`ハンドラで検証します。例:`this.before('CREATE', 'Books', req => { if(!req.data.title) req.error('Title is required'); });`",
"category": "CAP",
"questionText": "CAP Node.jsで独自のバリデーションロジックを追加するには?",
"similarityScore": 0.6561137337027667
},
{
"ID": "6",
"answerText": "CAPはBTP上でNode.js/Javaによりクラウドネイティブアプリを開発するモデル、RAPはABAP CloudやS/4HANA上でABAPによりODataサービスを構築するモデルです。",
"category": "RAP",
"questionText": "SAP開発におけるCAPとRAPの違いは何ですか?",
"similarityScore": 0.6543961554764786
}
]
}

