Search is one of those features that can make or break an application. Users expect it to be instant, forgiving of typos, and smart about relevance. Building that experience from scratch is a significant investment. That is exactly why we built bx-meilisearch; a BoxLang-native module that puts the full power of Meilisearch at your fingertips with a fluent, chainable DSL that feels right at home in any BoxLang application. With that, we are excited to announce that bx-meilisearch is available now for all BoxLang+, BoxLang Starter, and BoxLang++ subscribers. π
π What Is Meilisearch?
Meilisearch is a lightning-fast, open-source search engine built for instant, relevant full-text search. It delivers search-as-you-type experiences with:
- β‘ Sub-50ms response times out of the box
- π€ Typo tolerance β returns useful results even when queries are misspelled
- π·οΈ Filtering and faceting β narrow results by categories, tags, price ranges, statuses, and more
- π Relevance-based ranking with customizable ranking rules
- π§© Simple deployment β a single binary or Docker container, no JVM required on the search side
It is a strong fit for site search, product catalogs, knowledge bases, internal tools, and any application where indexed records need to be queried quickly and intuitively.
π¦ Installation
Install bx-meilisearch using the standard BoxLang module workflow:
# Quick installer (OS installations)
install-bx-module bx-plus,bx-meilisearch
# CommandBox (web servers)
box install bx-plus,bx-meilisearch
Configuration
The module auto-configures from environment variables β the fastest path to getting started:
export MEILISEARCH_URL=http://localhost:7700
export MEILISEARCH_MASTER_KEY=your-master-key
Or configure explicitly in boxlang.json:
{
"modules": {
"meilisearch": {
"enabled": true,
"settings": {
"url" : "http://localhost:7700",
"masterKey": "your-master-key"
}
}
}
}
Requirements: BoxLang 1.12+, Java 21+, and a running Meilisearch instance reachable over HTTP or HTTPS.
π‘ The Fluent API Philosophy
Everything in bx-meilisearch flows from a single built-in function: meilisearch(). Every operation chains off it, making your code read like a sentence.
// This is what search looks like in bx-meilisearch
results = meilisearch()
.index( "books" )
.search( "boxlang" )
.filter( "genre = programming" )
.limit( 10 )
.send()
No verbose configuration objects. No HTTP client boilerplate. No manual JSON serialization. Just a clean, readable chain that maps directly to Meilisearch's API surface.
ποΈ Index Management
Indexes are the top-level containers for your searchable data. bx-meilisearch gives you full lifecycle control.
// Create an index with a primary key
meilisearch()
.index( "books" )
.create( "id" )
// Get index details
var info = meilisearch()
.index( "books" )
.get()
// Get index statistics (document count, field distribution, etc.)
var stats = meilisearch()
.index( "books" )
.stats()
// Compact an index to reduce fragmentation
meilisearch()
.index( "books" )
.compact()
// Delete an index and all its documents
meilisearch()
.index( "books" )
.delete()
Listing and Swapping
// List all indexes
var allIndexes = meilisearch()
.indexes()
.list()
// Swap two indexes β great for zero-downtime re-indexing deployments
meilisearch()
.indexes()
.swap( [ "books", "books_staging" ] )
// Get global database stats across all indexes
var dbStats = meilisearch()
.indexes()
.stats()
The swap operation is particularly powerful for production deployments. Build and populate a staging index, validate it, then swap it atomically with the live index β zero downtime, zero risk. β
π Document Management
Adding, updating, and deleting documents all return Task objects because Meilisearch processes writes asynchronously. More on Tasks below.
Adding Documents
// Add or replace documents (full replacement on primary key match)
var task = meilisearch()
.index( "books" )
.documents()
.add( [
{ id: 1, title: "BoxLang in Action", author: "Ortus Solutions", genre: "programming" },
{ id: 2, title: "Search-Driven Apps", author: "Jane Doe", genre: "architecture" }
] )
.orReplace()
// Add or update documents (partial update on primary key match)
var task = meilisearch()
.index( "books" )
.documents()
.add( [
{ id: 1, title: "BoxLang in Action β Second Edition" }
] )
.orUpdate()
orReplace() fully replaces a matching document. orUpdate() merges changes, leaving unspecified fields intact. Choose the one that fits your data pipeline.
Retrieving Documents
// Get a page of documents
var page = meilisearch()
.index( "books" )
.documents()
.get( { offset: 0, limit: 20 } )
// Get a single document by ID
var book = meilisearch()
.index( "books" )
.document( "1" )
.get()
Deleting Documents
// Delete a single document
var task = meilisearch()
.index( "books" )
.document( "1" )
.delete()
// Delete all documents (keeps the index, clears the data)
var task = meilisearch()
.index( "books" )
.documents()
.deleteAll()
π The Search DSL
Search is where bx-meilisearch really shines. The fluent DSL lets you compose rich queries in a natural, readable way.
Basic Search
var results = meilisearch()
.index( "books" )
.search( "tolkien" )
.send()
// results.hits β array of matching documents
// results.query β "tolkien"
// results.processingTimeMs β typically < 5
// results.estimatedTotalHits β total match count
Filtered and Paginated Search
var results = meilisearch()
.index( "books" )
.search( "fantasy" )
.filter( "genre = fantasy AND year > 2000" )
.limit( 10 )
.offset( 20 )
.attributesToRetrieve( "title,author,year" )
.sort( "year:desc" )
.send()
Initial Parameters Shorthand
You can pass an initial parameters struct as the second argument to search() and chain from there:
var results = meilisearch()
.index( "books" )
.search( "tolkien", { limit: 5, offset: 0 } )
.filter( "genre = fantasy" )
.send()
Faceted Search
Faceting lets you build category-based navigation UIs. Any attribute listed in filterableAttributes can be faceted.
var results = meilisearch()
.index( "books" )
.search( "fiction" )
.filter( "genre = fiction" )
.facets( "genre,year" )
.send()
Facet Value Search
Search for specific values within a facet β useful for autocomplete-style facet pickers:
var facetResult = meilisearch()
.index( "books" )
.facetSearch( "genre", {
facetQuery : "fic",
q : "fantasy",
matchingStrategy: "all"
} )
.send()
// facetResult.facetHits β [ { value: "fiction", count: 7 } ]
GET vs POST
All searches default to POST. Use sendWithGet() when your context requires a GET request:
var results = meilisearch()
.index( "books" )
.search( "tolkien" )
.limit( 10 )
.sendWithGet()
βοΈ Index Settings
Settings control how Meilisearch indexes and ranks your data. Getting them right is the difference between good search and great search.
meilisearch()
.index( "books" )
.settings()
.update( {
"searchableAttributes": [ "title", "description", "author" ],
"filterableAttributes": [ "genre", "year", "available" ],
"sortableAttributes" : [ "year", "price" ],
"rankingRules": [
"words",
"typo",
"proximity",
"attribute",
"sort",
"exactness"
],
"stopWords": [ "the", "a", "an" ],
"synonyms": {
"computer": [ "pc", "laptop", "desktop" ]
}
} )
// Inspect current settings
var current = meilisearch()
.index( "books" )
.settings()
.list()
// Reset all settings to Meilisearch defaults
meilisearch()
.index( "books" )
.settings()
.reset()
π‘ Tip: Always define
filterableAttributesbefore running filtered searches. Meilisearch builds a separate filter index for each filterable attribute β attributes not listed here cannot be used infilter()calls.
β³ Async Task Tracking
Meilisearch processes writes asynchronously. Every operation that modifies data β creating indexes, adding documents, updating settings β returns a Task object immediately while the work happens in the background.
var task = meilisearch()
.index( "books" )
.documents()
.add( [
{ id: 1, title: "Book 1" },
{ id: 2, title: "Book 2" }
] )
.orReplace()
// Inspect the task immediately
println( "Status : " & task.getStatus() ) // "enqueued"
println( "Type : " & task.getType() ) // "documentAdditionOrUpdate"
println( "Task UID: " & task.getTaskUid() ) // "42"
// Block until complete (polls every 1s, 30s timeout by default)
var completed = task.waitForCompletion()
if ( completed.getStatus() == "succeeded" ) {
println( "Indexed: " & completed.getDetails().receivedDocuments & " documents" )
} else if ( completed.getStatus() == "failed" ) {
println( "Error: " & completed.getError().message )
}
Task Status Lifecycle
enqueued β processing β succeeded
β failed
β canceled
All Task Getters
task.getStatus() // Current status
task.getType() // Operation type
task.getTaskUid() // Unique task ID
task.getIndexUid() // Target index
task.getEnqueuedAt() // ISO timestamp when queued
task.getStartedAt() // ISO timestamp when processing began
task.getFinishedAt() // ISO timestamp when done
task.getDuration() // Processing duration
task.getDetails() // Operation-specific detail struct
task.getError() // Error struct if failed
task.getCanceledBy() // UID of canceling task (if applicable)
task.isComplete() // true if succeeded, failed, or canceled
task.toStruct() // Full task data as a plain struct
Custom Poll Interval and Timeout
// Poll every 2 seconds, timeout after 60 seconds
var completed = task.waitForCompletion( 2000, 60000 )
Listing and Managing Tasks
// List all tasks
var allTasks = meilisearch()
.tasks()
.list()
// Filter tasks by status and type
var pending = meilisearch()
.tasks()
.list( {
limit : 20,
statuses: "enqueued,processing",
types : "documentAdditionOrUpdate"
} )
// Get a single task by UID
var task = meilisearch()
.tasks()
.get( "123" )
// Cancel enqueued tasks
meilisearch()
.tasks()
.cancel( { statuses: "enqueued" } )
// Delete old completed tasks
meilisearch()
.tasks()
.delete( {
statuses : "succeeded,failed",
beforeEnqueuedAt: "2025-01-01T00:00: 00Z"
} )
π Webhooks
Webhooks let Meilisearch push task completion notifications to your application instead of polling.
// Create a webhook
meilisearch()
.webhooks()
.create( {
url : "https://your-app.com/webhooks/meilisearch",
headers: {
authorization: "Bearer YOUR_WEBHOOK_SECRET"
}
} )
// List all webhooks
var hooks = meilisearch()
.webhooks()
.list()
// Update a webhook
meilisearch()
.webhooks()
.webhook( "WEBHOOK_UUID" )
.update( { headers: { referer: null } } ) // null removes the field
// Delete a webhook
meilisearch()
.webhooks()
.webhook( "WEBHOOK_UUID" )
.delete()
β οΈ You can configure up to 20 webhooks. Having many active simultaneously may impact indexing performance.
π¦ Batches
Batches show how Meilisearch groups tasks for parallel processing β useful for monitoring bulk operations:
// List all batches
var batches = meilisearch()
.batches()
.list( { limit: 10 } )
// batches.results[1].stats.status.succeeded β count of succeeded tasks in batch
// Get a specific batch
var batch = meilisearch()
.batches()
.get( "0" )
// batch.progress.percentage β current completion percentage
π€ Similar Documents (AI-Powered)
If your index has vector embeddings configured, you can find semantically similar documents:
// Find documents similar to document ID "123"
var similar = meilisearch()
.index( "books" )
.document( "123" )
.similar( {
embedder : "default",
limit : 10,
filter : "genre = fiction",
showRankingScore: true
} )
.send()
// similar.hits β array of similar documents ranked by vector similarity
This requires an embedder configured in your index settings β see Meilisearch's AI-Powered Search docs for embedder setup.
π Network and Sharding (Experimental)
For horizontal scaling with federated search across multiple Meilisearch instances:
// Enable the experimental network feature first
meilisearch()
.experimentalFeatures()
.set( "network", true )
// Configure a multi-instance network
meilisearch()
.network()
.update( {
self : "ms-00",
sharding: true,
remotes : {
"ms-00": { url: "http://localhost:7700", searchApiKey: "key0" },
"ms-01": { url: "http://remote-instance:7700", searchApiKey: "key1" }
}
} )
π§ͺ Experimental Features
Toggle Meilisearch experimental features from BoxLang:
// List all experimental features and their current state
var features = meilisearch()
.experimentalFeatures()
.list()
// Enable a feature
meilisearch()
.experimentalFeatures()
.set( "metrics", true )
// Set multiple features at once
meilisearch()
.experimentalFeatures()
.setAll( {
"metrics" : true,
"exportPayer": false
} )
πΈ Snapshots
Create point-in-time backups of your Meilisearch instance:
var result = meilisearch()
.snapshots()
.create()
π API Key Management
Manage scoped API keys for multi-tenant or security-conscious deployments:
// List keys
var keys = meilisearch()
.keys()
.list( { offset: 0, limit: 10 } )
// Get a specific key
var key = meilisearch()
.keys()
.get( "abc123" )
// Update a key
meilisearch()
.keys()
.update( "abc123", { name: "Read-Only Search Key" } )
// Delete a key
meilisearch()
.keys()
.delete( "abc123" )
π₯ Health and Version
Quick operational checks:
// Health check β returns { "status": "available" } when healthy
var health = meilisearch()
.health()
// Version info β commitSha, commitDate, pkgVersion
var version = meilisearch()
.version()
π Real-World Example: Book Catalog Search
Let's put it all together. Here is a complete flow: create an index, configure it for great search, populate it with documents, and query it with filters and facets.
// 1. Create the index
meilisearch()
.index( "books" )
.create( "id" )
// 2. Configure index settings for optimal search
meilisearch()
.index( "books" )
.settings()
.update( {
"searchableAttributes": [ "title", "description", "author", "tags" ],
"filterableAttributes": [ "genre", "year", "available", "rating" ],
"sortableAttributes" : [ "year", "rating", "price" ],
"rankingRules": [
"words",
"typo",
"proximity",
"attribute",
"sort",
"exactness"
],
"stopWords": [ "the", "a", "an", "of" ],
"synonyms" : {
"programming": [ "coding", "development", "software" ],
"boxlang" : [ "bxlang", "bl" ]
}
} )
// 3. Add documents and wait for indexing to complete
var task = meilisearch()
.index( "books" )
.documents()
.add( [
{
id : 1,
title : "BoxLang in Action",
author : "Ortus Solutions",
genre : "programming",
year : 2025,
rating : 5.0,
available : true,
tags : [ "boxlang", "jvm", "dynamic" ],
description: "The definitive guide to building applications with BoxLang."
},
{
id : 2,
title : "Search-Driven Applications",
author : "Jane Doe",
genre : "architecture",
year : 2024,
rating : 4.7,
available : true,
tags : [ "search", "meilisearch", "architecture" ],
description: "Design and build applications around search-first user experiences."
},
{
id : 3,
title : "The JVM Handbook",
author : "John Smith",
genre : "programming",
year : 2023,
rating : 4.2,
available : false,
tags : [ "jvm", "java", "performance" ],
description: "Deep dive into JVM internals, memory management, and performance tuning."
}
] )
.orReplace()
// Wait for indexing to finish
var completed = task.waitForCompletion()
println( "Indexed: " & completed.getDetails().indexedDocuments & " books β
" )
// 4. Search with filters, sorting, and facets
var results = meilisearch()
.index( "books" )
.search( "programming" )
.filter( "available = true AND rating >= 4.5" )
.sort( "rating:desc" )
.limit( 10 )
.facets( "genre,year" )
.attributesToRetrieve( "title,author,genre,year,rating" )
.send()
// 5. Work with results
println( "Found: " & results.estimatedTotalHits & " books" )
println( "Query time: " & results.processingTimeMs & "ms" )
for ( var book in results.hits ) {
println( "π " & book.title & " by " & book.author & " (" & book.year & ") β β " & book.rating )
}
// Facet distribution
if ( !isNull( results.facetDistribution ) ) {
println( "Genres: " & serializeJSON( results.facetDistribution.genre ) )
}
π― Use Cases at a Glance
| Use Case | Why Meilisearch + BoxLang |
|---|---|
| Site / doc search | Instant typo-tolerant full-text search |
| Product catalog | Filters, facets, sorting by price/rating |
| Knowledge base | Index tickets, docs, procedures |
| Admin dashboards | Fast search across internal records |
| Search MVPs | Lightweight deployment, minimal ops overhead |
| Multi-tenant SaaS | Index-per-tenant with scoped API keys |
π Resources
- π¦ Install:
box install bx-meilisearchorinstall-bx-module bx-meilisearch - π Subscription: boxlang.io/plans β Available for all +/Starter/++ tiers
- π BoxLang Docs: boxlang.ortusbooks.com
- π Meilisearch Docs: meilisearch.com/docs
- π¬ Community: community.ortussolutions.com
- π§ Contact: info@boxlang.io
Search is no longer a feature that only large teams with Elasticsearch clusters can afford to ship properly. With bx-meilisearch and BoxLang, you get a production-grade, typo-tolerant, faceted search experience in under an hour β with a fluent API that stays out of your way. π
Give it a spin and let us know what you build!
β The Ortus Team π
Add Your Comment