From 2004ba09eec681175a94f289bb472399db08aa86 Mon Sep 17 00:00:00 2001 From: Mingshi Liu <113382730+mingshl@users.noreply.github.com> Date: Tue, 11 Jul 2023 11:00:20 -0700 Subject: [PATCH] [Search pipelines] Add Global Ignore_failure options for Processors (#8373) * Add Global Ingore_failure options for Processors Signed-off-by: Mingshi Liu * add changelog Signed-off-by: Mingshi Liu * Add ignore_failure to 40_rename_response Signed-off-by: Mingshi Liu * Change Boolean to boolean and refactor AbstractProcessor Signed-off-by: Mingshi Liu * rename to isIgnoreFailure and add tests Signed-off-by: Mingshi Liu * rename to isIgnoreFailure and add tests Signed-off-by: Mingshi Liu * add ignoreFailure to runSearchPhaseResultsTransformer Signed-off-by: Mingshi Liu * fix filter query and change log warn message Signed-off-by: Mingshi Liu * Add test on matching each processor stat Signed-off-by: Mingshi Liu * Add test on matching each processor stat Signed-off-by: Mingshi Liu * remove extra spaces and words Signed-off-by: Mingshi Liu * use IGNORE_FAILURE_KEY Signed-off-by: Mingshi Liu --------- Signed-off-by: Mingshi Liu --- CHANGELOG.md | 1 + .../common/FilterQueryRequestProcessor.java | 13 +- .../common/RenameFieldResponseProcessor.java | 24 +- .../common/ScriptRequestProcessor.java | 8 +- .../FilterQueryRequestProcessorTests.java | 6 +- .../RenameFieldResponseProcessorTests.java | 7 +- .../common/ScriptRequestProcessorTests.java | 11 +- .../search_pipeline/40_rename_response.yml | 33 +- .../opensearch/ingest/ConfigurationUtils.java | 8 +- .../search/pipeline}/AbstractProcessor.java | 15 +- .../opensearch/search/pipeline/Pipeline.java | 50 ++- .../search/pipeline/PipelineWithMetrics.java | 6 +- .../opensearch/search/pipeline/Processor.java | 6 + .../pipeline/SearchPipelineServiceTests.java | 379 +++++++++++++----- 14 files changed, 440 insertions(+), 127 deletions(-) rename {modules/search-pipeline-common/src/main/java/org/opensearch/search/pipeline/common => server/src/main/java/org/opensearch/search/pipeline}/AbstractProcessor.java (60%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f0d733bd7c55..0de0d823aa764 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,6 +101,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Adds mock implementation for TelemetryPlugin ([#7545](https://github.com/opensearch-project/OpenSearch/issues/7545)) - Support transport action names when registering NamedRoutes ([#7957](https://github.com/opensearch-project/OpenSearch/pull/7957)) - Create concept of persistent ThreadContext headers that are unstashable ([#8291]()https://github.com/opensearch-project/OpenSearch/pull/8291) +- [Search pipelines] Add Global Ignore_failure options for Processors ([#8373](https://github.com/opensearch-project/OpenSearch/pull/8373)) - Enable Partial Flat Object ([#7997](https://github.com/opensearch-project/OpenSearch/pull/7997)) - Add jdk.incubator.vector module support for JDK 20+ ([#8601](https://github.com/opensearch-project/OpenSearch/pull/8601)) diff --git a/modules/search-pipeline-common/src/main/java/org/opensearch/search/pipeline/common/FilterQueryRequestProcessor.java b/modules/search-pipeline-common/src/main/java/org/opensearch/search/pipeline/common/FilterQueryRequestProcessor.java index d8862aa59cede..c776181a64b26 100644 --- a/modules/search-pipeline-common/src/main/java/org/opensearch/search/pipeline/common/FilterQueryRequestProcessor.java +++ b/modules/search-pipeline-common/src/main/java/org/opensearch/search/pipeline/common/FilterQueryRequestProcessor.java @@ -21,6 +21,7 @@ import org.opensearch.ingest.ConfigurationUtils; import org.opensearch.search.builder.SearchSourceBuilder; import org.opensearch.search.pipeline.Processor; +import org.opensearch.search.pipeline.AbstractProcessor; import org.opensearch.search.pipeline.SearchRequestProcessor; import java.io.InputStream; @@ -53,12 +54,13 @@ public String getType() { /** * Constructor that takes a filter query. * - * @param tag processor tag - * @param description processor description + * @param tag processor tag + * @param description processor description + * @param ignoreFailure option to ignore failure * @param filterQuery the query that will be added as a filter to incoming queries */ - public FilterQueryRequestProcessor(String tag, String description, QueryBuilder filterQuery) { - super(tag, description); + FilterQueryRequestProcessor(String tag, String description, boolean ignoreFailure, QueryBuilder filterQuery) { + super(tag, description, ignoreFailure); this.filterQuery = filterQuery; } @@ -101,6 +103,7 @@ public FilterQueryRequestProcessor create( Map> processorFactories, String tag, String description, + boolean ignoreFailure, Map config, PipelineContext pipelineContext ) throws Exception { @@ -114,7 +117,7 @@ public FilterQueryRequestProcessor create( XContentParser parser = XContentType.JSON.xContent() .createParser(namedXContentRegistry, LoggingDeprecationHandler.INSTANCE, stream) ) { - return new FilterQueryRequestProcessor(tag, description, parseInnerQueryBuilder(parser)); + return new FilterQueryRequestProcessor(tag, description, ignoreFailure, parseInnerQueryBuilder(parser)); } } } diff --git a/modules/search-pipeline-common/src/main/java/org/opensearch/search/pipeline/common/RenameFieldResponseProcessor.java b/modules/search-pipeline-common/src/main/java/org/opensearch/search/pipeline/common/RenameFieldResponseProcessor.java index c8b3c06a71562..c410895304f76 100644 --- a/modules/search-pipeline-common/src/main/java/org/opensearch/search/pipeline/common/RenameFieldResponseProcessor.java +++ b/modules/search-pipeline-common/src/main/java/org/opensearch/search/pipeline/common/RenameFieldResponseProcessor.java @@ -19,6 +19,7 @@ import org.opensearch.ingest.ConfigurationUtils; import org.opensearch.search.SearchHit; import org.opensearch.search.pipeline.Processor; +import org.opensearch.search.pipeline.AbstractProcessor; import org.opensearch.search.pipeline.SearchRequestProcessor; import org.opensearch.search.pipeline.SearchResponseProcessor; @@ -41,14 +42,22 @@ public class RenameFieldResponseProcessor extends AbstractProcessor implements S /** * Constructor that takes a target field to rename and the new name * - * @param tag processor tag - * @param description processor description - * @param oldField name of field to be renamed - * @param newField name of field that will replace the old field + * @param tag processor tag + * @param description processor description + * @param ignoreFailure option to ignore failure + * @param oldField name of field to be renamed + * @param newField name of field that will replace the old field * @param ignoreMissing if true, do not throw error if oldField does not exist within search response */ - public RenameFieldResponseProcessor(String tag, String description, String oldField, String newField, boolean ignoreMissing) { - super(tag, description); + public RenameFieldResponseProcessor( + String tag, + String description, + boolean ignoreFailure, + String oldField, + String newField, + boolean ignoreMissing + ) { + super(tag, description, ignoreFailure); this.oldField = oldField; this.newField = newField; this.ignoreMissing = ignoreMissing; @@ -140,13 +149,14 @@ public RenameFieldResponseProcessor create( Map> processorFactories, String tag, String description, + boolean ignoreFailure, Map config, PipelineContext pipelineContext ) throws Exception { String oldField = ConfigurationUtils.readStringProperty(TYPE, tag, config, "field"); String newField = ConfigurationUtils.readStringProperty(TYPE, tag, config, "target_field"); boolean ignoreMissing = ConfigurationUtils.readBooleanProperty(TYPE, tag, config, "ignore_missing", false); - return new RenameFieldResponseProcessor(tag, description, oldField, newField, ignoreMissing); + return new RenameFieldResponseProcessor(tag, description, ignoreFailure, oldField, newField, ignoreMissing); } } } diff --git a/modules/search-pipeline-common/src/main/java/org/opensearch/search/pipeline/common/ScriptRequestProcessor.java b/modules/search-pipeline-common/src/main/java/org/opensearch/search/pipeline/common/ScriptRequestProcessor.java index 43ab3d4622d6b..e100fe0e3edce 100644 --- a/modules/search-pipeline-common/src/main/java/org/opensearch/search/pipeline/common/ScriptRequestProcessor.java +++ b/modules/search-pipeline-common/src/main/java/org/opensearch/search/pipeline/common/ScriptRequestProcessor.java @@ -25,6 +25,7 @@ import org.opensearch.script.ScriptType; import org.opensearch.script.SearchScript; import org.opensearch.search.pipeline.Processor; +import org.opensearch.search.pipeline.AbstractProcessor; import org.opensearch.search.pipeline.SearchRequestProcessor; import org.opensearch.search.pipeline.common.helpers.SearchRequestMap; @@ -54,6 +55,7 @@ public final class ScriptRequestProcessor extends AbstractProcessor implements S * * @param tag The processor's tag. * @param description The processor's description. + * @param ignoreFailure The option to ignore failure * @param script The {@link Script} to execute. * @param precompiledSearchScript The {@link Script} precompiled * @param scriptService The {@link ScriptService} used to execute the script. @@ -61,11 +63,12 @@ public final class ScriptRequestProcessor extends AbstractProcessor implements S ScriptRequestProcessor( String tag, String description, + boolean ignoreFailure, Script script, @Nullable SearchScript precompiledSearchScript, ScriptService scriptService ) { - super(tag, description); + super(tag, description, ignoreFailure); this.script = script; this.precompiledSearchScript = precompiledSearchScript; this.scriptService = scriptService; @@ -146,6 +149,7 @@ public ScriptRequestProcessor create( Map> registry, String processorTag, String description, + boolean ignoreFailure, Map config, PipelineContext pipelineContext ) throws Exception { @@ -174,7 +178,7 @@ public ScriptRequestProcessor create( } catch (ScriptException e) { throw newConfigurationException(TYPE, processorTag, null, e); } - return new ScriptRequestProcessor(processorTag, description, script, searchScript, scriptService); + return new ScriptRequestProcessor(processorTag, description, ignoreFailure, script, searchScript, scriptService); } } } diff --git a/modules/search-pipeline-common/src/test/java/org/opensearch/search/pipeline/common/FilterQueryRequestProcessorTests.java b/modules/search-pipeline-common/src/test/java/org/opensearch/search/pipeline/common/FilterQueryRequestProcessorTests.java index ecf746af556a2..576660d05b96d 100644 --- a/modules/search-pipeline-common/src/test/java/org/opensearch/search/pipeline/common/FilterQueryRequestProcessorTests.java +++ b/modules/search-pipeline-common/src/test/java/org/opensearch/search/pipeline/common/FilterQueryRequestProcessorTests.java @@ -23,7 +23,7 @@ public class FilterQueryRequestProcessorTests extends AbstractBuilderTestCase { public void testFilterQuery() throws Exception { QueryBuilder filterQuery = new TermQueryBuilder("field", "value"); - FilterQueryRequestProcessor filterQueryRequestProcessor = new FilterQueryRequestProcessor(null, null, filterQuery); + FilterQueryRequestProcessor filterQueryRequestProcessor = new FilterQueryRequestProcessor(null, null, false, filterQuery); QueryBuilder incomingQuery = new TermQueryBuilder("text", "foo"); SearchSourceBuilder source = new SearchSourceBuilder().query(incomingQuery); SearchRequest request = new SearchRequest().source(source); @@ -39,13 +39,13 @@ public void testFilterQuery() throws Exception { public void testFactory() throws Exception { FilterQueryRequestProcessor.Factory factory = new FilterQueryRequestProcessor.Factory(this.xContentRegistry()); Map configMap = new HashMap<>(Map.of("query", Map.of("term", Map.of("field", "value")))); - FilterQueryRequestProcessor processor = factory.create(Collections.emptyMap(), null, null, configMap, null); + FilterQueryRequestProcessor processor = factory.create(Collections.emptyMap(), null, null, false, configMap, null); assertEquals(new TermQueryBuilder("field", "value"), processor.filterQuery); // Missing "query" parameter: expectThrows( IllegalArgumentException.class, - () -> factory.create(Collections.emptyMap(), null, null, Collections.emptyMap(), null) + () -> factory.create(Collections.emptyMap(), null, null, false, Collections.emptyMap(), null) ); } } diff --git a/modules/search-pipeline-common/src/test/java/org/opensearch/search/pipeline/common/RenameFieldResponseProcessorTests.java b/modules/search-pipeline-common/src/test/java/org/opensearch/search/pipeline/common/RenameFieldResponseProcessorTests.java index 7f3a2acfbdc08..b7d1510d065ae 100644 --- a/modules/search-pipeline-common/src/test/java/org/opensearch/search/pipeline/common/RenameFieldResponseProcessorTests.java +++ b/modules/search-pipeline-common/src/test/java/org/opensearch/search/pipeline/common/RenameFieldResponseProcessorTests.java @@ -58,6 +58,7 @@ public void testRenameResponse() throws Exception { RenameFieldResponseProcessor renameFieldResponseProcessor = new RenameFieldResponseProcessor( null, null, + false, "field 0", "new field", false @@ -74,6 +75,7 @@ public void testRenameResponseWithMapping() throws Exception { RenameFieldResponseProcessor renameFieldResponseProcessor = new RenameFieldResponseProcessor( null, null, + false, "field 0", "new field", true @@ -97,6 +99,7 @@ public void testMissingField() throws Exception { RenameFieldResponseProcessor renameFieldResponseProcessor = new RenameFieldResponseProcessor( null, null, + false, "field", "new field", false @@ -115,7 +118,7 @@ public void testFactory() throws Exception { config.put("target_field", newField); RenameFieldResponseProcessor.Factory factory = new RenameFieldResponseProcessor.Factory(); - RenameFieldResponseProcessor processor = factory.create(Collections.emptyMap(), null, null, config, null); + RenameFieldResponseProcessor processor = factory.create(Collections.emptyMap(), null, null, false, config, null); assertEquals(processor.getType(), "rename_field"); assertEquals(processor.getOldField(), oldField); assertEquals(processor.getNewField(), newField); @@ -123,7 +126,7 @@ public void testFactory() throws Exception { expectThrows( OpenSearchParseException.class, - () -> factory.create(Collections.emptyMap(), null, null, Collections.emptyMap(), null) + () -> factory.create(Collections.emptyMap(), null, null, false, Collections.emptyMap(), null) ); } } diff --git a/modules/search-pipeline-common/src/test/java/org/opensearch/search/pipeline/common/ScriptRequestProcessorTests.java b/modules/search-pipeline-common/src/test/java/org/opensearch/search/pipeline/common/ScriptRequestProcessorTests.java index 2fb3b2345e7e2..df383e778c7ba 100644 --- a/modules/search-pipeline-common/src/test/java/org/opensearch/search/pipeline/common/ScriptRequestProcessorTests.java +++ b/modules/search-pipeline-common/src/test/java/org/opensearch/search/pipeline/common/ScriptRequestProcessorTests.java @@ -82,7 +82,7 @@ public void setupScripting() { } public void testScriptingWithoutPrecompiledScriptFactory() throws Exception { - ScriptRequestProcessor processor = new ScriptRequestProcessor(randomAlphaOfLength(10), null, script, null, scriptService); + ScriptRequestProcessor processor = new ScriptRequestProcessor(randomAlphaOfLength(10), null, false, script, null, scriptService); SearchRequest searchRequest = new SearchRequest(); searchRequest.source(createSearchSourceBuilder()); @@ -92,7 +92,14 @@ public void testScriptingWithoutPrecompiledScriptFactory() throws Exception { } public void testScriptingWithPrecompiledIngestScript() throws Exception { - ScriptRequestProcessor processor = new ScriptRequestProcessor(randomAlphaOfLength(10), null, script, searchScript, scriptService); + ScriptRequestProcessor processor = new ScriptRequestProcessor( + randomAlphaOfLength(10), + null, + false, + script, + searchScript, + scriptService + ); SearchRequest searchRequest = new SearchRequest(); searchRequest.source(createSearchSourceBuilder()); diff --git a/modules/search-pipeline-common/src/yamlRestTest/resources/rest-api-spec/test/search_pipeline/40_rename_response.yml b/modules/search-pipeline-common/src/yamlRestTest/resources/rest-api-spec/test/search_pipeline/40_rename_response.yml index 3b705f9bd5356..0528440f7584d 100644 --- a/modules/search-pipeline-common/src/yamlRestTest/resources/rest-api-spec/test/search_pipeline/40_rename_response.yml +++ b/modules/search-pipeline-common/src/yamlRestTest/resources/rest-api-spec/test/search_pipeline/40_rename_response.yml @@ -63,6 +63,26 @@ teardown: } - match: { acknowledged: true } + - do: + search_pipeline.put: + id: "my_pipeline_4" + body: > + { + "description": "test pipeline with ignore missing false and ignore failure true", + "response_processors": [ + { + "rename_field": + { + "field": "aa", + "target_field": "b", + "ignore_missing": false, + "ignore_failure": true + } + } + ] + } + - match: { acknowledged: true } + - do: indices.create: index: test @@ -119,8 +139,8 @@ teardown: - match: { hits.total.value: 1 } - match: {hits.hits.0._source: { "a": "foo" } } - # Pipeline with ignore_missing set to true - # Should still pass even though index does not contain field + # Pipeline with ignore_missing set to false + # Should throw illegal_argument_exception - do: catch: bad_request search: @@ -128,6 +148,15 @@ teardown: search_pipeline: "my_pipeline_3" - match: { error.type: "illegal_argument_exception" } + # Pipeline with ignore_missing set to false and ignore_failure set to true + # Should return while catching error + - do: + search: + index: test + search_pipeline: "my_pipeline_4" + - match: { hits.total.value: 1 } + - match: {hits.hits.0._source: { "a": "foo" } } + # No source, using stored_fields - do: search: diff --git a/server/src/main/java/org/opensearch/ingest/ConfigurationUtils.java b/server/src/main/java/org/opensearch/ingest/ConfigurationUtils.java index dc41d1985fe42..b212e67a59434 100644 --- a/server/src/main/java/org/opensearch/ingest/ConfigurationUtils.java +++ b/server/src/main/java/org/opensearch/ingest/ConfigurationUtils.java @@ -67,6 +67,7 @@ public final class ConfigurationUtils { public static final String TAG_KEY = "tag"; public static final String DESCRIPTION_KEY = "description"; + public static final String IGNORE_FAILURE_KEY = "ignore_failure"; private ConfigurationUtils() {} @@ -194,7 +195,7 @@ public static String readOptionalStringOrIntProperty( return readStringOrInt(processorType, processorTag, propertyName, value); } - public static Boolean readBooleanProperty( + public static boolean readBooleanProperty( String processorType, String processorTag, Map configuration, @@ -214,7 +215,7 @@ private static Boolean readBoolean(String processorType, String processorTag, St return null; } if (value instanceof Boolean) { - return (Boolean) value; + return (boolean) value; } throw newConfigurationException( processorType, @@ -530,10 +531,11 @@ public static Processor readProcessor( ) throws Exception { String tag = ConfigurationUtils.readOptionalStringProperty(null, null, config, TAG_KEY); String description = ConfigurationUtils.readOptionalStringProperty(null, tag, config, DESCRIPTION_KEY); + boolean ignoreFailure = ConfigurationUtils.readBooleanProperty(null, null, config, IGNORE_FAILURE_KEY, false); Script conditionalScript = extractConditional(config); Processor.Factory factory = processorFactories.get(type); + if (factory != null) { - boolean ignoreFailure = ConfigurationUtils.readBooleanProperty(null, null, config, "ignore_failure", false); List> onFailureProcessorConfigs = ConfigurationUtils.readOptionalList( null, null, diff --git a/modules/search-pipeline-common/src/main/java/org/opensearch/search/pipeline/common/AbstractProcessor.java b/server/src/main/java/org/opensearch/search/pipeline/AbstractProcessor.java similarity index 60% rename from modules/search-pipeline-common/src/main/java/org/opensearch/search/pipeline/common/AbstractProcessor.java rename to server/src/main/java/org/opensearch/search/pipeline/AbstractProcessor.java index e62497cb54db5..d197051c5952b 100644 --- a/modules/search-pipeline-common/src/main/java/org/opensearch/search/pipeline/common/AbstractProcessor.java +++ b/server/src/main/java/org/opensearch/search/pipeline/AbstractProcessor.java @@ -6,20 +6,20 @@ * compatible open source license. */ -package org.opensearch.search.pipeline.common; - -import org.opensearch.search.pipeline.Processor; +package org.opensearch.search.pipeline; /** * Base class for common processor behavior. */ -abstract class AbstractProcessor implements Processor { +public abstract class AbstractProcessor implements Processor { private final String tag; private final String description; + private final boolean ignoreFailure; - protected AbstractProcessor(String tag, String description) { + protected AbstractProcessor(String tag, String description, boolean ignoreFailure) { this.tag = tag; this.description = description; + this.ignoreFailure = ignoreFailure; } @Override @@ -31,4 +31,9 @@ public String getTag() { public String getDescription() { return description; } + + @Override + public boolean isIgnoreFailure() { + return ignoreFailure; + } } diff --git a/server/src/main/java/org/opensearch/search/pipeline/Pipeline.java b/server/src/main/java/org/opensearch/search/pipeline/Pipeline.java index 92826eee5a4f4..5ada11b59556b 100644 --- a/server/src/main/java/org/opensearch/search/pipeline/Pipeline.java +++ b/server/src/main/java/org/opensearch/search/pipeline/Pipeline.java @@ -8,6 +8,8 @@ package org.opensearch.search.pipeline; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.opensearch.action.search.SearchPhaseContext; import org.opensearch.action.search.SearchPhaseResults; import org.opensearch.action.search.SearchRequest; @@ -32,6 +34,9 @@ class Pipeline { public static final String REQUEST_PROCESSORS_KEY = "request_processors"; public static final String RESPONSE_PROCESSORS_KEY = "response_processors"; public static final String PHASE_PROCESSORS_KEY = "phase_results_processors"; + + private static final Logger logger = LogManager.getLogger(Pipeline.class); + private final String id; private final String description; private final Integer version; @@ -132,7 +137,18 @@ SearchRequest transformRequest(SearchRequest request) throws SearchPipelineProce request = processor.processRequest(request); } catch (Exception e) { onRequestProcessorFailed(processor); - throw e; + if (processor.isIgnoreFailure()) { + logger.warn( + "The exception from request processor [" + + processor.getType() + + "] in the search pipeline [" + + id + + "] was ignored", + e + ); + } else { + throw e; + } } finally { long took = TimeUnit.NANOSECONDS.toMillis(relativeTimeSupplier.getAsLong() - start); afterRequestProcessor(processor, took); @@ -161,7 +177,18 @@ SearchResponse transformResponse(SearchRequest request, SearchResponse response) response = processor.processResponse(request, response); } catch (Exception e) { onResponseProcessorFailed(processor); - throw e; + if (processor.isIgnoreFailure()) { + logger.warn( + "The exception from response processor [" + + processor.getType() + + "] in the search pipeline [" + + id + + "] was ignored", + e + ); + } else { + throw e; + } } finally { long took = TimeUnit.NANOSECONDS.toMillis(relativeTimeSupplier.getAsLong() - start); afterResponseProcessor(processor, took); @@ -184,12 +211,27 @@ void runSearchPhaseResultsTransformer( String currentPhase, String nextPhase ) throws SearchPipelineProcessingException { - try { for (SearchPhaseResultsProcessor searchPhaseResultsProcessor : searchPhaseResultsProcessors) { if (currentPhase.equals(searchPhaseResultsProcessor.getBeforePhase().getName()) && nextPhase.equals(searchPhaseResultsProcessor.getAfterPhase().getName())) { - searchPhaseResultsProcessor.process(searchPhaseResult, context); + try { + searchPhaseResultsProcessor.process(searchPhaseResult, context); + } catch (Exception e) { + if (searchPhaseResultsProcessor.isIgnoreFailure()) { + logger.warn( + "The exception from search phase results processor [" + + searchPhaseResultsProcessor.getType() + + "] in the search pipeline [" + + id + + "] was ignored", + e + ); + } else { + throw e; + } + } + } } } catch (RuntimeException e) { diff --git a/server/src/main/java/org/opensearch/search/pipeline/PipelineWithMetrics.java b/server/src/main/java/org/opensearch/search/pipeline/PipelineWithMetrics.java index 060894a37e5ed..3e473f070d9c6 100644 --- a/server/src/main/java/org/opensearch/search/pipeline/PipelineWithMetrics.java +++ b/server/src/main/java/org/opensearch/search/pipeline/PipelineWithMetrics.java @@ -22,6 +22,7 @@ import java.util.function.LongSupplier; import static org.opensearch.ingest.ConfigurationUtils.TAG_KEY; +import static org.opensearch.ingest.ConfigurationUtils.IGNORE_FAILURE_KEY; import static org.opensearch.ingest.Pipeline.DESCRIPTION_KEY; import static org.opensearch.ingest.Pipeline.VERSION_KEY; @@ -150,8 +151,11 @@ private static List readProcessors( } Map config = (Map) entry.getValue(); String tag = ConfigurationUtils.readOptionalStringProperty(null, null, config, TAG_KEY); + boolean ignoreFailure = ConfigurationUtils.readBooleanProperty(null, null, config, IGNORE_FAILURE_KEY, false); String description = ConfigurationUtils.readOptionalStringProperty(null, tag, config, DESCRIPTION_KEY); - processors.add(processorFactories.get(type).create(processorFactories, tag, description, config, pipelineContext)); + processors.add( + processorFactories.get(type).create(processorFactories, tag, description, ignoreFailure, config, pipelineContext) + ); if (config.isEmpty() == false) { String processorName = type; if (tag != null) { diff --git a/server/src/main/java/org/opensearch/search/pipeline/Processor.java b/server/src/main/java/org/opensearch/search/pipeline/Processor.java index cc96132479c74..fb33f46acada4 100644 --- a/server/src/main/java/org/opensearch/search/pipeline/Processor.java +++ b/server/src/main/java/org/opensearch/search/pipeline/Processor.java @@ -43,6 +43,11 @@ public interface Processor { */ String getDescription(); + /** + * Gets the setting of ignoreFailure of a processor. + */ + boolean isIgnoreFailure(); + /** * A factory that knows how to construct a processor based on a map of maps. */ @@ -63,6 +68,7 @@ T create( Map> processorFactories, String tag, String description, + boolean ignoreFailure, Map config, PipelineContext pipelineContext ) throws Exception; diff --git a/server/src/test/java/org/opensearch/search/pipeline/SearchPipelineServiceTests.java b/server/src/test/java/org/opensearch/search/pipeline/SearchPipelineServiceTests.java index 84f39e4bdab42..0103d4c677b00 100644 --- a/server/src/test/java/org/opensearch/search/pipeline/SearchPipelineServiceTests.java +++ b/server/src/test/java/org/opensearch/search/pipeline/SearchPipelineServiceTests.java @@ -78,16 +78,16 @@ public class SearchPipelineServiceTests extends OpenSearchTestCase { private static final SearchPipelinePlugin DUMMY_PLUGIN = new SearchPipelinePlugin() { @Override public Map> getRequestProcessors(Parameters parameters) { - return Map.of("foo", (factories, tag, description, config, ctx) -> null); + return Map.of("foo", (factories, tag, description, ignoreFailure, config, ctx) -> null); } public Map> getResponseProcessors(Parameters parameters) { - return Map.of("bar", (factories, tag, description, config, ctx) -> null); + return Map.of("bar", (factories, tag, description, ignoreFailure, config, ctx) -> null); } @Override public Map> getSearchPhaseResultsProcessors(Parameters parameters) { - return Map.of("zoe", (factories, tag, description, config, ctx) -> null); + return Map.of("zoe", (factories, tag, description, ignoreFailure, config, ctx) -> null); } }; @@ -208,39 +208,15 @@ public void testResolveIndexDefaultPipeline() throws Exception { assertEquals(5, pipelinedRequest.source().size()); } - private static abstract class FakeProcessor implements Processor { - private final String type; - private final String tag; - private final String description; - - protected FakeProcessor(String type, String tag, String description) { - this.type = type; - this.tag = tag; - this.description = description; - } - - @Override - public String getType() { - return type; - } - - @Override - public String getTag() { - return tag; - } - - @Override - public String getDescription() { - return description; - } - } - - private static class FakeRequestProcessor extends FakeProcessor implements SearchRequestProcessor { + private static class FakeRequestProcessor extends AbstractProcessor implements SearchRequestProcessor { private final Consumer executor; - public FakeRequestProcessor(String type, String tag, String description, Consumer executor) { - super(type, tag, description); + private final String type; + + public FakeRequestProcessor(String type, String tag, String description, boolean ignoreFailure, Consumer executor) { + super(tag, description, ignoreFailure); this.executor = executor; + this.type = type; } @Override @@ -248,14 +224,30 @@ public SearchRequest processRequest(SearchRequest request) throws Exception { executor.accept(request); return request; } + + /* + * Gets the type of processor + */ + @Override + public String getType() { + return this.type; + } } - private static class FakeResponseProcessor extends FakeProcessor implements SearchResponseProcessor { + private static class FakeResponseProcessor extends AbstractProcessor implements SearchResponseProcessor { private final Consumer executor; + private String type; - public FakeResponseProcessor(String type, String tag, String description, Consumer executor) { - super(type, tag, description); + public FakeResponseProcessor( + String type, + String tag, + String description, + boolean ignoreFailure, + Consumer executor + ) { + super(tag, description, ignoreFailure); this.executor = executor; + this.type = type; } @Override @@ -263,19 +255,31 @@ public SearchResponse processResponse(SearchRequest request, SearchResponse resp executor.accept(response); return response; } + + /** + * Gets the type of processor + */ + @Override + public String getType() { + return this.type; + } } - private static class FakeSearchPhaseResultsProcessor extends FakeProcessor implements SearchPhaseResultsProcessor { + private static class FakeSearchPhaseResultsProcessor extends AbstractProcessor implements SearchPhaseResultsProcessor { private Consumer querySearchResultConsumer; + private String type; + public FakeSearchPhaseResultsProcessor( String type, String tag, String description, + boolean ignoreFailure, Consumer querySearchResultConsumer ) { - super(type, tag, description); + super(tag, description, ignoreFailure); this.querySearchResultConsumer = querySearchResultConsumer; + this.type = type; } @Override @@ -297,30 +301,45 @@ public SearchPhaseName getBeforePhase() { public SearchPhaseName getAfterPhase() { return SearchPhaseName.FETCH; } + + /** + * Gets the type of processor + */ + @Override + public String getType() { + return this.type; + } } private SearchPipelineService createWithProcessors() { Map> requestProcessors = new HashMap<>(); - requestProcessors.put("scale_request_size", (processorFactories, tag, description, config, ctx) -> { + requestProcessors.put("scale_request_size", (processorFactories, tag, description, ignoreFailure, config, ctx) -> { float scale = ((Number) config.remove("scale")).floatValue(); return new FakeRequestProcessor( "scale_request_size", tag, description, + ignoreFailure, req -> req.source().size((int) (req.source().size() * scale)) ); }); Map> responseProcessors = new HashMap<>(); - responseProcessors.put("fixed_score", (processorFactories, tag, description, config, ctx) -> { + responseProcessors.put("fixed_score", (processorFactories, tag, description, ignoreFailure, config, ctx) -> { float score = ((Number) config.remove("score")).floatValue(); - return new FakeResponseProcessor("fixed_score", tag, description, rsp -> rsp.getHits().forEach(h -> h.score(score))); + return new FakeResponseProcessor( + "fixed_score", + tag, + description, + ignoreFailure, + rsp -> rsp.getHits().forEach(h -> h.score(score)) + ); }); Map> searchPhaseProcessors = new HashMap<>(); - searchPhaseProcessors.put("max_score", (processorFactories, tag, description, config, context) -> { + searchPhaseProcessors.put("max_score", (processorFactories, tag, description, ignoreFailure, config, context) -> { final float finalScore = config.containsKey("score") ? ((Number) config.remove("score")).floatValue() : 100f; final Consumer querySearchResultConsumer = (result) -> result.queryResult().topDocs().maxScore = finalScore; - return new FakeSearchPhaseResultsProcessor("max_score", tag, description, querySearchResultConsumer); + return new FakeSearchPhaseResultsProcessor("max_score", tag, description, ignoreFailure, querySearchResultConsumer); }); return createWithProcessors(requestProcessors, responseProcessors, searchPhaseProcessors); @@ -893,7 +912,7 @@ public void testInfo() { } public void testExceptionOnPipelineCreation() { - Map> badFactory = Map.of("bad_factory", (pf, t, f, c, ctx) -> { + Map> badFactory = Map.of("bad_factory", (pf, t, i, f, c, ctx) -> { throw new RuntimeException(); }); SearchPipelineService searchPipelineService = createWithProcessors(badFactory, Collections.emptyMap(), Collections.emptyMap()); @@ -910,12 +929,12 @@ public void testExceptionOnPipelineCreation() { } public void testExceptionOnRequestProcessing() { - SearchRequestProcessor throwingRequestProcessor = new FakeRequestProcessor("throwing_request", null, null, r -> { + SearchRequestProcessor throwingRequestProcessor = new FakeRequestProcessor("throwing_request", null, null, false, r -> { throw new RuntimeException(); }); Map> throwingRequestProcessorFactory = Map.of( "throwing_request", - (pf, t, f, c, ctx) -> throwingRequestProcessor + (pf, t, i, f, c, ctx) -> throwingRequestProcessor ); SearchPipelineService searchPipelineService = createWithProcessors( @@ -935,12 +954,12 @@ public void testExceptionOnRequestProcessing() { } public void testExceptionOnResponseProcessing() throws Exception { - SearchResponseProcessor throwingResponseProcessor = new FakeResponseProcessor("throwing_response", null, null, r -> { + SearchResponseProcessor throwingResponseProcessor = new FakeResponseProcessor("throwing_response", null, null, false, r -> { throw new RuntimeException(); }); Map> throwingResponseProcessorFactory = Map.of( "throwing_response", - (pf, t, f, c, ctx) -> throwingResponseProcessor + (pf, t, i, f, c, ctx) -> throwingResponseProcessor ); SearchPipelineService searchPipelineService = createWithProcessors( @@ -962,61 +981,103 @@ public void testExceptionOnResponseProcessing() throws Exception { expectThrows(SearchPipelineProcessingException.class, () -> pipelinedRequest.transformResponse(response)); } + public void testCatchExceptionOnRequestProcessing() throws IllegalAccessException { + SearchRequestProcessor throwingRequestProcessor = new FakeRequestProcessor("throwing_request", null, null, true, r -> { + throw new RuntimeException(); + }); + Map> throwingRequestProcessorFactory = Map.of( + "throwing_request", + (pf, t, i, f, c, ctx) -> throwingRequestProcessor + ); + + SearchPipelineService searchPipelineService = createWithProcessors( + throwingRequestProcessorFactory, + Collections.emptyMap(), + Collections.emptyMap() + ); + + Map pipelineSourceMap = new HashMap<>(); + pipelineSourceMap.put(Pipeline.REQUEST_PROCESSORS_KEY, List.of(Map.of("throwing_request", Collections.emptyMap()))); + + SearchSourceBuilder sourceBuilder = SearchSourceBuilder.searchSource().searchPipelineSource(pipelineSourceMap); + SearchRequest searchRequest = new SearchRequest().source(sourceBuilder); + + // Caught Exception thrown when processing the request and produced warn level logging message + try (MockLogAppender mockAppender = MockLogAppender.createForLoggers(LogManager.getLogger(Pipeline.class))) { + mockAppender.addExpectation( + new MockLogAppender.SeenEventExpectation( + "test1", + Pipeline.class.getCanonicalName(), + Level.WARN, + "The exception from request processor [throwing_request] in the search pipeline [_ad_hoc_pipeline] was ignored" + ) + ); + PipelinedRequest pipelinedRequest = searchPipelineService.resolvePipeline(searchRequest); + mockAppender.assertAllExpectationsMatched(); + } + } + + public void testCatchExceptionOnResponseProcessing() throws Exception { + SearchResponseProcessor throwingResponseProcessor = new FakeResponseProcessor("throwing_response", null, null, true, r -> { + throw new RuntimeException(); + }); + Map> throwingResponseProcessorFactory = Map.of( + "throwing_response", + (pf, t, i, f, c, ctx) -> throwingResponseProcessor + ); + + SearchPipelineService searchPipelineService = createWithProcessors( + Collections.emptyMap(), + throwingResponseProcessorFactory, + Collections.emptyMap() + ); + + Map pipelineSourceMap = new HashMap<>(); + pipelineSourceMap.put(Pipeline.RESPONSE_PROCESSORS_KEY, List.of(Map.of("throwing_response", Collections.emptyMap()))); + + SearchSourceBuilder sourceBuilder = SearchSourceBuilder.searchSource().size(100).searchPipelineSource(pipelineSourceMap); + SearchRequest searchRequest = new SearchRequest().source(sourceBuilder); + + PipelinedRequest pipelinedRequest = searchPipelineService.resolvePipeline(searchRequest); + + SearchResponse response = new SearchResponse(null, null, 0, 0, 0, 0, null, null); + + // Caught Exception thrown when processing response and produced warn level logging message + try (MockLogAppender mockAppender = MockLogAppender.createForLoggers(LogManager.getLogger(Pipeline.class))) { + mockAppender.addExpectation( + new MockLogAppender.SeenEventExpectation( + "test1", + Pipeline.class.getCanonicalName(), + Level.WARN, + "The exception from response processor [throwing_response] in the search pipeline [_ad_hoc_pipeline] was ignored" + ) + ); + pipelinedRequest.transformResponse(response); + mockAppender.assertAllExpectationsMatched(); + } + } + public void testStats() throws Exception { - SearchRequestProcessor throwingRequestProcessor = new FakeRequestProcessor("throwing_request", "1", null, r -> { + SearchRequestProcessor throwingRequestProcessor = new FakeRequestProcessor("throwing_request", "1", null, false, r -> { throw new RuntimeException(); }); Map> requestProcessors = Map.of( "successful_request", - (pf, t, f, c, ctx) -> new FakeRequestProcessor("successful_request", "2", null, r -> {}), + (pf, t, i, f, c, ctx) -> new FakeRequestProcessor("successful_request", "2", null, false, r -> {}), "throwing_request", - (pf, t, f, c, ctx) -> throwingRequestProcessor + (pf, t, i, f, c, ctx) -> throwingRequestProcessor ); - SearchResponseProcessor throwingResponseProcessor = new FakeResponseProcessor("throwing_response", "3", null, r -> { + SearchResponseProcessor throwingResponseProcessor = new FakeResponseProcessor("throwing_response", "3", null, false, r -> { throw new RuntimeException(); }); Map> responseProcessors = Map.of( "successful_response", - (pf, t, f, c, ctx) -> new FakeResponseProcessor("successful_response", "4", null, r -> {}), + (pf, t, i, f, c, ctx) -> new FakeResponseProcessor("successful_response", "4", null, false, r -> {}), "throwing_response", - (pf, t, f, c, ctx) -> throwingResponseProcessor + (pf, t, i, f, c, ctx) -> throwingResponseProcessor ); - SearchPipelineService searchPipelineService = createWithProcessors(requestProcessors, responseProcessors, Collections.emptyMap()); - SearchPipelineMetadata metadata = new SearchPipelineMetadata( - Map.of( - "good_response_pipeline", - new PipelineConfiguration( - "good_response_pipeline", - new BytesArray("{\"response_processors\" : [ { \"successful_response\": {} } ] }"), - XContentType.JSON - ), - "bad_response_pipeline", - new PipelineConfiguration( - "bad_response_pipeline", - new BytesArray("{\"response_processors\" : [ { \"throwing_response\": {} } ] }"), - XContentType.JSON - ), - "good_request_pipeline", - new PipelineConfiguration( - "good_request_pipeline", - new BytesArray("{\"request_processors\" : [ { \"successful_request\": {} } ] }"), - XContentType.JSON - ), - "bad_request_pipeline", - new PipelineConfiguration( - "bad_request_pipeline", - new BytesArray("{\"request_processors\" : [ { \"throwing_request\": {} } ] }"), - XContentType.JSON - ) - ) - ); - ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); - ClusterState previousState = clusterState; - clusterState = ClusterState.builder(clusterState) - .metadata(Metadata.builder().putCustom(SearchPipelineMetadata.TYPE, metadata)) - .build(); - searchPipelineService.applyClusterState(new ClusterChangedEvent("", clusterState, previousState)); + SearchPipelineService searchPipelineService = getSearchPipelineService(requestProcessors, responseProcessors); SearchRequest request = new SearchRequest(); SearchResponse response = new SearchResponse(null, null, 0, 0, 0, 0, null, null); @@ -1079,6 +1140,142 @@ public void testStats() throws Exception { } } + public void testStatsEnabledIgnoreFailure() throws Exception { + SearchRequestProcessor throwingRequestProcessor = new FakeRequestProcessor("throwing_request", "1", null, true, r -> { + throw new RuntimeException(); + }); + Map> requestProcessorsEnableIgnoreFailure = Map.of( + "successful_request", + (pf, t, i, f, c, ctx) -> new FakeRequestProcessor("successful_request", "2", null, true, r -> {}), + "throwing_request", + (pf, t, i, f, c, ctx) -> throwingRequestProcessor + ); + SearchResponseProcessor throwingResponseProcessor = new FakeResponseProcessor("throwing_response", "3", null, true, r -> { + throw new RuntimeException(); + }); + Map> responseProcessorsEnableIgnoreFailure = Map.of( + "successful_response", + (pf, t, i, f, c, ctx) -> new FakeResponseProcessor("successful_response", "4", null, true, r -> {}), + "throwing_response", + (pf, t, i, f, c, ctx) -> throwingResponseProcessor + ); + + SearchPipelineService searchPipelineService = getSearchPipelineService( + requestProcessorsEnableIgnoreFailure, + responseProcessorsEnableIgnoreFailure + ); + + SearchRequest request = new SearchRequest(); + SearchResponse response = new SearchResponse(null, null, 0, 0, 0, 0, null, null); + + searchPipelineService.resolvePipeline(request.pipeline("good_request_pipeline")).transformResponse(response); + // Caught Exception here + searchPipelineService.resolvePipeline(request.pipeline("bad_request_pipeline")).transformResponse(response); + searchPipelineService.resolvePipeline(request.pipeline("good_response_pipeline")).transformResponse(response); + // Caught Exception here + searchPipelineService.resolvePipeline(request.pipeline("bad_response_pipeline")).transformResponse(response); + + // when ignoreFailure enabled, the search pipelines will all succeed. + SearchPipelineStats stats = searchPipelineService.stats(); + assertPipelineStats(stats.getTotalRequestStats(), 2, 0); + assertPipelineStats(stats.getTotalResponseStats(), 2, 0); + + for (SearchPipelineStats.PerPipelineStats perPipelineStats : stats.getPipelineStats()) { + SearchPipelineStats.PipelineDetailStats detailStats = stats.getPerPipelineProcessorStats() + .get(perPipelineStats.getPipelineId()); + switch (perPipelineStats.getPipelineId()) { + case "good_request_pipeline": + assertPipelineStats(perPipelineStats.getRequestStats(), 1, 0); + assertPipelineStats(perPipelineStats.getResponseStats(), 0, 0); + assertEquals(1, detailStats.requestProcessorStats().size()); + assertEquals(0, detailStats.responseProcessorStats().size()); + assertEquals("successful_request:2", detailStats.requestProcessorStats().get(0).getProcessorName()); + assertEquals("successful_request", detailStats.requestProcessorStats().get(0).getProcessorType()); + assertPipelineStats(detailStats.requestProcessorStats().get(0).getStats(), 1, 0); + break; + case "bad_request_pipeline": + // pipeline succeed when ignore failure is true + assertPipelineStats(perPipelineStats.getRequestStats(), 1, 0); + assertPipelineStats(perPipelineStats.getResponseStats(), 0, 0); + assertEquals(1, detailStats.requestProcessorStats().size()); + assertEquals(0, detailStats.responseProcessorStats().size()); + assertEquals("throwing_request:1", detailStats.requestProcessorStats().get(0).getProcessorName()); + assertEquals("throwing_request", detailStats.requestProcessorStats().get(0).getProcessorType()); + // processor stats got 1 count and 1 failed + assertPipelineStats(detailStats.requestProcessorStats().get(0).getStats(), 1, 1); + break; + case "good_response_pipeline": + assertPipelineStats(perPipelineStats.getRequestStats(), 0, 0); + assertPipelineStats(perPipelineStats.getResponseStats(), 1, 0); + assertEquals(0, detailStats.requestProcessorStats().size()); + assertEquals(1, detailStats.responseProcessorStats().size()); + assertEquals("successful_response:4", detailStats.responseProcessorStats().get(0).getProcessorName()); + assertEquals("successful_response", detailStats.responseProcessorStats().get(0).getProcessorType()); + assertPipelineStats(detailStats.responseProcessorStats().get(0).getStats(), 1, 0); + break; + case "bad_response_pipeline": + // pipeline succeed when ignore failure is true + assertPipelineStats(perPipelineStats.getRequestStats(), 0, 0); + assertPipelineStats(perPipelineStats.getResponseStats(), 1, 0); + assertEquals(0, detailStats.requestProcessorStats().size()); + assertEquals(1, detailStats.responseProcessorStats().size()); + assertEquals("throwing_response:3", detailStats.responseProcessorStats().get(0).getProcessorName()); + assertEquals("throwing_response", detailStats.responseProcessorStats().get(0).getProcessorType()); + // processor stats got 1 count and 1 failed + assertPipelineStats(detailStats.responseProcessorStats().get(0).getStats(), 1, 1); + break; + } + } + + } + + private SearchPipelineService getSearchPipelineService( + Map> requestProcessorsEnableIgnoreFailure, + Map> responseProcessorsEnableIgnoreFailure + ) { + SearchPipelineService searchPipelineService = createWithProcessors( + requestProcessorsEnableIgnoreFailure, + responseProcessorsEnableIgnoreFailure, + Collections.emptyMap() + ); + + SearchPipelineMetadata metadata = new SearchPipelineMetadata( + Map.of( + "good_response_pipeline", + new PipelineConfiguration( + "good_response_pipeline", + new BytesArray("{\"response_processors\" : [ { \"successful_response\": {} } ] }"), + XContentType.JSON + ), + "bad_response_pipeline", + new PipelineConfiguration( + "bad_response_pipeline", + new BytesArray("{\"response_processors\" : [ { \"throwing_response\": {} } ] }"), + XContentType.JSON + ), + "good_request_pipeline", + new PipelineConfiguration( + "good_request_pipeline", + new BytesArray("{\"request_processors\" : [ { \"successful_request\": {} } ] }"), + XContentType.JSON + ), + "bad_request_pipeline", + new PipelineConfiguration( + "bad_request_pipeline", + new BytesArray("{\"request_processors\" : [ { \"throwing_request\": {} } ] }"), + XContentType.JSON + ) + ) + ); + ClusterState clusterState = ClusterState.builder(new ClusterName("_name")).build(); + ClusterState previousState = clusterState; + clusterState = ClusterState.builder(clusterState) + .metadata(Metadata.builder().putCustom(SearchPipelineMetadata.TYPE, metadata)) + .build(); + searchPipelineService.applyClusterState(new ClusterChangedEvent("", clusterState, previousState)); + return searchPipelineService; + } + private static void assertPipelineStats(OperationStats stats, long count, long failed) { assertEquals(stats.getCount(), count); assertEquals(stats.getFailedCount(), failed); @@ -1086,11 +1283,11 @@ private static void assertPipelineStats(OperationStats stats, long count, long f public void testAdHocRejectingProcessor() { String processorType = "ad_hoc_rejecting"; - Map> requestProcessorFactories = Map.of(processorType, (pf, t, d, c, ctx) -> { + Map> requestProcessorFactories = Map.of(processorType, (pf, t, d, i, c, ctx) -> { if (ctx.getPipelineSource() == Processor.PipelineSource.SEARCH_REQUEST) { throw new IllegalArgumentException(processorType + " cannot be created as part of a pipeline defined in a search request"); } - return new FakeRequestProcessor(processorType, t, d, r -> {}); + return new FakeRequestProcessor(processorType, t, d, i, r -> {}); }); SearchPipelineService searchPipelineService = createWithProcessors(