Survey Design
Advanced Features
Dynamic Search

Dynamic Search (also called Search API) allows a select_one, select_multiple, or text field to load its choices from an Elasticsearch index at runtime as the enumerator types. This is the right approach when your choice list is too large to bundle in a CSV file or is updated frequently.

search-api() only works with Elasticsearch _search endpoints. It does not support arbitrary REST APIs or other backends. For offline or non-ES data, use rawquery with a bundled SQLite file instead.


search-api() appearance

The dynamic search is configured through the appearance column using the search-api() function:

search-api(method, url, post_body, value_column, display, data_path, save_path)

Parameters

ParameterDescription
methodAlways use 'POST'
urlThe Elasticsearch _search endpoint (e.g., https://es.example.com/my_index/_search)
post_bodyElasticsearch query DSL body sent to the endpoint. Use %__input__% as a placeholder for the enumerator's current search text
value_columnThe key to use as the stored value — typically _id or a field inside _source (e.g., _source.id)
displayThe key (or template) to use as the label shown in the dropdown. Supports ##key## placeholders (e.g., ##_source.name##) and @{func} expressions
data_pathJSONPath to the array of hits in the ES response — always $.hits.hits
save_pathA name under which the selected hit object is saved for use by other fields via pulldata()

Basic example

A health facility lookup where the enumerator types part of the facility name:

typenamelabelappearance
select_onefacilitySelect health facilitysearch-api('POST', 'https://es.example.com/facilities/_search', '{"query":{"match":{"name":"%__input__%"}},"size":20}', '_id', '##_source.name##', '$.hits.hits', 'facility_data')

Elasticsearch receives the query when the enumerator types "nair" and returns:

{
  "hits": {
    "hits": [
      {"_id": "HF001", "_source": {"name": "Nairobi Central Clinic"}},
      {"_id": "HF002", "_source": {"name": "Nairobi West Hospital"}}
    ]
  }
}

The dropdown shows Nairobi Central Clinic and Nairobi West Hospital; the stored value is HF001 or HF002.


Advanced display formatting

Using ##key## templates

Show multiple fields in the label:

search-api('POST', 'https://es.example.com/my_index/_search', '{"query":{"match":{"name":"%__input__%"}},"size":20}', '_id', '##_source.name## (##_source.district##)', '$.hits.hits', 'res')

Displayed as: Nairobi Central Clinic (Nairobi).

Using @{func} expressions

Apply conditional logic in the display label:

search-api('POST', 'https://es.example.com/my_index/_search', '{"query":{"match":{"name":"%__input__%"}},"size":20}', '_id',
  '@{if_else(eq("##_source.status##", "active"), "✓ ##_source.name##", "✗ ##_source.name##")}',
  '$.hits.hits', 'res')

Active results show ✓ Clinic Name; inactive show ✗ Clinic Name.


Setting a default value: search-default-api()

Use search-default-api() after search-api() to pre-populate the field with a default choice loaded from a separate Elasticsearch query (e.g., when editing an existing record):

appearance: search-api(...) search-default-api('POST', 'https://es.example.com/my_index/_search', '{"query":{"term":{"_id":"##saved_id##"}}}', '_id', '##_source.name##', '$.hits.hits')

Custom separator for select_multiple: search-default-separator()

For select_multiple fields, specify how multiple selected values are joined in the stored string:

appearance: search-api(...) search-default-separator(' || ')

Default separator is a space.


Supported question types

Question typeUse case
select_oneSingle selection from search results
select_multipleMultiple selections from search results
textAutocomplete — enumerator types freely but can select a suggestion

Using saved response data

When the enumerator selects a result, rtSurvey saves the full matching object from the response under the save_path name. Other fields can read from it in two ways.

Method 1 — pulldata() lookup

Use pulldata('save_path', 'key') to read a top-level field from the saved object:

typenamelabelcalculation
select_onefacilitySelect facility
calculatefacility_districtpulldata('facility_data', '_source.district')
calculatefacility_typepulldata('facility_data', '_source.type')
calculatefacility_bedspulldata('facility_data', '_source.beds')

The appearance on the select_one row must include search-api(..., 'facility_data') with the matching save_path.

For deeply nested fields, combine with substr-jsonpath():

substr-jsonpath(pulldata('facility_data', '_source'), '$.location.district')

Local file search with search()

search() performs offline, column-equality lookups against a bundled SQLite .db file — no network connection and no SQL queries required. Use it when your reference data is packaged with the form and you need a simple "find all rows where column equals value" lookup.

Signature

search(path, 'matches', 'list_name', ${field})

Parameters

ParameterDescription
pathPath to the SQLite table: concat(${family_path}, '/file.db::table')
'matches'Match operator — returns rows where the column equals the value
'list_name'Column name to match against
${field}Value to match; typically a field reference

Example

Populate a select_one from a local database by matching the value of ${d307} against the list_name column in the externalData table of d307.db:

typenamelabelappearance
select_onefacilitySelect facilitysearch(concat(${family_path}, '/d307.db::externalData'), 'matches', 'list_name', ${d307})

When ${d307} equals "HF001", search() returns every row in externalData where list_name = 'HF001' — entirely from the bundled file, with no network call.

search() vs rawquery autocomplete

Featuresearch()rawquery autocomplete
SQL queriesNo — column equality onlyYes — full SELECT … FROM …
Parameterized inputColumn value matchEmbedded in SQL string
Free-text entryYes — enumerator can type freelyNo — must select from list
UNION / multi-tableNot supportedSupported

When to use each

  • Use search() when you need a straightforward equality match against a bundled .db file, the form must work offline, and you want to allow free-text entry.
  • Use rawquery autocomplete when you need SQL flexibility (JOINs, UNIONs, computed columns) or must query across multiple tables, and the form has reliable connectivity.

Best Practices

  1. Ensure the Elasticsearch endpoint responds within 1–2 seconds — slow responses make search feel unresponsive.
  2. Use %__input__% in the post_body so Elasticsearch only returns matching results, not the entire index.
  3. Add "size": 20 (or similar) to the ES query body to cap results — returning thousands of hits defeats the purpose of search.
  4. Use an ES match or multi_match query rather than wildcard for better performance on large indices.
  5. Ensure the ES index has the right field mappings (e.g., text with an analyzer) for the fields you search on.

Limitations

  • search-api() only works with Elasticsearch _search endpoints — it does not support other REST APIs or backends.
  • Dynamic Search requires network connectivity — it does not work offline. Use dataSetting.csv + rawquery for offline scenarios.
  • The %__input__% placeholder is injected as-is into the ES query body; validate or sanitize on the server side if needed.
  • Complex @{func} display expressions may have limited support across all rtSurvey client versions.