uqa-js: The Browser Database That Turns SQL, Graphs, Search, and Vectors into One Algebra

A TypeScript port of Unified Query Algebra that uses posting lists, functors, and calibrated fusion to make hybrid queries feel like one native system.

9 min read · cognica-io/uqa-js

A wide editorial illustration of four distinct query streams flowing into one mechanical sorting chamber. One stream looks like a table grid, one like branching graph nodes, one like stacked index cards, and one like clustered points, and all four exit as a single ordered ledger. It explains the core idea that different query paradigms can be normalized into one shared posting-list algebra.
Four query worlds, one shared currency. UQA turns each result shape into the same sortable object before it fuses anything else.
Key Takeaways

Most hybrid search systems start by bolting features together. uqa-js starts by asking a sharper question: what if SQL rows, graph traversals, full-text hits, and vector neighbors were all just different views of the same thing? That is the project’s real trick, and it is why the code reads less like a mashup and more like a formal port of a query engine.

One Posting List to Rule Them All

The core abstraction is the PostingList: a sorted list of (DocId, Payload). In a classic search engine, that is enough to represent inverted indexes. In uqa-js, it becomes the universal currency for query results. Once everything is in that shape, set operations like union, intersection, and difference can combine very different retrieval modes without needing a new rulebook for each one.

The engine first normalizes different query types into the same posting-list form, then applies algebra and scoring as separate steps.

That separation matters. The project does not treat scoring as a cosmetic afterthought. It treats scoring as a second pass on top of a shared algebra. That is the difference between a clever integration layer and a system that can actually reason about mixed query semantics.

Why Hybrid Search Gets Hard So Fast

Hybrid retrieval sounds simple until you try to compare scores that were never meant to live on the same scale. BM25 is not vector similarity. A graph match is not a keyword hit. A relational filter is not a relevance score at all. Most systems solve that mismatch with hand-tuned weights and hope the ranking behaves.

ProblemSiloed stackuqa-js
Query modelEach backend speaks its own languageDifferent models collapse into posting lists first
Score handlingHeuristic score blendingCalibrated fusion with probabilistic meaning
IntegrationGlue code between enginesOne algebraic execution layer
DeploymentMultiple services and adaptersBrowser-native orchestration with pushdown where possible
Cross-paradigm joinsAd hoc and brittleFunctor-based casting between models

That is the pressure point uqa-js is aiming at. It wants to turn mixed retrieval from a bag of heuristics into something closer to a mathematically legible pipeline.

A close-up editorial illustration of two score streams being poured into a calibration device. One stream is visually heavier and jagged, suggesting BM25-like text scores, while the other is smoother and clustered, suggesting vector similarity. The device aligns both into a shared scale before they merge into one ranked output ledger. It explains why hybrid search needs calibration rather than simple score addition.
The hard part is not combining results. It is making scores mean something comparable before they are combined.

How uqa-js Collapses the Models

The repository is organized like an engine, not a toolkit. src/core/ holds the mathematical base, src/operators/ implements the algebra, src/sql/ and src/graph/ parse different languages, src/storage/ handles indexes, and src/fdw/ connects to external systems such as DuckDB and Arrow. That layout says the quiet part out loud: the job is coordination.

// Simplified mental model
const sqlHits = await engine.sql(queryA)
const graphHits = await engine.graph(queryB)
const textHits = await engine.search(queryC)
const vectorHits = await engine.knn(queryD)

const unified = PostingList.union(
  sqlHits,
  graphHits,
  textHits,
  vectorHits
)

const ranked = fusion.deepMerge(unified)

The important move is the normalization step. If every source can be translated into a posting list, then operators do not have to care where the rows came from. They only need to know how to combine them.

The Functor Trick

This is where the project stops being merely multi-model and starts being formally interesting. The repository includes functor machinery such as GraphToRelationalFunctor and TextToVectorFunctor. In plain language, those are casting rules. They let one model be re-expressed in another without pretending the original structure never existed.

That matters for cross-paradigm joins. A graph relationship can become a relational row. Text can be lifted into vector space. Once the cast is explicit, the engine can do something most hybrids cannot: compose data models without collapsing them into a lowest-common-denominator schema.

Calibrating Relevance Instead of Guessing It

The scoring layer is where the engineering discipline shows up. A naive hybrid engine might add BM25 and vector similarity together and call it relevance. uqa-js instead leans on Bayesian calibration and likelihood ratios so that scores become comparable before they are fused. That is a more honest answer to the ranking problem.

ApproachWhat it doesTrade-off
Raw score additionAdds search signals directlyFast, but mathematically sloppy
Weighted heuristicsAssigns manual importance to each signalFlexible, but fragile and hard to tune
Bayesian calibrationConverts signals into comparable likelihoodsMore work upfront, but much cleaner fusion

That distinction is the difference between a demo and an engine. If you care about retrieval quality, the ranking math matters as much as the index structure.

DuckDB, Arrow, and the Manager of Managers Pattern

The foreign data wrapper layer makes the architecture pragmatic. Rather than re-implement every storage and execution backend, uqa-js can push work outward to specialized systems like DuckDB and Apache Arrow while keeping the hybrid logic local. It behaves like a manager of managers.

That is a strong pattern for local-first AI. Let the columnar engine stay columnar. Let the relational backend do relational work. Let the unified algebra own the awkward part, which is combining results across paradigms without losing semantics.

LayerBest atWhat uqa-js keeps local
DuckDB / ArrowEfficient relational and columnar executionHybrid fusion and cross-model composition
Search indexesFast text retrievalCalibration and result composition
Vector indexesNearest-neighbor lookupScore alignment and merging
uqa-js coreUnified query algebraThe orchestration logic

What This Says About the Future of Local-First AI

The obvious pitch is browser-native execution. The deeper pitch is that browser-native execution becomes interesting only when the engine underneath is expressive enough to unify real query modes. If a local app needs semantic search, graph logic, and relational structure, shipping five disconnected systems is a tax. uqa-js is trying to reduce that tax without reducing the problem.

That is why this repo stands out. It is not just another hybrid search layer. It is an attempt to make multiple database paradigms mathematically interchangeable inside one browser-scale engine.