Browse Source

Synthetic `_source`: `ignore_malformed` for `ip` (#90038)

This adds synthetic `_source` support for `ip` fields with
`ignore_malfored` set to `true`. We save the field values in hidden
stored field, just like we do for `ignore_above` keyword fields. Then we
load them at load time.
Nik Everett 3 years ago
parent
commit
d0cf9f5034

+ 5 - 0
docs/changelog/90038.yaml

@@ -0,0 +1,5 @@
+pr: 90038
+summary: "Synthetic `_source`: `ignore_malformed` for `ip`"
+area: TSDB
+type: enhancement
+issues: []

+ 1 - 2
docs/reference/mapping/types/ip.asciidoc

@@ -155,8 +155,7 @@ GET my-index-000001/_search
 ==== Synthetic source
 `ip` fields support <<synthetic-source,synthetic `_source`>> in their default
 configuration. Synthetic `_source` cannot be used together with
-<<ignore-malformed,`ignore_malformed`>>, <<copy-to,`copy_to`>>, or with
-<<doc-values,`doc_values`>> disabled.
+<<copy-to,`copy_to`>> or with <<doc-values,`doc_values`>> disabled.
 
 Synthetic source always sorts `ip` fields and removes duplicates. For example:
 [source,console,id=synthetic-source-ip-example]

+ 96 - 0
rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/get/100_synthetic_source.yml

@@ -583,3 +583,99 @@ _source filtering:
       _source:
         kwd: foo
   - is_false: fields
+
+---
+ip with ignore_malformed:
+  - skip:
+      version: " - 8.5.99"
+      reason: introduced in 8.6.0
+
+  - do:
+      indices.create:
+        index: test
+        body:
+          mappings:
+            _source:
+              mode: synthetic
+            properties:
+              ip:
+                type: ip
+                ignore_malformed: true
+
+  - do:
+      index:
+        index:   test
+        id:      1
+        refresh: true
+        body:
+          ip: 192.168.0.1
+  - do:
+      index:
+        index:   test
+        id:      2
+        refresh: true
+        body:
+          ip: garbage
+  - do:
+      index:
+        index:   test
+        id:      3
+        refresh: true
+        body:
+          ip:
+            - 10.10.1.1
+            - 192.8.1.2
+            - hot garbage
+            - 7
+  - do:
+      catch: "/failed to parse field \\[ip\\] of type \\[ip\\] in document with id '4'. Preview of field's value: '\\{object=wow\\}'/"
+      index:
+        index:   test
+        id:      4
+        refresh: true
+        body:
+          ip:
+            object: wow
+
+  - do:
+      get:
+        index: test
+        id:    1
+  - match: {_index: "test"}
+  - match: {_id: "1"}
+  - match: {_version: 1}
+  - match: {found: true}
+  - match:
+      _source:
+        ip: 192.168.0.1
+  - is_false: fields
+
+  - do:
+      get:
+        index: test
+        id:    2
+  - match: {_index: "test"}
+  - match: {_id: "2"}
+  - match: {_version: 1}
+  - match: {found: true}
+  - match:
+      _source:
+        ip: garbage
+  - is_false: fields
+
+  - do:
+      get:
+        index: test
+        id:    3
+  - match: {_index: "test"}
+  - match: {_id: "3"}
+  - match: {_version: 1}
+  - match: {found: true}
+  - match:
+      _source:
+        ip:
+          - 10.10.1.1
+          - 192.8.1.2
+          - hot garbage # fields saved by ignore_malformed are sorted after doc values
+          - 7
+  - is_false: fields

+ 93 - 0
rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/get/110_ignore_malformed.yml

@@ -0,0 +1,93 @@
+---
+ip:
+  - skip:
+      version: " - 8.4.99"
+      reason: introduced in 8.5.0
+
+  - do:
+      indices.create:
+        index: test
+        body:
+          mappings:
+            properties:
+              ip:
+                type: ip
+                ignore_malformed: true
+
+  - do:
+      index:
+        index:   test
+        id:      1
+        refresh: true
+        body:
+          ip: 192.168.0.1
+  - do:
+      index:
+        index:   test
+        id:      2
+        refresh: true
+        body:
+          ip: garbage
+  - do:
+      index:
+        index:   test
+        id:      3
+        refresh: true
+        body:
+          ip:
+            - 10.10.1.1
+            - 192.8.1.2
+            - hot garbage
+            - 7
+  - do:
+      catch: "/failed to parse field \\[ip\\] of type \\[ip\\] in document with id '4'. Preview of field's value: '\\{object=wow\\}'/"
+      index:
+        index:   test
+        id:      4
+        refresh: true
+        body:
+          ip:
+            object: wow
+
+  - do:
+      get:
+        index: test
+        id:    1
+  - match: {_index: "test"}
+  - match: {_id: "1"}
+  - match: {_version: 1}
+  - match: {found: true}
+  - match:
+      _source:
+        ip: 192.168.0.1
+  - is_false: fields
+
+  - do:
+      get:
+        index: test
+        id:    2
+  - match: {_index: "test"}
+  - match: {_id: "2"}
+  - match: {_version: 1}
+  - match: {found: true}
+  - match:
+      _source:
+        ip: garbage
+  - is_false: fields
+
+  - do:
+      get:
+        index: test
+        id:    3
+  - match: {_index: "test"}
+  - match: {_id: "3"}
+  - match: {_version: 1}
+  - match: {found: true}
+  - match:
+      _source:
+        ip:
+          - 10.10.1.1
+          - 192.8.1.2
+          - hot garbage # fields saved by ignore_malformed are sorted after doc values
+          - 7
+  - is_false: fields

+ 197 - 0
server/src/main/java/org/elasticsearch/index/mapper/IgnoreMalformedStoredValues.java

@@ -0,0 +1,197 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.index.mapper;
+
+import org.apache.lucene.document.StoredField;
+import org.apache.lucene.util.BytesRef;
+import org.elasticsearch.common.util.ByteUtils;
+import org.elasticsearch.xcontent.XContentBuilder;
+import org.elasticsearch.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import static java.util.Collections.emptyList;
+
+/**
+ * Saves malformed values to stored fields so they can be loaded for synthetic
+ * {@code _source}.
+ */
+public abstract class IgnoreMalformedStoredValues {
+    /**
+     * Build a {@link StoredField} for the value on which the parser is
+     * currently positioned.
+     * <p>
+     * We try to use {@link StoredField}'s native types for fields where
+     * possible but we have to preserve more type information than
+     * stored fields support, so we encode all of those into stored fields'
+     * {@code byte[]} type and then encode type information in the first byte.
+     * </p>
+     */
+    public static StoredField storedField(String fieldName, XContentParser parser) throws IOException {
+        String name = name(fieldName);
+        return switch (parser.currentToken()) {
+            case VALUE_STRING -> new StoredField(name, parser.text());
+            case VALUE_NUMBER -> switch (parser.numberType()) {
+                    case INT -> new StoredField(name, parser.intValue());
+                    case LONG -> new StoredField(name, parser.longValue());
+                    case DOUBLE -> new StoredField(name, parser.doubleValue());
+                    case FLOAT -> new StoredField(name, parser.floatValue());
+                    case BIG_INTEGER -> new StoredField(name, encode((BigInteger) parser.numberValue()));
+                    case BIG_DECIMAL -> new StoredField(name, encode((BigDecimal) parser.numberValue()));
+                };
+            case VALUE_BOOLEAN -> new StoredField(name, new byte[] { parser.booleanValue() ? (byte) 't' : (byte) 'f' });
+            case VALUE_EMBEDDED_OBJECT -> new StoredField(name, encode(parser.binaryValue()));
+            default -> throw new IllegalArgumentException("synthetic _source doesn't support malformed objects");
+        };
+    }
+
+    /**
+     * Build a {@link IgnoreMalformedStoredValues} that never contains any values.
+     */
+    public static IgnoreMalformedStoredValues empty() {
+        return EMPTY;
+    }
+
+    /**
+     * Build a {@link IgnoreMalformedStoredValues} that loads from stored fields.
+     */
+    public static IgnoreMalformedStoredValues stored(String fieldName) {
+        return new Stored(fieldName);
+    }
+
+    /**
+     * A {@link Stream} mapping stored field paths to a place to put them
+     * so they can be included in the next document.
+     */
+    public abstract Stream<Map.Entry<String, SourceLoader.SyntheticFieldLoader.StoredFieldLoader>> storedFieldLoaders();
+
+    /**
+     * How many values has this field loaded for this document?
+     */
+    public abstract int count();
+
+    /**
+     * Write values for this document.
+     */
+    public abstract void write(XContentBuilder b) throws IOException;
+
+    private static final Empty EMPTY = new Empty();
+
+    private static class Empty extends IgnoreMalformedStoredValues {
+        @Override
+        public Stream<Map.Entry<String, SourceLoader.SyntheticFieldLoader.StoredFieldLoader>> storedFieldLoaders() {
+            return Stream.empty();
+        }
+
+        @Override
+        public int count() {
+            return 0;
+        }
+
+        @Override
+        public void write(XContentBuilder b) throws IOException {}
+    }
+
+    private static class Stored extends IgnoreMalformedStoredValues {
+        private final String fieldName;
+
+        private List<Object> values = emptyList();
+
+        Stored(String fieldName) {
+            this.fieldName = fieldName;
+        }
+
+        @Override
+        public Stream<Map.Entry<String, SourceLoader.SyntheticFieldLoader.StoredFieldLoader>> storedFieldLoaders() {
+            return Stream.of(Map.entry(name(fieldName), values -> this.values = values));
+        }
+
+        @Override
+        public int count() {
+            return values.size();
+        }
+
+        @Override
+        public void write(XContentBuilder b) throws IOException {
+            for (Object v : values) {
+                if (v instanceof BytesRef r) {
+                    decodeAndWrite(b, r);
+                } else {
+                    b.value(v);
+                }
+            }
+            values = emptyList();
+        }
+
+        private void decodeAndWrite(XContentBuilder b, BytesRef r) throws IOException {
+            switch (r.bytes[r.offset]) {
+                case 'b':
+                    b.value(r.bytes, r.offset + 1, r.length - 1);
+                    return;
+                case 'd':
+                    if (r.length < 5) {
+                        throw new IllegalArgumentException("Can't decode " + r);
+                    }
+                    int scale = ByteUtils.readIntLE(r.bytes, r.offset + 1);
+                    b.value(new BigDecimal(new BigInteger(r.bytes, r.offset + 5, r.length - 5), scale));
+                    return;
+                case 'f':
+                    if (r.length != 1) {
+                        throw new IllegalArgumentException("Can't decode " + r);
+                    }
+                    b.value(false);
+                    return;
+                case 'i':
+                    b.value(new BigInteger(r.bytes, r.offset + 1, r.length - 1));
+                    return;
+                case 't':
+                    if (r.length != 1) {
+                        throw new IllegalArgumentException("Can't decode " + r);
+                    }
+                    b.value(true);
+                    return;
+                default:
+                    throw new IllegalArgumentException("Can't decode " + r);
+            }
+        }
+    }
+
+    private static String name(String fieldName) {
+        return fieldName + "._ignore_malformed";
+    }
+
+    private static byte[] encode(BigInteger n) {
+        byte[] twosCompliment = n.toByteArray();
+        byte[] encoded = new byte[1 + twosCompliment.length];
+        encoded[0] = 'i';
+        System.arraycopy(twosCompliment, 0, encoded, 1, twosCompliment.length);
+        return encoded;
+    }
+
+    private static byte[] encode(BigDecimal n) {
+        byte[] twosCompliment = n.unscaledValue().toByteArray();
+        byte[] encoded = new byte[5 + twosCompliment.length];
+        encoded[0] = 'd';
+        ByteUtils.writeIntLE(n.scale(), encoded, 1);
+        System.arraycopy(twosCompliment, 0, encoded, 5, twosCompliment.length);
+        return encoded;
+    }
+
+    private static byte[] encode(byte[] b) {
+        byte[] encoded = new byte[1 + b.length];
+        encoded[0] = 'b';
+        System.arraycopy(b, 0, encoded, 1, b.length);
+        return encoded;
+    }
+}

+ 5 - 6
server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java

@@ -478,6 +478,10 @@ public class IpFieldMapper extends FieldMapper {
         } catch (IllegalArgumentException e) {
             if (ignoreMalformed) {
                 context.addIgnoredField(fieldType().name());
+                if (context.isSyntheticSource()) {
+                    // Save a copy of the field so synthetic source can load it
+                    context.doc().add(IgnoreMalformedStoredValues.storedField(name(), context.parser()));
+                }
                 return;
             } else {
                 throw e;
@@ -548,17 +552,12 @@ public class IpFieldMapper extends FieldMapper {
                 "field [" + name() + "] of type [" + typeName() + "] doesn't support synthetic source because it doesn't have doc values"
             );
         }
-        if (ignoreMalformed) {
-            throw new IllegalArgumentException(
-                "field [" + name() + "] of type [" + typeName() + "] doesn't support synthetic source because it ignores malformed ips"
-            );
-        }
         if (copyTo.copyToFields().isEmpty() != true) {
             throw new IllegalArgumentException(
                 "field [" + name() + "] of type [" + typeName() + "] doesn't support synthetic source because it declares copy_to"
             );
         }
-        return new SortedSetDocValuesSyntheticFieldLoader(name(), simpleName(), null) {
+        return new SortedSetDocValuesSyntheticFieldLoader(name(), simpleName(), null, ignoreMalformed) {
             @Override
             protected BytesRef convert(BytesRef value) {
                 byte[] bytes = Arrays.copyOfRange(value.bytes, value.offset, value.offset + value.length);

+ 2 - 1
server/src/main/java/org/elasticsearch/index/mapper/KeywordFieldMapper.java

@@ -1105,7 +1105,8 @@ public final class KeywordFieldMapper extends FieldMapper {
         return new SortedSetDocValuesSyntheticFieldLoader(
             name(),
             simpleName,
-            fieldType().ignoreAbove == Defaults.IGNORE_ABOVE ? null : originalName()
+            fieldType().ignoreAbove == Defaults.IGNORE_ABOVE ? null : originalName(),
+            false
         ) {
             @Override
             protected BytesRef convert(BytesRef value) {

+ 37 - 9
server/src/main/java/org/elasticsearch/index/mapper/SortedSetDocValuesSyntheticFieldLoader.java

@@ -43,21 +43,41 @@ public abstract class SortedSetDocValuesSyntheticFieldLoader implements SourceLo
     private final String storedValuesName;
     private List<Object> storedValues = emptyList();
 
+    /**
+     * Optionally loads malformed values from stored fields.
+     */
+    private final IgnoreMalformedStoredValues ignoreMalformedValues;
+
     /**
      * Build a loader from doc values and, optionally, a stored field.
      * @param name the name of the field to load from doc values
      * @param simpleName the name to give the field in the rendered {@code _source}
      * @param storedValuesName the name of a stored field to load or null if there aren't any stored field for this field
+     * @param loadIgnoreMalformedValues should we load values skipped by {@code ignore_malfored}
      */
-    public SortedSetDocValuesSyntheticFieldLoader(String name, String simpleName, @Nullable String storedValuesName) {
+    public SortedSetDocValuesSyntheticFieldLoader(
+        String name,
+        String simpleName,
+        @Nullable String storedValuesName,
+        boolean loadIgnoreMalformedValues
+    ) {
         this.name = name;
         this.simpleName = simpleName;
         this.storedValuesName = storedValuesName;
+        this.ignoreMalformedValues = loadIgnoreMalformedValues
+            ? IgnoreMalformedStoredValues.stored(name)
+            : IgnoreMalformedStoredValues.empty();
     }
 
     @Override
     public Stream<Map.Entry<String, StoredFieldLoader>> storedFieldLoaders() {
-        return storedValuesName == null ? Stream.of() : Stream.of(Map.entry(storedValuesName, values -> this.storedValues = values));
+        if (storedValuesName == null) {
+            return ignoreMalformedValues.storedFieldLoaders();
+        }
+        return Stream.concat(
+            Stream.of(Map.entry(storedValuesName, values -> this.storedValues = values)),
+            ignoreMalformedValues.storedFieldLoaders()
+        );
     }
 
     @Override
@@ -87,12 +107,12 @@ public abstract class SortedSetDocValuesSyntheticFieldLoader implements SourceLo
 
     @Override
     public boolean hasValue() {
-        return docValues.count() > 0 || storedValues.isEmpty() == false;
+        return docValues.count() > 0 || storedValues.isEmpty() == false || ignoreMalformedValues.count() > 0;
     }
 
     @Override
     public void write(XContentBuilder b) throws IOException {
-        int total = docValues.count() + storedValues.size();
+        int total = docValues.count() + storedValues.size() + ignoreMalformedValues.count();
         switch (total) {
             case 0:
                 return;
@@ -101,23 +121,31 @@ public abstract class SortedSetDocValuesSyntheticFieldLoader implements SourceLo
                 if (docValues.count() > 0) {
                     assert docValues.count() == 1;
                     assert storedValues.isEmpty();
+                    assert ignoreMalformedValues.count() == 0;
                     docValues.write(b);
-                } else {
+                } else if (storedValues.isEmpty() == false) {
                     assert docValues.count() == 0;
                     assert storedValues.size() == 1;
-                    BytesRef ref = (BytesRef) storedValues.get(0);
-                    b.utf8Value(ref.bytes, ref.offset, ref.length);
+                    assert ignoreMalformedValues.count() == 0;
+                    BytesRef converted = convert((BytesRef) storedValues.get(0));
+                    b.utf8Value(converted.bytes, converted.offset, converted.length);
                     storedValues = emptyList();
+                } else {
+                    assert docValues.count() == 0;
+                    assert storedValues.isEmpty();
+                    assert ignoreMalformedValues.count() == 1;
+                    ignoreMalformedValues.write(b);
                 }
                 return;
             default:
                 b.startArray(simpleName);
                 docValues.write(b);
                 for (Object v : storedValues) {
-                    BytesRef ref = (BytesRef) v;
-                    b.utf8Value(ref.bytes, ref.offset, ref.length);
+                    BytesRef converted = convert((BytesRef) v);
+                    b.utf8Value(converted.bytes, converted.offset, converted.length);
                 }
                 storedValues = emptyList();
+                ignoreMalformedValues.write(b);
                 b.endArray();
                 return;
         }

+ 3 - 0
server/src/main/java/org/elasticsearch/index/mapper/SourceLoader.java

@@ -207,6 +207,9 @@ public interface SourceLoader {
          */
         DocValuesLoader docValuesLoader(LeafReader leafReader, int[] docIdsInLeaf) throws IOException;
 
+        /**
+         * Has this field loaded any values for this document?
+         */
         boolean hasValue();
 
         /**

+ 131 - 0
server/src/test/java/org/elasticsearch/index/mapper/IgnoreMalformedStoredValuesTests.java

@@ -0,0 +1,131 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.index.mapper;
+
+import org.apache.lucene.document.StoredField;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.xcontent.XContentBuilder;
+import org.elasticsearch.xcontent.XContentParser;
+import org.elasticsearch.xcontent.XContentParserConfiguration;
+import org.elasticsearch.xcontent.cbor.CborXContent;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.List;
+import java.util.stream.Stream;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class IgnoreMalformedStoredValuesTests extends ESTestCase {
+    public void testIgnoreMalformedBoolean() throws IOException {
+        boolean b = randomBoolean();
+        XContentParser p = ignoreMalformed(b);
+        assertThat(p.currentToken(), equalTo(XContentParser.Token.VALUE_BOOLEAN));
+        assertThat(p.booleanValue(), equalTo(b));
+    }
+
+    public void testIgnoreMalformedString() throws IOException {
+        String s = randomAlphaOfLength(5);
+        XContentParser p = ignoreMalformed(s);
+        assertThat(p.currentToken(), equalTo(XContentParser.Token.VALUE_STRING));
+        assertThat(p.text(), equalTo(s));
+    }
+
+    public void testIgnoreMalformedInt() throws IOException {
+        int i = randomInt();
+        XContentParser p = ignoreMalformed(i);
+        assertThat(p.currentToken(), equalTo(XContentParser.Token.VALUE_NUMBER));
+        assertThat(p.numberType(), equalTo(XContentParser.NumberType.INT));
+        assertThat(p.intValue(), equalTo(i));
+    }
+
+    public void testIgnoreMalformedLong() throws IOException {
+        long l = randomLong();
+        XContentParser p = ignoreMalformed(l);
+        assertThat(p.currentToken(), equalTo(XContentParser.Token.VALUE_NUMBER));
+        assertThat(p.numberType(), equalTo(XContentParser.NumberType.LONG));
+        assertThat(p.longValue(), equalTo(l));
+    }
+
+    public void testIgnoreMalformedFloat() throws IOException {
+        float f = randomFloat();
+        XContentParser p = ignoreMalformed(f);
+        assertThat(p.currentToken(), equalTo(XContentParser.Token.VALUE_NUMBER));
+        assertThat(p.numberType(), equalTo(XContentParser.NumberType.FLOAT));
+        assertThat(p.floatValue(), equalTo(f));
+    }
+
+    public void testIgnoreMalformedDouble() throws IOException {
+        double d = randomDouble();
+        XContentParser p = ignoreMalformed(d);
+        assertThat(p.currentToken(), equalTo(XContentParser.Token.VALUE_NUMBER));
+        assertThat(p.numberType(), equalTo(XContentParser.NumberType.DOUBLE));
+        assertThat(p.doubleValue(), equalTo(d));
+    }
+
+    public void testIgnoreMalformedBigInteger() throws IOException {
+        BigInteger i = randomBigInteger();
+        XContentParser p = ignoreMalformed(i);
+        assertThat(p.currentToken(), equalTo(XContentParser.Token.VALUE_NUMBER));
+        assertThat(p.numberType(), equalTo(XContentParser.NumberType.BIG_INTEGER));
+        assertThat(p.numberValue(), equalTo(i));
+    }
+
+    public void testIgnoreMalformedBigDecimal() throws IOException {
+        BigDecimal d = new BigDecimal(randomBigInteger(), randomInt());
+        XContentParser p = ignoreMalformed(d);
+        assertThat(p.currentToken(), equalTo(XContentParser.Token.VALUE_NUMBER));
+        assertThat(p.numberType(), equalTo(XContentParser.NumberType.BIG_DECIMAL));
+        assertThat(p.numberValue(), equalTo(d));
+    }
+
+    public void testIgnoreMalformedBytes() throws IOException {
+        byte[] b = randomByteArrayOfLength(10);
+        XContentParser p = ignoreMalformed(b);
+        assertThat(p.currentToken(), equalTo(XContentParser.Token.VALUE_EMBEDDED_OBJECT));
+        assertThat(p.binaryValue(), equalTo(b));
+    }
+
+    private static XContentParser ignoreMalformed(Object value) throws IOException {
+        String fieldName = randomAlphaOfLength(10);
+        StoredField s = ignoreMalformedStoredField(value);
+        Object stored = Stream.of(s.numericValue(), s.binaryValue(), s.stringValue()).filter(v -> v != null).findFirst().get();
+        IgnoreMalformedStoredValues values = IgnoreMalformedStoredValues.stored(fieldName);
+        values.storedFieldLoaders().forEach(e -> e.getValue().load(List.of(stored)));
+        return parserFrom(values, fieldName);
+    }
+
+    private static StoredField ignoreMalformedStoredField(Object value) throws IOException {
+        XContentBuilder b = CborXContent.contentBuilder();
+        b.startObject().field("name", value).endObject();
+        XContentParser p = CborXContent.cborXContent.createParser(XContentParserConfiguration.EMPTY, BytesReference.bytes(b).streamInput());
+        assertThat(p.nextToken(), equalTo(XContentParser.Token.START_OBJECT));
+        assertThat(p.nextToken(), equalTo(XContentParser.Token.FIELD_NAME));
+        assertThat(p.currentName(), equalTo("name"));
+        assertThat(p.nextToken().isValue(), equalTo(true));
+
+        return IgnoreMalformedStoredValues.storedField("foo.name", p);
+    }
+
+    private static XContentParser parserFrom(IgnoreMalformedStoredValues values, String fieldName) throws IOException {
+        XContentBuilder b = CborXContent.contentBuilder();
+        b.startObject();
+        b.field(fieldName);
+        values.write(b);
+        b.endObject();
+        XContentParser p = CborXContent.cborXContent.createParser(XContentParserConfiguration.EMPTY, BytesReference.bytes(b).streamInput());
+        assertThat(p.nextToken(), equalTo(XContentParser.Token.START_OBJECT));
+        assertThat(p.nextToken(), equalTo(XContentParser.Token.FIELD_NAME));
+        assertThat(p.currentName(), equalTo(fieldName));
+        assertThat(p.nextToken().isValue(), equalTo(true));
+        return p;
+    }
+}

+ 31 - 12
server/src/test/java/org/elasticsearch/index/mapper/IpFieldMapperTests.java

@@ -21,11 +21,14 @@ import org.elasticsearch.common.network.NetworkAddress;
 import org.elasticsearch.core.Tuple;
 import org.elasticsearch.index.termvectors.TermVectorsService;
 import org.elasticsearch.script.IpFieldScript;
+import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.xcontent.XContentBuilder;
 
 import java.io.IOException;
 import java.net.InetAddress;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
 import static org.hamcrest.Matchers.containsString;
@@ -314,28 +317,45 @@ public class IpFieldMapperTests extends MapperTestCase {
     protected SyntheticSourceSupport syntheticSourceSupport() {
         return new SyntheticSourceSupport() {
             private final InetAddress nullValue = usually() ? null : randomIp(randomBoolean());
+            private final boolean ignoreMalformed = rarely();
 
             @Override
             public SyntheticSourceExample example(int maxValues) {
                 if (randomBoolean()) {
-                    Tuple<String, InetAddress> v = generateValue();
-                    return new SyntheticSourceExample(v.v1(), NetworkAddress.format(v.v2()), this::mapping);
+                    Tuple<Object, Object> v = generateValue();
+                    if (v.v2()instanceof InetAddress a) {
+                        return new SyntheticSourceExample(v.v1(), NetworkAddress.format(a), this::mapping);
+                    }
+                    return new SyntheticSourceExample(v.v1(), v.v2(), this::mapping);
                 }
-                List<Tuple<String, InetAddress>> values = randomList(1, maxValues, this::generateValue);
-                List<String> in = values.stream().map(Tuple::v1).toList();
-                List<String> outList = values.stream()
-                    .map(v -> new BytesRef(InetAddressPoint.encode(v.v2())))
+                List<Tuple<Object, Object>> values = randomList(1, maxValues, this::generateValue);
+                List<Object> in = values.stream().map(Tuple::v1).toList();
+                List<Object> outList = values.stream()
+                    .filter(v -> v.v2() instanceof InetAddress)
+                    .map(v -> new BytesRef(InetAddressPoint.encode((InetAddress) v.v2())))
                     .collect(Collectors.toSet())
                     .stream()
                     .sorted()
                     .map(v -> InetAddressPoint.decode(v.bytes))
                     .map(NetworkAddress::format)
-                    .toList();
+                    .collect(Collectors.toCollection(ArrayList::new));
+                values.stream().filter(v -> false == v.v2() instanceof InetAddress).map(v -> v.v2()).forEach(outList::add);
                 Object out = outList.size() == 1 ? outList.get(0) : outList;
                 return new SyntheticSourceExample(in, out, this::mapping);
             }
 
-            private Tuple<String, InetAddress> generateValue() {
+            private Tuple<Object, Object> generateValue() {
+                if (ignoreMalformed && randomBoolean()) {
+                    List<Supplier<Object>> choices = List.of(
+                        () -> randomAlphaOfLength(3),
+                        ESTestCase::randomInt,
+                        ESTestCase::randomLong,
+                        ESTestCase::randomFloat,
+                        ESTestCase::randomDouble
+                    );
+                    Object v = randomFrom(choices).get();
+                    return Tuple.tuple(v, v);
+                }
                 if (nullValue != null && randomBoolean()) {
                     return Tuple.tuple(null, nullValue);
                 }
@@ -354,6 +374,9 @@ public class IpFieldMapperTests extends MapperTestCase {
                 if (rarely()) {
                     b.field("store", false);
                 }
+                if (ignoreMalformed) {
+                    b.field("ignore_malformed", true);
+                }
             }
 
             @Override
@@ -362,10 +385,6 @@ public class IpFieldMapperTests extends MapperTestCase {
                     new SyntheticSourceInvalidExample(
                         equalTo("field [field] of type [ip] doesn't support synthetic source because it doesn't have doc values"),
                         b -> b.field("type", "ip").field("doc_values", false)
-                    ),
-                    new SyntheticSourceInvalidExample(
-                        equalTo("field [field] of type [ip] doesn't support synthetic source because it ignores malformed ips"),
-                        b -> b.field("type", "ip").field("ignore_malformed", true)
                     )
                 );
             }

+ 1 - 1
x-pack/plugin/mapper-version/src/main/java/org/elasticsearch/xpack/versionfield/VersionStringFieldMapper.java

@@ -406,7 +406,7 @@ public class VersionStringFieldMapper extends FieldMapper {
                 "field [" + name() + "] of type [" + typeName() + "] doesn't support synthetic source because it declares copy_to"
             );
         }
-        return new SortedSetDocValuesSyntheticFieldLoader(name(), simpleName(), null) {
+        return new SortedSetDocValuesSyntheticFieldLoader(name(), simpleName(), null, false) {
             @Override
             protected BytesRef convert(BytesRef value) {
                 return VersionEncoder.decodeVersion(value);