Skip to content

Query API

POST /neowiki/v0/query/cypher runs a read-only Cypher query against the configured Neo4j backend and returns results as a structured JSON envelope.

Stability

Pre-1.0. The endpoint, request shape, response envelope, and error contract may change without notice until 1.0. Do not treat /neowiki/v0/query/cypher as stable for third-party integrations yet.

Discovery flow

Before writing a query, a client or LLM should learn the data model. The recommended sequence:

  1. GET /neowiki/v0/schemas — Returns a list of available Schema names (e.g. Person, Company, Document).
  2. GET /neowiki/v0/schema/{name} — Returns the full Property Definitions for one Schema: property names, types, and constraints. This tells you which node properties exist on :Subject:SchemaName nodes and what values they hold.
  3. (Optional) GET /neowiki/v0/subject/{id} — Fetch a live Subject to see the actual data shape, including any Statements that differ from the Schema defaults.
  4. Read the Graph Model — Describes the Neo4j label and relationship structure in full. Essential for writing Cypher beyond simple MATCH (s:Subject) patterns: Page nodes, HasSubject relationships, typed Subject-to-Subject relationships, and available node properties.
  5. POST /neowiki/v0/query/cypher — Run the query.

Endpoint contract

Request

POST /rest.php/neowiki/v0/query/cypher
Content-Type: application/json
json
{
    "cypher": "MATCH (s:Subject:Person) WHERE s.`Birth year` > $minYear RETURN s.name AS name, s.`Birth year` AS year",
    "parameters": { "minYear": 2000 }
}
FieldTypeRequiredDescription
cypherstringYesA read-only Cypher query. Single statement. Queries with write keywords cause a writeQueryRejected error.
parametersobjectNoParameter name → value map. Reference as $name in the query. Defaults to {}.

Successful response (200)

json
{
    "columns": ["name", "year"],
    "rows": [
        { "name": "Alice Fontaine", "year": 2001 },
        { "name": "Ben Markov",     "year": 2003 }
    ],
    "truncated": false,
    "resultCount": 2,
    "durationMs": 14
}
FieldTypeDescription
columnsarray of stringRETURN aliases in declaration order. Separate from rows because JSON object key order is not guaranteed.
rowsarray of objectEach row keyed by RETURN alias.
truncatedbooleantrue when the result was cut at maxRows. Narrow the query or use the expensive tier for more rows.
resultCountintegerNumber of rows returned (always ≤ maxRows).
durationMsintegerServer-measured query execution time in milliseconds, excluding network round-trip.

Cell normalization

Scalar Cypher values (strings, integers, floats, booleans, null) pass through as-is. Complex types surface as:

Cypher typeJSON shape
Node{ "id": ..., "labels": [...], "properties": {...} }
Relationship{ "id": ..., "type": "...", "startNodeId": ..., "endNodeId": ..., "properties": {...} }
UnboundRelationship{ "id": ..., "type": "...", "properties": {...} } (no start/end node ids; appears in undirected pattern matches)
Path{ "nodes": [...], "relationships": [...] }

Temporal and spatial values (e.g. datetime(), point()) are not supported. Cast to a scalar in the query: toString(s.creationTime), point.x(p).

Error response

json
{
    "errorType": "queryTimeout",
    "message": "Query exceeded the 30 second timeout."
}
errorTypeHTTPWhen
emptyQuery400cypher is empty or contains only whitespace.
parameterMissing400Query references $x but parameters contains no key x.
cypherSyntaxError400Neo4j reports a parse or syntax error for the query.
writeQueryRejected422Query contains a write keyword or otherwise fails the read-only validator.
queryTimeout408Query execution exceeded timeoutSeconds for the caller's tier.
rateLimitExceeded429Request frequency exceeded the neowiki-query rate limit.
permissionDenied403Caller lacks the neowiki-query right.
backendUnavailable503No graph backend is reachable (graceful degradation, see ADR 19).
internalError500Anything else.

errorType strings are stable across releases. Clients and LLMs should branch on errorType, not on message text — message is translated and may change.

Example walkthrough

This example follows the full discovery flow for a wiki that has a Person schema, then queries for all people born after the year 2000.

Step 1 — List available schemas

bash
curl http://localhost:8484/rest.php/neowiki/v0/schemas

Response (abbreviated):

json
[
    { "name": "Company" },
    { "name": "Document" },
    { "name": "Person" }
]

Step 2 — Fetch the Person schema

bash
curl http://localhost:8484/rest.php/neowiki/v0/schema/Person

Response (abbreviated):

json
{
    "name": "Person",
    "description": "A human being.",
    "properties": [
        { "name": "Birth year", "type": "number", "required": false },
        { "name": "Nationality", "type": "select", "required": false },
        { "name": "Employer",   "type": "relation", "required": false }
    ]
}

From this you know: the Neo4j node for a Person has a Birth year numeric property, a Nationality string property, and outgoing Employer relationships to other Subject nodes.

Step 3 — (Optional) Sample a subject

bash
curl http://localhost:8484/rest.php/neowiki/v0/subject/s1abc5def6ghi78

Step 4 — Run the query

Using the property name discovered in Step 2 (Birth year). Property names with spaces must be backtick-escaped in Cypher.

bash
curl -X POST http://localhost:8484/rest.php/neowiki/v0/query/cypher \
     -H 'Content-Type: application/json' \
     -d '{
       "cypher": "MATCH (s:Subject:Person) WHERE s.`Birth year` > $minYear RETURN s.name, s.`Birth year` AS year",
       "parameters": { "minYear": 2000 }
     }'

Response:

json
{
    "columns":     ["name", "year"],
    "rows":        [
        { "name": "Alice Fontaine", "year": 2001 },
        { "name": "Ben Markov",     "year": 2003 }
    ],
    "truncated":   false,
    "resultCount": 2,
    "durationMs":  11
}

Always prefer parameterized queries ($name syntax) over string concatenation. Parameters are passed safely to the database driver and protect against injection.

Limits and tiers

Two resource tiers control per-query timeout and row cap:

TierWhoTimeoutRow cap
defaultAny user with neowiki-query30 s5,000 rows
expensiveAny user with the core apihighlimits right300 s50,000 rows

apihighlimits is a MediaWiki core right granted by default to bot and sysop groups. It exists precisely for "heavier API queries" and is reused rather than introducing a NeoWiki-specific right.

When truncated is true, the result was cut at maxRows. Narrow the query with WHERE clauses, add LIMIT to the Cypher, or use an account with apihighlimits for a higher cap.

Overriding limits

php
// LocalSettings.php
$wgNeoWikiQueryLimits = [
    'default'   => [ 'timeoutSeconds' => 15,   'maxRows' => 1000  ],
    'expensive' => [ 'timeoutSeconds' => 120,  'maxRows' => 20000 ],
];

Both keys must be present. The values above are examples; tune for your deployment's hardware and expected query patterns.

Rate limits

Request frequency is controlled via MediaWiki's standard $wgRateLimits mechanism, keyed as neowiki-query. Defaults shipped by NeoWiki:

Caller typeDefault limit
Anonymous10 requests / 60 s
Logged-in user60 requests / 60 s
Bot1000 requests / 60 s
Sysop / BureaucratUnlimited (via core noratelimit right)

Other accounts can be exempted from rate limits by granting them the core noratelimit right.

Override for your site:

php
// LocalSettings.php — example: tighten anonymous access, raise for logged-in users
$wgRateLimits['neowiki-query'] = [
    'anon' => [ 5, 60 ],
    'user' => [ 120, 60 ],
];

Permissions

RightDefault groupsPurpose
neowiki-query* (everyone, including anonymous visitors)Required to call the query endpoint.
apihighlimitsbot, sysop (core defaults)Grants the expensive resource tier.

To restrict the endpoint on a closed wiki:

php
// LocalSettings.php
$wgGroupPermissions['*']['neowiki-query'] = false;
$wgGroupPermissions['user']['neowiki-query'] = true;  // logged-in users only

Or remove it from all groups and grant it selectively:

php
$wgGroupPermissions['*']['neowiki-query'] = false;
$wgGroupPermissions['researcher']['neowiki-query'] = true;

Deployment notes

NeoWiki enforces a transaction timeout in-process via Neo4jQueryLimits::$timeoutSeconds. This is the primary protection against long-running queries. For defense-in-depth, configure Neo4j itself with matching server-side limits so that a bug or misconfiguration in the application layer does not leave the database exposed:

ini
# neo4j.conf
db.transaction.timeout=60s
db.memory.transaction.max=512m

The db.transaction.timeout value should be slightly above the expensive tier timeout (300 s by default) so that legitimate long queries are not killed server-side before the application timeout fires, but runaway queries that bypass the application layer are still bounded. A value such as 360s gives a 60-second grace margin. db.memory.transaction.max caps per-transaction heap to prevent a single query from exhausting server memory.

These settings apply to all connections to the Neo4j instance, not only NeoWiki traffic. Adjust to your deployment's needs.