);
}
### Anti_patterns
- Pattern: Using Admin API key in frontend code | Why: Admin key exposes full index control including deletion | Fix: Use search-only API key with restrictions
- Pattern: Not using /lite client for frontend | Why: Full client includes unnecessary code for search | Fix: Import from algoliasearch/lite for smaller bundle
### References
- https://www.algolia.com/doc/api-reference/widgets/react
- https://www.algolia.com/doc/libraries/javascript/v5/methods/search/
### Next.js Server-Side Rendering
SSR integration for Next.js with react-instantsearch-nextjs package.
Use instead of for SSR.
Supports both Pages Router and App Router (experimental).
Key considerations:
- Set dynamic = 'force-dynamic' for fresh results
- Handle URL synchronization with routing prop
- Use getServerState for initial state
### Code_example
// app/search/page.tsx
import { InstantSearchNext } from 'react-instantsearch-nextjs';
import { searchClient, INDEX_NAME } from '@/lib/algolia';
import { SearchBox, Hits, RefinementList } from 'react-instantsearch';
// Force dynamic rendering for fresh search results
export const dynamic = 'force-dynamic';
export default function SearchPage() {
return (
);
}
// For custom routing (URL synchronization)
import { history } from 'instantsearch.js/es/lib/routers';
import { simple } from 'instantsearch.js/es/lib/stateMappings';
typeof window === 'undefined'
? new URL(url) as unknown as Location
: window.location,
}),
stateMapping: simple(),
}}
>
{/* widgets */}
### Anti_patterns
- Pattern: Using InstantSearch component for Next.js SSR | Why: Regular component doesn't support server-side rendering | Fix: Use InstantSearchNext from react-instantsearch-nextjs
- Pattern: Static rendering for search pages | Why: Search results must be fresh for each request | Fix: Set export const dynamic = 'force-dynamic'
### References
- https://www.npmjs.com/package/react-instantsearch-nextjs
- https://www.algolia.com/developers/code-exchange/instantsearch-and-next-js-starter
### Data Synchronization and Indexing
Indexing strategies for keeping Algolia in sync with your data.
Three main approaches:
1. Full Reindexing - Replace entire index (expensive)
2. Full Record Updates - Replace individual records
3. Partial Updates - Update specific attributes only
Best practices:
- Batch records (ideal: 10MB, 1K-10K records per batch)
- Use incremental updates when possible
- partialUpdateObjects for attribute-only changes
- Avoid deleteBy (computationally expensive)
### Code_example
// lib/algolia-admin.ts (SERVER ONLY)
import algoliasearch from 'algoliasearch';
// Admin client - NEVER expose to frontend
const adminClient = algoliasearch(
process.env.ALGOLIA_APP_ID!,
process.env.ALGOLIA_ADMIN_KEY! // Admin key for indexing
);
const index = adminClient.initIndex('products');
// Batch indexing (recommended approach)
export async function indexProducts(products: Product[]) {
const records = products.map((p) => ({
objectID: p.id, // Required unique identifier
name: p.name,
description: p.description,
price: p.price,
category: p.category,
inStock: p.inventory > 0,
createdAt: p.createdAt.getTime(), // Use timestamps for sorting
}));
// Batch in chunks of ~1000-5000 records
const BATCH_SIZE = 1000;
for (let i = 0; i < records.length; i += BATCH_SIZE) {
const batch = records.slice(i, i + BATCH_SIZE);
await index.saveObjects(batch);
}
}
// Partial update - update only specific fields
export async function updateProductPrice(productId: string, price: number) {
await index.partialUpdateObject({
objectID: productId,
price,
updatedAt: Date.now(),
});
}
// Partial update with operations
export async function incrementViewCount(productId: string) {
await index.partialUpdateObject({
objectID: productId,
viewCount: {
_operation: 'Increment',
value: 1,
},
});
}
// Delete records (prefer this over deleteBy)
export async function deleteProducts(productIds: string[]) {
await index.deleteObjects(productIds);
}
// Full reindex with zero-downtime (atomic swap)
export async function fullReindex(products: Product[]) {
const tempIndex = adminClient.initIndex('products_temp');
// Index to temp index
await tempIndex.saveObjects(
products.map((p) => ({
objectID: p.id,
...p,
}))
);
// Copy settings from main index
await adminClient.copyIndex('products', 'products_temp', {
scope: ['settings', 'synonyms', 'rules'],
});
// Atomic swap
await adminClient.moveIndex('products_temp', 'products');
}
### Anti_patterns
- Pattern: Using deleteBy for bulk deletions | Why: deleteBy is computationally expensive and rate limited | Fix: Use deleteObjects with array of objectIDs
- Pattern: Indexing one record at a time | Why: Creates indexing queue, slows down process | Fix: Batch records in groups of 1K-10K
- Pattern: Full reindex for small changes | Why: Wastes operations, slower than incremental | Fix: Use partialUpdateObject for attribute changes
### References
- https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/in-depth/the-different-synchronization-strategies
- https://www.algolia.com/blog/engineering/search-indexing-best-practices-for-top-performance-with-code-samples
### API Key Security and Restrictions
Secure API key configuration for Algolia.
Key types:
- Admin API Key: Full control (indexing, settings, deletion)
- Search-Only API Key: Safe for frontend
- Secured API Keys: Generated from base key with restrictions
Restrictions available:
- Indices: Limit accessible indices
- Rate limit: Limit API calls per hour per IP
- Validity: Set expiration time
- HTTP referrers: Restrict to specific URLs
- Query parameters: Enforce search parameters
### Code_example
// NEVER do this - admin key in frontend
// const client = algoliasearch(appId, ADMIN_KEY); // WRONG!
// Correct: Use search-only key in frontend
const searchClient = algoliasearch(
process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!,
process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY!
);
// Server-side: Generate secured API key
// lib/algolia-secured-key.ts
import algoliasearch from 'algoliasearch';
const adminClient = algoliasearch(
process.env.ALGOLIA_APP_ID!,
process.env.ALGOLIA_ADMIN_KEY!
);
// Generate user-specific secured key
export function generateSecuredKey(userId: string) {
const searchKey = process.env.ALGOLIA_SEARCH_KEY!;
return adminClient.generateSecuredApiKey(searchKey, {
// User can only see their own data
filters: `userId:${userId}`,
// Key expires in 1 hour
validUntil: Math.floor(Date.now() / 1000) + 3600,
// Restrict to specific index
restrictIndices: ['user_documents'],
});
}
// Rate-limited key for public APIs
export async function createRateLimitedKey() {
const { key } = await adminClient.addApiKey({
acl: ['search'],
indexes: ['products'],
description: 'Public search with rate limit',
maxQueriesPerIPPerHour: 1000,
referers: ['https://mysite.com/*'],
validity: 0, // Never expires
});
return key;
}
// API endpoint to get user's secured key
// app/api/search-key/route.ts
import { auth } from '@/lib/auth';
import { generateSecuredKey } from '@/lib/algolia-secured-key';
export async function GET() {
const session = await auth();
if (!session?.user) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
const securedKey = generateSecuredKey(session.user.id);
return Response.json({ key: securedKey });
}
### Anti_patterns
- Pattern: Hardcoding Admin API key in client code | Why: Exposes full index control to attackers | Fix: Use search-only key with restrictions
- Pattern: Using same key for all users | Why: Can't restrict data access per user | Fix: Generate secured API keys with user filters
- Pattern: No rate limiting on public search | Why: Bots can exhaust your search quota | Fix: Set maxQueriesPerIPPerHour on API key
### References
- https://www.algolia.com/doc/guides/security/api-keys
- https://support.algolia.com/hc/en-us/articles/14339249272977-What-are-the-best-practices-to-manage-Algolia-API-keys-in-my-code-and-protect-them
### Custom Ranking and Relevance Tuning
Configure searchable attributes and custom ranking for relevance.
Searchable attributes (order matters):
1. Most important fields first (title, name)
2. Secondary fields next (description, tags)
3. Exclude non-searchable fields (image_url, id)
Custom ranking:
- Add business metrics (popularity, rating, date)
- Use desc() for descending, asc() for ascending
### Code_example
// scripts/configure-index.ts
import algoliasearch from 'algoliasearch';
const adminClient = algoliasearch(
process.env.ALGOLIA_APP_ID!,
process.env.ALGOLIA_ADMIN_KEY!
);
const index = adminClient.initIndex('products');
async function configureIndex() {
await index.setSettings({
// Searchable attributes in order of importance
searchableAttributes: [
'name', // Most important
'brand',
'category',
'description', // Least important
],
// Attributes for faceting/filtering
attributesForFaceting: [
'category',
'brand',
'filterOnly(inStock)', // Filter only, not displayed
'searchable(tags)', // Searchable facet
],
// Custom ranking (after text relevance)
customRanking: [
'desc(popularity)', // Most popular first
'desc(rating)', // Then by rating
'desc(createdAt)', // Then by recency
],
// Typo tolerance
typoTolerance: true,
minWordSizefor1Typo: 4,
minWordSizefor2Typos: 8,
// Query settings
queryLanguages: ['en'],
removeStopWords: ['en'],
// Highlighting
attributesToHighlight: ['name', 'description'],
highlightPreTag: '',
highlightPostTag: '',
// Pagination
hitsPerPage: 20,
paginationLimitedTo: 1000,
// Distinct (deduplication)
attributeForDistinct: 'productFamily',
distinct: true,
});
// Add synonyms
await index.saveSynonyms([
{
objectID: 'phone-mobile',
type: 'synonym',
synonyms: ['phone', 'mobile', 'cell', 'smartphone'],
},
{
objectID: 'laptop-notebook',
type: 'oneWaySynonym',
input: 'laptop',
synonyms: ['notebook', 'portable computer'],
},
]);
// Add rules (query-based customization)
await index.saveRules([
{
objectID: 'boost-sale-items',
condition: {
anchoring: 'contains',
pattern: 'sale',
},
consequence: {
params: {
filters: 'onSale:true',
optionalFilters: ['featured:true'],
},
},
},
]);
console.log('Index configured successfully');
}
configureIndex();
### Anti_patterns
- Pattern: Searching all attributes equally | Why: Reduces relevance, matches in descriptions rank same as titles | Fix: Order searchableAttributes by importance
- Pattern: No custom ranking | Why: Relies only on text matching, ignores business value | Fix: Add popularity, rating, or recency to customRanking
- Pattern: Indexing raw dates as strings | Why: Can't sort by date correctly | Fix: Use timestamps (getTime()) for date sorting
### References
- https://www.algolia.com/doc/guides/managing-results/relevance-overview
- https://www.algolia.com/doc/guides/managing-results/must-do/custom-ranking
### Faceted Search and Filtering
Implement faceted navigation with refinement lists, range sliders,
and hierarchical menus.
Widget types:
- RefinementList: Multi-select checkboxes
- Menu: Single-select list
- HierarchicalMenu: Nested categories
- RangeInput/RangeSlider: Numeric ranges
- ToggleRefinement: Boolean filters
### Code_example
'use client';
import {
InstantSearch,
SearchBox,
Hits,
RefinementList,
HierarchicalMenu,
RangeInput,
ToggleRefinement,
ClearRefinements,
CurrentRefinements,
Stats,
SortBy,
} from 'react-instantsearch';
import { searchClient, INDEX_NAME } from '@/lib/algolia';
export function ProductSearch() {
return (
{/* Filters Sidebar */}
{/* Results */}
);
}
// For sorting, create replica indices
// products_price_asc: customRanking: ['asc(price)']
// products_price_desc: customRanking: ['desc(price)']
// products_rating_desc: customRanking: ['desc(rating)']
### Anti_patterns
- Pattern: Faceting on non-faceted attributes | Why: Must declare attributesForFaceting in settings | Fix: Add attributes to attributesForFaceting array
- Pattern: Not using filterOnly() for hidden filters | Why: Wastes facet computation on non-displayed attributes | Fix: Use filterOnly(attribute) for filters you won't show
### References
- https://www.algolia.com/doc/guides/managing-results/refine-results/faceting
- https://www.algolia.com/doc/api-reference/widgets/refinement-list/react
### Query Suggestions and Autocomplete
Implement autocomplete with query suggestions and instant results.
Uses @algolia/autocomplete-js for standalone autocomplete or
integrate with InstantSearch using SearchBox.
Query Suggestions require a separate index generated by Algolia.
### Code_example
// Standalone Autocomplete
// components/Autocomplete.tsx
'use client';
import { autocomplete, getAlgoliaResults } from '@algolia/autocomplete-js';
import algoliasearch from 'algoliasearch/lite';
import { useEffect, useRef } from 'react';
import '@algolia/autocomplete-theme-classic';
const searchClient = algoliasearch(
process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!,
process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY!
);
export function Autocomplete() {
const containerRef = useRef(null);
useEffect(() => {
if (!containerRef.current) return;
const search = autocomplete({
container: containerRef.current,
placeholder: 'Search for products',
openOnFocus: true,
getSources({ query }) {
if (!query) return [];
return [
// Query suggestions
{
sourceId: 'suggestions',
getItems() {
return getAlgoliaResults({
searchClient,
queries: [
{
indexName: 'products_query_suggestions',
query,
params: { hitsPerPage: 5 },
},
],
});
},
templates: {
header() {
return 'Suggestions';
},
item({ item, html }) {
return html`${item.query}`;
},
},
},
// Instant results
{
sourceId: 'products',
getItems() {
return getAlgoliaResults({
searchClient,
queries: [
{
indexName: 'products',
query,
params: { hitsPerPage: 8 },
},
],
});
},
templates: {
header() {
return 'Products';
},
item({ item, html }) {
return html`
${item.name}$${item.price}
`;
},
},
onSelect({ item, setQuery, refresh }) {
// Navigate on selection
window.location.href = `/products/${item.objectID}`;
},
},
];
},
});
return () => search.destroy();
}, []);
return ;
}
// Combined with InstantSearch
import { connectSearchBox } from 'react-instantsearch';
import { autocomplete } from '@algolia/autocomplete-js';
// Or use built-in Autocomplete widget
import { Autocomplete as AlgoliaAutocomplete } from 'react-instantsearch';
export function SearchWithAutocomplete() {
return (
);
}
### Anti_patterns
- Pattern: Creating autocomplete without debouncing | Why: Every keystroke triggers search, wastes operations | Fix: Algolia autocomplete handles debouncing automatically
- Pattern: Not using Query Suggestions index | Why: Missing search analytics for popular queries | Fix: Enable Query Suggestions in Algolia dashboard
### References
- https://www.algolia.com/doc/ui-libraries/autocomplete/introduction/what-is-autocomplete
- https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/query-suggestions/how-to/optimizing-query-suggestions-relevance/js
## Sharp Edges
### Admin API Key in Frontend Code
Severity: CRITICAL
### Indexing Rate Limits and Throttling
Severity: HIGH
### Record Size and Index Limits
Severity: MEDIUM
### PII in Index Names Visible in Network
Severity: MEDIUM
### Searchable Attributes Order Affects Relevance
Severity: MEDIUM
### Full Reindex Consumes All Operations
Severity: MEDIUM
### Every Keystroke Counts as Search Operation
Severity: MEDIUM
### SSR Hydration Mismatch with InstantSearch
Severity: MEDIUM
### Replica Indices for Sorting Multiply Storage
Severity: LOW
### Faceting Requires attributesForFaceting Declaration
Severity: MEDIUM
## Validation Checks
### Admin API Key in Client Code
Severity: ERROR
Admin API key must never be exposed to client-side code
Message: Admin API key exposed to client. Use search-only key.
### Hardcoded Algolia API Key
Severity: ERROR
API keys should use environment variables
Message: Hardcoded Algolia credentials. Use environment variables.
### Search Key Used for Indexing
Severity: ERROR
Indexing operations require admin key, not search key
Message: Search key used for indexing. Use admin key for write operations.
### Single Record Indexing in Loop
Severity: WARNING
Batch records together for efficient indexing
Message: Single record indexing in loop. Use saveObjects for batch indexing.
### Using deleteBy for Deletion
Severity: WARNING
deleteBy is expensive and rate-limited
Message: deleteBy is expensive. Prefer deleteObjects with specific IDs.
### Frequent Full Reindex
Severity: WARNING
Full reindex wastes operations on unchanged data
Message: Frequent full reindex. Consider incremental sync for unchanged data.
### Full Client Instead of Lite
Severity: INFO
Use lite client for smaller bundle in frontend
Message: Full Algolia client imported. Use algoliasearch/lite for frontend.
### Regular InstantSearch in Next.js
Severity: WARNING
Use react-instantsearch-nextjs for SSR support
Message: Using regular InstantSearch. Use InstantSearchNext for Next.js SSR.
### Missing Searchable Attributes Configuration
Severity: WARNING
Configure searchableAttributes for better relevance
Message: No searchableAttributes configured. Set attribute priority for relevance.
### Missing Custom Ranking
Severity: INFO
Custom ranking improves business relevance
Message: No customRanking configured. Add business metrics (popularity, rating).
## Collaboration
### Delegation Triggers
- user needs e-commerce checkout -> stripe-integration (Product search leading to purchase)
- user needs search analytics -> segment-cdp (Track search queries and results)
- user needs user authentication -> clerk-auth (Secured API keys per user)
- user needs database setup -> postgres-wizard (Source data for indexing)
- user needs serverless deployment -> aws-serverless (Lambda for indexing jobs)
## When to Use
- User mentions or implies: adding search to
- User mentions or implies: algolia
- User mentions or implies: instantsearch
- User mentions or implies: search api
- User mentions or implies: search functionality
- User mentions or implies: typeahead
- User mentions or implies: autocomplete search
- User mentions or implies: faceted search
- User mentions or implies: search index
- User mentions or implies: search as you type
## Limitations
- Use this skill only when the task clearly matches the scope described above.
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.