blob: b466e964cc9d8162070ab5209701d24abccb9221 [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.benchmark.byTask.feeds;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.context.SpatialContextFactory;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Shape;
import org.apache.lucene.benchmark.byTask.utils.Config;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.spatial.SpatialStrategy;
import org.apache.lucene.spatial.composite.CompositeSpatialStrategy;
import org.apache.lucene.spatial.prefix.RecursivePrefixTreeStrategy;
import org.apache.lucene.spatial.prefix.tree.PackedQuadPrefixTree;
import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree;
import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTreeFactory;
import org.apache.lucene.spatial.serialized.SerializedDVStrategy;
/**
* Indexes spatial data according to a configured {@link SpatialStrategy} with optional
* shape transformation via a configured {@link ShapeConverter}. The converter can turn points into
* circles and bounding boxes, in order to vary the type of indexing performance tests.
* Unless it's subclass-ed to do otherwise, this class configures a {@link SpatialContext},
* {@link SpatialPrefixTree}, and {@link RecursivePrefixTreeStrategy}. The Strategy is made
* available to a query maker via the static method {@link #getSpatialStrategy(int)}.
* See spatial.alg for a listing of spatial parameters, in particular those starting with "spatial."
* and "doc.spatial".
*/
public class SpatialDocMaker extends DocMaker {
public static final String SPATIAL_FIELD = "spatial";
//cache spatialStrategy by round number
private static Map<Integer,SpatialStrategy> spatialStrategyCache = new HashMap<>();
private SpatialStrategy strategy;
private ShapeConverter shapeConverter;
/**
* Looks up the SpatialStrategy from the given round --
* {@link org.apache.lucene.benchmark.byTask.utils.Config#getRoundNumber()}. It's an error
* if it wasn't created already for this round -- when SpatialDocMaker is initialized.
*/
public static SpatialStrategy getSpatialStrategy(int roundNumber) {
SpatialStrategy result = spatialStrategyCache.get(roundNumber);
if (result == null) {
throw new IllegalStateException("Strategy should have been init'ed by SpatialDocMaker by now");
}
return result;
}
/**
* Builds a SpatialStrategy from configuration options.
*/
protected SpatialStrategy makeSpatialStrategy(final Config config) {
//A Map view of Config that prefixes keys with "spatial."
Map<String, String> configMap = new AbstractMap<String, String>() {
@Override
public Set<Entry<String, String>> entrySet() {
throw new UnsupportedOperationException();
}
@Override
public String get(Object key) {
return config.get("spatial." + key, null);
}
};
SpatialContext ctx = SpatialContextFactory.makeSpatialContext(configMap, null);
return makeSpatialStrategy(config, configMap, ctx);
}
protected SpatialStrategy makeSpatialStrategy(final Config config, Map<String, String> configMap,
SpatialContext ctx) {
//TODO once strategies have factories, we could use them here.
final String strategyName = config.get("spatial.strategy", "rpt");
switch (strategyName) {
case "rpt": return makeRPTStrategy(SPATIAL_FIELD, config, configMap, ctx);
case "composite": return makeCompositeStrategy(config, configMap, ctx);
//TODO add more as-needed
default: throw new IllegalStateException("Unknown spatial.strategy: " + strategyName);
}
}
protected RecursivePrefixTreeStrategy makeRPTStrategy(String spatialField, Config config,
Map<String, String> configMap, SpatialContext ctx) {
//A factory for the prefix tree grid
SpatialPrefixTree grid = SpatialPrefixTreeFactory.makeSPT(configMap, null, ctx);
RecursivePrefixTreeStrategy strategy = new RecursivePrefixTreeStrategy(grid, spatialField);
strategy.setPointsOnly(config.get("spatial.docPointsOnly", false));
final boolean pruneLeafyBranches = config.get("spatial.pruneLeafyBranches", true);
if (grid instanceof PackedQuadPrefixTree) {
((PackedQuadPrefixTree) grid).setPruneLeafyBranches(pruneLeafyBranches);
strategy.setPruneLeafyBranches(false);//always leave it to packed grid, even though it isn't the same
} else {
strategy.setPruneLeafyBranches(pruneLeafyBranches);
}
int prefixGridScanLevel = config.get("query.spatial.prefixGridScanLevel", -4);
if (prefixGridScanLevel < 0)
prefixGridScanLevel = grid.getMaxLevels() + prefixGridScanLevel;
strategy.setPrefixGridScanLevel(prefixGridScanLevel);
double distErrPct = config.get("spatial.distErrPct", .025);//doc & query; a default
strategy.setDistErrPct(distErrPct);
return strategy;
}
protected SerializedDVStrategy makeSerializedDVStrategy(String spatialField, Config config,
Map<String, String> configMap, SpatialContext ctx) {
return new SerializedDVStrategy(ctx, spatialField);
}
protected SpatialStrategy makeCompositeStrategy(Config config, Map<String, String> configMap, SpatialContext ctx) {
final CompositeSpatialStrategy strategy = new CompositeSpatialStrategy(
SPATIAL_FIELD, makeRPTStrategy(SPATIAL_FIELD + "_rpt", config, configMap, ctx),
makeSerializedDVStrategy(SPATIAL_FIELD + "_sdv", config, configMap, ctx)
);
strategy.setOptimizePredicates(config.get("query.spatial.composite.optimizePredicates", true));
return strategy;
}
@Override
public void setConfig(Config config, ContentSource source) {
super.setConfig(config, source);
SpatialStrategy existing = spatialStrategyCache.get(config.getRoundNumber());
if (existing == null) {
//new round; we need to re-initialize
strategy = makeSpatialStrategy(config);
spatialStrategyCache.put(config.getRoundNumber(), strategy);
//TODO remove previous round config?
shapeConverter = makeShapeConverter(strategy, config, "doc.spatial.");
System.out.println("Spatial Strategy: " + strategy);
}
}
/**
* Optionally converts points to circles, and optionally bbox'es result.
*/
public static ShapeConverter makeShapeConverter(final SpatialStrategy spatialStrategy,
Config config, String configKeyPrefix) {
//by default does no conversion
final double radiusDegrees = config.get(configKeyPrefix+"radiusDegrees", 0.0);
final double plusMinus = config.get(configKeyPrefix+"radiusDegreesRandPlusMinus", 0.0);
final boolean bbox = config.get(configKeyPrefix + "bbox", false);
return new ShapeConverter() {
@Override
public Shape convert(Shape shape) {
if (shape instanceof Point && (radiusDegrees != 0.0 || plusMinus != 0.0)) {
Point point = (Point)shape;
double radius = radiusDegrees;
if (plusMinus > 0.0) {
Random random = new Random(point.hashCode());//use hashCode so it's reproducibly random
radius += random.nextDouble() * 2 * plusMinus - plusMinus;
radius = Math.abs(radius);//can happen if configured plusMinus > radiusDegrees
}
shape = spatialStrategy.getSpatialContext().makeCircle(point, radius);
}
if (bbox)
shape = shape.getBoundingBox();
return shape;
}
};
}
/** Converts one shape to another. Created by
* {@link #makeShapeConverter(org.apache.lucene.spatial.SpatialStrategy, org.apache.lucene.benchmark.byTask.utils.Config, String)} */
public interface ShapeConverter {
Shape convert(Shape shape);
}
@Override
public Document makeDocument() throws Exception {
DocState docState = getDocState();
Document doc = super.makeDocument();
// Set SPATIAL_FIELD from body
DocData docData = docState.docData;
// makeDocument() resets docState.getBody() so we can't look there; look in Document
String shapeStr = doc.getField(DocMaker.BODY_FIELD).stringValue();
Shape shape = makeShapeFromString(strategy, docData.getName(), shapeStr);
if (shape != null) {
shape = shapeConverter.convert(shape);
//index
for (Field f : strategy.createIndexableFields(shape)) {
doc.add(f);
}
}
return doc;
}
public static Shape makeShapeFromString(SpatialStrategy strategy, String name, String shapeStr) {
if (shapeStr != null && shapeStr.length() > 0) {
try {
return strategy.getSpatialContext().readShapeFromWkt(shapeStr);
} catch (Exception e) {//InvalidShapeException TODO
System.err.println("Shape "+name+" wasn't parseable: "+e+" (skipping it)");
return null;
}
}
return null;
}
@Override
public Document makeDocument(int size) throws Exception {
//TODO consider abusing the 'size' notion to number of shapes per document
throw new UnsupportedOperationException();
}
}