While recordset and get_recordset let the controller decide which records to expose, clients
often need to narrow or reorder the returned set themselves. The framework handles this through a
pluggable list of filter backends, run in order against the recordset.
filter_backends is an ordered array of filter classes. The default pipeline is:
self.filter_backends = [
RESTFramework::QueryFilter,
RESTFramework::OrderingFilter,
RESTFramework::SearchFilter,
]
To add Ransack support, append RESTFramework::RansackFilter:
class ApiController < ApplicationController
include RESTFramework::Controller
self.filter_backends = [
RESTFramework::QueryFilter,
RESTFramework::OrderingFilter,
RESTFramework::SearchFilter,
RESTFramework::RansackFilter,
]
end
You can also remove backends you don’t want by setting filter_backends explicitly. Custom
backends must respond to filter_data(data) and accept a controller: kwarg in new.
QueryFilter turns query-string params into where clauses. For the request
GET /api/movies?cool=true&genre=scifi, it calls
recordset.where(cool: "true", genre: "scifi").
You can append a predicate suffix to any field name to use a comparison other than equality:
| Suffix | Meaning | Example |
|---|---|---|
_lt |
Less than | ?year_lt=2000 |
_lte |
Less than or equal | ?year_lte=2000 |
_gt |
Greater than | ?rating_gt=7.5 |
_gte |
Greater than or equal | ?rating_gte=7.5 |
_not |
Not equal (translates to where.not) |
?status_not=archived |
_in |
In a comma-separated list | ?genre_in=scifi,drama |
_cont |
SQL LIKE '%value%' (contains substring) |
?name_cont=star |
_true |
Field is true |
N/A — use raw key in params |
_false |
Field is false |
N/A — use raw key in params |
_null |
Field is NULL |
N/A — use raw key in params |
Multiple predicates on the same field are combined correctly:
GET /api/movies?year_gte=1990&year_lt=2000&genre_in=scifi,drama
If a field is an association, you can filter on any of its sub_fields (see
Fields in the Controllers section) using dot notation, optionally
with a predicate:
GET /api/movies?director.name_cont=nolan
GET /api/movies?cast_members.id_in=1,2,3
When a sub-field filter is used, QueryFilter automatically adds includes(:director) (etc.) to
the relation to avoid N+1 queries.
By default QueryFilter allows any field in fields. Set filter_fields to a narrower
whitelist:
class Api::MoviesController < ApiController
self.model = Movie
self.fields = [ :id, :name, :release_date, :enabled, :internal_notes ]
self.filter_fields = [ :id, :name, :release_date, :enabled ]
end
QueryFilter is included in filter_backends by default and requires no extra configuration.
Clients can request a different sort order with the ordering query parameter:
GET /api/movies?ordering=name
GET /api/movies?ordering=-name # Descending
GET /api/movies?ordering=director,-name # Multi-column sort
GET /api/movies?ordering=director.name # Sub-field
| Attribute | Default | Purpose |
|---|---|---|
ordering_query_param |
"ordering" |
The query parameter name. Set to nil to disable ordering. |
ordering_fields |
nil = fields |
Whitelist of fields allowed to be ordered by. |
ordering_no_reorder |
false |
Use .order(...) instead of .reorder(...). |
By default the filter uses .reorder, which clears any existing ordering on the recordset. Set
ordering_no_reorder = true to append to the recordset’s existing order instead.
SearchFilter does a simple multi-column LIKE search against string columns:
GET /api/movies?search=star
| Attribute | Default | Purpose |
|---|---|---|
search_query_param |
"search" |
The query parameter name. |
search_fields |
nil |
Explicit list of fields to search. Falls back to default label-like columns. |
search_ilike |
false |
Use ILIKE (case-insensitive) instead of LIKE. PostgreSQL only. |
When search_fields is unset, SearchFilter searches any field in the controller’s fields
that also matches RESTFramework.config.search_columns (by default: name, label, login,
title, email, username, url, description, note). So on most models, search just works
if you have any of those columns.
All searched values are cast to strings inside the query (CAST(column AS VARCHAR)), so you can
search numeric or date columns too. MySQL/Trilogy uses CHAR instead of VARCHAR automatically.
For more advanced filtering (joins, grouping, custom predicates), add
RESTFramework::RansackFilter to filter_backends. This requires the ransack gem.
GET /api/movies?q[name_cont]=star&q[director_name_start]=chris
| Attribute | Default | Purpose |
|---|---|---|
ransack_query_param |
"q" |
Query parameter for the ransack hash. |
ransack_options |
nil |
Hash passed through to ransack(q, opts). |
ransack_distinct |
true |
Default distinct: for .result(distinct:). |
ransack_distinct_query_param |
"distinct" |
Query param that lets clients override distinct per request. |
Clients can override distinct per request with e.g. ?distinct=false. You still need to
configure ransackable_attributes / ransackable_associations on your models — the framework
doesn’t bypass Ransack’s own safety controls.
Any class implementing this minimal interface can be plugged into filter_backends:
class MyFilter < RESTFramework::Filters::BaseFilter
def initialize(controller:)
@controller = controller
end
def filter_data(data)
# Return a new (or the same) ActiveRecord::Relation.
data.where(...)
end
end
The framework iterates filter_backends in order, passing the result of each backend into the
next, so backends compose naturally (e.g., QueryFilter narrows, then OrderingFilter sorts,
then SearchFilter intersects).
show TooBy default the framework filters the recordset before looking up a single record (via
get_record). This means that if a record would be filtered out of the list, it’s also hidden
from show/update/destroy — your filters double as access control.
To disable this and always look up against the full recordset, set:
self.filter_recordset_before_find = false