Item search — global, per-catalogue, and per-category, with optional
scope composition (catalogue_uuids AND category_uuids).
Matches case-insensitively against name, description, sku, and
the multilang data JSONB. Excludes items in deleted catalogues or
deleted categories. Uncategorized items are included unless a
:category_uuids filter narrows the search.
Public surface is re-exported from PhoenixKitCatalogue.Catalogue.
Summary
Functions
Returns the total number of items matching search_items/2's filters.
Ignores :limit/:offset/:order. Same scope opts as search_items/2.
Total match count for search_items_in_catalogue/3.
Total match count for search_items_in_category/3.
Narrows any query with an :item named binding to the items a search
term matches — name, description, SKU, and every translated string in
the record's data.
Categories whose NAME or description matches, within one catalogue.
Searches items with flexible scope.
Searches items within a specific catalogue. Convenience wrapper
around search_items/2 with catalogue_uuids: [catalogue_uuid] in
the admin's Manual document order (category position, then item
position, name, uuid) — a stable walk through a catalogue's
categories. It used to carry its own copy of that chain; it now
delegates, so the two cannot drift. Pins :order — a caller's own
:order is overridden, since Manual is this wrapper's contract.
Searches items within a specific category. Convenience wrapper around
search_items/2 with category_uuids: [category_uuid].
Functions
@spec count_search_items(String.t(), keyword()) :: non_neg_integer()
Returns the total number of items matching search_items/2's filters.
Ignores :limit/:offset/:order. Same scope opts as search_items/2.
@spec count_search_items_in_catalogue(Ecto.UUID.t(), String.t()) :: non_neg_integer()
Total match count for search_items_in_catalogue/3.
@spec count_search_items_in_category(Ecto.UUID.t(), String.t()) :: non_neg_integer()
Total match count for search_items_in_category/3.
@spec match_text(Ecto.Query.t(), String.t() | nil) :: Ecto.Query.t()
Narrows any query with an :item named binding to the items a search
term matches — name, description, SKU, and every translated string in
the record's data.
Public because the attribute filter's FACET COUNTS have to agree with
the list beside them: a value offered as live while a search is on has
to still be live under that search, and it can only promise that by
asking the same question the listing asks. nil or a blank term is
"no text constraint" and leaves the query alone.
Callers pass a raw user string; trimming and LIKE-escaping happen here.
@spec search_categories(Ecto.UUID.t(), String.t(), keyword()) :: [ PhoenixKitCatalogue.Schemas.Category.t() ]
Categories whose NAME or description matches, within one catalogue.
Item search never covered these: searching a catalogue for a category it contains returned nothing, and the page looked like it had matched only because that category's own card happened to be on screen (Max, 2026-08-28).
:parent_uuid narrows to that category's SUBTREE (itself excluded),
mirroring how search_items_in_category/3 scopes items when the user
has drilled in. Deleted categories and categories of deleted
catalogues are excluded, as everywhere else.
@spec search_items(String.t(), keyword()) :: [PhoenixKitCatalogue.Schemas.Item.t()]
Searches items with flexible scope.
Options
:catalogue_uuids— list of catalogue UUIDs to scope to.nilor[]= all.:category_uuids— list of category UUIDs to scope to.nilor[]= all + uncategorized. Must contain only non-nil UUIDs; passing[nil]raisesArgumentError(use:only => :uncategorized_onlyfor that intent).:include_descendants— whentrue(default since V103), each entry in:category_uuidsis expanded to include every descendant category in the nested-category tree. Passfalseto scope strictly to the given UUIDs.:only—:uncategorized_onlyrestricts to items with nocategory_uuid;:categorized_onlyrestricts to items that belong to some category.nil(default) is unrestricted. Combining:uncategorized_onlywith a non-empty:category_uuidsis a logical contradiction and raisesArgumentError.:statuses— list of item statuses to include ("active","inactive","discontinued").nilor[]= all non-deleted (the historical default). Soft-deleted rows stay excluded even if"deleted"is listed. Atoms are accepted and stringified.:order—:position(default: the admin's Manual document order — catalogue position, category position, item position, name;{:position, dir}is accepted and the direction ignored, like the admin's Manual sort),:name, or{field, :asc | :desc}forname/sku/base_price/status. Anything else raisesArgumentError. Known limits of the Manual chain: catalogue positions are one sequence per folder level, so across folders it is position then name rather than the index's folder walk; and a subtree listing (a search with:include_descendants) orders by each category's sibling position, not a depth-first walk — the same order the admin's own in-catalogue search has always had.:limit— max results (default 50).:offset— paging offset (default 0).:preload— extra associations appended to the default[:catalogue, category: :catalogue]. Pass[catalogue_rules: :referenced_catalogue]for smart-pricing.
@spec search_items_in_catalogue(Ecto.UUID.t(), String.t(), keyword()) :: [ PhoenixKitCatalogue.Schemas.Item.t() ]
Searches items within a specific catalogue. Convenience wrapper
around search_items/2 with catalogue_uuids: [catalogue_uuid] in
the admin's Manual document order (category position, then item
position, name, uuid) — a stable walk through a catalogue's
categories. It used to carry its own copy of that chain; it now
delegates, so the two cannot drift. Pins :order — a caller's own
:order is overridden, since Manual is this wrapper's contract.
Same :preload opt as search_items/2 (extra associations appended
to the default [:catalogue, category: :catalogue]).
@spec search_items_in_category(Ecto.UUID.t(), String.t(), keyword()) :: [ PhoenixKitCatalogue.Schemas.Item.t() ]
Searches items within a specific category. Convenience wrapper around
search_items/2 with category_uuids: [category_uuid].