Skip to content

Search and Filtering

This guide covers implementing search, filters, scopes, and sorting.

Overview

Plutonium provides built-in support for:

  • Search - Full-text search across fields
  • Filters - Input filters for specific fields (dropdown panel)
  • Scopes - Predefined query shortcuts (quick filter buttons)
  • Sorting - Column-based ordering

Define global search in the definition:

ruby
class PostDefinition < ResourceDefinition
  search do |scope, query|
    scope.where("title ILIKE ?", "%#{query}%")
  end
end
ruby
search do |scope, query|
  scope.where(
    "title ILIKE :q OR content ILIKE :q OR author_name ILIKE :q",
    q: "%#{query}%"
  )
end

Search with Associations

ruby
search do |scope, query|
  scope.joins(:author).where(
    "posts.title ILIKE :q OR users.name ILIKE :q",
    q: "%#{query}%"
  ).distinct
end

Split Search Terms

ruby
search do |scope, query|
  terms = query.split(/\s+/)
  terms.reduce(scope) do |current_scope, term|
    current_scope.where("title ILIKE ?", "%#{term}%")
  end
end

Full-Text Search (PostgreSQL)

ruby
search do |scope, query|
  scope.where(
    "to_tsvector('english', title || ' ' || body) @@ plainto_tsquery('english', ?)",
    query
  )
end

Filters

Plutonium provides 6 built-in filter types. Use shorthand symbols or full class names.

Text Filter

String/text filtering with pattern matching:

ruby
class PostDefinition < ResourceDefinition
  # Shorthand (recommended)
  filter :title, with: :text, predicate: :contains
  filter :status, with: :text, predicate: :eq

  # Full class name also works
  filter :slug, with: Plutonium::Query::Filters::Text, predicate: :starts_with
end

Available Predicates

PredicateSQLDescription
:eq= valueExact match (default)
:not_eq!= valueNot equal
:containsLIKE %value%Contains text
:not_containsNOT LIKE %value%Does not contain
:starts_withLIKE value%Starts with
:ends_withLIKE %valueEnds with
:matchesLIKE valuePattern match (* becomes %)
:not_matchesNOT LIKE valueDoes not match pattern

Boolean Filter

True/false filtering for boolean columns:

ruby
# Basic
filter :active, with: :boolean

# Custom labels
filter :published, with: :boolean, true_label: "Published", false_label: "Draft"

Renders a select dropdown with "All", true label ("Yes"), and false label ("No").

Date Filter

Single date filtering with comparison predicates:

ruby
filter :created_at, with: :date, predicate: :gteq  # On or after
filter :due_date, with: :date, predicate: :lt      # Before
filter :published_at, with: :date, predicate: :eq  # On exact date

Available Predicates

PredicateDescription
:eqOn this date (default)
:not_eqNot on this date
:ltBefore date
:lteqOn or before date
:gtAfter date
:gteqOn or after date

Date Range Filter

Filter between two dates (from/to):

ruby
# Basic
filter :created_at, with: :date_range

# Custom labels
filter :published_at, with: :date_range,
  from_label: "Published from",
  to_label: "Published to"

Renders two date pickers. Both are optional - users can filter with just "from" or just "to".

Select Filter

Filter from predefined choices:

ruby
# Static choices (array)
filter :status, with: :select, choices: %w[draft published archived]

# Dynamic choices (proc)
filter :category, with: :select, choices: -> { Category.pluck(:name) }

# Multiple selection
filter :tags, with: :select, choices: %w[ruby rails js], multiple: true

Association Filter

Filter by associated record:

ruby
# Basic - infers Category class from :category key
filter :category, with: :association

# Explicit class
filter :author, with: :association, class_name: User

# Multiple selection
filter :tags, with: :association, class_name: Tag, multiple: true

Renders a resource select dropdown. Converts filter key to foreign key (:category -> :category_id).

Filter Summary Table

TypeSymbolInput ParamsOptions
Text:textquerypredicate:
Boolean:booleanvaluetrue_label:, false_label:
Date:datevaluepredicate:
Date Range:date_rangefrom, tofrom_label:, to_label:
Select:selectvaluechoices:, multiple:
Association:associationvalueclass_name:, multiple:

Custom Filter Class

ruby
class PriceRangeFilter < Plutonium::Query::Filter
  def apply(scope, min: nil, max: nil)
    scope = scope.where("price >= ?", min) if min.present?
    scope = scope.where("price <= ?", max) if max.present?
    scope
  end

  def customize_inputs
    input :min, as: :number
    input :max, as: :number
    field :min, placeholder: "Min price..."
    field :max, placeholder: "Max price..."
  end
end

# Use in definition
filter :price, with: PriceRangeFilter

Scopes

Scopes appear as quick filter buttons. They reference model scopes or use inline blocks.

Basic Scopes

Reference existing model scopes:

ruby
class PostDefinition < ResourceDefinition
  scope :published    # Uses Post.published
  scope :draft        # Uses Post.draft
  scope :featured     # Uses Post.featured
end

Inline Scopes

Use block syntax with the scope passed as an argument:

ruby
scope(:recent) { |scope| scope.where("created_at > ?", 1.week.ago) }
scope(:this_month) { |scope| scope.where(created_at: Time.current.all_month) }

With Controller Context

Inline scopes have access to controller context like current_user:

ruby
scope(:mine) { |scope| scope.where(author: current_user) }
scope(:my_team) { |scope| scope.where(team: current_user.team) }

Default Scope

Set a scope as default:

ruby
class PostDefinition < ResourceDefinition
  scope :published, default: true  # Applied by default
  scope :draft
  scope :archived
end

When a default scope is set:

  • The default scope is applied on initial page load
  • The default scope button is highlighted (not "All")
  • Clicking "All" shows all records without any scope filter

Sorting

Define Sortable Fields

ruby
class PostDefinition < ResourceDefinition
  sort :title
  sort :created_at
  sort :view_count

  # Multiple at once
  sorts :title, :created_at, :view_count
end

Default Sort Order

ruby
# Field and direction
default_sort :created_at, :desc
default_sort :title, :asc

# Complex sorting with block
default_sort { |scope| scope.order(featured: :desc, created_at: :desc) }

Note: Default sort only applies when no sort params are provided. The system default is :id, :desc.

URL Parameters

Query parameters are structured under q:

/posts?q[search]=rails
/posts?q[title][query]=widget
/posts?q[status][value]=published
/posts?q[created_at][from]=2024-01-01&q[created_at][to]=2024-12-31
/posts?q[scope]=recent
/posts?q[sort_fields][]=created_at&q[sort_directions][created_at]=desc

Complete Example

ruby
class ProductDefinition < ResourceDefinition
  # Full-text search
  search do |scope, query|
    scope.where(
      "name ILIKE :q OR description ILIKE :q",
      q: "%#{query}%"
    )
  end

  # Filters
  filter :name, with: :text, predicate: :contains
  filter :status, with: :select, choices: %w[draft active discontinued]
  filter :featured, with: :boolean
  filter :created_at, with: :date_range
  filter :price, with: :date, predicate: :gteq
  filter :category, with: :association

  # Quick scopes (reference model scopes)
  scope :active, default: true
  scope :featured
  scope(:recent) { |scope| scope.where("created_at > ?", 1.week.ago) }

  # Sortable columns
  sorts :name, :price, :created_at

  # Default: newest first
  default_sort :created_at, :desc
end

Released under the MIT License.