Enhancing NICE Cognigy.AI Responses with Algolia Search via Node.js
What You Will Build
- A Node.js webhook service that intercepts Cognigy.AI query intents, executes faceted Algolia searches with language and location filters, applies custom ranking with click-through rate data, injects results into Cognigy context variables, serves cached results on timeout, and logs search latency.
- This integration uses the Cognigy.AI Webhook Trigger and the Algolia Search REST API via the
algoliasearchSDK. - The implementation covers Node.js 18+ with Express, modern async/await patterns, and production-ready error handling.
Prerequisites
- Node.js 18 or higher
expressandalgoliasearchnpm packages- Cognigy.AI webhook endpoint configured with a shared secret
- Algolia index containing attributes:
language,location,ctr_score,relevance_score,title,description - Algolia API key with
searchpermission - Required Cognigy configuration: Webhook trigger set to POST, payload format set to JSON, timeout threshold configured in Cognigy chatflow
Authentication Setup
Cognigy.AI validates webhook requests using a shared secret. You must verify the request signature before processing. Algolia requires an Application ID and a search-only API key. Store these in environment variables.
// .env
COGNIGY_WEBHOOK_SECRET=your_shared_secret_here
ALGOLIA_APP_ID=your_algolia_app_id
ALGOLIA_API_KEY=your_algolia_search_api_key
ALGOLIA_INDEX_NAME=knowledge_base_v2
Load environment variables securely. Use dotenv for local development and platform secrets in production.
require('dotenv').config();
const COGNIGY_SECRET = process.env.COGNIGY_WEBHOOK_SECRET;
const ALGOLIA_APP_ID = process.env.ALGOLIA_APP_ID;
const ALGOLIA_API_KEY = process.env.ALGOLIA_API_KEY;
const ALGOLIA_INDEX = process.env.ALGOLIA_INDEX_NAME;
Implementation
Step 1: Webhook Endpoint and Request Validation
Cognigy.AI sends a POST request to your webhook URL. The payload contains user input, detected intent, and current context variables. Validate the request using HMAC-SHA256 to prevent unauthorized calls.
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
function validateCognigySignature(req) {
const signature = req.headers['x-cognigy-signature'];
if (!signature || !COGNIGY_SECRET) {
return false;
}
const payload = JSON.stringify(req.body);
const hmac = crypto.createHmac('sha256', COGNIGY_SECRET);
hmac.update(payload);
const calculatedSignature = hmac.digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(calculatedSignature));
}
app.post('/webhook/algolia-search', (req, res) => {
if (!validateCognigySignature(req)) {
return res.status(401).json({ error: 'Invalid webhook signature' });
}
const { userMessage, context, intent } = req.body;
if (!userMessage || !context) {
return res.status(400).json({ error: 'Missing userMessage or context payload' });
}
if (intent !== 'query_intent') {
return res.status(400).json({ error: 'Webhook only handles query_intent' });
}
// Proceed to search logic
handleSearchRequest(userMessage, context).then(result => {
res.json(result);
}).catch(err => {
console.error('Webhook processing failed:', err);
res.status(500).json({ error: 'Internal processing error' });
});
});
Expected Cognigy request body structure:
{
"userMessage": "pricing for enterprise tier",
"context": {
"userLanguage": "en",
"userLocation": "us-east-1",
"sessionId": "sess_8829103"
},
"intent": "query_intent"
}
Step 2: Algolia Faceted Search Construction
Initialize the Algolia client and construct a faceted search request. Filter by language and location to ensure region-specific results. Use facets to retrieve distribution data if needed for downstream logic.
const algoliasearch = require('algoliasearch');
const client = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_API_KEY);
const index = client.initIndex(ALGOLIA_INDEX);
async function buildAlgoliaQuery(query, language, location) {
const searchParams = {
query: query,
facets: ['language', 'location'],
filters: `language:${language} AND location:${location}`,
hitsPerPage: 5,
page: 0,
attributesToRetrieve: ['title', 'description', 'url', 'ctr_score', 'relevance_score'],
attributesToHighlight: ['title', 'description'],
highlightPreTag: '<em>',
highlightPostTag: '</em>',
responseFields: ['hits', 'processingTimeMS', 'query']
};
return searchParams;
}
Step 3: Custom Ranking, CTR Integration and Retry Logic
Algolia allows custom ranking via the ranking parameter. Combine default relevance with click-through rate data by using customRanking to prioritize high-CTR documents. Implement exponential backoff for 429 rate limit responses.
const MAX_RETRIES = 3;
const BASE_DELAY_MS = 200;
async function executeAlgoliaSearch(searchParams, attempt = 1) {
try {
const result = await index.search(searchParams.query, {
...searchParams,
customRanking: ['desc(relevance_score)', 'desc(ctr_score)', 'desc(popularity)']
});
return result;
} catch (error) {
if (error.status === 429 && attempt < MAX_RETRIES) {
const delay = BASE_DELAY_MS * Math.pow(2, attempt - 1);
console.warn(`Algolia 429 rate limit hit. Retrying in ${delay}ms (attempt ${attempt})`);
await new Promise(resolve => setTimeout(resolve, delay));
return executeAlgoliaSearch(searchParams, attempt + 1);
}
throw error;
}
}
Step 4: Timeout Handling, Cache Fallback and Latency Logging
Webhooks must respond quickly. Cognigy.AI terminates connections after a configurable timeout. Implement a Promise.race pattern to enforce a strict timeout. Serve cached results for popular queries when the index times out. Log latency for performance tuning.
const nodeCache = require('node-cache');
const searchCache = new nodeCache({ stdTTL: 300, checkperiod: 60 });
const SEARCH_TIMEOUT_MS = 2500;
async function handleSearchRequest(userMessage, context) {
const startTime = Date.now();
const cacheKey = `search:${userMessage}:${context.userLanguage}:${context.userLocation}`;
// Check cache first
const cachedResult = searchCache.get(cacheKey);
if (cachedResult) {
const latency = Date.now() - startTime;
console.log(`Cache hit for query. Latency: ${latency}ms`);
return formatCognigyResponse(cachedResult, 'cache');
}
const searchParams = await buildAlgoliaQuery(
userMessage,
context.userLanguage || 'en',
context.userLocation || 'global'
);
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Algolia search timeout')), SEARCH_TIMEOUT_MS)
);
try {
const result = await Promise.race([
executeAlgoliaSearch(searchParams),
timeoutPromise
]);
const latency = Date.now() - startTime;
console.log(`Search completed. Latency: ${latency}ms. Hits: ${result.hits.length}`);
// Cache successful result
searchCache.set(cacheKey, result.hits);
return formatCognigyResponse(result.hits, 'live');
} catch (error) {
const latency = Date.now() - startTime;
console.warn(`Search fallback triggered. Latency: ${latency}ms. Reason: ${error.message}`);
// Serve popular cached queries on timeout or failure
const popularKey = `popular:${context.userLanguage || 'en'}`;
const popularResults = searchCache.get(popularKey) || [];
return formatCognigyResponse(popularResults, 'fallback');
}
}
Step 5: Cognigy Context Injection
Cognigy.AI expects a specific JSON structure to update context variables. Map Algolia hits to an array of objects and inject them into the context payload. Preserve existing context variables to prevent data loss.
function formatCognigyResponse(hits, source) {
const formattedHits = hits.map(hit => ({
title: hit._highlightResult?.title?.value || hit.title,
description: hit._highlightResult?.description?.value || hit.description,
url: hit.url,
relevanceScore: hit.relevance_score,
ctrScore: hit.ctr_score
}));
return {
context: {
algoliaResults: formattedHits,
searchSource: source,
searchTimestamp: new Date().toISOString(),
resultCount: formattedHits.length
},
response: {
text: source === 'fallback'
? 'Showing recent popular results while our search service recovers.'
: `Found ${formattedHits.length} results for your query.`
}
};
}
Complete Working Example
require('dotenv').config();
const express = require('express');
const crypto = require('crypto');
const algoliasearch = require('algoliasearch');
const nodeCache = require('node-cache');
const COGNIGY_SECRET = process.env.COGNIGY_WEBHOOK_SECRET;
const ALGOLIA_APP_ID = process.env.ALGOLIA_APP_ID;
const ALGOLIA_API_KEY = process.env.ALGOLIA_API_KEY;
const ALGOLIA_INDEX = process.env.ALGOLIA_INDEX_NAME;
const SEARCH_TIMEOUT_MS = 2500;
const MAX_RETRIES = 3;
const BASE_DELAY_MS = 200;
const app = express();
app.use(express.json());
const client = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_API_KEY);
const index = client.initIndex(ALGOLIA_INDEX);
const searchCache = new nodeCache({ stdTTL: 300, checkperiod: 60 });
function validateCognigySignature(req) {
const signature = req.headers['x-cognigy-signature'];
if (!signature || !COGNIGY_SECRET) return false;
const payload = JSON.stringify(req.body);
const hmac = crypto.createHmac('sha256', COGNIGY_SECRET);
hmac.update(payload);
const calculatedSignature = hmac.digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(calculatedSignature));
}
async function buildAlgoliaQuery(query, language, location) {
return {
query: query,
facets: ['language', 'location'],
filters: `language:${language} AND location:${location}`,
hitsPerPage: 5,
page: 0,
attributesToRetrieve: ['title', 'description', 'url', 'ctr_score', 'relevance_score'],
attributesToHighlight: ['title', 'description'],
highlightPreTag: '<em>',
highlightPostTag: '</em>',
responseFields: ['hits', 'processingTimeMS', 'query']
};
}
async function executeAlgoliaSearch(searchParams, attempt = 1) {
try {
return await index.search(searchParams.query, {
...searchParams,
customRanking: ['desc(relevance_score)', 'desc(ctr_score)', 'desc(popularity)']
});
} catch (error) {
if (error.status === 429 && attempt < MAX_RETRIES) {
const delay = BASE_DELAY_MS * Math.pow(2, attempt - 1);
console.warn(`Algolia 429 rate limit hit. Retrying in ${delay}ms (attempt ${attempt})`);
await new Promise(resolve => setTimeout(resolve, delay));
return executeAlgoliaSearch(searchParams, attempt + 1);
}
throw error;
}
}
function formatCognigyResponse(hits, source) {
const formattedHits = hits.map(hit => ({
title: hit._highlightResult?.title?.value || hit.title,
description: hit._highlightResult?.description?.value || hit.description,
url: hit.url,
relevanceScore: hit.relevance_score,
ctrScore: hit.ctr_score
}));
return {
context: {
algoliaResults: formattedHits,
searchSource: source,
searchTimestamp: new Date().toISOString(),
resultCount: formattedHits.length
},
response: {
text: source === 'fallback'
? 'Showing recent popular results while our search service recovers.'
: `Found ${formattedHits.length} results for your query.`
}
};
}
async function handleSearchRequest(userMessage, context) {
const startTime = Date.now();
const cacheKey = `search:${userMessage}:${context.userLanguage}:${context.userLocation}`;
const cachedResult = searchCache.get(cacheKey);
if (cachedResult) {
const latency = Date.now() - startTime;
console.log(`Cache hit for query. Latency: ${latency}ms`);
return formatCognigyResponse(cachedResult, 'cache');
}
const searchParams = await buildAlgoliaQuery(
userMessage,
context.userLanguage || 'en',
context.userLocation || 'global'
);
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Algolia search timeout')), SEARCH_TIMEOUT_MS)
);
try {
const result = await Promise.race([
executeAlgoliaSearch(searchParams),
timeoutPromise
]);
const latency = Date.now() - startTime;
console.log(`Search completed. Latency: ${latency}ms. Hits: ${result.hits.length}`);
searchCache.set(cacheKey, result.hits);
return formatCognigyResponse(result.hits, 'live');
} catch (error) {
const latency = Date.now() - startTime;
console.warn(`Search fallback triggered. Latency: ${latency}ms. Reason: ${error.message}`);
const popularKey = `popular:${context.userLanguage || 'en'}`;
const popularResults = searchCache.get(popularKey) || [];
return formatCognigyResponse(popularResults, 'fallback');
}
}
app.post('/webhook/algolia-search', (req, res) => {
if (!validateCognigySignature(req)) {
return res.status(401).json({ error: 'Invalid webhook signature' });
}
const { userMessage, context, intent } = req.body;
if (!userMessage || !context) {
return res.status(400).json({ error: 'Missing userMessage or context payload' });
}
if (intent !== 'query_intent') {
return res.status(400).json({ error: 'Webhook only handles query_intent' });
}
handleSearchRequest(userMessage, context).then(result => {
res.json(result);
}).catch(err => {
console.error('Webhook processing failed:', err);
res.status(500).json({ error: 'Internal processing error' });
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Cognigy-Algolia webhook running on port ${PORT}`));
Common Errors and Debugging
Error: 401 Unauthorized Webhook Signature Mismatch
- What causes it: The shared secret in Cognigy.AI does not match the environment variable, or the payload body is modified by a proxy before reaching your endpoint.
- How to fix it: Ensure the raw JSON body is passed to the HMAC calculation. Disable any middleware that modifies
req.body. Verify the secret matches exactly in Cognigy webhook settings and your.envfile. - Code showing the fix: Use
JSON.stringify(req.body)before hashing, and ensureapp.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf; } }))is configured if you need to verify the raw buffer.
Error: 429 Algolia Rate Limit Exceeded
- What causes it: Concurrent webhook triggers exceed your Algolia plan query quota, or the retry logic is missing backoff delays.
- How to fix it: Implement exponential backoff. Cache aggressive queries. Use Algolia’s
x-algolia-api-keyheader to rotate keys if using multiple read-only keys. - Code showing the fix: The
executeAlgoliaSearchfunction already implementsMath.pow(2, attempt - 1)delay scaling. IncreaseBASE_DELAY_MSto 500 if using a shared cluster.
Error: 504 Gateway Timeout from Cognigy.AI
- What causes it: The webhook response exceeds Cognigy’s configured timeout threshold, usually caused by unoptimized Algolia queries or missing index filters.
- How to fix it: Reduce
hitsPerPage, limitattributesToRetrieve, and enforce a strictPromise.racetimeout. Ensure your Algolia index has proper compound indices forlanguageandlocation. - Code showing the fix: The
timeoutPromiseinhandleSearchRequestcuts off execution atSEARCH_TIMEOUT_MS. Lower this value to 1500ms if Cognigy is configured for fast responses.
Error: Context Variable Overflow or Type Mismatch
- What causes it: Injecting oversized JSON arrays into Cognigy context variables exceeds platform limits, or returning undefined values breaks chatflow variable binding.
- How to fix it: Cap
hitsPerPageto 5 or fewer. Flatten nested objects. Provide default values for missing attributes. - Code showing the fix: The
formatCognigyResponsefunction uses optional chaining and fallbacks (hit._highlightResult?.title?.value || hit.title) to guarantee consistent types.