Skip to content
107 changes: 102 additions & 5 deletions nitrite/src/main/java/org/dizitart/no2/filters/ElementMatchFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@
import org.dizitart.no2.collection.NitriteId;
import org.dizitart.no2.common.tuples.Pair;
import org.dizitart.no2.exceptions.FilterException;
import org.dizitart.no2.index.IndexMap;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
Expand All @@ -35,13 +37,19 @@
* @author Anindya Chatterjee
* @since 1.0
*/
class ElementMatchFilter extends NitriteFilter {
private final String field;
class ElementMatchFilter extends ComparableFilter {
private final Filter elementFilter;

ElementMatchFilter(String field, Filter elementFilter) {
super(field);
this.elementFilter = elementFilter;
this.field = field;
}

@Override
public Comparable<?> getComparable() {
// ElementMatchFilter doesn't use the comparable value directly
// It delegates to the inner filter for index operations
return null;
}

@Override
Expand All @@ -56,7 +64,7 @@ public boolean apply(Pair<NitriteId, Document> element) {
}

Document document = element.getSecond();
Object fieldValue = document.get(field);
Object fieldValue = document.get(getField());
if (fieldValue == null) {
return false;
}
Expand All @@ -77,9 +85,98 @@ public boolean apply(Pair<NitriteId, Document> element) {
}
}

@Override
public List<?> applyOnIndex(IndexMap indexMap) {
// If the element filter is a ComparableFilter, we can use the index
// Since arrays are indexed by individual elements, we can directly
// apply the inner filter on the index
if (elementFilter instanceof ComparableFilter) {
return ((ComparableFilter) elementFilter).applyOnIndex(indexMap);
}

// For other filter types (AND, OR, NOT with comparable filters),
// we need to handle them differently
if (elementFilter instanceof AndFilter) {
return applyAndFilterOnIndex((AndFilter) elementFilter, indexMap);
} else if (elementFilter instanceof OrFilter) {
return applyOrFilterOnIndex((OrFilter) elementFilter, indexMap);
}

// If we can't use index, return empty list to trigger collection scan
return new ArrayList<>();
}

private List<?> applyAndFilterOnIndex(AndFilter andFilter, IndexMap indexMap) {
// For AND filters, we need to check if all filters are comparable
// and if so, apply them sequentially (intersection)
List<Filter> filters = andFilter.getFilters();
List<?> result = null;

for (Filter filter : filters) {
if (filter instanceof ComparableFilter) {
List<?> filterResult = ((ComparableFilter) filter).applyOnIndex(indexMap);
if (result == null) {
result = filterResult;
} else {
// Intersection of results
result = intersect(result, filterResult);
}
if (result.isEmpty()) {
return result; // Short-circuit if no matches
}
} else {
// If any filter is not comparable, we can't use index
return new ArrayList<>();
}
}

return result != null ? result : new ArrayList<>();
}

private List<?> applyOrFilterOnIndex(OrFilter orFilter, IndexMap indexMap) {
// For OR filters, we union the results from each comparable filter
List<Filter> filters = orFilter.getFilters();
Set<Object> resultSet = new HashSet<>();

for (Filter filter : filters) {
if (filter instanceof ComparableFilter) {
List<?> filterResult = ((ComparableFilter) filter).applyOnIndex(indexMap);
if (filterResult != null && !filterResult.isEmpty()) {
resultSet.addAll(filterResult);
}
} else {
// If any filter is not comparable, we can't use index
return new ArrayList<>();
}
}

return new ArrayList<>(resultSet);
}

private List<?> intersect(List<?> list1, List<?> list2) {
if (list1 == null || list1.isEmpty() || list2 == null || list2.isEmpty()) {
return new ArrayList<>();
}

// Convert the second list to a set for O(1) lookup
Set<Object> set2 = new HashSet<>(list2);
List<Object> result = new ArrayList<>();

for (Object item : list1) {
if (item != null && set2.contains(item)) {
result.add(item);
}
}
// Explicitly handle intersection of null values
if (list1.contains(null) && list2.contains(null)) {
result.add(null);
}
return result;
}

@Override
public String toString() {
return "elemMatch(" + field + " : " + elementFilter.toString() + ")";
return "elemMatch(" + getField() + " : " + elementFilter.toString() + ")";
}

@SuppressWarnings("rawtypes")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -627,4 +627,85 @@ public void testSortByIndexAscendingLessThan() {

assertArrayEquals(nonIndexedResult, indexedResult);
}

@Test
public void testFindByArrayFieldIndexWithElemMatch() {
// Create a collection with array field
NitriteCollection userCollection = db.getCollection("users");

// Insert documents with array of emails
for (int i = 0; i < 1000; i++) {
Document doc = Document.createDocument("name", "user" + i)
.put("emails", new String[]{"user" + i + "@example.com", "user" + i + "@test.com"});
userCollection.insert(doc);
}

// Add a specific test document
userCollection.insert(Document.createDocument("name", "testuser")
.put("emails", new String[]{"[email protected]", "[email protected]"}));

// Measure query time WITHOUT index
long startWithoutIndex = System.nanoTime();
DocumentCursor cursorWithoutIndex = userCollection.find(
where("emails").elemMatch(org.dizitart.no2.filters.FluentFilter.$.eq("[email protected]")));
long withoutIndexCount = cursorWithoutIndex.size();
long endWithoutIndex = System.nanoTime();
long timeWithoutIndex = (endWithoutIndex - startWithoutIndex) / 1_000_000;

assertEquals(1, withoutIndexCount);

// Create index on emails field
userCollection.createIndex(IndexOptions.indexOptions(IndexType.NON_UNIQUE), "emails");

// Measure query time WITH index
long startWithIndex = System.nanoTime();
DocumentCursor cursorWithIndex = userCollection.find(
where("emails").elemMatch(org.dizitart.no2.filters.FluentFilter.$.eq("[email protected]")));
long withIndexCount = cursorWithIndex.size();
long endWithIndex = System.nanoTime();
long timeWithIndex = (endWithIndex - startWithIndex) / 1_000_000;

assertEquals(1, withIndexCount);

// With index should be faster or at least not significantly slower
// We're being lenient here because timing can vary, but index should help
System.out.println("Time without index: " + timeWithoutIndex + " ms");
System.out.println("Time with index: " + timeWithIndex + " ms");

// Verify index is actually being used by checking the find plan
DocumentCursor cursor = userCollection.find(
where("emails").elemMatch(org.dizitart.no2.filters.FluentFilter.$.eq("[email protected]")));
assertNotNull(cursor);
assertEquals(1, cursor.size());
}

@Test
public void testFindByArrayFieldIndexWithElemMatchComplexFilter() {
// Create a collection with array field
NitriteCollection productCollection = db.getCollection("products");

// Insert documents with array of scores
for (int i = 0; i < 100; i++) {
Document doc = Document.createDocument("name", "product" + i)
.put("scores", new Integer[]{i, i + 10, i + 20});
productCollection.insert(doc);
}

// Create index on scores field
productCollection.createIndex(IndexOptions.indexOptions(IndexType.NON_UNIQUE), "scores");

// Query with elemMatch using gt filter
DocumentCursor cursor = productCollection.find(
where("scores").elemMatch(org.dizitart.no2.filters.FluentFilter.$.gt(95)));

// Should find products where at least one score is > 95
assertTrue(cursor.size() > 0);

// Query with elemMatch using lt filter
cursor = productCollection.find(
where("scores").elemMatch(org.dizitart.no2.filters.FluentFilter.$.lt(5)));

// Should find products where at least one score is < 5
assertTrue(cursor.size() > 0);
}
}
Loading