Full-Text Search for Qualitative Coding
Find every interview excerpt or open-ended answer that mentions a word, and let a coder pull the matching items into the coder's own queue.
Potato builds a full-text index over the text of every item when the server starts, and an admin can search that index in any project, with or without QDA Mode. A search for copay returns each interview excerpt or open-ended survey answer containing a word that starts with "copay". Results arrive ranked by relevance, with the matching words highlighted in a short snippet. The optional setting search.annotator_claim adds a Find panel to the coding page. In the Find panel, a coder searches the same index and claims matching items into the coder's own queue. Search and QDA Mode both arrived in Potato 2.6.0.
FTS5 is the full-text search extension of SQLite. SQLite is the embedded database behind Potato's project store. Potato keeps the index as a table in <task_dir>/project.sqlite, and the same file holds memos and the codebook. The index contains the words of the items. Codes, memos, and annotations are outside the index, and a search never applies a code to the passages the search finds.
Search serves two jobs in a qualitative project:
- Finding passages to code. A coder working through a corpus by topic searches for "bus" or "copay", claims the items that mention the word, and codes the relevant passages with a span scheme.
- Checking a term across the corpus. An analyst or adjudicator writing a code's inclusion criteria searches for a term to see which items use the term. No coder's queue changes.
Configuration
Search needs no configuration. A project with no search block indexes every item at startup and serves admin search. The config below is a complete QDA Mode project that also turns on search-and-claim for the coder:
annotation_task_name: Clinic access interviews
task_dir: .
output_annotation_dir: annotation_output/
data_files:
- data/interviews.json
item_properties:
id_key: id
text_key: text
admin_api_key: change-this-key
qda_mode:
enabled: true
search:
annotator_claim: true
annotation_schemes:
- annotation_type: span
name: codes
description: Highlight a passage and apply a code
codebook: true
labels: [access barriers, cost concerns, provider trust]QDA Mode assumes one coder over the whole corpus. Under qda_mode.enabled: true, Potato accepts annotator_claim with any assignment design. The admin_api_key lets you call admin search from a script or the command line.
The search block takes four keys:
| Option | Default | Description |
|---|---|---|
search.enabled | true | Build the index at startup and serve the search endpoints. With false, Potato builds no index, and admin search answers 503. |
search.backend | fts5 | The index engine. fts5 is the only backend in Potato 2.13.1. Any other value turns search off and logs a warning. |
search.max_instances | 100000 | The largest number of items indexed. Potato indexes items in load order up to the cap. Items past the cap stay out of the index, and the server log reports the cap. |
search.annotator_claim | false | Show the Find panel to coders and serve the coder search and claim endpoints. Potato refuses to start when the setting conflicts with the assignment design (see the assignment guard). |
Who can search
Admin search and coder search read the same index through different endpoints:
| Who | Endpoint | Available when | Results per request |
|---|---|---|---|
| Admins and adjudicators | GET /admin/api/search?q=<query>&limit=<n> | search.enabled is true | 50 by default, up to 500 |
| Any signed-in coder | GET /api/search?q=<query>&limit=<n>, and the Find panel | search.annotator_claim: true | 50 by default, up to 200; the Find panel asks for 50 |
| Any signed-in coder | POST /api/search/claim with {"instance_id": "<id>"} | search.annotator_claim: true | One item per request |
Admin access means the admin API key in an X-API-Key header, or a signed-in user whose role grants the admin dashboard. An adjudicator in a project with the adjudication block enabled can use admin search as well. Admin search is read-only and changes no queue. Potato 2.13.1 has no search box on the admin dashboard, and admins search through the endpoint:
curl -H "X-API-Key: change-this-key" \
"http://localhost:8000/admin/api/search?q=copay&limit=20"The endpoint returns JSON in this shape:
{
"count": 1,
"query": "copay",
"results": [
{
"id": "P01-03",
"instance_id": "P01-03",
"score": -0.525,
"snippet": "My \u0002copay\u0003 went up again this year, so I skipped the follow…"
}
]
}The snippet runs up to 12 words around the best-matching part of the item and marks each matched word with the control characters U+0002 and U+0003. The Find panel turns the marks into highlights. score is the FTS5 BM25 rank. A lower score is a better match, and results arrive best match first. count is the number of results returned. limit caps the count, and the response does not report how many items match beyond the cap.
Query syntax
Potato splits a query at every character that is not a letter, a digit, or an underscore, and returns the items that contain every resulting word as a prefix. Potato never passes the query to FTS5 as query syntax. The rules below follow from that one step:
- Every word must appear.
bus linematches an item containing both "bus" and "line", in either order and at any distance apart. - Each word matches as a prefix.
copaymatches "copay", "copays", and "copayment".carematches "career" and "careful" along with "care". - Case and accents are ignored.
Copayandcopayreturn the same items, andcafematches "café". - Punctuation splits words.
self-caresearches for "self" and "care".cost-of-carereturns any item containing "cost", "of", and "care" anywhere in the text. - Apostrophes split words.
couldn'tsearches for "couldn" and "t". The item containing "couldn't" is found, and the snippet also highlights other words beginning with "t". - Operators are ordinary words. Quotation marks and
*are dropped, andOR,NOT, andNEARbecome words the item must contain. A query such ascost OR careusually returns nothing. - Word forms are not merged.
runmatches "running" through the prefix rule, andrannever matches "run". Search separately for each form a participant might use.
Indexed text and index rebuilds
Potato indexes one string per item, chosen in this order:
- The field named by
item_properties.text_key(defaulttext). - A list or object under
text_key, such as a transcript stored as a list of turns. Potato indexes the JSON text the coding page shows, field names included. - For an item with no usable value under
text_key, the first non-empty field amongcaption,description,transcript,prompt,question,title,content, andmessage.
Items with none of these fields stay out of the index, and the server log reports how many items were skipped. An image with no caption is the common case. Item IDs, case fields such as participant_id, and any other item field are not indexed, and a search for a participant ID finds nothing. Cases group excerpts by participant.
Potato rebuilds the whole index each time the server starts. For a study loaded from a data_directory, Potato rebuilds the index again after the directory loads, and the directory watcher reindexes when new files arrive. When the Python build's SQLite lacks FTS5, Potato leaves search off, the search endpoints answer 503, and the rest of Potato runs normally.
Search-and-claim
The Find panel
With search.annotator_claim: true, the coding page shows a Find button. The Find panel takes a query and lists up to 50 matches, each with the snippet, the item ID, and a Claim button. Claim adds the item to the end of the coder's queue and replaces the button with "In your queue". A claim on an item already in the coder's queue changes nothing, and the claim endpoint reports "already_assigned": true. The panel does not open the item, and the coder reaches a claimed item by moving forward through the queue.
The claim endpoint accepts any item ID in the project. An item another coder already holds can be claimed as well. An unknown ID returns 404, a request without a session returns 401, and a project without annotator_claim returns 403.
The assignment guard
A coder who claims items is choosing the sample, and many study designs depend on Potato making that choice. With search.annotator_claim: true, Potato refuses to start when the config also sets any of the following:
| Setting | Reason Potato refuses |
|---|---|
assignment_strategy set to random, diversity_clustering, max_diversity, active_learning, llm_confidence, least_annotated, category_based, batch, psychometric, model_review, or engine | The strategy's sample or order depends on Potato choosing each item. |
num_annotators_per_item above 1, an overlap_sample.count above 1, or the older keys max_annotations_per_item and min_annotators_per_instance above 1 | Potato cannot guarantee that overlap items reach several coders. |
attention_checks.enabled or gold_standards.enabled | Coders could find and skip check items. |
icl_labeling.enabled | Blind verification items for a model would become findable. |
adjudication.enabled | Potato curates the adjudication queue. |
crowdsourcing.provider set to any value except expert, or a legacy prolific or mturk block or login type | A paid task is the assigned unit, and claiming breaks payment and coverage. |
The guard allows the assignment strategies fixed_order and priority, and fixed_order is Potato's default. A config with assignment_strategy: random and num_annotators_per_item: 2 fails validation with this message:
ERRORS:
- search.annotator_claim: true is incompatible with this configuration:
- assignment_strategy: random (self-selection breaks sampling/ordering)
- num_annotators_per_item: 2 (inter-annotator overlap cannot be guaranteed under self-selection)Under qda_mode.enabled: true or solo_mode.enabled: true, Potato skips every check in the table above. A QDA Mode team project that assigns an overlap sample for intercoder reliability therefore starts with claim on, and coders can then claim items outside the sample Potato assigned to each coder. Leave annotator_claim off while a team codes a reliability sample, and turn the setting on afterward if coders choosing their own items fits the design. Admin search falls outside the guard, because admin search changes no queue.
Searching while you code in QDA Mode
A researcher who searches a corpus for a term and then codes each passage where the term appears follows these steps in Potato:
- Open Find and search for the term, for example
transport. The prefix rule also returns "transportation" and "transported". - Read the snippets, and claim each item whose snippet looks relevant.
- Code each claimed item when the item comes up in your queue. Highlight the passage and apply a code from the codebook, or press
ito make an in-vivo code from the selection. - Attach a memo to an item or a selection to record why the passage fits the code, or why a near-miss falls outside the code.
Search returns items, with one snippet per item. An item that mentions the term five times appears once, and the full item opens only from the queue. Potato 2.13.1 has no keyword-in-context list of every occurrence and no step that applies a code to every search match. Each coding decision stays with the coder reading the passage.
Limits
- Item text only. Codes, memos, annotations, item IDs, and fields other than the indexed text are not searchable.
- No phrase, Boolean, proximity, or stemmed search. Every query is a set of prefixes that must all appear.
- One snippet per item. A snippet runs up to 12 words, and an item appears once however often the term occurs.
- Capped result lists. Admin search returns at most 500 items per request, coder search at most 200, and the Find panel 50.
- Languages written without spaces. Chinese or Japanese text is indexed as runs of characters between punctuation marks, and a query matches only at the start of a run.
- Media without text. An image, audio, or video item is indexed only through a caption or transcript field named by
text_keyor found among the fallback fields.
Upstream example project
From a Potato checkout, start the upstream search example and query the admin endpoint with the example's key:
python potato/flask_server.py start examples/advanced/search-example/config.yaml -p 8000
curl -H "X-API-Key: search-example-key" \
"http://localhost:8000/admin/api/search?q=rare"The example loads six short status reports, and three of the reports mention "rare". The example turns on search-and-claim under fixed_order assignment with max_annotations_per_user: 2. To see the Find panel in a project of interview excerpts, start the QDA Mode example at examples/advanced/qda-mode-example/config.yaml instead.
Further reading
- QDA Mode, for the workspace search belongs to
- Codebook, for the codes a coder applies to the passages search finds
- Memos, for notes on items and selections
- Task Assignment, for the assignment strategies the guard checks
- Coding Interview Transcripts, for the full path from recordings to coded excerpts
For implementation details, see the source documentation.