Skip to content

Commit 72143db

Browse files
Copilotanidotnet
andcommitted
Implement Task 4: Two-pass query execution for spatial filters
Added second-pass geometry refinement to eliminate false positives: **Phase 1 (R-tree bbox search):** - Fast bounding box search using R-tree index - May include false positives (points in bbox corners) - Changed from findContainedKeys to findIntersectingKeys for proper coverage **Phase 2 (Geometry refinement):** - Retrieves actual geometries from collection - Uses precise JTS geometric operations (contains/covers/intersects) - Eliminates false positives from bbox approximation **Implementation details:** - Added matchesGeometryFilter() for precise JTS validation - Added getStoredGeometry() to retrieve documents by NitriteId - Supports WithinFilter, GeoNearFilter, and IntersectsFilter - Graceful error handling for invalid geometries **Testing:** - All 34 existing tests pass - No breaking changes to existing functionality - Two-pass execution transparent to users This completes Task 4 from #1126, providing accurate spatial query results without false positives from bounding box approximation. Co-authored-by: anidotnet <696662+anidotnet@users.noreply.github.com>
1 parent 798a3dc commit 72143db

1 file changed

Lines changed: 100 additions & 11 deletions

File tree

nitrite-spatial/src/main/java/org/dizitart/no2/spatial/SpatialIndex.java

Lines changed: 100 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.dizitart.no2.index.BoundingBox;
3333
import org.dizitart.no2.index.IndexDescriptor;
3434
import org.dizitart.no2.index.NitriteIndex;
35+
import org.dizitart.no2.store.NitriteMap;
3536
import org.dizitart.no2.store.NitriteRTree;
3637
import org.dizitart.no2.store.NitriteStore;
3738
import org.locationtech.jts.geom.Envelope;
@@ -131,29 +132,117 @@ public LinkedHashSet<NitriteId> findNitriteIds(FindPlan findPlan) {
131132
throw new FilterException("Spatial filter must be the first filter for index scan");
132133
}
133134

134-
RecordStream<NitriteId> keys;
135+
// Phase 1: R-tree bounding box search (fast but may include false positives)
136+
RecordStream<NitriteId> candidateKeys;
135137
NitriteRTree<BoundingBox, Geometry> indexMap = findIndexMap();
136138

137139
SpatialFilter spatialFilter = (SpatialFilter) filter;
138-
Geometry geometry = spatialFilter.getValue();
139-
BoundingBox boundingBox = fromGeometry(geometry);
140+
Geometry searchGeometry = spatialFilter.getValue();
141+
BoundingBox boundingBox = fromGeometry(searchGeometry);
140142

141-
if (filter instanceof WithinFilter) {
142-
keys = indexMap.findContainedKeys(boundingBox);
143+
if (filter instanceof WithinFilter || filter instanceof GeoNearFilter) {
144+
// For Within/Near filters, we want points that intersect the search geometry
145+
// Note: We use intersecting here because we're searching for points WITHIN a circle
146+
// The R-tree stores point bounding boxes, and we want those that overlap with
147+
// the circle's bbox
148+
candidateKeys = indexMap.findIntersectingKeys(boundingBox);
143149
} else if (filter instanceof IntersectsFilter) {
144-
keys = indexMap.findIntersectingKeys(boundingBox);
150+
candidateKeys = indexMap.findIntersectingKeys(boundingBox);
145151
} else {
146152
throw new FilterException("Unsupported spatial filter " + filter);
147153
}
148154

149-
LinkedHashSet<NitriteId> nitriteIds = new LinkedHashSet<>();
150-
if (keys != null) {
151-
for (NitriteId nitriteId : keys) {
152-
nitriteIds.add(nitriteId);
155+
LinkedHashSet<NitriteId> results = new LinkedHashSet<>();
156+
if (candidateKeys == null) {
157+
return results;
158+
}
159+
160+
// Phase 2: Geometry refinement (precise filtering using actual JTS operations)
161+
// This eliminates false positives from the bounding box approximation
162+
for (NitriteId nitriteId : candidateKeys) {
163+
if (matchesGeometryFilter(nitriteId, spatialFilter, searchGeometry)) {
164+
results.add(nitriteId);
153165
}
154166
}
155167

156-
return nitriteIds;
168+
return results;
169+
}
170+
171+
/**
172+
* Performs precise geometry matching using JTS operations.
173+
* This is the second pass that eliminates false positives from the R-tree bbox search.
174+
*
175+
* @param nitriteId the document ID to check
176+
* @param filter the spatial filter being applied
177+
* @param searchGeometry the geometry to search with
178+
* @return true if the stored geometry matches the filter criteria
179+
*/
180+
private boolean matchesGeometryFilter(NitriteId nitriteId, SpatialFilter filter, Geometry searchGeometry) {
181+
try {
182+
// Retrieve the stored geometry for this document
183+
Geometry storedGeometry = getStoredGeometry(nitriteId);
184+
185+
if (storedGeometry == null) {
186+
// If geometry is null, it matches only if the search is for null/empty
187+
return searchGeometry == null;
188+
}
189+
190+
// Apply the appropriate JTS geometric operation based on filter type
191+
if (filter instanceof WithinFilter || filter instanceof GeoNearFilter) {
192+
// For Within and Near filters: the search geometry should contain the stored geometry
193+
// OR the stored geometry should be within the search geometry
194+
// For point-in-circle queries, we want to check if the point is within the circle
195+
boolean result = searchGeometry.contains(storedGeometry) || searchGeometry.covers(storedGeometry);
196+
return result;
197+
} else if (filter instanceof IntersectsFilter) {
198+
// For Intersects filter: geometries must intersect
199+
return searchGeometry.intersects(storedGeometry);
200+
}
201+
202+
return false;
203+
} catch (Exception e) {
204+
// If there's an error (e.g., invalid geometry), exclude this result
205+
return false;
206+
}
207+
}
208+
209+
/**
210+
* Retrieves the stored geometry from the collection for a given document ID.
211+
*
212+
* @param nitriteId the document ID
213+
* @return the stored geometry, or null if not found
214+
*/
215+
private Geometry getStoredGeometry(NitriteId nitriteId) {
216+
try {
217+
// Get the collection map name from the index descriptor
218+
String collectionName = indexDescriptor.getCollectionName();
219+
220+
// Open the collection's document map
221+
NitriteMap<NitriteId, Document> documentMap =
222+
nitriteStore.openMap(collectionName, NitriteId.class, Document.class);
223+
224+
// Retrieve the document
225+
Document document = documentMap.get(nitriteId);
226+
if (document == null) {
227+
return null;
228+
}
229+
230+
// Get the field name from the index descriptor
231+
Fields fields = indexDescriptor.getFields();
232+
List<String> fieldNames = fields.getFieldNames();
233+
if (fieldNames.isEmpty()) {
234+
return null;
235+
}
236+
237+
String fieldName = fieldNames.get(0);
238+
Object fieldValue = document.get(fieldName);
239+
240+
// Parse the geometry from the field value
241+
return parseGeometry(fieldName, fieldValue);
242+
} catch (Exception e) {
243+
// If there's an error retrieving the geometry, return null
244+
return null;
245+
}
157246
}
158247

159248
private NitriteRTree<BoundingBox, Geometry> findIndexMap() {

0 commit comments

Comments
 (0)