meilisearch-crystal

Crystal SDK for the Meilisearch API

meilisearch-crystal

A type-safe Meilisearch client for Crystal, inspired by the official meilisearch-ruby SDK and the architecture of jgaskins/meilisearch.

  • Type-safe at the boundary. Server schema is fully typed (Index, Query, Task, Key, errors). Documents and search hits default to JSON::Any and opt into concrete types with as: Book.
  • Async tasks, first-class. Mutations return TaskResults; wait on them with client.wait_for_task (fiber-friendly, non-blocking).
  • Streaming document uploads. Upsert an Enumerable and documents stream to Meilisearch as NDJSON — constant memory, lazy iterators supported.
  • Framework-agnostic. No ORM, no framework bindings. Works from Lucky, Kemal, Athena, or bare scripts.
  • Zero runtime dependencies. Only the Crystal stdlib (http, json).

CI GitHub Pages

Table of contents

Quick start

require "meilisearch-crystal"

client = Meilisearch::Crystal::Client.new(
  url: "http://localhost:7700",
  api_key: "masterKey",
)

client.indexes.create("books", "id")
# => Meilisearch::Crystal::TaskResult

book = {id: 1, title: "Shazam", rating: 7.5}
client.index("books").documents.upsert("books", [book])
# => Meilisearch::Crystal::TaskResult

client.index("books").search("shazam").hits
# => [{"id" => 1_i64, "title" => "Shazam", "rating" => 7.5}]

Installation

  1. Add the dependency to your shard.yml:

    dependencies:
      meilisearch-crystal:
        github: sandroscosta/meilisearch-crystal
    
  2. Run shards install

Configuration

Configure defaults once, then use Meilisearch::Crystal.client:

Meilisearch::Crystal.configure do |config|
  config.url     = ENV["MEILISEARCH_URL"]     # default http://localhost:7700
  config.api_key = ENV["MEILISEARCH_API_KEY"]
  config.timeout = 5.seconds                  # default 5 seconds
end

client = Meilisearch::Crystal.client   # built lazily from the above

Or construct clients explicitly — great for multiple instances:

prod = Meilisearch::Crystal::Client.new(url: "https://meili.prod.example", api_key: "...")
staging = Meilisearch::Crystal::Client.new(url: "https://meili.staging.example", api_key: "...")

When constructed without arguments, Client.new falls back to the MEILISEARCH_URL / MEILISEARCH_API_KEY environment variables.

Typed vs raw results

Documents and search hits are your domain types, so the client doesn't force one on you. By default you get JSON::Any; pass as: to get a typed array.

struct Book
  include JSON::Serializable

  getter id : Int32
  getter title : String
  getter rating : Float64?
end

# raw — no types needed:
results = client.index("books").search("shazam")
results.hits                      # Array(JSON::Any)

# typed — compile-time checked:
results = client.index("books").search("shazam", as: Book)
results.hits                      # Array(Book)

Documents

index = client.index("books")

# upsert (add or replace, keyed by primary key)
index.documents.upsert("books", [{id: 1, title: "Shazam"}])
index.documents.upsert!("books", [{id: 1, title: "Shazam"}]) # waits for the task

# upsert-patch (add or merge, missing fields preserved)
index.documents.upsert_patch("books", [{id: 1, rating: 7.5}])

# fetch documents (POST documents/fetch) — raw or typed
index.documents.fetch("books", limit: 10, as: Book)

# delete documents
index.documents.delete("books", 1)                    # by primary key
index.documents.delete("books", filter: "rating < 5") # matching a filter

# streaming upsert — bounded memory, works with any Enumerable
client.index("books").documents.upsert("books", PostQuery.new)

Search

query = Meilisearch::Crystal::Query.new(
  q: "shazam",
  limit: 10,
  filter: "rating > 5",
  sort: ["rating:desc"],
)
index.search(query)
# => Meilisearch::Crystal::SearchResponse(JSON::Any)

request = Meilisearch::Crystal::FacetSearchRequest.new(
  "genres",
  "fiction",
  filter: "rating > 3",
)
index.facet_search(request)
# => Meilisearch::Crystal::FacetSearchResponse

index.similar(1, "default", as: Book)

# multi-search across indexes, optionally federated:
queries = [Meilisearch::Crystal::Query.new(index_uid: "books", q: "shazam")]
client.search.multi(queries, as: Book)
client.search.federated(
  queries,
  Meilisearch::Crystal::MultiSearch::Federation.new(limit: 20),
  as: Book,
)

Settings

settings = client.index("books").settings             # typed Settings struct
client.settings.update("books", Meilisearch::Crystal::Settings.new(
  filterable_attributes: ["rating"],
  sortable_attributes: ["rating"],
))
client.settings.reset("books")

Tasks

task = client.index("books").documents.upsert("books", [{id: 1, title: "Shazam"}])
completed = client.wait_for_task(task)                # blocks this fiber, not the process
completed.succeeded?                                  # typed predicate

API keys

key = client.keys.create(["search"], ["books"])
client.keys.list
client.keys.get?(key.uid)
client.keys.delete(key.uid)

Usage notes

  • All mutating operations are asynchronous in Meilisearch: they return a TaskResult. Use client.wait_for_task(...) or the !-suffixed variants when you need synchronous behavior.
  • Document types only need to_json(JSON::Builder) to be upserted, and from_json (e.g. via JSON::Serializable) to be fetched typed. Anything with those is valid — NamedTuples, Hash(String, JSON::Any), or your own structs.
  • The client does not auto-retry requests; handle errors where they matter. Server-originated failures raise Meilisearch::Crystal::ApiError, carrying a parsed Meilisearch::Crystal::Error with a typed error code.

Development

shards install

# unit specs (webmock-based, no server needed)
crystal spec spec/meilisearch/crystal/*_spec.cr

# integration specs — spin up a real Meilisearch first:
docker run --rm -p 7700:7700 -e MEILI_MASTER_KEY=test-master-key getmeili/meilisearch:v1.53.0
MEILISEARCH_INTEGRATION=1 MEILISEARCH_API_KEY=test-master-key crystal spec

# lint + format
bin/ameba
crystal tool format --check

API docs

Generated API documentation is published to GitHub Pages from main and is available at https://sandroscosta.github.io/meilisearch-crystal/. Build locally with:

crystal docs

Contributors

Repository

meilisearch-crystal

Owner
Statistic
  • 0
  • 0
  • 0
  • 0
  • 2
  • about 2 hours ago
  • August 11, 2026
License

MIT License

Links
Synced at

Tue, 11 Aug 2026 22:48:16 GMT

Languages