🗄️ Database documents to OpenSearch
Template: nifi/user_templates/opensearch_ingest_docs_db_to_es.json
View the flow definition on GitHub
🎯 Purpose
This flow incrementally reads text documents from a PostgreSQL table and indexes each
record in OpenSearch. The bundled configuration targets the sample
medical_reports_text table and uses docid as the stable document identifier.
🔗 Processor chain
GenerateTableFetch-medical_reports_text
-> ExecuteSQLRecord
-> PutElasticsearchRecord
GenerateTableFetchcreates paginated SQL queries and tracks the maximumdocidit has seen.ExecuteSQLRecordexecutes each query and writes embedded-schema Avro.PutElasticsearchRecordreads the Avro records and indexes them using/docidas the OpenSearch document ID.
Failure and retry relationships are routed to funnels for inspection.
📋 Bundled defaults
| Setting | Value |
|---|---|
| Database URL | jdbc:postgresql://samples-db:5432/db_samples |
| Database user | test |
| JDBC driver | /opt/nifi/drivers/postgresql-42.7.7.jar |
| Source table | medical_reports_text |
| Incremental/partition column | docid |
| Partition size | 10000 |
| Returned columns | sampleid, typeid, dct, filename, document, and docid cast to text |
| OpenSearch endpoint | https://elasticsearch-1:9200 |
| Target index | ${generatetablefetch.tableName} (medical_reports_text by default) |
| Index operation | index |
| Document ID | /docid |
| Bulk batch size | 10000 |
✅ Requirements
- The core stack is running; for the bundled sample database use
make -C deploy start-data-infra. - The source table has a numeric, indexed, monotonically increasing maximum-value
column. The sample schema provides integer primary key
docid. - OpenSearch certificates exist and are mounted into NiFi.
- Database and OpenSearch credentials are available. Sensitive passwords are not included in the exported JSON.
📥 Import the flow
- Open NiFi at
https://localhost:8443and uploadnifi/user_templates/opensearch_ingest_docs_db_to_es.jsonas a process group. - Enter
opensearch_ingest_docs_db_to_esand keep it stopped while configuring it. - Right-click an empty area, select Configure, and open Controller Services.
Sensitive passwords are omitted from exported NiFi flows, so the database, SSL, and OpenSearch services require configuration after every fresh import.
🎛️ Configure the controller services
Configure and enable the services in this order:
| Controller service | Used by |
|---|---|
DBCPConnectionPool |
GenerateTableFetch-medical_reports_text, ExecuteSQLRecord |
AvroRecordSetWriter |
ExecuteSQLRecord |
AvroReader |
PutElasticsearchRecord |
JsonRecordSetWriter |
PutElasticsearchRecord result output |
StandardSSLContextService |
ElasticSearchClientServiceImpl |
ElasticSearchClientServiceImpl |
PutElasticsearchRecord |
1. DBCPConnectionPool
Configure the sample PostgreSQL connection as follows:
| Property | Bundled value |
|---|---|
| Database Connection URL | jdbc:postgresql://samples-db:5432/db_samples |
| Database Driver Class Name | org.postgresql.Driver |
| Database Driver Locations | /opt/nifi/drivers/postgresql-42.7.7.jar |
| Database User | Value of POSTGRES_USER_SAMPLES, test by default |
| Password | Value of POSTGRES_PASSWORD_SAMPLES |
| Password Source | PASSWORD |
| Max Total Connections | 8 |
| Maximum Idle Connections | 8 |
| Minimum Idle Connections | 0 |
| Max Wait Time | 500 millis |
Read the sensitive value from security/env/users_database.env; do not store it in the
flow JSON. If using another database, update the URL, driver class, driver JAR, and
credentials together. Apply the configuration and enable the service. A driver error
usually means the JAR path does not exist inside the NiFi container.
2. AvroRecordSetWriter
This service writes the rows returned by ExecuteSQLRecord:
| Property | Value |
|---|---|
| Schema Access Strategy | inherit-record-schema |
| Schema Write Strategy | avro-embedded |
| Compression Format | NONE |
| Encoder Pool Size | 32 |
| Cache Size | 1000 |
Leave the other exported properties unchanged, apply, and enable the service.
3. AvroReader
This service reads the embedded-schema Avro produced by the preceding writer:
| Property | Value |
|---|---|
| Schema Access Strategy | embedded-avro-schema |
| Fast Reader Enabled | true |
| Cache Size | 1000 |
No schema registry is required. Apply and enable the service.
4. JsonRecordSetWriter
This writer is selected as the result writer on PutElasticsearchRecord:
| Property | Value |
|---|---|
| Schema Access Strategy | inherit-record-schema |
| Schema Write Strategy | no-schema |
| Output Grouping | output-array |
| Serialized JSON Input Handling | ENABLED |
| Pretty Print JSON | false |
| Suppress Null Values | never-suppress |
| Compression Format | none |
Apply and enable the service.
5. StandardSSLContextService
Configure the certificate stores mounted by the standard Compose deployment:
| 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 | PKCS12 |
| TLS Protocol | TLS |
Read ES_CERTIFICATE_PASSWORD from
security/env/certificates_elasticsearch.env. Use paths and store types matching the
actual mounted files when deploying outside the standard stack. Apply and enable the
service.
6. ElasticSearchClientServiceImpl
Configure the OpenSearch client after the SSL service is enabled:
| Property | Bundled value |
|---|---|
| HTTP Hosts | https://elasticsearch-1: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 the credentials from security/env/users_elasticsearch.env. The
elasticsearch-1 hostname is reachable on the Compose network; use a reachable host
when NiFi runs elsewhere. Apply and enable the service.
⚙️ Configure each processor
Return to the process-group canvas and configure the processors in flow order.
1. GenerateTableFetch-medical_reports_text
| Property | Bundled value | Guidance |
|---|---|---|
| Database Connection Pooling Service | DBCPConnectionPool |
Must be enabled |
| Database Type | PostgreSQL |
Match the source database |
| Table Name | medical_reports_text |
Change for another source table |
| Columns to Return | sampleid,typeid,dct,filename,"document",CAST(docid AS VARCHAR(255)) AS docid |
Preserve a string docid for indexing |
| Maximum-value Columns | docid |
Must be numeric and monotonically increasing |
| Column for Value Partitioning | docid |
Should be indexed by the database |
| Partition Size | 10000 |
Reduce for wider records or a small test |
| Output Empty FlowFile on Zero Results | false |
Keep |
The processor emits SQL statements rather than database rows. Its success relationship
must connect to ExecuteSQLRecord; failure must connect to its funnel. It stores the
largest observed docid as state. Clear state only when intentionally re-reading rows.
2. ExecuteSQLRecord
| Property | Bundled value | Guidance |
|---|---|---|
| Database Connection Pooling Service | DBCPConnectionPool |
Must be enabled |
| Record Writer | AvroRecordSetWriter |
Must be enabled |
| Max Rows Per FlowFile | 0 |
Do not split a generated partition |
| Output Batch Size | 0 |
Keep |
| Fetch Size | 0 |
Driver default |
| Use Avro Logical Types | true |
Preserves date/time semantics |
| Set Auto Commit | true |
Bundled default |
Leave SQL Query empty: the processor receives each generated query in the incoming
FlowFile content. Connect success to PutElasticsearchRecord and failure to its
funnel.
If a target mapping rejects dates or timestamps, adjust the conversion and OpenSearch mapping deliberately rather than disabling logical types without checking the resulting JSON representation.
3. PutElasticsearchRecord
| Property | Bundled value | Guidance |
|---|---|---|
| Client Service | ElasticSearchClientServiceImpl |
Must be enabled |
| Record Reader | AvroReader |
Must be enabled |
| Result Record Writer | JsonRecordSetWriter |
Must be enabled |
| Index | ${generatetablefetch.tableName} |
Resolves to medical_reports_text |
| Index Operation | index |
Replaces a document with the same ID |
| ID Record Path | /docid |
Leading / is required by RecordPath |
| Retain Record Path ID Field | false |
Removes docid from _source after using it |
| Batch Size | 10000 |
Keep unless cluster sizing requires tuning |
| Max JSON Field String Length | 64 MB |
Covers large clinical text fields |
| Log Error Responses | false |
Enable while diagnosing rejected records |
| Output Error Responses | false |
Bundled flow does not emit record-level responses |
The successful and original relationships are auto-terminated. retry is routed to
one funnel, while failure and errors are routed to another. Keep those queues for
diagnosis.
✅ Pre-start validation
Before starting the process group, confirm that all six controller services show
Enabled, all three processors are valid, and the table, columns, partitioning key,
index, and /docid RecordPath match the source schema. Start with a reduced partition
size or test table, then inspect bulletins and every failure queue before processing the
full source.
▶️ Run and verify
Start the process group and watch the queues between all three processors. The default
target is medical_reports_text:
curl --cacert security/certificates/elastic/opensearch/elastic-stack-ca.crt.pem \
-u '<user>:<password>' \
'https://localhost:9200/medical_reports_text/_count'
Because the flow uses docid and the index operation, processing the same source row
again replaces the document with that ID instead of creating another generated ID.
⚠️ State and recovery
GenerateTableFetch stores incremental state. Clear its state only when intentionally
re-reading older rows. Before doing so, confirm that the target document ID is stable and
that re-indexing is safe. Check the failure funnels and NiFi bulletins before advancing
or discarding queued FlowFiles.