Problem
The Format facet on /records currently exposes the raw record_formats.name field. This produces ~27+ visible options on the sidebar — many of which are noisy near-duplicates of each other caused by data entry drift.
Examples from production data today:
Singles: 7-inch (13,242) — and separately Singles: 7-Inch (1), Singles: 7-inch. (1), Singles: 7-rpm (1) — all the same physical thing
LPs: 10/12-inch (4,324) and LPs 10/12-inch (1) — same thing, missing colon
Gold Standard Singles with "447" prefix (44) and Gold Standard Singles with '447' prefix (separate row, only the quote char differs)
- 22 record_formats are not assigned to any
record_type_id (covering 4 records)
This makes the facet effectively useless for filtering — users can't tell which entries are real and which are typos, and the long tail dilutes the signal of the top buckets.
Data model recap
We have two tables:
record_formats — 57 distinct values (granular, noisy). records.record_format_id points here.
record_types — 5 clean values (broad, parent). record_formats.record_type_id points here.
So record_type is already modeled as the parent of record_format. We just don't surface it on the index page.
Counts across the active collection (user 1, 20,380 records) when grouped by record_type:
| Type |
Records |
| 45s |
14,710 |
| LPs |
4,536 |
| Picture Sleeves |
1,007 |
| 78s |
123 |
| Sound postcard |
0 |
That's 99.98% coverage in 4 buckets vs 27+ confusing rows today.
Proposal
Two-phase rollout. Phase 1 is a quick UX win; Phase 2 unlocks deeper filtering once the data is cleaned.
Phase 1 — Replace Format facet with Type facet (this PR)
- Add
has_one :record_type, through: :record_format on Record
- Replace the
@formats query in RecordsController#load_filter_options with @record_types (join through record_formats, group by record_types.name, hide buckets with count = 0)
- Update the sidebar partial to render Type instead of Format
- Switch the Ransack predicate from
record_format_id_in to filter by record_format_id_in derived from the selected Type (or via a new record_type_id ransacker)
- Verify existing sort options keyed on
record_format_* still work (the FK stays, only the facet changes)
Phase 2 — Add Format as cascading sub-facet (separate issue/PR)
After data normalization, when a Type is selected, expose its child Formats as a second filter group:
- Backfill the 22 orphan formats with the correct
record_type_id
- Merge typo variants into canonical rows (
Singles: 7-Inch → Singles: 7-inch, etc.)
- Drop empty/zero-count formats
- Render Format checkboxes only when a Type is active
Wireframes
Current — single noisy Format facet
+--------------------------------+
| Format ^ |
+--------------------------------+
| [ ] Singles: 7-inch 13,242 |
| [ ] LPs: 10/12-inch 4,324 |
| [ ] Promotional Singles 1,149 |
| [ ] Picture Sleeves 1,002 |
| [ ] EPs: 7-inch 274 |
| [ ] Singles: 10/12-inch 135 |
| [ ] Singles: 78 rpm 121 |
| [ ] Singles: 12-inch 64 |
| [ ] Gold Standard Sing... 44 |
| [ ] Picture Disc Singles 16 |
| [ ] EPs: 7-inch 45 rpm 7 |
| [ ] EPs: 10-inch 6 |
| [ ] Gold Standard Pict... 5 |
| [ ] EPs: 12-inch 3 |
| [ ] Promotional 12-inch... 3 |
| [ ] Singles: 12-inch 33/45 2 |
| [ ] Gold Standard Sing... 2 |
| [ ] Promotional Single... 2 |
| [ ] LPs 10/12-inch 1 | <-- typo dup of row 2
| [ ] 78 rpm Album 1 |
| [ ] Picture Sleeve 1 | <-- singular vs plural dup
| [ ] Plastic Soundsheets... 1 |
| [ ] EPs: 7-inch 33/45 1 |
| [ ] Promotional LPs 1 |
| [ ] Albums: 78 rpm 1 |
+--------------------------------+
Phase 1 — Type only (ship now)
+--------------------------------+
| Type ^ |
+--------------------------------+
| [ ] 45s 14,710 |
| [ ] LPs 4,536 |
| [ ] Picture Sleeves 1,007 |
| [ ] 78s 123 |
+--------------------------------+
Phase 2 — Type + cascading Format sub-facet (after cleanup)
When a Type is checked, its child formats appear indented below it:
+--------------------------------+
| Type ^ |
+--------------------------------+
| [x] 45s 14,710 |
| [ ] Singles: 7-inch 13,242 |
| [ ] Promotional Sgl. 1,149 |
| [ ] EPs: 7-inch 274 |
| [ ] Picture Disc Sgl 16 |
| [ ] Gold Std "447" 44 |
| [ ] Gold Std "GB" 2 |
| [ ] LPs 4,536 |
| [ ] Picture Sleeves 1,007 |
| [ ] 78s 123 |
+--------------------------------+
(Picture Sleeves remains a top-level Type because it's a category in its own right, not a sub-format of 45s.)
Recommendation
Do Phase 1 now, ship it as its own PR. It's a small, low-risk change that immediately makes the facet useful for the 99.98% of records that have a clean type assignment.
Treat Phase 2 as gated on data cleanup. Don't expose the cascading Format sub-facet until the typo dedup + orphan backfill is done, otherwise the sub-list will reproduce the same noise problem at a smaller scale. File a separate cleanup issue for the data work and a follow-up issue for the UI.
Out of scope
- Changing the schema (we're keeping
record_format_id on records)
- Changing the record detail page (Format stays visible there)
- Changing admin/edit forms
Problem
The Format facet on
/recordscurrently exposes the rawrecord_formats.namefield. This produces ~27+ visible options on the sidebar — many of which are noisy near-duplicates of each other caused by data entry drift.Examples from production data today:
Singles: 7-inch(13,242) — and separatelySingles: 7-Inch(1),Singles: 7-inch.(1),Singles: 7-rpm(1) — all the same physical thingLPs: 10/12-inch(4,324) andLPs 10/12-inch(1) — same thing, missing colonGold Standard Singles with "447" prefix(44) andGold Standard Singles with '447' prefix(separate row, only the quote char differs)record_type_id(covering 4 records)This makes the facet effectively useless for filtering — users can't tell which entries are real and which are typos, and the long tail dilutes the signal of the top buckets.
Data model recap
We have two tables:
record_formats— 57 distinct values (granular, noisy).records.record_format_idpoints here.record_types— 5 clean values (broad, parent).record_formats.record_type_idpoints here.So
record_typeis already modeled as the parent ofrecord_format. We just don't surface it on the index page.Counts across the active collection (user 1, 20,380 records) when grouped by
record_type:That's 99.98% coverage in 4 buckets vs 27+ confusing rows today.
Proposal
Two-phase rollout. Phase 1 is a quick UX win; Phase 2 unlocks deeper filtering once the data is cleaned.
Phase 1 — Replace Format facet with Type facet (this PR)
has_one :record_type, through: :record_formatonRecord@formatsquery inRecordsController#load_filter_optionswith@record_types(join throughrecord_formats, group byrecord_types.name, hide buckets with count = 0)record_format_id_into filter byrecord_format_id_inderived from the selected Type (or via a newrecord_type_idransacker)record_format_*still work (the FK stays, only the facet changes)Phase 2 — Add Format as cascading sub-facet (separate issue/PR)
After data normalization, when a Type is selected, expose its child Formats as a second filter group:
record_type_idSingles: 7-Inch→Singles: 7-inch, etc.)Wireframes
Current — single noisy Format facet
Phase 1 — Type only (ship now)
Phase 2 — Type + cascading Format sub-facet (after cleanup)
When a Type is checked, its child formats appear indented below it:
(Picture Sleeves remains a top-level Type because it's a category in its own right, not a sub-format of 45s.)
Recommendation
Do Phase 1 now, ship it as its own PR. It's a small, low-risk change that immediately makes the facet useful for the 99.98% of records that have a clean type assignment.
Treat Phase 2 as gated on data cleanup. Don't expose the cascading Format sub-facet until the typo dedup + orphan backfill is done, otherwise the sub-list will reproduce the same noise problem at a smaller scale. File a separate cleanup issue for the data work and a follow-up issue for the UI.
Out of scope
record_format_idonrecords)