Skip to main content

README.md

# Meili

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`:

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

## Configuration

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

```elixir
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:

```elixir
# 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:

```elixir
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

```elixir
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:

```bash
mix test
```