Search Engine Package
A powerful search integration supporting Meilisearch and Algolia, providing full-text search, indexing, faceted search, and real-time updates for your application.
Installation
bun add @stacksjs/search-engine
Basic Usage
import { useSearchEngine, useMeilisearch, useAlgolia } from '@stacksjs/search-engine'
const search = useSearchEngine()
const results = await search.search('products', 'wireless headphones')
await search.addDocuments('products', [
{ id: 1, name: 'Wireless Headphones', price: 99.99 }
])
Configuration
Environment Variables
# Search engine driver
SEARCH_ENGINE_DRIVER=meilisearch
# Meilisearch configuration
MEILISEARCH_HOST=http://localhost:7700
MEILISEARCH_KEY=your-master-key
# Algolia configuration
ALGOLIA_APP_ID=your-app-id
ALGOLIA_API_KEY=your-api-key
ALGOLIA_SEARCH_KEY=your-search-key
Configuration File
export default {
driver: 'meilisearch',
meilisearch: {
host: 'http://localhost:7700',
apiKey: 'masterKey',
indexPrefix: 'myapp_'
},
algolia: {
appId: 'your-app-id',
apiKey: 'your-api-key',
searchKey: 'your-search-key',
indexPrefix: 'myapp_'
}
}
Meilisearch
Basic Search
import { useMeilisearch } from '@stacksjs/search-engine'
const meilisearch = useMeilisearch()
const results = await meilisearch.search('products', 'headphones')
const results = await meilisearch.search('products', 'headphones', {
limit: 20,
offset: 0,
filter: 'category = "electronics" AND price < 200',
sort: ['price:asc'],
attributesToRetrieve: ['id', 'name', 'price', 'description'],
attributesToHighlight: ['name', 'description'],
attributesToCrop: ['description'],
cropLength: 50
})
console.log(results.hits)
console.log(results.query)
console.log(results.processingTimeMs)
console.log(results.estimatedTotalHits)
Faceted Search
const results = await meilisearch.search('products', 'laptop', {
facets: ['category', 'brand', 'color'],
filter: 'price >= 500 AND price <= 1500'
})
console.log(results.facetDistribution)
Indexing Documents
import { addDocuments, updateDocuments, deleteDocuments } from '@stacksjs/search-engine'
await addDocuments('products', [
{
id: 1,
name: 'Wireless Mouse',
description: 'Ergonomic wireless mouse with long battery life',
category: 'electronics',
brand: 'Logitech',
price: 49.99,
inStock: true,
rating: 4.5
},
{
id: 2,
name: 'Mechanical Keyboard',
description: 'RGB mechanical keyboard with cherry switches',
category: 'electronics',
brand: 'Corsair',
price: 129.99,
inStock: true,
rating: 4.8
}
])
await updateDocuments('products', [
{ id: 1, price: 44.99 }
])
await deleteDocuments('products', [1, 2])
await flushDocuments('products')
Index Management
import { indexList, createIndex, deleteIndex } from '@stacksjs/search-engine'
const indexes = await indexList()
await createIndex('products', 'id')
await deleteIndex('products')
Index Settings
import { getSettings, updateSettings } from '@stacksjs/search-engine'
const settings = await getSettings('products')
await updateSettings('products', {
searchableAttributes: [
'name',
'description',
'category',
'brand'
],
filterableAttributes: [
'category',
'brand',
'price',
'inStock',
'rating'
],
sortableAttributes: [
'price',
'rating',
'createdAt'
],
rankingRules: [
'words',
'typo',
'proximity',
'attribute',
'sort',
'exactness',
'rating:desc'
],
stopWords: ['the', 'a', 'an', 'is', 'are'],
synonyms: {
'phone': ['smartphone', 'mobile', 'cell'],
'laptop': ['notebook', 'computer']
},
displayedAttributes: ['*'],
distinctAttribute: 'productGroup',
typoTolerance: {
enabled: true,
minWordSizeForTypos: {
oneTypo: 5,
twoTypos: 9
},
disableOnWords: ['exact-match'],
disableOnAttributes: ['sku']
},
pagination: {
maxTotalHits: 10000
}
})
Algolia
Basic Search
import { useAlgolia } from '@stacksjs/search-engine'
const algolia = useAlgolia()
const results = await algolia.search('products', 'headphones')
const results = await algolia.search('products', 'headphones', {
hitsPerPage: 20,
page: 0,
filters: 'category:electronics AND price < 200',
facets: ['category', 'brand'],
attributesToRetrieve: ['name', 'price', 'description'],
attributesToHighlight: ['name', 'description'],
highlightPreTag: '<mark>',
highlightPostTag: '</mark>'
})
Multi-Index Search
const results = await algolia.multiSearch([
{ indexName: 'products', query: 'laptop' },
{ indexName: 'categories', query: 'laptop' },
{ indexName: 'brands', query: 'laptop' }
])
Indexing with Algolia
await algolia.saveObjects('products', [
{ objectID: '1', name: 'Product 1', price: 99.99 },
{ objectID: '2', name: 'Product 2', price: 149.99 }
])
await algolia.partialUpdateObjects('products', [
{ objectID: '1', price: 89.99 }
])
await algolia.deleteObjects('products', ['1', '2'])
await algolia.clearObjects('products')
Model Integration
Searchable Models
export default {
name: 'Product',
table: 'products',
searchable: true,
searchIndex: 'products',
searchableAttributes: [
'name',
'description',
'category',
'brand',
'sku'
],
toSearchableArray() {
return {
id: this.id,
name: this.name,
description: this.description,
category: this.category?.name,
brand: this.brand?.name,
price: this.price,
inStock: this.inventory > 0,
rating: this.averageRating,
createdAt: this.createdAt.getTime()
}
}
}
Auto-Sync
const product = await Product.create({
name: 'New Product',
price: 99.99
})
await product.update({ price: 89.99 })
await product.delete()
Manual Sync
await product.searchable()
await product.unsearchable()
await Product.where('category', 'electronics').searchable()
await Product.reindex()
Search Features
Highlighting
const results = await search.search('products', 'wireless mouse', {
attributesToHighlight: ['name', 'description'],
highlightPreTag: '<em>',
highlightPostTag: '</em>'
})
results.hits.forEach(hit => {
console.log(hit._formatted.name)
})
Typo Tolerance
const results = await search.search('products', 'wireles headphnes')
Geo Search
const results = await search.search('stores', '', {
filter: '_geoRadius(48.8566, 2.3522, 1000)',
sort: ['_geoPoint(48.8566, 2.3522):asc']
})
Filtering
const results = await search.search('products', 'laptop', {
filter: [
'category = "electronics"',
'brand IN ["Apple", "Dell", "HP"]',
'price >= 500',
'price <= 2000',
'inStock = true',
'rating >= 4'
].join(' AND ')
})
Sorting
const results = await search.search('products', 'laptop', {
sort: [
'featured:desc',
'price:asc',
'rating:desc'
]
})
Real-time Updates
Webhook Integration
import { onIndexUpdate } from '@stacksjs/search-engine'
onIndexUpdate('products', async (event) => {
console.log('Index updated:', event.type)
console.log('Documents:', event.documentIds)
})
Queue Integration
import { queueIndex, queueDelete } from '@stacksjs/search-engine'
await queueIndex('products', product.toSearchableArray())
await queueDelete('products', product.id)
Edge Cases
Handling Large Datasets
const products = await Product.all()
const batches = chunk(products, 500)
for (const batch of batches) {
await addDocuments('products', batch.map(p => p.toSearchableArray()))
await sleep(100)
}
Handling Connection Failures
try {
const results = await search.search('products', 'query')
} catch (error) {
if (error.code === 'ECONNREFUSED') {
const results = await Product.whereLike('name', `%query%`).get()
return results
}
throw error
}
Index Versioning
const newIndex = 'products_v2'
await createIndex(newIndex)
await addDocuments(newIndex, documents)
await swapIndexes('products', newIndex)
Empty Results Handling
const results = await search.search('products', 'nonexistent')
if (results.hits.length === 0) {
const suggestions = await search.search('products', '', {
limit: 5,
sort: ['rating:desc']
})
return { hits: [], suggestions: suggestions.hits }
}
API Reference
Driver Functions
| Function | Description |
useSearchEngine() | Get configured search driver |
useMeilisearch() | Get Meilisearch client |
useAlgolia() | Get Algolia client |
Document Operations
| Function | Description |
addDocuments(index, docs) | Add documents to index |
updateDocuments(index, docs) | Update existing documents |
deleteDocuments(index, ids) | Delete documents by ID |
flushDocuments(index) | Delete all documents |
Index Operations
| Function | Description |
indexList() | List all indexes |
createIndex(name, key?) | Create new index |
deleteIndex(name) | Delete index |
getSettings(index) | Get index settings |
updateSettings(index, settings) | Update settings |
Search Methods
| Method | Description |
search(index, query, options?) | Perform search |
multiSearch(queries) | Search multiple indexes |
Search Options
| Option | Description |
limit / hitsPerPage | Results per page |
offset / page | Pagination |
filter / filters | Filter expression |
sort | Sort order |
facets | Facet attributes |
attributesToRetrieve | Fields to return |
attributesToHighlight | Fields to highlight |
attributesToCrop | Fields to crop |
cropLength | Crop length |