We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Chapter 3: Creating a Better Search UI
Custom Actions with Arguments
-
Goal: search for artists or information using a string:
iex> Tunez.Music.search_artists("fur") {:ok, [%Tunez.Music.Artist{name: "Valkyrie's Fury"}, ...]}
Designing a Search Action
-
Add a
read :searchaction inlib/tunez/music/artist.ex:read :search do argument(:query, :ci_string) do constraints(allow_empty?: true) default("") end filter(expr(contains(name, ^arg(:query)))) end
Filters with Expressions
-
Use
Ash.Query.filter/2for filtering:iex> require Ash.Query iex> Ash.Query.filter(Tunez.Music.Album, year_released == 2024) |> Ash.read() -
Supports case-insensitive substring matches:
iex> Tunez.Music.Artist |> Ash.Query.for_read(:search, %{query: "co"}) |> Ash.read()
Speeding Things Up with Custom Database Indexes
-
Add GIN index for
namecolumn to speed up searches:# lib/tunez/repo.ex def installed_extensions, do: ["ash-functions", "pg-trgm"] # lib/tunez/music/artist.ex postgres do table("artists") repo(Tunez.Repo) custom_indexes do index("name gin_trgm_ops", name: "artists_name_trgm_index", using: "GIN") end end -
Run migration:
mix ash.codegen add_gin_index_for_artist_name_search mix ash.migrate
Integrating Search into the UI
-
Define search in domain:
resource Tunez.Music.Artist do define :search_artists, action: :search, args: [:query] end -
Handle search params in LiveView (
index_live.ex):query_text = Map.get(params, "q", "") artists = Tunez.Music.search_artists!(query_text) socket = assign(socket, :query_text, query_text) |> assign(:artists, artists) -
Add search box to header in template:
<.search_box query={@query_text} method="get" phx-submit="search" />
Dynamically Sorting Artists
Letting Users Set a Sort Method
-
Add sort changer in UI:
<.sort_changer selected={@sort_by} /> -
Capture sorting in
handle_params:sort_by = Map.get(params, "sort_by") |> validate_sort_by() socket = assign(socket, :sort_by, sort_by)
Base Query for a Read Action
-
Example query:
Tunez.Music.Artist |> Ash.Query.for_read(:read) |> Ash.Query.sort(name: :asc) |> Ash.Query.limit(1) |> Ash.read()
Using sort_input
-
Allows multiple sort fields:
Tunez.Music.search_artists("the", [query: [sort_input: "-name"]]) -
Attributes must be public:
attribute :name, :string, allow_nil?: false, public?: true create_timestamp(:inserted_at, public?: true) update_timestamp(:updated_at, public?: true) -
Update
handle_paramsto pass sort input to search:artists = Tunez.Music.search_artists!(query_text, query: [sort_input: sort_by]) -
Example sort options:
[ {"recently updated", "-updated_at"}, {"recently added", "-inserted_at"}, {"name", "name"} ]
Pagination of Search Results
Adding Pagination Support
-
Add pagination to
read :search:read :search do pagination offset?: true, default_limit: 12 end -
Returns
Ash.Page.Offsetstruct with results.
Showing Paginated Data in the Catalog
-
Handle pages in LiveView:
page = Tunez.Music.search_artists!(query_text, query: [sort_input: sort_by], page: page_params) socket = assign(socket, :page, page) -
Render paginated results and navigation:
<.pagination_links page={@page} query_text={@query_text} sort_by={@sort_by} /> -
Generate page links:
def query_string(page, query_text, sort_by, which) do case AshPhoenix.LiveView.page_link_params(page, which) do :invalid -> [] list -> list end |> Keyword.put(:q, query_text) |> Keyword.put(:sort_by, sort_by) |> remove_empty() end
No DB Field? No Problem—Calculations
Calculating Data
-
Example in
Albumresource:calculations do calculate :years_ago, :integer, expr(2025 - year_released) calculate :string_years_ago, :string, expr("wow, this was released " <> years_ago <> " years ago!") end -
Load calculations with related records:
Tunez.Music.get_artist_by_id(uuid, load: [albums: [:years_ago]])
Relationship Calculations as Aggregates
-
Define aggregates in
Artistresource:aggregates do count :album_count, :albums first :latest_album_year_released, :albums, :year_released first :cover_image_url, :albums, :cover_image_url end -
Use aggregates like attributes in LiveView:
page = Tunez.Music.search_artists!(query_text, page: page_params, query: [sort_input: sort_by], load: [:album_count, :latest_album_year_released, :cover_image_url] ) -
Default load for domain:
define(:search_artists, action: :search, args: [:query], default_options: fn -> [load: [:album_count, :latest_album_year_released, :cover_image_url]] end )
Sorting Based on Aggregate Data
-
Add sorting options for aggregates:
[ {"recently updated", "-updated_at"}, {"recently added", "-inserted_at"}, {"name", "name"}, {"number of albums", "-album_count"}, {"latest album release", "-latest_album_year_released"} ] -
Make aggregates public in
artist.ex:aggregates do count :album_count, :albums, public?: true first :latest_album_year_released, :albums, :year_released, public?: true end