/* * 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.script; import org.elasticsearch.action.search.SearchPhaseExecutionException; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.lookup.IndexField; import org.elasticsearch.search.lookup.IndexFieldTerm; import org.elasticsearch.search.lookup.IndexLookup; import org.elasticsearch.search.lookup.LeafIndexLookup; import org.elasticsearch.search.lookup.TermPosition; import org.elasticsearch.test.ESIntegTestCase; import org.hamcrest.Matchers; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.function.Function; import static java.util.Collections.emptyList; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.greaterThanOrEqualTo; public class IndexLookupIT extends ESIntegTestCase { private static final String INCLUDE_ALL = "_FREQUENCIES|_OFFSETS|_PAYLOADS|_POSITIONS|_CACHE"; private static final int ALL_FLAGS = IndexLookup.FLAG_FREQUENCIES | IndexLookup.FLAG_OFFSETS | IndexLookup.FLAG_PAYLOADS | IndexLookup.FLAG_POSITIONS | IndexLookup.FLAG_CACHE; private static final String INCLUDE_ALL_BUT_CACHE = "_FREQUENCIES|_OFFSETS|_PAYLOADS|_POSITIONS"; private static final int ALL_FLAGS_WITHOUT_CACHE = IndexLookup.FLAG_FREQUENCIES | IndexLookup.FLAG_OFFSETS | IndexLookup.FLAG_PAYLOADS | IndexLookup.FLAG_POSITIONS; private HashMap<String, List<Object>> expectedEndOffsetsArray; private HashMap<String, List<Object>> expectedPayloadsArray; private HashMap<String, List<Object>> expectedPositionsArray; private HashMap<String, List<Object>> expectedStartOffsetsArray; @Override protected Collection<Class<? extends Plugin>> nodePlugins() { return Collections.singleton(CustomScriptPlugin.class); } public static class CustomScriptPlugin extends MockScriptPlugin { @Override @SuppressWarnings("unchecked") protected Map<String, Function<Map<String, Object>, Object>> pluginScripts() { Map<String, Function<Map<String, Object>, Object>> scripts = new HashMap<>(); scripts.put("term = _index['int_payload_field']['c']; term.tf()", vars -> tf(vars, "int_payload_field", "c")); scripts.put("term = _index['int_payload_field']['b']; term.tf()", vars -> tf(vars, "int_payload_field", "b")); scripts.put("Sum the payloads of [float_payload_field][b]", vars -> payloadSum(vars, "float_payload_field", "b")); scripts.put("Sum the payloads of [int_payload_field][b]", vars -> payloadSum(vars, "int_payload_field", "b")); scripts.put("createPositionsArrayScriptIterateTwice[b," + INCLUDE_ALL + ",position]", vars -> createPositionsArrayScriptIterateTwice(vars, "b", ALL_FLAGS, p -> p.position)); scripts.put("createPositionsArrayScriptIterateTwice[b," + INCLUDE_ALL + ",startOffset]", vars -> createPositionsArrayScriptIterateTwice(vars, "b", ALL_FLAGS, p -> p.startOffset)); scripts.put("createPositionsArrayScriptIterateTwice[b," + INCLUDE_ALL + ",endOffset]", vars -> createPositionsArrayScriptIterateTwice(vars, "b", ALL_FLAGS, p -> p.endOffset)); scripts.put("createPositionsArrayScriptIterateTwice[b," + INCLUDE_ALL + ",payloadAsInt(-1)]", vars -> createPositionsArrayScriptIterateTwice(vars, "b", ALL_FLAGS, p -> p.payloadAsInt(-1))); scripts.put("createPositionsArrayScriptIterateTwice[b,_FREQUENCIES|_OFFSETS|_PAYLOADS|_POSITIONS,position]", vars -> createPositionsArrayScriptIterateTwice(vars, "b", ALL_FLAGS_WITHOUT_CACHE, p -> p.position)); scripts.put("createPositionsArrayScriptIterateTwice[b,_FREQUENCIES|_OFFSETS|_PAYLOADS|_POSITIONS,startOffset]", vars -> createPositionsArrayScriptIterateTwice(vars, "b", ALL_FLAGS_WITHOUT_CACHE, p -> p.startOffset)); scripts.put("createPositionsArrayScriptIterateTwice[b,_FREQUENCIES|_OFFSETS|_PAYLOADS|_POSITIONS,endOffset]", vars -> createPositionsArrayScriptIterateTwice(vars, "b", ALL_FLAGS_WITHOUT_CACHE, p -> p.endOffset)); scripts.put("createPositionsArrayScriptIterateTwice[b,_FREQUENCIES|_OFFSETS|_PAYLOADS|_POSITIONS,payloadAsInt(-1)]", vars -> createPositionsArrayScriptIterateTwice(vars, "b", ALL_FLAGS_WITHOUT_CACHE, p -> p.payloadAsInt(-1))); scripts.put("createPositionsArrayScriptGetInfoObjectTwice[b,_FREQUENCIES|_OFFSETS|_PAYLOADS|_POSITIONS,position]", vars -> createPositionsArrayScriptGetInfoObjectTwice(vars, "b", ALL_FLAGS_WITHOUT_CACHE, p -> p.position)); scripts.put("createPositionsArrayScriptGetInfoObjectTwice[b,_FREQUENCIES|_OFFSETS|_PAYLOADS|_POSITIONS,startOffset]", vars -> createPositionsArrayScriptGetInfoObjectTwice(vars, "b", ALL_FLAGS_WITHOUT_CACHE, p -> p.startOffset)); scripts.put("createPositionsArrayScriptGetInfoObjectTwice[b,_FREQUENCIES|_OFFSETS|_PAYLOADS|_POSITIONS,endOffset]", vars -> createPositionsArrayScriptGetInfoObjectTwice(vars, "b", ALL_FLAGS_WITHOUT_CACHE, p -> p.endOffset)); scripts.put("createPositionsArrayScriptGetInfoObjectTwice[b,_FREQUENCIES|_OFFSETS|_PAYLOADS|_POSITIONS,payloadAsInt(-1)]", vars -> createPositionsArrayScriptGetInfoObjectTwice(vars, "b", ALL_FLAGS_WITHOUT_CACHE, p -> p.payloadAsInt(-1))); scripts.put("createPositionsArrayScript[int_payload_field,b,_POSITIONS,position]", vars -> createPositionsArrayScript(vars, "int_payload_field", "b", IndexLookup.FLAG_POSITIONS, p -> p.position)); scripts.put("createPositionsArrayScript[int_payload_field,b,_OFFSETS,position]", vars -> createPositionsArrayScript(vars, "int_payload_field", "b", IndexLookup.FLAG_OFFSETS, p -> p.position)); scripts.put("createPositionsArrayScript[int_payload_field,b,_OFFSETS,startOffset]", vars -> createPositionsArrayScript(vars, "int_payload_field", "b", IndexLookup.FLAG_OFFSETS, p -> p.startOffset)); scripts.put("createPositionsArrayScript[int_payload_field,b,_OFFSETS,endOffset]", vars -> createPositionsArrayScript(vars, "int_payload_field", "b", IndexLookup.FLAG_OFFSETS, p -> p.endOffset)); scripts.put("createPositionsArrayScript[int_payload_field,b,_OFFSETS,payloadAsInt(-1)]", vars -> createPositionsArrayScript(vars, "int_payload_field", "b", IndexLookup.FLAG_OFFSETS, p -> p.payloadAsInt(-1))); scripts.put("createPositionsArrayScript[int_payload_field,b,_PAYLOADS,position]", vars -> createPositionsArrayScript(vars, "int_payload_field", "b", IndexLookup.FLAG_PAYLOADS, p -> p.position)); scripts.put("createPositionsArrayScript[int_payload_field,b,_PAYLOADS,startOffset]", vars -> createPositionsArrayScript(vars, "int_payload_field", "b", IndexLookup.FLAG_PAYLOADS, p -> p.startOffset)); scripts.put("createPositionsArrayScript[int_payload_field,b,_PAYLOADS,endOffset]", vars -> createPositionsArrayScript(vars, "int_payload_field", "b", IndexLookup.FLAG_PAYLOADS, p -> p.endOffset)); scripts.put("createPositionsArrayScript[int_payload_field,b,_PAYLOADS,payloadAsInt(-1)]", vars -> createPositionsArrayScript(vars, "int_payload_field", "b", IndexLookup.FLAG_PAYLOADS, p -> p.payloadAsInt(-1))); int posoffpay = IndexLookup.FLAG_POSITIONS|IndexLookup.FLAG_OFFSETS|IndexLookup.FLAG_PAYLOADS; scripts.put("createPositionsArrayScript[int_payload_field,b,_POSITIONS|_OFFSETS|_PAYLOADS,position]", vars -> createPositionsArrayScript(vars, "int_payload_field", "b", posoffpay, p -> p.position)); scripts.put("createPositionsArrayScript[int_payload_field,b,_POSITIONS|_OFFSETS|_PAYLOADS,startOffset]", vars -> createPositionsArrayScript(vars, "int_payload_field", "b", posoffpay, p -> p.startOffset)); scripts.put("createPositionsArrayScript[int_payload_field,b,_POSITIONS|_OFFSETS|_PAYLOADS,endOffset]", vars -> createPositionsArrayScript(vars, "int_payload_field", "b", posoffpay, p -> p.endOffset)); scripts.put("createPositionsArrayScript[int_payload_field,b,_POSITIONS|_OFFSETS|_PAYLOADS,payloadAsInt(-1)]", vars -> createPositionsArrayScript(vars, "int_payload_field", "b", posoffpay, p -> p.payloadAsInt(-1))); scripts.put("createPositionsArrayScript[int_payload_field,b,_FREQUENCIES|_OFFSETS|_PAYLOADS|_POSITIONS,position]", vars -> createPositionsArrayScript(vars, "int_payload_field", "b", ALL_FLAGS_WITHOUT_CACHE, p -> p.position)); scripts.put("createPositionsArrayScript[int_payload_field,b,_FREQUENCIES|_OFFSETS|_PAYLOADS|_POSITIONS,startOffset]", vars -> createPositionsArrayScript(vars, "int_payload_field", "b", ALL_FLAGS_WITHOUT_CACHE, p -> p.startOffset)); scripts.put("createPositionsArrayScript[int_payload_field,b,_FREQUENCIES|_OFFSETS|_PAYLOADS|_POSITIONS,endOffset]", vars -> createPositionsArrayScript(vars, "int_payload_field", "b", ALL_FLAGS_WITHOUT_CACHE, p -> p.endOffset)); scripts.put("createPositionsArrayScript[int_payload_field,b,_FREQUENCIES|_OFFSETS|_PAYLOADS|_POSITIONS,payloadAsInt(-1)]", vars -> createPositionsArrayScript(vars, "int_payload_field", "b", ALL_FLAGS_WITHOUT_CACHE, p -> p.payloadAsInt(-1))); scripts.put("createPositionsArrayScript" + "[float_payload_field,b," + INCLUDE_ALL + ",payloadAsFloat(-1)]", vars -> createPositionsArrayScript(vars,"float_payload_field", "b", ALL_FLAGS, p -> p.payloadAsFloat(-1))); scripts.put("createPositionsArrayScript" + "[string_payload_field,b," + INCLUDE_ALL + ",payloadAsString()]", vars -> createPositionsArrayScript(vars,"string_payload_field", "b", ALL_FLAGS, TermPosition::payloadAsString)); scripts.put("createPositionsArrayScript" + "[int_payload_field,c," + INCLUDE_ALL + ",payloadAsInt(-1)]", vars -> createPositionsArrayScript(vars,"int_payload_field", "c", ALL_FLAGS, p -> p.payloadAsInt(-1))); // Call with different flags twice, equivalent to: // term = _index['int_payload_field']['b']; return _index['int_payload_field'].get('b', _POSITIONS).tf(); scripts.put("Call with different flags twice", vars -> { LeafIndexLookup leafIndexLookup = (LeafIndexLookup) vars.get("_index"); IndexField indexField = leafIndexLookup.get("int_payload_field"); // 1st call indexField.get("b"); try { // 2nd call, must throws an exception return indexField.get("b", IndexLookup.FLAG_POSITIONS).tf(); } catch (IOException e) { throw new ScriptException(e.getMessage(), e, emptyList(), "Call with different flags twice", CustomScriptPlugin.NAME); } }); // Call with same flags twice: equivalent to: // term = _index['int_payload_field'].get('b', _POSITIONS | _FREQUENCIES);return _index['int_payload_field']['b'].tf(); scripts.put("Call with same flags twice", vars -> { LeafIndexLookup leafIndexLookup = (LeafIndexLookup) vars.get("_index"); IndexField indexField = leafIndexLookup.get("int_payload_field"); // 1st call indexField.get("b", IndexLookup.FLAG_POSITIONS | IndexLookup.FLAG_FREQUENCIES); try { // 2nd call, must throws an exception return indexField.get("b").tf(); } catch (IOException e) { throw new ScriptException(e.getMessage(), e, emptyList(), "Call with same flags twice", CustomScriptPlugin.NAME); } }); // get the number of all docs scripts.put("_index.numDocs()", vars -> ((LeafIndexLookup) vars.get("_index")).numDocs()); // get the number of docs with field float_payload_field scripts.put("_index['float_payload_field'].docCount()", vars -> indexFieldScript(vars, "float_payload_field", indexField -> { try { return indexField.docCount(); } catch (IOException e) { throw new ScriptException(e.getMessage(), e, emptyList(), "docCount()", CustomScriptPlugin.NAME); } })); // corner case: what if the field does not exist? scripts.put("_index['non_existent_field'].docCount()", vars -> indexFieldScript(vars, "non_existent_field", indexField -> { try { return indexField.docCount(); } catch (IOException e) { throw new ScriptException(e.getMessage(), e, emptyList(), "docCount()", CustomScriptPlugin.NAME); } })); // get the number of all tokens in all docs scripts.put("_index['float_payload_field'].sumttf()", vars -> indexFieldScript(vars, "float_payload_field", indexField -> { try { return indexField.sumttf(); } catch (IOException e) { throw new ScriptException(e.getMessage(), e, emptyList(), "sumttf()", CustomScriptPlugin.NAME); } })); // corner case get the number of all tokens in all docs for non existent // field scripts.put("_index['non_existent_field'].sumttf()", vars -> indexFieldScript(vars, "non_existent_field", indexField -> { try { return indexField.sumttf(); } catch (IOException e) { throw new ScriptException(e.getMessage(), e, emptyList(), "sumttf()", CustomScriptPlugin.NAME); } })); // get the sum of doc freqs in all docs scripts.put("_index['float_payload_field'].sumdf()", vars -> indexFieldScript(vars, "float_payload_field", indexField -> { try { return indexField.sumdf(); } catch (IOException e) { throw new ScriptException(e.getMessage(), e, emptyList(), "sumdf()", CustomScriptPlugin.NAME); } })); // get the sum of doc freqs in all docs for non existent field scripts.put("_index['non_existent_field'].sumdf()", vars -> indexFieldScript(vars, "non_existent_field", indexField -> { try { return indexField.sumdf(); } catch (IOException e) { throw new ScriptException(e.getMessage(), e, emptyList(), "sumdf()", CustomScriptPlugin.NAME); } })); // check term frequencies for 'a' scripts.put("term = _index['float_payload_field']['a']; if (term != null) {term.tf()}", vars -> indexFieldTermScript(vars, "float_payload_field", "a", indexFieldTerm -> { try { if (indexFieldTerm != null) { return indexFieldTerm.tf(); } } catch (IOException e) { throw new ScriptException(e.getMessage(), e, emptyList(), "term.tf()", CustomScriptPlugin.NAME); } return null; })); // check doc frequencies for 'c' scripts.put("term = _index['float_payload_field']['c']; if (term != null) {term.df()}", vars -> indexFieldTermScript(vars, "float_payload_field", "c", indexFieldTerm -> { try { if (indexFieldTerm != null) { return indexFieldTerm.df(); } } catch (IOException e) { throw new ScriptException(e.getMessage(), e, emptyList(), "term.df()", CustomScriptPlugin.NAME); } return null; })); // check doc frequencies for term that does not exist scripts.put("term = _index['float_payload_field']['non_existent_term']; if (term != null) {term.df()}", vars -> indexFieldTermScript(vars, "float_payload_field", "non_existent_term", indexFieldTerm -> { try { if (indexFieldTerm != null) { return indexFieldTerm.df(); } } catch (IOException e) { throw new ScriptException(e.getMessage(), e, emptyList(), "term.df()", CustomScriptPlugin.NAME); } return null; })); // check doc frequencies for term that does not exist scripts.put("term = _index['non_existent_field']['non_existent_term']; if (term != null) {term.tf()}", vars -> indexFieldTermScript(vars, "non_existent_field", "non_existent_term", indexFieldTerm -> { try { if (indexFieldTerm != null) { return indexFieldTerm.tf(); } } catch (IOException e) { throw new ScriptException(e.getMessage(), e, emptyList(), "term.tf()", CustomScriptPlugin.NAME); } return null; })); // check total term frequencies for 'a' scripts.put("term = _index['float_payload_field']['a']; if (term != null) {term.ttf()}", vars -> indexFieldTermScript(vars, "float_payload_field", "a", indexFieldTerm -> { try { if (indexFieldTerm != null) { return indexFieldTerm.ttf(); } } catch (IOException e) { throw new ScriptException(e.getMessage(), e, emptyList(), "term.ttf()", CustomScriptPlugin.NAME); } return null; })); return scripts; } @SuppressWarnings("unchecked") static Object indexFieldScript(Map<String, Object> vars, String fieldName, Function<IndexField, Object> fn) { LeafIndexLookup leafIndexLookup = (LeafIndexLookup) vars.get("_index"); return fn.apply(leafIndexLookup.get(fieldName)); } @SuppressWarnings("unchecked") static Object indexFieldTermScript(Map<String, Object> vars, String fieldName, String term, Function<IndexFieldTerm, Object> fn) { return indexFieldScript(vars, fieldName, indexField -> fn.apply(indexField.get(term))); } @SuppressWarnings("unchecked") static Object tf(Map<String, Object> vars, String fieldName, String term) { return indexFieldTermScript(vars, fieldName, term, indexFieldTerm -> { try { return indexFieldTerm.tf(); } catch (IOException e) { throw new RuntimeException("Mocked script error when retrieving TF for [" + fieldName + "][" + term + "]"); } }); } // Sum the payloads for a given field term, equivalent to: // term = _index['float_payload_field'].get('b', _FREQUENCIES|_OFFSETS|_PAYLOADS|_POSITIONS|_CACHE); // payloadSum=0; // for (pos in term) { // payloadSum += pos.payloadAsInt(0) // }; // return payloadSum; @SuppressWarnings("unchecked") static Object payloadSum(Map<String, Object> vars, String fieldName, String term) { return indexFieldScript(vars, fieldName, indexField -> { IndexFieldTerm indexFieldTerm = indexField.get(term, IndexLookup.FLAG_FREQUENCIES | IndexLookup.FLAG_OFFSETS | IndexLookup.FLAG_PAYLOADS | IndexLookup.FLAG_POSITIONS | IndexLookup.FLAG_CACHE); int payloadSum = 0; for (TermPosition position : indexFieldTerm) { payloadSum += position.payloadAsInt(0); } return payloadSum; }); } @SuppressWarnings("unchecked") static List<Object> createPositionsArrayScriptGetInfoObjectTwice(Map<String, Object> vars, String term, int flags, Function<TermPosition, Object> fn) { LeafIndexLookup leafIndexLookup = (LeafIndexLookup) vars.get("_index"); IndexField indexField = leafIndexLookup.get("int_payload_field"); // 1st call IndexFieldTerm indexFieldTerm = indexField.get(term, flags); List<Object> array = new ArrayList<>(); for (TermPosition position : indexFieldTerm) { array.add(fn.apply(position)); } // 2nd call indexField.get(term, flags); array = new ArrayList<>(); for (TermPosition position : indexFieldTerm) { array.add(fn.apply(position)); } return array; } @SuppressWarnings("unchecked") static List<Object> createPositionsArrayScriptIterateTwice(Map<String, Object> vars, String term, int flags, Function<TermPosition, Object> fn) { LeafIndexLookup leafIndexLookup = (LeafIndexLookup) vars.get("_index"); IndexField indexField = leafIndexLookup.get("int_payload_field"); IndexFieldTerm indexFieldTerm = indexField.get(term, flags); // 1st iteration List<Object> array = new ArrayList<>(); for (TermPosition position : indexFieldTerm) { array.add(fn.apply(position)); } // 2nd iteration array = new ArrayList<>(); for (TermPosition position : indexFieldTerm) { array.add(fn.apply(position)); } return array; } @SuppressWarnings("unchecked") static List<Object> createPositionsArrayScript(Map<String, Object> vars, String field, String term, int flags, Function<TermPosition, Object> fn) { LeafIndexLookup leafIndexLookup = (LeafIndexLookup) vars.get("_index"); IndexField indexField = leafIndexLookup.get(field); IndexFieldTerm indexFieldTerm = indexField.get(term, flags); List<Object> array = new ArrayList<>(); for (TermPosition position : indexFieldTerm) { array.add(fn.apply(position)); } return array; } } void initTestData() throws InterruptedException, ExecutionException, IOException { HashMap<String, List<Object>> emptyArray = new HashMap<>(); List<Object> empty1 = new ArrayList<>(); empty1.add(-1); empty1.add(-1); emptyArray.put("1", empty1); List<Object> empty2 = new ArrayList<>(); empty2.add(-1); empty2.add(-1); emptyArray.put("2", empty2); List<Object> empty3 = new ArrayList<>(); empty3.add(-1); empty3.add(-1); emptyArray.put("3", empty3); expectedPositionsArray = new HashMap<>(); List<Object> pos1 = new ArrayList<>(); pos1.add(1); pos1.add(2); expectedPositionsArray.put("1", pos1); List<Object> pos2 = new ArrayList<>(); pos2.add(0); pos2.add(1); expectedPositionsArray.put("2", pos2); List<Object> pos3 = new ArrayList<>(); pos3.add(0); pos3.add(4); expectedPositionsArray.put("3", pos3); expectedPayloadsArray = new HashMap<>(); List<Object> pay1 = new ArrayList<>(); pay1.add(2); pay1.add(3); expectedPayloadsArray.put("1", pay1); List<Object> pay2 = new ArrayList<>(); pay2.add(1); pay2.add(2); expectedPayloadsArray.put("2", pay2); List<Object> pay3 = new ArrayList<>(); pay3.add(1); pay3.add(-1); expectedPayloadsArray.put("3", pay3); /* * "a|1 b|2 b|3 c|4 d " "b|1 b|2 c|3 d|4 a " "b|1 c|2 d|3 a|4 b " */ expectedStartOffsetsArray = new HashMap<>(); List<Object> starts1 = new ArrayList<>(); starts1.add(4); starts1.add(8); expectedStartOffsetsArray.put("1", starts1); List<Object> starts2 = new ArrayList<>(); starts2.add(0); starts2.add(4); expectedStartOffsetsArray.put("2", starts2); List<Object> starts3 = new ArrayList<>(); starts3.add(0); starts3.add(16); expectedStartOffsetsArray.put("3", starts3); expectedEndOffsetsArray = new HashMap<>(); List<Object> ends1 = new ArrayList<>(); ends1.add(7); ends1.add(11); expectedEndOffsetsArray.put("1", ends1); List<Object> ends2 = new ArrayList<>(); ends2.add(3); ends2.add(7); expectedEndOffsetsArray.put("2", ends2); List<Object> ends3 = new ArrayList<>(); ends3.add(3); ends3.add(17); expectedEndOffsetsArray.put("3", ends3); XContentBuilder mapping = XContentFactory.jsonBuilder() .startObject() .startObject("type1") .startObject("properties") .startObject("int_payload_field") .field("type", "text") .field("index_options", "offsets") .field("analyzer", "payload_int") .endObject() .endObject() .endObject() .endObject(); assertAcked(prepareCreate("test").addMapping("type1", mapping).setSettings( Settings.builder() .put(indexSettings()) .put("index.analysis.analyzer.payload_int.tokenizer", "whitespace") .putArray("index.analysis.analyzer.payload_int.filter", "delimited_int") .put("index.analysis.filter.delimited_int.delimiter", "|") .put("index.analysis.filter.delimited_int.encoding", "int") .put("index.analysis.filter.delimited_int.type", "delimited_payload_filter"))); indexRandom(true, client().prepareIndex("test", "type1", "1").setSource("int_payload_field", "a|1 b|2 b|3 c|4 d "), client() .prepareIndex("test", "type1", "2").setSource("int_payload_field", "b|1 b|2 c|3 d|4 a "), client().prepareIndex("test", "type1", "3").setSource("int_payload_field", "b|1 c|2 d|3 a|4 b ")); ensureGreen(); } public void testTwoScripts() throws Exception { initTestData(); Script scriptFieldScript = createScript("term = _index['int_payload_field']['c']; term.tf()"); Script scoreScript = createScript("term = _index['int_payload_field']['b']; term.tf()"); Map<String, Object> expectedResultsField = new HashMap<>(); expectedResultsField.put("1", 1); expectedResultsField.put("2", 1); expectedResultsField.put("3", 1); Map<String, Object> expectedResultsScore = new HashMap<>(); expectedResultsScore.put("1", 2f); expectedResultsScore.put("2", 2f); expectedResultsScore.put("3", 2f); checkOnlyFunctionScore(scoreScript, expectedResultsScore, 3); checkValueInEachDocWithFunctionScore(scriptFieldScript, expectedResultsField, scoreScript, expectedResultsScore, 3); } public void testCallWithDifferentFlagsFails() throws Exception { initTestData(); final int numPrimaries = getNumShards("test").numPrimaries; final String expectedError = "You must call get with all required flags! " + "Instead of _index['int_payload_field'].get('b', _FREQUENCIES) and _index['int_payload_field'].get('b', _POSITIONS)" + " call _index['int_payload_field'].get('b', _FREQUENCIES | _POSITIONS) once]"; // should throw an exception, we cannot call with different flags twice // if the flags of the second call were not included in the first call. Script script = createScript("Call with different flags twice"); try { SearchResponse response = client().prepareSearch("test") .setQuery(QueryBuilders.matchAllQuery()) .addScriptField("tvtest", script) .get(); // (partial) success when at least one shard succeeds assertThat(numPrimaries, greaterThan(response.getShardFailures().length)); assertThat(response.getFailedShards(), greaterThanOrEqualTo(1)); for (ShardSearchFailure failure : response.getShardFailures()) { assertThat(failure.reason(), containsString(expectedError)); } } catch (SearchPhaseExecutionException e) { // Exception thrown when *all* shards fail assertThat(numPrimaries, equalTo(e.shardFailures().length)); for (ShardSearchFailure failure : e.shardFailures()) { assertThat(failure.reason(), containsString(expectedError)); } } // Should not throw an exception this way round script = createScript("Call with same flags twice"); assertThat(client().prepareSearch("test") .setQuery(QueryBuilders.matchAllQuery()) .addScriptField("tvtest", script) .get().getHits().getTotalHits(), greaterThan(0L)); } private void checkOnlyFunctionScore(Script scoreScript, Map<String, Object> expectedScore, int numExpectedDocs) { SearchResponse sr = client().prepareSearch("test") .setQuery(QueryBuilders.functionScoreQuery(ScoreFunctionBuilders.scriptFunction(scoreScript))).execute() .actionGet(); assertHitCount(sr, numExpectedDocs); for (SearchHit hit : sr.getHits().getHits()) { assertThat("for doc " + hit.getId(), ((Float) expectedScore.get(hit.getId())).doubleValue(), Matchers.closeTo(hit.getScore(), 1.e-4)); } } public void testDocumentationExample() throws Exception { initTestData(); Script script = createScript("Sum the payloads of [float_payload_field][b]"); // non existing field: sum should be 0 HashMap<String, Object> zeroArray = new HashMap<>(); zeroArray.put("1", 0); zeroArray.put("2", 0); zeroArray.put("3", 0); checkValueInEachDoc(script, zeroArray, 3); script = createScript("Sum the payloads of [int_payload_field][b]"); // existing field: sums should be as here: zeroArray.put("1", 5); zeroArray.put("2", 3); zeroArray.put("3", 1); checkValueInEachDoc(script, zeroArray, 3); } public void testIteratorAndRecording() throws Exception { initTestData(); // call twice with record: should work as expected Script script = createPositionsArrayScriptIterateTwice("b", INCLUDE_ALL, "position"); checkArrayValsInEachDoc(script, expectedPositionsArray, 3); script = createPositionsArrayScriptIterateTwice("b", INCLUDE_ALL, "startOffset"); checkArrayValsInEachDoc(script, expectedStartOffsetsArray, 3); script = createPositionsArrayScriptIterateTwice("b", INCLUDE_ALL, "endOffset"); checkArrayValsInEachDoc(script, expectedEndOffsetsArray, 3); script = createPositionsArrayScriptIterateTwice("b", INCLUDE_ALL, "payloadAsInt(-1)"); checkArrayValsInEachDoc(script, expectedPayloadsArray, 3); // no record and get iterator twice: should fail script = createPositionsArrayScriptIterateTwice("b", INCLUDE_ALL_BUT_CACHE, "position"); checkExceptions(script); script = createPositionsArrayScriptIterateTwice("b", INCLUDE_ALL_BUT_CACHE, "startOffset"); checkExceptions(script); script = createPositionsArrayScriptIterateTwice("b", INCLUDE_ALL_BUT_CACHE, "endOffset"); checkExceptions(script); script = createPositionsArrayScriptIterateTwice("b", INCLUDE_ALL_BUT_CACHE, "payloadAsInt(-1)"); checkExceptions(script); // no record and get termObject twice and iterate: should fail script = createPositionsArrayScriptGetInfoObjectTwice("b", INCLUDE_ALL_BUT_CACHE, "position"); checkExceptions(script); script = createPositionsArrayScriptGetInfoObjectTwice("b", INCLUDE_ALL_BUT_CACHE, "startOffset"); checkExceptions(script); script = createPositionsArrayScriptGetInfoObjectTwice("b", INCLUDE_ALL_BUT_CACHE, "endOffset"); checkExceptions(script); script = createPositionsArrayScriptGetInfoObjectTwice("b", INCLUDE_ALL_BUT_CACHE, "payloadAsInt(-1)"); checkExceptions(script); } private Script createPositionsArrayScriptGetInfoObjectTwice(String term, String flags, String what) { return createScript("createPositionsArrayScriptGetInfoObjectTwice[" + term + "," + flags + "," + what + "]"); } private Script createPositionsArrayScriptIterateTwice(String term, String flags, String what) { return createScript("createPositionsArrayScriptIterateTwice[" + term + "," + flags + "," + what + "]"); } private Script createPositionsArrayScript(String field, String term, String flags, String what) { return createScript("createPositionsArrayScript[" + field + "," + term + "," + flags + "," + what + "]"); } private Script createPositionsArrayScriptDefaultGet(String field, String term, String what) { return createScript("createPositionsArrayScriptDefaultGet[" + field + "," + term + "," + what + "]"); } private Script createScript(String script) { return new Script(ScriptType.INLINE, CustomScriptPlugin.NAME, script, Collections.emptyMap()); } public void testFlags() throws Exception { initTestData(); // check default flag Script script = createPositionsArrayScriptDefaultGet("int_payload_field", "b", "position"); // there should be no positions /* TODO: the following tests fail with the new postings enum apis because of a bogus assert in BlockDocsEnum checkArrayValsInEachDoc(script, emptyArray, 3); script = createPositionsArrayScriptDefaultGet("int_payload_field", "b", "startOffset"); // there should be no offsets checkArrayValsInEachDoc(script, emptyArray, 3); script = createPositionsArrayScriptDefaultGet("int_payload_field", "b", "endOffset"); // there should be no offsets checkArrayValsInEachDoc(script, emptyArray, 3); script = createPositionsArrayScriptDefaultGet("int_payload_field", "b", "payloadAsInt(-1)"); // there should be no payload checkArrayValsInEachDoc(script, emptyArray, 3); // check FLAG_FREQUENCIES flag script = createPositionsArrayScript("int_payload_field", "b", "_FREQUENCIES", "position"); // there should be no positions checkArrayValsInEachDoc(script, emptyArray, 3); script = createPositionsArrayScript("int_payload_field", "b", "_FREQUENCIES", "startOffset"); // there should be no offsets checkArrayValsInEachDoc(script, emptyArray, 3); script = createPositionsArrayScript("int_payload_field", "b", "_FREQUENCIES", "endOffset"); // there should be no offsets checkArrayValsInEachDoc(script, emptyArray, 3); script = createPositionsArrayScript("int_payload_field", "b", "_FREQUENCIES", "payloadAsInt(-1)"); // there should be no payloads checkArrayValsInEachDoc(script, emptyArray, 3);*/ // check FLAG_POSITIONS flag script = createPositionsArrayScript("int_payload_field", "b", "_POSITIONS", "position"); // there should be positions checkArrayValsInEachDoc(script, expectedPositionsArray, 3); /* TODO: these tests make a bogus assumption that asking for positions will return only positions script = createPositionsArrayScript("int_payload_field", "b", "_POSITIONS", "startOffset"); // there should be no offsets checkArrayValsInEachDoc(script, emptyArray, 3); script = createPositionsArrayScript("int_payload_field", "b", "_POSITIONS", "endOffset"); // there should be no offsets checkArrayValsInEachDoc(script, emptyArray, 3); script = createPositionsArrayScript("int_payload_field", "b", "_POSITIONS", "payloadAsInt(-1)"); // there should be no payloads checkArrayValsInEachDoc(script, emptyArray, 3);*/ // check FLAG_OFFSETS flag script = createPositionsArrayScript("int_payload_field", "b", "_OFFSETS", "position"); // there should be positions and s forth ... checkArrayValsInEachDoc(script, expectedPositionsArray, 3); script = createPositionsArrayScript("int_payload_field", "b", "_OFFSETS", "startOffset"); checkArrayValsInEachDoc(script, expectedStartOffsetsArray, 3); script = createPositionsArrayScript("int_payload_field", "b", "_OFFSETS", "endOffset"); checkArrayValsInEachDoc(script, expectedEndOffsetsArray, 3); script = createPositionsArrayScript("int_payload_field", "b", "_OFFSETS", "payloadAsInt(-1)"); checkArrayValsInEachDoc(script, expectedPayloadsArray, 3); // check FLAG_PAYLOADS flag script = createPositionsArrayScript("int_payload_field", "b", "_PAYLOADS", "position"); checkArrayValsInEachDoc(script, expectedPositionsArray, 3); script = createPositionsArrayScript("int_payload_field", "b", "_PAYLOADS", "startOffset"); checkArrayValsInEachDoc(script, expectedStartOffsetsArray, 3); script = createPositionsArrayScript("int_payload_field", "b", "_PAYLOADS", "endOffset"); checkArrayValsInEachDoc(script, expectedEndOffsetsArray, 3); script = createPositionsArrayScript("int_payload_field", "b", "_PAYLOADS", "payloadAsInt(-1)"); checkArrayValsInEachDoc(script, expectedPayloadsArray, 3); // check all flags String allFlags = "_POSITIONS|_OFFSETS|_PAYLOADS"; script = createPositionsArrayScript("int_payload_field", "b", allFlags, "position"); checkArrayValsInEachDoc(script, expectedPositionsArray, 3); script = createPositionsArrayScript("int_payload_field", "b", allFlags, "startOffset"); checkArrayValsInEachDoc(script, expectedStartOffsetsArray, 3); script = createPositionsArrayScript("int_payload_field", "b", allFlags, "endOffset"); checkArrayValsInEachDoc(script, expectedEndOffsetsArray, 3); script = createPositionsArrayScript("int_payload_field", "b", allFlags, "payloadAsInt(-1)"); checkArrayValsInEachDoc(script, expectedPayloadsArray, 3); // check all flags without record script = createPositionsArrayScript("int_payload_field", "b", INCLUDE_ALL_BUT_CACHE, "position"); checkArrayValsInEachDoc(script, expectedPositionsArray, 3); script = createPositionsArrayScript("int_payload_field", "b", INCLUDE_ALL_BUT_CACHE, "startOffset"); checkArrayValsInEachDoc(script, expectedStartOffsetsArray, 3); script = createPositionsArrayScript("int_payload_field", "b", INCLUDE_ALL_BUT_CACHE, "endOffset"); checkArrayValsInEachDoc(script, expectedEndOffsetsArray, 3); script = createPositionsArrayScript("int_payload_field", "b", INCLUDE_ALL_BUT_CACHE, "payloadAsInt(-1)"); checkArrayValsInEachDoc(script, expectedPayloadsArray, 3); } private void checkArrayValsInEachDoc(Script script, HashMap<String, List<Object>> expectedArray, int expectedHitSize) { SearchResponse sr = client().prepareSearch("test").setQuery(QueryBuilders.matchAllQuery()).addScriptField("tvtest", script) .execute().actionGet(); assertHitCount(sr, expectedHitSize); int nullCounter = 0; for (SearchHit hit : sr.getHits().getHits()) { Object result = hit.getFields().get("tvtest").getValues(); Object expectedResult = expectedArray.get(hit.getId()); assertThat("for doc " + hit.getId(), result, equalTo(expectedResult)); if (expectedResult != null) { nullCounter++; } } assertThat(nullCounter, equalTo(expectedArray.size())); } public void testAllExceptPosAndOffset() throws Exception { XContentBuilder mapping = XContentFactory.jsonBuilder().startObject().startObject("type1").startObject("properties") .startObject("float_payload_field").field("type", "text").field("index_options", "offsets").field("term_vector", "no") .field("analyzer", "payload_float").endObject().startObject("string_payload_field").field("type", "text") .field("index_options", "offsets").field("term_vector", "no").field("analyzer", "payload_string").endObject() .startObject("int_payload_field").field("type", "text").field("index_options", "offsets") .field("analyzer", "payload_int").endObject().endObject().endObject().endObject(); assertAcked(prepareCreate("test").addMapping("type1", mapping).setSettings( Settings.builder() .put(indexSettings()) .put("index.analysis.analyzer.payload_float.tokenizer", "whitespace") .putArray("index.analysis.analyzer.payload_float.filter", "delimited_float") .put("index.analysis.filter.delimited_float.delimiter", "|") .put("index.analysis.filter.delimited_float.encoding", "float") .put("index.analysis.filter.delimited_float.type", "delimited_payload_filter") .put("index.analysis.analyzer.payload_string.tokenizer", "whitespace") .putArray("index.analysis.analyzer.payload_string.filter", "delimited_string") .put("index.analysis.filter.delimited_string.delimiter", "|") .put("index.analysis.filter.delimited_string.encoding", "identity") .put("index.analysis.filter.delimited_string.type", "delimited_payload_filter") .put("index.analysis.analyzer.payload_int.tokenizer", "whitespace") .putArray("index.analysis.analyzer.payload_int.filter", "delimited_int") .put("index.analysis.filter.delimited_int.delimiter", "|") .put("index.analysis.filter.delimited_int.encoding", "int") .put("index.analysis.filter.delimited_int.type", "delimited_payload_filter") .put("index.number_of_shards", 1))); indexRandom(true, client().prepareIndex("test", "type1", "1").setSource("float_payload_field", "a|1 b|2 a|3 b "), client() .prepareIndex("test", "type1", "2").setSource("string_payload_field", "a|a b|b a|a b "), client().prepareIndex("test", "type1", "3").setSource("float_payload_field", "a|4 b|5 a|6 b "), client().prepareIndex("test", "type1", "4").setSource("string_payload_field", "a|b b|a a|b b "), client().prepareIndex("test", "type1", "5").setSource("float_payload_field", "c "), client().prepareIndex("test", "type1", "6").setSource("int_payload_field", "c|1")); // get the number of all docs Script script = createScript("_index.numDocs()"); checkValueInEachDoc(6, script, 6); // get the number of docs with field float_payload_field script = createScript("_index['float_payload_field'].docCount()"); checkValueInEachDoc(3, script, 6); // corner case: what if the field does not exist? script = createScript("_index['non_existent_field'].docCount()"); checkValueInEachDoc(0, script, 6); // get the number of all tokens in all docs script = createScript("_index['float_payload_field'].sumttf()"); checkValueInEachDoc(9, script, 6); // corner case get the number of all tokens in all docs for non existent // field script = createScript("_index['non_existent_field'].sumttf()"); checkValueInEachDoc(0, script, 6); // get the sum of doc freqs in all docs script = createScript("_index['float_payload_field'].sumdf()"); checkValueInEachDoc(5, script, 6); // get the sum of doc freqs in all docs for non existent field script = createScript("_index['non_existent_field'].sumdf()"); checkValueInEachDoc(0, script, 6); // check term frequencies for 'a' script = createScript("term = _index['float_payload_field']['a']; if (term != null) {term.tf()}"); Map<String, Object> expectedResults = new HashMap<>(); expectedResults.put("1", 2); expectedResults.put("2", 0); expectedResults.put("3", 2); expectedResults.put("4", 0); expectedResults.put("5", 0); expectedResults.put("6", 0); checkValueInEachDoc(script, expectedResults, 6); expectedResults.clear(); // check doc frequencies for 'c' script = createScript("term = _index['float_payload_field']['c']; if (term != null) {term.df()}"); expectedResults.put("1", 1L); expectedResults.put("2", 1L); expectedResults.put("3", 1L); expectedResults.put("4", 1L); expectedResults.put("5", 1L); expectedResults.put("6", 1L); checkValueInEachDoc(script, expectedResults, 6); expectedResults.clear(); // check doc frequencies for term that does not exist script = createScript("term = _index['float_payload_field']['non_existent_term']; if (term != null) {term.df()}"); expectedResults.put("1", 0L); expectedResults.put("2", 0L); expectedResults.put("3", 0L); expectedResults.put("4", 0L); expectedResults.put("5", 0L); expectedResults.put("6", 0L); checkValueInEachDoc(script, expectedResults, 6); expectedResults.clear(); // check doc frequencies for term that does not exist script = createScript("term = _index['non_existent_field']['non_existent_term']; if (term != null) {term.tf()}"); expectedResults.put("1", 0); expectedResults.put("2", 0); expectedResults.put("3", 0); expectedResults.put("4", 0); expectedResults.put("5", 0); expectedResults.put("6", 0); checkValueInEachDoc(script, expectedResults, 6); expectedResults.clear(); // check total term frequencies for 'a' script = createScript("term = _index['float_payload_field']['a']; if (term != null) {term.ttf()}"); expectedResults.put("1", 4L); expectedResults.put("2", 4L); expectedResults.put("3", 4L); expectedResults.put("4", 4L); expectedResults.put("5", 4L); expectedResults.put("6", 4L); checkValueInEachDoc(script, expectedResults, 6); expectedResults.clear(); // check float payload for 'b' HashMap<String, List<Object>> expectedPayloadsArray = new HashMap<>(); script = createPositionsArrayScript("float_payload_field", "b", INCLUDE_ALL, "payloadAsFloat(-1)"); float missingValue = -1; List<Object> payloadsFor1 = new ArrayList<>(); payloadsFor1.add(2f); payloadsFor1.add(missingValue); expectedPayloadsArray.put("1", payloadsFor1); List<Object> payloadsFor2 = new ArrayList<>(); payloadsFor2.add(5f); payloadsFor2.add(missingValue); expectedPayloadsArray.put("3", payloadsFor2); expectedPayloadsArray.put("6", new ArrayList<>()); expectedPayloadsArray.put("5", new ArrayList<>()); expectedPayloadsArray.put("4", new ArrayList<>()); expectedPayloadsArray.put("2", new ArrayList<>()); checkArrayValsInEachDoc(script, expectedPayloadsArray, 6); // check string payload for 'b' expectedPayloadsArray.clear(); payloadsFor1.clear(); payloadsFor2.clear(); script = createPositionsArrayScript("string_payload_field", "b", INCLUDE_ALL, "payloadAsString()"); payloadsFor1.add("b"); payloadsFor1.add(null); expectedPayloadsArray.put("2", payloadsFor1); payloadsFor2.add("a"); payloadsFor2.add(null); expectedPayloadsArray.put("4", payloadsFor2); expectedPayloadsArray.put("6", new ArrayList<>()); expectedPayloadsArray.put("5", new ArrayList<>()); expectedPayloadsArray.put("3", new ArrayList<>()); expectedPayloadsArray.put("1", new ArrayList<>()); checkArrayValsInEachDoc(script, expectedPayloadsArray, 6); // check int payload for 'c' expectedPayloadsArray.clear(); payloadsFor1.clear(); payloadsFor2.clear(); script = createPositionsArrayScript("int_payload_field", "c", INCLUDE_ALL, "payloadAsInt(-1)"); payloadsFor1 = new ArrayList<>(); payloadsFor1.add(1); expectedPayloadsArray.put("6", payloadsFor1); expectedPayloadsArray.put("5", new ArrayList<>()); expectedPayloadsArray.put("4", new ArrayList<>()); expectedPayloadsArray.put("3", new ArrayList<>()); expectedPayloadsArray.put("2", new ArrayList<>()); expectedPayloadsArray.put("1", new ArrayList<>()); checkArrayValsInEachDoc(script, expectedPayloadsArray, 6); } private void checkExceptions(Script script) { try { SearchResponse sr = client().prepareSearch("test").setQuery(QueryBuilders.matchAllQuery()).addScriptField("tvtest", script) .execute().actionGet(); assertThat(sr.getHits().getHits().length, equalTo(0)); ShardSearchFailure[] shardFails = sr.getShardFailures(); for (ShardSearchFailure fail : shardFails) { assertThat(fail.reason().indexOf("Cannot iterate twice! If you want to iterate more that once, add _CACHE explicitly."), Matchers.greaterThan(-1)); } } catch (SearchPhaseExecutionException ex) { assertThat( "got " + ex.toString(), ex.toString().indexOf("Cannot iterate twice! If you want to iterate more that once, add _CACHE explicitly."), Matchers.greaterThan(-1)); } } private void checkValueInEachDocWithFunctionScore(Script fieldScript, Map<String, Object> expectedFieldVals, Script scoreScript, Map<String, Object> expectedScore, int numExpectedDocs) { SearchResponse sr = client().prepareSearch("test") .setQuery(QueryBuilders.functionScoreQuery(ScoreFunctionBuilders.scriptFunction(scoreScript))) .addScriptField("tvtest", fieldScript).execute().actionGet(); assertHitCount(sr, numExpectedDocs); for (SearchHit hit : sr.getHits().getHits()) { Object result = hit.getFields().get("tvtest").getValues().get(0); Object expectedResult = expectedFieldVals.get(hit.getId()); assertThat("for doc " + hit.getId(), result, equalTo(expectedResult)); assertThat("for doc " + hit.getId(), ((Float) expectedScore.get(hit.getId())).doubleValue(), Matchers.closeTo(hit.getScore(), 1.e-4)); } } private void checkValueInEachDoc(Script script, Map<String, Object> expectedResults, int numExpectedDocs) { SearchResponse sr = client().prepareSearch("test").setQuery(QueryBuilders.matchAllQuery()).addScriptField("tvtest", script) .execute().actionGet(); assertHitCount(sr, numExpectedDocs); for (SearchHit hit : sr.getHits().getHits()) { Object result = hit.getFields().get("tvtest").getValues().get(0); Object expectedResult = expectedResults.get(hit.getId()); assertThat("for doc " + hit.getId(), result, equalTo(expectedResult)); } } private void checkValueInEachDoc(int value, Script script, int numExpectedDocs) { SearchResponse sr = client().prepareSearch("test").setQuery(QueryBuilders.matchAllQuery()).addScriptField("tvtest", script) .execute().actionGet(); assertHitCount(sr, numExpectedDocs); for (SearchHit hit : sr.getHits().getHits()) { Object result = hit.getFields().get("tvtest").getValues().get(0); if (result instanceof Integer) { assertThat(result, equalTo(value)); } else if (result instanceof Long) { assertThat(((Long) result).intValue(), equalTo(value)); } else { fail(); } } } }