Skip to content

Commit 3616f9f

Browse files
Copilotanidotnet
andauthored
Fix OR filters returning duplicate documents when using multiple indexes (#1184)
* Initial plan * Add test case for OR filter duplicate results issue Co-authored-by: anidotnet <696662+anidotnet@users.noreply.github.com> * Fix OR filter duplicate results with multiple indexes Co-authored-by: anidotnet <696662+anidotnet@users.noreply.github.com> * Update tests to reflect correct OR filter behavior Co-authored-by: anidotnet <696662+anidotnet@users.noreply.github.com> * Improve test comments with document field values Co-authored-by: anidotnet <696662+anidotnet@users.noreply.github.com> * Update expected cursor size in compound index tests Changed the expected cursor size from 5 to 3 in CollectionFindByCompoundIndexTest for both mvstore and rocksdb adapters to reflect the correct number of results returned by the query. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: anidotnet <696662+anidotnet@users.noreply.github.com> Co-authored-by: Anindya Chatterjee <anidotnet@gmail.com>
1 parent 12e7035 commit 3616f9f

5 files changed

Lines changed: 60 additions & 11 deletions

File tree

nitrite-mvstore-adapter/src/test/java/org/dizitart/no2/integration/collection/CollectionFindByCompoundIndexTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ public void testFindByOrFilter() throws ParseException {
200200

201201
FindPlan findPlan = cursor.getFindPlan();
202202
assertEquals(3, findPlan.getSubPlans().size());
203-
assertEquals(5, cursor.size());
203+
assertEquals(3, cursor.size());
204204

205205
// distinct
206206
cursor = collection.find(

nitrite-rocksdb-adapter/src/test/java/org/dizitart/no2/integration/collection/CollectionFindByCompoundIndexTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ public void testFindByOrFilter() throws ParseException {
200200

201201
FindPlan findPlan = cursor.getFindPlan();
202202
assertEquals(3, findPlan.getSubPlans().size());
203-
assertEquals(5, cursor.size());
203+
assertEquals(3, cursor.size());
204204

205205
// distinct
206206
cursor = collection.find(

nitrite/src/main/java/org/dizitart/no2/collection/operation/ReadOperations.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,9 @@ private RecordStream<Pair<NitriteId, Document>> findSuitableStream(FindPlan find
127127
// concat all suitable stream of all sub plans
128128
rawStream = new ConcatStream(subStreams);
129129

130-
if (findPlan.isDistinct()) {
131-
// return only distinct items
132-
rawStream = new DistinctStream(rawStream);
133-
}
130+
// Always apply distinct stream for OR filters to avoid duplicates
131+
// when the same document matches multiple sub-plans (different indexes)
132+
rawStream = new DistinctStream(rawStream);
134133
} else {
135134
// and or single filter
136135
if (findPlan.getByIdFilter() != null) {

nitrite/src/test/java/org/dizitart/no2/integration/collection/CollectionFindByCompoundIndexTest.java

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,13 @@ public void testFindByOrFilterAndFilter() {
8686
)
8787
);
8888

89-
assertEquals(3, cursor.size());
89+
// With the fix, OR filters no longer return duplicates
90+
// doc2 = {firstName: "fn2", lastName: "ln2"}
91+
// doc3 = {firstName: "fn3", lastName: "ln2"}
92+
// First AND: lastName=ln2 AND firstName!=fn1 → matches doc2 and doc3
93+
// Second AND: firstName=fn3 AND lastName=ln2 → matches doc3
94+
// Union without duplicates: doc2 and doc3 (2 total)
95+
assertEquals(2, cursor.size());
9096

9197
FindPlan findPlan = cursor.getFindPlan();
9298
assertNull(findPlan.getIndexScanFilter());
@@ -101,11 +107,11 @@ public void testFindByOrFilterAndFilter() {
101107
d.get("firstName", String.class).equals("fn2")
102108
&& d.get("lastName", String.class).equals("ln2")).count());
103109

104-
assertEquals(2, cursor.toList().stream().filter(d ->
110+
assertEquals(1, cursor.toList().stream().filter(d ->
105111
d.get("firstName", String.class).equals("fn3")
106112
&& d.get("lastName", String.class).equals("ln2")).count());
107113

108-
// distinct test
114+
// distinct test - should still return the same results since we're already deduplicating
109115
cursor = collection.find(
110116
or(
111117
and(
@@ -231,11 +237,21 @@ public void testFindByOrFilter() throws ParseException {
231237
)
232238
);
233239

240+
// With the fix, OR filters no longer return duplicates
241+
// doc1 = {firstName: "fn1", lastName: "ln1", birthDay: "2012-07-01"}
242+
// doc2 = {firstName: "fn2", lastName: "ln2", birthDay: "2010-06-12"}
243+
// doc3 = {firstName: "fn3", lastName: "ln2", birthDay: "2014-04-17"}
244+
// Flattened OR conditions:
245+
// 1. lastName=ln2 → doc2, doc3
246+
// 2. firstName!=fn1 → doc2, doc3
247+
// 3. birthDay=2012-07-01 → doc1
248+
// 4. firstName!=fn1 → doc2, doc3 (duplicate)
249+
// Union without duplicates: doc1, doc2, doc3 (3 total)
234250
FindPlan findPlan = cursor.getFindPlan();
235251
assertEquals(3, findPlan.getSubPlans().size());
236-
assertEquals(5, cursor.size());
252+
assertEquals(3, cursor.size());
237253

238-
// distinct
254+
// distinct test - should still return the same results since we're already deduplicating
239255
cursor = collection.find(
240256
or(
241257
or(

nitrite/src/test/java/org/dizitart/no2/integration/collection/IssueTest.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,20 @@
22

33
import org.dizitart.no2.Nitrite;
44
import org.dizitart.no2.collection.Document;
5+
import org.dizitart.no2.collection.DocumentCursor;
56
import org.dizitart.no2.collection.NitriteCollection;
7+
import org.dizitart.no2.filters.Filter;
68
import org.dizitart.no2.filters.FluentFilter;
79
import org.dizitart.no2.index.IndexOptions;
810
import org.dizitart.no2.index.IndexType;
911
import org.junit.After;
1012
import org.junit.Before;
1113
import org.junit.Test;
1214

15+
import java.util.ArrayList;
16+
import java.util.Iterator;
17+
import java.util.List;
18+
1319
import static org.junit.Assert.assertEquals;
1420

1521
public class IssueTest {
@@ -47,4 +53,32 @@ public void testOriginalIssue() {
4753
assertEquals(1, collection.find(FluentFilter.where("value").lte(42L)).size());
4854
assertEquals(1, collection.find(FluentFilter.where("value").gte(42L)).size());
4955
}
56+
57+
@Test
58+
public void testMultipleIndexesOrFilterDuplicates() {
59+
NitriteCollection items = db.getCollection("items");
60+
items.createIndex(IndexOptions.indexOptions(IndexType.NON_UNIQUE), "field_a");
61+
items.createIndex(IndexOptions.indexOptions(IndexType.NON_UNIQUE), "field_b");
62+
63+
Document doc = Document.createDocument();
64+
doc.put("field_a", "A");
65+
doc.put("field_b", "B");
66+
items.insert(doc);
67+
68+
Filter aFilter = FluentFilter.where("field_a").eq("A");
69+
Filter bFilter = FluentFilter.where("field_b").eq("B");
70+
71+
Filter orFilter = Filter.or(aFilter, bFilter);
72+
73+
DocumentCursor cursor = items.find(orFilter);
74+
Iterator<Document> docIter = cursor.iterator();
75+
76+
List<Long> matches = new ArrayList<>();
77+
while (docIter.hasNext()) {
78+
Document match = docIter.next();
79+
long id = match.getId().getIdValue();
80+
matches.add(id);
81+
}
82+
assertEquals("Single document must yield single match", 1, matches.size());
83+
}
5084
}

0 commit comments

Comments
 (0)