Introduction
Meilisearch has proven itself in production environments across industries. Its simplicity, speed, and relevance make it an excellent choice for various applications. This article explores real-world use cases, implementation patterns, and best practices learned from deploying Meilisearch at scale.
We will examine production scenarios ranging from e-commerce platforms to documentation search, mobile applications to multi-tenant SaaS systems. Each use case includes implementation details and code examples you can adapt for your own projects.
E-commerce Search
E-commerce search is one of the most demanding search applications. Users expect instant, relevant results with sophisticated filtering and sorting.
Key Requirements
E-commerce search requires:
- Instant search-as-you-type
- Faceted filtering (category, price, brand)
- Typo tolerance for product names
- Relevance tuning by popularity and sales
- Availability tracking
- Price-based sorting
Implementation
// Initialize Meilisearch for e-commerce
const { MeiliSearch } = require('meilisearch')
const client = new MeiliSearch({
host: process.env.MEILI_HOST,
apiKey: process.env.MEILI_MASTER_KEY
})
const productsIndex = client.index('products')
// Configure settings for e-commerce
async function configureProductIndex() {
await productsIndex.updateSettings({
searchableAttributes: [
'title^3', // High priority
'brand^2',
'description',
'category_path',
'tags'
],
filterableAttributes: [
'category_id',
'brand_id',
'price',
'in_stock',
'rating',
'attributes'
],
sortableAttributes: [
'price',
'rating',
'sales_count',
'created_at'
],
typoTolerance: {
enabled: true,
minWordSizeForTypos: {
oneTypo: 4,
twoTypos: 8
}
}
})
}
// Search with facets
async function searchProducts(query, filters, page = 1) {
const results = await productsIndex.search(query, {
filter: buildFilterString(filters),
facets: ['category_id', 'brand_id', 'in_stock'],
sort: [filters.sort || 'sales_count:desc'],
limit: 24,
page: page,
hitsPerPage: 24,
attributesToHighlight: ['title', 'description'],
attributesToRetrieve: [
'id', 'title', 'price', 'original_price',
'image_url', 'brand', 'rating', 'in_stock'
]
})
return results
}
// Build filter string from user selections
function buildFilterString(filters) {
const filterParts = []
if (filters.category) {
filterParts.push(`category_id = ${filters.category}`)
}
if (filters.brand) {
filterParts.push(`brand_id IN [${filters.brand.join(',')}]`)
}
if (filters.minPrice) {
filterParts.push(`price >= ${filters.minPrice}`)
}
if (filters.maxPrice) {
filterParts.push(`price <= ${filters.maxPrice}`)
}
if (filters.inStock) {
filterParts.push('in_stock = true')
}
return filterParts.join(' AND ')
}
Autocomplete with Popular Suggestions
async function getAutocompleteSuggestions(query) {
// Get product matches
const products = await productsIndex.search(query, {
limit: 5,
attributesToRetrieve: ['id', 'title', 'image_url', 'price']
})
// Get brand matches
const brands = await brandsIndex.search(query, {
limit: 3,
attributesToRetrieve: ['id', 'name', 'logo']
})
// Get category matches
const categories = await categoriesIndex.search(query, {
limit: 3,
attributesToRetrieve: ['id', 'name']
})
return {
products: products.hits,
brands: brands.hits,
categories: categories.hits,
suggestions: generateQuerySuggestions(query)
}
}
Personalization
async function personalizedSearch(userId, query, browsingHistory) {
// Boost user's preferred brands
const preferredBrands = await getUserBrandPreferences(userId)
const boostQuery = preferredBrands.length > 0
? `${query} ${preferredBrands.join(' ')}`
: query
const results = await productsIndex.search(boostQuery, {
filter: browsingHistory.excludedCategories
? `NOT category_id IN [${browsingHistory.excludedCategories.join(',')}]`
: undefined,
limit: 24,
attributesToRetrieve: ['*']
})
return results
}
Documentation Search
Documentation sites require different search capabilities, including code snippet handling and hierarchical content.
Key Requirements
Documentation search needs:
- Full-text search across code and prose
- Section-level matching
- Typo tolerance for technical terms
- Version-aware results
- Instant feedback
Implementation
const docsIndex = client.index('documentation')
async function configureDocsIndex() {
await docsIndex.updateSettings({
searchableAttributes: [
'title^3',
'headings^2',
'content',
'code_examples',
'tags'
],
filterableAttributes: [
'version',
'category',
'language',
'product'
],
sortableAttributes: ['title', 'last_updated'],
typoTolerance: {
enabled: true,
// Allow more typos for technical terms
minWordSizeForTypos: {
oneTypo: 3,
twoTypos: 6
}
},
// Prioritize exact matches in headings
rankingRules: [
'words',
'typo',
'proximity',
'attribute',
'exactness',
'sort'
]
})
}
Document Structure
// Index documentation with proper structure
const docStructure = {
id: 'getting-started-installation',
title: 'Installation',
product: 'api',
version: '2.0',
category: 'getting-started',
language: 'en',
headings: [
'Installation',
'Requirements',
'Quick Start',
'Next Steps'
],
content: `## Installation
Install the SDK using npm:
\`\`\`bash
npm install @example/api-client
\`\`\`
## Requirements
- Node.js 18+
- npm or yarn`,
code_examples: [
'npm install @example/api-client',
'const client = new APIClient({ apiKey: "..." })'
],
last_updated: '2026-01-15',
slug: '/docs/getting-started/installation'
}
Section-Level Search
async function searchDocumentation(query, version) {
const results = await docsIndex.search(query, {
filter: version ? `version = "${version}"` : undefined,
limit: 10,
attributesToHighlight: ['content', 'title'],
attributesToRetrieve: [
'id', 'title', 'slug', 'version',
'category', 'last_updated'
],
// Highlight matching sections
showMatchesPosition: true
})
// Group by category
const groupedResults = groupBy(results.hits, 'category')
return groupedResults
}
Mobile Application Search
Mobile search requires special considerations for limited screen space, offline capabilities, and touch interfaces.
Key Requirements
Mobile search needs:
- Compact result display
- Offline search capability
- Voice search support
- Recent searches
- Location-based results
Implementation
// React Native example
import { MeiliSearch } from 'meilisearch'
const client = new MeiliSearch({
host: 'https://your-app.meilisearch.cloud',
apiKey: 'your_mobile_api_key'
})
// Local cache for offline search
const AsyncStorage = require('@react-native-async-storage/asyncStorage')
class MobileSearchService {
constructor() {
this.recentSearches = []
this.loadRecentSearches()
}
async loadRecentSearches() {
const stored = await AsyncStorage.getItem('recentSearches')
if (stored) {
this.recentSearches = JSON.parse(stored)
}
}
async saveRecentSearch(query) {
this.recentSearches = [
query,
...this.recentSearches.filter(q => q !== query)
].slice(0, 10)
await AsyncStorage.setItem(
'recentSearches',
JSON.stringify(this.recentSearches)
)
}
async search(query) {
// Save to recent searches
await this.saveRecentSearch(query)
const results = await client.index('products').search(query, {
limit: 20,
attributesToRetrieve: [
'id', 'title', 'price', 'image_url', 'category'
],
// Smaller payload for mobile
attributesToHighlight: ['title']
})
return {
hits: results.hits,
query: results.query,
processingTime: results.processingTimeMs,
// For infinite scroll
hasMore: results.estimatedTotalHits > 20
}
}
}
export const searchService = new MobileSearchService()
Voice Search Integration
// Using Web Speech API for voice search
class VoiceSearch {
constructor(onResult) {
this.recognition = new (window.SpeechRecognition ||
window.webkitSpeechRecognition)()
this.recognition.continuous = false
this.recognition.interimResults = false
this.recognition.onresult = (event) => {
const transcript = event.results[0][0].transcript
onResult(transcript)
}
}
start() {
this.recognition.start()
}
stop() {
this.recognition.stop()
}
}
// Usage in React component
function SearchScreen() {
const [query, setQuery] = useState('')
const handleVoiceSearch = (transcript) => {
setQuery(transcript)
performSearch(transcript)
}
return (
<View>
<SearchBar
value={query}
onChangeText={setQuery}
onVoicePress={() => new VoiceSearch(handleVoiceSearch).start()}
/>
<SearchResults />
</View>
)
}
Multi-Tenant Applications
Multi-tenant search requires strict data isolation while maintaining efficient resource usage.
Key Requirements
Multi-tenant search needs:
- Tenant isolation
- Resource sharing efficiency
- Custom relevance per tenant
- Tenant-specific dictionaries
Implementation with Tenant Tokens
const { MeiliSearch } = require('meilisearch')
// Generate tenant-specific tokens server-side
function generateTenantToken(tenantId, permissions) {
const client = new MeiliSearch({
host: process.env.MEILI_HOST,
apiKey: process.env.MEILI_MASTER_KEY
})
const apiKey = client.generateTenantToken(
tenantId,
permissions,
{ expiresIn: '24h' }
)
return apiKey
}
// Client uses tenant-specific key
function getClientForTenant(tenantId, tenantApiKey) {
return new MeiliSearch({
host: process.env.MEILI_HOST,
apiKey: tenantApiKey
})
}
Per-Tenant Index Configuration
// Manage multiple tenants with shared infrastructure
class MultiTenantManager {
constructor() {
this.client = new MeiliSearch({
host: process.env.MEILI_HOST,
apiKey: process.env.MEILI_MASTER_KEY
})
}
async createTenantIndex(tenantId, config) {
const indexName = `tenant_${tenantId}`
// Create index
await this.client.createIndex(indexName, {
primaryKey: 'id'
})
const index = this.client.index(indexName)
// Apply tenant-specific configuration
await index.updateSettings({
searchableAttributes: config.searchableAttributes,
filterableAttributes: config.filterableAttributes,
sortableAttributes: config.sortableAttributes,
typoTolerance: config.typoTolerance || {
enabled: true,
minWordSizeForTypos: { oneTypo: 4, twoTypos: 8 }
}
})
return indexName
}
async searchAsTenant(tenantId, query, options) {
const index = this.client.index(`tenant_${tenantId}`)
return await index.search(query, options)
}
}
Data Isolation Strategies
// Option 1: Separate indexes per tenant
// Pros: Complete isolation, easy cleanup
// Cons: More resources, less efficient
// Option 2: Single index with tenant field
// Pros: Efficient resource usage
// Cons: Requires careful filter management
async function searchWithTenantFilter(tenantId, query) {
const index = client.index('shared_index')
return await index.search(query, {
// Always filter by tenant
filter: `tenant_id = "${tenantId}"`,
limit: 20
})
}
Geo-Spatial Search
Location-aware search is essential for applications like store finders, delivery apps, and real estate listings.
Key Requirements
Geo-search needs:
- Distance-based sorting
- Radius filtering
- Multi-location support
- Map integration
Implementation
While Meilisearch doesn’t have native geo capabilities, you can implement geo-search using a hybrid approach:
// Store coordinates as arrays for distance calculations
const locationIndex = client.index('locations')
await locationIndex.updateSettings({
searchableAttributes: ['name', 'address', 'description'],
filterableAttributes: ['category', 'city', 'country'],
sortableAttributes: ['name', 'rating']
})
// Index locations with coordinates
const locations = [
{
id: 'store-1',
name: 'Downtown Branch',
address: '123 Main St, New York, NY',
city: 'New York',
country: 'USA',
coordinates: [40.7128, -74.0060], // [lat, lon]
category: 'retail'
},
{
id: 'store-2',
name: 'Brooklyn Branch',
address: '456 Atlantic Ave, Brooklyn, NY',
city: 'Brooklyn',
country: 'USA',
coordinates: [40.6782, -73.9442],
category: 'retail'
}
]
await locationIndex.add_documents(locations)
// Haversine distance calculation (for sorting)
function calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371 // Earth's radius in km
const dLat = toRad(lat2 - lat1)
const dLon = toRad(lon2 - lon1)
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
Math.sin(dLon/2) * Math.sin(dLon/2)
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))
return R * c
}
function toRad(deg) {
return deg * (Math.PI/180)
}
// Search with geo-filtering (client-side radius)
async function searchNearby(query, lat, lon, radiusKm) {
// Get more results than needed
const results = await locationIndex.search(query, {
filter: 'category = "retail"',
limit: 50,
attributesToRetrieve: ['*']
})
// Filter by distance client-side
const nearby = results.hits
.filter(hit => {
const [hitLat, hitLon] = hit.coordinates
const distance = calculateDistance(lat, lon, hitLat, hitLon)
hit.distance = distance
return distance <= radiusKm
})
.sort((a, b) => a.distance - b.distance)
.slice(0, 20)
return nearby
}
Real-Time Inventory Search
Inventory search requires real-time updates and availability filtering.
Implementation
const inventoryIndex = client.index('inventory')
// Configure for real-time updates
async function configureInventorySearch() {
await inventoryIndex.updateSettings({
searchableAttributes: [
'sku^3',
'product_name^2',
'description',
'brand',
'tags'
],
filterableAttributes: [
'warehouse_id',
'quantity',
'status',
'category',
'price'
],
typoTolerance: {
enabled: true
}
})
}
// Real-time inventory updates
async function updateInventory(sku, quantity, warehouseId) {
const task = await inventoryIndex.updateDocuments([{
id: sku,
quantity: quantity,
status: quantity > 0 ? 'available' : 'out_of_stock',
warehouse_id: warehouseId,
last_updated: new Date().toISOString()
}])
return task
}
// Search with availability filter
async function searchAvailableProducts(query) {
return await inventoryIndex.search(query, {
filter: 'quantity > 0 AND status = "available"',
sort: ['quantity:desc'],
limit: 50,
attributesToRetrieve: [
'id', 'sku', 'product_name', 'quantity',
'price', 'warehouse_id'
]
})
}
Best Practices for Production
These patterns ensure reliable production deployments.
Error Handling
class MeilisearchService {
constructor(client) {
this.client = client
this.maxRetries = 3
this.retryDelay = 1000
}
async searchWithRetry(indexName, query, options = {}, attempt = 1) {
try {
const index = this.client.index(indexName)
return await index.search(query, options)
} catch (error) {
if (attempt >= this.maxRetries) {
throw error
}
// Exponential backoff
await new Promise(r => setTimeout(r, this.retryDelay * attempt))
return this.searchWithRetry(indexName, query, options, attempt + 1)
}
}
isRetryableError(error) {
const retryableCodes = ['code', 'internal', 'timeout']
return error.code && retryableCodes.includes(error.code)
}
}
Rate Limiting
const rateLimit = require('express-rate-limit')
const searchLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per minute
message: { error: 'Too many searches, please try again later' }
})
app.use('/api/search', searchLimiter)
Monitoring
const promClient = require('prom-client')
const searchDuration = new promClient.Histogram({
name: 'meilisearch_query_duration_ms',
help: 'Duration of Meilisearch queries in milliseconds',
labelNames: ['index', 'result_count'],
buckets: [10, 25, 50, 100, 250, 500]
})
async function monitoredSearch(indexName, query) {
const start = process.hrtime.bigint()
try {
const results = await client.index(indexName).search(query)
const duration = Number(process.hrtime.bigint() - start) / 1e6
searchDuration.labels({
index: indexName,
result_count: results.estimatedTotalHits
}).observe(duration)
return results
} catch (error) {
// Log error
console.error('Search error:', error)
throw error
}
}
Performance Optimization
// Optimize for large result sets
async function optimizedSearch(query, pagination) {
const index = client.index('products')
// Use offset-based pagination for better performance
const offset = (pagination.page - 1) * pagination.limit
return await index.search(query, {
limit: pagination.limit,
offset: offset,
// Only retrieve needed attributes
attributesToRetrieve: ['id', 'title', 'price', 'image_url'],
attributesToHighlight: ['title'],
// Request facets for filter UI
facets: ['category', 'brand'],
// Use cold start optimization
filter: 'status = "active"'
})
}
// Implement caching for frequent queries
const queryCache = new Map()
const CACHE_TTL = 60000 // 1 minute
async function cachedSearch(query) {
const cached = queryCache.get(query)
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.results
}
const results = await client.index('products').search(query)
queryCache.set(query, {
results,
timestamp: Date.now()
})
return results
}
Scaling Patterns
Handle growth with these strategies.
Horizontal Scaling
// Client-side load balancing
class MeilisearchPool {
constructor(endpoints) {
this.endpoints = endpoints.map(url => ({
url,
client: new MeiliSearch({ host: url }),
requests: 0
}))
this.current = 0
}
getClient() {
const client = this.endpoints[this.current]
this.current = (this.current + 1) % this.endpoints.length
return client.client
}
async search(query, options) {
// Try each endpoint until success
for (let i = 0; i < this.endpoints.length; i++) {
try {
const client = this.getClient()
return await client.index('products').search(query, options)
} catch (error) {
console.error(`Search failed on endpoint ${i}:`, error)
}
}
throw new Error('All search endpoints failed')
}
}
Data Partitioning
// Partition by category for better performance
class PartitionedSearch {
constructor() {
this.partitions = {
electronics: new MeiliSearch({ host: process.env.MEILI_ELECTRONICS }),
clothing: new MeiliSearch({ host: process.env.MEILI_CLOTHING }),
home: new MeiliSearch({ host: process.env.MEILI_HOME })
}
}
async search(query, category = null) {
if (category && this.partitions[category]) {
// Search specific partition
return this.partitions[category].index('products').search(query)
}
// Search all partitions
const results = await Promise.all(
Object.values(this.partitions).map(client =>
client.index('products').search(query, { limit: 10 })
)
)
// Merge and deduplicate results
return this.mergeResults(results)
}
}
External Resources
- Meilisearch Examples
- E-commerce Search Best Practices
- Documentation Search Guide
- Meilisearch Community
Conclusion
Meilisearch excels in production environments across diverse use cases. Its combination of speed, relevance, and simplicity makes it an excellent choice for e-commerce, documentation, mobile, multi-tenant, and real-time applications.
Key production insights:
- Configure searchable, filterable, and sortable attributes appropriately
- Implement proper error handling and retry logic
- Use caching for frequently accessed content
- Monitor performance and scale proactively
- Consider partitioning for very large datasets
These real-world patterns and best practices will help you build robust, scalable search experiences with Meilisearch.
This completes our comprehensive Meilisearch article series. You now have the foundation to implement everything from basic search to sophisticated AI-powered applications.
Comments