/* * 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.messy.tests; import org.apache.lucene.util.ArrayUtil; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.index.query.functionscore.random.RandomScoreFunctionBuilder; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.script.Script; import org.elasticsearch.script.ScriptService.ScriptType; import org.elasticsearch.script.groovy.GroovyPlugin; import org.elasticsearch.search.SearchHit; import org.elasticsearch.test.ESIntegTestCase; import org.hamcrest.CoreMatchers; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.index.query.QueryBuilders.*; import static org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders.*; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures; import static org.hamcrest.Matchers.*; public class RandomScoreFunctionTests extends ESIntegTestCase { @Override protected Collection<Class<? extends Plugin>> nodePlugins() { return pluginList(GroovyPlugin.class); } public void testConsistentHitsWithSameSeed() throws Exception { createIndex("test"); ensureGreen(); // make sure we are done otherwise preference could change? int docCount = randomIntBetween(100, 200); for (int i = 0; i < docCount; i++) { index("test", "type", "" + i, jsonBuilder().startObject().endObject()); } flush(); refresh(); int outerIters = scaledRandomIntBetween(10, 20); for (int o = 0; o < outerIters; o++) { final int seed = randomInt(); String preference = randomRealisticUnicodeOfLengthBetween(1, 10); // at least one char!! // randomPreference should not start with '_' (reserved for known preference types (e.g. _shards, _primary) while (preference.startsWith("_")) { preference = randomRealisticUnicodeOfLengthBetween(1, 10); } int innerIters = scaledRandomIntBetween(2, 5); SearchHit[] hits = null; for (int i = 0; i < innerIters; i++) { SearchResponse searchResponse = client().prepareSearch() .setSize(docCount) // get all docs otherwise we are prone to tie-breaking .setPreference(preference) .setQuery(functionScoreQuery(matchAllQuery(), randomFunction(seed))) .execute().actionGet(); assertThat("Failures " + Arrays.toString(searchResponse.getShardFailures()), searchResponse.getShardFailures().length, CoreMatchers.equalTo(0)); final int hitCount = searchResponse.getHits().getHits().length; final SearchHit[] currentHits = searchResponse.getHits().getHits(); ArrayUtil.timSort(currentHits, new Comparator<SearchHit>() { @Override public int compare(SearchHit o1, SearchHit o2) { // for tie-breaking we have to resort here since if the score is // identical we rely on collection order which might change. int cmp = Float.compare(o1.getScore(), o2.getScore()); return cmp == 0 ? o1.getId().compareTo(o2.getId()) : cmp; } }); if (i == 0) { assertThat(hits, nullValue()); hits = currentHits; } else { assertThat(hits.length, equalTo(searchResponse.getHits().getHits().length)); for (int j = 0; j < hitCount; j++) { assertThat("" + j, currentHits[j].score(), equalTo(hits[j].score())); assertThat("" + j, currentHits[j].id(), equalTo(hits[j].id())); } } // randomly change some docs to get them in different segments int numDocsToChange = randomIntBetween(20, 50); while (numDocsToChange > 0) { int doc = randomInt(docCount-1);// watch out this is inclusive the max values! index("test", "type", "" + doc, jsonBuilder().startObject().endObject()); --numDocsToChange; } flush(); refresh(); } } } public void testScoreAccessWithinScript() throws Exception { assertAcked(prepareCreate("test").addMapping("type", "body", "type=string", "index", "type=" + randomFrom(new String[] { "short", "float", "long", "integer", "double" }))); ensureYellow(); int docCount = randomIntBetween(100, 200); for (int i = 0; i < docCount; i++) { client().prepareIndex("test", "type", "" + i).setSource("body", randomFrom(Arrays.asList("foo", "bar", "baz")), "index", i + 1) // we add 1 to the index field to make sure that the scripts below never compute log(0) .get(); } refresh(); Map<String, Object> params = new HashMap<>(); params.put("factor", randomIntBetween(2, 4)); // Test for accessing _score SearchResponse resp = client() .prepareSearch("test") .setQuery( functionScoreQuery(matchQuery("body", "foo")).add(fieldValueFactorFunction("index").factor(2)).add( scriptFunction(new Script("log(doc['index'].value + (factor * _score))", ScriptType.INLINE, null, params)))) .get(); assertNoFailures(resp); SearchHit firstHit = resp.getHits().getAt(0); assertThat(firstHit.getScore(), greaterThan(1f)); // Test for accessing _score.intValue() resp = client() .prepareSearch("test") .setQuery( functionScoreQuery(matchQuery("body", "foo")).add(fieldValueFactorFunction("index").factor(2)).add( scriptFunction(new Script("log(doc['index'].value + (factor * _score.intValue()))", ScriptType.INLINE, null, params)))).get(); assertNoFailures(resp); firstHit = resp.getHits().getAt(0); assertThat(firstHit.getScore(), greaterThan(1f)); // Test for accessing _score.longValue() resp = client() .prepareSearch("test") .setQuery( functionScoreQuery(matchQuery("body", "foo")).add(fieldValueFactorFunction("index").factor(2)).add( scriptFunction(new Script("log(doc['index'].value + (factor * _score.longValue()))", ScriptType.INLINE, null, params)))).get(); assertNoFailures(resp); firstHit = resp.getHits().getAt(0); assertThat(firstHit.getScore(), greaterThan(1f)); // Test for accessing _score.floatValue() resp = client() .prepareSearch("test") .setQuery( functionScoreQuery(matchQuery("body", "foo")).add(fieldValueFactorFunction("index").factor(2)).add( scriptFunction(new Script("log(doc['index'].value + (factor * _score.floatValue()))", ScriptType.INLINE, null, params)))).get(); assertNoFailures(resp); firstHit = resp.getHits().getAt(0); assertThat(firstHit.getScore(), greaterThan(1f)); // Test for accessing _score.doubleValue() resp = client() .prepareSearch("test") .setQuery( functionScoreQuery(matchQuery("body", "foo")).add(fieldValueFactorFunction("index").factor(2)).add( scriptFunction(new Script("log(doc['index'].value + (factor * _score.doubleValue()))", ScriptType.INLINE, null, params)))).get(); assertNoFailures(resp); firstHit = resp.getHits().getAt(0); assertThat(firstHit.getScore(), greaterThan(1f)); } public void testSeedReportedInExplain() throws Exception { createIndex("test"); ensureGreen(); index("test", "type", "1", jsonBuilder().startObject().endObject()); flush(); refresh(); int seed = 12345678; SearchResponse resp = client().prepareSearch("test") .setQuery(functionScoreQuery(matchAllQuery(), randomFunction(seed))) .setExplain(true) .get(); assertNoFailures(resp); assertEquals(1, resp.getHits().totalHits()); SearchHit firstHit = resp.getHits().getAt(0); assertThat(firstHit.explanation().toString(), containsString("" + seed)); } public void testNoDocs() throws Exception { createIndex("test"); ensureGreen(); SearchResponse resp = client().prepareSearch("test") .setQuery(functionScoreQuery(matchAllQuery(), randomFunction(1234))) .get(); assertNoFailures(resp); assertEquals(0, resp.getHits().totalHits()); } public void testScoreRange() throws Exception { // all random scores should be in range [0.0, 1.0] createIndex("test"); ensureGreen(); int docCount = randomIntBetween(100, 200); for (int i = 0; i < docCount; i++) { String id = randomRealisticUnicodeOfCodepointLengthBetween(1, 50); index("test", "type", id, jsonBuilder().startObject().endObject()); } flush(); refresh(); int iters = scaledRandomIntBetween(10, 20); for (int i = 0; i < iters; ++i) { int seed = randomInt(); SearchResponse searchResponse = client().prepareSearch() .setQuery(functionScoreQuery(matchAllQuery(), randomFunction(seed))) .setSize(docCount) .execute().actionGet(); assertNoFailures(searchResponse); for (SearchHit hit : searchResponse.getHits().getHits()) { assertThat(hit.score(), allOf(greaterThanOrEqualTo(0.0f), lessThanOrEqualTo(1.0f))); } } } public void testSeeds() throws Exception { createIndex("test"); ensureGreen(); final int docCount = randomIntBetween(100, 200); for (int i = 0; i < docCount; i++) { index("test", "type", "" + i, jsonBuilder().startObject().endObject()); } flushAndRefresh(); assertNoFailures(client().prepareSearch() .setSize(docCount) // get all docs otherwise we are prone to tie-breaking .setQuery(functionScoreQuery(matchAllQuery(), randomFunction(randomInt()))) .execute().actionGet()); assertNoFailures(client().prepareSearch() .setSize(docCount) // get all docs otherwise we are prone to tie-breaking .setQuery(functionScoreQuery(matchAllQuery(), randomFunction(randomLong()))) .execute().actionGet()); assertNoFailures(client().prepareSearch() .setSize(docCount) // get all docs otherwise we are prone to tie-breaking .setQuery(functionScoreQuery(matchAllQuery(), randomFunction(randomRealisticUnicodeOfLengthBetween(10, 20)))) .execute().actionGet()); } public void checkDistribution() throws Exception { int count = 10000; assertAcked(prepareCreate("test")); ensureGreen(); for (int i = 0; i < count; i++) { index("test", "type", "" + i, jsonBuilder().startObject().endObject()); } flush(); refresh(); int[] matrix = new int[count]; for (int i = 0; i < count; i++) { SearchResponse searchResponse = client().prepareSearch() .setQuery(functionScoreQuery(matchAllQuery(), new RandomScoreFunctionBuilder())) .execute().actionGet(); matrix[Integer.valueOf(searchResponse.getHits().getAt(0).id())]++; } int filled = 0; int maxRepeat = 0; int sumRepeat = 0; for (int i = 0; i < matrix.length; i++) { int value = matrix[i]; sumRepeat += value; maxRepeat = Math.max(maxRepeat, value); if (value > 0) { filled++; } } System.out.println(); System.out.println("max repeat: " + maxRepeat); System.out.println("avg repeat: " + sumRepeat / (double) filled); System.out.println("distribution: " + filled / (double) count); int percentile50 = filled / 2; int percentile25 = (filled / 4); int percentile75 = percentile50 + percentile25; int sum = 0; for (int i = 0; i < matrix.length; i++) { if (matrix[i] == 0) { continue; } sum += i * matrix[i]; if (percentile50 == 0) { System.out.println("median: " + i); } else if (percentile25 == 0) { System.out.println("percentile_25: " + i); } else if (percentile75 == 0) { System.out.println("percentile_75: " + i); } percentile50--; percentile25--; percentile75--; } System.out.println("mean: " + sum / (double) count); } }