Query API
The query engine supports indexed field queries with boolean combinators, pagination, sorting, aggregations, projections, and an explain mode.
Endpoint Summary
| Method | Path | Description | Auth | Status Codes |
|---|---|---|---|---|
| POST | /files/query | Execute a query | Yes | 200, 400, 404, 500 |
| POST | /files/search | Global cross-directory search | Yes | 200, 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"],
"include_matches": false,
"max_matches_per_result": 5,
"snippet_chars": 160,
"match_context_lines": 2,
"max_locator_scan_bytes": 268435456,
"explain": false
}
| Field | Type | Required | Description |
|---|---|---|---|
path | string | Yes | Directory path to query within |
where | object/array | Yes | Query filter (see below) |
limit | integer | No | Max results to return (server default applies if omitted) |
offset | integer | No | Skip this many results |
order_by | array | No | Sort fields with direction |
after | string | No | Cursor for forward pagination |
before | string | No | Cursor for backward pagination |
include_total | boolean | No | Include total_count in response (default: false) |
select | array | No | Project specific fields in results |
aggregate | object | No | Run aggregations instead of returning results |
include_matches | boolean | No | Include request-time hit locators for returned results (default: false) |
max_matches_per_result | integer | No | Maximum hit locators per result (default: 5, max: 50) |
snippet_chars | integer | No | Maximum snippet characters per locator (default: 160, max: 4096) |
match_context_lines | integer | No | Context line count for stored-file line fetch hints |
max_locator_scan_bytes | integer | No | Caller cap for stored-file locator scans; server clamps to its hard cap |
explain | string/boolean | No | "plan", "analyze", or true for query plan |
Hit locators are opt-in. They are generated only for the current page after authorization filtering, not during index lookup.
Query Operators
Each field query is an object with field, op, and value:
{"field": "age", "op": "gt", "value": 21}
Comparison Operators
| Operator | Description | Value Type | Example |
|---|---|---|---|
eq | Exact match | any | {"field": "status", "op": "eq", "value": "active"} |
gt | Greater than | number/string | {"field": "age", "op": "gt", "value": 21} |
lt | Less than | number/string | {"field": "age", "op": "lt", "value": 65} |
between | Inclusive range | number/string | {"field": "age", "op": "between", "value": 21, "value2": 65} |
in | Match any value in a set | array | {"field": "status", "op": "in", "value": ["active", "pending"]} |
Text Search Operators
These operators require the appropriate index type to be configured.
| Operator | Description | Index Required | Example |
|---|---|---|---|
contains | Substring match | trigram | {"field": "name", "op": "contains", "value": "alice"} |
similar | Fuzzy trigram match with threshold | trigram | {"field": "name", "op": "similar", "value": "alice", "threshold": 0.3} |
phonetic | Sounds-like match | phonetic | {"field": "name", "op": "phonetic", "value": "smith"} |
fuzzy | Configurable fuzzy match | trigram | See below |
match | Multi-strategy combined match | trigram + 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"
}
| Parameter | Values | Default |
|---|---|---|
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
}
| Parameter | Type | Default | Description |
|---|---|---|---|
threshold | float | 0.3 | Minimum 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
{
"items": [
{
"path": "/users/alice.json",
"size": 256,
"content_type": "application/json",
"created_at": 1775968398000,
"updated_at": 1775968398000,
"score": 1.0,
"matched_by": ["name"]
}
],
"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
}
}
| Field | Type | Description |
|---|---|---|
items | array | Matching file metadata with scores |
has_more | boolean | Whether more results exist beyond the current page |
total_count | integer | Total matching results (only if include_total: true) |
next_cursor | string | Cursor for the next page (if has_more is true) |
prev_cursor | string | Cursor for the previous page |
default_limit_hit | boolean | Present and true when the server’s default limit was applied |
default_limit | integer | The server’s default limit value (present with default_limit_hit) |
meta | object | Reindex progress metadata (present only during active reindex) |
Result Fields
Each result object contains:
| Field | Type | Description |
|---|---|---|
path | string | Full path to the matched file |
size | integer | File size in bytes |
content_type | string | MIME type (nullable) |
created_at | integer | Creation timestamp (ms) |
updated_at | integer | Last update timestamp (ms) |
score | float | Relevance score (1.0 = exact match) |
matched_by | array | List of field names that matched |
content_hash | string | Whole-file content hash, present when include_matches is true and the record has a hash |
matches | array | Hit locators, present when include_matches is true |
matches_truncated | boolean | True if more locators were available than returned |
locator_status | string | complete, partial, or unsupported |
Hit Locators
When include_matches is true, /files/query and /files/search add bounded hit locators to each returned result. This is designed for agents that need to search first and then fetch only relevant ranges.
For JSON files, matching indexed fields are reported as field-value locators with JSON Pointer fetch hints:
{
"path": "/users/alice.json",
"content_hash": "b3c1...",
"matches": [
{
"id": "m_0001",
"query": "Alice",
"matched_text": "Alice",
"field": "name",
"operator": "eq",
"source": {
"type": "field-value",
"field": "name",
"json_pointer": "/name",
"value_type": "string"
},
"range": {
"char": { "start": 0, "end": 5, "unit": "unicode-scalar", "basis": "field-value" }
},
"fetch": {
"json_pointer": "/name",
"preferred": "json_pointer"
},
"snippet": {
"text": "Alice",
"highlight": [{ "start": 0, "end": 5, "unit": "unicode-scalar" }],
"truncated_before": false,
"truncated_after": false
},
"confidence": "exact",
"scan_status": "complete"
}
],
"matches_truncated": false,
"locator_status": "complete"
}
Metadata matches such as @filename use source.type = "metadata". Plain-text or unstructured UTF-8 file matches use source.type = "stored-file" and include byte, character, line, and column ranges plus byte_range and line_range fetch hints.
Use POST /files/fetch range mode to fetch the returned ranges. Pass if_content_hash or if_updated_at from the search result to avoid fetching a stale range after the file changes.
Sorting
Sort results by one or more fields:
{
"order_by": [
{"field": "name", "direction": "asc"},
{"field": "created_at", "direction": "desc"}
]
}
| Direction | Description |
|---|---|
asc | Ascending (default) |
desc | Descending |
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 Field | Maps To |
|---|---|
@path | path |
@score | score |
@size | size |
@content_type | content_type |
@created_at | created_at |
@updated_at | updated_at |
@matched_by | matched_by |
@content_hash | content_hash |
@matches | matches |
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"]
}
}
| Field | Type | Description |
|---|---|---|
count | boolean | Include a count of matching records |
sum | array | Fields to sum |
avg | array | Fields to average |
min | array | Fields to find minimum |
max | array | Fields to find maximum |
group_by | array | Fields 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"
}
| Value | Description |
|---|---|
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. Prefix a field name with @ to query against built-in file attributes instead of indexed document fields.
AeorDB uses virtual-field indexes when they are configured. New databases bootstrap default virtual-field indexes at /.aeordb-config/indexes.json; older databases can add the same config and run a forced reindex to backfill them. If no index exists for a supported virtual field, AeorDB falls back to scanning FileRecord metadata for compatibility.
Available Virtual Fields
| Field | Type | Description |
|---|---|---|
@path | string | Full file path (e.g., /docs/report.pdf) |
@filename | string | Filename only – the last segment of the path (e.g., report.pdf) |
@file_name | string | Alias for @filename; queries use the canonical @filename index |
@extension | string | File extension after the last . (e.g., pdf) |
@content_type | string | MIME type (e.g., application/pdf) |
@hash | string | Raw whole-file content hash (blake3(file bytes)) |
@size | u64 | File size in bytes |
@created_at | i64 | Creation timestamp in milliseconds |
@updated_at | i64 | Last update timestamp in milliseconds |
Supported Operators
String virtual fields (@path, @filename, @file_name, @extension, @content_type, @hash) 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 are derived from the FileRecord header and payload. Query execution is index-first:
- Default indexes. New databases index every built-in virtual field for exact lookup by default.
@filenamealso gets trigram and phonetic indexes forcontains,similar,fuzzy,phonetic, andmatch. - Ancestor glob indexes. If a query is scoped below a glob-indexed directory, AeorDB can use the ancestor index and filter candidates back down to the requested path.
- Scan fallback. If a database does not have a matching virtual-field index, AeorDB scans FileRecords under the query path so legacy queries continue to work. This fallback is O(n) over files under the path.
- Combinable with indexed fields. You can mix virtual fields and indexed fields in the same
whereclause using boolean combinators (and,or,not). - Canonical aliases.
@file_nameis accepted as an alias but is indexed and queried as@filename.
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}
]
}
}'
Global Search
Search across all indexed directories in the database.
POST /files/search
| Field | Type | Required | Description |
|---|---|---|---|
query | string | No | Broad search — searched against all trigram, phonetic, soundex, and dmetaphone indexed fields |
where | object | No | Structured query filter (same syntax as /files/query) |
path | string | No | Scope search to a subtree (default: / = everything) |
limit | integer | No | Max results (default: 50, max: 1000) |
offset | integer | No | Skip results |
include_matches | boolean | No | Include request-time hit locators for returned results (default: false) |
max_matches_per_result | integer | No | Maximum hit locators per result (default: 5, max: 50) |
snippet_chars | integer | No | Maximum snippet characters per locator (default: 160, max: 4096) |
match_context_lines | integer | No | Context line count for stored-file line fetch hints |
max_locator_scan_bytes | integer | No | Caller cap for stored-file locator scans; server clamps to its hard cap |
At least one of query or where is required.
Broad Search
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}'
Structured Search
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. The locator fields below are present only when include_matches is true:
{
"results": [
{
"path": "/users/alice.json",
"score": 0.95,
"matched_by": ["@filename"],
"source": "/",
"size": 256,
"content_type": "application/json",
"created_at": 1775968398000,
"updated_at": 1775968398000,
"content_hash": "b3c1...",
"matches": [],
"matches_truncated": false,
"locator_status": "complete"
}
],
"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
| Status | Condition |
|---|---|
| 400 | Invalid query structure, missing field/op, unsupported operation, range query on non-range converter |
| 404 | Query path or index not found |
| 500 | Internal query execution failure |