diff --git a/TESTING.asciidoc b/TESTING.asciidoc index 1e984a17f3cda..5e5207b279eff 100644 --- a/TESTING.asciidoc +++ b/TESTING.asciidoc @@ -512,7 +512,9 @@ into it vagrant ssh debian-9 -------------------------------------------- -Now inside the VM, to run the https://github.com/sstephenson/bats[bats] packaging tests +Now inside the VM, start the packaging tests from the terminal. There are two packaging +test projects. The old ones are written with https://github.com/sstephenson/bats[bats] +and only run on linux. To run them do -------------------------------------------- cd $PACKAGING_ARCHIVES @@ -524,18 +526,36 @@ sudo bats $BATS_TESTS/*.bats sudo bats $BATS_TESTS/20_tar_package.bats $BATS_TESTS/25_tar_plugins.bats -------------------------------------------- -To run the Java packaging tests, again inside the VM +The new packaging tests are written in Java and run on both linux and windows. On +linux (again, inside the VM) -------------------------------------------- -bash $PACKAGING_TESTS/run-tests.sh +# run the full suite +sudo bash $PACKAGING_TESTS/run-tests.sh + +# run specific test cases +sudo bash $PACKAGING_TESTS/run-tests.sh \ + org.elasticsearch.packaging.test.DefaultZipTests \ + org.elasticsearch.packaging.test.OssZipTests -------------------------------------------- -or on Windows +or on Windows, from a terminal running as Administrator -------------------------------------------- +# run the full suite powershell -File $Env:PACKAGING_TESTS/run-tests.ps1 + +# run specific test cases +powershell -File $Env:PACKAGING_TESTS/run-tests.ps1 ` + org.elasticsearch.packaging.test.DefaultZipTests ` + org.elasticsearch.packaging.test.OssZipTests -------------------------------------------- +Note that on Windows boxes when running from inside the GUI, you may have to log out and +back in to the `vagrant` user (password `vagrant`) for the environment variables that +locate the packaging tests and distributions to take effect, due to how vagrant provisions +Windows machines. + When you've made changes you want to test, keep the VM up and reload the tests and distributions inside by running (on the host) diff --git a/Vagrantfile b/Vagrantfile index 66ec60820eaba..d53c80754e637 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -237,6 +237,7 @@ def linux_common(config, config.vm.provision 'markerfile', type: 'shell', inline: <<-SHELL touch /etc/is_vagrant_vm + touch /is_vagrant_vm # for consistency between linux and windows SHELL # This prevents leftovers from previous tests using the diff --git a/buildSrc/src/main/groovy/org/elasticsearch/gradle/vagrant/VagrantTestPlugin.groovy b/buildSrc/src/main/groovy/org/elasticsearch/gradle/vagrant/VagrantTestPlugin.groovy index 2e02911b7a754..6e8a5fd15edf1 100644 --- a/buildSrc/src/main/groovy/org/elasticsearch/gradle/vagrant/VagrantTestPlugin.groovy +++ b/buildSrc/src/main/groovy/org/elasticsearch/gradle/vagrant/VagrantTestPlugin.groovy @@ -52,6 +52,8 @@ class VagrantTestPlugin implements Plugin { static final List DISTRIBUTIONS = unmodifiableList([ 'archives:tar', 'archives:oss-tar', + 'archives:zip', + 'archives:oss-zip', 'packages:rpm', 'packages:oss-rpm', 'packages:deb', @@ -242,13 +244,27 @@ class VagrantTestPlugin implements Plugin { Task createLinuxRunnerScript = project.tasks.create('createLinuxRunnerScript', FileContentsTask) { dependsOn copyPackagingTests file "${testsDir}/run-tests.sh" - contents "java -cp \"\$PACKAGING_TESTS/*\" org.junit.runner.JUnitCore ${-> project.extensions.esvagrant.testClass}" + contents """\ + if [ "\$#" -eq 0 ]; then + test_args=( "${-> project.extensions.esvagrant.testClass}" ) + else + test_args=( "\$@" ) + fi + java -cp "\$PACKAGING_TESTS/*" org.elasticsearch.packaging.VMTestRunner "\${test_args[@]}" + """ } Task createWindowsRunnerScript = project.tasks.create('createWindowsRunnerScript', FileContentsTask) { dependsOn copyPackagingTests file "${testsDir}/run-tests.ps1" + // the use of $args rather than param() here is deliberate because the syntax for array (multivalued) parameters is likely + // a little trappy for those unfamiliar with powershell contents """\ - java -cp "\$Env:PACKAGING_TESTS/*" org.junit.runner.JUnitCore ${-> project.extensions.esvagrant.testClass} + if (\$args.Count -eq 0) { + \$testArgs = @("${-> project.extensions.esvagrant.testClass}") + } else { + \$testArgs = \$args + } + java -cp "\$Env:PACKAGING_TESTS/*" org.elasticsearch.packaging.VMTestRunner @testArgs exit \$LASTEXITCODE """ } @@ -525,9 +541,10 @@ class VagrantTestPlugin implements Plugin { if (LINUX_BOXES.contains(box)) { javaPackagingTest.command = 'ssh' - javaPackagingTest.args = ['--command', 'bash "$PACKAGING_TESTS/run-tests.sh"'] + javaPackagingTest.args = ['--command', 'sudo bash "$PACKAGING_TESTS/run-tests.sh"'] } else { javaPackagingTest.command = 'winrm' + // winrm commands run as administrator javaPackagingTest.args = ['--command', 'powershell -File "$Env:PACKAGING_TESTS/run-tests.ps1"'] } diff --git a/docs/painless/painless-casting.asciidoc b/docs/painless/painless-casting.asciidoc index ec4f9919bd043..a3624f9083145 100644 --- a/docs/painless/painless-casting.asciidoc +++ b/docs/painless/painless-casting.asciidoc @@ -1,172 +1,456 @@ [[painless-casting]] === Casting -Casting is the conversion of one type to another. Implicit casts are casts that -occur automatically, such as during an assignment operation. Explicit casts are -casts where you use the casting operator to explicitly convert one type to -another. This is necessary during operations where the cast cannot be inferred. +A cast converts the value of an original type to the equivalent value of a +target type. An implicit cast infers the target type and automatically occurs +during certain <>. An explicit cast specifies +the target type and forcefully occurs as its own operation. Use the *cast +operator* to specify an explicit cast. -To cast to a new type, precede the expression by the new type enclosed in -parentheses, for example -`(int)x`. +*Errors* -The following sections specify the implicit casts that can be performed and the -explicit casts that are allowed. The only other permitted cast is casting -a single character `String` to a `char`. +* If during a cast there exists no equivalent value for the target type. +* If an implicit cast is given, but an explicit cast is required. -*Grammar:* +*Grammar* [source,ANTLR4] ---- cast: '(' TYPE ')' expression ---- -[[numeric-casting]] -==== Numeric Casting +*Examples* -The following table shows the allowed implicit and explicit casts between -numeric types. Read the table by row. To find out if you need to explicitly -cast from type A to type B, find the row for type A and scan across to the -column for type B. +* Valid casts. ++ +[source,Painless] +---- +<1> int i = (int)5L; +<2> Map m = new HashMap(); +<3> HashMap hm = (HashMap)m; +---- ++ +<1> declare `int i`; + explicit cast `long 5` to `int 5` -> `int 5`; + assign `int 5` to `i` +<2> declare `Map m`; + allocate `HashMap` instance -> `HashMap reference`; + implicit cast `HashMap reference` to `Map reference` -> `Map reference`; + assign `Map reference` to `m` +<3> declare `HashMap hm`; + access `m` -> `Map reference`; + explicit cast `Map reference` to `HashMap reference` -> `HashMap reference`; + assign `HashMap reference` to `hm` + +[[numeric-type-casting]] +==== Numeric Type Casting -IMPORTANT: Explicit casts between numeric types can result in some data loss. A -smaller numeric type cannot necessarily accommodate the value from a larger -numeric type. You might also lose precision when casting from integer types -to floating point types. +A <> cast converts the value of an original +numeric type to the equivalent value of a target numeric type. A cast between +two numeric type values results in data loss when the value of the original +numeric type is larger than the target numeric type can accommodate. A cast +between an integer type value and a floating point type value can result in +precision loss. + +The allowed casts for values of each numeric type are shown as a row in the +following table: |==== -| | byte | short | char | int | long | float | double -| byte | | implicit | implicit | implicit | implicit | implicit | implicit -| short | explicit | | explicit | implicit | implicit | implicit | implicit -| char | explicit | explicit | | implicit | implicit | implicit | implicit -| int | explicit | explicit | explicit | | implicit | implicit | implicit -| long | explicit | explicit | explicit | explicit | | implicit | implicit -| float | explicit | explicit | explicit | explicit | explicit | | implicit +| | byte | short | char | int | long | float | double +| byte | | implicit | implicit | implicit | implicit | implicit | implicit +| short | explicit | | explicit | implicit | implicit | implicit | implicit +| char | explicit | explicit | | implicit | implicit | implicit | implicit +| int | explicit | explicit | explicit | | implicit | implicit | implicit +| long | explicit | explicit | explicit | explicit | | implicit | implicit +| float | explicit | explicit | explicit | explicit | explicit | | implicit | double | explicit | explicit | explicit | explicit | explicit | explicit | |==== +*Examples* + +* Valid numeric type casts. ++ +[source,Painless] +---- +<1> int a = 1; +<2> long b = a; +<3> short c = (short)b; +<4> double e = (double)a; +---- ++ +<1> declare `int a`; + assign `int 1` to `a` +<2> declare `long b`; + access `a` -> `int 1`; + implicit cast `int 1` to `long 1` -> `long 1`; + assign `long 1` to `b` +<3> declare `short c`; + access `b` -> `long 1`; + explicit cast `long 1` to `short 1` -> `short 1`; + assign `short 1` value to `c` +<4> declare `double e`; + access `a` -> `int 1`; + explicit cast `int 1` to `double 1.0`; + assign `double 1.0` to `e`; + (note the explicit cast is extraneous since an implicit cast is valid) ++ +* Invalid numeric type casts resulting in errors. ++ +[source,Painless] +---- +<1> int a = 1.0; // error +<2> int b = 2; +<3> byte c = b; // error +---- ++ +<1> declare `int i`; + *error* -> cannot implicit cast `double 1.0` to `int 1`; + (note an explicit cast is valid) +<2> declare `int b`; + assign `int 2` to `b` +<3> declare byte `c`; + access `b` -> `int 2`; + *error* -> cannot implicit cast `int 2` to `byte 2`; + (note an explicit cast is valid) + +[[reference-type-casting]] +==== Reference Type Casting + +A <> cast converts the value of an original +reference type to the equivalent value of a target reference type. An implicit +cast between two reference type values is allowed when the original reference +type is a descendant of the target type. An explicit cast between two reference +type values is allowed when the original type is a descendant of the target type +or the target type is a descendant of the original type. + +*Examples* + +* Valid reference type casts. ++ +[source,Painless] +---- +<1> List x; +<2> ArrayList y = new ArrayList(); +<3> x = y; +<4> y = (ArrayList)x; +<5> x = (List)y; +---- ++ +<1> declare `List x`; + assign default value `null` to `x` +<2> declare `ArrayList y`; + allocate `ArrayList` instance -> `ArrayList reference`; + assign `ArrayList reference` to `y`; +<3> access `y` -> `ArrayList reference`; + implicit cast `ArrayList reference` to `List reference` -> `List reference`; + assign `List reference` to `x`; + (note `ArrayList` is a descendant of `List`) +<4> access `x` -> `List reference`; + explicit cast `List reference` to `ArrayList reference` + -> `ArrayList reference`; + assign `ArrayList reference` to `y`; +<5> access `y` -> `ArrayList reference`; + explicit cast `ArrayList reference` to `List reference` -> `List reference`; + assign `List reference` to `x`; + (note the explicit cast is extraneous, and an implicit cast is valid) ++ +* Invalid reference type casts resulting in errors. ++ +[source,Painless] +---- +<1> List x = new ArrayList(); +<2> ArrayList y = x; // error +<3> Map m = (Map)x; // error +---- ++ +<1> declare `List x`; + allocate `ArrayList` instance -> `ArrayList reference`; + implicit cast `ArrayList reference` to `List reference` -> `List reference`; + assign `List reference` to `x` +<2> declare `ArrayList y`; + access `x` -> `List reference`; + *error* -> cannot implicit cast `List reference` to `ArrayList reference`; + (note an explicit cast is valid since `ArrayList` is a descendant of `List`) +<3> declare `ArrayList y`; + access `x` -> `List reference`; + *error* -> cannot explicit cast `List reference` to `Map reference`; + (note no cast would be valid since neither `List` nor `Map` is a descendant + of the other) + +[[dynamic-type-casting]] +==== Dynamic Type Casting + +A <> cast converts the value of an original +`def` type to the equivalent value of any target type or converts the value of +any original type to the equivalent value of a target `def` type. + +An implicit cast from any original type value to a `def` type value is always +allowed. An explicit cast from any original type value to a `def` type value is +always allowed but never necessary. + +An implicit or explicit cast from an original `def` type value to +any target type value is allowed if and only if the cast is normally allowed +based on the current type value the `def` type value represents. + +*Examples* -Example(s) -[source,Java] ----- -int a = 1; // Declare int variable a and set it to the literal - // value 1 -long b = a; // Declare long variable b and set it to int variable - // a with an implicit cast to convert from int to long -short c = (short)b; // Declare short variable c, explicitly cast b to a - // short, and assign b to c -byte d = a; // ERROR: Casting an int to a byte requires an explicit - // cast -double e = (double)a; // Explicitly cast int variable a to a double and assign - // it to the double variable e. The explicit cast is - // allowed, but it is not necessary. ----- - -[[reference-casting]] -==== Reference Casting - -A reference type can be implicitly cast to another reference type as long as -the type being cast _from_ is a descendant of the type being cast _to_. A -reference type can be explicitly cast _to_ if the type being cast to is a -descendant of the type being cast _from_. - -*Examples:* -[source,Java] ----- -List x; // Declare List variable x -ArrayList y = new ArrayList(); // Declare ArrayList variable y and assign it a - // newly allocated ArrayList [1] -x = y; // Assign Arraylist y to List x using an - // implicit cast -y = (ArrayList)x; // Explicitly cast List x to an ArrayList and - // assign it to ArrayList y -x = (List)y; // Set List x to ArrayList y using an explicit - // cast (the explicit cast is not necessary) -y = x; // ERROR: List x cannot be implicitly cast to - // an ArrayList, an explicit cast is required -Map m = y; // ERROR: Cannot implicitly or explicitly cast [2] - // an ArrayList to a Map, no relationship - // exists between the two types. ----- -[1] `ArrayList` is a descendant of the `List` type. -[2] `Map` is unrelated to the `List` and `ArrayList` types. - -[[def-type-casting]] -==== def Type Casting -All primitive and reference types can always be implicitly cast to -`def`. While it is possible to explicitly cast to `def`, it is not necessary. - -However, it is not always possible to implicitly cast a `def` to other -primitive and reference types. An explicit cast is required if an explicit -cast would normally be required between the non-def types. - - -*Examples:* -[source,Java] ----- -def x; // Declare def variable x and set it to null -x = 3; // Set the def variable x to the literal 3 with an implicit - // cast from int to def -double a = x; // Declare double variable a and set it to def variable x, - // which contains a double -int b = x; // ERROR: Results in a run-time error because an explicit cast is - // required to cast from a double to an int -int c = (int)x; // Declare int variable c, explicitly cast def variable x to an - // int, and assign x to c +* Valid dynamic type casts with any original type to a target `def` type. ++ +[source,Painless] ---- +<1> def d0 = 3; +<2> d0 = new ArrayList(); +<3> Object o = new HashMap(); +<4> def d1 = o; +<5> int i = d1.size(); +---- ++ +<1> declare `def d0`; + implicit cast `int 3` to `def`; + assign `int 3` to `d0` +<2> allocate `ArrayList` instance -> `ArrayList reference`; + implicit cast `ArrayList reference` to `def` -> `def`; + assign `def` to `d0` +<3> declare `Object o`; + allocate `HashMap` instance -> `HashMap reference`; + implicit cast `HashMap reference` to `Object reference` + -> `Object reference`; + assign `Object reference` to `o` +<4> declare `def d1`; + access `o` -> `Object reference`; + implicit cast `Object reference` to `def` -> `def`; + assign `def` to `d1` +<5> declare `int i`; + access `d1` -> `def`; + implicit cast `def` to `HashMap reference` -> HashMap reference`; + call `size` on `HashMap reference` -> `int 0`; + assign `int 0` to `i`; + (note `def` was implicit cast to `HashMap reference` since `HashMap` is the + child-most descendant type value that the `def` type value + represents) ++ +* Valid dynamic type casts with an original `def` type to any target type. ++ +[source,Painless] +---- +<1> def d = 1.0; +<2> int i = (int)d; +<3> d = 1; +<4> float f = d; +<5> d = new ArrayList(); +<6> List l = d; +---- ++ +<1> declare `def d`; + implicit cast `double 1.0` to `def` -> `def`; + assign `def` to `d` +<2> declare `int i`; + access `d` -> `def`; + implicit cast `def` to `double 1.0` -> `double 1.0`; + explicit cast `double 1.0` to `int 1` -> `int 1`; + assign `int 1` to `i`; + (note the explicit cast is necessary since a `double` value cannot be + converted to an `int` value implicitly) +<3> assign `int 1` to `d`; + (note the switch in the type `d` represents from `double` to `int`) +<4> declare `float i`; + access `d` -> `def`; + implicit cast `def` to `int 1` -> `int 1`; + implicit cast `int 1` to `float 1.0` -> `float 1.0`; + assign `float 1.0` to `f` +<5> allocate `ArrayList` instance -> `ArrayList reference`; + assign `ArrayList reference` to `d`; + (note the switch in the type `d` represents from `int` to `ArrayList`) +<6> declare `List l`; + access `d` -> `def`; + implicit cast `def` to `ArrayList reference` -> `ArrayList reference`; + implicit cast `ArrayList reference` to `List reference` -> `List reference`; + assign `List reference` to `l` ++ +* Invalid dynamic type casts resulting in errors. ++ +[source,Painless] +---- +<1> def d = 1; +<2> short s = d; // error +<3> d = new HashMap(); +<4> List l = d; // error +---- +<1> declare `def d`; + implicit cast `int 1` to `def` -> `def`; + assign `def` to `d` +<2> declare `short s`; + access `d` -> `def`; + implicit cast `def` to `int 1` -> `int 1`; + *error* -> cannot implicit cast `int 1` to `short 1`; + (note an explicit cast is valid) +<3> allocate `HashMap` instance -> `HashMap reference`; + implicit cast `HashMap reference` to `def` -> `def`; + assign `def` to `d` +<4> declare `List l`; + access `d` -> `def`; + implicit cast `def` to `HashMap reference`; + *error* -> cannot implicit cast `HashMap reference` to `List reference`; + (note no cast would be valid since neither `HashMap` nor `List` is a + descendant of the other) + +[[string-character-casting]] +==== String to Character Casting + +Use the *cast operator* to convert a <> value into a +<> value. + +*Errors* + +* If the `String` type value isn't one character in length. +* If the `String` type value is `null`. + +*Examples* + +* Casting string literals into `char` type values. ++ +[source,Painless] +---- +<1> char c = (char)"C" +<2> c = (char)'c' +---- ++ +<1> declare `char c`; + explicit cast `String "C"` to `char C` -> `char C`; + assign `char C` to `c` +<2> explicit cast `String 'c'` to `char c` -> `char c`; + assign `char c` to `c` ++ +* Casting a `String` reference into a `char` value. ++ +[source,Painless] +---- +<1> String s = "s"; +<2> char c = (char)s; +---- +<1> declare `String s`; + assign `String "s"` to `s`; +<2> declare `char c` + access `s` -> `String "s"`; + explicit cast `String "s"` to `char s` -> `char s`; + assign `char s` to `c` [[boxing-unboxing]] ==== Boxing and Unboxing -Boxing is where a cast is used to convert a primitive type to its corresponding -reference type. Unboxing is the reverse, converting a reference type to the -corresponding primitive type. +Boxing is a special type of cast used to convert a primitive type to its +corresponding reference type. Unboxing is the reverse used to convert a +reference type to its corresponding primitive type. + +Implicit boxing/unboxing occurs during the following operations: + +* Conversions between a `def` type and a primitive type will be implicitly + boxed/unboxed as necessary, though this is referred to as an implicit cast + throughout the documentation. +* Method/function call arguments will be implicitly boxed/unboxed as necessary. +* A primitive type value will be implicitly boxed when a reference type method + call is invoked on it. -There are two places Painless performs implicit boxing and unboxing: +Explicit boxing/unboxing is not allowed. Use the reference type API to +explicitly convert a primitive type value to its respective reference type +value and vice versa. -* When you call methods, Painless automatically boxes and unboxes arguments -so you can specify either primitive types or their corresponding reference -types. -* When you use the `def` type, Painless automatically boxes and unboxes as -needed when converting to and from `def`. +*Errors* -The casting operator does not support any way to explicitly box a primitive -type or unbox a reference type. +* If an explicit cast is made to box/unbox a primitive type. -If a primitive type needs to be converted to a reference type, the Painless -reference type API supports methods that can do that. However, under normal -circumstances this should not be necessary. +*Examples* -*Examples:* -[source,Java] +* Uses of implicit boxing/unboxing. ++ +[source,Painless] ---- -Integer x = 1; // ERROR: not a legal implicit cast -Integer y = (Integer)1; // ERROR: not a legal explicit cast -int a = new Integer(1); // ERROR: not a legal implicit cast -int b = (int)new Integer(1); // ERROR: not a legal explicit cast +<1> List l = new ArrayList(); +<2> l.add(1); +<3> Integer I = Integer.valueOf(0); +<4> int i = l.get(i); ---- ++ +<1> declare `List l`; + allocate `ArrayList` instance -> `ArrayList reference`; + assign `ArrayList reference` to `l`; +<2> access `l` -> `List reference`; + implicit cast `int 1` to `def` -> `def`; + call `add` on `List reference` with arguments (`def`); + (note internally `int 1` is boxed to `Integer 1` to store as a `def` type + value) +<3> declare `Integer I`; + call `valueOf` on `Integer` with arguments of (`int 0`) -> `Integer 0`; + assign `Integer 0` to `I`; +<4> declare `int i`; + access `I` -> `Integer 0`; + unbox `Integer 0` -> `int 0`; + access `l` -> `List reference`; + call `get` on `List reference` with arguments (`int 0`) -> `def`; + implicit cast `def` to `int 1` -> `int 1`; + assign `int 1` to `i`; + (note internally `int 1` is unboxed from `Integer 1` when loaded from a + `def` type value) ++ +* Uses of invalid boxing/unboxing resulting in errors. ++ +[source,Painless] +---- +<1> Integer x = 1; // error +<2> Integer y = (Integer)1; // error +<3> int a = Integer.valueOf(1); // error +<4> int b = (int)Integer.valueOf(1); // error +---- ++ +<1> declare `Integer x`; + *error* -> cannot implicit box `int 1` to `Integer 1` during assignment +<2> declare `Integer y`; + *error* -> cannot explicit box `int 1` to `Integer 1` during assignment +<3> declare `int a`; + call `valueOf` on `Integer` with arguments of (`int 1`) -> `Integer 1`; + *error* -> cannot implicit unbox `Integer 1` to `int 1` during assignment +<4> declare `int a`; + call `valueOf` on `Integer` with arguments of (`int 1`) -> `Integer 1`; + *error* -> cannot explicit unbox `Integer 1` to `int 1` during assignment [[promotion]] ==== Promotion -Promotion is where certain operations require types to be either a minimum -numerical type or for two (or more) types to be equivalent. -The documentation for each operation that has these requirements -includes promotion tables that describe how this is handled. +Promotion is when a single value is implicitly cast to a certain type or +multiple values are implicitly cast to the same type as required for evaluation +by certain operations. Each operation that requires promotion has a promotion +table that shows all required implicit casts based on the type(s) of value(s). A +value can be promoted to a `def` type at compile-time; however, the promoted +type value is derived from what the `def` type value represents at run-time. -When an operation promotes a type or types, the resultant type -of the operation is the promoted type. Types can be promoted to def -at compile-time; however, at run-time, the resultant type will be the -promotion of the types the `def` is representing. +*Errors* -*Examples:* -[source,Java] ----- -2 + 2.0 // Add the literal int 2 and the literal double 2.0. The literal - // 2 is promoted to a double and the resulting value is a double. +* If a specific operation cannot find an allowed promotion type for the type(s) + of value(s) given. -def x = 1; // Declare def variable x and set it to the literal int 1 through - // an implicit cast -x + 2.0F // Add def variable x and the literal float 2.0. - // At compile-time the types are promoted to def. - // At run-time the types are promoted to float. +*Examples* + +* Uses of promotion. ++ +[source,Painless] +---- +<1> double d = 2 + 2.0; +<2> def x = 1; +<3> float f = x + 2.0F; ---- +<1> declare `double d`; + promote `int 2` and `double 2.0 @0` -> `double 2.0 @0`; + implicit cast `int 2` to `double 2.0 @1` -> `double 2.0 @1`; + add `double 2.0 @1` and `double 2.0 @0` -> `double 4.0`; + assign `double 4.0` to `d` +<2> declare `def x`; + implicit cast `int 1` to `def` -> `def`; + assign `def` to `x`; +<3> declare `float f`; + access `x` -> `def`; + implicit cast `def` to `int 1` -> `int 1`; + promote `int 1` and `float 2.0` -> `float 2.0`; + implicit cast `int 1` to `float 1.0` -> `float `1.0`; + add `float 1.0` and `float 2.0` -> `float 3.0`; + assign `float 3.0` to `f`; + (note this example illustrates promotion done at run-time as promotion + done at compile-time would have resolved to a `def` type value) diff --git a/docs/painless/painless-comments.asciidoc b/docs/painless/painless-comments.asciidoc index 588e464d97f78..bde30e37e04e1 100644 --- a/docs/painless/painless-comments.asciidoc +++ b/docs/painless/painless-comments.asciidoc @@ -1,12 +1,12 @@ [[painless-comments]] === Comments -Use the `//` token anywhere on a line to specify a single-line comment. All -characters from the `//` token to the end of the line are ignored. Use an -opening `/*` token and a closing `*/` token to specify a multi-line comment. -Multi-line comments can start anywhere on a line, and all characters in between -the `/*` token and `*/` token are ignored. Comments can be included anywhere -within a script. +Use a comment to annotate or explain code within a script. Use the `//` token +anywhere on a line to specify a single-line comment. All characters from the +`//` token to the end of the line are ignored. Use an opening `/*` token and a +closing `*/` token to specify a multi-line comment. Multi-line comments can +start anywhere on a line, and all characters in between the `/*` token and `*/` +token are ignored. Comments can be included anywhere within a script. *Grammar* [source,ANTLR4] diff --git a/docs/painless/painless-identifiers.asciidoc b/docs/painless/painless-identifiers.asciidoc index 17073e3d4c415..7762f56cb7b23 100644 --- a/docs/painless/painless-identifiers.asciidoc +++ b/docs/painless/painless-identifiers.asciidoc @@ -1,10 +1,10 @@ [[painless-identifiers]] === Identifiers -Specify identifiers to <>, <>, and -<> variables, <>, and -<>. <> and -<> cannot be used as identifiers. +Use an identifier as a named token to specify a +<>, <>, +<>, <>, or function. +<> cannot be used as identifiers. *Grammar* [source,ANTLR4] diff --git a/docs/painless/painless-keywords.asciidoc b/docs/painless/painless-keywords.asciidoc index cb3bafbd20f13..39a2201fd2b4b 100644 --- a/docs/painless/painless-keywords.asciidoc +++ b/docs/painless/painless-keywords.asciidoc @@ -1,9 +1,9 @@ [[painless-keywords]] === Keywords -The keywords in the table below are reserved for built-in language -features. These keywords cannot be used as -<> or <>. +Keywords are reserved tokens for built-in language features and cannot be used +as <> within a script. The following are +keywords: [cols="^1,^1,^1,^1,^1"] |==== diff --git a/docs/painless/painless-lang-spec.asciidoc b/docs/painless/painless-lang-spec.asciidoc index ba6595000ae2f..5e6b84d8c57d1 100644 --- a/docs/painless/painless-lang-spec.asciidoc +++ b/docs/painless/painless-lang-spec.asciidoc @@ -6,7 +6,7 @@ Painless syntax is similar to Java syntax along with some additional features such as dynamic typing, Map and List accessor shortcuts, and array initializers. As a direct comparison to Java, there are some important differences, especially related to the casting model. For more detailed -conceptual information about the basic constructs that Java and Painless share, +conceptual information about the basic constructs that Painless and Java share, refer to the corresponding topics in the https://docs.oracle.com/javase/specs/jls/se8/html/index.html[Java Language Specification]. diff --git a/docs/painless/painless-literals.asciidoc b/docs/painless/painless-literals.asciidoc index 441cb264f1e15..ebf7eaa07b657 100644 --- a/docs/painless/painless-literals.asciidoc +++ b/docs/painless/painless-literals.asciidoc @@ -1,18 +1,19 @@ [[painless-literals]] === Literals -Use literals to specify different types of values directly in a script. +Use a literal to specify a value directly in an +<>. [[integers]] ==== Integers -Use integer literals to specify an integer value in decimal, octal, or hex -notation of the <> `int`, `long`, `float`, +Use an integer literal to specify an integer type value in decimal, octal, or +hex notation of a <> `int`, `long`, `float`, or `double`. Use the following single letter designations to specify the -<>: `l` or `L` for `long`, `f` or `F` for -`float`, and `d` or `D` for `double`. If not specified, the type defaults to -`int`. Use `0` as a prefix to specify an integer literal as octal, and use -`0x` or `0X` as a prefix to specify an integer literal as hex. +primitive type: `l` or `L` for `long`, `f` or `F` for `float`, and `d` or `D` +for `double`. If not specified, the type defaults to `int`. Use `0` as a prefix +to specify an integer literal as octal, and use `0x` or `0X` as a prefix to +specify an integer literal as hex. *Grammar* [source,ANTLR4] @@ -46,11 +47,10 @@ HEX: '-'? '0' [xX] [0-9a-fA-F]+ [lL]?; [[floats]] ==== Floats -Use floating point literals to specify a floating point value of the -<> `float` or `double`. Use the following -single letter designations to specify the <>: -`f` or `F` for `float` and `d` or `D` for `double`. If not specified, the type defaults -to `double`. +Use a floating point literal to specify a floating point type value of a +<> `float` or `double`. Use the following +single letter designations to specify the primitive type: `f` or `F` for `float` +and `d` or `D` for `double`. If not specified, the type defaults to `double`. *Grammar* [source,ANTLR4] @@ -81,7 +81,7 @@ EXPONENT: ( [eE] [+\-]? [0-9]+ ); [[strings]] ==== Strings -Use string literals to specify <> values with +Use a string literal to specify a <> value with either single-quotes or double-quotes. Use a `\"` token to include a double-quote as part of a double-quoted string literal. Use a `\'` token to include a single-quote as part of a single-quoted string literal. Use a `\\` @@ -117,26 +117,6 @@ STRING: ( '"' ( '\\"' | '\\\\' | ~[\\"] )*? '"' ) [[characters]] ==== Characters -Use the <> to convert string literals or -<> values into <> values. -<> values converted into -<> values must be exactly one character in length -or an error will occur. - -*Examples* - -* Casting string literals into <> values. -+ -[source,Painless] ----- -(char)"C" -(char)'c' ----- -+ -* Casting a <> value into a <> value. -+ -[source,Painless] ----- -String s = "s"; -char c = (char)s; ----- +A character literal cannot be specified directly. Instead, use the +<> to convert a `String` type value +into a `char` type value. diff --git a/docs/painless/painless-operators.asciidoc b/docs/painless/painless-operators.asciidoc index 915d811fa441b..8329686f663af 100644 --- a/docs/painless/painless-operators.asciidoc +++ b/docs/painless/painless-operators.asciidoc @@ -240,6 +240,7 @@ operator. See Function Calls [MARK] for more information. The brackets operator `[]` is used to create and access arrays, lists, and maps. The braces operator `{}` is used to intialize arrays. +[[array-initialization]] ===== Creating and Initializing Arrays You create and initialize arrays using the brackets `[]` and braces `{}` @@ -248,9 +249,49 @@ initialize each dimension with are specified as a comma-separated list enclosed in braces. For example, `new int[] {1, 2, 3}` creates a one dimensional `int` array with a size of 3 and the values 1, 2, and 3. -For more information about allocating and initializing arrays, see <>. +To allocate an array, you use the `new` keyword followed by the type and a +set of brackets for each dimension. You can explicitly define the size of each dimension by specifying an expression within the brackets, or initialize each +dimension with the desired number of values. The allocated size of each +dimension is its permanent size. +To initialize an array, specify the values you want to initialize +each dimension with as a comma-separated list of expressions enclosed in braces. +For example, `new int[] {1, 2, 3}` creates a one-dimensional `int` array with a +size of 3 and the values 1, 2, and 3. + +When you initialize an array, the order of the expressions is maintained. Each expression used as part of the initialization is converted to the +array's type. An error occurs if the types do not match. + +*Grammar:* +[source,ANTLR4] +---- +declare_array: TYPE ('[' ']')+; + +array_initialization: 'new' TYPE '[' ']' '{' expression (',' expression) '}' + | 'new' TYPE '[' ']' '{' '}'; +---- + +*Examples:* +[source,Java] +---- +int[] x = new int[5]; // Declare int array x and assign it a newly + // allocated int array with a size of 5 +def[][] y = new def[5][5]; // Declare the 2-dimensional def array y and + // assign it a newly allocated 2-dimensional + // array where both dimensions have a size of 5 +int[] x = new int[] {1, 2, 3}; // Declare int array x and set it to an int + // array with values 1, 2, 3 and a size of 3 +int i = 1; +long l = 2L; +float f = 3.0F; +double d = 4.0; +String s = "5"; +def[] da = new def[] {i, l, f*d, s}; // Declare def array da and set it to + // a def array with a size of 4 and the + // values i, l, f*d, and s +---- + +[[array-access]] ===== Accessing Array Elements Elements in an array are stored and accessed using the brackets `[]` operator. @@ -298,6 +339,7 @@ return d[z]; // Access the 1st element of array d using the NOTE: The use of the `def` type in the second example means that the types cannot be resolved until runtime. +[[array-length]] ===== Array Length Arrays contain a special member known as 'length' that is a read-only value that contains the size of the array. This member can be accessed from an array using the dot operator. @@ -727,6 +769,7 @@ def e; // declares the def variable e e = new HashMap(m); // sets e to a newly allocated HashMap using the constructor with a single argument m ---- +[[new-array]] ==== New Array An array type instance can be allocated using the new operator. The format starts with the new operator followed by the type followed by a series of opening and closing braces each containing an expression for the size of the dimension. diff --git a/docs/painless/painless-types.asciidoc b/docs/painless/painless-types.asciidoc index 9d575a2069ae3..a897b8e8a04f0 100644 --- a/docs/painless/painless-types.asciidoc +++ b/docs/painless/painless-types.asciidoc @@ -1,269 +1,466 @@ [[painless-types]] === Types -Painless supports both dynamic and static types. Static types are split into -_primitive types_ and _reference types_. - -[[dynamic-types]] -==== Dynamic Types - -Painless supports one dynamic type: `def`. The `def` type can represent any -primitive or reference type. When you use the `def` type, it mimics the exact -behavior of whatever type it represents at runtime. The default value for the -def type is `null.` - -Internally, if the `def` type represents a primitive type, it is converted to the -corresponding reference type. It still behaves like the primitive type, however, -including within the casting model. The `def` type can be assigned to different -types during the course of script execution. - -IMPORTANT: Because a `def` type variable can be assigned to different types -during execution, type conversion errors that occur when using the `def` type -happen at runtime. - -Using the `def` type can have a slight impact on performance. If performance is -critical, it's better to declare static types. - -*Examples:* -[source,Java] ----- -def x = 1; // Declare def variable x and set it to the - // literal int 1 -def l = new ArrayList(); // Declare def variable l and set it a newly - // allocated ArrayList ----- +A type is a classification of data used to define the properties of a value. +These properties specify what data a value represents and the rules for how a +value is evaluated during an <>. Each type +belongs to one of the following categories: <>, +<>, or <>. [[primitive-types]] ==== Primitive Types -Primitive types are allocated directly onto the stack according to the standard -Java memory model. - -Primitive types can behave as their corresponding (<>) -reference type. This means any piece of a reference type can be accessed or -called through the primitive type. Operations performed in this manner convert -the primitive type to its corresponding reference type at runtime and perform -the field access or method call without needing to perform any other -operations. - -Painless supports the following primitive types. - -byte:: -An 8-bit, signed, two's complement integer. -Range: [-128, 127]. -Default value: 0. -Reference type: Byte. - -short:: -A 16-bit, signed, two's complement integer. -Range: [-32768, 32767]. -Default value: 0. -Reference type: Short. - -char:: -A 16-bit Unicode character. -Range: [0, 65535]. -Default value: 0 or `\u0000`. -Reference type: Character. - -int:: -A 32-bit, signed, two's complement integer. -Range: [-2^32, 2^32-1]. -Default value: 0. -Reference type: Integer. - -long:: -A 64-bit, signed, two's complement integer. -Range: [-2^64, 2^64-1]. -Default value: 0. -Reference type: Long. - -float:: -A 32-bit, single-precision, IEEE 754 floating point number. -Range: Depends on multiple factors. -Default value: 0.0. -Reference type: Float. - -double:: -A 64-bit, double-precision, IEEE 754 floating point number. -Range: Depends on multiple factors. -Default value: 0.0. -Reference type: Double. - -boolean:: -A logical quanity with two possible values: true and false. -Range: true/false. -Default value: false. -Reference type: Boolean. - - -*Examples:* -[source,Java] +A primitive type represents basic data built natively into the JVM and is +allocated to non-heap memory. Declare a primitive type +<>, and assign it a primitive type value for +evaluation during later operations. The default value for a newly-declared +primitive type variable is listed as part of the definitions below. A primitive +type value is copied during an assignment or as an argument for a +method/function call. + +A primitive type has a corresponding reference type (also known as a boxed +type). Use the <> or +<> on a primitive type value to force +evaluation as its corresponding reference type value. + +The following primitive types are available: + +[horizontal] +`byte`:: +8-bit, signed, two's complement integer +* range: [`-128`, `127`] +* default value: `0` +* reference type: `Byte` + +`short`:: +16-bit, signed, two's complement integer +* range: [`-32768`, `32767`] +* default value: `0` +* reference type: `Short` + +`char`:: +16-bit, unsigned, Unicode character +* range: [`0`, `65535`] +* default value: `0` or `\u0000` +* reference type: `Character` + +`int`:: +32-bit, signed, two's complement integer +* range: [`-2^32`, `2^32-1`] +* default value: `0` +* reference type: `Integer` + +`long`:: +64-bit, signed, two's complement integer +* range: [`-2^64`, `2^64-1`] +* default value: `0` +* reference type: `Long` + +`float`:: +32-bit, signed, single-precision, IEEE 754 floating point number +* default value: `0.0` +* reference type: `Float` + +`double`:: +64-bit, signed, double-precision, IEEE 754 floating point number +* default value: `0.0` +* reference type: `Double` + +`boolean`:: +logical quantity with two possible values of `true` and `false` +* default value: `false` +* reference type: `Boolean` + +*Examples* + +* Primitive types used in declaration, declaration and assignment. ++ +[source,Painless] ---- -int i = 1; // Declare variable i as an int and set it to the - // literal 1 -double d; // Declare variable d as a double and set it to the - // default value of 0.0 -boolean b = true; // Declare variable b as a boolean and set it to true +<1> int i = 1; +<2> double d; +<3> boolean b = true; ---- - -Using methods from the corresponding reference type on a primitive type. - -[source,Java] ++ +<1> declare `int i`; + assign `int 1` to `i` +<2> declare `double d`; + assign default `double 0.0` to `d` +<3> declare `boolean b`; + assign `boolean true` to `b` ++ +* Method call on a primitive type using the corresponding reference type. ++ +[source,Painless] ---- -int i = 1; // Declare variable i as an int and set it to the - // literal 1 -i.toString(); // Invokes the Integer method toString on variable i +<1> int i = 1; +<2> i.toString(); ---- ++ +<1> declare `int i`; + assign `int 1` to `i` +<2> access `i` -> `int 1`; + box `int 1` -> `Integer 1 reference`; + call `toString` on `Integer 1 reference` -> `String '1'` [[reference-types]] ==== Reference Types -Reference types are similar to Java classes and can contain multiple pieces -known as _members_. However, reference types do not support access modifiers. -You allocate reference type instances on the heap using the `new` operator. - -Reference types can have both static and non-static members: - -* Static members are shared by all instances of the same reference type and -can be accessed without allocating an instance of the reference type. For -example `Integer.MAX_VALUE`. -* Non-static members are specific to an instance of the reference type -and can only be accessed through the allocated instance. - -The default value for a reference type is `null`, indicating that no memory has -been allocated for it. When you assign `null` to a reference type, its previous -value is discarded and garbage collected in accordance with the Java memory -model as long as there are no other references to that value. - -A reference type can contain: - -* Zero to many primitive types. Primitive type members can be static or -non-static and read-only or read-write. -* Zero to many reference types. Reference type members can be static or -non-static and read-only or read-write. -* Methods that call an internal function to return a value and/or manipulate -the primitive or reference type members. Method members can be static or -non-static. -* Constructors that call an internal function to return a newly-allocated -reference type instance. Constructors are non-static methods that can -optionally manipulate the primitive and reference type members. - -Reference types support a Java-style inheritance model. Consider types A and B. -Type A is considered to be a parent of B, and B a child of A, if B inherits -(is able to access as its own) all of A's fields and methods. Type B is +A reference type is a named construct (object), potentially representing +multiple pieces of data (member fields) and logic to manipulate that data +(member methods), defined as part of the application programming interface +(API) for scripts. + +A reference type instance is a single set of data for one reference type +object allocated to the heap. Use the +<> to allocate a reference type +instance. Use a reference type instance to load from, store to, and manipulate +complex data. + +A reference type value refers to a reference type instance, and multiple +reference type values may refer to the same reference type instance. A change to +a reference type instance will affect all reference type values referring to +that specific instance. + +Declare a reference type <>, and assign it a +reference type value for evaluation during later operations. The default value +for a newly-declared reference type variable is `null`. A reference type value +is shallow-copied during an assignment or as an argument for a method/function +call. Assign `null` to a reference type variable to indicate the reference type +value refers to no reference type instance. The JVM will garbage collect a +reference type instance when it is no longer referred to by any reference type +values. Pass `null` as an argument to a method/function call to indicate the +argument refers to no reference type instance. + +A reference type object defines zero-to-many of each of the following: + +static member field:: + +A static member field is a named and typed piece of data. Each reference type +*object* contains one set of data representative of its static member fields. +Use the <> in correspondence with the +reference type object name to access a static member field for loading and +storing to a specific reference type *object*. No reference type instance +allocation is necessary to use a static member field. + +non-static member field:: + +A non-static member field is a named and typed piece of data. Each reference +type *instance* contains one set of data representative of its reference type +object's non-static member fields. Use the +<> for loading and storing to a non-static +member field of a specific reference type *instance*. An allocated reference +type instance is required to use a non-static member field. + +static member method:: + +A static member method is a function called on a reference type *object*. Use +the <> in correspondence with the reference +type object name to call a static member method. No reference type instance +allocation is necessary to use a static member method. + +non-static member method:: + +A non-static member method is a function called on a reference type *instance*. +A non-static member method called on a reference type instance can load from and +store to non-static member fields of that specific reference type instance. Use +the <> in correspondence with a specific +reference type instance to call a non-static member method. An allocated +reference type instance is required to use a non-static member method. + +constructor:: + +A constructor is a special type of function used to allocate a reference type +*instance* defined by a specific reference type *object*. Use the +<> to allocate a reference type +instance. + +A reference type object follows a basic inheritance model. Consider types A and +B. Type A is considered to be a parent of B, and B a child of A, if B inherits +(is able to access as its own) all of A's non-static members. Type B is considered a descendant of A if there exists a recursive parent-child relationship from B to A with none to many types in between. In this case, B -inherits all of A's fields and methods along with all of the fields and -methods of the types in between. Type B is also considered to be a type A -in both relationships. - -For the complete list of Painless reference types and their supported methods, -see the https://www.elastic.co/guide/en/elasticsearch/reference/current/painless-api-reference.html[Painless API Reference]. +inherits all of A's non-static members along with all of the non-static members +of the types in between. Type B is also considered to be a type A in both +relationships. -For more information about working with reference types, see -<> and <>. +*Examples* -*Examples:* -[source,Java] +* Reference types evaluated in several different operations. ++ +[source,Painless] +---- +<1> List l = new ArrayList(); +<2> l.add(1); +<3> int i = l.get(0) + 2; +---- ++ +<1> declare `List l`; + allocate `ArrayList` instance -> `ArrayList reference`; + implicit cast `ArrayList reference` to `List reference` -> `List reference`; + assign `List reference` to `l` +<2> access `l` -> `List reference`; + implicit cast `int 1` to `def` -> `def` + call `add` on `List reference` with arguments (`def`) +<3> declare `int i`; + access `l` -> `List reference`; + call `get` on `List reference` with arguments (`int 0`) -> `def`; + implicit cast `def` to `int 1` -> `int 1`; + add `int 1` and `int 2` -> `int 3`; + assign `int 3` to `i` ++ +* Sharing a reference type instance. ++ +[source,Painless] ---- -ArrayList al = new ArrayList(); // Declare variable al as an ArrayList and - // set it to a newly allocated ArrayList -List l = new ArrayList(); // Declare variable l as a List and set - // it to a newly allocated ArrayList, which is - // allowed because ArrayList inherits from List -Map m; // Declare variable m as a Map and set it - // to the default value of null +<1> List l0 = new ArrayList(); +<2> List l1 = l0; +<3> l0.add(1); +<4> l1.add(2); +<5> int i = l1.get(0) + l0.get(1); ---- ++ +<1> declare `List l0`; + allocate `ArrayList` instance -> `ArrayList reference`; + implicit cast `ArrayList reference` to `List reference` -> `List reference`; + assign `List reference` to `l0` +<2> declare `List l1`; + access `l0` -> `List reference`; + assign `List reference` to `l1` + (note `l0` and `l1` refer to the same instance known as a shallow-copy) +<3> access `l0` -> `List reference`; + implicit cast `int 1` to `def` -> `def` + call `add` on `List reference` with arguments (`def`) +<4> access `l1` -> `List reference`; + implicit cast `int 2` to `def` -> `def` + call `add` on `List reference` with arguments (`def`) +<5> declare `int i`; + access `l0` -> `List reference`; + call `get` on `List reference` with arguments (`int 0`) -> `def @0`; + implicit cast `def @0` to `int 1` -> `int 1`; + access `l1` -> `List reference`; + call `get` on `List reference` with arguments (`int 1`) -> `def @1`; + implicit cast `def @1` to `int 2` -> `int 2`; + add `int 1` and `int 2` -> `int 3`; + assign `int 3` to `i`; ++ +* Using the static members of a reference type. ++ +[source,Painless] +---- +<1> int i = Integer.MAX_VALUE; +<2> long l = Long.parseLong("123L"); +---- ++ +<1> declare `int i`; + access `MAX_VALUE` on `Integer` -> `int 2147483647`; + assign `int 2147483647` to `i` +<2> declare `long l`; + call `parseLong` on `Long` with arguments (`long 123`) -> `long 123`; + assign `long 123` to `l` + +[[dynamic-types]] +==== Dynamic Types + +A dynamic type value can represent the value of any primitive type or +reference type using a single type name `def`. A `def` type value mimics +the behavior of whatever value it represents at run-time and will always +represent the child-most descendant type value of any type value when evaluated +during operations. + +Declare a `def` type <>, and assign it +any type of value for evaluation during later operations. The default value +for a newly-declared `def` type variable is `null`. A `def` type variable or +method/function parameter can change the type it represents during the +compilation and evaluation of a script. + +Using the `def` type can have a slight impact on performance. Use only primitive +types and reference types directly when performance is critical. + +*Errors* -Directly accessing static pieces of a reference type. +* If a `def` type value represents an inappropriate type for evaluation of an + operation at run-time. -[source,Java] +*Examples* + +* General uses of the `def` type. ++ +[source,Painless] +---- +<1> def dp = 1; +<2> def dr = new ArrayList(); +<3> dr = dp; +---- ++ +<1> declare `def dp`; + implicit cast `int 1` to `def` -> `def`; + assign `def` to `dp` +<2> declare `def dr`; + allocate `ArrayList` instance -> `ArrayList reference`; + implicit cast `ArrayList reference` to `def` -> `def`; + assign `def` to `dr` +<3> access `dp` -> `def`; + assign `def` to `dr`; + (note the switch in the type `dr` represents from `ArrayList` to `int`) ++ +* A `def` type value representing the child-most descendant of a value. ++ +[source,Painless] ---- -Integer.MAX_VALUE // a static field access -Long.parseLong("123L") // a static function call +<1> Object l = new ArrayList(); +<2> def d = l; +<3> d.ensureCapacity(10); ---- ++ +<1> declare `Object l`; + allocate `ArrayList` instance -> `ArrayList reference`; + implicit cast `ArrayList reference` to `Object reference` + -> `Object reference`; + assign `Object reference` to `l` +<2> declare `def d`; + access `l` -> `Object reference`; + implicit cast `Object reference` to `def` -> `def`; + assign `def` to `d`; +<3> access `d` -> `def`; + implicit cast `def` to `ArrayList reference` -> `ArrayList reference`; + call `ensureCapacity` on `ArrayList reference` with arguments (`int 10`); + (note `def` was implicit cast to `ArrayList reference` + since ArrayList` is the child-most descendant type value that the + `def` type value represents) [[string-type]] ==== String Type -A `String` is a specialized reference type that is immutable and does not have -to be explicitly allocated. You can directly assign to a `String` without first -allocating it with the `new` keyword. (Strings can be allocated with the `new` -keyword, but it's not required.) +The `String` type is a specialized reference type that does not require +explicit allocation. Use a <> to directly evaluate a +`String` type value. While not required, the +<> can allocate `String` type +instances. -When assigning a value to a `String`, you must enclose the text in single or -double quotes. Strings are allocated according to the standard Java Memory Model. -The default value for a `String` is `null.` +*Examples* -*Examples:* -[source,Java] +* General use of the `String` type. ++ +[source,Painless] ---- -String r = "some text"; // Declare String r and set it to the - // String "some text" -String s = 'some text'; // Declare String s and set it to the - // String 'some text' -String t = new String("some text"); // Declare String t and set it to the - // String "some text" -String u; // Declare String u and set it to the - // default value null +<1> String r = "some text"; +<2> String s = 'some text'; +<3> String t = new String("some text"); +<4> String u; ---- ++ +<1> declare `String r`; + assign `String "some text"` to `r` +<2> declare `String s`; + assign `String 'some text'` to `s` +<3> declare `String t`; + allocate `String` instance with arguments (`String "some text"`) + -> `String "some text"`; + assign `String "some text"` to `t` +<4> declare `String u`; + assign default `null` to `u` [[void-type]] ==== void Type -The `void` type represents the concept of no type. In Painless, `void` declares -that a function has no return value. +The `void` type represents the concept of a lack of type. Use the `void` type to +indicate a function returns no value. -[[array-type]] -==== Array Type +*Examples* -Arrays contain a series of elements of the same type that can be allocated -simultaneously. Painless supports both single and multi-dimensional arrays for -all types except void (including `def`). - -You declare an array by specifying a type followed by a series of empty brackets, -where each set of brackets represents a dimension. Declared arrays have a default -value of `null` and are themselves a reference type. - -To allocate an array, you use the `new` keyword followed by the type and a -set of brackets for each dimension. You can explicitly define the size of each dimension by specifying an expression within the brackets, or initialize each -dimension with the desired number of values. The allocated size of each -dimension is its permanent size. - -To initialize an array, specify the values you want to initialize -each dimension with as a comma-separated list of expressions enclosed in braces. -For example, `new int[] {1, 2, 3}` creates a one-dimensional `int` array with a -size of 3 and the values 1, 2, and 3. +* Use of the `void` type in a function. ++ +[source,Painless] +---- +void addToList(List l, def d) { + l.add(d); +} +---- -When you initialize an array, the order of the expressions is maintained. Each expression used as part of the initialization is converted to the -array's type. An error occurs if the types do not match. +[[array-type]] +==== Array Type -*Grammar:* -[source,ANTLR4] +An array type is a specialized reference type where an array type instance +represents a series of values allocated to the heap. All values in an array +type instance are of the same type. Each value is assigned an index from within +the range `[0, length)` where length is the total number of values allocated for +the array type instance. + +Use the <> or the +<> to allocate an array +type instance. Declare an array type <>, and +assign it an array type value for evaluation during later operations. The +default value for a newly-declared array type variable is `null`. An array type +value is shallow-copied during an assignment or as an argument for a +method/function call. Assign `null` to an array type variable to indicate the +array type value refers to no array type instance. The JVM will garbage collect +an array type instance when it is no longer referred to by any array type +values. Pass `null` as an argument to a method/function call to indicate the +argument refers to no array type instance. + +Use the <> to retrieve the length of an +array type value as an int type value. Use the +<> to load from and store to individual +values within an array type value. + +When an array type instance is allocated with multiple dimensions using the +range `[2, d]` where `d >= 2`, each dimension in the range `[1, d-1]` is also +an array type. The array type of each dimension, `n`, is an array type with the +number of dimensions equal to `d-n`. For example, consider `int[][][]` with 3 +dimensions. The 3rd dimension, `d-3`, is the primitive type `int`. The 2nd +dimension, `d-2`, is the array type `int[]`. And the 1st dimension, `d-1` is +the array type `int[][]`. + +*Examples* + +* General use of single-dimensional arrays. ++ +[source,Painless] ---- -declare_array: TYPE ('[' ']')+; - -array_initialization: 'new' TYPE '[' ']' '{' expression (',' expression) '}' - | 'new' TYPE '[' ']' '{' '}'; +<1> int[] x; +<2> float[] y = new float[10]; +<3> def z = new float[5]; +<4> y[9] = 1.0F; +<5> z[0] = y[9]; ---- - -*Examples:* -[source,Java] ++ +<1> declare `int[] x`; + assign default `null` to `x` +<2> declare `float[] y`; + allocate `1-d float array` instance with `length [10]` + -> `1-d float array reference`; + assign `1-d float array reference` to `y` +<3> declare `def z`; + allocate `1-d float array` instance with `length [5]` + -> `1-d float array reference`; + implicit cast `1-d float array reference` to `def` -> `def`; + assign `def` to `z` +<4> access `y` -> `1-d float array reference`; + assign `float 1.0` to `index [9]` of `1-d float array reference` +<5> access `y` -> `1-d float array reference @0`; + access `index [9]` of `1-d float array reference @0` -> `float 1.0`; + access `z` -> `def`; + implicit cast `def` to `1-d float array reference @1` + -> `1-d float array reference @1`; + assign `float 1.0` to `index [0]` of `1-d float array reference @1` ++ +* Use of a multi-dimensional array. ++ +[source,Painless] ---- -int[] x = new int[5]; // Declare int array x and assign it a newly - // allocated int array with a size of 5 -def[][] y = new def[5][5]; // Declare the 2-dimensional def array y and - // assign it a newly allocated 2-dimensional - // array where both dimensions have a size of 5 -int[] x = new int[] {1, 2, 3}; // Declare int array x and set it to an int - // array with values 1, 2, 3 and a size of 3 -int i = 1; -long l = 2L; -float f = 3.0F; -double d = 4.0; -String s = "5"; -def[] da = new def[] {i, l, f*d, s}; // Declare def array da and set it to - // a def array with a size of 4 and the - // values i, l, f*d, and s +<1> int[][][] ia3 = new int[2][3][4]; +<2> ia3[1][2][3] = 99; +<3> int i = ia3[1][2][3]; ---- ++ +<1> declare `int[][][] ia`; + allocate `3-d int array` instance with length `[2, 3, 4]` + -> `3-d int array reference`; + assign `3-d int array reference` to `ia3` +<2> access `ia3` -> `3-d int array reference`; + assign `int 99` to `index [1, 2, 3]` of `3-d int array reference` +<3> declare `int i`; + access `ia3` -> `3-d int array reference`; + access `index [1, 2, 3]` of `3-d int array reference` -> `int 99`; + assign `int 99` to `i` diff --git a/docs/painless/painless-variables.asciidoc b/docs/painless/painless-variables.asciidoc index 9756676a08b5b..8b8782b151132 100644 --- a/docs/painless/painless-variables.asciidoc +++ b/docs/painless/painless-variables.asciidoc @@ -1,29 +1,31 @@ [[painless-variables]] === Variables -<> variables to <> values for -<> in expressions. Specify variables as a -<>, <>, or -<>. Variable operations follow the structure of a -standard JVM in relation to instruction execution and memory usage. +A variable loads and stores a value for evaluation during +<>. [[declaration]] ==== Declaration -Declare variables before use with the format of <> -<>. Specify a comma-separated list of -<> following the <> -to declare multiple variables in a single statement. Use an -<> statement combined with a declaration statement to -immediately assign a value to a variable. Variables not immediately assigned a -value will have a default value assigned implicitly based on the -<>. +Declare a variable before use with the format of <> +followed by <>. Declare an +<> variable using an opening `[` token and a closing `]` +token for each dimension directly after the identifier. Specify a +comma-separated list of identifiers following the type to declare multiple +variables in a single statement. Use an <> +combined with a declaration to immediately assign a value to a variable. +A variable not immediately assigned a value will have a default value assigned +implicitly based on the type. + +*Errors* + +* If a variable is used prior to or without declaration. *Grammar* [source,ANTLR4] ---- declaration : type ID assignment? (',' ID assignment?)*; -type: ID ('[' ']')*; +type: ID ('.' ID)* ('[' ']')*; assignment: '=' expression; ---- @@ -35,27 +37,43 @@ assignment: '=' expression; ---- <1> int x; <2> List y; -<3> int x, y, z; -<4> def[] d; +<3> int x, y = 5, z; +<4> def d; <5> int i = 10; ----- -+ -<1> declare a variable of type `int` and identifier `x` -<2> declare a variable of type `List` and identifier `y` -<3> declare three variables of type `int` and identifiers `x`, `y`, `z` -<4> declare a variable of type `def[]` and identifier `d` -<5> declare a variable of type `int` and identifier `i`; - assign the integer literal `10` to `i` +<6> float[] f; +<7> Map[][] m; +---- ++ +<1> declare `int x`; + assign default `null` to `x` +<2> declare `List y`; + assign default `null` to `y` +<3> declare `int x`; + assign default `int 0` to `x`; + declare `int y`; + assign `int 5` to `y`; + declare `int z`; + assign default `int 0` to `z`; +<4> declare `def d`; + assign default `null` to `d` +<5> declare `int i`; + assign `int 10` to `i` +<6> declare `float[] f`; + assign default `null` to `f` +<7> declare `Map[][] m`; + assign default `null` to `m` [[assignment]] ==== Assignment -Use the `equals` operator (`=`) to assign a value to a variable. Any expression +Use the *assignment operator* to store a value in a variable. Any operation that produces a value can be assigned to any variable as long as the -<> are the same or the resultant -<> can be implicitly <> to -the variable <>. Otherwise, an error will occur. -<> values are shallow-copied when assigned. +<> are the same or the resultant type can be +<> to the variable type. + +*Errors* + +* If the type of value is unable to match the type of variable. *Grammar* [source,ANTLR4] @@ -65,7 +83,7 @@ assignment: ID '=' expression *Examples* -* Variable assignment with an <>. +* Variable assignment with an integer literal. + [source,Painless] ---- @@ -73,10 +91,11 @@ assignment: ID '=' expression <2> i = 10; ---- + -<1> declare `int i` -<2> assign `10` to `i` +<1> declare `int i`; + assign default `int 0` to `i` +<2> assign `int 10` to `i` + -* <> combined with immediate variable assignment. +* Declaration combined with immediate assignment. + [source,Painless] ---- @@ -84,11 +103,12 @@ assignment: ID '=' expression <2> double j = 2.0; ---- + -<1> declare `int i`; assign `10` to `i` -<2> declare `double j`; assign `2.0` to `j` +<1> declare `int i`; + assign `int 10` to `i` +<2> declare `double j`; + assign `double 2.0` to `j` + -* Assignment of one variable to another using -<>. +* Assignment of one variable to another using primitive types. + [source,Painless] ---- @@ -96,11 +116,13 @@ assignment: ID '=' expression <2> int j = i; ---- + -<1> declare `int i`; assign `10` to `i` -<2> declare `int j`; assign `j` to `i` +<1> declare `int i`; + assign `int 10` to `i` +<2> declare `int j`; + access `i` -> `int 10`; + assign `int 10` to `j` + -* Assignment with <> using the -<>. +* Assignment with reference types using the *new instance operator*. + [source,Painless] ---- @@ -108,12 +130,15 @@ assignment: ID '=' expression <2> Map m = new HashMap(); ---- + -<1> declare `ArrayList l`; assign a newly-allocated `Arraylist` to `l` -<2> declare `Map m`; assign a newly-allocated `HashMap` to `m` - with an implicit cast to `Map` +<1> declare `ArrayList l`; + allocate `ArrayList` instance -> `ArrayList reference`; + assign `ArrayList reference` to `l` +<2> declare `Map m`; + allocate `HashMap` instance -> `HashMap reference`; + implicit cast `HashMap reference` to `Map reference` -> `Map reference`; + assign `Map reference` to `m` + -* Assignment of one variable to another using -<>. +* Assignment of one variable to another using reference types. + [source,Painless] ---- @@ -123,8 +148,52 @@ assignment: ID '=' expression <4> m = k; ---- + -<1> declare `List l`; assign a newly-allocated `Arraylist` to `l` - with an implicit cast to `List` -<2> declare `List k`; assign a shallow-copy of `l` to `k` +<1> declare `List l`; + allocate `ArrayList` instance -> `ArrayList reference`; + implicit cast `ArrayList reference` to `List reference` -> `List reference`; + assign `List reference` to `l` +<2> declare `List k`; + access `l` -> `List reference`; + assign `List reference` to `k`; + (note `l` and `k` refer to the same instance known as a shallow-copy) <3> declare `List m`; -<4> assign a shallow-copy of `k` to `m` + assign default `null` to `m` +<4> access `k` -> `List reference`; + assign `List reference` to `m`; + (note `l`, `k`, and `m` refer to the same instance) ++ +* Assignment with an array type variable using the *new array operator*. ++ +[source,Painless] +---- +<1> int[] ia1; +<2> ia1 = new int[2]; +<3> ia1[0] = 1; +<4> int[] ib1 = ia1; +<5> int[][] ic2 = new int[2][5]; +<6> ic2[1][3] = 2; +<7> ic2[0] = ia1; +---- ++ +<1> declare `int[] ia1`; + assign default `null` to `ia1` +<2> allocate `1-d int array` instance with `length [2]` + -> `1-d int array reference`; + assign `1-d int array reference` to `ia1` +<3> access `ia1` -> `1-d int array reference`; + assign `int 1` to `index [0]` of `1-d int array reference` +<4> declare `int[] ib1`; + access `ia1` -> `1-d int array reference`; + assign `1-d int array reference` to `ib1`; + (note `ia1` and `ib1` refer to the same instance known as a shallow copy) +<5> declare `int[][] ic2`; + allocate `2-d int array` instance with `length [2, 5]` + -> `2-d int array reference`; + assign `2-d int array reference` to `ic2` +<6> access `ic2` -> `2-d int array reference`; + assign `int 2` to `index [1, 3]` of `2-d int array reference` +<7> access `ia1` -> `1-d int array reference`; + access `ic2` -> `2-d int array reference`; + assign `1-d int array reference` to + `index [0]` of `2-d int array reference`; + (note `ia1`, `ib1`, and `index [0]` of `ia2` refer to the same instance) diff --git a/docs/reference/mapping/types.asciidoc b/docs/reference/mapping/types.asciidoc index 2cbc3a5bc54ad..ecb2e8dace23a 100644 --- a/docs/reference/mapping/types.asciidoc +++ b/docs/reference/mapping/types.asciidoc @@ -40,6 +40,8 @@ string:: <> and <> <>:: Defines parent/child relation for documents within the same index +<>:: Record numeric features to boost hits at query time. + [float] === Multi-fields @@ -86,6 +88,6 @@ include::types/percolator.asciidoc[] include::types/parent-join.asciidoc[] - +include::types/feature.asciidoc[] diff --git a/docs/reference/mapping/types/feature.asciidoc b/docs/reference/mapping/types/feature.asciidoc new file mode 100644 index 0000000000000..3b5e78d5fb46b --- /dev/null +++ b/docs/reference/mapping/types/feature.asciidoc @@ -0,0 +1,59 @@ +[[feature]] +=== Feature datatype + +A `feature` field can index numbers so that they can later be used to boost +documents in queries with a <> query. + +[source,js] +-------------------------------------------------- +PUT my_index +{ + "mappings": { + "_doc": { + "properties": { + "pagerank": { + "type": "feature" <1> + }, + "url_length": { + "type": "feature", + "positive_score_impact": false <2> + } + } + } + } +} + +PUT my_index/_doc/1 +{ + "pagerank": 8, + "url_length": 22 +} + +GET my_index/_search +{ + "query": { + "feature": { + "field": "pagerank" + } + } +} +-------------------------------------------------- +// CONSOLE +<1> Feature fields must use the `feature` field type +<2> Features that correlate negatively with the score need to declare it + +NOTE: `feature` fields only support single-valued fields and strictly positive +values. Multi-valued fields and negative values will be rejected. + +NOTE: `feature` fields do not support querying, sorting or aggregating. They may +only be used within <> queries. + +NOTE: `feature` fields only preserve 9 significant bits for the precision, which +translates to a relative error of about 0.4%. + +Features that correlate negatively with the score should set +`positive_score_impact` to `false` (defaults to `true`). This will be used by +the <> query to modify the scoring formula +in such a way that the score decreases with the value of the feature instead of +increasing. For instance in web search, the url length is a commonly used +feature which correlates negatively with scores. diff --git a/docs/reference/query-dsl/feature-query.asciidoc b/docs/reference/query-dsl/feature-query.asciidoc new file mode 100644 index 0000000000000..19c29b1cf3ab8 --- /dev/null +++ b/docs/reference/query-dsl/feature-query.asciidoc @@ -0,0 +1,181 @@ +[[query-dsl-feature-query]] +=== Feature Query + +The `feature` query is a specialized query that only works on +<> fields. Its goal is to boost the score of documents based +on the values of numeric features. It is typically put in a `should` clause of +a <> query so that its score is added to the score +of the query. + +Compared to using <> or other +ways to modify the score, this query has the benefit of being able to +efficiently skip non-competitive hits when +<> is set to `false`. Speedups may be +spectacular. + +Here is an example: + +[source,js] +-------------------------------------------------- +PUT test +{ + "mappings": { + "_doc": { + "properties": { + "pagerank": { + "type": "feature" + }, + "url_length": { + "type": "feature", + "positive_score_impact": false + } + } + } + } +} + +PUT test/_doc/1 +{ + "pagerank": 10, + "url_length": 50 +} + +PUT test/_doc/2 +{ + "pagerank": 100, + "url_length": 20 +} + +POST test/_refresh + +GET test/_search +{ + "query": { + "feature": { + "field": "pagerank" + } + } +} + +GET test/_search +{ + "query": { + "feature": { + "field": "url_length" + } + } +} +-------------------------------------------------- +// CONSOLE + +[float] +=== Supported functions + +The `feature` query supports 3 functions in order to boost scores using the +values of features. If you do not know where to start, we recommend that you +start with the `saturation` function, which is the default when no function is +provided. + +[float] +==== Saturation + +This function gives a score that is equal to `S / (S + pivot)` where `S` is the +value of the feature and `pivot` is a configurable pivot value so that the +result will be less than +0.5+ if `S` is less than pivot and greater than +0.5+ +otherwise. Scores are always is +(0, 1)+. + +If the feature has a negative score impact then the function will be computed as +`pivot / (S + pivot)`, which decreases when `S` increases. + +[source,js] +-------------------------------------------------- +GET test/_search +{ + "query": { + "feature": { + "field": "pagerank", + "saturation": { + "pivot": 8 + } + } + } +} +-------------------------------------------------- +// CONSOLE +// TEST[continued] + +If +pivot+ is not supplied then Elasticsearch will compute a default value that +will be approximately equal to the geometric mean of all feature values that +exist in the index. We recommend this if you haven't had the opportunity to +train a good pivot value. + +[source,js] +-------------------------------------------------- +GET test/_search +{ + "query": { + "feature": { + "field": "pagerank", + "saturation": {} + } + } +} +-------------------------------------------------- +// CONSOLE +// TEST[continued] + +[float] +==== Logarithm + +This function gives a score that is equal to `log(scaling_factor + S)` where +`S` is the value of the feature and `scaling_factor` is a configurable scaling +factor. Scores are unbounded. + +This function only supports features that have a positive score impact. + +[source,js] +-------------------------------------------------- +GET test/_search +{ + "query": { + "feature": { + "field": "pagerank", + "log": { + "scaling_factor": 4 + } + } + } +} +-------------------------------------------------- +// CONSOLE +// TEST[continued] + +[float] +==== Sigmoid + +This function is an extension of `saturation` which adds a configurable +exponent. Scores are computed as `S^exp^ / (S^exp^ + pivot^exp^)`. Like for the +`saturation` function, `pivot` is the value of `S` that gives a score of +0.5+ +and scores are in +(0, 1)+. + +`exponent` must be positive, but is typically in +[0.5, 1]+. A good value should +be computed via traning. If you don't have the opportunity to do so, we recommend +that you stick to the `saturation` function instead. + +[source,js] +-------------------------------------------------- +GET test/_search +{ + "query": { + "feature": { + "field": "pagerank", + "sigmoid": { + "pivot": 7, + "exponent": 0.6 + } + } + } +} +-------------------------------------------------- +// CONSOLE +// TEST[continued] diff --git a/docs/reference/query-dsl/geo-shape-query.asciidoc b/docs/reference/query-dsl/geo-shape-query.asciidoc index 08b504951e1bf..4e00a2f49b475 100644 --- a/docs/reference/query-dsl/geo-shape-query.asciidoc +++ b/docs/reference/query-dsl/geo-shape-query.asciidoc @@ -93,6 +93,7 @@ to 'shapes'. * `type` - Index type where the pre-indexed shape is. * `path` - The field specified as path containing the pre-indexed shape. Defaults to 'shape'. +* `routing` - The routing of the shape document if required. The following is an example of using the Filter with a pre-indexed shape: diff --git a/docs/reference/query-dsl/special-queries.asciidoc b/docs/reference/query-dsl/special-queries.asciidoc index a062fa7ddb1fb..4c69889040eb1 100644 --- a/docs/reference/query-dsl/special-queries.asciidoc +++ b/docs/reference/query-dsl/special-queries.asciidoc @@ -19,6 +19,11 @@ This query allows a script to act as a filter. Also see the This query finds queries that are stored as documents that match with the specified document. +<>:: + +A query that computes scores based on the values of numeric features and is +able to efficiently skip non-competitive hits. + <>:: A query that accepts other queries as json or yaml string. @@ -29,4 +34,6 @@ include::script-query.asciidoc[] include::percolate-query.asciidoc[] +include::feature-query.asciidoc[] + include::wrapper-query.asciidoc[] diff --git a/docs/reference/search/request/docvalue-fields.asciidoc b/docs/reference/search/request/docvalue-fields.asciidoc index b4d2493d8536d..9d917c27ab084 100644 --- a/docs/reference/search/request/docvalue-fields.asciidoc +++ b/docs/reference/search/request/docvalue-fields.asciidoc @@ -11,13 +11,38 @@ GET /_search "query" : { "match_all": {} }, - "docvalue_fields" : ["test1", "test2"] + "docvalue_fields" : [ + { + "field": "my_ip_field", <1> + "format": "use_field_mapping" <2> + }, + { + "field": "my_date_field", + "format": "epoch_millis" <3> + } + ] } -------------------------------------------------- // CONSOLE +<1> the name of the field +<2> the special `use_field_mapping` format tells Elasticsearch to use the format from the mapping +<3> date fields may use a custom format Doc value fields can work on fields that are not stored. Note that if the fields parameter specifies fields without docvalues it will try to load the value from the fielddata cache causing the terms for that field to be loaded to memory (cached), which will result in more memory consumption. +[float] +==== Custom formats + +While most fields do not support custom formats, some of them do: + - <> fields can take any <>. + - <> fields accept a https://docs.oracle.com/javase/8/docs/api/java/text/DecimalFormat.html[DecimalFormat pattern]. + +All fields support the special `use_field_mapping` format, which tells +Elasticsearch to use the mappings to figure out a default format. + +NOTE: The default is currently to return the same output as +<>. However it will change in 7.0 +to behave as if the `use_field_mapping` format was provided. diff --git a/docs/reference/search/request/inner-hits.asciidoc b/docs/reference/search/request/inner-hits.asciidoc index dce6bb2a2d8bc..887ae2bdf149e 100644 --- a/docs/reference/search/request/inner-hits.asciidoc +++ b/docs/reference/search/request/inner-hits.asciidoc @@ -242,7 +242,12 @@ POST test/_search }, "inner_hits": { "_source" : false, - "docvalue_fields" : ["comments.text.keyword"] + "docvalue_fields" : [ + { + "field": "comments.text.keyword", + "format": "use_field_mapping" + } + ] } } } diff --git a/docs/reference/search/request/search-type.asciidoc b/docs/reference/search/request/search-type.asciidoc index 622b01c453e0a..7cac034f29c25 100644 --- a/docs/reference/search/request/search-type.asciidoc +++ b/docs/reference/search/request/search-type.asciidoc @@ -7,7 +7,7 @@ scattered to all the relevant shards and then all the results are gathered back. When doing scatter/gather type execution, there are several ways to do that, specifically with search engines. -One of the questions when executing a distributed search is how much +One of the questions when executing a distributed search is how many results to retrieve from each shard. For example, if we have 10 shards, the 1st shard might hold the most relevant results from 0 till 10, with other shards results ranking below it. For this reason, when executing a diff --git a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/FeatureFieldMapper.java b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/FeatureFieldMapper.java new file mode 100644 index 0000000000000..5b0158ff55b5f --- /dev/null +++ b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/FeatureFieldMapper.java @@ -0,0 +1,248 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.index.mapper; + +import org.apache.lucene.document.FeatureField; +import org.apache.lucene.index.IndexOptions; +import org.apache.lucene.index.IndexableField; +import org.apache.lucene.index.Term; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.TermQuery; +import org.elasticsearch.common.lucene.Lucene; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser.Token; +import org.elasticsearch.common.xcontent.support.XContentMapValues; +import org.elasticsearch.index.fielddata.IndexFieldData; +import org.elasticsearch.index.fielddata.plain.DocValuesIndexFieldData; +import org.elasticsearch.index.query.QueryShardContext; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * A {@link FieldMapper} that exposes Lucene's {@link FeatureField}. + */ +public class FeatureFieldMapper extends FieldMapper { + + public static final String CONTENT_TYPE = "feature"; + + public static class Defaults { + public static final MappedFieldType FIELD_TYPE = new FeatureFieldType(); + + static { + FIELD_TYPE.setTokenized(false); + FIELD_TYPE.setIndexOptions(IndexOptions.NONE); + FIELD_TYPE.setHasDocValues(false); + FIELD_TYPE.setOmitNorms(true); + FIELD_TYPE.freeze(); + } + } + + public static class Builder extends FieldMapper.Builder { + + public Builder(String name) { + super(name, Defaults.FIELD_TYPE, Defaults.FIELD_TYPE); + builder = this; + } + + @Override + public FeatureFieldType fieldType() { + return (FeatureFieldType) super.fieldType(); + } + + public Builder positiveScoreImpact(boolean v) { + fieldType().setPositiveScoreImpact(v); + return builder; + } + + @Override + public FeatureFieldMapper build(BuilderContext context) { + setupFieldType(context); + return new FeatureFieldMapper( + name, fieldType, defaultFieldType, + context.indexSettings(), multiFieldsBuilder.build(this, context), copyTo); + } + } + + public static class TypeParser implements Mapper.TypeParser { + @Override + public Mapper.Builder parse(String name, Map node, ParserContext parserContext) throws MapperParsingException { + FeatureFieldMapper.Builder builder = new FeatureFieldMapper.Builder(name); + for (Iterator> iterator = node.entrySet().iterator(); iterator.hasNext();) { + Map.Entry entry = iterator.next(); + String propName = entry.getKey(); + Object propNode = entry.getValue(); + if (propName.equals("positive_score_impact")) { + builder.positiveScoreImpact(XContentMapValues.nodeBooleanValue(propNode)); + iterator.remove(); + } + } + return builder; + } + } + + public static final class FeatureFieldType extends MappedFieldType { + + private boolean positiveScoreImpact = true; + + public FeatureFieldType() { + setIndexAnalyzer(Lucene.KEYWORD_ANALYZER); + setSearchAnalyzer(Lucene.KEYWORD_ANALYZER); + } + + protected FeatureFieldType(FeatureFieldType ref) { + super(ref); + this.positiveScoreImpact = ref.positiveScoreImpact; + } + + public FeatureFieldType clone() { + return new FeatureFieldType(this); + } + + @Override + public boolean equals(Object o) { + if (super.equals(o) == false) { + return false; + } + FeatureFieldType other = (FeatureFieldType) o; + return Objects.equals(positiveScoreImpact, other.positiveScoreImpact); + } + + @Override + public int hashCode() { + int h = super.hashCode(); + h = 31 * h + Objects.hashCode(positiveScoreImpact); + return h; + } + + @Override + public void checkCompatibility(MappedFieldType other, List conflicts) { + super.checkCompatibility(other, conflicts); + if (positiveScoreImpact != ((FeatureFieldType) other).positiveScoreImpact()) { + conflicts.add("mapper [" + name() + "] has different [positive_score_impact] values"); + } + } + + @Override + public String typeName() { + return CONTENT_TYPE; + } + + public boolean positiveScoreImpact() { + return positiveScoreImpact; + } + + public void setPositiveScoreImpact(boolean positiveScoreImpact) { + checkIfFrozen(); + this.positiveScoreImpact = positiveScoreImpact; + } + + @Override + public Query existsQuery(QueryShardContext context) { + return new TermQuery(new Term("_feature", name())); + } + + @Override + public Query nullValueQuery() { + if (nullValue() == null) { + return null; + } + return termQuery(nullValue(), null); + } + + @Override + public IndexFieldData.Builder fielddataBuilder(String fullyQualifiedIndexName) { + failIfNoDocValues(); + return new DocValuesIndexFieldData.Builder(); + } + + @Override + public Query termQuery(Object value, QueryShardContext context) { + throw new UnsupportedOperationException("Queries on [feature] fields are not supported"); + } + } + + private FeatureFieldMapper(String simpleName, MappedFieldType fieldType, MappedFieldType defaultFieldType, + Settings indexSettings, MultiFields multiFields, CopyTo copyTo) { + super(simpleName, fieldType, defaultFieldType, indexSettings, multiFields, copyTo); + assert fieldType.indexOptions().compareTo(IndexOptions.DOCS_AND_FREQS) <= 0; + } + + @Override + protected FeatureFieldMapper clone() { + return (FeatureFieldMapper) super.clone(); + } + + @Override + public FeatureFieldType fieldType() { + return (FeatureFieldType) super.fieldType(); + } + + @Override + protected void parseCreateField(ParseContext context, List fields) throws IOException { + float value; + if (context.externalValueSet()) { + Object v = context.externalValue(); + if (v instanceof Number) { + value = ((Number) v).floatValue(); + } else { + value = Float.parseFloat(v.toString()); + } + } else if (context.parser().currentToken() == Token.VALUE_NULL) { + // skip + return; + } else { + value = context.parser().floatValue(); + } + + if (context.doc().getByKey(name()) != null) { + throw new IllegalArgumentException("[feature] fields do not support indexing multiple values for the same field [" + name() + + "] in the same document"); + } + + if (fieldType().positiveScoreImpact() == false) { + value = 1 / value; + } + + context.doc().addWithKey(name(), new FeatureField("_feature", name(), value)); + } + + @Override + protected String contentType() { + return CONTENT_TYPE; + } + + @Override + protected void doXContentBody(XContentBuilder builder, boolean includeDefaults, Params params) throws IOException { + super.doXContentBody(builder, includeDefaults, params); + + if (includeDefaults || fieldType().nullValue() != null) { + builder.field("null_value", fieldType().nullValue()); + } + + if (includeDefaults || fieldType().positiveScoreImpact() == false) { + builder.field("positive_score_impact", fieldType().positiveScoreImpact()); + } + } +} diff --git a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/FeatureMetaFieldMapper.java b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/FeatureMetaFieldMapper.java new file mode 100644 index 0000000000000..2102a029a6ad6 --- /dev/null +++ b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/FeatureMetaFieldMapper.java @@ -0,0 +1,151 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.index.mapper; + +import org.apache.lucene.index.IndexOptions; +import org.apache.lucene.index.IndexableField; +import org.apache.lucene.search.Query; +import org.elasticsearch.common.lucene.Lucene; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.query.QueryShardContext; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * This meta field only exists because feature fields index everything into a + * common _feature field and Elasticsearch has a custom codec that complains + * when fields exist in the index and not in mappings. + */ +public class FeatureMetaFieldMapper extends MetadataFieldMapper { + + public static final String NAME = "_feature"; + + public static final String CONTENT_TYPE = "_feature"; + + public static class Defaults { + public static final MappedFieldType FIELD_TYPE = new FeatureMetaFieldType(); + + static { + FIELD_TYPE.setIndexOptions(IndexOptions.DOCS_AND_FREQS); + FIELD_TYPE.setTokenized(true); + FIELD_TYPE.setStored(false); + FIELD_TYPE.setOmitNorms(true); + FIELD_TYPE.setIndexAnalyzer(Lucene.KEYWORD_ANALYZER); + FIELD_TYPE.setSearchAnalyzer(Lucene.KEYWORD_ANALYZER); + FIELD_TYPE.setName(NAME); + FIELD_TYPE.freeze(); + } + } + + public static class Builder extends MetadataFieldMapper.Builder { + + public Builder(MappedFieldType existing) { + super(NAME, existing == null ? Defaults.FIELD_TYPE : existing, Defaults.FIELD_TYPE); + } + + @Override + public FeatureMetaFieldMapper build(BuilderContext context) { + setupFieldType(context); + return new FeatureMetaFieldMapper(fieldType, context.indexSettings()); + } + } + + public static class TypeParser implements MetadataFieldMapper.TypeParser { + @Override + public MetadataFieldMapper.Builder parse(String name, + Map node, ParserContext parserContext) throws MapperParsingException { + return new Builder(parserContext.mapperService().fullName(NAME)); + } + + @Override + public MetadataFieldMapper getDefault(MappedFieldType fieldType, ParserContext context) { + final Settings indexSettings = context.mapperService().getIndexSettings().getSettings(); + if (fieldType != null) { + return new FeatureMetaFieldMapper(indexSettings, fieldType); + } else { + return parse(NAME, Collections.emptyMap(), context) + .build(new BuilderContext(indexSettings, new ContentPath(1))); + } + } + } + + public static final class FeatureMetaFieldType extends MappedFieldType { + + public FeatureMetaFieldType() { + } + + protected FeatureMetaFieldType(FeatureMetaFieldType ref) { + super(ref); + } + + @Override + public FeatureMetaFieldType clone() { + return new FeatureMetaFieldType(this); + } + + @Override + public String typeName() { + return CONTENT_TYPE; + } + + @Override + public Query existsQuery(QueryShardContext context) { + throw new UnsupportedOperationException("Cannot run exists query on [_feature]"); + } + + @Override + public Query termQuery(Object value, QueryShardContext context) { + throw new UnsupportedOperationException("The [_feature] field may not be queried directly"); + } + } + + private FeatureMetaFieldMapper(Settings indexSettings, MappedFieldType existing) { + this(existing.clone(), indexSettings); + } + + private FeatureMetaFieldMapper(MappedFieldType fieldType, Settings indexSettings) { + super(NAME, fieldType, Defaults.FIELD_TYPE, indexSettings); + } + + @Override + public void preParse(ParseContext context) throws IOException {} + + @Override + protected void parseCreateField(ParseContext context, List fields) throws IOException { + throw new AssertionError("Should never be called"); + } + + @Override + public void postParse(ParseContext context) throws IOException {} + + @Override + protected String contentType() { + return CONTENT_TYPE; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder; + } +} diff --git a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/MapperExtrasPlugin.java b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/MapperExtrasPlugin.java index 2b249a5fe6e09..4a9aea21a8a53 100644 --- a/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/MapperExtrasPlugin.java +++ b/modules/mapper-extras/src/main/java/org/elasticsearch/index/mapper/MapperExtrasPlugin.java @@ -19,21 +19,37 @@ package org.elasticsearch.index.mapper; +import org.elasticsearch.index.mapper.MetadataFieldMapper.TypeParser; +import org.elasticsearch.index.query.FeatureQueryBuilder; import org.elasticsearch.plugins.MapperPlugin; import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.plugins.SearchPlugin; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; -public class MapperExtrasPlugin extends Plugin implements MapperPlugin { +public class MapperExtrasPlugin extends Plugin implements MapperPlugin, SearchPlugin { @Override public Map getMappers() { Map mappers = new LinkedHashMap<>(); mappers.put(ScaledFloatFieldMapper.CONTENT_TYPE, new ScaledFloatFieldMapper.TypeParser()); mappers.put(TokenCountFieldMapper.CONTENT_TYPE, new TokenCountFieldMapper.TypeParser()); + mappers.put(FeatureFieldMapper.CONTENT_TYPE, new FeatureFieldMapper.TypeParser()); return Collections.unmodifiableMap(mappers); } + @Override + public Map getMetadataMappers() { + return Collections.singletonMap(FeatureMetaFieldMapper.CONTENT_TYPE, new FeatureMetaFieldMapper.TypeParser()); + } + + @Override + public List> getQueries() { + return Collections.singletonList( + new QuerySpec<>(FeatureQueryBuilder.NAME, FeatureQueryBuilder::new, p -> FeatureQueryBuilder.PARSER.parse(p, null))); + } + } diff --git a/modules/mapper-extras/src/main/java/org/elasticsearch/index/query/FeatureQueryBuilder.java b/modules/mapper-extras/src/main/java/org/elasticsearch/index/query/FeatureQueryBuilder.java new file mode 100644 index 0000000000000..761de46731dda --- /dev/null +++ b/modules/mapper-extras/src/main/java/org/elasticsearch/index/query/FeatureQueryBuilder.java @@ -0,0 +1,354 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.index.query; + +import org.apache.lucene.document.FeatureField; +import org.apache.lucene.search.MatchNoDocsQuery; +import org.apache.lucene.search.Query; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.mapper.FeatureFieldMapper.FeatureFieldType; +import org.elasticsearch.index.mapper.MappedFieldType; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; + +/** + * Query to run on a [feature] field. + */ +public final class FeatureQueryBuilder extends AbstractQueryBuilder { + + /** + * Scoring function for a [feature] field. + */ + public abstract static class ScoreFunction { + + private ScoreFunction() {} // prevent extensions by users + + abstract void writeTo(StreamOutput out) throws IOException; + + abstract Query toQuery(String feature, boolean positiveScoreImpact) throws IOException; + + abstract void doXContent(XContentBuilder builder) throws IOException; + + /** + * A scoring function that scores documents as {@code Math.log(scalingFactor + S)} + * where S is the value of the static feature. + */ + public static class Log extends ScoreFunction { + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "log", a -> new Log((Float) a[0])); + static { + PARSER.declareFloat(ConstructingObjectParser.constructorArg(), new ParseField("scaling_factor")); + } + + private final float scalingFactor; + + public Log(float scalingFactor) { + this.scalingFactor = scalingFactor; + } + + private Log(StreamInput in) throws IOException { + this(in.readFloat()); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || obj.getClass() != getClass()) { + return false; + } + Log that = (Log) obj; + return scalingFactor == that.scalingFactor; + } + + @Override + public int hashCode() { + return Float.hashCode(scalingFactor); + } + + @Override + void writeTo(StreamOutput out) throws IOException { + out.writeByte((byte) 0); + out.writeFloat(scalingFactor); + } + + @Override + void doXContent(XContentBuilder builder) throws IOException { + builder.startObject("log"); + builder.field("scaling_factor", scalingFactor); + builder.endObject(); + } + + @Override + Query toQuery(String feature, boolean positiveScoreImpact) throws IOException { + if (positiveScoreImpact == false) { + throw new IllegalArgumentException("Cannot use the [log] function with a field that has a negative score impact as " + + "it would trigger negative scores"); + } + return FeatureField.newLogQuery("_feature", feature, DEFAULT_BOOST, scalingFactor); + } + } + + /** + * A scoring function that scores documents as {@code S / (S + pivot)} where S is + * the value of the static feature. + */ + public static class Saturation extends ScoreFunction { + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "saturation", a -> new Saturation((Float) a[0])); + static { + PARSER.declareFloat(ConstructingObjectParser.optionalConstructorArg(), new ParseField("pivot")); + } + + private final Float pivot; + + /** Constructor with a default pivot, computed as the geometric average of + * all feature values in the index. */ + public Saturation() { + this((Float) null); + } + + public Saturation(float pivot) { + this(Float.valueOf(pivot)); + } + + private Saturation(Float pivot) { + this.pivot = pivot; + } + + private Saturation(StreamInput in) throws IOException { + this(in.readOptionalFloat()); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || obj.getClass() != getClass()) { + return false; + } + Saturation that = (Saturation) obj; + return Objects.equals(pivot, that.pivot); + } + + @Override + public int hashCode() { + return Objects.hashCode(pivot); + } + + @Override + void writeTo(StreamOutput out) throws IOException { + out.writeByte((byte) 1); + out.writeOptionalFloat(pivot); + } + + @Override + void doXContent(XContentBuilder builder) throws IOException { + builder.startObject("saturation"); + if (pivot != null) { + builder.field("pivot", pivot); + } + builder.endObject(); + } + + @Override + Query toQuery(String feature, boolean positiveScoreImpact) throws IOException { + if (pivot == null) { + return FeatureField.newSaturationQuery("_feature", feature); + } else { + return FeatureField.newSaturationQuery("_feature", feature, DEFAULT_BOOST, pivot); + } + } + } + + /** + * A scoring function that scores documents as {@code S^exp / (S^exp + pivot^exp)} + * where S is the value of the static feature. + */ + public static class Sigmoid extends ScoreFunction { + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "sigmoid", a -> new Sigmoid((Float) a[0], ((Float) a[1]).floatValue())); + static { + PARSER.declareFloat(ConstructingObjectParser.constructorArg(), new ParseField("pivot")); + PARSER.declareFloat(ConstructingObjectParser.constructorArg(), new ParseField("exponent")); + } + + private final float pivot; + private final float exp; + + public Sigmoid(float pivot, float exp) { + this.pivot = pivot; + this.exp = exp; + } + + private Sigmoid(StreamInput in) throws IOException { + this(in.readFloat(), in.readFloat()); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || obj.getClass() != getClass()) { + return false; + } + Sigmoid that = (Sigmoid) obj; + return pivot == that.pivot + && exp == that.exp; + } + + @Override + public int hashCode() { + return Objects.hash(pivot, exp); + } + + @Override + void writeTo(StreamOutput out) throws IOException { + out.writeByte((byte) 2); + out.writeFloat(pivot); + out.writeFloat(exp); + } + + @Override + void doXContent(XContentBuilder builder) throws IOException { + builder.startObject("sigmoid"); + builder.field("pivot", pivot); + builder.field("exponent", exp); + builder.endObject(); + } + + @Override + Query toQuery(String feature, boolean positiveScoreImpact) throws IOException { + return FeatureField.newSigmoidQuery("_feature", feature, DEFAULT_BOOST, pivot, exp); + } + } + } + + private static ScoreFunction readScoreFunction(StreamInput in) throws IOException { + byte b = in.readByte(); + switch (b) { + case 0: + return new ScoreFunction.Log(in); + case 1: + return new ScoreFunction.Saturation(in); + case 2: + return new ScoreFunction.Sigmoid(in); + default: + throw new IOException("Illegal score function id: " + b); + } + } + + public static ConstructingObjectParser PARSER = new ConstructingObjectParser<>( + "feature", args -> { + final String field = (String) args[0]; + final float boost = args[1] == null ? DEFAULT_BOOST : (Float) args[1]; + final String queryName = (String) args[2]; + long numNonNulls = Arrays.stream(args, 3, args.length).filter(Objects::nonNull).count(); + final FeatureQueryBuilder query; + if (numNonNulls > 1) { + throw new IllegalArgumentException("Can only specify one of [log], [saturation] and [sigmoid]"); + } else if (numNonNulls == 0) { + query = new FeatureQueryBuilder(field, new ScoreFunction.Saturation()); + } else { + ScoreFunction scoreFunction = (ScoreFunction) Arrays.stream(args, 3, args.length) + .filter(Objects::nonNull) + .findAny() + .get(); + query = new FeatureQueryBuilder(field, scoreFunction); + } + query.boost(boost); + query.queryName(queryName); + return query; + }); + static { + PARSER.declareString(ConstructingObjectParser.constructorArg(), new ParseField("field")); + PARSER.declareFloat(ConstructingObjectParser.optionalConstructorArg(), BOOST_FIELD); + PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), NAME_FIELD); + PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), + ScoreFunction.Log.PARSER, new ParseField("log")); + PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), + ScoreFunction.Saturation.PARSER, new ParseField("saturation")); + PARSER.declareObject(ConstructingObjectParser.optionalConstructorArg(), + ScoreFunction.Sigmoid.PARSER, new ParseField("sigmoid")); + } + + public static final String NAME = "feature"; + + private final String field; + private final ScoreFunction scoreFunction; + + public FeatureQueryBuilder(String field, ScoreFunction scoreFunction) { + this.field = Objects.requireNonNull(field); + this.scoreFunction = Objects.requireNonNull(scoreFunction); + } + + public FeatureQueryBuilder(StreamInput in) throws IOException { + super(in); + this.field = in.readString(); + this.scoreFunction = readScoreFunction(in); + } + + @Override + public String getWriteableName() { + return NAME; + } + + @Override + protected void doWriteTo(StreamOutput out) throws IOException { + out.writeString(field); + scoreFunction.writeTo(out); + } + + @Override + protected void doXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(getName()); + builder.field("field", field); + scoreFunction.doXContent(builder); + printBoostAndQueryName(builder); + builder.endObject(); + } + + @Override + protected Query doToQuery(QueryShardContext context) throws IOException { + final MappedFieldType ft = context.fieldMapper(field); + if (ft == null) { + return new MatchNoDocsQuery(); + } + if (ft instanceof FeatureFieldType == false) { + throw new IllegalArgumentException("[feature] query only works on [feature] fields, not [" + ft.typeName() + "]"); + } + final FeatureFieldType fft = (FeatureFieldType) ft; + return scoreFunction.toQuery(field, fft.positiveScoreImpact()); + } + + @Override + protected boolean doEquals(FeatureQueryBuilder other) { + return Objects.equals(field, other.field) && Objects.equals(scoreFunction, other.scoreFunction); + } + + @Override + protected int doHashCode() { + return Objects.hash(field, scoreFunction); + } + +} diff --git a/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/FeatureFieldMapperTests.java b/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/FeatureFieldMapperTests.java new file mode 100644 index 0000000000000..2e9fa98cbbe97 --- /dev/null +++ b/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/FeatureFieldMapperTests.java @@ -0,0 +1,173 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.index.mapper; + +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.tokenattributes.TermFrequencyAttribute; +import org.apache.lucene.document.FeatureField; +import org.apache.lucene.index.IndexableField; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.compress.CompressedXContent; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.index.IndexService; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.test.ESSingleNodeTestCase; +import org.hamcrest.Matchers; +import org.junit.Before; + +import java.io.IOException; +import java.util.Collection; + +public class FeatureFieldMapperTests extends ESSingleNodeTestCase { + + IndexService indexService; + DocumentMapperParser parser; + + @Before + public void setup() { + indexService = createIndex("test"); + parser = indexService.mapperService().documentMapperParser(); + } + + @Override + protected Collection> getPlugins() { + return pluginList(MapperExtrasPlugin.class); + } + + private static int getFrequency(TokenStream tk) throws IOException { + TermFrequencyAttribute freqAttribute = tk.addAttribute(TermFrequencyAttribute.class); + tk.reset(); + assertTrue(tk.incrementToken()); + int freq = freqAttribute.getTermFrequency(); + assertFalse(tk.incrementToken()); + return freq; + } + + public void testDefaults() throws Exception { + String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type") + .startObject("properties").startObject("field").field("type", "feature").endObject().endObject() + .endObject().endObject()); + + DocumentMapper mapper = parser.parse("type", new CompressedXContent(mapping)); + + assertEquals(mapping, mapper.mappingSource().toString()); + + ParsedDocument doc1 = mapper.parse(SourceToParse.source("test", "type", "1", BytesReference + .bytes(XContentFactory.jsonBuilder() + .startObject() + .field("field", 10) + .endObject()), + XContentType.JSON)); + + IndexableField[] fields = doc1.rootDoc().getFields("_feature"); + assertEquals(1, fields.length); + assertThat(fields[0], Matchers.instanceOf(FeatureField.class)); + FeatureField featureField1 = (FeatureField) fields[0]; + + ParsedDocument doc2 = mapper.parse(SourceToParse.source("test", "type", "1", BytesReference + .bytes(XContentFactory.jsonBuilder() + .startObject() + .field("field", 12) + .endObject()), + XContentType.JSON)); + + FeatureField featureField2 = (FeatureField) doc2.rootDoc().getFields("_feature")[0]; + + int freq1 = getFrequency(featureField1.tokenStream(null, null)); + int freq2 = getFrequency(featureField2.tokenStream(null, null)); + assertTrue(freq1 < freq2); + } + + public void testNegativeScoreImpact() throws Exception { + String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type") + .startObject("properties").startObject("field").field("type", "feature") + .field("positive_score_impact", false).endObject().endObject() + .endObject().endObject()); + + DocumentMapper mapper = parser.parse("type", new CompressedXContent(mapping)); + + assertEquals(mapping, mapper.mappingSource().toString()); + + ParsedDocument doc1 = mapper.parse(SourceToParse.source("test", "type", "1", BytesReference + .bytes(XContentFactory.jsonBuilder() + .startObject() + .field("field", 10) + .endObject()), + XContentType.JSON)); + + IndexableField[] fields = doc1.rootDoc().getFields("_feature"); + assertEquals(1, fields.length); + assertThat(fields[0], Matchers.instanceOf(FeatureField.class)); + FeatureField featureField1 = (FeatureField) fields[0]; + + ParsedDocument doc2 = mapper.parse(SourceToParse.source("test", "type", "1", BytesReference + .bytes(XContentFactory.jsonBuilder() + .startObject() + .field("field", 12) + .endObject()), + XContentType.JSON)); + + FeatureField featureField2 = (FeatureField) doc2.rootDoc().getFields("_feature")[0]; + + int freq1 = getFrequency(featureField1.tokenStream(null, null)); + int freq2 = getFrequency(featureField2.tokenStream(null, null)); + assertTrue(freq1 > freq2); + } + + public void testRejectMultiValuedFields() throws MapperParsingException, IOException { + String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type") + .startObject("properties").startObject("field").field("type", "feature").endObject().startObject("foo") + .startObject("properties").startObject("field").field("type", "feature").endObject().endObject() + .endObject().endObject().endObject().endObject()); + + DocumentMapper mapper = parser.parse("type", new CompressedXContent(mapping)); + + assertEquals(mapping, mapper.mappingSource().toString()); + + MapperParsingException e = null;/*expectThrows(MapperParsingException.class, + () -> mapper.parse(SourceToParse.source("test", "type", "1", BytesReference + .bytes(XContentFactory.jsonBuilder() + .startObject() + .field("field", Arrays.asList(10, 20)) + .endObject()), + XContentType.JSON))); + assertEquals("[feature] fields do not support indexing multiple values for the same field [field] in the same document", + e.getCause().getMessage());*/ + + e = expectThrows(MapperParsingException.class, + () -> mapper.parse(SourceToParse.source("test", "type", "1", BytesReference + .bytes(XContentFactory.jsonBuilder() + .startObject() + .startArray("foo") + .startObject() + .field("field", 10) + .endObject() + .startObject() + .field("field", 20) + .endObject() + .endArray() + .endObject()), + XContentType.JSON))); + assertEquals("[feature] fields do not support indexing multiple values for the same field [foo.field] in the same document", + e.getCause().getMessage()); + } +} diff --git a/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/FeatureFieldTypeTests.java b/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/FeatureFieldTypeTests.java new file mode 100644 index 0000000000000..9debd0736602c --- /dev/null +++ b/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/FeatureFieldTypeTests.java @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.index.mapper; + +import org.junit.Before; + +public class FeatureFieldTypeTests extends FieldTypeTestCase { + + @Override + protected MappedFieldType createDefaultFieldType() { + return new FeatureFieldMapper.FeatureFieldType(); + } + + @Before + public void setupProperties() { + addModifier(new Modifier("positive_score_impact", false) { + @Override + public void modify(MappedFieldType ft) { + FeatureFieldMapper.FeatureFieldType tft = (FeatureFieldMapper.FeatureFieldType)ft; + tft.setPositiveScoreImpact(tft.positiveScoreImpact() == false); + } + @Override + public void normalizeOther(MappedFieldType other) { + super.normalizeOther(other); + ((FeatureFieldMapper.FeatureFieldType) other).setPositiveScoreImpact(true); + } + }); + } +} diff --git a/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/FeatureMetaFieldMapperTests.java b/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/FeatureMetaFieldMapperTests.java new file mode 100644 index 0000000000000..99697b1abaf58 --- /dev/null +++ b/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/FeatureMetaFieldMapperTests.java @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.index.mapper; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.compress.CompressedXContent; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.index.IndexService; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.test.ESSingleNodeTestCase; +import org.junit.Before; + +import java.util.Collection; + +public class FeatureMetaFieldMapperTests extends ESSingleNodeTestCase { + + IndexService indexService; + DocumentMapperParser parser; + + @Before + public void setup() { + indexService = createIndex("test"); + parser = indexService.mapperService().documentMapperParser(); + } + + @Override + protected Collection> getPlugins() { + return pluginList(MapperExtrasPlugin.class); + } + + public void testBasics() throws Exception { + String mapping = Strings.toString(XContentFactory.jsonBuilder().startObject().startObject("type") + .startObject("properties").startObject("field").field("type", "feature").endObject().endObject() + .endObject().endObject()); + + DocumentMapper mapper = parser.parse("type", new CompressedXContent(mapping)); + + assertEquals(mapping, mapper.mappingSource().toString()); + assertNotNull(mapper.metadataMapper(FeatureMetaFieldMapper.class)); + } +} diff --git a/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/FeatureMetaFieldTypeTests.java b/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/FeatureMetaFieldTypeTests.java new file mode 100644 index 0000000000000..ef261573c9682 --- /dev/null +++ b/modules/mapper-extras/src/test/java/org/elasticsearch/index/mapper/FeatureMetaFieldTypeTests.java @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.index.mapper; + +public class FeatureMetaFieldTypeTests extends FieldTypeTestCase { + + @Override + protected MappedFieldType createDefaultFieldType() { + return new FeatureMetaFieldMapper.FeatureMetaFieldType(); + } + +} diff --git a/modules/mapper-extras/src/test/java/org/elasticsearch/index/query/FeatureQueryBuilderTests.java b/modules/mapper-extras/src/test/java/org/elasticsearch/index/query/FeatureQueryBuilderTests.java new file mode 100644 index 0000000000000..883dce5f3858c --- /dev/null +++ b/modules/mapper-extras/src/test/java/org/elasticsearch/index/query/FeatureQueryBuilderTests.java @@ -0,0 +1,130 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.index.query; + +import org.apache.lucene.document.FeatureField; +import org.apache.lucene.search.MatchNoDocsQuery; +import org.apache.lucene.search.Query; +import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.compress.CompressedXContent; +import org.elasticsearch.index.mapper.MapperExtrasPlugin; +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.index.query.FeatureQueryBuilder.ScoreFunction; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.search.internal.SearchContext; +import org.elasticsearch.test.AbstractQueryTestCase; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.Matchers.either; + +public class FeatureQueryBuilderTests extends AbstractQueryTestCase { + + @Override + protected void initializeAdditionalMappings(MapperService mapperService) throws IOException { + for (String type : getCurrentTypes()) { + mapperService.merge(type, new CompressedXContent(Strings.toString(PutMappingRequest.buildFromSimplifiedDef(type, + "my_feature_field", "type=feature", + "my_negative_feature_field", "type=feature,positive_score_impact=false"))), MapperService.MergeReason.MAPPING_UPDATE); + } + } + + @Override + protected Collection> getPlugins() { + return Collections.singleton(MapperExtrasPlugin.class); + } + + @Override + protected FeatureQueryBuilder doCreateTestQueryBuilder() { + ScoreFunction function; + switch (random().nextInt(3)) { + case 0: + function = new ScoreFunction.Log(1 + randomFloat()); + break; + case 1: + if (randomBoolean()) { + function = new ScoreFunction.Saturation(); + } else { + function = new ScoreFunction.Saturation(randomFloat()); + } + break; + case 2: + function = new ScoreFunction.Sigmoid(randomFloat(), randomFloat()); + break; + default: + throw new AssertionError(); + } + return new FeatureQueryBuilder("my_feature_field", function); + } + + @Override + protected void doAssertLuceneQuery(FeatureQueryBuilder queryBuilder, Query query, SearchContext context) throws IOException { + Class expectedClass = FeatureField.newSaturationQuery("", "", 1, 1).getClass(); + assertThat(query, either(instanceOf(MatchNoDocsQuery.class)).or(instanceOf(expectedClass))); + } + + @Override + @AwaitsFix(bugUrl="https://github.com/elastic/elasticsearch/issues/30605") + public void testUnknownField() { + super.testUnknownField(); + } + + public void testDefaultScoreFunction() throws IOException { + assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0); + String query = "{\n" + + " \"feature\" : {\n" + + " \"field\": \"my_feature_field\"\n" + + " }\n" + + "}"; + Query parsedQuery = parseQuery(query).toQuery(createShardContext()); + assertEquals(FeatureField.newSaturationQuery("_feature", "my_feature_field"), parsedQuery); + } + + public void testIllegalField() throws IOException { + assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0); + String query = "{\n" + + " \"feature\" : {\n" + + " \"field\": \"" + STRING_FIELD_NAME + "\"\n" + + " }\n" + + "}"; + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> parseQuery(query).toQuery(createShardContext())); + assertEquals("[feature] query only works on [feature] fields, not [text]", e.getMessage()); + } + + public void testIllegalCombination() throws IOException { + assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0); + String query = "{\n" + + " \"feature\" : {\n" + + " \"field\": \"my_negative_feature_field\",\n" + + " \"log\" : {\n" + + " \"scaling_factor\": 4.5\n" + + " }\n" + + " }\n" + + "}"; + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> parseQuery(query).toQuery(createShardContext())); + assertEquals( + "Cannot use the [log] function with a field that has a negative score impact as it would trigger negative scores", + e.getMessage()); + } +} diff --git a/modules/mapper-extras/src/test/resources/rest-api-spec/test/feature/10_basic.yml b/modules/mapper-extras/src/test/resources/rest-api-spec/test/feature/10_basic.yml new file mode 100644 index 0000000000000..8318550876509 --- /dev/null +++ b/modules/mapper-extras/src/test/resources/rest-api-spec/test/feature/10_basic.yml @@ -0,0 +1,160 @@ +setup: + - skip: + version: " - 6.99.99" + reason: "The feature field/query was introduced in 7.0.0" + + - do: + indices.create: + index: test + body: + settings: + number_of_replicas: 0 + mappings: + _doc: + properties: + pagerank: + type: feature + url_length: + type: feature + positive_score_impact: false + + - do: + index: + index: test + type: _doc + id: 1 + body: + pagerank: 10 + url_length: 50 + + - do: + index: + index: test + type: _doc + id: 2 + body: + pagerank: 100 + url_length: 20 + + - do: + indices.refresh: {} + +--- +"Positive log": + + - do: + search: + body: + query: + feature: + field: pagerank + log: + scaling_factor: 3 + + - match: + hits.total: 2 + + - match: + hits.hits.0._id: "2" + + - match: + hits.hits.1._id: "1" + +--- +"Positive saturation": + + - do: + search: + body: + query: + feature: + field: pagerank + saturation: + pivot: 20 + + - match: + hits.total: 2 + + - match: + hits.hits.0._id: "2" + + - match: + hits.hits.1._id: "1" + +--- +"Positive sigmoid": + + - do: + search: + body: + query: + feature: + field: pagerank + sigmoid: + pivot: 20 + exponent: 0.6 + + - match: + hits.total: 2 + + - match: + hits.hits.0._id: "2" + + - match: + hits.hits.1._id: "1" + +--- +"Negative log": + + - do: + catch: bad_request + search: + body: + query: + feature: + field: url_length + log: + scaling_factor: 3 + +--- +"Negative saturation": + + - do: + search: + body: + query: + feature: + field: url_length + saturation: + pivot: 20 + + - match: + hits.total: 2 + + - match: + hits.hits.0._id: "2" + + - match: + hits.hits.1._id: "1" + +--- +"Negative sigmoid": + + - do: + search: + body: + query: + feature: + field: url_length + sigmoid: + pivot: 20 + exponent: 0.6 + + - match: + hits.total: 2 + + - match: + hits.hits.0._id: "2" + + - match: + hits.hits.1._id: "1" diff --git a/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpPipeliningHandler.java b/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpPipeliningHandler.java index e930ffe7424a9..52dfbff6d3e03 100644 --- a/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpPipeliningHandler.java +++ b/modules/transport-netty4/src/main/java/org/elasticsearch/http/netty4/Netty4HttpPipeliningHandler.java @@ -56,7 +56,7 @@ public Netty4HttpPipeliningHandler(Logger logger, final int maxEventsHeld) { @Override public void channelRead(final ChannelHandlerContext ctx, final Object msg) { if (msg instanceof LastHttpContent) { - HttpPipelinedRequest pipelinedRequest = aggregator.read(((LastHttpContent) msg).retain()); + HttpPipelinedRequest pipelinedRequest = aggregator.read(((LastHttpContent) msg)); ctx.fireChannelRead(pipelinedRequest); } else { ctx.fireChannelRead(msg); diff --git a/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NioHttpPipeliningHandler.java b/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NioHttpPipeliningHandler.java index 57a14e7819d4e..2b702042ba7a8 100644 --- a/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NioHttpPipeliningHandler.java +++ b/plugins/transport-nio/src/main/java/org/elasticsearch/http/nio/NioHttpPipeliningHandler.java @@ -56,7 +56,7 @@ public NioHttpPipeliningHandler(Logger logger, final int maxEventsHeld) { @Override public void channelRead(final ChannelHandlerContext ctx, final Object msg) { if (msg instanceof LastHttpContent) { - HttpPipelinedRequest pipelinedRequest = aggregator.read(((LastHttpContent) msg).retain()); + HttpPipelinedRequest pipelinedRequest = aggregator.read(((LastHttpContent) msg)); ctx.fireChannelRead(pipelinedRequest); } else { ctx.fireChannelRead(msg); diff --git a/qa/vagrant/build.gradle b/qa/vagrant/build.gradle index 52a6bb1efb5f5..a8dfe89b67869 100644 --- a/qa/vagrant/build.gradle +++ b/qa/vagrant/build.gradle @@ -29,9 +29,9 @@ plugins { dependencies { compile "junit:junit:${versions.junit}" compile "org.hamcrest:hamcrest-core:${versions.hamcrest}" + compile "org.hamcrest:hamcrest-library:${versions.hamcrest}" - // needs to be on the classpath for JarHell - testRuntime project(':libs:elasticsearch-core') + compile project(':libs:elasticsearch-core') // pulls in the jar built by this project and its dependencies packagingTest project(path: project.path, configuration: 'runtime') diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/PackagingTests.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/PackagingTests.java index 0b5e7a3b6e0d2..fa7f8e8ef78c5 100644 --- a/qa/vagrant/src/main/java/org/elasticsearch/packaging/PackagingTests.java +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/PackagingTests.java @@ -19,13 +19,20 @@ package org.elasticsearch.packaging; -import org.junit.Test; +import org.elasticsearch.packaging.test.OssTarTests; +import org.elasticsearch.packaging.test.OssZipTests; +import org.elasticsearch.packaging.test.DefaultTarTests; +import org.elasticsearch.packaging.test.DefaultZipTests; -/** - * This class doesn't have any tests yet - */ -public class PackagingTests { +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; - @Test - public void testDummy() {} -} +@RunWith(Suite.class) +@SuiteClasses({ + DefaultTarTests.class, + DefaultZipTests.class, + OssTarTests.class, + OssZipTests.class +}) +public class PackagingTests {} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/VMTestRunner.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/VMTestRunner.java new file mode 100644 index 0000000000000..a8fd2c2770783 --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/VMTestRunner.java @@ -0,0 +1,40 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.packaging; + +import org.junit.runner.JUnitCore; + +import java.nio.file.Files; +import java.nio.file.Paths; + +/** + * Ensures that the current JVM is running on a virtual machine before delegating to {@link JUnitCore}. We just check for the existence + * of a special file that we create during VM provisioning. + */ +public class VMTestRunner { + public static void main(String[] args) { + if (Files.exists(Paths.get("/is_vagrant_vm"))) { + JUnitCore.main(args); + } else { + throw new RuntimeException("This filesystem does not have an expected marker file indicating it's a virtual machine. These " + + "tests should only run in a virtual machine because they're destructive."); + } + } +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/ArchiveTestCase.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/ArchiveTestCase.java new file mode 100644 index 0000000000000..f683cb9c145db --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/ArchiveTestCase.java @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.packaging.test; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; + +import org.elasticsearch.packaging.util.Distribution; +import org.elasticsearch.packaging.util.Installation; + +import static org.elasticsearch.packaging.util.Cleanup.cleanEverything; +import static org.elasticsearch.packaging.util.Archives.installArchive; +import static org.elasticsearch.packaging.util.Archives.verifyArchiveInstallation; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assume.assumeThat; + +/** + * Tests that apply to the archive distributions (tar, zip). To add a case for a distribution, subclass and + * override {@link ArchiveTestCase#distribution()}. These tests should be the same across all archive distributions + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public abstract class ArchiveTestCase { + + private static Installation installation; + + /** The {@link Distribution} that should be tested in this case */ + protected abstract Distribution distribution(); + + @BeforeClass + public static void cleanup() { + installation = null; + cleanEverything(); + } + + @Before + public void onlyCompatibleDistributions() { + assumeThat(distribution().packaging.compatible, is(true)); + } + + @Test + public void test10Install() { + installation = installArchive(distribution()); + verifyArchiveInstallation(installation, distribution()); + } +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/DefaultTarTests.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/DefaultTarTests.java new file mode 100644 index 0000000000000..9b359a329c1bb --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/DefaultTarTests.java @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.packaging.test; + +import org.elasticsearch.packaging.util.Distribution; + +public class DefaultTarTests extends ArchiveTestCase { + + @Override + protected Distribution distribution() { + return Distribution.DEFAULT_TAR; + } +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/DefaultZipTests.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/DefaultZipTests.java new file mode 100644 index 0000000000000..d9a6353a8c6f3 --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/DefaultZipTests.java @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.packaging.test; + +import org.elasticsearch.packaging.util.Distribution; + +public class DefaultZipTests extends ArchiveTestCase { + + @Override + protected Distribution distribution() { + return Distribution.DEFAULT_ZIP; + } +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/OssTarTests.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/OssTarTests.java new file mode 100644 index 0000000000000..86637fc9d48e3 --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/OssTarTests.java @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.packaging.test; + +import org.elasticsearch.packaging.util.Distribution; + +public class OssTarTests extends ArchiveTestCase { + + @Override + protected Distribution distribution() { + return Distribution.OSS_TAR; + } +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/OssZipTests.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/OssZipTests.java new file mode 100644 index 0000000000000..b6cd1e596a09b --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/test/OssZipTests.java @@ -0,0 +1,30 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.packaging.test; + +import org.elasticsearch.packaging.util.Distribution; + +public class OssZipTests extends ArchiveTestCase { + + @Override + protected Distribution distribution() { + return Distribution.OSS_ZIP; + } +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Archives.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Archives.java new file mode 100644 index 0000000000000..4a00570bf30f9 --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Archives.java @@ -0,0 +1,239 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.packaging.util; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; + +import static org.elasticsearch.packaging.util.FileMatcher.Fileness.Directory; +import static org.elasticsearch.packaging.util.FileMatcher.Fileness.File; +import static org.elasticsearch.packaging.util.FileMatcher.file; +import static org.elasticsearch.packaging.util.FileMatcher.p644; +import static org.elasticsearch.packaging.util.FileMatcher.p660; +import static org.elasticsearch.packaging.util.FileMatcher.p755; +import static org.elasticsearch.packaging.util.FileUtils.getCurrentVersion; +import static org.elasticsearch.packaging.util.FileUtils.getDefaultArchiveInstallPath; +import static org.elasticsearch.packaging.util.FileUtils.getPackagingArchivesDir; +import static org.elasticsearch.packaging.util.FileUtils.lsGlob; + +import static org.elasticsearch.packaging.util.FileUtils.mv; +import static org.elasticsearch.packaging.util.Platforms.isDPKG; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; + +/** + * Installation and verification logic for archive distributions + */ +public class Archives { + + public static Installation installArchive(Distribution distribution) { + return installArchive(distribution, getDefaultArchiveInstallPath(), getCurrentVersion()); + } + + public static Installation installArchive(Distribution distribution, Path fullInstallPath, String version) { + final Shell sh = new Shell(); + + final Path distributionFile = getPackagingArchivesDir().resolve(distribution.filename(version)); + final Path baseInstallPath = fullInstallPath.getParent(); + final Path extractedPath = baseInstallPath.resolve("elasticsearch-" + version); + + assertThat("distribution file must exist", Files.exists(distributionFile), is(true)); + assertThat("elasticsearch must not already be installed", lsGlob(baseInstallPath, "elasticsearch*"), empty()); + + if (distribution.packaging == Distribution.Packaging.TAR) { + + if (Platforms.LINUX) { + sh.run("tar", "-C", baseInstallPath.toString(), "-xzpf", distributionFile.toString()); + } else { + throw new RuntimeException("Distribution " + distribution + " is not supported on windows"); + } + + } else if (distribution.packaging == Distribution.Packaging.ZIP) { + + if (Platforms.LINUX) { + sh.run("unzip", distributionFile.toString(), "-d", baseInstallPath.toString()); + } else { + sh.run("powershell.exe", "-Command", + "Add-Type -AssemblyName 'System.IO.Compression.Filesystem'; " + + "[IO.Compression.ZipFile]::ExtractToDirectory('" + distributionFile + "', '" + baseInstallPath + "')"); + } + + } else { + throw new RuntimeException("Distribution " + distribution + " is not a known archive type"); + } + + assertThat("archive was extracted", Files.exists(extractedPath), is(true)); + + mv(extractedPath, fullInstallPath); + + assertThat("extracted archive moved to install location", Files.exists(fullInstallPath)); + final List installations = lsGlob(baseInstallPath, "elasticsearch*"); + assertThat("only the intended installation exists", installations, hasSize(1)); + assertThat("only the intended installation exists", installations.get(0), is(fullInstallPath)); + + if (Platforms.LINUX) { + setupArchiveUsersLinux(fullInstallPath); + } + + return new Installation(fullInstallPath); + } + + private static void setupArchiveUsersLinux(Path installPath) { + final Shell sh = new Shell(); + + if (sh.runIgnoreExitCode("getent", "group", "elasticsearch").isSuccess() == false) { + if (isDPKG()) { + sh.run("addgroup", "--system", "elasticsearch"); + } else { + sh.run("groupadd", "-r", "elasticsearch"); + } + } + + if (sh.runIgnoreExitCode("id", "elasticsearch").isSuccess() == false) { + if (isDPKG()) { + sh.run("adduser", + "--quiet", + "--system", + "--no-create-home", + "--ingroup", "elasticsearch", + "--disabled-password", + "--shell", "/bin/false", + "elasticsearch"); + } else { + sh.run("useradd", + "--system", + "-M", + "--gid", "elasticsearch", + "--shell", "/sbin/nologin", + "--comment", "elasticsearch user", + "elasticsearch"); + } + } + sh.run("chown", "-R", "elasticsearch:elasticsearch", installPath.toString()); + } + + public static void verifyArchiveInstallation(Installation installation, Distribution distribution) { + // on Windows for now we leave the installation owned by the vagrant user that the tests run as. Since the vagrant account + // is a local administrator, the files really end up being owned by the local administrators group. In the future we'll + // install and run elasticesearch with a role user on Windows + final String owner = Platforms.WINDOWS + ? "BUILTIN\\Administrators" + : "elasticsearch"; + + verifyOssInstallation(installation, distribution, owner); + if (distribution.flavor == Distribution.Flavor.DEFAULT) { + verifyDefaultInstallation(installation, distribution, owner); + } + } + + private static void verifyOssInstallation(Installation es, Distribution distribution, String owner) { + Stream.of( + es.home, + es.config, + es.plugins, + es.modules, + es.logs + ).forEach(dir -> assertThat(dir, file(Directory, owner, owner, p755))); + + assertThat(Files.exists(es.data), is(false)); + assertThat(Files.exists(es.scripts), is(false)); + + assertThat(es.home.resolve("bin"), file(Directory, owner, owner, p755)); + assertThat(es.home.resolve("lib"), file(Directory, owner, owner, p755)); + assertThat(Files.exists(es.config.resolve("elasticsearch.keystore")), is(false)); + + Stream.of( + "bin/elasticsearch", + "bin/elasticsearch-env", + "bin/elasticsearch-keystore", + "bin/elasticsearch-plugin", + "bin/elasticsearch-translog" + ).forEach(executable -> { + + assertThat(es.home.resolve(executable), file(File, owner, owner, p755)); + + if (distribution.packaging == Distribution.Packaging.ZIP) { + assertThat(es.home.resolve(executable + ".bat"), file(File, owner)); + } + }); + + if (distribution.packaging == Distribution.Packaging.ZIP) { + Stream.of( + "bin/elasticsearch-service.bat", + "bin/elasticsearch-service-mgr.exe", + "bin/elasticsearch-service-x64.exe" + ).forEach(executable -> assertThat(es.home.resolve(executable), file(File, owner))); + } + + Stream.of( + "elasticsearch.yml", + "jvm.options", + "log4j2.properties" + ).forEach(config -> assertThat(es.config.resolve(config), file(File, owner, owner, p660))); + + Stream.of( + "NOTICE.txt", + "LICENSE.txt", + "README.textile" + ).forEach(doc -> assertThat(es.home.resolve(doc), file(File, owner, owner, p644))); + } + + private static void verifyDefaultInstallation(Installation es, Distribution distribution, String owner) { + + Stream.of( + "bin/elasticsearch-certgen", + "bin/elasticsearch-certutil", + "bin/elasticsearch-croneval", + "bin/elasticsearch-migrate", + "bin/elasticsearch-saml-metadata", + "bin/elasticsearch-setup-passwords", + "bin/elasticsearch-sql-cli", + "bin/elasticsearch-syskeygen", + "bin/elasticsearch-users", + "bin/x-pack-env", + "bin/x-pack-security-env", + "bin/x-pack-watcher-env" + ).forEach(executable -> { + + assertThat(es.home.resolve(executable), file(File, owner, owner, p755)); + + if (distribution.packaging == Distribution.Packaging.ZIP) { + assertThat(es.home.resolve(executable + ".bat"), file(File, owner)); + } + }); + + // at this time we only install the current version of archive distributions, but if that changes we'll need to pass + // the version through here + assertThat(es.home.resolve("bin/elasticsearch-sql-cli-" + getCurrentVersion() + ".jar"), file(File, owner, owner, p755)); + + Stream.of( + "users", + "users_roles", + "roles.yml", + "role_mapping.yml", + "log4j2.properties" + ).forEach(config -> assertThat(es.config.resolve(config), file(File, owner, owner, p660))); + } + +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Cleanup.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Cleanup.java new file mode 100644 index 0000000000000..9e9150c9c1814 --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Cleanup.java @@ -0,0 +1,121 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.packaging.util; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.elasticsearch.packaging.util.FileUtils.getTempDir; +import static org.elasticsearch.packaging.util.FileUtils.lsGlob; +import static org.elasticsearch.packaging.util.Platforms.isAptGet; +import static org.elasticsearch.packaging.util.Platforms.isDPKG; +import static org.elasticsearch.packaging.util.Platforms.isRPM; +import static org.elasticsearch.packaging.util.Platforms.isSystemd; +import static org.elasticsearch.packaging.util.Platforms.isYUM; + +public class Cleanup { + + private static final List ELASTICSEARCH_FILES_LINUX = Arrays.asList( + "/usr/share/elasticsearch", + "/etc/elasticsearch", + "/var/lib/elasticsearch", + "/var/log/elasticsearch", + "/etc/default/elasticsearch", + "/etc/sysconfig/elasticsearch", + "/var/run/elasticsearch", + "/usr/share/doc/elasticsearch", + "/usr/lib/systemd/system/elasticsearch.conf", + "/usr/lib/tmpfiles.d/elasticsearch.conf", + "/usr/lib/sysctl.d/elasticsearch.conf" + ); + + // todo + private static final List ELASTICSEARCH_FILES_WINDOWS = Collections.emptyList(); + + public static void cleanEverything() { + final Shell sh = new Shell(); + + // kill elasticsearch processes + if (Platforms.WINDOWS) { + + // the view of processes returned by Get-Process doesn't expose command line arguments, so we use WMI here + sh.runIgnoreExitCode("powershell.exe", "-Command", + "Get-WmiObject Win32_Process | " + + "Where-Object { $_.CommandLine -Match 'org.elasticsearch.bootstrap.Elasticsearch' } | " + + "ForEach-Object { $_.Terminate() }"); + + } else { + + sh.runIgnoreExitCode("pkill", "-u", "elasticsearch"); + sh.runIgnoreExitCode("bash", "-c", + "ps aux | grep -i 'org.elasticsearch.bootstrap.Elasticsearch' | awk {'print $2'} | xargs kill -9"); + + } + + if (Platforms.LINUX) { + purgePackagesLinux(); + } + + // remove elasticsearch users + if (Platforms.LINUX) { + sh.runIgnoreExitCode("userdel", "elasticsearch"); + sh.runIgnoreExitCode("groupdel", "elasticsearch"); + } + + // delete files that may still exist + lsGlob(getTempDir(), "elasticsearch*").forEach(FileUtils::rm); + final List filesToDelete = Platforms.WINDOWS + ? ELASTICSEARCH_FILES_WINDOWS + : ELASTICSEARCH_FILES_LINUX; + filesToDelete.stream() + .map(Paths::get) + .filter(Files::exists) + .forEach(FileUtils::rm); + + // disable elasticsearch service + // todo add this for windows when adding tests for service intallation + if (Platforms.LINUX && isSystemd()) { + sh.run("systemctl", "unmask", "systemd-sysctl.service"); + } + } + + private static void purgePackagesLinux() { + final Shell sh = new Shell(); + + if (isRPM()) { + sh.runIgnoreExitCode("rpm", "--quiet", "-e", "elasticsearch", "elasticsearch-oss"); + } + + if (isYUM()) { + sh.runIgnoreExitCode("yum", "remove", "-y", "elasticsearch", "elasticsearch-oss"); + } + + if (isDPKG()) { + sh.runIgnoreExitCode("dpkg", "--purge", "elasticsearch", "elasticsearch-oss"); + } + + if (isAptGet()) { + sh.runIgnoreExitCode("apt-get", "--quiet", "--yes", "purge", "elasticsearch", "elasticsearch-oss"); + } + } +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Distribution.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Distribution.java new file mode 100644 index 0000000000000..4f0c8751ca4e7 --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Distribution.java @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.packaging.util; + +public enum Distribution { + + OSS_TAR(Packaging.TAR, Flavor.OSS), + OSS_ZIP(Packaging.ZIP, Flavor.OSS), + OSS_DEB(Packaging.DEB, Flavor.OSS), + OSS_RPM(Packaging.RPM, Flavor.OSS), + + DEFAULT_TAR(Packaging.TAR, Flavor.DEFAULT), + DEFAULT_ZIP(Packaging.ZIP, Flavor.DEFAULT), + DEFAULT_DEB(Packaging.DEB, Flavor.DEFAULT), + DEFAULT_RPM(Packaging.RPM, Flavor.DEFAULT); + + public final Packaging packaging; + public final Flavor flavor; + + Distribution(Packaging packaging, Flavor flavor) { + this.packaging = packaging; + this.flavor = flavor; + } + + public String filename(String version) { + return flavor.name + "-" + version + packaging.extension; + } + + public enum Packaging { + + TAR(".tar.gz", Platforms.LINUX), + ZIP(".zip", true), + DEB(".deb", Platforms.isDPKG()), + RPM(".rpm", Platforms.isRPM()); + + /** The extension of this distribution's file */ + public final String extension; + + /** Whether the distribution is intended for use on the platform the current JVM is running on */ + public final boolean compatible; + + Packaging(String extension, boolean compatible) { + this.extension = extension; + this.compatible = compatible; + } + } + + public enum Flavor { + + OSS("elasticsearch-oss"), + DEFAULT("elasticsearch"); + + public final String name; + + Flavor(String name) { + this.name = name; + } + } +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/FileMatcher.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/FileMatcher.java new file mode 100644 index 0000000000000..9fdf6d60081a3 --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/FileMatcher.java @@ -0,0 +1,137 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.packaging.util; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.PosixFileAttributes; +import java.nio.file.attribute.PosixFilePermission; +import java.util.Objects; +import java.util.Set; + +import static org.elasticsearch.packaging.util.FileUtils.getBasicFileAttributes; +import static org.elasticsearch.packaging.util.FileUtils.getFileOwner; +import static org.elasticsearch.packaging.util.FileUtils.getPosixFileAttributes; + +import static java.nio.file.attribute.PosixFilePermissions.fromString; + +/** + * Asserts that a file at a path matches its status as Directory/File, and its owner. If on a posix system, also matches the permission + * set is what we expect. + * + * This class saves information about its failed matches in instance variables and so instances should not be reused + */ +public class FileMatcher extends TypeSafeMatcher { + + public enum Fileness { File, Directory } + + public static final Set p755 = fromString("rwxr-xr-x"); + public static final Set p660 = fromString("rw-rw----"); + public static final Set p644 = fromString("rw-r--r--"); + + private final Fileness fileness; + private final String owner; + private final String group; + private final Set posixPermissions; + + private String mismatch; + + public FileMatcher(Fileness fileness, String owner, String group, Set posixPermissions) { + this.fileness = Objects.requireNonNull(fileness); + this.owner = Objects.requireNonNull(owner); + this.group = group; + this.posixPermissions = posixPermissions; + } + + @Override + protected boolean matchesSafely(Path path) { + if (Files.exists(path) == false) { + mismatch = "Does not exist"; + return false; + } + + if (Platforms.WINDOWS) { + final BasicFileAttributes attributes = getBasicFileAttributes(path); + final String attributeViewOwner = getFileOwner(path); + + if (fileness.equals(Fileness.Directory) != attributes.isDirectory()) { + mismatch = "Is " + (attributes.isDirectory() ? "a directory" : "a file"); + return false; + } + + if (attributeViewOwner.contains(owner) == false) { + mismatch = "Owned by " + attributeViewOwner; + return false; + } + } else { + final PosixFileAttributes attributes = getPosixFileAttributes(path); + + if (fileness.equals(Fileness.Directory) != attributes.isDirectory()) { + mismatch = "Is " + (attributes.isDirectory() ? "a directory" : "a file"); + return false; + } + + if (owner.equals(attributes.owner().getName()) == false) { + mismatch = "Owned by " + attributes.owner().getName(); + return false; + } + + if (group != null && group.equals(attributes.group().getName()) == false) { + mismatch = "Owned by group " + attributes.group().getName(); + return false; + } + + if (posixPermissions != null && posixPermissions.equals(attributes.permissions()) == false) { + mismatch = "Has permissions " + attributes.permissions(); + return false; + } + } + + return true; + } + + @Override + public void describeMismatchSafely(Path path, Description description) { + description.appendText("path ").appendValue(path); + if (mismatch != null) { + description.appendText(mismatch); + } + } + + @Override + public void describeTo(Description description) { + description.appendValue("file/directory: ").appendValue(fileness) + .appendText(" with owner ").appendValue(owner) + .appendText(" with group ").appendValue(group) + .appendText(" with posix permissions ").appendValueList("[", ",", "]", posixPermissions); + } + + public static FileMatcher file(Fileness fileness, String owner) { + return file(fileness, owner, null, null); + } + + public static FileMatcher file(Fileness fileness, String owner, String group, Set permissions) { + return new FileMatcher(fileness, owner, group, permissions); + } +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/FileUtils.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/FileUtils.java new file mode 100644 index 0000000000000..ad826675244a0 --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/FileUtils.java @@ -0,0 +1,134 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.packaging.util; + +import org.elasticsearch.core.internal.io.IOUtils; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileOwnerAttributeView; +import java.nio.file.attribute.PosixFileAttributes; +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsNot.not; +import static org.hamcrest.text.IsEmptyString.isEmptyOrNullString; + +/** + * Wrappers and convenience methods for common filesystem operations + */ +public class FileUtils { + + public static List lsGlob(Path directory, String glob) { + List paths = new ArrayList<>(); + try (DirectoryStream stream = Files.newDirectoryStream(directory, glob)) { + + for (Path path : stream) { + paths.add(path); + } + return paths; + + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static void rm(Path... paths) { + try { + IOUtils.rm(paths); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static Path mv(Path source, Path target) { + try { + return Files.move(source, target); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static String slurp(Path file) { + try { + return String.join("\n", Files.readAllLines(file)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Gets the owner of a file in a way that should be supported by all filesystems that have a concept of file owner + */ + public static String getFileOwner(Path path) { + try { + FileOwnerAttributeView view = Files.getFileAttributeView(path, FileOwnerAttributeView.class); + return view.getOwner().getName(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Gets attributes that are supported by all filesystems + */ + public static BasicFileAttributes getBasicFileAttributes(Path path) { + try { + return Files.readAttributes(path, BasicFileAttributes.class); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Gets attributes that are supported by posix filesystems + */ + public static PosixFileAttributes getPosixFileAttributes(Path path) { + try { + return Files.readAttributes(path, PosixFileAttributes.class); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + // vagrant creates /tmp for us in windows so we use that to avoid long paths + public static Path getTempDir() { + return Paths.get("/tmp"); + } + + public static Path getDefaultArchiveInstallPath() { + return getTempDir().resolve("elasticsearch"); + } + + public static String getCurrentVersion() { + return slurp(getPackagingArchivesDir().resolve("version")); + } + + public static Path getPackagingArchivesDir() { + String fromEnv = System.getenv("PACKAGING_ARCHIVES"); + assertThat(fromEnv, not(isEmptyOrNullString())); + return Paths.get(fromEnv); + } +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Installation.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Installation.java new file mode 100644 index 0000000000000..d231762d06227 --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Installation.java @@ -0,0 +1,58 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.packaging.util; + +import java.nio.file.Path; + +/** + * Represents an installation of Elasticsearch + */ +public class Installation { + + public final Path home; + public final Path config; + public final Path data; + public final Path logs; + public final Path plugins; + public final Path modules; + public final Path scripts; + + public Installation(Path home, Path config, Path data, Path logs, Path plugins, Path modules, Path scripts) { + this.home = home; + this.config = config; + this.data = data; + this.logs = logs; + this.plugins = plugins; + this.modules = modules; + this.scripts = scripts; + } + + public Installation(Path home) { + this( + home, + home.resolve("config"), + home.resolve("data"), + home.resolve("logs"), + home.resolve("plugins"), + home.resolve("modules"), + home.resolve("scripts") + ); + } +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Platforms.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Platforms.java new file mode 100644 index 0000000000000..230af8efc2dbb --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Platforms.java @@ -0,0 +1,68 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.packaging.util; + +public class Platforms { + public static final String OS_NAME = System.getProperty("os.name"); + public static final boolean LINUX = OS_NAME.startsWith("Linux"); + public static final boolean WINDOWS = OS_NAME.startsWith("Windows"); + + public static boolean isDPKG() { + if (WINDOWS) { + return false; + } + return new Shell().runIgnoreExitCode("which", "dpkg").isSuccess(); + } + + public static boolean isAptGet() { + if (WINDOWS) { + return false; + } + return new Shell().runIgnoreExitCode("which", "apt-get").isSuccess(); + } + + public static boolean isRPM() { + if (WINDOWS) { + return false; + } + return new Shell().runIgnoreExitCode("which", "rpm").isSuccess(); + } + + public static boolean isYUM() { + if (WINDOWS) { + return false; + } + return new Shell().runIgnoreExitCode("which", "yum").isSuccess(); + } + + public static boolean isSystemd() { + if (WINDOWS) { + return false; + } + return new Shell().runIgnoreExitCode("which", "systemctl").isSuccess(); + } + + public static boolean isSysVInit() { + if (WINDOWS) { + return false; + } + return new Shell().runIgnoreExitCode("which", "service").isSuccess(); + } +} diff --git a/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Shell.java b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Shell.java new file mode 100644 index 0000000000000..3adc0b62e04ea --- /dev/null +++ b/qa/vagrant/src/main/java/org/elasticsearch/packaging/util/Shell.java @@ -0,0 +1,193 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.packaging.util; + +import org.elasticsearch.common.SuppressForbidden; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static java.util.Collections.emptyMap; + +/** + * Wrapper to run shell commands and collect their outputs in a less verbose way + */ +public class Shell { + + final Map env; + final Path workingDirectory; + + public Shell() { + this(emptyMap(), null); + } + + public Shell(Map env) { + this(env, null); + } + + public Shell(Path workingDirectory) { + this(emptyMap(), workingDirectory); + } + + public Shell(Map env, Path workingDirectory) { + this.env = new HashMap<>(env); + this.workingDirectory = workingDirectory; + } + + public Result run(String... command) { + Result result = runIgnoreExitCode(command); + if (result.isSuccess() == false) { + throw new RuntimeException("Command was not successful: [" + String.join(" ", command) + "] result: " + result.toString()); + } + return result; + } + + public Result runIgnoreExitCode(String... command) { + ProcessBuilder builder = new ProcessBuilder(); + builder.command(command); + + if (workingDirectory != null) { + setWorkingDirectory(builder, workingDirectory); + } + + if (env != null && env.isEmpty() == false) { + for (Map.Entry entry : env.entrySet()) { + builder.environment().put(entry.getKey(), entry.getValue()); + } + } + + try { + + Process process = builder.start(); + + StringBuilder stdout = new StringBuilder(); + StringBuilder stderr = new StringBuilder(); + + Thread stdoutThread = new Thread(new StreamCollector(process.getInputStream(), stdout)); + Thread stderrThread = new Thread(new StreamCollector(process.getErrorStream(), stderr)); + + stdoutThread.start(); + stderrThread.start(); + + stdoutThread.join(); + stderrThread.join(); + + int exitCode = process.waitFor(); + + return new Result(exitCode, stdout.toString(), stderr.toString()); + + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + @SuppressForbidden(reason = "ProcessBuilder expects java.io.File") + private static void setWorkingDirectory(ProcessBuilder builder, Path path) { + builder.directory(path.toFile()); + } + + public String toString() { + return new StringBuilder() + .append("<") + .append(this.getClass().getName()) + .append(" ") + .append("env = [") + .append(env) + .append("]") + .append("workingDirectory = [") + .append(workingDirectory) + .append("]") + .append(">") + .toString(); + } + + public static class Result { + public final int exitCode; + public final String stdout; + public final String stderr; + + public Result(int exitCode, String stdout, String stderr) { + this.exitCode = exitCode; + this.stdout = stdout; + this.stderr = stderr; + } + + public boolean isSuccess() { + return exitCode == 0; + } + + public String toString() { + return new StringBuilder() + .append("<") + .append(this.getClass().getName()) + .append(" ") + .append("exitCode = [") + .append(exitCode) + .append("]") + .append(" ") + .append("stdout = [") + .append(stdout) + .append("]") + .append(" ") + .append("stderr = [") + .append(stderr) + .append("]") + .append(">") + .toString(); + } + } + + private static class StreamCollector implements Runnable { + private final InputStream input; + private final Appendable appendable; + + StreamCollector(InputStream input, Appendable appendable) { + this.input = Objects.requireNonNull(input); + this.appendable = Objects.requireNonNull(appendable); + } + + public void run() { + try { + + BufferedReader reader = new BufferedReader(reader(input)); + String line; + + while ((line = reader.readLine()) != null) { + appendable.append(line); + appendable.append("\n"); + } + + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @SuppressForbidden(reason = "the system's default character set is a best guess of what subprocesses will use") + private static InputStreamReader reader(InputStream inputStream) { + return new InputStreamReader(inputStream); + } + } +} diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/search.inner_hits/10_basic.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/search.inner_hits/10_basic.yml index 40e9d705ea4ad..884a50507c7b7 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/search.inner_hits/10_basic.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/search.inner_hits/10_basic.yml @@ -45,9 +45,8 @@ setup: "Nested doc version and seqIDs": - skip: - # fixed in 6.0.1 - version: " - 6.0.0" - reason: "version and seq IDs where not accurate in previous versions" + version: " - 6.3.99" + reason: "object notation for docvalue_fields was introduced in 6.4" - do: index: @@ -61,7 +60,7 @@ setup: - do: search: - body: { "query" : { "nested" : { "path" : "nested_field", "query" : { "match_all" : {} }, "inner_hits" : { version: true, "docvalue_fields": ["_seq_no"]} }}, "version": true, "docvalue_fields" : ["_seq_no"] } + body: { "query" : { "nested" : { "path" : "nested_field", "query" : { "match_all" : {} }, "inner_hits" : { version: true, "docvalue_fields": [ { "field": "_seq_no", "format": "use_field_mapping" } ]} }}, "version": true, "docvalue_fields" : [ { "field": "_seq_no", "format": "use_field_mapping" } ] } - match: { hits.total: 1 } - match: { hits.hits.0._index: "test" } @@ -84,7 +83,7 @@ setup: - do: search: - body: { "query" : { "nested" : { "path" : "nested_field", "query" : { "match_all" : {} }, "inner_hits" : { version: true, "docvalue_fields": ["_seq_no"]} }}, "version": true, "docvalue_fields" : ["_seq_no"] } + body: { "query" : { "nested" : { "path" : "nested_field", "query" : { "match_all" : {} }, "inner_hits" : { version: true, "docvalue_fields": [ { "field": "_seq_no", "format": "use_field_mapping" } ]} }}, "version": true, "docvalue_fields" : [ { "field": "_seq_no", "format": "use_field_mapping" } ] } - match: { hits.total: 1 } - match: { hits.hits.0._index: "test" } diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/search/10_source_filtering.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/search/10_source_filtering.yml index 3830a68b28fa0..59692873cc456 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/search/10_source_filtering.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/search/10_source_filtering.yml @@ -133,7 +133,53 @@ setup: --- "docvalue_fields": + - skip: + version: " - 6.3.99" + reason: format option was added in 6.4 + features: warnings - do: + warnings: + - 'Doc-value field [count] is not using a format. The output will change in 7.0 when doc value fields get formatted based on mappings by default. It is recommended to pass [format=use_field_mapping] with the doc value field in order to opt in for the future behaviour and ease the migration to 7.0.' + search: + body: + docvalue_fields: [ "count" ] + - match: { hits.hits.0.fields.count: [1] } + +--- +"docvalue_fields as url param": + - skip: + version: " - 6.3.99" + reason: format option was added in 6.4 + features: warnings + - do: + warnings: + - 'Doc-value field [count] is not using a format. The output will change in 7.0 when doc value fields get formatted based on mappings by default. It is recommended to pass [format=use_field_mapping] with the doc value field in order to opt in for the future behaviour and ease the migration to 7.0.' search: docvalue_fields: [ "count" ] - match: { hits.hits.0.fields.count: [1] } + +--- +"docvalue_fields with default format": + - skip: + version: " - 6.3.99" + reason: format option was added in 6.4 + - do: + search: + body: + docvalue_fields: + - field: "count" + format: "use_field_mapping" + - match: { hits.hits.0.fields.count: [1] } + +--- +"docvalue_fields with explicit format": + - skip: + version: " - 6.3.99" + reason: format option was added in 6.4 + - do: + search: + body: + docvalue_fields: + - field: "count" + format: "#.0" + - match: { hits.hits.0.fields.count: ["1.0"] } diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/search/30_limits.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/search/30_limits.yml index da9c739ed6677..905635e1d10b6 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/search/30_limits.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/search/30_limits.yml @@ -62,6 +62,9 @@ setup: --- "Docvalues_fields size limit": + - skip: + version: " - 6.3.99" + reason: "The object notation for docvalue_fields is only supported on 6.4+" - do: catch: /Trying to retrieve too many docvalue_fields\. Must be less than or equal to[:] \[2\] but was \[3\]\. This limit can be set by changing the \[index.max_docvalue_fields_search\] index level setting\./ search: @@ -69,7 +72,13 @@ setup: body: query: match_all: {} - docvalue_fields: ["one", "two", "three"] + docvalue_fields: + - field: "one" + format: "use_field_mapping" + - field: "two" + format: "use_field_mapping" + - field: "three" + format: "use_field_mapping" --- "Script_fields size limit": diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/snapshot.get_repository/10_basic.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/snapshot.get_repository/10_basic.yml index b944fe43791e4..47f5ac0934ce9 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/snapshot.get_repository/10_basic.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/snapshot.get_repository/10_basic.yml @@ -51,6 +51,9 @@ setup: --- "Verify created repository": + - skip: + version: " - 6.99.99" + reason: AwaitsFix for https://github.com/elastic/elasticsearch/issues/30807 - do: snapshot.verify_repository: repository: test_repo_get_2 diff --git a/server/src/main/java/org/elasticsearch/action/search/ExpandSearchPhase.java b/server/src/main/java/org/elasticsearch/action/search/ExpandSearchPhase.java index 1e11f126bb6f0..d9ed6e6792f85 100644 --- a/server/src/main/java/org/elasticsearch/action/search/ExpandSearchPhase.java +++ b/server/src/main/java/org/elasticsearch/action/search/ExpandSearchPhase.java @@ -153,7 +153,7 @@ private SearchSourceBuilder buildExpandSearchSourceBuilder(InnerHitBuilder optio } } if (options.getDocValueFields() != null) { - options.getDocValueFields().forEach(groupSource::docValueField); + options.getDocValueFields().forEach(ff -> groupSource.docValueField(ff.field, ff.format)); } if (options.getStoredFieldsContext() != null && options.getStoredFieldsContext().fieldNames() != null) { options.getStoredFieldsContext().fieldNames().forEach(groupSource::storedField); diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchRequestBuilder.java b/server/src/main/java/org/elasticsearch/action/search/SearchRequestBuilder.java index 91ac46c1d62b8..424db04ce3903 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchRequestBuilder.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchRequestBuilder.java @@ -290,11 +290,21 @@ public SearchRequestBuilder setFetchSource(@Nullable String[] includes, @Nullabl * * @param name The field to get from the docvalue */ - public SearchRequestBuilder addDocValueField(String name) { - sourceBuilder().docValueField(name); + public SearchRequestBuilder addDocValueField(String name, String format) { + sourceBuilder().docValueField(name, format); return this; } + /** + * Adds a docvalue based field to load and return. The field does not have to be stored, + * but its recommended to use non analyzed or numeric fields. + * + * @param name The field to get from the docvalue + */ + public SearchRequestBuilder addDocValueField(String name) { + return addDocValueField(name, null); + } + /** * Adds a stored field to load and return (note, it must be stored) as part of the search request. */ diff --git a/server/src/main/java/org/elasticsearch/cluster/AckedClusterStateTaskListener.java b/server/src/main/java/org/elasticsearch/cluster/AckedClusterStateTaskListener.java index 148a1dea3095f..a4767507ef1aa 100644 --- a/server/src/main/java/org/elasticsearch/cluster/AckedClusterStateTaskListener.java +++ b/server/src/main/java/org/elasticsearch/cluster/AckedClusterStateTaskListener.java @@ -25,7 +25,11 @@ public interface AckedClusterStateTaskListener extends ClusterStateTaskListener { /** - * Called to determine which nodes the acknowledgement is expected from + * Called to determine which nodes the acknowledgement is expected from. + * + * As this method will be called multiple times to determine the set of acking nodes, + * it is crucial for it to return consistent results: Given the same listener instance + * and the same node parameter, the method implementation should return the same result. * * @param discoveryNode a node * @return true if the node is expected to send ack back, false otherwise diff --git a/server/src/main/java/org/elasticsearch/cluster/AckedClusterStateUpdateTask.java b/server/src/main/java/org/elasticsearch/cluster/AckedClusterStateUpdateTask.java index faf2f30bb3ed4..8d61fe964265d 100644 --- a/server/src/main/java/org/elasticsearch/cluster/AckedClusterStateUpdateTask.java +++ b/server/src/main/java/org/elasticsearch/cluster/AckedClusterStateUpdateTask.java @@ -61,7 +61,7 @@ public boolean mustAck(DiscoveryNode discoveryNode) { * @param e optional error that might have been thrown */ public void onAllNodesAcked(@Nullable Exception e) { - listener.onResponse(newResponse(true)); + listener.onResponse(newResponse(e == null)); } protected abstract Response newResponse(boolean acknowledged); diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataMappingService.java b/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataMappingService.java index b8e898cf6f5e3..82d947b4158a2 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataMappingService.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/MetaDataMappingService.java @@ -363,7 +363,7 @@ public boolean mustAck(DiscoveryNode discoveryNode) { @Override public void onAllNodesAcked(@Nullable Exception e) { - listener.onResponse(new ClusterStateUpdateResponse(true)); + listener.onResponse(new ClusterStateUpdateResponse(e == null)); } @Override diff --git a/server/src/main/java/org/elasticsearch/cluster/service/MasterService.java b/server/src/main/java/org/elasticsearch/cluster/service/MasterService.java index 54a6568af3fa2..1757548c28b09 100644 --- a/server/src/main/java/org/elasticsearch/cluster/service/MasterService.java +++ b/server/src/main/java/org/elasticsearch/cluster/service/MasterService.java @@ -563,7 +563,7 @@ private static class AckCountDownListener implements Discovery.AckListener { private final AckedClusterStateTaskListener ackedTaskListener; private final CountDown countDown; - private final DiscoveryNodes nodes; + private final DiscoveryNode masterNode; private final long clusterStateVersion; private final Future ackTimeoutCallback; private Exception lastFailure; @@ -572,15 +572,14 @@ private static class AckCountDownListener implements Discovery.AckListener { ThreadPool threadPool) { this.ackedTaskListener = ackedTaskListener; this.clusterStateVersion = clusterStateVersion; - this.nodes = nodes; + this.masterNode = nodes.getMasterNode(); int countDown = 0; for (DiscoveryNode node : nodes) { - if (ackedTaskListener.mustAck(node)) { + //we always wait for at least the master node + if (node.equals(masterNode) || ackedTaskListener.mustAck(node)) { countDown++; } } - //we always wait for at least 1 node (the master) - countDown = Math.max(1, countDown); logger.trace("expecting {} acknowledgements for cluster_state update (version: {})", countDown, clusterStateVersion); this.countDown = new CountDown(countDown); this.ackTimeoutCallback = threadPool.schedule(ackedTaskListener.ackTimeout(), ThreadPool.Names.GENERIC, () -> onTimeout()); @@ -588,11 +587,8 @@ private static class AckCountDownListener implements Discovery.AckListener { @Override public void onNodeAck(DiscoveryNode node, @Nullable Exception e) { - if (!ackedTaskListener.mustAck(node)) { - //we always wait for the master ack anyway - if (!node.equals(nodes.getMasterNode())) { - return; - } + if (node.equals(masterNode) == false && ackedTaskListener.mustAck(node) == false) { + return; } if (e == null) { logger.trace("ack received from node [{}], cluster_state update (version: {})", node, clusterStateVersion); diff --git a/server/src/main/java/org/elasticsearch/discovery/zen/NodesFaultDetection.java b/server/src/main/java/org/elasticsearch/discovery/zen/NodesFaultDetection.java index 218e6e3f63f95..d19cc98441b79 100644 --- a/server/src/main/java/org/elasticsearch/discovery/zen/NodesFaultDetection.java +++ b/server/src/main/java/org/elasticsearch/discovery/zen/NodesFaultDetection.java @@ -44,6 +44,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Supplier; import static org.elasticsearch.common.util.concurrent.ConcurrentCollections.newConcurrentMap; @@ -66,13 +67,16 @@ public void onPingReceived(PingRequest pingRequest) {} private final ConcurrentMap nodesFD = newConcurrentMap(); - private volatile long clusterStateVersion = ClusterState.UNKNOWN_VERSION; + private final Supplier clusterStateSupplier; private volatile DiscoveryNode localNode; - public NodesFaultDetection(Settings settings, ThreadPool threadPool, TransportService transportService, ClusterName clusterName) { + public NodesFaultDetection(Settings settings, ThreadPool threadPool, TransportService transportService, + Supplier clusterStateSupplier, ClusterName clusterName) { super(settings, threadPool, transportService, clusterName); + this.clusterStateSupplier = clusterStateSupplier; + logger.debug("[node ] uses ping_interval [{}], ping_timeout [{}], ping_retries [{}]", pingInterval, pingRetryTimeout, pingRetryCount); @@ -208,15 +212,18 @@ private boolean running() { return NodeFD.this.equals(nodesFD.get(node)); } + private PingRequest newPingRequest() { + return new PingRequest(node, clusterName, localNode, clusterStateSupplier.get().version()); + } + @Override public void run() { if (!running()) { return; } - final PingRequest pingRequest = new PingRequest(node, clusterName, localNode, clusterStateVersion); final TransportRequestOptions options = TransportRequestOptions.builder().withType(TransportRequestOptions.Type.PING) .withTimeout(pingRetryTimeout).build(); - transportService.sendRequest(node, PING_ACTION_NAME, pingRequest, options, new TransportResponseHandler() { + transportService.sendRequest(node, PING_ACTION_NAME, newPingRequest(), options, new TransportResponseHandler() { @Override public PingResponse newInstance() { return new PingResponse(); @@ -254,7 +261,7 @@ public void handleException(TransportException exp) { } } else { // resend the request, not reschedule, rely on send timeout - transportService.sendRequest(node, PING_ACTION_NAME, pingRequest, options, this); + transportService.sendRequest(node, PING_ACTION_NAME, newPingRequest(), options, this); } } diff --git a/server/src/main/java/org/elasticsearch/discovery/zen/ZenDiscovery.java b/server/src/main/java/org/elasticsearch/discovery/zen/ZenDiscovery.java index 4621e6769e962..02b2822fcf431 100644 --- a/server/src/main/java/org/elasticsearch/discovery/zen/ZenDiscovery.java +++ b/server/src/main/java/org/elasticsearch/discovery/zen/ZenDiscovery.java @@ -205,7 +205,7 @@ public ZenDiscovery(Settings settings, ThreadPool threadPool, TransportService t this.masterFD = new MasterFaultDetection(settings, threadPool, transportService, this::clusterState, masterService, clusterName); this.masterFD.addListener(new MasterNodeFailureListener()); - this.nodesFD = new NodesFaultDetection(settings, threadPool, transportService, clusterName); + this.nodesFD = new NodesFaultDetection(settings, threadPool, transportService, this::clusterState, clusterName); this.nodesFD.addListener(new NodeFaultDetectionListener()); this.pendingStatesQueue = new PendingClusterStatesQueue(logger, MAX_PENDING_CLUSTER_STATES_SETTING.get(settings)); diff --git a/server/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java index d7d9797dad88b..490f8ee72c45d 100644 --- a/server/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/GeoShapeQueryBuilder.java @@ -29,6 +29,7 @@ import org.apache.lucene.spatial.query.SpatialArgs; import org.apache.lucene.spatial.query.SpatialOperation; import org.apache.lucene.util.SetOnce; +import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.get.GetRequest; import org.elasticsearch.action.get.GetResponse; @@ -77,6 +78,7 @@ public class GeoShapeQueryBuilder extends AbstractQueryBuilder supplier = new SetOnce<>(); queryRewriteContext.registerAsyncAction((client, listener) -> { GetRequest getRequest = new GetRequest(indexedShapeIndex, indexedShapeType, indexedShapeId); + getRequest.routing(indexedShapeRouting); fetch(client, getRequest, indexedShapePath, ActionListener.wrap(builder-> { supplier.set(builder); listener.onResponse(null); diff --git a/server/src/main/java/org/elasticsearch/index/query/InnerHitBuilder.java b/server/src/main/java/org/elasticsearch/index/query/InnerHitBuilder.java index 6bb8e0259fb15..92da1bc3b65f9 100644 --- a/server/src/main/java/org/elasticsearch/index/query/InnerHitBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/InnerHitBuilder.java @@ -33,6 +33,7 @@ import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.builder.SearchSourceBuilder.ScriptField; import org.elasticsearch.search.fetch.StoredFieldsContext; +import org.elasticsearch.search.fetch.subphase.DocValueFieldsContext.FieldAndFormat; import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder; import org.elasticsearch.search.sort.SortBuilder; @@ -45,6 +46,7 @@ import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; import static org.elasticsearch.common.xcontent.XContentParser.Token.END_OBJECT; @@ -65,7 +67,8 @@ public final class InnerHitBuilder implements Writeable, ToXContentObject { PARSER.declareBoolean(InnerHitBuilder::setVersion, SearchSourceBuilder.VERSION_FIELD); PARSER.declareBoolean(InnerHitBuilder::setTrackScores, SearchSourceBuilder.TRACK_SCORES_FIELD); PARSER.declareStringArray(InnerHitBuilder::setStoredFieldNames, SearchSourceBuilder.STORED_FIELDS_FIELD); - PARSER.declareStringArray(InnerHitBuilder::setDocValueFields, SearchSourceBuilder.DOCVALUE_FIELDS_FIELD); + PARSER.declareObjectArray(InnerHitBuilder::setDocValueFields, + (p,c) -> FieldAndFormat.fromXContent(p), SearchSourceBuilder.DOCVALUE_FIELDS_FIELD); PARSER.declareField((p, i, c) -> { try { Set scriptFields = new HashSet<>(); @@ -102,7 +105,7 @@ public final class InnerHitBuilder implements Writeable, ToXContentObject { private StoredFieldsContext storedFieldsContext; private QueryBuilder query = DEFAULT_INNER_HIT_QUERY; private List> sorts; - private List docValueFields; + private List docValueFields; private Set scriptFields; private HighlightBuilder highlightBuilder; private FetchSourceContext fetchSourceContext; @@ -134,7 +137,18 @@ public InnerHitBuilder(StreamInput in) throws IOException { version = in.readBoolean(); trackScores = in.readBoolean(); storedFieldsContext = in.readOptionalWriteable(StoredFieldsContext::new); - docValueFields = (List) in.readGenericValue(); + if (in.getVersion().before(Version.V_6_4_0)) { + List fieldList = (List) in.readGenericValue(); + if (fieldList == null) { + docValueFields = null; + } else { + docValueFields = fieldList.stream() + .map(field -> new FieldAndFormat(field, null)) + .collect(Collectors.toList()); + } + } else { + docValueFields = in.readBoolean() ? in.readList(FieldAndFormat::new) : null; + } if (in.readBoolean()) { int size = in.readVInt(); scriptFields = new HashSet<>(size); @@ -174,7 +188,16 @@ public void writeTo(StreamOutput out) throws IOException { out.writeBoolean(version); out.writeBoolean(trackScores); out.writeOptionalWriteable(storedFieldsContext); - out.writeGenericValue(docValueFields); + if (out.getVersion().before(Version.V_6_4_0)) { + out.writeGenericValue(docValueFields == null + ? null + : docValueFields.stream().map(ff -> ff.field).collect(Collectors.toList())); + } else { + out.writeBoolean(docValueFields != null); + if (docValueFields != null) { + out.writeList(docValueFields); + } + } boolean hasScriptFields = scriptFields != null; out.writeBoolean(hasScriptFields); if (hasScriptFields) { @@ -248,7 +271,9 @@ private void writeToBWC(StreamOutput out, out.writeBoolean(version); out.writeBoolean(trackScores); out.writeOptionalWriteable(storedFieldsContext); - out.writeGenericValue(docValueFields); + out.writeGenericValue(docValueFields == null + ? null + : docValueFields.stream().map(ff -> ff.field).collect(Collectors.toList())); boolean hasScriptFields = scriptFields != null; out.writeBoolean(hasScriptFields); if (hasScriptFields) { @@ -390,14 +415,14 @@ public InnerHitBuilder setStoredFieldNames(List fieldNames) { /** * Gets the docvalue fields. */ - public List getDocValueFields() { + public List getDocValueFields() { return docValueFields; } /** * Sets the stored fields to load from the docvalue and return. */ - public InnerHitBuilder setDocValueFields(List docValueFields) { + public InnerHitBuilder setDocValueFields(List docValueFields) { this.docValueFields = docValueFields; return this; } @@ -405,14 +430,21 @@ public InnerHitBuilder setDocValueFields(List docValueFields) { /** * Adds a field to load from the docvalue and return. */ - public InnerHitBuilder addDocValueField(String field) { + public InnerHitBuilder addDocValueField(String field, String format) { if (docValueFields == null) { docValueFields = new ArrayList<>(); } - docValueFields.add(field); + docValueFields.add(new FieldAndFormat(field, null)); return this; } + /** + * Adds a field to load from doc values and return. + */ + public InnerHitBuilder addDocValueField(String field) { + return addDocValueField(field, null); + } + public Set getScriptFields() { return scriptFields; } @@ -489,8 +521,15 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws } if (docValueFields != null) { builder.startArray(SearchSourceBuilder.DOCVALUE_FIELDS_FIELD.getPreferredName()); - for (String docValueField : docValueFields) { - builder.value(docValueField); + for (FieldAndFormat docValueField : docValueFields) { + if (docValueField.format == null) { + builder.value(docValueField.field); + } else { + builder.startObject() + .field("field", docValueField.field) + .field("format", docValueField.format) + .endObject(); + } } builder.endArray(); } diff --git a/server/src/main/java/org/elasticsearch/node/Node.java b/server/src/main/java/org/elasticsearch/node/Node.java index d917018eb8eb5..c5df30a2d8771 100644 --- a/server/src/main/java/org/elasticsearch/node/Node.java +++ b/server/src/main/java/org/elasticsearch/node/Node.java @@ -231,6 +231,7 @@ public static final Settings addNodeNameIfNeeded(Settings settings, final String private final Lifecycle lifecycle = new Lifecycle(); private final Injector injector; private final Settings settings; + private final Settings originalSettings; private final Environment environment; private final NodeEnvironment nodeEnvironment; private final PluginsService pluginsService; @@ -261,6 +262,7 @@ protected Node(final Environment environment, Collection logger.info("initializing ..."); } try { + originalSettings = environment.settings(); Settings tmpSettings = Settings.builder().put(environment.settings()) .put(Client.CLIENT_TYPE_SETTING_S.getKey(), CLIENT_TYPE).build(); @@ -568,7 +570,14 @@ protected void processRecoverySettings(ClusterSettings clusterSettings, Recovery } /** - * The settings that were used to create the node. + * The original settings that were used to create the node + */ + public Settings originalSettings() { + return originalSettings; + } + + /** + * The settings that are used by this node. Contains original settings as well as additional settings provided by plugins. */ public Settings settings() { return this.settings; diff --git a/server/src/main/java/org/elasticsearch/plugins/ClusterPlugin.java b/server/src/main/java/org/elasticsearch/plugins/ClusterPlugin.java index 5e58aa5a3a926..61145c7a1d7cf 100644 --- a/server/src/main/java/org/elasticsearch/plugins/ClusterPlugin.java +++ b/server/src/main/java/org/elasticsearch/plugins/ClusterPlugin.java @@ -70,6 +70,8 @@ default void onNodeStarted() { * Returns a map of {@link ClusterState.Custom} supplier that should be invoked to initialize the initial clusterstate. * This allows custom clusterstate extensions to be always present and prevents invariants where clusterstates are published * but customs are not initialized. + * + * TODO: Remove this whole concept of InitialClusterStateCustomSupplier, it's not used anymore */ default Map> getInitialClusterStateCustomSupplier() { return Collections.emptyMap(); } } diff --git a/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java b/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java index 3098dc03a8c71..4074d1a8fa1be 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java @@ -214,7 +214,7 @@ private static void parseSearchSource(final SearchSourceBuilder searchSourceBuil if (Strings.hasText(sDocValueFields)) { String[] sFields = Strings.splitStringByCommaToArray(sDocValueFields); for (String field : sFields) { - searchSourceBuilder.docValueField(field); + searchSourceBuilder.docValueField(field, null); } } } diff --git a/server/src/main/java/org/elasticsearch/search/DocValueFormat.java b/server/src/main/java/org/elasticsearch/search/DocValueFormat.java index 1824f17941b0e..8677370fc9927 100644 --- a/server/src/main/java/org/elasticsearch/search/DocValueFormat.java +++ b/server/src/main/java/org/elasticsearch/search/DocValueFormat.java @@ -49,17 +49,17 @@ public interface DocValueFormat extends NamedWriteable { /** Format a long value. This is used by terms and histogram aggregations * to format keys for fields that use longs as a doc value representation * such as the {@code long} and {@code date} fields. */ - String format(long value); + Object format(long value); /** Format a double value. This is used by terms and stats aggregations * to format keys for fields that use numbers as a doc value representation * such as the {@code long}, {@code double} or {@code date} fields. */ - String format(double value); + Object format(double value); /** Format a binary value. This is used by terms aggregations to format * keys for fields that use binary doc value representations such as the * {@code keyword} and {@code ip} fields. */ - String format(BytesRef value); + Object format(BytesRef value); /** Parse a value that was formatted with {@link #format(long)} back to the * original long value. */ @@ -85,13 +85,13 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - public String format(long value) { - return Long.toString(value); + public Long format(long value) { + return value; } @Override - public String format(double value) { - return Double.toString(value); + public Double format(double value) { + return value; } @Override @@ -235,13 +235,13 @@ public void writeTo(StreamOutput out) throws IOException { } @Override - public String format(long value) { - return java.lang.Boolean.valueOf(value != 0).toString(); + public Boolean format(long value) { + return java.lang.Boolean.valueOf(value != 0); } @Override - public String format(double value) { - return java.lang.Boolean.valueOf(value != 0).toString(); + public Boolean format(double value) { + return java.lang.Boolean.valueOf(value != 0); } @Override diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramAggregationBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramAggregationBuilder.java index c72e9d22dc0ae..bb391f21f1e40 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramAggregationBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramAggregationBuilder.java @@ -407,8 +407,8 @@ DateTimeZone rewriteTimeZone(QueryShardContext context) throws IOException { final long high = nextTransition; final DocValueFormat format = ft.docValueFormat(null, null); - final String formattedLow = format.format(low); - final String formattedHigh = format.format(high); + final Object formattedLow = format.format(low); + final Object formattedHigh = format.format(high); if (ft.isFieldWithinQuery(reader, formattedLow, formattedHigh, true, false, tz, null, context) == Relation.WITHIN) { // All values in this reader have the same offset despite daylight saving times. diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java index dfa12db0cd31c..84dec2c983e28 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalDateHistogram.java @@ -107,7 +107,7 @@ public void writeTo(StreamOutput out) throws IOException { @Override public String getKeyAsString() { - return format.format(key); + return format.format(key).toString(); } @Override @@ -138,7 +138,7 @@ Bucket reduce(List buckets, ReduceContext context) { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - String keyAsString = format.format(key); + String keyAsString = format.format(key).toString(); if (keyed) { builder.startObject(keyAsString); } else { diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java index b3516b04dfc3c..1831e012a318c 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/histogram/InternalHistogram.java @@ -103,7 +103,7 @@ public void writeTo(StreamOutput out) throws IOException { @Override public String getKeyAsString() { - return format.format(key); + return format.format(key).toString(); } @Override @@ -134,7 +134,7 @@ Bucket reduce(List buckets, ReduceContext context) { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - String keyAsString = format.format(key); + String keyAsString = format.format(key).toString(); if (keyed) { builder.startObject(keyAsString); } else { diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalBinaryRange.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalBinaryRange.java index e7a3c35231c1d..afa3be702cc33 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalBinaryRange.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalBinaryRange.java @@ -155,7 +155,7 @@ public Object getFrom() { @Override public String getFromAsString() { - return from == null ? null : format.format(from); + return from == null ? null : format.format(from).toString(); } @Override @@ -165,7 +165,7 @@ public Object getTo() { @Override public String getToAsString() { - return to == null ? null : format.format(to); + return to == null ? null : format.format(to).toString(); } @Override diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalRange.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalRange.java index 9485d534ab9fa..cde90727e49fc 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalRange.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/range/InternalRange.java @@ -98,7 +98,7 @@ public String getFromAsString() { if (Double.isInfinite(from)) { return null; } else { - return format.format(from); + return format.format(from).toString(); } } @@ -107,7 +107,7 @@ public String getToAsString() { if (Double.isInfinite(to)) { return null; } else { - return format.format(to); + return format.format(to).toString(); } } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTerms.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTerms.java index 29a8e16d9b942..2d22b61472a5a 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTerms.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantLongTerms.java @@ -78,7 +78,7 @@ public Object getKey() { @Override public String getKeyAsString() { - return format.format(term); + return format.format(term).toString(); } @Override diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTerms.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTerms.java index b170171f5cfad..a73ee1818cf6a 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTerms.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantStringTerms.java @@ -83,7 +83,7 @@ public Number getKeyAsNumber() { @Override public String getKeyAsString() { - return format.format(termBytes); + return format.format(termBytes).toString(); } @Override diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTermsAggregatorFactory.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTermsAggregatorFactory.java index da87e59a6fdfd..abdc195b514a1 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTermsAggregatorFactory.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTermsAggregatorFactory.java @@ -153,12 +153,12 @@ private long getBackgroundFrequency(String value) throws IOException { } public long getBackgroundFrequency(BytesRef termBytes) throws IOException { - String value = config.format().format(termBytes); + String value = config.format().format(termBytes).toString(); return getBackgroundFrequency(value); } public long getBackgroundFrequency(long termNum) throws IOException { - String value = config.format().format(termNum); + String value = config.format().format(termNum).toString(); return getBackgroundFrequency(value); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTextAggregatorFactory.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTextAggregatorFactory.java index 688acfae413ca..c35b0bfd2d095 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTextAggregatorFactory.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/significant/SignificantTextAggregatorFactory.java @@ -135,7 +135,7 @@ private long getBackgroundFrequency(String value) throws IOException { } public long getBackgroundFrequency(BytesRef termBytes) throws IOException { - String value = format.format(termBytes); + String value = format.format(termBytes).toString(); return getBackgroundFrequency(value); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/DoubleTerms.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/DoubleTerms.java index b87ce51c4bd25..65e684e315027 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/DoubleTerms.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/DoubleTerms.java @@ -63,7 +63,7 @@ protected void writeTermTo(StreamOutput out) throws IOException { @Override public String getKeyAsString() { - return format.format(term); + return format.format(term).toString(); } @Override @@ -90,7 +90,7 @@ Bucket newBucket(long docCount, InternalAggregations aggs, long docCountError) { protected final XContentBuilder keyToXContent(XContentBuilder builder) throws IOException { builder.field(CommonFields.KEY.getPreferredName(), term); if (format != DocValueFormat.RAW) { - builder.field(CommonFields.KEY_AS_STRING.getPreferredName(), format.format(term)); + builder.field(CommonFields.KEY_AS_STRING.getPreferredName(), format.format(term).toString()); } return builder; } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTerms.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTerms.java index 025c397d3bd00..7857df8713b6e 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTerms.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/LongTerms.java @@ -63,7 +63,7 @@ protected void writeTermTo(StreamOutput out) throws IOException { @Override public String getKeyAsString() { - return format.format(term); + return format.format(term).toString(); } @Override @@ -90,7 +90,7 @@ Bucket newBucket(long docCount, InternalAggregations aggs, long docCountError) { protected final XContentBuilder keyToXContent(XContentBuilder builder) throws IOException { builder.field(CommonFields.KEY.getPreferredName(), term); if (format != DocValueFormat.RAW) { - builder.field(CommonFields.KEY_AS_STRING.getPreferredName(), format.format(term)); + builder.field(CommonFields.KEY_AS_STRING.getPreferredName(), format.format(term).toString()); } return builder; } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/StringTerms.java b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/StringTerms.java index 11d0b40c7ce39..4971f74f03dc5 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/StringTerms.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/bucket/terms/StringTerms.java @@ -80,7 +80,7 @@ public Number getKeyAsNumber() { @Override public String getKeyAsString() { - return format.format(termBytes); + return format.format(termBytes).toString(); } @Override diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalNumericMetricsAggregation.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalNumericMetricsAggregation.java index dba16397fc050..b3439671580e7 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalNumericMetricsAggregation.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/InternalNumericMetricsAggregation.java @@ -48,7 +48,7 @@ protected SingleValue(StreamInput in) throws IOException { @Override public String getValueAsString() { - return format.format(value()); + return format.format(value()).toString(); } @Override @@ -79,7 +79,7 @@ protected MultiValue(StreamInput in) throws IOException { public abstract double value(String name); public String valueAsString(String name) { - return format.format(value(name)); + return format.format(value(name)).toString(); } @Override diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/avg/InternalAvg.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/avg/InternalAvg.java index c30574c576de8..285ea469aed9e 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/avg/InternalAvg.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/avg/InternalAvg.java @@ -113,7 +113,7 @@ public InternalAvg doReduce(List aggregations, ReduceContex public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { builder.field(CommonFields.VALUE.getPreferredName(), count != 0 ? getValue() : null); if (count != 0 && format != DocValueFormat.RAW) { - builder.field(CommonFields.VALUE_AS_STRING.getPreferredName(), format.format(getValue())); + builder.field(CommonFields.VALUE_AS_STRING.getPreferredName(), format.format(getValue()).toString()); } return builder; } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/max/InternalMax.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/max/InternalMax.java index 112d379362700..449351b88b169 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/max/InternalMax.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/max/InternalMax.java @@ -85,7 +85,7 @@ public XContentBuilder doXContentBody(XContentBuilder builder, Params params) th boolean hasValue = !Double.isInfinite(max); builder.field(CommonFields.VALUE.getPreferredName(), hasValue ? max : null); if (hasValue && format != DocValueFormat.RAW) { - builder.field(CommonFields.VALUE_AS_STRING.getPreferredName(), format.format(max)); + builder.field(CommonFields.VALUE_AS_STRING.getPreferredName(), format.format(max).toString()); } return builder; } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/min/InternalMin.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/min/InternalMin.java index dcf180dde89a4..886642c222baf 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/min/InternalMin.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/min/InternalMin.java @@ -85,7 +85,7 @@ public XContentBuilder doXContentBody(XContentBuilder builder, Params params) th boolean hasValue = !Double.isInfinite(min); builder.field(CommonFields.VALUE.getPreferredName(), hasValue ? min : null); if (hasValue && format != DocValueFormat.RAW) { - builder.field(CommonFields.VALUE_AS_STRING.getPreferredName(), format.format(min)); + builder.field(CommonFields.VALUE_AS_STRING.getPreferredName(), format.format(min).toString()); } return builder; } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/hdr/AbstractInternalHDRPercentiles.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/hdr/AbstractInternalHDRPercentiles.java index 2ff3e6d3becb2..48d35de6cb6ab 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/hdr/AbstractInternalHDRPercentiles.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/hdr/AbstractInternalHDRPercentiles.java @@ -137,7 +137,7 @@ public XContentBuilder doXContentBody(XContentBuilder builder, Params params) th builder.field(CommonFields.KEY.getPreferredName(), keys[i]); builder.field(CommonFields.VALUE.getPreferredName(), value); if (format != DocValueFormat.RAW) { - builder.field(CommonFields.VALUE_AS_STRING.getPreferredName(), format.format(value)); + builder.field(CommonFields.VALUE_AS_STRING.getPreferredName(), format.format(value).toString()); } builder.endObject(); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/tdigest/AbstractInternalTDigestPercentiles.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/tdigest/AbstractInternalTDigestPercentiles.java index 143775b5bfd15..3806d7feb9550 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/tdigest/AbstractInternalTDigestPercentiles.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/percentiles/tdigest/AbstractInternalTDigestPercentiles.java @@ -120,7 +120,7 @@ public XContentBuilder doXContentBody(XContentBuilder builder, Params params) th builder.field(CommonFields.KEY.getPreferredName(), keys[i]); builder.field(CommonFields.VALUE.getPreferredName(), value); if (format != DocValueFormat.RAW) { - builder.field(CommonFields.VALUE_AS_STRING.getPreferredName(), format.format(value)); + builder.field(CommonFields.VALUE_AS_STRING.getPreferredName(), format.format(value).toString()); } builder.endObject(); } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/sum/InternalSum.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/sum/InternalSum.java index fb64d168db6aa..cedcdd4aab07d 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/sum/InternalSum.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/sum/InternalSum.java @@ -95,7 +95,7 @@ public InternalSum doReduce(List aggregations, ReduceContex public XContentBuilder doXContentBody(XContentBuilder builder, Params params) throws IOException { builder.field(CommonFields.VALUE.getPreferredName(), sum); if (format != DocValueFormat.RAW) { - builder.field(CommonFields.VALUE_AS_STRING.getPreferredName(), format.format(sum)); + builder.field(CommonFields.VALUE_AS_STRING.getPreferredName(), format.format(sum).toString()); } return builder; } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregationBuilder.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregationBuilder.java index a528814e2891d..836bd7fc9ff03 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregationBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregationBuilder.java @@ -39,6 +39,7 @@ import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.builder.SearchSourceBuilder.ScriptField; import org.elasticsearch.search.fetch.StoredFieldsContext; +import org.elasticsearch.search.fetch.subphase.DocValueFieldsContext.FieldAndFormat; import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import org.elasticsearch.search.fetch.subphase.ScriptFieldsContext; import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder; @@ -70,7 +71,7 @@ public class TopHitsAggregationBuilder extends AbstractAggregationBuilder> sorts = null; private HighlightBuilder highlightBuilder; private StoredFieldsContext storedFieldsContext; - private List fieldDataFields; + private List docValueFields; private Set scriptFields; private FetchSourceContext fetchSourceContext; @@ -91,7 +92,7 @@ protected TopHitsAggregationBuilder(TopHitsAggregationBuilder clone, new HighlightBuilder(clone.highlightBuilder, clone.highlightBuilder.highlightQuery(), clone.highlightBuilder.fields()); this.storedFieldsContext = clone.storedFieldsContext == null ? null : new StoredFieldsContext(clone.storedFieldsContext); - this.fieldDataFields = clone.fieldDataFields == null ? null : new ArrayList<>(clone.fieldDataFields); + this.docValueFields = clone.docValueFields == null ? null : new ArrayList<>(clone.docValueFields); this.scriptFields = clone.scriptFields == null ? null : new HashSet<>(clone.scriptFields); this.fetchSourceContext = clone.fetchSourceContext == null ? null : new FetchSourceContext(clone.fetchSourceContext.fetchSource(), clone.fetchSourceContext.includes(), @@ -112,9 +113,9 @@ public TopHitsAggregationBuilder(StreamInput in) throws IOException { fetchSourceContext = in.readOptionalWriteable(FetchSourceContext::new); if (in.readBoolean()) { int size = in.readVInt(); - fieldDataFields = new ArrayList<>(size); + docValueFields = new ArrayList<>(size); for (int i = 0; i < size; i++) { - fieldDataFields.add(in.readString()); + docValueFields.add(new FieldAndFormat(in)); } } storedFieldsContext = in.readOptionalWriteable(StoredFieldsContext::new); @@ -143,12 +144,12 @@ public TopHitsAggregationBuilder(StreamInput in) throws IOException { protected void doWriteTo(StreamOutput out) throws IOException { out.writeBoolean(explain); out.writeOptionalWriteable(fetchSourceContext); - boolean hasFieldDataFields = fieldDataFields != null; + boolean hasFieldDataFields = docValueFields != null; out.writeBoolean(hasFieldDataFields); if (hasFieldDataFields) { - out.writeVInt(fieldDataFields.size()); - for (String fieldName : fieldDataFields) { - out.writeString(fieldName); + out.writeVInt(docValueFields.size()); + for (FieldAndFormat ff : docValueFields) { + ff.writeTo(out); } } out.writeOptionalWriteable(storedFieldsContext); @@ -404,40 +405,33 @@ public StoredFieldsContext storedFields() { } /** - * Adds a field to load from the field data cache and return as part of + * Adds a field to load from doc values and return as part of * the search request. */ - public TopHitsAggregationBuilder fieldDataField(String fieldDataField) { - if (fieldDataField == null) { - throw new IllegalArgumentException("[fieldDataField] must not be null: [" + name + "]"); + public TopHitsAggregationBuilder docValueField(String docValueField, String format) { + if (docValueField == null) { + throw new IllegalArgumentException("[docValueField] must not be null: [" + name + "]"); } - if (fieldDataFields == null) { - fieldDataFields = new ArrayList<>(); + if (docValueFields == null) { + docValueFields = new ArrayList<>(); } - fieldDataFields.add(fieldDataField); + docValueFields.add(new FieldAndFormat(docValueField, format)); return this; } /** - * Adds fields to load from the field data cache and return as part of + * Adds a field to load from doc values and return as part of * the search request. */ - public TopHitsAggregationBuilder fieldDataFields(List fieldDataFields) { - if (fieldDataFields == null) { - throw new IllegalArgumentException("[fieldDataFields] must not be null: [" + name + "]"); - } - if (this.fieldDataFields == null) { - this.fieldDataFields = new ArrayList<>(); - } - this.fieldDataFields.addAll(fieldDataFields); - return this; + public TopHitsAggregationBuilder docValueField(String docValueField) { + return docValueField(docValueField, null); } /** * Gets the field-data fields. */ - public List fieldDataFields() { - return fieldDataFields; + public List fieldDataFields() { + return docValueFields; } /** @@ -587,7 +581,7 @@ protected TopHitsAggregatorFactory doBuild(SearchContext context, AggregatorFact optionalSort = SortBuilder.buildSort(sorts, context.getQueryShardContext()); } return new TopHitsAggregatorFactory(name, from, size, explain, version, trackScores, optionalSort, highlightBuilder, - storedFieldsContext, fieldDataFields, fields, fetchSourceContext, context, parent, subfactoriesBuilder, metaData); + storedFieldsContext, docValueFields, fields, fetchSourceContext, context, parent, subfactoriesBuilder, metaData); } @Override @@ -603,10 +597,15 @@ protected XContentBuilder internalXContent(XContentBuilder builder, Params param if (storedFieldsContext != null) { storedFieldsContext.toXContent(SearchSourceBuilder.STORED_FIELDS_FIELD.getPreferredName(), builder); } - if (fieldDataFields != null) { + if (docValueFields != null) { builder.startArray(SearchSourceBuilder.DOCVALUE_FIELDS_FIELD.getPreferredName()); - for (String fieldDataField : fieldDataFields) { - builder.value(fieldDataField); + for (FieldAndFormat dvField : docValueFields) { + builder.startObject() + .field("field", dvField.field); + if (dvField.format != null) { + builder.field("format", dvField.format); + } + builder.endObject(); } builder.endArray(); } @@ -725,14 +724,9 @@ public static TopHitsAggregationBuilder parse(String aggregationName, XContentPa } else if (SearchSourceBuilder.DOCVALUE_FIELDS_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { List fieldDataFields = new ArrayList<>(); while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { - if (token == XContentParser.Token.VALUE_STRING) { - fieldDataFields.add(parser.text()); - } else { - throw new ParsingException(parser.getTokenLocation(), "Expected [" + XContentParser.Token.VALUE_STRING - + "] in [" + currentFieldName + "] but found [" + token + "]", parser.getTokenLocation()); - } + FieldAndFormat ff = FieldAndFormat.fromXContent(parser); + factory.docValueField(ff.field, ff.format); } - factory.fieldDataFields(fieldDataFields); } else if (SearchSourceBuilder.SORT_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { List> sorts = SortBuilder.fromXContent(parser); factory.sorts(sorts); @@ -752,7 +746,7 @@ public static TopHitsAggregationBuilder parse(String aggregationName, XContentPa @Override protected int doHashCode() { - return Objects.hash(explain, fetchSourceContext, fieldDataFields, storedFieldsContext, from, highlightBuilder, + return Objects.hash(explain, fetchSourceContext, docValueFields, storedFieldsContext, from, highlightBuilder, scriptFields, size, sorts, trackScores, version); } @@ -761,7 +755,7 @@ protected boolean doEquals(Object obj) { TopHitsAggregationBuilder other = (TopHitsAggregationBuilder) obj; return Objects.equals(explain, other.explain) && Objects.equals(fetchSourceContext, other.fetchSourceContext) - && Objects.equals(fieldDataFields, other.fieldDataFields) + && Objects.equals(docValueFields, other.docValueFields) && Objects.equals(storedFieldsContext, other.storedFieldsContext) && Objects.equals(from, other.from) && Objects.equals(highlightBuilder, other.highlightBuilder) diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregatorFactory.java b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregatorFactory.java index 09c26b169e528..416c984610503 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregatorFactory.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregatorFactory.java @@ -25,6 +25,7 @@ import org.elasticsearch.search.aggregations.pipeline.PipelineAggregator; import org.elasticsearch.search.fetch.StoredFieldsContext; import org.elasticsearch.search.fetch.subphase.DocValueFieldsContext; +import org.elasticsearch.search.fetch.subphase.DocValueFieldsContext.FieldAndFormat; import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import org.elasticsearch.search.fetch.subphase.ScriptFieldsContext; import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder; @@ -47,13 +48,13 @@ public class TopHitsAggregatorFactory extends AggregatorFactory sort; private final HighlightBuilder highlightBuilder; private final StoredFieldsContext storedFieldsContext; - private final List docValueFields; + private final List docValueFields; private final List scriptFields; private final FetchSourceContext fetchSourceContext; TopHitsAggregatorFactory(String name, int from, int size, boolean explain, boolean version, boolean trackScores, Optional sort, HighlightBuilder highlightBuilder, StoredFieldsContext storedFieldsContext, - List docValueFields, List scriptFields, FetchSourceContext fetchSourceContext, + List docValueFields, List scriptFields, FetchSourceContext fetchSourceContext, SearchContext context, AggregatorFactory parent, AggregatorFactories.Builder subFactories, Map metaData) throws IOException { super(name, context, parent, subFactories, metaData); diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalSimpleValue.java b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalSimpleValue.java index 4fa4f1f6c8620..2eac04a9581be 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalSimpleValue.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/InternalSimpleValue.java @@ -85,7 +85,7 @@ public XContentBuilder doXContentBody(XContentBuilder builder, Params params) th boolean hasValue = !(Double.isInfinite(value) || Double.isNaN(value)); builder.field(CommonFields.VALUE.getPreferredName(), hasValue ? value : null); if (hasValue && format != DocValueFormat.RAW) { - builder.field(CommonFields.VALUE_AS_STRING.getPreferredName(), format.format(value)); + builder.field(CommonFields.VALUE_AS_STRING.getPreferredName(), format.format(value).toString()); } return builder; } diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/bucketmetrics/InternalBucketMetricValue.java b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/bucketmetrics/InternalBucketMetricValue.java index 76a240d317839..a7ef024028f60 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/bucketmetrics/InternalBucketMetricValue.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/bucketmetrics/InternalBucketMetricValue.java @@ -108,7 +108,7 @@ public XContentBuilder doXContentBody(XContentBuilder builder, Params params) th boolean hasValue = !Double.isInfinite(value); builder.field(CommonFields.VALUE.getPreferredName(), hasValue ? value : null); if (hasValue && format != DocValueFormat.RAW) { - builder.field(CommonFields.VALUE_AS_STRING.getPreferredName(), format.format(value)); + builder.field(CommonFields.VALUE_AS_STRING.getPreferredName(), format.format(value).toString()); } builder.startArray(KEYS_FIELD.getPreferredName()); for (String key : keys) { diff --git a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/bucketmetrics/percentile/InternalPercentilesBucket.java b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/bucketmetrics/percentile/InternalPercentilesBucket.java index 42a8b28a51a9f..5d13638f70a34 100644 --- a/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/bucketmetrics/percentile/InternalPercentilesBucket.java +++ b/server/src/main/java/org/elasticsearch/search/aggregations/pipeline/bucketmetrics/percentile/InternalPercentilesBucket.java @@ -97,7 +97,7 @@ public double percentile(double percent) throws IllegalArgumentException { @Override public String percentileAsString(double percent) { - return format.format(percentile(percent)); + return format.format(percentile(percent)).toString(); } DocValueFormat formatter() { diff --git a/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java b/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java index c4a6b3da6b1d4..c42a1a12a1877 100644 --- a/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java @@ -47,6 +47,7 @@ import org.elasticsearch.search.aggregations.PipelineAggregationBuilder; import org.elasticsearch.search.collapse.CollapseBuilder; import org.elasticsearch.search.fetch.StoredFieldsContext; +import org.elasticsearch.search.fetch.subphase.DocValueFieldsContext.FieldAndFormat; import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder; import org.elasticsearch.search.internal.SearchContext; @@ -64,6 +65,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; import static org.elasticsearch.index.query.AbstractQueryBuilder.parseInnerQueryBuilder; @@ -162,7 +164,7 @@ public static HighlightBuilder highlight() { private int terminateAfter = SearchContext.DEFAULT_TERMINATE_AFTER; private StoredFieldsContext storedFieldsContext; - private List docValueFields; + private List docValueFields; private List scriptFields; private FetchSourceContext fetchSourceContext; @@ -197,7 +199,22 @@ public SearchSourceBuilder(StreamInput in) throws IOException { aggregations = in.readOptionalWriteable(AggregatorFactories.Builder::new); explain = in.readOptionalBoolean(); fetchSourceContext = in.readOptionalWriteable(FetchSourceContext::new); - docValueFields = (List) in.readGenericValue(); + if (in.getVersion().before(Version.V_6_4_0)) { + List dvFields = (List) in.readGenericValue(); + if (dvFields == null) { + docValueFields = null; + } else { + docValueFields = dvFields.stream() + .map(field -> new FieldAndFormat(field, null)) + .collect(Collectors.toList()); + } + } else { + if (in.readBoolean()) { + docValueFields = in.readList(FieldAndFormat::new); + } else { + docValueFields = null; + } + } storedFieldsContext = in.readOptionalWriteable(StoredFieldsContext::new); from = in.readVInt(); highlightBuilder = in.readOptionalWriteable(HighlightBuilder::new); @@ -246,7 +263,16 @@ public void writeTo(StreamOutput out) throws IOException { out.writeOptionalWriteable(aggregations); out.writeOptionalBoolean(explain); out.writeOptionalWriteable(fetchSourceContext); - out.writeGenericValue(docValueFields); + if (out.getVersion().before(Version.V_6_4_0)) { + out.writeGenericValue(docValueFields == null + ? null + : docValueFields.stream().map(ff -> ff.field).collect(Collectors.toList())); + } else { + out.writeBoolean(docValueFields != null); + if (docValueFields != null) { + out.writeList(docValueFields); + } + } out.writeOptionalWriteable(storedFieldsContext); out.writeVInt(from); out.writeOptionalWriteable(highlightBuilder); @@ -764,22 +790,30 @@ public StoredFieldsContext storedFields() { /** * Gets the docvalue fields. */ - public List docValueFields() { + public List docValueFields() { return docValueFields; } /** - * Adds a field to load from the docvalue and return as part of the + * Adds a field to load from the doc values and return as part of the * search request. */ - public SearchSourceBuilder docValueField(String name) { + public SearchSourceBuilder docValueField(String name, @Nullable String format) { if (docValueFields == null) { docValueFields = new ArrayList<>(); } - docValueFields.add(name); + docValueFields.add(new FieldAndFormat(name, format)); return this; } + /** + * Adds a field to load from the doc values and return as part of the + * search request. + */ + public SearchSourceBuilder docValueField(String name) { + return docValueField(name, null); + } + /** * Adds a script field under the given name with the provided script. * @@ -1076,12 +1110,7 @@ public void parseXContent(XContentParser parser, boolean checkTrailingTokens) th } else if (DOCVALUE_FIELDS_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { docValueFields = new ArrayList<>(); while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { - if (token == XContentParser.Token.VALUE_STRING) { - docValueFields.add(parser.text()); - } else { - throw new ParsingException(parser.getTokenLocation(), "Expected [" + XContentParser.Token.VALUE_STRING + - "] in [" + currentFieldName + "] but found [" + token + "]", parser.getTokenLocation()); - } + docValueFields.add(FieldAndFormat.fromXContent(parser)); } } else if (INDICES_BOOST_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { @@ -1177,8 +1206,13 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws if (docValueFields != null) { builder.startArray(DOCVALUE_FIELDS_FIELD.getPreferredName()); - for (String docValueField : docValueFields) { - builder.value(docValueField); + for (FieldAndFormat docValueField : docValueFields) { + builder.startObject() + .field("field", docValueField.field); + if (docValueField.format != null) { + builder.field("format", docValueField.format); + } + builder.endObject(); } builder.endArray(); } diff --git a/server/src/main/java/org/elasticsearch/search/fetch/subphase/DocValueFieldsContext.java b/server/src/main/java/org/elasticsearch/search/fetch/subphase/DocValueFieldsContext.java index 325d28e459282..cf1596fd326b9 100644 --- a/server/src/main/java/org/elasticsearch/search/fetch/subphase/DocValueFieldsContext.java +++ b/server/src/main/java/org/elasticsearch/search/fetch/subphase/DocValueFieldsContext.java @@ -18,23 +18,111 @@ */ package org.elasticsearch.search.fetch.subphase; +import org.elasticsearch.Version; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContent; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentParser.Token; + +import java.io.IOException; import java.util.List; +import java.util.Objects; /** * All the required context to pull a field from the doc values. */ public class DocValueFieldsContext { - private final List fields; + public static final String USE_DEFAULT_FORMAT = "use_field_mapping"; + + /** + * Wrapper around a field name and the format that should be used to + * display values of this field. + */ + public static final class FieldAndFormat implements Writeable { + + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("script", + a -> new FieldAndFormat((String) a[0], (String) a[1])); + static { + PARSER.declareString(ConstructingObjectParser.constructorArg(), new ParseField("field")); + PARSER.declareStringOrNull(ConstructingObjectParser.optionalConstructorArg(), new ParseField("format")); + } + + /** + * Parse a {@link FieldAndFormat} from some {@link XContent}. + */ + public static FieldAndFormat fromXContent(XContentParser parser) throws IOException { + Token token = parser.currentToken(); + if (token.isValue()) { + return new FieldAndFormat(parser.text(), null); + } else { + return PARSER.apply(parser, null); + } + } + + /** The name of the field. */ + public final String field; + + /** The format of the field, or {@code null} if defaults should be used. */ + public final String format; + + /** Sole constructor. */ + public FieldAndFormat(String field, @Nullable String format) { + this.field = Objects.requireNonNull(field); + this.format = format; + } + + /** Serialization constructor. */ + public FieldAndFormat(StreamInput in) throws IOException { + this.field = in.readString(); + if (in.getVersion().onOrAfter(Version.V_6_4_0)) { + format = in.readOptionalString(); + } else { + format = null; + } + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(field); + if (out.getVersion().onOrAfter(Version.V_6_4_0)) { + out.writeOptionalString(format); + } + } + + @Override + public int hashCode() { + int h = field.hashCode(); + h = 31 * h + Objects.hashCode(format); + return h; + } + + @Override + public boolean equals(Object obj) { + if (obj == null || getClass() != obj.getClass()) { + return false; + } + FieldAndFormat other = (FieldAndFormat) obj; + return field.equals(other.field) && Objects.equals(format, other.format); + } + + } + + private final List fields; - public DocValueFieldsContext(List fields) { + public DocValueFieldsContext(List fields) { this.fields = fields; } /** * Returns the required docvalue fields */ - public List fields() { + public List fields() { return this.fields; } } diff --git a/server/src/main/java/org/elasticsearch/search/fetch/subphase/DocValueFieldsFetchSubPhase.java b/server/src/main/java/org/elasticsearch/search/fetch/subphase/DocValueFieldsFetchSubPhase.java index 60def08c89104..a1562e118fb86 100644 --- a/server/src/main/java/org/elasticsearch/search/fetch/subphase/DocValueFieldsFetchSubPhase.java +++ b/server/src/main/java/org/elasticsearch/search/fetch/subphase/DocValueFieldsFetchSubPhase.java @@ -20,19 +20,32 @@ import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.index.ReaderUtil; +import org.apache.lucene.index.SortedNumericDocValues; import org.elasticsearch.common.document.DocumentField; +import org.elasticsearch.common.logging.DeprecationLogger; +import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.index.fielddata.AtomicFieldData; +import org.elasticsearch.index.fielddata.AtomicNumericFieldData; +import org.elasticsearch.index.fielddata.IndexFieldData; +import org.elasticsearch.index.fielddata.IndexNumericFieldData; import org.elasticsearch.index.fielddata.ScriptDocValues; +import org.elasticsearch.index.fielddata.SortedBinaryDocValues; +import org.elasticsearch.index.fielddata.SortedNumericDoubleValues; import org.elasticsearch.index.mapper.MappedFieldType; +import org.elasticsearch.search.DocValueFormat; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.fetch.FetchSubPhase; +import org.elasticsearch.search.fetch.subphase.DocValueFieldsContext.FieldAndFormat; import org.elasticsearch.search.internal.SearchContext; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; +import java.util.List; +import java.util.Objects; /** * Query sub phase which pulls data from doc values @@ -41,6 +54,8 @@ */ public final class DocValueFieldsFetchSubPhase implements FetchSubPhase { + private static final DeprecationLogger DEPRECATION_LOGGER = new DeprecationLogger(Loggers.getLogger(DocValueFieldsFetchSubPhase.class)); + @Override public void hitsExecute(SearchContext context, SearchHit[] hits) throws IOException { @@ -48,9 +63,10 @@ public void hitsExecute(SearchContext context, SearchHit[] hits) throws IOExcept // retrieve the `doc_value` associated with the collapse field String name = context.collapse().getFieldType().name(); if (context.docValueFieldsContext() == null) { - context.docValueFieldsContext(new DocValueFieldsContext(Collections.singletonList(name))); - } else if (context.docValueFieldsContext().fields().contains(name) == false) { - context.docValueFieldsContext().fields().add(name); + context.docValueFieldsContext(new DocValueFieldsContext( + Collections.singletonList(new FieldAndFormat(name, DocValueFieldsContext.USE_DEFAULT_FORMAT)))); + } else if (context.docValueFieldsContext().fields().stream().map(ff -> ff.field).anyMatch(name::equals) == false) { + context.docValueFieldsContext().fields().add(new FieldAndFormat(name, DocValueFieldsContext.USE_DEFAULT_FORMAT)); } } @@ -59,24 +75,51 @@ public void hitsExecute(SearchContext context, SearchHit[] hits) throws IOExcept } hits = hits.clone(); // don't modify the incoming hits - Arrays.sort(hits, (a, b) -> Integer.compare(a.docId(), b.docId())); + Arrays.sort(hits, Comparator.comparingInt(SearchHit::docId)); - for (String field : context.docValueFieldsContext().fields()) { + for (FieldAndFormat fieldAndFormat : context.docValueFieldsContext().fields()) { + String field = fieldAndFormat.field; MappedFieldType fieldType = context.mapperService().fullName(field); if (fieldType != null) { + final IndexFieldData indexFieldData = context.getForField(fieldType); + final DocValueFormat format; + if (fieldAndFormat.format == null) { + DEPRECATION_LOGGER.deprecated("Doc-value field [" + fieldAndFormat.field + "] is not using a format. The output will " + + "change in 7.0 when doc value fields get formatted based on mappings by default. It is recommended to pass " + + "[format={}] with the doc value field in order to opt in for the future behaviour and ease the migration to " + + "7.0.", DocValueFieldsContext.USE_DEFAULT_FORMAT); + format = null; + } else { + String formatDesc = fieldAndFormat.format; + if (Objects.equals(formatDesc, DocValueFieldsContext.USE_DEFAULT_FORMAT)) { + formatDesc = null; + } + format = fieldType.docValueFormat(formatDesc, null); + } LeafReaderContext subReaderContext = null; AtomicFieldData data = null; - ScriptDocValues values = null; + ScriptDocValues scriptValues = null; // legacy + SortedBinaryDocValues binaryValues = null; // binary / string / ip fields + SortedNumericDocValues longValues = null; // int / date fields + SortedNumericDoubleValues doubleValues = null; // floating-point fields for (SearchHit hit : hits) { // if the reader index has changed we need to get a new doc values reader instance if (subReaderContext == null || hit.docId() >= subReaderContext.docBase + subReaderContext.reader().maxDoc()) { int readerIndex = ReaderUtil.subIndex(hit.docId(), context.searcher().getIndexReader().leaves()); subReaderContext = context.searcher().getIndexReader().leaves().get(readerIndex); - data = context.getForField(fieldType).load(subReaderContext); - values = data.getScriptValues(); + data = indexFieldData.load(subReaderContext); + if (format == null) { + scriptValues = data.getScriptValues(); + } else if (indexFieldData instanceof IndexNumericFieldData) { + if (((IndexNumericFieldData) indexFieldData).getNumericType().isFloatingPoint()) { + doubleValues = ((AtomicNumericFieldData) data).getDoubleValues(); + } else { + longValues = ((AtomicNumericFieldData) data).getLongValues(); + } + } else { + binaryValues = data.getBytesValues(); + } } - int subDocId = hit.docId() - subReaderContext.docBase; - values.setNextDocId(subDocId); if (hit.fieldsOrNull() == null) { hit.fields(new HashMap<>(2)); } @@ -85,7 +128,33 @@ public void hitsExecute(SearchContext context, SearchHit[] hits) throws IOExcept hitField = new DocumentField(field, new ArrayList<>(2)); hit.getFields().put(field, hitField); } - hitField.getValues().addAll(values); + final List values = hitField.getValues(); + + int subDocId = hit.docId() - subReaderContext.docBase; + if (scriptValues != null) { + scriptValues.setNextDocId(subDocId); + values.addAll(scriptValues); + } else if (binaryValues != null) { + if (binaryValues.advanceExact(subDocId)) { + for (int i = 0, count = binaryValues.docValueCount(); i < count; ++i) { + values.add(format.format(binaryValues.nextValue())); + } + } + } else if (longValues != null) { + if (longValues.advanceExact(subDocId)) { + for (int i = 0, count = longValues.docValueCount(); i < count; ++i) { + values.add(format.format(longValues.nextValue())); + } + } + } else if (doubleValues != null) { + if (doubleValues.advanceExact(subDocId)) { + for (int i = 0, count = doubleValues.docValueCount(); i < count; ++i) { + values.add(format.format(doubleValues.nextValue())); + } + } + } else { + throw new AssertionError("Unreachable code"); + } } } } diff --git a/server/src/test/java/org/elasticsearch/action/admin/indices/settings/put/UpdateSettingsRequestStreamableTests.java b/server/src/test/java/org/elasticsearch/action/admin/indices/settings/put/UpdateSettingsRequestStreamableTests.java index 114af3c13e707..e8dd3943cb762 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/indices/settings/put/UpdateSettingsRequestStreamableTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/indices/settings/put/UpdateSettingsRequestStreamableTests.java @@ -24,6 +24,7 @@ import org.elasticsearch.common.settings.Settings.Builder; import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.test.AbstractStreamableTestCase; +import org.elasticsearch.test.ESTestCase; import java.util.ArrayList; import java.util.Arrays; @@ -38,8 +39,9 @@ public class UpdateSettingsRequestStreamableTests extends AbstractStreamableTest protected UpdateSettingsRequest mutateInstance(UpdateSettingsRequest request) { UpdateSettingsRequest mutation = copyRequest(request); List mutators = new ArrayList<>(); - mutators.add(() -> mutation.masterNodeTimeout(randomTimeValue())); - mutators.add(() -> mutation.timeout(randomTimeValue())); + mutators.add(() -> mutation + .masterNodeTimeout(randomValueOtherThan(request.masterNodeTimeout().getStringRep(), ESTestCase::randomTimeValue))); + mutators.add(() -> mutation.timeout(randomValueOtherThan(request.masterNodeTimeout().getStringRep(), ESTestCase::randomTimeValue))); mutators.add(() -> mutation.settings(mutateSettings(request.settings()))); mutators.add(() -> mutation.indices(mutateIndices(request.indices()))); mutators.add(() -> mutation.indicesOptions(randomValueOtherThan(request.indicesOptions(), @@ -72,7 +74,7 @@ public static UpdateSettingsRequest createTestItem() { private static UpdateSettingsRequest copyRequest(UpdateSettingsRequest request) { UpdateSettingsRequest result = new UpdateSettingsRequest(request.settings(), request.indices()); - result.masterNodeTimeout(request.timeout()); + result.masterNodeTimeout(request.masterNodeTimeout()); result.timeout(request.timeout()); result.indicesOptions(request.indicesOptions()); result.setPreserveExisting(request.isPreserveExisting()); diff --git a/server/src/test/java/org/elasticsearch/cluster/ack/AckClusterUpdateSettingsIT.java b/server/src/test/java/org/elasticsearch/cluster/ack/AckClusterUpdateSettingsIT.java index ab3f82fff75f5..a11ceddf28788 100644 --- a/server/src/test/java/org/elasticsearch/cluster/ack/AckClusterUpdateSettingsIT.java +++ b/server/src/test/java/org/elasticsearch/cluster/ack/AckClusterUpdateSettingsIT.java @@ -23,6 +23,7 @@ import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsResponse; import org.elasticsearch.action.admin.indices.close.CloseIndexResponse; +import org.elasticsearch.action.admin.indices.create.CreateIndexResponse; import org.elasticsearch.action.admin.indices.open.OpenIndexResponse; import org.elasticsearch.client.Client; import org.elasticsearch.cluster.ClusterState; @@ -33,8 +34,16 @@ import org.elasticsearch.cluster.routing.allocation.decider.ThrottlingAllocationDecider; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.discovery.DiscoverySettings; +import org.elasticsearch.discovery.zen.PublishClusterStateAction; +import org.elasticsearch.plugins.Plugin; import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.test.ESIntegTestCase.ClusterScope; +import org.elasticsearch.test.transport.MockTransportService; +import org.elasticsearch.transport.TransportService; + +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Stream; import static org.elasticsearch.test.ESIntegTestCase.Scope.TEST; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; @@ -43,6 +52,11 @@ @ClusterScope(scope = TEST, minNumDataNodes = 2) public class AckClusterUpdateSettingsIT extends ESIntegTestCase { + @Override + protected Collection> nodePlugins() { + return Arrays.asList(MockTransportService.TestPlugin.class); + } + @Override protected Settings nodeSettings(int nodeOrdinal) { return Settings.builder() @@ -156,4 +170,32 @@ public void testOpenIndexNoAcknowledgement() { assertThat(openIndexResponse.isAcknowledged(), equalTo(false)); ensureGreen("test"); // make sure that recovery from disk has completed, so that check index doesn't fail. } + + public void testAckingFailsIfNotPublishedToAllNodes() { + String masterNode = internalCluster().getMasterName(); + String nonMasterNode = Stream.of(internalCluster().getNodeNames()) + .filter(node -> node.equals(masterNode) == false).findFirst().get(); + + MockTransportService masterTransportService = + (MockTransportService) internalCluster().getInstance(TransportService.class, masterNode); + MockTransportService nonMasterTransportService = + (MockTransportService) internalCluster().getInstance(TransportService.class, nonMasterNode); + + logger.info("blocking cluster state publishing from master [{}] to non master [{}]", masterNode, nonMasterNode); + if (randomBoolean() && internalCluster().numMasterNodes() != 2) { + masterTransportService.addFailToSendNoConnectRule(nonMasterTransportService, PublishClusterStateAction.SEND_ACTION_NAME); + } else { + masterTransportService.addFailToSendNoConnectRule(nonMasterTransportService, PublishClusterStateAction.COMMIT_ACTION_NAME); + } + + CreateIndexResponse response = client().admin().indices().prepareCreate("test").get(); + assertFalse(response.isAcknowledged()); + + logger.info("waiting for cluster to reform"); + masterTransportService.clearRule(nonMasterTransportService); + + ensureStableCluster(internalCluster().size()); + + assertAcked(client().admin().indices().prepareDelete("test")); + } } diff --git a/server/src/test/java/org/elasticsearch/discovery/DiscoveryDisruptionIT.java b/server/src/test/java/org/elasticsearch/discovery/DiscoveryDisruptionIT.java index 344c5567a8657..f688291237813 100644 --- a/server/src/test/java/org/elasticsearch/discovery/DiscoveryDisruptionIT.java +++ b/server/src/test/java/org/elasticsearch/discovery/DiscoveryDisruptionIT.java @@ -53,7 +53,6 @@ import static org.elasticsearch.cluster.metadata.IndexMetaData.INDEX_NUMBER_OF_REPLICAS_SETTING; import static org.elasticsearch.cluster.metadata.IndexMetaData.INDEX_NUMBER_OF_SHARDS_SETTING; -import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.hamcrest.Matchers.greaterThanOrEqualTo; /** @@ -256,8 +255,8 @@ public void testElectMasterWithLatestVersion() throws Exception { internalCluster().setDisruptionScheme(isolatePreferredMaster); isolatePreferredMaster.startDisrupting(); - assertAcked(client(randomFrom(nonPreferredNodes)).admin().indices().prepareCreate("test").setSettings( - Settings.builder().put(INDEX_NUMBER_OF_SHARDS_SETTING.getKey(), 1).put(INDEX_NUMBER_OF_REPLICAS_SETTING.getKey(), 0))); + client(randomFrom(nonPreferredNodes)).admin().indices().prepareCreate("test").setSettings( + Settings.builder().put(INDEX_NUMBER_OF_SHARDS_SETTING.getKey(), 1).put(INDEX_NUMBER_OF_REPLICAS_SETTING.getKey(), 0)).get(); internalCluster().clearDisruptionScheme(false); internalCluster().setDisruptionScheme(isolateAllNodes); diff --git a/server/src/test/java/org/elasticsearch/discovery/ZenFaultDetectionTests.java b/server/src/test/java/org/elasticsearch/discovery/ZenFaultDetectionTests.java index f32e93bb82dbd..03c0df43591ba 100644 --- a/server/src/test/java/org/elasticsearch/discovery/ZenFaultDetectionTests.java +++ b/server/src/test/java/org/elasticsearch/discovery/ZenFaultDetectionTests.java @@ -175,17 +175,19 @@ public void testNodesFaultDetectionConnectOnDisconnect() throws InterruptedExcep final Settings pingSettings = Settings.builder() .put(FaultDetection.CONNECT_ON_NETWORK_DISCONNECT_SETTING.getKey(), shouldRetry) .put(FaultDetection.PING_INTERVAL_SETTING.getKey(), "5m").build(); - ClusterState clusterState = ClusterState.builder(new ClusterName("test")).nodes(buildNodesForA(true)).build(); + ClusterState clusterState = ClusterState.builder(new ClusterName("test")).version(randomNonNegativeLong()) + .nodes(buildNodesForA(true)).build(); NodesFaultDetection nodesFDA = new NodesFaultDetection(Settings.builder().put(settingsA).put(pingSettings).build(), - threadPool, serviceA, clusterState.getClusterName()); + threadPool, serviceA, () -> clusterState, clusterState.getClusterName()); nodesFDA.setLocalNode(nodeA); NodesFaultDetection nodesFDB = new NodesFaultDetection(Settings.builder().put(settingsB).put(pingSettings).build(), - threadPool, serviceB, clusterState.getClusterName()); + threadPool, serviceB, () -> clusterState, clusterState.getClusterName()); nodesFDB.setLocalNode(nodeB); final CountDownLatch pingSent = new CountDownLatch(1); nodesFDB.addListener(new NodesFaultDetection.Listener() { @Override public void onPingReceived(NodesFaultDetection.PingRequest pingRequest) { + assertThat(pingRequest.clusterStateVersion(), equalTo(clusterState.version())); pingSent.countDown(); } }); diff --git a/server/src/test/java/org/elasticsearch/discovery/single/SingleNodeDiscoveryIT.java b/server/src/test/java/org/elasticsearch/discovery/single/SingleNodeDiscoveryIT.java index fc284b9f5e80c..fdc36152cc895 100644 --- a/server/src/test/java/org/elasticsearch/discovery/single/SingleNodeDiscoveryIT.java +++ b/server/src/test/java/org/elasticsearch/discovery/single/SingleNodeDiscoveryIT.java @@ -150,7 +150,6 @@ public Path nodeConfigPath(int nodeOrdinal) { internalCluster().getClusterName(), configurationSource, 0, - false, "other", Arrays.asList(getTestTransportPlugin(), MockHttpTransport.TestPlugin.class), Function.identity())) { diff --git a/server/src/test/java/org/elasticsearch/index/mapper/BooleanFieldTypeTests.java b/server/src/test/java/org/elasticsearch/index/mapper/BooleanFieldTypeTests.java index d119a27f22eb5..8621e7758383c 100644 --- a/server/src/test/java/org/elasticsearch/index/mapper/BooleanFieldTypeTests.java +++ b/server/src/test/java/org/elasticsearch/index/mapper/BooleanFieldTypeTests.java @@ -36,8 +36,8 @@ public void setupProperties() { public void testValueFormat() { MappedFieldType ft = createDefaultFieldType(); - assertEquals("false", ft.docValueFormat(null, null).format(0)); - assertEquals("true", ft.docValueFormat(null, null).format(1)); + assertEquals(false, ft.docValueFormat(null, null).format(0)); + assertEquals(true, ft.docValueFormat(null, null).format(1)); } public void testValueForSearch() { diff --git a/server/src/test/java/org/elasticsearch/index/query/GeoShapeQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/GeoShapeQueryBuilderTests.java index 3282077ba6a77..6356b2122edbf 100644 --- a/server/src/test/java/org/elasticsearch/index/query/GeoShapeQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/GeoShapeQueryBuilderTests.java @@ -59,6 +59,7 @@ public class GeoShapeQueryBuilderTests extends AbstractQueryTestCase randomAlphaOfLengthBetween(1, 16))); } - innerHits.setDocValueFields(randomListStuff(16, () -> randomAlphaOfLengthBetween(1, 16))); + innerHits.setDocValueFields(randomListStuff(16, + () -> new FieldAndFormat(randomAlphaOfLengthBetween(1, 16), + randomBoolean() ? null : DocValueFieldsContext.USE_DEFAULT_FORMAT))); // Random script fields deduped on their field name. Map scriptFields = new HashMap<>(); for (SearchSourceBuilder.ScriptField field: randomListStuff(16, InnerHitBuilderTests::randomScript)) { @@ -187,9 +191,9 @@ static InnerHitBuilder mutate(InnerHitBuilder original) throws IOException { modifiers.add(() -> copy.setName(randomValueOtherThan(copy.getName(), () -> randomAlphaOfLengthBetween(1, 16)))); modifiers.add(() -> { if (randomBoolean()) { - copy.setDocValueFields(randomValueOtherThan(copy.getDocValueFields(), () -> { - return randomListStuff(16, () -> randomAlphaOfLengthBetween(1, 16)); - })); + copy.setDocValueFields(randomValueOtherThan(copy.getDocValueFields(), + () -> randomListStuff(16, () -> new FieldAndFormat(randomAlphaOfLengthBetween(1, 16), + randomBoolean() ? null : DocValueFieldsContext.USE_DEFAULT_FORMAT)))); } else { copy.addDocValueField(randomAlphaOfLengthBetween(1, 16)); } diff --git a/server/src/test/java/org/elasticsearch/search/DocValueFormatTests.java b/server/src/test/java/org/elasticsearch/search/DocValueFormatTests.java index 7bf5308eb635e..e5cfbf98b3db9 100644 --- a/server/src/test/java/org/elasticsearch/search/DocValueFormatTests.java +++ b/server/src/test/java/org/elasticsearch/search/DocValueFormatTests.java @@ -85,20 +85,20 @@ public void testSerialization() throws Exception { } public void testRawFormat() { - assertEquals("0", DocValueFormat.RAW.format(0)); - assertEquals("-1", DocValueFormat.RAW.format(-1)); - assertEquals("1", DocValueFormat.RAW.format(1)); + assertEquals(0L, DocValueFormat.RAW.format(0)); + assertEquals(-1L, DocValueFormat.RAW.format(-1)); + assertEquals(1L, DocValueFormat.RAW.format(1)); - assertEquals("0.0", DocValueFormat.RAW.format(0d)); - assertEquals("0.5", DocValueFormat.RAW.format(.5d)); - assertEquals("-1.0", DocValueFormat.RAW.format(-1d)); + assertEquals(0d, DocValueFormat.RAW.format(0d)); + assertEquals(.5d, DocValueFormat.RAW.format(.5d)); + assertEquals(-1d, DocValueFormat.RAW.format(-1d)); assertEquals("abc", DocValueFormat.RAW.format(new BytesRef("abc"))); } public void testBooleanFormat() { - assertEquals("false", DocValueFormat.BOOLEAN.format(0)); - assertEquals("true", DocValueFormat.BOOLEAN.format(1)); + assertEquals(false, DocValueFormat.BOOLEAN.format(0)); + assertEquals(true, DocValueFormat.BOOLEAN.format(1)); } public void testIpFormat() { diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalSumTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalSumTests.java index 884f9bfbe0d20..aa9d25af49e8a 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalSumTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/InternalSumTests.java @@ -37,7 +37,7 @@ public class InternalSumTests extends InternalAggregationTestCase { @Override protected InternalSum createTestInstance(String name, List pipelineAggregators, Map metaData) { double value = frequently() ? randomDouble() : randomFrom(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, Double.NaN); - DocValueFormat formatter = randomFrom(new DocValueFormat.Decimal("###.##"), DocValueFormat.BOOLEAN, DocValueFormat.RAW); + DocValueFormat formatter = randomFrom(new DocValueFormat.Decimal("###.##"), DocValueFormat.RAW); return new InternalSum(name, value, formatter, pipelineAggregators, metaData); } diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsIT.java index 4f8493c0b001f..952eb22848e1a 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsIT.java @@ -583,7 +583,7 @@ public void testFetchFeatures() { .highlighter(new HighlightBuilder().field("text")) .explain(true) .storedField("text") - .fieldDataField("field1") + .docValueField("field1") .scriptField("script", new Script(ScriptType.INLINE, MockScriptEngine.NAME, "5", Collections.emptyMap())) .fetchSource("text", null) .version(true) @@ -865,7 +865,7 @@ public void testNestedFetchFeatures() { .addAggregation( nested("to-comments", "comments").subAggregation( topHits("top-comments").size(1).highlighter(new HighlightBuilder().field(hlField)).explain(true) - .fieldDataField("comments.user") + .docValueField("comments.user") .scriptField("script", new Script(ScriptType.INLINE, MockScriptEngine.NAME, "5", Collections.emptyMap())).fetchSource("comments.message", null) .version(true).sort("comments.date", SortOrder.ASC))).get(); assertHitCount(searchResponse, 2); diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsTests.java index e2ef28480fa79..4d2331b86f2ef 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsTests.java @@ -81,7 +81,7 @@ protected final TopHitsAggregationBuilder createTestAggregatorBuilder() { if (randomBoolean()) { int fieldDataFieldsSize = randomInt(25); for (int i = 0; i < fieldDataFieldsSize; i++) { - factory.fieldDataField(randomAlphaOfLengthBetween(5, 50)); + factory.docValueField(randomAlphaOfLengthBetween(5, 50)); } } if (randomBoolean()) { diff --git a/server/src/test/java/org/elasticsearch/search/fields/SearchFieldsIT.java b/server/src/test/java/org/elasticsearch/search/fields/SearchFieldsIT.java index 36062860202a5..a8a2669ef9b4a 100644 --- a/server/src/test/java/org/elasticsearch/search/fields/SearchFieldsIT.java +++ b/server/src/test/java/org/elasticsearch/search/fields/SearchFieldsIT.java @@ -803,6 +803,65 @@ public void testFieldsPulledFromFieldData() throws Exception { assertThat(searchResponse.getHits().getAt(0).getFields().get("text_field").getValue(), equalTo("foo")); assertThat(searchResponse.getHits().getAt(0).getFields().get("keyword_field").getValue(), equalTo("foo")); assertThat(searchResponse.getHits().getAt(0).getFields().get("ip_field").getValue(), equalTo("::1")); + + builder = client().prepareSearch().setQuery(matchAllQuery()) + .addDocValueField("text_field", "use_field_mapping") + .addDocValueField("keyword_field", "use_field_mapping") + .addDocValueField("byte_field", "use_field_mapping") + .addDocValueField("short_field", "use_field_mapping") + .addDocValueField("integer_field", "use_field_mapping") + .addDocValueField("long_field", "use_field_mapping") + .addDocValueField("float_field", "use_field_mapping") + .addDocValueField("double_field", "use_field_mapping") + .addDocValueField("date_field", "use_field_mapping") + .addDocValueField("boolean_field", "use_field_mapping") + .addDocValueField("ip_field", "use_field_mapping"); + searchResponse = builder.execute().actionGet(); + + assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L)); + assertThat(searchResponse.getHits().getHits().length, equalTo(1)); + fields = new HashSet<>(searchResponse.getHits().getAt(0).getFields().keySet()); + assertThat(fields, equalTo(newHashSet("byte_field", "short_field", "integer_field", "long_field", + "float_field", "double_field", "date_field", "boolean_field", "text_field", "keyword_field", + "ip_field"))); + + assertThat(searchResponse.getHits().getAt(0).getFields().get("byte_field").getValue().toString(), equalTo("1")); + assertThat(searchResponse.getHits().getAt(0).getFields().get("short_field").getValue().toString(), equalTo("2")); + assertThat(searchResponse.getHits().getAt(0).getFields().get("integer_field").getValue(), equalTo((Object) 3L)); + assertThat(searchResponse.getHits().getAt(0).getFields().get("long_field").getValue(), equalTo((Object) 4L)); + assertThat(searchResponse.getHits().getAt(0).getFields().get("float_field").getValue(), equalTo((Object) 5.0)); + assertThat(searchResponse.getHits().getAt(0).getFields().get("double_field").getValue(), equalTo((Object) 6.0d)); + assertThat(searchResponse.getHits().getAt(0).getFields().get("date_field").getValue(), + equalTo(Joda.forPattern("dateOptionalTime").printer().print(date))); + assertThat(searchResponse.getHits().getAt(0).getFields().get("boolean_field").getValue(), equalTo((Object) true)); + assertThat(searchResponse.getHits().getAt(0).getFields().get("text_field").getValue(), equalTo("foo")); + assertThat(searchResponse.getHits().getAt(0).getFields().get("keyword_field").getValue(), equalTo("foo")); + assertThat(searchResponse.getHits().getAt(0).getFields().get("ip_field").getValue(), equalTo("::1")); + + builder = client().prepareSearch().setQuery(matchAllQuery()) + .addDocValueField("byte_field", "#.0") + .addDocValueField("short_field", "#.0") + .addDocValueField("integer_field", "#.0") + .addDocValueField("long_field", "#.0") + .addDocValueField("float_field", "#.0") + .addDocValueField("double_field", "#.0") + .addDocValueField("date_field", "epoch_millis"); + searchResponse = builder.execute().actionGet(); + + assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L)); + assertThat(searchResponse.getHits().getHits().length, equalTo(1)); + fields = new HashSet<>(searchResponse.getHits().getAt(0).getFields().keySet()); + assertThat(fields, equalTo(newHashSet("byte_field", "short_field", "integer_field", "long_field", + "float_field", "double_field", "date_field"))); + + assertThat(searchResponse.getHits().getAt(0).getFields().get("byte_field").getValue(), equalTo("1.0")); + assertThat(searchResponse.getHits().getAt(0).getFields().get("short_field").getValue(), equalTo("2.0")); + assertThat(searchResponse.getHits().getAt(0).getFields().get("integer_field").getValue(), equalTo("3.0")); + assertThat(searchResponse.getHits().getAt(0).getFields().get("long_field").getValue(), equalTo("4.0")); + assertThat(searchResponse.getHits().getAt(0).getFields().get("float_field").getValue(), equalTo("5.0")); + assertThat(searchResponse.getHits().getAt(0).getFields().get("double_field").getValue(), equalTo("6.0")); + assertThat(searchResponse.getHits().getAt(0).getFields().get("date_field").getValue(), + equalTo(Joda.forPattern("epoch_millis").printer().print(date))); } public void testScriptFields() throws Exception { diff --git a/server/src/test/java/org/elasticsearch/search/geo/GeoShapeIntegrationIT.java b/server/src/test/java/org/elasticsearch/search/geo/GeoShapeIntegrationIT.java index a8f559ce35e4c..83b795621189b 100644 --- a/server/src/test/java/org/elasticsearch/search/geo/GeoShapeIntegrationIT.java +++ b/server/src/test/java/org/elasticsearch/search/geo/GeoShapeIntegrationIT.java @@ -31,6 +31,7 @@ import org.elasticsearch.indices.IndicesService; import org.elasticsearch.test.ESIntegTestCase; +import static org.elasticsearch.index.query.QueryBuilders.geoShapeQuery; import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.hamcrest.Matchers.equalTo; @@ -121,6 +122,43 @@ public void testIgnoreMalformed() throws Exception { assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L)); } + /** + * Test that the indexed shape routing can be provided if it is required + */ + public void testIndexShapeRouting() throws Exception { + String mapping = "{\n" + + " \"_routing\": {\n" + + " \"required\": true\n" + + " },\n" + + " \"properties\": {\n" + + " \"shape\": {\n" + + " \"type\": \"geo_shape\"\n" + + " }\n" + + " }\n" + + " }"; + + + // create index + assertAcked(client().admin().indices().prepareCreate("test").addMapping("doc", mapping, XContentType.JSON).get()); + ensureGreen(); + + String source = "{\n" + + " \"shape\" : {\n" + + " \"type\" : \"circle\",\n" + + " \"coordinates\" : [-45.0, 45.0],\n" + + " \"radius\" : \"100m\"\n" + + " }\n" + + "}"; + + indexRandom(true, client().prepareIndex("test", "doc", "0").setSource(source, XContentType.JSON).setRouting("ABC")); + + SearchResponse searchResponse = client().prepareSearch("test").setQuery( + geoShapeQuery("shape", "0", "doc").indexedShapeIndex("test").indexedShapeRouting("ABC") + ).get(); + + assertThat(searchResponse.getHits().getTotalHits(), equalTo(1L)); + } + private String findNodeName(String index) { ClusterState state = client().admin().cluster().prepareState().get().getState(); IndexShardRoutingTable shard = state.getRoutingTable().index(index).shard(0); diff --git a/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java b/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java index 7210fadd7ead5..505a5937d290b 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/test/ESIntegTestCase.java @@ -1829,7 +1829,7 @@ protected TestCluster buildTestCluster(Scope scope, long seed) throws IOExceptio return new InternalTestCluster(seed, createTempDir(), supportsDedicatedMasters, getAutoMinMasterNodes(), minNumDataNodes, maxNumDataNodes, InternalTestCluster.clusterName(scope.name(), seed) + "-cluster", nodeConfigurationSource, getNumClientNodes(), - InternalTestCluster.DEFAULT_ENABLE_HTTP_PIPELINING, nodePrefix, mockPlugins, getClientWrapper()); + nodePrefix, mockPlugins, getClientWrapper()); } protected NodeConfigurationSource getNodeConfigSource() { diff --git a/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java b/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java index 7eec3d345f5b9..b0eef695b7ff1 100644 --- a/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java +++ b/test/framework/src/main/java/org/elasticsearch/test/InternalTestCluster.java @@ -176,8 +176,6 @@ public final class InternalTestCluster extends TestCluster { static final int DEFAULT_MIN_NUM_CLIENT_NODES = 0; static final int DEFAULT_MAX_NUM_CLIENT_NODES = 1; - static final boolean DEFAULT_ENABLE_HTTP_PIPELINING = true; - /* sorted map to make traverse order reproducible, concurrent since we do checks on it not within a sync block */ private final NavigableMap nodes = new TreeMap<>(); @@ -224,7 +222,7 @@ public final class InternalTestCluster extends TestCluster { public InternalTestCluster(long clusterSeed, Path baseDir, boolean randomlyAddDedicatedMasters, boolean autoManageMinMasterNodes, int minNumDataNodes, int maxNumDataNodes, String clusterName, NodeConfigurationSource nodeConfigurationSource, int numClientNodes, - boolean enableHttpPipelining, String nodePrefix, Collection> mockPlugins, Function clientWrapper) { + String nodePrefix, Collection> mockPlugins, Function clientWrapper) { super(clusterSeed); this.autoManageMinMasterNodes = autoManageMinMasterNodes; this.clientWrapper = clientWrapper; @@ -913,7 +911,7 @@ private void clearDataIfNeeded(RestartCallback callback) throws IOException { private void createNewNode(final Settings newSettings) { final long newIdSeed = NodeEnvironment.NODE_ID_SEED_SETTING.get(node.settings()) + 1; // use a new seed to make sure we have new node id - Settings finalSettings = Settings.builder().put(node.settings()).put(newSettings).put(NodeEnvironment.NODE_ID_SEED_SETTING.getKey(), newIdSeed).build(); + Settings finalSettings = Settings.builder().put(node.originalSettings()).put(newSettings).put(NodeEnvironment.NODE_ID_SEED_SETTING.getKey(), newIdSeed).build(); if (DISCOVERY_ZEN_MINIMUM_MASTER_NODES_SETTING.exists(finalSettings) == false) { throw new IllegalStateException(DISCOVERY_ZEN_MINIMUM_MASTER_NODES_SETTING.getKey() + " is not configured after restart of [" + name + "]"); diff --git a/test/framework/src/test/java/org/elasticsearch/test/test/InternalTestClusterTests.java b/test/framework/src/test/java/org/elasticsearch/test/test/InternalTestClusterTests.java index c70708c73acbf..23f44c560baeb 100644 --- a/test/framework/src/test/java/org/elasticsearch/test/test/InternalTestClusterTests.java +++ b/test/framework/src/test/java/org/elasticsearch/test/test/InternalTestClusterTests.java @@ -19,8 +19,6 @@ */ package org.elasticsearch.test.test; -import org.elasticsearch.common.settings.Setting; -import org.elasticsearch.core.internal.io.IOUtils; import org.apache.lucene.util.LuceneTestCase; import org.elasticsearch.client.Client; import org.elasticsearch.cluster.ClusterName; @@ -28,6 +26,7 @@ import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.network.NetworkModule; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.internal.io.IOUtils; import org.elasticsearch.discovery.DiscoverySettings; import org.elasticsearch.discovery.zen.ZenDiscovery; import org.elasticsearch.env.NodeEnvironment; @@ -63,8 +62,6 @@ import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertFileExists; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertFileNotExists; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.greaterThan; -import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.not; /** @@ -86,16 +83,15 @@ public void testInitializiationIsConsistent() { String clusterName = randomRealisticUnicodeOfCodepointLengthBetween(1, 10); NodeConfigurationSource nodeConfigurationSource = NodeConfigurationSource.EMPTY; int numClientNodes = randomIntBetween(0, 10); - boolean enableHttpPipelining = randomBoolean(); String nodePrefix = randomRealisticUnicodeOfCodepointLengthBetween(1, 10); Path baseDir = createTempDir(); InternalTestCluster cluster0 = new InternalTestCluster(clusterSeed, baseDir, masterNodes, randomBoolean(), minNumDataNodes, maxNumDataNodes, clusterName, nodeConfigurationSource, numClientNodes, - enableHttpPipelining, nodePrefix, Collections.emptyList(), Function.identity()); + nodePrefix, Collections.emptyList(), Function.identity()); InternalTestCluster cluster1 = new InternalTestCluster(clusterSeed, baseDir, masterNodes, randomBoolean(), minNumDataNodes, maxNumDataNodes, clusterName, nodeConfigurationSource, numClientNodes, - enableHttpPipelining, nodePrefix, Collections.emptyList(), Function.identity()); + nodePrefix, Collections.emptyList(), Function.identity()); // TODO: this is not ideal - we should have a way to make sure ports are initialized in the same way assertClusters(cluster0, cluster1, false); @@ -211,16 +207,15 @@ public Settings transportClientSettings() { } }; - boolean enableHttpPipelining = randomBoolean(); String nodePrefix = "foobar"; Path baseDir = createTempDir(); InternalTestCluster cluster0 = new InternalTestCluster(clusterSeed, baseDir, masterNodes, autoManageMinMasterNodes, minNumDataNodes, maxNumDataNodes, clusterName1, nodeConfigurationSource, numClientNodes, - enableHttpPipelining, nodePrefix, mockPlugins(), Function.identity()); + nodePrefix, mockPlugins(), Function.identity()); InternalTestCluster cluster1 = new InternalTestCluster(clusterSeed, baseDir, masterNodes, autoManageMinMasterNodes, minNumDataNodes, maxNumDataNodes, clusterName2, nodeConfigurationSource, numClientNodes, - enableHttpPipelining, nodePrefix, mockPlugins(), Function.identity()); + nodePrefix, mockPlugins(), Function.identity()); assertClusters(cluster0, cluster1, false); long seed = randomLong(); @@ -280,12 +275,11 @@ public Settings transportClientSettings() { .put(NetworkModule.TRANSPORT_TYPE_KEY, transportClient).build(); } }; - boolean enableHttpPipelining = randomBoolean(); String nodePrefix = "test"; Path baseDir = createTempDir(); InternalTestCluster cluster = new InternalTestCluster(clusterSeed, baseDir, masterNodes, true, minNumDataNodes, maxNumDataNodes, clusterName1, nodeConfigurationSource, numClientNodes, - enableHttpPipelining, nodePrefix, mockPlugins(), Function.identity()); + nodePrefix, mockPlugins(), Function.identity()); try { cluster.beforeTest(random(), 0.0); final int originalMasterCount = cluster.numMasterNodes(); @@ -390,7 +384,7 @@ public Settings transportClientSettings() { return Settings.builder() .put(NetworkModule.TRANSPORT_TYPE_KEY, transportClient).build(); } - }, 0, randomBoolean(), "", mockPlugins(), Function.identity()); + }, 0, "", mockPlugins(), Function.identity()); cluster.beforeTest(random(), 0.0); List roles = new ArrayList<>(); for (int i = 0; i < numNodes; i++) { @@ -473,12 +467,13 @@ public Settings transportClientSettings() { .put(NetworkModule.TRANSPORT_TYPE_KEY, transportClient).build(); } }; - boolean enableHttpPipelining = randomBoolean(); String nodePrefix = "test"; Path baseDir = createTempDir(); + List> plugins = new ArrayList<>(mockPlugins()); + plugins.add(NodeAttrCheckPlugin.class); InternalTestCluster cluster = new InternalTestCluster(randomLong(), baseDir, false, true, 2, 2, - "test", nodeConfigurationSource, 0, enableHttpPipelining, nodePrefix, - mockPlugins(), Function.identity()); + "test", nodeConfigurationSource, 0, nodePrefix, + plugins, Function.identity()); try { cluster.beforeTest(random(), 0.0); assertMMNinNodeSetting(cluster, 2); @@ -509,4 +504,26 @@ public Settings onNodeStopped(String nodeName) throws Exception { cluster.close(); } } + + /** + * Plugin that adds a simple node attribute as setting and checks if that node attribute is not already defined. + * Allows to check that the full-cluster restart logic does not copy over plugin-derived settings. + */ + public static class NodeAttrCheckPlugin extends Plugin { + + private final Settings settings; + + public NodeAttrCheckPlugin(Settings settings) { + this.settings = settings; + } + + @Override + public Settings additionalSettings() { + if (settings.get("node.attr.dummy") != null) { + fail("dummy setting already exists"); + } + return Settings.builder().put("node.attr.dummy", true).build(); + } + + } } diff --git a/x-pack/docs/en/ml/configuring.asciidoc b/x-pack/docs/en/ml/configuring.asciidoc index 9e7b787dcea60..b794d3ebd3330 100644 --- a/x-pack/docs/en/ml/configuring.asciidoc +++ b/x-pack/docs/en/ml/configuring.asciidoc @@ -34,8 +34,17 @@ The scenarios in this section describe some best practices for generating useful * <> * <> +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/ml/customurl.asciidoc include::customurl.asciidoc[] + +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/ml/aggregations.asciidoc include::aggregations.asciidoc[] + +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/ml/categories.asciidoc include::categories.asciidoc[] + +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/ml/populations.asciidoc include::populations.asciidoc[] + +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/ml/transforms.asciidoc include::transforms.asciidoc[] diff --git a/x-pack/docs/en/ml/functions.asciidoc b/x-pack/docs/en/ml/functions.asciidoc index a59b289266760..ae5f768e05697 100644 --- a/x-pack/docs/en/ml/functions.asciidoc +++ b/x-pack/docs/en/ml/functions.asciidoc @@ -18,27 +18,6 @@ variations (for example, `count`, `low_count`, and `high_count`). These variatio apply one-sided tests, detecting anomalies only when the values are low or high, depending one which alternative is used. -//For some functions, you can optionally specify a field name in the -//`by_field_name` property. The analysis then considers whether there is an -//anomaly for one of more specific values of that field. In {kib}, use the -//**Key Fields** field in multi-metric jobs or the **by_field_name** field in -//advanced jobs. -//// -TODO: Per Sophie, "This is incorrect... Split Data refers to a partition_field_name. Over fields can only be added in Adv Config... - -Can you please remove the explanations for by/over/partition fields from the documentation for analytical functions. It's a complex topic and will be easier to review in a separate exercise." -//// - -//For some functions, you can also optionally specify a field name in the -//`over_field_name` property. This property shifts the analysis to be population- -//or peer-based and uses the field to split the data. In {kib}, use the -//**Split Data** field in multi-metric jobs or the **over_field_name** field in -//advanced jobs. - -//You can specify a `partition_field_name` with any function. The analysis is then -//segmented with completely independent baselines for each value of that field. -//In {kib}, use the **partition_field_name** field in advanced jobs. - You can specify a `summary_count_field_name` with any function except `metric`. When you use `summary_count_field_name`, the {ml} features expect the input data to be pre-aggregated. The value of the `summary_count_field_name` field @@ -55,13 +34,6 @@ functions are strongly affected by empty buckets. For this reason, there are `non_null_sum` and `non_zero_count` functions, which are tolerant to sparse data. These functions effectively ignore empty buckets. -//// -Some functions can benefit from overlapping buckets. This improves the overall -accuracy of the results but at the cost of a 2 bucket delay in seeing the results. - -The table below provides a high-level summary of the analytical functions provided by the API. Each of the functions is described in detail over the following pages. Note the examples given in these pages use single Detector Configuration objects. -//// - * <> * <> * <> @@ -70,10 +42,23 @@ The table below provides a high-level summary of the analytical functions provid * <> * <> +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/ml/functions/count.asciidoc include::functions/count.asciidoc[] + +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/ml/functions/geo.asciidoc include::functions/geo.asciidoc[] + +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/ml/functions/info.asciidoc include::functions/info.asciidoc[] + +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/ml/functions/metric.asciidoc include::functions/metric.asciidoc[] + +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/ml/functions/rare.asciidoc include::functions/rare.asciidoc[] + +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/ml/functions/sum.asciidoc include::functions/sum.asciidoc[] + +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/ml/functions/time.asciidoc include::functions/time.asciidoc[] diff --git a/x-pack/docs/en/ml/getting-started.asciidoc b/x-pack/docs/en/ml/getting-started.asciidoc index 5b15de51f0bca..2fd4f1ebe4972 100644 --- a/x-pack/docs/en/ml/getting-started.asciidoc +++ b/x-pack/docs/en/ml/getting-started.asciidoc @@ -72,9 +72,20 @@ significant changes to the system. You can alternatively assign the For more information, see <> and <>. +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/ml/getting-started-data.asciidoc include::getting-started-data.asciidoc[] + +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/ml/getting-started-wizards.asciidoc include::getting-started-wizards.asciidoc[] + +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/ml/getting-started-single.asciidoc include::getting-started-single.asciidoc[] + +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/ml/getting-started-multi.asciidoc include::getting-started-multi.asciidoc[] + +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/ml/getting-started-forecast.asciidoc include::getting-started-forecast.asciidoc[] + +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/ml/getting-started-next.asciidoc include::getting-started-next.asciidoc[] diff --git a/x-pack/docs/en/ml/overview.asciidoc b/x-pack/docs/en/ml/overview.asciidoc index b82a281acb0d5..5c941b4eda24c 100644 --- a/x-pack/docs/en/ml/overview.asciidoc +++ b/x-pack/docs/en/ml/overview.asciidoc @@ -17,4 +17,5 @@ include::calendars.asciidoc[] There are a few concepts that are core to {ml} in {xpack}. Understanding these concepts from the outset will tremendously help ease the learning process. +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/ml/architecture.asciidoc include::architecture.asciidoc[] diff --git a/x-pack/docs/en/security/auditing.asciidoc b/x-pack/docs/en/security/auditing/event-types.asciidoc similarity index 52% rename from x-pack/docs/en/security/auditing.asciidoc rename to x-pack/docs/en/security/auditing/event-types.asciidoc index ee508a5ac8d2d..1a6d4b02b0c86 100644 --- a/x-pack/docs/en/security/auditing.asciidoc +++ b/x-pack/docs/en/security/auditing/event-types.asciidoc @@ -1,50 +1,10 @@ [role="xpack"] -[[auditing]] -== Auditing security events - -You can enable auditing to keep track of security-related events such as -authentication failures and refused connections. Logging these events enables you -to monitor your cluster for suspicious activity and provides evidence in the -event of an attack. - -[IMPORTANT] -============================================================================ -Audit logs are **disabled** by default. To enable this functionality, you -must set `xpack.security.audit.enabled` to `true` in `elasticsearch.yml`. -============================================================================ - -{Security} provides two ways to persist audit logs: - -* The <> output, which persists events to - a dedicated `_access.log` file on the host's file system. -* The <> output, which persists events to an Elasticsearch index. -The audit index can reside on the same cluster, or a separate cluster. - -By default, only the `logfile` output is used when enabling auditing. -To facilitate browsing and analyzing the events, you can also enable -indexing by setting `xpack.security.audit.outputs` in `elasticsearch.yml`: - -[source,yaml] ----------------------------- -xpack.security.audit.outputs: [ index, logfile ] ----------------------------- - -The `index` output type should be used in conjunction with the `logfile` -output type Because it is possible for the `index` output type to lose -messages if the target index is unavailable, the `access.log` should be -used as the official record of events. - -NOTE: Audit events are batched for indexing so there is a lag before -events appear in the index. You can control how frequently batches of -events are pushed to the index by setting -`xpack.security.audit.index.flush_interval` in `elasticsearch.yml`. - [float] [[audit-event-types]] === Audit event types -Each request may generate multiple audit events. -The following is a list of the events that can be generated: +When you are <>, each request can generate +multiple audit events. The following is a list of the events that can be generated: |====== | `anonymous_access_denied` | | | Logged when a request is denied due to a missing @@ -281,195 +241,3 @@ The log level determines which attributes are included in a log entry. | `rule` | The <> rule that denied the request. |====== - -[float] -[[audit-log-output]] -=== Logfile audit output - -The `logfile` audit output is the default output for auditing. It writes data to -the `_access.log` file in the logs directory. - -[float] -[[audit-log-entry-format]] -=== Log entry format - -The format of a log entry is: - -[source,txt] ----------------------------------------------------------------------------- -[] [] [] [] ----------------------------------------------------------------------------- - -`` :: When the event occurred. You can configure the - timestamp format in `log4j2.properties`. -`` :: Information about the local node that generated - the log entry. You can control what node information - is included by configuring the - {ref}/auditing-settings.html#node-audit-settings[local node info settings]. -`` :: The layer from which this event originated: - `rest`, `transport` or `ip_filter`. -`` :: The type of event that occurred: `anonymous_access_denied`, - `authentication_failed`, `access_denied`, `access_granted`, - `connection_granted`, `connection_denied`. -`` :: A comma-separated list of key-value pairs that contain - data pertaining to the event. Formatted as - `attr1=[val1], attr2=[val2]`. See <> for the attributes that can be included - for each type of event. - -[float] -[[audit-log-settings]] -=== Logfile output settings - -The events and some other information about what gets logged can be -controlled using settings in the `elasticsearch.yml` file. See -{ref}/auditing-settings.html#event-audit-settings[Audited Event Settings] and -{ref}/auditing-settings.html#node-audit-settings[Local Node Info Settings]. - -IMPORTANT: No filtering is performed when auditing, so sensitive data may be -audited in plain text when including the request body in audit events. - -[[logging-file]] -You can also configure how the logfile is written in the `log4j2.properties` -file located in `CONFIG_DIR`. By default, audit information is appended to the -`_access.log` file located in the standard Elasticsearch `logs` directory -(typically located at `$ES_HOME/logs`). The file rolls over on a daily basis. - -[float] -[[audit-log-ignore-policy]] -=== Logfile audit events ignore policies - -The comprehensive audit trail is necessary to ensure accountability. It offers tremendous -value during incident response and can even be required for demonstrating compliance. - -The drawback of an audited system is represented by the inevitable performance penalty incurred. -In all truth, the audit trail spends _I/O ops_ that are not available anymore for the user's queries. -Sometimes the verbosity of the audit trail may become a problem that the event type restrictions, -<>, will not alleviate. - -*Audit events ignore policies* are a finer way to tune the verbosity of the audit trail. -These policies define rules that match audit events which will be _ignored_ (read as: not printed). -Rules match on the values of attributes of audit events and complement the <> method. -Imagine the corpus of audit events and the policies chopping off unwanted events. - -IMPORTANT: When utilizing audit events ignore policies you are acknowledging potential -accountability gaps that could render illegitimate actions undetectable. -Please take time to review these policies whenever your system architecture changes. - -A policy is a named set of filter rules. Each filter rule applies to a single event attribute, -one of the `users`, `realms`, `roles` or `indices` attributes. The filter rule defines -a list of {ref}/query-dsl-regexp-query.html#regexp-syntax[Lucene regexp], *any* of which has to match the value of the audit -event attribute for the rule to match. -A policy matches an event if *all* the rules comprising it match the event. -An audit event is ignored, therefore not printed, if it matches *any* policy. All other -non-matching events are printed as usual. - -All policies are defined under the `xpack.security.audit.logfile.events.ignore_filters` -settings namespace. For example, the following policy named _example1_ matches -events from the _kibana_ or _admin_user_ principals **and** operating over indices of the -wildcard form _app-logs*_: - -[source,yaml] ----------------------------- -xpack.security.audit.logfile.events.ignore_filters: - example1: - users: ["kibana", "admin_user"] - indices: ["app-logs*"] ----------------------------- - -An audit event generated by the _kibana_ user and operating over multiple indices -, some of which do not match the indices wildcard, will not match. -As expected, operations generated by all other users (even operating only on indices that -match the _indices_ filter) will not match this policy either. - -Audit events of different types may have <>. -If an event does not contain an attribute for which some policy defines filters, the -event will not match the policy. -For example, the following policy named _example2_, will never match `authentication_success` or -`authentication_failed` events, irrespective of the user's roles, because these -event schemas do not contain the `role` attribute: - -[source,yaml] ----------------------------- -xpack.security.audit.logfile.events.ignore_filters: - example2: - roles: ["admin", "ops_admin_*"] ----------------------------- - -Likewise, any events of users with multiple roles, some of which do not match the -regexps will not match this policy. - -For completeness, although practical use cases should be sparse, a filter can match -a missing attribute of an event, using the empty string ("") or the empty list ([]). -For example, the following policy will match events that do not have the `indices` -attribute (`anonymous_access_denied`, `authentication_success` and other types) as well -as events over the _next_ index. - -[source,yaml] ----------------------------- -xpack.security.audit.logfile.events.ignore_filters: - example3: - indices: ["next", ""] ----------------------------- - - -[float] -[[audit-index]] -=== Index audit output - -In addition to logging to a file, you can store audit logs in Elasticsearch -rolling indices. These indices can be either on the same cluster, or on a -remote cluster. You configure the following settings in -`elasticsearch.yml` to control how audit entries are indexed. To enable -this output, you need to configure the setting `xpack.security.audit.outputs` -in the `elasticsearch.yml` file: - -[source,yaml] ----------------------------- -xpack.security.audit.outputs: [ index, logfile ] ----------------------------- - -For more configuration options, see -{ref}/auditing-settings.html#index-audit-settings[Audit log indexing configuration settings]. - -IMPORTANT: No filtering is performed when auditing, so sensitive data may be -audited in plain text when including the request body in audit events. - -[float] -==== Audit index settings - -You can also configure settings for the indices that the events are stored in. -These settings are configured in the `xpack.security.audit.index.settings` namespace -in `elasticsearch.yml`. For example, the following configuration sets the -number of shards and replicas to 1 for the audit indices: - -[source,yaml] ----------------------------- -xpack.security.audit.index.settings: - index: - number_of_shards: 1 - number_of_replicas: 1 ----------------------------- - -[float] -==== Forwarding audit logs to a remote cluster - -To index audit events to a remote Elasticsearch cluster, you configure -the following `xpack.security.audit.index.client` settings: - -* `xpack.security.audit.index.client.hosts` -* `xpack.security.audit.index.client.cluster.name` -* `xpack.security.audit.index.client.xpack.security.user` - -For more information about these settings, see -{ref}/auditing-settings.html#remote-audit-settings[Remote Audit Log Indexing Configuration Settings]. - -You can pass additional settings to the remote client by specifying them in the -`xpack.security.audit.index.client` namespace. For example, to allow the remote -client to discover all of the nodes in the remote cluster you can specify the -`client.transport.sniff` setting: - -[source,yaml] ----------------------------- -xpack.security.audit.index.client.transport.sniff: true ----------------------------- diff --git a/x-pack/docs/en/security/auditing/forwarding-logs.asciidoc b/x-pack/docs/en/security/auditing/forwarding-logs.asciidoc new file mode 100644 index 0000000000000..01ed0f72e746d --- /dev/null +++ b/x-pack/docs/en/security/auditing/forwarding-logs.asciidoc @@ -0,0 +1,24 @@ +[role="xpack"] +[float] +[[forwarding-audit-logfiles]] +==== Forwarding audit logs to a remote cluster + +To index audit events to a remote Elasticsearch cluster, you configure +the following `xpack.security.audit.index.client` settings: + +* `xpack.security.audit.index.client.hosts` +* `xpack.security.audit.index.client.cluster.name` +* `xpack.security.audit.index.client.xpack.security.user` + +For more information about these settings, see +{ref}/auditing-settings.html#remote-audit-settings[Remote Audit Log Indexing Configuration Settings]. + +You can pass additional settings to the remote client by specifying them in the +`xpack.security.audit.index.client` namespace. For example, to allow the remote +client to discover all of the nodes in the remote cluster you can specify the +`client.transport.sniff` setting: + +[source,yaml] +---------------------------- +xpack.security.audit.index.client.transport.sniff: true +---------------------------- diff --git a/x-pack/docs/en/security/auditing/index.asciidoc b/x-pack/docs/en/security/auditing/index.asciidoc new file mode 100644 index 0000000000000..e82fd4397fb71 --- /dev/null +++ b/x-pack/docs/en/security/auditing/index.asciidoc @@ -0,0 +1,15 @@ + +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/auditing/overview.asciidoc +include::overview.asciidoc[] + +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/auditing/event-types.asciidoc +include::event-types.asciidoc[] + +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/auditing/output-logfile.asciidoc +include::output-logfile.asciidoc[] + +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/auditing/output-index.asciidoc +include::output-index.asciidoc[] + +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/auditing/forwarding-logs.asciidoc +include::forwarding-logs.asciidoc[] \ No newline at end of file diff --git a/x-pack/docs/en/security/auditing/output-index.asciidoc b/x-pack/docs/en/security/auditing/output-index.asciidoc new file mode 100644 index 0000000000000..e3ba805d715e7 --- /dev/null +++ b/x-pack/docs/en/security/auditing/output-index.asciidoc @@ -0,0 +1,38 @@ +[role="xpack"] +[float] +[[audit-index]] +=== Index audit output + +In addition to logging to a file, you can store audit logs in Elasticsearch +rolling indices. These indices can be either on the same cluster, or on a +remote cluster. You configure the following settings in +`elasticsearch.yml` to control how audit entries are indexed. To enable +this output, you need to configure the setting `xpack.security.audit.outputs` +in the `elasticsearch.yml` file: + +[source,yaml] +---------------------------- +xpack.security.audit.outputs: [ index, logfile ] +---------------------------- + +For more configuration options, see +{ref}/auditing-settings.html#index-audit-settings[Audit log indexing configuration settings]. + +IMPORTANT: No filtering is performed when auditing, so sensitive data may be +audited in plain text when including the request body in audit events. + +[float] +==== Audit index settings + +You can also configure settings for the indices that the events are stored in. +These settings are configured in the `xpack.security.audit.index.settings` namespace +in `elasticsearch.yml`. For example, the following configuration sets the +number of shards and replicas to 1 for the audit indices: + +[source,yaml] +---------------------------- +xpack.security.audit.index.settings: + index: + number_of_shards: 1 + number_of_replicas: 1 +---------------------------- diff --git a/x-pack/docs/en/security/auditing/output-logfile.asciidoc b/x-pack/docs/en/security/auditing/output-logfile.asciidoc new file mode 100644 index 0000000000000..095f57cf61e48 --- /dev/null +++ b/x-pack/docs/en/security/auditing/output-logfile.asciidoc @@ -0,0 +1,130 @@ +[role="xpack"] +[float] +[[audit-log-output]] +=== Logfile audit output + +The `logfile` audit output is the default output for auditing. It writes data to +the `_access.log` file in the logs directory. + +[float] +[[audit-log-entry-format]] +=== Log entry format + +The format of a log entry is: + +[source,txt] +---------------------------------------------------------------------------- +[] [] [] [] +---------------------------------------------------------------------------- + +`` :: When the event occurred. You can configure the + timestamp format in `log4j2.properties`. +`` :: Information about the local node that generated + the log entry. You can control what node information + is included by configuring the + {ref}/auditing-settings.html#node-audit-settings[local node info settings]. +`` :: The layer from which this event originated: + `rest`, `transport` or `ip_filter`. +`` :: The type of event that occurred: `anonymous_access_denied`, + `authentication_failed`, `access_denied`, `access_granted`, + `connection_granted`, `connection_denied`. +`` :: A comma-separated list of key-value pairs that contain + data pertaining to the event. Formatted as + `attr1=[val1], attr2=[val2]`. See <> for the attributes that can be included + for each type of event. + +[float] +[[audit-log-settings]] +=== Logfile output settings + +The events and some other information about what gets logged can be +controlled using settings in the `elasticsearch.yml` file. See +{ref}/auditing-settings.html#event-audit-settings[Audited Event Settings] and +{ref}/auditing-settings.html#node-audit-settings[Local Node Info Settings]. + +IMPORTANT: No filtering is performed when auditing, so sensitive data may be +audited in plain text when including the request body in audit events. + +[[logging-file]] +You can also configure how the logfile is written in the `log4j2.properties` +file located in `CONFIG_DIR`. By default, audit information is appended to the +`_access.log` file located in the standard Elasticsearch `logs` directory +(typically located at `$ES_HOME/logs`). The file rolls over on a daily basis. + +[float] +[[audit-log-ignore-policy]] +=== Logfile audit events ignore policies + +The comprehensive audit trail is necessary to ensure accountability. It offers tremendous +value during incident response and can even be required for demonstrating compliance. + +The drawback of an audited system is represented by the inevitable performance penalty incurred. +In all truth, the audit trail spends _I/O ops_ that are not available anymore for the user's queries. +Sometimes the verbosity of the audit trail may become a problem that the event type restrictions, +<>, will not alleviate. + +*Audit events ignore policies* are a finer way to tune the verbosity of the audit trail. +These policies define rules that match audit events which will be _ignored_ (read as: not printed). +Rules match on the values of attributes of audit events and complement the <> method. +Imagine the corpus of audit events and the policies chopping off unwanted events. + +IMPORTANT: When utilizing audit events ignore policies you are acknowledging potential +accountability gaps that could render illegitimate actions undetectable. +Please take time to review these policies whenever your system architecture changes. + +A policy is a named set of filter rules. Each filter rule applies to a single event attribute, +one of the `users`, `realms`, `roles` or `indices` attributes. The filter rule defines +a list of {ref}/query-dsl-regexp-query.html#regexp-syntax[Lucene regexp], *any* of which has to match the value of the audit +event attribute for the rule to match. +A policy matches an event if *all* the rules comprising it match the event. +An audit event is ignored, therefore not printed, if it matches *any* policy. All other +non-matching events are printed as usual. + +All policies are defined under the `xpack.security.audit.logfile.events.ignore_filters` +settings namespace. For example, the following policy named _example1_ matches +events from the _kibana_ or _admin_user_ principals **and** operating over indices of the +wildcard form _app-logs*_: + +[source,yaml] +---------------------------- +xpack.security.audit.logfile.events.ignore_filters: + example1: + users: ["kibana", "admin_user"] + indices: ["app-logs*"] +---------------------------- + +An audit event generated by the _kibana_ user and operating over multiple indices +, some of which do not match the indices wildcard, will not match. +As expected, operations generated by all other users (even operating only on indices that +match the _indices_ filter) will not match this policy either. + +Audit events of different types may have <>. +If an event does not contain an attribute for which some policy defines filters, the +event will not match the policy. +For example, the following policy named _example2_, will never match `authentication_success` or +`authentication_failed` events, irrespective of the user's roles, because these +event schemas do not contain the `role` attribute: + +[source,yaml] +---------------------------- +xpack.security.audit.logfile.events.ignore_filters: + example2: + roles: ["admin", "ops_admin_*"] +---------------------------- + +Likewise, any events of users with multiple roles, some of which do not match the +regexps will not match this policy. + +For completeness, although practical use cases should be sparse, a filter can match +a missing attribute of an event, using the empty string ("") or the empty list ([]). +For example, the following policy will match events that do not have the `indices` +attribute (`anonymous_access_denied`, `authentication_success` and other types) as well +as events over the _next_ index. + +[source,yaml] +---------------------------- +xpack.security.audit.logfile.events.ignore_filters: + example3: + indices: ["next", ""] +---------------------------- diff --git a/x-pack/docs/en/security/auditing/overview.asciidoc b/x-pack/docs/en/security/auditing/overview.asciidoc new file mode 100644 index 0000000000000..b60122612a0ab --- /dev/null +++ b/x-pack/docs/en/security/auditing/overview.asciidoc @@ -0,0 +1,40 @@ +[role="xpack"] +[[auditing]] +== Auditing security events + +You can enable auditing to keep track of security-related events such as +authentication failures and refused connections. Logging these events enables you +to monitor your cluster for suspicious activity and provides evidence in the +event of an attack. + +[IMPORTANT] +============================================================================ +Audit logs are **disabled** by default. To enable this functionality, you +must set `xpack.security.audit.enabled` to `true` in `elasticsearch.yml`. +============================================================================ + +{Security} provides two ways to persist audit logs: + +* The <> output, which persists events to + a dedicated `_access.log` file on the host's file system. +* The <> output, which persists events to an Elasticsearch index. +The audit index can reside on the same cluster, or a separate cluster. + +By default, only the `logfile` output is used when enabling auditing. +To facilitate browsing and analyzing the events, you can also enable +indexing by setting `xpack.security.audit.outputs` in `elasticsearch.yml`: + +[source,yaml] +---------------------------- +xpack.security.audit.outputs: [ index, logfile ] +---------------------------- + +The `index` output type should be used in conjunction with the `logfile` +output type Because it is possible for the `index` output type to lose +messages if the target index is unavailable, the `access.log` should be +used as the official record of events. + +NOTE: Audit events are batched for indexing so there is a lag before +events appear in the index. You can control how frequently batches of +events are pushed to the index by setting +`xpack.security.audit.index.flush_interval` in `elasticsearch.yml`. diff --git a/x-pack/docs/en/security/authentication/native-realm.asciidoc b/x-pack/docs/en/security/authentication/native-realm.asciidoc index f7b514b81449f..6aa0a72fc8495 100644 --- a/x-pack/docs/en/security/authentication/native-realm.asciidoc +++ b/x-pack/docs/en/security/authentication/native-realm.asciidoc @@ -10,7 +10,7 @@ manage user passwords. [float] ==== Configuring a native realm -See {ref}/[Configuring a native realm]. +See {ref}/configuring-native-realm.html[Configuring a native realm]. [[native-settings]] ==== Native realm settings diff --git a/x-pack/docs/en/security/authentication/overview.asciidoc b/x-pack/docs/en/security/authentication/overview.asciidoc index da5f6a4ea3cea..7633f02b6765b 100644 --- a/x-pack/docs/en/security/authentication/overview.asciidoc +++ b/x-pack/docs/en/security/authentication/overview.asciidoc @@ -24,28 +24,41 @@ attach your user credentials to the requests sent to {es}. For example, when using realms that support usernames and passwords you can simply attach {wikipedia}/Basic_access_authentication[basic auth] header to the requests. +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/authentication/built-in-users.asciidoc include::built-in-users.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/authentication/internal-users.asciidoc include::internal-users.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/authentication/realms.asciidoc include::realms.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/authentication/active-directory-realm.asciidoc include::active-directory-realm.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/authentication/file-realm.asciidoc include::file-realm.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/authentication/ldap-realm.asciidoc include::ldap-realm.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/authentication/native-realm.asciidoc include::native-realm.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/authentication/pki-realm.asciidoc include::pki-realm.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/authentication/saml-realm.asciidoc include::saml-realm.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/authentication/custom-realm.asciidoc include::custom-realm.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/authentication/anonymous-access.asciidoc include::anonymous-access.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/authentication/user-cache.asciidoc include::user-cache.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/authentication/saml-guide.asciidoc include::saml-guide.asciidoc[] diff --git a/x-pack/docs/en/security/authorization/overview.asciidoc b/x-pack/docs/en/security/authorization/overview.asciidoc index 98a1ad8b786b6..e5b61e585c67c 100644 --- a/x-pack/docs/en/security/authorization/overview.asciidoc +++ b/x-pack/docs/en/security/authorization/overview.asciidoc @@ -49,18 +49,26 @@ As an administrator, you will need to define the roles that you want to use, then assign users to the roles. These can be assigned to users in a number of ways depending on the realms by which the users are authenticated. +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/authorization/built-in-roles.asciidoc include::built-in-roles.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/authorization/managing-roles.asciidoc include::managing-roles.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/authorization/privileges.asciidoc include::privileges.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/authorization/alias-privileges.asciidoc include::alias-privileges.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/authorization/mapping-roles.asciidoc include::mapping-roles.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/authorization/field-and-document-access-control.asciidoc include::field-and-document-access-control.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/authorization/run-as-privilege.asciidoc include::run-as-privilege.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/authorization/custom-roles-provider.asciidoc include::custom-roles-provider.asciidoc[] diff --git a/x-pack/docs/en/security/ccs-clients-integrations.asciidoc b/x-pack/docs/en/security/ccs-clients-integrations.asciidoc index cbf4ede328e48..f744e6d7092e3 100644 --- a/x-pack/docs/en/security/ccs-clients-integrations.asciidoc +++ b/x-pack/docs/en/security/ccs-clients-integrations.asciidoc @@ -32,14 +32,20 @@ or at least communicate with the cluster in a secured way: * {kibana-ref}/secure-reporting.html[Reporting] * {winlogbeat-ref}/securing-beats.html[Winlogbeat] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/ccs-clients-integrations/cross-cluster.asciidoc include::ccs-clients-integrations/cross-cluster.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/ccs-clients-integrations/java.asciidoc include::ccs-clients-integrations/java.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/ccs-clients-integrations/http.asciidoc include::ccs-clients-integrations/http.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/ccs-clients-integrations/hadoop.asciidoc include::ccs-clients-integrations/hadoop.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/ccs-clients-integrations/beats.asciidoc include::ccs-clients-integrations/beats.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/ccs-clients-integrations/monitoring.asciidoc include::ccs-clients-integrations/monitoring.asciidoc[] diff --git a/x-pack/docs/en/security/ccs-clients-integrations/cross-cluster.asciidoc b/x-pack/docs/en/security/ccs-clients-integrations/cross-cluster.asciidoc index bf4800d50d226..eceb0315b20c9 100644 --- a/x-pack/docs/en/security/ccs-clients-integrations/cross-cluster.asciidoc +++ b/x-pack/docs/en/security/ccs-clients-integrations/cross-cluster.asciidoc @@ -155,4 +155,5 @@ GET two:logs-2017.04/_search <1> // TEST[skip:todo] //TBD: Is there a missing description of the <1> callout above? +:edit_url: https://github.com/elastic/kibana/edit/{branch}/x-pack/docs/en/security/cross-cluster-kibana.asciidoc include::{xkb-repo-dir}/security/cross-cluster-kibana.asciidoc[] diff --git a/x-pack/docs/en/security/index.asciidoc b/x-pack/docs/en/security/index.asciidoc index 96e9287ca01ec..ed891b9a1ba7e 100644 --- a/x-pack/docs/en/security/index.asciidoc +++ b/x-pack/docs/en/security/index.asciidoc @@ -108,7 +108,7 @@ include::authentication/overview.asciidoc[] include::authorization/overview.asciidoc[] :edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/auditing.asciidoc -include::auditing.asciidoc[] +include::auditing/index.asciidoc[] :edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/securing-communications.asciidoc include::securing-communications.asciidoc[] diff --git a/x-pack/docs/en/security/reference.asciidoc b/x-pack/docs/en/security/reference.asciidoc index 9c65fd6479a4f..ba770c152329b 100644 --- a/x-pack/docs/en/security/reference.asciidoc +++ b/x-pack/docs/en/security/reference.asciidoc @@ -7,4 +7,5 @@ * {ref}/security-api.html[Security API] * {ref}/xpack-commands.html[Security Commands] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/reference/files.asciidoc include::reference/files.asciidoc[] diff --git a/x-pack/docs/en/security/securing-communications.asciidoc b/x-pack/docs/en/security/securing-communications.asciidoc index ef07f0113cb59..11f6b3dc5616e 100644 --- a/x-pack/docs/en/security/securing-communications.asciidoc +++ b/x-pack/docs/en/security/securing-communications.asciidoc @@ -17,6 +17,7 @@ This section shows how to: The authentication of new nodes helps prevent a rogue node from joining the cluster and receiving data through replication. +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/securing-communications/setting-up-ssl.asciidoc include::securing-communications/setting-up-ssl.asciidoc[] //TO-DO: These sections can be removed when all links to them are removed. diff --git a/x-pack/docs/en/security/securing-communications/securing-elasticsearch.asciidoc b/x-pack/docs/en/security/securing-communications/securing-elasticsearch.asciidoc index da4e3a40b7d16..09cb118f68466 100644 --- a/x-pack/docs/en/security/securing-communications/securing-elasticsearch.asciidoc +++ b/x-pack/docs/en/security/securing-communications/securing-elasticsearch.asciidoc @@ -29,8 +29,17 @@ information, see <>. For more information about encrypting communications across the Elastic Stack, see {xpack-ref}/encrypting-communications.html[Encrypting Communications]. +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/securing-communications/node-certificates.asciidoc include::node-certificates.asciidoc[] + +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/securing-communications/tls-transport.asciidoc include::tls-transport.asciidoc[] + +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/securing-communications/tls-http.asciidoc include::tls-http.asciidoc[] + +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/securing-communications/tls-ad.asciidoc include::tls-ad.asciidoc[] + +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/securing-communications/tls-ldap.asciidoc include::tls-ldap.asciidoc[] \ No newline at end of file diff --git a/x-pack/docs/en/settings/audit-settings.asciidoc b/x-pack/docs/en/settings/audit-settings.asciidoc index 14e5d6fa28f46..6274fae790b8c 100644 --- a/x-pack/docs/en/settings/audit-settings.asciidoc +++ b/x-pack/docs/en/settings/audit-settings.asciidoc @@ -121,7 +121,9 @@ To index audit events to a remote {es} cluster, you configure the following `xpack.security.audit.index.client.hosts`:: Specifies a comma-separated list of `host:port` pairs. These hosts should be -nodes in the remote cluster. +nodes in the remote cluster. If you are using default values for the +<> setting, you can omit the +`port` value. Otherwise, it must match the `transport.tcp.port` setting. `xpack.security.audit.index.client.cluster.name`:: Specifies the name of the remote cluster. diff --git a/x-pack/docs/en/settings/security-settings.asciidoc b/x-pack/docs/en/settings/security-settings.asciidoc index 2c4df292857f3..4e9d85f1900ae 100644 --- a/x-pack/docs/en/settings/security-settings.asciidoc +++ b/x-pack/docs/en/settings/security-settings.asciidoc @@ -1145,7 +1145,7 @@ Path to the PEM encoded file containing the private key. The passphrase that is used to decrypt the private key. This value is optional as the key might not be encrypted. -`xpack.ssl.secure_key_passphrase` ({<>):: +`xpack.ssl.secure_key_passphrase` (<>):: The passphrase that is used to decrypt the private key. This value is optional as the key might not be encrypted. diff --git a/x-pack/docs/en/settings/ssl-settings.asciidoc b/x-pack/docs/en/settings/ssl-settings.asciidoc index 722aeeea15592..655dfb74a6498 100644 --- a/x-pack/docs/en/settings/ssl-settings.asciidoc +++ b/x-pack/docs/en/settings/ssl-settings.asciidoc @@ -90,7 +90,7 @@ Path to the keystore that holds the private key and certificate. +{ssl-prefix}.ssl.keystore.password+:: Password to the keystore. -+{ssl-prefix}.ssl.keystore.secure_password` (<>):: ++{ssl-prefix}.ssl.keystore.secure_password+ (<>):: Password to the keystore. +{ssl-prefix}.ssl.keystore.key_password+:: diff --git a/x-pack/docs/en/sql/endpoints/translate.asciidoc b/x-pack/docs/en/sql/endpoints/translate.asciidoc index 278211411305a..9c1d71af5d35e 100644 --- a/x-pack/docs/en/sql/endpoints/translate.asciidoc +++ b/x-pack/docs/en/sql/endpoints/translate.asciidoc @@ -23,8 +23,14 @@ Which returns: { "size" : 10, "docvalue_fields" : [ - "page_count", - "release_date" + { + "field": "page_count", + "format": "use_field_mapping" + }, + { + "field": "release_date", + "format": "epoch_millis" + } ], "_source": { "includes": [ diff --git a/x-pack/docs/en/watcher/actions.asciidoc b/x-pack/docs/en/watcher/actions.asciidoc index 72489443b3c12..de2516b0589cc 100644 --- a/x-pack/docs/en/watcher/actions.asciidoc +++ b/x-pack/docs/en/watcher/actions.asciidoc @@ -259,20 +259,28 @@ PUT _xpack/watcher/watch/log_event_watch <1> A `condition` that only applies to the `notify_pager` action, which restricts its execution to when the condition succeeds (at least 5 hits in this case). +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/actions/email.asciidoc include::actions/email.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/actions/webhook.asciidoc include::actions/webhook.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/actions/index.asciidoc include::actions/index.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/actions/logging.asciidoc include::actions/logging.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/actions/hipchat.asciidoc include::actions/hipchat.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/actions/slack.asciidoc include::actions/slack.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/actions/pagerduty.asciidoc include::actions/pagerduty.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/actions/jira.asciidoc include::actions/jira.asciidoc[] [float] diff --git a/x-pack/docs/en/watcher/condition.asciidoc b/x-pack/docs/en/watcher/condition.asciidoc index e83981667d5f7..50424dc132a43 100644 --- a/x-pack/docs/en/watcher/condition.asciidoc +++ b/x-pack/docs/en/watcher/condition.asciidoc @@ -28,12 +28,17 @@ conditions are met. In addition to the watch wide condition, you can also configure conditions per <>. +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/condition/always.asciidoc include::condition/always.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/condition/never.asciidoc include::condition/never.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/condition/compare.asciidoc include::condition/compare.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/condition/array-compare.asciidoc include::condition/array-compare.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/condition/script.asciidoc include::condition/script.asciidoc[] diff --git a/x-pack/docs/en/watcher/example-watches.asciidoc b/x-pack/docs/en/watcher/example-watches.asciidoc index 2d747caba5cc4..2a402b20261d7 100644 --- a/x-pack/docs/en/watcher/example-watches.asciidoc +++ b/x-pack/docs/en/watcher/example-watches.asciidoc @@ -9,6 +9,8 @@ For more example watches you can use as a starting point for building custom watches, see the https://github.com/elastic/examples/tree/master/Alerting[Example Watches] in the Elastic Examples repo. +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/example-watches/example-watch-clusterstatus.asciidoc include::example-watches/example-watch-clusterstatus.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/example-watches/example-watch-meetupdata.asciidoc include::example-watches/example-watch-meetupdata.asciidoc[] diff --git a/x-pack/docs/en/watcher/input.asciidoc b/x-pack/docs/en/watcher/input.asciidoc index d74f5cd80f1eb..6dee849c735f9 100644 --- a/x-pack/docs/en/watcher/input.asciidoc +++ b/x-pack/docs/en/watcher/input.asciidoc @@ -19,10 +19,14 @@ execution context. NOTE: If you don't define an input for a watch, an empty payload is loaded into the execution context. +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/input/simple.asciidoc include::input/simple.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/input/search.asciidoc include::input/search.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/input/http.asciidoc include::input/http.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/input/chain.asciidoc include::input/chain.asciidoc[] diff --git a/x-pack/docs/en/watcher/java.asciidoc b/x-pack/docs/en/watcher/java.asciidoc index bcf41252433ba..e5cb6b54b0c65 100644 --- a/x-pack/docs/en/watcher/java.asciidoc +++ b/x-pack/docs/en/watcher/java.asciidoc @@ -101,20 +101,29 @@ XPackClient xpackClient = new XPackClient(client); WatcherClient watcherClient = xpackClient.watcher(); -------------------------------------------------- +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/java/put-watch.asciidoc include::java/put-watch.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/java/get-watch.asciidoc include::java/get-watch.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/java/delete-watch.asciidoc include::java/delete-watch.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/java/execute-watch.asciidoc include::java/execute-watch.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/java/ack-watch.asciidoc include::java/ack-watch.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/java/activate-watch.asciidoc include::java/activate-watch.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/java/deactivate-watch.asciidoc include::java/deactivate-watch.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/java/stats.asciidoc include::java/stats.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/java/service.asciidoc include::java/service.asciidoc[] diff --git a/x-pack/docs/en/watcher/transform.asciidoc b/x-pack/docs/en/watcher/transform.asciidoc index 1b99d595b9c8f..0351c9b8c1214 100644 --- a/x-pack/docs/en/watcher/transform.asciidoc +++ b/x-pack/docs/en/watcher/transform.asciidoc @@ -55,8 +55,11 @@ part of the definition of the `my_webhook` action. <1> A watch level `transform` <2> An action level `transform` +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/transform/search.asciidoc include::transform/search.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/transform/script.asciidoc include::transform/script.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/transform/chain.asciidoc include::transform/chain.asciidoc[] \ No newline at end of file diff --git a/x-pack/docs/en/watcher/trigger.asciidoc b/x-pack/docs/en/watcher/trigger.asciidoc index ee52dbba3bd7a..af830e829a45e 100644 --- a/x-pack/docs/en/watcher/trigger.asciidoc +++ b/x-pack/docs/en/watcher/trigger.asciidoc @@ -9,4 +9,5 @@ the trigger and triggering the watch when needed. {watcher} is designed to support different types of triggers, but only time-based <> triggers are currently available. +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/trigger/schedule.asciidoc include::trigger/schedule.asciidoc[] diff --git a/x-pack/docs/en/watcher/trigger/schedule.asciidoc b/x-pack/docs/en/watcher/trigger/schedule.asciidoc index 7cd38c5fc9ba0..abbc3f5cfe8e5 100644 --- a/x-pack/docs/en/watcher/trigger/schedule.asciidoc +++ b/x-pack/docs/en/watcher/trigger/schedule.asciidoc @@ -26,16 +26,23 @@ once per minute. For more information about throttling, see * <> * <> +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/schedule/hourly.asciidoc include::schedule/hourly.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/schedule/daily.asciidoc include::schedule/daily.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/schedule/weekly.asciidoc include::schedule/weekly.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/schedule/monthly.asciidoc include::schedule/monthly.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/schedule/yearly.asciidoc include::schedule/yearly.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/schedule/cron.asciidoc include::schedule/cron.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/watcher/schedule/interval.asciidoc include::schedule/interval.asciidoc[] diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java index fa0c239aab17d..99e6a10ad92de 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java @@ -223,6 +223,7 @@ protected PutLicenseResponse newResponse(boolean acknowledged) { @Override public ClusterState execute(ClusterState currentState) throws Exception { + XPackPlugin.checkReadyForXPackCustomMetadata(currentState); MetaData currentMetadata = currentState.metaData(); LicensesMetaData licensesMetaData = currentMetadata.custom(LicensesMetaData.TYPE); Version trialVersion = null; @@ -341,7 +342,7 @@ protected void doStart() throws ElasticsearchException { if (clusterService.lifecycleState() == Lifecycle.State.STARTED) { final ClusterState clusterState = clusterService.state(); if (clusterState.blocks().hasGlobalBlock(GatewayService.STATE_NOT_RECOVERED_BLOCK) == false && - clusterState.nodes().getMasterNode() != null) { + clusterState.nodes().getMasterNode() != null && XPackPlugin.isReadyForXPackCustomMetadata(clusterState)) { final LicensesMetaData currentMetaData = clusterState.metaData().custom(LicensesMetaData.TYPE); boolean noLicense = currentMetaData == null || currentMetaData.getLicense() == null; if (clusterState.getNodes().isLocalNodeElectedMaster() && @@ -374,6 +375,12 @@ public void clusterChanged(ClusterChangedEvent event) { final ClusterState previousClusterState = event.previousState(); final ClusterState currentClusterState = event.state(); if (!currentClusterState.blocks().hasGlobalBlock(GatewayService.STATE_NOT_RECOVERED_BLOCK)) { + if (XPackPlugin.isReadyForXPackCustomMetadata(currentClusterState) == false) { + logger.debug("cannot add license to cluster as the following nodes might not understand the license metadata: {}", + () -> XPackPlugin.nodesNotReadyForXPackCustomMetadata(currentClusterState)); + return; + } + final LicensesMetaData prevLicensesMetaData = previousClusterState.getMetaData().custom(LicensesMetaData.TYPE); final LicensesMetaData currentLicensesMetaData = currentClusterState.getMetaData().custom(LicensesMetaData.TYPE); if (logger.isDebugEnabled()) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/StartBasicClusterTask.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/StartBasicClusterTask.java index 355482872d629..0cf949a69906f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/StartBasicClusterTask.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/StartBasicClusterTask.java @@ -13,6 +13,7 @@ import org.elasticsearch.cluster.ClusterStateUpdateTask; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.common.Nullable; +import org.elasticsearch.xpack.core.XPackPlugin; import java.time.Clock; import java.util.Collections; @@ -59,6 +60,7 @@ public void clusterStateProcessed(String source, ClusterState oldState, ClusterS @Override public ClusterState execute(ClusterState currentState) throws Exception { + XPackPlugin.checkReadyForXPackCustomMetadata(currentState); LicensesMetaData licensesMetaData = currentState.metaData().custom(LicensesMetaData.TYPE); License currentLicense = LicensesMetaData.extractLicense(licensesMetaData); if (currentLicense == null || currentLicense.type().equals("basic") == false) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/StartTrialClusterTask.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/StartTrialClusterTask.java index 355672dedf717..5c5c03151ba26 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/StartTrialClusterTask.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/StartTrialClusterTask.java @@ -13,6 +13,7 @@ import org.elasticsearch.cluster.ClusterStateUpdateTask; import org.elasticsearch.cluster.metadata.MetaData; import org.elasticsearch.common.Nullable; +import org.elasticsearch.xpack.core.XPackPlugin; import java.time.Clock; import java.util.Collections; @@ -64,6 +65,7 @@ public void clusterStateProcessed(String source, ClusterState oldState, ClusterS @Override public ClusterState execute(ClusterState currentState) throws Exception { + XPackPlugin.checkReadyForXPackCustomMetadata(currentState); LicensesMetaData currentLicensesMetaData = currentState.metaData().custom(LicensesMetaData.TYPE); if (request.isAcknowledged() == false) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/StartupSelfGeneratedLicenseTask.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/StartupSelfGeneratedLicenseTask.java index 77695f64538bc..823283ac5a852 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/StartupSelfGeneratedLicenseTask.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/StartupSelfGeneratedLicenseTask.java @@ -16,6 +16,7 @@ import org.elasticsearch.common.Nullable; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.xpack.core.XPackPlugin; import java.time.Clock; import java.util.UUID; @@ -49,6 +50,7 @@ public void clusterStateProcessed(String source, ClusterState oldState, ClusterS @Override public ClusterState execute(ClusterState currentState) throws Exception { + XPackPlugin.checkReadyForXPackCustomMetadata(currentState); final MetaData metaData = currentState.metaData(); final LicensesMetaData currentLicensesMetaData = metaData.custom(LicensesMetaData.TYPE); // do not generate a license if any license is present diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackPlugin.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackPlugin.java index 5ee46f3b3c97a..77d521e2d4322 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackPlugin.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackPlugin.java @@ -9,15 +9,20 @@ import org.apache.lucene.util.SetOnce; import org.bouncycastle.operator.OperatorCreationException; import org.elasticsearch.SpecialPermission; +import org.elasticsearch.Version; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.GenericAction; import org.elasticsearch.action.support.ActionFilter; import org.elasticsearch.client.Client; import org.elasticsearch.client.transport.TransportClient; +import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.Booleans; import org.elasticsearch.common.inject.Binder; import org.elasticsearch.common.inject.Module; import org.elasticsearch.common.inject.multibindings.Multibinder; @@ -33,6 +38,7 @@ import org.elasticsearch.env.Environment; import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.license.LicenseService; +import org.elasticsearch.license.LicensesMetaData; import org.elasticsearch.license.Licensing; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.plugins.ExtensiblePlugin; @@ -46,10 +52,13 @@ import org.elasticsearch.xpack.core.action.TransportXPackUsageAction; import org.elasticsearch.xpack.core.action.XPackInfoAction; import org.elasticsearch.xpack.core.action.XPackUsageAction; +import org.elasticsearch.xpack.core.ml.MLMetadataField; import org.elasticsearch.xpack.core.rest.action.RestXPackInfoAction; import org.elasticsearch.xpack.core.rest.action.RestXPackUsageAction; +import org.elasticsearch.xpack.core.security.authc.TokenMetaData; import org.elasticsearch.xpack.core.ssl.SSLConfigurationReloader; import org.elasticsearch.xpack.core.ssl.SSLService; +import org.elasticsearch.xpack.core.watcher.WatcherMetaData; import javax.security.auth.DestroyFailedException; @@ -62,14 +71,19 @@ import java.time.Clock; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; public class XPackPlugin extends XPackClientPlugin implements ScriptPlugin, ExtensiblePlugin { private static Logger logger = ESLoggerFactory.getLogger(XPackPlugin.class); private static DeprecationLogger deprecationLogger = new DeprecationLogger(logger); + public static final String XPACK_INSTALLED_NODE_ATTR = "xpack.installed"; + // TODO: clean up this library to not ask for write access to all system properties! static { // invoke this clinit in unbound with permissions to access all system properties @@ -138,6 +152,75 @@ protected Clock getClock() { public static LicenseService getSharedLicenseService() { return licenseService.get(); } public static XPackLicenseState getSharedLicenseState() { return licenseState.get(); } + /** + * Checks if the cluster state allows this node to add x-pack metadata to the cluster state, + * and throws an exception otherwise. + * This check should be called before installing any x-pack metadata to the cluster state, + * to ensure that the other nodes that are part of the cluster will be able to deserialize + * that metadata. Note that if the cluster state already contains x-pack metadata, this + * check assumes that the nodes are already ready to receive additional x-pack metadata. + * Having this check properly in place everywhere allows to install x-pack into a cluster + * using a rolling restart. + */ + public static void checkReadyForXPackCustomMetadata(ClusterState clusterState) { + if (alreadyContainsXPackCustomMetadata(clusterState)) { + return; + } + List notReadyNodes = nodesNotReadyForXPackCustomMetadata(clusterState); + if (notReadyNodes.isEmpty() == false) { + throw new IllegalStateException("The following nodes are not ready yet for enabling x-pack custom metadata: " + notReadyNodes); + } + } + + /** + * Checks if the cluster state allows this node to add x-pack metadata to the cluster state. + * See {@link #checkReadyForXPackCustomMetadata} for more details. + */ + public static boolean isReadyForXPackCustomMetadata(ClusterState clusterState) { + return alreadyContainsXPackCustomMetadata(clusterState) || nodesNotReadyForXPackCustomMetadata(clusterState).isEmpty(); + } + + /** + * Returns the list of nodes that won't allow this node from adding x-pack metadata to the cluster state. + * See {@link #checkReadyForXPackCustomMetadata} for more details. + */ + public static List nodesNotReadyForXPackCustomMetadata(ClusterState clusterState) { + // check that all nodes would be capable of deserializing newly added x-pack metadata + final List notReadyNodes = StreamSupport.stream(clusterState.nodes().spliterator(), false).filter(node -> { + final String xpackInstalledAttr = node.getAttributes().getOrDefault(XPACK_INSTALLED_NODE_ATTR, "false"); + + // The node attribute XPACK_INSTALLED_NODE_ATTR was only introduced in 6.3.0, so when + // we have an older node in this mixed-version cluster without any x-pack metadata, + // we want to prevent x-pack from adding custom metadata + return node.getVersion().before(Version.V_6_3_0) || Booleans.parseBoolean(xpackInstalledAttr) == false; + }).collect(Collectors.toList()); + + return notReadyNodes; + } + + private static boolean alreadyContainsXPackCustomMetadata(ClusterState clusterState) { + final MetaData metaData = clusterState.metaData(); + return metaData.custom(LicensesMetaData.TYPE) != null || + metaData.custom(MLMetadataField.TYPE) != null || + metaData.custom(WatcherMetaData.TYPE) != null || + clusterState.custom(TokenMetaData.TYPE) != null; + } + + @Override + public Settings additionalSettings() { + final String xpackInstalledNodeAttrSetting = "node.attr." + XPACK_INSTALLED_NODE_ATTR; + + if (settings.get(xpackInstalledNodeAttrSetting) != null) { + throw new IllegalArgumentException("Directly setting [" + xpackInstalledNodeAttrSetting + "] is not permitted"); + } + + if (transportClientMode) { + return super.additionalSettings(); + } else { + return Settings.builder().put(super.additionalSettings()).put(xpackInstalledNodeAttrSetting, "true").build(); + } + } + @Override public Collection createGuiceModules() { ArrayList modules = new ArrayList<>(); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/AbstractLicenseServiceTestCase.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/AbstractLicenseServiceTestCase.java index 2f110f4f8a9e8..5bc33ae330a18 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/license/AbstractLicenseServiceTestCase.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/license/AbstractLicenseServiceTestCase.java @@ -18,14 +18,16 @@ import org.elasticsearch.env.Environment; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.watcher.ResourceWatcherService; +import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.core.watcher.watch.ClockMock; import org.junit.After; import org.junit.Before; import java.nio.file.Path; +import java.util.Arrays; -import static java.util.Collections.emptyMap; import static java.util.Collections.emptySet; +import static java.util.Collections.singletonMap; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -66,6 +68,7 @@ protected void setInitialState(License license, XPackLicenseState licenseState, when(state.metaData()).thenReturn(metaData); final DiscoveryNode mockNode = getLocalNode(); when(discoveryNodes.getMasterNode()).thenReturn(mockNode); + when(discoveryNodes.spliterator()).thenReturn(Arrays.asList(mockNode).spliterator()); when(discoveryNodes.isLocalNodeElectedMaster()).thenReturn(false); when(state.nodes()).thenReturn(discoveryNodes); when(state.getNodes()).thenReturn(discoveryNodes); // it is really ridiculous we have nodes() and getNodes()... @@ -76,7 +79,8 @@ protected void setInitialState(License license, XPackLicenseState licenseState, } protected DiscoveryNode getLocalNode() { - return new DiscoveryNode("b", buildNewFakeTransportAddress(), emptyMap(), emptySet(), Version.CURRENT); + return new DiscoveryNode("b", buildNewFakeTransportAddress(), singletonMap(XPackPlugin.XPACK_INSTALLED_NODE_ATTR, "true"), + emptySet(), Version.CURRENT); } @After diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/XPackPluginTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/XPackPluginTests.java new file mode 100644 index 0000000000000..59731cab71db8 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/XPackPluginTests.java @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.core; + +import org.elasticsearch.Version; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.ClusterName; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.node.DiscoveryNode; +import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.VersionUtils; +import org.elasticsearch.xpack.core.security.authc.TokenMetaData; +import org.elasticsearch.xpack.core.ssl.SSLService; + +import java.util.Collections; +import java.util.Map; + +import static org.hamcrest.Matchers.containsString; + +public class XPackPluginTests extends ESTestCase { + + public void testXPackInstalledAttrClash() throws Exception { + Settings.Builder builder = Settings.builder(); + builder.put("node.attr." + XPackPlugin.XPACK_INSTALLED_NODE_ATTR, randomBoolean()); + if (randomBoolean()) { + builder.put(Client.CLIENT_TYPE_SETTING_S.getKey(), "transport"); + } + XPackPlugin xpackPlugin = createXPackPlugin(builder.put("path.home", createTempDir()).build()); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, xpackPlugin::additionalSettings); + assertThat(e.getMessage(), + containsString("Directly setting [node.attr." + XPackPlugin.XPACK_INSTALLED_NODE_ATTR + "] is not permitted")); + } + + public void testXPackInstalledAttrExists() throws Exception { + XPackPlugin xpackPlugin = createXPackPlugin(Settings.builder().put("path.home", createTempDir()).build()); + assertEquals("true", xpackPlugin.additionalSettings().get("node.attr." + XPackPlugin.XPACK_INSTALLED_NODE_ATTR)); + } + + public void testNodesNotReadyForXPackCustomMetadata() { + boolean compatible; + boolean nodesCompatible = true; + DiscoveryNodes.Builder discoveryNodes = DiscoveryNodes.builder(); + + for (int i = 0; i < randomInt(3); i++) { + final Version version = VersionUtils.randomVersion(random()); + final Map attributes; + if (randomBoolean() && version.onOrAfter(Version.V_6_3_0)) { + attributes = Collections.singletonMap(XPackPlugin.XPACK_INSTALLED_NODE_ATTR, "true"); + } else { + nodesCompatible = false; + attributes = Collections.emptyMap(); + } + + discoveryNodes.add(new DiscoveryNode("node_" + i, buildNewFakeTransportAddress(), attributes, Collections.emptySet(), + Version.CURRENT)); + } + ClusterState.Builder clusterStateBuilder = ClusterState.builder(ClusterName.DEFAULT); + + if (randomBoolean()) { + clusterStateBuilder.putCustom(TokenMetaData.TYPE, new TokenMetaData(Collections.emptyList(), new byte[0])); + compatible = true; + } else { + compatible = nodesCompatible; + } + + ClusterState clusterState = clusterStateBuilder.nodes(discoveryNodes.build()).build(); + + assertEquals(XPackPlugin.nodesNotReadyForXPackCustomMetadata(clusterState).isEmpty(), nodesCompatible); + assertEquals(XPackPlugin.isReadyForXPackCustomMetadata(clusterState), compatible); + + if (compatible == false) { + IllegalStateException e = expectThrows(IllegalStateException.class, + () -> XPackPlugin.checkReadyForXPackCustomMetadata(clusterState)); + assertThat(e.getMessage(), containsString("The following nodes are not ready yet for enabling x-pack custom metadata:")); + } + } + + private XPackPlugin createXPackPlugin(Settings settings) throws Exception { + return new XPackPlugin(settings, null){ + + @Override + protected void setSslService(SSLService sslService) { + // disable + } + + @Override + protected void setLicenseState(XPackLicenseState licenseState) { + // disable + } + }; + } + +} diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java index bdefabdb294e5..a1714a8e3f5db 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/MachineLearning.java @@ -316,12 +316,8 @@ public Settings additionalSettings() { } private void addMlNodeAttribute(Settings.Builder additionalSettings, String attrName, String value) { - // Unfortunately we cannot simply disallow any value, because the internal cluster integration - // test framework will restart nodes with settings copied from the node immediately before it - // was stopped. The best we can do is reject inconsistencies, and report this in a way that - // makes clear that setting the node attribute directly is not allowed. String oldValue = settings.get(attrName); - if (oldValue == null || oldValue.equals(value)) { + if (oldValue == null) { additionalSettings.put(attrName, value); } else { reportClashingNodeAttribute(attrName); @@ -487,7 +483,7 @@ public List getRestHandlers(Settings settings, RestController restC new RestStartDatafeedAction(settings, restController), new RestStopDatafeedAction(settings, restController), new RestDeleteModelSnapshotAction(settings, restController), - new RestDeleteExpiredDataAction(settings, restController), + new RestDeleteExpiredDataAction(settings, restController), new RestForecastJobAction(settings, restController), new RestGetCalendarsAction(settings, restController), new RestPutCalendarAction(settings, restController), diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteDatafeedAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteDatafeedAction.java index 220d97e89ba14..8c9eabe6de19a 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteDatafeedAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteDatafeedAction.java @@ -21,6 +21,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.core.ml.MLMetadataField; import org.elasticsearch.xpack.core.ml.MlMetadata; import org.elasticsearch.xpack.core.ml.action.DeleteDatafeedAction; @@ -120,6 +121,7 @@ protected DeleteDatafeedAction.Response newResponse(boolean acknowledged) { @Override public ClusterState execute(ClusterState currentState) { + XPackPlugin.checkReadyForXPackCustomMetadata(currentState); MlMetadata currentMetadata = MlMetadata.getMlMetadata(currentState); PersistentTasksCustomMetaData persistentTasks = currentState.getMetaData().custom(PersistentTasksCustomMetaData.TYPE); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportFinalizeJobExecutionAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportFinalizeJobExecutionAction.java index e939c6ef31a2f..8e7a0fef41e89 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportFinalizeJobExecutionAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportFinalizeJobExecutionAction.java @@ -19,6 +19,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.core.ml.action.FinalizeJobExecutionAction; import org.elasticsearch.xpack.core.ml.MLMetadataField; import org.elasticsearch.xpack.core.ml.MlMetadata; @@ -57,6 +58,7 @@ protected void masterOperation(FinalizeJobExecutionAction.Request request, Clust clusterService.submitStateUpdateTask(source, new ClusterStateUpdateTask() { @Override public ClusterState execute(ClusterState currentState) { + XPackPlugin.checkReadyForXPackCustomMetadata(currentState); MlMetadata mlMetadata = MlMetadata.getMlMetadata(currentState); MlMetadata.Builder mlMetadataBuilder = new MlMetadata.Builder(mlMetadata); Date finishedTime = new Date(); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPutDatafeedAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPutDatafeedAction.java index 2b4304a205b13..08a9dfb09c1d9 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPutDatafeedAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportPutDatafeedAction.java @@ -28,6 +28,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.XPackField; +import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.ml.MLMetadataField; import org.elasticsearch.xpack.core.ml.MlMetadata; @@ -141,6 +142,7 @@ public ClusterState execute(ClusterState currentState) { } private ClusterState putDatafeed(PutDatafeedAction.Request request, ClusterState clusterState) { + XPackPlugin.checkReadyForXPackCustomMetadata(clusterState); MlMetadata currentMetadata = MlMetadata.getMlMetadata(clusterState); MlMetadata newMetadata = new MlMetadata.Builder(currentMetadata) .putDatafeed(request.getDatafeed(), threadPool.getThreadContext()).build(); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/JobManager.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/JobManager.java index 3694e41a8b850..2042d917f6057 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/JobManager.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/job/JobManager.java @@ -30,6 +30,7 @@ import org.elasticsearch.env.Environment; import org.elasticsearch.index.analysis.AnalysisRegistry; import org.elasticsearch.persistent.PersistentTasksCustomMetaData; +import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.core.ml.MLMetadataField; import org.elasticsearch.xpack.core.ml.MachineLearningField; import org.elasticsearch.xpack.core.ml.MlMetadata; @@ -184,6 +185,9 @@ public void putJob(PutJobAction.Request request, AnalysisRegistry analysisRegist DEPRECATION_LOGGER.deprecated("Creating jobs with delimited data format is deprecated. Please use xcontent instead."); } + // pre-flight check, not necessarily required, but avoids figuring this out while on the CS update thread + XPackPlugin.checkReadyForXPackCustomMetadata(state); + MlMetadata currentMlMetadata = MlMetadata.getMlMetadata(state); if (currentMlMetadata.getJobs().containsKey(job.getId())) { actionListener.onFailure(ExceptionsHelper.jobAlreadyExists(job.getId())); @@ -565,6 +569,7 @@ private static MlMetadata.Builder createMlMetadataBuilder(ClusterState currentSt } private static ClusterState buildNewClusterState(ClusterState currentState, MlMetadata.Builder builder) { + XPackPlugin.checkReadyForXPackCustomMetadata(currentState); ClusterState.Builder newState = ClusterState.builder(currentState); newState.metaData(MetaData.builder(currentState.getMetaData()).putCustom(MLMetadataField.TYPE, builder.build()).build()); return newState.build(); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/MachineLearningTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/MachineLearningTests.java index 9b141380c65eb..2f1aa29d919d9 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/MachineLearningTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/MachineLearningTests.java @@ -6,7 +6,6 @@ package org.elasticsearch.xpack.ml; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.env.Environment; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.monitor.os.OsStats; import org.elasticsearch.test.ESTestCase; @@ -37,13 +36,11 @@ public void testNoAttributes_givenNoClash() { public void testNoAttributes_givenSameAndMlEnabled() { Settings.Builder builder = Settings.builder(); if (randomBoolean()) { - builder.put("xpack.ml.enabled", true); - builder.put("node.attr.ml.enabled", true); + builder.put("xpack.ml.enabled", randomBoolean()); } if (randomBoolean()) { int maxOpenJobs = randomIntBetween(5, 15); builder.put("xpack.ml.max_open_jobs", maxOpenJobs); - builder.put("node.attr.ml.max_open_jobs", maxOpenJobs); } MachineLearning machineLearning = createMachineLearning(builder.put("path.home", createTempDir()).build()); assertNotNull(machineLearning.additionalSettings()); @@ -51,16 +48,8 @@ public void testNoAttributes_givenSameAndMlEnabled() { public void testNoAttributes_givenClash() { Settings.Builder builder = Settings.builder(); - boolean enabled = true; - if (randomBoolean()) { - enabled = randomBoolean(); - builder.put("xpack.ml.enabled", enabled); - } - if (randomBoolean()) { - builder.put("xpack.ml.max_open_jobs", randomIntBetween(9, 12)); - } if (randomBoolean()) { - builder.put("node.attr.ml.enabled", !enabled); + builder.put("node.attr.ml.enabled", randomBoolean()); } else { builder.put("node.attr.ml.max_open_jobs", randomIntBetween(13, 15)); } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/MockClientBuilder.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/MockClientBuilder.java index 437a965dcf02a..61fc73d1641e1 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/MockClientBuilder.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/job/persistence/MockClientBuilder.java @@ -197,6 +197,7 @@ public MockClientBuilder prepareSearchExecuteListener(String index, SearchRespon when(builder.setFetchSource(anyBoolean())).thenReturn(builder); when(builder.setScroll(anyString())).thenReturn(builder); when(builder.addDocValueField(any(String.class))).thenReturn(builder); + when(builder.addDocValueField(any(String.class), any(String.class))).thenReturn(builder); when(builder.addSort(any(String.class), any(SortOrder.class))).thenReturn(builder); when(builder.setQuery(any())).thenReturn(builder); when(builder.setSize(anyInt())).thenReturn(builder); @@ -246,6 +247,7 @@ public MockClientBuilder prepareSearch(String index, String type, int from, int when(builder.setSize(eq(size))).thenReturn(builder); when(builder.setFetchSource(eq(true))).thenReturn(builder); when(builder.addDocValueField(any(String.class))).thenReturn(builder); + when(builder.addDocValueField(any(String.class), any(String.class))).thenReturn(builder); when(builder.addSort(any(String.class), any(SortOrder.class))).thenReturn(builder); when(builder.get()).thenReturn(response); when(client.prepareSearch(eq(index))).thenReturn(builder); @@ -262,6 +264,7 @@ public MockClientBuilder prepareSearchAnySize(String index, String type, SearchR when(builder.setSize(any(Integer.class))).thenReturn(builder); when(builder.setFetchSource(eq(true))).thenReturn(builder); when(builder.addDocValueField(any(String.class))).thenReturn(builder); + when(builder.addDocValueField(any(String.class), any(String.class))).thenReturn(builder); when(builder.addSort(any(String.class), any(SortOrder.class))).thenReturn(builder); when(builder.get()).thenReturn(response); when(client.prepareSearch(eq(index))).thenReturn(builder); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 44f5afd4bdb07..133093df33a13 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -317,7 +317,7 @@ public Collection createGuiceModules() { } modules.add(b -> XPackPlugin.bindFeatureSet(b, SecurityFeatureSet.class)); - + if (enabled == false) { modules.add(b -> { b.bind(Realms.class).toProvider(Providers.of(null)); // for SecurityFeatureSet @@ -903,15 +903,6 @@ public UnaryOperator> getIndexTemplateMetaDat }; } - @Override - public Map> getInitialClusterStateCustomSupplier() { - if (enabled) { - return Collections.singletonMap(TokenMetaData.TYPE, () -> tokenService.get().getTokenMetaData()); - } else { - return Collections.emptyMap(); - } - } - @Override public Function> getFieldFilter() { if (enabled) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java index d23415f87dfcc..2934fb8062de4 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java @@ -8,6 +8,9 @@ import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.BytesRefBuilder; +import org.elasticsearch.cluster.ClusterStateUpdateTask; +import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.common.Priority; import org.elasticsearch.core.internal.io.IOUtils; import org.apache.lucene.util.UnicodeUtil; import org.elasticsearch.ElasticsearchSecurityException; @@ -63,6 +66,7 @@ import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.search.SearchHit; +import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.XPackField; import org.elasticsearch.xpack.core.security.ScrollHelper; @@ -107,6 +111,7 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Supplier; @@ -1327,6 +1332,8 @@ public TimeValue masterNodeTimeout() { @Override public ClusterState execute(ClusterState currentState) throws Exception { + XPackPlugin.checkReadyForXPackCustomMetadata(currentState); + if (tokenMetaData.equals(currentState.custom(TokenMetaData.TYPE))) { return currentState; } @@ -1347,6 +1354,15 @@ private void initialize(ClusterService clusterService) { return; } + if (state.nodes().isLocalNodeElectedMaster()) { + if (XPackPlugin.isReadyForXPackCustomMetadata(state)) { + installTokenMetadata(state.metaData()); + } else { + logger.debug("cannot add token metadata to cluster as the following nodes might not understand the metadata: {}", + () -> XPackPlugin.nodesNotReadyForXPackCustomMetadata(state)); + } + } + TokenMetaData custom = event.state().custom(TokenMetaData.TYPE); if (custom != null && custom.equals(getTokenMetaData()) == false) { logger.info("refresh keys"); @@ -1360,6 +1376,39 @@ private void initialize(ClusterService clusterService) { }); } + // to prevent too many cluster state update tasks to be queued for doing the same update + private final AtomicBoolean installTokenMetadataInProgress = new AtomicBoolean(false); + + private void installTokenMetadata(MetaData metaData) { + if (metaData.custom(TokenMetaData.TYPE) == null) { + if (installTokenMetadataInProgress.compareAndSet(false, true)) { + clusterService.submitStateUpdateTask("install-token-metadata", new ClusterStateUpdateTask(Priority.URGENT) { + @Override + public ClusterState execute(ClusterState currentState) { + XPackPlugin.checkReadyForXPackCustomMetadata(currentState); + + if (currentState.custom(TokenMetaData.TYPE) == null) { + return ClusterState.builder(currentState).putCustom(TokenMetaData.TYPE, getTokenMetaData()).build(); + } else { + return currentState; + } + } + + @Override + public void onFailure(String source, Exception e) { + installTokenMetadataInProgress.set(false); + logger.error("unable to install token metadata", e); + } + + @Override + public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) { + installTokenMetadataInProgress.set(false); + } + }); + } + } + } + /** * For testing */ diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailTests.java index ec448f14e9160..dab3d023f65d3 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/IndexAuditTrailTests.java @@ -214,7 +214,7 @@ protected void addDefaultSecurityTransportType(Settings.Builder builder, Setting mockPlugins.add(getTestTransportPlugin()); } remoteCluster = new InternalTestCluster(randomLong(), createTempDir(), false, true, numNodes, numNodes, cluster2Name, - cluster2SettingsSource, 0, false, SECOND_CLUSTER_NODE_PREFIX, mockPlugins, + cluster2SettingsSource, 0, SECOND_CLUSTER_NODE_PREFIX, mockPlugins, useSecurity ? getClientWrapper() : Function.identity()); remoteCluster.beforeTest(random(), 0.5); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/RemoteIndexAuditTrailStartingTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/RemoteIndexAuditTrailStartingTests.java index 7002803a3d49c..96bba962237fe 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/RemoteIndexAuditTrailStartingTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/index/RemoteIndexAuditTrailStartingTests.java @@ -117,7 +117,7 @@ public Settings nodeSettings(int nodeOrdinal) { } }; remoteCluster = new InternalTestCluster(randomLong(), createTempDir(), false, true, numNodes, numNodes, - cluster2Name, cluster2SettingsSource, 0, false, SECOND_CLUSTER_NODE_PREFIX, getMockPlugins(), getClientWrapper()); + cluster2Name, cluster2SettingsSource, 0, SECOND_CLUSTER_NODE_PREFIX, getMockPlugins(), getClientWrapper()); remoteCluster.beforeTest(random(), 0.0); assertNoTimeout(remoteCluster.client().admin().cluster().prepareHealth().setWaitForGreenStatus().get()); } diff --git a/x-pack/plugin/sql/sql-proto/src/test/java/org/elasticsearch/xpack/sql/plugin/SqlTranslateResponseTests.java b/x-pack/plugin/sql/sql-proto/src/test/java/org/elasticsearch/xpack/sql/plugin/SqlTranslateResponseTests.java index 061b70a55d975..76f73fada0663 100644 --- a/x-pack/plugin/sql/sql-proto/src/test/java/org/elasticsearch/xpack/sql/plugin/SqlTranslateResponseTests.java +++ b/x-pack/plugin/sql/sql-proto/src/test/java/org/elasticsearch/xpack/sql/plugin/SqlTranslateResponseTests.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.sql.plugin; import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.search.fetch.subphase.DocValueFieldsContext; import org.elasticsearch.test.AbstractStreamableTestCase; import java.io.IOException; @@ -18,7 +19,7 @@ protected SqlTranslateResponse createTestInstance() { if (randomBoolean()) { long docValues = iterations(5, 10); for (int i = 0; i < docValues; i++) { - s.docValueField(randomAlphaOfLength(10)); + s.docValueField(randomAlphaOfLength(10), DocValueFieldsContext.USE_DEFAULT_FORMAT); } } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/Querier.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/Querier.java index 62941a5b14f07..055e34758cc75 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/Querier.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/Querier.java @@ -341,12 +341,12 @@ protected void handleResponse(SearchResponse response, ActionListener sourceFields = new LinkedHashSet<>(); - final Set docFields = new LinkedHashSet<>(); + final Set docFields = new LinkedHashSet<>(); final Map scriptFields = new LinkedHashMap<>(); boolean trackScores = false; @@ -47,8 +49,8 @@ public void addSourceField(String field) { /** * Retrieve the requested field from doc values (or fielddata) of the document */ - public void addDocField(String field) { - docFields.add(field); + public void addDocField(String field, String format) { + docFields.add(new FieldAndFormat(field, format)); } /** @@ -67,7 +69,8 @@ public void build(SearchSourceBuilder sourceBuilder) { if (!sourceFields.isEmpty()) { sourceBuilder.fetchSource(sourceFields.toArray(Strings.EMPTY_ARRAY), null); } - docFields.forEach(sourceBuilder::docValueField); + docFields.forEach(field -> sourceBuilder.docValueField(field.field, + field.format == null ? DocValueFieldsContext.USE_DEFAULT_FORMAT : field.format)); scriptFields.forEach(sourceBuilder::scriptField); } } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/extractor/FieldHitExtractor.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/extractor/FieldHitExtractor.java index 159127fb24cb2..66e177530547f 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/extractor/FieldHitExtractor.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/execution/search/extractor/FieldHitExtractor.java @@ -5,12 +5,16 @@ */ package org.elasticsearch.xpack.sql.execution.search.extractor; +import org.elasticsearch.Version; import org.elasticsearch.common.Strings; import org.elasticsearch.common.document.DocumentField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.search.SearchHit; import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; +import org.elasticsearch.xpack.sql.type.DataType; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; import org.joda.time.ReadableDateTime; import java.io.IOException; @@ -41,15 +45,17 @@ private static String[] sourcePath(String name, boolean useDocValue, String hitN } private final String fieldName, hitName; + private final DataType dataType; private final boolean useDocValue; private final String[] path; - public FieldHitExtractor(String name, boolean useDocValue) { - this(name, useDocValue, null); + public FieldHitExtractor(String name, DataType dataType, boolean useDocValue) { + this(name, dataType, useDocValue, null); } - public FieldHitExtractor(String name, boolean useDocValue, String hitName) { + public FieldHitExtractor(String name, DataType dataType, boolean useDocValue, String hitName) { this.fieldName = name; + this.dataType = dataType; this.useDocValue = useDocValue; this.hitName = hitName; @@ -64,6 +70,16 @@ public FieldHitExtractor(String name, boolean useDocValue, String hitName) { FieldHitExtractor(StreamInput in) throws IOException { fieldName = in.readString(); + if (in.getVersion().onOrAfter(Version.V_6_4_0)) { + String esType = in.readOptionalString(); + if (esType != null) { + dataType = DataType.fromEsType(esType); + } else { + dataType = null; + } + } else { + dataType = null; + } useDocValue = in.readBoolean(); hitName = in.readOptionalString(); path = sourcePath(fieldName, useDocValue, hitName); @@ -77,6 +93,9 @@ public String getWriteableName() { @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(fieldName); + if (out.getVersion().onOrAfter(Version.V_6_4_0)) { + out.writeOptionalString(dataType == null ? null : dataType.esType); + } out.writeBoolean(useDocValue); out.writeOptionalString(hitName); } @@ -117,6 +136,9 @@ private Object unwrapMultiValue(Object values) { if (values instanceof Map) { throw new SqlIllegalArgumentException("Objects (returned by [{}]) are not supported", fieldName); } + if (values instanceof String && dataType == DataType.DATE) { + return new DateTime(Long.parseLong(values.toString()), DateTimeZone.UTC); + } if (values instanceof Long || values instanceof Double || values instanceof String || values instanceof Boolean || values instanceof ReadableDateTime) { return values; diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeProcessor.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeProcessor.java index 6aa6b6a50e9bc..d135b8a086566 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeProcessor.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/datetime/DateTimeProcessor.java @@ -9,9 +9,11 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.xpack.sql.SqlIllegalArgumentException; import org.elasticsearch.xpack.sql.expression.function.scalar.processor.runtime.Processor; +import org.joda.time.DateTime; import org.joda.time.DateTimeFieldType; import org.joda.time.DateTimeZone; import org.joda.time.ReadableDateTime; +import org.joda.time.ReadableInstant; import java.io.IOException; import java.util.Objects; @@ -78,15 +80,21 @@ public Object process(Object l) { return null; } - if (!(l instanceof ReadableDateTime)) { - throw new SqlIllegalArgumentException("A date/time is required; received {}", l); + ReadableDateTime dt; + if (l instanceof String) { + // 6.4+ + final long millis = Long.parseLong(l.toString()); + dt = new DateTime(millis, DateTimeZone.forTimeZone(timeZone)); + } else if (l instanceof ReadableInstant) { + // 6.3- + dt = (ReadableDateTime) l; + if (!TimeZone.getTimeZone("UTC").equals(timeZone)) { + dt = dt.toDateTime().withZone(DateTimeZone.forTimeZone(timeZone)); + } + } else { + throw new SqlIllegalArgumentException("A string or a date is required; received {}", l); } - ReadableDateTime dt = (ReadableDateTime) l; - - if (!TimeZone.getTimeZone("UTC").equals(timeZone)) { - dt = dt.toDateTime().withZone(DateTimeZone.forTimeZone(timeZone)); - } return extractor.extract(dt); } diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/container/QueryContainer.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/container/QueryContainer.java index bca180315d9e5..9f9c1bb21bb31 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/container/QueryContainer.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/container/QueryContainer.java @@ -173,7 +173,7 @@ private String aliasName(Attribute attr) { // reference methods // private FieldExtraction topHitFieldRef(FieldAttribute fieldAttr) { - return new SearchHitFieldRef(aliasName(fieldAttr), fieldAttr.field().hasDocValues()); + return new SearchHitFieldRef(aliasName(fieldAttr), fieldAttr.field().getDataType(), fieldAttr.field().hasDocValues()); } private Tuple nestedHitFieldRef(FieldAttribute attr) { @@ -184,7 +184,8 @@ private Tuple nestedHitFieldRef(FieldAttribute Query q = rewriteToContainNestedField(query, attr.location(), attr.nestedParent().name(), name, attr.field().hasDocValues()); - SearchHitFieldRef nestedFieldRef = new SearchHitFieldRef(name, attr.field().hasDocValues(), attr.parent().name()); + SearchHitFieldRef nestedFieldRef = new SearchHitFieldRef(name, attr.field().getDataType(), + attr.field().hasDocValues(), attr.parent().name()); nestedRefs.add(nestedFieldRef); return new Tuple<>(new QueryContainer(q, aggs, columns, aliases, pseudoFunctions, scalarFunctions, sort, limit), nestedFieldRef); diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/container/SearchHitFieldRef.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/container/SearchHitFieldRef.java index 6a7f24b447e55..7f799108d28ec 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/container/SearchHitFieldRef.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/querydsl/container/SearchHitFieldRef.java @@ -6,18 +6,21 @@ package org.elasticsearch.xpack.sql.querydsl.container; import org.elasticsearch.xpack.sql.execution.search.SqlSourceBuilder; +import org.elasticsearch.xpack.sql.type.DataType; public class SearchHitFieldRef extends FieldReference { private final String name; + private final DataType dataType; private final boolean docValue; private final String hitName; - public SearchHitFieldRef(String name, boolean useDocValueInsteadOfSource) { - this(name, useDocValueInsteadOfSource, null); + public SearchHitFieldRef(String name, DataType dataType, boolean useDocValueInsteadOfSource) { + this(name, dataType, useDocValueInsteadOfSource, null); } - public SearchHitFieldRef(String name, boolean useDocValueInsteadOfSource, String hitName) { + public SearchHitFieldRef(String name, DataType dataType, boolean useDocValueInsteadOfSource, String hitName) { this.name = name; + this.dataType = dataType; this.docValue = useDocValueInsteadOfSource; this.hitName = hitName; } @@ -31,6 +34,10 @@ public String name() { return name; } + public DataType getDataType() { + return dataType; + } + public boolean useDocValue() { return docValue; } @@ -42,7 +49,8 @@ public void collectFields(SqlSourceBuilder sourceBuilder) { return; } if (docValue) { - sourceBuilder.addDocField(name); + String format = dataType == DataType.DATE ? "epoch_millis" : null; + sourceBuilder.addDocField(name, format); } else { sourceBuilder.addSourceField(name); } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/action/SqlLicenseIT.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/action/SqlLicenseIT.java index a97f66763a9b6..8988f70672ac6 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/action/SqlLicenseIT.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/action/SqlLicenseIT.java @@ -16,6 +16,7 @@ import org.elasticsearch.license.License.OperationMode; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.search.fetch.subphase.DocValueFieldsContext; import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import org.elasticsearch.test.hamcrest.ElasticsearchAssertions; import org.elasticsearch.transport.Netty4Plugin; @@ -150,7 +151,8 @@ public void testSqlTranslateActionLicense() throws Exception { SqlTranslateResponse response = client().prepareExecute(SqlTranslateAction.INSTANCE).query("SELECT * FROM test").get(); SearchSourceBuilder source = response.source(); - assertThat(source.docValueFields(), Matchers.contains("count")); + assertThat(source.docValueFields(), Matchers.contains( + new DocValueFieldsContext.FieldAndFormat("count", DocValueFieldsContext.USE_DEFAULT_FORMAT))); FetchSourceContext fetchSource = source.fetchSource(); assertThat(fetchSource.includes(), Matchers.arrayContaining("data")); } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/action/SqlTranslateActionIT.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/action/SqlTranslateActionIT.java index 5de9cfca97a36..a4c440eb9dfad 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/action/SqlTranslateActionIT.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/action/SqlTranslateActionIT.java @@ -8,6 +8,7 @@ import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.search.fetch.subphase.DocValueFieldsContext; import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import org.elasticsearch.search.sort.SortBuilders; import org.elasticsearch.xpack.sql.plugin.SqlTranslateAction; @@ -35,7 +36,9 @@ public void testSqlTranslateAction() throws Exception { FetchSourceContext fetch = source.fetchSource(); assertEquals(true, fetch.fetchSource()); assertArrayEquals(new String[] { "data" }, fetch.includes()); - assertEquals(singletonList("count"), source.docValueFields()); + assertEquals( + singletonList(new DocValueFieldsContext.FieldAndFormat("count", DocValueFieldsContext.USE_DEFAULT_FORMAT)), + source.docValueFields()); assertEquals(singletonList(SortBuilders.fieldSort("count")), source.sorts()); } } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/execution/search/SqlSourceBuilderTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/execution/search/SqlSourceBuilderTests.java index 0d57ad97c9831..6ee843c2c6371 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/execution/search/SqlSourceBuilderTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/execution/search/SqlSourceBuilderTests.java @@ -24,8 +24,8 @@ public void testSqlSourceBuilder() { ssb.trackScores(); ssb.addSourceField("foo"); ssb.addSourceField("foo2"); - ssb.addDocField("bar"); - ssb.addDocField("bar2"); + ssb.addDocField("bar", null); + ssb.addDocField("bar2", null); final Script s = new Script("eggplant"); ssb.addScriptField("baz", s); final Script s2 = new Script("potato"); @@ -35,7 +35,7 @@ public void testSqlSourceBuilder() { assertTrue(source.trackScores()); FetchSourceContext fsc = source.fetchSource(); assertThat(Arrays.asList(fsc.includes()), contains("foo", "foo2")); - assertThat(source.docValueFields(), contains("bar", "bar2")); + assertThat(source.docValueFields().stream().map(ff -> ff.field).collect(Collectors.toList()), contains("bar", "bar2")); Map scriptFields = source.scriptFields() .stream() .collect(Collectors.toMap(SearchSourceBuilder.ScriptField::fieldName, SearchSourceBuilder.ScriptField::script)); diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/execution/search/extractor/ComputingExtractorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/execution/search/extractor/ComputingExtractorTests.java index 74721eca22af1..375de112fe878 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/execution/search/extractor/ComputingExtractorTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/execution/search/extractor/ComputingExtractorTests.java @@ -70,7 +70,7 @@ protected ComputingExtractor mutateInstance(ComputingExtractor instance) throws public void testGet() { String fieldName = randomAlphaOfLength(5); ChainingProcessor extractor = new ChainingProcessor( - new HitExtractorProcessor(new FieldHitExtractor(fieldName, true)), + new HitExtractorProcessor(new FieldHitExtractor(fieldName, null, true)), new MathProcessor(MathOperation.LOG)); int times = between(1, 1000); diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/execution/search/extractor/FieldHitExtractorTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/execution/search/extractor/FieldHitExtractorTests.java index de36969898c20..a9e1349e8316b 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/execution/search/extractor/FieldHitExtractorTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/execution/search/extractor/FieldHitExtractorTests.java @@ -14,6 +14,9 @@ import org.elasticsearch.test.AbstractWireSerializingTestCase; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.xpack.sql.SqlException; +import org.elasticsearch.xpack.sql.type.DataType; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; import java.io.IOException; import java.util.ArrayList; @@ -31,7 +34,7 @@ public class FieldHitExtractorTests extends AbstractWireSerializingTestCase instanceReader() { @Override protected FieldHitExtractor mutateInstance(FieldHitExtractor instance) throws IOException { - return new FieldHitExtractor(instance.fieldName() + "mutated", true, instance.hitName()); + return new FieldHitExtractor(instance.fieldName() + "mutated", null, true, instance.hitName()); } @AwaitsFix(bugUrl = "implement after we're sure of the InnerHitExtractor's implementation") @@ -60,7 +63,7 @@ public void testGetDottedValueWithDocValues() { String child = randomAlphaOfLength(5); String fieldName = grandparent + "." + parent + "." + child; - FieldHitExtractor extractor = new FieldHitExtractor(fieldName, true); + FieldHitExtractor extractor = new FieldHitExtractor(fieldName, null, true); int times = between(1, 1000); for (int i = 0; i < times; i++) { @@ -84,7 +87,7 @@ public void testGetDottedValueWithSource() throws Exception { String child = randomAlphaOfLength(5); String fieldName = grandparent + "." + parent + "." + child; - FieldHitExtractor extractor = new FieldHitExtractor(fieldName, false); + FieldHitExtractor extractor = new FieldHitExtractor(fieldName, null, false); int times = between(1, 1000); for (int i = 0; i < times; i++) { @@ -123,7 +126,7 @@ public void testGetDottedValueWithSource() throws Exception { public void testGetDocValue() { String fieldName = randomAlphaOfLength(5); - FieldHitExtractor extractor = new FieldHitExtractor(fieldName, true); + FieldHitExtractor extractor = new FieldHitExtractor(fieldName, null, true); int times = between(1, 1000); for (int i = 0; i < times; i++) { @@ -139,9 +142,19 @@ public void testGetDocValue() { } } + public void testGetDate() { + long millis = 1526467911780L; + List documentFieldValues = Collections.singletonList(Long.toString(millis)); + SearchHit hit = new SearchHit(1); + DocumentField field = new DocumentField("my_date_field", documentFieldValues); + hit.fields(singletonMap("my_date_field", field)); + FieldHitExtractor extractor = new FieldHitExtractor("my_date_field", DataType.DATE, true); + assertEquals(new DateTime(millis, DateTimeZone.UTC), extractor.extract(hit)); + } + public void testGetSource() throws IOException { String fieldName = randomAlphaOfLength(5); - FieldHitExtractor extractor = new FieldHitExtractor(fieldName, false); + FieldHitExtractor extractor = new FieldHitExtractor(fieldName, null, false); int times = between(1, 1000); for (int i = 0; i < times; i++) { @@ -164,12 +177,12 @@ public void testGetSource() throws IOException { } public void testToString() { - assertEquals("hit.field@hit", new FieldHitExtractor("hit.field", true, "hit").toString()); + assertEquals("hit.field@hit", new FieldHitExtractor("hit.field", null, true, "hit").toString()); } public void testMultiValuedDocValue() { String fieldName = randomAlphaOfLength(5); - FieldHitExtractor fe = new FieldHitExtractor(fieldName, true); + FieldHitExtractor fe = new FieldHitExtractor(fieldName, null, true); SearchHit hit = new SearchHit(1); DocumentField field = new DocumentField(fieldName, asList("a", "b")); hit.fields(singletonMap(fieldName, field)); @@ -179,7 +192,7 @@ public void testMultiValuedDocValue() { public void testMultiValuedSourceValue() throws IOException { String fieldName = randomAlphaOfLength(5); - FieldHitExtractor fe = new FieldHitExtractor(fieldName, false); + FieldHitExtractor fe = new FieldHitExtractor(fieldName, null, false); SearchHit hit = new SearchHit(1); XContentBuilder source = JsonXContent.contentBuilder(); source.startObject(); { @@ -194,7 +207,7 @@ public void testMultiValuedSourceValue() throws IOException { public void testSingleValueArrayInSource() throws IOException { String fieldName = randomAlphaOfLength(5); - FieldHitExtractor fe = new FieldHitExtractor(fieldName, false); + FieldHitExtractor fe = new FieldHitExtractor(fieldName, null, false); SearchHit hit = new SearchHit(1); XContentBuilder source = JsonXContent.contentBuilder(); Object value = randomValue(); @@ -208,14 +221,14 @@ public void testSingleValueArrayInSource() throws IOException { } public void testExtractSourcePath() { - FieldHitExtractor fe = new FieldHitExtractor("a.b.c", false); + FieldHitExtractor fe = new FieldHitExtractor("a.b.c", null, false); Object value = randomValue(); Map map = singletonMap("a", singletonMap("b", singletonMap("c", value))); assertThat(fe.extractFromSource(map), is(value)); } public void testExtractSourceIncorrectPath() { - FieldHitExtractor fe = new FieldHitExtractor("a.b.c.d", false); + FieldHitExtractor fe = new FieldHitExtractor("a.b.c.d", null, false); Object value = randomNonNullValue(); Map map = singletonMap("a", singletonMap("b", singletonMap("c", value))); SqlException ex = expectThrows(SqlException.class, () -> fe.extractFromSource(map)); @@ -223,7 +236,7 @@ public void testExtractSourceIncorrectPath() { } public void testMultiValuedSource() { - FieldHitExtractor fe = new FieldHitExtractor("a", false); + FieldHitExtractor fe = new FieldHitExtractor("a", null, false); Object value = randomValue(); Map map = singletonMap("a", asList(value, value)); SqlException ex = expectThrows(SqlException.class, () -> fe.extractFromSource(map)); diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/sql/translate.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/sql/translate.yml index b3d93e5298810..f47ea2d4d7e15 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/sql/translate.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/sql/translate.yml @@ -1,5 +1,10 @@ --- "Translate SQL": + - skip: + version: " - 6.3.99" + reason: format option was added in 6.4 + features: warnings + - do: bulk: refresh: true @@ -23,7 +28,8 @@ - str excludes: [] docvalue_fields: - - int + - field: int + format: use_field_mapping sort: - int: order: asc diff --git a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/transport/actions/service/TransportWatcherServiceAction.java b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/transport/actions/service/TransportWatcherServiceAction.java index fa78208494f94..6b2bb26ef45f0 100644 --- a/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/transport/actions/service/TransportWatcherServiceAction.java +++ b/x-pack/plugin/watcher/src/main/java/org/elasticsearch/xpack/watcher/transport/actions/service/TransportWatcherServiceAction.java @@ -23,6 +23,7 @@ import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.XPackPlugin; import org.elasticsearch.xpack.core.watcher.WatcherMetaData; import org.elasticsearch.xpack.core.watcher.transport.actions.service.WatcherServiceAction; import org.elasticsearch.xpack.core.watcher.transport.actions.service.WatcherServiceRequest; @@ -86,6 +87,8 @@ protected WatcherServiceResponse newResponse(boolean acknowledged) { @Override public ClusterState execute(ClusterState clusterState) { + XPackPlugin.checkReadyForXPackCustomMetadata(clusterState); + WatcherMetaData newWatcherMetaData = new WatcherMetaData(manuallyStopped); WatcherMetaData currentMetaData = clusterState.metaData().custom(WatcherMetaData.TYPE); diff --git a/x-pack/qa/sql/no-security/src/test/java/org/elasticsearch/xpack/qa/sql/nosecurity/CliExplainIT.java b/x-pack/qa/sql/no-security/src/test/java/org/elasticsearch/xpack/qa/sql/nosecurity/CliExplainIT.java index f913395759c47..63831c2d4dec0 100644 --- a/x-pack/qa/sql/no-security/src/test/java/org/elasticsearch/xpack/qa/sql/nosecurity/CliExplainIT.java +++ b/x-pack/qa/sql/no-security/src/test/java/org/elasticsearch/xpack/qa/sql/nosecurity/CliExplainIT.java @@ -103,7 +103,10 @@ public void testExplainWithWhere() throws IOException { assertThat(readLine(), startsWith(" \"excludes\" : [ ]")); assertThat(readLine(), startsWith(" },")); assertThat(readLine(), startsWith(" \"docvalue_fields\" : [")); - assertThat(readLine(), startsWith(" \"i\"")); + assertThat(readLine(), startsWith(" {")); + assertThat(readLine(), startsWith(" \"field\" : \"i\"")); + assertThat(readLine(), startsWith(" \"format\" : \"use_field_mapping\"")); + assertThat(readLine(), startsWith(" }")); assertThat(readLine(), startsWith(" ],")); assertThat(readLine(), startsWith(" \"sort\" : [")); assertThat(readLine(), startsWith(" {"));