Overview
InstantSearch provides two AI-powered connectors:- Chat: Conversational search interface powered by AI agents
- Filter Suggestions: AI-generated filter recommendations based on search context
Chat
Create a conversational search experience where users can ask questions in natural language.Basic Setup
import { connectChat } from 'instantsearch.js/es/connectors';
const renderChat = (renderOptions, isFirstRender) => {
const {
messages,
status,
sendMessage,
input,
setInput,
} = renderOptions;
if (isFirstRender) {
const container = document.querySelector('#chat');
container.innerHTML = `
<div id="messages"></div>
<div class="input-area">
<input id="chat-input" type="text" placeholder="Ask a question..." />
<button id="send-btn">Send</button>
</div>
`;
document.querySelector('#send-btn').addEventListener('click', () => {
const input = document.querySelector('#chat-input');
sendMessage(input.value);
input.value = '';
});
}
// Render messages
const messagesContainer = document.querySelector('#messages');
messagesContainer.innerHTML = messages.map(msg => `
<div class="message ${msg.role}">
${msg.parts.map(part =>
part.type === 'text' ? `<p>${part.text}</p>` : ''
).join('')}
</div>
`).join('');
// Show loading state
if (status === 'loading') {
messagesContainer.innerHTML += '<div class="loading">Thinking...</div>';
}
};
const chatWidget = connectChat(renderChat);
search.addWidgets([
chatWidget({
agentId: 'your-agent-id', // From Algolia Agent Studio
}),
]);
Chat with Custom Transport
Use a custom AI endpoint:const chatWidget = connectChat(renderChat);
search.addWidgets([
chatWidget({
transport: {
api: 'https://your-ai-api.com/chat',
headers: {
'Authorization': 'Bearer YOUR_TOKEN',
},
prepareSendMessagesRequest({ messages, trigger }) {
return {
body: {
messages,
stream: true,
},
};
},
},
}),
]);
Chat State Management
Fromsrc/connectors/chat/connectChat.ts, the chat connector provides:
type ChatRenderState = {
// Current chat messages
messages: UIMessage[];
// Chat status: 'idle' | 'loading' | 'streaming' | 'error'
status: string;
// Send a message
sendMessage: (message: string) => void;
// Regenerate last response
regenerate: () => void;
// Stop ongoing generation
stop: () => void;
// Clear error state
clearError: () => void;
// Error if any
error: Error | null;
// Current input value
input: string;
// Set input value
setInput: (input: string) => void;
// Current search state
indexUiState: IndexUiState;
// Set search state
setIndexUiState: (state: IndexUiState) => void;
};
Tool Integration
Implement custom tools for the AI to use:const chatWidget = connectChat(renderChat);
search.addWidgets([
chatWidget({
agentId: 'your-agent-id',
tools: {
search_index: {
onToolCall({ toolCall, addToolResult }) {
// AI wants to search the index
const { query, facetFilters } = toolCall.args;
// Perform search
performSearch(query, facetFilters).then(results => {
addToolResult({
output: JSON.stringify(results.hits.slice(0, 5)),
});
});
},
},
get_product_details: {
onToolCall({ toolCall, addToolResult }) {
const { productId } = toolCall.args;
fetch(`/api/products/${productId}`).then(res => res.json()).then(product => {
addToolResult({
output: JSON.stringify(product),
});
});
},
},
},
}),
]);
Apply Filters from Chat
The connector provides methods to apply filters from AI suggestions:// From src/connectors/chat/connectChat.ts
function updateStateFromSearchToolInput(
params: { query?: string; facetFilters?: string[][] },
helper: AlgoliaSearchHelper
) {
// Clear existing filters
const attributesToClear = getAttributesToClear({ results: helper.lastResults!, helper });
helper.setState(clearRefinements({ helper, attributesToClear }));
// Apply new facet filters
if (params.facetFilters) {
const attributes = flat(params.facetFilters).map((filter) => {
const [attribute, value] = filter.split(':');
return { attribute, value };
});
attributes.forEach(({ attribute, value }) => {
if (!helper.state.isDisjunctiveFacet(attribute)) {
const s = helper.state.addDisjunctiveFacet(attribute);
helper.setState(s);
}
helper.toggleFacetRefinement(attribute, value);
});
}
// Set query
if (params.query) {
helper.setQuery(params.query);
}
helper.search();
}
Chat with Suggestions
Extract and display suggestions from AI responses:const renderChat = (renderOptions, isFirstRender) => {
const { messages, suggestions, sendMessage } = renderOptions;
// Render messages...
// Render suggestions
if (suggestions && suggestions.length > 0) {
const suggestionsHtml = `
<div class="suggestions">
<p>You might also ask:</p>
${suggestions.map(suggestion => `
<button onClick="${() => sendMessage(suggestion)}">
${suggestion}
</button>
`).join('')}
</div>
`;
container.insertAdjacentHTML('beforeend', suggestionsHtml);
}
};
Filter Suggestions
AI-powered filter recommendations that help users refine their search.Basic Setup
import { connectFilterSuggestions } from 'instantsearch.js/es/connectors';
const renderFilterSuggestions = (renderOptions, isFirstRender) => {
const { suggestions, isLoading, refine } = renderOptions;
const container = document.querySelector('#filter-suggestions');
if (isLoading) {
container.innerHTML = '<div class="loading">Loading suggestions...</div>';
return;
}
if (!suggestions.length) {
container.innerHTML = '';
return;
}
container.innerHTML = `
<div class="filter-suggestions">
<h4>Suggested Filters</h4>
<div class="suggestions-list">
${suggestions.map(({ attribute, value, label, count }) => `
<button
class="suggestion-chip"
onClick="${() => refine(attribute, value)}"
>
${label}
<span class="count">${count}</span>
</button>
`).join('')}
</div>
</div>
`;
};
const filterSuggestionsWidget = connectFilterSuggestions(renderFilterSuggestions);
search.addWidgets([
filterSuggestionsWidget({
agentId: 'your-agent-id',
maxSuggestions: 3,
attributes: ['brand', 'category', 'color'], // Optional: limit to specific attributes
}),
]);
How Filter Suggestions Work
Fromsrc/connectors/filter-suggestions/connectFilterSuggestions.ts:
- Debounced requests - Waits for search state to stabilize
- Context-aware - Sends query, hits sample, and current refinements
- Minimum skeleton duration - Shows loading for at least 300ms to avoid flashing
const fetchSuggestions = (results: SearchResults, renderOptions: RenderOptions) => {
if (!results?.hits?.length) {
suggestions = [];
isLoading = false;
return;
}
const loadingStartTime = Date.now();
isLoading = true;
// Prepare request payload
const messageText = JSON.stringify({
query: results.query,
facets: facetsToSend,
hitsSample: results.hits.slice(0, hitsToSample),
currentRefinements,
maxSuggestions,
});
fetch(endpoint, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({
messages: [{
id: `sr-${Date.now()}`,
createdAt: new Date().toISOString(),
role: 'user',
parts: [{ type: 'text', text: messageText }],
}],
}),
})
.then(response => response.json())
.then(data => {
const parsedSuggestions = JSON.parse(data.parts[1].text);
suggestions = parsedSuggestions.slice(0, maxSuggestions);
})
.finally(() => {
// Ensure minimum skeleton duration
const elapsed = Date.now() - loadingStartTime;
const remainingDelay = Math.max(0, MIN_SKELETON_DURATION_MS - elapsed);
setTimeout(() => {
isLoading = false;
renderFn(getWidgetRenderState(renderOptions), false);
}, remainingDelay);
});
};
Custom Transport
Use your own AI endpoint:const filterSuggestionsWidget = connectFilterSuggestions(renderFilterSuggestions);
search.addWidgets([
filterSuggestionsWidget({
transport: {
api: 'https://your-ai-api.com/suggestions',
headers: {
'Authorization': 'Bearer YOUR_TOKEN',
},
prepareSendMessagesRequest(body) {
return {
body: {
...body,
model: 'gpt-4',
},
};
},
},
maxSuggestions: 5,
debounceMs: 500,
}),
]);
Debouncing
Control how quickly suggestions are fetched:filterSuggestionsWidget({
agentId: 'your-agent-id',
debounceMs: 500, // Wait 500ms after search state changes
hitsToSample: 10, // Send 10 hits for context
})
Transform Suggestions
Filter or modify suggestions before rendering:filterSuggestionsWidget({
agentId: 'your-agent-id',
transformItems(suggestions) {
return suggestions
.filter(s => s.count > 0) // Only show suggestions with results
.map(s => ({
...s,
label: s.label.toUpperCase(), // Transform label
}));
},
})
Complete AI Search Example
Combining chat and filter suggestions:import instantsearch from 'instantsearch.js';
import {
searchBox,
hits,
refinementList,
} from 'instantsearch.js/es/widgets';
import {
connectChat,
connectFilterSuggestions,
} from 'instantsearch.js/es/connectors';
const search = instantsearch({
indexName: 'products',
searchClient,
routing: true,
});
// Chat interface
const renderChat = (renderOptions, isFirstRender) => {
const {
messages,
status,
error,
sendMessage,
regenerate,
stop,
clearError,
suggestions,
} = renderOptions;
const container = document.querySelector('#chat');
if (isFirstRender) {
container.innerHTML = `
<div id="chat-messages"></div>
<div id="chat-suggestions"></div>
<div class="chat-input">
<input id="chat-input" placeholder="Ask about products..." />
<button id="send-btn">Send</button>
</div>
`;
const input = document.querySelector('#chat-input');
const sendBtn = document.querySelector('#send-btn');
sendBtn.addEventListener('click', () => {
if (input.value.trim()) {
sendMessage(input.value);
input.value = '';
}
});
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && input.value.trim()) {
sendMessage(input.value);
input.value = '';
}
});
}
// Render messages
const messagesContainer = document.querySelector('#chat-messages');
messagesContainer.innerHTML = messages.map((msg, index) => {
const isLast = index === messages.length - 1;
const parts = msg.parts || [];
return `
<div class="message ${msg.role}">
${parts.map(part => {
if (part.type === 'text') {
return `<p>${part.text}</p>`;
}
return '';
}).join('')}
${isLast && msg.role === 'assistant' ? `
<div class="message-actions">
<button onClick="${regenerate}">Regenerate</button>
</div>
` : ''}
</div>
`;
}).join('');
// Show loading/error states
if (status === 'loading' || status === 'streaming') {
messagesContainer.innerHTML += `
<div class="message assistant loading">
<div class="typing-indicator">Thinking...</div>
<button onClick="${stop}">Stop</button>
</div>
`;
}
if (error) {
messagesContainer.innerHTML += `
<div class="error">
<p>Error: ${error.message}</p>
<button onClick="${clearError}">Dismiss</button>
</div>
`;
}
// Render suggestions
const suggestionsContainer = document.querySelector('#chat-suggestions');
if (suggestions && suggestions.length > 0) {
suggestionsContainer.innerHTML = `
<div class="suggestions">
<p>You might also ask:</p>
${suggestions.map(suggestion => `
<button onClick="${() => {
sendMessage(suggestion);
}}">
${suggestion}
</button>
`).join('')}
</div>
`;
} else {
suggestionsContainer.innerHTML = '';
}
};
// Filter suggestions
const renderFilterSuggestions = (renderOptions) => {
const { suggestions, isLoading, refine } = renderOptions;
const container = document.querySelector('#filter-suggestions');
if (isLoading) {
container.innerHTML = `
<div class="filter-suggestions loading">
<div class="skeleton"></div>
<div class="skeleton"></div>
<div class="skeleton"></div>
</div>
`;
return;
}
if (!suggestions.length) {
container.innerHTML = '';
return;
}
container.innerHTML = `
<div class="filter-suggestions">
<h4>Suggested Filters</h4>
${suggestions.map(({ attribute, value, label, count }) => `
<button
class="suggestion-chip"
onClick="${() => refine(attribute, value)}"
>
<span class="label">${label}</span>
<span class="count">${count.toLocaleString()}</span>
</button>
`).join('')}
</div>
`;
};
// Initialize widgets
const chatWidget = connectChat(renderChat);
const filterSuggestionsWidget = connectFilterSuggestions(renderFilterSuggestions);
search.addWidgets([
searchBox({ container: '#searchbox' }),
// AI Chat
chatWidget({
agentId: 'your-agent-id',
tools: {
search_index: {
onToolCall({ toolCall, addToolResult, applyFilters }) {
// Apply filters from AI
applyFilters(toolCall.args);
// Return results to AI
addToolResult({
output: 'Filters applied',
});
},
},
},
}),
// Filter Suggestions
filterSuggestionsWidget({
agentId: 'your-agent-id',
maxSuggestions: 3,
debounceMs: 400,
attributes: ['brand', 'category', 'color', 'size'],
hitsToSample: 5,
}),
// Standard widgets
hits({
container: '#hits',
templates: {
item: (hit, { html, components }) => html`
<article>
<img src="${hit.image}" alt="${hit.name}" />
<h3>${components.Highlight({ hit, attribute: 'name' })}</h3>
<p>$${hit.price}</p>
</article>
`,
},
}),
refinementList({
container: '#brand',
attribute: 'brand',
}),
]);
search.start();
Best Practices
Provide Context
Send relevant hits and current refinements for better AI suggestions.
Handle Errors
Always implement error handling and display user-friendly messages.
Debounce Requests
Use appropriate debounce delays to avoid excessive API calls.
Show Loading States
Provide visual feedback during AI operations.
Styling
/* Chat Interface */
.message {
padding: 12px;
margin: 8px 0;
border-radius: 8px;
}
.message.user {
background: #e3f2fd;
margin-left: 40px;
}
.message.assistant {
background: #f5f5f5;
margin-right: 40px;
}
.typing-indicator {
display: flex;
align-items: center;
gap: 4px;
}
/* Filter Suggestions */
.filter-suggestions {
margin: 16px 0;
padding: 16px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.suggestion-chip {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
margin: 4px;
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 16px;
cursor: pointer;
transition: all 0.2s;
}
.suggestion-chip:hover {
background: #e0e0e0;
transform: translateY(-1px);
}
.suggestion-chip .count {
background: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.875em;
}
/* Loading skeleton */
.skeleton {
height: 32px;
margin: 8px 0;
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: loading 1.5s infinite;
border-radius: 16px;
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}