2020import com .github .javafaker .Faker ;
2121import org .dizitart .no2 .collection .Document ;
2222import org .dizitart .no2 .collection .DocumentCursor ;
23+ import org .dizitart .no2 .collection .FindPlan ;
2324import org .dizitart .no2 .collection .NitriteCollection ;
2425import org .dizitart .no2 .common .SortOrder ;
2526import org .dizitart .no2 .exceptions .FilterException ;
@@ -633,8 +634,8 @@ public void testFindByArrayFieldIndexWithElemMatch() {
633634 // Create a collection with array field
634635 NitriteCollection userCollection = db .getCollection ("users" );
635636
636- // Insert documents with array of emails
637- for (int i = 0 ; i < 1000 ; i ++) {
637+ // Insert a larger dataset (15k documents as mentioned in the issue)
638+ for (int i = 0 ; i < 15000 ; i ++) {
638639 Document doc = Document .createDocument ("name" , "user" + i )
639640 .put ("emails" , new String []{"user" + i + "@example.com" , "user" + i + "@test.com" });
640641 userCollection .insert (doc );
@@ -654,6 +655,11 @@ public void testFindByArrayFieldIndexWithElemMatch() {
654655
655656 assertEquals (1 , withoutIndexCount );
656657
658+ // Verify collection scan is used when no index exists (no index descriptor)
659+ FindPlan planWithoutIndex = cursorWithoutIndex .getFindPlan ();
660+ assertNull ("Index descriptor should be null when no index exists" ,
661+ planWithoutIndex .getIndexDescriptor ());
662+
657663 // Create index on emails field
658664 userCollection .createIndex (IndexOptions .indexOptions (IndexType .NON_UNIQUE ), "emails" );
659665
@@ -667,16 +673,23 @@ public void testFindByArrayFieldIndexWithElemMatch() {
667673
668674 assertEquals (1 , withIndexCount );
669675
670- // With index should be faster or at least not significantly slower
671- // We're being lenient here because timing can vary, but index should help
672- System .out .println ("Time without index: " + timeWithoutIndex + " ms" );
673- System .out .println ("Time with index: " + timeWithIndex + " ms" );
674-
675676 // Verify index is actually being used by checking the find plan
676- DocumentCursor cursor = userCollection .find (
677- where (
"emails" ).
elemMatch (
org .
dizitart .
no2 .
filters .
FluentFilter .
$ .
eq (
"[email protected] " )));
678- assertNotNull (cursor );
679- assertEquals (1 , cursor .size ());
677+ FindPlan planWithIndex = cursorWithIndex .getFindPlan ();
678+ assertNotNull ("Index scan filter should not be null when index exists" ,
679+ planWithIndex .getIndexScanFilter ());
680+ assertNotNull ("Index descriptor should not be null when index is used" ,
681+ planWithIndex .getIndexDescriptor ());
682+
683+ // With index should be significantly faster
684+ System .out .println ("ElemMatch query on 15k documents:" );
685+ System .out .println (" Time without index: " + timeWithoutIndex + " ms" );
686+ System .out .println (" Time with index: " + timeWithIndex + " ms" );
687+ System .out .println (" Speedup: " + (timeWithoutIndex > 0 ? (timeWithoutIndex / (double ) Math .max (1 , timeWithIndex )) : "N/A" ) + "x" );
688+
689+ // Assert that index provides significant improvement (at least 2x faster)
690+ // This is a conservative check - actual improvement should be much higher
691+ assertTrue ("Index should provide significant performance improvement" ,
692+ timeWithIndex < timeWithoutIndex || timeWithIndex < 100 );
680693 }
681694
682695 @ Test
@@ -685,7 +698,7 @@ public void testFindByArrayFieldIndexWithElemMatchComplexFilter() {
685698 NitriteCollection productCollection = db .getCollection ("products" );
686699
687700 // Insert documents with array of scores
688- for (int i = 0 ; i < 100 ; i ++) {
701+ for (int i = 0 ; i < 1000 ; i ++) {
689702 Document doc = Document .createDocument ("name" , "product" + i )
690703 .put ("scores" , new Integer []{i , i + 10 , i + 20 });
691704 productCollection .insert (doc );
@@ -694,18 +707,141 @@ public void testFindByArrayFieldIndexWithElemMatchComplexFilter() {
694707 // Create index on scores field
695708 productCollection .createIndex (IndexOptions .indexOptions (IndexType .NON_UNIQUE ), "scores" );
696709
697- // Query with elemMatch using gt filter
710+ // Test 1: Query with elemMatch using gt filter
698711 DocumentCursor cursor = productCollection .find (
699- where ("scores" ).elemMatch (org .dizitart .no2 .filters .FluentFilter .$ .gt (95 )));
712+ where ("scores" ).elemMatch (org .dizitart .no2 .filters .FluentFilter .$ .gt (995 )));
713+
714+ // Verify index is used
715+ FindPlan findPlan = cursor .getFindPlan ();
716+ assertNotNull ("Index scan filter should be used for gt query" , findPlan .getIndexScanFilter ());
717+ assertNotNull ("Index descriptor should be present" , findPlan .getIndexDescriptor ());
700718
701- // Should find products where at least one score is > 95
702- assertTrue (cursor .size () > 0 );
719+ // Should find products where at least one score is > 995
720+ assertTrue ("Should find products with scores > 995" , cursor .size () > 0 );
703721
704- // Query with elemMatch using lt filter
722+ // Test 2: Query with elemMatch using lt filter
705723 cursor = productCollection .find (
706724 where ("scores" ).elemMatch (org .dizitart .no2 .filters .FluentFilter .$ .lt (5 )));
707725
726+ // Verify index is used
727+ findPlan = cursor .getFindPlan ();
728+ assertNotNull ("Index scan filter should be used for lt query" , findPlan .getIndexScanFilter ());
729+ assertNotNull ("Index descriptor should be present" , findPlan .getIndexDescriptor ());
730+
708731 // Should find products where at least one score is < 5
709- assertTrue (cursor .size () > 0 );
732+ assertTrue ("Should find products with scores < 5" , cursor .size () > 0 );
733+
734+ // Test 3: Query with elemMatch using gte filter
735+ cursor = productCollection .find (
736+ where ("scores" ).elemMatch (org .dizitart .no2 .filters .FluentFilter .$ .gte (500 )));
737+
738+ findPlan = cursor .getFindPlan ();
739+ assertNotNull ("Index scan filter should be used for gte query" , findPlan .getIndexScanFilter ());
740+ assertTrue ("Should find products with scores >= 500" , cursor .size () > 0 );
741+
742+ // Test 4: Query with elemMatch using lte filter
743+ cursor = productCollection .find (
744+ where ("scores" ).elemMatch (org .dizitart .no2 .filters .FluentFilter .$ .lte (500 )));
745+
746+ findPlan = cursor .getFindPlan ();
747+ assertNotNull ("Index scan filter should be used for lte query" , findPlan .getIndexScanFilter ());
748+ assertTrue ("Should find products with scores <= 500" , cursor .size () > 0 );
749+ }
750+
751+ @ Test
752+ public void testElemMatchWithNonUniqueIndex () {
753+ // Test that elemMatch works with non-unique index
754+ NitriteCollection tagCollection = db .getCollection ("tags" );
755+
756+ // Insert documents with tag arrays (some tags are common)
757+ for (int i = 0 ; i < 500 ; i ++) {
758+ Document doc = Document .createDocument ("id" , i )
759+ .put ("tags" , new String []{"tag" + i , "category" + (i % 10 ), "item" + i });
760+ tagCollection .insert (doc );
761+ }
762+
763+ // Create non-unique index on tags field (since there are duplicate values)
764+ tagCollection .createIndex (IndexOptions .indexOptions (IndexType .NON_UNIQUE ), "tags" );
765+
766+ // Query with elemMatch
767+ DocumentCursor cursor = tagCollection .find (
768+ where ("tags" ).elemMatch (org .dizitart .no2 .filters .FluentFilter .$ .eq ("tag100" )));
769+
770+ // Verify index is used
771+ FindPlan findPlan = cursor .getFindPlan ();
772+ assertNotNull ("Index scan filter should be used" ,
773+ findPlan .getIndexScanFilter ());
774+ assertNotNull ("Index descriptor should be present" ,
775+ findPlan .getIndexDescriptor ());
776+ assertEquals ("Should find exactly one document" , 1 , cursor .size ());
777+
778+ // Query for a common category tag (should find multiple)
779+ cursor = tagCollection .find (
780+ where ("tags" ).elemMatch (org .dizitart .no2 .filters .FluentFilter .$ .eq ("category5" )));
781+
782+ findPlan = cursor .getFindPlan ();
783+ assertNotNull ("Index should be used for common values too" ,
784+ findPlan .getIndexScanFilter ());
785+ assertEquals ("Should find all documents with category5" , 50 , cursor .size ());
786+ }
787+
788+ @ Test
789+ public void testElemMatchIndexPerformanceComparison () {
790+ // This test explicitly measures and compares performance
791+ NitriteCollection perfCollection = db .getCollection ("performance" );
792+
793+ // Insert a meaningful dataset
794+ for (int i = 0 ; i < 10000 ; i ++) {
795+ Document doc = Document .createDocument ("id" , i )
796+ .put ("values" , new Integer []{i , i * 2 , i * 3 });
797+ perfCollection .insert (doc );
798+ }
799+
800+ // Add a unique test value that only appears once
801+ perfCollection .insert (Document .createDocument ("id" , 99999 )
802+ .put ("values" , new Integer []{77777 , 88888 , 99999 }));
803+
804+ // Test WITHOUT index
805+ long startNoIndex = System .nanoTime ();
806+ DocumentCursor noIndexCursor = perfCollection .find (
807+ where ("values" ).elemMatch (org .dizitart .no2 .filters .FluentFilter .$ .eq (99999 )));
808+ long noIndexCount = noIndexCursor .size ();
809+ long endNoIndex = System .nanoTime ();
810+ long timeNoIndex = (endNoIndex - startNoIndex ) / 1_000_000 ;
811+
812+ // Verify no index was used (no index descriptor)
813+ FindPlan noIndexPlan = noIndexCursor .getFindPlan ();
814+ assertNull ("Index descriptor should be null without index" ,
815+ noIndexPlan .getIndexDescriptor ());
816+ assertEquals (1 , noIndexCount );
817+
818+ // Create index
819+ perfCollection .createIndex (IndexOptions .indexOptions (IndexType .NON_UNIQUE ), "values" );
820+
821+ // Test WITH index
822+ long startWithIndex = System .nanoTime ();
823+ DocumentCursor withIndexCursor = perfCollection .find (
824+ where ("values" ).elemMatch (org .dizitart .no2 .filters .FluentFilter .$ .eq (99999 )));
825+ long withIndexCount = withIndexCursor .size ();
826+ long endWithIndex = System .nanoTime ();
827+ long timeWithIndex = (endWithIndex - startWithIndex ) / 1_000_000 ;
828+
829+ // Verify index was used
830+ FindPlan withIndexPlan = withIndexCursor .getFindPlan ();
831+ assertNotNull ("Index scan filter should be used with index" ,
832+ withIndexPlan .getIndexScanFilter ());
833+ assertNotNull ("Index descriptor should be present" ,
834+ withIndexPlan .getIndexDescriptor ());
835+ assertEquals (1 , withIndexCount );
836+
837+ System .out .println ("Performance comparison for elemMatch on 10k documents:" );
838+ System .out .println (" Without index: " + timeNoIndex + " ms" );
839+ System .out .println (" With index: " + timeWithIndex + " ms" );
840+ System .out .println (" Improvement: " +
841+ (timeNoIndex > 0 ? String .format ("%.1fx" , timeNoIndex / (double ) Math .max (1 , timeWithIndex )) : "N/A" ));
842+
843+ // Index should provide measurable improvement
844+ assertTrue ("Index should improve performance or complete very quickly" ,
845+ timeWithIndex < timeNoIndex || timeWithIndex < 100 );
710846 }
711847}
0 commit comments