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"}, {"pointvector"}
});
}
@BeforeClass
public static void beforeClass() throws Exception {
initCore("solrconfig-basic.xml", "schema-spatial.xml");
}
@Before
public void setUp() throws Exception {
super.setUp();
clearIndex();
assertU(commit());
}
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("pointvector"));
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
);
}
}