Build configuration

One JSON file drives everything the index can do — fields and weights, facets, typed values, geo points, autocomplete, and vectors.

Build with a config file:

npx rangefind build --config rangefind.config.json

Paths inside the config resolve relative to the config file. A complete example, annotated below:

{
  "input": "places.jsonl",
  "output": "public/rangefind",
  "scanWorkers": 8,
  "fields": [
    { "name": "title", "path": "name", "weight": 6.0, "b": 0.4, "phrase": true },
    { "name": "address", "path": "address_search", "weight": 10.0, "b": 0.2 },
    { "name": "body", "path": "description", "weight": 1.0, "b": 0.75 }
  ],
  "alwaysIndexFields": ["title", "address"],
  "facets": [{ "name": "category", "path": "category" }],
  "numbers": [
    { "name": "population", "path": "population", "type": "int" },
    { "name": "prominence", "path": "prominence", "type": "float" }
  ],
  "rankPrior": { "field": "prominence", "boost": 0.45, "overfetch": 4 },
  "booleans": [{ "name": "open_now", "path": "open" }],
  "geo": [{ "name": "location", "latPath": "lat", "lonPath": "lon" }],
  "suggest": [{ "path": "name", "weightPath": "population" }],
  "vectors": [{ "name": "embedding", "path": "embedding", "dims": 384 }],
  "display": ["name", "category", "prominence", "lat", "lon", "url"],
  "meta": { "attribution": "© Your Data Source", "license": "CC-BY-4.0" }
}

Text fields

Each entry in fields maps a JSON path to a searchable field:

Key Meaning
name field identifier used in scoring and stats
path dot path into each JSONL document (arrays are joined)
weight BM25F weight — how much a match here counts
b length normalization (0 = ignore field length, 0.75 = classic BM25)
phrase emit phrase/proximity signals for multi-word matches

alwaysIndexFields lists fields whose terms are always kept. Other fields compete for the per-document posting budget (targetPostingsPerDoc, default 12): the highest-scoring terms win, which keeps huge documents from bloating the index while short titles stay fully searchable.

Facets, numbers, booleans

These become compact doc-value columns and filter bitmaps, range-read only when a query filters or sorts.

rankPrior can use a numeric field normalized to [0, 1] as a conservative relevance tie-breaker. Rangefind reranks a bounded overfetch window with score *= 1 + boost * prior; it does not run on empty queries, explicit sorts, or distance sorts. Include the prior field in display when practical so the rerank reuses the hydrated result payload and needs no doc-value read. Queries can override rankPriorBoost or set it to 0 for an A/B baseline.

Geo fields

"geo": [{ "name": "location", "latPath": "lat", "lonPath": "lon" }]

Each geo field builds a static KD tree plus hidden lat/lon doc-values so text queries can verify geo filters exactly. Query with near (radius or nearest-first), box, distance sort, and distance boosts — see Geo search.

Autocomplete

"suggest": [{ "path": "name", "weightPath": "population" }]

Builds search-as-you-type suggestions into the authority sidecar: diacritic-folded prefix and mid-token matching, ranked by the weight path (or corpus frequency), with precomputed hot lists so the first keystroke is one small fetch.

Vectors

"vectors": [{ "name": "embedding", "path": "embedding", "dims": 384 }]

Embeddings are quantized to int8 in an IVF layout for cosine top-k and hybrid search. You bring the embedding model; rangefind stores and searches the vectors.

Display payloads

display lists the fields returned with each hit. Keep payloads lean — they're what the runtime fetches for every result. String entries pass through; object entries support { "name", "path", "maxChars" } truncation.

Provenance

meta is copied verbatim into the manifest next to built_at — attribution, license, generator, data version. Runtimes expose it as engine.manifest.meta so a UI can render required attribution (the OSM integration fills in ODbL-compliant defaults automatically).

Tuning that matters

Option Default When to touch it
scanWorkers 1 set to CPU count − 1 for large builds
targetPostingsPerDoc 12 raise for long documents that need deep recall
analysis none multilingual profile — see Multilingual
scoringStats frozen stats artifact for sharded builds
resumeBuild on (large profile) interrupted builds continue from checkpoints

Everything else — pack sizes, codecs, segment merge policy — has measured defaults; the build writes debug/build-telemetry.json if you want to see exactly where time and bytes went.