Packages

An Ash extension that brings full-text search to your resources via Meilisearch.

Current section

Files

Jump to
ash_meilisearch lib mix tasks ash_meilisearch.reindex.ex
Raw

lib/mix/tasks/ash_meilisearch.reindex.ex

defmodule Mix.Tasks.AshMeilisearch.Reindex do
@shortdoc "Reindex all records of an AshMeilisearch-enabled resource"
@moduledoc """
Reindex all records of a given AshMeilisearch-enabled resource.
## Usage
mix ash_meilisearch.reindex MyApp.Blog.Post
mix ash_meilisearch.reindex MyApp.Blog.Post --batch-size 200
## Options
* `--batch-size` - Number of records per batch (default: 100)
* `--help` - Show help message
The task uses keyset pagination for memory-efficient processing of large datasets.
"""
use Mix.Task
require Ash.Query
@impl Mix.Task
def run(args) do
Mix.Task.run("app.start")
Logger.configure(level: :warning)
{options, args, _} =
OptionParser.parse(args,
switches: [batch_size: :integer, help: :boolean],
aliases: [b: :batch_size, h: :help]
)
if options[:help] || length(args) == 0 do
print_help()
System.halt(0)
end
resource_module_name = List.first(args)
resource_module = Module.concat([resource_module_name])
batch_size = options[:batch_size] || 100
validate_resource!(resource_module, resource_module_name)
Mix.shell().info("Reindexing #{resource_module_name}...")
total_count = Ash.count!(Ash.Query.new(resource_module))
Mix.shell().info("Found #{total_count} records")
if total_count == 0 do
Mix.shell().info("No records to reindex.")
System.halt(0)
end
Mix.shell().info("Processing #{total_count} records in batches of #{batch_size}")
start_time = System.monotonic_time(:millisecond)
final_stats = process_batches(resource_module, batch_size)
elapsed_seconds = div(System.monotonic_time(:millisecond) - start_time, 1000)
records_per_second =
if elapsed_seconds > 0,
do: Float.round(final_stats.successful / elapsed_seconds, 1),
else: "N/A"
Mix.shell().info(
"Reindex complete. Processed #{final_stats.successful}/#{total_count} records in #{elapsed_seconds}s (#{records_per_second} records/sec)"
)
if final_stats.failed > 0 do
Mix.shell().error("#{final_stats.failed} records failed to reindex")
final_stats.failed_ids
|> Enum.take(10)
|> Enum.each(&Mix.shell().error(" Failed ID: #{&1}"))
if length(final_stats.failed_ids) > 10 do
Mix.shell().error(" ... and #{length(final_stats.failed_ids) - 10} more")
end
end
end
defp validate_resource!(resource_module, resource_module_name) do
unless Code.ensure_loaded?(resource_module) and
function_exported?(resource_module, :spark_dsl_config, 0) do
Mix.shell().error("Error: '#{resource_module_name}' is not an Ash resource")
System.halt(1)
end
unless AshMeilisearch.Info.meilisearch_configured?(resource_module) do
Mix.shell().error(
"Error: '#{resource_module_name}' does not have AshMeilisearch configured"
)
System.halt(1)
end
unless Ash.Resource.Info.action(resource_module, :reindex) do
Mix.shell().error("Error: '#{resource_module_name}' does not have a :reindex action")
System.halt(1)
end
end
defp print_help do
Mix.shell().info("""
AshMeilisearch Resource Reindex Task
Usage: mix ash_meilisearch.reindex [ResourceModule] [options]
Arguments:
ResourceModule The Ash resource module to reindex (e.g., MyApp.Blog.Post)
Options:
-b, --batch-size Number of records per batch (default: 100)
-h, --help Show this help message
""")
end
defp process_batches(
resource_module,
batch_size,
page \\ nil,
acc \\ %{successful: 0, failed: 0, failed_ids: []}
) do
page =
if page do
Ash.page!(page, :next)
else
query =
resource_module
|> Ash.Query.new()
|> Ash.Query.load(:search_document)
|> Ash.Query.sort(id: :asc)
|> Ash.Query.page(limit: batch_size)
Ash.read!(query)
end
valid_documents =
Enum.filter(page.results, fn record ->
record.search_document not in [nil, %{}]
end)
new_acc =
if length(valid_documents) > 0 do
batch_documents = Enum.map(valid_documents, & &1.search_document)
case AshMeilisearch.add_documents(resource_module, batch_documents) do
{:ok, _task} ->
%{acc | successful: acc.successful + length(valid_documents)}
{:error, error} ->
Mix.shell().error("Batch failed: #{inspect(error)}")
failed_ids = Enum.map(page.results, & &1.id)
%{
acc
| failed: acc.failed + length(page.results),
failed_ids: acc.failed_ids ++ failed_ids
}
end
else
acc
end
if page.more? do
process_batches(resource_module, batch_size, page, new_acc)
else
new_acc
end
end
end