DEV Community

Cover image for ActiveFields: Search
Kirill Usanov
Kirill Usanov

Posted on

ActiveFields: Search

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
])
Enter fullscreen mode Exit fullscreen mode

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" },
})
Enter fullscreen mode Exit fullscreen mode

3. Permitted Parameters - direct from controllers:

# In your controller
def index
  @posts = Post.where_active_fields(active_fields_finders_params)
end
Enter fullscreen mode Exit fullscreen mode

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" }])
Enter fullscreen mode Exit fullscreen mode

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" }])
Enter fullscreen mode Exit fullscreen mode

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 }])
Enter fullscreen mode Exit fullscreen mode

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 }])
Enter fullscreen mode Exit fullscreen mode

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 },
])
Enter fullscreen mode Exit fullscreen mode

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" },
])
Enter fullscreen mode Exit fullscreen mode

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,
)
Enter fullscreen mode Exit fullscreen mode

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
) %>
Enter fullscreen mode Exit fullscreen mode

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" }])
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
alexey2257 profile image
Alexey

Thanks for the article.
Will it work only on PostgreSQL 17+? I think most of production dbs run on much older versions, unfortunately

Collapse
 
exterminate profile image
Kirill Usanov

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.

Collapse
 
exterminate profile image
Kirill Usanov

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.