mirror of https://github.com/apache/lucene.git
LUCENE-6780: add missing classes
git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1709927 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
d369057766
commit
91e6bef3c7
|
@ -0,0 +1,67 @@
|
||||||
|
package org.apache.lucene.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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Represents a lat/lon rectangle. */
|
||||||
|
public class GeoRect {
|
||||||
|
public final double minLon;
|
||||||
|
public final double maxLon;
|
||||||
|
public final double minLat;
|
||||||
|
public final double maxLat;
|
||||||
|
|
||||||
|
public GeoRect(double minLon, double maxLon, double minLat, double maxLat) {
|
||||||
|
if (GeoUtils.isValidLon(minLon) == false) {
|
||||||
|
throw new IllegalArgumentException("invalid minLon " + minLon);
|
||||||
|
}
|
||||||
|
if (GeoUtils.isValidLon(maxLon) == false) {
|
||||||
|
throw new IllegalArgumentException("invalid maxLon " + maxLon);
|
||||||
|
}
|
||||||
|
if (GeoUtils.isValidLat(minLat) == false) {
|
||||||
|
throw new IllegalArgumentException("invalid minLat " + minLat);
|
||||||
|
}
|
||||||
|
if (GeoUtils.isValidLat(maxLat) == false) {
|
||||||
|
throw new IllegalArgumentException("invalid maxLat " + maxLat);
|
||||||
|
}
|
||||||
|
this.minLon = minLon;
|
||||||
|
this.maxLon = maxLon;
|
||||||
|
this.minLat = minLat;
|
||||||
|
this.maxLat = maxLat;
|
||||||
|
assert maxLat >= minLat;
|
||||||
|
|
||||||
|
// NOTE: cannot assert maxLon >= minLon since this rect could cross the dateline
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder b = new StringBuilder();
|
||||||
|
b.append("GeoRect(lon=");
|
||||||
|
b.append(minLon);
|
||||||
|
b.append(" TO ");
|
||||||
|
b.append(maxLon);
|
||||||
|
if (maxLon < minLon) {
|
||||||
|
b.append(" (crosses dateline!)");
|
||||||
|
}
|
||||||
|
b.append(" lat=");
|
||||||
|
b.append(minLat);
|
||||||
|
b.append(" TO ");
|
||||||
|
b.append(maxLat);
|
||||||
|
b.append(")");
|
||||||
|
|
||||||
|
return b.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,759 @@
|
||||||
|
package org.apache.lucene.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 java.io.IOException;
|
||||||
|
import java.text.DecimalFormat;
|
||||||
|
import java.text.DecimalFormatSymbols;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import org.apache.lucene.document.Document;
|
||||||
|
import org.apache.lucene.document.Field;
|
||||||
|
import org.apache.lucene.document.NumericDocValuesField;
|
||||||
|
import org.apache.lucene.index.DirectoryReader;
|
||||||
|
import org.apache.lucene.index.IndexReader;
|
||||||
|
import org.apache.lucene.index.IndexWriter;
|
||||||
|
import org.apache.lucene.index.IndexWriterConfig;
|
||||||
|
import org.apache.lucene.index.LeafReaderContext;
|
||||||
|
import org.apache.lucene.index.MultiDocValues;
|
||||||
|
import org.apache.lucene.index.NumericDocValues;
|
||||||
|
import org.apache.lucene.index.RandomIndexWriter;
|
||||||
|
import org.apache.lucene.index.Term;
|
||||||
|
import org.apache.lucene.search.IndexSearcher;
|
||||||
|
import org.apache.lucene.search.Query;
|
||||||
|
import org.apache.lucene.search.SimpleCollector;
|
||||||
|
import org.apache.lucene.store.Directory;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
|
||||||
|
// TODO: cutover TestGeoUtils too?
|
||||||
|
|
||||||
|
public abstract class BaseGeoPointTestCase extends LuceneTestCase {
|
||||||
|
|
||||||
|
protected static final String FIELD_NAME = "point";
|
||||||
|
|
||||||
|
private static final double LON_SCALE = (0x1L<<GeoUtils.BITS)/360.0D;
|
||||||
|
private static final double LAT_SCALE = (0x1L<<GeoUtils.BITS)/180.0D;
|
||||||
|
|
||||||
|
private static double originLat;
|
||||||
|
private static double originLon;
|
||||||
|
private static double lonRange;
|
||||||
|
private static double latRange;
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void beforeClassBase() throws Exception {
|
||||||
|
// Between 1.0 and 3.0:
|
||||||
|
lonRange = 2 * (random().nextDouble() + 0.5);
|
||||||
|
latRange = 2 * (random().nextDouble() + 0.5);
|
||||||
|
|
||||||
|
originLon = GeoUtils.normalizeLon(GeoUtils.MIN_LON_INCL + lonRange + (GeoUtils.MAX_LON_INCL - GeoUtils.MIN_LON_INCL - 2 * lonRange) * random().nextDouble());
|
||||||
|
originLat = GeoUtils.normalizeLat(GeoUtils.MIN_LAT_INCL + latRange + (GeoUtils.MAX_LAT_INCL - GeoUtils.MIN_LAT_INCL - 2 * latRange) * random().nextDouble());
|
||||||
|
}
|
||||||
|
|
||||||
|
// A particularly tricky adversary for BKD tree:
|
||||||
|
@Nightly
|
||||||
|
public void testSamePointManyTimes() throws Exception {
|
||||||
|
int numPoints = atLeast(1000);
|
||||||
|
// TODO: GeoUtils are potentially slow if we use small=false with heavy testing
|
||||||
|
boolean small = random().nextBoolean();
|
||||||
|
|
||||||
|
// Every doc has 2 points:
|
||||||
|
double theLat = randomLat(small);
|
||||||
|
double theLon = randomLon(small);
|
||||||
|
|
||||||
|
double[] lats = new double[numPoints];
|
||||||
|
Arrays.fill(lats, theLat);
|
||||||
|
|
||||||
|
double[] lons = new double[numPoints];
|
||||||
|
Arrays.fill(lons, theLon);
|
||||||
|
|
||||||
|
verify(small, lats, lons);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nightly
|
||||||
|
public void testAllLatEqual() throws Exception {
|
||||||
|
int numPoints = atLeast(10000);
|
||||||
|
// TODO: GeoUtils are potentially slow if we use small=false with heavy testing
|
||||||
|
// boolean small = random().nextBoolean();
|
||||||
|
boolean small = true;
|
||||||
|
double lat = randomLat(small);
|
||||||
|
double[] lats = new double[numPoints];
|
||||||
|
double[] lons = new double[numPoints];
|
||||||
|
|
||||||
|
boolean haveRealDoc = false;
|
||||||
|
|
||||||
|
for(int docID=0;docID<numPoints;docID++) {
|
||||||
|
int x = random().nextInt(20);
|
||||||
|
if (x == 17) {
|
||||||
|
// Some docs don't have a point:
|
||||||
|
lats[docID] = Double.NaN;
|
||||||
|
if (VERBOSE) {
|
||||||
|
System.out.println(" doc=" + docID + " is missing");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (docID > 0 && x == 14 && haveRealDoc) {
|
||||||
|
int oldDocID;
|
||||||
|
while (true) {
|
||||||
|
oldDocID = random().nextInt(docID);
|
||||||
|
if (Double.isNaN(lats[oldDocID]) == false) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fully identical point:
|
||||||
|
lons[docID] = lons[oldDocID];
|
||||||
|
if (VERBOSE) {
|
||||||
|
System.out.println(" doc=" + docID + " lat=" + lat + " lon=" + lons[docID] + " (same lat/lon as doc=" + oldDocID + ")");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lons[docID] = randomLon(small);
|
||||||
|
haveRealDoc = true;
|
||||||
|
if (VERBOSE) {
|
||||||
|
System.out.println(" doc=" + docID + " lat=" + lat + " lon=" + lons[docID]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lats[docID] = lat;
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(small, lats, lons);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nightly
|
||||||
|
public void testAllLonEqual() throws Exception {
|
||||||
|
int numPoints = atLeast(10000);
|
||||||
|
// TODO: GeoUtils are potentially slow if we use small=false with heavy testing
|
||||||
|
// boolean small = random().nextBoolean();
|
||||||
|
boolean small = true;
|
||||||
|
double theLon = randomLon(small);
|
||||||
|
double[] lats = new double[numPoints];
|
||||||
|
double[] lons = new double[numPoints];
|
||||||
|
|
||||||
|
boolean haveRealDoc = false;
|
||||||
|
|
||||||
|
//System.out.println("theLon=" + theLon);
|
||||||
|
|
||||||
|
for(int docID=0;docID<numPoints;docID++) {
|
||||||
|
int x = random().nextInt(20);
|
||||||
|
if (x == 17) {
|
||||||
|
// Some docs don't have a point:
|
||||||
|
lats[docID] = Double.NaN;
|
||||||
|
if (VERBOSE) {
|
||||||
|
System.out.println(" doc=" + docID + " is missing");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (docID > 0 && x == 14 && haveRealDoc) {
|
||||||
|
int oldDocID;
|
||||||
|
while (true) {
|
||||||
|
oldDocID = random().nextInt(docID);
|
||||||
|
if (Double.isNaN(lats[oldDocID]) == false) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fully identical point:
|
||||||
|
lats[docID] = lats[oldDocID];
|
||||||
|
if (VERBOSE) {
|
||||||
|
System.out.println(" doc=" + docID + " lat=" + lats[docID] + " lon=" + theLon + " (same lat/lon as doc=" + oldDocID + ")");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lats[docID] = randomLat(small);
|
||||||
|
haveRealDoc = true;
|
||||||
|
if (VERBOSE) {
|
||||||
|
System.out.println(" doc=" + docID + " lat=" + lats[docID] + " lon=" + theLon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lons[docID] = theLon;
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(small, lats, lons);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nightly
|
||||||
|
public void testMultiValued() throws Exception {
|
||||||
|
int numPoints = atLeast(10000);
|
||||||
|
// Every doc has 2 points:
|
||||||
|
double[] lats = new double[2*numPoints];
|
||||||
|
double[] lons = new double[2*numPoints];
|
||||||
|
Directory dir = newDirectory();
|
||||||
|
IndexWriterConfig iwc = newIndexWriterConfig();
|
||||||
|
initIndexWriterConfig(FIELD_NAME, iwc);
|
||||||
|
|
||||||
|
// We rely on docID order:
|
||||||
|
iwc.setMergePolicy(newLogMergePolicy());
|
||||||
|
RandomIndexWriter w = new RandomIndexWriter(random(), dir, iwc);
|
||||||
|
|
||||||
|
// TODO: GeoUtils are potentially slow if we use small=false with heavy testing
|
||||||
|
boolean small = random().nextBoolean();
|
||||||
|
//boolean small = true;
|
||||||
|
|
||||||
|
for (int id=0;id<numPoints;id++) {
|
||||||
|
Document doc = new Document();
|
||||||
|
lats[2*id] = randomLat(small);
|
||||||
|
lons[2*id] = randomLon(small);
|
||||||
|
doc.add(newStringField("id", ""+id, Field.Store.YES));
|
||||||
|
addPointToDoc(FIELD_NAME, doc, lats[2*id], lons[2*id]);
|
||||||
|
lats[2*id+1] = randomLat(small);
|
||||||
|
lons[2*id+1] = randomLon(small);
|
||||||
|
addPointToDoc(FIELD_NAME, doc, lats[2*id+1], lons[2*id+1]);
|
||||||
|
|
||||||
|
if (VERBOSE) {
|
||||||
|
System.out.println("id=" + id);
|
||||||
|
System.out.println(" lat=" + lats[2*id] + " lon=" + lons[2*id]);
|
||||||
|
System.out.println(" lat=" + lats[2*id+1] + " lon=" + lons[2*id+1]);
|
||||||
|
}
|
||||||
|
w.addDocument(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (random().nextBoolean()) {
|
||||||
|
w.forceMerge(1);
|
||||||
|
}
|
||||||
|
IndexReader r = w.getReader();
|
||||||
|
w.close();
|
||||||
|
|
||||||
|
// We can't wrap with "exotic" readers because the BKD query must see the BKDDVFormat:
|
||||||
|
IndexSearcher s = newSearcher(r, false);
|
||||||
|
|
||||||
|
int iters = atLeast(75);
|
||||||
|
for (int iter=0;iter<iters;iter++) {
|
||||||
|
GeoRect rect = randomRect(small, small == false);
|
||||||
|
|
||||||
|
if (VERBOSE) {
|
||||||
|
System.out.println("\nTEST: iter=" + iter + " bbox=" + rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
Query query = newBBoxQuery(FIELD_NAME, rect);
|
||||||
|
|
||||||
|
final FixedBitSet hits = new FixedBitSet(r.maxDoc());
|
||||||
|
s.search(query, new SimpleCollector() {
|
||||||
|
|
||||||
|
private int docBase;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean needsScores() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doSetNextReader(LeafReaderContext context) throws IOException {
|
||||||
|
docBase = context.docBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void collect(int doc) {
|
||||||
|
hits.set(docBase+doc);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
boolean fail = false;
|
||||||
|
|
||||||
|
for(int docID=0;docID<lats.length/2;docID++) {
|
||||||
|
double latDoc1 = lats[2*docID];
|
||||||
|
double lonDoc1 = lons[2*docID];
|
||||||
|
double latDoc2 = lats[2*docID+1];
|
||||||
|
double lonDoc2 = lons[2*docID+1];
|
||||||
|
|
||||||
|
Boolean result1 = rectContainsPoint(rect, latDoc1, lonDoc1);
|
||||||
|
if (result1 == null) {
|
||||||
|
// borderline case: cannot test
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Boolean result2 = rectContainsPoint(rect, latDoc2, lonDoc2);
|
||||||
|
if (result2 == null) {
|
||||||
|
// borderline case: cannot test
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean expected = result1 == Boolean.TRUE || result2 == Boolean.TRUE;
|
||||||
|
|
||||||
|
if (hits.get(docID) != expected) {
|
||||||
|
String id = s.doc(docID).get("id");
|
||||||
|
if (expected) {
|
||||||
|
System.out.println(Thread.currentThread().getName() + ": id=" + id + " docID=" + docID + " should match but did not");
|
||||||
|
} else {
|
||||||
|
System.out.println(Thread.currentThread().getName() + ": id=" + id + " docID=" + docID + " should not match but did");
|
||||||
|
}
|
||||||
|
System.out.println(" rect=" + rect);
|
||||||
|
System.out.println(" lat=" + latDoc1 + " lon=" + lonDoc1 + "\n lat=" + latDoc2 + " lon=" + lonDoc2);
|
||||||
|
System.out.println(" result1=" + result1 + " result2=" + result2);
|
||||||
|
fail = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fail) {
|
||||||
|
fail("some hits were wrong");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.close();
|
||||||
|
dir.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testRandomTiny() throws Exception {
|
||||||
|
// Make sure single-leaf-node case is OK:
|
||||||
|
doTestRandom(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testRandomMedium() throws Exception {
|
||||||
|
doTestRandom(10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nightly
|
||||||
|
public void testRandomBig() throws Exception {
|
||||||
|
doTestRandom(200000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doTestRandom(int count) throws Exception {
|
||||||
|
|
||||||
|
int numPoints = atLeast(count);
|
||||||
|
|
||||||
|
if (VERBOSE) {
|
||||||
|
System.out.println("TEST: numPoints=" + numPoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
double[] lats = new double[numPoints];
|
||||||
|
double[] lons = new double[numPoints];
|
||||||
|
|
||||||
|
// TODO: GeoUtils are potentially slow if we use small=false with heavy testing
|
||||||
|
boolean small = random().nextBoolean();
|
||||||
|
|
||||||
|
boolean haveRealDoc = false;
|
||||||
|
|
||||||
|
for (int id=0;id<numPoints;id++) {
|
||||||
|
int x = random().nextInt(20);
|
||||||
|
if (x == 17) {
|
||||||
|
// Some docs don't have a point:
|
||||||
|
lats[id] = Double.NaN;
|
||||||
|
if (VERBOSE) {
|
||||||
|
System.out.println(" id=" + id + " is missing");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id > 0 && x < 3 && haveRealDoc) {
|
||||||
|
int oldID;
|
||||||
|
while (true) {
|
||||||
|
oldID = random().nextInt(id);
|
||||||
|
if (Double.isNaN(lats[oldID]) == false) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x == 0) {
|
||||||
|
// Identical lat to old point
|
||||||
|
lats[id] = lats[oldID];
|
||||||
|
lons[id] = randomLon(small);
|
||||||
|
if (VERBOSE) {
|
||||||
|
System.out.println(" id=" + id + " lat=" + lats[id] + " lon=" + lons[id] + " (same lat as doc=" + oldID + ")");
|
||||||
|
}
|
||||||
|
} else if (x == 1) {
|
||||||
|
// Identical lon to old point
|
||||||
|
lats[id] = randomLat(small);
|
||||||
|
lons[id] = lons[oldID];
|
||||||
|
if (VERBOSE) {
|
||||||
|
System.out.println(" id=" + id + " lat=" + lats[id] + " lon=" + lons[id] + " (same lon as doc=" + oldID + ")");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert x == 2;
|
||||||
|
// Fully identical point:
|
||||||
|
lats[id] = lats[oldID];
|
||||||
|
lons[id] = lons[oldID];
|
||||||
|
if (VERBOSE) {
|
||||||
|
System.out.println(" id=" + id + " lat=" + lats[id] + " lon=" + lons[id] + " (same lat/lon as doc=" + oldID + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lats[id] = randomLat(small);
|
||||||
|
lons[id] = randomLon(small);
|
||||||
|
haveRealDoc = true;
|
||||||
|
if (VERBOSE) {
|
||||||
|
System.out.println(" id=" + id + " lat=" + lats[id] + " lon=" + lons[id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(small, lats, lons);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long scaleLon(final double val) {
|
||||||
|
return (long) ((val-GeoUtils.MIN_LON_INCL) * LON_SCALE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long scaleLat(final double val) {
|
||||||
|
return (long) ((val-GeoUtils.MIN_LAT_INCL) * LAT_SCALE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double unscaleLon(final long val) {
|
||||||
|
return (val / LON_SCALE) + GeoUtils.MIN_LON_INCL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double unscaleLat(final long val) {
|
||||||
|
return (val / LAT_SCALE) + GeoUtils.MIN_LAT_INCL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double randomLat(boolean small) {
|
||||||
|
double result;
|
||||||
|
if (small) {
|
||||||
|
result = GeoUtils.normalizeLat(originLat + latRange * (random().nextDouble() - 0.5));
|
||||||
|
} else {
|
||||||
|
result = -90 + 180.0 * random().nextDouble();
|
||||||
|
}
|
||||||
|
return unscaleLat(scaleLat(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
public double randomLon(boolean small) {
|
||||||
|
double result;
|
||||||
|
if (small) {
|
||||||
|
result = GeoUtils.normalizeLon(originLon + lonRange * (random().nextDouble() - 0.5));
|
||||||
|
} else {
|
||||||
|
result = -180 + 360.0 * random().nextDouble();
|
||||||
|
}
|
||||||
|
return unscaleLon(scaleLon(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected GeoRect randomRect(boolean small, boolean canCrossDateLine) {
|
||||||
|
double lat0 = randomLat(small);
|
||||||
|
double lat1 = randomLat(small);
|
||||||
|
double lon0 = randomLon(small);
|
||||||
|
double lon1 = randomLon(small);
|
||||||
|
|
||||||
|
if (lat1 < lat0) {
|
||||||
|
double x = lat0;
|
||||||
|
lat0 = lat1;
|
||||||
|
lat1 = x;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canCrossDateLine == false && lon1 < lon0) {
|
||||||
|
double x = lon0;
|
||||||
|
lon0 = lon1;
|
||||||
|
lon1 = x;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GeoRect(lon0, lon1, lat0, lat1);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void initIndexWriterConfig(String field, IndexWriterConfig iwc) {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void addPointToDoc(String field, Document doc, double lat, double lon);
|
||||||
|
|
||||||
|
protected abstract Query newBBoxQuery(String field, GeoRect bbox);
|
||||||
|
|
||||||
|
protected abstract Query newDistanceQuery(String field, double centerLat, double centerLon, double radiusMeters);
|
||||||
|
|
||||||
|
protected abstract Query newDistanceRangeQuery(String field, double centerLat, double centerLon, double minRadiusMeters, double radiusMeters);
|
||||||
|
|
||||||
|
protected abstract Query newPolygonQuery(String field, double[] lats, double[] lons);
|
||||||
|
|
||||||
|
/** Returns null if it's borderline case */
|
||||||
|
protected abstract Boolean rectContainsPoint(GeoRect rect, double pointLat, double pointLon);
|
||||||
|
|
||||||
|
/** Returns null if it's borderline case */
|
||||||
|
protected abstract Boolean polyRectContainsPoint(GeoRect rect, double pointLat, double pointLon);
|
||||||
|
|
||||||
|
/** Returns null if it's borderline case */
|
||||||
|
protected abstract Boolean circleContainsPoint(double centerLat, double centerLon, double radiusMeters, double pointLat, double pointLon);
|
||||||
|
|
||||||
|
protected abstract Boolean distanceRangeContainsPoint(double centerLat, double centerLon, double minRadiusMeters, double radiusMeters, double pointLat, double pointLon);
|
||||||
|
|
||||||
|
private static abstract class VerifyHits {
|
||||||
|
|
||||||
|
public void test(boolean small, IndexSearcher s, NumericDocValues docIDToID, Set<Integer> deleted, Query query, double[] lats, double[] lons) throws Exception {
|
||||||
|
int maxDoc = s.getIndexReader().maxDoc();
|
||||||
|
final FixedBitSet hits = new FixedBitSet(maxDoc);
|
||||||
|
s.search(query, new SimpleCollector() {
|
||||||
|
|
||||||
|
private int docBase;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean needsScores() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doSetNextReader(LeafReaderContext context) throws IOException {
|
||||||
|
docBase = context.docBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void collect(int doc) {
|
||||||
|
hits.set(docBase+doc);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
boolean fail = false;
|
||||||
|
|
||||||
|
for(int docID=0;docID<maxDoc;docID++) {
|
||||||
|
int id = (int) docIDToID.get(docID);
|
||||||
|
Boolean expected;
|
||||||
|
if (deleted.contains(id)) {
|
||||||
|
expected = false;
|
||||||
|
} else if (Double.isNaN(lats[id])) {
|
||||||
|
expected = false;
|
||||||
|
} else {
|
||||||
|
expected = shouldMatch(lats[id], lons[id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// null means it's a borderline case which is allowed to be wrong:
|
||||||
|
if (expected != null && hits.get(docID) != expected) {
|
||||||
|
if (expected) {
|
||||||
|
System.out.println(Thread.currentThread().getName() + ": id=" + id + " should match but did not");
|
||||||
|
} else {
|
||||||
|
System.out.println(Thread.currentThread().getName() + ": id=" + id + " should not match but did");
|
||||||
|
}
|
||||||
|
System.out.println(" small=" + small + " query=" + query +
|
||||||
|
" docID=" + docID + "\n lat=" + lats[id] + " lon=" + lons[id] +
|
||||||
|
"\n deleted?=" + deleted.contains(id));
|
||||||
|
if (Double.isNaN(lats[id]) == false) {
|
||||||
|
describe(docID, lats[id], lons[id]);
|
||||||
|
}
|
||||||
|
fail = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fail) {
|
||||||
|
fail("some hits were wrong");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return true if we definitely should match, false if we definitely
|
||||||
|
* should not match, and null if it's a borderline case which might
|
||||||
|
* go either way. */
|
||||||
|
protected abstract Boolean shouldMatch(double lat, double lon);
|
||||||
|
|
||||||
|
protected abstract void describe(int docID, double lat, double lon);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void verify(boolean small, double[] lats, double[] lons) throws Exception {
|
||||||
|
IndexWriterConfig iwc = newIndexWriterConfig();
|
||||||
|
// Else we can get O(N^2) merging:
|
||||||
|
int mbd = iwc.getMaxBufferedDocs();
|
||||||
|
if (mbd != -1 && mbd < lats.length/100) {
|
||||||
|
iwc.setMaxBufferedDocs(lats.length/100);
|
||||||
|
}
|
||||||
|
initIndexWriterConfig(FIELD_NAME, iwc);
|
||||||
|
Directory dir;
|
||||||
|
if (lats.length > 100000) {
|
||||||
|
dir = newFSDirectory(createTempDir(getClass().getSimpleName()));
|
||||||
|
} else {
|
||||||
|
dir = newDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Integer> deleted = new HashSet<>();
|
||||||
|
// RandomIndexWriter is too slow here:
|
||||||
|
IndexWriter w = new IndexWriter(dir, iwc);
|
||||||
|
for(int id=0;id<lats.length;id++) {
|
||||||
|
Document doc = new Document();
|
||||||
|
doc.add(newStringField("id", ""+id, Field.Store.NO));
|
||||||
|
doc.add(new NumericDocValuesField("id", id));
|
||||||
|
if (Double.isNaN(lats[id]) == false) {
|
||||||
|
addPointToDoc(FIELD_NAME, doc, lats[id], lons[id]);
|
||||||
|
}
|
||||||
|
w.addDocument(doc);
|
||||||
|
if (id > 0 && random().nextInt(100) == 42) {
|
||||||
|
int idToDelete = random().nextInt(id);
|
||||||
|
w.deleteDocuments(new Term("id", ""+idToDelete));
|
||||||
|
deleted.add(idToDelete);
|
||||||
|
if (VERBOSE) {
|
||||||
|
System.out.println(" delete id=" + idToDelete);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (random().nextBoolean()) {
|
||||||
|
w.forceMerge(1);
|
||||||
|
}
|
||||||
|
final IndexReader r = DirectoryReader.open(w, true);
|
||||||
|
w.close();
|
||||||
|
|
||||||
|
// We can't wrap with "exotic" readers because the BKD query must see the BKDDVFormat:
|
||||||
|
IndexSearcher s = newSearcher(r, false);
|
||||||
|
|
||||||
|
// Make sure queries are thread safe:
|
||||||
|
int numThreads = TestUtil.nextInt(random(), 2, 5);
|
||||||
|
|
||||||
|
List<Thread> threads = new ArrayList<>();
|
||||||
|
final int iters = atLeast(75);
|
||||||
|
|
||||||
|
final CountDownLatch startingGun = new CountDownLatch(1);
|
||||||
|
final AtomicBoolean failed = new AtomicBoolean();
|
||||||
|
|
||||||
|
for(int i=0;i<numThreads;i++) {
|
||||||
|
Thread thread = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
_run();
|
||||||
|
} catch (Exception e) {
|
||||||
|
failed.set(true);
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void _run() throws Exception {
|
||||||
|
startingGun.await();
|
||||||
|
|
||||||
|
NumericDocValues docIDToID = MultiDocValues.getNumericValues(r, "id");
|
||||||
|
|
||||||
|
for (int iter=0;iter<iters && failed.get() == false;iter++) {
|
||||||
|
|
||||||
|
if (VERBOSE) {
|
||||||
|
System.out.println("\nTEST: iter=" + iter + " s=" + s);
|
||||||
|
}
|
||||||
|
Query query;
|
||||||
|
VerifyHits verifyHits;
|
||||||
|
|
||||||
|
if (random().nextBoolean()) {
|
||||||
|
// BBox: don't allow dateline crossing when testing small:
|
||||||
|
final GeoRect bbox = randomRect(small, small == false);
|
||||||
|
|
||||||
|
query = newBBoxQuery(FIELD_NAME, bbox);
|
||||||
|
|
||||||
|
verifyHits = new VerifyHits() {
|
||||||
|
@Override
|
||||||
|
protected Boolean shouldMatch(double pointLat, double pointLon) {
|
||||||
|
return rectContainsPoint(bbox, pointLat, pointLon);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
protected void describe(int docID, double lat, double lon) {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} else if (random().nextBoolean()) {
|
||||||
|
// Distance
|
||||||
|
final boolean rangeQuery = random().nextBoolean();
|
||||||
|
final double centerLat = randomLat(small);
|
||||||
|
final double centerLon = randomLon(small);
|
||||||
|
|
||||||
|
double radiusMeters;
|
||||||
|
double minRadiusMeters;
|
||||||
|
|
||||||
|
if (small) {
|
||||||
|
// Approx 3 degrees lon at the equator:
|
||||||
|
radiusMeters = random().nextDouble() * 333000 + 1.0;
|
||||||
|
} else {
|
||||||
|
// So the query can cover at most 50% of the earth's surface:
|
||||||
|
radiusMeters = random().nextDouble() * GeoProjectionUtils.SEMIMAJOR_AXIS * Math.PI / 2.0 + 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate a random minimum radius between 1% and 95% the max radius
|
||||||
|
minRadiusMeters = (0.01 + 0.94 * random().nextDouble()) * radiusMeters;
|
||||||
|
|
||||||
|
if (VERBOSE) {
|
||||||
|
final DecimalFormat df = new DecimalFormat("#,###.00", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
||||||
|
System.out.println(" radiusMeters = " + df.format(radiusMeters)
|
||||||
|
+ ((rangeQuery == true) ? " minRadiusMeters = " + df.format(minRadiusMeters) : ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (rangeQuery == true) {
|
||||||
|
query = newDistanceRangeQuery(FIELD_NAME, centerLat, centerLon, minRadiusMeters, radiusMeters);
|
||||||
|
} else {
|
||||||
|
query = newDistanceQuery(FIELD_NAME, centerLat, centerLon, radiusMeters);
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
if (e.getMessage().contains("exceeds maxRadius")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyHits = new VerifyHits() {
|
||||||
|
@Override
|
||||||
|
protected Boolean shouldMatch(double pointLat, double pointLon) {
|
||||||
|
if (rangeQuery == false) {
|
||||||
|
return circleContainsPoint(centerLat, centerLon, radiusMeters, pointLat, pointLon);
|
||||||
|
} else {
|
||||||
|
return distanceRangeContainsPoint(centerLat, centerLon, minRadiusMeters, radiusMeters, pointLat, pointLon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void describe(int docID, double pointLat, double pointLon) {
|
||||||
|
double distanceKM = SloppyMath.haversin(centerLat, centerLon, pointLat, pointLon);
|
||||||
|
System.out.println(" docID=" + docID + " centerLon=" + centerLon + " centerLat=" + centerLat
|
||||||
|
+ " pointLon=" + pointLon + " pointLat=" + pointLat + " distanceMeters=" + (distanceKM * 1000)
|
||||||
|
+ " vs" + ((rangeQuery == true) ? " minRadiusMeters=" + minRadiusMeters : "") + " radiusMeters=" + radiusMeters);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: get poly query working with dateline crossing too (how?)!
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// TODO: poly query can't handle dateline crossing yet:
|
||||||
|
final GeoRect bbox = randomRect(small, false);
|
||||||
|
|
||||||
|
// Polygon
|
||||||
|
double[] lats = new double[5];
|
||||||
|
double[] lons = new double[5];
|
||||||
|
lats[0] = bbox.minLat;
|
||||||
|
lons[0] = bbox.minLon;
|
||||||
|
lats[1] = bbox.maxLat;
|
||||||
|
lons[1] = bbox.minLon;
|
||||||
|
lats[2] = bbox.maxLat;
|
||||||
|
lons[2] = bbox.maxLon;
|
||||||
|
lats[3] = bbox.minLat;
|
||||||
|
lons[3] = bbox.maxLon;
|
||||||
|
lats[4] = bbox.minLat;
|
||||||
|
lons[4] = bbox.minLon;
|
||||||
|
query = newPolygonQuery(FIELD_NAME, lats, lons);
|
||||||
|
|
||||||
|
verifyHits = new VerifyHits() {
|
||||||
|
@Override
|
||||||
|
protected Boolean shouldMatch(double pointLat, double pointLon) {
|
||||||
|
return polyRectContainsPoint(bbox, pointLat, pointLon);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void describe(int docID, double lat, double lon) {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query != null) {
|
||||||
|
|
||||||
|
if (VERBOSE) {
|
||||||
|
System.out.println(" query=" + query);
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyHits.test(small, s, docIDToID, deleted, query, lats, lons);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
thread.setName("T" + i);
|
||||||
|
thread.start();
|
||||||
|
threads.add(thread);
|
||||||
|
}
|
||||||
|
startingGun.countDown();
|
||||||
|
for(Thread thread : threads) {
|
||||||
|
thread.join();
|
||||||
|
}
|
||||||
|
IOUtils.close(r, dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue