You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ho...@apache.org on 2018/01/15 00:33:55 UTC
[2/2] lucene-solr:branch_7x: SOLR-3218: Added range faceting support
for CurrencyFieldType
SOLR-3218: Added range faceting support for CurrencyFieldType
(cherry picked from commit 6dcbb2d412028075b84567edd60d7fcb56032d14)
Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/f805d020
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/f805d020
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/f805d020
Branch: refs/heads/branch_7x
Commit: f805d020129508a62e53a861176a0487e4549525
Parents: c9916e3
Author: Chris Hostetter <ho...@apache.org>
Authored: Sun Jan 14 16:30:24 2018 -0700
Committer: Chris Hostetter <ho...@apache.org>
Committed: Sun Jan 14 16:30:39 2018 -0700
----------------------------------------------------------------------
solr/CHANGES.txt | 3 +
.../handler/component/RangeFacetRequest.java | 73 ++-
.../apache/solr/schema/CurrencyFieldType.java | 165 +------
.../org/apache/solr/schema/CurrencyValue.java | 231 +++++++++
.../apache/solr/search/facet/FacetRange.java | 115 ++++-
.../solr/schema/CurrencyFieldTypeTest.java | 235 +++++++++
.../search/CurrencyRangeFacetCloudTest.java | 483 +++++++++++++++++++
.../conf/velocity/VM_global_library.vm | 6 +-
...king-with-currencies-and-exchange-rates.adoc | 1 +
.../client/solrj/response/QueryResponse.java | 14 +-
.../solr/client/solrj/response/RangeFacet.java | 6 +
11 files changed, 1162 insertions(+), 170 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/f805d020/solr/CHANGES.txt
----------------------------------------------------------------------
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index f805ab4..2ffb7e8 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -64,6 +64,9 @@ New Features
* SOLR-11063: Suggesters should accept required freedisk as a hint (noble)
+* SOLR-3218: Added range faceting support for CurrencyFieldType. This includes both "facet.range" as well
+ as json.facet's "type:range" (Andrew Morrison, Jan Høydahl, Vitaliy Zhovtyuk, hossman)
+
Bug Fixes
----------------------
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/f805d020/solr/core/src/java/org/apache/solr/handler/component/RangeFacetRequest.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/handler/component/RangeFacetRequest.java b/solr/core/src/java/org/apache/solr/handler/component/RangeFacetRequest.java
index c234866..8d47a93 100644
--- a/solr/core/src/java/org/apache/solr/handler/component/RangeFacetRequest.java
+++ b/solr/core/src/java/org/apache/solr/handler/component/RangeFacetRequest.java
@@ -31,8 +31,11 @@ import org.apache.solr.common.params.RequiredSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.schema.CurrencyFieldType;
+import org.apache.solr.schema.CurrencyValue;
import org.apache.solr.schema.DatePointField;
import org.apache.solr.schema.DateRangeField;
+import org.apache.solr.schema.ExchangeRateProvider;
import org.apache.solr.schema.FieldType;
import org.apache.solr.schema.IndexSchema;
import org.apache.solr.schema.SchemaField;
@@ -189,6 +192,8 @@ public class RangeFacetRequest extends FacetComponent.FacetBase {
(SolrException.ErrorCode.BAD_REQUEST,
"Unable to range facet on Point field of unexpected type:" + this.facetOn);
}
+ } else if (ft instanceof CurrencyFieldType) {
+ calc = new CurrencyRangeEndpointCalculator(this);
} else {
throw new SolrException
(SolrException.ErrorCode.BAD_REQUEST,
@@ -451,12 +456,14 @@ public class RangeFacetRequest extends FacetComponent.FacetBase {
this.field = rfr.getSchemaField();
}
- public T getComputedEnd() {
+ /** The Computed End point of all ranges, as an Object of type suitable for direct inclusion in the response data */
+ public Object getComputedEnd() {
assert computed;
return computedEnd;
}
- public T getStart() {
+ /** The Start point of all ranges, as an Object of type suitable for direct inclusion in the response data */
+ public Object getStart() {
assert computed;
return start;
}
@@ -756,6 +763,68 @@ public class RangeFacetRequest extends FacetComponent.FacetBase {
}
}
+ private static class CurrencyRangeEndpointCalculator
+ extends RangeEndpointCalculator<CurrencyValue> {
+ private String defaultCurrencyCode;
+ private ExchangeRateProvider exchangeRateProvider;
+ public CurrencyRangeEndpointCalculator(final RangeFacetRequest rangeFacetRequest) {
+ super(rangeFacetRequest);
+ if(!(this.field.getType() instanceof CurrencyFieldType)) {
+ throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
+ "Cannot perform range faceting over non CurrencyField fields");
+ }
+ defaultCurrencyCode =
+ ((CurrencyFieldType)this.field.getType()).getDefaultCurrency();
+ exchangeRateProvider =
+ ((CurrencyFieldType)this.field.getType()).getProvider();
+ }
+
+ @Override
+ protected Object parseGap(String rawval) throws java.text.ParseException {
+ return parseVal(rawval).strValue();
+ }
+
+ @Override
+ public String formatValue(CurrencyValue val) {
+ return val.strValue();
+ }
+
+ /** formats the value as a String since {@link CurrencyValue} is not suitable for response writers */
+ @Override
+ public Object getComputedEnd() {
+ assert computed;
+ return formatValue(computedEnd);
+ }
+
+ /** formats the value as a String since {@link CurrencyValue} is not suitable for response writers */
+ @Override
+ public Object getStart() {
+ assert computed;
+ return formatValue(start);
+ }
+
+ @Override
+ protected CurrencyValue parseVal(String rawval) {
+ return CurrencyValue.parse(rawval, defaultCurrencyCode);
+ }
+
+ @Override
+ public CurrencyValue parseAndAddGap(CurrencyValue value, String gap) {
+ if(value == null) {
+ throw new NullPointerException("Cannot perform range faceting on null CurrencyValue");
+ }
+ CurrencyValue gapCurrencyValue =
+ CurrencyValue.parse(gap, defaultCurrencyCode);
+ long gapAmount =
+ CurrencyValue.convertAmount(this.exchangeRateProvider,
+ gapCurrencyValue.getCurrencyCode(),
+ gapCurrencyValue.getAmount(),
+ value.getCurrencyCode());
+ return new CurrencyValue(value.getAmount() + gapAmount,
+ value.getCurrencyCode());
+ }
+ }
+
/**
* Represents a single facet range (or gap) for which the count is to be calculated
*/
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/f805d020/solr/core/src/java/org/apache/solr/schema/CurrencyFieldType.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/schema/CurrencyFieldType.java b/solr/core/src/java/org/apache/solr/schema/CurrencyFieldType.java
index a6ba164..97195da 100644
--- a/solr/core/src/java/org/apache/solr/schema/CurrencyFieldType.java
+++ b/solr/core/src/java/org/apache/solr/schema/CurrencyFieldType.java
@@ -89,6 +89,12 @@ public class CurrencyFieldType extends FieldType implements SchemaAware, Resourc
return null;
}
+ /** The identifier code for the default currency of this field type */
+ public String getDefaultCurrency() {
+ return defaultCurrency;
+ }
+
+
@Override
protected void init(IndexSchema schema, Map<String, String> args) {
super.init(schema, args);
@@ -666,164 +672,5 @@ public class CurrencyFieldType extends FieldType implements SchemaAware, Resourc
}
}
- /**
- * Represents a Currency field value, which includes a long amount and ISO currency code.
- */
- static class CurrencyValue {
- private long amount;
- private String currencyCode;
-
- /**
- * Constructs a new currency value.
- *
- * @param amount The amount.
- * @param currencyCode The currency code.
- */
- public CurrencyValue(long amount, String currencyCode) {
- this.amount = amount;
- this.currencyCode = currencyCode;
- }
-
- /**
- * Constructs a new currency value by parsing the specific input.
- * <p/>
- * Currency values are expected to be in the format <amount>,<currency code>,
- * for example, "500,USD" would represent 5 U.S. Dollars.
- * <p/>
- * If no currency code is specified, the default is assumed.
- *
- * @param externalVal The value to parse.
- * @param defaultCurrency The default currency.
- * @return The parsed CurrencyValue.
- */
- public static CurrencyValue parse(String externalVal, String defaultCurrency) {
- if (externalVal == null) {
- return null;
- }
- String amount = externalVal;
- String code = defaultCurrency;
-
- if (externalVal.contains(",")) {
- String[] amountAndCode = externalVal.split(",");
- amount = amountAndCode[0];
- code = amountAndCode[1];
- }
-
- if (amount.equals("*")) {
- return null;
- }
-
- Currency currency = getCurrency(code);
-
- if (currency == null) {
- throw new SolrException(ErrorCode.BAD_REQUEST, "Currency code not supported by this JVM: " + code);
- }
-
- try {
- double value = Double.parseDouble(amount);
- long currencyValue = Math.round(value * Math.pow(10.0, currency.getDefaultFractionDigits()));
-
- return new CurrencyValue(currencyValue, code);
- } catch (NumberFormatException e) {
- throw new SolrException(ErrorCode.BAD_REQUEST, e);
- }
- }
-
- /**
- * The amount of the CurrencyValue.
- *
- * @return The amount.
- */
- public long getAmount() {
- return amount;
- }
-
- /**
- * The ISO currency code of the CurrencyValue.
- *
- * @return The currency code.
- */
- public String getCurrencyCode() {
- return currencyCode;
- }
-
- /**
- * Performs a currency conversion & unit conversion.
- *
- * @param exchangeRates Exchange rates to apply.
- * @param sourceCurrencyCode The source currency code.
- * @param sourceAmount The source amount.
- * @param targetCurrencyCode The target currency code.
- * @return The converted indexable units after the exchange rate and currency fraction digits are applied.
- */
- public static long convertAmount(ExchangeRateProvider exchangeRates, String sourceCurrencyCode, long sourceAmount, String targetCurrencyCode) {
- double exchangeRate = exchangeRates.getExchangeRate(sourceCurrencyCode, targetCurrencyCode);
- return convertAmount(exchangeRate, sourceCurrencyCode, sourceAmount, targetCurrencyCode);
- }
-
- /**
- * Performs a currency conversion & unit conversion.
- *
- * @param exchangeRate Exchange rate to apply.
- * @param sourceFractionDigits The fraction digits of the source.
- * @param sourceAmount The source amount.
- * @param targetFractionDigits The fraction digits of the target.
- * @return The converted indexable units after the exchange rate and currency fraction digits are applied.
- */
- public static long convertAmount(final double exchangeRate, final int sourceFractionDigits, final long sourceAmount, final int targetFractionDigits) {
- int digitDelta = targetFractionDigits - sourceFractionDigits;
- double value = ((double) sourceAmount * exchangeRate);
-
- if (digitDelta != 0) {
- if (digitDelta < 0) {
- for (int i = 0; i < -digitDelta; i++) {
- value *= 0.1;
- }
- } else {
- for (int i = 0; i < digitDelta; i++) {
- value *= 10.0;
- }
- }
- }
-
- return (long) value;
- }
-
- /**
- * Performs a currency conversion & unit conversion.
- *
- * @param exchangeRate Exchange rate to apply.
- * @param sourceCurrencyCode The source currency code.
- * @param sourceAmount The source amount.
- * @param targetCurrencyCode The target currency code.
- * @return The converted indexable units after the exchange rate and currency fraction digits are applied.
- */
- public static long convertAmount(double exchangeRate, String sourceCurrencyCode, long sourceAmount, String targetCurrencyCode) {
- if (targetCurrencyCode.equals(sourceCurrencyCode)) {
- return sourceAmount;
- }
-
- int sourceFractionDigits = Currency.getInstance(sourceCurrencyCode).getDefaultFractionDigits();
- Currency targetCurrency = Currency.getInstance(targetCurrencyCode);
- int targetFractionDigits = targetCurrency.getDefaultFractionDigits();
- return convertAmount(exchangeRate, sourceFractionDigits, sourceAmount, targetFractionDigits);
- }
-
- /**
- * Returns a new CurrencyValue that is the conversion of this CurrencyValue to the specified currency.
- *
- * @param exchangeRates The exchange rate provider.
- * @param targetCurrencyCode The target currency code to convert this CurrencyValue to.
- * @return The converted CurrencyValue.
- */
- public CurrencyValue convertTo(ExchangeRateProvider exchangeRates, String targetCurrencyCode) {
- return new CurrencyValue(convertAmount(exchangeRates, this.getCurrencyCode(), this.getAmount(), targetCurrencyCode), targetCurrencyCode);
- }
-
- @Override
- public String toString() {
- return String.valueOf(amount) + "," + currencyCode;
- }
- }
}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/f805d020/solr/core/src/java/org/apache/solr/schema/CurrencyValue.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/schema/CurrencyValue.java b/solr/core/src/java/org/apache/solr/schema/CurrencyValue.java
new file mode 100644
index 0000000..4c43422
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/schema/CurrencyValue.java
@@ -0,0 +1,231 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.solr.schema;
+
+import org.apache.solr.common.SolrException;
+
+import java.util.Currency;
+
+/**
+ * Represents a Currency field value, which includes a long amount and ISO currency code.
+ */
+public class CurrencyValue implements Comparable<CurrencyValue> {
+ private long amount;
+ private String currencyCode;
+
+ /**
+ * Constructs a new currency value.
+ *
+ * @param amount The amount.
+ * @param currencyCode The currency code.
+ */
+ public CurrencyValue(long amount, String currencyCode) {
+ this.amount = amount;
+ this.currencyCode = currencyCode;
+ }
+
+ /**
+ * Constructs a new currency value by parsing the specific input.
+ * <p>
+ * Currency values are expected to be in the format <amount>,<currency code>,
+ * for example, "500,USD" would represent 5 U.S. Dollars.
+ * </p>
+ * <p>
+ * If no currency code is specified, the default is assumed.
+ * </p>
+ * @param externalVal The value to parse.
+ * @param defaultCurrency The default currency.
+ * @return The parsed CurrencyValue.
+ */
+ public static CurrencyValue parse(String externalVal, String defaultCurrency) {
+ if (externalVal == null) {
+ return null;
+ }
+ String amount = externalVal;
+ String code = defaultCurrency;
+
+ if (externalVal.contains(",")) {
+ String[] amountAndCode = externalVal.split(",");
+ amount = amountAndCode[0];
+ code = amountAndCode[1];
+ }
+
+ if (amount.equals("*")) {
+ return null;
+ }
+
+ Currency currency = CurrencyField.getCurrency(code);
+
+ if (currency == null) {
+ throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Currency code not supported by this JVM: " + code);
+ }
+
+ try {
+ double value = Double.parseDouble(amount);
+ long currencyValue = Math.round(value * Math.pow(10.0, currency.getDefaultFractionDigits()));
+
+ return new CurrencyValue(currencyValue, code);
+ } catch (NumberFormatException e) {
+ throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
+ }
+ }
+
+ /**
+ * The amount of the CurrencyValue.
+ *
+ * @return The amount.
+ */
+ public long getAmount() {
+ return amount;
+ }
+
+ /**
+ * The ISO currency code of the CurrencyValue.
+ *
+ * @return The currency code.
+ */
+ public String getCurrencyCode() {
+ return currencyCode;
+ }
+
+ /**
+ * Performs a currency conversion & unit conversion.
+ *
+ * @param exchangeRates Exchange rates to apply.
+ * @param sourceCurrencyCode The source currency code.
+ * @param sourceAmount The source amount.
+ * @param targetCurrencyCode The target currency code.
+ * @return The converted indexable units after the exchange rate and currency fraction digits are applied.
+ */
+ public static long convertAmount(ExchangeRateProvider exchangeRates, String sourceCurrencyCode, long sourceAmount, String targetCurrencyCode) {
+ double exchangeRate = exchangeRates.getExchangeRate(sourceCurrencyCode, targetCurrencyCode);
+ return convertAmount(exchangeRate, sourceCurrencyCode, sourceAmount, targetCurrencyCode);
+ }
+
+ /**
+ * Performs a currency conversion & unit conversion.
+ *
+ * @param exchangeRate Exchange rate to apply.
+ * @param sourceFractionDigits The fraction digits of the source.
+ * @param sourceAmount The source amount.
+ * @param targetFractionDigits The fraction digits of the target.
+ * @return The converted indexable units after the exchange rate and currency fraction digits are applied.
+ */
+ public static long convertAmount(final double exchangeRate, final int sourceFractionDigits, final long sourceAmount, final int targetFractionDigits) {
+ int digitDelta = targetFractionDigits - sourceFractionDigits;
+ double value = ((double) sourceAmount * exchangeRate);
+
+ if (digitDelta != 0) {
+ if (digitDelta < 0) {
+ for (int i = 0; i < -digitDelta; i++) {
+ value *= 0.1;
+ }
+ } else {
+ for (int i = 0; i < digitDelta; i++) {
+ value *= 10.0;
+ }
+ }
+ }
+
+ return (long) value;
+ }
+
+ /**
+ * Performs a currency conversion & unit conversion.
+ *
+ * @param exchangeRate Exchange rate to apply.
+ * @param sourceCurrencyCode The source currency code.
+ * @param sourceAmount The source amount.
+ * @param targetCurrencyCode The target currency code.
+ * @return The converted indexable units after the exchange rate and currency fraction digits are applied.
+ */
+ public static long convertAmount(double exchangeRate, String sourceCurrencyCode, long sourceAmount, String targetCurrencyCode) {
+ if (targetCurrencyCode.equals(sourceCurrencyCode)) {
+ return sourceAmount;
+ }
+
+ int sourceFractionDigits = Currency.getInstance(sourceCurrencyCode).getDefaultFractionDigits();
+ Currency targetCurrency = Currency.getInstance(targetCurrencyCode);
+ int targetFractionDigits = targetCurrency.getDefaultFractionDigits();
+ return convertAmount(exchangeRate, sourceFractionDigits, sourceAmount, targetFractionDigits);
+ }
+
+ /**
+ * Returns a new CurrencyValue that is the conversion of this CurrencyValue to the specified currency.
+ *
+ * @param exchangeRates The exchange rate provider.
+ * @param targetCurrencyCode The target currency code to convert this CurrencyValue to.
+ * @return The converted CurrencyValue.
+ */
+ public CurrencyValue convertTo(ExchangeRateProvider exchangeRates, String targetCurrencyCode) {
+ return new CurrencyValue(convertAmount(exchangeRates, this.getCurrencyCode(), this.getAmount(), targetCurrencyCode), targetCurrencyCode);
+ }
+
+ /**
+ * Returns a string representing the currency value such as "3.14,USD" for
+ * a CurrencyValue of $3.14 USD.
+ */
+ public String strValue() {
+ int digits = 0;
+ try {
+ Currency currency =
+ Currency.getInstance(this.getCurrencyCode());
+ if (currency == null) {
+ throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
+ "Invalid currency code " + this.getCurrencyCode());
+ }
+ digits = currency.getDefaultFractionDigits();
+}
+ catch(IllegalArgumentException exception) {
+ throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
+ "Invalid currency code " + this.getCurrencyCode());
+ }
+
+ String amount = Long.toString(this.getAmount());
+ if (this.getAmount() == 0) {
+ amount += "000000".substring(0,digits);
+ }
+ return
+ amount.substring(0, amount.length() - digits)
+ + "." + amount.substring(amount.length() - digits)
+ + "," + this.getCurrencyCode();
+ }
+
+ @Override
+ public int compareTo(CurrencyValue o) {
+ if(o == null) {
+ throw new NullPointerException("Cannot compare CurrencyValue to a null values");
+ }
+ if(!getCurrencyCode().equals(o.getCurrencyCode())) {
+ throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
+ "Cannot compare CurrencyValues when their currencies are not equal");
+ }
+ if(o.getAmount() < getAmount()) {
+ return 1;
+ }
+ if(o.getAmount() == getAmount()) {
+ return 0;
+ }
+ return -1;
+ }
+
+ @Override
+ public String toString() {
+ return strValue();
+ }
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/f805d020/solr/core/src/java/org/apache/solr/search/facet/FacetRange.java
----------------------------------------------------------------------
diff --git a/solr/core/src/java/org/apache/solr/search/facet/FacetRange.java b/solr/core/src/java/org/apache/solr/search/facet/FacetRange.java
index 1176a77..09b8ec0 100644
--- a/solr/core/src/java/org/apache/solr/search/facet/FacetRange.java
+++ b/solr/core/src/java/org/apache/solr/search/facet/FacetRange.java
@@ -29,6 +29,9 @@ import org.apache.lucene.util.NumericUtils;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.FacetParams;
import org.apache.solr.common.util.SimpleOrderedMap;
+import org.apache.solr.schema.CurrencyFieldType;
+import org.apache.solr.schema.CurrencyValue;
+import org.apache.solr.schema.ExchangeRateProvider;
import org.apache.solr.schema.FieldType;
import org.apache.solr.schema.PointField;
import org.apache.solr.schema.SchemaField;
@@ -120,6 +123,14 @@ class FacetRangeProcessor extends FacetProcessor<FacetRange> {
}
}
+ /**
+ * Returns a {@link Calc} instance to use for <em>term</em> faceting over a numeric field.
+ * This metod is unused for <code>range</code> faceting, and exists solely as a helper method for other classes
+ *
+ * @param sf A field to facet on, must be of a type such that {@link FieldType#getNumberType} is non null
+ * @return a <code>Calc</code> instance with {@link Calc#bitsToValue} and {@link Calc#bitsToSortableBits} methods suitable for the specified field.
+ * @see FacetFieldProcessorByHashDV
+ */
public static Calc getNumericCalc(SchemaField sf) {
Calc calc;
final FieldType ft = sf.getType();
@@ -203,6 +214,8 @@ class FacetRangeProcessor extends FacetProcessor<FacetRange> {
(SolrException.ErrorCode.BAD_REQUEST,
"Unable to range facet on tried field of unexpected type:" + freq.field);
}
+ } else if (ft instanceof CurrencyFieldType) {
+ calc = new CurrencyCalc(sf);
} else {
throw new SolrException
(SolrException.ErrorCode.BAD_REQUEST,
@@ -260,7 +273,7 @@ class FacetRangeProcessor extends FacetProcessor<FacetRange> {
(include.contains(FacetParams.FacetRangeInclude.EDGE) &&
0 == high.compareTo(end)));
- Range range = new Range(low, low, high, incLower, incUpper);
+ Range range = new Range(calc.buildRangeLabel(low), low, high, incLower, incUpper);
rangeList.add( range );
low = high;
@@ -400,15 +413,29 @@ class FacetRangeProcessor extends FacetProcessor<FacetRange> {
this.field = field;
}
+ /**
+ * Used by {@link FacetFieldProcessorByHashDV} for field faceting on numeric types -- not used for <code>range</code> faceting
+ */
public Comparable bitsToValue(long bits) {
return bits;
}
+ /**
+ * Used by {@link FacetFieldProcessorByHashDV} for field faceting on numeric types -- not used for <code>range</code> faceting
+ */
public long bitsToSortableBits(long bits) {
return bits;
}
/**
+ * Given the low value for a bucket, generates the appropraite "label" object to use.
+ * By default return the low object unmodified.
+ */
+ public Object buildRangeLabel(Comparable low) {
+ return low;
+ }
+
+ /**
* Formats a value into a label used in a response
* Default Impl just uses toString()
*/
@@ -605,6 +632,84 @@ class FacetRangeProcessor extends FacetProcessor<FacetRange> {
}
}
+ private static class CurrencyCalc extends Calc {
+ private String defaultCurrencyCode;
+ private ExchangeRateProvider exchangeRateProvider;
+ public CurrencyCalc(final SchemaField field) {
+ super(field);
+ if(!(this.field.getType() instanceof CurrencyFieldType)) {
+ throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
+ "Cannot perform range faceting over non CurrencyField fields");
+ }
+ defaultCurrencyCode =
+ ((CurrencyFieldType)this.field.getType()).getDefaultCurrency();
+ exchangeRateProvider =
+ ((CurrencyFieldType)this.field.getType()).getProvider();
+ }
+
+ /**
+ * Throws a Server Error that this type of operation is not supported for this field
+ * {@inheritDoc}
+ */
+ @Override
+ public Comparable bitsToValue(long bits) {
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
+ "Currency Field " + field.getName() + " can not be used in this way");
+ }
+
+ /**
+ * Throws a Server Error that this type of operation is not supported for this field
+ * {@inheritDoc}
+ */
+ @Override
+ public long bitsToSortableBits(long bits) {
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
+ "Currency Field " + field.getName() + " can not be used in this way");
+ }
+
+ /**
+ * Returns the short string representation of the CurrencyValue
+ * @see CurrencyValue#strValue
+ */
+ @Override
+ public Object buildRangeLabel(Comparable low) {
+ return ((CurrencyValue)low).strValue();
+ }
+
+ @Override
+ public String formatValue(Comparable val) {
+ return ((CurrencyValue)val).strValue();
+ }
+
+ @Override
+ protected Comparable parseStr(final String rawval) throws java.text.ParseException {
+ return CurrencyValue.parse(rawval, defaultCurrencyCode);
+ }
+
+ @Override
+ protected Object parseGap(final String rawval) throws java.text.ParseException {
+ return parseStr(rawval);
+ }
+
+ @Override
+ protected Comparable parseAndAddGap(Comparable value, String gap) throws java.text.ParseException{
+ if (value == null) {
+ throw new NullPointerException("Cannot perform range faceting on null CurrencyValue");
+ }
+ CurrencyValue val = (CurrencyValue) value;
+ CurrencyValue gapCurrencyValue =
+ CurrencyValue.parse(gap, defaultCurrencyCode);
+ long gapAmount =
+ CurrencyValue.convertAmount(this.exchangeRateProvider,
+ gapCurrencyValue.getCurrencyCode(),
+ gapCurrencyValue.getAmount(),
+ val.getCurrencyCode());
+ return new CurrencyValue(val.getAmount() + gapAmount,
+ val.getCurrencyCode());
+
+ }
+
+ }
// this refineFacets method is patterned after FacetFieldProcessor.refineFacets and should
// probably be merged when range facet becomes more like field facet in it's ability to sort and limit
@@ -709,16 +814,14 @@ class FacetRangeProcessor extends FacetProcessor<FacetRange> {
(include.contains(FacetParams.FacetRangeInclude.EDGE) &&
0 == high.compareTo(end)));
- Range range = new Range(low, low, high, incLower, incUpper);
+ Range range = new Range(calc.buildRangeLabel(low), low, high, incLower, incUpper);
// now refine this range
SimpleOrderedMap<Object> bucket = new SimpleOrderedMap<>();
- FieldType ft = sf.getType();
- bucket.add("val", range.low); // use "low" instead of bucketVal because it will be the right type (we may have been passed back long instead of int for example)
- // String internal = ft.toInternal( tobj.toString() ); // TODO - we need a better way to get from object to query...
-
+ bucket.add("val", range.label);
+
Query domainQ = sf.getType().getRangeQuery(null, sf, range.low == null ? null : calc.formatValue(range.low), range.high==null ? null : calc.formatValue(range.high), range.includeLower, range.includeUpper);
fillBucket(bucket, domainQ, null, skip, facetInfo);
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/f805d020/solr/core/src/test/org/apache/solr/schema/CurrencyFieldTypeTest.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/schema/CurrencyFieldTypeTest.java b/solr/core/src/test/org/apache/solr/schema/CurrencyFieldTypeTest.java
index c2f8f2d..c50f5c5 100644
--- a/solr/core/src/test/org/apache/solr/schema/CurrencyFieldTypeTest.java
+++ b/solr/core/src/test/org/apache/solr/schema/CurrencyFieldTypeTest.java
@@ -26,8 +26,11 @@ import java.util.Set;
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
import org.apache.lucene.index.IndexableField;
import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.SolrParams;
import org.apache.solr.core.SolrCore;
import org.apache.solr.util.RTimer;
+
import org.junit.Assume;
import org.junit.BeforeClass;
import org.junit.Ignore;
@@ -459,6 +462,238 @@ public class CurrencyFieldTypeTest extends SolrTestCaseJ4 {
}
@Test
+ public void testStringValue() throws Exception {
+ assertEquals("3.14,USD", new CurrencyValue(314, "USD").strValue());
+ assertEquals("-3.14,GBP", new CurrencyValue(-314, "GBP").strValue());
+ assertEquals("3.14,GBP", new CurrencyValue(314, "GBP").strValue());
+
+ CurrencyValue currencyValue = new CurrencyValue(314, "XYZ");
+ try {
+ String string = currencyValue.strValue();
+ fail("Expected SolrException");
+ } catch (SolrException exception) {
+ } catch (Throwable throwable) {
+ fail("Expected SolrException");
+ }
+ }
+
+ @Test
+ public void testRangeFacet() throws Exception {
+ assumeTrue("This test is only applicable to the XML file based exchange rate provider " +
+ "because it excercies the asymetric exchange rates option it supports",
+ expectedProviderClass.equals(FileExchangeRateProvider.class));
+
+ clearIndex();
+
+ // NOTE: in our test conversions EUR uses an asynetric echange rate
+ // these are the equivilent values when converting to: USD EUR GBP
+ assertU(adoc("id", "" + 1, fieldName, "10.00,USD")); // 10.00,USD 25.00,EUR 5.00,GBP
+ assertU(adoc("id", "" + 2, fieldName, "15.00,EUR")); // 7.50,USD 15.00,EUR 7.50,GBP
+ assertU(adoc("id", "" + 3, fieldName, "6.00,GBP")); // 12.00,USD 12.00,EUR 6.00,GBP
+ assertU(adoc("id", "" + 4, fieldName, "7.00,EUR")); // 3.50,USD 7.00,EUR 3.50,GBP
+ assertU(adoc("id", "" + 5, fieldName, "2,GBP")); // 4.00,USD 4.00,EUR 2.00,GBP
+ assertU(commit());
+
+ for (String suffix : Arrays.asList("", ",USD")) {
+ assertQ("Ensure that we get correct facet counts back in USD (explicit or implicit default) (facet.range)",
+ req("fl", "*,score", "q", "*:*", "rows", "0", "facet", "true",
+ "facet.range", fieldName,
+ "f." + fieldName + ".facet.range.start", "4.00" + suffix,
+ "f." + fieldName + ".facet.range.end", "11.00" + suffix,
+ "f." + fieldName + ".facet.range.gap", "1.00" + suffix,
+ "f." + fieldName + ".facet.range.other", "all")
+ ,"count(//lst[@name='counts']/int)=7"
+ ,"//lst[@name='counts']/int[@name='4.00,USD']='1'"
+ ,"//lst[@name='counts']/int[@name='5.00,USD']='0'"
+ ,"//lst[@name='counts']/int[@name='6.00,USD']='0'"
+ ,"//lst[@name='counts']/int[@name='7.00,USD']='1'"
+ ,"//lst[@name='counts']/int[@name='8.00,USD']='0'"
+ ,"//lst[@name='counts']/int[@name='9.00,USD']='0'"
+ ,"//lst[@name='counts']/int[@name='10.00,USD']='1'"
+ ,"//int[@name='after']='1'"
+ ,"//int[@name='before']='1'"
+ ,"//int[@name='between']='3'"
+ );
+ assertQ("Ensure that we get correct facet counts back in USD (explicit or implicit default) (json.facet)",
+ req("fl", "*,score", "q", "*:*", "rows", "0", "json.facet",
+ "{ xxx : { type:range, field:" + fieldName + ", " +
+ " start:'4.00"+suffix+"', gap:'1.00"+suffix+"', end:'11.00"+suffix+"', other:all } }")
+ ,"count(//lst[@name='xxx']/arr[@name='buckets']/lst)=7"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='4.00,USD']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='5.00,USD']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='6.00,USD']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='7.00,USD']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='8.00,USD']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='9.00,USD']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='10.00,USD']]"
+ ,"//lst[@name='xxx']/lst[@name='before' ]/int[@name='count'][.='1']"
+ ,"//lst[@name='xxx']/lst[@name='after' ]/int[@name='count'][.='1']"
+ ,"//lst[@name='xxx']/lst[@name='between']/int[@name='count'][.='3']"
+ );
+ }
+
+ assertQ("Zero value as start range point + mincount (facet.range)",
+ req("fl", "*,score", "q", "*:*", "rows", "0", "facet", "true", "facet.mincount", "1",
+ "facet.range", fieldName,
+ "f." + fieldName + ".facet.range.start", "0,USD",
+ "f." + fieldName + ".facet.range.end", "11.00,USD",
+ "f." + fieldName + ".facet.range.gap", "1.00,USD",
+ "f." + fieldName + ".facet.range.other", "all")
+ ,"count(//lst[@name='counts']/int)=4"
+ ,"//lst[@name='counts']/int[@name='3.00,USD']='1'"
+ ,"//lst[@name='counts']/int[@name='4.00,USD']='1'"
+ ,"//lst[@name='counts']/int[@name='7.00,USD']='1'"
+ ,"//lst[@name='counts']/int[@name='10.00,USD']='1'"
+ ,"//int[@name='before']='0'"
+ ,"//int[@name='after']='1'"
+ ,"//int[@name='between']='4'"
+ );
+ assertQ("Zero value as start range point + mincount (json.facet)",
+ req("fl", "*,score", "q", "*:*", "rows", "0", "json.facet",
+ "{ xxx : { type:range, mincount:1, field:" + fieldName +
+ ", start:'0.00,USD', gap:'1.00,USD', end:'11.00,USD', other:all } }")
+ ,"count(//lst[@name='xxx']/arr[@name='buckets']/lst)=4"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='3.00,USD']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='4.00,USD']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='7.00,USD']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='10.00,USD']]"
+ ,"//lst[@name='xxx']/lst[@name='before' ]/int[@name='count'][.='0']"
+ ,"//lst[@name='xxx']/lst[@name='after' ]/int[@name='count'][.='1']"
+ ,"//lst[@name='xxx']/lst[@name='between']/int[@name='count'][.='4']"
+ );
+
+ // NOTE: because of asymetric EUR exchange rate, these buckets are diff then the similar looking USD based request above
+ // This request converts the values in each doc into EUR to decide what range buck it's in.
+ assertQ("Ensure that we get correct facet counts back in EUR (facet.range)",
+ req("fl", "*,score", "q", "*:*", "rows", "0", "facet", "true",
+ "facet.range", fieldName,
+ "f." + fieldName + ".facet.range.start", "8.00,EUR",
+ "f." + fieldName + ".facet.range.end", "22.00,EUR",
+ "f." + fieldName + ".facet.range.gap", "2.00,EUR",
+ "f." + fieldName + ".facet.range.other", "all"
+ )
+ , "count(//lst[@name='counts']/int)=7"
+ , "//lst[@name='counts']/int[@name='8.00,EUR']='0'"
+ , "//lst[@name='counts']/int[@name='10.00,EUR']='0'"
+ , "//lst[@name='counts']/int[@name='12.00,EUR']='1'"
+ , "//lst[@name='counts']/int[@name='14.00,EUR']='1'"
+ , "//lst[@name='counts']/int[@name='16.00,EUR']='0'"
+ , "//lst[@name='counts']/int[@name='18.00,EUR']='0'"
+ , "//lst[@name='counts']/int[@name='20.00,EUR']='0'"
+ , "//int[@name='before']='2'"
+ , "//int[@name='after']='1'"
+ , "//int[@name='between']='2'"
+ );
+ assertQ("Ensure that we get correct facet counts back in EUR (json.facet)",
+ req("fl", "*,score", "q", "*:*", "rows", "0", "json.facet",
+ "{ xxx : { type:range, field:" + fieldName + ", start:'8.00,EUR', gap:'2.00,EUR', end:'22.00,EUR', other:all } }")
+ ,"count(//lst[@name='xxx']/arr[@name='buckets']/lst)=7"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='8.00,EUR']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='10.00,EUR']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='12.00,EUR']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='14.00,EUR']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='16.00,EUR']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='18.00,EUR']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='20.00,EUR']]"
+ ,"//lst[@name='xxx']/lst[@name='before' ]/int[@name='count'][.='2']"
+ ,"//lst[@name='xxx']/lst[@name='after' ]/int[@name='count'][.='1']"
+ ,"//lst[@name='xxx']/lst[@name='between']/int[@name='count'][.='2']"
+ );
+
+
+ // GBP has a symetric echange rate with USD, so these counts are *similar* to the USD based request above...
+ // but the asymetric EUR/USD rate means that when computing counts realtive to GBP the EUR based docs wind up in
+ // diff buckets
+ assertQ("Ensure that we get correct facet counts back in GBP (facet.range)",
+ req("fl", "*,score", "q", "*:*", "rows", "0", "facet", "true",
+ "facet.range", fieldName,
+ "f." + fieldName + ".facet.range.start", "2.00,GBP",
+ "f." + fieldName + ".facet.range.end", "5.50,GBP",
+ "f." + fieldName + ".facet.range.gap", "0.50,GBP",
+ "f." + fieldName + ".facet.range.other", "all"
+ )
+ , "count(//lst[@name='counts']/int)=7"
+ , "//lst[@name='counts']/int[@name='2.00,GBP']='1'"
+ , "//lst[@name='counts']/int[@name='2.50,GBP']='0'"
+ , "//lst[@name='counts']/int[@name='3.00,GBP']='0'"
+ , "//lst[@name='counts']/int[@name='3.50,GBP']='1'"
+ , "//lst[@name='counts']/int[@name='4.00,GBP']='0'"
+ , "//lst[@name='counts']/int[@name='4.50,GBP']='0'"
+ , "//lst[@name='counts']/int[@name='5.00,GBP']='1'"
+ , "//int[@name='before']='0'"
+ , "//int[@name='after']='2'"
+ , "//int[@name='between']='3'"
+ );
+ assertQ("Ensure that we get correct facet counts back in GBP (json.facet)",
+ req("fl", "*,score", "q", "*:*", "rows", "0", "json.facet",
+ "{ xxx : { type:range, field:" + fieldName + ", start:'2.00,GBP', gap:'0.50,GBP', end:'5.50,GBP', other:all } }")
+ ,"count(//lst[@name='xxx']/arr[@name='buckets']/lst)=7"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='2.00,GBP']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='2.50,GBP']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='3.00,GBP']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='3.50,GBP']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='4.00,GBP']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='4.50,GBP']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='5.00,GBP']]"
+ ,"//lst[@name='xxx']/lst[@name='before' ]/int[@name='count'][.='0']"
+ ,"//lst[@name='xxx']/lst[@name='after' ]/int[@name='count'][.='2']"
+ ,"//lst[@name='xxx']/lst[@name='between']/int[@name='count'][.='3']"
+ );
+
+ assertQ("Ensure that we can set a gap in a currency other than the start and end currencies (facet.range)",
+ req("fl", "*,score", "q", "*:*", "rows", "0", "facet", "true",
+ "facet.range", fieldName,
+ "f." + fieldName + ".facet.range.start", "4.00,USD",
+ "f." + fieldName + ".facet.range.end", "11.00,USD",
+ "f." + fieldName + ".facet.range.gap", "0.50,GBP",
+ "f." + fieldName + ".facet.range.other", "all"
+ )
+ , "count(//lst[@name='counts']/int)=7"
+ , "//lst[@name='counts']/int[@name='4.00,USD']='1'"
+ , "//lst[@name='counts']/int[@name='5.00,USD']='0'"
+ , "//lst[@name='counts']/int[@name='6.00,USD']='0'"
+ , "//lst[@name='counts']/int[@name='7.00,USD']='1'"
+ , "//lst[@name='counts']/int[@name='8.00,USD']='0'"
+ , "//lst[@name='counts']/int[@name='9.00,USD']='0'"
+ , "//lst[@name='counts']/int[@name='10.00,USD']='1'"
+ , "//int[@name='before']='1'"
+ , "//int[@name='after']='1'"
+ , "//int[@name='between']='3'"
+ );
+ assertQ("Ensure that we can set a gap in a currency other than the start and end currencies (json.facet)",
+ req("fl", "*,score", "q", "*:*", "rows", "0", "json.facet",
+ "{ xxx : { type:range, field:" + fieldName + ", start:'4.00,USD', gap:'0.50,GBP', end:'11.00,USD', other:all } }")
+ ,"count(//lst[@name='xxx']/arr[@name='buckets']/lst)=7"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='4.00,USD']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='5.00,USD']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='6.00,USD']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='7.00,USD']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='8.00,USD']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='0']][str[@name='val'][.='9.00,USD']]"
+ ,"//lst[@name='xxx']/arr[@name='buckets']/lst[int[@name='count'][.='1']][str[@name='val'][.='10.00,USD']]"
+
+ ,"//lst[@name='xxx']/lst[@name='before' ]/int[@name='count'][.='1']"
+ ,"//lst[@name='xxx']/lst[@name='after' ]/int[@name='count'][.='1']"
+ ,"//lst[@name='xxx']/lst[@name='between']/int[@name='count'][.='3']"
+ );
+
+ for (SolrParams facet : Arrays.asList(params("facet", "true",
+ "facet.range", fieldName,
+ "f." + fieldName + ".facet.range.start", "4.00,USD",
+ "f." + fieldName + ".facet.range.end", "11.00,EUR",
+ "f." + fieldName + ".facet.range.gap", "1.00,USD",
+ "f." + fieldName + ".facet.range.other", "all"),
+ params("json.facet",
+ "{ xxx : { type:range, field:" + fieldName + ", start:'4.00,USD', " +
+ " gap:'1.00,USD', end:'11.00,EUR', other:all } }"))) {
+ assertQEx("Ensure that we throw an error if we try to use different start and end currencies",
+ "Cannot compare CurrencyValues when their currencies are not equal",
+ req(facet, "q", "*:*"),
+ SolrException.ErrorCode.BAD_REQUEST);
+ }
+ }
+
+ @Test
public void testMockFieldType() throws Exception {
assumeTrue("This test is only applicable to the mock exchange rate provider",
expectedProviderClass.equals(MockExchangeRateProvider.class));
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/f805d020/solr/core/src/test/org/apache/solr/search/CurrencyRangeFacetCloudTest.java
----------------------------------------------------------------------
diff --git a/solr/core/src/test/org/apache/solr/search/CurrencyRangeFacetCloudTest.java b/solr/core/src/test/org/apache/solr/search/CurrencyRangeFacetCloudTest.java
new file mode 100644
index 0000000..c4b9281
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/search/CurrencyRangeFacetCloudTest.java
@@ -0,0 +1,483 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.apache.solr.search;
+
+import java.lang.invoke.MethodHandles;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.lucene.util.TestUtil;
+import org.apache.solr.client.solrj.SolrQuery;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.request.UpdateRequest;
+import org.apache.solr.client.solrj.response.QueryResponse;
+import org.apache.solr.client.solrj.response.RangeFacet;
+import org.apache.solr.cloud.SolrCloudTestCase;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CoreAdminParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.schema.CurrencyFieldTypeTest;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class CurrencyRangeFacetCloudTest extends SolrCloudTestCase {
+
+ private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private static final String COLLECTION = MethodHandles.lookup().lookupClass().getName();
+ private static final String CONF = COLLECTION + "_configSet";
+
+ private static String FIELD = null; // randomized
+
+ private static final List<String> STR_VALS = Arrays.asList("x0", "x1", "x2");
+ // NOTE: in our test conversions EUR uses an asynetric echange rate
+ // these are the equivilent values relative to: USD EUR GBP
+ private static final List<String> VALUES = Arrays.asList("10.00,USD", // 10.00,USD 25.00,EUR 5.00,GBP
+ "15.00,EUR", // 7.50,USD 15.00,EUR 7.50,GBP
+ "6.00,GBP", // 12.00,USD 12.00,EUR 6.00,GBP
+ "7.00,EUR", // 3.50,USD 7.00,EUR 3.50,GBP
+ "2,GBP"); // 4.00,USD 4.00,EUR 2.00,GBP
+ private static final int NUM_DOCS = STR_VALS.size() * VALUES.size();
+
+ @BeforeClass
+ public static void setupCluster() throws Exception {
+ CurrencyFieldTypeTest.assumeCurrencySupport("USD", "EUR", "MXN", "GBP", "JPY", "NOK");
+ FIELD = usually() ? "amount_CFT" : "amount";
+
+ final int numShards = TestUtil.nextInt(random(),1,5);
+ final int numReplicas = 1;
+ final int maxShardsPerNode = 1;
+ final int nodeCount = numShards * numReplicas;
+
+ configureCluster(nodeCount)
+ .addConfig(CONF, Paths.get(TEST_HOME(), "collection1", "conf"))
+ .configure();
+
+ assertEquals(0, (CollectionAdminRequest.createCollection(COLLECTION, CONF, numShards, numReplicas)
+ .setMaxShardsPerNode(maxShardsPerNode)
+ .setProperties(Collections.singletonMap(CoreAdminParams.CONFIG, "solrconfig-minimal.xml"))
+ .process(cluster.getSolrClient())).getStatus());
+
+ cluster.getSolrClient().setDefaultCollection(COLLECTION);
+
+ for (int id = 0; id < NUM_DOCS; id++) { // we're indexing each Currency value in 3 docs, each with a diff 'x_s' field value
+ // use modulo to pick the values, so we don't add the docs in strict order of either VALUES of STR_VALS
+ // (that way if we want ot filter by id later, it's an independent variable)
+ final String x = STR_VALS.get(id % STR_VALS.size());
+ final String val = VALUES.get(id % VALUES.size());
+ assertEquals(0, (new UpdateRequest().add(sdoc("id", "" + id,
+ "x_s", x,
+ FIELD, val))
+ ).process(cluster.getSolrClient()).getStatus());
+
+ }
+ assertEquals(0, cluster.getSolrClient().commit().getStatus());
+ }
+
+ public void testSimpleRangeFacetsOfSymetricRates() throws Exception {
+
+ for (boolean use_mincount : Arrays.asList(true, false)) {
+
+ // exchange rates relative to USD...
+ //
+ // for all of these permutations, the numDocs in each bucket that we get back should be the same
+ // (regardless of the any asymetric echanges ranges, or the currency used for the 'gap') because the
+ // start & end are always in USD.
+ //
+ // NOTE:
+ // - 0,1,2 are the *input* start,gap,end
+ // - 3,4,5 are the *normalized* start,gap,end expected in the response
+ for (List<String> args : Arrays.asList(// default currency is USD
+ Arrays.asList("4", "1.00", "11.0",
+ "4.00,USD", "1.00,USD", "11.00,USD"),
+ // explicit USD
+ Arrays.asList("4,USD", "1,USD", "11,USD",
+ "4.00,USD", "1.00,USD", "11.00,USD"),
+ // Gap can be in diff currency (but start/end must currently match)
+ Arrays.asList("4.00,USD", "000.50,GBP", "11,USD",
+ "4.00,USD", ".50,GBP", "11.00,USD"),
+ Arrays.asList("4.00,USD", "2,EUR", "11,USD",
+ "4.00,USD", "2.00,EUR", "11.00,USD"))) {
+
+ assertEquals(6, args.size()); // sanity check
+
+ // first let's check facet.range
+ SolrQuery solrQuery = new SolrQuery("q", "*:*", "rows", "0", "facet", "true", "facet.range", FIELD,
+ "facet.mincount", (use_mincount ? "3" : "0"),
+ "f." + FIELD + ".facet.range.start", args.get(0),
+ "f." + FIELD + ".facet.range.gap", args.get(1),
+ "f." + FIELD + ".facet.range.end", args.get(2),
+ "f." + FIELD + ".facet.range.other", "all");
+ QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
+ try {
+ assertEquals(NUM_DOCS, rsp.getResults().getNumFound());
+
+ final String start = args.get(3);
+ final String gap = args.get(4);
+ final String end = args.get(5);
+
+ final List<RangeFacet> range_facets = rsp.getFacetRanges();
+ assertEquals(1, range_facets.size());
+ final RangeFacet result = range_facets.get(0);
+ assertEquals(FIELD, result.getName());
+ assertEquals(start, result.getStart());
+ assertEquals(gap, result.getGap());
+ assertEquals(end, result.getEnd());
+ assertEquals(3, result.getBefore());
+ assertEquals(3, result.getAfter());
+ assertEquals(9, result.getBetween());
+
+ List<RangeFacet.Count> counts = result.getCounts();
+ if (use_mincount) {
+ assertEquals(3, counts.size());
+ for (int i = 0; i < 3; i++) {
+ RangeFacet.Count bucket = counts.get(i);
+ assertEquals((4 + (i * 3)) + ".00,USD", bucket.getValue());
+ assertEquals("bucket #" + i, 3, bucket.getCount());
+ }
+ } else {
+ assertEquals(7, counts.size());
+ for (int i = 0; i < 7; i++) {
+ RangeFacet.Count bucket = counts.get(i);
+ assertEquals((4 + i) + ".00,USD", bucket.getValue());
+ assertEquals("bucket #" + i, (i == 0 || i == 3 || i == 6) ? 3 : 0, bucket.getCount());
+ }
+ }
+ } catch (AssertionError|RuntimeException ae) {
+ throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
+ }
+
+ // same basic logic, w/json.facet
+ solrQuery = new SolrQuery("q", "*:*", "rows", "0", "json.facet",
+ "{ foo:{ type:range, field:"+FIELD+", mincount:"+(use_mincount ? 3 : 0)+", " +
+ " start:'"+args.get(0)+"', gap:'"+args.get(1)+"', end:'"+args.get(2)+"', other:all}}");
+ rsp = cluster.getSolrClient().query(solrQuery);
+ try {
+ assertEquals(NUM_DOCS, rsp.getResults().getNumFound());
+
+ final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
+
+ assertEqualsHACK("before", 3L, ((NamedList)foo.get("before")).get("count"));
+ assertEqualsHACK("after", 3L, ((NamedList)foo.get("after")).get("count"));
+ assertEqualsHACK("between", 9L, ((NamedList)foo.get("between")).get("count"));
+
+ final List<NamedList> buckets = (List<NamedList>) foo.get("buckets");
+
+ if (use_mincount) {
+ assertEquals(3, buckets.size());
+ for (int i = 0; i < 3; i++) {
+ NamedList bucket = buckets.get(i);
+ assertEquals((4 + (3 * i)) + ".00,USD", bucket.get("val"));
+ assertEqualsHACK("bucket #" + i, 3L, bucket.get("count"));
+ }
+ } else {
+ assertEquals(7, buckets.size());
+ for (int i = 0; i < 7; i++) {
+ NamedList bucket = buckets.get(i);
+ assertEquals((4 + i) + ".00,USD", bucket.get("val"));
+ assertEqualsHACK("bucket #" + i, (i == 0 || i == 3 || i == 6) ? 3L : 0L, bucket.get("count"));
+ }
+ }
+ } catch (AssertionError|RuntimeException ae) {
+ throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
+ }
+
+ }
+ }
+ }
+
+ public void testFacetRangeOfAsymetricRates() throws Exception {
+ // facet.range: exchange rates relative to EUR...
+ //
+ // because of the asymetric echange rate, the counts for these buckets will be different
+ // then if we just converted the EUR values to USD
+ for (boolean use_mincount : Arrays.asList(true, false)) {
+ final SolrQuery solrQuery = new SolrQuery("q", "*:*", "rows", "0", "facet", "true", "facet.range", FIELD,
+ "facet.mincount", (use_mincount ? "3" : "0"),
+ "f." + FIELD + ".facet.range.start", "8,EUR",
+ "f." + FIELD + ".facet.range.gap", "2,EUR",
+ "f." + FIELD + ".facet.range.end", "22,EUR",
+ "f." + FIELD + ".facet.range.other", "all");
+ final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
+ try {
+ assertEquals(NUM_DOCS, rsp.getResults().getNumFound());
+ final List<RangeFacet> range_facets = rsp.getFacetRanges();
+ assertEquals(1, range_facets.size());
+ final RangeFacet result = range_facets.get(0);
+ assertEquals(FIELD, result.getName());
+ assertEquals("8.00,EUR", result.getStart());
+ assertEquals("2.00,EUR", result.getGap());
+ assertEquals("22.00,EUR", result.getEnd());
+ assertEquals(6, result.getBefore());
+ assertEquals(3, result.getAfter());
+ assertEquals(6, result.getBetween());
+
+ List<RangeFacet.Count> counts = result.getCounts();
+ if (use_mincount) {
+ assertEquals(2, counts.size());
+ for (int i = 0; i < 2; i++) {
+ RangeFacet.Count bucket = counts.get(i);
+ assertEquals((12 + (i * 2)) + ".00,EUR", bucket.getValue());
+ assertEquals("bucket #" + i, 3, bucket.getCount());
+ }
+ } else {
+ assertEquals(7, counts.size());
+ for (int i = 0; i < 7; i++) {
+ RangeFacet.Count bucket = counts.get(i);
+ assertEquals((8 + (i * 2)) + ".00,EUR", bucket.getValue());
+ assertEquals("bucket #" + i, (i == 2 || i == 3) ? 3 : 0, bucket.getCount());
+ }
+ }
+ } catch (AssertionError|RuntimeException ae) {
+ throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
+ }
+ }
+ }
+
+ public void testJsonFacetRangeOfAsymetricRates() throws Exception {
+ // json.facet: exchange rates relative to EUR (same as testFacetRangeOfAsymetricRates)
+ //
+ // because of the asymetric echange rate, the counts for these buckets will be different
+ // then if we just converted the EUR values to USD
+ for (boolean use_mincount : Arrays.asList(true, false)) {
+ final SolrQuery solrQuery = new SolrQuery("q", "*:*", "rows", "0", "json.facet",
+ "{ foo:{ type:range, field:"+FIELD+", start:'8,EUR', " +
+ " mincount:"+(use_mincount ? 3 : 0)+", " +
+ " gap:'2,EUR', end:'22,EUR', other:all}}");
+ final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
+ try {
+ assertEquals(NUM_DOCS, rsp.getResults().getNumFound());
+
+ final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
+
+ assertEqualsHACK("before", 6L, ((NamedList)foo.get("before")).get("count"));
+ assertEqualsHACK("after", 3L, ((NamedList)foo.get("after")).get("count"));
+ assertEqualsHACK("between", 6L, ((NamedList)foo.get("between")).get("count"));
+
+ final List<NamedList> buckets = (List<NamedList>) foo.get("buckets");
+
+ if (use_mincount) {
+ assertEquals(2, buckets.size());
+ for (int i = 0; i < 2; i++) {
+ NamedList bucket = buckets.get(i);
+ assertEquals((12 + (i * 2)) + ".00,EUR", bucket.get("val"));
+ assertEqualsHACK("bucket #" + i, 3L, bucket.get("count"));
+ }
+ } else {
+ assertEquals(7, buckets.size());
+ for (int i = 0; i < 7; i++) {
+ NamedList bucket = buckets.get(i);
+ assertEquals((8 + (i * 2)) + ".00,EUR", bucket.get("val"));
+ assertEqualsHACK("bucket #" + i, (i == 2 || i == 3) ? 3L : 0L, bucket.get("count"));
+ }
+ }
+ } catch (AssertionError|RuntimeException ae) {
+ throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
+ }
+ }
+ }
+
+ public void testFacetRangeCleanErrorOnMissmatchCurrency() {
+ final String expected = "Cannot compare CurrencyValues when their currencies are not equal";
+ ignoreException(expected);
+
+ // test to check clean error when start/end have diff currency (facet.range)
+ final SolrQuery solrQuery = new SolrQuery("q", "*:*", "rows", "0", "facet", "true", "facet.range", FIELD,
+ "f." + FIELD + ".facet.range.start", "0,EUR",
+ "f." + FIELD + ".facet.range.gap", "10,EUR",
+ "f." + FIELD + ".facet.range.end", "100,USD");
+ final SolrException ex = expectThrows(SolrException.class, () -> {
+ final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
+ });
+ assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());
+ assertTrue(ex.getMessage(), ex.getMessage().contains(expected));
+ }
+
+ public void testJsonFacetCleanErrorOnMissmatchCurrency() {
+ final String expected = "Cannot compare CurrencyValues when their currencies are not equal";
+ ignoreException(expected);
+
+ // test to check clean error when start/end have diff currency (json.facet)
+ final SolrQuery solrQuery = new SolrQuery("q", "*:*", "json.facet",
+ "{ x:{ type:range, field:"+FIELD+", " +
+ " start:'0,EUR', gap:'10,EUR', end:'100,USD' } }");
+ final SolrException ex = expectThrows(SolrException.class, () -> {
+ final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
+ });
+ assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());
+ assertTrue(ex.getMessage(), ex.getMessage().contains(expected));
+ }
+
+ @Test
+ public void testJsonRangeFacetWithSubFacet() throws Exception {
+
+ // range facet, with terms facet nested under using limit=2 w/overrequest disabled
+ // filter out the first 5 docs (by id) which should ensure that regardless of sharding:
+ // - x2 being the top term for the 1st range bucket
+ // - x0 being the top term for the 2nd range bucket
+ // - the 2nd term in each bucket may vary based on shard/doc placement, but the count will always be '1'
+ // ...and in many cases (based on the shard/doc placement) this will require refinement to backfill the top terms
+ final String filter = "id_i1:["+VALUES.size()+" TO *]";
+
+ // the *facet* results should be the same regardless of wether we filter via fq, or using a domain filter on the top facet
+ for (boolean use_domain : Arrays.asList(true, false)) {
+ final String domain = use_domain ? "domain: { filter:'" + filter + "'}," : "";
+ final SolrQuery solrQuery = new SolrQuery("q", (use_domain ? "*:*" : filter),
+ "rows", "0", "json.facet",
+ "{ bar:{ type:range, field:"+FIELD+", " + domain +
+ " start:'0,EUR', gap:'10,EUR', end:'20,EUR', other:all " +
+ " facet: { foo:{ type:terms, field:x_s, " +
+ " refine:true, limit:2, overrequest:0" +
+ " } } } }");
+ final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
+ try {
+ // this top level result count sanity check that should vary based on how we are filtering our facets...
+ assertEquals(use_domain ? 15 : 10, rsp.getResults().getNumFound());
+
+ final NamedList<Object> bar = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("bar");
+ final List<NamedList<Object>> bar_buckets = (List<NamedList<Object>>) bar.get("buckets");
+ final NamedList<Object> before = (NamedList<Object>) bar.get("before");
+ final NamedList<Object> between = (NamedList<Object>) bar.get("between");
+ final NamedList<Object> after = (NamedList<Object>) bar.get("after");
+
+ // sanity check our high level expectations...
+ assertEquals("bar num buckets", 2, bar_buckets.size());
+ assertEqualsHACK("before count", 0L, before.get("count"));
+ assertEqualsHACK("between count", 8L, between.get("count"));
+ assertEqualsHACK("after count", 2L, after.get("count"));
+
+ // drill into the various buckets...
+
+ // before should have no subfacets since it's empty...
+ assertNull("before has foo???", before.get("foo"));
+
+ // our 2 range buckets & their sub facets...
+ for (int i = 0; i < 2; i++) {
+ final NamedList<Object> bucket = bar_buckets.get(i);
+ assertEquals((i * 10) + ".00,EUR", bucket.get("val"));
+ assertEqualsHACK("bucket #" + i, 4L, bucket.get("count"));
+ final List<NamedList<Object>> foo_buckets = ((NamedList<List<NamedList<Object>>>)bucket.get("foo")).get("buckets");
+ assertEquals("bucket #" + i + " foo num buckets", 2, foo_buckets.size());
+ assertEquals("bucket #" + i + " foo top term", (0==i ? "x2" : "x0"), foo_buckets.get(0).get("val"));
+ assertEqualsHACK("bucket #" + i + " foo top count", 2, foo_buckets.get(0).get("count"));
+ // NOTE: we can't make any assertions about the 2nd val..
+ // our limit + randomized sharding could result in either remaining term being picked.
+ // but for eiter term, the count should be exactly the same...
+ assertEqualsHACK("bucket #" + i + " foo 2nd count", 1, foo_buckets.get(1).get("count"));
+ }
+
+ { // between...
+ final List<NamedList<Object>> buckets = ((NamedList<List<NamedList<Object>>>)between.get("foo")).get("buckets");
+ assertEquals("between num buckets", 2, buckets.size());
+ // the counts should both be 3, and the term order should break the tie...
+ assertEquals("between bucket top", "x0", buckets.get(0).get("val"));
+ assertEqualsHACK("between bucket top count", 3L, buckets.get(0).get("count"));
+ assertEquals("between bucket 2nd", "x2", buckets.get(1).get("val"));
+ assertEqualsHACK("between bucket 2nd count", 3L, buckets.get(1).get("count"));
+ }
+
+ { // after...
+ final List<NamedList<Object>> buckets = ((NamedList<List<NamedList<Object>>>)after.get("foo")).get("buckets");
+ assertEquals("after num buckets", 2, buckets.size());
+ // the counts should both be 1, and the term order should break the tie...
+ assertEquals("after bucket top", "x1", buckets.get(0).get("val"));
+ assertEqualsHACK("after bucket top count", 1L, buckets.get(0).get("count"));
+ assertEquals("after bucket 2nd", "x2", buckets.get(1).get("val"));
+ assertEqualsHACK("after bucket 2nd count", 1L, buckets.get(1).get("count"));
+ }
+
+ } catch (AssertionError|RuntimeException ae) {
+ throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
+ }
+ }
+ }
+
+ @Test
+ public void testJsonRangeFacetAsSubFacet() throws Exception {
+
+ // limit=1, overrequest=1, with refinement enabled
+ // filter out the first 5 docs (by id), which should ensure that 'x2' is the top bucket overall...
+ // ...except in some rare sharding cases where it doesn't make it into the top 2 terms.
+ //
+ // So the filter also explicitly accepts all 'x2' docs -- ensuring we have enough matches containing that term for it
+ // to be enough of a candidate in phase#1, but for many shard arrangements it won't be returned by all shards resulting
+ // in refinement being neccessary to get the x_s:x2 sub-shard ranges from shard(s) where x_s:x2 is only tied for the
+ // (shard local) top term count and would lose the (index order) tie breaker with x_s:x0 or x_s:x1
+ final String filter = "id_i1:["+VALUES.size()+" TO *] OR x_s:x2";
+
+ // the *facet* results should be the same regardless of wether we filter via fq, or using a domain filter on the top facet
+ for (boolean use_domain : Arrays.asList(true, false)) {
+ final String domain = use_domain ? "domain: { filter:'" + filter + "'}," : "";
+ final SolrQuery solrQuery = new SolrQuery("q", (use_domain ? "*:*" : filter),
+ "rows", "0", "json.facet",
+ "{ foo:{ type:terms, field:x_s, refine:true, limit:1, overrequest:1, " + domain +
+ " facet: { bar:{ type:range, field:"+FIELD+", other:all, " +
+ " start:'8,EUR', gap:'2,EUR', end:'22,EUR' }} } }");
+ final QueryResponse rsp = cluster.getSolrClient().query(solrQuery);
+ try {
+ // this top level result count sanity check that should vary based on how we are filtering our facets...
+ assertEquals(use_domain ? 15 : 11, rsp.getResults().getNumFound());
+
+ final NamedList<Object> foo = ((NamedList<NamedList<Object>>)rsp.getResponse().get("facets")).get("foo");
+
+ // sanity check...
+ // because of the facet limit, foo should only have 1 bucket
+ // because of the fq, the val should be "x2" and the count=5
+ final List<NamedList<Object>> foo_buckets = (List<NamedList<Object>>) foo.get("buckets");
+ assertEquals(1, foo_buckets.size());
+ assertEquals("x2", foo_buckets.get(0).get("val"));
+ assertEqualsHACK("foo bucket count", 5L, foo_buckets.get(0).get("count"));
+
+ final NamedList<Object> bar = (NamedList<Object>)foo_buckets.get(0).get("bar");
+
+ // these are the 'x2' specific counts, based on our fq...
+
+ assertEqualsHACK("before", 2L, ((NamedList)bar.get("before")).get("count"));
+ assertEqualsHACK("after", 1L, ((NamedList)bar.get("after")).get("count"));
+ assertEqualsHACK("between", 2L, ((NamedList)bar.get("between")).get("count"));
+
+ final List<NamedList> buckets = (List<NamedList>) bar.get("buckets");
+ assertEquals(7, buckets.size());
+ for (int i = 0; i < 7; i++) {
+ NamedList bucket = buckets.get(i);
+ assertEquals((8 + (i * 2)) + ".00,EUR", bucket.get("val"));
+ // 12,EUR & 15,EUR are the 2 values that align with x2 docs
+ assertEqualsHACK("bucket #" + i, (i == 2 || i == 3) ? 1L : 0L, bucket.get("count"));
+ }
+ } catch (AssertionError|RuntimeException ae) {
+ throw new AssertionError(solrQuery.toString() + " -> " + rsp.toString() + " ===> " + ae.getMessage(), ae);
+ }
+ }
+ }
+
+ /**
+ * HACK to work around SOLR-11775.
+ * Asserts that the 'actual' argument is a (non-null) Number, then compares it's 'longValue' to the 'expected' argument
+ */
+ private static void assertEqualsHACK(String msg, long expected, Object actual) {
+ assertNotNull(msg, actual);
+ assertTrue(msg + " ... NOT A NUMBER: " + actual.getClass(), Number.class.isInstance(actual));
+ assertEquals(msg, expected, ((Number)actual).longValue());
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/f805d020/solr/server/solr/configsets/sample_techproducts_configs/conf/velocity/VM_global_library.vm
----------------------------------------------------------------------
diff --git a/solr/server/solr/configsets/sample_techproducts_configs/conf/velocity/VM_global_library.vm b/solr/server/solr/configsets/sample_techproducts_configs/conf/velocity/VM_global_library.vm
index 76516b7..ef2157c 100644
--- a/solr/server/solr/configsets/sample_techproducts_configs/conf/velocity/VM_global_library.vm
+++ b/solr/server/solr/configsets/sample_techproducts_configs/conf/velocity/VM_global_library.vm
@@ -173,7 +173,11 @@ $val##
#macro(range_get_to_value $inval, $gapval)
#if(${gapval.class.name} == "java.lang.String")
-$inval$gapval##
+#if($gapval.startsWith("+"))
+$inval$gapval## Typically date gaps start with +
+#else
+$inval+$gapval## If the gap does not start with a "+", we add it, such as for currency value
+#end
#elseif(${gapval.class.name} == "java.lang.Float" || ${inval.class.name} == "java.lang.Float")
$math.toDouble($math.add($inval,$gapval))##
#else
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/f805d020/solr/solr-ref-guide/src/working-with-currencies-and-exchange-rates.adoc
----------------------------------------------------------------------
diff --git a/solr/solr-ref-guide/src/working-with-currencies-and-exchange-rates.adoc b/solr/solr-ref-guide/src/working-with-currencies-and-exchange-rates.adoc
index 5b1e23a..4ee5711 100644
--- a/solr/solr-ref-guide/src/working-with-currencies-and-exchange-rates.adoc
+++ b/solr/solr-ref-guide/src/working-with-currencies-and-exchange-rates.adoc
@@ -24,6 +24,7 @@ The `currency` FieldType provides support for monetary values to Solr/Lucene wit
* Sorting
* Currency parsing by either currency code or symbol
* Symmetric & asymmetric exchange rates (asymmetric exchange rates are useful if there are fees associated with exchanging the currency)
+* Range faceting (using either `facet.range` or `type:range` in `json.facet`) as long as the `start` and `end` values are specified in the same Currency.
== Configuring Currencies
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/f805d020/solr/solrj/src/java/org/apache/solr/client/solrj/response/QueryResponse.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/response/QueryResponse.java b/solr/solrj/src/java/org/apache/solr/client/solrj/response/QueryResponse.java
index 4e78005..3124a0b 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/response/QueryResponse.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/response/QueryResponse.java
@@ -384,7 +384,7 @@ public class QueryResponse extends SolrResponseBase
Number between = (Number) values.get("between");
rangeFacet = new RangeFacet.Numeric(facet.getKey(), start, end, gap, before, after, between);
- } else {
+ } else if (rawGap instanceof String && values.get("start") instanceof Date) {
String gap = (String) rawGap;
Date start = (Date) values.get("start");
Date end = (Date) values.get("end");
@@ -394,8 +394,18 @@ public class QueryResponse extends SolrResponseBase
Number between = (Number) values.get("between");
rangeFacet = new RangeFacet.Date(facet.getKey(), start, end, gap, before, after, between);
+ } else {
+ String gap = (String) rawGap;
+ String start = (String) values.get("start");
+ String end = (String) values.get("end");
+
+ Number before = (Number) values.get("before");
+ Number after = (Number) values.get("after");
+ Number between = (Number) values.get("between");
+
+ rangeFacet = new RangeFacet.Currency(facet.getKey(), start, end, gap, before, after, between);
}
-
+
NamedList<Integer> counts = (NamedList<Integer>) values.get("counts");
for (Map.Entry<String, Integer> entry : counts) {
rangeFacet.addCount(entry.getKey(), entry.getValue());
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/f805d020/solr/solrj/src/java/org/apache/solr/client/solrj/response/RangeFacet.java
----------------------------------------------------------------------
diff --git a/solr/solrj/src/java/org/apache/solr/client/solrj/response/RangeFacet.java b/solr/solrj/src/java/org/apache/solr/client/solrj/response/RangeFacet.java
index 6829c17..b970ef5 100644
--- a/solr/solrj/src/java/org/apache/solr/client/solrj/response/RangeFacet.java
+++ b/solr/solrj/src/java/org/apache/solr/client/solrj/response/RangeFacet.java
@@ -97,6 +97,12 @@ public abstract class RangeFacet<B, G> {
}
+ public static class Currency extends RangeFacet<String, String> {
+ public Currency(String name, String start, String end, String gap, Number before, Number after, Number between) {
+ super(name, start, end, gap, before, after, between);
+ }
+ }
+
public static class Count {
private final String value;