← Back to work

Library author and engineer (solo build) · 2026

PolySearch

One SearchEngine interface across Elasticsearch, OpenSearch and Solr: a unified query language, autocomplete and faceting that behave the same on every engine, and a compare mode that runs one query across all three and shows where their relevance disagrees.

Source ↗
Cover image for PolySearch

The problem

Most search code is written against one engine and quietly assumes that engine forever. Elasticsearch, OpenSearch and Solr all build on Lucene and BM25, yet each speaks a different query language and disagrees in subtle ways about ranking, so switching engines or comparing them honestly means rewriting queries and trusting your memory. I wanted a single, typed interface that hides those differences where they do not matter and surfaces them where they do. PolySearch is the result: a TypeScript library, CLI and web UI I built solo.

Approach

Everything hangs off one idea: every backend implements the same SearchEngine interface, and the rest of the code depends only on that interface. A caller builds one engine-neutral query, and a per-engine translator compiles it into that engine's native form. The translators are pure functions, so they are unit-tested without a running engine, and one place (createEngine) is where a concrete backend is chosen. The headline feature is a compare mode that runs the same query across all three engines and reports where they agree and where they rank documents differently, because relevance is a ranking, not a raw score you can compare across engines.

Architecture

How I built it

I shipped it as a sequence of small, versioned releases, each one leaving the project green and landing as a single commit, so the git history reads like a delivery log. A Next.js web UI sits on top: it seeds a sample catalogue into all three engines, runs the compare view, and demonstrates the autocomplete dropdown and facet sidebar live in the browser.

What I'd do differently

The autocomplete uses prefix matching rather than each engine's native completion suggester, a deliberate choice so it works on existing indexes with no schema changes; a production build aiming for the lowest possible latency would add the native suggesters behind the same interface. Semantic (vector) search and synonyms are the natural next features, and the interface is designed to absorb them without changing the callers.