blob: 55ad1d7e46604d97cfda94676529402c355d0278 [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.lucene.spatial;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.MockAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.RandomIndexWriter;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.util.IOUtils;
import org.apache.lucene.util.LuceneTestCase;
import org.apache.lucene.util.LuceneTestCase.SuppressSysoutChecks;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.distance.DistanceUtils;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Rectangle;
import static com.carrotsearch.randomizedtesting.RandomizedTest.randomDouble;
import static com.carrotsearch.randomizedtesting.RandomizedTest.randomGaussian;
import static com.carrotsearch.randomizedtesting.RandomizedTest.randomInt;
import static com.carrotsearch.randomizedtesting.RandomizedTest.randomIntBetween;
/** A base test class for spatial lucene. It's mostly Lucene generic. */
@SuppressSysoutChecks(bugUrl = "These tests use JUL extensively.")
public abstract class SpatialTestCase extends LuceneTestCase {
protected Logger log = Logger.getLogger(getClass().getName());
private DirectoryReader indexReader;
protected RandomIndexWriter indexWriter;
private Directory directory;
private Analyzer analyzer;
protected IndexSearcher indexSearcher;
protected SpatialContext ctx;//subclass must initialize
@Override
public void setUp() throws Exception {
super.setUp();
directory = newDirectory();
analyzer = new MockAnalyzer(random());
indexWriter = new RandomIndexWriter(random(), directory, LuceneTestCase.newIndexWriterConfig(random(), analyzer));
indexReader = indexWriter.getReader();
indexSearcher = newSearcher(indexReader);
}
@Override
public void tearDown() throws Exception {
IOUtils.close(indexWriter, indexReader, analyzer, directory);
super.tearDown();
}
// ================================================= Helper Methods ================================================
protected void addDocument(Document doc) throws IOException {
indexWriter.addDocument(doc);
}
protected void addDocumentsAndCommit(List<Document> documents) throws IOException {
for (Document document : documents) {
indexWriter.addDocument(document);
}
commit();
}
protected void deleteAll() throws IOException {
indexWriter.deleteAll();
}
protected void commit() throws IOException {
indexWriter.commit();
DirectoryReader newReader = DirectoryReader.openIfChanged(indexReader);
if (newReader != null) {
IOUtils.close(indexReader);
indexReader = newReader;
}
indexSearcher = newSearcher(indexReader);
}
protected void verifyDocumentsIndexed(int numDocs) {
assertEquals(numDocs, indexReader.numDocs());
}
protected SearchResults executeQuery(Query query, int numDocs) {
try {
TopDocs topDocs = indexSearcher.search(query, numDocs);
List<SearchResult> results = new ArrayList<>();
for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
results.add(new SearchResult(scoreDoc.score, indexSearcher.doc(scoreDoc.doc)));
}
return new SearchResults(topDocs.totalHits.value, results);
} catch (IOException ioe) {
throw new RuntimeException("IOException thrown while executing query", ioe);
}
}
protected Point randomPoint() {
final Rectangle WB = ctx.getWorldBounds();
return ctx.makePoint(
randomIntBetween((int) WB.getMinX(), (int) WB.getMaxX()),
randomIntBetween((int) WB.getMinY(), (int) WB.getMaxY()));
}
protected Rectangle randomRectangle() {
return randomRectangle(ctx.getWorldBounds());
}
protected Rectangle randomRectangle(Rectangle bounds) {
double[] xNewStartAndWidth = randomSubRange(bounds.getMinX(), bounds.getWidth());
double xMin = xNewStartAndWidth[0];
double xMax = xMin + xNewStartAndWidth[1];
if (bounds.getCrossesDateLine()) {
xMin = DistanceUtils.normLonDEG(xMin);
xMax = DistanceUtils.normLonDEG(xMax);
}
double[] yNewStartAndHeight = randomSubRange(bounds.getMinY(), bounds.getHeight());
double yMin = yNewStartAndHeight[0];
double yMax = yMin + yNewStartAndHeight[1];
return ctx.makeRectangle(xMin, xMax, yMin, yMax);
}
/** Returns new minStart and new length that is inside the range specified by the arguments. */
protected double[] randomSubRange(double boundStart, double boundLen) {
if (boundLen >= 3 && usually()) { // typical
// prefer integers for ease of debugability ... and prefer 1/16th of bound
int intBoundStart = (int) Math.ceil(boundStart);
int intBoundEnd = (int) (boundStart + boundLen);
int intBoundLen = intBoundEnd - intBoundStart;
int newLen = (int) randomGaussianMeanMax(intBoundLen / 16.0, intBoundLen);
int newStart = intBoundStart + randomInt(intBoundLen - newLen);
return new double[]{newStart, newLen};
} else { // (no int rounding)
double newLen = randomGaussianMeanMax(boundLen / 16, boundLen);
double newStart = boundStart + (boundLen - newLen == 0 ? 0 : (randomDouble() % (boundLen - newLen)));
return new double[]{newStart, newLen};
}
}
private double randomGaussianMinMeanMax(double min, double mean, double max) {
assert mean > min;
return randomGaussianMeanMax(mean - min, max - min) + min;
}
/**
* Within one standard deviation (68% of the time) the result is "close" to
* mean. By "close": when greater than mean, it's the lesser of 2*mean or half
* way to max, when lesser than mean, it's the greater of max-2*mean or half
* way to 0. The other 32% of the time it's in the rest of the range, touching
* either 0 or max but never exceeding.
*/
private double randomGaussianMeanMax(double mean, double max) {
// DWS: I verified the results empirically
assert mean <= max && mean >= 0;
double g = randomGaussian();
double mean2 = mean;
double flip = 1;
if (g < 0) {
mean2 = max - mean;
flip = -1;
g *= -1;
}
// pivot is the distance from mean2 towards max where the boundary of
// 1 standard deviation alters the calculation
double pivotMax = max - mean2;
double pivot = Math.min(mean2, pivotMax / 2);//from 0 to max-mean2
assert pivot >= 0 && pivotMax >= pivot && g >= 0;
double pivotResult;
if (g <= 1)
pivotResult = pivot * g;
else
pivotResult = Math.min(pivotMax, (g - 1) * (pivotMax - pivot) + pivot);
double result = mean + flip * pivotResult;
return (result < 0 || result > max) ? mean : result; // due this due to computational numerical precision
}
// ================================================= Inner Classes =================================================
protected static class SearchResults {
public long numFound;
public List<SearchResult> results;
public SearchResults(long numFound, List<SearchResult> results) {
this.numFound = numFound;
this.results = results;
}
public StringBuilder toDebugString() {
StringBuilder str = new StringBuilder();
str.append("found: ").append(numFound).append('[');
for(SearchResult r : results) {
String id = r.getId();
str.append(id).append(", ");
}
str.append(']');
return str;
}
@Override
public String toString() {
return "[found:"+numFound+" "+results+"]";
}
}
protected static class SearchResult {
public float score;
public Document document;
public SearchResult(float score, Document document) {
this.score = score;
this.document = document;
}
public String getId() {
return document.get("id");
}
@Override
public String toString() {
return "["+score+"="+document+"]";
}
}
}