---
title: "Query JSON"
url: https://recursiveai.org/oneindex/docs/query-json/
description: "A search as data: a JSON document with \"v\": 1, checked against a schema as it is read. It is meant for programs and language models that build a query…"
version: "0.4.0-preview.2"
source: https://github.com/recursive-ai-oss/oneindex-csharp/blob/177f49bb3974e9e44df6d1a800342a9d353a0984/docs/query-json.md
---

# Query JSON

A search as data: a JSON document with `"v": 1`, checked against a schema as it is read. It is meant for programs and language models that build a query without a C# compiler, and it compiles to the same query objects as every other surface. It is not compatible with Elasticsearch's query DSL.

```json
{
  "v": 1,
  "q": "overdue \"acme corp\" -draft",
  "filter": "Tags = invoice AND Modified > 2024-01-01",
  "limit": 10,
  "highlight": true
}
```

```csharp
SearchQuery<Invoice> search = index.Parse(json);            // throws QueryParseException with the JSON path
QueryRequest request = QueryJson.Read(json, index.Schema);   // the same, as data
string canonical = QueryJson.Write(request);
```

## The document

| Property | Meaning |
|---|---|
| `v` | **Required.** The format version, the whole number 1. Any other value is an error. |
| `q` | Free text, as typed by a user ([user-text.md](https://recursiveai.org/oneindex/docs/user-text/)). Never an error. |
| `query` | A structured query node (below). When both `q` and `query` are given, both must match. |
| `filter` | A [filter string](https://recursiveai.org/oneindex/docs/filter-strings/), or a query node. Selects without scoring. |
| `limit` | Hits to return, 0 to 10,000. Default 20. |
| `offset` | Hits to skip, 0 to 10,000. Default 0. `limit + offset` is at most 10,000. |
| `countExactly` | `true` for an exact `TotalHits`. Default false (a lower bound). |
| `highlight` | `true` for the default snippets, or `{"fragmentLength": 1..10000, "maxFragments": 1..100}` (defaults 160 and 3). |
| `sort` | 1 to 16 strings that order the hits instead of relevance: `Field:asc`, `Field:desc`, `_score`, `_geoPoint(lat, lng):asc`, each optionally ending in `:first` or `:last` for documents without a value. See [sorting.md](https://recursiveai.org/oneindex/docs/sorting/). |
| `searchAfter` | A cursor token (`SortCursor.ToToken()`, also `NextCursor` of the previous page) to continue after; needs `sort` made the same way (or `rankingRules`, whose keys the token belongs to), and excludes `offset`. |
| `rankingRules` | Ranking rules in order, as strings: `words`, `typo`, `proximity`, `attribute`, `sort`, `exactness`, `field:asc`, `field:desc` (at most 12; see [ranking-rules.md](https://recursiveai.org/oneindex/docs/ranking-rules/)). A property rule must name a keyword, bool, numeric or date column of the schema, or reading fails with a positioned error (`$.rankingRules[1]`) and a suggestion. Absent: the rules the type declares. `[]`: rank by relevance. Added to version 1; documents without it read the same. |

With no `q` and no `query`, every document is a candidate. Unknown properties are errors that name the path and the nearest known name. These names are reserved for later versions and rejected with a message of their own: `facets`, `aggregations`, `distinct`, `vector`. `hybrid` is a property: an object that fuses the text search of `q` with a vector search over its embedding, and a node type `hybrid` joins `knn` for callers that bring a vector (both in `hybrid.md`; additive to version 1, so every document that was accepted reads the same). `rerank` is a property too: an object `{"window":50}` that rescores the best hits of the search with the index's reranker; it reads the text of `q` and is described in `rerank.md` (additive to version 1 as well). (`sort` and `searchAfter` were reserved in the first drafts of version 1 and are now properties: documents that used to be rejected are accepted, and every document that was accepted reads the same.)

Limits: the document is at most 1 MiB, strings 8,192 characters, query nodes nested 32 deep, 2,048 nodes in all. Invalid UTF-8 and lone surrogates are errors.

## Query nodes

A node is an object with a `type`. Every node accepts `"boost"` (a non-negative number; default 1). Fields are checked against the schema: the name must exist and be indexed, and the kind of field decides what the node accepts.

| `type` | Properties | Meaning |
|---|---|---|
| `matchAll` | | every document |
| `text` | `text` | user text across the full-text fields (what `q` is) |
| `strict` | `text` | a [strict query](https://recursiveai.org/oneindex/docs/query-syntax/); its errors keep their offset, line and column inside the message and `$.….text` as the path |
| `filter` | `filter` | a [filter string](https://recursiveai.org/oneindex/docs/filter-strings/) used as a query |
| `term` | `field`, `value` | the exact value. A keyword takes a string; a full-text field takes one *analyzed* term (not analyzed again); numbers, dates and booleans take a JSON number, an ISO date string or `true`/`false` |
| `phrase` | `field`, `terms` (1 to 256 strings), `slop` (0 to 1000) | analyzed terms that must occur in order |
| `prefix` | `field`, `prefix`, `scoreTerms` (default false) | terms that start with the analyzed prefix; every match scores the boost, or with `scoreTerms` the BM25F scores of the terms |
| `fuzzy` | `field`, `term`, `maxEdits` (0 to 2, default 2), `prefixLength` (default 0), `transpositions` (default true), `maxExpansions` (1 to 1024, default 50) | the closest terms within the edit distance |
| `regex` | `field`, `pattern` | terms that the expression matches in full (checked when read) |
| `range` | `field`, at most one of `gt`/`gte`, at most one of `lt`/`lte`, at least one bound | numbers, dates (ISO strings) or keywords (strings); not booleans |
| `exists` | `field` | documents that have a value |
| `multiFieldTerm` | `fields`, `term` | one analyzed term in any of the full-text or keyword fields, scored as one BM25F term |
| `typoTerm` | `fields`, `term`, `maxTypos` (0 to 2, default 2), `prefix` (default false), `exactFields` | one analyzed word matched with typos and optionally as the beginning of longer words; what user text expands to ([typo-tolerance.md](https://recursiveai.org/oneindex/docs/typo-tolerance/)) |
| `geoRadius` | `field` (a geo point field), `lat`, `lng`, `meters` | documents with a point within the distance ([geo.md](https://recursiveai.org/oneindex/docs/geo/)) |
| `geoBoundingBox` | `field`, `north`, `south`, `west`, `east` | documents with a point in the rectangle; `west` greater than `east` crosses the antimeridian |
| `bool` | `must`, `filter`, `should`, `mustNot` (arrays of nodes), `minimumShouldMatch` | all `must` and `filter` clauses, no `mustNot` clause, and enough `should` clauses; `filter` clauses do not score |
| `proximity` | `query` (a node), `words` (single-word nodes `term`, `multiFieldTerm` or `typoTerm`, at most 64), `weight` (0 to 4, default 0.1) | the score of `query` plus a term-proximity bonus for the words; see [scoring.md](https://recursiveai.org/oneindex/docs/scoring/). Text queries (`q`, `text`) with two or more words read as this node, and write back as it |
| `moreLikeThis` | `text` or `id`, `fields`, `maxQueryTerms` (1 to 1,024, default 25), `minTermFrequency`, `minDocFrequency`, `minimumShouldMatch` (above 0 to 1, default 0.3), `boostTerms`, `includeSource` | documents that resemble a text or an indexed document ([more-like-this.md](https://recursiveai.org/oneindex/docs/more-like-this/)) |
| `functionScore` | `query` (a node; default `matchAll`), `functions` (1 to 32 function objects), `scoreMode` (`multiply`, `sum`, `avg`, `first`, `max`, `min`; default `multiply`), `boostMode` (`multiply`, `replace`, `sum`, `avg`, `max`, `min`; default `multiply`), `maxBoost` (above 0), `minScore` (not negative) | the score of `query` changed by functions of column values; see [function-scores.md](https://recursiveai.org/oneindex/docs/function-scores/) |

A **function object** has a `type`, an optional `weight` (not negative; default 1) and an optional `filter` (a filter string or a node):

| `type` | Properties |
|---|---|
| `weight` | |
| `fieldValueFactor` | `field` (numeric, date or bool column), `factor` (default 1), `modifier` (`none`, `log`, `log1p`, `log2p`, `ln`, `ln1p`, `ln2p`, `square`, `sqrt`, `reciprocal`), `missing` |
| `gauss`, `exp`, `linear` | `field` (a numeric, date or geo point column), `origin` (a number; for a date field also an ISO date string; for a geo point `{"lat":…,"lng":…}`), `scale` (above 0; milliseconds for dates, metres for geo points), `offset` (default 0), `decay` (above 0 and below 1; default 0.5) |

Function fields need a column, not an index. The custom function of the low-level API has no JSON form. A written `origin` is always a number (a date as milliseconds since 1970).

`phrase`, `prefix`, `fuzzy`, `regex` and `multiFieldTerm` carry **already analyzed** terms, as the query objects do: write them lower case for the default analyzer. To search words the way a user would, use `text` or `strict` (`{"type":"strict","text":"Title:\"acme corp\""}`).

Numbers on integer fields may be fractional in a `range` (rounded inward, as in filter strings). Dates are ISO strings: `2024-01-31`, `2024-01-31T10:00:00Z`, with an optional fraction and offset.

## Paths in dynamic JSON

Wherever a node names a `field`, the name of a `[Dynamic]` property, a dot and a path name a path inside it (`"field":"Meta.customer.age"`). `term`, `range` and `exists` choose the kind of value from the JSON type of the value given (string, number, boolean; a string bound that reads as a date makes a date range); `prefix`, `fuzzy`, `regex` and `phrase` apply to strings. See dynamic.md.

## Errors

A `QueryParseException` has `Path` (`$.query.must[1].field`), `Detail`, and `Suggestion` for a misspelt field, property or type. Text that is not JSON has no path; its `Line` and `Column` locate the syntax error. A `filter` or `strict` string with a mistake reports the position inside that string in the message, and the path of the string.

## Canonical form and round trips

`QueryJson.Write` is canonical: properties in a fixed order (`v`, `query`, `filter`, `limit`, `offset`, `countExactly`, `highlight`, `sort`, `searchAfter`, `rankingRules`), nodes with `type` first, no white space, defaults left out, boosts as shortest round-trip numbers.

- **Object to JSON to object is identity:** `Read(Write(request))` equals `request` for every request whose boolean clauses are grouped as `must`, `filter`, `should`, `mustNot` (the shapes of all our parsers). Interleaved clauses are written grouped, which keeps the order inside each group; their meaning is the same.
- **JSON to object to JSON is canonical:** any accepted document, whatever its property order or spacing, and with `q`, `text`, `strict` and `filter` nodes already compiled into query nodes, writes as one canonical document. Writing is deterministic.
- `Range(v, v)` with both bounds included and a value that is not text is written as a `term`; `Range` with no bounds as `exists`; a `MatchAll` as `matchAll`.
- `Boost` of 1 is not written. Include flags on an open bound are normalized to true.
