diff --git a/apps/desktop/src/components/document/DocumentBrowser.vue b/apps/desktop/src/components/document/DocumentBrowser.vue index 730b549160..64f2dfa223 100644 --- a/apps/desktop/src/components/document/DocumentBrowser.vue +++ b/apps/desktop/src/components/document/DocumentBrowser.vue @@ -22,7 +22,7 @@ import type { DynamoDbIndexInfo, DynamoDbTableDescription } from "@/lib/backend/ import { useConnectionStore } from "@/stores/connectionStore"; import { clampSearchSplitWidth } from "@/lib/dataGrid/dataGridSearchSplit"; import { documentViewerFontStyle } from "@/lib/document/documentViewerFontStyle"; -import { clampDocumentPage, documentPageRequestLimit, resetElasticsearchDocumentTotals, resolveElasticsearchDocumentTotals } from "@/lib/document/elasticsearchDocumentTotals"; +import { clampDocumentPage, clampElasticsearchRequestLimit, documentPageRequestLimit, resetElasticsearchDocumentTotals, resolveElasticsearchDocumentTotals } from "@/lib/document/elasticsearchDocumentTotals"; import { canGoNextDocumentPage, isSameDocumentQueryTotalCountRequest, resolveDocumentQueryTotals, type DocumentQueryTotalCountRequest } from "@/lib/document/documentQueryTotals"; import { arrayObjectAncestorPathForDocumentField, @@ -246,7 +246,7 @@ const canGoNextPage = computed(() => { }); }); const documentRequestLimit = computed(() => { - if (documentStoreProvider.value.kind !== "elasticsearch" || paginationTotal.value === undefined) return pageSize.value; + if (documentStoreProvider.value.kind !== "elasticsearch") return pageSize.value; return documentPageRequestLimit(page.value, pageSize.value, paginationTotal.value); }); @@ -1357,7 +1357,10 @@ async function load(options: { page?: number; append?: boolean; offset?: number; throw new Error(t("dynamodb.pageCursorUnavailable")); } const skip = storeKind === "dynamodb" ? 0 : (options.offset ?? requestPage * pageSize.value); - const requestLimit = options.limit ?? (storeKind === "elasticsearch" && paginationTotal.value !== undefined ? documentPageRequestLimit(requestPage, pageSize.value, paginationTotal.value) : pageSize.value); + const requestedLimit = options.limit ?? (storeKind === "elasticsearch" ? documentPageRequestLimit(requestPage, pageSize.value, paginationTotal.value) : pageSize.value); + // Elasticsearch rejects from+size beyond index.max_result_window even when an explicit limit + // (e.g. from the per-tab page size control) bypassed the default-path clamp above. + const requestLimit = storeKind === "elasticsearch" ? clampElasticsearchRequestLimit(skip, requestedLimit) : requestedLimit; const result = await api.documentFindDocuments(connectionId, database, collection, skip, requestLimit, filter, undefined, sort, undefined, executionId, cursor); if (documentLoadExecutionId.value !== executionId) return; if (connectionId !== props.connectionId || database !== props.database || collection !== props.collection || storeKind !== documentStoreProvider.value.kind) return; diff --git a/apps/desktop/src/lib/__tests__/document/elasticsearchDocumentTotals.spec.ts b/apps/desktop/src/lib/__tests__/document/elasticsearchDocumentTotals.spec.ts index 10b0634bf2..95452f2b6a 100644 --- a/apps/desktop/src/lib/__tests__/document/elasticsearchDocumentTotals.spec.ts +++ b/apps/desktop/src/lib/__tests__/document/elasticsearchDocumentTotals.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { clampDocumentPage, documentPageRequestLimit, resetElasticsearchDocumentTotals, resolveElasticsearchDocumentTotals } from "@/lib/document/elasticsearchDocumentTotals"; +import { clampDocumentPage, clampElasticsearchRequestLimit, documentPageRequestLimit, resetElasticsearchDocumentTotals, resolveElasticsearchDocumentTotals } from "@/lib/document/elasticsearchDocumentTotals"; describe("Elasticsearch document totals", () => { it("keeps a lower-bound search total separate from an exact background count", () => { @@ -42,4 +42,18 @@ describe("Elasticsearch document totals", () => { expect(documentPageRequestLimit(30, 333, 10_000)).toBe(10); expect(documentPageRequestLimit(0, 100, undefined)).toBe(100); }); + + it("caps the initial page request to Elasticsearch's default max_result_window even without a known total (#7231)", () => { + // Opening an index for the first time has no paginationTotal yet; a "rows per page" preference + // set above 10,000 must not be forwarded to Elasticsearch as-is, or the from+size search fails outright. + expect(documentPageRequestLimit(0, 100_000, undefined)).toBe(10_000); + expect(documentPageRequestLimit(0, 5_000, undefined)).toBe(5_000); + }); + + it("clamps an explicit request limit against the remaining from+size window at a given skip", () => { + expect(clampElasticsearchRequestLimit(0, 100_000)).toBe(10_000); + expect(clampElasticsearchRequestLimit(9_000, 5_000)).toBe(1_000); + expect(clampElasticsearchRequestLimit(20_000, 5_000)).toBe(0); + expect(clampElasticsearchRequestLimit(0, 500)).toBe(500); + }); }); diff --git a/apps/desktop/src/lib/document/elasticsearchDocumentTotals.ts b/apps/desktop/src/lib/document/elasticsearchDocumentTotals.ts index 91f63f09ec..4e432dbebc 100644 --- a/apps/desktop/src/lib/document/elasticsearchDocumentTotals.ts +++ b/apps/desktop/src/lib/document/elasticsearchDocumentTotals.ts @@ -41,9 +41,24 @@ export function clampDocumentPage(page: number, pageSize: number, paginationTota return Math.min(normalizedPage, lastPage); } +// Elasticsearch rejects any from+size combination above this by default (index.max_result_window). +// DBX can't know a target index's actual configured value up front, so it clamps requests to the +// out-of-the-box default rather than letting an oversized "rows per page" preference reach the cluster as-is. +export const ELASTICSEARCH_DEFAULT_MAX_RESULT_WINDOW = 10_000; + +// Clamps a requested page size against the from+size window still available at a given skip offset, +// regardless of where the requested limit came from (a settings default or an explicit override). +export function clampElasticsearchRequestLimit(skip: number, limit: number): number { + const normalizedSkip = Math.max(0, Math.floor(skip)); + const normalizedLimit = Math.max(1, Math.floor(limit)); + return Math.min(normalizedLimit, Math.max(0, ELASTICSEARCH_DEFAULT_MAX_RESULT_WINDOW - normalizedSkip)); +} + export function documentPageRequestLimit(page: number, pageSize: number, paginationTotal?: number): number { const normalizedPageSize = Math.max(1, Math.floor(pageSize)); - if (paginationTotal === undefined) return normalizedPageSize; - const remaining = paginationTotal - Math.max(0, Math.floor(page)) * normalizedPageSize; - return remaining > 0 ? Math.min(normalizedPageSize, remaining) : normalizedPageSize; + const skip = Math.max(0, Math.floor(page)) * normalizedPageSize; + const cappedPageSize = clampElasticsearchRequestLimit(skip, normalizedPageSize); + if (paginationTotal === undefined) return cappedPageSize; + const remaining = paginationTotal - skip; + return remaining > 0 ? Math.min(cappedPageSize, remaining) : cappedPageSize; }