Skip to content

Commit

Permalink
Support GEOSHAPE field type in RediSearch (#3561)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
sazzad16 committed Oct 18, 2023
1 parent eef560d commit 25ffc37
Show file tree
Hide file tree
Showing 6 changed files with 281 additions and 18 deletions.
22 changes: 15 additions & 7 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,21 @@
<version>2.10.1</version>
</dependency>

<!-- UNIX socket connection support -->
<dependency>
<groupId>com.kohlschutter.junixsocket</groupId>
<artifactId>junixsocket-core</artifactId>
<version>2.8.1</version>
<type>pom</type>
<scope>test</scope>
</dependency>
<!-- Well-known text representation of geometry in RediSearch support -->
<dependency>
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
<version>1.19.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
Expand All @@ -91,13 +106,6 @@
<version>${slf4j.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.kohlschutter.junixsocket</groupId>
<artifactId>junixsocket-core</artifactId>
<version>2.6.1</version>
<type>pom</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
Expand Down
14 changes: 7 additions & 7 deletions src/main/java/redis/clients/jedis/search/SearchProtocol.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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
* <a href="https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry"> well-known
* text (WKT)</a> in {@code toString()} method can be used.
*
* This example uses the <a href="https://github.com/locationtech/jts">JTS</a> library.
* <pre>
* {@code
* <dependency>
* <groupId>org.locationtech.jts</groupId>
* <artifactId>jts-core</artifactId>
* <version>1.19.0</version>
* </dependency>
* }
* </pre>
*/
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.
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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")));
Expand Down

0 comments on commit 25ffc37

Please sign in to comment.