rishabhdaim commented on code in PR #3143:
URL: https://github.com/apache/jackrabbit-oak/pull/3143#discussion_r4093542858


##########
oak-search-mongot/src/main/java/org/apache/jackrabbit/oak/plugins/index/mongot/query/MongotIndex.java:
##########
@@ -218,8 +223,26 @@ private Cursor streamingCursor(MongoCollection<Document> 
collection,
                     MongotResultAdapter.excerpts(result, 
request.excerptColumns()),
                     null, null);
         }, documents::close);
+        // Use a query-specific $count aggregation instead of rows::getSize: 
the latter
+        // would fully drain the streaming cursor (in aggregateCursor()'s 
batches) the
+        // moment anything calls Cursor.getSize()/JCR's RowIterator.getSize() 
- a common
+        // "N results found" UI pattern - even when the caller never intends 
to iterate
+        // all rows. $count is cheap (server-side count, no document bodies 
returned)
+        // and, unlike the unused getSizeEstimator(IndexPlan) override above, 
reflects
+        // this query's actual filter rather than the whole index's document 
count.
+        SizeEstimator sizeEstimator = () -> countMatches(collection, pipeline, 
definition);
         return new FulltextPathCursor(rows, NEVER_REWOUND, plan,
-                plan.getFilter().getQueryLimits(), rows::getSize);
+                plan.getFilter().getQueryLimits(), sizeEstimator);
+    }
+
+    private static long countMatches(MongoCollection<Document> collection,
+                                     List<Document> pipeline,
+                                     MongotIndexDefinition definition) {
+        List<Document> countPipeline = new ArrayList<>(pipeline);
+        countPipeline.add(new Document("$count", "n"));

Review Comment:
   ๐ŸŸ  **P2** ยท ๐ŸŽฏ Correctness
   
   **๐Ÿ” Issue:** `$count` runs before `transformPath`, duplicate suppression, 
and `shouldInclude`; a relative-property plan can therefore map several Mongo 
hits to one returned Oak path, making exact size larger than iteration.
   
   **๐Ÿ› ๏ธ Fix:** Use the server-side count only for one-to-one plans; otherwise 
count accepted paths through the same rules, for example:
   ```java
   Set<String> paths = new HashSet<>();
   long count = 0;
   while (cursor.hasNext()) {
       String path = getPlanResult(plan).transformPath(
               cursor.next().getString(MongoFieldNames.PATH));
       if (path != null && paths.add(path) && shouldInclude(path, plan)) {
           count++;
       }
   }
   return count;
   ```
   Please add a regression case where multiple relative-property hits transform 
to the same parent and assert `getSize()` equals the iterated row count.



##########
oak-search-mongot/src/main/java/org/apache/jackrabbit/oak/plugins/index/mongot/query/MongotIndex.java:
##########
@@ -218,8 +223,26 @@ private Cursor streamingCursor(MongoCollection<Document> 
collection,
                     MongotResultAdapter.excerpts(result, 
request.excerptColumns()),
                     null, null);
         }, documents::close);
+        // Use a query-specific $count aggregation instead of rows::getSize: 
the latter
+        // would fully drain the streaming cursor (in aggregateCursor()'s 
batches) the
+        // moment anything calls Cursor.getSize()/JCR's RowIterator.getSize() 
- a common
+        // "N results found" UI pattern - even when the caller never intends 
to iterate
+        // all rows. $count is cheap (server-side count, no document bodies 
returned)
+        // and, unlike the unused getSizeEstimator(IndexPlan) override above, 
reflects
+        // this query's actual filter rather than the whole index's document 
count.
+        SizeEstimator sizeEstimator = () -> countMatches(collection, pipeline, 
definition);

Review Comment:
   ๐ŸŸ  **P2** ยท ๐Ÿ”ง Resource lifecycle
   
   **๐Ÿ” Issue:** The result `MongoCursor` is opened before this estimator is 
returned, but a size-only caller never advances `rows`, so `documents::close` 
is never reached and the result cursor/first batch remains open.
   
   **๐Ÿ› ๏ธ Fix:** Make `MongotResultIterator` initialize its source on first row 
access, e.g. pass `() -> aggregateCursor(collection, pipeline, definition)` and 
keep a nullable realized cursor that is closed on exhaustion/failure. Add a 
test that calls only `getSize()` and verifies the result-cursor supplier was 
not invoked.



##########
oak-search-mongot/src/main/java/org/apache/jackrabbit/oak/plugins/index/mongot/query/MongotIndex.java:
##########
@@ -218,8 +223,26 @@ private Cursor streamingCursor(MongoCollection<Document> 
collection,
                     MongotResultAdapter.excerpts(result, 
request.excerptColumns()),
                     null, null);
         }, documents::close);
+        // Use a query-specific $count aggregation instead of rows::getSize: 
the latter
+        // would fully drain the streaming cursor (in aggregateCursor()'s 
batches) the
+        // moment anything calls Cursor.getSize()/JCR's RowIterator.getSize() 
- a common
+        // "N results found" UI pattern - even when the caller never intends 
to iterate
+        // all rows. $count is cheap (server-side count, no document bodies 
returned)
+        // and, unlike the unused getSizeEstimator(IndexPlan) override above, 
reflects
+        // this query's actual filter rather than the whole index's document 
count.
+        SizeEstimator sizeEstimator = () -> countMatches(collection, pipeline, 
definition);

Review Comment:
   ๐ŸŸก **P3** ยท โšก Performance
   
   **๐Ÿ” Issue:** `FulltextPathCursor` uses `estimatedSize != 0` as its cache 
sentinel, so this estimator is invoked again after every zero result and each 
call issues another aggregation.
   
   **๐Ÿ› ๏ธ Fix:** Cache initialization separately in `FulltextPathCursor`:
   ```java
   private boolean sizeEstimated;
   
   if (!sizeEstimated) {
       estimatedSize = sizeEstimator.getSize();
       sizeEstimated = true;
   }
   return estimatedSize;
   ```
   A regression test should call exact `getSize()` twice on an empty result and 
assert one count aggregation.



##########
oak-search-mongot/src/main/java/org/apache/jackrabbit/oak/plugins/index/mongot/query/MongotIndex.java:
##########
@@ -218,8 +223,26 @@ private Cursor streamingCursor(MongoCollection<Document> 
collection,
                     MongotResultAdapter.excerpts(result, 
request.excerptColumns()),
                     null, null);
         }, documents::close);
+        // Use a query-specific $count aggregation instead of rows::getSize: 
the latter
+        // would fully drain the streaming cursor (in aggregateCursor()'s 
batches) the
+        // moment anything calls Cursor.getSize()/JCR's RowIterator.getSize() 
- a common
+        // "N results found" UI pattern - even when the caller never intends 
to iterate
+        // all rows. $count is cheap (server-side count, no document bodies 
returned)
+        // and, unlike the unused getSizeEstimator(IndexPlan) override above, 
reflects
+        // this query's actual filter rather than the whole index's document 
count.
+        SizeEstimator sizeEstimator = () -> countMatches(collection, pipeline, 
definition);
         return new FulltextPathCursor(rows, NEVER_REWOUND, plan,
-                plan.getFilter().getQueryLimits(), rows::getSize);
+                plan.getFilter().getQueryLimits(), sizeEstimator);
+    }
+
+    private static long countMatches(MongoCollection<Document> collection,
+                                     List<Document> pipeline,
+                                     MongotIndexDefinition definition) {
+        List<Document> countPipeline = new ArrayList<>(pipeline);
+        countPipeline.add(new Document("$count", "n"));

Review Comment:
   ๐ŸŸ  **P2** ยท โšก Performance
   
   **๐Ÿ” Issue:** Cloning the result pipeline carries `$sort` into `$count`; 
sorting does not change cardinality, but it is a blocking stage and can make an 
ordered exact-size request process and buffer the full ordered set.
   
   **๐Ÿ› ๏ธ Fix:** Build a count-specific pipeline that keeps search/filter stages 
and the required sort-field `$match`, but drops result-only `$sort` and the 
final projection before appending `$count`. At minimum, remove top-level 
`$sort` stages from the copied pipeline and add a test that inspects the 
ordered-query count pipeline.



##########
oak-search-mongot/src/main/java/org/apache/jackrabbit/oak/plugins/index/mongot/query/MongotIndex.java:
##########
@@ -268,9 +291,30 @@ private static MongoCursor<Document> 
aggregateCursor(MongoCollection<Document> c
                 + 
TimeUnit.MILLISECONDS.toNanos(SEARCH_READINESS_TIMEOUT_MILLIS);
         while (true) {
             try {
-                int[] fetchSizes = definition.getQueryFetchSizes();
+                // Each driver batch is a full round-trip to the server, so 
the batch
+                // size should be as large as the configuration allows: using 
the
+                // smallest configured fetch size here previously turned a 
query with
+                // many hits into hundreds of round-trips and dominated query 
latency
+                // (e.g. ~18s for ~2000 hits at fetchSizes[0]=10, vs. <1s at 
1000; the
+                // MongoDB driver holds one batch at a time, and the server 
caps a
+                // single batch response at 16MB regardless of the requested 
size, so
+                // this is safe rather than causing unbounded per-batch 
memory/time).
+                //
+                // NOTE / follow-up optimization opportunity (out of scope 
here): this
+                // always uses the largest configured size, even for queries 
that only
+                // need a handful of rows (e.g. a small LIMIT / typeahead 
query), which
+                // wastes server/network work building an oversized first 
batch. A
+                // properly adaptive scheme (small first batch, growing only 
if the
+                // caller keeps pulling) needs a row-limit hint to be threaded 
through
+                // from the query plan down to here - that hint doesn't 
currently exist
+                // in the backend-neutral IndexPlan/Filter/FulltextIndex SPI 
(oak-search)
+                // that this and the Elastic/Lucene backends all implement, so 
it isn't
+                // something this module can fix alone. Worth revisiting as a 
shared,
+                // implementation-neutral improvement to 
FulltextIndex/IndexPlan rather
+                // than a mongot-only special case.
+                int batchSize = 
Arrays.stream(definition.getQueryFetchSizes()).max().getAsInt();

Review Comment:
   ๐ŸŸ  **P2** ยท ๐Ÿ”„ Compatibility
   
   **๐Ÿ” Issue:** Existing definitions treat `queryFetchSizes` as the 
Elastic-compatible sequence, but this changes every Mongot initial batch to its 
maximum; custom definitions that intentionally start small silently change 
behavior.
   
   **๐Ÿ› ๏ธ Fix:** Add a scalar Mongot `queryBatchSize` and use 
`definition.getQueryBatchSize()` here. For compatibility, preserve 
`queryFetchSizes[0]` when a legacy custom sequence is present and the new 
scalar is absent; the new default can still be `1000` for definitions using 
defaults.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to