Blog

Luis Majano

May 12, 2026

Spread the word


Share your thoughts

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.

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
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()

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()

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 filterableAttributes before running filtered searches. Meilisearch builds a separate filter index for each filterable attribute β€” attributes not listed here cannot be used in filter() 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()

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 CaseWhy Meilisearch + BoxLang
Site / doc searchInstant typo-tolerant full-text search
Product catalogFilters, facets, sorting by price/rating
Knowledge baseIndex tickets, docs, procedures
Admin dashboardsFast search across internal records
Search MVPsLightweight deployment, minimal ops overhead
Multi-tenant SaaSIndex-per-tenant with scoped API keys

πŸ“š Resources


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

Recent Entries

Ortus & BoxLang April Recap 2026

Ortus & BoxLang April Recap 2026

This collection brings together the latest updates, releases, events, and insights from the Ortus ecosystem, covering BoxLang, ColdBox, and modern CFML development. From major product launches and AI advancements to in-depth technical guides and real-world modernization strategies, these resources highlight how developers and organizations are building scalable, future-ready applications on the JVM.

It also captures key moments from the community, including webinars and major events li...

Victor Campos
Victor Campos
May 05, 2026