SOLR-3304 Solr adapters for the new Lucene 4 spatial module

git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1386457 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
David Wayne Smiley 2012-09-17 03:17:02 +00:00
parent eb122170c7
commit bf05f70a0b
9 changed files with 860 additions and 6 deletions

View File

@ -0,0 +1,226 @@
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.spatial4j.core.context.SpatialContext;
import com.spatial4j.core.context.SpatialContextFactory;
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.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.util.MapListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public abstract class AbstractSpatialFieldType<T extends SpatialStrategy> extends FieldType {
/** A local-param with one of "none" (default), "distance", or "recipDistance". */
public static final String SCORE_PARAM = "score";
protected final Logger log = LoggerFactory.getLogger( getClass() );
protected SpatialContext ctx;
protected SpatialArgsParser argsParser;
private final ConcurrentHashMap<String, T> fieldStrategyMap = new ConcurrentHashMap<String,T>();
@Override
protected void init(IndexSchema schema, Map<String, String> args) {
super.init(schema, args);
//Solr expects us to remove the parameters we've used.
MapListener<String, String> argsWrap = new MapListener<String, String>(args);
ctx = SpatialContextFactory.makeSpatialContext(argsWrap, schema.getResourceLoader().getClassLoader());
args.keySet().removeAll(argsWrap.getSeenKeys());
argsParser = new SpatialArgsParser();//might make pluggable some day?
}
//--------------------------------------------------------------
// Indexing
//--------------------------------------------------------------
@Override
public final StorableField createField(SchemaField field, Object val, float boost) {
throw new IllegalStateException("should be calling createFields because isPolyField() is true");
}
@Override
public final 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 = ctx.readShape(shapeStr);
}
if( shape == null ) {
log.debug("Field {}: null shape for input: {}", field, val);
return null;
}
Field[] indexableFields = null;
if (field.indexed()) {
T strategy = getStrategy(field.getName());
indexableFields = strategy.createIndexableFields(shape);
}
StoredField storedField = null;
if (field.stored()) {
if (shapeStr == null)
shapeStr = shapeToString(shape);
storedField = new StoredField(field.getName(), shapeStr);
}
if (indexableFields == null) {
if (storedField == null)
return null;
return new Field[]{storedField};
} else {
if (storedField == null)
return indexableFields;
Field[] result = new Field[indexableFields.length+1];
System.arraycopy(indexableFields,0,result,0,indexableFields.length);
result[result.length-1] = storedField;
return result;
}
}
protected String shapeToString(Shape shape) {
return ctx.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
//--------------------------------------------------------------
@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());
Shape shape1 = ctx.readShape(part1);
Shape shape2 = ctx.readShape(part2);
if (!(shape1 instanceof Point) || !(shape2 instanceof Point))
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Both sides of spatial range query must be points: " + field.getName());
Point p1 = (Point) shape1;
Point p2 = (Point) shape2;
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 an 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, argsParser.parse(externalVal, ctx));
}
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))
valueSource = strategy.makeDistanceValueSource(spatialArgs.getShape().getCenter());
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'");
Filter filter = strategy.makeFilter(spatialArgs);
return new FilteredQuery(new FunctionQuery(valueSource), 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) {
T strategy = fieldStrategyMap.get(fieldName);
//double-checked locking idiom
if (strategy == null) {
synchronized (fieldStrategyMap) {
strategy = fieldStrategyMap.get(fieldName);
if (strategy == null) {
strategy = newSpatialStrategy(fieldName);
fieldStrategyMap.put(fieldName,strategy);
}
}
}
return strategy;
}
@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());
}
}

View File

@ -0,0 +1,71 @@
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 org.apache.lucene.spatial.prefix.PrefixTreeStrategy;
import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree;
import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTreeFactory;
import org.apache.lucene.spatial.query.SpatialArgsParser;
import org.apache.solr.util.MapListener;
import java.util.Map;
public abstract class AbstractSpatialPrefixTreeFieldType<T extends PrefixTreeStrategy> extends AbstractSpatialFieldType<T> {
/** @see org.apache.lucene.spatial.prefix.RecursivePrefixTreeStrategy#setDefaultFieldValuesArrayLen(int) */
public static final String DEFAULT_FIELD_VALUES_ARRAY_LEN = "defaultFieldValuesArrayLen";
protected SpatialPrefixTree grid;
private Double distErrPct;
private Integer defaultFieldValuesArrayLen;
@Override
protected void init(IndexSchema schema, Map<String, String> args) {
super.init(schema, args);
//Solr expects us to remove the parameters we've used.
MapListener<String, String> argsWrap = new MapListener<String, String>(args);
grid = SpatialPrefixTreeFactory.makeSPT(argsWrap, schema.getResourceLoader().getClassLoader(), ctx);
args.keySet().removeAll(argsWrap.getSeenKeys());
String v = args.remove(SpatialArgsParser.DIST_ERR_PCT);
if (v != null)
distErrPct = Double.valueOf(v);
v = args.remove(DEFAULT_FIELD_VALUES_ARRAY_LEN);
if (v != null)
defaultFieldValuesArrayLen = Integer.valueOf(v);
}
@Override
protected T newSpatialStrategy(String fieldName) {
T strat = newPrefixTreeStrategy(fieldName);
if (distErrPct != null)
strat.setDistErrPct(distErrPct);
if (defaultFieldValuesArrayLen != null)
strat.setDefaultFieldValuesArrayLen(defaultFieldValuesArrayLen);
log.info(this.toString()+" strat: "+strat+" maxLevels: "+ grid.getMaxLevels());//TODO output maxDetailKm
return strat;
}
protected abstract T newPrefixTreeStrategy(String fieldName);
}

View File

@ -0,0 +1,47 @@
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 org.apache.lucene.spatial.prefix.RecursivePrefixTreeStrategy;
import java.util.Map;
public class SpatialRecursivePrefixTreeFieldType extends AbstractSpatialPrefixTreeFieldType<RecursivePrefixTreeStrategy> {
/** @see RecursivePrefixTreeStrategy#setPrefixGridScanLevel(int) */
public static final String PREFIX_GRID_SCAN_LEVEL = "prefixGridScanLevel";
private Integer prefixGridScanLevel;
@Override
protected void init(IndexSchema schema, Map<String, String> args) {
super.init(schema, args);
String v = args.remove(PREFIX_GRID_SCAN_LEVEL);
if (v != null)
prefixGridScanLevel = Integer.valueOf(v);
}
@Override
protected RecursivePrefixTreeStrategy newPrefixTreeStrategy(String fieldName) {
RecursivePrefixTreeStrategy strategy = new RecursivePrefixTreeStrategy(grid, fieldName);
if (prefixGridScanLevel != null)
strategy.setPrefixGridScanLevel(prefixGridScanLevel);
return strategy;
}
}

View File

@ -0,0 +1,29 @@
/*
* 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 org.apache.lucene.spatial.prefix.TermQueryPrefixTreeStrategy;
public class SpatialTermQueryPrefixTreeFieldType extends AbstractSpatialPrefixTreeFieldType<TermQueryPrefixTreeStrategy> {
@Override
protected TermQueryPrefixTreeStrategy newPrefixTreeStrategy(String fieldName) {
return new TermQueryPrefixTreeStrategy(grid,fieldName);
}
}

View File

@ -0,0 +1,78 @@
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 org.apache.lucene.spatial.vector.TwoDoublesStrategy;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class SpatialTwoDoublesFieldType extends AbstractSpatialFieldType<TwoDoublesStrategy> implements SchemaAware {
protected String numberFieldName = "tdouble";//in example schema defaults to non-zero precision step -- a good choice
private int precisionStep;
@Override
protected void init(IndexSchema schema, Map<String, String> args) {
super.init(schema, args);
String v = args.remove( "numberType" );
if( v != null ) {
numberFieldName = v;
}
}
@Override
public void inform(IndexSchema schema) {
FieldType fieldType = schema.getFieldTypeByName(numberFieldName);
if( fieldType == null ) {
throw new RuntimeException( "Can not find number field: "+ numberFieldName);
}
//TODO support other numeric types in the future
if( !(fieldType instanceof TrieDoubleField) ) {
throw new RuntimeException( "field type must be TrieDoubleField: "+ fieldType);
}
precisionStep = ((TrieField)fieldType).getPrecisionStep();
//Just set these, delegate everything else to the field type
final int p = (INDEXED | TOKENIZED | OMIT_NORMS | OMIT_TF_POSITIONS);
List<SchemaField> newFields = new ArrayList<SchemaField>();
for( SchemaField sf : schema.getFields().values() ) {
if( sf.getType() == this ) {
String name = sf.getName();
newFields.add(new SchemaField(name + TwoDoublesStrategy.SUFFIX_X, fieldType, p, null));
newFields.add(new SchemaField(name + TwoDoublesStrategy.SUFFIX_Y, fieldType, p, null));
}
}
for (SchemaField newField : newFields) {
schema.getFields().put(newField.getName(), newField);
}
}
@Override
protected TwoDoublesStrategy newSpatialStrategy(String fieldName) {
TwoDoublesStrategy strategy = new TwoDoublesStrategy(ctx, fieldName);
strategy.setPrecisionStep(precisionStep);
return strategy;
}
}

View File

@ -0,0 +1,59 @@
package org.apache.solr.util;
/*
* 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.collect.ForwardingMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* Wraps another map, keeping track of each key that was seen via {@link #get(Object)} or {@link #remove(Object)}.
*/
@SuppressWarnings("unchecked")
public class MapListener<K, V> extends ForwardingMap<K, V> {
private final Map<K, V> target;
private final Set<K> seenKeys;
public MapListener(Map<K, V> target) {
this.target = target;
seenKeys = new HashSet<K>(target.size());
}
public Set<K> getSeenKeys() {
return seenKeys;
}
@Override
public V get(Object key) {
seenKeys.add((K) key);
return super.get(key);
}
@Override
public V remove(Object key) {
seenKeys.add((K) key);
return super.remove(key);
}
@Override
protected Map<K, V> delegate() {
return target;
}
}

View File

@ -0,0 +1,63 @@
<?xml version="1.0" ?>
<!--
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.
-->
<schema name="test" version="1.4">
<types>
<fieldType name="tint" class="solr.TrieIntField" precisionStep="8" omitNorms="true" positionIncrementGap="0"/>
<fieldType name="tfloat" class="solr.TrieFloatField" precisionStep="8" omitNorms="true" positionIncrementGap="0"/>
<fieldType name="tlong" class="solr.TrieLongField" precisionStep="8" omitNorms="true" positionIncrementGap="0"/>
<fieldType name="tdouble" class="solr.TrieDoubleField" precisionStep="8" omitNorms="true" positionIncrementGap="0"/>
<fieldType name="string" class="solr.StrField" sortMissingLast="true"/>
<fieldType name="srpt_geohash" class="solr.SpatialRecursivePrefixTreeFieldType"
prefixTree="geohash"
/>
<fieldType name="srpt_quad" class="solr.SpatialRecursivePrefixTreeFieldType"
prefixTree="quad"
/>
<fieldType name="srpt_100km" class="solr.SpatialRecursivePrefixTreeFieldType"
maxDetailDist="0.9"
/>
<fieldType name="stqpt_geohash" class="solr.SpatialTermQueryPrefixTreeFieldType"
prefixTree="geohash" />
<fieldType name="stqpt_u" class="solr.SpatialTermQueryPrefixTreeFieldType"
geo="false" distCalculator="cartesian^2" worldBounds="0 0 1000 1000"/>
<fieldType name="twodoubles" class="solr.SpatialTwoDoublesFieldType"
numberType="tdouble"/>
</types>
<fields>
<field name="id" type="string" required="true"/>
<field name="srpt_geohash" type="srpt_geohash" multiValued="true" />
<field name="srpt_quad" type="srpt_quad" multiValued="true" />
<field name="stqpt_geohash" type="stqpt_geohash" multiValued="true" />
<field name="twodoubles" type="twodoubles" />
</fields>
<uniqueKey>id</uniqueKey>
</schema>

View File

@ -0,0 +1,280 @@
package org.apache.solr.search;
/*
* 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.carrotsearch.randomizedtesting.RandomizedTest;
import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
import com.spatial4j.core.context.SpatialContext;
import com.spatial4j.core.distance.DistanceUtils;
import org.apache.solr.SolrTestCaseJ4;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.Arrays;
/**
* Test Solr 4's new spatial capabilities from the new Lucene spatial module. Don't thoroughly test it here because
* Lucene spatial has its own tests. Some of these tests were ported from Solr 3 spatial tests.
*/
public class TestSolr4Spatial extends SolrTestCaseJ4 {
private String fieldName;
public TestSolr4Spatial(String fieldName) {
this.fieldName = fieldName;
}
@ParametersFactory
public static Iterable<Object[]> parameters() {
return Arrays.asList(new Object[][]{
{"srpt_geohash"}, {"srpt_quad"}, {"stqpt_geohash"}, {"twodoubles"}
});
}
@BeforeClass
public static void beforeClass() throws Exception {
initCore("solrconfig-basic.xml", "schema-spatial.xml");
}
@Before
public void setUp() throws Exception {
super.setUp();
clearIndex();
}
private void setupDocs() {
assertU(adoc("id", "1", fieldName, "32.7693246, -79.9289094"));
assertU(adoc("id", "2", fieldName, "33.7693246, -80.9289094"));
assertU(adoc("id", "3", fieldName, "-32.7693246, 50.9289094"));
assertU(adoc("id", "4", fieldName, "-50.7693246, 60.9289094"));
assertU(adoc("id", "5", fieldName, "0,0"));
assertU(adoc("id", "6", fieldName, "0.1,0.1"));
assertU(adoc("id", "7", fieldName, "-0.1,-0.1"));
assertU(adoc("id", "8", fieldName, "0,179.9"));
assertU(adoc("id", "9", fieldName, "0,-179.9"));
assertU(adoc("id", "10", fieldName, "89.9,50"));
assertU(adoc("id", "11", fieldName, "89.9,-130"));
assertU(adoc("id", "12", fieldName, "-89.9,50"));
assertU(adoc("id", "13", fieldName, "-89.9,-130"));
assertU(commit());
}
@Test
public void testIntersectFilter() throws Exception {
setupDocs();
//Try some edge cases
checkHits(fieldName, "1,1", 175, 3, 5, 6, 7);
checkHits(fieldName, "0,179.8", 200, 2, 8, 9);
checkHits(fieldName, "89.8, 50", 200, 2, 10, 11);//this goes over the north pole
checkHits(fieldName, "-89.8, 50", 200, 2, 12, 13);//this goes over the south pole
//try some normal cases
checkHits(fieldName, "33.0,-80.0", 300, 2);
//large distance
checkHits(fieldName, "1,1", 5000, 3, 5, 6, 7);
//Because we are generating a box based on the west/east longitudes and the south/north latitudes, which then
//translates to a range query, which is slightly more inclusive. Thus, even though 0.0 is 15.725 kms away,
//it will be included, b/c of the box calculation.
checkHits(fieldName, false, "0.1,0.1", 15, 2, 5, 6);
//try some more
clearIndex();
assertU(adoc("id", "14", fieldName, "0,5"));
assertU(adoc("id", "15", fieldName, "0,15"));
//3000KM from 0,0, see http://www.movable-type.co.uk/scripts/latlong.html
assertU(adoc("id", "16", fieldName, "18.71111,19.79750"));
assertU(adoc("id", "17", fieldName, "44.043900,-95.436643"));
assertU(commit());
checkHits(fieldName, "0,0", 1000, 1, 14);
checkHits(fieldName, "0,0", 2000, 2, 14, 15);
checkHits(fieldName, false, "0,0", 3000, 3, 14, 15, 16);
checkHits(fieldName, "0,0", 3001, 3, 14, 15, 16);
checkHits(fieldName, "0,0", 3000.1, 3, 14, 15, 16);
//really fine grained distance and reflects some of the vagaries of how we are calculating the box
checkHits(fieldName, "43.517030,-96.789603", 109, 0);
//falls outside of the real distance, but inside the bounding box
checkHits(fieldName, true, "43.517030,-96.789603", 110, 0);
checkHits(fieldName, false, "43.517030,-96.789603", 110, 1, 17);
}
@Test
public void checkResultFormat() throws Exception {
//Check input and output format is the same
String IN = "89.9,-130";//lat,lon
String OUT = IN;//IDENTICAL!
assertU(adoc("id", "11", fieldName, IN));
assertU(commit());
assertQ(req(
"fl", "id," + fieldName, "q", "*:*", "rows", "1000",
"fq", "{!field needScore=false f="+fieldName+"}Intersects(Circle(89.9,-130 d=9))"),
"//result/doc/*[@name='" + fieldName + "']//text()='" + OUT + "'");
}
@Test
public void checkQueryEmptyIndex() {
checkHits(fieldName, "0,0", 100, 0);//doesn't error
}
private void checkHits(String fieldName, String pt, double distKM, int count, int ... docIds) {
checkHits(fieldName, true, pt, distKM, count, docIds);
}
private void checkHits(String fieldName, boolean exact, String ptStr, double distKM, int count, int ... docIds) {
String [] tests = new String[docIds != null && docIds.length > 0 ? docIds.length + 1 : 1];
//test for presence of required ids first
int i = 0;
if (docIds != null && docIds.length > 0) {
for (int docId : docIds) {
tests[i++] = "//result/doc/*[@name='id'][.='" + docId + "']";
}
}
//check total length last; maybe response includes ids it shouldn't. Nicer to check this last instead of first so
// that there may be a more specific detailed id to investigate.
tests[i++] = "*[count(//doc)=" + count + "]";
//never actually need the score but lets test
String score = new String[]{null, "none","distance","recipDistance"}[random().nextInt(4)];
double distDEG = DistanceUtils.dist2Degrees(distKM, DistanceUtils.EARTH_MEAN_RADIUS_KM);
String circleStr = "Circle(" + ptStr.replaceAll(" ", "") + " d=" + distDEG + ")";
String shapeStr;
if (exact) {
shapeStr = circleStr;
} else {//bbox
//the GEO is an assumption
SpatialContext ctx = SpatialContext.GEO;
shapeStr = ctx.toString( ctx.readShape(circleStr).getBoundingBox() );
}
//FYI default distErrPct=0.025 works with the tests in this file
assertQ(req(
"fl", "id", "q","*:*", "rows", "1000",
"fq", "{!field f=" + fieldName + (score==null?"":" score="+score)
+ "}Intersects(" + shapeStr + ")"),
tests);
}
@Test
public void testRangeSyntax() {
setupDocs();
//match docId 1
int docId = 1;
int count = 1;
boolean needScore = random().nextBoolean();//never actually need the score but lets test
assertQ(req(
"fl", "id", "q","*:*", "rows", "1000",
"fq", "{! needScore="+needScore+" df="+fieldName+"}[32,-80 TO 33,-79]"),//lower-left to upper-right
"//result/doc/*[@name='id'][.='" + docId + "']",
"*[count(//doc)=" + count + "]");
}
@Test
public void testSort() throws Exception {
assertU(adoc("id", "100", fieldName, "1,2"));
assertU(adoc("id", "101", fieldName, "4,-1"));
assertU(adoc("id", "999", fieldName, "70,70"));//far away from these queries
assertU(commit());
//test absence of score=distance means it doesn't score
assertJQ(req(
"q", fieldName +":\"Intersects(Circle(3,4 d=9))\"",
"fl","id,score")
, 1e-9
, "/response/docs/[0]/score==1.0"
, "/response/docs/[1]/score==1.0"
);
//score by distance
assertJQ(req(
"q", "{! score=distance}"+fieldName +":\"Intersects(Circle(3,4 d=9))\"",
"fl","id,score",
"sort","score asc")//want ascending due to increasing distance
, 1e-3
, "/response/docs/[0]/id=='100'"
, "/response/docs/[0]/score==2.827493"
, "/response/docs/[1]/id=='101'"
, "/response/docs/[1]/score==5.089807"
);
//score by recipDistance
assertJQ(req(
"q", "{! score=recipDistance}"+fieldName +":\"Intersects(Circle(3,4 d=9))\"",
"fl","id,score",
"sort","score desc")//want descending
, 1e-3
, "/response/docs/[0]/id=='100'"
, "/response/docs/[0]/score==0.3099695"
, "/response/docs/[1]/id=='101'"
, "/response/docs/[1]/score==0.19970943"
);
//query again with the query point closer to #101, and check the new ordering
assertJQ(req(
"q", "{! score=distance}"+fieldName +":\"Intersects(Circle(4,0 d=9))\"",
"fl","id,score",
"sort","score asc")//want ascending due to increasing distance
, 1e-4
, "/response/docs/[0]/id=='101'"
, "/response/docs/[1]/id=='100'"
);
//use sort=query(...)
assertJQ(req(
"q","-id:999",//exclude that doc
"fl","id,score",
"sort","query($sortQuery) asc", //want ascending due to increasing distance
"sortQuery", "{! score=distance}"+fieldName +":\"Intersects(Circle(3,4 d=9))\"" )
, 1e-4
, "/response/docs/[0]/id=='100'"
, "/response/docs/[1]/id=='101'" );
//check reversed direction with query point closer to #101
assertJQ(req(
"q","-id:999",//exclude that doc
"fl","id,score",
"sort","query($sortQuery) asc", //want ascending due to increasing distance
"sortQuery", "{! score=distance}"+fieldName +":\"Intersects(Circle(4,0 d=9))\"" )
, 1e-4
, "/response/docs/[0]/id=='101'"
, "/response/docs/[1]/id=='100'" );
}
@Test
public void testSortMultiVal() throws Exception {
RandomizedTest.assumeFalse("Multivalue not supported for this field", fieldName.equals("twodoubles"));
assertU(adoc("id", "100", fieldName, "1,2"));//1 point
assertU(adoc("id", "101", fieldName, "4,-1", fieldName, "3,5"));//2 points, 2nd is pretty close to query point
assertU(commit());
assertJQ(req(
"q", "{! score=distance}"+fieldName +":\"Intersects(Circle(3,4 d=9))\"",
"fl","id,score",
"sort","score asc")//want ascending due to increasing distance
, 1e-4
, "/response/docs/[0]/id=='101'"
, "/response/docs/[0]/score==0.99862987"//dist to 3,5
);
}
}

View File

@ -681,11 +681,12 @@
<!-- A specialized field for geospatial search. If indexed, this fieldType must not be multivalued. -->
<fieldType name="location" class="solr.LatLonType" subFieldSuffix="_coordinate"/>
<!--
A Geohash is a compact representation of a latitude longitude pair in a single field.
See http://wiki.apache.org/solr/SpatialSearch
-->
<fieldtype name="geohash" class="solr.GeoHashField"/>
<!-- An alternative geospatial field type new to Solr 4. It supports multiValued and polygon shapes.
For more information about this and other Spatial fields new to Solr 4, see:
http://wiki.apache.org/solr/SolrAdaptersForLuceneSpatial4
-->
<fieldType name="location_rpt" class="solr.SpatialRecursivePrefixTreeFieldType"
geo="true" distErrPct="0.025" maxDetailDist="0.000009" />
<!-- Money/currency field type. See http://wiki.apache.org/solr/MoneyFieldType
Parameters:
@ -693,7 +694,7 @@
precisionStep: Specifies the precisionStep for the TrieLong field used for the amount
providerClass: Lets you plug in other exchange provider backend:
solr.FileExchangeRateProvider is the default and takes one parameter:
currencyConfig: name of an xml file holding exhange rates
currencyConfig: name of an xml file holding exchange rates
solr.OpenExchangeRatesOrgProvider uses rates from openexchangerates.org:
ratesFileLocation: URL or path to rates JSON file (default latest.json on the web)
refreshInterval: Number of minutes between each rates fetch (default: 1440, min: 60)