I've been working with ActiveFields, a gem for handling dynamic fields using the Entity-Attribute-Value (EAV) pattern. One of the challenges with EAV is querying those custom fields efficiently. ActiveFields includes a search feature that handles this, and I thought I'd share how it works.
The Problem
When you store custom fields in an EAV pattern, searching becomes tricky. You need to handle type casting, field-specific operators, and construct queries efficiently. ActiveFields provides a unified search interface that supports all built-in field types.
The API
The main method is where_active_fields, which supports all 13 built-in field types with type-specific operations. The method accepts three different input formats:
1. Array of Hashes - useful for programmatic queries:
Post.where_active_fields([
{ name: "integer_array", operator: "any_gteq", value: 5 },
{ "name" => "text", operator: "=", "value" => "Lasso" },
{ n: "boolean", op: "!=", v: false }, # Compact form
])
2. Hash of Hashes - works with Rails fields_for:
Post.where_active_fields({
"0" => { name: "integer_array", operator: "any_gteq", value: 5 },
"1" => { "name" => "text", operator: "=", "value" => "Lasso" },
})
3. Permitted Parameters - direct from controllers:
# In your controller
def index
@posts = Post.where_active_fields(active_fields_finders_params)
end
You can use n/name, op/operator, and v/value in compact form, and mix string or symbol keys as needed.
Field Types and Operations
Each field type has operations suited to its data type. Here are some examples:
Text Fields
Text fields support 13 different operations, including exact matches and case-insensitive substring searches:
# Exact match
Post.where_active_fields([{ name: "title", operator: "=", value: "Hello" }])
# Starts with (case-sensitive)
Post.where_active_fields([{ name: "title", operator: "^", value: "Hello" }])
# Contains (case-insensitive)
Post.where_active_fields([{ name: "description", operator: "~*", value: "ruby" }])
# Doesn't end with
Post.where_active_fields([{ name: "title", operator: "!$", value: "World" }])
Numeric Fields
Integer, Decimal, Date, and DateTime fields support standard comparison operations:
# Greater than or equal
Post.where_active_fields([{ name: "age", operator: ">=", value: 18 }])
# Less than
Order.where_active_fields([{ name: "price", operator: "<", value: 100.50 }])
# Not equal
Post.where_active_fields([{ name: "status", operator: "!=", value: "archived" }])
Array Fields
Array fields support additional operations:
- Any element matching a condition
- All elements matching a condition
- Array size constraints
- Inclusion/exclusion of specific values
# Find posts where tags array contains "ruby"
Post.where_active_fields([{ name: "tags", operator: "|=", value: "ruby" }])
# Find posts where any tag starts with "web"
Post.where_active_fields([{ name: "tags", operator: "|^", value: "web" }])
# Find posts with at least 5 tags
Post.where_active_fields([{ name: "tags", operator: "#>=", value: 5 }])
# Find products where all customer ratings are 4 or higher
Product.where_active_fields([{ name: "ratings", operator: "&>=", value: 4 }])
# Find products where any customer gave a 5+ rating
Product.where_active_fields([{ name: "ratings", operator: "|>=", value: 5 }])
The operators follow a pattern: |= for "includes", |> for "any greater than", &> for "all greater than", #= for "size equals", etc.
Boolean Fields
# Find published posts
Post.where_active_fields([{ name: "published", operator: "=", value: true }])
# Find unpublished posts
Post.where_active_fields([{ name: "published", operator: "!=", value: true }])
Examples
Here are some practical use cases:
E-commerce Product Search
# Find products with:
# - Price between 50 and 200
# - In stock (boolean = true)
# - Tags include "electronics"
# - Rating >= 4.5
Product.where_active_fields([
{ name: "price", operator: ">=", value: 50 },
{ name: "price", operator: "<=", value: 200 },
{ name: "in_stock", operator: "=", value: true },
{ name: "tags", operator: "|=", value: "electronics" },
{ name: "rating", operator: ">=", value: 4.5 },
])
Content Management System
# Find articles:
# - Published after a certain date
# - Title contains "Rails" (case-insensitive)
# - Has at least 3 categories
# - Author name equals "John"
Article.where_active_fields([
{ name: "published_at", operator: ">=", value: Date.current - 30.days },
{ name: "title", operator: "~*", value: "Rails" },
{ name: "categories", operator: "#>=", value: 3 },
{ name: "author_name", operator: "=", value: "John" },
])
Multi-Tenant Applications
ActiveFields supports scoping, which is useful for multi-tenant applications:
# Search within a specific scope (tenant)
User.where_active_fields(
[
{ name: "department", operator: "=", value: "Engineering" },
{ name: "skills", operator: "|=", value: "Ruby" },
],
scope: Current.tenant.id,
)
Implementation Details
The search functionality uses PostgreSQL 17+ JSON capabilities. For singular fields, it uses CAST operations. For array fields, it uses jsonb_path_exists and jsonb_path_query_array functions with JSONPath expressions.
Some benefits:
- Type-safe: Values are automatically cast to the correct type
- Efficient: Uses PostgreSQL native JSON functions
- Flexible: Supports complex queries without writing raw SQL
- Maintainable: All search logic is encapsulated in finder classes
Building Search Forms
The gem integrates with Rails forms. The scaffold generator provides a helper method to render search forms:
<%= render_active_fields_finders_form(
active_fields: Post.active_fields,
url: posts_path
) %>
This generates a form with appropriate inputs for each field type.
Getting Started
The search feature works once you have ActiveFields set up. Call where_active_fields on any model that has has_active_fields:
class Post < ApplicationRecord
has_active_fields
end
# Then you can search:
Post.where_active_fields([{ name: "custom_field", operator: "=", value: "something" }])
Summary
ActiveFields search feature provides a unified API for querying dynamic fields. The where_active_fields method handles type casting, field-specific operators, and query construction, which simplifies working with custom fields.
Key features:
- One method (
where_active_fields) handles all search scenarios - Supports 13 field types with type-specific operations
- Built on PostgreSQL 17+ JSON functions
- Extensible: you can add custom field types with its own search operations
- Type-safe: automatic value casting
If you need custom fields with search capabilities, ActiveFields might be worth checking out.
Top comments (3)
Thanks for the article.
Will it work only on PostgreSQL 17+? I think most of production dbs run on much older versions, unfortunately
It uses some PostgreSQL jsonpath methods that were added in 17 version.
E.g. typecasting methods:
$[*] ? (@.timestamp_tz() == $value.timestamp_tz())So unfortunately older versions are not fully supported - some queries (generally for array field types) would fail.
Update:
Ran tests against PostgreSQL 15.
Search does not work for the following field types:
DecimalArray,DateArray,DateTimeArray.All other 10 field types work correctly.