Skip to content

Commit 7cd6de7

Browse files
authored
Adds example plugin for custom ingest processor (#112282)
* Adds example plugin for custom ingest processor Adds an example for creating a plugin with a simple custom ingest processor. The example processor repeats the value of an expected filed in a document, or ignores it if the expected field does not exist. Closes #111539
1 parent 6ff0246 commit 7cd6de7

File tree

8 files changed

+220
-2
lines changed

8 files changed

+220
-2
lines changed

docs/changelog/112282.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pr: 112282
2+
summary: Adds example plugin for custom ingest processor
3+
area: Ingest Node
4+
type: enhancement
5+
issues:
6+
- 111539

docs/plugins/development/creating-classic-plugins.asciidoc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,13 @@ for the plugin. If you need other resources, package them into a resources JAR.
3232
The {es} repository contains {es-repo}tree/main/plugins/examples[examples of plugins]. Some of these include:
3333

3434
* a plugin with {es-repo}tree/main/plugins/examples/custom-settings[custom settings]
35+
* a plugin with a {es-repo}tree/main/plugins/examples/custom-processor[custom ingest processor]
3536
* adding {es-repo}tree/main/plugins/examples/rest-handler[custom rest endpoints]
3637
* adding a {es-repo}tree/main/plugins/examples/rescore[custom rescorer]
3738
* a script {es-repo}tree/main/plugins/examples/script-expert-scoring[implemented in Java]
3839

3940
These examples provide the bare bones needed to get started. For more
40-
information about how to write a plugin, we recommend looking at the
41+
information about how to write a plugin, we recommend looking at the
4142
{es-repo}tree/main/plugins/[source code of existing plugins] for inspiration.
4243

4344
[discrete]
@@ -88,4 +89,4 @@ for more information.
8889
[[plugin-descriptor-file-classic]]
8990
==== The plugin descriptor file for classic plugins
9091

91-
include::plugin-descriptor-file.asciidoc[]
92+
include::plugin-descriptor-file.asciidoc[]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
apply plugin: 'elasticsearch.esplugin'
9+
apply plugin: 'elasticsearch.yaml-rest-test'
10+
11+
esplugin {
12+
name 'custom-processor'
13+
description 'An example plugin showing how to register a custom ingest processor'
14+
classname 'org.elasticsearch.example.customprocessor.ExampleProcessorPlugin'
15+
licenseFile rootProject.file('SSPL-1.0+ELASTIC-LICENSE-2.0.txt')
16+
noticeFile rootProject.file('NOTICE.txt')
17+
}
18+
19+
dependencies {
20+
yamlRestTestRuntimeOnly "org.apache.logging.log4j:log4j-core:${log4jVersion}"
21+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.example.customprocessor;
10+
11+
import org.elasticsearch.ingest.Processor;
12+
import org.elasticsearch.plugins.IngestPlugin;
13+
import org.elasticsearch.plugins.Plugin;
14+
15+
import java.util.Map;
16+
17+
public class ExampleProcessorPlugin extends Plugin implements IngestPlugin {
18+
19+
@Override
20+
public Map<String, Processor.Factory> getProcessors(Processor.Parameters parameters) {
21+
return Map.of(ExampleRepeatProcessor.TYPE, new ExampleRepeatProcessor.Factory());
22+
}
23+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package org.elasticsearch.example.customprocessor;
2+
3+
import org.elasticsearch.ingest.AbstractProcessor;
4+
import org.elasticsearch.ingest.ConfigurationUtils;
5+
import org.elasticsearch.ingest.IngestDocument;
6+
import org.elasticsearch.ingest.Processor;
7+
8+
import java.util.Map;
9+
10+
/**
11+
* Example of adding an ingest processor with a plugin.
12+
*/
13+
public class ExampleRepeatProcessor extends AbstractProcessor {
14+
public static final String TYPE = "repeat";
15+
public static final String FIELD_KEY_NAME = "field";
16+
17+
private final String field;
18+
19+
ExampleRepeatProcessor(String tag, String description, String field) {
20+
super(tag, description);
21+
this.field = field;
22+
}
23+
24+
@Override
25+
public IngestDocument execute(IngestDocument document) {
26+
Object val = document.getFieldValue(field, Object.class, true);
27+
28+
if (val instanceof String string) {
29+
String repeated = string.concat(string);
30+
document.setFieldValue(field, repeated);
31+
}
32+
return document;
33+
}
34+
35+
@Override
36+
public String getType() {
37+
return TYPE;
38+
}
39+
40+
public static class Factory implements Processor.Factory {
41+
42+
@Override
43+
public ExampleRepeatProcessor create(
44+
Map<String, Processor.Factory> registry,
45+
String tag,
46+
String description,
47+
Map<String, Object> config
48+
) {
49+
String field = ConfigurationUtils.readStringProperty(TYPE, tag, config, FIELD_KEY_NAME);
50+
return new ExampleRepeatProcessor(tag, description, field);
51+
}
52+
}
53+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
package org.elasticsearch.example.customprocessor;
9+
10+
import com.carrotsearch.randomizedtesting.annotations.Name;
11+
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
12+
import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate;
13+
import org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase;
14+
15+
/**
16+
* {@link ExampleProcessorClientYamlTestSuiteIT} executes the plugin's REST API integration tests.
17+
* <p>
18+
* The tests can be executed using the command: ./gradlew :custom-processor:yamlRestTest
19+
* <p>
20+
* This class extends {@link ESClientYamlSuiteTestCase}, which takes care of parsing the YAML files
21+
* located in the src/yamlRestTest/resources/rest-api-spec/test/ directory and validates them against the
22+
* custom REST API definition files located in src/yamlRestTest/resources/rest-api-spec/api/.
23+
* <p>
24+
* Once validated, {@link ESClientYamlSuiteTestCase} executes the REST tests against a single node
25+
* integration cluster which has the plugin already installed by the Gradle build script.
26+
* </p>
27+
*/
28+
public class ExampleProcessorClientYamlTestSuiteIT extends ESClientYamlSuiteTestCase {
29+
30+
public ExampleProcessorClientYamlTestSuiteIT(@Name("yaml") ClientYamlTestCandidate testCandidate) {
31+
super(testCandidate);
32+
}
33+
34+
@ParametersFactory
35+
public static Iterable<Object[]> parameters() throws Exception {
36+
// The test executes all the test candidates by default
37+
// see ESClientYamlSuiteTestCase.REST_TESTS_SUITE
38+
return ESClientYamlSuiteTestCase.createParameters();
39+
}
40+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"Custom processor is present":
2+
- do:
3+
ingest.put_pipeline:
4+
id: pipeline1
5+
body: >
6+
{
7+
"processors": [
8+
{
9+
"repeat" : {
10+
"field": "test"
11+
}
12+
}
13+
]
14+
}
15+
- match: { acknowledged: true }
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
setup:
2+
- do:
3+
ingest.put_pipeline:
4+
id: pipeline1
5+
body: >
6+
{
7+
"processors": [
8+
{
9+
"repeat" : {
10+
"field": "to_repeat"
11+
}
12+
}
13+
]
14+
}
15+
---
16+
teardown:
17+
- do:
18+
ingest.delete_pipeline:
19+
id: pipeline1
20+
ignore: 404
21+
22+
- do:
23+
indices.delete:
24+
index: index1
25+
ignore: 404
26+
---
27+
"Process document":
28+
# index a document with field to be processed
29+
- do:
30+
index:
31+
id: doc1
32+
index: index1
33+
pipeline: pipeline1
34+
body: { to_repeat: "foo" }
35+
- match: { result: "created" }
36+
37+
# validate document is processed
38+
- do:
39+
get:
40+
index: index1
41+
id: doc1
42+
- match: { _source: { to_repeat: "foofoo" } }
43+
---
44+
"Does not process document without field":
45+
# index a document without field to be processed
46+
- do:
47+
index:
48+
id: doc1
49+
index: index1
50+
pipeline: pipeline1
51+
body: { field1: "foo" }
52+
- match: { result: "created" }
53+
54+
# validate document is not processed
55+
- do:
56+
get:
57+
index: index1
58+
id: doc1
59+
- match: { _source: { field1: "foo" } }

0 commit comments

Comments
 (0)