A powerful search integration supporting Meilisearch and Algolia, providing full-text search, indexing, faceted search, and real-time updates for your application.
Installation
bunadd@stacksjs/search-engine
Basic Usage
import{useSearchEngine,useMeilisearch,useAlgolia}from'@stacksjs/search-engine'// Use configured search engineconstsearch=useSearchEngine()// Search documentsconstresults=awaitsearch.search('products','wireless headphones')// Add documents to indexawaitsearch.addDocuments('products',[{id:1,name:'Wireless Headphones',price:99.99}])
import{useMeilisearch}from'@stacksjs/search-engine'constmeilisearch=useMeilisearch()// Simple searchconstresults=awaitmeilisearch.search('products','headphones')// Search with optionsconstresults=awaitmeilisearch.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)// Search resultsconsole.log(results.query)// Original queryconsole.log(results.processingTimeMs)// Search timeconsole.log(results.estimatedTotalHits)// Total matches
import{addDocuments,updateDocuments,deleteDocuments}from'@stacksjs/search-engine'// Add documentsawaitaddDocuments('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}])// Update documents (merges with existing)awaitupdateDocuments('products',[{id:1,price:44.99}// Only updates price])// Delete documentsawaitdeleteDocuments('products',[1,2])// Delete all documentsawaitflushDocuments('products')
Index Management
import{indexList,createIndex,deleteIndex}from'@stacksjs/search-engine'// List all indexesconstindexes=awaitindexList()// Create index with primary keyawaitcreateIndex('products','id')// Delete indexawaitdeleteIndex('products')
Index Settings
import{getSettings,updateSettings}from'@stacksjs/search-engine'// Get current settingsconstsettings=awaitgetSettings('products')// Update settingsawaitupdateSettings('products',{// Searchable attributes (in priority order)searchableAttributes:['name','description','category','brand'],// Filterable attributes (for filtering and facets)filterableAttributes:['category','brand','price','inStock','rating'],// Sortable attributessortableAttributes:['price','rating','createdAt'],// Ranking rules (order matters)rankingRules:['words','typo','proximity','attribute','sort','exactness','rating:desc'// Custom ranking by rating],// Stop words (ignored in search)stopWords:['the','a','an','is','are'],// Synonymssynonyms:{'phone':['smartphone','mobile','cell'],'laptop':['notebook','computer']},// Displayed attributes (returned in results)displayedAttributes:['*'],// All attributes// Distinct attribute (for deduplication)distinctAttribute:'productGroup',// Typo tolerancetypoTolerance:{enabled:true,minWordSizeForTypos:{oneTypo:5,twoTypos:9},disableOnWords:['exact-match'],disableOnAttributes:['sku']},// Pagination limitspagination:{maxTotalHits:10000}})
Algolia
Basic Search
import{useAlgolia}from'@stacksjs/search-engine'constalgolia=useAlgolia()// Simple searchconstresults=awaitalgolia.search('products','headphones')// Search with parametersconstresults=awaitalgolia.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
// Search across multiple indexesconstresults=awaitalgolia.multiSearch([{indexName:'products',query:'laptop'},{indexName:'categories',query:'laptop'},{indexName:'brands',query:'laptop'}])
// app/Models/Product.tsexportdefault{name:'Product',table:'products',searchable:true,searchIndex:'products',// Attributes to indexsearchableAttributes:['name','description','category','brand','sku'],// Transform data for searchtoSearchableArray(){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
// Models automatically sync on create/update/deleteconstproduct=awaitProduct.create({name:'New Product',price:99.99})// Automatically indexed in search engineawaitproduct.update({price:89.99})// Automatically updated in search indexawaitproduct.delete()// Automatically removed from search index
Manual Sync
// Sync single modelawaitproduct.searchable()// Remove from searchawaitproduct.unsearchable()// Bulk syncawaitProduct.where('category','electronics').searchable()// Rebuild entire indexawaitProduct.reindex()
// Search with typosconstresults=awaitsearch.search('products','wireles headphnes')// Still matches "wireless headphones"
Geo Search
// Search near a locationconstresults=awaitsearch.search('stores','',{filter:'_geoRadius(48.8566, 2.3522, 1000)',// 1km radius around Parissort:['_geoPoint(48.8566, 2.3522):asc']})
Filtering
// Complex filtersconstresults=awaitsearch.search('products','laptop',{filter:['category = "electronics"','brand IN ["Apple", "Dell", "HP"]','price >= 500','price <= 2000','inStock = true','rating >= 4'].join(' AND ')})
Sorting
// Sort by multiple attributesconstresults=awaitsearch.search('products','laptop',{sort:['featured:desc','price:asc','rating:desc']})
Real-time Updates
Webhook Integration
// Listen for search index updatesimport{onIndexUpdate}from'@stacksjs/search-engine'onIndexUpdate('products',async(event)=>{console.log('Index updated:',event.type)// 'add', 'update', 'delete'console.log('Documents:',event.documentIds)})
Queue Integration
// Async indexing via queueimport{queueIndex,queueDelete}from'@stacksjs/search-engine'// Queue document for indexingawaitqueueIndex('products',product.toSearchableArray())// Queue deletionawaitqueueDelete('products',product.id)
Edge Cases
Handling Large Datasets
// Batch indexing for large datasetsconstproducts=awaitProduct.all()constbatches=chunk(products,500)for(constbatchofbatches){awaitaddDocuments('products',batch.map(p=>p.toSearchableArray()))// Optional: Add delay to avoid rate limitingawaitsleep(100)}
Handling Connection Failures
try{constresults=awaitsearch.search('products','query')}catch(error){if(error.code==='ECONNREFUSED'){// Search engine unavailable, fallback to databaseconstresults=awaitProduct.whereLike('name',`%query%`).get()returnresults}throwerror}
Index Versioning
// Create new index versionconstnewIndex='products_v2'awaitcreateIndex(newIndex)// Populate new indexawaitaddDocuments(newIndex,documents)// Swap indexes atomicallyawaitswapIndexes('products',newIndex)
Empty Results Handling
constresults=awaitsearch.search('products','nonexistent')if(results.hits.length===0){// Show suggestionsconstsuggestions=awaitsearch.search('products','',{limit:5,sort:['rating:desc']})return{hits:[],suggestions:suggestions.hits}}