| package org.apache.solr.schema; |
| |
| /* |
| * 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. |
| */ |
| |
| import com.google.common.base.Throwables; |
| import com.google.common.cache.Cache; |
| import com.google.common.cache.CacheBuilder; |
| import com.spatial4j.core.context.SpatialContext; |
| import com.spatial4j.core.context.SpatialContextFactory; |
| import com.spatial4j.core.distance.DistanceUtils; |
| import com.spatial4j.core.io.LegacyShapeReadWriterFormat; |
| import com.spatial4j.core.shape.Point; |
| import com.spatial4j.core.shape.Rectangle; |
| import com.spatial4j.core.shape.Shape; |
| import org.apache.lucene.document.Field; |
| import org.apache.lucene.document.StoredField; |
| import org.apache.lucene.index.StorableField; |
| import org.apache.lucene.queries.function.FunctionQuery; |
| import org.apache.lucene.queries.function.ValueSource; |
| import org.apache.lucene.search.Filter; |
| import org.apache.lucene.search.FilteredQuery; |
| import org.apache.lucene.search.Query; |
| import org.apache.lucene.search.SortField; |
| import org.apache.lucene.spatial.SpatialStrategy; |
| import org.apache.lucene.spatial.query.SpatialArgs; |
| import org.apache.lucene.spatial.query.SpatialArgsParser; |
| import org.apache.lucene.spatial.query.SpatialOperation; |
| import org.apache.lucene.uninverting.UninvertingReader.Type; |
| import org.apache.solr.common.SolrException; |
| import org.apache.solr.common.params.SolrParams; |
| import org.apache.solr.response.TextResponseWriter; |
| import org.apache.solr.search.QParser; |
| import org.apache.solr.search.SpatialOptions; |
| import org.apache.solr.util.MapListener; |
| import org.apache.solr.util.SpatialUtils; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.io.IOException; |
| import java.text.ParseException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.concurrent.Callable; |
| import java.util.concurrent.ExecutionException; |
| |
| /** |
| * Abstract base class for Solr FieldTypes based on a Lucene 4 {@link SpatialStrategy}. |
| * |
| * @lucene.experimental |
| */ |
| public abstract class AbstractSpatialFieldType<T extends SpatialStrategy> extends FieldType implements SpatialQueryable { |
| |
| /** A local-param with one of "none" (default), "distance", or "recipDistance". */ |
| public static final String SCORE_PARAM = "score"; |
| /** A local-param boolean that can be set to false to only return the |
| * FunctionQuery (score), and thus not do filtering. |
| */ |
| public static final String FILTER_PARAM = "filter"; |
| |
| protected final Logger log = LoggerFactory.getLogger( getClass() ); |
| |
| protected SpatialContext ctx; |
| protected SpatialArgsParser argsParser; |
| |
| private final Cache<String, T> fieldStrategyCache = CacheBuilder.newBuilder().build(); |
| |
| @Override |
| protected void init(IndexSchema schema, Map<String, String> args) { |
| super.init(schema, args); |
| |
| String units = args.remove("units"); |
| if (!"degrees".equals(units)) |
| throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, |
| "Must specify units=\"degrees\" on field types with class "+getClass().getSimpleName()); |
| |
| //replace legacy rect format with ENVELOPE |
| String wbStr = args.get("worldBounds"); |
| if (wbStr != null && !wbStr.toUpperCase(Locale.ROOT).startsWith("ENVELOPE")) { |
| log.warn("Using old worldBounds format? Should use ENVELOPE(xMin, xMax, yMax, yMin)."); |
| String[] parts = wbStr.split(" ");//"xMin yMin xMax yMax" |
| if (parts.length == 4) { |
| args.put("worldBounds", |
| "ENVELOPE(" + parts[0] + ", " + parts[2] + ", " + parts[3] + ", " + parts[1] + ")"); |
| } //else likely eventual exception |
| } |
| |
| //Solr expects us to remove the parameters we've used. |
| MapListener<String, String> argsWrap = new MapListener<>(args); |
| ctx = SpatialContextFactory.makeSpatialContext(argsWrap, schema.getResourceLoader().getClassLoader()); |
| args.keySet().removeAll(argsWrap.getSeenKeys()); |
| |
| argsParser = newSpatialArgsParser(); |
| } |
| |
| protected SpatialArgsParser newSpatialArgsParser() { |
| return new SpatialArgsParser() { |
| @Override |
| protected Shape parseShape(String str, SpatialContext ctx) throws ParseException { |
| return AbstractSpatialFieldType.this.parseShape(str); |
| } |
| }; |
| } |
| |
| //-------------------------------------------------------------- |
| // Indexing |
| //-------------------------------------------------------------- |
| |
| @Override |
| public final Field createField(SchemaField field, Object val, float boost) { |
| throw new IllegalStateException("instead call createFields() because isPolyField() is true"); |
| } |
| |
| @Override |
| public Type getUninversionType(SchemaField sf) { |
| return null; |
| } |
| |
| @Override |
| public List<StorableField> createFields(SchemaField field, Object val, float boost) { |
| String shapeStr = null; |
| Shape shape = null; |
| if (val instanceof Shape) { |
| shape = ((Shape) val); |
| } else { |
| shapeStr = val.toString(); |
| shape = parseShape(shapeStr); |
| } |
| if (shape == null) { |
| log.debug("Field {}: null shape for input: {}", field, val); |
| return Collections.emptyList(); |
| } |
| |
| List<StorableField> result = new ArrayList<>(); |
| if (field.indexed()) { |
| T strategy = getStrategy(field.getName()); |
| result.addAll(Arrays.asList(strategy.createIndexableFields(shape))); |
| } |
| |
| if (field.stored()) { |
| if (shapeStr == null) |
| shapeStr = shapeToString(shape); |
| result.add(new StoredField(field.getName(), shapeStr)); |
| } |
| |
| return result; |
| } |
| |
| protected Shape parseShape(String str) { |
| if (str.length() == 0) |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "empty string shape"); |
| //In Solr trunk we only support "lat, lon" (or x y) as an additional format; in v4.0 we do the |
| // weird Circle & Rect formats too (Spatial4j LegacyShapeReadWriterFormat). |
| try { |
| Shape shape = LegacyShapeReadWriterFormat.readShapeOrNull(str, ctx); |
| if (shape != null) |
| return shape; |
| return ctx.readShapeFromWkt(str); |
| } catch (Exception e) { |
| String message = e.getMessage(); |
| if (!message.contains(str)) |
| message = "Couldn't parse shape '" + str + "' because: " + message; |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, message, e); |
| } |
| } |
| |
| /** |
| * Returns a String version of a shape to be used for the stored value. This method in Solr is only called if for some |
| * reason a Shape object is passed to the field type (perhaps via a custom UpdateRequestProcessor), |
| * *and* the field is marked as stored. <em>The default implementation throws an exception.</em> |
| * <p/> |
| * Spatial4j 0.4 is probably the last release to support SpatialContext.toString(shape) but it's deprecated with no |
| * planned replacement. Shapes do have a toString() method but they are generally internal/diagnostic and not |
| * standard WKT. |
| * The solution is subclassing and calling ctx.toString(shape) or directly using LegacyShapeReadWriterFormat or |
| * passing in some sort of custom wrapped shape that holds a reference to a String or can generate it. |
| */ |
| protected String shapeToString(Shape shape) { |
| // return ctx.toString(shape); |
| throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, |
| "Getting a String from a Shape is no longer possible. See javadocs for commentary."); |
| } |
| |
| /** Called from {@link #getStrategy(String)} upon first use by fieldName. } */ |
| protected abstract T newSpatialStrategy(String fieldName); |
| |
| @Override |
| public final boolean isPolyField() { |
| return true; |
| } |
| |
| //-------------------------------------------------------------- |
| // Query Support |
| //-------------------------------------------------------------- |
| |
| /** |
| * Implemented for compatibility with geofilt & bbox query parsers: |
| * {@link SpatialQueryable}. |
| */ |
| @Override |
| public Query createSpatialQuery(QParser parser, SpatialOptions options) { |
| Point pt = SpatialUtils.parsePointSolrException(options.pointStr, ctx); |
| |
| double distDeg = DistanceUtils.dist2Degrees(options.distance, options.radius); |
| |
| Shape shape = ctx.makeCircle(pt, distDeg); |
| if (options.bbox) |
| shape = shape.getBoundingBox(); |
| |
| SpatialArgs spatialArgs = new SpatialArgs(SpatialOperation.Intersects, shape); |
| return getQueryFromSpatialArgs(parser, options.field, spatialArgs); |
| } |
| |
| @Override |
| public Query getRangeQuery(QParser parser, SchemaField field, String part1, String part2, boolean minInclusive, boolean maxInclusive) { |
| if (!minInclusive || !maxInclusive) |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Both sides of spatial range query must be inclusive: " + field.getName()); |
| Point p1 = SpatialUtils.parsePointSolrException(part1, ctx); |
| Point p2 = SpatialUtils.parsePointSolrException(part2, ctx); |
| |
| Rectangle bbox = ctx.makeRectangle(p1, p2); |
| SpatialArgs spatialArgs = new SpatialArgs(SpatialOperation.Intersects, bbox); |
| return getQueryFromSpatialArgs(parser, field, spatialArgs);//won't score by default |
| } |
| |
| @Override |
| public ValueSource getValueSource(SchemaField field, QParser parser) { |
| //This is different from Solr 3 LatLonType's approach which uses the MultiValueSource concept to directly expose |
| // the x & y pair of FieldCache value sources. |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, |
| "A ValueSource isn't directly available from this field. Instead try a query using the distance as the score."); |
| } |
| |
| @Override |
| public Query getFieldQuery(QParser parser, SchemaField field, String externalVal) { |
| return getQueryFromSpatialArgs(parser, field, parseSpatialArgs(externalVal)); |
| } |
| |
| protected SpatialArgs parseSpatialArgs(String externalVal) { |
| try { |
| return argsParser.parse(externalVal, ctx); |
| } catch (SolrException e) { |
| throw e; |
| } catch (Exception e) { |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e); |
| } |
| } |
| |
| private Query getQueryFromSpatialArgs(QParser parser, SchemaField field, SpatialArgs spatialArgs) { |
| T strategy = getStrategy(field.getName()); |
| |
| SolrParams localParams = parser.getLocalParams(); |
| String score = (localParams == null ? null : localParams.get(SCORE_PARAM)); |
| if (score == null || "none".equals(score) || "".equals(score)) { |
| //FYI Solr FieldType doesn't have a getFilter(). We'll always grab |
| // getQuery() but it's possible a strategy has a more efficient getFilter |
| // that could be wrapped -- no way to know. |
| //See SOLR-2883 needScore |
| return strategy.makeQuery(spatialArgs); //ConstantScoreQuery |
| } |
| |
| //We get the valueSource for the score then the filter and combine them. |
| ValueSource valueSource; |
| if ("distance".equals(score)) { |
| double multiplier = 1.0;//TODO support units=kilometers |
| valueSource = strategy.makeDistanceValueSource(spatialArgs.getShape().getCenter(), multiplier); |
| } else if ("recipDistance".equals(score)) { |
| valueSource = strategy.makeRecipDistanceValueSource(spatialArgs.getShape()); |
| } else { |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "'score' local-param must be one of 'none', 'distance', or 'recipDistance'"); |
| } |
| FunctionQuery functionQuery = new FunctionQuery(valueSource); |
| |
| if (localParams != null && !localParams.getBool(FILTER_PARAM, true)) |
| return functionQuery; |
| |
| Filter filter = strategy.makeFilter(spatialArgs); |
| return new FilteredQuery(functionQuery, filter); |
| } |
| |
| /** |
| * Gets the cached strategy for this field, creating it if necessary |
| * via {@link #newSpatialStrategy(String)}. |
| * @param fieldName Mandatory reference to the field name |
| * @return Non-null. |
| */ |
| public T getStrategy(final String fieldName) { |
| try { |
| return fieldStrategyCache.get(fieldName, new Callable<T>() { |
| @Override |
| public T call() throws Exception { |
| return newSpatialStrategy(fieldName); |
| } |
| }); |
| } catch (ExecutionException e) { |
| throw Throwables.propagate(e.getCause()); |
| } |
| } |
| |
| @Override |
| public void write(TextResponseWriter writer, String name, StorableField f) throws IOException { |
| writer.writeStr(name, f.stringValue(), true); |
| } |
| |
| @Override |
| public SortField getSortField(SchemaField field, boolean top) { |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Sorting not supported on SpatialField: " + field.getName()+ |
| ", instead try sorting by query."); |
| } |
| } |
| |
| |