/* See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * Esri Inc. 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 com.esri.gpt.catalog.lucene; import com.esri.gpt.framework.geometry.Envelope; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.lucene.index.IndexReader; import org.apache.lucene.search.FieldCache; import org.apache.lucene.search.function.DocValues; import org.apache.lucene.search.function.ValueSource; /** * * An implementation of the Lucene ValueSource model to support spatial relevance ranking. * <p/> * * The algorithm is implemented as envelope on envelope overlays rather than * complex polygon on complex polygon overlays. * * <p/> * Spatial relevance scoring algorithm: * * <br/> queryArea = the area of the input query envelope * <br/> targetArea = the area of the target envelope (per Lucene document) * <br/> intersectionArea = the area of the intersection for the query/target envelopes * <br/> queryPower = the weighting power associated with the query envelope (default = 1.0) * <br/> targetPower = the weighting power associated with the target envelope (default = 1.0) * * <br/> queryPower and targetPower are initialized from gpt.xml configuration parameters: * <br/> /gptConfig/catalog/parameter@key="spatialRelevance.queryPower" and * <br/> /gptConfig/catalog/parameter@key="spatialRelevance.queryPower" * * <br/> queryRatio = intersectionArea / queryArea; * <br/> targetRatio = intersectionArea / targetArea; * <br/> queryFactor = Math.pow(queryRatio,queryPower); * <br/> targetFactor = Math.pow(targetRatio,targetPower); * <br/> score = queryFactor * targetFactor; * */ public class SpatialRankingValueSource extends ValueSource { /** class variables ========================================================= */ /** The class name hash code. */ private static final int HCODE = SpatialRankingValueSource.class.hashCode(); /** The Logger. */ private static Logger LOGGER = Logger.getLogger(SpatialRankingValueSource.class.getName()); /** instance variables ====================================================== */ /** Properties associated with the query envelope */ private double qryArea; private double qryPower = 2.0; private double qryMinX; private double qryMinY; private double qryMaxX; private double qryMaxY; private boolean qryCrossedDateline = false; /** Properties associated with the target envelope (the document's envelope) */ private String tgtField = "envelope.full"; private double tgtPower = 0.5; /** constructors ============================================================ */ /** * Constructor. * @param queryEnvelope the query envelope * @param queryPower the query power (scoring algorithm) * @param targetPower the target power (scoring algorithm) */ public SpatialRankingValueSource(Envelope queryEnvelope, double queryPower, double targetPower) { // initialize this.qryPower = queryPower; this.tgtPower = targetPower; this.qryMinX = queryEnvelope.getMinX(); this.qryMinY = queryEnvelope.getMinY(); this.qryMaxX = queryEnvelope.getMaxX(); this.qryMaxY = queryEnvelope.getMaxY(); if (this.qryMinX > this.qryMaxX) { this.qryCrossedDateline = true; this.qryArea = Math.abs(qryMaxX + 360.0 - qryMinX) * Math.abs(qryMaxY - qryMinY); } else { this.qryArea = Math.abs(qryMaxX - qryMinX) * Math.abs(qryMaxY - qryMinY); } } /** methods ================================================================= */ /** * Calculates the spatial ranking score. * @param doc the document id * @param delimitedEnvelope the envelope.full field value * @return the score */ private float calculateScore(int doc, String delimitedEnvelope) { double score = 0; if (delimitedEnvelope== null) return (float)score; String[] tokens = delimitedEnvelope.split(";"); if (tokens.length == 6) { double tgtMinX = Double.valueOf(tokens[0]); double tgtMinY = Double.valueOf(tokens[1]); double tgtMaxX = Double.valueOf(tokens[2]); double tgtMaxY = Double.valueOf(tokens[3]); double tgtArea = Double.valueOf(tokens[4]); boolean tgtCrossedDateline = Boolean.valueOf(tokens[5]); // determine the area of the target and the area of intersection, if ((this.qryArea > 0) && (tgtArea > 0)) { double top = Math.min(this.qryMaxY,tgtMaxY); double bottom = Math.max(this.qryMinY,tgtMinY); double height = top - bottom; double width = 0; // queries that do not cross the date line if (!this.qryCrossedDateline) { // documents that do not cross the date line if (!tgtCrossedDateline) { double left = Math.max(this.qryMinX,tgtMinX); double right = Math.min(this.qryMaxX,tgtMaxX); width = right - left; // documents that cross the date line } else { double tgtWestLeft = Math.max(this.qryMinX,tgtMinX); double tgtWestRight = Math.min(this.qryMaxX,180.0); double tgtWestWidth = tgtWestRight - tgtWestLeft; if (tgtWestWidth > 0) { width = tgtWestWidth; } else { double tgtEastLeft = Math.max(this.qryMinX,-180.0); double tgtEastRight = Math.min(this.qryMaxX,tgtMaxX); double tgtEastWidth = tgtEastRight - tgtEastLeft; if (tgtEastWidth > 0) { width = tgtEastWidth; } } } // queries that cross the date line } else { // documents that do not cross the date line if (!tgtCrossedDateline) { double qryWestLeft = Math.max(this.qryMinX,tgtMinX); double qryWestRight = Math.min(tgtMaxX,180.0); double qryWestWidth = qryWestRight - qryWestLeft; if (qryWestWidth > 0) { width = qryWestWidth; } else { double qryEastLeft = Math.max(tgtMinX,-180.0); double qryEastRight = Math.min(this.qryMaxX,tgtMaxX); double qryEastWidth = qryEastRight - qryEastLeft; if (qryEastWidth > 0) { width = qryEastWidth; } } // documents that cross the date line } else { double left = Math.max(this.qryMinX,tgtMinX); double right = Math.min(this.qryMaxX,tgtMaxX); width = right + 360.0 - left; } } // calculate the score if ((width > 0) && (height > 0)) { double intersectionArea = width * height; double queryRatio = intersectionArea / this.qryArea; double targetRatio = intersectionArea / tgtArea; double queryFactor = Math.pow(queryRatio,this.qryPower); double targetFactor = Math.pow(targetRatio,this.tgtPower); score = queryFactor * targetFactor * 10000.0; boolean bLog = false; if (bLog && LOGGER.isLoggable(Level.FINER)) { StringBuffer sb = new StringBuffer(); sb.append("\nscore="+score); sb.append("\n queryEnv="+this.qryMinX+","+this.qryMinY+","+this.qryMaxX+","+this.qryMaxY); sb.append(" targetEnv="+tgtMinX+","+tgtMinY+","+tgtMaxX+","+tgtMaxY); sb.append("\n intersectionArea="+intersectionArea); sb.append(" queryArea="+this.qryArea+" targetArea="+tgtArea); sb.append("\n queryRatio="+queryRatio+" targetRatio="+targetRatio); sb.append("\n queryFactor="+queryFactor+" targetFactor="+targetFactor); sb.append(" (queryPower="+this.qryPower+" targetPower="+this.tgtPower+")"); LOGGER.finer(sb.toString()); } } } } return (float)score; } /** * Returns the ValueSource description. * @return the description */ @Override public String description() { return "SpatialRankingValueSource("+this.tgtField+")"; } /** * Determines if this ValueSource is equal to another. * @param o the ValueSource to compare * @return <code>true</code> if the two objects are based upon the same query envelope */ @Override public boolean equals(Object o) { if (o.getClass() != SpatialRankingValueSource.class) return false; SpatialRankingValueSource other = (SpatialRankingValueSource)o; return this.getDelimiterQueryParameters().equals(other.getDelimiterQueryParameters()); } /** * Returns the delimited query parameters. * <br/>Applies to ValueSource.equals and ValueSource.hashCode. * @return the delimited parameters */ public String getDelimiterQueryParameters() { return this.qryMinX+";"+this.qryMinY+";"+this.qryMaxX+";"+this.qryMaxY+";"+ this.qryPower+";"+this.tgtPower; } /** * Returns the DocValues used by the function query. * @param reader the index reader * @return the values */ @Override public DocValues getValues(IndexReader reader) throws IOException { final String[] arr = FieldCache.DEFAULT.getStrings(reader,this.tgtField); return new DocValues() { @Override public float floatVal(int doc) { return calculateScore(doc,arr[doc]); } @Override public String toString(int doc) { return description()+"="+floatVal(doc); } Object getInnerArray() { return arr; } }; } /** * Returns the ValueSource hash code. * @return the hash code */ @Override public int hashCode() { return HCODE+this.getDelimiterQueryParameters().hashCode(); } }