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
facets— keyword dimensions with per-query counts. UselabelPathfor a display label distinct from the value.numbers— typed columns (int,float,date) usable in range filters and sorting. Add"sortable": truesemantics via query-time sort.booleans— true/false columns for filters.
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.