/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch 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.elasticsearch.search.suggest.completion.context;
import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.geo.GeoUtils;
import org.elasticsearch.common.xcontent.ObjectParser;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.index.query.QueryParseContext;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import static org.elasticsearch.search.suggest.completion.context.GeoContextMapping.CONTEXT_BOOST;
import static org.elasticsearch.search.suggest.completion.context.GeoContextMapping.CONTEXT_NEIGHBOURS;
import static org.elasticsearch.search.suggest.completion.context.GeoContextMapping.CONTEXT_PRECISION;
import static org.elasticsearch.search.suggest.completion.context.GeoContextMapping.CONTEXT_VALUE;
/**
* Defines the query context for {@link GeoContextMapping}
*/
public final class GeoQueryContext implements ToXContent {
public static final String NAME = "geo";
private final GeoPoint geoPoint;
private final int boost;
private final int precision;
private final List<Integer> neighbours;
private GeoQueryContext(GeoPoint geoPoint, int boost, int precision, List<Integer> neighbours) {
this.geoPoint = geoPoint;
this.boost = boost;
this.precision = precision;
this.neighbours = neighbours;
}
/**
* Returns the geo point of the context
*/
public GeoPoint getGeoPoint() {
return geoPoint;
}
/**
* Returns the query-time boost of the context
*/
public int getBoost() {
return boost;
}
/**
* Returns the precision (length) for the geohash
*/
public int getPrecision() {
return precision;
}
/**
* Returns the precision levels at which geohash cells neighbours are considered
*/
public List<Integer> getNeighbours() {
return neighbours;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GeoQueryContext that = (GeoQueryContext) o;
if (boost != that.boost) return false;
if (precision != that.precision) return false;
if (geoPoint != null ? !geoPoint.equals(that.geoPoint) : that.geoPoint != null) return false;
return neighbours != null ? neighbours.equals(that.neighbours) : that.neighbours == null;
}
@Override
public int hashCode() {
int result = geoPoint != null ? geoPoint.hashCode() : 0;
result = 31 * result + boost;
result = 31 * result + precision;
result = 31 * result + (neighbours != null ? neighbours.hashCode() : 0);
return result;
}
public static Builder builder() {
return new Builder();
}
private static ObjectParser<GeoQueryContext.Builder, Void> GEO_CONTEXT_PARSER = new ObjectParser<>(NAME, null);
static {
GEO_CONTEXT_PARSER.declareField((parser, geoQueryContext, geoContextMapping) -> geoQueryContext.setGeoPoint(GeoUtils.parseGeoPoint(parser)), new ParseField(CONTEXT_VALUE), ObjectParser.ValueType.OBJECT);
GEO_CONTEXT_PARSER.declareInt(GeoQueryContext.Builder::setBoost, new ParseField(CONTEXT_BOOST));
// TODO : add string support for precision for GeoUtils.geoHashLevelsForPrecision()
GEO_CONTEXT_PARSER.declareInt(GeoQueryContext.Builder::setPrecision, new ParseField(CONTEXT_PRECISION));
// TODO : add string array support for precision for GeoUtils.geoHashLevelsForPrecision()
GEO_CONTEXT_PARSER.declareIntArray(GeoQueryContext.Builder::setNeighbours, new ParseField(CONTEXT_NEIGHBOURS));
GEO_CONTEXT_PARSER.declareDouble(GeoQueryContext.Builder::setLat, new ParseField("lat"));
GEO_CONTEXT_PARSER.declareDouble(GeoQueryContext.Builder::setLon, new ParseField("lon"));
}
public static GeoQueryContext fromXContent(QueryParseContext context) throws IOException {
XContentParser parser = context.parser();
XContentParser.Token token = parser.currentToken();
GeoQueryContext.Builder builder = new Builder();
if (token == XContentParser.Token.START_OBJECT) {
GEO_CONTEXT_PARSER.parse(parser, builder, null);
} else if (token == XContentParser.Token.VALUE_STRING) {
builder.setGeoPoint(GeoPoint.fromGeohash(parser.text()));
} else {
throw new ElasticsearchParseException("geo context must be an object or string");
}
return builder.build();
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
builder.startObject(CONTEXT_VALUE);
builder.field("lat", geoPoint.getLat());
builder.field("lon", geoPoint.getLon());
builder.endObject();
builder.field(CONTEXT_BOOST, boost);
builder.field(CONTEXT_NEIGHBOURS, neighbours);
builder.field(CONTEXT_PRECISION, precision);
builder.endObject();
return builder;
}
public static class Builder {
private GeoPoint geoPoint;
private int boost = 1;
private int precision = 12;
private List<Integer> neighbours = Collections.emptyList();
public Builder() {
}
/**
* Sets the query-time boost for the context
* Defaults to 1
*/
public Builder setBoost(int boost) {
if (boost <= 0) {
throw new IllegalArgumentException("boost must be greater than 0");
}
this.boost = boost;
return this;
}
/**
* Sets the precision level for computing the geohash from the context geo point.
* Defaults to using index-time precision level
*/
public Builder setPrecision(int precision) {
if (precision < 1 || precision > 12) {
throw new IllegalArgumentException("precision must be between 1 and 12");
}
this.precision = precision;
return this;
}
/**
* Sets the precision levels at which geohash cells neighbours are considered.
* Defaults to only considering neighbours at the index-time precision level
*/
public Builder setNeighbours(List<Integer> neighbours) {
for (int neighbour : neighbours) {
if (neighbour < 1 || neighbour > 12) {
throw new IllegalArgumentException("neighbour value must be between 1 and 12");
}
}
this.neighbours = neighbours;
return this;
}
/**
* Sets the geo point of the context.
* This is a required field
*/
public Builder setGeoPoint(GeoPoint geoPoint) {
Objects.requireNonNull(geoPoint, "geoPoint must not be null");
this.geoPoint = geoPoint;
return this;
}
private double lat = Double.NaN;
void setLat(double lat) {
this.lat = lat;
}
private double lon = Double.NaN;
void setLon(double lon) {
this.lon = lon;
}
public GeoQueryContext build() {
if (geoPoint == null) {
if (Double.isNaN(lat) == false && Double.isNaN(lon) == false) {
geoPoint = new GeoPoint(lat, lon);
}
}
Objects.requireNonNull(geoPoint, "geoPoint must not be null");
return new GeoQueryContext(geoPoint, boost, precision, neighbours);
}
}
}