| /* |
| * 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(); |
| } |
| } |