diff --git a/solr/core/src/java/org/apache/solr/schema/AbstractSpatialFieldType.java b/solr/core/src/java/org/apache/solr/schema/AbstractSpatialFieldType.java new file mode 100644 index 00000000000..96facfbc9b0 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/schema/AbstractSpatialFieldType.java @@ -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 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 fieldStrategyMap = new ConcurrentHashMap(); + + @Override + protected void init(IndexSchema schema, Map args) { + super.init(schema, args); + + //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()); + + 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()); + } +} + + diff --git a/solr/core/src/java/org/apache/solr/schema/AbstractSpatialPrefixTreeFieldType.java b/solr/core/src/java/org/apache/solr/schema/AbstractSpatialPrefixTreeFieldType.java new file mode 100644 index 00000000000..7297ba495c3 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/schema/AbstractSpatialPrefixTreeFieldType.java @@ -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 extends AbstractSpatialFieldType { + + /** @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 args) { + super.init(schema, args); + + //Solr expects us to remove the parameters we've used. + MapListener argsWrap = new MapListener(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); + +} diff --git a/solr/core/src/java/org/apache/solr/schema/SpatialRecursivePrefixTreeFieldType.java b/solr/core/src/java/org/apache/solr/schema/SpatialRecursivePrefixTreeFieldType.java new file mode 100644 index 00000000000..51c5a49fcbe --- /dev/null +++ b/solr/core/src/java/org/apache/solr/schema/SpatialRecursivePrefixTreeFieldType.java @@ -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 { + + /** @see RecursivePrefixTreeStrategy#setPrefixGridScanLevel(int) */ + public static final String PREFIX_GRID_SCAN_LEVEL = "prefixGridScanLevel"; + + private Integer prefixGridScanLevel; + + @Override + protected void init(IndexSchema schema, Map 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; + } +} + diff --git a/solr/core/src/java/org/apache/solr/schema/SpatialTermQueryPrefixTreeFieldType.java b/solr/core/src/java/org/apache/solr/schema/SpatialTermQueryPrefixTreeFieldType.java new file mode 100644 index 00000000000..a75f1c95cfe --- /dev/null +++ b/solr/core/src/java/org/apache/solr/schema/SpatialTermQueryPrefixTreeFieldType.java @@ -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 { + + @Override + protected TermQueryPrefixTreeStrategy newPrefixTreeStrategy(String fieldName) { + return new TermQueryPrefixTreeStrategy(grid,fieldName); + } +} + diff --git a/solr/core/src/java/org/apache/solr/schema/SpatialTwoDoublesFieldType.java b/solr/core/src/java/org/apache/solr/schema/SpatialTwoDoublesFieldType.java new file mode 100644 index 00000000000..0db9b6ebdda --- /dev/null +++ b/solr/core/src/java/org/apache/solr/schema/SpatialTwoDoublesFieldType.java @@ -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 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 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 newFields = new ArrayList(); + 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; + } + +} + diff --git a/solr/core/src/java/org/apache/solr/util/MapListener.java b/solr/core/src/java/org/apache/solr/util/MapListener.java new file mode 100644 index 00000000000..c6b4b375cb8 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/util/MapListener.java @@ -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 extends ForwardingMap { + private final Map target; + private final Set seenKeys; + + public MapListener(Map target) { + this.target = target; + seenKeys = new HashSet(target.size()); + } + + public Set 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 delegate() { + return target; + } +} diff --git a/solr/core/src/test-files/solr/collection1/conf/schema-spatial.xml b/solr/core/src/test-files/solr/collection1/conf/schema-spatial.xml new file mode 100644 index 00000000000..e094c7238f4 --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/schema-spatial.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id + + diff --git a/solr/core/src/test/org/apache/solr/search/TestSolr4Spatial.java b/solr/core/src/test/org/apache/solr/search/TestSolr4Spatial.java new file mode 100644 index 00000000000..6c73fdb9be1 --- /dev/null +++ b/solr/core/src/test/org/apache/solr/search/TestSolr4Spatial.java @@ -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 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 + ); + } + +} diff --git a/solr/example/solr/collection1/conf/schema.xml b/solr/example/solr/collection1/conf/schema.xml index eb6303b69ff..b34f9969075 100755 --- a/solr/example/solr/collection1/conf/schema.xml +++ b/solr/example/solr/collection1/conf/schema.xml @@ -681,11 +681,12 @@ - - + +