An idiomatic, high-performance, and lightweight Meilisearch client for Elixir built on top of Req and Finch.

Why Meili?

  • No Bloat: Built directly on Req, leveraging connection pooling and HTTP/2 stream multiplexing out-of-the-box. No heavy middleware layers.
  • Idiomatic Elixir:
    • Provides both a global/default client configuration (via Application environment) and explicit client structs (for multi-tenant setups).
    • Automatically converts Elixir-style snake_case keys to Meilisearch-style camelCase keys for query parameters, search options, and settings.
    • Standard operations return {:ok, result} / {:error, error} and suffix-raising bang (!) functions (e.g., search!/3).
    • Custom exception structures (Meili.Error) wrap Meilisearch-specific API errors (code, type, message, link).
  • Asynchronous Polling: Includes a robust wait_for_task/3 helper that polls tasks with backoff and raises clear error messages if tasks fail.

Installation

Add meili to your list of dependencies in mix.exs:

def deps do
  [
    {:meili, "~> 0.1.0"}
  ]
end

Configuration

In your config/config.exs or config/runtime.exs:

config :meili,
  url: System.get_env("MEILI_HTTP_ADDR") || "http://localhost:7700",
  key: System.get_env("MEILI_MASTER_KEY")

Quick Start

Global Client Mode

If you're using a single Meilisearch instance, configure your credentials in the config and call operations directly:

# Create an index
{:ok, task} = Meili.create_index("movies", primary_key: "id")
# Block until index creation completes
Meili.wait_for_task!(task["taskUid"])

# Add documents
documents = [
  %{id: 1, title: "Star Wars: Episode IV", genre: "Sci-Fi"},
  %{id: 2, title: "The Dark Knight", genre: "Action"}
]
{:ok, task} = Meili.add_documents("movies", documents)
Meili.wait_for_task!(task["taskUid"])

# Search documents (note: snake_case parameters are automatically converted to camelCase)
{:ok, results} = Meili.search("movies", "star wars", limit: 5, show_ranking_score: true)
IO.inspect(results["hits"])

Explicit Client Mode (Multi-tenant / Dynamic Setup)

If you need to connect to multiple instances dynamically:

client = Meili.client(url: "https://my-meili-instance.com", key: "secret-key")

# Perform search with the explicit client
{:ok, results} = Meili.search(client, "movies", "batman")

Settings Management

settings = %{
  searchable_attributes: ["title", "overview"],
  filterable_attributes: ["genre"],
  ranking_rules: ["words", "typo", "proximity"]
}

{:ok, task} = Meili.Settings.update("movies", settings)
Meili.wait_for_task!(task["taskUid"])

Testing

This library comes with a mock-server testing suite built using Bypass. You can run the tests locally without needing a live Meilisearch instance:

mix test