|
| 1 | +/* |
| 2 | + * SPDX-License-Identifier: Apache-2.0 |
| 3 | + * |
| 4 | + * The OpenSearch Contributors require contributions made to |
| 5 | + * this file be licensed under the Apache-2.0 license or a |
| 6 | + * compatible open source license. |
| 7 | + */ |
| 8 | + |
| 9 | +package org.opensearch.geo.search.aggregations.bucket; |
| 10 | + |
| 11 | +import com.carrotsearch.hppc.ObjectIntHashMap; |
| 12 | +import com.carrotsearch.hppc.ObjectIntMap; |
| 13 | +import org.opensearch.Version; |
| 14 | +import org.opensearch.action.index.IndexRequestBuilder; |
| 15 | +import org.opensearch.cluster.metadata.IndexMetadata; |
| 16 | +import org.opensearch.common.geo.GeoPoint; |
| 17 | +import org.opensearch.common.geo.GeoShapeDocValue; |
| 18 | +import org.opensearch.common.settings.Settings; |
| 19 | +import org.opensearch.common.xcontent.XContentBuilder; |
| 20 | +import org.opensearch.geo.GeoModulePluginIntegTestCase; |
| 21 | +import org.opensearch.geo.tests.common.RandomGeoGenerator; |
| 22 | +import org.opensearch.geo.tests.common.RandomGeoGeometryGenerator; |
| 23 | +import org.opensearch.geometry.Geometry; |
| 24 | +import org.opensearch.geometry.Rectangle; |
| 25 | +import org.opensearch.test.VersionUtils; |
| 26 | + |
| 27 | +import java.util.ArrayList; |
| 28 | +import java.util.HashSet; |
| 29 | +import java.util.List; |
| 30 | +import java.util.Random; |
| 31 | +import java.util.Set; |
| 32 | + |
| 33 | +import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; |
| 34 | +import static org.opensearch.test.hamcrest.OpenSearchAssertions.assertAcked; |
| 35 | + |
| 36 | +/** |
| 37 | + * This is the base class for all the Bucket Aggregation related integration tests. Use this class to add common |
| 38 | + * methods which can be used across different bucket aggregations. If there is any common code that can be used |
| 39 | + * across other integration test too then this is not the class. Use {@link GeoModulePluginIntegTestCase} |
| 40 | + */ |
| 41 | +public abstract class AbstractGeoBucketAggregationIntegTest extends GeoModulePluginIntegTestCase { |
| 42 | + |
| 43 | + protected static final int MAX_PRECISION_FOR_GEO_SHAPES_AGG_TESTING = 4; |
| 44 | + |
| 45 | + protected static final int NUM_DOCS = 100; |
| 46 | + |
| 47 | + protected static final String GEO_SHAPE_INDEX_NAME = "geoshape_index"; |
| 48 | + |
| 49 | + protected static Rectangle boundingRectangleForGeoShapesAgg; |
| 50 | + |
| 51 | + protected static ObjectIntMap<String> expectedDocsCountForGeoShapes; |
| 52 | + |
| 53 | + protected static ObjectIntMap<String> expectedDocCountsForSingleGeoPoint; |
| 54 | + |
| 55 | + protected static ObjectIntMap<String> multiValuedExpectedDocCountsGeoPoint; |
| 56 | + |
| 57 | + protected static final String GEO_SHAPE_FIELD_NAME = "location_geo_shape"; |
| 58 | + |
| 59 | + protected static final String GEO_POINT_FIELD_NAME = "location"; |
| 60 | + |
| 61 | + protected static final String KEYWORD_FIELD_NAME = "city"; |
| 62 | + |
| 63 | + protected static String smallestGeoHash = null; |
| 64 | + |
| 65 | + protected final Version version = VersionUtils.randomIndexCompatibleVersion(random()); |
| 66 | + |
| 67 | + @Override |
| 68 | + protected boolean forbidPrivateIndexSettings() { |
| 69 | + return false; |
| 70 | + } |
| 71 | + |
| 72 | + /** |
| 73 | + * Prepares a GeoShape index for testing the GeoShape bucket aggregations. Different bucket aggregations can use |
| 74 | + * different techniques for creating buckets. Override the method |
| 75 | + * {@link AbstractGeoBucketAggregationIntegTest#generateBucketsForGeometry} in the test class for creating the |
| 76 | + * buckets which will then be used for verifications. |
| 77 | + * |
| 78 | + * @param random {@link Random} |
| 79 | + * @throws Exception thrown during index creation. |
| 80 | + */ |
| 81 | + protected void prepareGeoShapeIndexForAggregations(final Random random) throws Exception { |
| 82 | + expectedDocsCountForGeoShapes = new ObjectIntHashMap<>(); |
| 83 | + final Settings settings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version).build(); |
| 84 | + final List<IndexRequestBuilder> geoshapes = new ArrayList<>(); |
| 85 | + assertAcked(prepareCreate(GEO_SHAPE_INDEX_NAME).setSettings(settings).setMapping(GEO_SHAPE_FIELD_NAME, "type" + "=geo_shape")); |
| 86 | + boolean isShapeIntersectingBB = false; |
| 87 | + for (int i = 0; i < NUM_DOCS;) { |
| 88 | + final Geometry geometry = RandomGeoGeometryGenerator.randomGeometry(random); |
| 89 | + final GeoShapeDocValue geometryDocValue = GeoShapeDocValue.createGeometryDocValue(geometry); |
| 90 | + // make sure that there is 1 shape is intersecting with the bounding box |
| 91 | + if (!isShapeIntersectingBB) { |
| 92 | + isShapeIntersectingBB = geometryDocValue.isIntersectingRectangle(boundingRectangleForGeoShapesAgg); |
| 93 | + if (!isShapeIntersectingBB && i == NUM_DOCS - 1) { |
| 94 | + continue; |
| 95 | + } |
| 96 | + } |
| 97 | + i++; |
| 98 | + final Set<String> values = generateBucketsForGeometry(geometry, geometryDocValue); |
| 99 | + geoshapes.add(indexGeoShape(GEO_SHAPE_INDEX_NAME, geometry)); |
| 100 | + for (final String hash : values) { |
| 101 | + expectedDocsCountForGeoShapes.put(hash, expectedDocsCountForGeoShapes.getOrDefault(hash, 0) + 1); |
| 102 | + } |
| 103 | + } |
| 104 | + indexRandom(true, geoshapes); |
| 105 | + ensureGreen(GEO_SHAPE_INDEX_NAME); |
| 106 | + } |
| 107 | + |
| 108 | + /** |
| 109 | + * Returns a set of buckets for the shape at different precision level. Override this method for different bucket |
| 110 | + * aggregations. |
| 111 | + * |
| 112 | + * @param geometry {@link Geometry} |
| 113 | + * @param geoShapeDocValue {@link GeoShapeDocValue} |
| 114 | + * @return A {@link Set} of {@link String} which represents the buckets. |
| 115 | + */ |
| 116 | + protected abstract Set<String> generateBucketsForGeometry(final Geometry geometry, final GeoShapeDocValue geoShapeDocValue); |
| 117 | + |
| 118 | + /** |
| 119 | + * Prepares a GeoPoint index for testing the GeoPoint bucket aggregations. Different bucket aggregations can use |
| 120 | + * different techniques for creating buckets. Override the method |
| 121 | + * {@link AbstractGeoBucketAggregationIntegTest#generateBucketsForGeoPoint} in the test class for creating the |
| 122 | + * buckets which will then be used for verifications. |
| 123 | + * |
| 124 | + * @param random {@link Random} |
| 125 | + * @throws Exception thrown during index creation. |
| 126 | + */ |
| 127 | + protected void prepareSingleValueGeoPointIndex(final Random random) throws Exception { |
| 128 | + expectedDocCountsForSingleGeoPoint = new ObjectIntHashMap<>(); |
| 129 | + createIndex("idx_unmapped"); |
| 130 | + final Settings settings = Settings.builder() |
| 131 | + .put(IndexMetadata.SETTING_VERSION_CREATED, version) |
| 132 | + .put("index.number_of_shards", 4) |
| 133 | + .put("index.number_of_replicas", 0) |
| 134 | + .build(); |
| 135 | + assertAcked( |
| 136 | + prepareCreate("idx").setSettings(settings) |
| 137 | + .setMapping(GEO_POINT_FIELD_NAME, "type=geo_point", KEYWORD_FIELD_NAME, "type=keyword") |
| 138 | + ); |
| 139 | + final List<IndexRequestBuilder> cities = new ArrayList<>(); |
| 140 | + for (int i = 0; i < NUM_DOCS; i++) { |
| 141 | + // generate random point |
| 142 | + final GeoPoint geoPoint = RandomGeoGenerator.randomPoint(random); |
| 143 | + cities.add(indexGeoPoint("idx", geoPoint.toString(), geoPoint.getLat() + ", " + geoPoint.getLon())); |
| 144 | + final Set<String> buckets = generateBucketsForGeoPoint(geoPoint); |
| 145 | + for (final String bucket : buckets) { |
| 146 | + expectedDocCountsForSingleGeoPoint.put(bucket, expectedDocCountsForSingleGeoPoint.getOrDefault(bucket, 0) + 1); |
| 147 | + } |
| 148 | + } |
| 149 | + indexRandom(true, cities); |
| 150 | + ensureGreen("idx_unmapped", "idx"); |
| 151 | + } |
| 152 | + |
| 153 | + protected void prepareMultiValuedGeoPointIndex(final Random random) throws Exception { |
| 154 | + multiValuedExpectedDocCountsGeoPoint = new ObjectIntHashMap<>(); |
| 155 | + final Settings settings = Settings.builder().put(IndexMetadata.SETTING_VERSION_CREATED, version).build(); |
| 156 | + final List<IndexRequestBuilder> cities = new ArrayList<>(); |
| 157 | + assertAcked( |
| 158 | + prepareCreate("multi_valued_idx").setSettings(settings) |
| 159 | + .setMapping(GEO_POINT_FIELD_NAME, "type=geo_point", KEYWORD_FIELD_NAME, "type=keyword") |
| 160 | + ); |
| 161 | + for (int i = 0; i < NUM_DOCS; i++) { |
| 162 | + final int numPoints = random.nextInt(4); |
| 163 | + final List<String> points = new ArrayList<>(); |
| 164 | + final Set<String> buckets = new HashSet<>(); |
| 165 | + for (int j = 0; j < numPoints; ++j) { |
| 166 | + // generate random point |
| 167 | + final GeoPoint geoPoint = RandomGeoGenerator.randomPoint(random); |
| 168 | + points.add(geoPoint.getLat() + "," + geoPoint.getLon()); |
| 169 | + buckets.addAll(generateBucketsForGeoPoint(geoPoint)); |
| 170 | + } |
| 171 | + cities.add(indexGeoPoints("multi_valued_idx", Integer.toString(i), points)); |
| 172 | + for (final String bucket : buckets) { |
| 173 | + multiValuedExpectedDocCountsGeoPoint.put(bucket, multiValuedExpectedDocCountsGeoPoint.getOrDefault(bucket, 0) + 1); |
| 174 | + } |
| 175 | + } |
| 176 | + indexRandom(true, cities); |
| 177 | + ensureGreen("multi_valued_idx"); |
| 178 | + } |
| 179 | + |
| 180 | + /** |
| 181 | + * Returns a set of buckets for the GeoPoint at different precision level. Override this method for different bucket |
| 182 | + * aggregations. |
| 183 | + * |
| 184 | + * @param geoPoint {@link GeoPoint} |
| 185 | + * @return A {@link Set} of {@link String} which represents the buckets. |
| 186 | + */ |
| 187 | + protected abstract Set<String> generateBucketsForGeoPoint(final GeoPoint geoPoint); |
| 188 | + |
| 189 | + /** |
| 190 | + * Indexes a GeoShape in the provided index. |
| 191 | + * @param index {@link String} index name |
| 192 | + * @param geometry {@link Geometry} the Geometry to be indexed |
| 193 | + * @return {@link IndexRequestBuilder} |
| 194 | + * @throws Exception thrown during creation of {@link IndexRequestBuilder} |
| 195 | + */ |
| 196 | + protected IndexRequestBuilder indexGeoShape(final String index, final Geometry geometry) throws Exception { |
| 197 | + XContentBuilder source = jsonBuilder().startObject(); |
| 198 | + source = source.field(GEO_SHAPE_FIELD_NAME, WKT.toWKT(geometry)); |
| 199 | + source = source.endObject(); |
| 200 | + return client().prepareIndex(index).setSource(source); |
| 201 | + } |
| 202 | + |
| 203 | + /** |
| 204 | + * Indexes a {@link List} of {@link GeoPoint}s in the provided Index name. |
| 205 | + * @param index {@link String} index name |
| 206 | + * @param name {@link String} value for the string field in index |
| 207 | + * @param latLon {@link List} of {@link String} representing the String representation of GeoPoint |
| 208 | + * @return {@link IndexRequestBuilder} |
| 209 | + * @throws Exception thrown during indexing. |
| 210 | + */ |
| 211 | + protected IndexRequestBuilder indexGeoPoints(final String index, final String name, final List<String> latLon) throws Exception { |
| 212 | + XContentBuilder source = jsonBuilder().startObject().field(KEYWORD_FIELD_NAME, name); |
| 213 | + if (latLon != null) { |
| 214 | + source = source.field(GEO_POINT_FIELD_NAME, latLon); |
| 215 | + } |
| 216 | + source = source.endObject(); |
| 217 | + return client().prepareIndex(index).setSource(source); |
| 218 | + } |
| 219 | + |
| 220 | + /** |
| 221 | + * Indexes a {@link GeoPoint} in the provided Index name. |
| 222 | + * @param index {@link String} index name |
| 223 | + * @param name {@link String} value for the string field in index |
| 224 | + * @param latLon {@link String} representing the String representation of GeoPoint |
| 225 | + * @return {@link IndexRequestBuilder} |
| 226 | + * @throws Exception thrown during indexing. |
| 227 | + */ |
| 228 | + protected IndexRequestBuilder indexGeoPoint(final String index, final String name, final String latLon) throws Exception { |
| 229 | + return indexGeoPoints(index, name, List.of(latLon)); |
| 230 | + } |
| 231 | + |
| 232 | + /** |
| 233 | + * Generates a Bounding Box of a fixed radius that can be used for shapes aggregations to reduce the size of |
| 234 | + * aggregation results. |
| 235 | + * @param random {@link Random} |
| 236 | + * @return {@link Rectangle} |
| 237 | + */ |
| 238 | + protected Rectangle getGridAggregationBoundingBox(final Random random) { |
| 239 | + final double radius = getRadiusOfBoundingBox(); |
| 240 | + assertTrue("The radius of Bounding Box is less than or equal to 0", radius > 0); |
| 241 | + return RandomGeoGeometryGenerator.randomRectangle(random, radius); |
| 242 | + } |
| 243 | + |
| 244 | + /** |
| 245 | + * Returns a radius for the Bounding box. Test classes can override this method to change the radius of BBox for |
| 246 | + * the test cases. If we increase this value, it will lead to creation of a lot of buckets that can lead of |
| 247 | + * IndexOutOfBoundsExceptions. |
| 248 | + * @return double |
| 249 | + */ |
| 250 | + protected double getRadiusOfBoundingBox() { |
| 251 | + return 5.0; |
| 252 | + } |
| 253 | + |
| 254 | +} |
0 commit comments