All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.solr.schema.AbstractSpatialFieldType Maven / Gradle / Ivy

There is a newer version: 9.7.0
Show newest version
/*
 * 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 com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StoredField;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.index.Term;
import org.apache.lucene.queries.function.FunctionScoreQuery;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.DoubleValuesSource;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.PrefixQuery;
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.spatial.spatial4j.Geo3dSpatialContextFactory;
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.uninverting.UninvertingReader.Type;
import org.apache.solr.util.DistanceUnits;
import org.apache.solr.util.MapListener;
import org.apache.solr.util.SpatialUtils;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.context.SpatialContextFactory;
import org.locationtech.spatial4j.distance.DistanceUtils;
import org.locationtech.spatial4j.io.ShapeReader;
import org.locationtech.spatial4j.io.ShapeWriter;
import org.locationtech.spatial4j.io.SupportedFormats;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Rectangle;
import org.locationtech.spatial4j.shape.Shape;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Abstract base class for Solr FieldTypes based on a Lucene 4 {@link SpatialStrategy}.
 *
 * @lucene.experimental
 */
public abstract class AbstractSpatialFieldType extends FieldType
    implements SpatialQueryable {

  /**
   * A local-param with one of "none" (default), "distance", "recipDistance" or supported values in
   * ({@link DistanceUnits#getSupportedUnits()}.
   */
  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";

  // score param values:
  public static final String DISTANCE = "distance";
  public static final String RECIP_DISTANCE = "recipDistance";
  public static final String NONE = "none";

  /** Optional param to pick the string conversion */
  public static final String FORMAT = "format";

  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  protected SpatialContext ctx;
  protected SpatialArgsParser argsParser;

  protected ShapeWriter shapeWriter;
  protected ShapeReader shapeReader;

  private final Cache fieldStrategyCache = Caffeine.newBuilder().build();

  protected DistanceUnits distanceUnits;

  protected final Set supportedScoreModes;

  protected AbstractSpatialFieldType() {
    this(Collections.emptySet());
  }

  protected AbstractSpatialFieldType(Set moreScoreModes) {
    Set set = new TreeSet<>(); // sorted for consistent display order
    set.add(NONE);
    set.add(DISTANCE);
    set.add(RECIP_DISTANCE);
    set.addAll(DistanceUnits.getSupportedUnits());
    set.addAll(moreScoreModes);
    supportedScoreModes = Collections.unmodifiableSet(set);
  }

  @Override
  protected void init(IndexSchema schema, Map args) {
    super.init(schema, args);

    if (ctx == null) { // subclass can set this directly
      final String CTX_PARAM = "spatialContextFactory";
      final String OLD_SPATIAL4J_PREFIX = "com.spatial4j.core";
      final String NEW_SPATIAL4J_PREFIX = "org.locationtech.spatial4j";
      for (Map.Entry argEntry : args.entrySet()) {
        // "JTS" is a convenience alias
        if (argEntry.getKey().equals(CTX_PARAM) && argEntry.getValue().equals("JTS")) {
          argEntry.setValue("org.locationtech.spatial4j.context.jts.JtsSpatialContextFactory");
          continue;
        }
        if (argEntry.getKey().equals(CTX_PARAM) && argEntry.getValue().equals("Geo3D")) {
          argEntry.setValue(Geo3dSpatialContextFactory.class.getName());
          continue;
        }
        // Warn about using old Spatial4j class names
        if (argEntry.getValue().contains(OLD_SPATIAL4J_PREFIX)) {
          log.warn(
              "Replace '{}' with '{}' in your schema", OLD_SPATIAL4J_PREFIX, NEW_SPATIAL4J_PREFIX);
          argEntry.setValue(
              argEntry.getValue().replace(OLD_SPATIAL4J_PREFIX, NEW_SPATIAL4J_PREFIX));
        }
      }

      // Solr expects us to remove the parameters we've used.
      MapListener argsWrap = new MapListener<>(args);
      ctx =
          SpatialContextFactory.makeSpatialContext(
              argsWrap, schema.getResourceLoader().getClassLoader());
      args.keySet().removeAll(argsWrap.getSeenKeys());
    }

    final String distanceUnitsStr = args.remove("distanceUnits");
    if (distanceUnitsStr == null) {
      this.distanceUnits = ctx.isGeo() ? DistanceUnits.KILOMETERS : DistanceUnits.DEGREES;
    } else {
      this.distanceUnits = parseDistanceUnits(distanceUnitsStr);
      if (this.distanceUnits == null)
        throw new SolrException(
            SolrException.ErrorCode.SERVER_ERROR,
            "Must specify distanceUnits as one of "
                + DistanceUnits.getSupportedUnits()
                + " on field types with class "
                + getClass().getSimpleName());
    }

    final SupportedFormats fmts = ctx.getFormats();
    String format = args.remove(FORMAT);
    if (format == null) {
      format = "WKT";
    }
    shapeWriter = fmts.getWriter(format);
    shapeReader = fmts.getReader(format);
    if (shapeWriter == null) {
      throw new SolrException(
          SolrException.ErrorCode.SERVER_ERROR, "Unknown Shape Format: " + format);
    }
    if (shapeReader == null) {
      throw new SolrException(
          SolrException.ErrorCode.SERVER_ERROR, "Unknown Shape Format: " + format);
    }

    argsParser = newSpatialArgsParser();
  }

  /**
   * if {@code str} is non-null, returns {@link org.apache.solr.util.DistanceUnits#valueOf(String)}
   * (which will return null if not found), else returns {@link #distanceUnits} (only null before
   * initialized in {@code init()}.
   *
   * @param str maybe null
   * @return maybe null
   */
  public DistanceUnits parseDistanceUnits(String str) {
    if (str == null) {
      return this.distanceUnits;
    } else {
      return DistanceUnits.valueOf(str);
    }
  }

  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) {
    throw new IllegalStateException("instead call createFields() because isPolyField() is true");
  }

  @Override
  public Type getUninversionType(SchemaField sf) {
    return null;
  }

  @Override
  public List createFields(SchemaField field, Object val) {
    String shapeStr = null;
    Shape shape;
    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 result = new ArrayList<>();
    if (field.indexed() || field.hasDocValues()) {
      T strategy = getStrategy(field.getName());
      result.addAll(Arrays.asList(strategy.createIndexableFields(shape)));
    }

    if (field.stored()) {
      result.add(new StoredField(field.getName(), getStoredValue(shape, shapeStr)));
    }

    return result;
  }

  /** Called by {@link #createFields(SchemaField, Object)} to get the stored value. */
  protected String getStoredValue(Shape shape, String shapeStr) {
    return (shapeStr == null) ? shapeToString(shape) : shapeStr;
  }

  /** Create a {@link Shape} from the input string */
  public Shape parseShape(String str) {
    str = str.trim();
    if (str.length() == 0)
      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "empty string shape");

    // If the first char is promising, try to parse with SpatialUtils.parsePoint
    char firstChar = str.charAt(0);
    if (firstChar == '+' || firstChar == '-' || (firstChar >= '0' && firstChar <= '9')) {
      try {
        return SpatialUtils.parsePoint(str, ctx);
      } catch (Exception e) { // ignore
      }
    }

    try {
      return shapeReader.read(str);
    } catch (Exception e) {
      String msg =
          "Unable to parse shape given formats"
              + " \"lat,lon\", \"x y\" or as "
              + shapeReader.getFormatName()
              + " because "
              + e;
      if (!msg.contains(str)) {
        msg += " input: " + str;
      }
      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, msg, e);
    }
  }

  /**
   * Returns a String version of a shape to be used for the stored value.
   *
   * 

The format can be selected using the initParam format={WKT|GeoJSON} */ public String shapeToString(Shape shape) { return shapeWriter.toString(shape); } /** 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 protected Query getSpecializedRangeQuery( 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 protected Query getSpecializedExistenceQuery(QParser parser, SchemaField field) { PrefixQuery query = new PrefixQuery(new Term(field.getName(), "")); query.setRewriteMethod(field.getType().getRewriteMethod(parser, field)); return query; } @Override public Query getFieldQuery(QParser parser, SchemaField field, String externalVal) { return getQueryFromSpatialArgs(parser, field, parseSpatialArgs(parser, externalVal)); } protected SpatialArgs parseSpatialArgs(QParser parser, String externalVal) { try { SpatialArgs args = argsParser.parse(externalVal, ctx); // Convert parsed args.distErr to degrees (using distanceUnits) if (args.getDistErr() != null) { args.setDistErr(args.getDistErr() * distanceUnits.multiplierFromThisUnitToDegrees()); } return args; } catch (SolrException e) { throw e; } catch (Exception e) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e); } } protected Query getQueryFromSpatialArgs( QParser parser, SchemaField field, SpatialArgs spatialArgs) { T strategy = getStrategy(field.getName()); SolrParams localParams = parser.getLocalParams(); // See SOLR-2883 needScore String scoreParam = (localParams == null ? null : localParams.get(SCORE_PARAM)); // We get the valueSource for the score then the filter and combine them. DoubleValuesSource valueSource = getValueSourceFromSpatialArgs(parser, field, spatialArgs, scoreParam, strategy); if (valueSource == null) { return strategy.makeQuery(spatialArgs); // assumed constant scoring } FunctionScoreQuery functionQuery = new FunctionScoreQuery(new MatchAllDocsQuery(), valueSource); if (localParams != null && !localParams.getBool(FILTER_PARAM, true)) return functionQuery; Query filterQuery = strategy.makeQuery(spatialArgs); return new BooleanQuery.Builder() .add(functionQuery, Occur.MUST) // matches everything and provides score .add(filterQuery, Occur.FILTER) // filters (score isn't used) .build(); } @Override public double getSphereRadius() { return distanceUnits.getEarthRadius(); } /** The set of values supported for the score local-param. Not null. */ public Set getSupportedScoreModes() { return supportedScoreModes; } protected DoubleValuesSource getValueSourceFromSpatialArgs( QParser parser, SchemaField field, SpatialArgs spatialArgs, String score, T strategy) { if (score == null) { return null; } final double multiplier; // default multiplier for degrees switch (score) { case "": case NONE: return null; case RECIP_DISTANCE: return strategy.makeRecipDistanceValueSource(spatialArgs.getShape()); case DISTANCE: multiplier = distanceUnits.multiplierFromDegreesToThisUnit(); break; default: DistanceUnits du = parseDistanceUnits(score); if (du != null) { multiplier = du.multiplierFromDegreesToThisUnit(); } else { throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "'score' local-param must be one of " + supportedScoreModes + ", it was: " + score); } } return strategy.makeDistanceValueSource(spatialArgs.getShape().getCenter(), multiplier); } /** * 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) { return fieldStrategyCache.get(fieldName, k -> newSpatialStrategy(fieldName)); } /** * @return The Spatial Context for this field type */ public SpatialContext getSpatialContext() { return ctx; } @Override public void write(TextResponseWriter writer, String name, IndexableField 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."); } public DistanceUnits getDistanceUnits() { return this.distanceUnits; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy