Qlik Answersをqlik-embedで外部サイトに埋め込む
- Shin
- 6 時間前
- 読了時間: 9分
今回は、Qlik Answersをqlik-embedで外部サイトに埋め込む方法をご紹介します。
以下の方法を用いることで、Qlik Answersの機能を搭載したチャットボットを、見た目を自由にカスタマイズして埋め込むことが可能です。
完成イメージ

Qlik Answersとは
Qlik AnswersはQlik Cloudに搭載されている生成AI機能です。 主な機能として、以下の2つが搭載されています。
・ナレッジベース:生成AIに学習させるデータを追加できる機能
・アシスタント:ナレッジベースに基づいたチャットボットを構築できる機能
上記の機能を用いることで、簡単にQlik Cloud上にRAGのチャットボットを構築することができます。
qlik-embedとは
qlik-embedを用いることで、Qlik上で作成したダッシュボードやチャートを、iframeタグを利用せずに外部のサイトに埋め込むことが可能です。
この機能は開発者向けツールとして公開されています。
詳細は以下のサイトをご覧ください。
作成手順
①データの準備
Qlik Cloudにアクセスし、ナレッジベースを作成します。

②アシスタントの作成
作成したナレッジベースを元にしたアシスタントを作成します。

※①②の詳細な手順に関しては以下の記事をご参照ください。
③Webサイトの構築
qlik-embedを用いて、アシスタントを埋め込んだWebサイトを構築します。
ファイル構成
│
├─node_modules
│ 中身は省略
│
├─public
│ index.html
│ scripts.js
│ styles.css
│
├─index.js
│
├─package-lock.json
│
├─package.json
│
└─.gitignore
コード例
index.html
index.htmlで最初に表示されるメッセージや、質問入力ボックス、送信ボタンなどのチャット画面のUIを作成します。
<!doctype html>
<html lang="en">
<head>
<!-- ドキュメントのメタデータと文字エンコーディングを指定 -->
<meta charset="UTF-8" />
<!-- レスポンシブデザインのためのビューポート設定 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- ブラウザのタブに表示されるウェブページのタイトル -->
<title>Qlik Assistant</title>
<!-- ウェブページのスタイリングのための外部スタイルシートへのリンク -->
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<!-- チャットインターフェースのメインコンテナ -->
<div class="chat-container">
<!-- チャットインターフェースのヘッダーセクション -->
<div class="chat-header">
<!-- チャットアシスタントのタイトル -->
<h4>Qlikなんでもチャット</h4>
<!-- アシスタントの機能の説明 -->
<span class="header-span">Qlikについての質問に、AIが回答します。</span>
</div>
<!-- チャットメッセージが表示されるボディセクション -->
<div class="chat-body" id="chat-body">
<!-- アシスタントからのメッセージの例 -->
<div class="message assistant">
<div class="bubble">
<!-- アシスタントの最初の挨拶メッセージ -->
<p>こんにちは!何でも聞いてね!</p>
</div>
</div>
</div>
<!-- 入力フィールドと送信ボタンのあるフッターセクション -->
<div class="chat-footer">
<!-- ユーザーが質問を入力するためのテキスト入力フィールド -->
<input
type="text"
id="chat-input"
placeholder="Qlikについて質問してみましょう…"
/>
<!-- ユーザーの質問を送信するためのボタン -->
<button id="send-btn">送信</button>
</div>
</div>
<!-- インタラクティビティのための外部JavaScriptファイルへのリンク -->
<script src="scripts.js"></script>
</body>
</html>
scripts.js
scripts.jsでフロントエンドのユーザ操作の処理を記述します。
document.addEventListener("DOMContentLoaded", () => {
// DOMツリーが完全に読み込み終わったことを検知
const chatBody = document.getElementById("chat-body"); // チャットメッセージが表示されるボディ
const chatInput = document.getElementById("chat-input"); // 質問を入力するためのテキスト入力フィールド
const sendButton = document.getElementById("send-btn"); // 送信ボタン
// ユーザーメッセージを即座に追加する関数
function appendUserMessage(message) {
const messageDiv = document.createElement("div"); // 新しいメッセージコンテナを作成
messageDiv.classList.add("message", "user"); // クラスを付与してユーザーメッセージであることを示す
const bubbleDiv = document.createElement("div"); // バブルコンテナを作成
bubbleDiv.classList.add("bubble"); // クラスを付与
bubbleDiv.innerHTML = `<p>${message}</p>`; // バブルコンテナにメッセージを挿入
messageDiv.appendChild(bubbleDiv); // メッセージコンテナにバブルコンテナを追加
chatBody.appendChild(messageDiv); // チャットボディにメッセージコンテナを追加
chatBody.scrollTop = chatBody.scrollHeight; // すべてが表示されるようにスクロールを調整
}
// アシスタントのメッセージバブルを作成し、ストリーミングテキストを更新する関数
function createAssistantBubble() {
const messageDiv = document.createElement("div"); // 新しいメッセージコンテナを作成
messageDiv.classList.add("message", "assistant"); // クラスを付与してアシスタントメッセージであることを示す
const bubbleDiv = document.createElement("div"); // バブルコンテナを作成
bubbleDiv.classList.add("bubble"); // クラスを付与
bubbleDiv.innerHTML = "<p></p>"; // 空の段落要素を挿入
messageDiv.appendChild(bubbleDiv); // メッセージコンテナにバブルコンテナを追加
chatBody.appendChild(messageDiv); // チャットボディにメッセージコンテナを追加
chatBody.scrollTop = chatBody.scrollHeight; // すべてが表示されるようにスクロールを調整
return bubbleDiv.querySelector("p"); // 段落要素を返す
}
// 質問をバックエンドに送信し、ストリーミングで回答を受け取る関数
function sendQuestion() {
const question = chatInput.value.trim(); // 入力された質問をトリム(空白除去)して取得
if (!question) return; // 質問が空なら何もしない
// ユーザーメッセージを追加
appendUserMessage(question);
chatInput.value = ""; // 入力フィールドをクリア
// アシスタントメッセージのバブルを作成
const assistantTextElement = createAssistantBubble();
// 回答をストリーミングで受け取るために接続を開く
const eventSource = new EventSource(
`/stream-answers?question=${encodeURIComponent(question)}`, // 質問をエンコードしてクエリパラメータとして送信
);
eventSource.onmessage = function (event) {
if (event.data === "[DONE]") {
eventSource.close(); // ストリーミングが終了したら接続を閉じる
} else {
assistantTextElement.innerHTML += event.data; // 応答データをアシスタントバブルに追加
chatBody.scrollTop = chatBody.scrollHeight; // スクロールを調整
}
};
eventSource.onerror = function (event) {
console.error("EventSource error:", event); // エラー時の処理
eventSource.close(); // 接続を閉じる
assistantTextElement.innerHTML += " [Error receiving stream]"; // エラーメッセージを表示
};
}
// 送信ボタンクリック時に質問を送信するイベントリスナーを追加
sendButton.addEventListener("click", sendQuestion);
// Enterキー押下時に質問を送信するイベントリスナーを追加
chatInput.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault(); // デフォルトのEnterキー動作を無効化
sendQuestion();
}
});
});
styles.css
style.cssでフロントエンドの見た目を記述します。
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Helvetica, Arial, sans-serif;
background-color: #f2f2f2;
padding: 50px;
}
.chat-container {
display: flex;
flex-direction: column;
height: 70vh;
max-width: 600px;
margin: 0 auto;
border: 1px solid #ddd;
background-color: #fff;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.chat-header {
padding: 1rem;
background-color: #4caf50;
color: #fff;
text-align: center;
font-size: 1.5rem;
font-weight: bold;
}
.chat-body {
flex: 1;
padding: 1rem;
overflow-y: auto;
background-color: #e9e9e9;
}
.chat-footer {
display: flex;
padding: 0.5rem;
border-top: 1px solid #ddd;
background-color: #fff;
}
.chat-footer input[type="text"] {
flex: 1;
padding: 0.75rem;
border: 1px solid #ccc;
border-radius: 20px;
outline: none;
font-size: 1rem;
}
.chat-footer button {
margin-left: 0.5rem;
padding: 0.75rem 1rem;
background-color: #4caf50;
border: none;
color: #fff;
border-radius: 20px;
cursor: pointer;
font-size: 1rem;
}
.chat-footer button:hover {
background-color: #45a049;
}
.message {
margin-bottom: 1rem;
display: flex;
position: relative;
max-width: 95%;
}
.message.assistant {
justify-content: flex-start;
}
.message.user {
justify-content: flex-end;
align-self: flex-end;
}
.bubble {
padding: 10px 15px;
border-radius: 20px;
position: relative;
font-size: 1rem;
line-height: 1.4;
word-wrap: break-word;
}
/* アシスタントのバブル(受信メッセージ) */
.message.assistant .bubble {
background-color: #e5e5ea;
color: #000;
border-top-left-radius: 0;
}
.message.assistant .bubble:before {
content: "";
position: absolute;
bottom: 0;
left: -10px;
width: 0;
height: 0;
border-top: 10px solid #e5e5ea;
border-right: 10px solid transparent;
}
/* ユーザーバブル(送信メッセージ) */
.message.user .bubble {
background-color: #007aff;
color: #fff;
border-top-right-radius: 0;
}
.message.user .bubble:after {
content: "";
position: absolute;
bottom: 0;
right: -10px;
width: 0;
height: 0;
border-top: 10px solid #007aff;
border-left: 10px solid transparent;
}
h4 {
margin: 0;
}
.header-span {
font-size: 14px;
}
index.js
index.jsでバックエンドの処理を記述します。 この部分でQlik AnswersのAPIの呼び出しを行っています。
import express from "express"; // Expressライブラリのインポート
import fetch from "node-fetch"; // Node-fetchライブラリのインポート
import path from "path"; // パス操作用ライブラリのインポート
import { fileURLToPath } from "url"; // URL操作用ライブラリのインポート
// ESモジュールのための__dirnameセットアップ
const __filename = fileURLToPath(import.meta.url); // 現在のファイル名を取得
const __dirname = path.dirname(__filename); // ディレクトリ名を取得
// ポートの定義とExpressアプリの初期化
const PORT = process.env.PORT || 3000; // 環境変数PORTが存在する場合はそれを使用、存在しない場合は3000を使用
const app = express(); // Expressアプリの作成
app.use(express.static("public")); // 「public」ディレクトリを静的ファイルとして設定
app.use(express.json()); // JSONパースミドルウェアの使用
// フロントエンドの提供
app.get("/", (req, res) => { // ルートパスのGETリクエストに応答
res.sendFile(path.join(__dirname, "public", "index.html")); // 「public/index.html」を返す
});
// Qlik Answers出力をストリームで返すエンドポイント
app.get("/stream-answers", async (req, res) => { // 「/stream-answers」パスのGETリクエストに応答
const question = req.query.question; // 質問クエリパラメータの取得
if (!question) { // 質問がない場合
res.status(400).send("No question provided"); // 400エラーを返す
return;
}
// ストリーミング応答のヘッダー設定
res.writeHead(200, {
"Content-Type": "text/event-stream", // コンテンツタイプをイベントストリームに設定
"Cache-Control": "no-cache", // キャッシュを無効に設定
Connection: "keep-alive", // 接続を保持する
});
const assistantId = "<Qlik AnswersのアシスタントID>";
const baseUrl = "https://<テナントのURLから引用>.qlikcloud.com/api/v1/assistants/"; // 基本URLの定義
const bearerToken = process.env["QlikApiKey"]; // 環境変数からBearerトークンを取得
try {
// 新しいスレッドを作成
const createThreadUrl = `${baseUrl}${assistantId}/threads`; // スレッド作成URLの定義
const threadResponse = await fetch(createThreadUrl, { // スレッド作成リクエストを送信
method: "POST", // POSTメソッドの使用
headers: { // リクエストヘッダーの設定
"Content-Type": "application/json", // コンテンツタイプの設定
Authorization: `Bearer ${bearerToken}`, // 認証ヘッダーの設定
},
body: JSON.stringify({ // リクエストボディの設定
name: `Conversation for question: ${question}`, // スレッド名の設定
}),
});
if (!threadResponse.ok) { // スレッド作成に失敗した場合
const errorData = await threadResponse.text();
res.write(`data: ${JSON.stringify({ error: errorData })}\n\n`); // エラーデータをストリームに出力
res.end();
return;
}
const threadData = await threadResponse.json(); // スレッド応答のJSONをパース
const threadId = threadData.id; // スレッドIDの取得
// Qlik Answersストリーミングエンドポイントを呼び出し
const streamUrl = `${baseUrl}${assistantId}/threads/${threadId}/actions/stream`; // ストリーミングURLの定義
const invokeResponse = await fetch(streamUrl, { // ストリーミングリクエストを送信
method: "POST", // POSTメソッドの使用
headers: { // リクエストヘッダーの設定
"Content-Type": "application/json", // コンテンツタイプの設定
Authorization: `Bearer ${bearerToken}`, // 認証ヘッダーの設定
},
body: JSON.stringify({ // リクエストボディの設定
input: {
prompt: question, // 質問の設定
promptType: "thread", // プロンプトタイプの設定
includeText: true, // テキストを含める設定
},
}),
});
if (!invokeResponse.ok) { // ストリーミングに失敗した場合
const errorData = await invokeResponse.text();
res.write(`data: ${JSON.stringify({ error: errorData })}\n\n`); // エラーデータをストリームに出力
res.end();
return;
}
// 応答テキストの処理とストリーム出力
const decoder = new TextDecoder(); // テキストデコーダーの設定
for await (const chunk of invokeResponse.body) { // 応答ボディをストリームで処理
let textChunk = decoder.decode(chunk); // チャンクをデコード
let parts = textChunk.split(/(?<=\})(?=\{)/); // チャンクを分割
for (const part of parts) {
let trimmedPart = part.trim();
if (!trimmedPart) continue;
try {
const parsed = JSON.parse(trimmedPart); // パースを試みる
if (parsed.output && parsed.output.trim() !== "") {
res.write(`data: ${parsed.output}\n\n`); // メッセージ部分をストリームに出力
}
} catch (e) {
if (trimmedPart && !trimmedPart.startsWith('{"sources"')) {
res.write(`data: ${trimmedPart}\n\n`); // 生のメッセージ部分をストリームに出力
}
}
}
}
res.write("data: [DONE]\n\n"); // 完了メッセージをストリームに出力
res.end();
} catch (error) {
res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
res.end();
}
});
// バックエンドサーバーのスタート
app.listen(PORT, () => {
console.log(`Backend running on port ${PORT}`);
});
このように、qlik-embedを用いて実装することで、UIや機能を様々にカスタマイズしながら、Qlik Answersの機能を活用することができます。
参考)
Comments