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
| Parameter | Description |
|---|---|
method | Always use 'POST' |
url | The Elasticsearch _search endpoint (e.g., https://es.example.com/my_index/_search) |
post_body | Elasticsearch query DSL body sent to the endpoint. Use %__input__% as a placeholder for the enumerator's current search text |
value_column | The key to use as the stored value — typically _id or a field inside _source (e.g., _source.id) |
display | The key (or template) to use as the label shown in the dropdown. Supports ##key## placeholders (e.g., ##_source.name##) and @{func} expressions |
data_path | JSONPath to the array of hits in the ES response — always $.hits.hits |
save_path | A 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:
| type | name | label | appearance |
|---|---|---|---|
| select_one | facility | Select health facility | search-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 type | Use case |
|---|---|
select_one | Single selection from search results |
select_multiple | Multiple selections from search results |
text | Autocomplete — 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:
| type | name | label | calculation |
|---|---|---|---|
| select_one | facility | Select facility | |
| calculate | facility_district | pulldata('facility_data', '_source.district') | |
| calculate | facility_type | pulldata('facility_data', '_source.type') | |
| calculate | facility_beds | pulldata('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
| Parameter | Description |
|---|---|
path | Path 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:
| type | name | label | appearance |
|---|---|---|---|
| select_one | facility | Select facility | search(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
| Feature | search() | rawquery autocomplete |
|---|---|---|
| SQL queries | No — column equality only | Yes — full SELECT … FROM … |
| Parameterized input | Column value match | Embedded in SQL string |
| Free-text entry | Yes — enumerator can type freely | No — must select from list |
| UNION / multi-table | Not supported | Supported |
When to use each
- Use
search()when you need a straightforward equality match against a bundled.dbfile, the form must work offline, and you want to allow free-text entry. - Use
rawqueryautocomplete when you need SQL flexibility (JOINs, UNIONs, computed columns) or must query across multiple tables, and the form has reliable connectivity.
Best Practices
- Ensure the Elasticsearch endpoint responds within 1–2 seconds — slow responses make search feel unresponsive.
- Use
%__input__%in thepost_bodyso Elasticsearch only returns matching results, not the entire index. - Add
"size": 20(or similar) to the ES query body to cap results — returning thousands of hits defeats the purpose of search. - Use an ES
matchormulti_matchquery rather thanwildcardfor better performance on large indices. - Ensure the ES index has the right field mappings (e.g.,
textwith an analyzer) for the fields you search on.
Limitations
search-api()only works with Elasticsearch_searchendpoints — it does not support other REST APIs or backends.- Dynamic Search requires network connectivity — it does not work offline. Use
dataSetting.csv+rawqueryfor 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.