Sharded indexes

Split a planet-sized corpus into regional indexes that build, update, and cache independently — and still behave as one engine with identical rankings.

One giant index has four walls: full rebuilds for every update, per-query costs that grow with corpus size, builder memory ceilings, and a single blast radius. Sharding removes all four — if results from different shards can be merged honestly. That's the hard part, and it's the part rangefind solves exactly.

Exact cross-shard scoring

BM25 scores depend on corpus-wide statistics (document counts, average field lengths, per-term document frequencies). Rangefind collects them once over all shard inputs into a scoring-stats artifact, and every shard build freezes against it:

import { collectScoringStats } from "rangefind/scoring-stats";
import { writeShardedRootManifest } from "rangefind/shards";
import { build } from "rangefind/builder";

const { statsPath, stats } = await collectScoringStats({
  config, // your resolved rangefind config
  inputs: [
    { id: "quebec",  input: "quebec.jsonl" },
    { id: "ontario", input: "ontario.jsonl" }
  ],
  outDir: "scoring-stats"
});

// each shard: a normal build with { "scoringStats": statsPath } in its config
await build({ configPath: "quebec.config.json" });
await build({ configPath: "ontario.config.json" });

writeShardedRootManifest({
  outDir: "public/rangefind",
  shards: [
    { id: "quebec",  bbox: stats.inputs[0].bbox, groups: ["canada"] },
    { id: "ontario", bbox: stats.inputs[1].bbox, groups: ["canada"] }
  ],
  scoringStats: stats
});

The guarantee, asserted in the test suite: a sharded index reproduces the monolithic build's rankings exactly — same result sets, scores equal to 1e-9. Measured on a 6.1M-place Québec corpus, building four shards (including the stats pass) took 423 s versus 421 s for the monolith, at the same total size.

Query-time federation

createSearch on the root manifest returns one engine over all shards:

Updating one region

Refresh a region's data, diff it against what the shard reflects, and ship the changes as a generational delta against the same stats artifact — proven identical to rebuilding the shard from scratch. A nightly OpenStreetMap region update uploads kilobytes, and every other shard's CDN cache stays warm. Regenerate the stats artifact only on major corpus shifts (it intentionally invalidates every shard, since statistics are frozen corpus-wide).

Planet OSM, concretely

The reference pipeline (osm-rangefind-index) builds all of OpenStreetMap as 310 shards — countries, split to states and provinces where large — from ~79 GB of Geofabrik extracts, on a single machine's idle hours:

For the full design — including how buildOsmShardedIndex wraps all of the above in one call — see the architecture document in the repository: docs/sharded-osm.md.