Implementing Faceted Search with Field Key Filtering in PropertyWebBuilder
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::FieldKeyautomatically scopes queries toActsAsTenant.current_tenant, ensuring each website maintains its own independent set of options - Internationalization – Labels are stored in a JSONB
translationscolumn and exposed through the Mobility gem viafield_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 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.
# 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 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.
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 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:
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 (lines 21-25), the standard implementation loads common facets:
@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:
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:
-
Insert a new row into
pwb_field_keysvia the admin UI or seed files, tagging it withenergy-labels -
Add the loading line to
app/controllers/concerns/search/form_setup.rb:@energy_labels = PwbTenant::FieldKey.get_options_by_tag('energy-labels') -
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_keystable provide the data source for all faceted search options, scoped per tenant viaPwbTenant::FieldKey SearchParamsServicenormalizes URL parameters into query criteria and generates canonical URLs for SEOSearch::FormSetupconcern loads facet options throughget_options_by_tagfor controller views- Foreign key matching against
prop_type_key,prop_state_key, andfeature_keycolumns 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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →