Qlik Answers transforms unstructured data into clear, AI-powered insights. Today, I'll show you how to integrate Qlik Answers directly into your web app using the newly releasedKnowledgebases API and Assistants API.
In this blog, we'll build a custom Football chat assistant from scratch powered by Qlik Answers.
We’ll leverage the Assistants API to power real-time Q&A while the knowledge base is already set up in Qlik Sense.
For those of you who prefer a ready-made solution, you can quickly embed the native Qlik Answers UI using qlik-embed:
<qlik-embed
ui="ai/assistant"
assistant-id="<assistant-id>"
></qlik-embed>
You can explore the ai/assistant parameters (and other UIs available in qlik-embed) on qlik.dev, or take a look at some of my previous blog posts here and here.
For full documentation on the Knowledgebases API and Assistants API, visitqlik.dev/apis/rest/assistants/ and qlik.dev/apis/rest/knowledgebases/.
Let’s dive in and see how you can take control of your Qlik Answers UI experience!
What Are Qlik Answers Assistants and Knowledgebases?
Before we start building our DIY solution, here’s a quick refresher:
Knowledgebases:Collections of individual data sources (like HTML, DOCX, TXT, PDFs) that power your Qlik Answers. (In our case, we built the KB in Qlik Sense!)
Assistants:The chat interface that interacts with users using retrieval-augmented generation (RAG). With generative AI in the mix, Qlik Answers delivers reliable, linked answers that help drive decision-making.
DIY the Qlik Answers Experience
Step 1: Get your data ready
Since we already created our knowledge base directly in Qlik Sense, we skip the Knowledgebases API. If you’d like to build one from scratch, check out the knowledgebases API documentation.
Step 2: Configure your assistant
With your knowledge base set, you create your assistant using the Assistants API. This is where the magic happens: you can manage conversation starters, customize follow-ups, and more. Visit the assistants API docs on qlik.dev. to learn more
Step 3: Build Your Custom UI
Now, let’s look at our custom chat UI code. We'll built a simple football-themed chat interface that lets users ask questions related to the NFL. The assistant’s answers stream in seamlessly to the interface.
HTML:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Football Assistant</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="chat-container">
<div class="chat-header">
<h4>Let's talk Football</h4>
<span class="header-span">You ask, Qlik answers.</span>
</div>
<div class="chat-body" id="chat-body">
<div class="message assistant">
<div class="bubble">
<p>Hey there, champ! Ask me anything.</p>
</div>
</div>
</div>
<div class="chat-footer">
<input
type="text"
id="chat-input"
placeholder="Type your Football related question..."
/>
<button id="send-btn">Send</button>
</div>
</div>
<script src="scripts.js"></script>
</body>
</html>
Frontend JS:
document.addEventListener("DOMContentLoaded", () => {
const chatBody = document.getElementById("chat-body");
const chatInput = document.getElementById("chat-input");
const sendButton = document.getElementById("send-btn");
// Append a user message immediately
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;
}
// Create an assistant bubble that we update with streaming text
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");
}
// Send the question to the backend and stream the answer
function sendQuestion() {
const question = chatInput.value.trim();
if (!question) return;
// Append the user's message
appendUserMessage(question);
chatInput.value = "";
// Create an assistant bubble for the answer
const assistantTextElement = createAssistantBubble();
// Open a connection to stream the answer
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);
chatInput.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
sendQuestion();
}
});
});
Backend node.js script:
import express from "express";
import fetch from "node-fetch";
import path from "path";
import { fileURLToPath } from "url";
// Setup __dirname for ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Define port and initialize Express app
const PORT = process.env.PORT || 3000;
const app = express();
app.use(express.static("public"));
app.use(express.json());
// Serve the frontend
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "public", "index.html"));
});
// Endpoint to stream Qlik Answers output
app.get("/stream-answers", async (req, res) => {
const question = req.query.question;
if (!question) {
res.status(400).send("No question provided");
return;
}
// Set headers for streaming response
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
const assistantId = "b82ae7a9-9911-4830-a4f3-f433e88496d2";
const baseUrl = "https://sense-demo.us.qlikcloud.com/api/v1/assistants/";
const bearerToken = process.env["apiKey"];
try {
// Create a new conversation thread
const createThreadUrl = `${baseUrl}${assistantId}/threads`;
const threadResponse = await fetch(createThreadUrl, {
method: "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();
const threadId = threadData.id;
// Invoke the Qlik Answers streaming endpoint
const streamUrl = `${baseUrl}${assistantId}/threads/${threadId}/actions/stream`;
const invokeResponse = await fetch(streamUrl, {
method: "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;
}
// Process and stream the response text
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();
}
});
// Start the backend server
app.listen(PORT, () => {
console.log(`Backend running on port ${PORT}`);
});
Breaking It Down
Okay, that was a lot of code! Let’s break it down into bite-sized pieces so you can see exactly how our custom Qlik Answers chat interface works.
1. The HTML
Our index.html creates a custom chat UI. It sets up:
A chat body where messages appear (initially with a friendly greeting from the assistant).
A chat footer with an input field and a send button for users to type their questions.
2. The Frontend JavaScript (scripts.js)
This script handles the user interaction:
Appending messages: When you type a question and hit send (or press Enter), your message is added to the chat window.
Creating chat bubbles: It creates separate message bubbles for you (the user) and the assistant.
Streaming the answer: It opens a connection to our backend so that as soon as the assistant’s response is ready, it streams into the assistant’s bubble. This gives you a live, real-time feel without any manual “typing” effect.
3. The Node.js Backend (index.js)
Our backend does the heavy lifting:
Creating a conversation thread: It uses the Assistants API to start a new thread for each question.
Invoking the streaming endpoint: It then sends your question to Qlik Answers and streams the response back.
Processing the stream: As chunks of text come in, the backend cleans them up—splitting any concatenated JSON and only sending the useful text to the frontend.
Closing the stream: Once the complete answer is sent, it signals the end so your chat bubble doesn’t wait indefinitely.
4. How It All Connects
When you send a question:
Your message is displayed immediately in your custom chat bubble.
The backend creates a thread and requests an answer from Qlik Answers.
The response is streamed back to your UI in real time, making it look like the assistant is typing out the answer as it arrives.
P.S: this is just a simple example to introduce you to the new Answers APIs and show you how to get started using them, you'll need to double check limitations and adhere to best practices when using the APIs in a production environment.
You can find the full code here:https://replit.com/@ouadielimouni/QA-Test-APIs#public/index.html
Happy coding - and, Go Birds 🦅!
...View More
Several years ago, I blogged about how creating a synthetic dimension using ValueList allowed us to color dimensions in a chart. ValueList is commonly used where there is not a dimension in the data model to use, thus creating a synthetic one with ValueList. You can read more about ValueList in mypreviousblog post. In this blog post, I am going to share how I used ValueList to handle omitted dimension values in a chart.
I recently ran into a scenario when creating visualizations based on survey data. In the survey, the participant was asked for their age as well as their age group. The ages were grouped into the following buckets:
Under 18
18-24
25-34
35-44
45-54
55-69
70+
Once I loaded the data, I realized that there were not participants for all the age groups, so my chart looked like the bar chart below. There was a bar and value for only the age groups that the participants fit in.
While I could leave the chart like this, I wanted to show all the age group buckets in the chart so that it was evident that there were no participants (0%) in the other age group buckets. In this example, the four age groups were consecutive, so it did not look odd to leave the chart as is but imagine if there were no participants in the 45-54 age bucket. The chart may look odd with the gap between 44 and 55.
I explored various ways to handle this. One way was to add rows to the respective table for the missing age group. This worked fine but I was not a fan of adding rows to the survey table that were not related to a specific participant. The option that I settled on was using ValueList to add the omitted age groups. While this option works well, it can lead to lengthy expressions for the measures. In this example, there were only seven age group buckets so it was manageable but if you had many dimensions values then it may not be the best option.
To update the bar chart using ValueList, I changed the dimension from
To
Then I changed the measure from
To
Using ValueList in the dimension created a synthetic dimension with each age group option that was included in the survey. Now I will see all the age buckets in the chart even if there were no participants that fell in the age group bucket. Since I am using ValueList for the dimension, I need to update the measure to use it as well. This is where a single line measure can become a lengthier measure because I need to create a measure for every value in the synthetic dimension, thus the nested if statement above. The result looks like this:
There are no gaps in the age buckets, and we can see all the age bucket options that were presented in the survey. I prefer this chart over the first bar chart I shared because I have a better understanding of the survey responses presented to the participants as well as the response they provided. I would be interested in hearing how others have handled similar scenarios.
Thanks,
Jennell
...View More
If you have been learning about Qlik AutoML or looking for examples to get started, you might have only came across Binary Classification problems (such as Customer churn, Employee retention etc…). In this post, we will be solving a different type of problem with Qlik AutoML using a Regression model.
What is Regression, and Why Does It Matter?
Regression is a type of supervised learning used to predict continuous outcomes like housing prices, sales revenue, or stock prices. In industries such as real estate, understanding the factors driving prices can guide better decision-making. For example, predicting house values based on income levels, population, and proximity to the ocean helps realtors and developers target key markets and optimize pricing strategies.
In the upcoming sections, we go through how to build and deploy a regression model using Qlik AutoML to predict house prices using the common California Housing Dataset.
Step 1: Defining the Problem
Before creating the AutoML experiment, let’s define the core elements of our use case:
Trigger: New houses or listing entries are added to the dataset.
Target: Predict the house's value.
Features: Latitude, longitude, median age, total rooms, total bedrooms, population, households, median income, and proximity to the ocean.
Step 2: AutoML
The California Housing dataset is split into Training (historical) housing_train.csv and Apply (new) housing_test.csv data files.
Start by uploading these files to your Qlik Cloud tenant.
(The files are attached at the end of the blog post)
Creating the AutoML Experiment
Start a New Experiment:
In your Qlik Cloud tenant, click onCreate → ML Experiment
Select Your Dataset:
Choosehousing_train.csvas your dataset. AutoML will automatically identify columns as features and recommend their types.
Set the Target Variable:
Choosemedian_house_valueas the target for prediction.
Ensure all relevant features are selected, and adjust any feature types if needed.
Run the Experiment:
Click Run Experiment and let AutoML analyze the data. After a few minutes, you'll see the initial results, including SHAP values and model performance metrics.
You can also take a look at the Compare and Analyze tabs for more advanced details.
Deploying the AutoML Model
Choose the top-performing model from the experiment results.
Click on Deploy
Creating Predictions
Once in the Deployment screen, add the Apply dataset, create a Prediction, and make sure to select SHAP and Coordinate SHAP as files to be generated. We will use these later on in our Qlik Sense Analytics app to gain explainabilityinsights.
Step 3: Creating the Qlik Sense Analytics App
Now it’s time to visualize the predictions:
Load the Predictions:
Navigate to the Catalog and locate the newly created Housing_test_Prediction.parquetfile. Click Create Analytics App.
Add additional data, including SHAP and Coordinate SHAP files as well as the apply dataset.
Build the Dashboard:
Create visualizations such as:
A SHAP ranking to highlight the most influential features.
A histogram showing the distribution of predicted house values.
A map with gradient colors to visualize house prices by location.
You can experiment with different visualization types to explore the data from multiple perspectives.
Understanding the results:
Based on the Qlik AutoML model, we can clearly see how features like income levels and ocean proximity can influence housing prices.
For more inspiration on how you can use your predictions within your Qlik Sense Apps or in your embedded use cases, check out my previous blog posts:
Building What-If Scenarios using SSE and the Qlik AutoML Prediction API
Exploring Qlik AutoML Real-time Predictions API
...View More
The tab container, formerly the container object, has many new features that allow developers to style the container tabs. In this blog, I will review some of these new features but first let’s do a quick review of what the tab container is and how you can use it. The tab container can be used to show many visualizations, one at a time, thus using less space on a sheet. It is ideal when there is the need to have many visualizations on a single sheet but not quite enough space to display them all. When using a tab container, tabs or menu items are selected to navigate through the visualizations. Tabs and visualizations can also be displayed based on a variable or the user’s access. Below is an example from the What’s New app.
There are many ways a visualization can be added to a container object. A visualization on the sheet or a master visualization can be dragged and dropped onto the tab container object, or a chart can be added from the content section of the tab container. When tabs are toggled on, a tab will exist for each visualization in the tab container.
Some of the new features of the tab container are around the tabs. For example, the font, font size, width, alignment and color for each tab can now be set by the developer. Here are the style settings for the KPI chart tab (Tab container > Content > KPI object > Styling > Tab).
Notice that the width of the tab can be set based on a percentage or pixels. This allows the developer to make accommodations for larger tab labels that may need more width to be fully visible. The background color can be set to a single color, or an expression can be used. This can be done for each tab in the tab container object. Each tab can also have a different font type and/or font size, if desired, although a consistent look and feel (font type and font size) among all tabs is ideal. Note that when the orientation of the tabs is vertical, the width of the tab cannot be set. These styles can also be applied from the styling section of the tab container (Tab container > Appearance > Presentation > Styling > Tabs). Applying a background color here, will apply it to the entire object and all tabs. From here you can also set the table label alignment to left, center or right.
Another new feature is the orientation of the tab container tabs can now be vertical. Notice in the image below how the top chart shows just the icons of the tab, and the bottom chart shows just the labels. Both are neat and clean.
When there are many tabs in a tab container, you may consider showing the menu. This is helpful when all tabs are not visible without scrolling. It provides another way to navigate the tab container.
The added tab container features allow developers to easily style the object so that it fits in better with the theme/style of the sheet it is on. This is a welcomed new feature.
Thanks,
Jennell
...View More
🎁🎄🎅 At Qlik, listening to our customers and users is at the heart of everything we do. For those of you who missed the ability to click on the Qlik logo or found it a bit quirky having all app assets, especially Sheets and Bookmarks, under a single button, we have great news! 🎉 Here’s an early Christmas gift from the dedicated teams atQlik– just for you. 🙇 Enjoy!
The new Navigation Menu object in Qlik allows for a flexible and easy way to add a menu to your Qlik Sense apps. Whether you're designing an app in Qlik Sense or embedding Qlik capabilities into a web platform, you should make this new object your go-to solution for organized and stylish navigation.
In this blog post, we'll explore into the new Navigation Menu object, its features, customization options, embedding capabilities, and key considerations for getting the most out of it. For a quick overview, check out the SaaS in 60 video below:
Navigation Menu in Qlik Sense Apps
Up until now, navigation in your apps has been limited to the following options:
Navigating through the built in Sheets tab in the assets panel
Custom built extensions
Or, in-app, using existing objects:
Using buttons:
Using the layout container in addition to buttons to create custom sidebars:
The new Navigation Menu object enhances your Qlik Sense app usability and makes it easier to achieve similar results faster tailored to your needs
Key Features:
Sheet Organization: Automatically arranges sheets and groups into a hierarchy for intuitive navigation.
Flexible Layouts: Choose from vertical, horizontal, or drawer-style menus, adapting to your app’s design needs.
Custom Styling: Personalize fonts (family, size, weight, and color), background images, align items, and hover and highlight effects. You can also toggle icons or resize the buttons.
Mobile-Optimized: Enable a list view for seamless navigation on mobile devices.
Integration-Friendly: Pair with "sheet title off" and selection bar for a minimalistic yet functional design.
You can toggle the "App Navigation bar > Navigation" or "Sheet Header" option to OFF in the UI settings and have the Navigation Menu object replace your traditional Sheets for a more minimalistic look.
👀 You can see the Navigation Menu object in action on the What’s New App:https://explore.qlik.com/app/4911b1af-2f3c-4d8a-9fd5-1de5b04d195c
Navigation Menu embedded in your Web Apps
For developers looking to incorporate Qlik analytics into their web applications, the Navigation Menu object can save time when developing a custom sheet navigation. You can easily embed the navigation menu object and customize it to meet your needs. (Learn more here)
How to Embed the Navigation Menu
Here’s a simple example of embedding a horizontal navigation menu in your web app using Nebula.js.
You can read more about Embedding and access the full documentation on qlik.dev:
const nuked = window.stardust.embed(app, {
context: { theme: "light" },
types: [
{
name: "sn-nav-menu",
load: () => Promise.resolve(window["sn-nav-menu"]),
},
],
});
nuked.render({
type: "sn-nav-menu",
element: document.querySelector(".menu"),
properties: {
layoutOptions: {
orientation: "horizontal",
alignment: "top-center",
},
},
});
Advanced Customization
Using JSON properties, you can customize the navigation menu extensively:
Adjust the orientation, alignment, and layout.
Add drawer functionality for compact navigation.
Style hover and highlight effects, font colors, and background images.
Align menu items to the left, center, or right for a consistent look.
These capabilities make the Navigation Menu a versatile tool for developers working on embedded analytics projects.
nuked.render({
type: "sn-nav-menu",
element: document.querySelector(".menu"),
properties: {
"layoutOptions": {
"drawerMode": false,
"hideDrawerIcon": false,
"orientation": "horizontal",
"layout": "fill",
"alignment": "top-center",
"separateItems": false,
"dividerColor": {
"color": "rgba(0,0,0,0.12)",
"index": -1
},
"largeItems": false,
"showItemIcons": false
},
"components": [
{
"key": "general"
},
{
"key": "theme",
"content": {
"fontSize": "18px",
"fontStyle": {
"bold": true,
"italic": false,
"underline": false,
"normal": true
},
"defaultColor": {
"index": 15,
"color": "#000000",
"alpha": 1
},
"defaultFontColor": {
"color": "#ffffff",
"alpha": 1
},
"highlightColor": {
"index": -1,
"color": "#3ba63b",
"alpha": 1
},
"highlightFontColor": {
"color": "#ffffff",
"alpha": 1
},
"hoverColor": {
"index": -1,
"color": "#ffa82e",
"alpha": 1
},
"borderRadius": "20px"
}
}
]
},
navigation: sheetObject.navigation,
});
Things to watch out for
While the Navigation Menu object is a fantastic addition, there are some key points to consider:
Content Security Policy (CSP): If you use background images from external URLs, ensure their origins are added to your tenant’s CSP allowlist. This step is essential for compliance and functionality.
Hierarchy Management: Group sheets effectively in your Qlik app to create a logical navigation structure.
Mobile Responsiveness: Test the menu thoroughly on various devices to ensure an optimal user experience, especially when using the list view.
Design Consistency: Align the menu’s styling with the rest of your app for a unified look and feel.
...View More
We’re excited to announce the launch of our enhanced Demo Site - explore.qlik.com, designed to showcase the full potential of Qlik’s product portfolio in an interactive and engaging way!
DBeaver is a popular and powerful SQL editor available built as an Eclipse Rich Client Platform (RCP) just like Qlik Talend Studio. The DBeaver Plugin is also available in the Eclipse Marketplace so it can be incorporated directly into your Studio environment. This document provides a step-by-step guide to extend Talend Studio with DBeaver.