Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support GEOSHAPE field type in RediSearch #3561

Merged
merged 7 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
sazzad16 marked this conversation as resolved.
Show resolved Hide resolved
<version>2.6.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,106 @@
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;

/**
sazzad16 marked this conversation as resolved.
Show resolved Hide resolved
* Since RediSearch 2.8.4, it is possible to do more advanced GEO querying with GEOSHAPE fields.
*
* We can use any object/library which produces
* <a href="https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry">
* Well-known text (WKT)</a> in {@code toString()} method.
*
* For this purpose of this example we will use
* <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 = "<HOST>";
sazzad16 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading