| /* |
| * 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.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; |
| import org.locationtech.spatial4j.context.SpatialContext; |
| import org.locationtech.spatial4j.context.SpatialContextFactory; |
| import org.locationtech.spatial4j.shape.Point; |
| import org.locationtech.spatial4j.shape.Shape; |
| |
| /** |
| * 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(); |
| } |
| } |