blob: e68f53685b65f46c00fda417f5707cf65531081d [file] [log] [blame]
/*
* 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.search;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import com.carrotsearch.randomizedtesting.annotations.Repeat;
import org.apache.lucene.geo.GeoTestUtil;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.params.FacetParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.metrics.MetricsMap;
import org.apache.solr.metrics.SolrMetricManager;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.util.SpatialUtils;
import org.apache.solr.util.TestUtils;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.distance.DistanceUtils;
import org.locationtech.spatial4j.shape.Point;
//Unlike TestSolr4Spatial, not parametrized / not generic.
public class TestSolr4Spatial2 extends SolrTestCaseJ4 {
@BeforeClass
public static void beforeClass() throws Exception {
initCore("solrconfig-spatial.xml", "schema-spatial.xml");
}
@Override
@Before
public void setUp() throws Exception {
super.setUp();
clearIndex();
RetrievalCombo.idCounter = 0;
}
@Test
public void testQuadTreeRobustness() {
assertU(adoc("id", "0", "oslocation", "244502.06 639062.07"));
// old (pre 8.3.0) still works
assertU(adoc("id", "0", "oslocationold", "244502.06 639062.07"));
}
@Test
public void testBBox() throws Exception {
String fieldName = random().nextBoolean() ? "bbox" : "bboxD_dynamic";
assertU(adoc("id", "0"));//nothing
assertU(adoc("id", "1", fieldName, "ENVELOPE(-10, 20, 15, 10)"));
assertU(adoc("id", "2", fieldName, "ENVELOPE(22, 22, 10, 10)"));//pt
assertU(commit());
assertJQ(req("q", "{!field f="+fieldName+" filter=false score=overlapRatio " +
"queryTargetProportion=0.25}" +
"Intersects(ENVELOPE(10,25,12,10))",
"fl", "*,score",
"debug", "results"),//explain info
"/response/docs/[0]/id=='2'",
"/response/docs/[0]/score==0.75]",
"/response/docs/[1]/id=='1'",
"/response/docs/[1]/score==0.26666668]",
"/response/docs/[2]/id=='0'",
"/response/docs/[2]/score==0.0",
"/response/docs/[1]/" + fieldName + "=='ENVELOPE(-10, 20, 15, 10)'"//stored value
);
//minSideLength with point query
assertJQ(req("q", "{!field f="+fieldName+" filter=false score=overlapRatio " +
"queryTargetProportion=0.5 minSideLength=1}" +
"Intersects(ENVELOPE(0,0,12,12))",//pt
"fl", "id,score",
"debug", "results"),//explain info
"/response/docs/[0]/id=='1'",
"/response/docs/[0]/score==0.50333333]"//just over 0.5
);
//area2D
assertJQ(req("q", "{!field f=" + fieldName + " filter=false score=area2D}" +
"Intersects(ENVELOPE(0,0,12,12))",//pt
"fl", "id,score",
"debug", "results"),//explain info
"/response/docs/[0]/id=='1'",
"/response/docs/[0]/score==" + (30f * 5f) + "]"//150
);
//area (not 2D)
assertJQ(req("q", "{!field f=" + fieldName + " filter=false score=area}" +
"Intersects(ENVELOPE(0,0,12,12))",//pt
"fl", "id,score",
"debug", "results"),//explain info
"/response/docs/[0]/id=='1'",
"/response/docs/[0]/score==" + 146.39793f + "]"//a bit less than 150
);
}
@Test
public void testBadScoreParam() throws Exception {
String fieldName = "bbox";
assertQEx("expect friendly error message",
"area2D",
req("{!field f=" + fieldName + " filter=false score=bogus}Intersects(ENVELOPE(0,0,12,12))"),
SolrException.ErrorCode.BAD_REQUEST);
}
@Test
public void testRptWithGeometryField() throws Exception {
//note: fails with "srpt_geohash" because it's not as precise
final boolean testCache = true;
final boolean testHeatmap = true;
final boolean testPolygon = false; // default spatialContext doesn't handle this
testRptWithGeometryField("srptgeom", testCache, testHeatmap, testPolygon);
}
@Test
public void testRptWithGeometryGeo3dField() throws Exception {
final boolean testCache = true;
final boolean testHeatmap = true;
final boolean testPolygon = true;
testRptWithGeometryField("srptgeom_geo3d", testCache, testHeatmap, testPolygon);
}
@Test
public void testRptWithGeometryGeo3dS2Field() throws Exception {
final boolean testCache = false; // the test data is designed to provoke the cache for non-S2
final boolean testHeatmap = false; // incompatible
final boolean testPolygon = true;
testRptWithGeometryField("srptgeom_s2_geo3d", testCache, testHeatmap, testPolygon);
}
@Test @Repeat(iterations = 10)
public void testLLPDecodeIsStableAndPrecise() throws Exception {
// test that LatLonPointSpatialField decode of docValue will round-trip (re-index then re-decode) to the same value
@SuppressWarnings({"resource", "IOResourceOpenedButNotSafelyClosed"})
SolrClient client = new EmbeddedSolrServer(h.getCore());// do NOT close it; it will close Solr
final String fld = "llp_1_dv_dvasst";
String ptOrig = GeoTestUtil.nextLatitude() + "," + GeoTestUtil.nextLongitude();
assertU(adoc("id", "0", fld, ptOrig));
assertU(commit());
// retrieve it (probably less precision)
String ptDecoded1 = (String) client.query(params("q", "id:0")).getResults().get(0).get(fld);
// now write it back
assertU(adoc("id", "0", fld, ptDecoded1));
assertU(commit());
// retrieve it; assert that it's the same as written
String ptDecoded2 = (String) client.query(params("q", "id:0")).getResults().get(0).get(fld);
assertEquals("orig:" + ptOrig, ptDecoded1, ptDecoded2);
// test that the representation is pretty accurate
final Point ptOrigObj = SpatialUtils.parsePoint(ptOrig, SpatialContext.GEO);
final Point ptDecodedObj = SpatialUtils.parsePoint(ptDecoded1, SpatialContext.GEO);
double deltaCentimeters = SpatialContext.GEO.calcDistance(ptOrigObj, ptDecodedObj) * DistanceUtils.DEG_TO_KM * 1000.0 * 100.0;
//See javadocs of LatLonDocValuesField for these constants
final Point absErrorPt = SpatialContext.GEO.getShapeFactory().pointXY(8.381903171539307E-8, 4.190951585769653E-8);
double deltaCentimetersMax
= SpatialContext.GEO.calcDistance(absErrorPt, 0,0) * DistanceUtils.DEG_TO_KM * 1000.0 * 100.0;
assertEquals(1.0420371840922256, deltaCentimetersMax, 0.0);// just so that we see it in black & white in the test
//max found by trial & error. If we used 8 decimal places then we could get down to 1.04cm accuracy but then we
// lose the ability to round-trip -- 40 would become 39.99999997 (ugh).
assertTrue("deltaCm too high: " + deltaCentimeters, deltaCentimeters < 1.41);
// Pt(x=105.29894270124083,y=-0.4371673760042398) to Pt(x=105.2989428,y=-0.4371673) is 1.38568
}
@Test
public void testLatLonRetrieval() throws Exception {
final String ptHighPrecision = "40.2996543270,-74.0824956673";
final String ptLossOfPrecision = "40.2996544,-74.0824957"; // rounded version of the one above, losing precision
// "_1" is single, "_N" is multiValued
// "_dv" is docValues (otherwise not), "_dvasst" is useDocValuesAsStored (otherwise not)
// "_st" is stored" (otherwise not)
// a random point using the number of decimal places we support for round-tripping.
String randPointStr =
new BigDecimal(GeoTestUtil.nextLatitude()).setScale(7, BigDecimal.ROUND_HALF_UP).stripTrailingZeros().toPlainString() +
"," + new BigDecimal(GeoTestUtil.nextLongitude()).setScale(7, BigDecimal.ROUND_HALF_UP).stripTrailingZeros().toPlainString();
List<RetrievalCombo> combos = Arrays.asList(
new RetrievalCombo("llp_1_dv_st", ptHighPrecision),
new RetrievalCombo("llp_N_dv_st", Arrays.asList("-40,40", "-45,45")),
new RetrievalCombo("llp_N_dv_st", Arrays.asList("-40,40")), // multiValued but 1 value
new RetrievalCombo("llp_1_dv_dvasst", ptHighPrecision, ptLossOfPrecision),
// this one comes back in a different order since it gets sorted low to high
new RetrievalCombo("llp_N_dv_dvasst", Arrays.asList("-40,40", "-45,45"), Arrays.asList("-45,45", "-40,40")),
new RetrievalCombo("llp_N_dv_dvasst", Arrays.asList(randPointStr)), // multiValued but 1 value
// edge cases. (note we sorted it as Lucene will internally)
new RetrievalCombo("llp_N_dv_dvasst", Arrays.asList(
"-90,180", "-90,-180",
"0,0", "0,180", "0,-180",
"90,0", "90,180", "90,-180")),
new RetrievalCombo("llp_1_dv", ptHighPrecision, ptLossOfPrecision),
new RetrievalCombo("llp_N_dv", Arrays.asList("-45,45", "-40,40"))
);
Collections.shuffle(combos, random());
// add and commit
for (RetrievalCombo combo : combos) {
SolrInputDocument doc = new SolrInputDocument();
doc.addField("id", "" + combo.id);
for (String indexValue : combo.indexValues) {
doc.addField(combo.fieldName, indexValue);
}
assertU(adoc(doc));
if (TestUtils.rarely()) { // induce segments to potentially change internal behavior
assertU(commit());
}
}
assertU(commit());
// create an assertJQ assertion string, once for fl=*, another for when the field is listed
List<String> assertJQsFlListed = new ArrayList<>();
List<String> assertJQsFlStar = new ArrayList<>();
for (RetrievalCombo combo : combos) {
String expect = "response/docs/[" + combo.id + "]/" + combo.fieldName + "==" + combo.expectReturnJSON;
assertJQsFlListed.add(expect);
if (combo.fieldName.endsWith("_dv")) {
expect = "response/docs/[" + combo.id + "]=={'id':'" + combo.id + "'}"; // only the id, nothing else
}
assertJQsFlStar.add(expect);
}
// check
assertJQ(req("q","*:*", "sort", "id asc",
"fl","*"),
assertJQsFlStar.toArray(new String[0]));
assertJQ(req("q","*:*", "sort", "id asc",
"fl", "id," + combos.stream().map(c -> c.fieldName).collect(Collectors.joining(","))),
assertJQsFlListed.toArray(new String[0]));
}
private static class RetrievalCombo {
static int idCounter = 0;
final int id = idCounter++;
final String fieldName;
final List<String> indexValues;
final String expectReturnJSON; //or null if not expected in response
RetrievalCombo(String fieldName, List<String> indexValues) { this(fieldName, indexValues, indexValues);}
RetrievalCombo(String fieldName, List<String> indexValues, List<String> returnValues) {
this.fieldName = fieldName;
this.indexValues = indexValues;
this.expectReturnJSON = returnValues.stream().collect(Collectors.joining("', '", "['", "']"));
}
RetrievalCombo(String fieldName, String indexValue) { this(fieldName, indexValue, indexValue); }
RetrievalCombo(String fieldName, String indexValue, String returnValue) {
this.fieldName = fieldName;
this.indexValues = Collections.singletonList(indexValue);
this.expectReturnJSON = "'" + returnValue + "'";
}
}
private void testRptWithGeometryField(String fieldName, boolean testCache, boolean testHeatmap, boolean testPolygon) throws Exception {
assertU(adoc("id", "0", fieldName, "ENVELOPE(-10, 20, 15, 10)"));
assertU(adoc("id", "1", fieldName, "BUFFER(POINT(-10 15), 5)"));//circle at top-left corner
assertU(optimize("maxSegments", "1"));// one segment.
assertU(commit());
// Search to the edge but not quite touching the indexed envelope of id=0. It requires geom validation to
// eliminate id=0. id=1 is found and doesn't require validation. cache=false means no query cache.
final SolrQueryRequest sameReq = req(
"q", "{!cache=false field f=" + fieldName + "}Intersects(ENVELOPE(-20, -10.0001, 30, 15.0001))",
"sort", "id asc");
assertJQ(sameReq, "/response/numFound==1", "/response/docs/[0]/id=='1'");
if (testCache) {
// The tricky thing is verifying the cache works correctly...
MetricsMap cacheMetrics = (MetricsMap) ((SolrMetricManager.GaugeWrapper)h.getCore().getCoreMetricManager().getRegistry().getMetrics().get("CACHE.searcher.perSegSpatialFieldCache_" + fieldName)).getGauge();
assertEquals("1", cacheMetrics.getValue().get("cumulative_inserts").toString());
assertEquals("0", cacheMetrics.getValue().get("cumulative_hits").toString());
// Repeat the query earlier
assertJQ(sameReq, "/response/numFound==1", "/response/docs/[0]/id=='1'");
assertEquals("1", cacheMetrics.getValue().get("cumulative_hits").toString());
assertEquals("1 segment",
1, getSearcher().getRawReader().leaves().size());
// Get key of first leaf reader -- this one contains the match for sure.
Object leafKey1 = getFirstLeafReaderKey();
// add new segment
assertU(adoc("id", "3"));
assertU(commit()); // sometimes merges (to one seg), sometimes won't
// can still find the same document
assertJQ(sameReq, "/response/numFound==1", "/response/docs/[0]/id=='1'");
// When there are new segments, we accumulate another hit. This tests the cache was not blown away on commit.
// (i.e. the cache instance is new but it should've been regenerated from the old one).
// Checking equality for the first reader's cache key indicates whether the cache should still be valid.
Object leafKey2 = getFirstLeafReaderKey();
// get the current instance of metrics - the old one may not represent the current cache instance
cacheMetrics = (MetricsMap) ((SolrMetricManager.GaugeWrapper)h.getCore().getCoreMetricManager().getRegistry().getMetrics().get("CACHE.searcher.perSegSpatialFieldCache_" + fieldName)).getGauge();
assertEquals(leafKey1.equals(leafKey2) ? "2" : "1", cacheMetrics.getValue().get("cumulative_hits").toString());
}
if (testHeatmap) {
// Now try to see if heatmaps work:
assertJQ(req("q", "*:*", "facet", "true", FacetParams.FACET_HEATMAP, fieldName, "json.nl", "map"),
"/facet_counts/facet_heatmaps/" + fieldName + "/minX==-180.0");
}
if (testPolygon) {
String polygonWKT = "POLYGON((-11 12, -11 11, 10.5 12, -11 12))"; //right-angle triangle. Counter-clockwise order
assertJQ(req(
"q", "{!cache=false field f=" + fieldName + "}Intersects(" + polygonWKT + ")",
"sort", "id asc"), "/response/numFound==2");
assertU(adoc("id", "9",
fieldName, "POLYGON((" + // rectangle. Counter-clockwise order.
"-118.080201721669 54.5864541583249," +
"-118.080078279314 54.5864541583249," +
"-118.080078279314 54.5865258517606," +
"-118.080201721669 54.5865258517606," +
"-118.080201721669 54.5864541583249))" ));
assertU(commit());
// should NOT match
assertJQ(req("q", fieldName+":[55.0260828,-115.5085624 TO 55.02646,-115.507337]"),
"/response/numFound==0");
}
}
protected SolrIndexSearcher getSearcher() {
// neat trick; needn't deal with the hassle RefCounted
return (SolrIndexSearcher) h.getCore().getInfoRegistry().get("searcher");
}
protected Object getFirstLeafReaderKey() {
return getSearcher().getRawReader().leaves().get(0).reader().getCoreCacheHelper().getKey();
}
@Test// SOLR-8541
public void testConstantScoreQueryWithFilterPartOnly() {
final String[] doc1 = {"id", "1", "srptgeom", "56.9485,24.0980"};
assertU(adoc(doc1));
assertU(commit());
ModifiableSolrParams params = new ModifiableSolrParams();
params.add("q", "{!geofilt sfield=\"srptgeom\" pt=\"56.9484,24.0981\" d=100}");
params.add("hl", "true");
params.add("hl.fl", "srptgeom");
assertQ(req(params), "*[count(//doc)=1]", "count(//lst[@name='highlighting']/*)=1");
}
@Test // SOLR-14802
public void testGeodistSortPossibleWithLatLonPointSpatialFieldOrSpatialRecursivePrefixTreeField() throws Exception {
assertU(adoc("id", "1", "llp", "53.4721936,-2.24703", "srpt_quad", "53.425272,-2.322356"));
assertU(commit());
assertJQ(req(
"q", "*:*",
"fq", "{!geofilt}",
"d", "50",
"pt", "53.4721936,-2.24703",
"sfield", "srpt_quad",
"sort", "min(geodist(),geodist(llp,53.4721936,-2.24703)) asc"
),
"/response/docs/[0]/id=='1'");
assertJQ(req(
"q", "*:*",
"fq", "{!geofilt}",
"d", "50",
"pt", "53.4721936,-2.24703",
"sfield", "srpt_quad",
"sort", "min(geodist(),geodist(53.4721936,-2.24703,llp)) asc" // moved llp to the end
),
"/response/docs/[0]/id=='1'");
assertJQ(req(
"q", "*:*",
"fq", "{!geofilt}",
"d", "50",
"pt", "53.4721936,-2.24703",
"sfield", "llp", // trying another field type
"sort", "min(geodist(),geodist(53.4721936,-2.24703,srpt_quad)) asc"
),
"/response/docs/[0]/id=='1'");
}
@Test // SOLR-14802
public void testGeodistSortOrderCorrectWithLatLonPointSpatialFieldAndSpatialRecursivePrefixTreeField() throws Exception {
assertU(adoc("id", "1", "llp", "53.4721936,-2.24703", "srpt_quad", "53.4721936,-2.24703"));
assertU(adoc("id", "2", "llp", "53.425272,-2.322356", "srpt_quad", "55.4721936,-2.24703"));
assertU(commit());
assertJQ(req(
"q", "*:*",
"fq", "{!geofilt}",
"d", "50",
"pt", "53.431669,-2.318720",
"sfield", "srpt_quad",
"sort", "min(geodist(),geodist(llp,53.431669,-2.318720)) asc"
),
"/response/docs/[0]/id=='2'");
assertJQ(req(
"q", "*:*",
"fq", "{!geofilt}",
"d", "50",
"pt", "53.4721936,-2.24703",
"sfield", "srpt_quad",
"sort", "min(geodist(),geodist(53.4721936,-2.24703,llp)) asc"
),
"/response/docs/[0]/id=='1'");
assertJQ(req(
"q", "*:*",
"fq", "{!geofilt}",
"d", "50",
"pt", "55.4721936,-2.24703",
"sfield", "srpt_quad",
"sort", "min(geodist(),geodist(55.4721936,-2.24703,llp)) asc"
),
"/response/docs/[0]/id=='2'");
}
}