Skip to content

Commit 25ffc37

Browse files
committed
Support GEOSHAPE field type in RediSearch (#3561)
* Support GEOSHAPE field type in RediSearch * dependency: GeoShape query is limited to PARAM argument only. So I couldn't implement helper functions in query builders. Because of that the dependency library is not required. * example * more comments * format * Address code review
1 parent eef560d commit 25ffc37

File tree

6 files changed

+281
-18
lines changed

6 files changed

+281
-18
lines changed

pom.xml

+15-7
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,21 @@
7373
<version>2.10.1</version>
7474
</dependency>
7575

76+
<!-- UNIX socket connection support -->
77+
<dependency>
78+
<groupId>com.kohlschutter.junixsocket</groupId>
79+
<artifactId>junixsocket-core</artifactId>
80+
<version>2.8.1</version>
81+
<type>pom</type>
82+
<scope>test</scope>
83+
</dependency>
84+
<!-- Well-known text representation of geometry in RediSearch support -->
85+
<dependency>
86+
<groupId>org.locationtech.jts</groupId>
87+
<artifactId>jts-core</artifactId>
88+
<version>1.19.0</version>
89+
<scope>test</scope>
90+
</dependency>
7691
<dependency>
7792
<groupId>junit</groupId>
7893
<artifactId>junit</artifactId>
@@ -91,13 +106,6 @@
91106
<version>${slf4j.version}</version>
92107
<scope>test</scope>
93108
</dependency>
94-
<dependency>
95-
<groupId>com.kohlschutter.junixsocket</groupId>
96-
<artifactId>junixsocket-core</artifactId>
97-
<version>2.6.1</version>
98-
<type>pom</type>
99-
<scope>test</scope>
100-
</dependency>
101109
<dependency>
102110
<groupId>org.mockito</groupId>
103111
<artifactId>mockito-inline</artifactId>

src/main/java/redis/clients/jedis/search/SearchProtocol.java

+7-7
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,13 @@ public byte[] getRaw() {
4949

5050
public enum SearchKeyword implements Rawable {
5151

52-
SCHEMA, TEXT, TAG, NUMERIC, GEO, VECTOR, VERBATIM, NOCONTENT, NOSTOPWORDS, WITHSCORES, LANGUAGE,
53-
INFIELDS, SORTBY, ASC, DESC, LIMIT, HIGHLIGHT, FIELDS, TAGS, SUMMARIZE, FRAGS, LEN, SEPARATOR,
54-
INKEYS, RETURN, FILTER, GEOFILTER, ADD, INCR, MAX, FUZZY, READ, DEL, DD, TEMPORARY, STOPWORDS,
55-
NOFREQS, NOFIELDS, NOOFFSETS, NOHL, SET, GET, ON, SORTABLE, UNF, PREFIX, LANGUAGE_FIELD, SCORE,
56-
SCORE_FIELD, SCORER, PARAMS, AS, DIALECT, SLOP, TIMEOUT, INORDER, EXPANDER, MAXTEXTFIELDS,
57-
SKIPINITIALSCAN, WITHSUFFIXTRIE, NOSTEM, NOINDEX, PHONETIC, WEIGHT, CASESENSITIVE,
58-
LOAD, APPLY, GROUPBY, MAXIDLE, WITHCURSOR, DISTANCE, TERMS, INCLUDE, EXCLUDE,
52+
SCHEMA, TEXT, TAG, NUMERIC, GEO, GEOSHAPE, VECTOR, VERBATIM, NOCONTENT, NOSTOPWORDS, WITHSCORES,
53+
LANGUAGE, INFIELDS, SORTBY, ASC, DESC, LIMIT, HIGHLIGHT, FIELDS, TAGS, SUMMARIZE, FRAGS, LEN,
54+
SEPARATOR, INKEYS, RETURN, FILTER, GEOFILTER, ADD, INCR, MAX, FUZZY, READ, DEL, DD, TEMPORARY,
55+
STOPWORDS, NOFREQS, NOFIELDS, NOOFFSETS, NOHL, SET, GET, ON, SORTABLE, UNF, PREFIX,
56+
LANGUAGE_FIELD, SCORE, SCORE_FIELD, SCORER, PARAMS, AS, DIALECT, SLOP, TIMEOUT, INORDER,
57+
EXPANDER, MAXTEXTFIELDS, SKIPINITIALSCAN, WITHSUFFIXTRIE, NOSTEM, NOINDEX, PHONETIC, WEIGHT,
58+
CASESENSITIVE, LOAD, APPLY, GROUPBY, MAXIDLE, WITHCURSOR, DISTANCE, TERMS, INCLUDE, EXCLUDE,
5959
SEARCH, AGGREGATE, QUERY, LIMITED, COUNT, REDUCE;
6060

6161
private final byte[] raw;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package redis.clients.jedis.search.schemafields;
2+
3+
import static redis.clients.jedis.search.SearchProtocol.SearchKeyword.GEOSHAPE;
4+
5+
import redis.clients.jedis.CommandArguments;
6+
import redis.clients.jedis.search.FieldName;
7+
8+
public class GeoShapeField extends SchemaField {
9+
10+
public enum CoordinateSystem {
11+
12+
/**
13+
* For cartesian (X,Y).
14+
*/
15+
FLAT,
16+
17+
/**
18+
* For geographic (lon, lat).
19+
*/
20+
SPHERICAL
21+
}
22+
23+
private final CoordinateSystem system;
24+
25+
public GeoShapeField(String fieldName, CoordinateSystem system) {
26+
super(fieldName);
27+
this.system = system;
28+
}
29+
30+
public GeoShapeField(FieldName fieldName, CoordinateSystem system) {
31+
super(fieldName);
32+
this.system = system;
33+
}
34+
35+
public static GeoShapeField of(String fieldName, CoordinateSystem system) {
36+
return new GeoShapeField(fieldName, system);
37+
}
38+
39+
@Override
40+
public GeoShapeField as(String attribute) {
41+
super.as(attribute);
42+
return this;
43+
}
44+
45+
@Override
46+
public void addParams(CommandArguments args) {
47+
args.addParams(fieldName).add(GEOSHAPE).add(system);
48+
}
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package redis.clients.jedis.examples;
2+
3+
import org.locationtech.jts.geom.Coordinate;
4+
import org.locationtech.jts.geom.Geometry;
5+
import org.locationtech.jts.geom.GeometryFactory;
6+
import org.locationtech.jts.geom.Polygon;
7+
import org.locationtech.jts.io.ParseException;
8+
import org.locationtech.jts.io.WKTReader;
9+
10+
import redis.clients.jedis.HostAndPort;
11+
import redis.clients.jedis.JedisPooled;
12+
import redis.clients.jedis.UnifiedJedis;
13+
import redis.clients.jedis.search.FTSearchParams;
14+
import redis.clients.jedis.search.SearchResult;
15+
import redis.clients.jedis.search.schemafields.GeoShapeField;
16+
17+
import static java.util.Collections.singletonMap;
18+
import static org.junit.Assert.assertEquals;
19+
import static redis.clients.jedis.search.RediSearchUtil.toStringMap;
20+
21+
/**
22+
* As of RediSearch 2.8.4, advanced GEO querying with GEOSHAPE fields is supported.
23+
*
24+
* Any object/library producing a
25+
* <a href="https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry"> well-known
26+
* text (WKT)</a> in {@code toString()} method can be used.
27+
*
28+
* This example uses the <a href="https://github.com/locationtech/jts">JTS</a> library.
29+
* <pre>
30+
* {@code
31+
* <dependency>
32+
* <groupId>org.locationtech.jts</groupId>
33+
* <artifactId>jts-core</artifactId>
34+
* <version>1.19.0</version>
35+
* </dependency>
36+
* }
37+
* </pre>
38+
*/
39+
public class GeoShapeFieldsUsageInRediSearch {
40+
41+
public static void main(String[] args) {
42+
43+
// We'll create geometry objects with GeometryFactory
44+
final GeometryFactory factory = new GeometryFactory();
45+
46+
final String host = "localhost";
47+
final int port = 6379;
48+
final HostAndPort address = new HostAndPort(host, port);
49+
50+
UnifiedJedis client = new JedisPooled(address);
51+
// client.setDefaultSearchDialect(3); // we can set default search dialect for the client (UnifiedJedis) object
52+
// to avoid setting dialect in every query.
53+
54+
// creating index
55+
client.ftCreate("geometry-index",
56+
GeoShapeField.of("geometry", GeoShapeField.CoordinateSystem.SPHERICAL) // 'SPHERICAL' is for geographic (lon, lat).
57+
// 'FLAT' coordinate system also available for cartesian (X,Y).
58+
);
59+
60+
// preparing data
61+
final Polygon small = factory.createPolygon(
62+
new Coordinate[]{new Coordinate(34.9001, 29.7001),
63+
new Coordinate(34.9001, 29.7100), new Coordinate(34.9100, 29.7100),
64+
new Coordinate(34.9100, 29.7001), new Coordinate(34.9001, 29.7001)}
65+
);
66+
67+
client.hset("small", toStringMap(singletonMap("geometry", small))); // setting data
68+
69+
final Polygon large = factory.createPolygon(
70+
new Coordinate[]{new Coordinate(34.9001, 29.7001),
71+
new Coordinate(34.9001, 29.7200), new Coordinate(34.9200, 29.7200),
72+
new Coordinate(34.9200, 29.7001), new Coordinate(34.9001, 29.7001)}
73+
);
74+
75+
client.hset("large", toStringMap(singletonMap("geometry", large))); // setting data
76+
77+
// searching
78+
final Polygon within = factory.createPolygon(
79+
new Coordinate[]{new Coordinate(34.9000, 29.7000),
80+
new Coordinate(34.9000, 29.7150), new Coordinate(34.9150, 29.7150),
81+
new Coordinate(34.9150, 29.7000), new Coordinate(34.9000, 29.7000)}
82+
);
83+
84+
SearchResult res = client.ftSearch("geometry-index",
85+
"@geometry:[within $poly]", // querying 'within' condition.
86+
// RediSearch also supports 'contains' condition.
87+
FTSearchParams.searchParams()
88+
.addParam("poly", within)
89+
.dialect(3) // DIALECT '3' is required for this query
90+
);
91+
assertEquals(1, res.getTotalResults());
92+
assertEquals(1, res.getDocuments().size());
93+
94+
// We can parse geometry objects with WKTReader
95+
try {
96+
final WKTReader reader = new WKTReader();
97+
Geometry object = reader.read(res.getDocuments().get(0).getString("geometry"));
98+
assertEquals(small, object);
99+
} catch (ParseException ex) {
100+
ex.printStackTrace(System.err);
101+
}
102+
}
103+
104+
// Note: As of RediSearch 2.8.4, only POLYGON and POINT objects are supported.
105+
}

src/test/java/redis/clients/jedis/modules/search/QueryBuilderTest.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,10 @@ public void testIntersectionBasic() {
9191

9292
@Test
9393
public void testIntersectionNested() {
94-
Node n = intersect().
95-
add(union("name", value("mark"), value("dvir"))).
96-
add("time", between(100, 200)).
97-
add(disjunct("created", lt(1000)));
94+
Node n = intersect()
95+
.add(union("name", value("mark"), value("dvir")))
96+
.add("time", between(100, 200))
97+
.add(disjunct("created", lt(1000)));
9898
assertEquals("(@name:(mark|dvir) @time:[100 200] -@created:[-inf (1000])", n.toString());
9999
}
100100

src/test/java/redis/clients/jedis/modules/search/SearchWithParamsTest.java

+101
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@
99
import org.junit.BeforeClass;
1010
import org.junit.Test;
1111

12+
import org.locationtech.jts.geom.Coordinate;
13+
import org.locationtech.jts.geom.GeometryFactory;
14+
import org.locationtech.jts.geom.Point;
15+
import org.locationtech.jts.geom.Polygon;
16+
import org.locationtech.jts.io.ParseException;
17+
import org.locationtech.jts.io.WKTReader;
18+
1219
import redis.clients.jedis.GeoCoordinate;
1320
import redis.clients.jedis.RedisProtocol;
1421
import redis.clients.jedis.args.GeoUnit;
@@ -333,6 +340,100 @@ public void geoFilterAndGeoCoordinateObject() {
333340
assertEquals(2, res.getTotalResults());
334341
}
335342

343+
@Test
344+
public void geoShapeFilterSpherical() throws ParseException {
345+
final WKTReader reader = new WKTReader();
346+
final GeometryFactory factory = new GeometryFactory();
347+
348+
assertOK(client.ftCreate(index, GeoShapeField.of("geom", GeoShapeField.CoordinateSystem.SPHERICAL)));
349+
350+
// polygon type
351+
final Polygon small = factory.createPolygon(new Coordinate[]{new Coordinate(34.9001, 29.7001),
352+
new Coordinate(34.9001, 29.7100), new Coordinate(34.9100, 29.7100),
353+
new Coordinate(34.9100, 29.7001), new Coordinate(34.9001, 29.7001)});
354+
client.hset("small", RediSearchUtil.toStringMap(Collections.singletonMap("geom", small)));
355+
356+
final Polygon large = factory.createPolygon(new Coordinate[]{new Coordinate(34.9001, 29.7001),
357+
new Coordinate(34.9001, 29.7200), new Coordinate(34.9200, 29.7200),
358+
new Coordinate(34.9200, 29.7001), new Coordinate(34.9001, 29.7001)});
359+
client.hset("large", RediSearchUtil.toStringMap(Collections.singletonMap("geom", large)));
360+
361+
// within condition
362+
final Polygon within = factory.createPolygon(new Coordinate[]{new Coordinate(34.9000, 29.7000),
363+
new Coordinate(34.9000, 29.7150), new Coordinate(34.9150, 29.7150),
364+
new Coordinate(34.9150, 29.7000), new Coordinate(34.9000, 29.7000)});
365+
366+
SearchResult res = client.ftSearch(index, "@geom:[within $poly]",
367+
FTSearchParams.searchParams().addParam("poly", within).dialect(3));
368+
assertEquals(1, res.getTotalResults());
369+
assertEquals(1, res.getDocuments().size());
370+
assertEquals(small, reader.read(res.getDocuments().get(0).getString("geom")));
371+
372+
// contains condition
373+
final Polygon contains = factory.createPolygon(new Coordinate[]{new Coordinate(34.9002, 29.7002),
374+
new Coordinate(34.9002, 29.7050), new Coordinate(34.9050, 29.7050),
375+
new Coordinate(34.9050, 29.7002), new Coordinate(34.9002, 29.7002)});
376+
377+
res = client.ftSearch(index, "@geom:[contains $poly]",
378+
FTSearchParams.searchParams().addParam("poly", contains).dialect(3));
379+
assertEquals(2, res.getTotalResults());
380+
assertEquals(2, res.getDocuments().size());
381+
382+
// point type
383+
final Point point = factory.createPoint(new Coordinate(34.9010, 29.7010));
384+
client.hset("point", RediSearchUtil.toStringMap(Collections.singletonMap("geom", point)));
385+
386+
res = client.ftSearch(index, "@geom:[within $poly]",
387+
FTSearchParams.searchParams().addParam("poly", within).dialect(3));
388+
assertEquals(2, res.getTotalResults());
389+
assertEquals(2, res.getDocuments().size());
390+
}
391+
392+
@Test
393+
public void geoShapeFilterFlat() throws ParseException {
394+
final WKTReader reader = new WKTReader();
395+
final GeometryFactory factory = new GeometryFactory();
396+
397+
assertOK(client.ftCreate(index, GeoShapeField.of("geom", GeoShapeField.CoordinateSystem.FLAT)));
398+
399+
// polygon type
400+
final Polygon small = factory.createPolygon(new Coordinate[]{new Coordinate(1, 1),
401+
new Coordinate(1, 100), new Coordinate(100, 100), new Coordinate(100, 1), new Coordinate(1, 1)});
402+
client.hset("small", RediSearchUtil.toStringMap(Collections.singletonMap("geom", small)));
403+
404+
final Polygon large = factory.createPolygon(new Coordinate[]{new Coordinate(1, 1),
405+
new Coordinate(1, 200), new Coordinate(200, 200), new Coordinate(200, 1), new Coordinate(1, 1)});
406+
client.hset("large", RediSearchUtil.toStringMap(Collections.singletonMap("geom", large)));
407+
408+
// within condition
409+
final Polygon within = factory.createPolygon(new Coordinate[]{new Coordinate(0, 0),
410+
new Coordinate(0, 150), new Coordinate(150, 150), new Coordinate(150, 0), new Coordinate(0, 0)});
411+
412+
SearchResult res = client.ftSearch(index, "@geom:[within $poly]",
413+
FTSearchParams.searchParams().addParam("poly", within).dialect(3));
414+
assertEquals(1, res.getTotalResults());
415+
assertEquals(1, res.getDocuments().size());
416+
assertEquals(small, reader.read(res.getDocuments().get(0).getString("geom")));
417+
418+
// contains condition
419+
final Polygon contains = factory.createPolygon(new Coordinate[]{new Coordinate(2, 2),
420+
new Coordinate(2, 50), new Coordinate(50, 50), new Coordinate(50, 2), new Coordinate(2, 2)});
421+
422+
res = client.ftSearch(index, "@geom:[contains $poly]",
423+
FTSearchParams.searchParams().addParam("poly", contains).dialect(3));
424+
assertEquals(2, res.getTotalResults());
425+
assertEquals(2, res.getDocuments().size());
426+
427+
// point type
428+
final Point point = factory.createPoint(new Coordinate(10, 10));
429+
client.hset("point", RediSearchUtil.toStringMap(Collections.singletonMap("geom", point)));
430+
431+
res = client.ftSearch(index, "@geom:[within $poly]",
432+
FTSearchParams.searchParams().addParam("poly", within).dialect(3));
433+
assertEquals(2, res.getTotalResults());
434+
assertEquals(2, res.getDocuments().size());
435+
}
436+
336437
@Test
337438
public void testQueryFlags() {
338439
assertOK(client.ftCreate(index, TextField.of("title")));

0 commit comments

Comments
 (0)