🧠 MedCAT annotations to OpenSearch
Template: nifi/user_templates/opensearch_ingest_docs_annotations_medcat_service_to_es.json
View the flow definition on GitHub
🎯 Purpose
This flow reads documents that already exist in OpenSearch, sends their text to the CogStack MedCAT service, expands the response into individual annotation documents, and writes those annotations to a separate OpenSearch index.
🔗 Processor chain
SearchElasticsearch
-> CogStackPrepareRecordForNlp
-> InvokeHTTP
-> CogStackParseCogStackServiceResult
-> SplitJson
-> EvaluateJsonPath-create_annotation_id_store_as_attritbute
-> PutElasticsearchJson
The preparation processor sends the document text plus a footer containing the source
fields. The response parser creates an annotation_id by combining the source document
ID and MedCAT annotation ID. EvaluateJsonPath copies it to a FlowFile attribute used
as the destination document ID.
📋 Bundled defaults
| Setting | Value |
|---|---|
| Source OpenSearch index | encounters |
| Source fields | id, text |
| Search condition | Documents where text exists |
| Pagination | Scroll, keep-alive 30 mins |
| Restart on finish | false |
| MedCAT endpoint | http://cogstack-medcat-service-production:5000/api/process_bulk |
| Document ID field | id |
| Document text field | text |
| Destination index | encounters_annotations |
| Index operation | index |
| Destination ID attribute | annotation_id |
✅ Requirements
- The source OpenSearch index exists and its documents contain stable
idandtextfields. - The MedCAT service and its configured model are running:
make -C deploy start-medcat-service
- NiFi can resolve
cogstack-medcat-service-productionon the Docker network, or theInvokeHTTPURL is changed to a reachable MedCAT endpoint. - OpenSearch and SSL controller-service passwords are entered after import.
📥 Import the flow
- Open NiFi at
https://localhost:8443and uploadnifi/user_templates/opensearch_ingest_docs_annotations_medcat_service_to_es.jsonas a process group. - Enter
opensearch_docs_ingest_annotations_to_esand keep it stopped. - Right-click an empty area, select Configure, and open Controller Services.
🎛️ Configure the controller services
This flow has two controller services. Configure the SSL context before the OpenSearch client that references it.
1. StandardSSLContextService
| Property | Bundled value |
|---|---|
| Keystore Filename | /security/certificates/elastic/opensearch/elasticsearch/elasticsearch-1/elasticsearch-1-keystore.jks |
| Keystore Password | Value of ES_CERTIFICATE_PASSWORD |
| Key Password | Value of ES_CERTIFICATE_PASSWORD |
| Keystore Type | JKS |
| Truststore Filename | /security/certificates/elastic/opensearch/elasticsearch/elasticsearch-1/elasticsearch-1-truststore.key |
| Truststore Password | Value of ES_CERTIFICATE_PASSWORD |
| Truststore Type | JKS |
| TLS Protocol | TLS |
Read the password from security/env/certificates_elasticsearch.env. When NiFi runs
outside the standard deployment, use certificate paths available inside that runtime
and store types matching those files. Apply and enable the service.
2. ElasticSearchClientServiceImpl
| Property | Bundled value |
|---|---|
| HTTP Hosts | https://elasticsearch-1:9200,https://elasticsearch-2:9200 |
| Authorization Scheme | BASIC |
| Username | Value of ELASTIC_USER, normally admin for OpenSearch |
| Password | Value of ELASTIC_PASSWORD |
| SSL Context Service | StandardSSLContextService |
| Connect Timeout | 5000 |
| Read Timeout | 60000 |
| Character Set | UTF-8 |
| Enable Compression | false |
| Sniff Cluster Nodes | false |
| Sniff on Failure | false |
| Suppress Null and Empty Values | always-suppress |
Read credentials from security/env/users_elasticsearch.env. The bundled hostnames
resolve on the Compose network; change them if NiFi reaches OpenSearch through another
address. Apply and enable this service only after the SSL context is enabled.
⚙️ Configure each processor
Return to the canvas and configure the processors in flow order.
1. SearchElasticsearch
| Property | Bundled value | Guidance |
|---|---|---|
| Client Service | ElasticSearchClientServiceImpl |
Must be enabled |
| Index | encounters |
Source document index |
| Query Definition Style | full |
Query property contains the full search body |
| Search Results Format | SOURCE_ONLY |
Passes only _source to NLP |
| Search Results Split | splitUp-no |
Keeps each result page together |
| Pagination Type | pagination-scroll |
Uses a scroll search |
| Pagination Keep Alive | 30 mins |
Must exceed page-processing time |
| Restart On Finish | false |
Performs one pass only |
| Output No Hits | false |
Emits nothing for an empty result |
| Max JSON Field String Length | 20 MB |
Increase only for known larger documents |
The bundled query selects only id and text and requires text to exist:
{
"_source": ["id", "text"],
"query": {
"bool": {
"must": [
{"exists": {"field": "text"}}
]
}
}
}
Use a restrictive query or test index for the first run. Connect hits and
aggregations to CogStackPrepareRecordForNlp; route failure and retry to the
search failure funnel. Review processor state before re-running a completed search.
2. CogStackPrepareRecordForNlp
| Property | Bundled value |
|---|---|
process_flow_file_type |
json |
document_id_field_name |
id |
document_text_field_name |
text |
The field names must match both the source mapping and the _source selection above.
Connect success to InvokeHTTP, route failure to its funnel, and leave original
auto-terminated.
3. InvokeHTTP
| Property | Bundled value | Guidance |
|---|---|---|
| HTTP URL | http://cogstack-medcat-service-production:5000/api/process_bulk |
Must resolve from NiFi |
| HTTP Method | POST |
Keep |
| Request Body Enabled | true |
Required |
| Request Content-Type | ${mime.type} |
Supplied by the preparation processor |
| Connection Timeout | 10 secs |
Bundled default |
| Socket Read Timeout | 60 secs |
Increase for legitimately slow batches |
| Socket Write Timeout | 120 secs |
Bundled default |
| Response Body Ignored | false |
Required by the parser |
| Response Generation Required | false |
Bundled default |
| HTTP/2 Disabled | True |
Bundled default |
Connect Response to CogStackParseCogStackServiceResult; route No Retry, Retry,
and Failure to the HTTP failure funnel. Original is auto-terminated. If using a
different MedCAT deployment, change the URL without changing the /api/process_bulk
contract expected by the preparation and parser processors.
4. CogStackParseCogStackServiceResult
| Property | Bundled value | Meaning |
|---|---|---|
service_message_type |
medcat |
Parse a MedCAT response |
document_id_field_name |
id |
Source identifier |
document_text_field_name |
text |
Source text field |
output_text_field_name |
text |
Restored output text |
medcat_output_mode |
not_set |
Normal annotations, not de-identification |
medcat_empty_text_handling |
drop |
Do not emit empty-text documents |
medcat_no_annotations_handling |
restore |
Restore a document with no annotations |
medcat_deid_keep_annotations |
true |
Relevant only in de-identification mode |
The parser combines the source document ID and MedCAT annotation ID into
annotation_id. Connect success to SplitJson, route failure to its funnel, and
leave original auto-terminated.
5. SplitJson
| Property | Bundled value |
|---|---|
| JsonPath Expression | $.* |
| Null Value Representation | empty string |
| Max String Length | 20 MB |
This produces one FlowFile per annotation. Connect split to
EvaluateJsonPath-create_annotation_id_store_as_attritbute, route failure to its
funnel, and leave original auto-terminated.
6. EvaluateJsonPath-create_annotation_id_store_as_attritbute
The processor name contains the original attritbute typo; this does not affect its
behavior.
| Property | Bundled value |
|---|---|
| Destination | flowfile-attribute |
| Return Type | auto-detect |
| Path Not Found Behavior | ignore |
Dynamic property annotation_id |
$.annotation_id |
| Max String Length | 20 MB |
Connect matched to PutElasticsearchJson; route unmatched and failure to their
funnels. An unmatched FlowFile cannot be indexed safely because the downstream writer
uses this attribute as its deterministic ID.
7. PutElasticsearchJson
| Property | Bundled value | Guidance |
|---|---|---|
| Client Service | ElasticSearchClientServiceImpl |
Must be enabled |
| Index | encounters_annotations |
Keep separate from source documents |
| Index Operation | index |
Replaces an annotation with the same ID |
| Identifier Attribute | annotation_id |
Must match EvaluateJsonPath |
| Input Content Format | Single JSON |
Required after SplitJson |
| Max FlowFiles Per Batch | 100 |
Reduce while testing if desired |
| Max JSON Field String Length | 20 MB |
Bundled default |
| Retain Identifier Field | true |
Keeps the identifier in _source |
| Log Error Responses | false |
Enable while diagnosing rejected annotations |
| Output Error Responses | false |
Bundled default |
The successful and original relationships are auto-terminated. Route failure,
retry, and errors to the OpenSearch failure funnel.
✅ Pre-start validation
Before starting, confirm that both controller services are enabled, all seven processors
are valid, the source query returns matching id and text fields, and the MedCAT URL
is reachable from NiFi. Confirm the destination index is not the source index. Start
with a restrictive query, then inspect HTTP responses, provenance, bulletins, and every
failure queue before processing the full source.
The bundled parser uses normal annotation mode (medcat_output_mode=not_set), not
de-identification mode. Each annotation becomes a separate destination document.
▶️ Run and verify
The search processor is configured for a single pass because Restart On Finish is
false. Start with a restrictive query or a test index, then confirm the annotation
index count:
curl --cacert security/certificates/elastic/opensearch/elastic-stack-ca.crt.pem \
-u '<user>:<password>' \
'https://localhost:9200/encounters_annotations/_count'
Using deterministic annotation_id values makes deliberate reprocessing idempotent for
the same source document and MedCAT annotation IDs.
⚠️ Failure handling
Search, HTTP, preparation, parsing, splitting, identifier extraction, and OpenSearch failures are routed to funnels. Documents with no returned annotations do not create annotation records in the bundled non-de-identification mode. Inspect MedCAT responses and NiFi provenance when input counts and annotation counts differ.