Overview
Theindex widget lets you target different indices within the same InstantSearch instance. Each index maintains its own search state, widgets, and results.
Basic Setup
import instantsearch from 'instantsearch.js';
import { index, searchBox, hits, configure } from 'instantsearch.js/es/widgets';
const search = instantsearch({
indexName: 'main_products',
searchClient,
});
search.addWidgets([
// Shared search box
searchBox({ container: '#searchbox' }),
// Main index results
hits({
container: '#products-hits',
templates: {
item: (hit) => `<div>${hit.name}</div>`,
},
}),
// Secondary index for articles
index({ indexName: 'articles' }).addWidgets([
hits({
container: '#articles-hits',
templates: {
item: (hit) => `<div>${hit.title}</div>`,
},
}),
configure({ hitsPerPage: 5 }),
]),
]);
search.start();
Index Widget API
Fromsrc/widgets/index/index.ts, the index widget provides:
index({
indexName: string, // Required: Algolia index name
indexId?: string, // Optional: Unique ID for URL/state
})
Index Methods
const articlesIndex = index({ indexName: 'articles' });
// Add widgets to the index
articlesIndex.addWidgets([
hits({ container: '#articles' }),
refinementList({ container: '#categories', attribute: 'category' }),
]);
// Remove widgets
articlesIndex.removeWidgets([hitsWidget]);
// Get the index name
articlesIndex.getIndexName(); // 'articles'
// Get the Helper instance
const helper = articlesIndex.getHelper();
// Get search results
const results = articlesIndex.getResults();
Federated Search Example
Create a federated search interface with products, articles, and FAQ:const search = instantsearch({
indexName: 'products',
searchClient,
routing: true,
});
// Shared search box
search.addWidgets([
searchBox({ container: '#searchbox' }),
]);
// Products index (main)
search.addWidgets([
configure({ hitsPerPage: 8 }),
hits({
container: '#products',
templates: {
item: (hit) => `
<article>
<img src="${hit.image}" alt="${hit.name}" />
<h3>${hit.name}</h3>
<p>$${hit.price}</p>
</article>
`,
},
}),
stats({ container: '#products-stats' }),
]);
// Articles index
search.addWidgets([
index({ indexName: 'articles' }).addWidgets([
configure({ hitsPerPage: 3 }),
hits({
container: '#articles',
templates: {
item: (hit) => `
<article>
<h4>${hit.title}</h4>
<p>${hit.excerpt}</p>
<a href="${hit.url}">Read more</a>
</article>
`,
},
}),
]),
]);
// FAQ index
search.addWidgets([
index({ indexName: 'faq' }).addWidgets([
configure({ hitsPerPage: 3 }),
hits({
container: '#faq',
templates: {
item: (hit) => `
<div>
<strong>${hit.question}</strong>
<p>${hit.answer}</p>
</div>
`,
},
}),
]),
]);
search.start();
Using Index IDs
When using multiple instances of the same index, provide unique IDs:// Same index, different refinements
search.addWidgets([
searchBox({ container: '#searchbox' }),
// Featured products (on sale)
index({
indexName: 'products',
indexId: 'featured',
}).addWidgets([
configure({
filters: 'on_sale:true',
hitsPerPage: 4,
}),
hits({ container: '#featured' }),
]),
// All products
index({
indexName: 'products',
indexId: 'all',
}).addWidgets([
hits({ container: '#all-products' }),
refinementList({ container: '#brand', attribute: 'brand' }),
]),
]);
Conditional Rendering
Show/hide indices based on results:const articlesIndex = index({ indexName: 'articles' }).addWidgets([
hits({
container: '#articles',
templates: {
empty: () => '',
item: (hit) => `<div>${hit.title}</div>`,
},
}),
]);
// Listen to results
articlesIndex.on('render', () => {
const results = articlesIndex.getResults();
const container = document.querySelector('#articles-section');
if (results && results.nbHits > 0) {
container.style.display = 'block';
} else {
container.style.display = 'none';
}
});
search.addWidgets([articlesIndex]);
Scoped Results
Access results from all indices:import { connectStats } from 'instantsearch.js/es/connectors';
const renderOverallStats = (renderOptions) => {
const { instantSearchInstance } = renderOptions;
const scopedResults = instantSearchInstance.mainIndex.getScopedResults();
const totalHits = scopedResults.reduce((total, { results }) => {
return total + (results?.nbHits || 0);
}, 0);
document.querySelector('#total-results').innerHTML = `
<p>Found ${totalHits} results across all indices</p>
<ul>
${scopedResults.map(({ indexId, results }) => `
<li>${indexId}: ${results?.nbHits || 0} results</li>
`).join('')}
</ul>
`;
};
const overallStats = connectStats(renderOverallStats);
search.addWidgets([overallStats({})]);
Routing with Multiple Indices
Each index’s state is namespaced in the URL:const search = instantsearch({
indexName: 'products',
searchClient,
routing: {
stateMapping: {
stateToRoute(uiState) {
return {
products: uiState.products,
articles: uiState.articles,
};
},
routeToState(routeState) {
return {
products: routeState.products || {},
articles: routeState.articles || {},
};
},
},
},
});
search.addWidgets([
searchBox({ container: '#searchbox' }),
hits({ container: '#products' }),
index({ indexName: 'articles' }).addWidgets([
hits({ container: '#articles' }),
]),
]);
?products[query]=phone&products[page]=2&articles[query]=phone
Isolated Indices (Experimental)
The
EXPERIMENTAL_isolated option is experimental and may change in future versions.const search = instantsearch({
indexName: 'products',
searchClient,
});
search.addWidgets([
searchBox({ container: '#searchbox' }),
hits({ container: '#products' }),
// Isolated index with independent state
index({
indexName: 'recommendations',
EXPERIMENTAL_isolated: true,
}).addWidgets([
configure({
filters: 'featured:true',
hitsPerPage: 4,
}),
hits({ container: '#recommendations' }),
]),
]);
Performance Optimization
Limit Results per Index
index({ indexName: 'articles' }).addWidgets([
configure({ hitsPerPage: 3 }), // Only fetch 3 results
hits({ container: '#articles' }),
]);
Conditional Queries
Only search certain indices when there’s a query:const articlesIndex = index({ indexName: 'articles' }).addWidgets([
configure({
hitsPerPage: 5,
}),
hits({ container: '#articles' }),
]);
search.addWidgets([
searchBox({
container: '#searchbox',
queryHook(query, search) {
if (query.length > 2) {
search(query);
}
},
}),
articlesIndex,
]);
Sort and Filter per Index
search.addWidgets([
// Main products
sortBy({
container: '#sort-by',
items: [
{ label: 'Relevance', value: 'products' },
{ label: 'Price Low to High', value: 'products_price_asc' },
{ label: 'Price High to Low', value: 'products_price_desc' },
],
}),
refinementList({ container: '#brand', attribute: 'brand' }),
// Articles with different sorting
index({ indexName: 'articles' }).addWidgets([
sortBy({
container: '#articles-sort',
items: [
{ label: 'Most Recent', value: 'articles_date_desc' },
{ label: 'Most Popular', value: 'articles_popular_desc' },
],
}),
refinementList({ container: '#article-category', attribute: 'category' }),
]),
]);
Implementation Details
Fromsrc/widgets/index/index.ts, each index widget:
- Creates its own Helper for state management
- Uses a derived helper attached to the main helper for searching
- Manages its own widgets independently
- Contributes to the URL via
getWidgetUiState - Schedules renders when results arrive
// Simplified from source
const index = (widgetParams: IndexWidgetParams): IndexWidget => {
const { indexName, indexId = indexName } = widgetParams;
let helper: Helper | null = null;
let derivedHelper: DerivedHelper | null = null;
return {
init({ instantSearchInstance, parent, uiState }) {
const mainHelper = instantSearchInstance.mainHelper;
// Create helper for this index
helper = algoliasearchHelper(
mainHelper.getClient(),
indexName,
parameters
);
// Create derived helper that merges with main helper
derivedHelper = mainHelper.derive(() =>
mergeSearchParameters(
mainHelper.state,
...resolveSearchParameters(this)
)
);
derivedHelper.on('result', ({ results }) => {
helper.lastResults = results;
instantSearchInstance.scheduleRender();
});
},
getWidgetUiState(uiState) {
return {
...uiState,
[indexId]: localUiState,
};
},
};
};
Complete Example
import instantsearch from 'instantsearch.js';
import {
index,
searchBox,
hits,
configure,
stats,
refinementList,
panel,
} from 'instantsearch.js/es/widgets';
const search = instantsearch({
indexName: 'products',
searchClient,
routing: true,
});
// Shared widgets
search.addWidgets([
searchBox({
container: '#searchbox',
placeholder: 'Search products and articles...',
}),
]);
// Main products index
search.addWidgets([
panel({ templates: { header: () => 'Products' } })(stats)({
container: '#products-stats',
}),
hits({
container: '#products-hits',
templates: {
item: (hit, { html, components }) => html`
<article>
<h3>${components.Highlight({ hit, attribute: 'name' })}</h3>
<p>$${hit.price}</p>
</article>
`,
},
}),
refinementList({
container: '#brand-filter',
attribute: 'brand',
}),
]);
// Articles index
search.addWidgets([
index({ indexName: 'articles', indexId: 'articles' }).addWidgets([
panel({ templates: { header: () => 'Articles' } })(stats)({
container: '#articles-stats',
}),
configure({ hitsPerPage: 3 }),
hits({
container: '#articles-hits',
templates: {
item: (hit, { html, components }) => html`
<article>
<h4>${components.Highlight({ hit, attribute: 'title' })}</h4>
<p>${hit.excerpt}</p>
<a href="${hit.url}">Read more →</a>
</article>
`,
},
}),
]),
]);
search.start();