SOLR-10303: Initial support for common date/time Stream Evaluators

This commit is contained in:
Gethin James 2017-03-17 14:07:50 +01:00 committed by Joel Bernstein
parent b767d61b9e
commit 24ab117a41
3 changed files with 329 additions and 1 deletions

View File

@ -43,6 +43,7 @@ import org.apache.solr.client.solrj.io.eval.CeilingEvaluator;
import org.apache.solr.client.solrj.io.eval.CoalesceEvaluator;
import org.apache.solr.client.solrj.io.eval.CosineEvaluator;
import org.apache.solr.client.solrj.io.eval.CubedRootEvaluator;
import org.apache.solr.client.solrj.io.eval.DateEvaluator;
import org.apache.solr.client.solrj.io.eval.DivideEvaluator;
import org.apache.solr.client.solrj.io.eval.EqualsEvaluator;
import org.apache.solr.client.solrj.io.eval.ExclusiveOrEvaluator;
@ -248,12 +249,17 @@ public class StreamHandler extends RequestHandlerBase implements SolrCoreAware,
.withFunctionName("cbrt", CubedRootEvaluator.class)
.withFunctionName("coalesce", CoalesceEvaluator.class)
.withFunctionName("uuid", UuidEvaluator.class)
// Conditional Stream Evaluators
.withFunctionName("if", IfThenElseEvaluator.class)
.withFunctionName("analyze", AnalyzeEvaluator.class)
;
// Date evaluators
for (DateEvaluator.FUNCTION function:DateEvaluator.FUNCTION.values()) {
streamFactory.withFunctionName(function.toString(), DateEvaluator.class);
}
// This pulls all the overrides and additions from the config
List<PluginInfo> pluginInfos = core.getSolrConfig().getPluginInfos(Expressible.class.getName());
for (PluginInfo pluginInfo : pluginInfos) {

View File

@ -0,0 +1,133 @@
/*
* 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.client.solrj.io.eval;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeParseException;
import java.time.temporal.IsoFields;
import java.util.Arrays;
import java.util.Locale;
import org.apache.solr.client.solrj.io.Tuple;
import org.apache.solr.client.solrj.io.stream.expr.Explanation;
import org.apache.solr.client.solrj.io.stream.expr.StreamExpression;
import org.apache.solr.client.solrj.io.stream.expr.StreamExpressionParameter;
import org.apache.solr.client.solrj.io.stream.expr.StreamFactory;
/**
* Provides numeric Date/Time stream evaluators
*/
public class DateEvaluator extends NumberEvaluator {
public enum FUNCTION {year, month, day, dayofyear, dayofquarter, hour, minute, quarter, week, second, epoch};
private FUNCTION function;
private String fieldName;
public DateEvaluator(StreamExpression expression, StreamFactory factory) throws IOException {
super(expression, factory);
String functionName = expression.getFunctionName();
try {
this.function = FUNCTION.valueOf(functionName);
} catch (IllegalArgumentException e) {
throw new IOException(String.format(Locale.ROOT,"Invalid date expression %s - expecting one of %s",functionName, Arrays.toString(FUNCTION.values())));
}
fieldName = factory.getValueOperand(expression, 0);
//Taken from Field evaluator
if(fieldName != null && fieldName.startsWith("'") && fieldName.endsWith("'") && fieldName.length() > 1){
fieldName = fieldName.substring(1, fieldName.length() - 1);
}
if(1 != subEvaluators.size()){
throw new IOException(String.format(Locale.ROOT,"Invalid expression %s - expecting one value but found %d",expression,subEvaluators.size()));
}
}
//TODO: Support non-string, eg. java.util.date or instant
@Override
public Number evaluate(Tuple tuple) throws IOException {
try {
String dateStr = (String) tuple.get(fieldName);
if (dateStr != null && !dateStr.isEmpty()) {
Instant instant = Instant.parse(dateStr);
if (function.equals(FUNCTION.epoch)) return instant.toEpochMilli();
LocalDateTime date = LocalDateTime.ofInstant(instant, ZoneOffset.UTC);
return evaluate(date);
}
} catch (ClassCastException | DateTimeParseException e) {
throw new IOException(String.format(Locale.ROOT,"Invalid field %s - The field must be a string formatted in the ISO_INSTANT date format.",fieldName));
}
return null;
}
/**
* Evaluate the date based on the specified function
* @param date
* @return the evaluated value
*/
private Number evaluate(LocalDateTime date) {
switch (function) {
case year:
return date.getYear();
case month:
return date.getMonthValue();
case day:
return date.getDayOfMonth();
case dayofyear:
return date.getDayOfYear();
case hour:
return date.getHour();
case minute:
return date.getMinute();
case second:
return date.getSecond();
case dayofquarter:
return date.get(IsoFields.DAY_OF_QUARTER);
case quarter:
return date.get(IsoFields.QUARTER_OF_YEAR);
case week:
return date.get(IsoFields.WEEK_OF_WEEK_BASED_YEAR);
}
return null;
}
@Override
public StreamExpressionParameter toExpression(StreamFactory factory) throws IOException {
return new StreamExpression(function.toString()).withParameter(fieldName);
}
@Override
public Explanation toExplanation(StreamFactory factory) throws IOException {
return new Explanation(nodeId.toString())
.withExpressionType(Explanation.ExpressionType.EVALUATOR)
.withImplementingClass(getClass().getName())
.withExpression(toExpression(factory).toString());
}
}

View File

@ -0,0 +1,189 @@
/*
* 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.client.solrj.io.stream.eval;
import java.io.IOException;
import java.util.Date;
import java.util.Map;
import org.apache.commons.collections.map.HashedMap;
import org.apache.solr.client.solrj.io.Tuple;
import org.apache.solr.client.solrj.io.eval.DateEvaluator;
import org.apache.solr.client.solrj.io.eval.StreamEvaluator;
import org.apache.solr.client.solrj.io.stream.expr.Explanation;
import org.apache.solr.client.solrj.io.stream.expr.StreamExpression;
import org.apache.solr.client.solrj.io.stream.expr.StreamExpressionParser;
import org.apache.solr.client.solrj.io.stream.expr.StreamFactory;
import org.junit.Test;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
/**
* Tests numeric Date/Time stream evaluators
*/
public class DateEvaluatorTest {
StreamFactory factory;
Map<String, Object> values;
public DateEvaluatorTest() {
super();
factory = new StreamFactory();
factory.withFunctionName("nope", DateEvaluator.class);
for (DateEvaluator.FUNCTION function : DateEvaluator.FUNCTION.values()) {
factory.withFunctionName(function.toString(), DateEvaluator.class);
}
values = new HashedMap();
}
@Test
public void testInvalidExpression() throws Exception {
StreamEvaluator evaluator;
try {
evaluator = factory.constructEvaluator("nope(a)");
evaluator.evaluate(new Tuple(null));
assertTrue(false);
} catch (Exception e) {
assertTrue(e.getCause().getCause().getMessage().contains("Invalid date expression nope"));
assertTrue(e.getCause().getCause().getMessage().contains("expecting one of [year, month, day"));
}
try {
evaluator = factory.constructEvaluator("week()");
assertTrue(false);
} catch (Exception e) {
assertTrue(e.getCause().getCause().getMessage().contains("Invalid expression week()"));
}
try {
evaluator = factory.constructEvaluator("week(a, b)");
assertTrue(false);
} catch (Exception e) {
assertTrue(e.getCause().getCause().getMessage().contains("expecting one value but found 2"));
}
try {
evaluator = factory.constructEvaluator("Week()");
assertTrue(false);
} catch (Exception e) {
assertTrue(e.getMessage().contains("Invalid evaluator expression Week() - function 'Week' is unknown"));
}
}
@Test
public void testInvalidValues() throws Exception {
StreamEvaluator evaluator = factory.constructEvaluator("year(a)");
try {
values.clear();
values.put("a", 12);
Object result = evaluator.evaluate(new Tuple(values));
assertTrue(false);
} catch (Exception e) {
assertEquals("Invalid field a - The field must be a string formatted in the ISO_INSTANT date format.", e.getMessage());
}
try {
values.clear();
values.put("a", "1995-12-31");
Object result = evaluator.evaluate(new Tuple(values));
assertTrue(false);
} catch (Exception e) {
assertEquals("Invalid field a - The field must be a string formatted in the ISO_INSTANT date format.", e.getMessage());
}
values.clear();
values.put("a", null);
assertNull(evaluator.evaluate(new Tuple(values)));
}
@Test
public void testAllFunctions() throws Exception {
//year, month, day, dayofyear, hour, minute, quarter, week, second, epoch
testFunction("year(a)", "1995-12-31T23:59:59Z", 1995);
testFunction("month(a)","1995-12-31T23:59:59Z", 12);
testFunction("day(a)", "1995-12-31T23:59:59Z", 31);
testFunction("dayofyear(a)", "1995-12-31T23:59:59Z", 365);
testFunction("dayofquarter(a)", "1995-12-31T23:59:59Z", 92);
testFunction("hour(a)", "1995-12-31T23:59:59Z", 23);
testFunction("minute(a)", "1995-12-31T23:59:59Z", 59);
testFunction("quarter(a)","1995-12-31T23:59:59Z", 4);
testFunction("week(a)", "1995-12-31T23:59:59Z", 52);
testFunction("second(a)", "1995-12-31T23:59:58Z", 58);
testFunction("epoch(a)", "1995-12-31T23:59:59Z", 820454399000l);
testFunction("year(a)", "2017-03-17T10:30:45Z", 2017);
testFunction("year('a')", "2017-03-17T10:30:45Z", 2017);
testFunction("month(a)","2017-03-17T10:30:45Z", 3);
testFunction("day(a)", "2017-03-17T10:30:45Z", 17);
testFunction("day('a')", "2017-03-17T10:30:45Z", 17);
testFunction("dayofyear(a)", "2017-03-17T10:30:45Z", 76);
testFunction("dayofquarter(a)", "2017-03-17T10:30:45Z", 76);
testFunction("hour(a)", "2017-03-17T10:30:45Z", 10);
testFunction("minute(a)", "2017-03-17T10:30:45Z", 30);
testFunction("quarter(a)","2017-03-17T10:30:45Z", 1);
testFunction("week(a)", "2017-03-17T10:30:45Z", 11);
testFunction("second(a)", "2017-03-17T10:30:45Z", 45);
testFunction("epoch(a)", "2017-03-17T10:30:45Z", 1489746645000l);
testFunction("epoch(a)", new Date(1489746645500l).toInstant().toString(), 1489746645500l);
testFunction("epoch(a)", new Date(820454399990l).toInstant().toString(), 820454399990l);
//Additionally test all functions to make sure they return a non-null number
for (DateEvaluator.FUNCTION function : DateEvaluator.FUNCTION.values()) {
StreamEvaluator evaluator = factory.constructEvaluator(function+"(a)");
values.clear();
values.put("a", "2017-03-17T10:30:45Z");
Object result = evaluator.evaluate(new Tuple(values));
assertNotNull(function+" should return a result",result);
assertTrue(function+" should return a number", result instanceof Number);
}
}
public void testFunction(String expression, String value, Number expected) throws Exception {
StreamEvaluator evaluator = factory.constructEvaluator(expression);
values.clear();
values.put("a", value);
Object result = evaluator.evaluate(new Tuple(values));
assertTrue(result instanceof Number);
assertEquals(expected, result);
}
@Test
public void testExplain() throws IOException {
StreamExpression express = StreamExpressionParser.parse("month('myfield')");
DateEvaluator dateEvaluator = new DateEvaluator(express,factory);
Explanation explain = dateEvaluator.toExplanation(factory);
assertEquals("month(myfield)", explain.getExpression());
express = StreamExpressionParser.parse("day(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbb)");
dateEvaluator = new DateEvaluator(express,factory);
explain = dateEvaluator.toExplanation(factory);
assertEquals("day(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbb)", explain.getExpression());
}
}