Core Concepts

How Search Works

Every search runs in two phases.

Phase 1 — Pattern Matching. The engine scans all documents using an inverted index with BM25 scoring. This phase has strong recall: it tolerates typos, partial words, and messy input without any preprocessing. The result is a ranked list of candidates.

Phase 2 — Coverage. The top 500 candidates (configurable) are re-evaluated for exact and near-exact token matches: whole words, fuzzy words, joined/split words, prefixes, and suffixes. Documents with confirmed token matches are rescored on a 16-bit scale and promoted above pure pattern-match results.

Coverage is enabled by default and fast enough that it doesn't add significant latency. Leave it on.

A search for "Wirless hedphones" will still surface "Wireless Headphones" — the pattern phase catches it despite the typos, and the coverage phase confirms the near-exact match (edit distance 1 on each word).

A heavily garbled query like "Wirlss edphons" will still return results — the pattern phase finds candidates — but coverage won't confirm any near-exact matches. In that case you get ranked results without a truncation point and without facet counts. The results are useful but less precise.

Language Handling — Inflections, Compound Words, Synonyms

Indx has no per-language tokenizer, stemmer or stop-word list, and does not need one for Norwegian, English or mixed-language data. The character-level matcher covers what a stemmer usually does:

  • Typos and spelling variants — one-edit tolerance on every word (CoverFuzzyWords).
  • Inflections and prefixes — a query word that is a prefix of a stored word matches (CoverPrefixSuffix): bok finds bokhandel; run finds running. The reverse direction (query running, stored run) is matched by the pattern phase too, and scores a little lower under coverage, since the query is not fully covered. Neither needs a synonym.
  • Compound words (sammensatte ord) — spesialpedagogikk matches spesial pedagogikk and vice versa without any rules (CoverJoinedWords). Both decomposition and recomposition work, and no dictionary is involved. Mark title-like fields HighResolution for the sharpest compound matching.
  • Domain vocabulary — synonyms expand the query before scoring and are editable by editors without code.

On a feature checklist this reads as: no stemmer to configure, because the matcher handles inflection by beginnings and endings. The one case left is an irregular form that shares neither with its base, such as bok and bøker or mouse and mice, and only there is a synonym entry the answer. Synonyms are never for typos or for translation.


Field Roles

Fields don't do anything until you explicitly assign them a role. You configure this after calling Init (which discovers the field names) and before calling Index.

RoleWhat it does
Searchable
The field is included in matching and scoring. At least one field must be searchable.
Filterable
The field can be used to filter results before or during a search.
Facetable
The field returns value counts in search results — useful for sidebar filters. Values whose count drops to 0 under the current filter are omitted from the response; keep them in the UI greyed out so the user can still deselect them.
Sortable
Results can be ordered by this field's value. Works on both numbers and strings.

A field can have multiple roles — a category field can be both filterable and facetable at the same time.

Weight

Searchable fields have a weight (default 1.25) that controls how much influence that field has on the final score. If name should matter more than description, give it a higher weight.

csharp
engine.GetField("name")!.Weight = 2.0f;
engine.GetField("description")!.Weight = 1.0f;

Weight is a relative value — it only matters in proportion to the weights of the other searchable fields.


Filters Are Server-Side

A search returns only a limited slice of results (maxNumberOfRecordsToReturn). If you filter that slice yourself after receiving it, you will miss matching documents that weren't in the top results.

Always pass filters to the engine so they are applied during search — the engine evaluates them against the full dataset, not just the returned page.

csharp
// Wrong: filter the array you got back
var results = engine.Search(query).Entries.Where(e => ...);

// Right: create a filter and give it to the query
var filter = engine.CreateValueFilter("category", "audio", out _);
var query  = new Query("wireless", 20) { Filter = filter };
var result = engine.Search(query);

The same applies to the HTTP API: create a filter server-side first, then pass its hashString in the search request.


Scoring

Results have a 16-bit score (0–65535). Coverage hits (exact and near-exact matches) always score higher than pure pattern-match results, which appear below them.

When sorting is applied alongside a text search, the score remains the primary sort — the sort field is only used as a tiebreaker. When searching with no query text (empty search), sorting becomes the primary ordering.


Synonyms

A dataset can carry a synonym list that expands queries at search time: when the query matches an entry, the entry's terms are appended to the query text before scoring. Multidirectional entries make a group of terms equivalent (inflections, spelling variants); one-way entries let a trigger like an acronym pull in its expansion without the reverse. Nothing is re-indexed and changes apply on the next search. See Set Up Synonyms.


The Engine Lifecycle

The engine must go through a fixed sequence before it can search:

Init()   →   configure fields   →   Load()   →   Index()   →   Search()
  • Init — reads the JSON structure and discovers field names
  • Configure — set roles and weights on the fields you care about
  • Load — streams the JSON documents into the engine
  • Index — builds the inverted index; engine enters the Ready state
  • Search — available once the engine is Ready

After indexing you can also insert, update, and delete individual documents without rebuilding the index — see Dynamic Operations.


An empty or null query string is valid, but requires EnableFacets = true — without it, no documents are returned. The result contains up to maxNumberOfRecordsToReturn documents, while facets are calculated across the entire dataset.

Results are ordered by sort field if one is specified, making empty search the natural companion to sorting and faceted navigation.