diff --git a/8/src/main/java/com/traveltime/plugin/solr/TimeFilterQParserPlugin.java b/8/src/main/java/com/traveltime/plugin/solr/TimeFilterQParserPlugin.java new file mode 100755 index 0000000..777c1ae --- /dev/null +++ b/8/src/main/java/com/traveltime/plugin/solr/TimeFilterQParserPlugin.java @@ -0,0 +1,44 @@ +package com.traveltime.plugin.solr; + +import com.traveltime.plugin.solr.cache.RequestCache; +import com.traveltime.plugin.solr.fetcher.JsonFetcherSingleton; +import com.traveltime.plugin.solr.query.timefilter.TimeFilterQueryParser; +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.common.util.NamedList; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.search.QParser; +import org.apache.solr.search.QParserPlugin; + +import java.net.URI; +import java.util.Optional; + +public class TimeFilterQParserPlugin extends QParserPlugin { + private String cacheName = RequestCache.NAME; + + private static final Integer DEFAULT_LOCATION_SIZE_LIMIT = 2000; + + @Override + public void init(NamedList args) { + Object cache = args.get("cache"); + if(cache != null) cacheName = cache.toString(); + + Object uriVal = args.get("api_uri"); + URI uri = null; + if(uriVal != null) uri = URI.create(uriVal.toString()); + + String appId = args.get("app_id").toString(); + String apiKey = args.get("api_key").toString(); + int locationLimit = + Optional.ofNullable(args.get("location_limit")) + .map(x -> Integer.parseInt(x.toString())) + .orElse(DEFAULT_LOCATION_SIZE_LIMIT); + + JsonFetcherSingleton.INSTANCE.init(uri, appId, apiKey, locationLimit); + } + + @Override + public QParser createParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) { + return new TimeFilterQueryParser(qstr, localParams, params, req, JsonFetcherSingleton.INSTANCE.getFetcher(), cacheName); + } + +} diff --git a/8/src/main/java/com/traveltime/plugin/solr/TraveltimeQParserPlugin.java b/8/src/main/java/com/traveltime/plugin/solr/TraveltimeQParserPlugin.java index 73b8bf7..1a5f8be 100755 --- a/8/src/main/java/com/traveltime/plugin/solr/TraveltimeQParserPlugin.java +++ b/8/src/main/java/com/traveltime/plugin/solr/TraveltimeQParserPlugin.java @@ -1,6 +1,7 @@ package com.traveltime.plugin.solr; import com.traveltime.plugin.solr.cache.RequestCache; +import com.traveltime.plugin.solr.fetcher.ProtoFetcherSingleton; import com.traveltime.plugin.solr.query.TraveltimeQueryParser; import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.util.NamedList; @@ -11,7 +12,6 @@ import java.net.URI; public class TraveltimeQParserPlugin extends QParserPlugin { - public static String PARAM_PREFIX = "traveltime_"; private String cacheName = RequestCache.NAME; @Override @@ -25,12 +25,12 @@ public void init(NamedList args) { String appId = args.get("app_id").toString(); String apiKey = args.get("api_key").toString(); - FetcherSingleton.INSTANCE.init(uri, appId, apiKey); + ProtoFetcherSingleton.INSTANCE.init(uri, appId, apiKey); } @Override public QParser createParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) { - return new TraveltimeQueryParser(qstr, localParams, params, req, FetcherSingleton.INSTANCE.getFetcher(), cacheName); + return new TraveltimeQueryParser(qstr, localParams, params, req, ProtoFetcherSingleton.INSTANCE.getFetcher(), cacheName); } } diff --git a/8/src/main/java/com/traveltime/plugin/solr/cache/ExactRequestCache.java b/8/src/main/java/com/traveltime/plugin/solr/cache/ExactRequestCache.java index bfe6700..f1d90ce 100755 --- a/8/src/main/java/com/traveltime/plugin/solr/cache/ExactRequestCache.java +++ b/8/src/main/java/com/traveltime/plugin/solr/cache/ExactRequestCache.java @@ -2,7 +2,7 @@ import com.traveltime.plugin.solr.query.TraveltimeQueryParameters; -public class ExactRequestCache extends RequestCache { +public class ExactRequestCache extends RequestCache { private final Object[] lock = new Object[0]; @Override diff --git a/8/src/main/java/com/traveltime/plugin/solr/cache/ExactTimeFilterRequestCache.java b/8/src/main/java/com/traveltime/plugin/solr/cache/ExactTimeFilterRequestCache.java new file mode 100755 index 0000000..214d132 --- /dev/null +++ b/8/src/main/java/com/traveltime/plugin/solr/cache/ExactTimeFilterRequestCache.java @@ -0,0 +1,22 @@ +package com.traveltime.plugin.solr.cache; + +import com.traveltime.plugin.solr.query.timefilter.TimeFilterQueryParameters; + +public class ExactTimeFilterRequestCache extends RequestCache { + private final Object[] lock = new Object[0]; + + @Override + public TravelTimes getOrFresh(TimeFilterQueryParameters key) { + TravelTimes result = get(key); + if (result == null) { + synchronized (lock) { + result = get(key); + if (result == null) { + result = new BasicTravelTimes(); + put(key, result); + } + } + } + return result; + } +} diff --git a/8/src/main/java/com/traveltime/plugin/solr/cache/FuzzyRequestCache.java b/8/src/main/java/com/traveltime/plugin/solr/cache/FuzzyRequestCache.java index 52cba97..aa73a4c 100755 --- a/8/src/main/java/com/traveltime/plugin/solr/cache/FuzzyRequestCache.java +++ b/8/src/main/java/com/traveltime/plugin/solr/cache/FuzzyRequestCache.java @@ -5,7 +5,7 @@ import java.util.Map; -public class FuzzyRequestCache extends RequestCache { +public class FuzzyRequestCache extends RequestCache { private final Object[] lock = new Object[0]; private Map args; diff --git a/8/src/main/java/com/traveltime/plugin/solr/cache/FuzzyTimeFilterRequestCache.java b/8/src/main/java/com/traveltime/plugin/solr/cache/FuzzyTimeFilterRequestCache.java new file mode 100755 index 0000000..69c1a01 --- /dev/null +++ b/8/src/main/java/com/traveltime/plugin/solr/cache/FuzzyTimeFilterRequestCache.java @@ -0,0 +1,33 @@ +package com.traveltime.plugin.solr.cache; + +import com.traveltime.plugin.solr.query.timefilter.TimeFilterQueryParameters; +import org.apache.solr.search.CacheRegenerator; + +import java.util.Map; + +public class FuzzyTimeFilterRequestCache extends RequestCache { + private final Object[] lock = new Object[0]; + private Map args; + + @Override + public Object init(Map args, Object persistence, CacheRegenerator regenerator) { + this.args = args; + return super.init(args, persistence, regenerator); + } + + @Override + public TravelTimes getOrFresh(TimeFilterQueryParameters key) { + key = key.withField(null).withTravelTime(0); + TravelTimes result = get(key); + if (result == null) { + synchronized (lock) { + result = get(key); + if (result == null) { + result = new LRUTimes(args); + put(key, result); + } + } + } + return result; + } +} diff --git a/8/src/main/java/com/traveltime/plugin/solr/cache/RequestCache.java b/8/src/main/java/com/traveltime/plugin/solr/cache/RequestCache.java index 496ffd4..c1e6ca1 100755 --- a/8/src/main/java/com/traveltime/plugin/solr/cache/RequestCache.java +++ b/8/src/main/java/com/traveltime/plugin/solr/cache/RequestCache.java @@ -7,10 +7,10 @@ import java.util.Map; -public abstract class RequestCache extends FastLRUCache { +public abstract class RequestCache

extends FastLRUCache { public static String NAME = "traveltime"; - public abstract TravelTimes getOrFresh(TraveltimeQueryParameters key); + public abstract TravelTimes getOrFresh(P key); @Override public void init(Map args, CacheRegenerator ignored) { diff --git a/8/src/main/java/com/traveltime/plugin/solr/fetcher/Fetcher.java b/8/src/main/java/com/traveltime/plugin/solr/fetcher/Fetcher.java new file mode 100644 index 0000000..2f62e77 --- /dev/null +++ b/8/src/main/java/com/traveltime/plugin/solr/fetcher/Fetcher.java @@ -0,0 +1,10 @@ +package com.traveltime.plugin.solr.fetcher; + +import com.traveltime.sdk.dto.common.Coordinates; +import java.util.ArrayList; + +import java.util.List; + +public interface Fetcher { + List getTimes(Params parameters, ArrayList points); +} diff --git a/8/src/main/java/com/traveltime/plugin/solr/fetcher/JsonFetcher.java b/8/src/main/java/com/traveltime/plugin/solr/fetcher/JsonFetcher.java new file mode 100755 index 0000000..0ab968f --- /dev/null +++ b/8/src/main/java/com/traveltime/plugin/solr/fetcher/JsonFetcher.java @@ -0,0 +1,158 @@ +package com.traveltime.plugin.solr.fetcher; + +import com.google.common.collect.Iterables; +import com.traveltime.plugin.solr.query.timefilter.TimeFilterQueryParameters; +import com.traveltime.plugin.solr.util.Util; +import com.traveltime.sdk.TravelTimeSDK; +import com.traveltime.sdk.auth.TravelTimeCredentials; +import com.traveltime.sdk.dto.common.Coordinates; +import com.traveltime.sdk.dto.common.Location; +import com.traveltime.sdk.dto.common.Property; +import com.traveltime.sdk.dto.requests.TimeFilterRequest; +import com.traveltime.sdk.dto.requests.timefilter.ArrivalSearch; +import com.traveltime.sdk.dto.requests.timefilter.DepartureSearch; +import com.traveltime.sdk.dto.responses.TimeFilterResponse; +import com.traveltime.sdk.dto.responses.errors.IOError; +import com.traveltime.sdk.dto.responses.errors.ResponseError; +import com.traveltime.sdk.dto.responses.errors.TravelTimeError; +import lombok.val; +import okhttp3.OkHttpClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.StreamSupport; + +public class JsonFetcher implements Fetcher { + private final TravelTimeSDK api; + + private final int locationSizeLimit; + + private final Logger log = LoggerFactory.getLogger(JsonFetcher.class); + + private void logError(TravelTimeError left) { + if (left instanceof IOError) { + val ioerr = (IOError) left; + log.warn(ioerr.getMessage()); + log.warn( + Arrays.stream(ioerr.getCause().getStackTrace()) + .map(StackTraceElement::toString) + .reduce("", (a, b) -> a + "\n\t" + b) + ); + } else if (left instanceof ResponseError) { + val error = (ResponseError) left; + log.warn(error.getDescription()); + } + } + + public JsonFetcher(URI uri, String id, String key, int locationSizeLimit) { + val auth = TravelTimeCredentials.builder().appId(id).apiKey(key).build(); + val client = new OkHttpClient.Builder() + .connectTimeout(5, TimeUnit.MINUTES) + .callTimeout(5, TimeUnit.MINUTES) + .readTimeout(5, TimeUnit.MINUTES) + .build(); + val builder = TravelTimeSDK.builder().credentials(auth).client(client); + if(uri != null) { + builder.baseProtoUri(uri); + } + api = builder.build(); + this.locationSizeLimit = locationSizeLimit; + } + + private Integer[] extractTimes(TimeFilterResponse response, Integer[] travelTimes) { + val result = response.getResults().get(0); + + result.getLocations() + .forEach(location -> + travelTimes[Integer.parseInt(location.getId())] = location.getProperties().get(0).getTravelTime() + ); + + result.getUnreachable() + .forEach(unreachableId -> + travelTimes[Integer.parseInt(unreachableId)] = -1 + ); + + return travelTimes; + } + + public List getTimes(TimeFilterQueryParameters parameters, ArrayList points) { + + val locations = IntStream + .range(0, points.size()) + .mapToObj(i -> new Location(String.valueOf(i), points.get(i))) + .collect(Collectors.toList()); + + val groupedLocations = Iterables.partition(locations, locationSizeLimit); + + val requests = StreamSupport.stream(groupedLocations.spliterator(), true) + .map(locationGroup -> { + val requestBuilder = TimeFilterRequest.builder(); + + requestBuilder + .location(parameters.getLocation()) + .locations(locations); + + switch (parameters.getSearchType()) { + case ARRIVAL: + val arrivalSearchBuilder = ArrivalSearch + .builder() + .id("search") + .arrivalLocationId(parameters.getLocation().getId()) + .departureLocationIds(locations.stream().map(Location::getId).collect(Collectors.toList())) + .arrivalTime(parameters.getTime()) + .travelTime(parameters.getTravelTime()) + .properties(Collections.singletonList(Property.TRAVEL_TIME)) + .transportation(parameters.getTransportation()); + val arrivalSearch = parameters + .getRange() + .map(arrivalSearchBuilder::range) + .orElse(arrivalSearchBuilder) + .build(); + requestBuilder.arrivalSearch(arrivalSearch); + break; + case DEPARTURE: + val departureSearchBuilder = DepartureSearch + .builder() + .id("search") + .departureLocationId(parameters.getLocation().getId()) + .arrivalLocationIds(locations.stream().map(Location::getId).collect(Collectors.toList())) + .departureTime(parameters.getTime()) + .travelTime(parameters.getTravelTime()) + .properties(Collections.singletonList(Property.TRAVEL_TIME)) + .transportation(parameters.getTransportation()); + val departureSearch = parameters + .getRange() + .map(departureSearchBuilder::range) + .orElse(departureSearchBuilder) + .build(); + requestBuilder.departureSearch(departureSearch); + break; + } + + return requestBuilder.build(); + }); + + log.info(String.format("Fetching %d locations", points.size())); + + Integer[] resultArray = new Integer[locations.size()]; + + requests.map(request -> Util.time(log, () -> api.send(request))) + .forEach(result -> result.fold(err -> { + logError(err); + throw new RuntimeException(err.toString()); + }, + succ -> extractTimes(succ, resultArray)) + ); + + return Arrays.stream(resultArray).collect(Collectors.toList()); + } + +} diff --git a/8/src/main/java/com/traveltime/plugin/solr/fetcher/JsonFetcherSingleton.java b/8/src/main/java/com/traveltime/plugin/solr/fetcher/JsonFetcherSingleton.java new file mode 100755 index 0000000..ae4fd4d --- /dev/null +++ b/8/src/main/java/com/traveltime/plugin/solr/fetcher/JsonFetcherSingleton.java @@ -0,0 +1,23 @@ +package com.traveltime.plugin.solr.fetcher; + +import java.net.URI; + +public enum JsonFetcherSingleton { + INSTANCE; + + private JsonFetcher underlying = null; + private final Object[] lock = new Object[0]; + + public void init(URI uri, String id, String key, int locationSizeLimit) { + if(underlying != null) return; + synchronized (lock) { + if(underlying != null) return; + underlying = new JsonFetcher(uri, id, key, locationSizeLimit); + } + } + + public JsonFetcher getFetcher() { + return underlying; + } + +} diff --git a/8/src/main/java/com/traveltime/plugin/solr/ProtoFetcher.java b/8/src/main/java/com/traveltime/plugin/solr/fetcher/ProtoFetcher.java similarity index 79% rename from 8/src/main/java/com/traveltime/plugin/solr/ProtoFetcher.java rename to 8/src/main/java/com/traveltime/plugin/solr/fetcher/ProtoFetcher.java index a33701f..7746c03 100755 --- a/8/src/main/java/com/traveltime/plugin/solr/ProtoFetcher.java +++ b/8/src/main/java/com/traveltime/plugin/solr/fetcher/ProtoFetcher.java @@ -1,26 +1,26 @@ -package com.traveltime.plugin.solr; +package com.traveltime.plugin.solr.fetcher; +import com.traveltime.plugin.solr.query.TraveltimeQueryParameters; import com.traveltime.plugin.solr.util.Util; import com.traveltime.sdk.TravelTimeSDK; import com.traveltime.sdk.auth.TravelTimeCredentials; import com.traveltime.sdk.dto.common.Coordinates; import com.traveltime.sdk.dto.requests.TimeFilterFastProtoRequest; -import com.traveltime.sdk.dto.requests.proto.Country; import com.traveltime.sdk.dto.requests.proto.OneToMany; -import com.traveltime.sdk.dto.requests.proto.Transportation; import com.traveltime.sdk.dto.responses.TimeFilterFastProtoResponse; import com.traveltime.sdk.dto.responses.errors.IOError; import com.traveltime.sdk.dto.responses.errors.ResponseError; import com.traveltime.sdk.dto.responses.errors.TravelTimeError; import lombok.val; -import org.slf4j.LoggerFactory; import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.net.URI; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; -public class ProtoFetcher { +public class ProtoFetcher implements Fetcher { private final TravelTimeSDK api; private final Logger log = LoggerFactory.getLogger(ProtoFetcher.class); @@ -49,18 +49,19 @@ public ProtoFetcher(URI uri, String id, String key) { api = builder.build(); } - public List getTimes(Coordinates origin, List destinations, int limit, Transportation mode, Country country) { + @Override + public List getTimes(TraveltimeQueryParameters params, ArrayList destinations) { val fastProto = TimeFilterFastProtoRequest .builder() .oneToMany( OneToMany .builder() - .country(country) - .transportation(mode) - .originCoordinate(origin) + .country(params.getCountry()) + .transportation(params.getMode()) + .originCoordinate(params.getOrigin()) .destinationCoordinates(destinations) - .travelTime(limit) + .travelTime(params.getLimit()) .build() ) .build(); diff --git a/8/src/main/java/com/traveltime/plugin/solr/FetcherSingleton.java b/8/src/main/java/com/traveltime/plugin/solr/fetcher/ProtoFetcherSingleton.java similarity index 84% rename from 8/src/main/java/com/traveltime/plugin/solr/FetcherSingleton.java rename to 8/src/main/java/com/traveltime/plugin/solr/fetcher/ProtoFetcherSingleton.java index a9b28c7..72a398f 100755 --- a/8/src/main/java/com/traveltime/plugin/solr/FetcherSingleton.java +++ b/8/src/main/java/com/traveltime/plugin/solr/fetcher/ProtoFetcherSingleton.java @@ -1,8 +1,8 @@ -package com.traveltime.plugin.solr; +package com.traveltime.plugin.solr.fetcher; import java.net.URI; -public enum FetcherSingleton { +public enum ProtoFetcherSingleton { INSTANCE; private ProtoFetcher underlying = null; diff --git a/8/src/main/java/com/traveltime/plugin/solr/query/ParamSource.java b/8/src/main/java/com/traveltime/plugin/solr/query/ParamSource.java new file mode 100644 index 0000000..7886305 --- /dev/null +++ b/8/src/main/java/com/traveltime/plugin/solr/query/ParamSource.java @@ -0,0 +1,32 @@ +package com.traveltime.plugin.solr.query; + +import lombok.val; +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.search.SyntaxError; + +import java.util.Optional; + +public class ParamSource { + private final SolrParams[] params; + + public static final String PARAM_PREFIX = "traveltime_"; + + public ParamSource(SolrParams... params) { + this.params = params; + } + + public String getParam(String name) throws SyntaxError { + return getOptionalParam(name).orElseThrow(() -> new SyntaxError("missing " + name + " parameter for TravelTime request")); + } + + public Optional getOptionalParam(String name) { + String param; + for (val source : params) { + param = source.get(PARAM_PREFIX + name); + if (param != null) return Optional.of(param); + param = source.get(name); + if (param != null) return Optional.of(param); + } + return Optional.empty(); + } +} diff --git a/8/src/main/java/com/traveltime/plugin/solr/query/QueryParams.java b/8/src/main/java/com/traveltime/plugin/solr/query/QueryParams.java new file mode 100644 index 0000000..0364df6 --- /dev/null +++ b/8/src/main/java/com/traveltime/plugin/solr/query/QueryParams.java @@ -0,0 +1,6 @@ +package com.traveltime.plugin.solr.query; + +public interface QueryParams { + String getField(); + int getTravelTime(); +} diff --git a/8/src/main/java/com/traveltime/plugin/solr/query/TraveltimeDelegatingCollector.java b/8/src/main/java/com/traveltime/plugin/solr/query/TraveltimeDelegatingCollector.java index 13df013..ba5969d 100755 --- a/8/src/main/java/com/traveltime/plugin/solr/query/TraveltimeDelegatingCollector.java +++ b/8/src/main/java/com/traveltime/plugin/solr/query/TraveltimeDelegatingCollector.java @@ -1,10 +1,9 @@ package com.traveltime.plugin.solr.query; - -import com.traveltime.plugin.solr.ProtoFetcher; import com.traveltime.plugin.solr.cache.RequestCache; import com.traveltime.plugin.solr.cache.TravelTimes; import com.traveltime.plugin.solr.cache.UnprotectedTimes; +import com.traveltime.plugin.solr.fetcher.Fetcher; import com.traveltime.plugin.solr.util.Util; import com.traveltime.sdk.dto.common.Coordinates; import it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap; @@ -20,11 +19,12 @@ import org.apache.lucene.util.BitSetIterator; import org.apache.lucene.util.FixedBitSet; import org.apache.solr.search.DelegatingCollector; + import java.io.IOException; import java.util.ArrayList; import java.util.List; -public class TraveltimeDelegatingCollector extends DelegatingCollector { +public class TraveltimeDelegatingCollector extends DelegatingCollector { private final LeafReaderContext[] contexts; private final int[] contextBaseStart; private final int[] contextBaseEnd; @@ -32,17 +32,17 @@ public class TraveltimeDelegatingCollector extends DelegatingCollector { private final int maxDoc; private final Int2FloatOpenHashMap score; private final FixedBitSet collectedGlobalDocs; - private final TraveltimeQueryParameters params; + private final Params params; private final float scoreWeight; private final Int2ObjectOpenHashMap globalDoc2Coords; - private final ProtoFetcher fetcher; - private final RequestCache cache; + private final Fetcher fetcher; + private final RequestCache cache; private Object2IntOpenHashMap pointToTime; private SortedNumericDocValues coords; - public TraveltimeDelegatingCollector(int maxDoc, int segments, TraveltimeQueryParameters params, float scoreWeight, ProtoFetcher fetcher, RequestCache cache) { + public TraveltimeDelegatingCollector(int maxDoc, int segments, Params params, float scoreWeight, Fetcher fetcher, RequestCache cache) { this.maxDoc = maxDoc; this.contexts = new LeafReaderContext[segments]; this.contextBaseStart = new int[segments]; @@ -86,7 +86,7 @@ private Object2IntOpenHashMap computePointToTime(ObjectCollection destinations = new ArrayList<>(nonCachedSet); @@ -94,18 +94,12 @@ private Object2IntOpenHashMap computePointToTime(ObjectCollection(); } else { - times = fetcher.getTimes( - params.getOrigin(), - destinations, - params.getLimit(), - params.getMode(), - params.getCountry() - ); + times = fetcher.getTimes(params, destinations); } - cachedResults.putAll(params.getLimit(), destinations, times); + cachedResults.putAll(params.getTravelTime(), destinations, times); - return cachedResults.mapToTimes(params.getLimit(), coords); + return cachedResults.mapToTimes(params.getTravelTime(), coords); } @Override @@ -154,8 +148,8 @@ public int docID() { @Override public float score() { - int limit = params.getLimit(); - int time = pointToTime.getOrDefault(globalDoc2Coords.get(docID()), params.getLimit() + 1); + int limit = params.getTravelTime(); + int time = pointToTime.getOrDefault(globalDoc2Coords.get(docID()), params.getTravelTime() + 1); float ttScore = (float) (limit - time + 1) / (limit + 1); return (1f - scoreWeight) * score.get(docID()) + scoreWeight * ttScore; } diff --git a/8/src/main/java/com/traveltime/plugin/solr/query/TraveltimeQueryParameters.java b/8/src/main/java/com/traveltime/plugin/solr/query/TraveltimeQueryParameters.java index 0b5b142..b8cccf0 100755 --- a/8/src/main/java/com/traveltime/plugin/solr/query/TraveltimeQueryParameters.java +++ b/8/src/main/java/com/traveltime/plugin/solr/query/TraveltimeQueryParameters.java @@ -15,13 +15,18 @@ import java.util.function.Function; @Data -public class TraveltimeQueryParameters { +public class TraveltimeQueryParameters implements QueryParams { private final String field; private final Coordinates origin; private final int limit; @With private final Transportation mode; @With private final Country country; + @Override + public int getTravelTime() { + return limit; + } + public static final String FIELD = "field"; public static final String ORIGIN = "origin"; public static final String MODE = "mode"; @@ -37,28 +42,25 @@ private static T findByNameOrError(String what, String name, Function fetcher; private final String cacheName; - public TraveltimeQueryParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req, ProtoFetcher fetcher, String cacheName) { + public TraveltimeQueryParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req, Fetcher fetcher, String cacheName) { super(qstr, localParams, params, req); this.fetcher = fetcher; this.cacheName = cacheName; } - private String getBestParam(String name) throws SyntaxError { - String param; - if (localParams != null) { - param = localParams.get(name); - if (param != null) return param; - param = localParams.get(PARAM_PREFIX + name); - if (param != null) return param; - } - param = params.get(PARAM_PREFIX + name); - if (param != null) return param; - param = params.get(name); - if (param != null) return param; - throw new SyntaxError("missing " + name + " parameter for TravelTime request"); - } - @Override - public TraveltimeSearchQuery parse() throws SyntaxError { + public TraveltimeSearchQuery parse() throws SyntaxError { + ParamSource paramSource = new ParamSource(localParams, params); float weight; try { - weight = Float.parseFloat(getBestParam(WEIGHT)); + weight = Float.parseFloat(paramSource.getParam(WEIGHT)); } catch (SyntaxError ignored) { weight = 0f; } catch (NumberFormatException e) { @@ -50,15 +35,8 @@ public TraveltimeSearchQuery parse() throws SyntaxError { throw new SyntaxError("Traveltime weight must be between 0 and 1"); } - val params = TraveltimeQueryParameters.fromStrings( - req.getSchema(), - getBestParam(TraveltimeQueryParameters.FIELD), - getBestParam(TraveltimeQueryParameters.ORIGIN), - getBestParam(TraveltimeQueryParameters.LIMIT), - getBestParam(TraveltimeQueryParameters.MODE), - getBestParam(TraveltimeQueryParameters.COUNTRY) - ); - return new TraveltimeSearchQuery(params, weight, fetcher, cacheName); + val params = TraveltimeQueryParameters.parse(req.getSchema(), paramSource); + return new TraveltimeSearchQuery<>(params, weight, fetcher, cacheName); } } diff --git a/8/src/main/java/com/traveltime/plugin/solr/query/TraveltimeSearchQuery.java b/8/src/main/java/com/traveltime/plugin/solr/query/TraveltimeSearchQuery.java index af91bbe..893a993 100755 --- a/8/src/main/java/com/traveltime/plugin/solr/query/TraveltimeSearchQuery.java +++ b/8/src/main/java/com/traveltime/plugin/solr/query/TraveltimeSearchQuery.java @@ -1,6 +1,6 @@ package com.traveltime.plugin.solr.query; -import com.traveltime.plugin.solr.ProtoFetcher; +import com.traveltime.plugin.solr.fetcher.Fetcher; import com.traveltime.plugin.solr.cache.RequestCache; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; @@ -12,10 +12,10 @@ @AllArgsConstructor @EqualsAndHashCode(callSuper = false) -public class TraveltimeSearchQuery extends ExtendedQueryBase implements PostFilter { - private final TraveltimeQueryParameters params; +public class TraveltimeSearchQuery extends ExtendedQueryBase implements PostFilter { + private final Params params; private final float weight; - private final ProtoFetcher fetcher; + private final Fetcher fetcher; private final String cacheName; @Override @@ -26,10 +26,10 @@ public String toString(String field) { @Override public DelegatingCollector getFilterCollector(IndexSearcher indexSearcher) { SolrIndexSearcher searcher = (SolrIndexSearcher)indexSearcher; - RequestCache cache = (RequestCache) searcher.getCache(cacheName); + RequestCache cache = (RequestCache) searcher.getCache(cacheName); int maxDoc = searcher.maxDoc(); int leafCount = searcher.getTopReaderContext().leaves().size(); - return new TraveltimeDelegatingCollector(maxDoc, leafCount, params, weight, fetcher, cache); + return new TraveltimeDelegatingCollector<>(maxDoc, leafCount, params, weight, fetcher, cache); } @Override diff --git a/8/src/main/java/com/traveltime/plugin/solr/query/TraveltimeValueSource.java b/8/src/main/java/com/traveltime/plugin/solr/query/TraveltimeValueSource.java index dba7ab4..9c35eb7 100755 --- a/8/src/main/java/com/traveltime/plugin/solr/query/TraveltimeValueSource.java +++ b/8/src/main/java/com/traveltime/plugin/solr/query/TraveltimeValueSource.java @@ -18,8 +18,8 @@ @RequiredArgsConstructor @EqualsAndHashCode(callSuper = false) -public class TraveltimeValueSource extends ValueSource { - private final TraveltimeQueryParameters params; +public class TraveltimeValueSource extends ValueSource { + private final Params params; @EqualsAndHashCode.Exclude private final TravelTimes cache; diff --git a/8/src/main/java/com/traveltime/plugin/solr/query/TraveltimeValueSourceParser.java b/8/src/main/java/com/traveltime/plugin/solr/query/TraveltimeValueSourceParser.java index ef451e0..4325637 100755 --- a/8/src/main/java/com/traveltime/plugin/solr/query/TraveltimeValueSourceParser.java +++ b/8/src/main/java/com/traveltime/plugin/solr/query/TraveltimeValueSourceParser.java @@ -4,15 +4,12 @@ import lombok.val; import org.apache.lucene.queries.function.ValueSource; import org.apache.solr.common.SolrException; -import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.util.NamedList; import org.apache.solr.request.SolrQueryRequest; import org.apache.solr.search.FunctionQParser; import org.apache.solr.search.SyntaxError; import org.apache.solr.search.ValueSourceParser; -import static com.traveltime.plugin.solr.TraveltimeQParserPlugin.PARAM_PREFIX; - public class TraveltimeValueSourceParser extends ValueSourceParser { private String cacheName = RequestCache.NAME; @@ -23,19 +20,10 @@ public void init(NamedList args) { if(cache != null) cacheName = cache.toString(); } - private String getParam(SolrParams params, String name) throws SyntaxError { - String param = params.get(PARAM_PREFIX + name); - if(param != null) return param; - param = params.get(name); - if(param != null) return param; - throw new SyntaxError("missing " + name + " parameter for TravelTime value source"); - } - - @Override public ValueSource parse(FunctionQParser fp) throws SyntaxError { SolrQueryRequest req = fp.getReq(); - RequestCache cache = (RequestCache) req.getSearcher().getCache(cacheName); + RequestCache cache = (RequestCache) req.getSearcher().getCache(cacheName); if (cache == null) { throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, @@ -43,18 +31,7 @@ public ValueSource parse(FunctionQParser fp) throws SyntaxError { ); } - SolrParams params = fp.getParams(); - - - val queryParameters = TraveltimeQueryParameters.fromStrings( - req.getSchema(), - getParam(params, TraveltimeQueryParameters.FIELD), - getParam(params, TraveltimeQueryParameters.ORIGIN), - getParam(params, TraveltimeQueryParameters.LIMIT), - getParam(params, TraveltimeQueryParameters.MODE), - getParam(params, TraveltimeQueryParameters.COUNTRY) - ); - - return new TraveltimeValueSource(queryParameters, cache.getOrFresh(queryParameters)); + val queryParameters = TraveltimeQueryParameters.parse(req.getSchema(), new ParamSource(fp.getParams())); + return new TraveltimeValueSource<>(queryParameters, cache.getOrFresh(queryParameters)); } } diff --git a/8/src/main/java/com/traveltime/plugin/solr/query/timefilter/TimeFilterQueryParameters.java b/8/src/main/java/com/traveltime/plugin/solr/query/timefilter/TimeFilterQueryParameters.java new file mode 100755 index 0000000..856113d --- /dev/null +++ b/8/src/main/java/com/traveltime/plugin/solr/query/timefilter/TimeFilterQueryParameters.java @@ -0,0 +1,136 @@ +package com.traveltime.plugin.solr.query.timefilter; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.traveltime.plugin.solr.query.ParamSource; +import com.traveltime.plugin.solr.query.QueryParams; +import com.traveltime.sdk.dto.common.Coordinates; +import com.traveltime.sdk.dto.common.FullRange; +import com.traveltime.sdk.dto.common.Location; +import com.traveltime.sdk.dto.common.Property; +import com.traveltime.sdk.dto.common.transportation.Transportation; +import com.traveltime.sdk.dto.responses.errors.TravelTimeError; +import com.traveltime.sdk.utils.JsonUtils; +import io.vavr.control.Either; +import lombok.Data; +import lombok.With; +import lombok.val; +import org.apache.solr.schema.IndexSchema; +import org.apache.solr.schema.LatLonPointSpatialField; +import org.apache.solr.search.SyntaxError; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +@Data +public class TimeFilterQueryParameters implements QueryParams { + @With private final String field; + private final Location location; + private final Instant time; + @With private final int travelTime; + private final Transportation transportation; + private final Optional range; + private final SearchType searchType; + + public enum SearchType { + ARRIVAL, DEPARTURE + } + + public static final String FIELD = "field"; + + public static final String DEPARTURE_LOCATION = "departure_location"; + public static final String DEPARTURE_TIME = "departure_time"; + + public static final String ARRIVAL_LOCATION = "arrival_location"; + public static final String ARRIVAL_TIME = "arrival_time"; + + public static final String TRAVEL_TIME = "travel_time"; + public static final String TRANSPORTATION = "transportation"; + public static final String RANGE = "range"; + + private static T getOrThrow(Either either) throws SyntaxError { + if(either.isLeft()) { + throw new SyntaxError(either.getLeft().getMessage()); + } else { + return either.get(); + } + } + + public static TimeFilterQueryParameters parse( + IndexSchema schema, + ParamSource paramSource + ) throws SyntaxError { + + String field = paramSource.getParam(TimeFilterQueryParameters.FIELD); + if (!(schema.getField(field).getType() instanceof LatLonPointSpatialField)) { + throw new SyntaxError("field[" + field + "] is not a LatLonPointSpatialField"); + } + + val arrivalTime = paramSource.getOptionalParam(TimeFilterQueryParameters.ARRIVAL_TIME); + val arrivalLocation = paramSource.getOptionalParam(TimeFilterQueryParameters.ARRIVAL_LOCATION); + + val departureTime = paramSource.getOptionalParam(TimeFilterQueryParameters.DEPARTURE_TIME); + val departureLocation = paramSource.getOptionalParam(TimeFilterQueryParameters.DEPARTURE_LOCATION); + + if(arrivalTime.isPresent() != arrivalLocation.isPresent()) { + throw new SyntaxError(String.format("Only one of [%s, %s] was defined. Either both must be defined or none.", TimeFilterQueryParameters.ARRIVAL_TIME, TimeFilterQueryParameters.ARRIVAL_LOCATION)); + } + + if(departureTime.isPresent() != departureLocation.isPresent()) { + throw new SyntaxError(String.format("Only one of [%s, %s] was defined. Either both must be defined or none.", TimeFilterQueryParameters.DEPARTURE_TIME, TimeFilterQueryParameters.DEPARTURE_LOCATION)); + } + + if(arrivalTime.isPresent() == departureTime.isPresent()) { + throw new SyntaxError("You must provide exactly one of the parameter sets of arrival time/location or departure time/location, but not both or neither."); + } + + val transportation = JsonUtils.fromJson(paramSource.getParam(TimeFilterQueryParameters.TRANSPORTATION), Transportation.class); + + val range = paramSource.getOptionalParam(TimeFilterQueryParameters.RANGE).map(r -> JsonUtils.fromJson(r, FullRange.class)); + Optional rangeOptional = range.isPresent() ? Optional.of(getOrThrow(range.get())) : Optional.empty(); + + int travelTime; + try { + travelTime = Integer.parseInt(paramSource.getParam(TimeFilterQueryParameters.TRAVEL_TIME)); + } catch (NumberFormatException e) { + throw new SyntaxError("Couldn't parse traveltime limit as an integer"); + } + if (travelTime <= 0) { + throw new SyntaxError("traveltime limit must be > 0"); + } + + TimeFilterQueryParameters queryParams; + if (arrivalTime.isPresent()) { + val locationCoords = JsonUtils.fromJson(paramSource.getParam(TimeFilterQueryParameters.ARRIVAL_LOCATION), Coordinates.class); + val time = Instant.parse(paramSource.getParam(TimeFilterQueryParameters.ARRIVAL_TIME)); + + val location = new Location("location", getOrThrow(locationCoords)); + + queryParams = new TimeFilterQueryParameters( + field, + location, + time, + travelTime, + getOrThrow(transportation), + rangeOptional, + SearchType.ARRIVAL + ); + } else { + val locationCoords = JsonUtils.fromJson(paramSource.getParam(TimeFilterQueryParameters.DEPARTURE_LOCATION), Coordinates.class); + val time = Instant.parse(paramSource.getParam(TimeFilterQueryParameters.DEPARTURE_TIME)); + + val location = new Location("location", getOrThrow(locationCoords)); + + queryParams = new TimeFilterQueryParameters( + field, + location, + time, + travelTime, + getOrThrow(transportation), + rangeOptional, + SearchType.DEPARTURE + ); + } + return queryParams; + } +} diff --git a/8/src/main/java/com/traveltime/plugin/solr/query/timefilter/TimeFilterQueryParser.java b/8/src/main/java/com/traveltime/plugin/solr/query/timefilter/TimeFilterQueryParser.java new file mode 100755 index 0000000..dbe0e23 --- /dev/null +++ b/8/src/main/java/com/traveltime/plugin/solr/query/timefilter/TimeFilterQueryParser.java @@ -0,0 +1,44 @@ +package com.traveltime.plugin.solr.query.timefilter; + +import com.traveltime.plugin.solr.fetcher.Fetcher; +import com.traveltime.plugin.solr.fetcher.JsonFetcher; +import com.traveltime.plugin.solr.query.ParamSource; +import com.traveltime.plugin.solr.query.TraveltimeSearchQuery; +import lombok.val; +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.search.QParser; +import org.apache.solr.search.SyntaxError; + +public class TimeFilterQueryParser extends QParser { + private static final String WEIGHT = "weight"; + + private final Fetcher fetcher; + private final String cacheName; + + public TimeFilterQueryParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req, Fetcher fetcher, String cacheName) { + super(qstr, localParams, params, req); + this.fetcher = fetcher; + this.cacheName = cacheName; + } + + @Override + public TraveltimeSearchQuery parse() throws SyntaxError { + ParamSource paramSource = new ParamSource(localParams, params); + float weight; + try { + weight = Float.parseFloat(paramSource.getParam(WEIGHT)); + } catch (SyntaxError ignored) { + weight = 0f; + } catch (NumberFormatException e) { + throw new SyntaxError("Couldn't parse traveltime weight as a float"); + } + if(weight < 0 || weight > 1) { + throw new SyntaxError("Traveltime weight must be between 0 and 1"); + } + + val params = TimeFilterQueryParameters.parse(req.getSchema(), paramSource); + return new TraveltimeSearchQuery<>(params, weight, fetcher, cacheName); + } + +} diff --git a/8/src/main/java/com/traveltime/plugin/solr/query/timefilter/TimeFilterValueSourceParser.java b/8/src/main/java/com/traveltime/plugin/solr/query/timefilter/TimeFilterValueSourceParser.java new file mode 100755 index 0000000..b3f0875 --- /dev/null +++ b/8/src/main/java/com/traveltime/plugin/solr/query/timefilter/TimeFilterValueSourceParser.java @@ -0,0 +1,38 @@ +package com.traveltime.plugin.solr.query.timefilter; + +import com.traveltime.plugin.solr.cache.RequestCache; +import com.traveltime.plugin.solr.query.ParamSource; +import com.traveltime.plugin.solr.query.TraveltimeValueSource; +import org.apache.lucene.queries.function.ValueSource; +import org.apache.solr.common.SolrException; +import org.apache.solr.common.util.NamedList; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.search.FunctionQParser; +import org.apache.solr.search.SyntaxError; +import org.apache.solr.search.ValueSourceParser; + +public class TimeFilterValueSourceParser extends ValueSourceParser { + private String cacheName = RequestCache.NAME; + + @Override + public void init(NamedList args) { + super.init(args); + Object cache = args.get("cache"); + if(cache != null) cacheName = cache.toString(); + } + + @Override + public ValueSource parse(FunctionQParser fp) throws SyntaxError { + SolrQueryRequest req = fp.getReq(); + RequestCache cache = (RequestCache) req.getSearcher().getCache(cacheName); + if (cache == null) { + throw new SolrException( + SolrException.ErrorCode.BAD_REQUEST, + "No request cache configured." + ); + } + + TimeFilterQueryParameters queryParams = TimeFilterQueryParameters.parse(req.getSchema(), new ParamSource(fp.getParams())); + return new TraveltimeValueSource<>(queryParams, cache.getOrFresh(queryParams)); + } +} diff --git a/README.md b/README.md index b19b585..d873986 100755 --- a/README.md +++ b/README.md @@ -5,11 +5,14 @@ Plugin for Solr that allows users to filter locations using the Traveltime API. This is a standard Solr plugin. The plugin jar must be copied into the [solr lib directory](https://solr.apache.org/guide/8_4/libs.html#lib-directories) -To use the plugin you **must** add a `queryParser` with the class `com.traveltime.plugin.solr.TraveltimeQParserPlugin`. +To use the plugin you **must** add a `queryParser` with the class `com.traveltime.plugin.solr.TraveltimeQParserPlugin` or +`com.traveltime.plugin.solr.TimeFilterQParserPlugin` (currently only available for solar version 8). This query parser has two mandatory string configuration options: - `app_id`: this is you API app id. - `api_key`: this is the api key that corresponds to the app id. +The `TimeFilterQParserPlugin` has an optional integer field `location_limit` which represents the maximum amount of locations +that can be sent in a single request. Defaults to 2000, only increase this parameter if you API plan supports larger requests. ```xml your_app_id_here @@ -21,16 +24,24 @@ To display the travel times returned by the TravelTime API you must configure tw ```xml ``` +or, if using `TimeFilterQParserPlugin` +```xml + +``` and a `cache`: ```xml +or + ``` or ```xml +or + ``` -## Querying data +## Querying data using proto time-filter/fast requests The traveltime query can only be used as a filter query, and can only be used with fields that are indexed as `location`. The query accepts the following (mandatory) configuration options: - `origin`: the point from which travel time will be measured. The accepted formats are: @@ -42,7 +53,40 @@ The query accepts the following (mandatory) configuration options: - `country`: Country that the `origin` is in. Currently may only be one of: `uk`, `nl`, `at`, `be`, `de`, `fr`, `ie`, `lt`. The configuration options may be passed as local query parameters: `?fq={!traveltime origin=51.53,-0.15 field=coords limit=7200 mode=pt country=uk}`, or as raw query parameters prefixed with `"traveltime_"`: `?fq={!traveltime}&traveltime_origin=51.53,-0.15&traveltime_field=coords&traveltime_limit=7200&traveltime_mode=pt&traveltime_country=uk}`. -If a parameter is specified in both ways, the local parameter takes precedence. +If a parameter is specified in both ways, the local parameter takes precedence. + +## Querying data using json time-filter requests +The query accepts the following configuration options: +- `field`: the document field that will be used as the destination in the Traveltime query. +- `travel_time`: the travel time limit in seconds. Must be non-negative. +- For arrival searches: + - `arrival_time`: arrival time in ISO8601 + - `arrival_location`: string containing a JSON object with `lat` and `lng` keys describing the coordinates + of the arrival location +- For departure searches: + - `departure_time`: departure time in ISO8601 + - `departure_location`: string containing a JSON object with `lat` and `lng` keys describing the coordinates + of the departure location +- `transportation`: a string containing a JSON object describing the transportation mode as defined by the Traveltime API: + https://docs.traveltime.com/api/reference/travel-time-distance-matrix#departure_searches-transportation +- (optional) `range`: : a string containing a JSON object describing the range search as defined by the Traveltime API: + https://docs.traveltime.com/api/reference/travel-time-distance-matrix#departure_searches-range + +An example query using `curl`: +```shell +curl + --data-urlencode 'q=*:*' + --data-urlencode 'traveltime_field=coords' + --data-urlencode 'traveltime_travel_time=3000' + --data-urlencode 'traveltime_arrival_location={"lat":51.536067,"lng":-0.153596}' + --data-urlencode 'traveltime_arrival_time=2022-12-19T15:00:00Z' + --data-urlencode 'transportation={"type":"public_transport"}' + --data-urlencode fq="{!traveltime weight=1}" + $URL +``` + +The configuration options may be passed as local query parameters, or as raw query parameters prefixed with `"traveltime_"`. +If a parameter is specified in both ways, the local parameter takes precedence. ## Displaying travel times diff --git a/build.gradle b/build.gradle index 645d6ff..82e165d 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,8 @@ configure(subprojects) { repositories { mavenCentral() maven { - url "https://maven.restlet.org/" + url "http://maven.restlet.org/" + allowInsecureProtocol = true } } @@ -44,7 +45,7 @@ configure(subprojects) { } dependencies { - implementation 'com.traveltime:traveltime-sdk-java:1.1.17' + implementation 'com.traveltime:traveltime-sdk-java:1.3.1' implementation 'io.vavr:vavr:0.10.4' implementation group: 'it.unimi.dsi', name: 'fastutil', version: '8.5.6' }