Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Query API

The query engine supports indexed field queries with boolean combinators, pagination, sorting, aggregations, projections, and an explain mode.

Endpoint Summary

MethodPathDescriptionAuthStatus Codes
POST/files/queryExecute a queryYes200, 400, 404, 500
POST/files/searchGlobal cross-directory searchYes200, 400, 500

POST /files/query

Execute a query against indexed fields within a directory path.

Request Body

{
  "path": "/users",
  "where": {
    "field": "age",
    "op": "gt",
    "value": 21
  },
  "limit": 20,
  "offset": 0,
  "order_by": [{"field": "name", "direction": "asc"}],
  "after": null,
  "before": null,
  "include_total": true,
  "select": ["@path", "@score", "name"],
  "explain": false
}
FieldTypeRequiredDescription
pathstringYesDirectory path to query within
whereobject/arrayYesQuery filter (see below)
limitintegerNoMax results to return (server default applies if omitted)
offsetintegerNoSkip this many results
order_byarrayNoSort fields with direction
afterstringNoCursor for forward pagination
beforestringNoCursor for backward pagination
include_totalbooleanNoInclude total_count in response (default: false)
selectarrayNoProject specific fields in results
aggregateobjectNoRun aggregations instead of returning results
explainstring/booleanNo"plan", "analyze", or true for query plan

Query Operators

Each field query is an object with field, op, and value:

{"field": "age", "op": "gt", "value": 21}

Comparison Operators

OperatorDescriptionValue TypeExample
eqExact matchany{"field": "status", "op": "eq", "value": "active"}
gtGreater thannumber/string{"field": "age", "op": "gt", "value": 21}
ltLess thannumber/string{"field": "age", "op": "lt", "value": 65}
betweenInclusive rangenumber/string{"field": "age", "op": "between", "value": 21, "value2": 65}
inMatch any value in a setarray{"field": "status", "op": "in", "value": ["active", "pending"]}

Text Search Operators

These operators require the appropriate index type to be configured.

OperatorDescriptionIndex RequiredExample
containsSubstring matchtrigram{"field": "name", "op": "contains", "value": "alice"}
similarFuzzy trigram match with thresholdtrigram{"field": "name", "op": "similar", "value": "alice", "threshold": 0.3}
phoneticSounds-like matchphonetic{"field": "name", "op": "phonetic", "value": "smith"}
fuzzyConfigurable fuzzy matchtrigramSee below
matchMulti-strategy combined matchtrigram + phonetic{"field": "name", "op": "match", "value": "alice"}

Fuzzy Operator Options

The fuzzy operator supports additional parameters:

{
  "field": "name",
  "op": "fuzzy",
  "value": "alice",
  "fuzziness": "auto",
  "algorithm": "damerau_levenshtein"
}
ParameterValuesDefault
fuzziness"auto" or integer (edit distance)"auto"
algorithm"damerau_levenshtein", "jaro_winkler""damerau_levenshtein"

Similar Operator Options

{
  "field": "name",
  "op": "similar",
  "value": "alice",
  "threshold": 0.3
}
ParameterTypeDefaultDescription
thresholdfloat0.3Minimum similarity score (0.0 to 1.0)

Boolean Combinators

Combine multiple conditions using and, or, and not:

AND

All conditions must match:

{
  "where": {
    "and": [
      {"field": "age", "op": "gt", "value": 21},
      {"field": "status", "op": "eq", "value": "active"}
    ]
  }
}

OR

At least one condition must match:

{
  "where": {
    "or": [
      {"field": "status", "op": "eq", "value": "active"},
      {"field": "status", "op": "eq", "value": "pending"}
    ]
  }
}

NOT

Invert a condition:

{
  "where": {
    "not": {"field": "status", "op": "eq", "value": "deleted"}
  }
}

Nested Boolean Logic

Combinators can be nested up to 32 levels deep. Queries exceeding this depth are rejected with a 400 error. Additionally, a single query can return at most 100,000 results – requests that would exceed this ceiling are truncated.

{
  "where": {
    "and": [
      {"field": "age", "op": "gt", "value": 21},
      {
        "or": [
          {"field": "role", "op": "eq", "value": "admin"},
          {"field": "role", "op": "eq", "value": "moderator"}
        ]
      }
    ]
  }
}

Legacy Array Format

An array at the top level is sugar for AND:

{
  "where": [
    {"field": "age", "op": "gt", "value": 21},
    {"field": "status", "op": "eq", "value": "active"}
  ]
}

Response Format

Standard Query Response

{
  "results": [
    {
      "path": "/users/alice.json",
      "size": 256,
      "content_type": "application/json",
      "created_at": 1775968398000,
      "updated_at": 1775968398000,
      "score": 1.0,
      "matched_by": ["age"]
    }
  ],
  "has_more": true,
  "total_count": 150,
  "next_cursor": "eyJwYXRoIjoiL3VzZXJzL2JvYi5qc29uIn0=",
  "prev_cursor": "eyJwYXRoIjoiL3VzZXJzL2Fhcm9uLmpzb24ifQ==",
  "meta": {
    "reindexing": 0.67,
    "reindexing_eta": 1775968398803,
    "reindexing_indexed": 670,
    "reindexing_total": 1000,
    "reindexing_stale_since": 1775968300000
  }
}
FieldTypeDescription
resultsarrayMatching file metadata with scores
has_morebooleanWhether more results exist beyond the current page
total_countintegerTotal matching results (only if include_total: true)
next_cursorstringCursor for the next page (if has_more is true)
prev_cursorstringCursor for the previous page
default_limit_hitbooleanPresent and true when the server’s default limit was applied
default_limitintegerThe server’s default limit value (present with default_limit_hit)
metaobjectReindex progress metadata (present only during active reindex)

Result Fields

Each result object contains:

FieldTypeDescription
pathstringFull path to the matched file
sizeintegerFile size in bytes
content_typestringMIME type (nullable)
created_atintegerCreation timestamp (ms)
updated_atintegerLast update timestamp (ms)
scorefloatRelevance score (1.0 = exact match)
matched_byarrayList of field names that matched

Sorting

Sort results by one or more fields:

{
  "order_by": [
    {"field": "name", "direction": "asc"},
    {"field": "created_at", "direction": "desc"}
  ]
}
DirectionDescription
ascAscending (default)
descDescending

Pagination

Offset-Based

{
  "limit": 20,
  "offset": 40
}

Cursor-Based

Use after or before with cursor values from a previous response:

{
  "limit": 20,
  "after": "eyJwYXRoIjoiL3VzZXJzL2JvYi5qc29uIn0="
}

Projection (select)

Return only specific fields in each result. Use @-prefixed names for built-in metadata fields:

{
  "select": ["@path", "@score", "name", "email"]
}
Virtual FieldMaps To
@pathpath
@scorescore
@sizesize
@content_typecontent_type
@created_atcreated_at
@updated_atupdated_at
@matched_bymatched_by

Envelope fields (has_more, next_cursor, total_count, meta) are never stripped by projection.


Aggregations

Run aggregate computations instead of returning individual results.

Request

{
  "path": "/orders",
  "where": {"field": "status", "op": "eq", "value": "complete"},
  "aggregate": {
    "count": true,
    "sum": ["total", "tax"],
    "avg": ["total"],
    "min": ["total"],
    "max": ["total"],
    "group_by": ["status"]
  }
}
FieldTypeDescription
countbooleanInclude a count of matching records
sumarrayFields to sum
avgarrayFields to average
minarrayFields to find minimum
maxarrayFields to find maximum
group_byarrayFields to group results by

Response

The response shape depends on whether group_by is used. Aggregation results are returned as a JSON object.


Explain Mode

Inspect the query execution plan without running the full query. Useful for debugging index usage and performance.

{
  "path": "/users",
  "where": {"field": "age", "op": "gt", "value": 21},
  "explain": "plan"
}
ValueDescription
true or "plan"Show the query plan
"analyze"Execute the query and include timing information

Virtual Fields

Virtual fields let you query file metadata directly – without defining any indexes. Prefix a field name with @ to query against built-in file attributes instead of indexed document fields.

Available Virtual Fields

FieldTypeDescription
@pathstringFull file path (e.g., /docs/report.pdf)
@filenamestringFilename only – the last segment of the path (e.g., report.pdf)
@extensionstringFile extension after the last . (e.g., pdf)
@content_typestringMIME type (e.g., application/pdf)
@sizeu64File size in bytes
@created_ati64Creation timestamp in milliseconds
@updated_ati64Last update timestamp in milliseconds

Supported Operators

String virtual fields (@path, @filename, @extension, @content_type) support:

eq, contains, in, gt, lt, similar (trigram), fuzzy (edit distance), phonetic (soundex/metaphone), match (fused multi-strategy)

Numeric virtual fields (@size, @created_at, @updated_at) support:

eq, gt, lt, between, in

How Virtual Fields Work

Virtual fields scan all files under the query path and evaluate each file’s metadata against the filter conditions. This means:

  • No index configuration required. Virtual field queries work immediately with zero setup.
  • Slower than indexed queries. Because every file is scanned, virtual field queries are O(n) where n is the number of files under the path. For small-to-medium datasets this is perfectly fine. For large datasets with millions of files, prefer indexed fields for hot query paths.
  • Combinable with indexed fields. You can mix virtual fields and indexed fields in the same where clause using boolean combinators (and, or, not).

Virtual Field Examples

Find all PDFs by extension:

curl -X POST http://localhost:6830/files/query \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "path": "/",
    "where": {"field": "@extension", "op": "eq", "value": "pdf"}
  }'

Fuzzy filename search (handles typos):

curl -X POST http://localhost:6830/files/query \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "path": "/docs",
    "where": {"field": "@filename", "op": "similar", "value": "report", "threshold": 0.3}
  }'

Find large files over 10 MB:

curl -X POST http://localhost:6830/files/query \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "path": "/",
    "where": {"field": "@size", "op": "gt", "value": 10485760}
  }'

Find images by content type:

curl -X POST http://localhost:6830/files/query \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "path": "/",
    "where": {"field": "@content_type", "op": "contains", "value": "image/"}
  }'

Combine multiple virtual fields – large PDFs:

curl -X POST http://localhost:6830/files/query \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "path": "/",
    "where": {
      "and": [
        {"field": "@extension", "op": "eq", "value": "pdf"},
        {"field": "@size", "op": "gt", "value": 1048576}
      ]
    }
  }'

Search across all indexed directories in the database.

POST /files/search

FieldTypeRequiredDescription
querystringNoBroad search — searched against all trigram, phonetic, soundex, and dmetaphone indexed fields
whereobjectNoStructured query filter (same syntax as /files/query)
pathstringNoScope search to a subtree (default: / = everything)
limitintegerNoMax results (default: 50, max: 1000)
offsetintegerNoSkip results

At least one of query or where is required.

Discovers all directories with fuzzy-capable indexes (trigram, phonetic, soundex, dmetaphone) and searches the term against every matching field:

curl -X POST http://localhost:6830/files/search \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query": "alice", "limit": 20}'

Same where clause syntax as /files/query, but searches across all directories that have the requested field indexed:

curl -X POST http://localhost:6830/files/search \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"where": {"field": "@size", "op": "gt", "value": 1048576}}'

Combined

Broad search filtered by structured conditions:

curl -X POST http://localhost:6830/files/search \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query": "report", "where": {"field": "@extension", "op": "eq", "value": "pdf"}}'

Response

Same format as /files/query, plus a source field per result indicating which directory’s index matched:

{
  "results": [
    {
      "path": "/users/alice.json",
      "score": 0.95,
      "matched_by": ["@filename"],
      "source": "/",
      "size": 256,
      "content_type": "application/json",
      "created_at": 1775968398000,
      "updated_at": 1775968398000
    }
  ],
  "has_more": false,
  "total_count": 1
}

Examples

Simple equality query

curl -X POST http://localhost:6830/files/query \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "path": "/users",
    "where": {"field": "status", "op": "eq", "value": "active"},
    "limit": 10
  }'

Fuzzy name search with pagination

curl -X POST http://localhost:6830/files/query \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "path": "/users",
    "where": {"field": "name", "op": "similar", "value": "alice", "threshold": 0.4},
    "limit": 20,
    "order_by": [{"field": "name", "direction": "asc"}],
    "include_total": true
  }'

Complex boolean query

curl -X POST http://localhost:6830/files/query \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "path": "/products",
    "where": {
      "and": [
        {"field": "price", "op": "between", "value": 10, "value2": 100},
        {
          "or": [
            {"field": "category", "op": "eq", "value": "electronics"},
            {"field": "category", "op": "eq", "value": "books"}
          ]
        },
        {"not": {"field": "status", "op": "eq", "value": "discontinued"}}
      ]
    },
    "order_by": [{"field": "price", "direction": "asc"}],
    "limit": 50
  }'

Aggregation with grouping

curl -X POST http://localhost:6830/files/query \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "path": "/orders",
    "where": {"field": "year", "op": "eq", "value": 2026},
    "aggregate": {
      "count": true,
      "sum": ["total"],
      "avg": ["total"],
      "group_by": ["status"]
    }
  }'

Error Responses

StatusCondition
400Invalid query structure, missing field/op, unsupported operation, range query on non-range converter
404Query path or index not found
500Internal query execution failure