Skip to content

Commit 4814849

Browse files
authored
GH-5277 Add check for geospatial lucene answers (#5278)
2 parents db88aa5 + 04cc815 commit 4814849

4 files changed

Lines changed: 172 additions & 7 deletions

File tree

core/queryalgebra/geosparql/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/function/geosparql/SpatialSupport.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
* <li>a WktWriter that only supports points</li>.
2828
* </ul>
2929
*/
30-
abstract class SpatialSupport {
30+
public abstract class SpatialSupport {
3131

3232
private static final SpatialContext spatialContext;
3333

@@ -50,15 +50,15 @@ abstract class SpatialSupport {
5050
wktWriter = support.createWktWriter();
5151
}
5252

53-
static SpatialContext getSpatialContext() {
53+
public static SpatialContext getSpatialContext() {
5454
return spatialContext;
5555
}
5656

57-
static SpatialAlgebra getSpatialAlgebra() {
57+
public static SpatialAlgebra getSpatialAlgebra() {
5858
return spatialAlgebra;
5959
}
6060

61-
static WktWriter getWktWriter() {
61+
public static WktWriter getWktWriter() {
6262
return wktWriter;
6363
}
6464

core/sail/lucene-api/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
<artifactId>rdf4j-queryalgebra-evaluation</artifactId>
2121
<version>${project.version}</version>
2222
</dependency>
23+
<dependency>
24+
<groupId>${project.groupId}</groupId>
25+
<artifactId>rdf4j-queryalgebra-geosparql</artifactId>
26+
<version>${project.version}</version>
27+
</dependency>
2328
<dependency>
2429
<groupId>${project.groupId}</groupId>
2530
<artifactId>rdf4j-repository-sail</artifactId>

core/sail/lucene-api/src/main/java/org/eclipse/rdf4j/sail/lucene/AbstractSearchIndex.java

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.eclipse.rdf4j.model.Resource;
3333
import org.eclipse.rdf4j.model.Statement;
3434
import org.eclipse.rdf4j.model.ValueFactory;
35+
import org.eclipse.rdf4j.model.base.CoreDatatype;
3536
import org.eclipse.rdf4j.model.impl.BooleanLiteral;
3637
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
3738
import org.eclipse.rdf4j.model.vocabulary.GEO;
@@ -41,10 +42,13 @@
4142
import org.eclipse.rdf4j.query.MalformedQueryException;
4243
import org.eclipse.rdf4j.query.algebra.Var;
4344
import org.eclipse.rdf4j.query.algebra.evaluation.QueryBindingSet;
45+
import org.eclipse.rdf4j.query.algebra.evaluation.function.geosparql.SpatialAlgebra;
46+
import org.eclipse.rdf4j.query.algebra.evaluation.function.geosparql.SpatialSupport;
4447
import org.eclipse.rdf4j.sail.Sail;
4548
import org.eclipse.rdf4j.sail.SailException;
4649
import org.eclipse.rdf4j.sail.lucene.util.MapOfListMaps;
4750
import org.locationtech.spatial4j.context.SpatialContext;
51+
import org.locationtech.spatial4j.io.ShapeReader;
4852
import org.locationtech.spatial4j.shape.Point;
4953
import org.locationtech.spatial4j.shape.Shape;
5054
import org.slf4j.Logger;
@@ -855,6 +859,59 @@ private Iterable<? extends DocumentResult> evaluateQuery(GeoRelationQuerySpec qu
855859
return hits;
856860
}
857861

862+
private IRI toSpatialOp(String relation) {
863+
if (GEOF.SF_INTERSECTS.stringValue().equals(relation)) {
864+
return GEOF.SF_INTERSECTS;
865+
} else if (GEOF.SF_DISJOINT.stringValue().equals(relation)) {
866+
return GEOF.SF_DISJOINT;
867+
} else if (GEOF.SF_EQUALS.stringValue().equals(relation)) {
868+
return GEOF.SF_EQUALS;
869+
} else if (GEOF.SF_OVERLAPS.stringValue().equals(relation)) {
870+
return GEOF.SF_OVERLAPS;
871+
} else if (GEOF.EH_COVERED_BY.stringValue().equals(relation)) {
872+
return GEOF.SF_WITHIN;
873+
} else if (GEOF.EH_COVERS.stringValue().equals(relation)) {
874+
return GEOF.EH_CONTAINS;
875+
} else if (GEOF.SF_WITHIN.stringValue().equals(relation)) {
876+
return GEOF.SF_WITHIN;
877+
} else if (GEOF.EH_CONTAINS.stringValue().equals(relation)) {
878+
return GEOF.EH_CONTAINS;
879+
}
880+
return null;
881+
}
882+
883+
private Shape readShape(String geo) {
884+
ShapeReader reader = SpatialSupport.getSpatialContext().getFormats().getWktReader();
885+
try {
886+
return reader.read(geo);
887+
} catch (IOException | ParseException e) {
888+
throw new SailException("Can't read geo wkt shape", e);
889+
}
890+
}
891+
892+
private boolean checkSpatialOp(IRI op, Shape arg1, Shape arg2) {
893+
SpatialAlgebra algebra = SpatialSupport.getSpatialAlgebra();
894+
if (op == GEOF.SF_INTERSECTS) {
895+
return algebra.sfIntersects(arg1, arg2);
896+
}
897+
if (op == GEOF.SF_DISJOINT) {
898+
return algebra.sfDisjoint(arg1, arg2);
899+
}
900+
if (op == GEOF.SF_EQUALS) {
901+
return algebra.sfEquals(arg1, arg2);
902+
}
903+
if (op == GEOF.SF_OVERLAPS) {
904+
return algebra.sfOverlaps(arg1, arg2);
905+
}
906+
if (op == GEOF.SF_WITHIN) {
907+
return algebra.sfWithin(arg1, arg2);
908+
}
909+
if (op == GEOF.EH_CONTAINS) {
910+
return algebra.ehContains(arg1, arg2);
911+
} else
912+
throw new SailException(new IllegalArgumentException("bad spatial op : " + op));
913+
}
914+
858915
private BindingSetCollection generateBindingSets(GeoRelationQuerySpec query,
859916
Iterable<? extends DocumentResult> hits) throws SailException {
860917
// Since one resource can be returned many times, it can lead now to
@@ -893,8 +950,25 @@ private BindingSetCollection generateBindingSets(GeoRelationQuerySpec query,
893950
}
894951

895952
List<String> geometries = doc.getProperty(SearchFields.getPropertyField(query.getGeoProperty()));
953+
boolean needValidation = geometries.size() != 1;
954+
IRI spatialOp = null;
955+
Shape funcGeo = null;
956+
if (needValidation) {
957+
spatialOp = toSpatialOp(query.getRelation());
958+
funcGeo = readShape(query.getQueryGeometry().getLabel());
959+
}
896960
for (String geometry : geometries) {
897961
QueryBindingSet derivedBindings = new QueryBindingSet();
962+
if (geoVar != null) {
963+
if (needValidation && spatialOp != null) {
964+
Shape geo = readShape(geometry);
965+
966+
if (!checkSpatialOp(spatialOp, funcGeo, geo)) {
967+
continue; // not inside the asked shape
968+
}
969+
}
970+
derivedBindings.addBinding(geoVar, SearchFields.wktToLiteral(geometry));
971+
}
898972
if (subjVar != null) {
899973
Resource resource = getResource(doc);
900974
derivedBindings.addBinding(subjVar, resource);
@@ -905,9 +979,6 @@ private BindingSetCollection generateBindingSets(GeoRelationQuerySpec query,
905979
derivedBindings.addBinding(contextVar.getName(), ctx);
906980
}
907981
}
908-
if (geoVar != null) {
909-
derivedBindings.addBinding(geoVar, SearchFields.wktToLiteral(geometry));
910-
}
911982
if (fVar != null) {
912983
derivedBindings.addBinding(fVar, BooleanLiteral.TRUE);
913984
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Eclipse RDF4J contributors.
3+
*
4+
* All rights reserved. This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Distribution License v1.0
6+
* which accompanies this distribution, and is available at
7+
* http://www.eclipse.org/org/documents/edl-v10.php.
8+
*
9+
* SPDX-License-Identifier: BSD-3-Clause
10+
*******************************************************************************/
11+
package org.eclipse.rdf4j.sail.lucene.impl;
12+
13+
import static org.junit.jupiter.api.Assertions.assertEquals;
14+
import static org.junit.jupiter.api.Assertions.assertFalse;
15+
import static org.junit.jupiter.api.Assertions.assertTrue;
16+
17+
import java.io.IOException;
18+
import java.io.StringReader;
19+
import java.util.stream.Collectors;
20+
21+
import org.apache.lucene.document.Document;
22+
import org.apache.lucene.index.IndexReader;
23+
import org.apache.lucene.index.IndexableField;
24+
import org.eclipse.rdf4j.model.base.CoreDatatype;
25+
import org.eclipse.rdf4j.model.util.Values;
26+
import org.eclipse.rdf4j.query.BindingSet;
27+
import org.eclipse.rdf4j.query.TupleQueryResult;
28+
import org.eclipse.rdf4j.repository.sail.SailRepository;
29+
import org.eclipse.rdf4j.repository.util.Repositories;
30+
import org.eclipse.rdf4j.rio.RDFFormat;
31+
import org.eclipse.rdf4j.sail.lucene.LuceneSail;
32+
import org.eclipse.rdf4j.sail.memory.MemoryStore;
33+
import org.junit.jupiter.api.Test;
34+
35+
public class LuceneGeoTest {
36+
@Test
37+
public void geoFailTest() {
38+
MemoryStore store = new MemoryStore();
39+
LuceneSail lucene = new LuceneSail();
40+
lucene.setParameter(LuceneSail.LUCENE_RAMDIR_KEY, "true");
41+
lucene.setParameter(LuceneSail.WKT_FIELDS, "https://example.org/#location");
42+
lucene.setBaseSail(store);
43+
SailRepository repo = new SailRepository(lucene);
44+
try {
45+
repo.init();
46+
47+
Repositories.consume(repo, conn -> {
48+
try {
49+
conn.add(new StringReader(
50+
"@prefix ex: <https://example.org/#> ."
51+
// point in
52+
+ "ex:s ex:location \"POINT(9.6929555 45.6762274)\"^^<http://www.opengis.net/ont/geosparql#wktLiteral> ."
53+
// point out
54+
+ "ex:s ex:location \"POINT(9.18457 45.466873)\"^^<http://www.opengis.net/ont/geosparql#wktLiteral> ."
55+
), "https://example.org/#", RDFFormat.TURTLE);
56+
} catch (IOException e) {
57+
throw new AssertionError(e);
58+
}
59+
});
60+
61+
lucene.reindex();
62+
63+
// a random polygon of Milan
64+
// POLYGON((9.000892639160158 45.3796432523812,9.381294250488283 45.3796432523812,9.381294250488283
65+
// 45.55420812072298,9.000892639160158 45.55420812072298,9.000892639160158 45.3796432523812))
66+
Repositories.consumeNoTransaction(repo, conn -> {
67+
try (TupleQueryResult res = conn.prepareTupleQuery(
68+
"PREFIX ex: <https://example.org/#> "
69+
+ "SELECT * { "
70+
+ " ?s ex:location ?loc "
71+
+ " FILTER (<http://www.opengis.net/def/function/geosparql/ehContains>(\"POLYGON((9.000892639160158 45.3796432523812,9.381294250488283 45.3796432523812,9.381294250488283 45.55420812072298,9.000892639160158 45.55420812072298,9.000892639160158 45.3796432523812))\"^^<http://www.opengis.net/ont/geosparql#wktLiteral>, ?loc)) "
72+
+ "} "
73+
).evaluate()) {
74+
assertTrue(res.hasNext(), "missing good value");
75+
BindingSet next = res.next();
76+
assertEquals(Values.iri("https://example.org/#s"), next.getValue("s"));
77+
assertEquals(Values.literal("POINT(9.18457 45.466873)", CoreDatatype.GEO.WKT_LITERAL),
78+
next.getValue("loc"));
79+
assertFalse(res.hasNext(), "more value(s) :"
80+
+ res.stream().map(Object::toString).collect(Collectors.joining("\n", "\n", "")));
81+
}
82+
});
83+
84+
} finally {
85+
repo.shutDown();
86+
}
87+
88+
}
89+
}

0 commit comments

Comments
 (0)