Skip to content

QIDO-RS — Search for Instances

Query based on ID for DICOM Objects (QIDO-RS) enables searching for studies and series by attributes (PS3.18 §10.6).

Method Path Description
GET /studies Search for studies
GET /studies/{study}/series Search for series in a study

The Accept header must allow application/dicom+json (application/*, */*, or no Accept header at all are accepted). Anything else is rejected with 406 (Not Acceptable).

Authorization

When viewer-JWT verification is enabled for the deployment, every request to this interface must carry Authorization: Bearer <jwt>, and results are restricted to the studies named in the token's scope. See JWT Authorization for the token format, scope semantics, and status codes.

Searchability: stored is not searchable

Search queries are served from search indexes maintained automatically by a background study-processing stage after instances are stored.

Newly stored studies become searchable once this processing has completed. After studies have been stored, they are asynchronously processed by the kastoria-health platform. Queries for studies that have not yet been processed return an empty array. A client that stores a study and immediately searches for it must expect this and retry after the processing stage has caught up.

Search parameters

Study level

Parameter Support Description
StudyInstanceUID Direct lookup Unique study identifier
PatientID Exact match, case-insensitive Patient identifier
StudyDate Single date or range Study date in YYYYMMDD format. Single date (20240101) or range (20240101-20240131, 20240101-, -20240131)
PatientName Leading components, case-insensitive Patient's name in DICOM PN form. The value names leading components and each matches whole: DOE matches DOE^JOHN, DOE^JOHN matches DOE^JOHN^Q but not DOE^JOHNSON. Matched against the alphabetic component group
StudyTime Single time or range Study time in DICOM TM format, HH[MM[SS[.FFFFFF]]]. Single time (143000) or range (080000-170000, 080000-, -170000)
AccessionNumber Exact match, case-insensitive Accession number
StudyDescription Exact match, case-insensitive Study description
ModalitiesInStudy List; matches any one modality, ignoring case Modality (e.g. CT, MR). A list separated by \ or , is an IN clause — the study matches when any of its modalities equals any listed value
limit Supported (default 100, maximum 1000) Maximum number of matches returned in one response
offset Supported (default 0, maximum 1000) Number of matches skipped before the first returned one. A value above the maximum is rejected with 400, not capped

Series level

Parameter Support Description
Modality List; matches any one, ignoring case Filter by modality. A list separated by \ or , is an IN clause
SeriesInstanceUID Exact match Filter by series UID (a UID has no case to ignore)
SeriesNumber Exact match Filter by series number
limit Supported (default 100, maximum 1000) Maximum number of matches returned in one response
offset Supported (default 0, maximum 1000) Number of matches skipped before the first returned one. A value above the maximum is rejected with 400, not capped

Attribute IDs

Query parameter keys may be encoded as either of the following (PS3.18 §8.3.4.1):

Value Example
{group}{element} 0020000D
{dicomKeyword} StudyInstanceUID

Matching semantics

Search type Supported attributes Example
Range query StudyDate, StudyTime StudyDate=20240101-20240131 — inclusive range. Either bound may be omitted (e.g. StudyDate=20240101- or StudyDate=-20240131)
Exact match StudyInstanceUID, PatientID, AccessionNumber, StudyDescription, ModalitiesInStudy PatientID=PATIENT001
Leading components PatientName PatientName=DOE matches DOE^JOHN — the value names leading PN components and each matches whole, so this is not an exact match on the full name

Notes:

  • A zero-length value is universal matching (PS3.4 §C.2.2.2.1): PatientName= places no constraint and every study matches. It is neither an error nor an ignored parameter, so it carries no Warning header field.
  • Matching ignores case for every attribute except StudyInstanceUID, which is a UID: aa-1234 and AA-1234 are one identifier, and chris and CHRIS are one person.
  • StudyTime values are compared by the instant they denote, so a query and a stored value written to different precision (090000 and 090000.000000) match.
  • The upper bound of a StudyTime range runs to the end of the unit it names, matching the way a StudyDate upper bound runs to the end of its day. -235959 includes a study acquired at 235959.500, and -0900 includes one at 090030. An explicit fraction names an instant instead, so -090030.000 ends exactly there.
  • A StudyTime naming the whole day — 000000-235959, 000000-, -235959, or a bare - — narrows nothing and is treated as absent rather than searched for.
  • Each query parameter must be supplied at most once. A repeated parameter — PatientID=A&PatientID=B — is rejected with 400 (Bad Request) naming the parameter.
  • Wildcard matching is not supported. Wildcard characters (*, ?) are not treated as wildcards in any search parameter. Fuzzy matching is not supported either.

Pagination

Both search transactions implement response pagination per PS3.18 §8.3.4.4.

Parameter Default Behavior
offset 0 Number of matches skipped before the first returned match; an explicit value above 1000 is rejected with 400
limit 100 Maximum matches returned in one response; an explicit value above 1000 is capped

maxResults (PS3.18 §8.3.4.4.1) is 1000 — the ceiling on an explicit limit, so no single response returns more than 1000 matches. A request that supplies no limit receives 100.

When matches remain beyond those returned, the response carries a Warning header field:

Warning: 299 {service}: There are more additional results that can be requested

The header announces that a further page can be requested. It does not carry a count of the matches not returned.

Both parameters must be non-negative integers. A malformed value is rejected with 400 (Bad Request) rather than ignored — silently dropping a limit would return the full result set to a caller that asked for one page.

offset may not exceed 1000 — the same ceiling as limit, so a request may skip no more than it may retrieve — and a larger one is rejected with 400 (Bad Request) rather than capped. Paging is offset-based with no cursor, so reaching a match means loading every match in front of it. A caller needing to go deeper should narrow the query rather than page further into an unnarrowed one.

Search depth

A search that combines two or more matching attributes is limited in how far back it reaches, but only where it has to be. Naming anything selective — a patient identifier, an accession number — settles the search almost at once, and it is then answered in full however old the matches are. The limit is reached only when every attribute named is a broad one.

Where a search is limited, the response is 200 (OK) and carries a Warning header field naming the earliest study date covered:

Warning: 299 {service}: Searched studies from 20241216 onward; earlier matches may exist

Results from that date onward are complete. Nothing is claimed about studies before it.

  • A PatientName search names no date. Where such a search is limited, the warning states the limit without dating it:
Warning: 299 {service}: The search was limited before every matching study was examined; further matches may exist

Both spellings mean the same thing to a caller: the result set is not known to be complete. Only the first tells you from when it is.

  • A search on a single matching attribute is not limited at all, and neither is a search on none. Both are answered in full.

  • An empty result set carries the warning too, when the search was limited. 200 (OK) with an empty array and no Warning header means no study matches. 200 (OK) with an empty array and this warning means no study matches within the range searched. These are different answers, and they are distinguished by the header rather than by the payload.

  • An explicit StudyDate narrows the search but does not remove the limit. It is the most effective thing a caller can supply: it bounds every attribute at once, so a search that names a date is normally answered in full and carries no warning whatever attributes accompany it.

  • A caller needing a longer history for a broad combination of attributes should page through it by date, a range at a time.

Unsupported query parameters

A query parameter this service does not match on is accepted, ignored, and announced. The response is 200 (OK) and carries a Warning header field naming every parameter that was ignored:

Warning: 299 {service}: The following query parameters are not supported and were ignored: PatientSex\StudyID

This applies to both search transactions, and to any parameter other than that level's matching attributes and the limit / offset controls — including an attribute the service returns but cannot match on, and a parameter it does not recognize at all. The result set is therefore wider than the request asked for: the ignored parameter narrowed nothing.

A 400 is not used here. PS3.18 reserves it for a query the service cannot parse, and PS3.4 §C.2.2.1.3 permits an Optional Key to be returned without being matched on — so a well-formed query the service can only partly honor is answered, with the shortfall stated.

Parameter names are separated by \, the DICOM multi-valued delimiter, rather than by a comma, because Warning is itself a comma-delimited list field. A name this service cannot repeat back is reported as (unprintable):

Warning: 299 {service}: The following query parameters are not supported and were ignored: PatientSex\(unprintable)

Both warnings are sent when both apply — a query naming an ignored attribute can still overflow its page — as two warn-values, the query's shortcoming before the response's:

Warning: 299 {service}: The following query parameters are not supported and were ignored: PatientSex
Warning: 299 {service}: There are more additional results that can be requested

Response

The response is an array of DICOM datasets in DICOM JSON format. Results are ordered by study date and time, most recent first. One exception: a search on PatientName that names fewer components than the stored name — SMITH matching SMITH^JOHN — is ordered most-recent-first within each matching patient, but is not ordered by date across several patients sharing those leading components.

Default study-level attributes:

Tag Attribute
(0020,000D) StudyInstanceUID
(0010,0020) PatientID
(0010,0010) PatientName
(0010,0030) PatientBirthDate
(0010,0040) PatientSex
(0020,0010) StudyID
(0008,0020) StudyDate
(0008,0030) StudyTime
(0008,0050) AccessionNumber
(0008,0061) ModalitiesInStudy
(0008,1030) StudyDescription
(0020,1206) NumberOfStudyRelatedSeries
(0020,1208) NumberOfStudyRelatedInstances

Default series-level attributes:

Tag Attribute
(0020,000D) StudyInstanceUID
(0020,000E) SeriesInstanceUID
(0020,0011) SeriesNumber
(0008,0005) SpecificCharacterSet
(0008,0060) Modality
(0008,0021) SeriesDate
(0008,0031) SeriesTime
(0008,103E) SeriesDescription
(0008,0070) Manufacturer
(0008,0080) InstitutionName
(0008,0090) ReferringPhysicianName
(0008,1090) ManufacturerModelName
(0018,0015) BodyPartExamined
(0018,1030) ProtocolName
(0020,1209) NumberOfSeriesRelatedInstances

Note: The includefield parameter, to return additional attributes beyond the defaults, is not currently supported.

Status codes

Code Description
200 (OK) The response payload contains all matching resources (an empty array when there are no matches)
400 (Bad Request) The query cannot be honored as written: a range that cannot match (a StudyDate range whose end precedes its start), a limit or offset that is not a non-negative integer, an offset above 1000, or a query parameter supplied more than once
401* (Unauthorized) Viewer-JWT verification is enabled and the request carries no valid bearer token
403* (Forbidden) A request for a study by UID, or for anything beneath it (series, instances, frames, metadata, etc.), names a Study Instance UID that is not in the token's scope
406 (Not Acceptable) The specified Accept header does not allow application/dicom+json
500 (Internal Server Error) The server encountered an unexpected error

* See above for an explanation of how to authorize using Viewer-JWT verification.

Response guarantees

  • An empty result set is returned with 200 (OK) and an empty JSON array — 204 (No Content) is not used.
  • A 200 (OK) always means the search ran to completion. A search that fails part-way through returns 500, and never a partial or empty array with 200 — a client must be able to distinguish "no studies match" from "the search could not be completed". A query the service cannot satisfy because of how it is written returns 400, so a caller can tell its own mistake from the server's.
  • A 200 (OK) does not by itself mean every parameter was applied. Two things narrow what the payload holds relative to what was asked, and both are stated in a Warning header field rather than left for the client to infer:
  • A parameter this service does not match on is ignored, so the result set is wider than the request.
  • Matches beyond limit are not returned, so the result set is narrower than the whole match set.

A response carrying no Warning header field is complete in every sense: every parameter was applied, no match was left behind, and no limit was placed on how far back the search reached.

Examples

Search studies by patient name and date

curl "http://localhost:3000/studies?PatientName=DOE^JOHN&StudyDate=20240101" \
 [-H 'Authorization: Bearer <jwt-token>' \ ]
  -H "Accept: application/dicom+json"

Search studies by date range

curl "http://localhost:3000/studies?StudyDate=20240101-20240131" \
 [-H 'Authorization: Bearer <jwt-token>' \ ]
  -H "Accept: application/dicom+json"

Search studies by patient ID

curl "http://localhost:3000/studies?PatientID=PATIENT001" \
 [-H 'Authorization: Bearer <jwt-token>' \ ]
  -H "Accept: application/dicom+json"

Direct lookup by study UID

curl "http://localhost:3000/studies?StudyInstanceUID=1.2.840.113619.2.55.3.604688119.868.1234567890.1" \
 [-H 'Authorization: Bearer <jwt-token>' \ ]
  -H "Accept: application/dicom+json"

Skip the first 100 matches and return up to 100 more:

curl "http://localhost:3000/studies?PatientID=PATIENT001&offset=100&limit=100" \
 [-H 'Authorization: Bearer <jwt-token>' \ ]
  -H "Accept: application/dicom+json"

Search series within a study

curl "http://localhost:3000/studies/1.2.840.113619.2.55.3.604688119.868.1234567890.1/series?Modality=CT" \
 [-H 'Authorization: Bearer <jwt-token>' \ ]
  -H "Accept: application/dicom+json"

Example response

[
  {
    "0020000D": {
      "vr": "UI",
      "Value": ["1.2.840.113619.2.55.3.604688119.868.1234567890.1"]
    },
    "00100020": {
      "vr": "LO",
      "Value": ["PATIENT001"]
    },
    "00100010": {
      "vr": "PN",
      "Value": [{ "Alphabetic": "DOE^JOHN" }]
    },
    "00080020": {
      "vr": "DA",
      "Value": ["20240101"]
    },
    "00080030": {
      "vr": "TM",
      "Value": ["120000"]
    },
    "00080050": {
      "vr": "SH",
      "Value": ["ACC001"]
    },
    "00080061": {
      "vr": "CS",
      "Value": ["CT"]
    },
    "00081030": {
      "vr": "LO",
      "Value": ["Example Study Description"]
    },
    "00201206": {
      "vr": "IS",
      "Value": [1]
    },
    "00201208": {
      "vr": "IS",
      "Value": [100]
    }
  }
]

Reading the Warning header

A search naming an unsupported parameter and more matches than fit on one page succeeds, but carries two warnings:

curl -i "http://localhost:3000/studies?PatientSex=F&StudyDate=20240101-20240131" [ \
  -H 'Authorization: Bearer <jwt-token>' ]
HTTP/1.1 200 OK
Content-Type: application/dicom+json
Warning: 299 kastoria-dicomweb: The following query parameters are not supported and were ignored: PatientSex
Warning: 299 kastoria-dicomweb: There are more additional results that can be requested

PatientSex is returned in the payload but cannot be matched on, so the result set is wider than the query asked for; and further pages remain.

Notes for client implementers

  • Do not treat 200 (OK) with an empty array as definitive without checking for a Warning header field. A search limited to a window returns the same empty payload whether nothing matches at all or nothing matches recently. The header is the only distinction.
  • Treat a 299 warning as information, not an error. It accompanies a successful response and does not indicate a failed or degraded search. A response may carry more than one — an ignored parameter, a limited window, and further pages available are independent conditions and can all apply at once.
  • To search a longer history, page by date rather than widening the range. Take the date the warning names, and issue the next request with a StudyDate range ending the day before it. Repeat until a response arrives without the warning, or until far enough back to stop. Each request is bounded, and together they cover the history without any single request being expensive.
  • Supply a StudyDate whenever one is known. It bounds every attribute at once, which is both faster and the surest way to receive a complete answer rather than a limited one.
  • Prefer StudyInstanceUID when it is known. It is answered as a direct lookup, without reading any index.

See also