mirror of https://github.com/apache/lucene.git
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:
parent
eb122170c7
commit
bf05f70a0b
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue