Offset semantics: offset skips N results the session can see. Without record locks it is passed straight to OpenSearch (unchanged fast path). With locks, hits are read from position 0 in batches (2x what is still needed, at least doubling the previous batch, capped at 1,000), filtered through QueryAction, the first offset accessible results are skipped and limit are kept. Consecutive pages no longer overlap or skip accessible hits.
Per-table mode now honors offset within each table, with the same semantics.
Every OpenSearch request keeps from + size within QuickSearchOpenSearchClient.MAX_RESULT_WINDOW (10,000). Accessible results beyond the first 10,000 raw hits are not reachable; this is documented on QuickSearchInput.getOffset(), the action's class comment and the README.
With locks on, totalHits counts results confirmed accessible; totalHitsIsLowerBound is true only when the scan stopped before reading every candidate (exact once every hit was read). hasMore is true when an accessible result exists past the page or unread candidates remain.
Hits are deduplicated by table and record id within a call, and the batches of one scan share a per-call preference so they hit the same shard copies (no mapping change). A refresh between batches can still move a hit across a batch boundary; dedupe prevents duplicates, but such a hit can be missed.
Work bound (security review)
A single-list search with locks reads at most 10,000 raw hits (MAX_RESULT_WINDOW) in batches of at most 1,000 (MAX_LOCKED_BATCH_SIZE) over at most 32 OpenSearch requests (MAX_LOCKED_ROUND_TRIPS), each followed by one record-lock QueryAction per table in the batch (ids chunked at 1,000).
In per-table mode the T searched tables share that budget: each table gets max(10,000 / T, min(10,000, 2 x limitPerTable)) hits and max(2, 32 / T) requests, and its last allowed request takes the rest of its share. One call therefore reads at most max(10,000, T x 2 x limitPerTable) raw hits over at most max(32, 2T) requests and as many lock queries, never more than develop's per-table cost or one full-window scan. Offset never raises the budget, and a fully locked table cannot starve the others. A spent share ends paging in that table like the result window does: hasMore turns false and totalHitsIsLowerBound stays true, so paging within a table stops past max(10,000 / T, 2 x limitPerTable) raw hits and a client looping while hasMore always terminates. The last allowed request takes up to 1,000 hits of the remaining share (documented in the README and QuickSearchInput.getOffset()).
Testing
QuickSearchActionSecurityTest: lock pages do not overlap or skip; offset past the accessible results; per-table offset with and without locks; the 10,000 cap and 1,000 batch cap; a deep offset (4,975) batching; accessible hits found only in later batches; a hit repeated across two batches counted once with an offset; paging per table until hasMore is false terminates; fully locked scans stay within the request and trip bounds with rawFloor reported; a fully locked table with 12,000 hits does not starve a second table; total raw hits across two fully locked tables stay within max(10,000, T x 2 x limitPerTable); exact total when every hit was read.
OpenSearchIntegrationTest pagination test extended with a per-table offset check against a real OpenSearch.
CI=true ./mvnw verify: 319 unit and 18 integration tests pass, JaCoCo coverage checks met.
## Summary
Fixes search paging in `QuickSearchAction`.
- Offset semantics: `offset` skips N results the session can see. Without record locks it is passed straight to OpenSearch (unchanged fast path). With locks, hits are read from position 0 in batches (2x what is still needed, at least doubling the previous batch, capped at 1,000), filtered through `QueryAction`, the first `offset` accessible results are skipped and `limit` are kept. Consecutive pages no longer overlap or skip accessible hits.
- Per-table mode now honors `offset` within each table, with the same semantics.
- Every OpenSearch request keeps `from + size` within `QuickSearchOpenSearchClient.MAX_RESULT_WINDOW` (10,000). Accessible results beyond the first 10,000 raw hits are not reachable; this is documented on `QuickSearchInput.getOffset()`, the action's class comment and the README.
- With locks on, `totalHits` counts results confirmed accessible; `totalHitsIsLowerBound` is true only when the scan stopped before reading every candidate (exact once every hit was read). `hasMore` is true when an accessible result exists past the page or unread candidates remain.
- Hits are deduplicated by table and record id within a call, and the batches of one scan share a per-call `preference` so they hit the same shard copies (no mapping change). A refresh between batches can still move a hit across a batch boundary; dedupe prevents duplicates, but such a hit can be missed.
## Work bound (security review)
A single-list search with locks reads at most 10,000 raw hits (`MAX_RESULT_WINDOW`) in batches of at most 1,000 (`MAX_LOCKED_BATCH_SIZE`) over at most 32 OpenSearch requests (`MAX_LOCKED_ROUND_TRIPS`), each followed by one record-lock `QueryAction` per table in the batch (ids chunked at 1,000).
In per-table mode the T searched tables share that budget: each table gets max(10,000 / T, min(10,000, 2 x limitPerTable)) hits and max(2, 32 / T) requests, and its last allowed request takes the rest of its share. One call therefore reads at most max(10,000, T x 2 x limitPerTable) raw hits over at most max(32, 2T) requests and as many lock queries, never more than develop's per-table cost or one full-window scan. Offset never raises the budget, and a fully locked table cannot starve the others. A spent share ends paging in that table like the result window does: `hasMore` turns false and `totalHitsIsLowerBound` stays true, so paging within a table stops past max(10,000 / T, 2 x limitPerTable) raw hits and a client looping while `hasMore` always terminates. The last allowed request takes up to 1,000 hits of the remaining share (documented in the README and `QuickSearchInput.getOffset()`).
## Testing
- `QuickSearchActionSecurityTest`: lock pages do not overlap or skip; offset past the accessible results; per-table offset with and without locks; the 10,000 cap and 1,000 batch cap; a deep offset (4,975) batching; accessible hits found only in later batches; a hit repeated across two batches counted once with an offset; paging per table until `hasMore` is false terminates; fully locked scans stay within the request and trip bounds with `rawFloor` reported; a fully locked table with 12,000 hits does not starve a second table; total raw hits across two fully locked tables stay within max(10,000, T x 2 x limitPerTable); exact total when every hit was read.
- `OpenSearchIntegrationTest` pagination test extended with a per-table offset check against a real OpenSearch.
- `CI=true ./mvnw verify`: 319 unit and 18 integration tests pass, JaCoCo coverage checks met.
Closes #16
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Fixes search paging in
QuickSearchAction.offsetskips N results the session can see. Without record locks it is passed straight to OpenSearch (unchanged fast path). With locks, hits are read from position 0 in batches (2x what is still needed, at least doubling the previous batch, capped at 1,000), filtered throughQueryAction, the firstoffsetaccessible results are skipped andlimitare kept. Consecutive pages no longer overlap or skip accessible hits.offsetwithin each table, with the same semantics.from + sizewithinQuickSearchOpenSearchClient.MAX_RESULT_WINDOW(10,000). Accessible results beyond the first 10,000 raw hits are not reachable; this is documented onQuickSearchInput.getOffset(), the action's class comment and the README.totalHitscounts results confirmed accessible;totalHitsIsLowerBoundis true only when the scan stopped before reading every candidate (exact once every hit was read).hasMoreis true when an accessible result exists past the page or unread candidates remain.preferenceso they hit the same shard copies (no mapping change). A refresh between batches can still move a hit across a batch boundary; dedupe prevents duplicates, but such a hit can be missed.Work bound (security review)
A single-list search with locks reads at most 10,000 raw hits (
MAX_RESULT_WINDOW) in batches of at most 1,000 (MAX_LOCKED_BATCH_SIZE) over at most 32 OpenSearch requests (MAX_LOCKED_ROUND_TRIPS), each followed by one record-lockQueryActionper table in the batch (ids chunked at 1,000).In per-table mode the T searched tables share that budget: each table gets max(10,000 / T, min(10,000, 2 x limitPerTable)) hits and max(2, 32 / T) requests, and its last allowed request takes the rest of its share. One call therefore reads at most max(10,000, T x 2 x limitPerTable) raw hits over at most max(32, 2T) requests and as many lock queries, never more than develop's per-table cost or one full-window scan. Offset never raises the budget, and a fully locked table cannot starve the others. A spent share ends paging in that table like the result window does:
hasMoreturns false andtotalHitsIsLowerBoundstays true, so paging within a table stops past max(10,000 / T, 2 x limitPerTable) raw hits and a client looping whilehasMorealways terminates. The last allowed request takes up to 1,000 hits of the remaining share (documented in the README andQuickSearchInput.getOffset()).Testing
QuickSearchActionSecurityTest: lock pages do not overlap or skip; offset past the accessible results; per-table offset with and without locks; the 10,000 cap and 1,000 batch cap; a deep offset (4,975) batching; accessible hits found only in later batches; a hit repeated across two batches counted once with an offset; paging per table untilhasMoreis false terminates; fully locked scans stay within the request and trip bounds withrawFloorreported; a fully locked table with 12,000 hits does not starve a second table; total raw hits across two fully locked tables stay within max(10,000, T x 2 x limitPerTable); exact total when every hit was read.OpenSearchIntegrationTestpagination test extended with a per-table offset check against a real OpenSearch.CI=true ./mvnw verify: 319 unit and 18 integration tests pass, JaCoCo coverage checks met.Closes #16