# AshMeilisearch
An Ash extension that brings full-text search to your resources via [Meilisearch](https://meilisearch.com).
- Automatically configures Meilisearch indexes based on your resource configuration
- Generates `:search` read actions that work with normal Ash queries and pagination
- Converts `Ash.Filter` and `Ash.Sort` to Meilisearch expressions
- Precomputes and denormalizes calculations/aggregates into the search index for blazing fast sorting/filtering
- CRUD hooks keep indexes in sync with your resources automatically
## Install and Configure
Add to your dependencies:
```elixir
def deps do
[
{:ash_meilisearch, "~> 0.1.0"}
]
end
```
Configure your Meilisearch connection and domains:
```elixir
# config/config.exs
config :ash_meilisearch,
host: "http://localhost:7700",
api_key: nil, # or your API key
domains: [MyApp.Blog, MyApp.Shop] # List all domains using AshMeilisearch
```
## Add Meilisearch to Resources
Add the extension and configure searchable fields:
```elixir
defmodule MyApp.Blog.Post do
use Ash.Resource,
domain: MyApp.Blog,
extensions: [AshMeilisearch]
meilisearch do
index "posts" # Index name (required)
action_name :search # Generated read action name (optional, defaults to :search)
# Order matters for relevance ranking (title gets higher relevance than content)
searchable_attributes [
:title,
:content,
author: [:name, :bio], # Relations
tags: [:name],
]
filterable_attributes [
:status,
:published_at,
:word_count, # Calculations
:comment_count, # Aggregations
author: [:name],
tags: [:name]
]
sortable_attributes [
:title,
:published_at,
:word_count,
:comment_count
]
end
# Configure CRUD hooks to keep Meilisearch index in sync
changes do
change AshMeilisearch.Changes.UpsertSearchDocument, on: [:create, :update]
change AshMeilisearch.Changes.DeleteSearchDocument, on: [:destroy]
end
end
```
## Use the Generated Search Action
Now you have a `:search` read action that works exactly like any other Ash read action:
```elixir
results = MyApp.Blog.Post
|> Ash.Query.for_read(:search, %{query: "web development"})
|> Ash.Query.filter(expr(status == :published and inserted_at > ^~D[2023-01-01]))
|> Ash.Query.sort(inserted_at: :desc)
|> Ash.Query.limit(10)
|> Ash.read!()
# Or define a code interface in your domain
define :search_posts, action: :search, args: [:query]
# Then use it with all standard Ash options (including pagination!)
MyApp.Blog.search_posts!("web development", [
page: [limit: 20, offset: 10],
load: [:author, :tags],
query: [
filter: [status: :published, inserted_at: [gt: ~D[2023-01-01]]],
sort: [inserted_at: :desc]
]
])
```
That's it! Your Ash resources now have full-text search capabilities with automatic sync, relationship indexing, and flexible querying options.
## Initial Data Population
To populate your search index for the first time with existing data, use the included mix task:
```bash
mix ash_meilisearch.reindex MyApp.Blog.Post
```
## API Reference
These functions provide direct access to Meilisearch operations and return raw Meilisearch responses, not Ash resources. For most use cases, you should use the generated `:search` action on your resources instead.
### Search Operations
```elixir
# Perform search on a resource's index
AshMeilisearch.search(MyApp.Post, "search query", limit: 10, filter: "status = published")
# Multisearch across multiple queries
queries = [
%{q: "elixir", limit: 5},
%{q: "phoenix", limit: 5}
]
AshMeilisearch.multisearch(MyApp.Post, queries, federation: %{limit: 10})
```
### Index Management
```elixir
# Add documents to index
documents = [%{id: 1, title: "Hello"}, %{id: 2, title: "World"}]
AshMeilisearch.add_documents(MyApp.Post, documents)
# Delete document from index
AshMeilisearch.delete_document(MyApp.Post, "doc-id-123")
# Get index name for resource
AshMeilisearch.index_name(MyApp.Post)
```
### Filter and Sort Translation
```elixir
# Convert Ash query filter to Meilisearch format
query = MyApp.Post
|> Ash.Query.filter(expr(status == :published and word_count > 500 and author.name == "Alice"))
AshMeilisearch.build_filter(query) # "status = published AND word_count > 500 AND author.name = Alice"
# Convert Ash query sort to Meilisearch format
query = MyApp.Post
|> Ash.Query.sort([comment_count: :desc, author.name: :asc, inserted_at: :desc])
AshMeilisearch.build_sort(query) # ["comment_count:desc", "author.name:asc", "inserted_at:desc"]
```