mirror of https://github.com/apache/lucene.git
SOLR-3218: Added range faceting support for CurrencyFieldType
This commit is contained in:
parent
972df6c69d
commit
6dcbb2d412
|
@ -84,6 +84,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
|
||||
----------------------
|
||||
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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,14 +413,28 @@ 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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
@ -458,6 +461,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",
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue