I run The DJ Calendar (https://thedjcalendar.com/) — a site with hundreds of artist pages. Every page has a search bar in the top-right corner that searches across every artist on the site.
It works instantly. No loading spinner. No debounce delay. No network request at all.
Here's how I built it with a prebuilt JSON index and zero dependencies.
The Problem
A site with hundreds of artists needs search. The obvious options are:
- Algolia / Meilisearch — great products, but adds a dependency, a monthly bill, and a server round-trip on every keystroke.
- Server-side search — hits your database on every keystroke. Fine for low traffic, but each search is a full HTTP request with latency.
- Lunr / Fuse.js — client-side libraries, but they're dependencies you don't control.
I wanted search that felt instant because it was instant. No network, no loading, no third-party script.
The Approach: Prebuilt JSON Index
At deploy time, a PHP script crawls the artist database and writes a flat JSON file. Every artist gets one entry with their name, genre, location, and a URL. The file sits on the server as static JSON — no PHP involved at runtime.
[
{
"name": "David Guetta",
"slug": "david-guetta",
"genres": ["EDM", "House", "Pop"],
"locations": ["Paris", "Ibiza", "Las Vegas"],
"url": "https://thedjcalendar.com/artist/david-guetta"
},
...
]
At page load, the search script fetches this JSON file once and stores it in memory. Every keystroke searches the local array.
Scored AND/OR Matching
The search function does scored matching across weighted fields. Name scores highest, genre and location score lower. A query like "david house" matches artists whose name OR genre contains "david" AND "house".
`function search(query, items) {
var terms = query.toLowerCase().split(' ');
var results = [];
for (var i = 0; i < items.length; i++) {
var item = items[i];
var score = 0;
var matchCount = 0;
for (var t = 0; t < terms.length; t++) {
var term = terms[t];
var fieldScore = 0;
// Name match — highest weight
if (item.name.toLowerCase().includes(term)) {
fieldScore = Math.max(fieldScore, 10);
}
// Genre match — medium weight
for (var g = 0; g < item.genres.length; g++) {
if (item.genres[g].toLowerCase().includes(term)) {
fieldScore = Math.max(fieldScore, 5);
}
}
// Location match — lower weight
for (var l = 0; l < item.locations.length; l++) {
if (item.locations[l].toLowerCase().includes(term)) {
fieldScore = Math.max(fieldScore, 3);
}
}
if (fieldScore > 0) matchCount++;
score += fieldScore;
}
// AND: all terms must match at least one field
if (matchCount === terms.length) {
results.push({ item: item, score: score });
}
}
results.sort(function(a, b) { return b.score - a.score; });
return results.slice(0, 20);
}`
The AND requirement prevents irrelevant results. If you type "las vegas pop", you get artists in Las Vegas whose genre is Pop — not every artist in Las Vegas.
ARIA Combobox
The search UI is a standard ARIA combobox pattern. The input has role="combobox", the results list has role="listbox", and each result is role="option".
Keyboard navigation: down/up arrows cycle through results, Enter selects, Escape closes. Focus is trapped in the combobox while it's open. The active option is announced via aria-activedescendant.
Why This Matters
This pattern works for any collection that fits in memory — artists, products, documentation pages, blog posts. The index is generated at build time, so there's zero runtime cost. The JSON file is cacheable by the browser and CDN.
I extracted this into a standalone repo: instant-search-index (https://github.com/bryanhamiltondev/instant-search-index). It includes the full combobox implementation with keyboard navigation, plus a PHP index builder you can adapt to any data source.
Lessons Learned
- JSON size matters. My index is ~150 KB gzipped. For larger datasets, chunk the index or lazy-load it.
- Scoring needs tuning. Start with simple weights and adjust based on real queries.
- AND matching is opinionated. Some search UIs work better with OR. Make it configurable.
- The ARIA pattern is non-negotiable. Search is one of the most-used features. It has to work for keyboard and screen reader users.
The full code, demo, and PHP index builder are on GitHub (https://github.com/bryanhamiltondev/instant-search-index). The live version runs on The DJ Calendar (https://thedjcalendar.com/https://thedjcalendar.com/).
All my production-extracted repos live at github.com/bryanhamiltondev (https://github.com/bryanhamiltondev). My portfolio is at bryanhamilton.info (https://www.bryanhamilton.info/).
Top comments (1)
The weighted AND matching is a neat fit for this index. I'd normalize whitespace before scoring: splitting on a literal space leaves empty terms, and every name includes an empty string. That makes a blank query return twenty arbitrary matches. Trimming, splitting on whitespace and dropping empty terms lets you handle an empty query explicitly. I'd add cases for leading spaces, repeated spaces, and a pasted tab so those inputs behave like the same clean query.