diff --git a/pom.xml b/pom.xml index 8e9e931bae..fbb20d39a5 100644 --- a/pom.xml +++ b/pom.xml @@ -73,6 +73,21 @@ 2.10.1 + + + com.kohlschutter.junixsocket + junixsocket-core + 2.8.1 + pom + test + + + + org.locationtech.jts + jts-core + 1.19.0 + test + junit junit @@ -91,13 +106,6 @@ ${slf4j.version} test - - com.kohlschutter.junixsocket - junixsocket-core - 2.6.1 - pom - test - org.mockito mockito-inline diff --git a/src/main/java/redis/clients/jedis/search/SearchProtocol.java b/src/main/java/redis/clients/jedis/search/SearchProtocol.java index 36780b569b..7f2ad482fb 100644 --- a/src/main/java/redis/clients/jedis/search/SearchProtocol.java +++ b/src/main/java/redis/clients/jedis/search/SearchProtocol.java @@ -49,13 +49,13 @@ public byte[] getRaw() { public enum SearchKeyword implements Rawable { - SCHEMA, TEXT, TAG, NUMERIC, GEO, VECTOR, VERBATIM, NOCONTENT, NOSTOPWORDS, WITHSCORES, LANGUAGE, - INFIELDS, SORTBY, ASC, DESC, LIMIT, HIGHLIGHT, FIELDS, TAGS, SUMMARIZE, FRAGS, LEN, SEPARATOR, - INKEYS, RETURN, FILTER, GEOFILTER, ADD, INCR, MAX, FUZZY, READ, DEL, DD, TEMPORARY, STOPWORDS, - NOFREQS, NOFIELDS, NOOFFSETS, NOHL, SET, GET, ON, SORTABLE, UNF, PREFIX, LANGUAGE_FIELD, SCORE, - SCORE_FIELD, SCORER, PARAMS, AS, DIALECT, SLOP, TIMEOUT, INORDER, EXPANDER, MAXTEXTFIELDS, - SKIPINITIALSCAN, WITHSUFFIXTRIE, NOSTEM, NOINDEX, PHONETIC, WEIGHT, CASESENSITIVE, - LOAD, APPLY, GROUPBY, MAXIDLE, WITHCURSOR, DISTANCE, TERMS, INCLUDE, EXCLUDE, + SCHEMA, TEXT, TAG, NUMERIC, GEO, GEOSHAPE, VECTOR, VERBATIM, NOCONTENT, NOSTOPWORDS, WITHSCORES, + LANGUAGE, INFIELDS, SORTBY, ASC, DESC, LIMIT, HIGHLIGHT, FIELDS, TAGS, SUMMARIZE, FRAGS, LEN, + SEPARATOR, INKEYS, RETURN, FILTER, GEOFILTER, ADD, INCR, MAX, FUZZY, READ, DEL, DD, TEMPORARY, + STOPWORDS, NOFREQS, NOFIELDS, NOOFFSETS, NOHL, SET, GET, ON, SORTABLE, UNF, PREFIX, + LANGUAGE_FIELD, SCORE, SCORE_FIELD, SCORER, PARAMS, AS, DIALECT, SLOP, TIMEOUT, INORDER, + EXPANDER, MAXTEXTFIELDS, SKIPINITIALSCAN, WITHSUFFIXTRIE, NOSTEM, NOINDEX, PHONETIC, WEIGHT, + CASESENSITIVE, LOAD, APPLY, GROUPBY, MAXIDLE, WITHCURSOR, DISTANCE, TERMS, INCLUDE, EXCLUDE, SEARCH, AGGREGATE, QUERY, LIMITED, COUNT, REDUCE; private final byte[] raw; diff --git a/src/main/java/redis/clients/jedis/search/schemafields/GeoShapeField.java b/src/main/java/redis/clients/jedis/search/schemafields/GeoShapeField.java new file mode 100644 index 0000000000..dd3b45e59e --- /dev/null +++ b/src/main/java/redis/clients/jedis/search/schemafields/GeoShapeField.java @@ -0,0 +1,49 @@ +package redis.clients.jedis.search.schemafields; + +import static redis.clients.jedis.search.SearchProtocol.SearchKeyword.GEOSHAPE; + +import redis.clients.jedis.CommandArguments; +import redis.clients.jedis.search.FieldName; + +public class GeoShapeField extends SchemaField { + + public enum CoordinateSystem { + + /** + * For cartesian (X,Y). + */ + FLAT, + + /** + * For geographic (lon, lat). + */ + SPHERICAL + } + + private final CoordinateSystem system; + + public GeoShapeField(String fieldName, CoordinateSystem system) { + super(fieldName); + this.system = system; + } + + public GeoShapeField(FieldName fieldName, CoordinateSystem system) { + super(fieldName); + this.system = system; + } + + public static GeoShapeField of(String fieldName, CoordinateSystem system) { + return new GeoShapeField(fieldName, system); + } + + @Override + public GeoShapeField as(String attribute) { + super.as(attribute); + return this; + } + + @Override + public void addParams(CommandArguments args) { + args.addParams(fieldName).add(GEOSHAPE).add(system); + } +} diff --git a/src/test/java/redis/clients/jedis/examples/GeoShapeFieldsUsageInRediSearch.java b/src/test/java/redis/clients/jedis/examples/GeoShapeFieldsUsageInRediSearch.java new file mode 100644 index 0000000000..db4db2cb0f --- /dev/null +++ b/src/test/java/redis/clients/jedis/examples/GeoShapeFieldsUsageInRediSearch.java @@ -0,0 +1,105 @@ +package redis.clients.jedis.examples; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.io.ParseException; +import org.locationtech.jts.io.WKTReader; + +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.JedisPooled; +import redis.clients.jedis.UnifiedJedis; +import redis.clients.jedis.search.FTSearchParams; +import redis.clients.jedis.search.SearchResult; +import redis.clients.jedis.search.schemafields.GeoShapeField; + +import static java.util.Collections.singletonMap; +import static org.junit.Assert.assertEquals; +import static redis.clients.jedis.search.RediSearchUtil.toStringMap; + +/** + * As of RediSearch 2.8.4, advanced GEO querying with GEOSHAPE fields is supported. + * + * Any object/library producing a + * well-known + * text (WKT) in {@code toString()} method can be used. + * + * This example uses the JTS library. + *
+ * {@code
+ * 
+ *   org.locationtech.jts
+ *   jts-core
+ *   1.19.0
+ * 
+ * }
+ * 
+ */ +public class GeoShapeFieldsUsageInRediSearch { + + public static void main(String[] args) { + + // We'll create geometry objects with GeometryFactory + final GeometryFactory factory = new GeometryFactory(); + + final String host = "localhost"; + final int port = 6379; + final HostAndPort address = new HostAndPort(host, port); + + UnifiedJedis client = new JedisPooled(address); + // client.setDefaultSearchDialect(3); // we can set default search dialect for the client (UnifiedJedis) object + // to avoid setting dialect in every query. + + // creating index + client.ftCreate("geometry-index", + GeoShapeField.of("geometry", GeoShapeField.CoordinateSystem.SPHERICAL) // 'SPHERICAL' is for geographic (lon, lat). + // 'FLAT' coordinate system also available for cartesian (X,Y). + ); + + // preparing data + final Polygon small = factory.createPolygon( + new Coordinate[]{new Coordinate(34.9001, 29.7001), + new Coordinate(34.9001, 29.7100), new Coordinate(34.9100, 29.7100), + new Coordinate(34.9100, 29.7001), new Coordinate(34.9001, 29.7001)} + ); + + client.hset("small", toStringMap(singletonMap("geometry", small))); // setting data + + final Polygon large = factory.createPolygon( + new Coordinate[]{new Coordinate(34.9001, 29.7001), + new Coordinate(34.9001, 29.7200), new Coordinate(34.9200, 29.7200), + new Coordinate(34.9200, 29.7001), new Coordinate(34.9001, 29.7001)} + ); + + client.hset("large", toStringMap(singletonMap("geometry", large))); // setting data + + // searching + final Polygon within = factory.createPolygon( + new Coordinate[]{new Coordinate(34.9000, 29.7000), + new Coordinate(34.9000, 29.7150), new Coordinate(34.9150, 29.7150), + new Coordinate(34.9150, 29.7000), new Coordinate(34.9000, 29.7000)} + ); + + SearchResult res = client.ftSearch("geometry-index", + "@geometry:[within $poly]", // querying 'within' condition. + // RediSearch also supports 'contains' condition. + FTSearchParams.searchParams() + .addParam("poly", within) + .dialect(3) // DIALECT '3' is required for this query + ); + assertEquals(1, res.getTotalResults()); + assertEquals(1, res.getDocuments().size()); + + // We can parse geometry objects with WKTReader + try { + final WKTReader reader = new WKTReader(); + Geometry object = reader.read(res.getDocuments().get(0).getString("geometry")); + assertEquals(small, object); + } catch (ParseException ex) { + ex.printStackTrace(System.err); + } + } + + // Note: As of RediSearch 2.8.4, only POLYGON and POINT objects are supported. +} diff --git a/src/test/java/redis/clients/jedis/modules/search/QueryBuilderTest.java b/src/test/java/redis/clients/jedis/modules/search/QueryBuilderTest.java index a63b625f50..7f152f8985 100644 --- a/src/test/java/redis/clients/jedis/modules/search/QueryBuilderTest.java +++ b/src/test/java/redis/clients/jedis/modules/search/QueryBuilderTest.java @@ -91,10 +91,10 @@ public void testIntersectionBasic() { @Test public void testIntersectionNested() { - Node n = intersect(). - add(union("name", value("mark"), value("dvir"))). - add("time", between(100, 200)). - add(disjunct("created", lt(1000))); + Node n = intersect() + .add(union("name", value("mark"), value("dvir"))) + .add("time", between(100, 200)) + .add(disjunct("created", lt(1000))); assertEquals("(@name:(mark|dvir) @time:[100 200] -@created:[-inf (1000])", n.toString()); } diff --git a/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java b/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java index 4784eaa3d3..792391b775 100644 --- a/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java +++ b/src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java @@ -9,6 +9,13 @@ import org.junit.BeforeClass; import org.junit.Test; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.io.ParseException; +import org.locationtech.jts.io.WKTReader; + import redis.clients.jedis.GeoCoordinate; import redis.clients.jedis.RedisProtocol; import redis.clients.jedis.args.GeoUnit; @@ -333,6 +340,100 @@ public void geoFilterAndGeoCoordinateObject() { assertEquals(2, res.getTotalResults()); } + @Test + public void geoShapeFilterSpherical() throws ParseException { + final WKTReader reader = new WKTReader(); + final GeometryFactory factory = new GeometryFactory(); + + assertOK(client.ftCreate(index, GeoShapeField.of("geom", GeoShapeField.CoordinateSystem.SPHERICAL))); + + // polygon type + final Polygon small = factory.createPolygon(new Coordinate[]{new Coordinate(34.9001, 29.7001), + new Coordinate(34.9001, 29.7100), new Coordinate(34.9100, 29.7100), + new Coordinate(34.9100, 29.7001), new Coordinate(34.9001, 29.7001)}); + client.hset("small", RediSearchUtil.toStringMap(Collections.singletonMap("geom", small))); + + final Polygon large = factory.createPolygon(new Coordinate[]{new Coordinate(34.9001, 29.7001), + new Coordinate(34.9001, 29.7200), new Coordinate(34.9200, 29.7200), + new Coordinate(34.9200, 29.7001), new Coordinate(34.9001, 29.7001)}); + client.hset("large", RediSearchUtil.toStringMap(Collections.singletonMap("geom", large))); + + // within condition + final Polygon within = factory.createPolygon(new Coordinate[]{new Coordinate(34.9000, 29.7000), + new Coordinate(34.9000, 29.7150), new Coordinate(34.9150, 29.7150), + new Coordinate(34.9150, 29.7000), new Coordinate(34.9000, 29.7000)}); + + SearchResult res = client.ftSearch(index, "@geom:[within $poly]", + FTSearchParams.searchParams().addParam("poly", within).dialect(3)); + assertEquals(1, res.getTotalResults()); + assertEquals(1, res.getDocuments().size()); + assertEquals(small, reader.read(res.getDocuments().get(0).getString("geom"))); + + // contains condition + final Polygon contains = factory.createPolygon(new Coordinate[]{new Coordinate(34.9002, 29.7002), + new Coordinate(34.9002, 29.7050), new Coordinate(34.9050, 29.7050), + new Coordinate(34.9050, 29.7002), new Coordinate(34.9002, 29.7002)}); + + res = client.ftSearch(index, "@geom:[contains $poly]", + FTSearchParams.searchParams().addParam("poly", contains).dialect(3)); + assertEquals(2, res.getTotalResults()); + assertEquals(2, res.getDocuments().size()); + + // point type + final Point point = factory.createPoint(new Coordinate(34.9010, 29.7010)); + client.hset("point", RediSearchUtil.toStringMap(Collections.singletonMap("geom", point))); + + res = client.ftSearch(index, "@geom:[within $poly]", + FTSearchParams.searchParams().addParam("poly", within).dialect(3)); + assertEquals(2, res.getTotalResults()); + assertEquals(2, res.getDocuments().size()); + } + + @Test + public void geoShapeFilterFlat() throws ParseException { + final WKTReader reader = new WKTReader(); + final GeometryFactory factory = new GeometryFactory(); + + assertOK(client.ftCreate(index, GeoShapeField.of("geom", GeoShapeField.CoordinateSystem.FLAT))); + + // polygon type + final Polygon small = factory.createPolygon(new Coordinate[]{new Coordinate(1, 1), + new Coordinate(1, 100), new Coordinate(100, 100), new Coordinate(100, 1), new Coordinate(1, 1)}); + client.hset("small", RediSearchUtil.toStringMap(Collections.singletonMap("geom", small))); + + final Polygon large = factory.createPolygon(new Coordinate[]{new Coordinate(1, 1), + new Coordinate(1, 200), new Coordinate(200, 200), new Coordinate(200, 1), new Coordinate(1, 1)}); + client.hset("large", RediSearchUtil.toStringMap(Collections.singletonMap("geom", large))); + + // within condition + final Polygon within = factory.createPolygon(new Coordinate[]{new Coordinate(0, 0), + new Coordinate(0, 150), new Coordinate(150, 150), new Coordinate(150, 0), new Coordinate(0, 0)}); + + SearchResult res = client.ftSearch(index, "@geom:[within $poly]", + FTSearchParams.searchParams().addParam("poly", within).dialect(3)); + assertEquals(1, res.getTotalResults()); + assertEquals(1, res.getDocuments().size()); + assertEquals(small, reader.read(res.getDocuments().get(0).getString("geom"))); + + // contains condition + final Polygon contains = factory.createPolygon(new Coordinate[]{new Coordinate(2, 2), + new Coordinate(2, 50), new Coordinate(50, 50), new Coordinate(50, 2), new Coordinate(2, 2)}); + + res = client.ftSearch(index, "@geom:[contains $poly]", + FTSearchParams.searchParams().addParam("poly", contains).dialect(3)); + assertEquals(2, res.getTotalResults()); + assertEquals(2, res.getDocuments().size()); + + // point type + final Point point = factory.createPoint(new Coordinate(10, 10)); + client.hset("point", RediSearchUtil.toStringMap(Collections.singletonMap("geom", point))); + + res = client.ftSearch(index, "@geom:[within $poly]", + FTSearchParams.searchParams().addParam("poly", within).dialect(3)); + assertEquals(2, res.getTotalResults()); + assertEquals(2, res.getDocuments().size()); + } + @Test public void testQueryFlags() { assertOK(client.ftCreate(index, TextField.of("title")));