[Feature][Connector-V2][Elasticsearch] Add Runtime Fields support (Elasticsearch 7.11+)#10201
Conversation
…asticsearch 7.11+)
…asticsearch 7.11+)
…asticsearch 7.11+)
…asticsearch 7.11+)
…asticsearch 7.11+)
…asticsearch 7.11+)
…asticsearch 7.11+)
…asticsearch 7.11+)
| | scroll_size | int | no | 100 | | ||
| | tls_verify_certificate | boolean | no | true | | ||
| | tls_verify_hostnames | boolean | no | true | | ||
| | tls_verify_hostname | boolean | no | true | |
There was a problem hiding this comment.
Got it, it seems that the file docs/en/connector-v2/source/Elasticsearch.md also has this problem, please fix it as well.
| public void startUp() throws Exception { | ||
| container = | ||
| new ElasticsearchContainer( | ||
| DockerImageName.parse("elasticsearch:8.9.0") |
There was a problem hiding this comment.
Is the docker image version es7 more reasonable, because this feature was introduced in version 7.11+?
If you must use version 8.9.0, I think there is no need to add the ElasticsearchRuntimeFieldsIT class. It is also possible to write e2e in ElasticsearchIT.
| @@ -49,6 +49,7 @@ support version >= 2.x and <= 8.x. | |||
| | tls_truststore_password | string | no | - | | |||
| | pit_keep_alive | long | no | 60000 (1 minute) | | |||
| | pit_batch_size | int | no | 100 | | |||
| | runtime_fields | array | no | - | | |||
There was a problem hiding this comment.
thx, please modify tls_verify_hostnames to tls_verify_hostname
…asticsearch 7.11+)
…asticsearch 7.11+)
…asticsearch 7.11+)
|
|
||
| public static final Option<List<Map<String, Object>>> RUNTIME_FIELDS = | ||
| Options.key("runtime_fields") | ||
| .type(new TypeReference<List<Map<String, Object>>>() {}) |
There was a problem hiding this comment.
If runtime_field is determined, there are only three attributes: name, type, and script. I suggest creating a new class RuntimeField. For example:
+ public class RuntimeField {
+ private String name;
+ private String type;
+ private String script;
+}There was a problem hiding this comment.
The required core fields for runtime_field are indeed name, type, and script. They form the minimal valid configuration. However, the current implementation also supports optional fields such as script_lang and script_params, which are added into the script block. So it’s not limited to only those three.
| private List<String> generateRuntimeTestData() throws IOException { | ||
| List<String> testData = new ArrayList<>(); | ||
|
|
||
| // Use a fixed date: 2024-01-15 (Monday) for predictable day_of_week |
…asticsearch 7.11+)
# Conflicts: # seatunnel-e2e/seatunnel-connector-v2-e2e/connector-elasticsearch-e2e/src/test/java/org/apache/seatunnel/e2e/connector/elasticsearch/ElasticsearchRuntimeFieldsIT.java
…asticsearch 7.11+)
|
@zhangshenghang PTAL |
# Conflicts: # docs/en/connector-v2/source/Elasticsearch.md # seatunnel-connectors-v2/connector-elasticsearch/src/main/java/org/apache/seatunnel/connectors/seatunnel/elasticsearch/source/ElasticsearchSourceReader.java # seatunnel-e2e/seatunnel-connector-v2-e2e/connector-elasticsearch-e2e/src/test/java/org/apache/seatunnel/e2e/connector/elasticsearch/ElasticsearchIT.java
Issue 1: Missing Elasticsearch version compatibility checkLocation: // Parse runtime fields configuration
Map<String, Object> runtimeFields = null;
if (readonlyConfig.getOptional(ElasticsearchSourceOptions.RUNTIME_FIELDS).isPresent()) {
runtimeFields =
parseRuntimeFields(
readonlyConfig.get(ElasticsearchSourceOptions.RUNTIME_FIELDS));
}Related context:
Problem description:
Potential risks:
Impact scope:
Severity: MAJOR Improvement suggestions: // Add version check in parseOneIndexQueryConfig method
if (readonlyConfig.getOptional(ElasticsearchSourceOptions.RUNTIME_FIELDS).isPresent()) {
// Check Elasticsearch version
try (EsRestClient esRestClient = EsRestClient.createInstance(connectionConfig)) {
ElasticsearchClusterInfo clusterInfo = esRestClient.getClusterInfo();
String version = clusterInfo.getClusterVersion();
if (!isRuntimeFieldsSupported(version)) {
throw new ElasticsearchConnectorException(
ElasticsearchConnectorErrorCode.SOURCE_CONFIG_ERROR,
String.format("Runtime fields require Elasticsearch 7.11 or higher, " +
"but current cluster version is: %s", version));
}
runtimeFields = parseRuntimeFields(
readonlyConfig.get(ElasticsearchSourceOptions.RUNTIME_FIELDS));
}
}
// Add version comparison helper method
private boolean isRuntimeFieldsSupported(String version) {
String[] parts = version.split("\\.");
int major = Integer.parseInt(parts[0]);
int minor = parts.length > 1 ? Integer.parseInt(parts[1]) : 0;
return major > 7 || (major == 7 && minor >= 11);
}Rationale:
Issue 2: Runtime Fields configuration validation is not strict enoughLocation: private Map<String, Object> parseRuntimeFields(List<Map<String, Object>> runtimeFieldsList) {
if (runtimeFieldsList == null || runtimeFieldsList.isEmpty()) {
return null;
}
Map<String, Object> runtimeMappings = new java.util.LinkedHashMap<>();
for (Map<String, Object> fieldConfig : runtimeFieldsList) {
String name = (String) fieldConfig.get("name");
String type = (String) fieldConfig.get("type");
String script = (String) fieldConfig.get("script");
if (name == null || type == null || script == null) {
log.warn("Invalid runtime field configuration: {}, skipping", fieldConfig);
continue; // Just skip, don't throw exception
}
// ...
}
return runtimeMappings.isEmpty() ? null : runtimeMappings;
}Related context:
Problem description:
Potential risks:
Impact scope:
Severity: MAJOR Improvement suggestions: private static final Set<String> SUPPORTED_RUNTIME_FIELD_TYPES =
new HashSet<>(Arrays.asList(
"boolean", "date", "double", "geo_point", "ip", "keyword", "long"
));
private Map<String, Object> parseRuntimeFields(List<Map<String, Object>> runtimeFieldsList) {
if (runtimeFieldsList == null || runtimeFieldsList.isEmpty()) {
return null;
}
Map<String, Object> runtimeMappings = new java.util.LinkedHashMap<>();
Set<String> fieldNames = new HashSet<>();
for (int i = 0; i < runtimeFieldsList.size(); i++) {
Map<String, Object> fieldConfig = runtimeFieldsList.get(i);
String name = (String) fieldConfig.get("name");
String type = (String) fieldConfig.get("type");
String script = (String) fieldConfig.get("script");
// Validate required fields
if (name == null || name.trim().isEmpty()) {
throw new ElasticsearchConnectorException(
ElasticsearchConnectorErrorCode.SOURCE_CONFIG_ERROR,
String.format("Runtime field at index %d: 'name' cannot be null or empty", i));
}
if (type == null || type.trim().isEmpty()) {
throw new ElasticsearchConnectorException(
ElasticsearchConnectorErrorCode.SOURCE_CONFIG_ERROR,
String.format("Runtime field '%s': 'type' cannot be null or empty", name));
}
if (script == null || script.trim().isEmpty()) {
throw new ElasticsearchConnectorException(
ElasticsearchConnectorErrorCode.SOURCE_CONFIG_ERROR,
String.format("Runtime field '%s': 'script' cannot be null or empty", name));
}
// Validate if type is supported
if (!SUPPORTED_RUNTIME_FIELD_TYPES.contains(type.toLowerCase())) {
throw new ElasticsearchConnectorException(
ElasticsearchConnectorErrorCode.SOURCE_CONFIG_ERROR,
String.format("Runtime field '%s': unsupported type '%s'. " +
"Supported types are: %s",
name, type, SUPPORTED_RUNTIME_FIELD_TYPES));
}
// Validate field name uniqueness
if (fieldNames.contains(name)) {
log.warn("Duplicate runtime field name: {}, skipping", name);
continue;
}
fieldNames.add(name);
// Validate field name format (Elasticsearch field name rules)
if (!name.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) {
throw new ElasticsearchConnectorException(
ElasticsearchConnectorErrorCode.SOURCE_CONFIG_ERROR,
String.format("Runtime field name '%s' contains invalid characters. " +
"Field names must start with a letter or underscore " +
"and contain only letters, digits, and underscores", name));
}
// Build configuration
Map<String, Object> fieldDef = new java.util.LinkedHashMap<>();
fieldDef.put("type", type);
Map<String, Object> scriptDef = new java.util.LinkedHashMap<>();
scriptDef.put("source", script);
if (fieldConfig.containsKey("script_lang")) {
scriptDef.put("lang", fieldConfig.get("script_lang"));
}
if (fieldConfig.containsKey("script_params")) {
scriptDef.put("params", fieldConfig.get("script_params"));
}
fieldDef.put("script", scriptDef);
runtimeMappings.put(name, fieldDef);
}
return runtimeMappings.isEmpty() ? null : runtimeMappings;
}Rationale:
Issue 3: Insufficient E2E test coverageLocation: @TestTemplate
public void testElasticsearchSourceWithRuntimeFields(TestContainer container)
throws IOException, InterruptedException {
Container.ExecResult execResult =
container.executeJob(
"/elasticsearch/elasticsearch_source_with_runtime_fields.conf");
Assertions.assertEquals(0, execResult.getExitCode(), "Job should complete successfully");
log.info("Runtime fields test completed successfully");
log.info("Job output: {}", execResult.getStdout());
}Related context:
Problem description:
Potential risks:
Impact scope:
Severity: MAJOR Improvement suggestions: @TestTemplate
public void testElasticsearchSourceWithRuntimeFields(TestContainer container)
throws IOException, InterruptedException {
// Prepare test data
String indexName = "st_index_runtime";
createTestIndexWithDataForRuntimeFields(indexName);
// Execute job
Container.ExecResult execResult =
container.executeJob("/elasticsearch/elasticsearch_source_with_runtime_fields.conf");
Assertions.assertEquals(0, execResult.getExitCode(), "Job should complete successfully");
// Verify runtime field calculation results
List<Map<String, Object>> docs = getDocumentsFromIndex(indexName);
Assertions.assertFalse(docs.isEmpty(), "Should have at least one document");
Map<String, Object> doc = docs.get(0);
// Verify day_of_week calculation is correct
Assertions.assertEquals("MONDAY", doc.get("day_of_week"));
// Verify c_int_doubled calculation is correct
Assertions.assertEquals(20, doc.get("c_int_doubled"));
// Verify full_name calculation is correct
Assertions.assertEquals("test_1_computed", doc.get("full_name"));
log.info("Runtime fields test completed successfully with data validation");
}
// Add additional test cases
@TestTemplate
public void testElasticsearchSourceWithInvalidRuntimeFieldType(TestContainer container)
throws IOException, InterruptedException {
// This test should fail, verify type checking logic
Container.ExecResult execResult = container.executeJob(
"/elasticsearch/elasticsearch_source_with_invalid_runtime_field_type.conf");
Assertions.assertNotEquals(0, execResult.getExitCode(),
"Job should fail with invalid runtime field type");
Assertions.assertTrue(execResult.getStderr().contains("unsupported type"),
"Error message should mention unsupported type");
}
@TestTemplate
public void testElasticsearchSourceWithEmptyRuntimeFields(TestContainer container)
throws IOException, InterruptedException {
// Test empty list configuration
Container.ExecResult execResult = container.executeJob(
"/elasticsearch/elasticsearch_source_with_empty_runtime_fields.conf");
Assertions.assertEquals(0, execResult.getExitCode(),
"Job should succeed with empty runtime fields list");
}Rationale:
Issue 4: Documentation lacks detailed explanation of script security and performance impactLocation: Related context:
Problem description:
Potential risks:
Impact scope:
Severity: MINOR Improvement suggestions: Add the following content to the documentation: ### runtime_fields [array]
**Security Considerations:**
Runtime fields execute Painless scripts on your Elasticsearch cluster. Keep these security best practices in mind:
1. **Script Injection**: Avoid using user-provided input directly in scripts without proper validation
2. **Resource Limits**: Complex scripts can consume significant CPU resources and impact query performance
3. **Access Control**: Ensure only authorized users can configure runtime fields
4. **Data Exposure**: Runtime fields can access all document fields, be cautious about sensitive data
**Performance Best Practices:**
1. **Keep Scripts Simple**: Prefer simple arithmetic and string operations over complex logic
2. **Avoid Heavy Computations**: Scripts like regex, date parsing with complex formats, or loops can be slow
3. **Use Indexed Fields for Frequent Queries**: If a runtime field is used frequently, consider indexing it
4. **Test with Real Data**: Always test runtime fields with a realistic dataset before production use
5. **Monitor Query Performance**: Use Elasticsearch's profiling API to identify slow runtime fields
**When to Avoid Runtime Fields:**
- High-throughput pipelines (more than 10,000 docs/second)
- Complex aggregations on runtime fields
- Frequently executed queries
- When deterministic performance is required
**Example of Efficient vs Inefficient Scripts:**
Good (simple arithmetic):
```hocon
{
name = "total_amount"
type = "double"
script = "emit(doc['quantity'].value * doc['price'].value)"
}Avoid (complex logic): {
name = "complex_calculation"
type = "double"
script = "double result = 0; for (int i = 0; i < doc['array_field'].length; i++) { result += doc['array_field'][i]; } emit(result)"
}并在文档中添加说明: **Important Notes:**
- If a runtime field has the same name as an existing field in the index,
the index field value takes precedence. Be careful to avoid naming conflicts.
- To avoid conflicts, use a prefix like `runtime_` for your runtime fields.Rationale:
|
…asticsearch 7.11+) (apache#10201)
…asticsearch 7.11+) (apache#10201)

runtime_fieldsoption to the Elasticsearch source to build runtime_mappings and include computed fields in scroll/PIT queriesPurpose of this pull request
Does this PR introduce any user-facing change?
How was this patch tested?
Check list
New License Guide
incompatible-changes.mdto describe the incompatibility caused by this PR.