search โ entity search API
cyoda-go version 0.7.1
search โ entity search API: synchronous direct search, asynchronous snapshot search, and entity statistics.
SYNOPSIS
Section titled โSYNOPSISโPOST /api/search/direct/{entityName}/{modelVersion}POST /api/search/async/{entityName}/{modelVersion}GET /api/search/async/{jobId}GET /api/search/async/{jobId}/statusPUT /api/search/async/{jobId}/cancelContext path prefix is CYODA_CONTEXT_PATH (default /api). All endpoints require Authorization: Bearer <token> except when CYODA_IAM_MODE=mock.
DESCRIPTION
Section titled โDESCRIPTIONโSearch operates against a specific entity model (entityName, modelVersion). Two modes are supported:
Synchronous (direct) search: POST /search/direct/{entityName}/{modelVersion}. Executes inline within the HTTP request. The response is an NDJSON stream (application/x-ndjson), one entity envelope per line. The default result limit is 1000 entities per request; the maximum is 10000 (values above 10000 are clamped to 10000).
Asynchronous search: POST /search/async/{entityName}/{modelVersion}. Submits a search job and returns a job UUID immediately. The search executes in a background goroutine (or in the pluginโs own executor for SelfExecutingSearchStore plugins). Results are retrieved by polling status and then fetching pages.
Both modes accept the same Condition DSL as the request body. When the storage plugin implements spi.Searcher, the condition is translated to a plugin-level predicate and pushed down to the backend. When translation fails (unsupported condition type) or an active transaction is present, the service falls back to in-memory filtering after a full GetAll scan.
CONDITION DSL
Section titled โCONDITION DSLโAll search requests accept a Condition JSON document as the POST body. Conditions are parsed recursively up to a maximum nesting depth of 50. Body size limit: 10 MiB.
SimpleCondition โ match a single JSON path against a scalar value:
{ "type": "simple", "jsonPath": "$.category", "operatorType": "EQUALS", "value": "physics"}type:"simple"jsonPath: JSONPath string (e.g.,"$.year","$.laureates[0].firstname")operatorType(also accepted asoperatororoperation): operator string (see valid values below)value: any JSON scalar
Valid operatorType values (exhaustive):
EQUALSโ exact equality; numeric-aware (JSON number vs string representation)NOT_EQUALโ inequality; inverse of EQUALSGREATER_THANโ numeric or lexicographic greater-thanLESS_THANโ numeric or lexicographic less-thanGREATER_OR_EQUALโ greater-than or equalLESS_OR_EQUALโ less-than or equalCONTAINSโ substring or array-element containmentNOT_CONTAINSโ inverse of CONTAINSSTARTS_WITHโ string prefix matchNOT_STARTS_WITHโ inverse of STARTS_WITHENDS_WITHโ string suffix matchNOT_ENDS_WITHโ inverse of ENDS_WITHLIKEโ SQL-style LIKE pattern (%= any sequence,_= any single char)IS_NULLโ field is absent or JSON nullNOT_NULLโ field is present and not JSON nullBETWEENโ range check (exclusive bounds);valuemust be a two-element array[low, high]BETWEEN_INCLUSIVEโ range check (inclusive bounds); samevalueshape as BETWEENMATCHES_PATTERNโ regular expression matchIEQUALSโ case-insensitive EQUALSINOT_EQUALโ case-insensitive NOT_EQUALICONTAINSโ case-insensitive CONTAINSINOT_CONTAINSโ case-insensitive NOT CONTAINSISTARTS_WITHโ case-insensitive STARTS_WITHINOT_STARTS_WITHโ case-insensitive NOT STARTS_WITHIENDS_WITHโ case-insensitive ENDS_WITHINOT_ENDS_WITHโ case-insensitive NOT ENDS_WITH
Operator strings outside this list are rejected with errors.BAD_REQUEST at request time; the error detail includes the canonical list.
LifecycleCondition โ match entity lifecycle metadata:
{ "type": "lifecycle", "field": "state", "operatorType": "EQUALS", "value": "APPROVED"}type:"lifecycle"field:"state","creationDate", or"previousTransition"operatorType(also accepted asoperatororoperation): operator string โ same valid values as forSimpleConditionvalue: any JSON scalar
GroupCondition โ combine conditions with a logical operator:
{ "type": "group", "operator": "AND", "conditions": [ { "type": "simple", "jsonPath": "$.year", "operatorType": "EQUALS", "value": "2024" }, { "type": "lifecycle", "field": "state", "operatorType": "EQUALS", "value": "NEW" } ]}type:"group"operator:"AND"or"OR"โ these are the only supported values; any other string produceserrors.BAD_REQUESTat match time (โunknown group operatorโ)conditions: array ofConditionobjects (recursive; maximum nesting depth 50)
"NOT" is not supported. An AND group with an empty conditions array evaluates to true (vacuous conjunction). An OR group with an empty conditions array evaluates to false (vacuous disjunction).
EMPTY CONDITION: Submitting an empty body ({}) or a body with no type field as the top-level search condition is rejected with errors.BAD_REQUEST โ the parser requires a valid type field. Submitting a valid AND group with an empty conditions array ({"type":"group","operator":"AND","conditions":[]}) is accepted and matches all entities โ this is the correct way to retrieve all entities without filtering.
ArrayCondition โ match positional values in a JSON array:
{ "type": "array", "jsonPath": "$.laureates", "values": ["John", null, "Hopfield"]}type:"array"jsonPath: path to the array fieldvalues: positional values;nullentries match any value at that index
FunctionCondition โ server-side function predicate dispatched to a compute member:
{ "type": "function", "function": { "name": "my-criteria-fn", "config": { "calculationNodesTags": "approval-service", "attachEntity": true, "responseTimeoutMs": 30000 } }}type:"function"function.name: string โ identifies the function; becomescriteriaId/criteriaNamein the dispatch request; required for routingfunction.config.calculationNodesTags: string โ comma-separated tags used to select a registered compute member; follows the same tag-intersection rules as processor dispatchfunction.config.attachEntity: boolean (optional, defaulttrue) โ whentrue, the full entity payload is included in the dispatch requestfunction.config.responseTimeoutMs: int64 (optional, default30000) โ timeout in milliseconds
The function is dispatched as EntityCriteriaCalculationRequest to the matching compute member โ see the grpc topic for the request/response shape. FunctionCondition cannot be translated to a storage-plugin pushdown filter; it always executes as a post-filter with in-memory entity loading.
ENDPOINTS
Section titled โENDPOINTSโPOST /api/search/direct/{entityName}/{modelVersion} โ Synchronous search
entityName(path): stringmodelVersion(path): int32pointInTime(query, optional): RFC 3339 date-time โ search against entity state at this instantlimit(query, optional): string-encoded integer, clamped to maximum 10000; default 1000
Request body: Condition JSON document.
Response: 200 OK, Content-Type: application/x-ndjson.
Each line is a complete entity envelope JSON object:
{"type":"ENTITY","data":{"category":"physics","year":"2024"},"meta":{"id":"74807f00-ed0d-11ee-a357-ae468cd3ed16","state":"NEW","creationDate":"2025-08-01T10:00:00.000000000Z","lastUpdateTime":"2025-08-01T10:00:00.000000000Z"}}{"type":"ENTITY","data":{"category":"chemistry","year":"2023"},"meta":{"id":"89abc100-ed0d-11ee-a357-ae468cd3ed16","state":"APPROVED","creationDate":"2025-07-15T09:00:00.000000000Z","lastUpdateTime":"2025-07-20T14:00:00.000000000Z"}}The stream is truncated on encode failure after the header has been sent; the client detects truncation via a connection error or incomplete last line.
POST /api/search/async/{entityName}/{modelVersion} โ Submit async search job
entityName(path): stringmodelVersion(path): int32pointInTime(query, optional): RFC 3339 โ if not provided, the current time is captured at submission
Request body: Condition JSON document.
Response: 200 OK, application/json โ bare UUID string (job ID):
"a1b2c3d4-e5f6-11ee-9e63-ae468cd3ed16"The job is stored with status RUNNING. For non-SelfExecutingSearchStore backends, a goroutine begins the search immediately using a background context derived from the submitting userโs tenant context.
GET /api/search/async/{jobId}/status โ Get async job status
jobId(path): UUID
Response: 200 OK, application/json:
{ "searchJobStatus": "SUCCESSFUL", "createTime": "2025-08-01T10:00:00.000000000Z", "entitiesCount": 42, "calculationTimeMillis": 145, "finishTime": "2025-08-01T10:00:00.145000000Z", "expirationDate": "2025-08-02T10:00:00.000000000Z"}searchJobStatus:"RUNNING","SUCCESSFUL","FAILED", or"CANCELLED"createTime: RFC 3339 with nanosecondsentitiesCount: total matching entities (0 while running)calculationTimeMillis: elapsed search time in millisecondsfinishTime: RFC 3339 with nanoseconds; absent when status isRUNNINGexpirationDate:createTime + 24hโ job results expire after this time
GET /api/search/async/{jobId} โ Retrieve async job results (paginated)
jobId(path): UUIDpageSize(query, optional): string-encoded integer, default1000pageNumber(query, optional): string-encoded integer, default0; offset =pageNumber * pageSize
The job must be in SUCCESSFUL status. Returns 400 BAD_REQUEST if the job is not yet complete.
Response: 200 OK, application/json:
{ "content": [ { "type": "ENTITY", "data": { "category": "physics", "year": "2024" }, "meta": { "id": "74807f00-ed0d-11ee-a357-ae468cd3ed16", "state": "NEW", "creationDate": "2025-08-01T10:00:00.000000000Z", "lastUpdateTime": "2025-08-01T10:00:00.000000000Z" } } ], "page": { "number": 0, "size": 1000, "totalElements": 42, "totalPages": 1 }}Results are fetched from the stored entity snapshots at the jobโs pointInTime. Entities deleted or modified after submission are returned as they existed at submission time.
PUT /api/search/async/{jobId}/cancel โ Cancel a running async job
jobId(path): UUID
Cancellation succeeds only when the job status is RUNNING. If the job has already reached a terminal state (SUCCESSFUL, FAILED, or CANCELLED), the server returns 400 Bad Request:
{ "detail": "snapshot by id=<jobId> is not running. current status=SUCCESSFUL", "properties": { "currentStatus": "SUCCESSFUL", "snapshotId": "<jobId>" }, "status": 400, "title": "Bad Request", "type": "about:blank"}On successful cancellation, response: 200 OK, application/json:
{ "isCancelled": true, "cancelled": true, "currentSearchJobStatus": "CANCELLED"}PAGINATION
Section titled โPAGINATIONโAsync search results use page-number pagination: pageNumber=0 is the first page, offset = pageNumber * pageSize. pageNumber and pageSize are both string-encoded integers in query parameters.
Synchronous search does not paginate; use the limit parameter (max 10000) to bound results. For large datasets, use async search with page retrieval.
errors.SEARCH_JOB_NOT_FOUNDโ404โ async job UUID does not exist.errors.SEARCH_JOB_ALREADY_TERMINALโ400โ cancel attempted on a job that is alreadySUCCESSFUL,FAILED, orCANCELLED; error code in response isBAD_REQUESTerrors.SEARCH_RESULT_LIMITโ result set exceeds configured limiterrors.SEARCH_SHARD_TIMEOUTโ per-shard search timeout exceeded (relevant for distributed backends)errors.INVALID_FIELD_PATHโ400โ condition references one or more JSONPath field paths absent from the modelโs locked schema; the response detail names each offending patherrors.CONDITION_TYPE_MISMATCHโ400โ condition value type is incompatible with the target fieldโs locked DataTypeerrors.BAD_REQUESTโ400โ malformed condition JSON, invalid limit/pageSize/pageNumber, result retrieval on non-SUCCESSFUL job, unknown async job ID in result retrieval
EXAMPLES
Section titled โEXAMPLESโSynchronous search โ match by field value:
curl -s -X POST \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"type":"simple","jsonPath":"$.category","operatorType":"EQUALS","value":"physics"}' \ "http://localhost:8080/api/search/direct/nobel-prize/1"Synchronous search โ match by lifecycle state:
curl -s -X POST \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"type":"lifecycle","field":"state","operatorType":"EQUALS","value":"APPROVED"}' \ "http://localhost:8080/api/search/direct/nobel-prize/1"Synchronous search โ AND group:
curl -s -X POST \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "type": "group", "operator": "AND", "conditions": [ {"type":"simple","jsonPath":"$.year","operatorType":"EQUALS","value":"2024"}, {"type":"lifecycle","field":"state","operatorType":"EQUALS","value":"NEW"} ] }' \ "http://localhost:8080/api/search/direct/nobel-prize/1"Synchronous search at point in time with limit:
curl -s -X POST \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"type":"group","operator":"AND","conditions":[]}' \ "http://localhost:8080/api/search/direct/nobel-prize/1?pointInTime=2025-08-01T00:00:00Z&limit=100"Submit async search:
JOB_ID=$(curl -s -X POST \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"type":"simple","jsonPath":"$.year","operatorType":"EQUALS","value":"2024"}' \ "http://localhost:8080/api/search/async/nobel-prize/1" | tr -d '"')Poll async job status:
curl -s -H "Authorization: Bearer $TOKEN" \ "http://localhost:8080/api/search/async/$JOB_ID/status"Retrieve async results (page 0):
curl -s -H "Authorization: Bearer $TOKEN" \ "http://localhost:8080/api/search/async/$JOB_ID?pageNumber=0&pageSize=500"Cancel an async job:
curl -s -X PUT \ -H "Authorization: Bearer $TOKEN" \ "http://localhost:8080/api/search/async/$JOB_ID/cancel"SEE ALSO
Section titled โSEE ALSOโ- crud
- models
- analytics
- errors.SEARCH_JOB_NOT_FOUND
- errors.SEARCH_JOB_ALREADY_TERMINAL
- errors.SEARCH_RESULT_LIMIT
- errors.SEARCH_SHARD_TIMEOUT
- openapi
See also
Section titled โSee alsoโcyoda help crudโ Entities are instances of models. Each entity has a UUID, a model reference (entityName,modelVersion), and a lifecycle state managed by the workflow engine. Creating an entity requires the referenced model to be inLOCKEDstate. All write operations run within a Cyoda transaction and return atransactionIdalongside the affected entity IDs.cyoda help modelsโ A model is a named, versioned schema registered per tenant. Every entity in the system is an instance of exactly one model. Models are identified by(entityName, modelVersion). The model ID is a deterministic UUID v5 derived from that key:UUID.newSHA1(NameSpaceURL, "{entityName}.{modelVersion}").cyoda help analyticsโ Cyoda Cloud exposes entity data as Trino SQL tables through a Trino connector. The connector uses the Schema Management REST API to discover table definitions and the WebSocket (STOMP) messaging API to stream entity rows at query time.cyoda help errors SEARCH_JOB_NOT_FOUNDโ Polling a search job by ID returns this error when the job ID is unknown or belongs to a different tenant. Jobs are tenant-scoped; a valid job ID from one tenant is not visible to another.cyoda help errors SEARCH_JOB_ALREADY_TERMINALโ Search jobs are long-running asynchronous operations. Once a job reaches a terminal state it cannot be cancelled, resumed, or otherwise modified. This error is returned when such an operation is attempted on a finished job.cyoda help errors SEARCH_RESULT_LIMITโ The server imposes an upper bound on the number of results returned per page and per job to protect cluster resources. Returned when the request exceeds this limit โ either by requesting too large a page size or by the matched result count exceeding the cap.cyoda help errors SEARCH_SHARD_TIMEOUTโ Distributed search fans out to multiple shards in parallel. If any shard does not return results before the search timeout expires, the job is marked failed and this error is returned. Occurs under high load, during partial cluster degradation, or with expensive queries.cyoda help errors INVALID_FIELD_PATHโ Before executing a search, the server validates that every data-field path referenced by the condition (e.g.$.price,$.profile.email) resolves against the target modelโs locked schema. Lifecycle paths (state,previousTransition, etc.) and meta paths ($._meta.*) bypass this check.cyoda help errors CONDITION_TYPE_MISMATCHโ Each field in a locked model has an inferred DataType (e.g. INTEGER, DOUBLE, BOOLEAN). When a search condition references a numeric or boolean field, the conditionโs value must be type-compatible with that field. For example, submitting a string value"abc"against a DOUBLE field is rejected.cyoda help openapiโ cyoda-go generates its OpenAPI 3.1 specification from the embeddedapi/openapi.yamlfile compiled into the binary at build time. The spec is served at/openapi.jsonwith runtime-patched server URLs. The Scalar API Reference UI is served at/docsand loads the spec from/openapi.json.
Raw formats
Section titled โRaw formatsโ/help/search.jsonโ full descriptor (matchesGET /help/{topic}envelope)/help/search.mdโ body only