Browse Source

Support `ignore_malformed` in boolean fields (#93239)

This PR enables the `ignore_malformed`parameter to be accepted as an option in 
boolean field mappings. Support for synthetic source is not added yet, so if
`ignore_malformed` is set to true, synthetic source isn't supported.

Closes #89542
Hritik Kumar 2 years ago
parent
commit
f5af004117

+ 7 - 0
docs/changelog/93239.yaml

@@ -0,0 +1,7 @@
+pr: 93239
+summary: Support ignore malformed in boolean fields
+area: Search
+type: bug
+issues:
+ - 89542
+

+ 1 - 0
docs/reference/mapping/params/ignore-malformed.asciidoc

@@ -49,6 +49,7 @@ PUT my-index-000001/_doc/2
 The `ignore_malformed` setting is currently supported by the following <<mapping-types,mapping types>>:
 The `ignore_malformed` setting is currently supported by the following <<mapping-types,mapping types>>:
 
 
 <<number>>::         `long`, `integer`, `short`, `byte`, `double`, `float`, `half_float`, `scaled_float`
 <<number>>::         `long`, `integer`, `short`, `byte`, `double`, `float`, `half_float`, `scaled_float`
+<<boolean>>::        `boolean`
 <<date>>::           `date`
 <<date>>::           `date`
 <<date_nanos>>::     `date_nanos`
 <<date_nanos>>::     `date_nanos`
 <<geo-point>>::     `geo_point` for lat/lon points
 <<geo-point>>::     `geo_point` for lat/lon points

+ 8 - 0
docs/reference/mapping/types/boolean.asciidoc

@@ -179,6 +179,14 @@ The following parameters are accepted by `boolean` fields:
     enabled can still be queried using term or range-based queries,
     enabled can still be queried using term or range-based queries,
     albeit slower.
     albeit slower.
 
 
+<<ignore-malformed, `ignore_malformed`>>::
+
+    Trying to index the wrong data type into a field throws an exception by
+    default, and rejects the whole document. If this parameter is set to true,
+    it allows the exception to be ignored. The malformed field is not indexed,
+    but other fields in the document are processed normally. Accepts `true` or `false`.
+    Note that this cannot be set if the `script` parameter is used.
+
 <<null-value,`null_value`>>::
 <<null-value,`null_value`>>::
 
 
     Accepts any of the true or false values listed above. The value is
     Accepts any of the true or false values listed above. The value is

+ 37 - 8
server/src/main/java/org/elasticsearch/index/mapper/BooleanFieldMapper.java

@@ -22,6 +22,7 @@ import org.apache.lucene.search.Query;
 import org.apache.lucene.search.TermRangeQuery;
 import org.apache.lucene.search.TermRangeQuery;
 import org.apache.lucene.util.BytesRef;
 import org.apache.lucene.util.BytesRef;
 import org.elasticsearch.Version;
 import org.elasticsearch.Version;
+import org.elasticsearch.common.Explicit;
 import org.elasticsearch.common.lucene.Lucene;
 import org.elasticsearch.common.lucene.Lucene;
 import org.elasticsearch.common.xcontent.support.XContentMapValues;
 import org.elasticsearch.common.xcontent.support.XContentMapValues;
 import org.elasticsearch.core.Booleans;
 import org.elasticsearch.core.Booleans;
@@ -84,7 +85,7 @@ public class BooleanFieldMapper extends FieldMapper {
         private final Parameter<Boolean> docValues = Parameter.docValuesParam(m -> toType(m).hasDocValues, true);
         private final Parameter<Boolean> docValues = Parameter.docValuesParam(m -> toType(m).hasDocValues, true);
         private final Parameter<Boolean> indexed = Parameter.indexParam(m -> toType(m).indexed, true);
         private final Parameter<Boolean> indexed = Parameter.indexParam(m -> toType(m).indexed, true);
         private final Parameter<Boolean> stored = Parameter.storeParam(m -> toType(m).stored, false);
         private final Parameter<Boolean> stored = Parameter.storeParam(m -> toType(m).stored, false);
-
+        private final Parameter<Explicit<Boolean>> ignoreMalformed;
         private final Parameter<Boolean> nullValue = new Parameter<>(
         private final Parameter<Boolean> nullValue = new Parameter<>(
             "null_value",
             "null_value",
             false,
             false,
@@ -104,17 +105,23 @@ public class BooleanFieldMapper extends FieldMapper {
 
 
         private final Version indexCreatedVersion;
         private final Version indexCreatedVersion;
 
 
-        public Builder(String name, ScriptCompiler scriptCompiler, Version indexCreatedVersion) {
+        public Builder(String name, ScriptCompiler scriptCompiler, boolean ignoreMalformedByDefault, Version indexCreatedVersion) {
             super(name);
             super(name);
             this.scriptCompiler = Objects.requireNonNull(scriptCompiler);
             this.scriptCompiler = Objects.requireNonNull(scriptCompiler);
             this.indexCreatedVersion = Objects.requireNonNull(indexCreatedVersion);
             this.indexCreatedVersion = Objects.requireNonNull(indexCreatedVersion);
-            this.script.precludesParameters(nullValue);
+            this.ignoreMalformed = Parameter.explicitBoolParam(
+                "ignore_malformed",
+                true,
+                m -> toType(m).ignoreMalformed,
+                ignoreMalformedByDefault
+            );
+            this.script.precludesParameters(ignoreMalformed, nullValue);
             addScriptValidation(script, indexed, docValues);
             addScriptValidation(script, indexed, docValues);
         }
         }
 
 
         @Override
         @Override
         protected Parameter<?>[] getParameters() {
         protected Parameter<?>[] getParameters() {
-            return new Parameter<?>[] { meta, docValues, indexed, nullValue, stored, script, onScriptError };
+            return new Parameter<?>[] { meta, docValues, indexed, nullValue, stored, script, onScriptError, ignoreMalformed };
         }
         }
 
 
         @Override
         @Override
@@ -148,7 +155,7 @@ public class BooleanFieldMapper extends FieldMapper {
     private static final Version MINIMUM_COMPATIBILITY_VERSION = Version.fromString("5.0.0");
     private static final Version MINIMUM_COMPATIBILITY_VERSION = Version.fromString("5.0.0");
 
 
     public static final TypeParser PARSER = new TypeParser(
     public static final TypeParser PARSER = new TypeParser(
-        (n, c) -> new Builder(n, c.scriptCompiler(), c.indexVersionCreated()),
+        (n, c) -> new Builder(n, c.scriptCompiler(), IGNORE_MALFORMED_SETTING.get(c.getSettings()), c.indexVersionCreated()),
         MINIMUM_COMPATIBILITY_VERSION
         MINIMUM_COMPATIBILITY_VERSION
     );
     );
 
 
@@ -367,6 +374,8 @@ public class BooleanFieldMapper extends FieldMapper {
     private final FieldValues<Boolean> scriptValues;
     private final FieldValues<Boolean> scriptValues;
     private final ScriptCompiler scriptCompiler;
     private final ScriptCompiler scriptCompiler;
     private final Version indexCreatedVersion;
     private final Version indexCreatedVersion;
+    private final Explicit<Boolean> ignoreMalformed;
+    private final boolean ignoreMalformedByDefault;
 
 
     protected BooleanFieldMapper(
     protected BooleanFieldMapper(
         String simpleName,
         String simpleName,
@@ -384,6 +393,8 @@ public class BooleanFieldMapper extends FieldMapper {
         this.scriptValues = builder.scriptValues();
         this.scriptValues = builder.scriptValues();
         this.scriptCompiler = builder.scriptCompiler;
         this.scriptCompiler = builder.scriptCompiler;
         this.indexCreatedVersion = builder.indexCreatedVersion;
         this.indexCreatedVersion = builder.indexCreatedVersion;
+        this.ignoreMalformed = builder.ignoreMalformed.getValue();
+        this.ignoreMalformedByDefault = builder.ignoreMalformed.getDefaultValue().value();
     }
     }
 
 
     @Override
     @Override
@@ -409,7 +420,15 @@ public class BooleanFieldMapper extends FieldMapper {
                 value = nullValue;
                 value = nullValue;
             }
             }
         } else {
         } else {
-            value = context.parser().booleanValue();
+            try {
+                value = context.parser().booleanValue();
+            } catch (IllegalArgumentException e) {
+                if (ignoreMalformed.value() && context.parser().currentToken().isValue()) {
+                    context.addIgnoredField(mappedFieldType.name());
+                } else {
+                    throw e;
+                }
+            }
         }
         }
         indexValue(context, value);
         indexValue(context, value);
     }
     }
@@ -443,7 +462,12 @@ public class BooleanFieldMapper extends FieldMapper {
 
 
     @Override
     @Override
     public FieldMapper.Builder getMergeBuilder() {
     public FieldMapper.Builder getMergeBuilder() {
-        return new Builder(simpleName(), scriptCompiler, indexCreatedVersion).init(this);
+        return new Builder(simpleName(), scriptCompiler, ignoreMalformedByDefault, indexCreatedVersion).init(this);
+    }
+
+    @Override
+    public boolean ignoreMalformed() {
+        return ignoreMalformed.value();
     }
     }
 
 
     @Override
     @Override
@@ -466,7 +490,12 @@ public class BooleanFieldMapper extends FieldMapper {
                 "field [" + name() + "] of type [" + typeName() + "] doesn't support synthetic source because it declares copy_to"
                 "field [" + name() + "] of type [" + typeName() + "] doesn't support synthetic source because it declares copy_to"
             );
             );
         }
         }
-        return new SortedNumericDocValuesSyntheticFieldLoader(name(), simpleName(), false) {
+        if (ignoreMalformed.value()) {
+            throw new IllegalArgumentException(
+                "field [" + name() + "] of type [" + typeName() + "] doesn't support synthetic source because it ignores malformed values"
+            );
+        }
+        return new SortedNumericDocValuesSyntheticFieldLoader(name(), simpleName(), ignoreMalformed.value()) {
             @Override
             @Override
             protected void writeValue(XContentBuilder b, long value) throws IOException {
             protected void writeValue(XContentBuilder b, long value) throws IOException {
                 b.value(value == 1);
                 b.value(value == 1);

+ 8 - 1
server/src/main/java/org/elasticsearch/index/mapper/DynamicFieldsBuilder.java

@@ -355,8 +355,15 @@ final class DynamicFieldsBuilder {
 
 
         @Override
         @Override
         public void newDynamicBooleanField(DocumentParserContext context, String name) throws IOException {
         public void newDynamicBooleanField(DocumentParserContext context, String name) throws IOException {
+            Settings settings = context.indexSettings().getSettings();
+            boolean ignoreMalformed = FieldMapper.IGNORE_MALFORMED_SETTING.get(settings);
             createDynamicField(
             createDynamicField(
-                new BooleanFieldMapper.Builder(name, ScriptCompiler.NONE, context.indexSettings().getIndexVersionCreated()),
+                new BooleanFieldMapper.Builder(
+                    name,
+                    ScriptCompiler.NONE,
+                    ignoreMalformed,
+                    context.indexSettings().getIndexVersionCreated()
+                ),
                 context
                 context
             );
             );
         }
         }

+ 11 - 3
server/src/test/java/org/elasticsearch/index/mapper/BooleanFieldMapperTests.java

@@ -202,14 +202,19 @@ public class BooleanFieldMapperTests extends MapperTestCase {
         assertThat(e.getMessage(), equalTo("Failed to parse mapping: Field [null_value] cannot be set in conjunction with field [script]"));
         assertThat(e.getMessage(), equalTo("Failed to parse mapping: Field [null_value] cannot be set in conjunction with field [script]"));
     }
     }
 
 
+    @Override
+    protected List<ExampleMalformedValue> exampleMalformedValues() {
+        return List.of(exampleMalformedValue("a").errorMatches("Failed to parse value [a] as only [true] or [false] are allowed."));
+    }
+
     @Override
     @Override
     protected boolean supportsIgnoreMalformed() {
     protected boolean supportsIgnoreMalformed() {
-        return false;
+        return true;
     }
     }
 
 
     @Override
     @Override
     protected SyntheticSourceSupport syntheticSourceSupport(boolean ignoreMalformed) {
     protected SyntheticSourceSupport syntheticSourceSupport(boolean ignoreMalformed) {
-        assertFalse("boolean doesn't support ignore_malformed", ignoreMalformed);
+        assumeFalse("boolean doesn't support ignore_malformed with synthetic source", ignoreMalformed);
         return new SyntheticSourceSupport() {
         return new SyntheticSourceSupport() {
             Boolean nullValue = usually() ? null : randomBoolean();
             Boolean nullValue = usually() ? null : randomBoolean();
 
 
@@ -247,8 +252,11 @@ public class BooleanFieldMapperTests extends MapperTestCase {
                     new SyntheticSourceInvalidExample(
                     new SyntheticSourceInvalidExample(
                         equalTo("field [field] of type [boolean] doesn't support synthetic source because it doesn't have doc values"),
                         equalTo("field [field] of type [boolean] doesn't support synthetic source because it doesn't have doc values"),
                         b -> b.field("type", "boolean").field("doc_values", false)
                         b -> b.field("type", "boolean").field("doc_values", false)
+                    ),
+                    new SyntheticSourceInvalidExample(
+                        equalTo("field [field] of type [boolean] doesn't support synthetic source because it ignores malformed values"),
+                        b -> b.field("type", "boolean").field("ignore_malformed", true)
                     )
                     )
-                // If boolean had ignore_malformed we'd fail to index here
                 );
                 );
             }
             }
         };
         };

+ 1 - 1
server/src/test/java/org/elasticsearch/index/mapper/BooleanScriptFieldTypeTests.java

@@ -319,7 +319,7 @@ public class BooleanScriptFieldTypeTests extends AbstractNonTextScriptFieldTypeT
     }
     }
 
 
     public void testDualingQueries() throws IOException {
     public void testDualingQueries() throws IOException {
-        BooleanFieldMapper ootb = new BooleanFieldMapper.Builder("foo", ScriptCompiler.NONE, Version.CURRENT).build(
+        BooleanFieldMapper ootb = new BooleanFieldMapper.Builder("foo", ScriptCompiler.NONE, false, Version.CURRENT).build(
             MapperBuilderContext.root(false)
             MapperBuilderContext.root(false)
         );
         );
         try (Directory directory = newDirectory(); RandomIndexWriter iw = new RandomIndexWriter(random(), directory)) {
         try (Directory directory = newDirectory(); RandomIndexWriter iw = new RandomIndexWriter(random(), directory)) {

+ 3 - 1
server/src/test/java/org/elasticsearch/index/mapper/FieldAliasMapperValidationTests.java

@@ -158,7 +158,9 @@ public class FieldAliasMapperValidationTests extends ESTestCase {
     }
     }
 
 
     private static FieldMapper createFieldMapper(String parent, String name) {
     private static FieldMapper createFieldMapper(String parent, String name) {
-        return new BooleanFieldMapper.Builder(name, ScriptCompiler.NONE, Version.CURRENT).build(new MapperBuilderContext(parent, false));
+        return new BooleanFieldMapper.Builder(name, ScriptCompiler.NONE, false, Version.CURRENT).build(
+            new MapperBuilderContext(parent, false)
+        );
     }
     }
 
 
     private static ObjectMapper createObjectMapper(String name) {
     private static ObjectMapper createObjectMapper(String name) {

+ 2 - 2
server/src/test/java/org/elasticsearch/index/mapper/MultiFieldsSerializationTests.java

@@ -37,10 +37,10 @@ public class MultiFieldsSerializationTests extends ESTestCase {
         sortedNames.sort(Comparator.naturalOrder());
         sortedNames.sort(Comparator.naturalOrder());
 
 
         for (String name : names) {
         for (String name : names) {
-            builder.add(new BooleanFieldMapper.Builder(name, ScriptCompiler.NONE, Version.CURRENT));
+            builder.add(new BooleanFieldMapper.Builder(name, ScriptCompiler.NONE, false, Version.CURRENT));
         }
         }
 
 
-        Mapper.Builder root = new BooleanFieldMapper.Builder("root", ScriptCompiler.NONE, Version.CURRENT);
+        Mapper.Builder root = new BooleanFieldMapper.Builder("root", ScriptCompiler.NONE, false, Version.CURRENT);
         FieldMapper.MultiFields multiFields = builder.build(root, MapperBuilderContext.root(false));
         FieldMapper.MultiFields multiFields = builder.build(root, MapperBuilderContext.root(false));
 
 
         String serialized = Strings.toString(multiFields);
         String serialized = Strings.toString(multiFields);