diff --git a/infinispan-remote/spatial-queries/pom.xml b/infinispan-remote/spatial-queries/pom.xml new file mode 100644 index 0000000..ae3093d --- /dev/null +++ b/infinispan-remote/spatial-queries/pom.xml @@ -0,0 +1,72 @@ + + + 4.0.0 + + infinispan-simple-tutorials + org.infinispan.tutorial.simple + 1.0.0-SNAPSHOT + ../../pom.xml + + infinispan-simple-tutorials-spatial-queries + Infinispan Simple Tutorials: Spatial Queries + + + + + org.infinispan + infinispan-bom + + 15.1.0-SNAPSHOT + pom + import + + + + + + + org.infinispan.tutorial.simple + connect-to-infinispan-server + ${project.version} + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + 3.26.3 + test + + + + + + + org.codehaus.mojo + exec-maven-plugin + + + + exec + + + + + java + + -Djava.net.preferIPv4Stack=true + -Djava.util.logging.config.file=src/main/resources/logging.properties + -classpath + + org.infinispan.tutorial.simple.remote.spatial.InfinispanSpatialQueries + + + + + + diff --git a/infinispan-remote/spatial-queries/src/main/java/org/infinispan/tutorial/simple/remote/spatial/Hiking.java b/infinispan-remote/spatial-queries/src/main/java/org/infinispan/tutorial/simple/remote/spatial/Hiking.java new file mode 100644 index 0000000..ecbcd2a --- /dev/null +++ b/infinispan-remote/spatial-queries/src/main/java/org/infinispan/tutorial/simple/remote/spatial/Hiking.java @@ -0,0 +1,13 @@ +package org.infinispan.tutorial.simple.remote.spatial; + +import org.infinispan.api.annotations.indexing.GeoField; +import org.infinispan.api.annotations.indexing.Indexed; +import org.infinispan.api.annotations.indexing.Keyword; +import org.infinispan.commons.api.query.geo.LatLng; +import org.infinispan.protostream.annotations.Proto; + +@Proto +@Indexed +public record Hiking(@Keyword String name, @GeoField LatLng start, @GeoField LatLng end) { + +} diff --git a/infinispan-remote/spatial-queries/src/main/java/org/infinispan/tutorial/simple/remote/spatial/InfinispanSpatialQueries.java b/infinispan-remote/spatial-queries/src/main/java/org/infinispan/tutorial/simple/remote/spatial/InfinispanSpatialQueries.java new file mode 100644 index 0000000..2d71e4e --- /dev/null +++ b/infinispan-remote/spatial-queries/src/main/java/org/infinispan/tutorial/simple/remote/spatial/InfinispanSpatialQueries.java @@ -0,0 +1,228 @@ +package org.infinispan.tutorial.simple.remote.spatial; + +import static org.infinispan.query.remote.client.ProtobufMetadataManagerConstants.PROTOBUF_METADATA_CACHE_NAME; + +import java.net.URI; +import java.util.List; + +import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.client.hotrod.RemoteCacheManager; +import org.infinispan.client.hotrod.configuration.ConfigurationBuilder; +import org.infinispan.commons.api.query.Query; +import org.infinispan.commons.api.query.QueryResult; +import org.infinispan.commons.api.query.geo.LatLng; +import org.infinispan.protostream.GeneratedSchema; +import org.infinispan.tutorial.simple.connect.TutorialsConnectorHelper; + +/** + * Copied and adapted from Katia's org.infinispan.tutorial.simple.remote.query.InfinispanRemoteQuery to + * display spatial queries feature. + */ +public class InfinispanSpatialQueries { + + public static final String MY_CACHE_NAME = "my-cache"; + + static RemoteCacheManager client; + static RemoteCache myCache; + + private static final LatLng MILAN_COORDINATES = LatLng.of(45.4685, 9.1824); + private static final LatLng COMO_COORDINATES = LatLng.of(45.8064, 9.0852); + private static final LatLng BOLOGNA_COORDINATES = LatLng.of(44.4949, 11.3426); + private static final LatLng MY_COORDINATES = LatLng.of(41.90847031512531, 12.455633288333539); + private static final LatLng ROME_COORDINATES = LatLng.of(41.8967, 12.4822); + private static final LatLng VENICE_COORDINATES = LatLng.of(45.4404, 12.3160); + private static final LatLng SELVA_COORDINATES = LatLng.of(46.5560, 11.7559); + + public static void main(String[] args) throws Exception { + connectToInfinispan(); + + addDataToCache(); + withinCircle(); + withinBox(); + withinPolygon(); + spatialProjection(); + spatialOrderBy(); + spatialProjection_OrderBy(); + alternativeMapping_entityHavingMultipleGeoPoints(); + alternativeMapping_entityHavingMultipleGeoFields(); + + disconnect(false); + } + + static QueryResult withinCircle() { + Query query = myCache.query("from tutorial.Restaurant r where r.location " + + "within circle(:lat, :lon, :distance)"); + query.setParameter("lat", MY_COORDINATES.latitude()); + query.setParameter("lon", MY_COORDINATES.longitude()); + query.setParameter("distance", 100); + QueryResult queryResult = query.execute(); + // Print the results + System.out.println("COUNT " + queryResult.count().value()); + System.out.println(queryResult.list()); + return queryResult; + } + + static QueryResult withinBox() { + Query query = myCache.query("from tutorial.Restaurant r where r.location " + + "within box(41.91, 12.45, 41.90, 12.46)"); + QueryResult queryResult = query.execute(); + // Print the results + System.out.println("COUNT " + queryResult.count().value()); + System.out.println(queryResult.list()); + return queryResult; + } + + static QueryResult withinPolygon() { + Query query = myCache.query("from tutorial.Restaurant r where r.location " + + "within polygon((41.91, 12.45), (41.91, 12.46), (41.90, 12.46), (41.90, 12.46))"); + QueryResult queryResult = query.execute(); + // Print the results + System.out.println("COUNT " + queryResult.count().value()); + System.out.println(queryResult.list()); + return queryResult; + } + + static List spatialProjection() { + Query query = myCache.query("select r.name, distance(r.location, 41.90847031512531, 12.455633288333539) " + + "from tutorial.Restaurant r"); + QueryResult queryResult = query.execute(); + List valueObjects = queryResult.list().stream() + .map(r -> new RestaurantDTO((String) r[0], (Double) r[1])) + .toList(); + // Print the results + System.out.println("COUNT " + queryResult.count().value()); + System.out.println(valueObjects); + return valueObjects; + } + + static QueryResult spatialOrderBy() { + Query query = myCache.query("from tutorial.Restaurant r where r.location " + + "order by distance(r.location, 41.90847031512531, 12.455633288333539)"); + QueryResult queryResult = query.execute(); + // Print the results + System.out.println("COUNT " + queryResult.count().value()); + System.out.println(queryResult.list()); + return queryResult; + } + + static List spatialProjection_OrderBy() { + Query query = myCache.query("select r.name, distance(r.location, 41.90847031512531, 12.455633288333539) " + + "from tutorial.Restaurant r where r.location " + + "order by distance(r.location, 41.90847031512531, 12.455633288333539)"); + QueryResult queryResult = query.execute(); + List valueObjects = queryResult.list().stream() + .map(r -> new RestaurantDTO((String) r[0], (Double) r[1])) + .toList(); + // Print the results + System.out.println("COUNT " + queryResult.count().value()); + System.out.println(valueObjects); + return valueObjects; + } + + static QueryResult alternativeMapping_entityHavingMultipleGeoPoints() { + Query query = myCache.query("from tutorial.TrainRoute r where r.departure " + + "within circle(:lat, :lon, :distance)"); + query.setParameter("lat", BOLOGNA_COORDINATES.latitude()); + query.setParameter("lon", BOLOGNA_COORDINATES.longitude()); + query.setParameter("distance", 300_000); + QueryResult queryResult = query.execute(); + // Print the results + System.out.println("COUNT " + queryResult.count().value()); + System.out.println(queryResult.list()); + return queryResult; + } + + static QueryResult alternativeMapping_entityHavingMultipleGeoFields() { + Query query = myCache.query("from tutorial.Hiking h where h.start " + + "within circle(:lat, :lon, :distance)"); + query.setParameter("lat", MY_COORDINATES.latitude()); + query.setParameter("lon", MY_COORDINATES.longitude()); + query.setParameter("distance", 150); + QueryResult queryResult = query.execute(); + // Print the results + System.out.println("COUNT " + queryResult.count()); + System.out.println(queryResult.list()); + return queryResult; + } + + static void connectToInfinispan() throws Exception { + ConfigurationBuilder builder = TutorialsConnectorHelper.connectionConfig(); + + // Add the Protobuf serialization context in the client + builder.addContextInitializer(new TutorialSchemaImpl()); + + // Use indexed cache + URI indexedCacheURI = InfinispanSpatialQueries.class.getClassLoader().getResource("indexedCache.xml").toURI(); + builder.remoteCache(MY_CACHE_NAME).configurationURI(indexedCacheURI); + + // Connect to the server + client = TutorialsConnectorHelper.connect(builder); + + // Create and add the Protobuf schema in the server + addProtoSchema(client); + + // Get the people cache, create it if needed with the default configuration + myCache = client.getCache(MY_CACHE_NAME); + } + + static int addDataToCache() { + myCache.put("La Locanda di Pietro", new Restaurant("La Locanda di Pietro", + "Roman-style pasta dishes & Lazio region wines at a cozy traditional trattoria with a shaded terrace.", + "Via Sebastiano Veniero, 28/c, 00192 Roma RM", 41.907903484609356, 12.45540543756422, 4.6f)); + myCache.put("Scialla The Original Street Food", new Restaurant("Scialla The Original Street Food", + "Pastas & traditional pizza pies served in an unassuming eatery with vegetarian options.", + "Vicolo del Farinone, 27, 00193 Roma RM", 41.90369455835456, 12.459566517195528, 4.7f)); + myCache.put("Trattoria Pizzeria Gli Archi", new Restaurant("Trattoria Pizzeria Gli Archi", + "Traditional trattoria with exposed brick walls, serving up antipasti, pizzas & pasta dishes.", + "Via Sebastiano Veniero, 26, 00192 Roma RM", 41.907930453801285, 12.455204785977637, 4.0f)); + myCache.put("Alla Bracioleria Gracchi Restaurant", new Restaurant("Alla Bracioleria Gracchi Restaurant", + "", "Via dei Gracchi, 19, 00192 Roma RM", 41.907129402661795, 12.458927251586584, 4.7f )); + myCache.put("Magazzino Scipioni", new Restaurant("Magazzino Scipioni", + "Contemporary venue with a focus on unique wines & seasonal Italian plates, plus a bottle shop.", + "Via degli Scipioni, 30, 00192 Roma RM", 41.90817843995448, 12.457118458698043, 4.6f)); + myCache.put("Dal Toscano Restaurant", new Restaurant("Dal Toscano Restaurant", + "Rich pastas, signature steaks & classic Tuscan dishes, plus Chianti wines, at a venerable trattoria.", + "Via Germanico, 58-60, 00192 Roma RM", 41.90785274056548, 12.45822050287784, 4.2f)); + myCache.put("Il Ciociaro", new Restaurant("Il Ciociaro", + "Long-running, old-school restaurant plating traditional staples, from carbonara to tiramisu.", + "Via Barletta, 21, 00192 Roma RM", 41.91038657525997, 12.458851939120656, 4.2f)); + + myCache.put("Rome-Milan", new TrainRoute("Rome-Milan", ROME_COORDINATES.latitude(), ROME_COORDINATES.longitude(), + MILAN_COORDINATES.latitude(), MILAN_COORDINATES.longitude())); + myCache.put("Bologna-Selva", new TrainRoute("Bologna-Selva", BOLOGNA_COORDINATES.latitude(), BOLOGNA_COORDINATES.longitude(), + SELVA_COORDINATES.latitude(), SELVA_COORDINATES.longitude())); + myCache.put("Milan-Como", new TrainRoute("Milan-Como", MILAN_COORDINATES.latitude(), MILAN_COORDINATES.longitude(), + COMO_COORDINATES.latitude(), COMO_COORDINATES.longitude())); + myCache.put("Bologna-Venice", new TrainRoute("Bologna-Venice", BOLOGNA_COORDINATES.latitude(), BOLOGNA_COORDINATES.longitude(), + VENICE_COORDINATES.latitude(), VENICE_COORDINATES.longitude())); + + myCache.put("1", new Hiking("track 1", LatLng.of(41.907903484609356, 12.45540543756422), + LatLng.of(41.90369455835456, 12.459566517195528))); + myCache.put("2", new Hiking("track 2", LatLng.of(41.90369455835456, 12.459566517195528), + LatLng.of(41.907930453801285, 12.455204785977637))); + myCache.put("3", new Hiking("track 3", LatLng.of(41.907930453801285, 12.455204785977637), + LatLng.of(41.907903484609356, 12.45540543756422))); + + return myCache.size(); + } + + public static void disconnect(boolean removeCaches) { + if (removeCaches) { + client.administration().removeCache(MY_CACHE_NAME); + } + + TutorialsConnectorHelper.stop(client); + } + + private static void addProtoSchema(RemoteCacheManager cacheManager) { + // Retrieve metadata cache + RemoteCache metadataCache = + cacheManager.getCache(PROTOBUF_METADATA_CACHE_NAME); + + // Define the new schema on the server too + GeneratedSchema schema = new TutorialSchemaImpl(); + metadataCache.put(schema.getProtoFileName(), schema.getProtoFile()); + } + + record RestaurantDTO(String name, Double distance){} +} diff --git a/infinispan-remote/spatial-queries/src/main/java/org/infinispan/tutorial/simple/remote/spatial/Restaurant.java b/infinispan-remote/spatial-queries/src/main/java/org/infinispan/tutorial/simple/remote/spatial/Restaurant.java new file mode 100644 index 0000000..f583c9a --- /dev/null +++ b/infinispan-remote/spatial-queries/src/main/java/org/infinispan/tutorial/simple/remote/spatial/Restaurant.java @@ -0,0 +1,23 @@ +package org.infinispan.tutorial.simple.remote.spatial; + +import org.infinispan.api.annotations.indexing.Basic; +import org.infinispan.api.annotations.indexing.GeoPoint; +import org.infinispan.api.annotations.indexing.Indexed; +import org.infinispan.api.annotations.indexing.Keyword; +import org.infinispan.api.annotations.indexing.Latitude; +import org.infinispan.api.annotations.indexing.Longitude; +import org.infinispan.api.annotations.indexing.Text; +import org.infinispan.protostream.annotations.Proto; + +@Proto +@Indexed +@GeoPoint(fieldName = "location", projectable = true, sortable = true) +public record Restaurant( + @Keyword(normalizer = "lowercase", projectable = true, sortable = true) String name, + @Text String description, + @Text String address, + @Latitude(fieldName = "location") Double latitude, + @Longitude(fieldName = "location") Double longitude, + @Basic Float score +) { +} diff --git a/infinispan-remote/spatial-queries/src/main/java/org/infinispan/tutorial/simple/remote/spatial/TrainRoute.java b/infinispan-remote/spatial-queries/src/main/java/org/infinispan/tutorial/simple/remote/spatial/TrainRoute.java new file mode 100644 index 0000000..6e06e4a --- /dev/null +++ b/infinispan-remote/spatial-queries/src/main/java/org/infinispan/tutorial/simple/remote/spatial/TrainRoute.java @@ -0,0 +1,21 @@ +package org.infinispan.tutorial.simple.remote.spatial; + +import org.infinispan.api.annotations.indexing.GeoPoint; +import org.infinispan.api.annotations.indexing.Indexed; +import org.infinispan.api.annotations.indexing.Keyword; +import org.infinispan.api.annotations.indexing.Latitude; +import org.infinispan.api.annotations.indexing.Longitude; +import org.infinispan.protostream.annotations.Proto; + +@Proto +@Indexed +@GeoPoint(fieldName = "departure", projectable = true, sortable = true) +@GeoPoint(fieldName = "arrival", projectable = true, sortable = true) +public record TrainRoute( + @Keyword(normalizer = "lowercase") String name, + @Latitude(fieldName = "departure") Double departureLat, + @Longitude(fieldName = "departure") Double departureLon, + @Latitude(fieldName = "arrival") Double arrivalLat, + @Longitude(fieldName = "arrival") Double arrivalLon +) { +} diff --git a/infinispan-remote/spatial-queries/src/main/java/org/infinispan/tutorial/simple/remote/spatial/TutorialSchema.java b/infinispan-remote/spatial-queries/src/main/java/org/infinispan/tutorial/simple/remote/spatial/TutorialSchema.java new file mode 100644 index 0000000..47eedba --- /dev/null +++ b/infinispan-remote/spatial-queries/src/main/java/org/infinispan/tutorial/simple/remote/spatial/TutorialSchema.java @@ -0,0 +1,13 @@ +package org.infinispan.tutorial.simple.remote.spatial; + +import org.infinispan.commons.api.query.geo.LatLng; +import org.infinispan.protostream.GeneratedSchema; +import org.infinispan.protostream.annotations.ProtoSchema; +import org.infinispan.protostream.annotations.ProtoSyntax; + +@ProtoSchema(schemaPackageName = "tutorial", + includeClasses = { Restaurant.class, TrainRoute.class, Hiking.class }, + dependsOn = LatLng.LatLngSchema.class, // this is required to use LatLng + syntax = ProtoSyntax.PROTO3) +public interface TutorialSchema extends GeneratedSchema { +} diff --git a/infinispan-remote/spatial-queries/src/main/resources/indexedCache.xml b/infinispan-remote/spatial-queries/src/main/resources/indexedCache.xml new file mode 100644 index 0000000..1fc3cfc --- /dev/null +++ b/infinispan-remote/spatial-queries/src/main/resources/indexedCache.xml @@ -0,0 +1,13 @@ + + + + + + + + tutorial.Hiking + tutorial.Restaurant + tutorial.TrainRoute + + + \ No newline at end of file diff --git a/infinispan-remote/spatial-queries/src/test/java/org/infinispan/tutorial/simple/remote/spatial/InfinispanSpatialQueriesTest.java b/infinispan-remote/spatial-queries/src/test/java/org/infinispan/tutorial/simple/remote/spatial/InfinispanSpatialQueriesTest.java new file mode 100644 index 0000000..c68645b --- /dev/null +++ b/infinispan-remote/spatial-queries/src/test/java/org/infinispan/tutorial/simple/remote/spatial/InfinispanSpatialQueriesTest.java @@ -0,0 +1,89 @@ +package org.infinispan.tutorial.simple.remote.spatial; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.infinispan.tutorial.simple.remote.spatial.InfinispanSpatialQueries.addDataToCache; +import static org.infinispan.tutorial.simple.remote.spatial.InfinispanSpatialQueries.alternativeMapping_entityHavingMultipleGeoFields; +import static org.infinispan.tutorial.simple.remote.spatial.InfinispanSpatialQueries.alternativeMapping_entityHavingMultipleGeoPoints; +import static org.infinispan.tutorial.simple.remote.spatial.InfinispanSpatialQueries.spatialOrderBy; +import static org.infinispan.tutorial.simple.remote.spatial.InfinispanSpatialQueries.spatialProjection; +import static org.infinispan.tutorial.simple.remote.spatial.InfinispanSpatialQueries.spatialProjection_OrderBy; +import static org.infinispan.tutorial.simple.remote.spatial.InfinispanSpatialQueries.withinBox; +import static org.infinispan.tutorial.simple.remote.spatial.InfinispanSpatialQueries.withinCircle; +import static org.infinispan.tutorial.simple.remote.spatial.InfinispanSpatialQueries.withinPolygon; + +import java.util.List; + +import org.infinispan.commons.api.query.QueryResult; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class InfinispanSpatialQueriesTest { + + @BeforeAll + public static void start() throws Exception { + InfinispanSpatialQueries.connectToInfinispan(); + } + + @AfterAll + public static void stop() { + InfinispanSpatialQueries.disconnect(true); + } + + @Test + public void testSpatialQueries() { + int size = addDataToCache(); + assertThat(size).isEqualTo(14); + + QueryResult queryResult = withinCircle(); + assertThat(queryResult.count().value()).isEqualTo(2); + assertThat(queryResult.list()).extracting(Restaurant::name) + .containsExactlyInAnyOrder("La Locanda di Pietro", "Trattoria Pizzeria Gli Archi"); + + queryResult = withinBox(); + assertThat(queryResult.count().value()).isEqualTo(6); + assertThat(queryResult.list()).extracting(Restaurant::name) + .containsExactlyInAnyOrder("La Locanda di Pietro", "Trattoria Pizzeria Gli Archi", "Magazzino Scipioni", + "Dal Toscano Restaurant", "Scialla The Original Street Food", "Alla Bracioleria Gracchi Restaurant"); + + queryResult = withinPolygon(); + assertThat(queryResult.count().value()).isEqualTo(6); + assertThat(queryResult.list()).extracting(Restaurant::name) + .containsExactlyInAnyOrder("La Locanda di Pietro", "Trattoria Pizzeria Gli Archi", "Magazzino Scipioni", + "Dal Toscano Restaurant", "Scialla The Original Street Food", "Alla Bracioleria Gracchi Restaurant"); + + List projection = spatialProjection(); + assertThat(projection) + .filteredOn(r -> r.distance().equals(65.78997502576355)).extracting(InfinispanSpatialQueries.RestaurantDTO::name) + .first().isEqualTo("La Locanda di Pietro"); + assertThat(projection) + .filteredOn(r -> r.distance().equals(622.8579549605669)).extracting(InfinispanSpatialQueries.RestaurantDTO::name) + .first().isEqualTo("Scialla The Original Street Food"); + + queryResult = spatialOrderBy(); + assertThat(queryResult.count().value()).isEqualTo(7); + assertThat(queryResult.list()).extracting(Restaurant::name) + .containsExactly("La Locanda di Pietro", "Trattoria Pizzeria Gli Archi", "Magazzino Scipioni", + "Dal Toscano Restaurant", "Alla Bracioleria Gracchi Restaurant", "Il Ciociaro", + "Scialla The Original Street Food"); + + projection = spatialProjection_OrderBy(); + assertThat(projection).extracting(InfinispanSpatialQueries.RestaurantDTO::name) + .containsExactly("La Locanda di Pietro", "Trattoria Pizzeria Gli Archi", "Magazzino Scipioni", + "Dal Toscano Restaurant", "Alla Bracioleria Gracchi Restaurant", "Il Ciociaro", + "Scialla The Original Street Food"); + assertThat(projection).extracting(InfinispanSpatialQueries.RestaurantDTO::distance) + .containsExactly(65.78997502576355, 69.72458363789359, 127.11531555461053, 224.8438726836208, + 310.6984480274634, 341.0897945700656, 622.8579549605669); + + QueryResult trainQueryResult = alternativeMapping_entityHavingMultipleGeoPoints(); + assertThat(trainQueryResult.count().value()).isEqualTo(3); + assertThat(trainQueryResult.list()).extracting(TrainRoute::name) + .containsExactlyInAnyOrder("Milan-Como", "Bologna-Venice", "Bologna-Selva"); + + QueryResult hikingQueryResult = alternativeMapping_entityHavingMultipleGeoFields(); + assertThat(hikingQueryResult.count().value()).isEqualTo(2); + assertThat(hikingQueryResult.list()).extracting(Hiking::name) + .containsExactlyInAnyOrder("track 1", "track 3"); + } +} diff --git a/pom.xml b/pom.xml index 8f2018a..ca14f4c 100644 --- a/pom.xml +++ b/pom.xml @@ -107,6 +107,7 @@ infinispan-remote/redis-client infinispan-remote/cross-site-replication infinispan-remote/opentelemetry + infinispan-remote/spatial-queries infinispan-embedded/cache-distributed infinispan-embedded/cache-invalidated infinispan-embedded/cache-replicated