Browse Source

Script: fields API for IP mapped type (#81396)

* Script: fields API for IP mapped type

Adds support in the scripting fields API for the `ip` mapped type,
including the runtime script type.

Adds a new value object, `IPAddress`, to avoid exposing Java's
`InetAddress`. `InetAddress` may cause name resolution if whitelisted
improperly.

`field('ip')`, implemented by `IpDocValuesField` exposes:
  `IPAddress get(IPAddress)`
  `IPAddress get(int, IPAddress)`
  `Iterator<IPAddress> iterator()`
  `List asStrings()`
  `String asString(String)`
  `String asString(int, String)`

`IPAddress` exposes:
  `boolean isV4()`
  `boolean isV6()`
  `String toString()`

Refs: #79105
Stuart Tettemer 3 years ago
parent
commit
bf4861c2dc

+ 5 - 0
docs/changelog/81396.yaml

@@ -0,0 +1,5 @@
+pr: 81396
+summary: "Script: fields API for IP mapped type"
+area: Infra/Scripting
+type: enhancement
+issues: []

+ 6 - 0
modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.net.txt

@@ -6,6 +6,12 @@
 # Side Public License, v 1.
 #
 
+class org.elasticsearch.script.field.IPAddress {
+    (String)
+    boolean isV4()
+    boolean isV6()
+}
+
 class org.elasticsearch.painless.api.CIDR {
     (String)
     boolean contains(String)

+ 8 - 0
modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.script.fields.txt

@@ -82,3 +82,11 @@ class org.elasticsearch.script.field.KeywordDocValuesField @dynamic_type {
   String get(String)
   String get(int, String)
 }
+
+class org.elasticsearch.script.field.IpDocValuesField @dynamic_type {
+  IPAddress get(IPAddress)
+  IPAddress get(int, IPAddress)
+  List asStrings()
+  String asString(String)
+  String asString(int, String)
+}

+ 0 - 5
modules/lang-painless/src/main/resources/org/elasticsearch/painless/org.elasticsearch.txt

@@ -122,11 +122,6 @@ class org.apache.lucene.util.BytesRef {
   String utf8ToString()
 }
 
-class org.elasticsearch.index.mapper.IpFieldMapper$IpFieldType$IpScriptDocValues {
-  String get(int)
-  String getValue()
-}
-
 class org.elasticsearch.search.lookup.FieldLookup {
   def getValue()
   List getValues()

+ 73 - 3
modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/50_script_doc_values.yml

@@ -53,7 +53,7 @@ setup:
                 date: 2017-01-01T12:11:12
                 nanos: 2015-01-01T12:10:30.123456789Z
                 geo_point: 41.12,-71.34
-                ip: 192.168.0.1
+                ip: 192.168.0.19
                 keyword: not split at all
                 long: 12348732141234
                 integer: 134134566
@@ -79,6 +79,7 @@ setup:
           body:
               rank: 3
               boolean: [true, false, true]
+              ip: ["10.1.2.3", "2001:db8::2:1"]
               date: [2017-01-01T12:11:12, 2018-01-01T12:11:12]
               nanos: [2015-01-01T12:10:30.123456789Z, 2015-01-01T12:10:30.987654321Z]
               keyword: ["one string", "another string"]
@@ -526,7 +527,7 @@ setup:
                     field:
                         script:
                             source: "doc['ip'].get(0)"
-    - match: { hits.hits.0.fields.field.0: "192.168.0.1" }
+    - match: { hits.hits.0.fields.field.0: "192.168.0.19" }
 
     - do:
         search:
@@ -537,7 +538,76 @@ setup:
                     field:
                         script:
                             source: "doc['ip'].value"
-    - match: { hits.hits.0.fields.field.0: "192.168.0.1" }
+    - match: { hits.hits.0.fields.field.0: "192.168.0.19" }
+
+    - do:
+        search:
+            rest_total_hits_as_int: true
+            body:
+                sort: [ { rank: asc } ]
+                script_fields:
+                    field:
+                        script:
+                            source: "field('ip').get(new IPAddress('127.0.0.1'))"
+                    field_string:
+                        script:
+                            source: "field('ip').asString('127.0.0.1')"
+    - match: { hits.hits.0.fields.field.0: "192.168.0.19" }
+    - match: { hits.hits.0.fields.field_string.0: "192.168.0.19" }
+    - match: { hits.hits.1.fields.field.0: "127.0.0.1" }
+    - match: { hits.hits.1.fields.field_string.0: "127.0.0.1" }
+    - match: { hits.hits.2.fields.field.0: "10.1.2.3" }
+    - match: { hits.hits.2.fields.field_string.0: "10.1.2.3" }
+
+    - do:
+        search:
+            rest_total_hits_as_int: true
+            body:
+                sort: [ { rank: asc } ]
+                script_fields:
+                    field:
+                        script:
+                            source: "field('ip').get(1, new IPAddress('127.0.0.1'))"
+                    field_string:
+                        script:
+                            source: "field('ip').asString(1, '127.0.0.1')"
+    - match: { hits.hits.0.fields.field.0: "127.0.0.1" }
+    - match: { hits.hits.0.fields.field_string.0: "127.0.0.1" }
+    - match: { hits.hits.1.fields.field.0: "127.0.0.1" }
+    - match: { hits.hits.1.fields.field_string.0: "127.0.0.1" }
+    - match: { hits.hits.2.fields.field.0: "2001:db8::2:1" }
+    - match: { hits.hits.2.fields.field_string.0: "2001:db8::2:1" }
+
+    - do:
+        search:
+            rest_total_hits_as_int: true
+            body:
+                sort: [ { rank: asc } ]
+                script_fields:
+                    field:
+                        script:
+                            source: "String.join(',', field('ip').asStrings())"
+    - match: { hits.hits.0.fields.field.0: "192.168.0.19" }
+    - match: { hits.hits.1.fields.field.0: "" }
+    - match: { hits.hits.2.fields.field.0: "10.1.2.3,2001:db8::2:1" }
+
+    - do:
+        search:
+            rest_total_hits_as_int: true
+            body:
+                sort: [ { rank: asc } ]
+                runtime_mappings:
+                    ip_script_field:
+                        type: ip
+                        script:
+                            source: "for (IPAddress addr : field('ip')) { String ip = addr.toString(); emit(ip.substring(0, ip.length() - 1) + field('ip').size()) }"
+                script_fields:
+                    field:
+                        script:
+                            source: "field('ip_script_field').get(new IPAddress('1.2.3.4'))"
+    - match: { hits.hits.0.fields.field.0: "192.168.0.11" }
+    - match: { hits.hits.1.fields.field.0: "1.2.3.4" }
+    - match: { hits.hits.2.fields.field.0: "10.1.2.2" }
 
 ---
 "keyword":

+ 9 - 33
server/src/main/java/org/elasticsearch/index/fielddata/IpScriptFieldData.java

@@ -8,45 +8,39 @@
 
 package org.elasticsearch.index.fielddata;
 
-import org.apache.lucene.document.InetAddressPoint;
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.util.BytesRef;
-import org.elasticsearch.common.bytes.BytesArray;
-import org.elasticsearch.common.bytes.BytesReference;
-import org.elasticsearch.common.network.InetAddresses;
-import org.elasticsearch.index.fielddata.ScriptDocValues.Strings;
-import org.elasticsearch.index.fielddata.ScriptDocValues.StringsSupplier;
-import org.elasticsearch.index.mapper.IpFieldMapper;
 import org.elasticsearch.indices.breaker.CircuitBreakerService;
 import org.elasticsearch.script.IpFieldScript;
-import org.elasticsearch.script.field.DelegateDocValuesField;
 import org.elasticsearch.script.field.DocValuesField;
+import org.elasticsearch.script.field.ToScriptField;
 import org.elasticsearch.search.aggregations.support.CoreValuesSourceType;
 import org.elasticsearch.search.aggregations.support.ValuesSourceType;
 
-import java.net.InetAddress;
-
 public class IpScriptFieldData extends BinaryScriptFieldData {
     public static class Builder implements IndexFieldData.Builder {
         private final String name;
         private final IpFieldScript.LeafFactory leafFactory;
+        private final ToScriptField<SortedBinaryDocValues> toScriptField;
 
-        public Builder(String name, IpFieldScript.LeafFactory leafFactory) {
+        public Builder(String name, IpFieldScript.LeafFactory leafFactory, ToScriptField<SortedBinaryDocValues> toScriptField) {
             this.name = name;
             this.leafFactory = leafFactory;
+            this.toScriptField = toScriptField;
         }
 
         @Override
         public IpScriptFieldData build(IndexFieldDataCache cache, CircuitBreakerService breakerService) {
-            return new IpScriptFieldData(name, leafFactory);
+            return new IpScriptFieldData(name, leafFactory, toScriptField);
         }
     }
 
     private final IpFieldScript.LeafFactory leafFactory;
+    private final ToScriptField<SortedBinaryDocValues> toScriptField;
 
-    private IpScriptFieldData(String fieldName, IpFieldScript.LeafFactory leafFactory) {
+    private IpScriptFieldData(String fieldName, IpFieldScript.LeafFactory leafFactory, ToScriptField<SortedBinaryDocValues> toScriptField) {
         super(fieldName);
         this.leafFactory = leafFactory;
+        this.toScriptField = toScriptField;
     }
 
     @Override
@@ -55,7 +49,7 @@ public class IpScriptFieldData extends BinaryScriptFieldData {
         return new BinaryScriptLeafFieldData() {
             @Override
             public DocValuesField<?> getScriptField(String name) {
-                return new DelegateDocValuesField(new Strings(new IpSupplier(getBytesValues())), name);
+                return toScriptField.getScriptField(getBytesValues(), name);
             }
 
             @Override
@@ -69,22 +63,4 @@ public class IpScriptFieldData extends BinaryScriptFieldData {
     public ValuesSourceType getValuesSourceType() {
         return CoreValuesSourceType.IP;
     }
-
-    /**
-     * Doc values supplier implementation for ips. We can't share
-     * {@link IpFieldMapper.IpFieldType.IpScriptDocValues} because it is based
-     * on global ordinals and we don't have those.
-     */
-    public static class IpSupplier extends StringsSupplier {
-
-        public IpSupplier(SortedBinaryDocValues in) {
-            super(in);
-        }
-
-        @Override
-        protected String bytesToString(BytesRef bytesRef) {
-            InetAddress addr = InetAddressPoint.decode(BytesReference.toBytes(new BytesArray(bytesRef)));
-            return InetAddresses.toAddrString(addr);
-        }
-    }
 }

+ 2 - 76
server/src/main/java/org/elasticsearch/index/mapper/IpFieldMapper.java

@@ -13,10 +13,8 @@ import org.apache.lucene.document.InetAddressPoint;
 import org.apache.lucene.document.SortedSetDocValuesField;
 import org.apache.lucene.document.StoredField;
 import org.apache.lucene.index.LeafReaderContext;
-import org.apache.lucene.index.SortedSetDocValues;
 import org.apache.lucene.search.MatchNoDocsQuery;
 import org.apache.lucene.search.Query;
-import org.apache.lucene.util.ArrayUtil;
 import org.apache.lucene.util.BytesRef;
 import org.elasticsearch.Version;
 import org.elasticsearch.common.bytes.BytesReference;
@@ -27,14 +25,12 @@ import org.elasticsearch.common.network.NetworkAddress;
 import org.elasticsearch.core.Nullable;
 import org.elasticsearch.core.Tuple;
 import org.elasticsearch.index.fielddata.IndexFieldData;
-import org.elasticsearch.index.fielddata.ScriptDocValues;
 import org.elasticsearch.index.fielddata.plain.SortedSetOrdinalsIndexFieldData;
-import org.elasticsearch.index.mapper.IpFieldMapper.IpFieldType.IpScriptDocValues.IpSupplier;
 import org.elasticsearch.index.query.SearchExecutionContext;
 import org.elasticsearch.script.IpFieldScript;
 import org.elasticsearch.script.Script;
 import org.elasticsearch.script.ScriptCompiler;
-import org.elasticsearch.script.field.DelegateDocValuesField;
+import org.elasticsearch.script.field.IpDocValuesField;
 import org.elasticsearch.search.DocValueFormat;
 import org.elasticsearch.search.aggregations.support.CoreValuesSourceType;
 import org.elasticsearch.search.lookup.FieldValues;
@@ -44,7 +40,6 @@ import org.elasticsearch.xcontent.XContentParser;
 import java.io.IOException;
 import java.net.InetAddress;
 import java.time.ZoneId;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -351,79 +346,10 @@ public class IpFieldMapper extends FieldMapper {
             return builder.apply(lower, upper);
         }
 
-        public static final class IpScriptDocValues extends ScriptDocValues<String> {
-
-            public static final class IpSupplier implements ScriptDocValues.Supplier<String> {
-
-                private final SortedSetDocValues in;
-                private long[] ords = new long[0];
-                private int count;
-
-                public IpSupplier(SortedSetDocValues in) {
-                    this.in = in;
-                }
-
-                @Override
-                public void setNextDocId(int docId) throws IOException {
-                    count = 0;
-                    if (in.advanceExact(docId)) {
-                        for (long ord = in.nextOrd(); ord != SortedSetDocValues.NO_MORE_ORDS; ord = in.nextOrd()) {
-                            ords = ArrayUtil.grow(ords, count + 1);
-                            ords[count++] = ord;
-                        }
-                    }
-                }
-
-                @Override
-                public String getInternal(int index) {
-                    try {
-                        BytesRef encoded = in.lookupOrd(ords[index]);
-                        InetAddress address = InetAddressPoint.decode(
-                            Arrays.copyOfRange(encoded.bytes, encoded.offset, encoded.offset + encoded.length)
-                        );
-                        return InetAddresses.toAddrString(address);
-                    } catch (IOException e) {
-                        throw new RuntimeException(e);
-                    }
-                }
-
-                @Override
-                public int size() {
-                    return count;
-                }
-            }
-
-            public IpScriptDocValues(IpSupplier supplier) {
-                super(supplier);
-            }
-
-            public String getValue() {
-                if (supplier.size() == 0) {
-                    return null;
-                } else {
-                    return get(0);
-                }
-            }
-
-            @Override
-            public String get(int index) {
-                return supplier.getInternal(index);
-            }
-
-            @Override
-            public int size() {
-                return supplier.size();
-            }
-        }
-
         @Override
         public IndexFieldData.Builder fielddataBuilder(String fullyQualifiedIndexName, Supplier<SearchLookup> searchLookup) {
             failIfNoDocValues();
-            return new SortedSetOrdinalsIndexFieldData.Builder(
-                name(),
-                CoreValuesSourceType.IP,
-                (dv, n) -> new DelegateDocValuesField(new IpScriptDocValues(new IpSupplier(dv)), n)
-            );
+            return new SortedSetOrdinalsIndexFieldData.Builder(name(), CoreValuesSourceType.IP, IpDocValuesField::new);
         }
 
         @Override

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

@@ -24,6 +24,7 @@ import org.elasticsearch.index.query.SearchExecutionContext;
 import org.elasticsearch.script.CompositeFieldScript;
 import org.elasticsearch.script.IpFieldScript;
 import org.elasticsearch.script.Script;
+import org.elasticsearch.script.field.IpDocValuesField;
 import org.elasticsearch.search.DocValueFormat;
 import org.elasticsearch.search.lookup.SearchLookup;
 import org.elasticsearch.search.runtime.IpScriptFieldExistsQuery;
@@ -91,7 +92,7 @@ public final class IpScriptFieldType extends AbstractScriptFieldType<IpFieldScri
 
     @Override
     public IpScriptFieldData.Builder fielddataBuilder(String fullyQualifiedIndexName, Supplier<SearchLookup> searchLookup) {
-        return new IpScriptFieldData.Builder(name(), leafFactory(searchLookup.get()));
+        return new IpScriptFieldData.Builder(name(), leafFactory(searchLookup.get()), IpDocValuesField::new);
     }
 
     @Override

+ 56 - 0
server/src/main/java/org/elasticsearch/script/field/IPAddress.java

@@ -0,0 +1,56 @@
+/*
+ * 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.script.field;
+
+import org.elasticsearch.common.network.InetAddresses;
+import org.elasticsearch.xcontent.ToXContent;
+import org.elasticsearch.xcontent.XContentBuilder;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+
+/**
+ * IP address for use in scripting.
+ */
+public class IPAddress implements ToXContent {
+    protected final InetAddress address;
+
+    IPAddress(InetAddress address) {
+        this.address = address;
+    }
+
+    public IPAddress(String address) {
+        this.address = InetAddresses.forString(address);
+    }
+
+    public boolean isV4() {
+        return address instanceof Inet4Address;
+    }
+
+    public boolean isV6() {
+        return address instanceof Inet6Address;
+    }
+
+    @Override
+    public String toString() {
+        return InetAddresses.toAddrString(address);
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        return builder.value(this.toString());
+    }
+
+    @Override
+    public boolean isFragment() {
+        return false;
+    }
+}

+ 228 - 0
server/src/main/java/org/elasticsearch/script/field/IpDocValuesField.java

@@ -0,0 +1,228 @@
+/*
+ * 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.script.field;
+
+import org.apache.lucene.document.InetAddressPoint;
+import org.apache.lucene.index.SortedSetDocValues;
+import org.apache.lucene.util.ArrayUtil;
+import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.BytesRefBuilder;
+import org.elasticsearch.common.bytes.BytesArray;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.network.InetAddresses;
+import org.elasticsearch.index.fielddata.IpScriptFieldData;
+import org.elasticsearch.index.fielddata.ScriptDocValues;
+import org.elasticsearch.index.fielddata.SortedBinaryDocValues;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+public class IpDocValuesField implements DocValuesField<IPAddress>, ScriptDocValues.Supplier<String> {
+    protected final String name;
+    protected final ScriptDocValues.Supplier<InetAddress> raw;
+
+    // used for backwards compatibility for old-style "doc" access
+    // as a delegate to this field class
+    protected ScriptDocValues.Strings strings = null;
+
+    public IpDocValuesField(SortedSetDocValues input, String name) {
+        this.name = name;
+        this.raw = new SortedSetIpSupplier(input);
+    }
+
+    public IpDocValuesField(SortedBinaryDocValues input, String name) {
+        this.name = name;
+        this.raw = new SortedBinaryIpSupplier(input);
+    }
+
+    @Override
+    public void setNextDocId(int docId) throws IOException {
+        raw.setNextDocId(docId);
+    }
+
+    @Override
+    public String getInternal(int index) {
+        return InetAddresses.toAddrString(raw.getInternal(index));
+    }
+
+    @Override
+    public ScriptDocValues<String> getScriptDocValues() {
+        if (strings == null) {
+            strings = new ScriptDocValues.Strings(this);
+        }
+
+        return strings;
+    }
+
+    public String asString(String defaultValue) {
+        return asString(0, defaultValue);
+    }
+
+    public String asString(int index, String defaultValue) {
+        if (isEmpty() || index < 0 || index >= size()) {
+            return defaultValue;
+        }
+
+        return getInternal(index);
+    }
+
+    public List<String> asStrings() {
+        if (isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        List<String> values = new ArrayList<>(size());
+        for (int i = 0; i < size(); i++) {
+            values.add(getInternal(i));
+        }
+
+        return values;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return size() == 0;
+    }
+
+    @Override
+    public int size() {
+        return raw.size();
+    }
+
+    public IPAddress get(IPAddress defaultValue) {
+        return get(0, defaultValue);
+    }
+
+    public IPAddress get(int index, IPAddress defaultValue) {
+        if (isEmpty() || index < 0 || index >= size()) {
+            return defaultValue;
+        }
+
+        return new IPAddress(raw.getInternal(index));
+    }
+
+    @Override
+    public Iterator<IPAddress> iterator() {
+        return new Iterator<IPAddress>() {
+            private int index = 0;
+
+            @Override
+            public boolean hasNext() {
+                return index < size();
+            }
+
+            @Override
+            public IPAddress next() {
+                if (hasNext() == false) {
+                    throw new NoSuchElementException();
+                }
+                return new IPAddress(raw.getInternal(index++));
+            }
+        };
+    }
+
+    /** Used if we have access to global ordinals */
+    protected static class SortedSetIpSupplier implements ScriptDocValues.Supplier<InetAddress> {
+        private final SortedSetDocValues in;
+        private long[] ords = new long[0];
+        private int count;
+
+        public SortedSetIpSupplier(SortedSetDocValues in) {
+            this.in = in;
+        }
+
+        @Override
+        public void setNextDocId(int docId) throws IOException {
+            count = 0;
+            if (in.advanceExact(docId)) {
+                for (long ord = in.nextOrd(); ord != SortedSetDocValues.NO_MORE_ORDS; ord = in.nextOrd()) {
+                    ords = ArrayUtil.grow(ords, count + 1);
+                    ords[count++] = ord;
+                }
+            }
+        }
+
+        @Override
+        public InetAddress getInternal(int index) {
+            try {
+                BytesRef encoded = in.lookupOrd(ords[index]);
+                return InetAddressPoint.decode(Arrays.copyOfRange(encoded.bytes, encoded.offset, encoded.offset + encoded.length));
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        @Override
+        public int size() {
+            return count;
+        }
+    }
+
+    /** Used if we do not have global ordinals, such as in the IP runtime field see: {@link IpScriptFieldData} */
+    protected static class SortedBinaryIpSupplier implements ScriptDocValues.Supplier<InetAddress> {
+        private final SortedBinaryDocValues in;
+        private BytesRefBuilder[] values = new BytesRefBuilder[0];
+        private int count;
+
+        public SortedBinaryIpSupplier(SortedBinaryDocValues in) {
+            this.in = in;
+        }
+
+        @Override
+        public void setNextDocId(int docId) throws IOException {
+            if (in.advanceExact(docId)) {
+                resize(in.docValueCount());
+                for (int i = 0; i < count; i++) {
+                    // We need to make a copy here, because BytesBinaryDVLeafFieldData's SortedBinaryDocValues
+                    // implementation reuses the returned BytesRef. Otherwise we would end up with the same BytesRef
+                    // instance for all slots in the values array.
+                    values[i].copyBytes(in.nextValue());
+                }
+            } else {
+                resize(0);
+            }
+        }
+
+        /**
+         * Set the {@link #size()} and ensure that the {@link #values} array can
+         * store at least that many entries.
+         */
+        private void resize(int newSize) {
+            count = newSize;
+            if (newSize > values.length) {
+                final int oldLength = values.length;
+                values = ArrayUtil.grow(values, count);
+                for (int i = oldLength; i < values.length; ++i) {
+                    values[i] = new BytesRefBuilder();
+                }
+            }
+        }
+
+        @Override
+        public InetAddress getInternal(int index) {
+            return InetAddressPoint.decode(BytesReference.toBytes(new BytesArray(values[index].toBytesRef())));
+        }
+
+        @Override
+        public int size() {
+            return count;
+        }
+    }
+}

+ 33 - 0
server/src/test/java/org/elasticsearch/script/field/IPAddressTests.java

@@ -0,0 +1,33 @@
+/*
+ * 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.script.field;
+
+import org.elasticsearch.test.ESTestCase;
+
+public class IPAddressTests extends ESTestCase {
+
+    public void testToString() {
+        String v4 = "192.168.7.255";
+        assertEquals(v4, new IPAddress(v4).toString());
+        String v6 = "b181:3a88:339c:97f5:2b40:5175:bf3d:f77e";
+        assertEquals(v6, new IPAddress(v6).toString());
+    }
+
+    public void testV4() {
+        IPAddress addr4 = new IPAddress("169.254.0.0");
+        assertTrue(addr4.isV4());
+        assertFalse(addr4.isV6());
+    }
+
+    public void testV6() {
+        IPAddress addr4 = new IPAddress("b181:3a88:339c:97f5:2b40:5175:bf3d:f77e");
+        assertFalse(addr4.isV4());
+        assertTrue(addr4.isV6());
+    }
+}