How to Really Design Search for a Database

A prescriptive Bonsai tutorial by Max Irwin on building search over a relational database. Its core argument: don’t mirror your database structure in your search index. The common failure modes — one index per table, foreign keys preserved, nested fields — produce slow, over-complicated search. Instead, design a single unified index around how people actually want to find things, and make every type of record universally findable through one schema.


Thesis

“One field per use case of how people want to find things.”

Database full-text features are not enough; use a dedicated engine (Elasticsearch / OpenSearch). But the engine choice is secondary to the schema — the index should be designed around user information needs, not around tables. See Unified Search Index.

The Universal 14-Field Schema

A single mapping applicable to any relational database. Records of every type (catalog, people, transactions) coexist in one index, distinguished by a type field:

FieldTypePurpose
idkeywordUnique record identifier
typekeywordSource table name — drives faceting/filtering
permissionskeywordAccess-control list (multivalued)
urlurlLink to record page
namesentityPrimary searchable names
emailsemailEmail addresses
notestextLong-form free text
akaentityAlternate names / resolved related entities
addressentityLocation data
amountnumericPrimary numeric value
created / updated / deleteddateLifecycle timestamps (soft delete)
detailsobjectStored-but-not-indexed display data (enabled: false)

Denormalization Over Foreign Keys

Relationships are resolved at index time into flat, multivalued fields rather than preserved as foreign keys or nested objects. In the Chinook example, an album’s ArtistId becomes the artist’s name in the aka array; a track’s genre/composer collapse into a single aka array. This makes related records discoverable through one query. The author notes nested fields “are slow and make things over-complicated.” See Denormalization for Search.

Custom Analyzers

  • analyze_entities — light stemming (English possessive only) to preserve proper nouns
  • analyze_text — full English analysis (stop words + stemming)
  • analyze_emails — UAX URL-email tokenizer
  • analyze_urls — path-hierarchy tokenizer with protocol stripping

Query Design

  • Base query: multi_match cross_fields with boosts — names^3, aka^2, then emails/notes/address/url.
  • Permissions filtering: a terms filter on the permissions array scoped to the user’s role (admin passes ["all","admin"]; a customer passes ["all","customer-1"]). Document-level access control baked into the index.
  • Partial-name recall: an added match_phrase_prefix clause (0.1 boost) lets “stair” match “Stairway to Heaven”.

When Not to Use Vectors

The article explicitly recommends against vector search for typical SaaS/database search unless data has rich free-text paragraphs and lexical techniques (prefix, fuzzy, n-grams) prove insufficient on a real recall problem. For entity-focused queries, vectors add noise.

Worked Example — Chinook Database

The 11-table Chinook music store is mapped into the unified schema (catalog, people, transaction tables); junction tables and enum tables are skipped. Adding a new table costs ~10 lines of mapping. Reference implementation uses a Rails Searchable concern. See Bonsai - Designing Search for a Relational Database.

People

Source References