# Implementing Faceted Search with Field Key Filtering in PropertyWebBuilder

> Learn to implement faceted search with field key filtering in PropertyWebBuilder using SearchParamsService and FormSetup. Enhance your property listings today.

- Repository: [Ed Tee/property_web_builder](https://github.com/etewiah/property_web_builder)
- Tags: how-to-guide
- Published: 2026-03-01

---

**Faceted search in PropertyWebBuilder relies on tenant-scoped field keys stored in the `pwb_field_keys` table, parsed through `SearchParamsService`, and rendered via the `Search::FormSetup` concern.**

PropertyWebBuilder implements faceted search using **field keys** as the canonical source for every searchable attribute, from property types to amenities. Each field key resides in the `pwb_field_keys` table and is automatically scoped to the current tenant (website), enabling multi-site deployments with isolated data sets. This architecture separates data configuration from application code, allowing property managers to add or modify search filters without touching Ruby source files.

## Understanding Field Keys as the Foundation

Field keys serve as the backbone of PropertyWebBuilder's search system. Stored in the `pwb_field_keys` table, each record defines a selectable option with a **global_key** that acts as the canonical identifier across the application.

The system provides three distinct advantages through this approach:

- **Tenant isolation** – `PwbTenant::FieldKey` automatically scopes queries to `ActsAsTenant.current_tenant`, ensuring each website maintains its own independent set of options
- **Internationalization** – Labels are stored in a **JSONB** `translations` column and exposed through the Mobility gem via `field_key.label`, automatically rendering in the visitor's locale
- **Configuration-driven changes** – Adding, removing, or reordering facet options requires only data updates via seed files or the admin UI, eliminating the need for code deployments

## The Three-Layer Architecture

Faceted search operates through three coordinated layers that transform URL parameters into filtered results.

### Data Definition Layer

The `Pwb::FieldKey` model in [`app/models/pwb/field_key.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/models/pwb/field_key.rb) defines the structure and retrieval methods for facet options. The critical method `get_options_by_tag(tag)` returns an array of `OpenStruct` objects containing `value`, `label`, and `sort_order` for populating select boxes and checkbox lists.

```ruby

# Returns tenant-scoped options for a specific facet category

@property_types = PwbTenant::FieldKey.get_options_by_tag('property-types')

# => [{ value: 'types.apartment', label: 'Apartment', sort_order: 0 }, ...]

```

### Request Processing Layer

`Pwb::SearchParamsService` in [`app/services/pwb/search_params_service.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/services/pwb/search_params_service.rb) handles the normalization of URL parameters into a clean criteria hash. The `from_url_params` method parses incoming requests like `?type=apartment&features=pool,garden` into structured Ruby hashes suitable for database querying.

```ruby
params = ActionController::Parameters.new(
  type: 'apartment',
  features: 'pool,garden',
  sort: 'price-asc',
  page: '2'
)

criteria = Pwb::SearchParamsService.new.from_url_params(params)
#=> { property_type: "apartment",

#     features: ["pool", "garden"],

#     sort: "price-asc",

#     page: 2 }

```

### Controller and View Layer

The `Search::FormSetup` concern in [`app/controllers/concerns/search/form_setup.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/controllers/concerns/search/form_setup.rb) bridges the data and presentation layers. Controllers include this concern to preload facet options into instance variables (e.g., `@property_types`, `@property_features`) that views render as search forms.

## Parsing URL Parameters into Search Criteria

When a user selects facets, values transmit as URL parameters and require normalization. The `SearchParamsService#from_url_params` method (lines 41-58) handles type coercion, array splitting for multi-select fields, and pagination defaults.

For SEO optimization, the service also provides `canonical_url` (lines 100-108) to strip unnecessary parameters like `page=1` and generate clean, indexable URLs:

```ruby
criteria = { property_type: 'apartment', features: ['pool', 'garden'], page: 1 }
url = Pwb::SearchParamsService.new.canonical_url(
  criteria,
  locale: :en,
  operation: :buy,
  host: 'example.com'
)
#=> "https://example.com/en/buy?type=apartment&features=pool,garden"

```

## Loading Facet Options for the UI

Controllers populate search forms by invoking `get_options_by_tag` with specific category tags. In [`app/controllers/concerns/search/form_setup.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/controllers/concerns/search/form_setup.rb) (lines 21-25), the standard implementation loads common facets:

```ruby
@property_types   = PwbTenant::FieldKey.get_options_by_tag('property-types')
@property_states  = PwbTenant::FieldKey.get_options_by_tag('property-states')
@property_features = PwbTenant::FieldKey.get_options_by_tag('property-features')

```

Each option object provides both the database value (the **global_key**) and the translated label, enabling the view layer to render localized interfaces without additional queries.

## Applying Filters to Property Queries

The final step applies the normalized criteria to `Pwb::RealtyAsset` records. Because field keys store canonical identifiers in foreign key columns (`prop_type_key`, `prop_state_key`, `feature_key`), filtering reduces to simple equality or `IN` clauses:

```ruby
criteria = Pwb::SearchParamsService.new.from_url_params(params)

assets = Pwb::RealtyAsset.all
assets = assets.where(prop_type_key: criteria[:property_type]) if criteria[:property_type]
assets = assets.where(prop_state_key: criteria[:prop_state_key]) if criteria[:prop_state_key]
assets = assets.joins(:features)
               .where(pwb_features: { feature_key: criteria[:features] }) if criteria[:features].present?

```

## Extending Facets with New Field Keys

Adding a new faceted filter (e.g., "energy-label") requires minimal code changes:

1. Insert a new row into `pwb_field_keys` via the admin UI or seed files, tagging it with `energy-labels`
2. Add the loading line to [`app/controllers/concerns/search/form_setup.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/controllers/concerns/search/form_setup.rb):
   ```ruby
   @energy_labels = PwbTenant::FieldKey.get_options_by_tag('energy-labels')
   ```

3. Create or modify a view partial to render the new facet using `@energy_labels`

No additional Ruby business logic is required; the existing `SearchParamsService` automatically handles unknown parameters, and the database schema supports arbitrary facet keys through the field key associations.

## Summary

- **Field keys** in the `pwb_field_keys` table provide the data source for all faceted search options, scoped per tenant via `PwbTenant::FieldKey`
- **`SearchParamsService`** normalizes URL parameters into query criteria and generates canonical URLs for SEO
- **`Search::FormSetup`** concern loads facet options through `get_options_by_tag` for controller views
- **Foreign key matching** against `prop_type_key`, `prop_state_key`, and `feature_key` columns enables efficient database filtering
- **Extending search** requires only data entry and a single line in the form setup concern, maintaining separation between configuration and code

## Frequently Asked Questions

### What are field keys in PropertyWebBuilder?

Field keys are database records in the `pwb_field_keys` table that define selectable options for property attributes like type, state, and features. Each field key has a unique **global_key** used for database storage and filtering, plus JSONB translations for internationalized display labels. They serve as the single source of truth for both search facets and property attribute values.

### How does tenant scoping work with field keys?

PropertyWebBuilder uses the `acts_as_tenant` gem to scope all `PwbTenant::FieldKey` queries to the current website automatically. When you call `get_options_by_tag`, the system filters by `ActsAsTenant.current_tenant`, ensuring that each property website sees only its own configured options. This enables multi-tenant deployments where different sites can have completely different facet options without code changes.

### Can I add custom facets without modifying Ruby code?

Yes, for data-only changes. You can add new options to existing facets by inserting records into `pwb_field_keys` through the admin UI or seed files. However, exposing a completely new facet category (like "energy-label") requires adding one line to [`app/controllers/concerns/search/form_setup.rb`](https://github.com/etewiah/property_web_builder/blob/main/app/controllers/concerns/search/form_setup.rb) to load the options, plus a view partial to render the UI. The search logic itself handles the new parameters without additional modifications.

### How are field key labels internationalized?

Labels are stored in a **JSONB** `translations` column within the `pwb_field_keys` table. The application uses the Mobility gem to expose these through `field_key.label`, which returns the appropriate translation based on the current locale. When `get_options_by_tag` returns options to the controller, each object includes the localized label ready for rendering in the visitor's language.