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

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

There is a newer version: 9.6.1
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 java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.lucene.document.StoredField;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.queries.function.valuesource.VectorValueSource;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.SortField;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.MapSolrParams;
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.locationtech.spatial4j.distance.DistanceUtils;

/**
 * A point type that indexes a point in an n-dimensional space as separate fields and supports range
 * queries. See {@link LatLonPointSpatialField} for geo-spatial queries.
 */
public class PointType extends CoordinateFieldType implements SpatialQueryable {

  @Override
  protected void init(IndexSchema schema, Map args) {
    SolrParams p = new MapSolrParams(args);
    dimension = p.getInt(DIMENSION, DEFAULT_DIMENSION);
    if (dimension < 1) {
      throw new SolrException(
          SolrException.ErrorCode.SERVER_ERROR, "The dimension must be > 0: " + dimension);
    }
    args.remove(DIMENSION);
    super.init(schema, args);

    // cache suffixes
    createSuffixCache(dimension);
  }

  @Override
  public boolean isPolyField() {
    return true; // really only true if the field is indexed
  }

  @Override
  public List createFields(SchemaField field, Object value) {
    String externalVal = value.toString();
    String[] point = parseCommaSeparatedList(externalVal, dimension);

    // TODO: this doesn't currently support polyFields as sub-field types
    List f = new ArrayList<>((dimension * 2) + 1);

    if (field.indexed()) {
      for (int i = 0; i < dimension; i++) {
        SchemaField sf = subField(field, i, schema);
        f.addAll(sf.createFields(point[i]));
      }
    }

    if (field.stored()) {
      String storedVal = externalVal; // normalize or not?
      f.add(createField(field.getName(), storedVal, StoredField.TYPE));
    }

    return f;
  }

  @Override
  public ValueSource getValueSource(SchemaField field, QParser parser) {
    ArrayList vs = new ArrayList<>(dimension);
    for (int i = 0; i < dimension; i++) {
      SchemaField sub = subField(field, i, schema);
      vs.add(sub.getType().getValueSource(sub, parser));
    }
    return new PointTypeValueSource(field, vs);
  }

  /**
   * It never makes sense to create a single field, so make it impossible to happen by throwing
   * UnsupportedOperationException
   */
  @Override
  public IndexableField createField(SchemaField field, Object value) {
    throw new UnsupportedOperationException(
        "PointType uses multiple fields.  field=" + field.getName());
  }

  @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 PointType " + field.getName());
  }

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

  /** Care should be taken in calling this with higher order dimensions for performance reasons. */
  @Override
  protected Query getSpecializedRangeQuery(
      QParser parser,
      SchemaField field,
      String part1,
      String part2,
      boolean minInclusive,
      boolean maxInclusive) {
    // Query could look like: [x1,y1 TO x2,y2] for 2 dimension, but could look like: [x1,y1,z1 TO
    // x2,y2,z2], and can be extrapolated to n-dimensions
    // thus, this query essentially creates a box, cube, etc.
    String[] p1 = parseCommaSeparatedList(part1, dimension);
    String[] p2 = parseCommaSeparatedList(part2, dimension);

    BooleanQuery.Builder result = new BooleanQuery.Builder();
    for (int i = 0; i < dimension; i++) {
      SchemaField subSF = subField(field, i, schema);
      // points must currently be ordered... should we support specifying any two opposite corner
      // points?
      result.add(
          subSF.getType().getRangeQuery(parser, subSF, p1[i], p2[i], minInclusive, maxInclusive),
          BooleanClause.Occur.MUST);
    }
    return result.build();
  }

  @Override
  public Query getFieldQuery(QParser parser, SchemaField field, String externalVal) {
    String[] p1 = parseCommaSeparatedList(externalVal, dimension);
    // TODO: should we assert that p1.length == dimension?
    BooleanQuery.Builder bq = new BooleanQuery.Builder();
    for (int i = 0; i < dimension; i++) {
      SchemaField sf = subField(field, i, schema);
      Query tq = sf.getType().getFieldQuery(parser, sf, p1[i]);
      bq.add(tq, BooleanClause.Occur.MUST);
    }
    return bq.build();
  }

  @Override
  protected void checkSupportsDocValues() {
    // DocValues supported only when enabled at the fieldType
    if (!hasProperty(DOC_VALUES)) {
      throw new UnsupportedOperationException(
          "PointType can't have docValues=true in the field definition, use docValues=true in the fieldType definition, or in subFieldType/subFieldSuffix");
    }
  }

  /**
   * Calculates the range and creates a RangeQuery (bounding box) wrapped in a BooleanQuery (unless
   * the dimension is 1, one range for every dimension, AND'd together by a Boolean
   *
   * @param parser The parser
   * @param options The {@link org.apache.solr.search.SpatialOptions} for this filter.
   * @return The Query representing the bounding box around the point.
   */
  @Override
  public Query createSpatialQuery(QParser parser, SpatialOptions options) {
    String[] pointStrs = parseCommaSeparatedList(options.pointStr, dimension);
    double[] point = new double[dimension];
    try {
      for (int i = 0; i < pointStrs.length; i++) {
        point[i] = Double.parseDouble(pointStrs[i]);
      }
    } catch (NumberFormatException e) {
      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
    }
    IndexSchema schema = parser.getReq().getSchema();

    if (dimension == 1) {
      // TODO: Handle distance measures
      String lower = String.valueOf(point[0] - options.distance);
      String upper = String.valueOf(point[0] + options.distance);
      SchemaField subSF = subField(options.field, 0, schema);
      // points must currently be ordered... should we support specifying any two opposite corner
      // points?
      return subSF.getType().getRangeQuery(parser, subSF, lower, upper, true, true);
    } else {
      BooleanQuery.Builder tmp = new BooleanQuery.Builder();
      // TODO: Handle distance measures, as this assumes Euclidean
      double[] ur = vectorBoxCorner(point, null, options.distance, true);
      double[] ll = vectorBoxCorner(point, null, options.distance, false);
      for (int i = 0; i < ur.length; i++) {
        SchemaField subSF = subField(options.field, i, schema);
        Query range =
            subSF
                .getType()
                .getRangeQuery(
                    parser, subSF, String.valueOf(ll[i]), String.valueOf(ur[i]), true, true);
        tmp.add(range, BooleanClause.Occur.MUST);
      }
      return tmp.build();
    }
  }

  private static final double SIN_PI_DIV_4 = Math.sin(Math.PI / 4);

  /**
   * Return the coordinates of a vector that is the corner of a box (upper right or lower left),
   * assuming a Rectangular coordinate system. Note, this does not apply for points on a sphere or
   * ellipse (although it could be used as an approximation).
   *
   * @param center The center point
   * @param result Holds the result, potentially resizing if needed.
   * @param distance The d from the center to the corner
   * @param upperRight If true, return the coords for the upper right corner, else return the lower
   *     left.
   * @return The point, either the upperLeft or the lower right
   */
  public static double[] vectorBoxCorner(
      double[] center, double[] result, double distance, boolean upperRight) {
    if (result == null || result.length != center.length) {
      result = new double[center.length];
    }
    if (upperRight == false) {
      distance = -distance;
    }
    // We don't care about the power here,
    // b/c we are always in a rectangular coordinate system, so any norm can be used by
    // using the definition of sine
    // sin(Pi/4) == (2^0.5)/2 == opp/hyp == opp/distance, solve for opp, similarly for cosine
    distance = SIN_PI_DIV_4 * distance;
    for (int i = 0; i < center.length; i++) {
      result[i] = center[i] + distance;
    }
    return result;
  }

  /**
   * Given a string containing dimension values encoded in it, separated by commas, return a
   * String array of length dimension containing the values.
   *
   * @param externalVal The value to parse
   * @param dimension The expected number of values for the point
   * @return An array of the values that make up the point (aka vector)
   * @throws SolrException if the dimension specified does not match the number found
   */
  public static String[] parseCommaSeparatedList(String externalVal, int dimension)
      throws SolrException {
    // TODO: Should we support sparse vectors?
    String[] out = new String[dimension];
    int idx = externalVal.indexOf(',');
    int end = idx;
    int start = 0;
    int i = 0;
    // we have a single point, dimension better be 1
    if (idx == -1 && dimension == 1 && externalVal.length() > 0) {
      out[0] = externalVal.trim();
      i = 1;
    } else if (idx > 0) {
      // if it is zero, that is an error
      // Parse out a comma separated list of values, as in: 73.5,89.2,7773.4
      for (; i < dimension; i++) {
        while (start < end && externalVal.charAt(start) == ' ') start++;
        while (end > start && externalVal.charAt(end - 1) == ' ') end--;
        if (start == end) {
          break;
        }
        out[i] = externalVal.substring(start, end);
        start = idx + 1;
        end = externalVal.indexOf(',', start);
        idx = end;
        if (end == -1) {
          end = externalVal.length();
        }
      }
    }
    if (i != dimension) {
      throw new SolrException(
          SolrException.ErrorCode.BAD_REQUEST,
          "incompatible dimension ("
              + dimension
              + ") and values ("
              + externalVal
              + ").  Only "
              + i
              + " values specified");
    }
    return out;
  }

  @Override
  public double getSphereRadius() {
    // This won't likely be used. You should probably be using LatLonPointSpatialField instead if
    // you felt the need for this. This is here just for backward compatibility reasons.
    return DistanceUtils.EARTH_MEAN_RADIUS_KM;
  }
}

class PointTypeValueSource extends VectorValueSource {
  private final SchemaField sf;

  public PointTypeValueSource(SchemaField sf, List sources) {
    super(sources);
    this.sf = sf;
  }

  @Override
  public String name() {
    return "point";
  }

  @Override
  public String description() {
    return name() + "(" + sf.getName() + ")";
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy