diff --git a/.idea/dictionaries/kimchy.xml b/.idea/dictionaries/kimchy.xml index 0ab91bc9b73..a000b6f177e 100644 --- a/.idea/dictionaries/kimchy.xml +++ b/.idea/dictionaries/kimchy.xml @@ -71,6 +71,7 @@ multi multicast multiline + mvel nanos newcount ngram @@ -109,6 +110,7 @@ traslog trie tuple + unboxed unicast unregister uptime diff --git a/.idea/modules/elasticsearch.iml b/.idea/modules/elasticsearch.iml index f4420294886..28e6c3eecd5 100644 --- a/.idea/modules/elasticsearch.iml +++ b/.idea/modules/elasticsearch.iml @@ -40,6 +40,17 @@ + + + + + + + + + + + diff --git a/build.gradle b/build.gradle index b478f6f055f..b05fed69071 100644 --- a/build.gradle +++ b/build.gradle @@ -37,6 +37,7 @@ allprojects { repositories { mavenCentral() mavenRepo urls: 'https://repository.jboss.org/nexus/content/groups/public' + mavenRepo urls: 'http://repository.codehaus.org/' mavenRepo urls: 'http://elasticsearch.googlecode.com/svn/maven' } } @@ -82,7 +83,8 @@ task explodedDist(dependsOn: [configurations.distLib], description: 'Builds a mi ant.delete { fileset(dir: explodedDistLibDir, includes: "joda-*.jar") } // no need joda, we jarjar it ant.delete { fileset(dir: explodedDistLibDir, includes: "snakeyaml-*.jar") } // no need snakeyaml, we jarjar it ant.delete { fileset(dir: explodedDistLibDir, includes: "sigar-*.jar") } // no need sigar directly under lib... - ant.delete { fileset(dir: explodedDistLibDir, includes: "netty-*.jar") } // no need sigar directly under lib... + ant.delete { fileset(dir: explodedDistLibDir, includes: "netty-*.jar") } // no need netty directly under lib... + ant.delete { fileset(dir: explodedDistLibDir, includes: "mvel2-*.jar") } // no need mvel2 directly under lib... ant.chmod(dir: "$explodedDistDir/bin", perm: "ugo+rx", includes: "**/*") } diff --git a/modules/elasticsearch/build.gradle b/modules/elasticsearch/build.gradle index 73579e63be8..2a45480af47 100644 --- a/modules/elasticsearch/build.gradle +++ b/modules/elasticsearch/build.gradle @@ -31,6 +31,8 @@ dependencies { compile 'joda-time:joda-time:1.6' + compile 'org.mvel:mvel2:2.0.17' + compile 'org.codehaus.jackson:jackson-core-asl:1.5.2' compile 'org.yaml:snakeyaml:1.6' @@ -70,12 +72,13 @@ jar << { jarjar(jarfile: jarjarArchivePath) { zipfileset(src: jar.archivePath) configurations.compile.files.findAll {file -> - ['jackson', 'joda', 'snakeyaml', 'netty'].any { file.name.contains(it) } + ['mvel', 'jackson', 'joda', 'snakeyaml', 'netty'].any { file.name.contains(it) } }.each { jarjarFile -> zipfileset(src: jarjarFile) { exclude(name: "META-INF/**") } } + rule pattern: "org.mvel2.**", result: "org.elasticsearch.util.mvel2.@1" rule pattern: "org.codehaus.jackson.**", result: "org.elasticsearch.util.jackson.@1" rule pattern: "org.joda.**", result: "org.elasticsearch.util.joda.@1" rule pattern: "org.yaml.**", result: "org.elasticsearch.util.yaml.@1" @@ -88,6 +91,7 @@ jar << { // seems like empty dirst still exists, unjar and clean them unjar(src: jar.archivePath, dest: "build/tmp/extracted") delete(dir: "build/tmp/extracted/org/codehaus") + delete(dir: "build/tmp/extracted/org/mvel2") delete(dir: "build/tmp/extracted/org/joda") delete(dir: "build/tmp/extracted/org/yaml") delete(dir: "build/tmp/extracted/org/jboss") @@ -162,6 +166,7 @@ uploadArchives { pom.dependencies = pom.dependencies.findAll {dep -> !dep.artifactId.contains('joda') } pom.dependencies = pom.dependencies.findAll {dep -> !dep.artifactId.contains('snakeyaml') } pom.dependencies = pom.dependencies.findAll {dep -> !dep.artifactId.contains('netty') } + pom.dependencies = pom.dependencies.findAll {dep -> !dep.artifactId.contains('mvel') } } } } \ No newline at end of file diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/cache/NodeCache.java b/modules/elasticsearch/src/main/java/org/elasticsearch/cache/NodeCache.java new file mode 100644 index 00000000000..c391487632f --- /dev/null +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/cache/NodeCache.java @@ -0,0 +1,34 @@ +/* + * Licensed to Elastic Search and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Elastic Search 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.cache; + +import org.elasticsearch.util.component.AbstractComponent; +import org.elasticsearch.util.inject.Inject; +import org.elasticsearch.util.settings.Settings; + +/** + * @author kimchy (shay.banon) + */ +public class NodeCache extends AbstractComponent { + + @Inject public NodeCache(Settings settings) { + super(settings); + } +} diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/util/lucene/search/function/FunctionProvider.java b/modules/elasticsearch/src/main/java/org/elasticsearch/cache/NodeCacheModule.java similarity index 78% rename from modules/elasticsearch/src/main/java/org/elasticsearch/util/lucene/search/function/FunctionProvider.java rename to modules/elasticsearch/src/main/java/org/elasticsearch/cache/NodeCacheModule.java index c00eb31913f..1c14ec9e3c4 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/util/lucene/search/function/FunctionProvider.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/cache/NodeCacheModule.java @@ -17,14 +17,16 @@ * under the License. */ -package org.elasticsearch.util.lucene.search.function; +package org.elasticsearch.cache; -import org.apache.lucene.index.IndexReader; +import org.elasticsearch.util.inject.AbstractModule; /** * @author kimchy (shay.banon) */ -public interface FunctionProvider { +public class NodeCacheModule extends AbstractModule { - Function function(IndexReader reader); + @Override protected void configure() { + bind(NodeCache.class).asEagerSingleton(); + } } diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/data/DocFieldData.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/data/DocFieldData.java index ab537ea4bf0..eb93e7ddc7f 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/data/DocFieldData.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/data/DocFieldData.java @@ -48,6 +48,10 @@ public abstract class DocFieldData { return fieldData.stringValue(docId); } + public String getStringValue() { + return stringValue(); + } + public FieldData.Type getType() { return fieldData.type(); } diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/data/longs/LongDocFieldData.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/data/longs/LongDocFieldData.java index aaccba3792f..97864cbb6ea 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/data/longs/LongDocFieldData.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/data/longs/LongDocFieldData.java @@ -20,6 +20,7 @@ package org.elasticsearch.index.field.data.longs; import org.elasticsearch.index.field.data.NumericDocFieldData; +import org.joda.time.MutableDateTime; /** * @author kimchy (shay.banon) @@ -37,4 +38,12 @@ public class LongDocFieldData extends NumericDocFieldData { public long[] getValues() { return fieldData.values(docId); } + + public MutableDateTime getDate() { + return fieldData.date(docId); + } + + public MutableDateTime[] getDates() { + return fieldData.dates(docId); + } } \ No newline at end of file diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/data/longs/LongFieldData.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/data/longs/LongFieldData.java index 6d4a7772746..f9a6c28e892 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/data/longs/LongFieldData.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/data/longs/LongFieldData.java @@ -24,7 +24,9 @@ import org.apache.lucene.search.FieldCache; import org.elasticsearch.index.field.data.FieldDataOptions; import org.elasticsearch.index.field.data.NumericFieldData; import org.elasticsearch.index.field.data.support.FieldDataLoader; +import org.elasticsearch.util.ThreadLocals; import org.elasticsearch.util.gnu.trove.TLongArrayList; +import org.joda.time.MutableDateTime; import java.io.IOException; @@ -34,7 +36,13 @@ import java.io.IOException; public abstract class LongFieldData extends NumericFieldData { static final long[] EMPTY_LONG_ARRAY = new long[0]; + static final MutableDateTime[] EMPTY_DATETIME_ARRAY = new MutableDateTime[0]; + private ThreadLocal> dateTimeCache = new ThreadLocal>() { + @Override protected ThreadLocals.CleanableValue initialValue() { + return new ThreadLocals.CleanableValue(new MutableDateTime()); + } + }; protected final long[] values; protected final int[] freqs; @@ -49,6 +57,14 @@ public abstract class LongFieldData extends NumericFieldData { abstract public long[] values(int docId); + public MutableDateTime date(int docId) { + MutableDateTime dateTime = dateTimeCache.get().get(); + dateTime.setMillis(value(docId)); + return dateTime; + } + + public abstract MutableDateTime[] dates(int docId); + @Override public LongDocFieldData docFieldData(int docId) { return super.docFieldData(docId); } diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/data/longs/MultiValueLongFieldData.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/data/longs/MultiValueLongFieldData.java index 1257b5b9480..99ceed4f217 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/data/longs/MultiValueLongFieldData.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/data/longs/MultiValueLongFieldData.java @@ -22,6 +22,7 @@ package org.elasticsearch.index.field.data.longs; import org.elasticsearch.index.field.data.FieldDataOptions; import org.elasticsearch.index.field.data.doubles.DoubleFieldData; import org.elasticsearch.util.ThreadLocals; +import org.joda.time.MutableDateTime; /** * @author kimchy (shay.banon) @@ -40,6 +41,20 @@ public class MultiValueLongFieldData extends LongFieldData { } }; + private ThreadLocal> dateTimesCache = new ThreadLocal>() { + @Override protected ThreadLocals.CleanableValue initialValue() { + MutableDateTime[][] value = new MutableDateTime[VALUE_CACHE_SIZE][]; + for (int i = 0; i < value.length; i++) { + value[i] = new MutableDateTime[i]; + for (int j = 0; j < i; j++) { + value[i][j] = new MutableDateTime(); + } + } + return new ThreadLocals.CleanableValue(value); + } + }; + + private ThreadLocal> valuesCache = new ThreadLocal>() { @Override protected ThreadLocals.CleanableValue initialValue() { long[][] value = new long[VALUE_CACHE_SIZE][]; @@ -86,6 +101,26 @@ public class MultiValueLongFieldData extends LongFieldData { } } + @Override public MutableDateTime[] dates(int docId) { + int[] docOrders = order[docId]; + if (docOrders == null) { + return EMPTY_DATETIME_ARRAY; + } + MutableDateTime[] dates; + if (docOrders.length < VALUE_CACHE_SIZE) { + dates = dateTimesCache.get().get()[docOrders.length]; + } else { + dates = new MutableDateTime[docOrders.length]; + for (int i = 0; i < dates.length; i++) { + dates[i] = new MutableDateTime(); + } + } + for (int i = 0; i < docOrders.length; i++) { + dates[i].setMillis(values[docOrders[i]]); + } + return dates; + } + @Override public double[] doubleValues(int docId) { int[] docOrders = order[docId]; if (docOrders == null) { diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/data/longs/SingleValueLongFieldData.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/data/longs/SingleValueLongFieldData.java index 7b6fc225381..a862184fcb5 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/data/longs/SingleValueLongFieldData.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/data/longs/SingleValueLongFieldData.java @@ -21,6 +21,7 @@ package org.elasticsearch.index.field.data.longs; import org.elasticsearch.index.field.data.FieldDataOptions; import org.elasticsearch.index.field.data.doubles.DoubleFieldData; +import org.joda.time.MutableDateTime; /** * @author kimchy (shay.banon) @@ -33,6 +34,14 @@ public class SingleValueLongFieldData extends LongFieldData { } }; + private ThreadLocal datesValuesCache = new ThreadLocal() { + @Override protected MutableDateTime[] initialValue() { + MutableDateTime[] date = new MutableDateTime[1]; + date[0] = new MutableDateTime(); + return date; + } + }; + private ThreadLocal valuesCache = new ThreadLocal() { @Override protected long[] initialValue() { return new long[1]; @@ -71,6 +80,16 @@ public class SingleValueLongFieldData extends LongFieldData { proc.onValue(docId, values[loc]); } + @Override public MutableDateTime[] dates(int docId) { + int loc = order[docId]; + if (loc == 0) { + return EMPTY_DATETIME_ARRAY; + } + MutableDateTime[] ret = datesValuesCache.get(); + ret[0].setMillis(values[loc]); + return ret; + } + @Override public double[] doubleValues(int docId) { int loc = order[docId]; if (loc == 0) { diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/function/FieldsFunction.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/function/FieldsFunction.java new file mode 100644 index 00000000000..1c04f2840f4 --- /dev/null +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/function/FieldsFunction.java @@ -0,0 +1,39 @@ +/* + * Licensed to Elastic Search and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Elastic Search 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.field.function; + +import org.apache.lucene.index.IndexReader; + +import java.util.Map; + +/** + * @author kimchy (shay.banon) + */ +public interface FieldsFunction { + + void setNextReader(IndexReader reader); + + /** + * @param docId + * @param vars The vars providing additional parameters, should be reused and has values added to it in execute + * @return + */ + Object execute(int docId, Map vars); +} diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/function/script/ScriptFieldsFunction.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/function/script/ScriptFieldsFunction.java new file mode 100644 index 00000000000..be4a4f94568 --- /dev/null +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/field/function/script/ScriptFieldsFunction.java @@ -0,0 +1,147 @@ +/* + * Licensed to Elastic Search and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Elastic Search 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.field.function.script; + +import org.apache.lucene.index.IndexReader; +import org.elasticsearch.ElasticSearchException; +import org.elasticsearch.ElasticSearchIllegalArgumentException; +import org.elasticsearch.index.cache.field.data.FieldDataCache; +import org.elasticsearch.index.field.data.FieldData; +import org.elasticsearch.index.field.data.FieldDataOptions; +import org.elasticsearch.index.field.function.FieldsFunction; +import org.elasticsearch.index.mapper.FieldMapper; +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.util.ThreadLocals; + +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * @author kimchy (shay.banon) + */ +public class ScriptFieldsFunction implements FieldsFunction, Map { + + private static ThreadLocal>> cachedFieldData = new ThreadLocal>>() { + @Override protected ThreadLocals.CleanableValue> initialValue() { + return new ThreadLocals.CleanableValue>(new HashMap()); + } + }; + + final Object script; + + final MapperService mapperService; + + final FieldDataCache fieldDataCache; + + final ScriptService scriptService; + + final Map localCacheFieldData = cachedFieldData.get().get(); + + IndexReader reader; + + int docId; + + public ScriptFieldsFunction(String script, ScriptService scriptService, MapperService mapperService, FieldDataCache fieldDataCache) { + this.scriptService = scriptService; + this.mapperService = mapperService; + this.fieldDataCache = fieldDataCache; + this.script = scriptService.compile(script); + } + + @Override public void setNextReader(IndexReader reader) { + this.reader = reader; + localCacheFieldData.clear(); + } + + @Override public Object execute(int docId, Map vars) { + this.docId = docId; + vars.put("doc", this); + return scriptService.execute(script, vars); + } + + // --- Map implementation for doc field data lookup + + @Override public Object get(Object key) { + // assume its a string... + String fieldName = key.toString(); + FieldData fieldData = localCacheFieldData.get(fieldName); + if (fieldData == null) { + FieldMapper mapper = mapperService.smartNameFieldMapper(fieldName); + if (mapper == null) { + throw new ElasticSearchIllegalArgumentException("No field found for [" + fieldName + "]"); + } + try { + fieldData = fieldDataCache.cache(mapper.fieldDataType(), reader, mapper.names().indexName(), FieldDataOptions.fieldDataOptions().withFreqs(false)); + } catch (IOException e) { + throw new ElasticSearchException("Failed to load field data for [" + fieldName + "]", e); + } + localCacheFieldData.put(fieldName, fieldData); + } + return fieldData.docFieldData(docId); + } + + public int size() { + throw new UnsupportedOperationException(); + } + + public boolean isEmpty() { + throw new UnsupportedOperationException(); + } + + public boolean containsKey(Object key) { + throw new UnsupportedOperationException(); + } + + public boolean containsValue(Object value) { + throw new UnsupportedOperationException(); + } + + public Object put(Object key, Object value) { + throw new UnsupportedOperationException(); + } + + public Object remove(Object key) { + throw new UnsupportedOperationException(); + } + + public void putAll(Map m) { + throw new UnsupportedOperationException(); + } + + public void clear() { + throw new UnsupportedOperationException(); + } + + public Set keySet() { + throw new UnsupportedOperationException(); + } + + public Collection values() { + throw new UnsupportedOperationException(); + } + + public Set entrySet() { + throw new UnsupportedOperationException(); + } +} diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/IndexQueryParserService.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/IndexQueryParserService.java index 712a54df2d9..6e8ec974cf5 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/IndexQueryParserService.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/IndexQueryParserService.java @@ -28,6 +28,7 @@ import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.query.xcontent.XContentIndexQueryParser; import org.elasticsearch.index.settings.IndexSettings; import org.elasticsearch.index.similarity.SimilarityService; +import org.elasticsearch.script.ScriptService; import org.elasticsearch.util.collect.ImmutableMap; import org.elasticsearch.util.inject.Inject; import org.elasticsearch.util.settings.Settings; @@ -39,7 +40,7 @@ import static org.elasticsearch.util.collect.Maps.*; import static org.elasticsearch.util.settings.ImmutableSettings.Builder.*; /** - * @author kimchy (Shay Banon) + * @author kimchy (shay.banon) */ public class IndexQueryParserService extends AbstractIndexComponent { @@ -53,10 +54,11 @@ public class IndexQueryParserService extends AbstractIndexComponent { private final Map indexQueryParsers; public IndexQueryParserService(Index index, MapperService mapperService, IndexCache indexCache, IndexEngine indexEngine, AnalysisService analysisService) { - this(index, EMPTY_SETTINGS, mapperService, indexCache, indexEngine, analysisService, null, null); + this(index, EMPTY_SETTINGS, new ScriptService(EMPTY_SETTINGS), mapperService, indexCache, indexEngine, analysisService, null, null); } @Inject public IndexQueryParserService(Index index, @IndexSettings Settings indexSettings, + ScriptService scriptService, MapperService mapperService, IndexCache indexCache, IndexEngine indexEngine, AnalysisService analysisService, @Nullable SimilarityService similarityService, @@ -80,7 +82,7 @@ public class IndexQueryParserService extends AbstractIndexComponent { } } if (!qparsers.containsKey(Defaults.DEFAULT)) { - IndexQueryParser defaultQueryParser = new XContentIndexQueryParser(index, indexSettings, mapperService, indexCache, indexEngine, analysisService, similarityService, null, null, Defaults.DEFAULT, null); + IndexQueryParser defaultQueryParser = new XContentIndexQueryParser(index, indexSettings, scriptService, mapperService, indexCache, indexEngine, analysisService, similarityService, null, null, Defaults.DEFAULT, null); qparsers.put(Defaults.DEFAULT, defaultQueryParser); } diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/CustomBoostFactorQueryParser.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/CustomBoostFactorQueryParser.java index 5d56ec1bc64..895dc3da90d 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/CustomBoostFactorQueryParser.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/CustomBoostFactorQueryParser.java @@ -26,7 +26,7 @@ import org.elasticsearch.index.query.QueryParsingException; import org.elasticsearch.index.settings.IndexSettings; import org.elasticsearch.util.Strings; import org.elasticsearch.util.inject.Inject; -import org.elasticsearch.util.lucene.search.function.BoostFactorFunctionProvider; +import org.elasticsearch.util.lucene.search.function.BoostScoreFunction; import org.elasticsearch.util.lucene.search.function.FunctionScoreQuery; import org.elasticsearch.util.settings.Settings; import org.elasticsearch.util.xcontent.XContentParser; @@ -75,7 +75,7 @@ public class CustomBoostFactorQueryParser extends AbstractIndexComponent impleme if (query == null) { throw new QueryParsingException(index, "[constant_factor_query] requires 'query' element"); } - FunctionScoreQuery functionScoreQuery = new FunctionScoreQuery(query, new BoostFactorFunctionProvider(boostFactor)); + FunctionScoreQuery functionScoreQuery = new FunctionScoreQuery(query, new BoostScoreFunction(boostFactor)); functionScoreQuery.setBoost(boost); return functionScoreQuery; } diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/CustomScoreQueryBuilder.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/CustomScoreQueryBuilder.java new file mode 100644 index 00000000000..f5521cb796e --- /dev/null +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/CustomScoreQueryBuilder.java @@ -0,0 +1,75 @@ +/* + * Licensed to Elastic Search and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Elastic Search 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.xcontent; + +import org.elasticsearch.util.xcontent.builder.XContentBuilder; + +import java.io.IOException; + +/** + * A query that uses a script to compute the score. + * + * @author kimchy (shay.banon) + */ +public class CustomScoreQueryBuilder extends BaseQueryBuilder { + + private final XContentQueryBuilder queryBuilder; + + private String script; + + private float boost = -1; + + /** + * A query that simply applies the boost factor to another query (multiply it). + * + * @param queryBuilder The query to apply the boost factor to. + */ + public CustomScoreQueryBuilder(XContentQueryBuilder queryBuilder) { + this.queryBuilder = queryBuilder; + } + + /** + * Sets the boost factor for this query. + */ + public CustomScoreQueryBuilder script(String script) { + this.script = script; + return this; + } + + /** + * Sets the boost for this query. Documents matching this query will (in addition to the normal + * weightings) have their score multiplied by the boost provided. + */ + public CustomScoreQueryBuilder boost(float boost) { + this.boost = boost; + return this; + } + + @Override protected void doXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(CustomScoreQueryParser.NAME); + builder.field("query"); + queryBuilder.toXContent(builder, params); + builder.field("script", script); + if (boost != -1) { + builder.field("boost", boost); + } + builder.endObject(); + } +} \ No newline at end of file diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/CustomScoreQueryParser.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/CustomScoreQueryParser.java new file mode 100644 index 00000000000..a08d150d938 --- /dev/null +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/CustomScoreQueryParser.java @@ -0,0 +1,127 @@ +/* + * Licensed to Elastic Search and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Elastic Search 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.xcontent; + +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.search.Explanation; +import org.apache.lucene.search.Query; +import org.elasticsearch.index.AbstractIndexComponent; +import org.elasticsearch.index.Index; +import org.elasticsearch.index.field.function.script.ScriptFieldsFunction; +import org.elasticsearch.index.query.QueryParsingException; +import org.elasticsearch.index.settings.IndexSettings; +import org.elasticsearch.util.Strings; +import org.elasticsearch.util.ThreadLocals; +import org.elasticsearch.util.inject.Inject; +import org.elasticsearch.util.lucene.search.function.FunctionScoreQuery; +import org.elasticsearch.util.lucene.search.function.ScoreFunction; +import org.elasticsearch.util.settings.Settings; +import org.elasticsearch.util.xcontent.XContentParser; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * @author kimchy (shay.banon) + */ +public class CustomScoreQueryParser extends AbstractIndexComponent implements XContentQueryParser { + + public static final String NAME = "custom_score"; + + @Inject public CustomScoreQueryParser(Index index, @IndexSettings Settings settings) { + super(index, settings); + } + + @Override public String[] names() { + return new String[]{NAME, Strings.toCamelCase(NAME)}; + } + + @Override public Query parse(QueryParseContext parseContext) throws IOException, QueryParsingException { + XContentParser parser = parseContext.parser(); + + Query query = null; + float boost = 1.0f; + String script = null; + + String currentFieldName = null; + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else if (token == XContentParser.Token.START_OBJECT) { + if ("query".equals(currentFieldName)) { + query = parseContext.parseInnerQuery(); + } + } else if (token.isValue()) { + if ("script".equals(currentFieldName)) { + script = parser.text(); + } else if ("boost".equals(currentFieldName)) { + boost = parser.floatValue(); + } + } + } + if (query == null) { + throw new QueryParsingException(index, "[custom_score] requires 'query' field"); + } + if (script == null) { + throw new QueryParsingException(index, "[custom_score] requires 'script' field"); + } + FunctionScoreQuery functionScoreQuery = new FunctionScoreQuery(query, + new ScriptScoreFunction(new ScriptFieldsFunction(script, parseContext.scriptService(), parseContext.mapperService(), parseContext.indexCache().fieldData()))); + functionScoreQuery.setBoost(boost); + return functionScoreQuery; + } + + private static ThreadLocal>> cachedVars = new ThreadLocal>>() { + @Override protected ThreadLocals.CleanableValue> initialValue() { + return new ThreadLocals.CleanableValue>(new HashMap()); + } + }; + + public static class ScriptScoreFunction implements ScoreFunction { + + private final ScriptFieldsFunction scriptFieldsFunction; + + private Map vars; + + private ScriptScoreFunction(ScriptFieldsFunction scriptFieldsFunction) { + this.scriptFieldsFunction = scriptFieldsFunction; + } + + @Override public void setNextReader(IndexReader reader) { + scriptFieldsFunction.setNextReader(reader); + vars = cachedVars.get().get(); + vars.clear(); + } + + @Override public float score(int docId, float subQueryScore) { + vars.put("score", subQueryScore); + return ((Number) scriptFieldsFunction.execute(docId, vars)).floatValue(); + } + + @Override public Explanation explain(int docId, Explanation subQueryExpl) { + float score = score(docId, subQueryExpl.getValue()); + Explanation exp = new Explanation(score, "script score function: product of:"); + exp.addDetail(subQueryExpl); + return exp; + } + } +} \ No newline at end of file diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/QueryBuilders.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/QueryBuilders.java index 8f6efbfa6a3..6b9cd9a65b6 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/QueryBuilders.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/QueryBuilders.java @@ -280,6 +280,15 @@ public abstract class QueryBuilders { return new CustomBoostFactorQueryBuilder(queryBuilder); } + /** + * A query that allows to define a custom scoring script. + * + * @param queryBuilder The query to custom score + */ + public static CustomScoreQueryBuilder customScoreQuery(XContentQueryBuilder queryBuilder) { + return new CustomScoreQueryBuilder(queryBuilder); + } + /** * A more like this query that finds documents that are "like" the provided {@link MoreLikeThisQueryBuilder#likeText(String)} * which is checked against the fields the query is constructed with. diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/QueryParseContext.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/QueryParseContext.java index 34666a190ff..6d498af181a 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/QueryParseContext.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/QueryParseContext.java @@ -30,6 +30,7 @@ import org.elasticsearch.index.mapper.FieldMappers; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.query.QueryParsingException; import org.elasticsearch.index.similarity.SimilarityService; +import org.elasticsearch.script.ScriptService; import org.elasticsearch.util.xcontent.XContentParser; import javax.annotation.Nullable; @@ -42,6 +43,8 @@ public class QueryParseContext { private final Index index; + private final ScriptService scriptService; + private final MapperService mapperService; private final SimilarityService similarityService; @@ -55,10 +58,12 @@ public class QueryParseContext { private XContentParser parser; public QueryParseContext(Index index, XContentQueryParserRegistry queryParserRegistry, + ScriptService scriptService, MapperService mapperService, SimilarityService similarityService, IndexCache indexCache, IndexEngine indexEngine) { this.index = index; this.queryParserRegistry = queryParserRegistry; + this.scriptService = scriptService; this.mapperService = mapperService; this.similarityService = similarityService; this.indexCache = indexCache; @@ -73,6 +78,10 @@ public class QueryParseContext { return parser; } + public ScriptService scriptService() { + return scriptService; + } + public MapperService mapperService() { return mapperService; } diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/XContentIndexQueryParser.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/XContentIndexQueryParser.java index d525c5320f4..d82ba4f7e5e 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/XContentIndexQueryParser.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/XContentIndexQueryParser.java @@ -33,6 +33,7 @@ import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryParsingException; import org.elasticsearch.index.settings.IndexSettings; import org.elasticsearch.index.similarity.SimilarityService; +import org.elasticsearch.script.ScriptService; import org.elasticsearch.util.ThreadLocals; import org.elasticsearch.util.inject.Inject; import org.elasticsearch.util.inject.assistedinject.Assisted; @@ -61,12 +62,14 @@ public class XContentIndexQueryParser extends AbstractIndexComponent implements private ThreadLocal> cache = new ThreadLocal>() { @Override protected ThreadLocals.CleanableValue initialValue() { - return new ThreadLocals.CleanableValue(new QueryParseContext(index, queryParserRegistry, mapperService, similarityService, indexCache, indexEngine)); + return new ThreadLocals.CleanableValue(new QueryParseContext(index, queryParserRegistry, scriptService, mapperService, similarityService, indexCache, indexEngine)); } }; private final String name; + private final ScriptService scriptService; + private final MapperService mapperService; private final SimilarityService similarityService; @@ -78,7 +81,7 @@ public class XContentIndexQueryParser extends AbstractIndexComponent implements private final XContentQueryParserRegistry queryParserRegistry; @Inject public XContentIndexQueryParser(Index index, - @IndexSettings Settings indexSettings, + @IndexSettings Settings indexSettings, ScriptService scriptService, MapperService mapperService, IndexCache indexCache, IndexEngine indexEngine, AnalysisService analysisService, @Nullable SimilarityService similarityService, @Nullable Map namedQueryParsers, @@ -86,6 +89,7 @@ public class XContentIndexQueryParser extends AbstractIndexComponent implements @Assisted String name, @Assisted @Nullable Settings settings) { super(index, indexSettings); this.name = name; + this.scriptService = scriptService; this.mapperService = mapperService; this.similarityService = similarityService; this.indexCache = indexCache; diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/XContentQueryParserRegistry.java b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/XContentQueryParserRegistry.java index f06cf6b1fc5..80d6b485483 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/XContentQueryParserRegistry.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/index/query/xcontent/XContentQueryParserRegistry.java @@ -60,6 +60,7 @@ public class XContentQueryParserRegistry { add(queryParsersMap, new FilteredQueryParser(index, indexSettings)); add(queryParsersMap, new ConstantScoreQueryParser(index, indexSettings)); add(queryParsersMap, new CustomBoostFactorQueryParser(index, indexSettings)); + add(queryParsersMap, new CustomScoreQueryParser(index, indexSettings)); add(queryParsersMap, new SpanTermQueryParser(index, indexSettings)); add(queryParsersMap, new SpanNotQueryParser(index, indexSettings)); add(queryParsersMap, new SpanFirstQueryParser(index, indexSettings)); diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/node/internal/InternalNode.java b/modules/elasticsearch/src/main/java/org/elasticsearch/node/internal/InternalNode.java index 6dc74ac4228..eea1d87b613 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/node/internal/InternalNode.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/node/internal/InternalNode.java @@ -22,6 +22,7 @@ package org.elasticsearch.node.internal; import org.elasticsearch.ElasticSearchException; import org.elasticsearch.Version; import org.elasticsearch.action.TransportActionModule; +import org.elasticsearch.cache.NodeCacheModule; import org.elasticsearch.client.Client; import org.elasticsearch.client.node.NodeClientModule; import org.elasticsearch.cluster.ClusterModule; @@ -49,6 +50,7 @@ import org.elasticsearch.plugins.PluginsModule; import org.elasticsearch.plugins.PluginsService; import org.elasticsearch.rest.RestController; import org.elasticsearch.rest.RestModule; +import org.elasticsearch.script.ScriptModule; import org.elasticsearch.search.SearchModule; import org.elasticsearch.search.SearchService; import org.elasticsearch.threadpool.ThreadPool; @@ -114,12 +116,14 @@ public final class InternalNode implements Node { ArrayList modules = new ArrayList(); modules.add(new PluginsModule(settings, pluginsService)); + modules.add(new SettingsModule(settings)); modules.add(new NodeModule(this)); modules.add(new NetworkModule()); + modules.add(new NodeCacheModule()); + modules.add(new ScriptModule()); modules.add(new JmxModule(settings)); modules.add(new EnvironmentModule(environment)); modules.add(new ClusterNameModule(settings)); - modules.add(new SettingsModule(settings)); modules.add(new ThreadPoolModule(settings)); modules.add(new TimerModule()); modules.add(new DiscoveryModule(settings)); diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/script/ScriptModule.java b/modules/elasticsearch/src/main/java/org/elasticsearch/script/ScriptModule.java new file mode 100644 index 00000000000..42326d1f0e9 --- /dev/null +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/script/ScriptModule.java @@ -0,0 +1,32 @@ +/* + * Licensed to Elastic Search and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Elastic Search 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.util.inject.AbstractModule; + +/** + * @author kimchy (shay.banon) + */ +public class ScriptModule extends AbstractModule { + + @Override protected void configure() { + bind(ScriptService.class).asEagerSingleton(); + } +} diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/script/ScriptService.java b/modules/elasticsearch/src/main/java/org/elasticsearch/script/ScriptService.java new file mode 100644 index 00000000000..51a80a3349a --- /dev/null +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/script/ScriptService.java @@ -0,0 +1,83 @@ +/* + * Licensed to Elastic Search and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Elastic Search 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.util.component.AbstractComponent; +import org.elasticsearch.util.concurrent.ConcurrentCollections; +import org.elasticsearch.util.inject.Inject; +import org.elasticsearch.util.math.UnboxedMathUtils; +import org.elasticsearch.util.settings.Settings; +import org.mvel2.MVEL; +import org.mvel2.ParserContext; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Map; +import java.util.concurrent.ConcurrentMap; + +/** + * @author kimchy (shay.banon) + */ +public class ScriptService extends AbstractComponent { + + private final ConcurrentMap cache = ConcurrentCollections.newConcurrentMap(); + + private final ParserContext parserContext; + + @Inject public ScriptService(Settings settings) { + super(settings); + + parserContext = new ParserContext(); + parserContext.addPackageImport("java.util"); + parserContext.addPackageImport("org.elasticsearch.util.gnu.trove"); + parserContext.addPackageImport("org.elasticsearch.util.joda"); + parserContext.addImport("time", MVEL.getStaticMethod(System.class, "currentTimeMillis", new Class[0])); + // unboxed version of Math, better performance since conversion from boxed to unboxed my mvel is not needed + for (Method m : UnboxedMathUtils.class.getMethods()) { + if ((m.getModifiers() & Modifier.STATIC) > 0) { + parserContext.addImport(m.getName(), m); + } + } + } + + public Object compile(String script) { + Object compiled = cache.get(script); + if (compiled != null) { + return compiled; + } + synchronized (cache) { + compiled = cache.get(script); + if (compiled != null) { + return compiled; + } + compiled = MVEL.compileExpression(script, parserContext); + cache.put(script, compiled); + } + return compiled; + } + + public Object execute(Object script, Map vars) { + return MVEL.executeExpression(script, vars); + } + + public void clear() { + cache.clear(); + } +} diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/search/facets/histogram/HistogramFacetCollector.java b/modules/elasticsearch/src/main/java/org/elasticsearch/search/facets/histogram/HistogramFacetCollector.java index dbef6818add..754bffb1b63 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/search/facets/histogram/HistogramFacetCollector.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/search/facets/histogram/HistogramFacetCollector.java @@ -45,6 +45,8 @@ public class HistogramFacetCollector extends AbstractFacetCollector { private final String fieldName; + private final String indexFieldName; + private final long interval; private final HistogramFacet.ComparatorType comparatorType; @@ -68,6 +70,7 @@ public class HistogramFacetCollector extends AbstractFacetCollector { if (mapper == null) { throw new FacetPhaseExecutionException(facetName, "No mapping found for field [" + fieldName + "]"); } + indexFieldName = mapper.names().indexName(); fieldDataType = mapper.fieldDataType(); histoProc = new HistogramProc(interval); @@ -78,7 +81,7 @@ public class HistogramFacetCollector extends AbstractFacetCollector { } @Override protected void doSetNextReader(IndexReader reader, int docBase) throws IOException { - fieldData = (NumericFieldData) fieldDataCache.cache(fieldDataType, reader, fieldName, fieldDataOptions().withFreqs(false)); + fieldData = (NumericFieldData) fieldDataCache.cache(fieldDataType, reader, indexFieldName, fieldDataOptions().withFreqs(false)); } @Override public Facet facet() { diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/search/facets/histogram/KeyValueHistogramFacetCollector.java b/modules/elasticsearch/src/main/java/org/elasticsearch/search/facets/histogram/KeyValueHistogramFacetCollector.java index 68f6940fb54..d390a713d1b 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/search/facets/histogram/KeyValueHistogramFacetCollector.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/search/facets/histogram/KeyValueHistogramFacetCollector.java @@ -43,8 +43,10 @@ import static org.elasticsearch.index.field.data.FieldDataOptions.*; public class KeyValueHistogramFacetCollector extends AbstractFacetCollector { private final String keyFieldName; + private final String keyIndexFieldName; private final String valueFieldName; + private final String valueIndexFieldName; private final long interval; @@ -73,12 +75,14 @@ public class KeyValueHistogramFacetCollector extends AbstractFacetCollector { if (mapper == null) { throw new FacetPhaseExecutionException(facetName, "No mapping found for key_field [" + keyFieldName + "]"); } + keyIndexFieldName = mapper.names().indexName(); keyFieldDataType = mapper.fieldDataType(); mapper = mapperService.smartNameFieldMapper(valueFieldName); if (mapper == null) { throw new FacetPhaseExecutionException(facetName, "No mapping found for value_field [" + valueFieldName + "]"); } + valueIndexFieldName = mapper.names().indexName(); valueFieldDataType = mapper.fieldDataType(); } @@ -121,8 +125,8 @@ public class KeyValueHistogramFacetCollector extends AbstractFacetCollector { } @Override protected void doSetNextReader(IndexReader reader, int docBase) throws IOException { - keyFieldData = (NumericFieldData) fieldDataCache.cache(keyFieldDataType, reader, keyFieldName, fieldDataOptions().withFreqs(false)); - valueFieldData = (NumericFieldData) fieldDataCache.cache(valueFieldDataType, reader, valueFieldName, fieldDataOptions().withFreqs(false)); + keyFieldData = (NumericFieldData) fieldDataCache.cache(keyFieldDataType, reader, keyIndexFieldName, fieldDataOptions().withFreqs(false)); + valueFieldData = (NumericFieldData) fieldDataCache.cache(valueFieldDataType, reader, valueIndexFieldName, fieldDataOptions().withFreqs(false)); } @Override public Facet facet() { diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/search/facets/statistical/StatisticalFacetCollector.java b/modules/elasticsearch/src/main/java/org/elasticsearch/search/facets/statistical/StatisticalFacetCollector.java index 2352c56df12..bd79285c11b 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/search/facets/statistical/StatisticalFacetCollector.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/search/facets/statistical/StatisticalFacetCollector.java @@ -40,6 +40,8 @@ public class StatisticalFacetCollector extends AbstractFacetCollector { private final String fieldName; + private final String indexFieldName; + private final FieldDataCache fieldDataCache; private final FieldData.Type fieldDataType; @@ -57,6 +59,7 @@ public class StatisticalFacetCollector extends AbstractFacetCollector { if (mapper == null) { throw new FacetPhaseExecutionException(facetName, "No mapping found for field [" + fieldName + "]"); } + indexFieldName = mapper.names().indexName(); fieldDataType = mapper.fieldDataType(); } @@ -65,7 +68,7 @@ public class StatisticalFacetCollector extends AbstractFacetCollector { } @Override protected void doSetNextReader(IndexReader reader, int docBase) throws IOException { - fieldData = (NumericFieldData) fieldDataCache.cache(fieldDataType, reader, fieldName, fieldDataOptions().withFreqs(false)); + fieldData = (NumericFieldData) fieldDataCache.cache(fieldDataType, reader, indexFieldName, fieldDataOptions().withFreqs(false)); } @Override public Facet facet() { diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/search/facets/terms/TermsFacetCollector.java b/modules/elasticsearch/src/main/java/org/elasticsearch/search/facets/terms/TermsFacetCollector.java index a72bdc21ab6..ef720523d9a 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/search/facets/terms/TermsFacetCollector.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/search/facets/terms/TermsFacetCollector.java @@ -54,6 +54,8 @@ public class TermsFacetCollector extends AbstractFacetCollector { private final String fieldName; + private final String indexFieldName; + private final int size; private final FieldData.Type fieldDataType; @@ -68,11 +70,12 @@ public class TermsFacetCollector extends AbstractFacetCollector { this.size = size; FieldMapper mapper = mapperService.smartNameFieldMapper(fieldName); + this.fieldName = fieldName; if (mapper != null) { - this.fieldName = mapper.names().indexName(); + this.indexFieldName = mapper.names().indexName(); this.fieldDataType = mapper.fieldDataType(); } else { - this.fieldName = fieldName; + this.indexFieldName = fieldName; this.fieldDataType = FieldData.Type.STRING; } @@ -80,7 +83,7 @@ public class TermsFacetCollector extends AbstractFacetCollector { } @Override protected void doSetNextReader(IndexReader reader, int docBase) throws IOException { - fieldData = fieldDataCache.cache(fieldDataType, reader, fieldName, fieldDataOptions().withFreqs(false)); + fieldData = fieldDataCache.cache(fieldDataType, reader, indexFieldName, fieldDataOptions().withFreqs(false)); } @Override protected void doCollect(int doc) throws IOException { diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/search/query/QueryPhase.java b/modules/elasticsearch/src/main/java/org/elasticsearch/search/query/QueryPhase.java index ea50131c00a..1c992a500b6 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/search/query/QueryPhase.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/search/query/QueryPhase.java @@ -29,7 +29,7 @@ import org.elasticsearch.search.internal.SearchContext; import org.elasticsearch.util.collect.ImmutableMap; import org.elasticsearch.util.inject.Inject; import org.elasticsearch.util.lucene.search.TermFilter; -import org.elasticsearch.util.lucene.search.function.BoostFactorFunctionProvider; +import org.elasticsearch.util.lucene.search.function.BoostScoreFunction; import org.elasticsearch.util.lucene.search.function.FunctionScoreQuery; import java.util.Map; @@ -63,7 +63,7 @@ public class QueryPhase implements SearchPhase { throw new SearchParseException(context, "No query specified in search request"); } if (context.queryBoost() != 1.0f) { - context.query(new FunctionScoreQuery(context.query(), new BoostFactorFunctionProvider(context.queryBoost()))); + context.query(new FunctionScoreQuery(context.query(), new BoostScoreFunction(context.queryBoost()))); } facetsPhase.preProcess(context); } diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/util/lucene/search/function/BoostFactorFunctionProvider.java b/modules/elasticsearch/src/main/java/org/elasticsearch/util/lucene/search/function/BoostScoreFunction.java similarity index 86% rename from modules/elasticsearch/src/main/java/org/elasticsearch/util/lucene/search/function/BoostFactorFunctionProvider.java rename to modules/elasticsearch/src/main/java/org/elasticsearch/util/lucene/search/function/BoostScoreFunction.java index 52e20625fe7..bfa57f74fee 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/util/lucene/search/function/BoostFactorFunctionProvider.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/util/lucene/search/function/BoostScoreFunction.java @@ -25,11 +25,11 @@ import org.apache.lucene.search.Explanation; /** * @author kimchy (shay.banon) */ -public class BoostFactorFunctionProvider implements FunctionProvider, Function { +public class BoostScoreFunction implements ScoreFunction { private final float boost; - public BoostFactorFunctionProvider(float boost) { + public BoostScoreFunction(float boost) { this.boost = boost; } @@ -38,8 +38,8 @@ public class BoostFactorFunctionProvider implements FunctionProvider, Function { return boost; } - @Override public Function function(IndexReader reader) { - return this; + @Override public void setNextReader(IndexReader reader) { + // nothing to do here... } @Override public float score(int docId, float subQueryScore) { @@ -57,7 +57,7 @@ public class BoostFactorFunctionProvider implements FunctionProvider, Function { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - BoostFactorFunctionProvider that = (BoostFactorFunctionProvider) o; + BoostScoreFunction that = (BoostScoreFunction) o; if (Float.compare(that.boost, boost) != 0) return false; diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/util/lucene/search/function/FunctionScoreQuery.java b/modules/elasticsearch/src/main/java/org/elasticsearch/util/lucene/search/function/FunctionScoreQuery.java index 40a36132e4a..4d22717772f 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/util/lucene/search/function/FunctionScoreQuery.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/util/lucene/search/function/FunctionScoreQuery.java @@ -34,20 +34,20 @@ import java.util.Set; */ public class FunctionScoreQuery extends Query { - private Query subQuery; - private FunctionProvider functionProvider; + Query subQuery; + final ScoreFunction function; - public FunctionScoreQuery(Query subQuery, FunctionProvider functionProvider) { + public FunctionScoreQuery(Query subQuery, ScoreFunction function) { this.subQuery = subQuery; - this.functionProvider = functionProvider; + this.function = function; } public Query getSubQuery() { return subQuery; } - public FunctionProvider getFunctionProvider() { - return functionProvider; + public ScoreFunction getFunction() { + return function; } @Override @@ -69,7 +69,7 @@ public class FunctionScoreQuery extends Query { return new CustomBoostFactorWeight(searcher); } - private class CustomBoostFactorWeight extends Weight { + class CustomBoostFactorWeight extends Weight { Searcher searcher; Weight subQueryWeight; @@ -105,7 +105,8 @@ public class FunctionScoreQuery extends Query { if (subQueryScorer == null) { return null; } - return new CustomBoostFactorScorer(getSimilarity(searcher), this, subQueryScorer, functionProvider.function(reader)); + function.setNextReader(reader); + return new CustomBoostFactorScorer(getSimilarity(searcher), this, subQueryScorer); } @Override @@ -115,7 +116,8 @@ public class FunctionScoreQuery extends Query { return subQueryExpl; } - Explanation functionExplanation = functionProvider.function(reader).explain(doc, subQueryExpl); + function.setNextReader(reader); + Explanation functionExplanation = function.explain(doc, subQueryExpl); float sc = getValue() * functionExplanation.getValue(); Explanation res = new ComplexExplanation(true, sc, "custom score, product of:"); res.addDetail(functionExplanation); @@ -125,16 +127,14 @@ public class FunctionScoreQuery extends Query { } - private class CustomBoostFactorScorer extends Scorer { + class CustomBoostFactorScorer extends Scorer { private final float subQueryWeight; private final Scorer scorer; - private final Function function; - private CustomBoostFactorScorer(Similarity similarity, CustomBoostFactorWeight w, Scorer scorer, Function function) throws IOException { + private CustomBoostFactorScorer(Similarity similarity, CustomBoostFactorWeight w, Scorer scorer) throws IOException { super(similarity); this.subQueryWeight = w.getValue(); this.scorer = scorer; - this.function = function; } @Override @@ -161,7 +161,7 @@ public class FunctionScoreQuery extends Query { public String toString(String field) { StringBuilder sb = new StringBuilder(); - sb.append("custom score (").append(subQuery.toString(field)).append(",function=").append(functionProvider).append(')'); + sb.append("custom score (").append(subQuery.toString(field)).append(",function=").append(function).append(')'); sb.append(ToStringUtils.boost(getBoost())); return sb.toString(); } @@ -171,11 +171,11 @@ public class FunctionScoreQuery extends Query { FunctionScoreQuery other = (FunctionScoreQuery) o; return this.getBoost() == other.getBoost() && this.subQuery.equals(other.subQuery) - && this.functionProvider.equals(other.functionProvider); + && this.function.equals(other.function); } public int hashCode() { - return subQuery.hashCode() + 31 * functionProvider.hashCode() ^ Float.floatToIntBits(getBoost()); + return subQuery.hashCode() + 31 * function.hashCode() ^ Float.floatToIntBits(getBoost()); } } diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/util/lucene/search/function/Function.java b/modules/elasticsearch/src/main/java/org/elasticsearch/util/lucene/search/function/ScoreFunction.java similarity index 89% rename from modules/elasticsearch/src/main/java/org/elasticsearch/util/lucene/search/function/Function.java rename to modules/elasticsearch/src/main/java/org/elasticsearch/util/lucene/search/function/ScoreFunction.java index 205d327ca5b..dda0ca3d8b8 100644 --- a/modules/elasticsearch/src/main/java/org/elasticsearch/util/lucene/search/function/Function.java +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/util/lucene/search/function/ScoreFunction.java @@ -19,12 +19,15 @@ package org.elasticsearch.util.lucene.search.function; +import org.apache.lucene.index.IndexReader; import org.apache.lucene.search.Explanation; /** * @author kimchy (shay.banon) */ -public interface Function { +public interface ScoreFunction { + + void setNextReader(IndexReader reader); float score(int docId, float subQueryScore); diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/util/math/MathRuntimeException.java b/modules/elasticsearch/src/main/java/org/elasticsearch/util/math/MathRuntimeException.java new file mode 100644 index 00000000000..550b0f0663e --- /dev/null +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/util/math/MathRuntimeException.java @@ -0,0 +1,540 @@ +/* + * Licensed to Elastic Search and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Elastic Search 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.util.math; + +import java.io.EOFException; +import java.io.IOException; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.text.MessageFormat; +import java.text.ParseException; +import java.util.*; + +/** + * Base class for commons-math unchecked exceptions. + * + * @version $Revision: 822850 $ $Date: 2009-10-07 14:56:42 -0400 (Wed, 07 Oct 2009) $ + * @since 2.0 + */ +public class MathRuntimeException extends RuntimeException { + + /** + * Serializable version identifier. + */ + private static final long serialVersionUID = -5128983364075381060L; + + /** + * Pattern used to build the message. + */ + private final String pattern; + + /** + * Arguments used to build the message. + */ + private final Object[] arguments; + + /** + * Constructs a new MathRuntimeException with specified + * formatted detail message. + * Message formatting is delegated to {@link java.text.MessageFormat}. + * + * @param pattern format specifier + * @param arguments format arguments + */ + public MathRuntimeException(final String pattern, final Object... arguments) { + this.pattern = pattern; + this.arguments = (arguments == null) ? new Object[0] : arguments.clone(); + } + + /** + * Constructs a new MathRuntimeException with specified + * nested Throwable root cause. + * + * @param rootCause the exception or error that caused this exception + * to be thrown. + */ + public MathRuntimeException(final Throwable rootCause) { + super(rootCause); + this.pattern = getMessage(); + this.arguments = new Object[0]; + } + + /** + * Constructs a new MathRuntimeException with specified + * formatted detail message and nested Throwable root cause. + * Message formatting is delegated to {@link java.text.MessageFormat}. + * + * @param rootCause the exception or error that caused this exception + * to be thrown. + * @param pattern format specifier + * @param arguments format arguments + */ + public MathRuntimeException(final Throwable rootCause, + final String pattern, final Object... arguments) { + super(rootCause); + this.pattern = pattern; + this.arguments = (arguments == null) ? new Object[0] : arguments.clone(); + } + + /** + * Translate a string to a given locale. + * + * @param s string to translate + * @param locale locale into which to translate the string + * @return translated string or original string + * for unsupported locales or unknown strings + */ + private static String translate(final String s, final Locale locale) { + try { + ResourceBundle bundle = + ResourceBundle.getBundle("org.apache.commons.math.MessagesResources", locale); + if (bundle.getLocale().getLanguage().equals(locale.getLanguage())) { + // the value of the resource is the translated string + return bundle.getString(s); + } + + } catch (MissingResourceException mre) { + // do nothing here + } + + // the locale is not supported or the resource is unknown + // don't translate and fall back to using the string as is + return s; + + } + + /** + * Builds a message string by from a pattern and its arguments. + * + * @param locale Locale in which the message should be translated + * @param pattern format specifier + * @param arguments format arguments + * @return a message string + */ + private static String buildMessage(final Locale locale, final String pattern, + final Object... arguments) { + return (pattern == null) ? "" : new MessageFormat(translate(pattern, locale), locale).format(arguments); + } + + /** + * Gets the pattern used to build the message of this throwable. + * + * @return the pattern used to build the message of this throwable + */ + public String getPattern() { + return pattern; + } + + /** + * Gets the arguments used to build the message of this throwable. + * + * @return the arguments used to build the message of this throwable + */ + public Object[] getArguments() { + return arguments.clone(); + } + + /** + * Gets the message in a specified locale. + * + * @param locale Locale in which the message should be translated + * @return localized message + */ + public String getMessage(final Locale locale) { + return buildMessage(locale, pattern, arguments); + } + + /** + * {@inheritDoc} + */ + @Override + public String getMessage() { + return getMessage(Locale.US); + } + + /** + * {@inheritDoc} + */ + @Override + public String getLocalizedMessage() { + return getMessage(Locale.getDefault()); + } + + /** + * Prints the stack trace of this exception to the standard error stream. + */ + @Override + public void printStackTrace() { + printStackTrace(System.err); + } + + /** + * Prints the stack trace of this exception to the specified stream. + * + * @param out the PrintStream to use for output + */ + @Override + public void printStackTrace(final PrintStream out) { + synchronized (out) { + PrintWriter pw = new PrintWriter(out, false); + printStackTrace(pw); + // Flush the PrintWriter before it's GC'ed. + pw.flush(); + } + } + + /** + * Constructs a new ArithmeticException with specified formatted detail message. + * Message formatting is delegated to {@link java.text.MessageFormat}. + * + * @param pattern format specifier + * @param arguments format arguments + * @return built exception + */ + public static ArithmeticException createArithmeticException(final String pattern, + final Object... arguments) { + return new ArithmeticException() { + + /** Serializable version identifier. */ + private static final long serialVersionUID = 7705628723242533939L; + + /** {@inheritDoc} */ + @Override + public String getMessage() { + return buildMessage(Locale.US, pattern, arguments); + } + + /** {@inheritDoc} */ + @Override + public String getLocalizedMessage() { + return buildMessage(Locale.getDefault(), pattern, arguments); + } + + }; + } + + /** + * Constructs a new ArrayIndexOutOfBoundsException with specified formatted detail message. + * Message formatting is delegated to {@link java.text.MessageFormat}. + * + * @param pattern format specifier + * @param arguments format arguments + * @return built exception + */ + public static ArrayIndexOutOfBoundsException createArrayIndexOutOfBoundsException(final String pattern, + final Object... arguments) { + return new ArrayIndexOutOfBoundsException() { + + /** Serializable version identifier. */ + private static final long serialVersionUID = -3394748305449283486L; + + /** {@inheritDoc} */ + @Override + public String getMessage() { + return buildMessage(Locale.US, pattern, arguments); + } + + /** {@inheritDoc} */ + @Override + public String getLocalizedMessage() { + return buildMessage(Locale.getDefault(), pattern, arguments); + } + + }; + } + + /** + * Constructs a new EOFException with specified formatted detail message. + * Message formatting is delegated to {@link java.text.MessageFormat}. + * + * @param pattern format specifier + * @param arguments format arguments + * @return built exception + */ + public static EOFException createEOFException(final String pattern, + final Object... arguments) { + return new EOFException() { + + /** Serializable version identifier. */ + private static final long serialVersionUID = 279461544586092584L; + + /** {@inheritDoc} */ + @Override + public String getMessage() { + return buildMessage(Locale.US, pattern, arguments); + } + + /** {@inheritDoc} */ + @Override + public String getLocalizedMessage() { + return buildMessage(Locale.getDefault(), pattern, arguments); + } + + }; + } + + /** + * Constructs a new IOException with specified nested + * Throwable root cause. + *

This factory method allows chaining of other exceptions within an + * IOException even for Java 5. The constructor for + * IOException with a cause parameter was introduced only + * with Java 6.

+ * + * @param rootCause the exception or error that caused this exception + * to be thrown. + * @return built exception + */ + public static IOException createIOException(final Throwable rootCause) { + IOException ioe = new IOException(rootCause.getLocalizedMessage()); + ioe.initCause(rootCause); + return ioe; + } + + /** + * Constructs a new IllegalArgumentException with specified formatted detail message. + * Message formatting is delegated to {@link java.text.MessageFormat}. + * + * @param pattern format specifier + * @param arguments format arguments + * @return built exception + */ + public static IllegalArgumentException createIllegalArgumentException(final String pattern, + final Object... arguments) { + return new IllegalArgumentException() { + + /** Serializable version identifier. */ + private static final long serialVersionUID = -6555453980658317913L; + + /** {@inheritDoc} */ + @Override + public String getMessage() { + return buildMessage(Locale.US, pattern, arguments); + } + + /** {@inheritDoc} */ + @Override + public String getLocalizedMessage() { + return buildMessage(Locale.getDefault(), pattern, arguments); + } + + }; + } + + /** + * Constructs a new IllegalArgumentException with specified nested + * Throwable root cause. + * + * @param rootCause the exception or error that caused this exception + * to be thrown. + * @return built exception + */ + public static IllegalArgumentException createIllegalArgumentException(final Throwable rootCause) { + IllegalArgumentException iae = new IllegalArgumentException(rootCause.getLocalizedMessage()); + iae.initCause(rootCause); + return iae; + } + + /** + * Constructs a new IllegalStateException with specified formatted detail message. + * Message formatting is delegated to {@link java.text.MessageFormat}. + * + * @param pattern format specifier + * @param arguments format arguments + * @return built exception + */ + public static IllegalStateException createIllegalStateException(final String pattern, + final Object... arguments) { + return new IllegalStateException() { + + /** Serializable version identifier. */ + private static final long serialVersionUID = -95247648156277208L; + + /** {@inheritDoc} */ + @Override + public String getMessage() { + return buildMessage(Locale.US, pattern, arguments); + } + + /** {@inheritDoc} */ + @Override + public String getLocalizedMessage() { + return buildMessage(Locale.getDefault(), pattern, arguments); + } + + }; + } + + /** + * Constructs a new ConcurrentModificationException with specified formatted detail message. + * Message formatting is delegated to {@link java.text.MessageFormat}. + * + * @param pattern format specifier + * @param arguments format arguments + * @return built exception + */ + public static ConcurrentModificationException createConcurrentModificationException(final String pattern, + final Object... arguments) { + return new ConcurrentModificationException() { + + /** Serializable version identifier. */ + private static final long serialVersionUID = 6134247282754009421L; + + /** {@inheritDoc} */ + @Override + public String getMessage() { + return buildMessage(Locale.US, pattern, arguments); + } + + /** {@inheritDoc} */ + @Override + public String getLocalizedMessage() { + return buildMessage(Locale.getDefault(), pattern, arguments); + } + + }; + } + + /** + * Constructs a new NoSuchElementException with specified formatted detail message. + * Message formatting is delegated to {@link java.text.MessageFormat}. + * + * @param pattern format specifier + * @param arguments format arguments + * @return built exception + */ + public static NoSuchElementException createNoSuchElementException(final String pattern, + final Object... arguments) { + return new NoSuchElementException() { + + /** Serializable version identifier. */ + private static final long serialVersionUID = 7304273322489425799L; + + /** {@inheritDoc} */ + @Override + public String getMessage() { + return buildMessage(Locale.US, pattern, arguments); + } + + /** {@inheritDoc} */ + @Override + public String getLocalizedMessage() { + return buildMessage(Locale.getDefault(), pattern, arguments); + } + + }; + } + + /** + * Constructs a new NullPointerException with specified formatted detail message. + * Message formatting is delegated to {@link java.text.MessageFormat}. + * + * @param pattern format specifier + * @param arguments format arguments + * @return built exception + */ + public static NullPointerException createNullPointerException(final String pattern, + final Object... arguments) { + return new NullPointerException() { + + /** Serializable version identifier. */ + private static final long serialVersionUID = -3075660477939965216L; + + /** {@inheritDoc} */ + @Override + public String getMessage() { + return buildMessage(Locale.US, pattern, arguments); + } + + /** {@inheritDoc} */ + @Override + public String getLocalizedMessage() { + return buildMessage(Locale.getDefault(), pattern, arguments); + } + + }; + } + + /** + * Constructs a new ParseException with specified + * formatted detail message. + * Message formatting is delegated to {@link java.text.MessageFormat}. + * + * @param offset offset at which error occurred + * @param pattern format specifier + * @param arguments format arguments + * @return built exception + */ + public static ParseException createParseException(final int offset, + final String pattern, + final Object... arguments) { + return new ParseException(null, offset) { + + /** Serializable version identifier. */ + private static final long serialVersionUID = -1103502177342465975L; + + /** {@inheritDoc} */ + @Override + public String getMessage() { + return buildMessage(Locale.US, pattern, arguments); + } + + /** {@inheritDoc} */ + @Override + public String getLocalizedMessage() { + return buildMessage(Locale.getDefault(), pattern, arguments); + } + + }; + } + + /** + * Create an {@link java.lang.RuntimeException} for an internal error. + * + * @param cause underlying cause + * @return an {@link java.lang.RuntimeException} for an internal error + */ + public static RuntimeException createInternalError(final Throwable cause) { + + final String pattern = "internal error, please fill a bug report at {0}"; + final String argument = "https://issues.apache.org/jira/browse/MATH"; + + return new RuntimeException() { + + /** Serializable version identifier. */ + private static final long serialVersionUID = -201865440834027016L; + + /** {@inheritDoc} */ + @Override + public String getMessage() { + return buildMessage(Locale.US, pattern, argument); + } + + /** {@inheritDoc} */ + @Override + public String getLocalizedMessage() { + return buildMessage(Locale.getDefault(), pattern, argument); + } + + }; + + } + +} diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/util/math/MathUtils.java b/modules/elasticsearch/src/main/java/org/elasticsearch/util/math/MathUtils.java new file mode 100644 index 00000000000..64b033794a5 --- /dev/null +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/util/math/MathUtils.java @@ -0,0 +1,1865 @@ +/* + * Licensed to Elastic Search and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Elastic Search 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.util.math; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Arrays; + +/** + * Some useful additions to the built-in functions in {@link Math}. + * + * @version $Revision: 927249 $ $Date: 2010-03-24 21:06:51 -0400 (Wed, 24 Mar 2010) $ + */ +public final class MathUtils { + + + /** + * Smallest positive number such that 1 - EPSILON is not numerically equal to 1. + */ + public static final double EPSILON = 0x1.0p-53; + + /** + * Safe minimum, such that 1 / SAFE_MIN does not overflow. + *

In IEEE 754 arithmetic, this is also the smallest normalized + * number 2-1022.

+ */ + public static final double SAFE_MIN = 0x1.0p-1022; + + /** + * 2 π. + * + * @since 2.1 + */ + public static final double TWO_PI = 2 * Math.PI; + + /** + * -1.0 cast as a byte. + */ + private static final byte NB = (byte) -1; + + /** + * -1.0 cast as a short. + */ + private static final short NS = (short) -1; + + /** + * 1.0 cast as a byte. + */ + private static final byte PB = (byte) 1; + + /** + * 1.0 cast as a short. + */ + private static final short PS = (short) 1; + + /** + * 0.0 cast as a byte. + */ + private static final byte ZB = (byte) 0; + + /** + * 0.0 cast as a short. + */ + private static final short ZS = (short) 0; + + /** + * Gap between NaN and regular numbers. + */ + private static final int NAN_GAP = 4 * 1024 * 1024; + + /** + * Offset to order signed double numbers lexicographically. + */ + private static final long SGN_MASK = 0x8000000000000000L; + + /** + * All long-representable factorials + */ + private static final long[] FACTORIALS = new long[]{ + 1l, 1l, 2l, + 6l, 24l, 120l, + 720l, 5040l, 40320l, + 362880l, 3628800l, 39916800l, + 479001600l, 6227020800l, 87178291200l, + 1307674368000l, 20922789888000l, 355687428096000l, + 6402373705728000l, 121645100408832000l, 2432902008176640000l}; + + /** + * Private Constructor + */ + private MathUtils() { + super(); + } + + /** + * Add two integers, checking for overflow. + * + * @param x an addend + * @param y an addend + * @return the sum x+y + * @throws ArithmeticException if the result can not be represented as an + * int + * @since 1.1 + */ + public static int addAndCheck(int x, int y) { + long s = (long) x + (long) y; + if (s < Integer.MIN_VALUE || s > Integer.MAX_VALUE) { + throw new ArithmeticException("overflow: add"); + } + return (int) s; + } + + /** + * Add two long integers, checking for overflow. + * + * @param a an addend + * @param b an addend + * @return the sum a+b + * @throws ArithmeticException if the result can not be represented as an + * long + * @since 1.2 + */ + public static long addAndCheck(long a, long b) { + return addAndCheck(a, b, "overflow: add"); + } + + /** + * Add two long integers, checking for overflow. + * + * @param a an addend + * @param b an addend + * @param msg the message to use for any thrown exception. + * @return the sum a+b + * @throws ArithmeticException if the result can not be represented as an + * long + * @since 1.2 + */ + private static long addAndCheck(long a, long b, String msg) { + long ret; + if (a > b) { + // use symmetry to reduce boundary cases + ret = addAndCheck(b, a, msg); + } else { + // assert a <= b + + if (a < 0) { + if (b < 0) { + // check for negative overflow + if (Long.MIN_VALUE - b <= a) { + ret = a + b; + } else { + throw new ArithmeticException(msg); + } + } else { + // opposite sign addition is always safe + ret = a + b; + } + } else { + // assert a >= 0 + // assert b >= 0 + + // check for positive overflow + if (a <= Long.MAX_VALUE - b) { + ret = a + b; + } else { + throw new ArithmeticException(msg); + } + } + } + return ret; + } + + /** + * Returns an exact representation of the Binomial + * Coefficient, "n choose k", the number of + * k-element subsets that can be selected from an + * n-element set. + *

+ * Preconditions: + *

    + *
  • 0 <= k <= n (otherwise + * IllegalArgumentException is thrown)
  • + *
  • The result is small enough to fit into a long. The + * largest value of n for which all coefficients are + * < Long.MAX_VALUE is 66. If the computed value exceeds + * Long.MAX_VALUE an ArithMeticException is + * thrown.
  • + *

+ * + * @param n the size of the set + * @param k the size of the subsets to be counted + * @return n choose k + * @throws IllegalArgumentException if preconditions are not met. + * @throws ArithmeticException if the result is too large to be represented + * by a long integer. + */ + public static long binomialCoefficient(final int n, final int k) { + checkBinomial(n, k); + if ((n == k) || (k == 0)) { + return 1; + } + if ((k == 1) || (k == n - 1)) { + return n; + } + // Use symmetry for large k + if (k > n / 2) + return binomialCoefficient(n, n - k); + + // We use the formula + // (n choose k) = n! / (n-k)! / k! + // (n choose k) == ((n-k+1)*...*n) / (1*...*k) + // which could be written + // (n choose k) == (n-1 choose k-1) * n / k + long result = 1; + if (n <= 61) { + // For n <= 61, the naive implementation cannot overflow. + int i = n - k + 1; + for (int j = 1; j <= k; j++) { + result = result * i / j; + i++; + } + } else if (n <= 66) { + // For n > 61 but n <= 66, the result cannot overflow, + // but we must take care not to overflow intermediate values. + int i = n - k + 1; + for (int j = 1; j <= k; j++) { + // We know that (result * i) is divisible by j, + // but (result * i) may overflow, so we split j: + // Filter out the gcd, d, so j/d and i/d are integer. + // result is divisible by (j/d) because (j/d) + // is relative prime to (i/d) and is a divisor of + // result * (i/d). + final long d = gcd(i, j); + result = (result / (j / d)) * (i / d); + i++; + } + } else { + // For n > 66, a result overflow might occur, so we check + // the multiplication, taking care to not overflow + // unnecessary. + int i = n - k + 1; + for (int j = 1; j <= k; j++) { + final long d = gcd(i, j); + result = mulAndCheck(result / (j / d), i / d); + i++; + } + } + return result; + } + + /** + * Returns a double representation of the Binomial + * Coefficient, "n choose k", the number of + * k-element subsets that can be selected from an + * n-element set. + *

+ * Preconditions: + *

    + *
  • 0 <= k <= n (otherwise + * IllegalArgumentException is thrown)
  • + *
  • The result is small enough to fit into a double. The + * largest value of n for which all coefficients are < + * Double.MAX_VALUE is 1029. If the computed value exceeds Double.MAX_VALUE, + * Double.POSITIVE_INFINITY is returned
  • + *

+ * + * @param n the size of the set + * @param k the size of the subsets to be counted + * @return n choose k + * @throws IllegalArgumentException if preconditions are not met. + */ + public static double binomialCoefficientDouble(final int n, final int k) { + checkBinomial(n, k); + if ((n == k) || (k == 0)) { + return 1d; + } + if ((k == 1) || (k == n - 1)) { + return n; + } + if (k > n / 2) { + return binomialCoefficientDouble(n, n - k); + } + if (n < 67) { + return binomialCoefficient(n, k); + } + + double result = 1d; + for (int i = 1; i <= k; i++) { + result *= (double) (n - k + i) / (double) i; + } + + return Math.floor(result + 0.5); + } + + /** + * Returns the natural log of the Binomial + * Coefficient, "n choose k", the number of + * k-element subsets that can be selected from an + * n-element set. + *

+ * Preconditions: + *

    + *
  • 0 <= k <= n (otherwise + * IllegalArgumentException is thrown)
  • + *

+ * + * @param n the size of the set + * @param k the size of the subsets to be counted + * @return n choose k + * @throws IllegalArgumentException if preconditions are not met. + */ + public static double binomialCoefficientLog(final int n, final int k) { + checkBinomial(n, k); + if ((n == k) || (k == 0)) { + return 0; + } + if ((k == 1) || (k == n - 1)) { + return Math.log(n); + } + + /* + * For values small enough to do exact integer computation, + * return the log of the exact value + */ + if (n < 67) { + return Math.log(binomialCoefficient(n, k)); + } + + /* + * Return the log of binomialCoefficientDouble for values that will not + * overflow binomialCoefficientDouble + */ + if (n < 1030) { + return Math.log(binomialCoefficientDouble(n, k)); + } + + if (k > n / 2) { + return binomialCoefficientLog(n, n - k); + } + + /* + * Sum logs for values that could overflow + */ + double logSum = 0; + + // n!/(n-k)! + for (int i = n - k + 1; i <= n; i++) { + logSum += Math.log(i); + } + + // divide by k! + for (int i = 2; i <= k; i++) { + logSum -= Math.log(i); + } + + return logSum; + } + + /** + * Check binomial preconditions. + * + * @param n the size of the set + * @param k the size of the subsets to be counted + * @throws IllegalArgumentException if preconditions are not met. + */ + private static void checkBinomial(final int n, final int k) + throws IllegalArgumentException { + if (n < k) { + throw MathRuntimeException.createIllegalArgumentException( + "must have n >= k for binomial coefficient (n,k), got n = {0}, k = {1}", + n, k); + } + if (n < 0) { + throw MathRuntimeException.createIllegalArgumentException( + "must have n >= 0 for binomial coefficient (n,k), got n = {0}", + n); + } + } + + /** + * Compares two numbers given some amount of allowed error. + * + * @param x the first number + * @param y the second number + * @param eps the amount of error to allow when checking for equality + * @return
  • 0 if {@link #equals(double, double, double) equals(x, y, eps)}
  • + *
  • < 0 if !{@link #equals(double, double, double) equals(x, y, eps)} && x < y
  • + *
  • > 0 if !{@link #equals(double, double, double) equals(x, y, eps)} && x > y
+ */ + public static int compareTo(double x, double y, double eps) { + if (equals(x, y, eps)) { + return 0; + } else if (x < y) { + return -1; + } + return 1; + } + + /** + * Returns the + * hyperbolic cosine of x. + * + * @param x double value for which to find the hyperbolic cosine + * @return hyperbolic cosine of x + */ + public static double cosh(double x) { + return (Math.exp(x) + Math.exp(-x)) / 2.0; + } + + /** + * Returns true iff both arguments are NaN or neither is NaN and they are + * equal + * + * @param x first value + * @param y second value + * @return true if the values are equal or both are NaN + */ + public static boolean equals(double x, double y) { + return (Double.isNaN(x) && Double.isNaN(y)) || x == y; + } + + /** + * Returns true iff both arguments are equal or within the range of allowed + * error (inclusive). + *

+ * Two NaNs are considered equals, as are two infinities with same sign. + *

+ * + * @param x first value + * @param y second value + * @param eps the amount of absolute error to allow + * @return true if the values are equal or within range of each other + */ + public static boolean equals(double x, double y, double eps) { + return equals(x, y) || (Math.abs(y - x) <= eps); + } + + /** + * Returns true iff both arguments are equal or within the range of allowed + * error (inclusive). + * Adapted from + * Bruce Dawson + * + * @param x first value + * @param y second value + * @param maxUlps {@code (maxUlps - 1)} is the number of floating point + * values between {@code x} and {@code y}. + * @return {@code true} if there are less than {@code maxUlps} floating + * point values between {@code x} and {@code y} + */ + public static boolean equals(double x, double y, int maxUlps) { + // Check that "maxUlps" is non-negative and small enough so that the + // default NAN won't compare as equal to anything. + assert maxUlps > 0 && maxUlps < NAN_GAP; + + long xInt = Double.doubleToLongBits(x); + long yInt = Double.doubleToLongBits(y); + + // Make lexicographically ordered as a two's-complement integer. + if (xInt < 0) { + xInt = SGN_MASK - xInt; + } + if (yInt < 0) { + yInt = SGN_MASK - yInt; + } + + return Math.abs(xInt - yInt) <= maxUlps; + } + + /** + * Returns true iff both arguments are null or have same dimensions + * and all their elements are {@link #equals(double,double) equals} + * + * @param x first array + * @param y second array + * @return true if the values are both null or have same dimension + * and equal elements + * @since 1.2 + */ + public static boolean equals(double[] x, double[] y) { + if ((x == null) || (y == null)) { + return !((x == null) ^ (y == null)); + } + if (x.length != y.length) { + return false; + } + for (int i = 0; i < x.length; ++i) { + if (!equals(x[i], y[i])) { + return false; + } + } + return true; + } + + /** + * Returns n!. Shorthand for n Factorial, the + * product of the numbers 1,...,n. + *

+ * Preconditions: + *

    + *
  • n >= 0 (otherwise + * IllegalArgumentException is thrown)
  • + *
  • The result is small enough to fit into a long. The + * largest value of n for which n! < + * Long.MAX_VALUE is 20. If the computed value exceeds Long.MAX_VALUE + * an ArithMeticException is thrown.
  • + *
+ *

+ * + * @param n argument + * @return n! + * @throws ArithmeticException if the result is too large to be represented + * by a long integer. + * @throws IllegalArgumentException if n < 0 + */ + public static long factorial(final int n) { + if (n < 0) { + throw MathRuntimeException.createIllegalArgumentException( + "must have n >= 0 for n!, got n = {0}", + n); + } + if (n > 20) { + throw new ArithmeticException( + "factorial value is too large to fit in a long"); + } + return FACTORIALS[n]; + } + + /** + * Returns n!. Shorthand for n Factorial, the + * product of the numbers 1,...,n as a double. + *

+ * Preconditions: + *

    + *
  • n >= 0 (otherwise + * IllegalArgumentException is thrown)
  • + *
  • The result is small enough to fit into a double. The + * largest value of n for which n! < + * Double.MAX_VALUE is 170. If the computed value exceeds + * Double.MAX_VALUE, Double.POSITIVE_INFINITY is returned
  • + *
+ *

+ * + * @param n argument + * @return n! + * @throws IllegalArgumentException if n < 0 + */ + public static double factorialDouble(final int n) { + if (n < 0) { + throw MathRuntimeException.createIllegalArgumentException( + "must have n >= 0 for n!, got n = {0}", + n); + } + if (n < 21) { + return factorial(n); + } + return Math.floor(Math.exp(factorialLog(n)) + 0.5); + } + + /** + * Returns the natural logarithm of n!. + *

+ * Preconditions: + *

    + *
  • n >= 0 (otherwise + * IllegalArgumentException is thrown)
  • + *

+ * + * @param n argument + * @return n! + * @throws IllegalArgumentException if preconditions are not met. + */ + public static double factorialLog(final int n) { + if (n < 0) { + throw MathRuntimeException.createIllegalArgumentException( + "must have n >= 0 for n!, got n = {0}", + n); + } + if (n < 21) { + return Math.log(factorial(n)); + } + double logSum = 0; + for (int i = 2; i <= n; i++) { + logSum += Math.log(i); + } + return logSum; + } + + /** + *

+ * Gets the greatest common divisor of the absolute value of two numbers, + * using the "binary gcd" method which avoids division and modulo + * operations. See Knuth 4.5.2 algorithm B. This algorithm is due to Josef + * Stein (1961). + *

+ * Special cases: + *
    + *
  • The invocations + * gcd(Integer.MIN_VALUE, Integer.MIN_VALUE), + * gcd(Integer.MIN_VALUE, 0) and + * gcd(0, Integer.MIN_VALUE) throw an + * ArithmeticException, because the result would be 2^31, which + * is too large for an int value.
  • + *
  • The result of gcd(x, x), gcd(0, x) and + * gcd(x, 0) is the absolute value of x, except + * for the special cases above. + *
  • The invocation gcd(0, 0) is the only one which returns + * 0.
  • + *
+ * + * @param p any number + * @param q any number + * @return the greatest common divisor, never negative + * @throws ArithmeticException if the result cannot be represented as a + * nonnegative int value + * @since 1.1 + */ + public static int gcd(final int p, final int q) { + int u = p; + int v = q; + if ((u == 0) || (v == 0)) { + if ((u == Integer.MIN_VALUE) || (v == Integer.MIN_VALUE)) { + throw MathRuntimeException.createArithmeticException( + "overflow: gcd({0}, {1}) is 2^31", + p, q); + } + return Math.abs(u) + Math.abs(v); + } + // keep u and v negative, as negative integers range down to + // -2^31, while positive numbers can only be as large as 2^31-1 + // (i.e. we can't necessarily negate a negative number without + // overflow) + /* assert u!=0 && v!=0; */ + if (u > 0) { + u = -u; + } // make u negative + if (v > 0) { + v = -v; + } // make v negative + // B1. [Find power of 2] + int k = 0; + while ((u & 1) == 0 && (v & 1) == 0 && k < 31) { // while u and v are + // both even... + u /= 2; + v /= 2; + k++; // cast out twos. + } + if (k == 31) { + throw MathRuntimeException.createArithmeticException( + "overflow: gcd({0}, {1}) is 2^31", + p, q); + } + // B2. Initialize: u and v have been divided by 2^k and at least + // one is odd. + int t = ((u & 1) == 1) ? v : -(u / 2)/* B3 */; + // t negative: u was odd, v may be even (t replaces v) + // t positive: u was even, v is odd (t replaces u) + do { + /* assert u<0 && v<0; */ + // B4/B3: cast out twos from t. + while ((t & 1) == 0) { // while t is even.. + t /= 2; // cast out twos + } + // B5 [reset max(u,v)] + if (t > 0) { + u = -t; + } else { + v = t; + } + // B6/B3. at this point both u and v should be odd. + t = (v - u) / 2; + // |u| larger: t positive (replace u) + // |v| larger: t negative (replace v) + } while (t != 0); + return -u * (1 << k); // gcd is u*2^k + } + + /** + *

+ * Gets the greatest common divisor of the absolute value of two numbers, + * using the "binary gcd" method which avoids division and modulo + * operations. See Knuth 4.5.2 algorithm B. This algorithm is due to Josef + * Stein (1961). + *

+ * Special cases: + *
    + *
  • The invocations + * gcd(Long.MIN_VALUE, Long.MIN_VALUE), + * gcd(Long.MIN_VALUE, 0L) and + * gcd(0L, Long.MIN_VALUE) throw an + * ArithmeticException, because the result would be 2^63, which + * is too large for a long value.
  • + *
  • The result of gcd(x, x), gcd(0L, x) and + * gcd(x, 0L) is the absolute value of x, except + * for the special cases above. + *
  • The invocation gcd(0L, 0L) is the only one which returns + * 0L.
  • + *
+ * + * @param p any number + * @param q any number + * @return the greatest common divisor, never negative + * @throws ArithmeticException if the result cannot be represented as a nonnegative long + * value + * @since 2.1 + */ + public static long gcd(final long p, final long q) { + long u = p; + long v = q; + if ((u == 0) || (v == 0)) { + if ((u == Long.MIN_VALUE) || (v == Long.MIN_VALUE)) { + throw MathRuntimeException.createArithmeticException( + "overflow: gcd({0}, {1}) is 2^63", + p, q); + } + return Math.abs(u) + Math.abs(v); + } + // keep u and v negative, as negative integers range down to + // -2^63, while positive numbers can only be as large as 2^63-1 + // (i.e. we can't necessarily negate a negative number without + // overflow) + /* assert u!=0 && v!=0; */ + if (u > 0) { + u = -u; + } // make u negative + if (v > 0) { + v = -v; + } // make v negative + // B1. [Find power of 2] + int k = 0; + while ((u & 1) == 0 && (v & 1) == 0 && k < 63) { // while u and v are + // both even... + u /= 2; + v /= 2; + k++; // cast out twos. + } + if (k == 63) { + throw MathRuntimeException.createArithmeticException( + "overflow: gcd({0}, {1}) is 2^63", + p, q); + } + // B2. Initialize: u and v have been divided by 2^k and at least + // one is odd. + long t = ((u & 1) == 1) ? v : -(u / 2)/* B3 */; + // t negative: u was odd, v may be even (t replaces v) + // t positive: u was even, v is odd (t replaces u) + do { + /* assert u<0 && v<0; */ + // B4/B3: cast out twos from t. + while ((t & 1) == 0) { // while t is even.. + t /= 2; // cast out twos + } + // B5 [reset max(u,v)] + if (t > 0) { + u = -t; + } else { + v = t; + } + // B6/B3. at this point both u and v should be odd. + t = (v - u) / 2; + // |u| larger: t positive (replace u) + // |v| larger: t negative (replace v) + } while (t != 0); + return -u * (1L << k); // gcd is u*2^k + } + + /** + * Returns an integer hash code representing the given double value. + * + * @param value the value to be hashed + * @return the hash code + */ + public static int hash(double value) { + return new Double(value).hashCode(); + } + + /** + * Returns an integer hash code representing the given double array. + * + * @param value the value to be hashed (may be null) + * @return the hash code + * @since 1.2 + */ + public static int hash(double[] value) { + return Arrays.hashCode(value); + } + + /** + * For a byte value x, this method returns (byte)(+1) if x >= 0 and + * (byte)(-1) if x < 0. + * + * @param x the value, a byte + * @return (byte)(+1) or (byte)(-1), depending on the sign of x + */ + public static byte indicator(final byte x) { + return (x >= ZB) ? PB : NB; + } + + /** + * For a double precision value x, this method returns +1.0 if x >= 0 and + * -1.0 if x < 0. Returns NaN if x is + * NaN. + * + * @param x the value, a double + * @return +1.0 or -1.0, depending on the sign of x + */ + public static double indicator(final double x) { + if (Double.isNaN(x)) { + return Double.NaN; + } + return (x >= 0.0) ? 1.0 : -1.0; + } + + /** + * For a float value x, this method returns +1.0F if x >= 0 and -1.0F if x < + * 0. Returns NaN if x is NaN. + * + * @param x the value, a float + * @return +1.0F or -1.0F, depending on the sign of x + */ + public static float indicator(final float x) { + if (Float.isNaN(x)) { + return Float.NaN; + } + return (x >= 0.0F) ? 1.0F : -1.0F; + } + + /** + * For an int value x, this method returns +1 if x >= 0 and -1 if x < 0. + * + * @param x the value, an int + * @return +1 or -1, depending on the sign of x + */ + public static int indicator(final int x) { + return (x >= 0) ? 1 : -1; + } + + /** + * For a long value x, this method returns +1L if x >= 0 and -1L if x < 0. + * + * @param x the value, a long + * @return +1L or -1L, depending on the sign of x + */ + public static long indicator(final long x) { + return (x >= 0L) ? 1L : -1L; + } + + /** + * For a short value x, this method returns (short)(+1) if x >= 0 and + * (short)(-1) if x < 0. + * + * @param x the value, a short + * @return (short)(+1) or (short)(-1), depending on the sign of x + */ + public static short indicator(final short x) { + return (x >= ZS) ? PS : NS; + } + + /** + *

+ * Returns the least common multiple of the absolute value of two numbers, + * using the formula lcm(a,b) = (a / gcd(a,b)) * b. + *

+ * Special cases: + *
    + *
  • The invocations lcm(Integer.MIN_VALUE, n) and + * lcm(n, Integer.MIN_VALUE), where abs(n) is a + * power of 2, throw an ArithmeticException, because the result + * would be 2^31, which is too large for an int value.
  • + *
  • The result of lcm(0, x) and lcm(x, 0) is + * 0 for any x. + *
+ * + * @param a any number + * @param b any number + * @return the least common multiple, never negative + * @throws ArithmeticException if the result cannot be represented as a nonnegative int + * value + * @since 1.1 + */ + public static int lcm(int a, int b) { + if (a == 0 || b == 0) { + return 0; + } + int lcm = Math.abs(mulAndCheck(a / gcd(a, b), b)); + if (lcm == Integer.MIN_VALUE) { + throw MathRuntimeException.createArithmeticException( + "overflow: lcm({0}, {1}) is 2^31", + a, b); + } + return lcm; + } + + /** + *

+ * Returns the least common multiple of the absolute value of two numbers, + * using the formula lcm(a,b) = (a / gcd(a,b)) * b. + *

+ * Special cases: + *
    + *
  • The invocations lcm(Long.MIN_VALUE, n) and + * lcm(n, Long.MIN_VALUE), where abs(n) is a + * power of 2, throw an ArithmeticException, because the result + * would be 2^63, which is too large for an int value.
  • + *
  • The result of lcm(0L, x) and lcm(x, 0L) is + * 0L for any x. + *
+ * + * @param a any number + * @param b any number + * @return the least common multiple, never negative + * @throws ArithmeticException if the result cannot be represented as a nonnegative long + * value + * @since 2.1 + */ + public static long lcm(long a, long b) { + if (a == 0 || b == 0) { + return 0; + } + long lcm = Math.abs(mulAndCheck(a / gcd(a, b), b)); + if (lcm == Long.MIN_VALUE) { + throw MathRuntimeException.createArithmeticException( + "overflow: lcm({0}, {1}) is 2^63", + a, b); + } + return lcm; + } + + /** + *

Returns the + * logarithm + * for base b of x. + *

+ *

Returns NaN if either argument is negative. If + * base is 0 and x is positive, 0 is returned. + * If base is positive and x is 0, + * Double.NEGATIVE_INFINITY is returned. If both arguments + * are 0, the result is NaN.

+ * + * @param base the base of the logarithm, must be greater than 0 + * @param x argument, must be greater than 0 + * @return the value of the logarithm - the number y such that base^y = x. + * @since 1.2 + */ + public static double log(double base, double x) { + return Math.log(x) / Math.log(base); + } + + /** + * Multiply two integers, checking for overflow. + * + * @param x a factor + * @param y a factor + * @return the product x*y + * @throws ArithmeticException if the result can not be represented as an + * int + * @since 1.1 + */ + public static int mulAndCheck(int x, int y) { + long m = ((long) x) * ((long) y); + if (m < Integer.MIN_VALUE || m > Integer.MAX_VALUE) { + throw new ArithmeticException("overflow: mul"); + } + return (int) m; + } + + /** + * Multiply two long integers, checking for overflow. + * + * @param a first value + * @param b second value + * @return the product a * b + * @throws ArithmeticException if the result can not be represented as an + * long + * @since 1.2 + */ + public static long mulAndCheck(long a, long b) { + long ret; + String msg = "overflow: multiply"; + if (a > b) { + // use symmetry to reduce boundary cases + ret = mulAndCheck(b, a); + } else { + if (a < 0) { + if (b < 0) { + // check for positive overflow with negative a, negative b + if (a >= Long.MAX_VALUE / b) { + ret = a * b; + } else { + throw new ArithmeticException(msg); + } + } else if (b > 0) { + // check for negative overflow with negative a, positive b + if (Long.MIN_VALUE / b <= a) { + ret = a * b; + } else { + throw new ArithmeticException(msg); + + } + } else { + // assert b == 0 + ret = 0; + } + } else if (a > 0) { + // assert a > 0 + // assert b > 0 + + // check for positive overflow with positive a, positive b + if (a <= Long.MAX_VALUE / b) { + ret = a * b; + } else { + throw new ArithmeticException(msg); + } + } else { + // assert a == 0 + ret = 0; + } + } + return ret; + } + + /** + * Get the next machine representable number after a number, moving + * in the direction of another number. + *

+ * If direction is greater than or equal tod, + * the smallest machine representable number strictly greater than + * d is returned; otherwise the largest representable number + * strictly less than d is returned.

+ *

+ * If d is NaN or Infinite, it is returned unchanged.

+ * + * @param d base number + * @param direction (the only important thing is whether + * direction is greater or smaller than d) + * @return the next machine representable number in the specified direction + * @since 1.2 + */ + public static double nextAfter(double d, double direction) { + + // handling of some important special cases + if (Double.isNaN(d) || Double.isInfinite(d)) { + return d; + } else if (d == 0) { + return (direction < 0) ? -Double.MIN_VALUE : Double.MIN_VALUE; + } + // special cases MAX_VALUE to infinity and MIN_VALUE to 0 + // are handled just as normal numbers + + // split the double in raw components + long bits = Double.doubleToLongBits(d); + long sign = bits & 0x8000000000000000L; + long exponent = bits & 0x7ff0000000000000L; + long mantissa = bits & 0x000fffffffffffffL; + + if (d * (direction - d) >= 0) { + // we should increase the mantissa + if (mantissa == 0x000fffffffffffffL) { + return Double.longBitsToDouble(sign | + (exponent + 0x0010000000000000L)); + } else { + return Double.longBitsToDouble(sign | + exponent | (mantissa + 1)); + } + } else { + // we should decrease the mantissa + if (mantissa == 0L) { + return Double.longBitsToDouble(sign | + (exponent - 0x0010000000000000L) | + 0x000fffffffffffffL); + } else { + return Double.longBitsToDouble(sign | + exponent | (mantissa - 1)); + } + } + + } + + /** + * Scale a number by 2scaleFactor. + *

If d is 0 or NaN or Infinite, it is returned unchanged.

+ * + * @param d base number + * @param scaleFactor power of two by which d sould be multiplied + * @return d × 2scaleFactor + * @since 2.0 + */ + public static double scalb(final double d, final int scaleFactor) { + + // handling of some important special cases + if ((d == 0) || Double.isNaN(d) || Double.isInfinite(d)) { + return d; + } + + // split the double in raw components + final long bits = Double.doubleToLongBits(d); + final long exponent = bits & 0x7ff0000000000000L; + final long rest = bits & 0x800fffffffffffffL; + + // shift the exponent + final long newBits = rest | (exponent + (((long) scaleFactor) << 52)); + return Double.longBitsToDouble(newBits); + + } + + /** + * Normalize an angle in a 2&pi wide interval around a center value. + *

This method has three main uses:

+ *
    + *
  • normalize an angle between 0 and 2π:
    + * a = MathUtils.normalizeAngle(a, Math.PI);
  • + *
  • normalize an angle between -π and +π
    + * a = MathUtils.normalizeAngle(a, 0.0);
  • + *
  • compute the angle between two defining angular positions:
    + * angle = MathUtils.normalizeAngle(end, start) - start;
  • + *
+ *

Note that due to numerical accuracy and since π cannot be represented + * exactly, the result interval is closed, it cannot be half-closed + * as would be more satisfactory in a purely mathematical view.

+ * + * @param a angle to normalize + * @param center center of the desired 2π interval for the result + * @return a-2kπ with integer k and center-π <= a-2kπ <= center+π + * @since 1.2 + */ + public static double normalizeAngle(double a, double center) { + return a - TWO_PI * Math.floor((a + Math.PI - center) / TWO_PI); + } + + /** + *

Normalizes an array to make it sum to a specified value. + * Returns the result of the transformation

+     *    x |-> x * normalizedSum / sum
+     * 
+ * applied to each non-NaN element x of the input array, where sum is the + * sum of the non-NaN entries in the input array.

+ * + *

Throws IllegalArgumentException if normalizedSum is infinite + * or NaN and ArithmeticException if the input array contains any infinite elements + * or sums to 0

+ * + *

Ignores (i.e., copies unchanged to the output array) NaNs in the input array.

+ * + * @param values input array to be normalized + * @param normalizedSum target sum for the normalized array + * @return normalized array + * @throws ArithmeticException if the input array contains infinite elements or sums to zero + * @throws IllegalArgumentException if the target sum is infinite or NaN + * @since 2.1 + */ + public static double[] normalizeArray(double[] values, double normalizedSum) + throws ArithmeticException, IllegalArgumentException { + if (Double.isInfinite(normalizedSum)) { + throw MathRuntimeException.createIllegalArgumentException( + "Cannot normalize to an infinite value"); + } + if (Double.isNaN(normalizedSum)) { + throw MathRuntimeException.createIllegalArgumentException( + "Cannot normalize to NaN"); + } + double sum = 0d; + final int len = values.length; + double[] out = new double[len]; + for (int i = 0; i < len; i++) { + if (Double.isInfinite(values[i])) { + throw MathRuntimeException.createArithmeticException( + "Array contains an infinite element, {0} at index {1}", values[i], i); + } + if (!Double.isNaN(values[i])) { + sum += values[i]; + } + } + if (sum == 0) { + throw MathRuntimeException.createArithmeticException( + "Array sums to zero"); + } + for (int i = 0; i < len; i++) { + if (Double.isNaN(values[i])) { + out[i] = Double.NaN; + } else { + out[i] = values[i] * normalizedSum / sum; + } + } + return out; + } + + /** + * Round the given value to the specified number of decimal places. The + * value is rounded using the {@link BigDecimal#ROUND_HALF_UP} method. + * + * @param x the value to round. + * @param scale the number of digits to the right of the decimal point. + * @return the rounded value. + * @since 1.1 + */ + public static double round(double x, int scale) { + return round(x, scale, BigDecimal.ROUND_HALF_UP); + } + + /** + * Round the given value to the specified number of decimal places. The + * value is rounded using the given method which is any method defined in + * {@link BigDecimal}. + * + * @param x the value to round. + * @param scale the number of digits to the right of the decimal point. + * @param roundingMethod the rounding method as defined in + * {@link BigDecimal}. + * @return the rounded value. + * @since 1.1 + */ + public static double round(double x, int scale, int roundingMethod) { + try { + return (new BigDecimal + (Double.toString(x)) + .setScale(scale, roundingMethod)) + .doubleValue(); + } catch (NumberFormatException ex) { + if (Double.isInfinite(x)) { + return x; + } else { + return Double.NaN; + } + } + } + + /** + * Round the given value to the specified number of decimal places. The + * value is rounding using the {@link BigDecimal#ROUND_HALF_UP} method. + * + * @param x the value to round. + * @param scale the number of digits to the right of the decimal point. + * @return the rounded value. + * @since 1.1 + */ + public static float round(float x, int scale) { + return round(x, scale, BigDecimal.ROUND_HALF_UP); + } + + /** + * Round the given value to the specified number of decimal places. The + * value is rounded using the given method which is any method defined in + * {@link BigDecimal}. + * + * @param x the value to round. + * @param scale the number of digits to the right of the decimal point. + * @param roundingMethod the rounding method as defined in + * {@link BigDecimal}. + * @return the rounded value. + * @since 1.1 + */ + public static float round(float x, int scale, int roundingMethod) { + float sign = indicator(x); + float factor = (float) Math.pow(10.0f, scale) * sign; + return (float) roundUnscaled(x * factor, sign, roundingMethod) / factor; + } + + /** + * Round the given non-negative, value to the "nearest" integer. Nearest is + * determined by the rounding method specified. Rounding methods are defined + * in {@link BigDecimal}. + * + * @param unscaled the value to round. + * @param sign the sign of the original, scaled value. + * @param roundingMethod the rounding method as defined in + * {@link BigDecimal}. + * @return the rounded value. + * @since 1.1 + */ + private static double roundUnscaled(double unscaled, double sign, + int roundingMethod) { + switch (roundingMethod) { + case BigDecimal.ROUND_CEILING: + if (sign == -1) { + unscaled = Math.floor(nextAfter(unscaled, Double.NEGATIVE_INFINITY)); + } else { + unscaled = Math.ceil(nextAfter(unscaled, Double.POSITIVE_INFINITY)); + } + break; + case BigDecimal.ROUND_DOWN: + unscaled = Math.floor(nextAfter(unscaled, Double.NEGATIVE_INFINITY)); + break; + case BigDecimal.ROUND_FLOOR: + if (sign == -1) { + unscaled = Math.ceil(nextAfter(unscaled, Double.POSITIVE_INFINITY)); + } else { + unscaled = Math.floor(nextAfter(unscaled, Double.NEGATIVE_INFINITY)); + } + break; + case BigDecimal.ROUND_HALF_DOWN: { + unscaled = nextAfter(unscaled, Double.NEGATIVE_INFINITY); + double fraction = unscaled - Math.floor(unscaled); + if (fraction > 0.5) { + unscaled = Math.ceil(unscaled); + } else { + unscaled = Math.floor(unscaled); + } + break; + } + case BigDecimal.ROUND_HALF_EVEN: { + double fraction = unscaled - Math.floor(unscaled); + if (fraction > 0.5) { + unscaled = Math.ceil(unscaled); + } else if (fraction < 0.5) { + unscaled = Math.floor(unscaled); + } else { + // The following equality test is intentional and needed for rounding purposes + if (Math.floor(unscaled) / 2.0 == Math.floor(Math + .floor(unscaled) / 2.0)) { // even + unscaled = Math.floor(unscaled); + } else { // odd + unscaled = Math.ceil(unscaled); + } + } + break; + } + case BigDecimal.ROUND_HALF_UP: { + unscaled = nextAfter(unscaled, Double.POSITIVE_INFINITY); + double fraction = unscaled - Math.floor(unscaled); + if (fraction >= 0.5) { + unscaled = Math.ceil(unscaled); + } else { + unscaled = Math.floor(unscaled); + } + break; + } + case BigDecimal.ROUND_UNNECESSARY: + if (unscaled != Math.floor(unscaled)) { + throw new ArithmeticException("Inexact result from rounding"); + } + break; + case BigDecimal.ROUND_UP: + unscaled = Math.ceil(nextAfter(unscaled, Double.POSITIVE_INFINITY)); + break; + default: + throw MathRuntimeException.createIllegalArgumentException( + "invalid rounding method {0}, valid methods: {1} ({2}), {3} ({4})," + + " {5} ({6}), {7} ({8}), {9} ({10}), {11} ({12}), {13} ({14}), {15} ({16})", + roundingMethod, + "ROUND_CEILING", BigDecimal.ROUND_CEILING, + "ROUND_DOWN", BigDecimal.ROUND_DOWN, + "ROUND_FLOOR", BigDecimal.ROUND_FLOOR, + "ROUND_HALF_DOWN", BigDecimal.ROUND_HALF_DOWN, + "ROUND_HALF_EVEN", BigDecimal.ROUND_HALF_EVEN, + "ROUND_HALF_UP", BigDecimal.ROUND_HALF_UP, + "ROUND_UNNECESSARY", BigDecimal.ROUND_UNNECESSARY, + "ROUND_UP", BigDecimal.ROUND_UP); + } + return unscaled; + } + + /** + * Returns the sign + * for byte value x. + *

+ * For a byte value x, this method returns (byte)(+1) if x > 0, (byte)(0) if + * x = 0, and (byte)(-1) if x < 0.

+ * + * @param x the value, a byte + * @return (byte)(+1), (byte)(0), or (byte)(-1), depending on the sign of x + */ + public static byte sign(final byte x) { + return (x == ZB) ? ZB : (x > ZB) ? PB : NB; + } + + /** + * Returns the sign + * for double precision x. + *

+ * For a double value x, this method returns + * +1.0 if x > 0, 0.0 if + * x = 0.0, and -1.0 if x < 0. + * Returns NaN if x is NaN.

+ * + * @param x the value, a double + * @return +1.0, 0.0, or -1.0, depending on the sign of x + */ + public static double sign(final double x) { + if (Double.isNaN(x)) { + return Double.NaN; + } + return (x == 0.0) ? 0.0 : (x > 0.0) ? 1.0 : -1.0; + } + + /** + * Returns the sign + * for float value x. + *

+ * For a float value x, this method returns +1.0F if x > 0, 0.0F if x = + * 0.0F, and -1.0F if x < 0. Returns NaN if x + * is NaN.

+ * + * @param x the value, a float + * @return +1.0F, 0.0F, or -1.0F, depending on the sign of x + */ + public static float sign(final float x) { + if (Float.isNaN(x)) { + return Float.NaN; + } + return (x == 0.0F) ? 0.0F : (x > 0.0F) ? 1.0F : -1.0F; + } + + /** + * Returns the sign + * for int value x. + *

+ * For an int value x, this method returns +1 if x > 0, 0 if x = 0, and -1 + * if x < 0.

+ * + * @param x the value, an int + * @return +1, 0, or -1, depending on the sign of x + */ + public static int sign(final int x) { + return (x == 0) ? 0 : (x > 0) ? 1 : -1; + } + + /** + * Returns the sign + * for long value x. + *

+ * For a long value x, this method returns +1L if x > 0, 0L if x = 0, and + * -1L if x < 0.

+ * + * @param x the value, a long + * @return +1L, 0L, or -1L, depending on the sign of x + */ + public static long sign(final long x) { + return (x == 0L) ? 0L : (x > 0L) ? 1L : -1L; + } + + /** + * Returns the sign + * for short value x. + *

+ * For a short value x, this method returns (short)(+1) if x > 0, (short)(0) + * if x = 0, and (short)(-1) if x < 0.

+ * + * @param x the value, a short + * @return (short)(+1), (short)(0), or (short)(-1), depending on the sign of + * x + */ + public static short sign(final short x) { + return (x == ZS) ? ZS : (x > ZS) ? PS : NS; + } + + /** + * Returns the + * hyperbolic sine of x. + * + * @param x double value for which to find the hyperbolic sine + * @return hyperbolic sine of x + */ + public static double sinh(double x) { + return (Math.exp(x) - Math.exp(-x)) / 2.0; + } + + /** + * Subtract two integers, checking for overflow. + * + * @param x the minuend + * @param y the subtrahend + * @return the difference x-y + * @throws ArithmeticException if the result can not be represented as an + * int + * @since 1.1 + */ + public static int subAndCheck(int x, int y) { + long s = (long) x - (long) y; + if (s < Integer.MIN_VALUE || s > Integer.MAX_VALUE) { + throw new ArithmeticException("overflow: subtract"); + } + return (int) s; + } + + /** + * Subtract two long integers, checking for overflow. + * + * @param a first value + * @param b second value + * @return the difference a-b + * @throws ArithmeticException if the result can not be represented as an + * long + * @since 1.2 + */ + public static long subAndCheck(long a, long b) { + long ret; + String msg = "overflow: subtract"; + if (b == Long.MIN_VALUE) { + if (a < 0) { + ret = a - b; + } else { + throw new ArithmeticException(msg); + } + } else { + // use additive inverse + ret = addAndCheck(a, -b, msg); + } + return ret; + } + + /** + * Raise an int to an int power. + * + * @param k number to raise + * @param e exponent (must be positive or null) + * @return ke + * @throws IllegalArgumentException if e is negative + */ + public static int pow(final int k, int e) + throws IllegalArgumentException { + + if (e < 0) { + throw MathRuntimeException.createIllegalArgumentException( + "cannot raise an integral value to a negative power ({0}^{1})", + k, e); + } + + int result = 1; + int k2p = k; + while (e != 0) { + if ((e & 0x1) != 0) { + result *= k2p; + } + k2p *= k2p; + e = e >> 1; + } + + return result; + + } + + /** + * Raise an int to a long power. + * + * @param k number to raise + * @param e exponent (must be positive or null) + * @return ke + * @throws IllegalArgumentException if e is negative + */ + public static int pow(final int k, long e) + throws IllegalArgumentException { + + if (e < 0) { + throw MathRuntimeException.createIllegalArgumentException( + "cannot raise an integral value to a negative power ({0}^{1})", + k, e); + } + + int result = 1; + int k2p = k; + while (e != 0) { + if ((e & 0x1) != 0) { + result *= k2p; + } + k2p *= k2p; + e = e >> 1; + } + + return result; + + } + + /** + * Raise a long to an int power. + * + * @param k number to raise + * @param e exponent (must be positive or null) + * @return ke + * @throws IllegalArgumentException if e is negative + */ + public static long pow(final long k, int e) + throws IllegalArgumentException { + + if (e < 0) { + throw MathRuntimeException.createIllegalArgumentException( + "cannot raise an integral value to a negative power ({0}^{1})", + k, e); + } + + long result = 1l; + long k2p = k; + while (e != 0) { + if ((e & 0x1) != 0) { + result *= k2p; + } + k2p *= k2p; + e = e >> 1; + } + + return result; + + } + + /** + * Raise a long to a long power. + * + * @param k number to raise + * @param e exponent (must be positive or null) + * @return ke + * @throws IllegalArgumentException if e is negative + */ + public static long pow(final long k, long e) + throws IllegalArgumentException { + + if (e < 0) { + throw MathRuntimeException.createIllegalArgumentException( + "cannot raise an integral value to a negative power ({0}^{1})", + k, e); + } + + long result = 1l; + long k2p = k; + while (e != 0) { + if ((e & 0x1) != 0) { + result *= k2p; + } + k2p *= k2p; + e = e >> 1; + } + + return result; + + } + + /** + * Raise a BigInteger to an int power. + * + * @param k number to raise + * @param e exponent (must be positive or null) + * @return ke + * @throws IllegalArgumentException if e is negative + */ + public static BigInteger pow(final BigInteger k, int e) + throws IllegalArgumentException { + + if (e < 0) { + throw MathRuntimeException.createIllegalArgumentException( + "cannot raise an integral value to a negative power ({0}^{1})", + k, e); + } + + return k.pow(e); + + } + + /** + * Raise a BigInteger to a long power. + * + * @param k number to raise + * @param e exponent (must be positive or null) + * @return ke + * @throws IllegalArgumentException if e is negative + */ + public static BigInteger pow(final BigInteger k, long e) + throws IllegalArgumentException { + + if (e < 0) { + throw MathRuntimeException.createIllegalArgumentException( + "cannot raise an integral value to a negative power ({0}^{1})", + k, e); + } + + BigInteger result = BigInteger.ONE; + BigInteger k2p = k; + while (e != 0) { + if ((e & 0x1) != 0) { + result = result.multiply(k2p); + } + k2p = k2p.multiply(k2p); + e = e >> 1; + } + + return result; + + } + + /** + * Raise a BigInteger to a BigInteger power. + * + * @param k number to raise + * @param e exponent (must be positive or null) + * @return ke + * @throws IllegalArgumentException if e is negative + */ + public static BigInteger pow(final BigInteger k, BigInteger e) + throws IllegalArgumentException { + + if (e.compareTo(BigInteger.ZERO) < 0) { + throw MathRuntimeException.createIllegalArgumentException( + "cannot raise an integral value to a negative power ({0}^{1})", + k, e); + } + + BigInteger result = BigInteger.ONE; + BigInteger k2p = k; + while (!BigInteger.ZERO.equals(e)) { + if (e.testBit(0)) { + result = result.multiply(k2p); + } + k2p = k2p.multiply(k2p); + e = e.shiftRight(1); + } + + return result; + + } + + /** + * Calculates the L1 (sum of abs) distance between two points. + * + * @param p1 the first point + * @param p2 the second point + * @return the L1 distance between the two points + */ + public static double distance1(double[] p1, double[] p2) { + double sum = 0; + for (int i = 0; i < p1.length; i++) { + sum += Math.abs(p1[i] - p2[i]); + } + return sum; + } + + /** + * Calculates the L1 (sum of abs) distance between two points. + * + * @param p1 the first point + * @param p2 the second point + * @return the L1 distance between the two points + */ + public static int distance1(int[] p1, int[] p2) { + int sum = 0; + for (int i = 0; i < p1.length; i++) { + sum += Math.abs(p1[i] - p2[i]); + } + return sum; + } + + /** + * Calculates the L2 (Euclidean) distance between two points. + * + * @param p1 the first point + * @param p2 the second point + * @return the L2 distance between the two points + */ + public static double distance(double[] p1, double[] p2) { + double sum = 0; + for (int i = 0; i < p1.length; i++) { + final double dp = p1[i] - p2[i]; + sum += dp * dp; + } + return Math.sqrt(sum); + } + + /** + * Calculates the L2 (Euclidean) distance between two points. + * + * @param p1 the first point + * @param p2 the second point + * @return the L2 distance between the two points + */ + public static double distance(int[] p1, int[] p2) { + double sum = 0; + for (int i = 0; i < p1.length; i++) { + final double dp = p1[i] - p2[i]; + sum += dp * dp; + } + return Math.sqrt(sum); + } + + /** + * Calculates the L (max of abs) distance between two points. + * + * @param p1 the first point + * @param p2 the second point + * @return the L distance between the two points + */ + public static double distanceInf(double[] p1, double[] p2) { + double max = 0; + for (int i = 0; i < p1.length; i++) { + max = Math.max(max, Math.abs(p1[i] - p2[i])); + } + return max; + } + + /** + * Calculates the L (max of abs) distance between two points. + * + * @param p1 the first point + * @param p2 the second point + * @return the L distance between the two points + */ + public static int distanceInf(int[] p1, int[] p2) { + int max = 0; + for (int i = 0; i < p1.length; i++) { + max = Math.max(max, Math.abs(p1[i] - p2[i])); + } + return max; + } + + /** + * Checks that the given array is sorted. + * + * @param val Values + * @param dir Order direction (-1 for decreasing, 1 for increasing) + * @param strict Whether the order should be strict + * @throws IllegalArgumentException if the array is not sorted. + */ + public static void checkOrder(double[] val, int dir, boolean strict) { + double previous = val[0]; + + int max = val.length; + for (int i = 1; i < max; i++) { + if (dir > 0) { + if (strict) { + if (val[i] <= previous) { + throw MathRuntimeException.createIllegalArgumentException("points {0} and {1} are not strictly increasing ({2} >= {3})", + i - 1, i, previous, val[i]); + } + } else { + if (val[i] < previous) { + throw MathRuntimeException.createIllegalArgumentException("points {0} and {1} are not increasing ({2} > {3})", + i - 1, i, previous, val[i]); + } + } + } else { + if (strict) { + if (val[i] >= previous) { + throw MathRuntimeException.createIllegalArgumentException("points {0} and {1} are not strictly decreasing ({2} <= {3})", + i - 1, i, previous, val[i]); + } + } else { + if (val[i] > previous) { + throw MathRuntimeException.createIllegalArgumentException("points {0} and {1} are not decreasing ({2} < {3})", + i - 1, i, previous, val[i]); + } + } + } + + previous = val[i]; + } + } +} diff --git a/modules/elasticsearch/src/main/java/org/elasticsearch/util/math/UnboxedMathUtils.java b/modules/elasticsearch/src/main/java/org/elasticsearch/util/math/UnboxedMathUtils.java new file mode 100644 index 00000000000..cdeec5d5d70 --- /dev/null +++ b/modules/elasticsearch/src/main/java/org/elasticsearch/util/math/UnboxedMathUtils.java @@ -0,0 +1,244 @@ +/* + * Licensed to Elastic Search and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Elastic Search 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.util.math; + +/** + * @author kimchy (shay.banon) + */ +public class UnboxedMathUtils { + + public static double sin(Double a) { + return Math.sin(a); + } + + public static double cos(Double a) { + return Math.cos(a); // default impl. delegates to StrictMath + } + + public static double tan(Double a) { + return Math.tan(a); // default impl. delegates to StrictMath + } + + public static double asin(Double a) { + return Math.asin(a); // default impl. delegates to StrictMath + } + + public static double acos(Double a) { + return Math.acos(a); // default impl. delegates to StrictMath + } + + public static double atan(Double a) { + return Math.atan(a); // default impl. delegates to StrictMath + } + + public static double toRadians(Double angdeg) { + return Math.toRadians(angdeg); + } + + public static double toDegrees(Double angrad) { + return Math.toDegrees(angrad); + } + + public static double exp(Double a) { + return Math.exp(a); + } + + public static double log(Double a) { + return Math.log(a); + } + + public static double log10(Double a) { + return Math.log10(a); + } + + public static double sqrt(Double a) { + return Math.sqrt(a); + } + + + public static double cbrt(Double a) { + return Math.cbrt(a); + } + + public static double IEEEremainder(Double f1, Double f2) { + return Math.IEEEremainder(f1, f2); + } + + public static double ceil(Double a) { + return Math.ceil(a); + } + + public static double floor(Double a) { + return Math.floor(a); + } + + public static double rint(Double a) { + return Math.rint(a); + } + + public static double atan2(Double y, Double x) { + return Math.atan2(y, x); + } + + public static double pow(Double a, Double b) { + return Math.pow(a, b); + } + + public static int round(Float a) { + return Math.round(a); + } + + public static long round(Double a) { + return Math.round(a); + } + + public static double random() { + return Math.random(); + } + + public static int abs(Integer a) { + return Math.abs(a); + } + + public static long abs(Long a) { + return Math.abs(a); + } + + public static float abs(Float a) { + return Math.abs(a); + } + + public static double abs(Double a) { + return Math.abs(a); + } + + public static int max(Integer a, Integer b) { + return Math.max(a, b); + } + + public static long max(Long a, Long b) { + return Math.max(a, b); + } + + public static float max(Float a, Float b) { + return Math.max(a, b); + } + + public static double max(Double a, Double b) { + return Math.max(a, b); + } + + public static int min(Integer a, Integer b) { + return Math.min(a, b); + } + + public static long min(Long a, Long b) { + return Math.min(a, b); + } + + public static float min(Float a, Float b) { + return Math.min(a, b); + } + + public static double min(Double a, Double b) { + return Math.min(a, b); + } + + public static double ulp(Double d) { + return Math.ulp(d); + } + + public static float ulp(Float f) { + return Math.ulp(f); + } + + public static double signum(Double d) { + return Math.signum(d); + } + + public static float signum(Float f) { + return Math.signum(f); + } + + public static double sinh(Double x) { + return Math.sinh(x); + } + + public static double cosh(Double x) { + return Math.cosh(x); + } + + public static double tanh(Double x) { + return Math.tanh(x); + } + + public static double hypot(Double x, Double y) { + return Math.hypot(x, y); + } + + public static double expm1(Double x) { + return Math.expm1(x); + } + + public static double log1p(Double x) { + return Math.log1p(x); + } + + public static double copySign(Double magnitude, Double sign) { + return Math.copySign(magnitude, sign); + } + + public static float copySign(Float magnitude, Float sign) { + return Math.copySign(magnitude, sign); + } + + public static int getExponent(Float f) { + return Math.getExponent(f); + } + + public static int getExponent(Double d) { + return Math.getExponent(d); + } + + public static double nextAfter(Double start, Double direction) { + return Math.nextAfter(start, direction); + } + + public static float nextAfter(Float start, Double direction) { + return Math.nextAfter(start, direction); + } + + public static double nextUp(Double d) { + return Math.nextUp(d); + } + + public static float nextUp(Float f) { + return Math.nextUp(f); + } + + + public static double scalb(Double d, Integer scaleFactor) { + return Math.scalb(d, scaleFactor); + } + + public static float scalb(Float f, Integer scaleFactor) { + return Math.scalb(f, scaleFactor); + } +} diff --git a/modules/elasticsearch/src/test/java/org/elasticsearch/index/field/data/longs/LongFieldDataTests.java b/modules/elasticsearch/src/test/java/org/elasticsearch/index/field/data/longs/LongFieldDataTests.java index 9631dc8f924..d48e46f4b6b 100644 --- a/modules/elasticsearch/src/test/java/org/elasticsearch/index/field/data/longs/LongFieldDataTests.java +++ b/modules/elasticsearch/src/test/java/org/elasticsearch/index/field/data/longs/LongFieldDataTests.java @@ -91,10 +91,12 @@ public class LongFieldDataTests { assertThat(sFieldData.hasValue(0), equalTo(true)); assertThat(sFieldData.docFieldData(0).isEmpty(), equalTo(false)); assertThat(sFieldData.value(0), equalTo(4l)); + assertThat(sFieldData.date(0).getMillis(), equalTo(4l)); assertThat(sFieldData.docFieldData(0).getValue(), equalTo(4l)); assertThat(sFieldData.values(0).length, equalTo(1)); assertThat(sFieldData.docFieldData(0).getValues().length, equalTo(1)); assertThat(sFieldData.values(0)[0], equalTo(4l)); + assertThat(sFieldData.dates(0)[0].getMillis(), equalTo(4l)); assertThat(sFieldData.docFieldData(0).getValues()[0], equalTo(4l)); assertThat(sFieldData.hasValue(1), equalTo(true)); @@ -141,9 +143,12 @@ public class LongFieldDataTests { assertThat(mFieldData.hasValue(1), equalTo(true)); assertThat(mFieldData.value(1), equalTo(104l)); + assertThat(mFieldData.date(1).getMillis(), equalTo(104l)); assertThat(mFieldData.values(1).length, equalTo(2)); assertThat(mFieldData.values(1)[0], equalTo(104l)); + assertThat(mFieldData.dates(1)[0].getMillis(), equalTo(104l)); assertThat(mFieldData.values(1)[1], equalTo(105l)); + assertThat(mFieldData.dates(1)[1].getMillis(), equalTo(105l)); assertThat(mFieldData.hasValue(2), equalTo(false)); diff --git a/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/SimpleIndexQueryParserTests.java b/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/SimpleIndexQueryParserTests.java index 999aec18ec6..7c38d82f6c3 100644 --- a/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/SimpleIndexQueryParserTests.java +++ b/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/SimpleIndexQueryParserTests.java @@ -30,8 +30,9 @@ import org.elasticsearch.index.cache.IndexCache; import org.elasticsearch.index.engine.robin.RobinIndexEngine; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.query.IndexQueryParser; +import org.elasticsearch.script.ScriptService; import org.elasticsearch.util.lucene.search.*; -import org.elasticsearch.util.lucene.search.function.BoostFactorFunctionProvider; +import org.elasticsearch.util.lucene.search.function.BoostScoreFunction; import org.elasticsearch.util.lucene.search.function.FunctionScoreQuery; import org.testng.annotations.Test; @@ -712,13 +713,23 @@ public class SimpleIndexQueryParserTests { assertThat(((TermFilter) constantScoreQuery.getFilter()).getTerm(), equalTo(new Term("name.last", "banon"))); } + @Test public void testCustomScoreQuery1() throws IOException { + IndexQueryParser queryParser = newQueryParser(); + String query = copyToStringFromClasspath("/org/elasticsearch/index/query/xcontent/custom_score1.json"); + Query parsedQuery = queryParser.parse(query); + assertThat(parsedQuery, instanceOf(FunctionScoreQuery.class)); + FunctionScoreQuery functionScoreQuery = (FunctionScoreQuery) parsedQuery; + assertThat(((TermQuery) functionScoreQuery.getSubQuery()).getTerm(), equalTo(new Term("name.last", "banon"))); + assertThat(functionScoreQuery.getFunction(), instanceOf(CustomScoreQueryParser.ScriptScoreFunction.class)); + } + @Test public void testCustomBoostFactorQueryBuilder() throws IOException { IndexQueryParser queryParser = newQueryParser(); Query parsedQuery = queryParser.parse(customBoostFactorQuery(termQuery("name.last", "banon")).boostFactor(1.3f)); assertThat(parsedQuery, instanceOf(FunctionScoreQuery.class)); FunctionScoreQuery functionScoreQuery = (FunctionScoreQuery) parsedQuery; assertThat(((TermQuery) functionScoreQuery.getSubQuery()).getTerm(), equalTo(new Term("name.last", "banon"))); - assertThat((double) ((BoostFactorFunctionProvider) functionScoreQuery.getFunctionProvider()).getBoost(), closeTo(1.3, 0.001)); + assertThat((double) ((BoostScoreFunction) functionScoreQuery.getFunction()).getBoost(), closeTo(1.3, 0.001)); } @@ -729,7 +740,7 @@ public class SimpleIndexQueryParserTests { assertThat(parsedQuery, instanceOf(FunctionScoreQuery.class)); FunctionScoreQuery functionScoreQuery = (FunctionScoreQuery) parsedQuery; assertThat(((TermQuery) functionScoreQuery.getSubQuery()).getTerm(), equalTo(new Term("name.last", "banon"))); - assertThat((double) ((BoostFactorFunctionProvider) functionScoreQuery.getFunctionProvider()).getBoost(), closeTo(1.3, 0.001)); + assertThat((double) ((BoostScoreFunction) functionScoreQuery.getFunction()).getBoost(), closeTo(1.3, 0.001)); } @Test public void testSpanTermQueryBuilder() throws IOException { @@ -946,7 +957,7 @@ public class SimpleIndexQueryParserTests { } private XContentIndexQueryParser newQueryParser() throws IOException { - return new XContentIndexQueryParser(new Index("test"), EMPTY_SETTINGS, + return new XContentIndexQueryParser(new Index("test"), EMPTY_SETTINGS, new ScriptService(EMPTY_SETTINGS), newMapperService(), new IndexCache(index), new RobinIndexEngine(index), new AnalysisService(index), null, null, null, "test", null); } diff --git a/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/custom_score1.json b/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/custom_score1.json new file mode 100644 index 00000000000..3d81d8210c0 --- /dev/null +++ b/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/custom_score1.json @@ -0,0 +1,8 @@ +{ + "custom_score" : { + "query" : { + "term" : { "name.last" : "banon"} + }, + "script" : "score * doc['name.first']" + } +} \ No newline at end of file diff --git a/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/guice/IndexQueryParserModuleTests.java b/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/guice/IndexQueryParserModuleTests.java index 146b4c86768..5cebdd66323 100644 --- a/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/guice/IndexQueryParserModuleTests.java +++ b/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/guice/IndexQueryParserModuleTests.java @@ -30,9 +30,11 @@ import org.elasticsearch.index.query.xcontent.XContentIndexQueryParser; import org.elasticsearch.index.query.xcontent.XContentQueryParserRegistry; import org.elasticsearch.index.settings.IndexSettingsModule; import org.elasticsearch.index.similarity.SimilarityModule; +import org.elasticsearch.script.ScriptModule; import org.elasticsearch.util.inject.Guice; import org.elasticsearch.util.inject.Injector; import org.elasticsearch.util.settings.Settings; +import org.elasticsearch.util.settings.SettingsModule; import org.testng.annotations.Test; import static org.elasticsearch.util.settings.ImmutableSettings.*; @@ -55,6 +57,8 @@ public class IndexQueryParserModuleTests { Index index = new Index("test"); Injector injector = Guice.createInjector( + new SettingsModule(settings), + new ScriptModule(), new IndexSettingsModule(settings), new IndexCacheModule(settings), new AnalysisModule(settings), diff --git a/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/plugin/IndexQueryParserPluginTests.java b/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/plugin/IndexQueryParserPluginTests.java index 8f3aa452aa5..ef647d605b3 100644 --- a/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/plugin/IndexQueryParserPluginTests.java +++ b/modules/elasticsearch/src/test/java/org/elasticsearch/index/query/xcontent/plugin/IndexQueryParserPluginTests.java @@ -30,10 +30,12 @@ import org.elasticsearch.index.query.xcontent.XContentIndexQueryParser; import org.elasticsearch.index.query.xcontent.XContentQueryParserRegistry; import org.elasticsearch.index.settings.IndexSettingsModule; import org.elasticsearch.index.similarity.SimilarityModule; +import org.elasticsearch.script.ScriptModule; import org.elasticsearch.util.inject.Guice; import org.elasticsearch.util.inject.Injector; import org.elasticsearch.util.settings.ImmutableSettings; import org.elasticsearch.util.settings.Settings; +import org.elasticsearch.util.settings.SettingsModule; import org.testng.annotations.Test; import static org.hamcrest.MatcherAssert.*; @@ -60,6 +62,8 @@ public class IndexQueryParserPluginTests { Index index = new Index("test"); Injector injector = Guice.createInjector( + new SettingsModule(settings), + new ScriptModule(), new IndexSettingsModule(settings), new IndexCacheModule(settings), new AnalysisModule(settings), diff --git a/modules/test/integration/src/test/java/org/elasticsearch/test/integration/search/customscore/CustomScoreSearchTests.java b/modules/test/integration/src/test/java/org/elasticsearch/test/integration/search/customscore/CustomScoreSearchTests.java new file mode 100644 index 00000000000..cbde9c6cd6a --- /dev/null +++ b/modules/test/integration/src/test/java/org/elasticsearch/test/integration/search/customscore/CustomScoreSearchTests.java @@ -0,0 +1,146 @@ +/* + * Licensed to Elastic Search and Shay Banon under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Elastic Search 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.test.integration.search.customscore; + +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.action.search.SearchType; +import org.elasticsearch.client.Client; +import org.elasticsearch.test.integration.AbstractNodesTests; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import static org.elasticsearch.client.Requests.*; +import static org.elasticsearch.index.query.xcontent.QueryBuilders.*; +import static org.elasticsearch.search.builder.SearchSourceBuilder.*; +import static org.elasticsearch.util.xcontent.XContentFactory.*; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; + +/** + * @author kimchy (shay.banon) + */ +@Test +public class CustomScoreSearchTests extends AbstractNodesTests { + + private Client client; + + @BeforeMethod public void createNodes() throws Exception { + startNode("server1"); + client = getClient(); + } + + @AfterMethod public void closeNodes() { + client.close(); + closeAllNodes(); + } + + protected Client getClient() { + return client("server1"); + } + + @Test + public void testCustomScriptBoost() throws Exception { + // execute a search before we create an index + try { + client.prepareSearch().setQuery(termQuery("test", "value")).execute().actionGet(); + assert false : "should fail"; + } catch (Exception e) { + // ignore, no indices + } + + try { + client.prepareSearch("test").setQuery(termQuery("test", "value")).execute().actionGet(); + assert false : "should fail"; + } catch (Exception e) { + // ignore, no indices + } + + client.admin().indices().create(createIndexRequest("test")).actionGet(); + client.index(indexRequest("test").type("type1").id("1") + .source(jsonBuilder().startObject().field("test", "value beck").field("num1", 1.0f).endObject())).actionGet(); + client.index(indexRequest("test").type("type1").id("2") + .source(jsonBuilder().startObject().field("test", "value check").field("num1", 2.0f).endObject())).actionGet(); + client.admin().indices().refresh(refreshRequest()).actionGet(); + + logger.info("--- QUERY_THEN_FETCH"); + + logger.info("running doc['num1'].value"); + SearchResponse response = client.search(searchRequest() + .searchType(SearchType.QUERY_THEN_FETCH) + .source(searchSource().explain(true).query(customScoreQuery(termQuery("test", "value")).script("doc['num1'].value"))) + ).actionGet(); + + assertThat(response.hits().totalHits(), equalTo(2l)); + logger.info("Hit[0] {} Explanation {}", response.hits().getAt(0).id(), response.hits().getAt(0).explanation()); + logger.info("Hit[1] {} Explanation {}", response.hits().getAt(1).id(), response.hits().getAt(1).explanation()); + assertThat(response.hits().getAt(0).id(), equalTo("2")); + assertThat(response.hits().getAt(1).id(), equalTo("1")); + + logger.info("running -doc['num1'].value"); + response = client.search(searchRequest() + .searchType(SearchType.QUERY_THEN_FETCH) + .source(searchSource().explain(true).query(customScoreQuery(termQuery("test", "value")).script("-doc['num1'].value"))) + ).actionGet(); + + assertThat(response.hits().totalHits(), equalTo(2l)); + logger.info("Hit[0] {} Explanation {}", response.hits().getAt(0).id(), response.hits().getAt(0).explanation()); + logger.info("Hit[1] {} Explanation {}", response.hits().getAt(1).id(), response.hits().getAt(1).explanation()); + assertThat(response.hits().getAt(0).id(), equalTo("1")); + assertThat(response.hits().getAt(1).id(), equalTo("2")); + + + logger.info("running pow(doc['num1'].value, 2)"); + response = client.search(searchRequest() + .searchType(SearchType.QUERY_THEN_FETCH) + .source(searchSource().explain(true).query(customScoreQuery(termQuery("test", "value")).script("pow(doc['num1'].value, 2)"))) + ).actionGet(); + + assertThat(response.hits().totalHits(), equalTo(2l)); + logger.info("Hit[0] {} Explanation {}", response.hits().getAt(0).id(), response.hits().getAt(0).explanation()); + logger.info("Hit[1] {} Explanation {}", response.hits().getAt(1).id(), response.hits().getAt(1).explanation()); + assertThat(response.hits().getAt(0).id(), equalTo("2")); + assertThat(response.hits().getAt(1).id(), equalTo("1")); + + logger.info("running max(doc['num1'].value, 1)"); + response = client.search(searchRequest() + .searchType(SearchType.QUERY_THEN_FETCH) + .source(searchSource().explain(true).query(customScoreQuery(termQuery("test", "value")).script("max(doc['num1'].value, 1d)"))) + ).actionGet(); + + assertThat(response.hits().totalHits(), equalTo(2l)); + logger.info("Hit[0] {} Explanation {}", response.hits().getAt(0).id(), response.hits().getAt(0).explanation()); + logger.info("Hit[1] {} Explanation {}", response.hits().getAt(1).id(), response.hits().getAt(1).explanation()); + assertThat(response.hits().getAt(0).id(), equalTo("2")); + assertThat(response.hits().getAt(1).id(), equalTo("1")); + + logger.info("running doc['num1'].value * score"); + response = client.search(searchRequest() + .searchType(SearchType.QUERY_THEN_FETCH) + .source(searchSource().explain(true).query(customScoreQuery(termQuery("test", "value")).script("doc['num1'].value * score"))) + ).actionGet(); + + assertThat(response.hits().totalHits(), equalTo(2l)); + logger.info("Hit[0] {} Explanation {}", response.hits().getAt(0).id(), response.hits().getAt(0).explanation()); + logger.info("Hit[1] {} Explanation {}", response.hits().getAt(1).id(), response.hits().getAt(1).explanation()); + assertThat(response.hits().getAt(0).id(), equalTo("2")); + assertThat(response.hits().getAt(1).id(), equalTo("1")); + } +} \ No newline at end of file diff --git a/modules/test/integration/src/test/java/org/elasticsearch/test/integration/search/customscore/CustomScoreSearchTests.yml b/modules/test/integration/src/test/java/org/elasticsearch/test/integration/search/customscore/CustomScoreSearchTests.yml new file mode 100644 index 00000000000..e700f1f35fa --- /dev/null +++ b/modules/test/integration/src/test/java/org/elasticsearch/test/integration/search/customscore/CustomScoreSearchTests.yml @@ -0,0 +1,6 @@ +cluster: + routing: + schedule: 100ms +index: + number_of_shards: 1 + number_of_replicas: 0