Search That Runs in the Browser

No server, still searchable

The usual objection to publishing architecture as static files is that you lose search. You do not — you move it. The index is built once at publication time, shipped to the browser, and queried there. For repositories up to a few thousand elements this works well enough that readers do not notice it is not a server.

Figure 1: Where the index is built and what it carries
Figure 1: Where the index is built and what it carries

What to put in it

The temptation is to index everything. Resist it, because index size is the one hard constraint in this design and documentation text will dominate it.

FieldInclude?Why
Element nameAlwaysThe overwhelming majority of queries
TypeAs a facetCheap, and readers narrow by it constantly
Package pathAs a facetAnswers "where does this live"
DocumentationTruncatedHigh match value, high byte cost
Tagged valuesSelected keysOwner and lifecycle yes; every key no
Relationship namesRarelyLarge, and readers seldom search them

Truncating documentation to the first few hundred characters is the single highest-leverage decision. Most architecture documentation front-loads its distinguishing terms in the opening sentences; the tail is elaboration that rarely changes whether a result matches.

The size ceiling

Every byte of index is downloaded before the first search works. That makes index size a first-load cost paid by every reader, including the ones who never search.

As a rough guide, a few megabytes is comfortable on a corporate LAN and tolerable over VPN. Past roughly 8 MB the first-load delay becomes noticeable enough that readers assume the page is broken. At that point the options are to shard the index by perspective, to index names and facets only, or to accept that you have outgrown browser-side search.

If your tooling can warn you when the index crosses a threshold, let it. This is a limit you cross gradually and discover suddenly.

How the index is actually built

The index is a publication artefact like any other: computed at generation time, from the same snapshot the pages are generated from, and shipped as one more static file. That single sentence carries two decisions worth making deliberately.

The first is to serialise a prebuilt index rather than shipping raw documents and letting the browser construct the index on arrival. Client-side libraries will happily do the latter, and for a few hundred entries it is fine; for a few thousand, index construction is a visible pause on every page load, paid by every reader, every visit. Building once at publication time moves that cost to the pipeline, where it runs once a week on a machine nobody is waiting for. The trade is that the serialised index is coupled to the library version that built it — pin the version, and treat a library upgrade as a change that regenerates the portal.

The second is determinism. Given the same snapshot and configuration, the build should produce byte-identical index files, which is mostly a matter of sorting entries before serialising and not embedding timestamps. Determinism sounds like purism until the first time verification flags a suspicious size change and you need to diff this week's index against last week's to see what actually moved — at which point a stable ordering is the difference between a readable diff and noise.

What ranking is still for

Facets carry the navigation load, but the result list still has an order, and a few cheap rules make that order feel intelligent. Boost the name field far above everything else: a reader typing "payment" wants Payment Gateway above forty elements that merely mention payments in their documentation. Within name matches, exact-prefix beats substring — "pay" should surface Payment Gateway before Bill Payments Adapter. And when scores tie, break the tie alphabetically rather than by insertion order, because a stable, predictable list reads as correct even when the ranking is doing nothing clever.

Classical relevance machinery earns little here. TF-IDF distinctions are built for corpora where documents differ wildly in length and vocabulary; architecture repositories are the opposite — thousands of short, similarly-shaped entries. One boost that does pay for itself: lifecycle. A reader searching a name almost always wants the production system, not its retired predecessor with the same name and a "-OLD" the modeller forgot to add. Ranking retired and planned elements below active ones, and saying so with a small chip in the result row, resolves the most common duplicate-name confusion without hiding anything.

What readers actually type

Two behaviours dominate, and designing for them is worth more than sophisticated ranking:

  • Prefix matching. Readers type three or four characters and expect to see candidates. If results only appear on exact whole-word matches, the search feels broken even though it is working.
  • Partial recall of a name. They remember "something gateway" or "the payment thing". Substring and fuzzy matching earn their cost here; strict tokenisation does not.

Phrase search and exclusion are worth having but are used by a small minority. Get prefix and fuzzy right first.

Beyond the box: search as the portal's navigation

Once the index is fast and the facets work, search quietly becomes the portal's primary navigation, and a few small investments acknowledge that reality. Make queries deep-linkable — the query and active facets encoded in the URL fragment — so a reader can paste "everything in Payments tagged critical" into a chat message and the recipient lands on the same filtered view. This costs an afternoon and changes how teams use the portal: the search stops being a private tool and becomes the way people point at slices of the architecture.

Give the box a keyboard shortcut — the slash key has become a convention readers try unprompted — and focus it on the shortcut rather than making them find the field. Treat the empty state as a front door instead of a void: before the first keystroke, show the type facets with counts and the most-visited elements, so a reader who does not know what to type can browse their way in. And on every element page, seed a prepared query — "everything else in this package", "other elements owned by this team" — because the moment a reader finishes one answer is the moment they have the next question, and a one-click pivot keeps them in the portal rather than back in email asking an architect.

One temptation to decline: heavy typo tolerance. Full fuzzy matching over every field doubles index lookups, and in a corpus of formal names it mostly surfaces noise — the reader who typed "paymnet" is served better by prefix matching on "paym" than by edit-distance guesses across three thousand entries. Spend that complexity budget on aliases instead; misspellings are rare in practice, but calling things by a different name than the model does is constant.

Facets beat ranking

On a general-purpose web search, ranking does the work. On an architecture repository, faceting does. The reason is that the corpus is small and highly structured: a query for "customer" might return sixty results, and no ranking function knows whether this particular reader wants the application, the data object, the capability or the business process.

Give them a type facet and a package facet and they will narrow sixty results to four in one click, reliably, without you having to guess. Facets also degrade gracefully: when ranking is wrong the reader is stuck, when a facet is wrong they simply pick a different one.

The thing that makes it feel fast

Search over a few thousand entries in the browser is genuinely fast — the matching is not the bottleneck. What readers perceive as slow is the first-load fetch of the index and, on a large result set, the rendering.

Two cheap wins: load the index lazily on first interaction with the search box rather than on page load, and cap the rendered result list at something like fifty with a count of the rest. Neither changes the search quality and together they remove almost all of the perceived latency.

Shipping the index without hurting first load

The index is the largest single asset in a published portal, and how you ship it determines whether readers experience the portal as fast or slow.

  • Load it lazily. Fetch on first focus of the search box, not on page load. Most readers arriving at an element page never search, and making them pay for the index is the most common mistake.
  • Compress it. Search indexes are highly repetitive and compress extremely well. Serving it gzipped typically cuts it by three quarters, and any web server will do this for you.
  • Cache it properly. Content-hash the filename so it can be cached indefinitely and invalidated by the next publication. Readers who visit weekly then download it once per publication rather than once per session.

Search on a scoped portal

Everything above assumes every reader may see every result, and for a portal generated from a deliberately public subset that assumption holds by construction — the index is built from the same filtered snapshot as the pages, and what was excluded at generation time cannot appear in a result. This is the quiet security advantage of the static-subset model: the search index cannot leak what it never contained.

The moment the portal serves different slices to different audiences, browser-side search becomes the most dangerous asset you ship. The index is a downloadable file containing names, paths and documentation fragments for everything it covers; sending one index to everyone means every reader holds the searchable summary of models they cannot open. The workable pattern is one index per audience scope, generated alongside each scope's pages, with the server choosing which index a session may fetch — the full treatment of that problem is its own article. The unworkable pattern is filtering results in the browser after downloading a global index, which is not access control at all; it is a polite request to the reader's developer tools. If the estate is scoped, the index inherits the scoping requirement whole.

A search box with no feedback loop drifts. The queries that fail teach you nothing, readers conclude the portal "doesn't have" things it has under a different name, and nobody owns the gap. On a static portal there is no server log of queries, but the page can keep a modest client-side record — the query, the result count, whether a result was clicked — and post it to the same lightweight endpoint that collects the portal's other usage signals, or simply hold it in local storage for support conversations if telemetry is unwelcome.

The report worth reading is short: the zero-result queries, weekly, deduplicated. It is a direct list of vocabulary mismatches between readers and the model. When five people search "CRM" and the model calls it Customer Engagement Platform, the fix is not search technology — it is an alias. Most modelling tools have a natural home for aliases in tagged values or properties; index them alongside the name, and the acronym every human uses finally finds the element the architects named formally. In our experience a dozen aliases harvested from three weeks of failed queries improve perceived search quality more than any ranking work, because they repair the actual failure, which was never the algorithm — it was that the model speaks architect and the readers speak business.

A worked size budget

To make the earlier hand-waving concrete, here is the arithmetic for a typical mid-sized estate: three thousand elements, names averaging 25 characters, package paths 60, documentation truncated at 240, a type facet and two tagged values per element. That is roughly 400 bytes of indexed text per element before serialisation overhead — call it 1.5 MB of raw index, which a prebuilt serialisation roughly doubles and gzip then cuts to a quarter. The number that travels over the wire lands near 700 KB: entirely comfortable, with headroom to triple the estate before the ceiling discussion starts.

Run the same arithmetic with untruncated documentation averaging 2,000 characters and the wire size passes 4 MB on its own — which is the quantified version of this article's first piece of advice. The budget is dominated by one decision, and it is the decision most portals never consciously make.

When the estate speaks more than one language

Belgian and international estates add a wrinkle the search literature mostly ignores: the model is in English, the business vocabulary is in French and Dutch, and the readers switch between all three without noticing they have done it. Two mechanics carry most of the weight.

The first is diacritics folding — indexing and querying through the same normalisation, so that "Trésorerie" and "Tresorerie" are the same token. This is a solved problem in every serious search library and an unsolved one in most portals, because someone has to remember to turn it on for both the build side and the query side; folding only one of the two produces the maddening behaviour where copy-pasted names match and typed ones do not. The second is treating translated names the same way as the aliases discussed above: a portal that publishes in several languages already carries per-language names for elements, and every one of them belongs in the index, pointing at the same entry, regardless of which language the reader's interface is currently showing. A Dutch-speaking analyst searching "betalingen" should land on Payment Gateway without either of them knowing the other's word for it.

What does not earn its keep at this corpus size is stemming — the linguistic reduction of "payments" to "pay". Stemmers are language-specific, wrong just often enough to be noticed, and mostly redundant once prefix matching works, since the prefix "pay" already covers the family. Fold the diacritics, index every name the organisation actually uses, and let prefix matching do the morphology; three thousand formal names are not a corpus that rewards linguistics.

When to give up on browser-side search

It is worth naming the exit condition in advance, because the alternative is discovering it as a gradual degradation nobody owns.

Three signals: the index passes the threshold where first load is noticeable on your slowest realistic connection; readers start reporting that search "does not find things" when it is actually finding too many; or you need cross-portal search across several publications.

At that point the honest options are sharding the index per perspective, reducing what is indexed to names and facets only, or accepting that you need a search service. All three are reasonable. What is not reasonable is shipping a 20 MB index and hoping nobody notices.