From 0109329a00719d5198979c91c92e92af12272add Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Sun, 18 Oct 2026 14:20:42 -0600 Subject: [PATCH 01/51] Document full-text snapshot continuation --- reference/resources/full-text-paging.md | 155 ++++++++++++++++++++++++ sidebarsReference.ts | 5 + 2 files changed, 160 insertions(+) create mode 100644 reference/resources/full-text-paging.md diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md new file mode 100644 index 000000000..dc8413084 --- /dev/null +++ b/reference/resources/full-text-paging.md @@ -0,0 +1,155 @@ +--- +title: Full-Text Paging +--- + +# Full-Text Paging + + + +Full-text continuation lets a client page through the original search matches while indexing +updates arrive. It is available through `Table.search()` and REST. Ordinary offset-based paging +continues to run each request against its own index snapshot. + +## Declare a full-text index + +Full-text indexes require RocksDB and an audited table. The query names the `FullText` declaration, +not a stored source field: + +```graphql +type Product @table(audit: true) { + id: ID @primaryKey + title: String + catalogSearch: FullText @fullText(fields: [{ name: "title" }]) +} +``` + +`catalogSearch` is a query-only declaration. Select the stored fields and optional `$score` in results. +Full-text matches use descending relevance order. + +## Page through `Table.search()` + +Set `continuation: true` to start. Continuation mode returns a promise for a bounded array; +ordinary searches keep their async iterable return value. + +```javascript +const query = { + conditions: [{ attribute: 'catalogSearch', comparator: 'matches', value: 'trail shoes' }], + select: ['id', 'title', '$score'], + limit: 25, +}; + +let continuation = true; +do { + const page = await tables.Product.search({ ...query, continuation }); + for (const record of page) console.log(record); + continuation = page.continuation; +} while (continuation); +``` + +The returned array has three additional properties: + +| Property | Meaning | +| -------------- | ------------------------------------------------------------------- | +| `continuation` | Token for the next page; absent when traversal is complete | +| `complete` | Whether the original search has been exhausted or explicitly closed | +| `expiresAt` | Absolute expiry time in milliseconds since the Unix epoch | + +These properties are not included when the array is serialized as JSON. Keep the same conditions +and selected fields when resuming. The page size may change. Do not combine continuation with a +nonzero offset, count request, or another sort order. + +In an authenticated custom Resource, pass `this.getContext()` as the second argument to +`tables.Product.search()` and set `checkPermission: true` in the query. This requests the caller's +table and searched-source permissions; passing context alone does not request that permission gate. +Application row filters still run on each page. + +## Page through REST + +Start with `continuation()`: + +```http +GET /Product/?catalogSearch=matches=trail%20shoes&select(id,title,$score)&limit(25)&continuation() +``` + +The body stays a JSON array. Read the next token from `Harper-Continuation`, URL-encode it, and +send it in `continuation(token)` with the same query: + +```javascript +const query = '/Product/?catalogSearch=matches=trail%20shoes&select(id,title,$score)&limit(25)'; +let token; +do { + const continuation = token ? `continuation(${encodeURIComponent(token)})` : 'continuation()'; + const response = await fetch(`${query}&${continuation}`); + if (!response.ok) throw new Error(await response.text()); + const records = await response.json(); + for (const record of records) console.log(record); + token = response.headers.get('Harper-Continuation'); +} while (token); +``` + +`Harper-Continuation-Complete` reports completion. `Harper-Continuation-Expires` contains the +absolute expiry time in milliseconds since the Unix epoch. These headers are exposed to browser +clients. Continuation responses use `Cache-Control: no-store`. JSON `QUERY` requests return the +same headers when their query body enables continuation. + +## Records and permissions can change + +The snapshot fixes the index matches and their ranking. It does not retain historical Harper +records or permissions. Each request loads current records and checks current access. Updated, +deleted, expired, or newly unauthorized records can be omitted, and new matches do not enter the +original search. + +Retrying a token resumes the same index boundary. The returned rows can differ if records or +permissions changed. A page may be empty when the remaining snapshot contains no eligible records; +use the continuation token or completion flag to decide when to stop. + +## Close or restart a search + +To stop early, repeat the query with its current token and `closeContinuation: true`. In REST, add +`closeContinuation()` alongside `continuation(token)`. Closing returns an empty completed page. + +Completion, close, expiry, index reset, definition changes, worker loss, or process restart can +invalidate a token. Resuming an unavailable snapshot returns `410`; start a new search. Once any +request finishes traversal, older tokens for that search also expire. + +Snapshots belong to one Harper instance. Its workers can handle continuation requests for each +other. Requests reaching a different Harper instance cannot resume that snapshot. + +## Configure resource limits + +Settings live under `storage.fullText` in `harper-config.yaml`: + +```yaml +storage: + fullText: + cursorTtlMilliseconds: 60000 + maxCursors: 4 + maxPageSize: 1000 + requestBudgetMilliseconds: 5000 + maxRawHitsPerRequest: 100000 +``` + +| Setting | Default | Scope | +| --------------------------- | ------: | ----------------------------------------------------------- | +| `cursorTtlMilliseconds` | 60,000 | Absolute search lifetime; paging does not extend it | +| `maxCursors` | 4 | Retained searches across all workers in one process | +| `maxRetainedQueryBytes` | 8 MiB | Retained native query text and identity bytes | +| `maxPageSize` | 1,000 | Returned records per page | +| `requestBudgetMilliseconds` | 5,000 | Freshness wait and page processing budget | +| `maxRawHitsPerRequest` | 100,000 | Fetched native hits per request, including replayed batches | +| `maxPendingRequests` | 32 | Pending requests per requesting worker | +| `maxPendingResponseBytes` | 16 MiB | Reserved response bytes per requesting worker | + +The usual individual configuration names also work, such as +`storage_fullText_maxRawHitsPerRequest` with `set_configuration`. Native process-wide admission +settings live under `storage.fullText.runtime` and require a restart when changed. Their defaults +are 128 resident indexes, 128 indexing threads, 256 search threads, 4 GiB configured writer memory, +8 GiB configured queue capacity, and 8 concurrent expensive searches. All limits apply together. + +A raw-hit budget failure returns `400`; narrow the query or increase the budget. Capacity exhaustion +and timeouts return errors instead of successful incomplete pages. If the snapshot itself runs out +of eligible matches, a shorter completed page is a valid result. + +Retained readers can keep old index segments on disk until released. These settings do not provide +a hard limit on total process memory or retained disk space. Native deadlines are cooperative, so the +request budget is not a hard wall-clock guarantee. diff --git a/sidebarsReference.ts b/sidebarsReference.ts index f997c1810..d7ea4b862 100644 --- a/sidebarsReference.ts +++ b/sidebarsReference.ts @@ -87,6 +87,11 @@ const sidebars: SidebarsConfig = { id: 'resources/query-optimization', label: 'Query Optimization', }, + { + type: 'doc', + id: 'resources/full-text-paging', + label: 'Full-Text Paging', + }, ], }, { From 8b0809102ff78bba38c0d874afd157c0791f52b2 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Sun, 18 Oct 2026 17:51:47 -0600 Subject: [PATCH 02/51] Clarify continuation callback and shared capacity requirements --- reference/resources/full-text-paging.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index dc8413084..8512fefa8 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -62,6 +62,8 @@ In an authenticated custom Resource, pass `this.getContext()` as the second argu `tables.Product.search()` and set `checkPermission: true` in the query. This requests the caller's table and searched-source permissions; passing context alone does not request that permission gate. Application row filters still run on each page. +The token does not store application callbacks such as `rowFilter` or `vectorFilter`. Supply them +again on every page, together with the caller's context and permission settings. ## Page through REST @@ -111,6 +113,8 @@ To stop early, repeat the query with its current token and `closeContinuation: t Completion, close, expiry, index reset, definition changes, worker loss, or process restart can invalidate a token. Resuming an unavailable snapshot returns `410`; start a new search. Once any request finishes traversal, older tokens for that search also expire. +Changing any `storage.fullText` setting also invalidates existing tokens, even when only a capacity +setting changes. Start a new search after changing these settings. Snapshots belong to one Harper instance. Its workers can handle continuation requests for each other. Requests reaching a different Harper instance cannot resume that snapshot. @@ -146,6 +150,11 @@ settings live under `storage.fullText.runtime` and require a restart when change are 128 resident indexes, 128 indexing threads, 256 search threads, 4 GiB configured writer memory, 8 GiB configured queue capacity, and 8 concurrent expensive searches. All limits apply together. +The cursor pool is shared by all callers. Four unfinished searches fill the default pool, so a fifth +receives `503` until a search finishes, closes, or expires. Ordinary searches remain available. +Configure `maxCursors` for the number of unfinished searches the application needs to retain. +There is no per-caller reservation or eviction policy. + A raw-hit budget failure returns `400`; narrow the query or increase the budget. Capacity exhaustion and timeouts return errors instead of successful incomplete pages. If the snapshot itself runs out of eligible matches, a shorter completed page is a valid result. From 06a5a9bbd6887da8ddfe312433ec4f03bd9ee0c8 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Sun, 18 Oct 2026 21:11:13 -0600 Subject: [PATCH 03/51] Document continuation transport sizes and joint admission limits --- reference/resources/full-text-paging.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index 8512fefa8..07d08b11c 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -94,6 +94,11 @@ absolute expiry time in milliseconds since the Unix epoch. These headers are exp clients. Continuation responses use `Cache-Control: no-store`. JSON `QUERY` requests return the same headers when their query body enables continuation. +Long record IDs make continuation tokens larger. Qualification with the released library produced +tokens of about 600 bytes for short IDs and nearly 8 KB near the supported ID limit. Review Harper's +`http.maxHeaderSize` setting and any proxy response-header and request-line limits. Leave room for the +token, query, authentication, and other headers; a token that fits Harper may exceed a proxy limit. + ## Records and permissions can change The snapshot fixes the index matches and their ranking. It does not retain historical Harper @@ -155,6 +160,12 @@ receives `503` until a search finishes, closes, or expires. Ordinary searches re Configure `maxCursors` for the number of unfinished searches the application needs to retain. There is no per-caller reservation or eviction policy. +Cursor slots, pending-request counts, and response-byte allowances apply separately. Raising +`maxCursors` does not raise `maxPendingRequests` or `maxPendingResponseBytes`. Across workers, +Harper reserves response space using the library's largest supported IDs and versions, rather than +the average size of the current records. Many searches may be retained while fewer pages can run +at once. + A raw-hit budget failure returns `400`; narrow the query or increase the budget. Capacity exhaustion and timeouts return errors instead of successful incomplete pages. If the snapshot itself runs out of eligible matches, a shorter completed page is a valid result. From 6923c72c2ca8b75af282db35f9e7fcb2cc0d3351 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Sun, 18 Oct 2026 21:43:22 -0600 Subject: [PATCH 04/51] Clarify close acknowledgement and completed-page metadata --- reference/resources/full-text-paging.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index 07d08b11c..996041829 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -90,8 +90,8 @@ do { ``` `Harper-Continuation-Complete` reports completion. `Harper-Continuation-Expires` contains the -absolute expiry time in milliseconds since the Unix epoch. These headers are exposed to browser -clients. Continuation responses use `Cache-Control: no-store`. JSON `QUERY` requests return the +absolute expiry time in milliseconds since the Unix epoch. Completed pages omit the token and +expiry. These headers are exposed to browser clients. Continuation responses use `Cache-Control: no-store`. JSON `QUERY` requests return the same headers when their query body enables continuation. Long record IDs make continuation tokens larger. Qualification with the released library produced @@ -115,6 +115,11 @@ use the continuation token or completion flag to decide when to stop. To stop early, repeat the query with its current token and `closeContinuation: true`. In REST, add `closeContinuation()` alongside `continuation(token)`. Closing returns an empty completed page. +Close and terminal pages wait for admission release before reporting success. If release fails, the +request returns an error instead. The revoked token may then return `410`, and its admission slot +can remain occupied until absolute expiry or worker exit. A successful close releases admission +capacity; it does not promise immediate removal of old index segments from disk. + Completion, close, expiry, index reset, definition changes, worker loss, or process restart can invalidate a token. Resuming an unavailable snapshot returns `410`; start a new search. Once any request finishes traversal, older tokens for that search also expire. From 3fbaced8521a18fa568ae4baa325462d2b266cf1 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Mon, 19 Oct 2026 06:23:45 -0600 Subject: [PATCH 05/51] Explain HEAD behavior for full-text continuation --- reference/resources/full-text-paging.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index 996041829..f51ae07d0 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -94,6 +94,8 @@ absolute expiry time in milliseconds since the Unix epoch. Completed pages omit expiry. These headers are exposed to browser clients. Continuation responses use `Cache-Control: no-store`. JSON `QUERY` requests return the same headers when their query body enables continuation. +HEAD rejects continuation requests because it cannot return the page records. Use GET or QUERY. + Long record IDs make continuation tokens larger. Qualification with the released library produced tokens of about 600 bytes for short IDs and nearly 8 KB near the supported ID limit. Review Harper's `http.maxHeaderSize` setting and any proxy response-header and request-line limits. Leave room for the From 8ea7e2c2f2f9ad252799441b2affb911c1694b26 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Mon, 19 Oct 2026 07:09:12 -0600 Subject: [PATCH 06/51] Document acknowledged continuation completion after cleanup --- reference/resources/full-text-paging.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index f51ae07d0..01a235bc9 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -121,6 +121,8 @@ Close and terminal pages wait for admission release before reporting success. If request returns an error instead. The revoked token may then return `410`, and its admission slot can remain occupied until absolute expiry or worker exit. A successful close releases admission capacity; it does not promise immediate removal of old index segments from disk. +Closing has a separate cleanup budget. Once release is acknowledged, the completed page can return +after its search-work deadline. Cancellation, transaction, and index-lifecycle checks still apply. Completion, close, expiry, index reset, definition changes, worker loss, or process restart can invalidate a token. Resuming an unavailable snapshot returns `410`; start a new search. Once any From e29315e59f6a4690d38ff98cc7870a111fe0fa59 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Mon, 19 Oct 2026 08:15:03 -0600 Subject: [PATCH 07/51] Explain continuation cleanup timing and filtering costs --- reference/resources/full-text-paging.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index 01a235bc9..967406a6a 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -158,6 +158,18 @@ storage: | `maxPendingRequests` | 32 | Pending requests per requesting worker | | `maxPendingResponseBytes` | 16 MiB | Reserved response bytes per requesting worker | +Search work and cleanup use separate windows based on `requestBudgetMilliseconds`. With the +5,000 ms default, a search that uses its full work budget and then closes can take about 10 seconds. +Waiting for other page requests already using the snapshot can add time. Native work checks its +deadline while running; these settings are not a hard limit on total response time. + +Ordinary searches can use secondary indexes to skip record reads and enlarge batches when many +matches are filtered out. Continued searches do not yet use those optimizations; they fetch up to +256 native hits per batch and check current records. A selective filter can therefore add many +record reads and, when the snapshot belongs to another worker, many worker requests. +Qualification and these optimizations are tracked in +[Qualify native full-text filtering and catalog-scale operation for v5.4](https://github.com/HarperFast/harper/issues/2512). + The usual individual configuration names also work, such as `storage_fullText_maxRawHitsPerRequest` with `set_configuration`. Native process-wide admission settings live under `storage.fullText.runtime` and require a restart when changed. Their defaults From 9dc5dc57e82806a025e14fe4c817f2f8535aab80 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Mon, 19 Oct 2026 10:31:06 -0600 Subject: [PATCH 08/51] Document continuation method restrictions --- reference/resources/full-text-paging.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index 967406a6a..c335625ed 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -94,7 +94,8 @@ absolute expiry time in milliseconds since the Unix epoch. Completed pages omit expiry. These headers are exposed to browser clients. Continuation responses use `Cache-Control: no-store`. JSON `QUERY` requests return the same headers when their query body enables continuation. -HEAD rejects continuation requests because it cannot return the page records. Use GET or QUERY. +Only GET and QUERY can return continuation pages. HEAD and mutating methods reject continuation +parameters with 400. OPTIONS still returns request metadata without running a search. Long record IDs make continuation tokens larger. Qualification with the released library produced tokens of about 600 bytes for short IDs and nearly 8 KB near the supported ID limit. Review Harper's From 8dee0125d3468923dcb6a9d8f31bea26f092c469 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Mon, 19 Oct 2026 10:43:21 -0600 Subject: [PATCH 09/51] Clarify supported continuation methods --- reference/resources/full-text-paging.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index c335625ed..0606d1626 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -94,8 +94,8 @@ absolute expiry time in milliseconds since the Unix epoch. Completed pages omit expiry. These headers are exposed to browser clients. Continuation responses use `Cache-Control: no-store`. JSON `QUERY` requests return the same headers when their query body enables continuation. -Only GET and QUERY can return continuation pages. HEAD and mutating methods reject continuation -parameters with 400. OPTIONS still returns request metadata without running a search. +GET and QUERY can return continuation pages. OPTIONS returns request metadata without running a +search. Other methods reject continuation parameters with 400. Long record IDs make continuation tokens larger. Qualification with the released library produced tokens of about 600 bytes for short IDs and nearly 8 KB near the supported ID limit. Review Harper's From f23ba40fc04d7b69867435614b4abeb5911e296e Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 07:17:14 -0600 Subject: [PATCH 10/51] Clarify continuation availability and response handling --- reference/resources/full-text-paging.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index 0606d1626..4d55ee9de 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -4,11 +4,12 @@ title: Full-Text Paging # Full-Text Paging - + Full-text continuation lets a client page through the original search matches while indexing updates arrive. It is available through `Table.search()` and REST. Ordinary offset-based paging continues to run each request against its own index snapshot. +The same continuation options work with autocomplete searches using `matches_prefix` and `matches_fuzzy_prefix`. ## Declare a full-text index @@ -28,7 +29,8 @@ Full-text matches use descending relevance order. ## Page through `Table.search()` -Set `continuation: true` to start. Continuation mode returns a promise for a bounded array; +Set `continuation: true` and a positive `limit` to start. The limit must not exceed `maxPageSize`. +Continuation mode returns a promise for a bounded array; ordinary searches keep their async iterable return value. ```javascript @@ -56,7 +58,7 @@ The returned array has three additional properties: These properties are not included when the array is serialized as JSON. Keep the same conditions and selected fields when resuming. The page size may change. Do not combine continuation with a -nonzero offset, count request, or another sort order. +nonzero offset, count request, another sort order, `explain`, or `sync`. In an authenticated custom Resource, pass `this.getContext()` as the second argument to `tables.Product.search()` and set `checkPermission: true` in the query. This requests the caller's @@ -94,6 +96,9 @@ absolute expiry time in milliseconds since the Unix epoch. Completed pages omit expiry. These headers are exposed to browser clients. Continuation responses use `Cache-Control: no-store`. JSON `QUERY` requests return the same headers when their query body enables continuation. +Read and parse the complete response body before saving its next token. A stream can fail after +headers are sent. If that happens, retry with the previous token while the snapshot remains valid. + GET and QUERY can return continuation pages. OPTIONS returns request metadata without running a search. Other methods reject continuation parameters with 400. From ebfebdbe5b9ebf961aeee98458941611293aff3e Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 07:29:20 -0600 Subject: [PATCH 11/51] Clarify continuation context and HTTP exceptions --- reference/resources/full-text-paging.md | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index 4d55ee9de..a72fa6b2c 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -29,7 +29,7 @@ Full-text matches use descending relevance order. ## Page through `Table.search()` -Set `continuation: true` and a positive `limit` to start. The limit must not exceed `maxPageSize`. +Set `continuation: true` and a positive integer `limit` to start. The limit must not exceed `maxPageSize`. Continuation mode returns a promise for a bounded array; ordinary searches keep their async iterable return value. @@ -57,14 +57,17 @@ The returned array has three additional properties: | `expiresAt` | Absolute expiry time in milliseconds since the Unix epoch | These properties are not included when the array is serialized as JSON. Keep the same conditions -and selected fields when resuming. The page size may change. Do not combine continuation with a -nonzero offset, count request, another sort order, `explain`, or `sync`. - -In an authenticated custom Resource, pass `this.getContext()` as the second argument to -`tables.Product.search()` and set `checkPermission: true` in the query. This requests the caller's +and selected fields when resuming. The page size may change. Use the default relevance order, or explicitly sort by descending +`$score` alone. A nonzero `offset`, another sort, or a truthy `count`, `explain`, or `sync` option +returns 400. + +In a static custom Resource method, pass the method's `context` argument as the second argument +to `tables.Product.search()`. In an instance method, use `this.getContext()` instead. See +[Resource context](./resource-api.md#getcontext-context) for both forms. Set `checkPermission: true` +in the query. This requests the caller's table and searched-source permissions; passing context alone does not request that permission gate. Application row filters still run on each page. -The token does not store application callbacks such as `rowFilter` or `vectorFilter`. Supply them +The token does not store application callbacks such as `rowFilter`. Supply them again on every page, together with the caller's context and permission settings. ## Page through REST @@ -93,14 +96,16 @@ do { `Harper-Continuation-Complete` reports completion. `Harper-Continuation-Expires` contains the absolute expiry time in milliseconds since the Unix epoch. Completed pages omit the token and -expiry. These headers are exposed to browser clients. Continuation responses use `Cache-Control: no-store`. JSON `QUERY` requests return the +expiry. Harper adds these names to `Access-Control-Expose-Headers` so browser clients can read them. +Continuation responses override the usual read-cache policy with `Cache-Control: no-store`. JSON `QUERY` requests return the same headers when their query body enables continuation. Read and parse the complete response body before saving its next token. A stream can fail after headers are sent. If that happens, retry with the previous token while the snapshot remains valid. GET and QUERY can return continuation pages. OPTIONS returns request metadata without running a -search. Other methods reject continuation parameters with 400. +search. HEAD is an exception to the usual GET-without-a-body behavior: it returns 400 before +opening or advancing a continuation. Other methods also reject continuation parameters with 400. Long record IDs make continuation tokens larger. Qualification with the released library produced tokens of about 600 bytes for short IDs and nearly 8 KB near the supported ID limit. Review Harper's @@ -148,6 +153,8 @@ storage: fullText: cursorTtlMilliseconds: 60000 maxCursors: 4 + maxRetainedQueryBytes: 8388608 + maxPendingResponseBytes: 16777216 maxPageSize: 1000 requestBudgetMilliseconds: 5000 maxRawHitsPerRequest: 100000 From 48411eeb8c0d2bf32a9e45aabafc058b40229742 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 07:32:27 -0600 Subject: [PATCH 12/51] Complete continuation examples and retry guidance --- reference/resources/full-text-paging.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index a72fa6b2c..2d2ad7be2 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -4,6 +4,8 @@ title: Full-Text Paging # Full-Text Paging +Continuation paging is available in v5.4.0. The full-text index declaration below is a prerequisite. + Full-text continuation lets a client page through the original search matches while indexing @@ -17,7 +19,7 @@ Full-text indexes require RocksDB and an audited table. The query names the `Ful not a stored source field: ```graphql -type Product @table(audit: true) { +type Product @table(audit: true) @export { id: ID @primaryKey title: String catalogSearch: FullText @fullText(fields: [{ name: "title" }]) @@ -25,7 +27,8 @@ type Product @table(audit: true) { ``` `catalogSearch` is a query-only declaration. Select the stored fields and optional `$score` in results. -Full-text matches use descending relevance order. +Full-text matches use descending relevance order. REST examples also require `rest: true` in the +application configuration; see [REST setup](../rest/overview.md). ## Page through `Table.search()` @@ -57,7 +60,7 @@ The returned array has three additional properties: | `expiresAt` | Absolute expiry time in milliseconds since the Unix epoch | These properties are not included when the array is serialized as JSON. Keep the same conditions -and selected fields when resuming. The page size may change. Use the default relevance order, or explicitly sort by descending +and selected fields, and use the same authenticated caller when resuming. The page size may change. Use the default relevance order, or explicitly sort by descending `$score` alone. A nonzero `offset`, another sort, or a truthy `count`, `explain`, or `sync` option returns 400. @@ -66,13 +69,17 @@ to `tables.Product.search()`. In an instance method, use `this.getContext()` ins [Resource context](./resource-api.md#getcontext-context) for both forms. Set `checkPermission: true` in the query. This requests the caller's table and searched-source permissions; passing context alone does not request that permission gate. -Application row filters still run on each page. +Changing the caller identity, including switching from an authenticated request to an internal +call without that context, returns 410. Permission changes for the same caller are checked on each +page. Application row filters still run on each page. The token does not store application callbacks such as `rowFilter`. Supply them again on every page, together with the caller's context and permission settings. ## Page through REST -Start with `continuation()`: +Use the single-argument `limit(n)` page size and start with `continuation()`. Do not use +`limit(start,end)`, request a count with `Prefer: count=`, or change the relevance order. The same +`explain` and `sync` restrictions as `Table.search()` apply; violations return 400. ```http GET /Product/?catalogSearch=matches=trail%20shoes&select(id,title,$score)&limit(25)&continuation() @@ -101,7 +108,9 @@ Continuation responses override the usual read-cache policy with `Cache-Control: same headers when their query body enables continuation. Read and parse the complete response body before saving its next token. A stream can fail after -headers are sent. If that happens, retry with the previous token while the snapshot remains valid. +headers are sent. If a resumed page fails this way, retry with the previous token while the snapshot remains valid. +If no token was received on the first page, start a new search. The first search may keep its cursor +slot until absolute expiry because it cannot be closed without a token. GET and QUERY can return continuation pages. OPTIONS returns request metadata without running a search. HEAD is an exception to the usual GET-without-a-body behavior: it returns 400 before From c7e263e84d9135e8cac9f9f77b5b0a2b1ffc106d Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 07:37:49 -0600 Subject: [PATCH 13/51] Link continuation from configuration and REST references --- reference/configuration/options.md | 4 ++++ reference/resources/resource-api.md | 4 ++++ reference/rest/headers.md | 6 ++++++ reference/rest/overview.md | 4 +++- reference/rest/querying.md | 10 ++++++++++ 5 files changed, 27 insertions(+), 1 deletion(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index d12128410..23a5bf733 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -302,6 +302,10 @@ replication: Database storage configuration. See [Storage Tuning](../database/storage-tuning.md) for guidance on tuning these options for production workloads, [Database Overview](../database/overview.md) for general database concepts, and [Compaction](../database/compaction.md) for reclaiming space inside existing files. +`storage.fullText` controls retained full-text searches and native search capacity. See +[Full-Text Paging](../resources/full-text-paging.md#configure-resource-limits) for the settings, +defaults and restart requirements. + ```yaml storage: path: ~/hdb/database diff --git a/reference/resources/resource-api.md b/reference/resources/resource-api.md index da064a259..d76d88fb7 100644 --- a/reference/resources/resource-api.md +++ b/reference/resources/resource-api.md @@ -1035,6 +1035,10 @@ Array of condition objects to filter records. Each condition: | `conditions` | Nested conditions array | | `operator` | `and` (default) or `or` for the nested `conditions` | +Full-text conditions name a query-only `FullText` declaration and use comparators such as `matches`, +`matches_prefix`, and `matches_fuzzy_prefix`. See +[Full-Text Paging](./full-text-paging.md) for snapshot continuation and its query restrictions. + Example with nested conditions: ```javascript diff --git a/reference/rest/headers.md b/reference/rest/headers.md index 71603c802..8cb4675b9 100644 --- a/reference/rest/headers.md +++ b/reference/rest/headers.md @@ -22,11 +22,17 @@ These headers are included in all Harper REST API responses: Collection responses to a [count request](./querying.md#pagination-and-total-count) additionally include `Content-Range`, `Range-Unit`, and `Preference-Applied` (). +Full-text continuation responses also return `Harper-Continuation-Complete`, plus +`Harper-Continuation` and `Harper-Continuation-Expires` while the search remains open. These names +are added to `Access-Control-Expose-Headers`. See +[Full-Text Paging](../resources/full-text-paging.md#page-through-rest). + ## Cache-Control Harper applies a tiered Cache-Control policy to REST responses. +Full-text continuation responses override this policy with `Cache-Control: no-store`. ### Anonymous reads diff --git a/reference/rest/overview.md b/reference/rest/overview.md index 0f60b27c6..86b1826e4 100644 --- a/reference/rest/overview.md +++ b/reference/rest/overview.md @@ -88,7 +88,9 @@ With both in place, the exported name becomes the base path (`Product` above, or Notes on this surface: - The trailing slash is significant throughout: `/Product` addresses the resource itself, `/Product/` addresses its collection of records. See [URL Structure](#url-structure). -- `HEAD` is served exactly as `GET` with the response body omitted. `QUERY` is accepted on the collection path (`QUERY /Product/`) and runs a search taken from the request body rather than the URL. +- `HEAD` is served as `GET` with the response body omitted, except that + [full-text continuation requests](../resources/full-text-paging.md#page-through-rest) return `400` + before opening or advancing a search. `QUERY` is accepted on the collection path (`QUERY /Product/`) and runs a search taken from the request body rather than the URL. - A collection `POST` only generates a primary key when the body omits the table's primary-key property. If the body supplies one, Harper stores the record under that value rather than replacing it, and a `POST` to a key that already exists fails with `409` instead of overwriting. - On a successful `POST`, the new record's primary key — supplied or generated — is returned in the `Location` response header. The header carries the bare key value, not a URL, so it is not a link to follow. - `PUT` replaces the stored record, with three exceptions that Harper always applies: a [`@createdTime`](../database/schema.md#createdtime) attribute keeps the original record's value, an [`@updatedTime`](../database/schema.md#updatedtime) attribute is re-stamped with the time of the write, and the primary key is forced to match the `{id}` in the URL even if the body carries a different one. diff --git a/reference/rest/querying.md b/reference/rest/querying.md index b8d7d0e66..d2cd8a308 100644 --- a/reference/rest/querying.md +++ b/reference/rest/querying.md @@ -136,6 +136,16 @@ GET /Product/?price=lt=100|[rating=5&[tag=fast|tag=scalable|tag=efficient]&inSto Harper supports special query functions using call syntax, included in the query string separated by `&`. +### `continuation()` and `closeContinuation()` + + + +Full-text searches can retain their original index snapshot between requests. Start with +`continuation()`, resume with `continuation(token)`, and close early with `closeContinuation()` +alongside the token. Use `limit(n)` without a nonzero offset or a count request. See +[Full-Text Paging](../resources/full-text-paging.md#page-through-rest) for the token headers, +query restrictions and recovery steps. + ### `select(properties)` Specify which properties to include in the response. From f04f3e6abad83f36ed7f31a85b2615ef34c3e4e3 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 07:54:20 -0600 Subject: [PATCH 14/51] Tighten paging references and callback guidance --- reference/resources/full-text-paging.md | 11 +++++++---- reference/resources/overview.md | 1 + reference/resources/resource-api.md | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index 2d2ad7be2..110b11f29 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -55,7 +55,7 @@ The returned array has three additional properties: | Property | Meaning | | -------------- | ------------------------------------------------------------------- | -| `continuation` | Token for the next page; absent when traversal is complete | +| `continuation` | Token for the next page; `undefined` when traversal is complete | | `complete` | Whether the original search has been exhausted or explicitly closed | | `expiresAt` | Absolute expiry time in milliseconds since the Unix epoch | @@ -72,13 +72,14 @@ table and searched-source permissions; passing context alone does not request th Changing the caller identity, including switching from an authenticated request to an internal call without that context, returns 410. Permission changes for the same caller are checked on each page. Application row filters still run on each page. -The token does not store application callbacks such as `rowFilter`. Supply them +The token does not store application callbacks such as `rowFilter` or +[`vectorFilter`](../database/schema.md#filtered-vector-search-with-a-function-predicate). Supply them again on every page, together with the caller's context and permission settings. ## Page through REST Use the single-argument `limit(n)` page size and start with `continuation()`. Do not use -`limit(start,end)`, request a count with `Prefer: count=`, or change the relevance order. The same +a nonzero starting offset with `limit(start,end)`, request a count with `Prefer: count=`, or change the relevance order. The same `explain` and `sync` restrictions as `Table.search()` apply; violations return 400. ```http @@ -193,7 +194,9 @@ Qualification and these optimizations are tracked in [Qualify native full-text filtering and catalog-scale operation for v5.4](https://github.com/HarperFast/harper/issues/2512). The usual individual configuration names also work, such as -`storage_fullText_maxRawHitsPerRequest` with `set_configuration`. Native process-wide admission +`storage_fullText_maxRawHitsPerRequest` with `set_configuration`. Follow the +[configuration restart procedure](../configuration/operations.md#set-configuration) after applying +changes with that operation. Native process-wide admission settings live under `storage.fullText.runtime` and require a restart when changed. Their defaults are 128 resident indexes, 128 indexing threads, 256 search threads, 4 GiB configured writer memory, 8 GiB configured queue capacity, and 8 concurrent expensive searches. All limits apply together. diff --git a/reference/resources/overview.md b/reference/resources/overview.md index d0574af6b..4a00d8f17 100644 --- a/reference/resources/overview.md +++ b/reference/resources/overview.md @@ -172,3 +172,4 @@ Parameterized routes also surface in the generated OpenAPI document (as template | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | [Resource API](./resource-api.md) | Complete reference for instance methods, static methods, the Query object, RequestTarget, and response handling | | [Query Optimization](./query-optimization.md) | How Harper executes queries and how to write performant conditions | +| [Full-Text Paging](./full-text-paging.md) | Resume full-text and autocomplete searches on their original index snapshot | diff --git a/reference/resources/resource-api.md b/reference/resources/resource-api.md index d76d88fb7..485a89cb4 100644 --- a/reference/resources/resource-api.md +++ b/reference/resources/resource-api.md @@ -1035,8 +1035,8 @@ Array of condition objects to filter records. Each condition: | `conditions` | Nested conditions array | | `operator` | `and` (default) or `or` for the nested `conditions` | -Full-text conditions name a query-only `FullText` declaration and use comparators such as `matches`, -`matches_prefix`, and `matches_fuzzy_prefix`. See +Full-text conditions name a query-only `FullText` declaration and use comparators `matches`, +`matches_all`, `matches_phrase`, `matches_prefix`, `matches_fuzzy`, and `matches_fuzzy_prefix`. See [Full-Text Paging](./full-text-paging.md) for snapshot continuation and its query restrictions. Example with nested conditions: From e22a0e6bfaa6a684ac49e207979ccba9acf134ca Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 07:55:30 -0600 Subject: [PATCH 15/51] Show copyable native runtime capacity settings --- reference/resources/full-text-paging.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index 110b11f29..54a5ef02f 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -201,6 +201,20 @@ settings live under `storage.fullText.runtime` and require a restart when change are 128 resident indexes, 128 indexing threads, 256 search threads, 4 GiB configured writer memory, 8 GiB configured queue capacity, and 8 concurrent expensive searches. All limits apply together. +The runtime keys use integer counts or bytes: + +```yaml +storage: + fullText: + runtime: + maxResidentIndexes: 128 + maxIndexingThreads: 128 + maxSearchThreads: 256 + maxWriterMemoryBytes: 4294967296 + maxQueuedBytes: 8589934592 + maxExpensiveSearches: 8 +``` + The cursor pool is shared by all callers. Four unfinished searches fill the default pool, so a fifth receives `503` until a search finishes, closes, or expires. Ordinary searches remain available. Configure `maxCursors` for the number of unfinished searches the application needs to retain. From b995dbe8ca953d8e19afbf320e20dc864af59e29 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 10:04:19 -0600 Subject: [PATCH 16/51] Place continuation version badges consistently --- reference/configuration/options.md | 4 ++-- reference/resources/full-text-paging.md | 4 ++-- reference/rest/headers.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index 23a5bf733..cca263158 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -302,9 +302,9 @@ replication: Database storage configuration. See [Storage Tuning](../database/storage-tuning.md) for guidance on tuning these options for production workloads, [Database Overview](../database/overview.md) for general database concepts, and [Compaction](../database/compaction.md) for reclaiming space inside existing files. -`storage.fullText` controls retained full-text searches and native search capacity. See +`storage.fullText` (v5.4.0) controls retained full-text searches and native search capacity. See [Full-Text Paging](../resources/full-text-paging.md#configure-resource-limits) for the settings, -defaults and restart requirements. +defaults and restart requirements. ```yaml storage: diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index 54a5ef02f..0e9f1d002 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -4,10 +4,10 @@ title: Full-Text Paging # Full-Text Paging -Continuation paging is available in v5.4.0. The full-text index declaration below is a prerequisite. - +The full-text index declaration below is a prerequisite. + Full-text continuation lets a client page through the original search matches while indexing updates arrive. It is available through `Table.search()` and REST. Ordinary offset-based paging continues to run each request against its own index snapshot. diff --git a/reference/rest/headers.md b/reference/rest/headers.md index 8cb4675b9..7a64a1f39 100644 --- a/reference/rest/headers.md +++ b/reference/rest/headers.md @@ -25,7 +25,7 @@ Collection responses to a [count request](./querying.md#pagination-and-total-cou Full-text continuation responses also return `Harper-Continuation-Complete`, plus `Harper-Continuation` and `Harper-Continuation-Expires` while the search remains open. These names are added to `Access-Control-Expose-Headers`. See -[Full-Text Paging](../resources/full-text-paging.md#page-through-rest). +[Full-Text Paging](../resources/full-text-paging.md#page-through-rest) (v5.4.0). ## Cache-Control From 66dfb1708b112312145b23247badb535b6c10bc9 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 16:46:12 -0600 Subject: [PATCH 17/51] Document explicit full-text paging and ordinary search budgets --- reference/resources/full-text-paging.md | 44 +++++++++++++++---------- reference/resources/resource-api.md | 2 +- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index 0e9f1d002..664ecc51f 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -9,7 +9,7 @@ title: Full-Text Paging The full-text index declaration below is a prerequisite. Full-text continuation lets a client page through the original search matches while indexing -updates arrive. It is available through `Table.search()` and REST. Ordinary offset-based paging +updates arrive. It is available through `Table.searchPage()` and REST. Ordinary offset-based paging continues to run each request against its own index snapshot. The same continuation options work with autocomplete searches using `matches_prefix` and `matches_fuzzy_prefix`. @@ -30,11 +30,11 @@ type Product @table(audit: true) @export { Full-text matches use descending relevance order. REST examples also require `rest: true` in the application configuration; see [REST setup](../rest/overview.md). -## Page through `Table.search()` +## Page through `Table.searchPage()` Set `continuation: true` and a positive integer `limit` to start. The limit must not exceed `maxPageSize`. -Continuation mode returns a promise for a bounded array; -ordinary searches keep their async iterable return value. +`searchPage()` returns a promise for a bounded array. Omit `continuation` or set it to `true` to start. +`Table.search()` keeps its async iterable return value and rejects continuation options. ```javascript const query = { @@ -45,7 +45,7 @@ const query = { let continuation = true; do { - const page = await tables.Product.search({ ...query, continuation }); + const page = await tables.Product.searchPage({ ...query, continuation }); for (const record of page) console.log(record); continuation = page.continuation; } while (continuation); @@ -65,7 +65,7 @@ and selected fields, and use the same authenticated caller when resuming. The pa returns 400. In a static custom Resource method, pass the method's `context` argument as the second argument -to `tables.Product.search()`. In an instance method, use `this.getContext()` instead. See +to `tables.Product.searchPage()`. In an instance method, use `this.getContext()` instead. See [Resource context](./resource-api.md#getcontext-context) for both forms. Set `checkPermission: true` in the query. This requests the caller's table and searched-source permissions; passing context alone does not request that permission gate. @@ -76,11 +76,15 @@ The token does not store application callbacks such as `rowFilter` or [`vectorFilter`](../database/schema.md#filtered-vector-search-with-a-function-predicate). Supply them again on every page, together with the caller's context and permission settings. +If your table overrides `search()`, also implement `searchPage()` with the same filtering policy. +This applies to static and instance methods. Otherwise paging returns 400, including through REST, +to avoid bypassing application filters. + ## Page through REST Use the single-argument `limit(n)` page size and start with `continuation()`. Do not use a nonzero starting offset with `limit(start,end)`, request a count with `Prefer: count=`, or change the relevance order. The same -`explain` and `sync` restrictions as `Table.search()` apply; violations return 400. +`explain` and `sync` restrictions as `Table.searchPage()` apply; violations return 400. ```http GET /Product/?catalogSearch=matches=trail%20shoes&select(id,title,$score)&limit(25)&continuation() @@ -168,18 +172,22 @@ storage: maxPageSize: 1000 requestBudgetMilliseconds: 5000 maxRawHitsPerRequest: 100000 + maxRawHitsPerOrdinaryRequest: 10000 ``` -| Setting | Default | Scope | -| --------------------------- | ------: | ----------------------------------------------------------- | -| `cursorTtlMilliseconds` | 60,000 | Absolute search lifetime; paging does not extend it | -| `maxCursors` | 4 | Retained searches across all workers in one process | -| `maxRetainedQueryBytes` | 8 MiB | Retained native query text and identity bytes | -| `maxPageSize` | 1,000 | Returned records per page | -| `requestBudgetMilliseconds` | 5,000 | Freshness wait and page processing budget | -| `maxRawHitsPerRequest` | 100,000 | Fetched native hits per request, including replayed batches | -| `maxPendingRequests` | 32 | Pending requests per requesting worker | -| `maxPendingResponseBytes` | 16 MiB | Reserved response bytes per requesting worker | +| Setting | Default | Scope | +| ------------------------------ | ------: | ----------------------------------------------------------- | +| `cursorTtlMilliseconds` | 60,000 | Absolute search lifetime; paging does not extend it | +| `maxCursors` | 4 | Retained searches across all workers in one process | +| `maxRetainedQueryBytes` | 8 MiB | Retained native query text and identity bytes | +| `maxPageSize` | 1,000 | Returned records per page | +| `requestBudgetMilliseconds` | 5,000 | Freshness wait and page processing budget | +| `maxRawHitsPerRequest` | 100,000 | Fetched native hits per request, including replayed batches | +| `maxRawHitsPerOrdinaryRequest` | 10,000 | Additional hit ceiling for searches without continuation | +| `maxPendingRequests` | 32 | Pending requests per requesting worker | +| `maxPendingResponseBytes` | 16 MiB | Reserved response bytes per requesting worker | + +Ordinary searches use the lower of `maxRawHitsPerRequest` and `maxRawHitsPerOrdinaryRequest`, and share the `requestBudgetMilliseconds` execution budget. Increase the ordinary ceiling explicitly if a selective query needs more work. Hitting a ceiling returns an error. Search work and cleanup use separate windows based on `requestBudgetMilliseconds`. With the 5,000 ms default, a search that uses its full work budget and then closes can take about 10 seconds. @@ -197,7 +205,7 @@ The usual individual configuration names also work, such as `storage_fullText_maxRawHitsPerRequest` with `set_configuration`. Follow the [configuration restart procedure](../configuration/operations.md#set-configuration) after applying changes with that operation. Native process-wide admission -settings live under `storage.fullText.runtime` and require a restart when changed. Their defaults +settings live under `storage.fullText.runtime` and require a full process restart when changed. Restarting only worker threads does not apply new native budgets; replacement workers keep the established process budgets. Their defaults are 128 resident indexes, 128 indexing threads, 256 search threads, 4 GiB configured writer memory, 8 GiB configured queue capacity, and 8 concurrent expensive searches. All limits apply together. diff --git a/reference/resources/resource-api.md b/reference/resources/resource-api.md index 485a89cb4..24aa1a8f3 100644 --- a/reference/resources/resource-api.md +++ b/reference/resources/resource-api.md @@ -1037,7 +1037,7 @@ Array of condition objects to filter records. Each condition: Full-text conditions name a query-only `FullText` declaration and use comparators `matches`, `matches_all`, `matches_phrase`, `matches_prefix`, `matches_fuzzy`, and `matches_fuzzy_prefix`. See -[Full-Text Paging](./full-text-paging.md) for snapshot continuation and its query restrictions. +`Table.searchPage()` and [Full-Text Paging](./full-text-paging.md) for snapshot continuation and its query restrictions. Example with nested conditions: From 88964e2b625c4e781d8cde6855b7dfd3999d0fb2 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 17:06:58 -0600 Subject: [PATCH 18/51] Make paging examples enforce caller permissions and complete the API reference --- reference/configuration/options.md | 5 +---- reference/resources/full-text-paging.md | 13 +++++++++---- reference/resources/resource-api.md | 14 ++++++++++++-- reference/rest/querying.md | 22 ++++++++++++---------- 4 files changed, 34 insertions(+), 20 deletions(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index cca263158..8e2a02528 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -302,10 +302,6 @@ replication: Database storage configuration. See [Storage Tuning](../database/storage-tuning.md) for guidance on tuning these options for production workloads, [Database Overview](../database/overview.md) for general database concepts, and [Compaction](../database/compaction.md) for reclaiming space inside existing files. -`storage.fullText` (v5.4.0) controls retained full-text searches and native search capacity. See -[Full-Text Paging](../resources/full-text-paging.md#configure-resource-limits) for the settings, -defaults and restart requirements. - ```yaml storage: path: ~/hdb/database @@ -315,6 +311,7 @@ storage: engine: rocksdb ``` +- `fullText` - Retained full-text searches and native search capacity. See [Full-Text Paging](../resources/full-text-paging.md#configure-resource-limits) for settings, defaults and restart requirements. - `engine` — The database storage engine to use for new databases. Currently supported engines are `rocksdb` and `lmdb`. The default is `rocksdb`. Existing databases will use the engine that was used when they were created. - `writeAsync` — Disable disk sync for higher throughput (**disables durability guarantees**); _Default_: `false` - `caching` — In-memory record caching; _Default_: `true` diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index 664ecc51f..c7f113808 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -36,16 +36,20 @@ Set `continuation: true` and a positive integer `limit` to start. The limit must `searchPage()` returns a promise for a bounded array. Omit `continuation` or set it to `true` to start. `Table.search()` keeps its async iterable return value and rejects continuation options. +Inside an authenticated Resource instance method: + ```javascript +const context = this.getContext(); const query = { conditions: [{ attribute: 'catalogSearch', comparator: 'matches', value: 'trail shoes' }], select: ['id', 'title', '$score'], limit: 25, + checkPermission: true, }; let continuation = true; do { - const page = await tables.Product.searchPage({ ...query, continuation }); + const page = await tables.Product.searchPage({ ...query, continuation }, context); for (const record of page) console.log(record); continuation = page.continuation; } while (continuation); @@ -62,7 +66,7 @@ The returned array has three additional properties: These properties are not included when the array is serialized as JSON. Keep the same conditions and selected fields, and use the same authenticated caller when resuming. The page size may change. Use the default relevance order, or explicitly sort by descending `$score` alone. A nonzero `offset`, another sort, or a truthy `count`, `explain`, or `sync` option -returns 400. +returns 400. In REST, the accepted explicit score order is `sort(-$score)` alone. In a static custom Resource method, pass the method's `context` argument as the second argument to `tables.Product.searchPage()`. In an instance method, use `this.getContext()` instead. See @@ -91,7 +95,7 @@ GET /Product/?catalogSearch=matches=trail%20shoes&select(id,title,$score)&limit( ``` The body stays a JSON array. Read the next token from `Harper-Continuation`, URL-encode it, and -send it in `continuation(token)` with the same query: +send it in `continuation(token)` with the same conditions and selected fields. The page size may change: ```javascript const query = '/Product/?catalogSearch=matches=trail%20shoes&select(id,title,$score)&limit(25)'; @@ -106,7 +110,7 @@ do { } while (token); ``` -`Harper-Continuation-Complete` reports completion. `Harper-Continuation-Expires` contains the +`Harper-Continuation-Complete` is present on every successful continuation page: the string `"false"` while more results remain, and `"true"` after completion or explicit close. `Harper-Continuation-Expires` contains the absolute expiry time in milliseconds since the Unix epoch. Completed pages omit the token and expiry. Harper adds these names to `Access-Control-Expose-Headers` so browser clients can read them. Continuation responses override the usual read-cache policy with `Cache-Control: no-store`. JSON `QUERY` requests return the @@ -168,6 +172,7 @@ storage: cursorTtlMilliseconds: 60000 maxCursors: 4 maxRetainedQueryBytes: 8388608 + maxPendingRequests: 32 maxPendingResponseBytes: 16777216 maxPageSize: 1000 requestBudgetMilliseconds: 5000 diff --git a/reference/resources/resource-api.md b/reference/resources/resource-api.md index 24aa1a8f3..543060f58 100644 --- a/reference/resources/resource-api.md +++ b/reference/resources/resource-api.md @@ -91,7 +91,17 @@ The `get()` method returns a `RecordObject` — a frozen plain object with all r ### `search(query: RequestTarget | Query, context?): ExtendedIterable` -Performs a query on the resource or table. This is called by `get()` on collection requests and can be overridden to define custom query behavior. The default implementation on tables queries by the `conditions`, `limit`, `offset`, `select`, and `sort` properties parsed from the URL or query object. See [Query Object](#query-object) below for available query options. See the [ExtendedIterable](#extendediterable) below for how to interact with the query results. +Performs a query on the resource or table. This is called by `get()` on collection requests and can be overridden to define custom query behavior. A table overriding `search()` must also implement `searchPage()` with the same filtering policy to enable continuation, including through REST. The default implementation on tables queries by the `conditions`, `limit`, `offset`, `select`, and `sort` properties parsed from the URL or query object. See [Query Object](#query-object) below for available query options. See the [ExtendedIterable](#extendediterable) below for how to interact with the query results. + +--- + +### `searchPage(query: RequestTarget | Query, context?): Promise` + + + +Tables use `searchPage()` for full-text and autocomplete continuation. Supply a positive `limit` and omit `continuation` or set it to `true` to start; supply the returned token to resume. The result is an array with `continuation`, `complete`, and `expiresAt` properties. `search()` stays iterable and rejects continuation options. + +When serving an authenticated caller, pass the caller's context and set `checkPermission: true` in the query. A custom `search()` override must explicitly implement `searchPage()` with the same filtering policy; otherwise paging returns 400. See [Full-Text Paging](./full-text-paging.md) for examples, supported queries, closing a search, and token expiry. --- @@ -1036,7 +1046,7 @@ Array of condition objects to filter records. Each condition: | `operator` | `and` (default) or `or` for the nested `conditions` | Full-text conditions name a query-only `FullText` declaration and use comparators `matches`, -`matches_all`, `matches_phrase`, `matches_prefix`, `matches_fuzzy`, and `matches_fuzzy_prefix`. See +`matches_all`, `matches_phrase`, `matches_prefix`, `matches_fuzzy`, and `matches_fuzzy_prefix`. Each also has a `not_`-prefixed form; a negated full-text condition requires a positive full-text condition in the query. See `Table.searchPage()` and [Full-Text Paging](./full-text-paging.md) for snapshot continuation and its query restrictions. Example with nested conditions: diff --git a/reference/rest/querying.md b/reference/rest/querying.md index d2cd8a308..70850598c 100644 --- a/reference/rest/querying.md +++ b/reference/rest/querying.md @@ -136,16 +136,6 @@ GET /Product/?price=lt=100|[rating=5&[tag=fast|tag=scalable|tag=efficient]&inSto Harper supports special query functions using call syntax, included in the query string separated by `&`. -### `continuation()` and `closeContinuation()` - - - -Full-text searches can retain their original index snapshot between requests. Start with -`continuation()`, resume with `continuation(token)`, and close early with `closeContinuation()` -alongside the token. Use `limit(n)` without a nonzero offset or a count request. See -[Full-Text Paging](../resources/full-text-paging.md#page-through-rest) for the token headers, -query restrictions and recovery steps. - ### `select(properties)` Specify which properties to include in the response. @@ -183,10 +173,22 @@ GET /Product/?rating=gt=3&sort(+name) GET /Product/?sort(+rating,-price) ``` +### `continuation()` and `closeContinuation()` + + + +Full-text searches can retain their original index snapshot between requests. Start with +`continuation()`, resume with `continuation(token)`, and close early with `closeContinuation()` +alongside the token. Use the single-argument `limit(end)` form without a starting offset or a count request. See +[Full-Text Paging](../resources/full-text-paging.md#page-through-rest) for the token headers, +query restrictions and recovery steps. + ## Pagination and Total Count +For full-text paging across requests, use [continuation](#continuation-and-closecontinuation) instead of offset paging or a count request. + Use `limit(start,end)` to page through a collection, and opt in to a total match count with the `Prefer` request header (`Prefer: count=exact`) so a client can render pagination (for example "1-25 of 1,234") without a second request. Counting is opt-in: without the header, no count is computed and no count headers are returned. It applies only to `GET`/`HEAD` requests and requires a bounded page — the `limit()` must be a non-negative integer no larger than **10,000**, and the requested window (offset + limit) no larger than **1,000,000**. A request with no `limit()`, one outside those bounds, or a non-numeric one is served normally with no count headers, since counting an unbounded page would defeat the point of paging. From ffb08f70feaa8c6100c00b0d3df5e861b6c8513c Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 17:23:58 -0600 Subject: [PATCH 19/51] Clarify conservative paging reservations and version availability --- reference/resources/full-text-paging.md | 4 +++- reference/rest/querying.md | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index c7f113808..4b8a4a0b4 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -32,7 +32,7 @@ application configuration; see [REST setup](../rest/overview.md). ## Page through `Table.searchPage()` -Set `continuation: true` and a positive integer `limit` to start. The limit must not exceed `maxPageSize`. +Use a positive integer `limit` no greater than `maxPageSize`. `searchPage()` returns a promise for a bounded array. Omit `continuation` or set it to `true` to start. `Table.search()` keeps its async iterable return value and rejects continuation options. @@ -194,6 +194,8 @@ storage: Ordinary searches use the lower of `maxRawHitsPerRequest` and `maxRawHitsPerOrdinaryRequest`, and share the `requestBudgetMilliseconds` execution budget. Increase the ordinary ceiling explicitly if a selective query needs more work. Hitting a ceiling returns an error. +A requesting worker reserves a conservative bound for a complete native batch before dispatch, including two-byte string encoding and message overhead. The reservation may exceed the returned payload. `maxPendingResponseBytes` can reject concurrent reads even when the cursor-count and pending-request limits have room. + Search work and cleanup use separate windows based on `requestBudgetMilliseconds`. With the 5,000 ms default, a search that uses its full work budget and then closes can take about 10 seconds. Waiting for other page requests already using the snapshot can add time. Native work checks its diff --git a/reference/rest/querying.md b/reference/rest/querying.md index 70850598c..2a9c75797 100644 --- a/reference/rest/querying.md +++ b/reference/rest/querying.md @@ -187,10 +187,10 @@ query restrictions and recovery steps. -For full-text paging across requests, use [continuation](#continuation-and-closecontinuation) instead of offset paging or a count request. - Use `limit(start,end)` to page through a collection, and opt in to a total match count with the `Prefer` request header (`Prefer: count=exact`) so a client can render pagination (for example "1-25 of 1,234") without a second request. +For full-text paging across requests (), use [continuation](#continuation-and-closecontinuation) instead of offset paging or a count request. + Counting is opt-in: without the header, no count is computed and no count headers are returned. It applies only to `GET`/`HEAD` requests and requires a bounded page — the `limit()` must be a non-negative integer no larger than **10,000**, and the requested window (offset + limit) no larger than **1,000,000**. A request with no `limit()`, one outside those bounds, or a non-numeric one is served normally with no count headers, since counting an unbounded page would defeat the point of paging. ### Requesting a count From fa1fb2a3df2e0c21f9ba0566edcea17cd3d6cd63 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 17:53:28 -0600 Subject: [PATCH 20/51] Document paging permission boundaries and upgrade work limits --- reference/configuration/options.md | 2 +- reference/resources/full-text-paging.md | 22 ++++++++++++++++++---- reference/resources/query-optimization.md | 6 ++++++ reference/resources/resource-api.md | 15 ++++++++++++++- reference/rest/querying.md | 4 +++- release-notes/v5-lincoln/5.4.md | 8 ++++++++ 6 files changed, 50 insertions(+), 7 deletions(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index 8e2a02528..14e668cec 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -311,8 +311,8 @@ storage: engine: rocksdb ``` -- `fullText` - Retained full-text searches and native search capacity. See [Full-Text Paging](../resources/full-text-paging.md#configure-resource-limits) for settings, defaults and restart requirements. - `engine` — The database storage engine to use for new databases. Currently supported engines are `rocksdb` and `lmdb`. The default is `rocksdb`. Existing databases will use the engine that was used when they were created. +- `fullText` - Full-text capacity controls (Added in: v5.4.0). See [Full-Text Paging](../resources/full-text-paging.md#configure-resource-limits) for settings, defaults and restart requirements. Native full-text search itself is available from v5.3.0. - `writeAsync` — Disable disk sync for higher throughput (**disables durability guarantees**); _Default_: `false` - `caching` — In-memory record caching; _Default_: `true` - `compression` — LZ4 record compression; _Default_: `true` (enabled by default since v4.3.0). Sub-options: `dictionary`, `threshold` diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index 4b8a4a0b4..1f2dadd3b 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -71,8 +71,8 @@ returns 400. In REST, the accepted explicit score order is `sort(-$score)` alone In a static custom Resource method, pass the method's `context` argument as the second argument to `tables.Product.searchPage()`. In an instance method, use `this.getContext()` instead. See [Resource context](./resource-api.md#getcontext-context) for both forms. Set `checkPermission: true` -in the query. This requests the caller's -table and searched-source permissions; passing context alone does not request that permission gate. +in the query on every page. Without it, a direct call does not request the caller's +table and searched-source permission checks; passing context alone does not enable them. Changing the caller identity, including switching from an authenticated request to an internal call without that context, returns 410. Permission changes for the same caller are checked on each page. Application row filters still run on each page. @@ -84,11 +84,17 @@ If your table overrides `search()`, also implement `searchPage()` with the same This applies to static and instance methods. Otherwise paging returns 400, including through REST, to avoid bypassing application filters. +Return the original page array from a custom Resource. Copying it with `map()`, `filter()` or +spread drops its paging metadata and a framework-serialized continuation response returns 400. +Use the query's `select` and filters instead. A Resource returning an explicit HTTP `Response` +owns its response headers, serialization and cache policy. + ## Page through REST Use the single-argument `limit(n)` page size and start with `continuation()`. Do not use a nonzero starting offset with `limit(start,end)`, request a count with `Prefer: count=`, or change the relevance order. The same `explain` and `sync` restrictions as `Table.searchPage()` apply; violations return 400. +The equivalent `limit(0,n)` form is accepted; it does not skip records within the retained search. ```http GET /Product/?catalogSearch=matches=trail%20shoes&select(id,title,$score)&limit(25)&continuation() @@ -116,6 +122,10 @@ expiry. Harper adds these names to `Access-Control-Expose-Headers` so browser cl Continuation responses override the usual read-cache policy with `Cache-Control: no-store`. JSON `QUERY` requests return the same headers when their query body enables continuation. +Request URL logging can record continuation tokens. Keep tokens out of logs and analytics. +QUERY accepts the token in its JSON body with the same query fields if it should stay out of the URL. +Tokens still require the same caller and query and expire with their search. + Read and parse the complete response body before saving its next token. A stream can fail after headers are sent. If a resumed page fails this way, retry with the previous token while the snapshot remains valid. If no token was received on the first page, start a new search. The first search may keep its cursor @@ -192,9 +202,9 @@ storage: | `maxPendingRequests` | 32 | Pending requests per requesting worker | | `maxPendingResponseBytes` | 16 MiB | Reserved response bytes per requesting worker | -Ordinary searches use the lower of `maxRawHitsPerRequest` and `maxRawHitsPerOrdinaryRequest`, and share the `requestBudgetMilliseconds` execution budget. Increase the ordinary ceiling explicitly if a selective query needs more work. Hitting a ceiling returns an error. +Ordinary searches use the lower of `maxRawHitsPerRequest` and `maxRawHitsPerOrdinaryRequest`, and share the `requestBudgetMilliseconds` execution budget. A raw-hit ceiling returns 400 instead of a partial page. Increasing the ordinary ceiling permits more native work and record reads per request; test selective queries before raising it. Replayed batches count toward the ceiling, so the number of distinct matches reached can be lower. -A requesting worker reserves a conservative bound for a complete native batch before dispatch, including two-byte string encoding and message overhead. The reservation may exceed the returned payload. `maxPendingResponseBytes` can reject concurrent reads even when the cursor-count and pending-request limits have room. +A requesting worker reserves a conservative bound for a complete native batch before dispatch, including two-byte string encoding and message overhead. The reservation may exceed the returned payload. `maxPendingResponseBytes` can reject concurrent reads with 503 even when the cursor-count and pending-request limits have room. A reservation rejection happens before dispatch and does not advance the search. Reduce concurrency and retry with the same token while it remains valid; this rejection does not include a `Retry-After` header. Search work and cleanup use separate windows based on `requestBudgetMilliseconds`. With the 5,000 ms default, a search that uses its full work budget and then closes can take about 10 seconds. @@ -216,6 +226,10 @@ settings live under `storage.fullText.runtime` and require a full process restar are 128 resident indexes, 128 indexing threads, 256 search threads, 4 GiB configured writer memory, 8 GiB configured queue capacity, and 8 concurrent expensive searches. All limits apply together. +Application-created workers inherit the established native settings too. Create them after +Harper initializes its full-text settings; workers created before that point return 503 when +they try to load full-text search. This prevents a worker from applying different process-wide limits. + The runtime keys use integer counts or bytes: ```yaml diff --git a/reference/resources/query-optimization.md b/reference/resources/query-optimization.md index dfe2cf9d1..bdc92d8b0 100644 --- a/reference/resources/query-optimization.md +++ b/reference/resources/query-optimization.md @@ -79,6 +79,12 @@ When a secondary index is still being built — for example, just after a new `@ This is a transient condition — the index finishes building in the background, after which the same query succeeds. Clients should retry (with backoff) rather than treating it as an empty result or a permanent failure. On the operations API the error body includes `code` and `retryable`, so callers can branch on `error.code === 'INDEX_REBUILDING'` or `error.retryable`. +### Full-Text Search Work Limits + + + +Full-text queries have configurable work limits, including ordinary searches without continuation. A selective filter can reach a limit before filling the requested page. See [Full-Text Paging](./full-text-paging.md#configure-resource-limits) for the limits and the cost of increasing them. + ## Relationships and Joins Harper supports relationship-based queries that join data across tables. See [Schema documentation](../database/schema.md) for how to define relationships. diff --git a/reference/resources/resource-api.md b/reference/resources/resource-api.md index 543060f58..81df0068b 100644 --- a/reference/resources/resource-api.md +++ b/reference/resources/resource-api.md @@ -1031,7 +1031,19 @@ body, so avoid putting sensitive detail in thrown error messages. ## Query Object -The `Query` object is accepted by `search()` and the static `get()` method. +The `Query` object is accepted by `search()`, `searchPage()`, and the static `get()` method. + +### `continuation` + +For `searchPage()`, omit this field or use `true` to start a search; use the returned token to resume. Keep the query and caller context consistent. See [Full-Text Paging](./full-text-paging.md#page-through-tablesearchpage). + +### `closeContinuation` + +Use `true` with an existing continuation token to close the search early. See [Close or restart a search](./full-text-paging.md#close-or-restart-a-search). + +### `checkPermission` + +Set `true` on every page when a direct table call acts for a caller, and pass that caller's context. Without it, the call does not request the caller's table and searched-source permission checks; context alone does not enable them. Automatic REST applies its permission checks. See [Full-Text Paging](./full-text-paging.md#page-through-tablesearchpage). ### `conditions` @@ -1099,6 +1111,7 @@ Special properties: - `$id` — Returns the primary key regardless of its name - `$updatedtime` — Returns the last-updated timestamp - `$distance` — When the query ranks or filters by a vector index, returns the computed distance from the target vector. See [Vector Indexing](../database/schema.md#vector-indexing). +- `$score` - When a query uses a full-text index, returns its relevance score. See [Full-Text Paging](./full-text-paging.md#page-through-tablesearchpage). #### Selecting related records diff --git a/reference/rest/querying.md b/reference/rest/querying.md index 2a9c75797..9d252a239 100644 --- a/reference/rest/querying.md +++ b/reference/rest/querying.md @@ -179,7 +179,7 @@ GET /Product/?sort(+rating,-price) Full-text searches can retain their original index snapshot between requests. Start with `continuation()`, resume with `continuation(token)`, and close early with `closeContinuation()` -alongside the token. Use the single-argument `limit(end)` form without a starting offset or a count request. See +alongside the token. Use `limit(n)` to set the page size. See [Full-Text Paging](../resources/full-text-paging.md#page-through-rest) for the token headers, query restrictions and recovery steps. @@ -193,6 +193,8 @@ For full-text paging across requests (), use [c Counting is opt-in: without the header, no count is computed and no count headers are returned. It applies only to `GET`/`HEAD` requests and requires a bounded page — the `limit()` must be a non-negative integer no larger than **10,000**, and the requested window (offset + limit) no larger than **1,000,000**. A request with no `limit()`, one outside those bounds, or a non-numeric one is served normally with no count headers, since counting an unbounded page would defeat the point of paging. +Full-text queries also have [search work limits](../resources/full-text-paging.md#configure-resource-limits). A count request does not remove those limits; a full-text query can fail before returning a page or count. + ### Requesting a count Send a `Prefer` header on a `GET` (or `HEAD`) request to a collection: diff --git a/release-notes/v5-lincoln/5.4.md b/release-notes/v5-lincoln/5.4.md index ad0d66d61..87dae03c6 100644 --- a/release-notes/v5-lincoln/5.4.md +++ b/release-notes/v5-lincoln/5.4.md @@ -8,6 +8,14 @@ title: '5.4' All patch release notes for 5.4.x are available on the [releases page](https://github.com/HarperFast/harper/releases?q=v5.4&expanded=true). +## Upgrade Notes + +### Full-Text Search Budgets and Snapshot Paging + +Full-text and autocomplete searches can retain their original index matches between requests through `Table.searchPage()` or REST continuation tokens. Ordinary `Table.search()` remains iterable. See [Full-Text Paging](/reference/v5/resources/full-text-paging). + +The new `storage.fullText` work limits also apply to searches without continuation: the ordinary fetched-hit ceiling defaults to 10,000 and the shared request work budget defaults to 5,000 ms. Replayed batches count toward the hit ceiling. Hitting the raw-hit ceiling returns 400 instead of a successful partial page. Test selective full-text queries before upgrading; increasing the ceiling permits more work and record reads per request. See [resource limits](/reference/v5/resources/full-text-paging#configure-resource-limits) before changing the defaults. + ## Replication ### Dedicated Replication Threads From 238315ff759e62c2f33b7172ca02f899c3cd3569 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 18:03:28 -0600 Subject: [PATCH 21/51] Clarify explicit paging dispatch and permanent worker initialization errors --- reference/resources/full-text-paging.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index 1f2dadd3b..a96b7aef0 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -84,7 +84,8 @@ If your table overrides `search()`, also implement `searchPage()` with the same This applies to static and instance methods. Otherwise paging returns 400, including through REST, to avoid bypassing application filters. -Return the original page array from a custom Resource. Copying it with `map()`, `filter()` or +A custom Resource supporting the framework's QUERY paging route must implement `searchPage()`. +Return the original page array. Copying it with `map()`, `filter()` or spread drops its paging metadata and a framework-serialized continuation response returns 400. Use the query's `select` and filters instead. A Resource returning an explicit HTTP `Response` owns its response headers, serialization and cache policy. @@ -227,8 +228,9 @@ are 128 resident indexes, 128 indexing threads, 256 search threads, 4 GiB config 8 GiB configured queue capacity, and 8 concurrent expensive searches. All limits apply together. Application-created workers inherit the established native settings too. Create them after -Harper initializes its full-text settings; workers created before that point return 503 when -they try to load full-text search. This prevents a worker from applying different process-wide limits. +Harper initializes its full-text settings. A worker created before that point cannot initialize +full-text search; recreate it after Harper initializes rather than retrying inside the same worker. +This prevents a worker from applying different process-wide limits. The runtime keys use integer counts or bytes: From 41b831ef25b8b70d52272debbbe089e3e0051d95 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 18:19:36 -0600 Subject: [PATCH 22/51] Clarify paging permissions, failure recovery and application startup --- reference/resources/full-text-paging.md | 21 +++++++++++++-------- reference/resources/query-optimization.md | 2 +- reference/resources/resource-api.md | 4 +++- release-notes/v5-lincoln/5.4.md | 10 +++++++--- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index a96b7aef0..ed6cef951 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -64,7 +64,9 @@ The returned array has three additional properties: | `expiresAt` | Absolute expiry time in milliseconds since the Unix epoch | These properties are not included when the array is serialized as JSON. Keep the same conditions -and selected fields, and use the same authenticated caller when resuming. The page size may change. Use the default relevance order, or explicitly sort by descending +and selected fields, and use the same authenticated caller when resuming. You may raise or lower +the requested `limit` on later pages within `maxPageSize`. Each page contains at most its requested +limit; a shorter page alone does not mean the search is complete. Use the default relevance order, or explicitly sort by descending `$score` alone. A nonzero `offset`, another sort, or a truthy `count`, `explain`, or `sync` option returns 400. In REST, the accepted explicit score order is `sort(-$score)` alone. @@ -102,7 +104,7 @@ GET /Product/?catalogSearch=matches=trail%20shoes&select(id,title,$score)&limit( ``` The body stays a JSON array. Read the next token from `Harper-Continuation`, URL-encode it, and -send it in `continuation(token)` with the same conditions and selected fields. The page size may change: +send it in `continuation(token)` with the same conditions and selected fields. The requested page size may increase or decrease within `maxPageSize`: ```javascript const query = '/Product/?catalogSearch=matches=trail%20shoes&select(id,title,$score)&limit(25)'; @@ -216,8 +218,6 @@ Ordinary searches can use secondary indexes to skip record reads and enlarge bat matches are filtered out. Continued searches do not yet use those optimizations; they fetch up to 256 native hits per batch and check current records. A selective filter can therefore add many record reads and, when the snapshot belongs to another worker, many worker requests. -Qualification and these optimizations are tracked in -[Qualify native full-text filtering and catalog-scale operation for v5.4](https://github.com/HarperFast/harper/issues/2512). The usual individual configuration names also work, such as `storage_fullText_maxRawHitsPerRequest` with `set_configuration`. Follow the @@ -227,10 +227,11 @@ settings live under `storage.fullText.runtime` and require a full process restar are 128 resident indexes, 128 indexing threads, 256 search threads, 4 GiB configured writer memory, 8 GiB configured queue capacity, and 8 concurrent expensive searches. All limits apply together. -Application-created workers inherit the established native settings too. Create them after -Harper initializes its full-text settings. A worker created before that point cannot initialize -full-text search; recreate it after Harper initializes rather than retrying inside the same worker. -This prevents a worker from applying different process-wide limits. +Harper captures the native settings before loading application components, so workers created +by a running application inherit them. In a standalone process, initialize Harper before creating +workers that use full-text search. A worker created before capture cannot initialize full-text +search; recreate it after Harper initializes rather than retrying inside the same worker. +Its initialization failure is non-retryable, not a temporary capacity refusal. The runtime keys use integer counts or bytes: @@ -251,6 +252,10 @@ receives `503` until a search finishes, closes, or expires. Ordinary searches re Configure `maxCursors` for the number of unfinished searches the application needs to retain. There is no per-caller reservation or eviction policy. +Cursor-count and pending-response capacity refusals return 503 without `Retry-After`. Close unused +searches or reduce concurrency, then retry with backoff while the token remains valid. An execution-budget +503 calls for a smaller page or less expensive query; changing the query requires a new search. + Cursor slots, pending-request counts, and response-byte allowances apply separately. Raising `maxCursors` does not raise `maxPendingRequests` or `maxPendingResponseBytes`. Across workers, Harper reserves response space using the library's largest supported IDs and versions, rather than diff --git a/reference/resources/query-optimization.md b/reference/resources/query-optimization.md index bdc92d8b0..5e72803dd 100644 --- a/reference/resources/query-optimization.md +++ b/reference/resources/query-optimization.md @@ -83,7 +83,7 @@ This is a transient condition — the index finishes building in the background, -Full-text queries have configurable work limits, including ordinary searches without continuation. A selective filter can reach a limit before filling the requested page. See [Full-Text Paging](./full-text-paging.md#configure-resource-limits) for the limits and the cost of increasing them. +Full-text queries have configurable work limits, including ordinary searches without continuation. A selective filter can exhaust a work limit and fail the request instead of returning a partial page. See [Full-Text Paging](./full-text-paging.md#configure-resource-limits) for the limits, errors and cost of increasing them. ## Relationships and Joins diff --git a/reference/resources/resource-api.md b/reference/resources/resource-api.md index 81df0068b..0f63da00b 100644 --- a/reference/resources/resource-api.md +++ b/reference/resources/resource-api.md @@ -99,7 +99,7 @@ Performs a query on the resource or table. This is called by `get()` on collecti -Tables use `searchPage()` for full-text and autocomplete continuation. Supply a positive `limit` and omit `continuation` or set it to `true` to start; supply the returned token to resume. The result is an array with `continuation`, `complete`, and `expiresAt` properties. `search()` stays iterable and rejects continuation options. +Tables use `searchPage()` for full-text and autocomplete continuation. Supply a positive `limit` and omit `continuation` or set it to `true` to start; supply the returned token to resume. `FullTextSearchPage` is an array of records with `continuation`, `complete`, and `expiresAt` properties; see the [result properties](./full-text-paging.md#page-through-tablesearchpage). `search()` stays iterable and rejects continuation options. When serving an authenticated caller, pass the caller's context and set `checkPermission: true` in the query. A custom `search()` override must explicitly implement `searchPage()` with the same filtering policy; otherwise paging returns 400. See [Full-Text Paging](./full-text-paging.md) for examples, supported queries, closing a search, and token expiry. @@ -1045,6 +1045,8 @@ Use `true` with an existing continuation token to close the search early. See [C Set `true` on every page when a direct table call acts for a caller, and pass that caller's context. Without it, the call does not request the caller's table and searched-source permission checks; context alone does not enable them. Automatic REST applies its permission checks. See [Full-Text Paging](./full-text-paging.md#page-through-tablesearchpage). +Set this field in server code; never copy permission-control fields from client data. + ### `conditions` Array of condition objects to filter records. Each condition: diff --git a/release-notes/v5-lincoln/5.4.md b/release-notes/v5-lincoln/5.4.md index 87dae03c6..f90565f3f 100644 --- a/release-notes/v5-lincoln/5.4.md +++ b/release-notes/v5-lincoln/5.4.md @@ -10,11 +10,15 @@ All patch release notes for 5.4.x are available on the [releases page](https://g ## Upgrade Notes -### Full-Text Search Budgets and Snapshot Paging +### Full-Text Search Budgets -Full-text and autocomplete searches can retain their original index matches between requests through `Table.searchPage()` or REST continuation tokens. Ordinary `Table.search()` remains iterable. See [Full-Text Paging](/reference/v5/resources/full-text-paging). +The new `storage.fullText` work limits also apply to searches without continuation: the ordinary fetched-hit ceiling defaults to 10,000 and the shared execution budget defaults to 5,000 ms. Replayed batches count toward the hit ceiling. Hitting the raw-hit ceiling returns 400 instead of a successful partial page; exhausting the execution budget returns 503. Test selective full-text queries before upgrading. Narrow an expensive query or reduce its page size before increasing a work limit; higher limits permit more work and record reads per request. See [resource limits](/reference/v5/resources/full-text-paging#configure-resource-limits) before changing the defaults. + +## Full-Text Search -The new `storage.fullText` work limits also apply to searches without continuation: the ordinary fetched-hit ceiling defaults to 10,000 and the shared request work budget defaults to 5,000 ms. Replayed batches count toward the hit ceiling. Hitting the raw-hit ceiling returns 400 instead of a successful partial page. Test selective full-text queries before upgrading; increasing the ceiling permits more work and record reads per request. See [resource limits](/reference/v5/resources/full-text-paging#configure-resource-limits) before changing the defaults. +### Snapshot Paging + +Full-text and autocomplete searches can retain their original index matches between requests through `Table.searchPage()` or REST continuation tokens. Ordinary `Table.search()` remains iterable. See [Full-Text Paging](/reference/v5/resources/full-text-paging). ## Replication From fb873fc9776dd5da959ea6fbfca9cd19c091cfd5 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 18:33:02 -0600 Subject: [PATCH 23/51] Use plain language for copied paging responses --- reference/resources/full-text-paging.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index ed6cef951..8b62eae54 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -88,7 +88,7 @@ to avoid bypassing application filters. A custom Resource supporting the framework's QUERY paging route must implement `searchPage()`. Return the original page array. Copying it with `map()`, `filter()` or -spread drops its paging metadata and a framework-serialized continuation response returns 400. +spread drops its paging metadata. Returning that copy through the normal Resource API returns 400. Use the query's `select` and filters instead. A Resource returning an explicit HTTP `Response` owns its response headers, serialization and cache policy. From f9ebdcd0fab439f03293ad8a84a0982002abd54c Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 18:52:15 -0600 Subject: [PATCH 24/51] Clarify paging response envelopes and managed worker routing --- reference/resources/full-text-paging.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index 8b62eae54..bdf075ef9 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -89,8 +89,9 @@ to avoid bypassing application filters. A custom Resource supporting the framework's QUERY paging route must implement `searchPage()`. Return the original page array. Copying it with `map()`, `filter()` or spread drops its paging metadata. Returning that copy through the normal Resource API returns 400. -Use the query's `select` and filters instead. A Resource returning an explicit HTTP `Response` -owns its response headers, serialization and cache policy. +Use the query's `select` and filters instead. To add custom headers, return +`{ headers, data: page }` with the original page array. A Resource returning an explicit HTTP +`Response` owns its response headers, serialization and cache policy. ## Page through REST @@ -232,6 +233,8 @@ by a running application inherit them. In a standalone process, initialize Harpe workers that use full-text search. A worker created before capture cannot initialize full-text search; recreate it after Harper initializes rather than retrying inside the same worker. Its initialization failure is non-retryable, not a temporary capacity refusal. +Retained paging still requires Harper's main thread or a managed worker. Inheriting native settings +does not add paging-message routing to application-created workers. The runtime keys use integer counts or bytes: From 1c4d88f9d0aae84f2bbe977804b8e431195c8ec9 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 19:03:27 -0600 Subject: [PATCH 25/51] Describe measured token sizes plainly --- reference/resources/full-text-paging.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index bdf075ef9..9faffaec3 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -139,7 +139,7 @@ GET and QUERY can return continuation pages. OPTIONS returns request metadata wi search. HEAD is an exception to the usual GET-without-a-body behavior: it returns 400 before opening or advancing a continuation. Other methods also reject continuation parameters with 400. -Long record IDs make continuation tokens larger. Qualification with the released library produced +Long record IDs make continuation tokens larger. Local measurements produced tokens of about 600 bytes for short IDs and nearly 8 KB near the supported ID limit. Review Harper's `http.maxHeaderSize` setting and any proxy response-header and request-line limits. Leave room for the token, query, authentication, and other headers; a token that fits Harper may exceed a proxy limit. From 06d3089df0ef859591c0c3b08ee303c3ac5371a2 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 19:04:59 -0600 Subject: [PATCH 26/51] Clarify continuation entry points and native runtime recovery --- reference/configuration/operations.md | 4 ++++ reference/resources/full-text-paging.md | 6 +++++- reference/rest/querying.md | 3 +++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/reference/configuration/operations.md b/reference/configuration/operations.md index 75d9ed695..dfb186bd1 100644 --- a/reference/configuration/operations.md +++ b/reference/configuration/operations.md @@ -18,6 +18,10 @@ For the full list of configurable options, see [Configuration Options](./options Modifies one or more Harper configuration parameters. **Requires a [restart](../operations-api/operations.md#restart) or [restart_service](../operations-api/operations.md#restart_service) to take effect** (a [restart](../operations-api/operations.md#restart) for `threads.v8Flags`). A change to `replication.threads` takes effect only on a full `restart`. +Changes to [`storage.fullText.runtime`](../resources/full-text-paging.md#configure-resource-limits) +(v5.4.0) require a full `restart`. `restart_service` replaces workers but keeps the established +process-wide native full-text budgets. + `operation` _(required)_ — must be `set_configuration` `replicated` _(optional)_ — set to `true` to apply the same configuration change to all cluster nodes in a single call. The origin node applies the change first; if its local write fails, nothing is sent to peers. Requires replication to be configured. diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index 9faffaec3..15e16f743 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -12,6 +12,7 @@ Full-text continuation lets a client page through the original search matches wh updates arrive. It is available through `Table.searchPage()` and REST. Ordinary offset-based paging continues to run each request against its own index snapshot. The same continuation options work with autocomplete searches using `matches_prefix` and `matches_fuzzy_prefix`. +Operations API searches do not support continuation options; use `Table.searchPage()` or REST. ## Declare a full-text index @@ -232,9 +233,12 @@ Harper captures the native settings before loading application components, so wo by a running application inherit them. In a standalone process, initialize Harper before creating workers that use full-text search. A worker created before capture cannot initialize full-text search; recreate it after Harper initializes rather than retrying inside the same worker. -Its initialization failure is non-retryable, not a temporary capacity refusal. +This throws `FullTextWorkerInitializationError` with status 500 and `retryable: false`. Retained paging still requires Harper's main thread or a managed worker. Inheriting native settings does not add paging-message routing to application-created workers. +Attempting retained paging from an unmanaged worker returns 503 with +`Full-text continuation requires a Harper-managed worker`; waiting and retrying on that worker +does not add routing. Run the page request through Harper's main thread or a managed worker. The runtime keys use integer counts or bytes: diff --git a/reference/rest/querying.md b/reference/rest/querying.md index 9d252a239..0ae96a24c 100644 --- a/reference/rest/querying.md +++ b/reference/rest/querying.md @@ -55,6 +55,9 @@ Harper uses [FIQL](https://datatracker.ietf.org/doc/html/draft-nottingham-atompu | `=`, `===` | Strict equality (no type conversion) | | `!==` | Strict inequality (no type conversion) | +Declared full-text fields use `=matches=`, `=matches_prefix=` or `=matches_fuzzy_prefix=`. +See [Full-Text Paging](../resources/full-text-paging.md#page-through-rest) for examples and continuation. + **Examples**: ```http From aec291ea12e1db1c418037bdc0c504bd4667675a Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 21:46:20 -0600 Subject: [PATCH 27/51] Document full-text limit and offset paging --- reference/configuration/operations.md | 4 - reference/configuration/options.md | 3 +- reference/resources/full-text-paging.md | 293 ++++------------------ reference/resources/overview.md | 1 - reference/resources/query-optimization.md | 6 - reference/resources/resource-api.md | 33 +-- reference/rest/headers.md | 6 - reference/rest/overview.md | 4 +- reference/rest/querying.md | 19 +- release-notes/v5-lincoln/5.4.md | 18 +- sidebarsReference.ts | 6 +- 11 files changed, 60 insertions(+), 333 deletions(-) diff --git a/reference/configuration/operations.md b/reference/configuration/operations.md index dfb186bd1..75d9ed695 100644 --- a/reference/configuration/operations.md +++ b/reference/configuration/operations.md @@ -18,10 +18,6 @@ For the full list of configurable options, see [Configuration Options](./options Modifies one or more Harper configuration parameters. **Requires a [restart](../operations-api/operations.md#restart) or [restart_service](../operations-api/operations.md#restart_service) to take effect** (a [restart](../operations-api/operations.md#restart) for `threads.v8Flags`). A change to `replication.threads` takes effect only on a full `restart`. -Changes to [`storage.fullText.runtime`](../resources/full-text-paging.md#configure-resource-limits) -(v5.4.0) require a full `restart`. `restart_service` replaces workers but keeps the established -process-wide native full-text budgets. - `operation` _(required)_ — must be `set_configuration` `replicated` _(optional)_ — set to `true` to apply the same configuration change to all cluster nodes in a single call. The origin node applies the change first; if its local write fails, nothing is sent to peers. Requires replication to be configured. diff --git a/reference/configuration/options.md b/reference/configuration/options.md index 14e668cec..27bd93331 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -312,7 +312,6 @@ storage: ``` - `engine` — The database storage engine to use for new databases. Currently supported engines are `rocksdb` and `lmdb`. The default is `rocksdb`. Existing databases will use the engine that was used when they were created. -- `fullText` - Full-text capacity controls (Added in: v5.4.0). See [Full-Text Paging](../resources/full-text-paging.md#configure-resource-limits) for settings, defaults and restart requirements. Native full-text search itself is available from v5.3.0. - `writeAsync` — Disable disk sync for higher throughput (**disables durability guarantees**); _Default_: `false` - `caching` — In-memory record caching; _Default_: `true` - `compression` — LZ4 record compression; _Default_: `true` (enabled by default since v4.3.0). Sub-options: `dictionary`, `threshold` @@ -579,3 +578,5 @@ my-component: - `.package` — NPM package name, GitHub repo (`user/repo`), or local path - `.port` — Port for the component; _Default_: value of `http.port` + +Full-text search work limits and native runtime admission settings are described in [Full-text paging](../resources/full-text-paging.md#configure-search-work). diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index 15e16f743..53955bad3 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -1,278 +1,77 @@ --- -title: Full-Text Paging +title: Full-text paging --- -# Full-Text Paging +# Full-text paging - + -The full-text index declaration below is a prerequisite. +Full-text and autocomplete searches use Harper's existing paging options. Each request runs a new search; Harper does not retain a search between pages. -Full-text continuation lets a client page through the original search matches while indexing -updates arrive. It is available through `Table.searchPage()` and REST. Ordinary offset-based paging -continues to run each request against its own index snapshot. -The same continuation options work with autocomplete searches using `matches_prefix` and `matches_fuzzy_prefix`. -Operations API searches do not support continuation options; use `Table.searchPage()` or REST. +## Request a page -## Declare a full-text index - -Full-text indexes require RocksDB and an audited table. The query names the `FullText` declaration, -not a stored source field: - -```graphql -type Product @table(audit: true) @export { - id: ID @primaryKey - title: String - catalogSearch: FullText @fullText(fields: [{ name: "title" }]) -} -``` - -`catalogSearch` is a query-only declaration. Select the stored fields and optional `$score` in results. -Full-text matches use descending relevance order. REST examples also require `rest: true` in the -application configuration; see [REST setup](../rest/overview.md). - -## Page through `Table.searchPage()` - -Use a positive integer `limit` no greater than `maxPageSize`. -`searchPage()` returns a promise for a bounded array. Omit `continuation` or set it to `true` to start. -`Table.search()` keeps its async iterable return value and rejects continuation options. - -Inside an authenticated Resource instance method: +Inside an authenticated Resource handler, use `limit` and `offset` with `Table.search()`. `Table.search()` returns an iterable, including when a custom Resource forwards or filters it. ```javascript -const context = this.getContext(); -const query = { - conditions: [{ attribute: 'catalogSearch', comparator: 'matches', value: 'trail shoes' }], - select: ['id', 'title', '$score'], - limit: 25, - checkPermission: true, -}; - -let continuation = true; -do { - const page = await tables.Product.searchPage({ ...query, continuation }, context); - for (const record of page) console.log(record); - continuation = page.continuation; -} while (continuation); +return Product.search( + { + checkPermission: true, + conditions: [{ attribute: 'catalogSearch', comparator: 'matches', value: 'trail shoes' }], + offset: 20, + limit: 20, + }, + this.getContext() +); ``` -The returned array has three additional properties: - -| Property | Meaning | -| -------------- | ------------------------------------------------------------------- | -| `continuation` | Token for the next page; `undefined` when traversal is complete | -| `complete` | Whether the original search has been exhausted or explicitly closed | -| `expiresAt` | Absolute expiry time in milliseconds since the Unix epoch | +The equivalent REST range has an inclusive start and exclusive end. This request skips 20 matching records and returns up to 20 more: -These properties are not included when the array is serialized as JSON. Keep the same conditions -and selected fields, and use the same authenticated caller when resuming. You may raise or lower -the requested `limit` on later pages within `maxPageSize`. Each page contains at most its requested -limit; a shorter page alone does not mean the search is complete. Use the default relevance order, or explicitly sort by descending -`$score` alone. A nonzero `offset`, another sort, or a truthy `count`, `explain`, or `sync` option -returns 400. In REST, the accepted explicit score order is `sort(-$score)` alone. - -In a static custom Resource method, pass the method's `context` argument as the second argument -to `tables.Product.searchPage()`. In an instance method, use `this.getContext()` instead. See -[Resource context](./resource-api.md#getcontext-context) for both forms. Set `checkPermission: true` -in the query on every page. Without it, a direct call does not request the caller's -table and searched-source permission checks; passing context alone does not enable them. -Changing the caller identity, including switching from an authenticated request to an internal -call without that context, returns 410. Permission changes for the same caller are checked on each -page. Application row filters still run on each page. -The token does not store application callbacks such as `rowFilter` or -[`vectorFilter`](../database/schema.md#filtered-vector-search-with-a-function-predicate). Supply them -again on every page, together with the caller's context and permission settings. - -If your table overrides `search()`, also implement `searchPage()` with the same filtering policy. -This applies to static and instance methods. Otherwise paging returns 400, including through REST, -to avoid bypassing application filters. - -A custom Resource supporting the framework's QUERY paging route must implement `searchPage()`. -Return the original page array. Copying it with `map()`, `filter()` or -spread drops its paging metadata. Returning that copy through the normal Resource API returns 400. -Use the query's `select` and filters instead. To add custom headers, return -`{ headers, data: page }` with the original page array. A Resource returning an explicit HTTP -`Response` owns its response headers, serialization and cache policy. - -## Page through REST - -Use the single-argument `limit(n)` page size and start with `continuation()`. Do not use -a nonzero starting offset with `limit(start,end)`, request a count with `Prefer: count=`, or change the relevance order. The same -`explain` and `sync` restrictions as `Table.searchPage()` apply; violations return 400. -The equivalent `limit(0,n)` form is accepted; it does not skip records within the retained search. - -```http -GET /Product/?catalogSearch=matches=trail%20shoes&select(id,title,$score)&limit(25)&continuation() +```text +GET /Product/?catalogSearch=matches=trail%20shoes&limit(20,40) ``` -The body stays a JSON array. Read the next token from `Harper-Continuation`, URL-encode it, and -send it in `continuation(token)` with the same conditions and selected fields. The requested page size may increase or decrease within `maxPageSize`: +Each page runs a new query. Changes to records, permissions, or the index can move results between requests, so separate pages may omit or repeat records. There is no retained search or cross-request snapshot. -```javascript -const query = '/Product/?catalogSearch=matches=trail%20shoes&select(id,title,$score)&limit(25)'; -let token; -do { - const continuation = token ? `continuation(${encodeURIComponent(token)})` : 'continuation()'; - const response = await fetch(`${query}&${continuation}`); - if (!response.ok) throw new Error(await response.text()); - const records = await response.json(); - for (const record of records) console.log(record); - token = response.headers.get('Harper-Continuation'); -} while (token); -``` +Within one request, Harper reads native batches on the same index reader until enough eligible records have been found or the matches are exhausted. Current source versions and record filters are checked before counting records toward the offset and limit. This traversal also lets prefix and fuzzy-prefix searches return more than the initial autocomplete batch. The library's ordinary search window is not a total-result cutoff for a bounded Harper request. -`Harper-Continuation-Complete` is present on every successful continuation page: the string `"false"` while more results remain, and `"true"` after completion or explicit close. `Harper-Continuation-Expires` contains the -absolute expiry time in milliseconds since the Unix epoch. Completed pages omit the token and -expiry. Harper adds these names to `Access-Control-Expose-Headers` so browser clients can read them. -Continuation responses override the usual read-cache policy with `Cache-Control: no-store`. JSON `QUERY` requests return the -same headers when their query body enables continuation. +Use a bounded `limit` for large result sets. An unbounded query that exceeds the native result window returns 400. Offset requests revisit earlier matches; deep offsets and restrictive filters can exhaust the work or time budget. An exhausted search returns its remaining eligible records, including an empty result. A budget failure returns an error instead of a successful incomplete page. -Request URL logging can record continuation tokens. Keep tokens out of logs and analytics. -QUERY accepts the token in its JSON body with the same query fields if it should stay out of the URL. -Tokens still require the same caller and query and expire with their search. +## Configure search work -Read and parse the complete response body before saving its next token. A stream can fail after -headers are sent. If a resumed page fails this way, retry with the previous token while the snapshot remains valid. -If no token was received on the first page, start a new search. The first search may keep its cursor -slot until absolute expiry because it cannot be closed without a token. +Search settings are under `storage.fullText` in `harper-config.yaml`: -GET and QUERY can return continuation pages. OPTIONS returns request metadata without running a -search. HEAD is an exception to the usual GET-without-a-body behavior: it returns 400 before -opening or advancing a continuation. Other methods also reject continuation parameters with 400. +| Setting | Default | Meaning | +| --------------------------- | ------: | ----------------------------------------------------------------------------------------------------------------- | +| `maxRawHitsPerRequest` | 10,000 | Fetched native hits, including the initial batch replay when switching to traversal | +| `requestBudgetMilliseconds` | 5,000 | Execution budget across native batches, source checks and highlight tracing, capped by the native execution limit | -Long record IDs make continuation tokens larger. Local measurements produced -tokens of about 600 bytes for short IDs and nearly 8 KB near the supported ID limit. Review Harper's -`http.maxHeaderSize` setting and any proxy response-header and request-line limits. Leave room for the -token, query, authentication, and other headers; a token that fits Harper may exceed a proxy limit. +The hit limit counts fetched hits, not every record Tantivy visits while scoring. Reaching it before the page fills or the search is exhausted returns 400. Exceeding the execution budget returns 503. Narrow the query, reduce the offset, or increase the relevant configured budget. Freshness waiting uses `waitForIndexMilliseconds` separately. Reader acquisition, query construction and some native operations can take additional time; the execution budget is not a hard response-time guarantee. -## Records and permissions can change +Each setting has the usual configuration parameter name, such as `storage_fullText_maxRawHitsPerRequest`, for `set_configuration` and environment overrides. Query work limits apply to new searches after configuration changes. -The snapshot fixes the index matches and their ranking. It does not retain historical Harper -records or permissions. Each request loads current records and checks current access. Updated, -deleted, expired, or newly unauthorized records can be omitted, and new matches do not enter the -original search. +## Configure native runtime admission -Retrying a token resumes the same index boundary. The returned rows can differ if records or -permissions changed. A page may be empty when the remaining snapshot contains no eligible records; -use the continuation token or completion flag to decide when to stop. +Native process admission settings live under `storage.fullText.runtime`: -## Close or restart a search +| Setting | Default | +| ---------------------- | ------: | +| `maxResidentIndexes` | 128 | +| `maxIndexingThreads` | 128 | +| `maxSearchThreads` | 256 | +| `maxWriterMemoryBytes` | 4 GiB | +| `maxQueuedBytes` | 8 GiB | +| `maxExpensiveSearches` | 8 | -To stop early, repeat the query with its current token and `closeContinuation: true`. In REST, add -`closeContinuation()` alongside `continuation(token)`. Closing returns an empty completed page. +These limits apply together. For example, two indexing threads and 64 MiB of writer memory per index reach the default thread and memory budgets at 64 indexes. Readers share an index's runtime; they do not allocate another writer. Admission limits are not hard bounds on total resident memory or disk usage. -Close and terminal pages wait for admission release before reporting success. If release fails, the -request returns an error instead. The revoked token may then return `410`, and its admission slot -can remain occupied until absolute expiry or worker exit. A successful close releases admission -capacity; it does not promise immediate removal of old index segments from disk. -Closing has a separate cleanup budget. Once release is acknowledged, the completed page can return -after its search-work deadline. Cancellation, transaction, and index-lifecycle checks still apply. - -Completion, close, expiry, index reset, definition changes, worker loss, or process restart can -invalidate a token. Resuming an unavailable snapshot returns `410`; start a new search. Once any -request finishes traversal, older tokens for that search also expire. -Changing any `storage.fullText` setting also invalidates existing tokens, even when only a capacity -setting changes. Start a new search after changing these settings. - -Snapshots belong to one Harper instance. Its workers can handle continuation requests for each -other. Requests reaching a different Harper instance cannot resume that snapshot. - -## Configure resource limits - -Settings live under `storage.fullText` in `harper-config.yaml`: - -```yaml -storage: - fullText: - cursorTtlMilliseconds: 60000 - maxCursors: 4 - maxRetainedQueryBytes: 8388608 - maxPendingRequests: 32 - maxPendingResponseBytes: 16777216 - maxPageSize: 1000 - requestBudgetMilliseconds: 5000 - maxRawHitsPerRequest: 100000 - maxRawHitsPerOrdinaryRequest: 10000 -``` - -| Setting | Default | Scope | -| ------------------------------ | ------: | ----------------------------------------------------------- | -| `cursorTtlMilliseconds` | 60,000 | Absolute search lifetime; paging does not extend it | -| `maxCursors` | 4 | Retained searches across all workers in one process | -| `maxRetainedQueryBytes` | 8 MiB | Retained native query text and identity bytes | -| `maxPageSize` | 1,000 | Returned records per page | -| `requestBudgetMilliseconds` | 5,000 | Freshness wait and page processing budget | -| `maxRawHitsPerRequest` | 100,000 | Fetched native hits per request, including replayed batches | -| `maxRawHitsPerOrdinaryRequest` | 10,000 | Additional hit ceiling for searches without continuation | -| `maxPendingRequests` | 32 | Pending requests per requesting worker | -| `maxPendingResponseBytes` | 16 MiB | Reserved response bytes per requesting worker | - -Ordinary searches use the lower of `maxRawHitsPerRequest` and `maxRawHitsPerOrdinaryRequest`, and share the `requestBudgetMilliseconds` execution budget. A raw-hit ceiling returns 400 instead of a partial page. Increasing the ordinary ceiling permits more native work and record reads per request; test selective queries before raising it. Replayed batches count toward the ceiling, so the number of distinct matches reached can be lower. - -A requesting worker reserves a conservative bound for a complete native batch before dispatch, including two-byte string encoding and message overhead. The reservation may exceed the returned payload. `maxPendingResponseBytes` can reject concurrent reads with 503 even when the cursor-count and pending-request limits have room. A reservation rejection happens before dispatch and does not advance the search. Reduce concurrency and retry with the same token while it remains valid; this rejection does not include a `Retry-After` header. - -Search work and cleanup use separate windows based on `requestBudgetMilliseconds`. With the -5,000 ms default, a search that uses its full work budget and then closes can take about 10 seconds. -Waiting for other page requests already using the snapshot can add time. Native work checks its -deadline while running; these settings are not a hard limit on total response time. - -Ordinary searches can use secondary indexes to skip record reads and enlarge batches when many -matches are filtered out. Continued searches do not yet use those optimizations; they fetch up to -256 native hits per batch and check current records. A selective filter can therefore add many -record reads and, when the snapshot belongs to another worker, many worker requests. - -The usual individual configuration names also work, such as -`storage_fullText_maxRawHitsPerRequest` with `set_configuration`. Follow the -[configuration restart procedure](../configuration/operations.md#set-configuration) after applying -changes with that operation. Native process-wide admission -settings live under `storage.fullText.runtime` and require a full process restart when changed. Restarting only worker threads does not apply new native budgets; replacement workers keep the established process budgets. Their defaults -are 128 resident indexes, 128 indexing threads, 256 search threads, 4 GiB configured writer memory, -8 GiB configured queue capacity, and 8 concurrent expensive searches. All limits apply together. - -Harper captures the native settings before loading application components, so workers created -by a running application inherit them. In a standalone process, initialize Harper before creating -workers that use full-text search. A worker created before capture cannot initialize full-text -search; recreate it after Harper initializes rather than retrying inside the same worker. -This throws `FullTextWorkerInitializationError` with status 500 and `retryable: false`. -Retained paging still requires Harper's main thread or a managed worker. Inheriting native settings -does not add paging-message routing to application-created workers. -Attempting retained paging from an unmanaged worker returns 503 with -`Full-text continuation requires a Harper-managed worker`; waiting and retrying on that worker -does not add routing. Run the page request through Harper's main thread or a managed worker. - -The runtime keys use integer counts or bytes: - -```yaml -storage: - fullText: - runtime: - maxResidentIndexes: 128 - maxIndexingThreads: 128 - maxSearchThreads: 256 - maxWriterMemoryBytes: 4294967296 - maxQueuedBytes: 8589934592 - maxExpensiveSearches: 8 -``` +Harper captures native runtime budgets before loading application components, or on an earlier main-thread native load or managed worker spawn. Application-created workers inherit that configuration. A worker created before capture fails with `FullTextWorkerInitializationError` (500, non-retryable); recreate it after Harper initializes. Replacement workers keep the established budgets after a live configuration change. Restart the Harper process to apply new native runtime budgets; restarting only HTTP workers is insufficient. -The cursor pool is shared by all callers. Four unfinished searches fill the default pool, so a fifth -receives `503` until a search finishes, closes, or expires. Ordinary searches remain available. -Configure `maxCursors` for the number of unfinished searches the application needs to retain. -There is no per-caller reservation or eviction policy. +Harper does not configure the library's native filter metadata or submit native filter or candidate-ID requests. Ordinary conditions continue through Harper's secondary-index gates and authoritative record predicates. Native page, response-byte and highlight limits still apply. Query terms and clauses remain limited by the installed library. -Cursor-count and pending-response capacity refusals return 503 without `Retry-After`. Close unused -searches or reduce concurrency, then retry with backoff while the token remains valid. An execution-budget -503 calls for a smaller page or less expensive query; changing the query requires a new search. +## Upgrade considerations -Cursor slots, pending-request counts, and response-byte allowances apply separately. Raising -`maxCursors` does not raise `maxPendingRequests` or `maxPendingResponseBytes`. Across workers, -Harper reserves response space using the library's largest supported IDs and versions, rather than -the average size of the current records. Many searches may be retained while fewer pages can run -at once. +This change uses the fulltext 0.6.0 native package. A format-incompatible upgrade rebuilds the local index from stored records; queries on that node return 503 until the index is ready. Full-text indexes require RocksDB and audit logging. -A raw-hit budget failure returns `400`; narrow the query or increase the budget. Capacity exhaustion -and timeouts return errors instead of successful incomplete pages. If the snapshot itself runs out -of eligible matches, a shorter completed page is a valid result. +The initial native autocomplete batch is not a total-result cap. Harper continues through later batches when needed to satisfy the requested page after filtering. Work and time limits still apply. -Retained readers can keep old index segments on disk until released. These settings do not provide -a hard limit on total process memory or retained disk space. Native deadlines are cooperative, so the -request budget is not a hard wall-clock guarantee. +Full-text counts are unavailable: a count response does not turn a native hit total into an authorized record count. For the ordinary REST paging syntax, see [Querying](../rest/querying.md#pagination-and-total-count). The JSON Operations API does not accept REST query syntax; use exported table routes or `Table.search()`. diff --git a/reference/resources/overview.md b/reference/resources/overview.md index 4a00d8f17..d0574af6b 100644 --- a/reference/resources/overview.md +++ b/reference/resources/overview.md @@ -172,4 +172,3 @@ Parameterized routes also surface in the generated OpenAPI document (as template | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | [Resource API](./resource-api.md) | Complete reference for instance methods, static methods, the Query object, RequestTarget, and response handling | | [Query Optimization](./query-optimization.md) | How Harper executes queries and how to write performant conditions | -| [Full-Text Paging](./full-text-paging.md) | Resume full-text and autocomplete searches on their original index snapshot | diff --git a/reference/resources/query-optimization.md b/reference/resources/query-optimization.md index 5e72803dd..dfe2cf9d1 100644 --- a/reference/resources/query-optimization.md +++ b/reference/resources/query-optimization.md @@ -79,12 +79,6 @@ When a secondary index is still being built — for example, just after a new `@ This is a transient condition — the index finishes building in the background, after which the same query succeeds. Clients should retry (with backoff) rather than treating it as an empty result or a permanent failure. On the operations API the error body includes `code` and `retryable`, so callers can branch on `error.code === 'INDEX_REBUILDING'` or `error.retryable`. -### Full-Text Search Work Limits - - - -Full-text queries have configurable work limits, including ordinary searches without continuation. A selective filter can exhaust a work limit and fail the request instead of returning a partial page. See [Full-Text Paging](./full-text-paging.md#configure-resource-limits) for the limits, errors and cost of increasing them. - ## Relationships and Joins Harper supports relationship-based queries that join data across tables. See [Schema documentation](../database/schema.md) for how to define relationships. diff --git a/reference/resources/resource-api.md b/reference/resources/resource-api.md index 0f63da00b..da064a259 100644 --- a/reference/resources/resource-api.md +++ b/reference/resources/resource-api.md @@ -91,17 +91,7 @@ The `get()` method returns a `RecordObject` — a frozen plain object with all r ### `search(query: RequestTarget | Query, context?): ExtendedIterable` -Performs a query on the resource or table. This is called by `get()` on collection requests and can be overridden to define custom query behavior. A table overriding `search()` must also implement `searchPage()` with the same filtering policy to enable continuation, including through REST. The default implementation on tables queries by the `conditions`, `limit`, `offset`, `select`, and `sort` properties parsed from the URL or query object. See [Query Object](#query-object) below for available query options. See the [ExtendedIterable](#extendediterable) below for how to interact with the query results. - ---- - -### `searchPage(query: RequestTarget | Query, context?): Promise` - - - -Tables use `searchPage()` for full-text and autocomplete continuation. Supply a positive `limit` and omit `continuation` or set it to `true` to start; supply the returned token to resume. `FullTextSearchPage` is an array of records with `continuation`, `complete`, and `expiresAt` properties; see the [result properties](./full-text-paging.md#page-through-tablesearchpage). `search()` stays iterable and rejects continuation options. - -When serving an authenticated caller, pass the caller's context and set `checkPermission: true` in the query. A custom `search()` override must explicitly implement `searchPage()` with the same filtering policy; otherwise paging returns 400. See [Full-Text Paging](./full-text-paging.md) for examples, supported queries, closing a search, and token expiry. +Performs a query on the resource or table. This is called by `get()` on collection requests and can be overridden to define custom query behavior. The default implementation on tables queries by the `conditions`, `limit`, `offset`, `select`, and `sort` properties parsed from the URL or query object. See [Query Object](#query-object) below for available query options. See the [ExtendedIterable](#extendediterable) below for how to interact with the query results. --- @@ -1031,21 +1021,7 @@ body, so avoid putting sensitive detail in thrown error messages. ## Query Object -The `Query` object is accepted by `search()`, `searchPage()`, and the static `get()` method. - -### `continuation` - -For `searchPage()`, omit this field or use `true` to start a search; use the returned token to resume. Keep the query and caller context consistent. See [Full-Text Paging](./full-text-paging.md#page-through-tablesearchpage). - -### `closeContinuation` - -Use `true` with an existing continuation token to close the search early. See [Close or restart a search](./full-text-paging.md#close-or-restart-a-search). - -### `checkPermission` - -Set `true` on every page when a direct table call acts for a caller, and pass that caller's context. Without it, the call does not request the caller's table and searched-source permission checks; context alone does not enable them. Automatic REST applies its permission checks. See [Full-Text Paging](./full-text-paging.md#page-through-tablesearchpage). - -Set this field in server code; never copy permission-control fields from client data. +The `Query` object is accepted by `search()` and the static `get()` method. ### `conditions` @@ -1059,10 +1035,6 @@ Array of condition objects to filter records. Each condition: | `conditions` | Nested conditions array | | `operator` | `and` (default) or `or` for the nested `conditions` | -Full-text conditions name a query-only `FullText` declaration and use comparators `matches`, -`matches_all`, `matches_phrase`, `matches_prefix`, `matches_fuzzy`, and `matches_fuzzy_prefix`. Each also has a `not_`-prefixed form; a negated full-text condition requires a positive full-text condition in the query. See -`Table.searchPage()` and [Full-Text Paging](./full-text-paging.md) for snapshot continuation and its query restrictions. - Example with nested conditions: ```javascript @@ -1113,7 +1085,6 @@ Special properties: - `$id` — Returns the primary key regardless of its name - `$updatedtime` — Returns the last-updated timestamp - `$distance` — When the query ranks or filters by a vector index, returns the computed distance from the target vector. See [Vector Indexing](../database/schema.md#vector-indexing). -- `$score` - When a query uses a full-text index, returns its relevance score. See [Full-Text Paging](./full-text-paging.md#page-through-tablesearchpage). #### Selecting related records diff --git a/reference/rest/headers.md b/reference/rest/headers.md index 7a64a1f39..71603c802 100644 --- a/reference/rest/headers.md +++ b/reference/rest/headers.md @@ -22,17 +22,11 @@ These headers are included in all Harper REST API responses: Collection responses to a [count request](./querying.md#pagination-and-total-count) additionally include `Content-Range`, `Range-Unit`, and `Preference-Applied` (). -Full-text continuation responses also return `Harper-Continuation-Complete`, plus -`Harper-Continuation` and `Harper-Continuation-Expires` while the search remains open. These names -are added to `Access-Control-Expose-Headers`. See -[Full-Text Paging](../resources/full-text-paging.md#page-through-rest) (v5.4.0). - ## Cache-Control Harper applies a tiered Cache-Control policy to REST responses. -Full-text continuation responses override this policy with `Cache-Control: no-store`. ### Anonymous reads diff --git a/reference/rest/overview.md b/reference/rest/overview.md index 86b1826e4..0f60b27c6 100644 --- a/reference/rest/overview.md +++ b/reference/rest/overview.md @@ -88,9 +88,7 @@ With both in place, the exported name becomes the base path (`Product` above, or Notes on this surface: - The trailing slash is significant throughout: `/Product` addresses the resource itself, `/Product/` addresses its collection of records. See [URL Structure](#url-structure). -- `HEAD` is served as `GET` with the response body omitted, except that - [full-text continuation requests](../resources/full-text-paging.md#page-through-rest) return `400` - before opening or advancing a search. `QUERY` is accepted on the collection path (`QUERY /Product/`) and runs a search taken from the request body rather than the URL. +- `HEAD` is served exactly as `GET` with the response body omitted. `QUERY` is accepted on the collection path (`QUERY /Product/`) and runs a search taken from the request body rather than the URL. - A collection `POST` only generates a primary key when the body omits the table's primary-key property. If the body supplies one, Harper stores the record under that value rather than replacing it, and a `POST` to a key that already exists fails with `409` instead of overwriting. - On a successful `POST`, the new record's primary key — supplied or generated — is returned in the `Location` response header. The header carries the bare key value, not a URL, so it is not a link to follow. - `PUT` replaces the stored record, with three exceptions that Harper always applies: a [`@createdTime`](../database/schema.md#createdtime) attribute keeps the original record's value, an [`@updatedTime`](../database/schema.md#updatedtime) attribute is re-stamped with the time of the write, and the primary key is forced to match the `{id}` in the URL even if the body carries a different one. diff --git a/reference/rest/querying.md b/reference/rest/querying.md index 0ae96a24c..03743191e 100644 --- a/reference/rest/querying.md +++ b/reference/rest/querying.md @@ -55,9 +55,6 @@ Harper uses [FIQL](https://datatracker.ietf.org/doc/html/draft-nottingham-atompu | `=`, `===` | Strict equality (no type conversion) | | `!==` | Strict inequality (no type conversion) | -Declared full-text fields use `=matches=`, `=matches_prefix=` or `=matches_fuzzy_prefix=`. -See [Full-Text Paging](../resources/full-text-paging.md#page-through-rest) for examples and continuation. - **Examples**: ```http @@ -176,28 +173,16 @@ GET /Product/?rating=gt=3&sort(+name) GET /Product/?sort(+rating,-price) ``` -### `continuation()` and `closeContinuation()` - - - -Full-text searches can retain their original index snapshot between requests. Start with -`continuation()`, resume with `continuation(token)`, and close early with `closeContinuation()` -alongside the token. Use `limit(n)` to set the page size. See -[Full-Text Paging](../resources/full-text-paging.md#page-through-rest) for the token headers, -query restrictions and recovery steps. - ## Pagination and Total Count +Full-text and autocomplete queries use the same paging syntax. See [Full-text paging](../resources/full-text-paging.md) for search work limits and behavior when records change between requests. + Use `limit(start,end)` to page through a collection, and opt in to a total match count with the `Prefer` request header (`Prefer: count=exact`) so a client can render pagination (for example "1-25 of 1,234") without a second request. -For full-text paging across requests (), use [continuation](#continuation-and-closecontinuation) instead of offset paging or a count request. - Counting is opt-in: without the header, no count is computed and no count headers are returned. It applies only to `GET`/`HEAD` requests and requires a bounded page — the `limit()` must be a non-negative integer no larger than **10,000**, and the requested window (offset + limit) no larger than **1,000,000**. A request with no `limit()`, one outside those bounds, or a non-numeric one is served normally with no count headers, since counting an unbounded page would defeat the point of paging. -Full-text queries also have [search work limits](../resources/full-text-paging.md#configure-resource-limits). A count request does not remove those limits; a full-text query can fail before returning a page or count. - ### Requesting a count Send a `Prefer` header on a `GET` (or `HEAD`) request to a collection: diff --git a/release-notes/v5-lincoln/5.4.md b/release-notes/v5-lincoln/5.4.md index f90565f3f..7245d5c19 100644 --- a/release-notes/v5-lincoln/5.4.md +++ b/release-notes/v5-lincoln/5.4.md @@ -8,18 +8,6 @@ title: '5.4' All patch release notes for 5.4.x are available on the [releases page](https://github.com/HarperFast/harper/releases?q=v5.4&expanded=true). -## Upgrade Notes - -### Full-Text Search Budgets - -The new `storage.fullText` work limits also apply to searches without continuation: the ordinary fetched-hit ceiling defaults to 10,000 and the shared execution budget defaults to 5,000 ms. Replayed batches count toward the hit ceiling. Hitting the raw-hit ceiling returns 400 instead of a successful partial page; exhausting the execution budget returns 503. Test selective full-text queries before upgrading. Narrow an expensive query or reduce its page size before increasing a work limit; higher limits permit more work and record reads per request. See [resource limits](/reference/v5/resources/full-text-paging#configure-resource-limits) before changing the defaults. - -## Full-Text Search - -### Snapshot Paging - -Full-text and autocomplete searches can retain their original index matches between requests through `Table.searchPage()` or REST continuation tokens. Ordinary `Table.search()` remains iterable. See [Full-Text Paging](/reference/v5/resources/full-text-paging). - ## Replication ### Dedicated Replication Threads @@ -75,3 +63,9 @@ The new `threads.v8Flags` setting takes a V8 flag, or a list of them, and applie ### Restore Checks a Backup Before Deleting the Database `restore_backup` no longer deletes a database before checking that the backup can be read. It first verifies the checksum of every database file in the backup, checks its transaction logs, opens each table file to confirm this version can read its format and compression, and, for a backup that captured blobs, checks that the blob snapshot is present. A backup that fails is refused and the database is left as it was (one an earlier restore left incomplete stays blocked); previously the restore deleted the database first and then failed, so a backup taken by a newer version, or one with a corrupt file, destroyed the database it was meant to restore. The check reads the whole backup once, so a restore takes longer, and an online restore keeps the database unavailable while it runs; it needs no extra disk space. A restore interrupted while it is still checking the backup leaves the destination's files and any preexisting incomplete-restore marker unchanged; a previously healthy database loads normally on the next start. Blob file contents are not checked. See [`restore_backup`](/reference/v5/backups/operations#restore_backup). + +### Full-text paging and search work limits + +Full-text and autocomplete queries use the existing `limit` and `offset` paging options. Harper reads additional native batches when needed after filtering, so the initial native result window no longer silently cuts off eligible results. Requests that exceed configured work or time budgets return an error. Separate page requests run independently and can see changes to records or index contents. + +See [Full-text paging](/reference/v5/resources/full-text-paging) for examples, limits and upgrade considerations. diff --git a/sidebarsReference.ts b/sidebarsReference.ts index d7ea4b862..a2306e408 100644 --- a/sidebarsReference.ts +++ b/sidebarsReference.ts @@ -87,11 +87,7 @@ const sidebars: SidebarsConfig = { id: 'resources/query-optimization', label: 'Query Optimization', }, - { - type: 'doc', - id: 'resources/full-text-paging', - label: 'Full-Text Paging', - }, + { type: 'doc', id: 'resources/full-text-paging', label: 'Full-text Paging' }, ], }, { From abcd092e086f5075ae9fe048e1c0368587e79626 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 21:50:23 -0600 Subject: [PATCH 28/51] Clarify full-text query syntax and budget recovery --- reference/configuration/operations.md | 2 ++ reference/resources/full-text-paging.md | 11 ++++++++++- reference/resources/resource-api.md | 2 ++ reference/rest/querying.md | 4 +++- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/reference/configuration/operations.md b/reference/configuration/operations.md index 75d9ed695..134980d32 100644 --- a/reference/configuration/operations.md +++ b/reference/configuration/operations.md @@ -18,6 +18,8 @@ For the full list of configurable options, see [Configuration Options](./options Modifies one or more Harper configuration parameters. **Requires a [restart](../operations-api/operations.md#restart) or [restart_service](../operations-api/operations.md#restart_service) to take effect** (a [restart](../operations-api/operations.md#restart) for `threads.v8Flags`). A change to `replication.threads` takes effect only on a full `restart`. +Full-text request work limits (`storage.fullText.maxRawHitsPerRequest` and `requestBudgetMilliseconds`) apply to new searches without a restart. Changes to `storage.fullText.runtime` require a full Harper process restart; `restart_service` does not replace the native process budget. See [Full-text paging](../resources/full-text-paging.md#configure-search-work). + `operation` _(required)_ — must be `set_configuration` `replicated` _(optional)_ — set to `true` to apply the same configuration change to all cluster nodes in a single call. The origin node applies the change first; if its local write fails, nothing is sent to peers. Requires replication to be configured. diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index 53955bad3..08585665a 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -74,4 +74,13 @@ This change uses the fulltext 0.6.0 native package. A format-incompatible upgrad The initial native autocomplete batch is not a total-result cap. Harper continues through later batches when needed to satisfy the requested page after filtering. Work and time limits still apply. -Full-text counts are unavailable: a count response does not turn a native hit total into an authorized record count. For the ordinary REST paging syntax, see [Querying](../rest/querying.md#pagination-and-total-count). The JSON Operations API does not accept REST query syntax; use exported table routes or `Table.search()`. +For an eligible bounded count request, full-text counts are unavailable and `Content-Range` uses `*` for the total. A native hit total is not an authorized record count. For the ordinary REST paging syntax, see [Querying](../rest/querying.md#pagination-and-total-count). The JSON Operations API does not accept REST query syntax; use exported table routes or `Table.search()`. + +## Errors and recovery + +| Situation | HTTP status | Action | +| --------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------ | +| Fetched-hit budget exhausted | 400 | Narrow the query, reduce the offset, or raise `maxRawHitsPerRequest`. | +| Execution budget exhausted | 503 | Narrow the query or adjust `requestBudgetMilliseconds`; retrying the same expensive query can hit the same limit. | +| Index rebuilding or unavailable | 503 | Wait for the local index to become ready before retrying. | +| Application worker started before native runtime configuration was captured | 500 | `FullTextWorkerInitializationError` is non-retryable in that worker. Recreate the worker after Harper initializes. | diff --git a/reference/resources/resource-api.md b/reference/resources/resource-api.md index da064a259..cf44bb715 100644 --- a/reference/resources/resource-api.md +++ b/reference/resources/resource-api.md @@ -1035,6 +1035,8 @@ Array of condition objects to filter records. Each condition: | `conditions` | Nested conditions array | | `operator` | `and` (default) or `or` for the nested `conditions` | +Full-text conditions use `matches`, `matches_all`, `matches_phrase`, `matches_prefix`, `matches_fuzzy`, or `matches_fuzzy_prefix` against a declared full-text index. See [Full-text paging](./full-text-paging.md) for a `Table.search()` example and work limits. + Example with nested conditions: ```javascript diff --git a/reference/rest/querying.md b/reference/rest/querying.md index 03743191e..bf705273a 100644 --- a/reference/rest/querying.md +++ b/reference/rest/querying.md @@ -55,6 +55,8 @@ Harper uses [FIQL](https://datatracker.ietf.org/doc/html/draft-nottingham-atompu | `=`, `===` | Strict equality (no type conversion) | | `!==` | Strict inequality (no type conversion) | +Full-text indexes support `=matches=`, `=matches_all=`, `=matches_phrase=`, `=matches_prefix=`, `=matches_fuzzy=`, and `=matches_fuzzy_prefix=`. These use the full-text index rather than ordinary string comparisons. See [Full-text paging](../resources/full-text-paging.md). + **Examples**: ```http @@ -157,7 +159,7 @@ GET /Product/?brand.name=Microsoft&select(name,brand{name}) ### `limit(end)` or `limit(start,end)` -Limit the number of results returned, with an optional starting offset. +Limit the number of results returned, with an optional starting offset. The start is inclusive and the end is exclusive: `limit(10,30)` skips 10 records and returns up to 20. ```http GET /Product/?rating=gt=3&inStock=true&select(rating,name)&limit(20) From ada02a40a50d25bafa07039ccbba435b80a5201c Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 21:51:04 -0600 Subject: [PATCH 29/51] Describe exact and estimated full-text counts accurately --- reference/resources/full-text-paging.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index 08585665a..959edbd66 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -74,7 +74,7 @@ This change uses the fulltext 0.6.0 native package. A format-incompatible upgrad The initial native autocomplete batch is not a total-result cap. Harper continues through later batches when needed to satisfy the requested page after filtering. Work and time limits still apply. -For an eligible bounded count request, full-text counts are unavailable and `Content-Range` uses `*` for the total. A native hit total is not an authorized record count. For the ordinary REST paging syntax, see [Querying](../rest/querying.md#pagination-and-total-count). The JSON Operations API does not accept REST query syntax; use exported table routes or `Table.search()`. +Full-text queries do not provide an exact record count. When exact counting is enabled and requested, `Content-Range` uses `*` for the total. Estimated counting can return a planner estimate when no opaque row or vector filter suppresses it. A native hit total is not an authorized record count. See [count preferences](../rest/querying.md#pagination-and-total-count) for the default downgrade from exact to estimated counting. For the ordinary REST paging syntax, see [Querying](../rest/querying.md#pagination-and-total-count). The JSON Operations API does not accept REST query syntax; use exported table routes or `Table.search()`. ## Errors and recovery From 9643499aa189983528fb5cfc342d25845ae172bc Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 21:59:24 -0600 Subject: [PATCH 30/51] Make paging prerequisites and operating limits discoverable --- reference/configuration/options.md | 3 +-- reference/resources/full-text-paging.md | 24 ++++++++++++++++++++---- release-notes/v5-lincoln/5.4.md | 6 ++++-- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index 27bd93331..c091b86cd 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -312,6 +312,7 @@ storage: ``` - `engine` — The database storage engine to use for new databases. Currently supported engines are `rocksdb` and `lmdb`. The default is `rocksdb`. Existing databases will use the engine that was used when they were created. +- `fullText` — Full-text request work limits and native process budgets; see [Full-text paging](../resources/full-text-paging.md#configure-search-work). - `writeAsync` — Disable disk sync for higher throughput (**disables durability guarantees**); _Default_: `false` - `caching` — In-memory record caching; _Default_: `true` - `compression` — LZ4 record compression; _Default_: `true` (enabled by default since v4.3.0). Sub-options: `dictionary`, `threshold` @@ -578,5 +579,3 @@ my-component: - `.package` — NPM package name, GitHub repo (`user/repo`), or local path - `.port` — Port for the component; _Default_: value of `http.port` - -Full-text search work limits and native runtime admission settings are described in [Full-text paging](../resources/full-text-paging.md#configure-search-work). diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index 959edbd66..fd0cf159f 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -8,6 +8,20 @@ title: Full-text paging Full-text and autocomplete searches use Harper's existing paging options. Each request runs a new search; Harper does not retain a search between pages. +## Before you start + +Use an audited RocksDB table with a declared full-text field. For example: + +```graphql +type Product @table(audit: true) @export { + id: ID @primaryKey + title: String + catalogSearch: FullText @fullText(fields: [{ name: "title" }]) +} +``` + +`catalogSearch` names the index over `title`; it is not a stored record value. The examples below search that index through an exported table route or an authenticated Resource handler. + ## Request a page Inside an authenticated Resource handler, use `limit` and `offset` with `Table.search()`. `Table.search()` returns an iterable, including when a custom Resource forwards or filters it. @@ -34,7 +48,7 @@ Each page runs a new query. Changes to records, permissions, or the index can mo Within one request, Harper reads native batches on the same index reader until enough eligible records have been found or the matches are exhausted. Current source versions and record filters are checked before counting records toward the offset and limit. This traversal also lets prefix and fuzzy-prefix searches return more than the initial autocomplete batch. The library's ordinary search window is not a total-result cutoff for a bounded Harper request. -Use a bounded `limit` for large result sets. An unbounded query that exceeds the native result window returns 400. Offset requests revisit earlier matches; deep offsets and restrictive filters can exhaust the work or time budget. An exhausted search returns its remaining eligible records, including an empty result. A budget failure returns an error instead of a successful incomplete page. +Use a bounded `limit` for large result sets. An unbounded query that exceeds the native result window returns 400. `offset + limit` must not exceed `maxRawHitsPerRequest`; replay and rejected hits can exhaust that budget even earlier. Offset requests revisit earlier matches; deep offsets and restrictive filters can exhaust the work or time budget. Harper can retain up to `offset + limit` accepted records while building a bounded result, so raising the work limit can also increase memory use. An exhausted search returns its remaining eligible records, including an empty result. A budget failure returns an error instead of a successful incomplete page. ## Configure search work @@ -64,9 +78,9 @@ Native process admission settings live under `storage.fullText.runtime`: These limits apply together. For example, two indexing threads and 64 MiB of writer memory per index reach the default thread and memory budgets at 64 indexes. Readers share an index's runtime; they do not allocate another writer. Admission limits are not hard bounds on total resident memory or disk usage. -Harper captures native runtime budgets before loading application components, or on an earlier main-thread native load or managed worker spawn. Application-created workers inherit that configuration. A worker created before capture fails with `FullTextWorkerInitializationError` (500, non-retryable); recreate it after Harper initializes. Replacement workers keep the established budgets after a live configuration change. Restart the Harper process to apply new native runtime budgets; restarting only HTTP workers is insufficient. +Harper captures native runtime budgets before loading application components, or on an earlier main-thread native load or managed worker spawn. Application-created workers inherit that configuration. A worker created before capture fails on its first native full-text load with `FullTextWorkerInitializationError` (500, non-retryable). Create application workers from a Harper application component, after runtime configuration has been captured; recreate an early worker rather than retrying inside it. Replacement workers keep the established budgets after a live configuration change. Restart the Harper process to apply new native runtime budgets; restarting only HTTP workers is insufficient. -Harper does not configure the library's native filter metadata or submit native filter or candidate-ID requests. Ordinary conditions continue through Harper's secondary-index gates and authoritative record predicates. Native page, response-byte and highlight limits still apply. Query terms and clauses remain limited by the installed library. +Native page, response-byte and highlight limits still apply. Query terms and clauses remain limited by the installed library. ## Upgrade considerations @@ -74,7 +88,7 @@ This change uses the fulltext 0.6.0 native package. A format-incompatible upgrad The initial native autocomplete batch is not a total-result cap. Harper continues through later batches when needed to satisfy the requested page after filtering. Work and time limits still apply. -Full-text queries do not provide an exact record count. When exact counting is enabled and requested, `Content-Range` uses `*` for the total. Estimated counting can return a planner estimate when no opaque row or vector filter suppresses it. A native hit total is not an authorized record count. See [count preferences](../rest/querying.md#pagination-and-total-count) for the default downgrade from exact to estimated counting. For the ordinary REST paging syntax, see [Querying](../rest/querying.md#pagination-and-total-count). The JSON Operations API does not accept REST query syntax; use exported table routes or `Table.search()`. +Full-text queries do not provide an exact record count. When exact counting is enabled and requested, `Content-Range` uses `*` for the total. Estimated counting can return a planner estimate when no opaque row or vector filter suppresses it. A native hit total is not an authorized record count. See [count preferences](../rest/querying.md#pagination-and-total-count) for the default downgrade from exact to estimated counting. For the ordinary REST paging syntax, see [Querying](../rest/querying.md#pagination-and-total-count). The JSON Operations API uses `search_by_conditions` with `limit` and `offset` rather than REST URL syntax. The same per-request behavior and work limits apply. ## Errors and recovery @@ -84,3 +98,5 @@ Full-text queries do not provide an exact record count. When exact counting is e | Execution budget exhausted | 503 | Narrow the query or adjust `requestBudgetMilliseconds`; retrying the same expensive query can hit the same limit. | | Index rebuilding or unavailable | 503 | Wait for the local index to become ready before retrying. | | Application worker started before native runtime configuration was captured | 500 | `FullTextWorkerInitializationError` is non-retryable in that worker. Recreate the worker after Harper initializes. | + +`INDEX_REBUILDING` and `DERIVED_INDEX_LAGGING` identify retryable readiness or coverage failures. A native busy response also returns 503; reduce concurrency or retry with backoff. HTTP status alone does not distinguish these from an execution-budget failure. diff --git a/release-notes/v5-lincoln/5.4.md b/release-notes/v5-lincoln/5.4.md index 7245d5c19..122cf5546 100644 --- a/release-notes/v5-lincoln/5.4.md +++ b/release-notes/v5-lincoln/5.4.md @@ -64,8 +64,10 @@ The new `threads.v8Flags` setting takes a V8 flag, or a list of them, and applie `restore_backup` no longer deletes a database before checking that the backup can be read. It first verifies the checksum of every database file in the backup, checks its transaction logs, opens each table file to confirm this version can read its format and compression, and, for a backup that captured blobs, checks that the blob snapshot is present. A backup that fails is refused and the database is left as it was (one an earlier restore left incomplete stays blocked); previously the restore deleted the database first and then failed, so a backup taken by a newer version, or one with a corrupt file, destroyed the database it was meant to restore. The check reads the whole backup once, so a restore takes longer, and an online restore keeps the database unavailable while it runs; it needs no extra disk space. A restore interrupted while it is still checking the backup leaves the destination's files and any preexisting incomplete-restore marker unchanged; a previously healthy database loads normally on the next start. Blob file contents are not checked. See [`restore_backup`](/reference/v5/backups/operations#restore_backup). -### Full-text paging and search work limits +## Full-Text Search -Full-text and autocomplete queries use the existing `limit` and `offset` paging options. Harper reads additional native batches when needed after filtering, so the initial native result window no longer silently cuts off eligible results. Requests that exceed configured work or time budgets return an error. Separate page requests run independently and can see changes to records or index contents. +### Paging and Search Work Limits + +Full-text and autocomplete queries use the existing `limit` and `offset` paging options. Harper reads additional native batches when needed after filtering, so the initial native result window no longer silently cuts off eligible results. The new `storage.fullText.maxRawHitsPerRequest` and `storage.fullText.requestBudgetMilliseconds` settings bound request work. Requests that exceed those budgets return an error. Separate page requests run independently and can see changes to records or index contents. See [Full-text paging](/reference/v5/resources/full-text-paging) for examples, limits and upgrade considerations. From cf7eba92d41d7105ab86e56a0a8f2e2f03c20019 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 22:10:44 -0600 Subject: [PATCH 31/51] Use the table global in the paging example --- reference/resources/full-text-paging.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index fd0cf159f..5dc2cef85 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -27,7 +27,7 @@ type Product @table(audit: true) @export { Inside an authenticated Resource handler, use `limit` and `offset` with `Table.search()`. `Table.search()` returns an iterable, including when a custom Resource forwards or filters it. ```javascript -return Product.search( +return tables.Product.search( { checkPermission: true, conditions: [{ attribute: 'catalogSearch', comparator: 'matches', value: 'trail shoes' }], @@ -84,7 +84,7 @@ Native page, response-byte and highlight limits still apply. Query terms and cla ## Upgrade considerations -This change uses the fulltext 0.6.0 native package. A format-incompatible upgrade rebuilds the local index from stored records; queries on that node return 503 until the index is ready. Full-text indexes require RocksDB and audit logging. +Harper 5.4 uses the fulltext 0.6.0 native package. A format-incompatible upgrade rebuilds the local index from stored records; queries on that node return 503 until the index is ready. Full-text indexes require RocksDB and audit logging. The initial native autocomplete batch is not a total-result cap. Harper continues through later batches when needed to satisfy the requested page after filtering. Work and time limits still apply. From 12fdb83b2fc92cef275f8b4ecb593ed2b1b6835e Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 22:12:12 -0600 Subject: [PATCH 32/51] Link the pinned native query limits --- reference/resources/full-text-paging.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md index 5dc2cef85..e2496107f 100644 --- a/reference/resources/full-text-paging.md +++ b/reference/resources/full-text-paging.md @@ -80,7 +80,7 @@ These limits apply together. For example, two indexing threads and 64 MiB of wri Harper captures native runtime budgets before loading application components, or on an earlier main-thread native load or managed worker spawn. Application-created workers inherit that configuration. A worker created before capture fails on its first native full-text load with `FullTextWorkerInitializationError` (500, non-retryable). Create application workers from a Harper application component, after runtime configuration has been captured; recreate an early worker rather than retrying inside it. Replacement workers keep the established budgets after a live configuration change. Restart the Harper process to apply new native runtime budgets; restarting only HTTP workers is insufficient. -Native page, response-byte and highlight limits still apply. Query terms and clauses remain limited by the installed library. +Native page, response-byte and highlight limits still apply. Fulltext 0.6.0 also limits each analyzed query leaf to 64 terms and the compiled query to 256 clauses. Raising Harper's work budgets does not raise those limits. See the [published library limits](https://github.com/HarperFast/fulltext/blob/v0.6.0/docs/limits.md) for the full list and recovery guidance. ## Upgrade considerations From 83cc98f1e8cac907a66aed736b6034ed28f393ac Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 22:23:38 -0600 Subject: [PATCH 33/51] Clarify paging count and configuration exceptions --- reference/configuration/operations.md | 4 ++-- reference/configuration/options.md | 2 +- reference/operations-api/operations.md | 10 +++++----- reference/rest/querying.md | 2 ++ 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/reference/configuration/operations.md b/reference/configuration/operations.md index 134980d32..b74335bc0 100644 --- a/reference/configuration/operations.md +++ b/reference/configuration/operations.md @@ -16,9 +16,9 @@ For the full list of configurable options, see [Configuration Options](./options ## Set Configuration -Modifies one or more Harper configuration parameters. **Requires a [restart](../operations-api/operations.md#restart) or [restart_service](../operations-api/operations.md#restart_service) to take effect** (a [restart](../operations-api/operations.md#restart) for `threads.v8Flags`). A change to `replication.threads` takes effect only on a full `restart`. +Modifies one or more Harper configuration parameters. **Most changes require a [restart](../operations-api/operations.md#restart) or [restart_service](../operations-api/operations.md#restart_service) to take effect** (a [restart](../operations-api/operations.md#restart) for `threads.v8Flags`). A change to `replication.threads` takes effect only on a full `restart`. -Full-text request work limits (`storage.fullText.maxRawHitsPerRequest` and `requestBudgetMilliseconds`) apply to new searches without a restart. Changes to `storage.fullText.runtime` require a full Harper process restart; `restart_service` does not replace the native process budget. See [Full-text paging](../resources/full-text-paging.md#configure-search-work). +Full-text request work limits (`storage.fullText.maxRawHitsPerRequest` and `storage.fullText.requestBudgetMilliseconds`) apply to new searches without a restart. Changes to `storage.fullText.runtime` require a full Harper process restart; `restart_service` does not replace the native process budget. See [Full-text paging](../resources/full-text-paging.md#configure-search-work). `operation` _(required)_ — must be `set_configuration` diff --git a/reference/configuration/options.md b/reference/configuration/options.md index c091b86cd..2b93582d9 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -312,7 +312,7 @@ storage: ``` - `engine` — The database storage engine to use for new databases. Currently supported engines are `rocksdb` and `lmdb`. The default is `rocksdb`. Existing databases will use the engine that was used when they were created. -- `fullText` — Full-text request work limits and native process budgets; see [Full-text paging](../resources/full-text-paging.md#configure-search-work). +- `fullText` — Full-text request work limits and native process budgets; see [search work limits](../resources/full-text-paging.md#configure-search-work) and [native process budgets](../resources/full-text-paging.md#configure-native-runtime-admission). - `writeAsync` — Disable disk sync for higher throughput (**disables durability guarantees**); _Default_: `false` - `caching` — In-memory record caching; _Default_: `true` - `compression` — LZ4 record compression; _Default_: `true` (enabled by default since v4.3.0). Sub-options: `dictionary`, `threshold` diff --git a/reference/operations-api/operations.md b/reference/operations-api/operations.md index 21815c98a..865848d90 100644 --- a/reference/operations-api/operations.md +++ b/reference/operations-api/operations.md @@ -1679,14 +1679,14 @@ Operations for reading and updating Harper configuration. Detailed documentation: [Configuration Overview](../configuration/overview.md) -| Operation | Description | Role Required | -| ------------------- | ---------------------------------------------------------------- | ------------- | -| `set_configuration` | Modifies Harper configuration file parameters (requires restart) | super_user | -| `get_configuration` | Returns the current Harper configuration | super_user | +| Operation | Description | Role Required | +| ------------------- | --------------------------------------------- | ------------- | +| `set_configuration` | Modifies Harper configuration file parameters | super_user | +| `get_configuration` | Returns the current Harper configuration | super_user | ### `set_configuration` -Updates configuration parameters in `harper-config.yaml`. A restart (`restart` or `restart_service`) is required for changes to take effect. +Updates configuration parameters in `harper-config.yaml`. Most changes require a restart (`restart` or `restart_service`) to take effect. Full-text request work limits apply to new searches without a restart; changes to the native process budgets require a full `restart`. See [Full-text paging](../resources/full-text-paging.md#configure-search-work). Supports `"replicated": true` to apply the same change to all cluster nodes in one call; per-node outcomes are returned in the response's `replicated` array. Only send cluster-appropriate parameters when replicating — node-local parameters (ports, `node.hostname`, file paths, TLS material, `replication.hostname`/`url`/`routes`) would overwrite every peer's local values. To apply the change cluster-wide, follow with `restart_service` using `"replicated": true` (which restarts nodes one at a time); `restart_service` restarts only worker threads, though, so a change to [`threads.v8Flags`](../configuration/options.md#applying-v8-flags) takes effect only after a `restart` sent to each node in turn (`restart` does not replicate). See [Configuration Operations](../configuration/operations.md#set-configuration) for details. diff --git a/reference/rest/querying.md b/reference/rest/querying.md index bf705273a..4ff839e56 100644 --- a/reference/rest/querying.md +++ b/reference/rest/querying.md @@ -222,6 +222,8 @@ The response status is always `200` — `Content-Range` is informational (Harper The total is reported as `*` (for example `Content-Range: items 0-24/*`) when it cannot be produced — an exact scan that hits its guardrail (counting the tail past the requested page is bounded by a 1,000,000-row cap and a ~1-second budget, and abandons the total rather than truncating the page), or a query with no cardinality estimate (for example a `!=` or `=ct=` (contains) condition). `Preference-Applied` still echoes the requested mode, so an unavailable total (`.../*`) is distinct from a request that asked for no count. +Full-text and autocomplete queries cannot provide an exact total. An estimated count may use the planner estimate when the query has no opaque row or vector filter. See [Full-text paging](../resources/full-text-paging.md#request-a-page). + ### Enabling exact counts Because an exact count scans the full matched set, it is **off by default**. Enable it in an application's REST configuration via the [`exactCount` option](./overview.md#configuration): From c7f3a79a733e0abecee9fa6494878351491fc8f4 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 22:31:51 -0600 Subject: [PATCH 34/51] Keep paging guidance in existing reference sections --- reference/configuration/operations.md | 2 +- reference/configuration/options.md | 13 ++- reference/configuration/overview.md | 2 +- reference/operations-api/operations.md | 4 +- reference/resources/full-text-paging.md | 102 ---------------------- reference/resources/query-optimization.md | 30 +++++++ reference/resources/resource-api.md | 2 +- reference/rest/querying.md | 6 +- release-notes/v5-lincoln/5.4.md | 2 +- sidebarsReference.ts | 1 - 10 files changed, 51 insertions(+), 113 deletions(-) delete mode 100644 reference/resources/full-text-paging.md diff --git a/reference/configuration/operations.md b/reference/configuration/operations.md index b74335bc0..177935496 100644 --- a/reference/configuration/operations.md +++ b/reference/configuration/operations.md @@ -18,7 +18,7 @@ For the full list of configurable options, see [Configuration Options](./options Modifies one or more Harper configuration parameters. **Most changes require a [restart](../operations-api/operations.md#restart) or [restart_service](../operations-api/operations.md#restart_service) to take effect** (a [restart](../operations-api/operations.md#restart) for `threads.v8Flags`). A change to `replication.threads` takes effect only on a full `restart`. -Full-text request work limits (`storage.fullText.maxRawHitsPerRequest` and `storage.fullText.requestBudgetMilliseconds`) apply to new searches without a restart. Changes to `storage.fullText.runtime` require a full Harper process restart; `restart_service` does not replace the native process budget. See [Full-text paging](../resources/full-text-paging.md#configure-search-work). +Full-text request work limits (`storage.fullText.maxRawHitsPerRequest` and `storage.fullText.requestBudgetMilliseconds`) apply to new searches without a restart. Changes to `storage.fullText.runtime` require a full Harper process restart; `restart_service` does not replace the native process budget. See [Full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging). `operation` _(required)_ — must be `set_configuration` diff --git a/reference/configuration/options.md b/reference/configuration/options.md index 2b93582d9..7ab42326b 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -312,7 +312,18 @@ storage: ``` - `engine` — The database storage engine to use for new databases. Currently supported engines are `rocksdb` and `lmdb`. The default is `rocksdb`. Existing databases will use the engine that was used when they were created. -- `fullText` — Full-text request work limits and native process budgets; see [search work limits](../resources/full-text-paging.md#configure-search-work) and [native process budgets](../resources/full-text-paging.md#configure-native-runtime-admission). +- `fullText` — Full-text search work and native process limits. Sub-options: + - `maxRawHitsPerRequest` — Maximum fetched hits per request, including replay when switching to native traversal; default `10000`. + - `requestBudgetMilliseconds` — Execution budget across native batches, source checks and highlights, capped by the native execution limit; default `5000` ms. Reader acquisition and freshness waiting have separate bounds. + - `runtime.maxResidentIndexes` — Maximum resident native indexes; default `128`. + - `runtime.maxIndexingThreads` — Maximum native indexing threads; default `128`. + - `runtime.maxSearchThreads` — Maximum native search threads; default `256`. + - `runtime.maxWriterMemoryBytes` — Native writer-memory admission budget; default `4294967296` (4 GiB). + - `runtime.maxQueuedBytes` — Native queued-write admission budget; default `8589934592` (8 GiB). + - `runtime.maxExpensiveSearches` — Maximum concurrent expensive native searches; default `8`. + + The runtime budgets apply together and are not hard bounds on total process memory. Work-limit changes through `set_configuration` apply to new searches; runtime-budget changes require a full Harper process restart. Use the usual underscore-separated parameter names, such as `storage_fullText_maxRawHitsPerRequest`. See [full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging) for costs and failure recovery. + - `writeAsync` — Disable disk sync for higher throughput (**disables durability guarantees**); _Default_: `false` - `caching` — In-memory record caching; _Default_: `true` - `compression` — LZ4 record compression; _Default_: `true` (enabled by default since v4.3.0). Sub-options: `dictionary`, `threshold` diff --git a/reference/configuration/overview.md b/reference/configuration/overview.md index 112327b24..309ef0726 100644 --- a/reference/configuration/overview.md +++ b/reference/configuration/overview.md @@ -21,7 +21,7 @@ For a complete reference of all available configuration options, see [Configurat ## The Configuration File -To change a configuration value, edit `harper-config.yaml` and save. **Harper must be restarted for changes to take effect.** +To change a configuration value, edit `harper-config.yaml` and save. **Restart Harper to apply file edits.** To change full-text request work limits without a restart, use [`set_configuration`](./operations.md#set-configuration). Native full-text runtime budgets still require a full process restart. Configuration keys use camelCase (e.g. `operationsApi`). Nested keys use dot notation conceptually (e.g. `operationsApi.network.port`). diff --git a/reference/operations-api/operations.md b/reference/operations-api/operations.md index 865848d90..b0179399a 100644 --- a/reference/operations-api/operations.md +++ b/reference/operations-api/operations.md @@ -270,7 +270,7 @@ Returns records with a matching value on any attribute. Supports wildcards (e.g. ### `search_by_conditions` -Returns records matching one or more conditions. Supports `operator` (`and`/`or`), `offset`, `limit`, nested `conditions` groups, and `sort` with multi-level tie-breaking. +Returns records matching one or more conditions. Supports `operator` (`and`/`or`), `offset`, `limit`, nested `conditions` groups, and `sort` with multi-level tie-breaking. Full-text conditions use comparators such as `matches` and `matches_prefix`; see [full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging) for paging costs and limits. ```json { @@ -1686,7 +1686,7 @@ Detailed documentation: [Configuration Overview](../configuration/overview.md) ### `set_configuration` -Updates configuration parameters in `harper-config.yaml`. Most changes require a restart (`restart` or `restart_service`) to take effect. Full-text request work limits apply to new searches without a restart; changes to the native process budgets require a full `restart`. See [Full-text paging](../resources/full-text-paging.md#configure-search-work). +Updates configuration parameters in `harper-config.yaml`. Most changes require a restart (`restart` or `restart_service`) to take effect. Full-text request work limits apply to new searches without a restart; changes to the native process budgets require a full `restart`. See [Full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging). Supports `"replicated": true` to apply the same change to all cluster nodes in one call; per-node outcomes are returned in the response's `replicated` array. Only send cluster-appropriate parameters when replicating — node-local parameters (ports, `node.hostname`, file paths, TLS material, `replication.hostname`/`url`/`routes`) would overwrite every peer's local values. To apply the change cluster-wide, follow with `restart_service` using `"replicated": true` (which restarts nodes one at a time); `restart_service` restarts only worker threads, though, so a change to [`threads.v8Flags`](../configuration/options.md#applying-v8-flags) takes effect only after a `restart` sent to each node in turn (`restart` does not replicate). See [Configuration Operations](../configuration/operations.md#set-configuration) for details. diff --git a/reference/resources/full-text-paging.md b/reference/resources/full-text-paging.md deleted file mode 100644 index e2496107f..000000000 --- a/reference/resources/full-text-paging.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -title: Full-text paging ---- - -# Full-text paging - - - -Full-text and autocomplete searches use Harper's existing paging options. Each request runs a new search; Harper does not retain a search between pages. - -## Before you start - -Use an audited RocksDB table with a declared full-text field. For example: - -```graphql -type Product @table(audit: true) @export { - id: ID @primaryKey - title: String - catalogSearch: FullText @fullText(fields: [{ name: "title" }]) -} -``` - -`catalogSearch` names the index over `title`; it is not a stored record value. The examples below search that index through an exported table route or an authenticated Resource handler. - -## Request a page - -Inside an authenticated Resource handler, use `limit` and `offset` with `Table.search()`. `Table.search()` returns an iterable, including when a custom Resource forwards or filters it. - -```javascript -return tables.Product.search( - { - checkPermission: true, - conditions: [{ attribute: 'catalogSearch', comparator: 'matches', value: 'trail shoes' }], - offset: 20, - limit: 20, - }, - this.getContext() -); -``` - -The equivalent REST range has an inclusive start and exclusive end. This request skips 20 matching records and returns up to 20 more: - -```text -GET /Product/?catalogSearch=matches=trail%20shoes&limit(20,40) -``` - -Each page runs a new query. Changes to records, permissions, or the index can move results between requests, so separate pages may omit or repeat records. There is no retained search or cross-request snapshot. - -Within one request, Harper reads native batches on the same index reader until enough eligible records have been found or the matches are exhausted. Current source versions and record filters are checked before counting records toward the offset and limit. This traversal also lets prefix and fuzzy-prefix searches return more than the initial autocomplete batch. The library's ordinary search window is not a total-result cutoff for a bounded Harper request. - -Use a bounded `limit` for large result sets. An unbounded query that exceeds the native result window returns 400. `offset + limit` must not exceed `maxRawHitsPerRequest`; replay and rejected hits can exhaust that budget even earlier. Offset requests revisit earlier matches; deep offsets and restrictive filters can exhaust the work or time budget. Harper can retain up to `offset + limit` accepted records while building a bounded result, so raising the work limit can also increase memory use. An exhausted search returns its remaining eligible records, including an empty result. A budget failure returns an error instead of a successful incomplete page. - -## Configure search work - -Search settings are under `storage.fullText` in `harper-config.yaml`: - -| Setting | Default | Meaning | -| --------------------------- | ------: | ----------------------------------------------------------------------------------------------------------------- | -| `maxRawHitsPerRequest` | 10,000 | Fetched native hits, including the initial batch replay when switching to traversal | -| `requestBudgetMilliseconds` | 5,000 | Execution budget across native batches, source checks and highlight tracing, capped by the native execution limit | - -The hit limit counts fetched hits, not every record Tantivy visits while scoring. Reaching it before the page fills or the search is exhausted returns 400. Exceeding the execution budget returns 503. Narrow the query, reduce the offset, or increase the relevant configured budget. Freshness waiting uses `waitForIndexMilliseconds` separately. Reader acquisition, query construction and some native operations can take additional time; the execution budget is not a hard response-time guarantee. - -Each setting has the usual configuration parameter name, such as `storage_fullText_maxRawHitsPerRequest`, for `set_configuration` and environment overrides. Query work limits apply to new searches after configuration changes. - -## Configure native runtime admission - -Native process admission settings live under `storage.fullText.runtime`: - -| Setting | Default | -| ---------------------- | ------: | -| `maxResidentIndexes` | 128 | -| `maxIndexingThreads` | 128 | -| `maxSearchThreads` | 256 | -| `maxWriterMemoryBytes` | 4 GiB | -| `maxQueuedBytes` | 8 GiB | -| `maxExpensiveSearches` | 8 | - -These limits apply together. For example, two indexing threads and 64 MiB of writer memory per index reach the default thread and memory budgets at 64 indexes. Readers share an index's runtime; they do not allocate another writer. Admission limits are not hard bounds on total resident memory or disk usage. - -Harper captures native runtime budgets before loading application components, or on an earlier main-thread native load or managed worker spawn. Application-created workers inherit that configuration. A worker created before capture fails on its first native full-text load with `FullTextWorkerInitializationError` (500, non-retryable). Create application workers from a Harper application component, after runtime configuration has been captured; recreate an early worker rather than retrying inside it. Replacement workers keep the established budgets after a live configuration change. Restart the Harper process to apply new native runtime budgets; restarting only HTTP workers is insufficient. - -Native page, response-byte and highlight limits still apply. Fulltext 0.6.0 also limits each analyzed query leaf to 64 terms and the compiled query to 256 clauses. Raising Harper's work budgets does not raise those limits. See the [published library limits](https://github.com/HarperFast/fulltext/blob/v0.6.0/docs/limits.md) for the full list and recovery guidance. - -## Upgrade considerations - -Harper 5.4 uses the fulltext 0.6.0 native package. A format-incompatible upgrade rebuilds the local index from stored records; queries on that node return 503 until the index is ready. Full-text indexes require RocksDB and audit logging. - -The initial native autocomplete batch is not a total-result cap. Harper continues through later batches when needed to satisfy the requested page after filtering. Work and time limits still apply. - -Full-text queries do not provide an exact record count. When exact counting is enabled and requested, `Content-Range` uses `*` for the total. Estimated counting can return a planner estimate when no opaque row or vector filter suppresses it. A native hit total is not an authorized record count. See [count preferences](../rest/querying.md#pagination-and-total-count) for the default downgrade from exact to estimated counting. For the ordinary REST paging syntax, see [Querying](../rest/querying.md#pagination-and-total-count). The JSON Operations API uses `search_by_conditions` with `limit` and `offset` rather than REST URL syntax. The same per-request behavior and work limits apply. - -## Errors and recovery - -| Situation | HTTP status | Action | -| --------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------ | -| Fetched-hit budget exhausted | 400 | Narrow the query, reduce the offset, or raise `maxRawHitsPerRequest`. | -| Execution budget exhausted | 503 | Narrow the query or adjust `requestBudgetMilliseconds`; retrying the same expensive query can hit the same limit. | -| Index rebuilding or unavailable | 503 | Wait for the local index to become ready before retrying. | -| Application worker started before native runtime configuration was captured | 500 | `FullTextWorkerInitializationError` is non-retryable in that worker. Recreate the worker after Harper initializes. | - -`INDEX_REBUILDING` and `DERIVED_INDEX_LAGGING` identify retryable readiness or coverage failures. A native busy response also returns 503; reduce concurrency or retry with backoff. HTTP status alone does not distinguish these from an execution-budget failure. diff --git a/reference/resources/query-optimization.md b/reference/resources/query-optimization.md index dfe2cf9d1..9cb42f545 100644 --- a/reference/resources/query-optimization.md +++ b/reference/resources/query-optimization.md @@ -79,6 +79,36 @@ When a secondary index is still being built — for example, just after a new `@ This is a transient condition — the index finishes building in the background, after which the same query succeeds. Clients should retry (with backoff) rather than treating it as an empty result or a permanent failure. On the operations API the error body includes `code` and `retryable`, so callers can branch on `error.code === 'INDEX_REBUILDING'` or `error.retryable`. +## Full-Text Search Work and Paging + + + + + +Full-text and autocomplete queries use Harper's existing `limit` and `offset` options. Each page runs a new query: changed records, permissions or index contents can cause records to repeat or be omitted between pages. There is no retained search across requests. See [REST paging and counts](../rest/querying.md#pagination-and-total-count) for URL syntax and count behavior. + +Within one request, Harper uses one native reader to fetch ranked batches. It checks source versions and record filters before counting records toward the requested offset and limit. Prefix and fuzzy-prefix searches can continue past the initial autocomplete batch. The native search window is not a total-result cutoff for bounded Harper queries. + +Use a bounded `limit` for large result sets. An unbounded search that exceeds the native result window returns an error. `offset + limit` must not exceed `storage.fullText.maxRawHitsPerRequest`; replay and rejected hits can exhaust that budget earlier. Offset requests revisit earlier matches and can retain up to `offset + limit` accepted records. Raising work limits can increase CPU, reads and memory use. + +The fetched-hit budget counts hits returned by the native library, not every record Tantivy visits while scoring. Exhausting that budget throws a 400 error; exhausting the execution budget throws 503. The adapter does not return a shortened result as if the search had completed. If a custom resource has already started streaming a response, a later query error ends that stream instead of changing its HTTP status. An exhausted search returns its remaining eligible records, which may be empty. + +Configure the two work limits and six native process budgets under [`storage.fullText`](../configuration/options.md#storage). Work-limit changes through `set_configuration` apply to new queries. The execution budget is not a hard response-time guarantee: reader acquisition, query construction and some native operations can take additional time. The full-text condition's `waitForIndexMilliseconds` controls freshness waiting separately. + +### Native runtime configuration + +Harper captures native process budgets before loading application components, or on an earlier main-thread native load or managed worker spawn. Application-created workers inherit that configuration. Create workers from a Harper application component after initialization. A worker created before capture fails on its first native full-text load with `FullTextWorkerInitializationError` (500, non-retryable); recreate that worker after Harper initializes. + +Replacement workers keep the captured budgets after a live configuration change. Apply new native runtime budgets with a full Harper process restart; `restart_service` only replaces workers. + +### Readiness and recovery + +Full-text indexes require audit logging. A format-incompatible native upgrade rebuilds the local index from stored records, and queries return 503 while it rebuilds. Harper 5.4 uses `@harperfast/fulltext` 0.6.0. + +`INDEX_REBUILDING` and `DERIVED_INDEX_LAGGING` identify retryable readiness or coverage failures. Retry with backoff while the index makes progress. Lost retained transaction history can require a rebuild; retries alone do not repair an index that cannot catch up. Check index readiness and logs if it stays unavailable. A native busy response also returns 503; reduce concurrency or retry with backoff. For an execution-budget failure, narrow the query, reduce the offset or adjust the work budget instead of repeatedly submitting the same expensive query. + +Native page, response-byte and highlight limits still apply. Fulltext 0.6.0 also limits each analyzed query leaf to 64 terms and the compiled query to 256 clauses. Raising Harper's work budgets does not raise those native limits. + ## Relationships and Joins Harper supports relationship-based queries that join data across tables. See [Schema documentation](../database/schema.md) for how to define relationships. diff --git a/reference/resources/resource-api.md b/reference/resources/resource-api.md index cf44bb715..76767d8e2 100644 --- a/reference/resources/resource-api.md +++ b/reference/resources/resource-api.md @@ -1035,7 +1035,7 @@ Array of condition objects to filter records. Each condition: | `conditions` | Nested conditions array | | `operator` | `and` (default) or `or` for the nested `conditions` | -Full-text conditions use `matches`, `matches_all`, `matches_phrase`, `matches_prefix`, `matches_fuzzy`, or `matches_fuzzy_prefix` against a declared full-text index. See [Full-text paging](./full-text-paging.md) for a `Table.search()` example and work limits. +Full-text conditions use `matches`, `matches_all`, `matches_phrase`, `matches_prefix`, `matches_fuzzy`, or `matches_fuzzy_prefix` against a declared full-text index. See [Full-text query execution](./query-optimization.md#full-text-search-work-and-paging) for a `Table.search()` example and work limits. Example with nested conditions: diff --git a/reference/rest/querying.md b/reference/rest/querying.md index 4ff839e56..af4adf9d9 100644 --- a/reference/rest/querying.md +++ b/reference/rest/querying.md @@ -55,7 +55,7 @@ Harper uses [FIQL](https://datatracker.ietf.org/doc/html/draft-nottingham-atompu | `=`, `===` | Strict equality (no type conversion) | | `!==` | Strict inequality (no type conversion) | -Full-text indexes support `=matches=`, `=matches_all=`, `=matches_phrase=`, `=matches_prefix=`, `=matches_fuzzy=`, and `=matches_fuzzy_prefix=`. These use the full-text index rather than ordinary string comparisons. See [Full-text paging](../resources/full-text-paging.md). +Full-text indexes support `=matches=`, `=matches_all=`, `=matches_phrase=`, `=matches_prefix=`, `=matches_fuzzy=`, and `=matches_fuzzy_prefix=`. These use the full-text index rather than ordinary string comparisons. See [Full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging). **Examples**: @@ -177,7 +177,7 @@ GET /Product/?sort(+rating,-price) ## Pagination and Total Count -Full-text and autocomplete queries use the same paging syntax. See [Full-text paging](../resources/full-text-paging.md) for search work limits and behavior when records change between requests. +Full-text and autocomplete queries use the same paging syntax. See [Full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging) for search work limits and behavior when records change between requests. @@ -222,7 +222,7 @@ The response status is always `200` — `Content-Range` is informational (Harper The total is reported as `*` (for example `Content-Range: items 0-24/*`) when it cannot be produced — an exact scan that hits its guardrail (counting the tail past the requested page is bounded by a 1,000,000-row cap and a ~1-second budget, and abandons the total rather than truncating the page), or a query with no cardinality estimate (for example a `!=` or `=ct=` (contains) condition). `Preference-Applied` still echoes the requested mode, so an unavailable total (`.../*`) is distinct from a request that asked for no count. -Full-text and autocomplete queries cannot provide an exact total. An estimated count may use the planner estimate when the query has no opaque row or vector filter. See [Full-text paging](../resources/full-text-paging.md#request-a-page). +Full-text and autocomplete queries cannot provide an exact total. An estimated count may use the planner estimate when the query has no opaque row or vector filter. See [Full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging). ### Enabling exact counts diff --git a/release-notes/v5-lincoln/5.4.md b/release-notes/v5-lincoln/5.4.md index 122cf5546..b0b203621 100644 --- a/release-notes/v5-lincoln/5.4.md +++ b/release-notes/v5-lincoln/5.4.md @@ -70,4 +70,4 @@ The new `threads.v8Flags` setting takes a V8 flag, or a list of them, and applie Full-text and autocomplete queries use the existing `limit` and `offset` paging options. Harper reads additional native batches when needed after filtering, so the initial native result window no longer silently cuts off eligible results. The new `storage.fullText.maxRawHitsPerRequest` and `storage.fullText.requestBudgetMilliseconds` settings bound request work. Requests that exceed those budgets return an error. Separate page requests run independently and can see changes to records or index contents. -See [Full-text paging](/reference/v5/resources/full-text-paging) for examples, limits and upgrade considerations. +See [Full-text query execution](/reference/v5/resources/query-optimization#full-text-search-work-and-paging) and [storage configuration](/reference/v5/configuration/options#storage) for limits, restart requirements and recovery guidance. diff --git a/sidebarsReference.ts b/sidebarsReference.ts index a2306e408..f997c1810 100644 --- a/sidebarsReference.ts +++ b/sidebarsReference.ts @@ -87,7 +87,6 @@ const sidebars: SidebarsConfig = { id: 'resources/query-optimization', label: 'Query Optimization', }, - { type: 'doc', id: 'resources/full-text-paging', label: 'Full-text Paging' }, ], }, { From f5f9a01feaee19f1f88168fc43ae464d8e154125 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 22:35:36 -0600 Subject: [PATCH 35/51] Document independent full-text result and work budgets --- reference/configuration/operations.md | 2 +- reference/configuration/options.md | 3 ++- reference/resources/query-optimization.md | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/reference/configuration/operations.md b/reference/configuration/operations.md index 177935496..eb36e0c23 100644 --- a/reference/configuration/operations.md +++ b/reference/configuration/operations.md @@ -18,7 +18,7 @@ For the full list of configurable options, see [Configuration Options](./options Modifies one or more Harper configuration parameters. **Most changes require a [restart](../operations-api/operations.md#restart) or [restart_service](../operations-api/operations.md#restart_service) to take effect** (a [restart](../operations-api/operations.md#restart) for `threads.v8Flags`). A change to `replication.threads` takes effect only on a full `restart`. -Full-text request work limits (`storage.fullText.maxRawHitsPerRequest` and `storage.fullText.requestBudgetMilliseconds`) apply to new searches without a restart. Changes to `storage.fullText.runtime` require a full Harper process restart; `restart_service` does not replace the native process budget. See [Full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging). +Full-text request work limits (`storage.fullText.maxRawHitsPerRequest`, `storage.fullText.maxResultsPerRequest` and `storage.fullText.requestBudgetMilliseconds`) apply to new searches without a restart. Changes to `storage.fullText.runtime` require a full Harper process restart; `restart_service` does not replace the native process budget. See [Full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging). `operation` _(required)_ — must be `set_configuration` diff --git a/reference/configuration/options.md b/reference/configuration/options.md index 7ab42326b..5db0d4fd8 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -313,7 +313,8 @@ storage: - `engine` — The database storage engine to use for new databases. Currently supported engines are `rocksdb` and `lmdb`. The default is `rocksdb`. Existing databases will use the engine that was used when they were created. - `fullText` — Full-text search work and native process limits. Sub-options: - - `maxRawHitsPerRequest` — Maximum fetched hits per request, including replay when switching to native traversal; default `10000`. + - `maxRawHitsPerRequest` — Maximum fetched hits per request; default `10000`. + - `maxResultsPerRequest` — Maximum retained offset plus limit, independent of fetched-hit work; default `10000`. - `requestBudgetMilliseconds` — Execution budget across native batches, source checks and highlights, capped by the native execution limit; default `5000` ms. Reader acquisition and freshness waiting have separate bounds. - `runtime.maxResidentIndexes` — Maximum resident native indexes; default `128`. - `runtime.maxIndexingThreads` — Maximum native indexing threads; default `128`. diff --git a/reference/resources/query-optimization.md b/reference/resources/query-optimization.md index 9cb42f545..253f65b54 100644 --- a/reference/resources/query-optimization.md +++ b/reference/resources/query-optimization.md @@ -89,11 +89,11 @@ Full-text and autocomplete queries use Harper's existing `limit` and `offset` op Within one request, Harper uses one native reader to fetch ranked batches. It checks source versions and record filters before counting records toward the requested offset and limit. Prefix and fuzzy-prefix searches can continue past the initial autocomplete batch. The native search window is not a total-result cutoff for bounded Harper queries. -Use a bounded `limit` for large result sets. An unbounded search that exceeds the native result window returns an error. `offset + limit` must not exceed `storage.fullText.maxRawHitsPerRequest`; replay and rejected hits can exhaust that budget earlier. Offset requests revisit earlier matches and can retain up to `offset + limit` accepted records. Raising work limits can increase CPU, reads and memory use. +Use a bounded `limit` for large result sets. An unbounded search that exceeds the native result window returns an error. `offset + limit` must not exceed either `storage.fullText.maxResultsPerRequest` or `storage.fullText.maxRawHitsPerRequest`; rejected hits can exhaust the raw-work budget earlier. Raising only the raw-hit budget allows more filtering work without raising the retained-record target. Offset requests revisit earlier matches and can retain up to `offset + limit` accepted records. Raising work limits can increase CPU, reads and memory use. The fetched-hit budget counts hits returned by the native library, not every record Tantivy visits while scoring. Exhausting that budget throws a 400 error; exhausting the execution budget throws 503. The adapter does not return a shortened result as if the search had completed. If a custom resource has already started streaming a response, a later query error ends that stream instead of changing its HTTP status. An exhausted search returns its remaining eligible records, which may be empty. -Configure the two work limits and six native process budgets under [`storage.fullText`](../configuration/options.md#storage). Work-limit changes through `set_configuration` apply to new queries. The execution budget is not a hard response-time guarantee: reader acquisition, query construction and some native operations can take additional time. The full-text condition's `waitForIndexMilliseconds` controls freshness waiting separately. +Configure the three request limits and six native process budgets under [`storage.fullText`](../configuration/options.md#storage). Work-limit changes through `set_configuration` apply to new queries. The execution budget is not a hard response-time guarantee: reader acquisition, query construction and some native operations can take additional time. The full-text condition's `waitForIndexMilliseconds` controls freshness waiting separately. ### Native runtime configuration From 0444a14415fd967df8f40cbbc9498399e50db269 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 22:43:05 -0600 Subject: [PATCH 36/51] Clarify query rejection and documentation pointers --- reference/resources/query-optimization.md | 6 +++--- reference/resources/resource-api.md | 2 +- reference/rest/querying.md | 6 ++++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/reference/resources/query-optimization.md b/reference/resources/query-optimization.md index 253f65b54..3aba628f9 100644 --- a/reference/resources/query-optimization.md +++ b/reference/resources/query-optimization.md @@ -87,9 +87,9 @@ This is a transient condition — the index finishes building in the background, Full-text and autocomplete queries use Harper's existing `limit` and `offset` options. Each page runs a new query: changed records, permissions or index contents can cause records to repeat or be omitted between pages. There is no retained search across requests. See [REST paging and counts](../rest/querying.md#pagination-and-total-count) for URL syntax and count behavior. -Within one request, Harper uses one native reader to fetch ranked batches. It checks source versions and record filters before counting records toward the requested offset and limit. Prefix and fuzzy-prefix searches can continue past the initial autocomplete batch. The native search window is not a total-result cutoff for bounded Harper queries. +Within one request, Harper uses one native reader to fetch ranked batches. It checks source versions and record filters before counting records toward the requested offset and limit. Prefix and fuzzy-prefix searches can return more than the native ordinary autocomplete window. The native search window is not a total-result cutoff for bounded Harper queries. -Use a bounded `limit` for large result sets. An unbounded search that exceeds the native result window returns an error. `offset + limit` must not exceed either `storage.fullText.maxResultsPerRequest` or `storage.fullText.maxRawHitsPerRequest`; rejected hits can exhaust the raw-work budget earlier. Raising only the raw-hit budget allows more filtering work without raising the retained-record target. Offset requests revisit earlier matches and can retain up to `offset + limit` accepted records. Raising work limits can increase CPU, reads and memory use. +Use a bounded `limit` for large result sets. An unbounded search that exceeds its native window or configured result/work budget throws 400; use a bounded query. `offset + limit` must not exceed either `storage.fullText.maxResultsPerRequest` or `storage.fullText.maxRawHitsPerRequest`; rejected hits can exhaust the raw-work budget earlier. Raising only the raw-hit budget allows more filtering work without raising the retained-record target. Offset requests revisit earlier matches and can retain up to `offset + limit` accepted records. Raising work limits can increase CPU, reads and memory use. The fetched-hit budget counts hits returned by the native library, not every record Tantivy visits while scoring. Exhausting that budget throws a 400 error; exhausting the execution budget throws 503. The adapter does not return a shortened result as if the search had completed. If a custom resource has already started streaming a response, a later query error ends that stream instead of changing its HTTP status. An exhausted search returns its remaining eligible records, which may be empty. @@ -105,7 +105,7 @@ Replacement workers keep the captured budgets after a live configuration change. Full-text indexes require audit logging. A format-incompatible native upgrade rebuilds the local index from stored records, and queries return 503 while it rebuilds. Harper 5.4 uses `@harperfast/fulltext` 0.6.0. -`INDEX_REBUILDING` and `DERIVED_INDEX_LAGGING` identify retryable readiness or coverage failures. Retry with backoff while the index makes progress. Lost retained transaction history can require a rebuild; retries alone do not repair an index that cannot catch up. Check index readiness and logs if it stays unavailable. A native busy response also returns 503; reduce concurrency or retry with backoff. For an execution-budget failure, narrow the query, reduce the offset or adjust the work budget instead of repeatedly submitting the same expensive query. +`INDEX_REBUILDING` and `DERIVED_INDEX_LAGGING` identify retryable readiness or coverage failures. Retry with backoff while the index makes progress. Transaction history retained under [`logging.auditRetention`](../logging/configuration.md#loggingauditretention) (default `3d`) is used for catch-up. Lost history can require a rebuild; retries alone do not repair an index that cannot catch up. Check index readiness and logs if it stays unavailable. Budget and native-busy failures do not carry those readiness codes. A native busy response also returns 503; reduce concurrency or retry with backoff. For an execution-budget failure, narrow the query, reduce the offset or adjust the work budget instead of repeatedly submitting the same expensive query. Native page, response-byte and highlight limits still apply. Fulltext 0.6.0 also limits each analyzed query leaf to 64 terms and the compiled query to 256 clauses. Raising Harper's work budgets does not raise those native limits. diff --git a/reference/resources/resource-api.md b/reference/resources/resource-api.md index 76767d8e2..202bf521c 100644 --- a/reference/resources/resource-api.md +++ b/reference/resources/resource-api.md @@ -1035,7 +1035,7 @@ Array of condition objects to filter records. Each condition: | `conditions` | Nested conditions array | | `operator` | `and` (default) or `or` for the nested `conditions` | -Full-text conditions use `matches`, `matches_all`, `matches_phrase`, `matches_prefix`, `matches_fuzzy`, or `matches_fuzzy_prefix` against a declared full-text index. See [Full-text query execution](./query-optimization.md#full-text-search-work-and-paging) for a `Table.search()` example and work limits. +Full-text conditions use `matches`, `matches_all`, `matches_phrase`, `matches_prefix`, `matches_fuzzy`, or `matches_fuzzy_prefix` against a declared full-text index. See [Full-text query execution](./query-optimization.md#full-text-search-work-and-paging) for paging costs and work limits. Example with nested conditions: diff --git a/reference/rest/querying.md b/reference/rest/querying.md index af4adf9d9..44201c153 100644 --- a/reference/rest/querying.md +++ b/reference/rest/querying.md @@ -177,14 +177,16 @@ GET /Product/?sort(+rating,-price) ## Pagination and Total Count -Full-text and autocomplete queries use the same paging syntax. See [Full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging) for search work limits and behavior when records change between requests. - Use `limit(start,end)` to page through a collection, and opt in to a total match count with the `Prefer` request header (`Prefer: count=exact`) so a client can render pagination (for example "1-25 of 1,234") without a second request. Counting is opt-in: without the header, no count is computed and no count headers are returned. It applies only to `GET`/`HEAD` requests and requires a bounded page — the `limit()` must be a non-negative integer no larger than **10,000**, and the requested window (offset + limit) no larger than **1,000,000**. A request with no `limit()`, one outside those bounds, or a non-numeric one is served normally with no count headers, since counting an unbounded page would defeat the point of paging. +Full-text and autocomplete queries use the same paging syntax. See [Full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging) for search work limits and behavior when records change between requests. + +The count-header guardrails do not override query-specific limits such as the full-text work budget. + ### Requesting a count Send a `Prefer` header on a `GET` (or `HEAD`) request to a collection: From a5f332a64f99ab66c0923334d139a161af0877d5 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 22:48:04 -0600 Subject: [PATCH 37/51] Account for bounded replay in the raw-hit budget --- reference/configuration/options.md | 2 +- reference/resources/query-optimization.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index 5db0d4fd8..d8cccbebb 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -313,7 +313,7 @@ storage: - `engine` — The database storage engine to use for new databases. Currently supported engines are `rocksdb` and `lmdb`. The default is `rocksdb`. Existing databases will use the engine that was used when they were created. - `fullText` — Full-text search work and native process limits. Sub-options: - - `maxRawHitsPerRequest` — Maximum fetched hits per request; default `10000`. + - `maxRawHitsPerRequest` — Maximum fetched hits per request, including bounded prefix replay when traversal is needed; default `10000`. - `maxResultsPerRequest` — Maximum retained offset plus limit, independent of fetched-hit work; default `10000`. - `requestBudgetMilliseconds` — Execution budget across native batches, source checks and highlights, capped by the native execution limit; default `5000` ms. Reader acquisition and freshness waiting have separate bounds. - `runtime.maxResidentIndexes` — Maximum resident native indexes; default `128`. diff --git a/reference/resources/query-optimization.md b/reference/resources/query-optimization.md index 3aba628f9..c01f6a739 100644 --- a/reference/resources/query-optimization.md +++ b/reference/resources/query-optimization.md @@ -89,9 +89,9 @@ Full-text and autocomplete queries use Harper's existing `limit` and `offset` op Within one request, Harper uses one native reader to fetch ranked batches. It checks source versions and record filters before counting records toward the requested offset and limit. Prefix and fuzzy-prefix searches can return more than the native ordinary autocomplete window. The native search window is not a total-result cutoff for bounded Harper queries. -Use a bounded `limit` for large result sets. An unbounded search that exceeds its native window or configured result/work budget throws 400; use a bounded query. `offset + limit` must not exceed either `storage.fullText.maxResultsPerRequest` or `storage.fullText.maxRawHitsPerRequest`; rejected hits can exhaust the raw-work budget earlier. Raising only the raw-hit budget allows more filtering work without raising the retained-record target. Offset requests revisit earlier matches and can retain up to `offset + limit` accepted records. Raising work limits can increase CPU, reads and memory use. +Use a bounded `limit` for large result sets. An unbounded search that exceeds its native window or configured result/work budget throws 400; use a bounded query. `offset + limit` must not exceed either `storage.fullText.maxResultsPerRequest` or `storage.fullText.maxRawHitsPerRequest`; replayed or rejected hits can exhaust the raw-work budget earlier. Raising only the raw-hit budget allows more filtering work without raising the retained-record target. Offset requests revisit earlier matches and can retain up to `offset + limit` accepted records. Raising work limits can increase CPU, reads and memory use. -The fetched-hit budget counts hits returned by the native library, not every record Tantivy visits while scoring. Exhausting that budget throws a 400 error; exhausting the execution budget throws 503. The adapter does not return a shortened result as if the search had completed. If a custom resource has already started streaming a response, a later query error ends that stream instead of changing its HTTP status. An exhausted search returns its remaining eligible records, which may be empty. +The fetched-hit budget includes the initial batch replay when Harper switches to native traversal. It counts hits returned by the native library, not every record Tantivy visits while scoring. Exhausting that budget throws a 400 error; exhausting the execution budget throws 503. The adapter does not return a shortened result as if the search had completed. If a custom resource has already started streaming a response, a later query error ends that stream instead of changing its HTTP status. An exhausted search returns its remaining eligible records, which may be empty. Configure the three request limits and six native process budgets under [`storage.fullText`](../configuration/options.md#storage). Work-limit changes through `set_configuration` apply to new queries. The execution budget is not a hard response-time guarantee: reader acquisition, query construction and some native operations can take additional time. The full-text condition's `waitForIndexMilliseconds` controls freshness waiting separately. From 481dfb09d9b327fbd9f728a0c26fe3a3052c62fc Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 22:49:33 -0600 Subject: [PATCH 38/51] Make full-text failure recovery and native bounds explicit --- reference/configuration/operations.md | 4 +++- reference/resources/query-optimization.md | 12 ++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/reference/configuration/operations.md b/reference/configuration/operations.md index eb36e0c23..11c0dee50 100644 --- a/reference/configuration/operations.md +++ b/reference/configuration/operations.md @@ -44,10 +44,12 @@ A replicated call sends the exact same values to every node. Do not include node ```json { - "message": "Configuration successfully set. You must restart HarperDB for new config settings to take effect." + "message": "Configuration successfully set. You must restart Harper for new config settings to take effect." } ``` +The response uses a generic restart message. The full-text request limits listed above apply to new searches without a restart. + ### Replicated body ```json diff --git a/reference/resources/query-optimization.md b/reference/resources/query-optimization.md index c01f6a739..64229296b 100644 --- a/reference/resources/query-optimization.md +++ b/reference/resources/query-optimization.md @@ -93,7 +93,7 @@ Use a bounded `limit` for large result sets. An unbounded search that exceeds it The fetched-hit budget includes the initial batch replay when Harper switches to native traversal. It counts hits returned by the native library, not every record Tantivy visits while scoring. Exhausting that budget throws a 400 error; exhausting the execution budget throws 503. The adapter does not return a shortened result as if the search had completed. If a custom resource has already started streaming a response, a later query error ends that stream instead of changing its HTTP status. An exhausted search returns its remaining eligible records, which may be empty. -Configure the three request limits and six native process budgets under [`storage.fullText`](../configuration/options.md#storage). Work-limit changes through `set_configuration` apply to new queries. The execution budget is not a hard response-time guarantee: reader acquisition, query construction and some native operations can take additional time. The full-text condition's `waitForIndexMilliseconds` controls freshness waiting separately. +Configure the three request limits and six native process budgets under [`storage.fullText`](../configuration/options.md#storage). Work-limit changes through `set_configuration` apply to new queries. The execution budget is not a hard response-time guarantee: reader acquisition, query construction and some native operations can take additional time. Freshness waiting and reader acquisition use separate bounds. ### Native runtime configuration @@ -105,9 +105,17 @@ Replacement workers keep the captured budgets after a live configuration change. Full-text indexes require audit logging. A format-incompatible native upgrade rebuilds the local index from stored records, and queries return 503 while it rebuilds. Harper 5.4 uses `@harperfast/fulltext` 0.6.0. +| Failure | HTTP status | Code and retry guidance | +| -------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------- | +| Raw-hit or retained-result budget | 400 | No readiness code. Narrow the query or adjust the named budget. | +| Execution budget | 503 | No readiness code. Narrow the query; the same expensive query can fail again. | +| Native busy | 503 | No readiness code. Reduce concurrency or retry with backoff. | +| Index readiness or coverage | 503 | `INDEX_REBUILDING` or `DERIVED_INDEX_LAGGING`, with `retryable: true`. | +| Worker loaded native search before runtime capture | 500 | `FullTextWorkerInitializationError`, with `retryable: false`; recreate that worker after initialization. | + `INDEX_REBUILDING` and `DERIVED_INDEX_LAGGING` identify retryable readiness or coverage failures. Retry with backoff while the index makes progress. Transaction history retained under [`logging.auditRetention`](../logging/configuration.md#loggingauditretention) (default `3d`) is used for catch-up. Lost history can require a rebuild; retries alone do not repair an index that cannot catch up. Check index readiness and logs if it stays unavailable. Budget and native-busy failures do not carry those readiness codes. A native busy response also returns 503; reduce concurrency or retry with backoff. For an execution-budget failure, narrow the query, reduce the offset or adjust the work budget instead of repeatedly submitting the same expensive query. -Native page, response-byte and highlight limits still apply. Fulltext 0.6.0 also limits each analyzed query leaf to 64 terms and the compiled query to 256 clauses. Raising Harper's work budgets does not raise those native limits. +Native page, response-byte and highlight limits still apply. Fulltext 0.6.0 also limits each analyzed query leaf to 64 terms and the compiled query to 256 clauses. Raising Harper's work budgets does not raise those native limits. See the [published native limits](https://github.com/HarperFast/fulltext/blob/v0.6.0/docs/limits.md) for page, byte and highlight bounds. ## Relationships and Joins From af0ee1a6130703f23bb27129d73e9369759fbf63 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 22:56:32 -0600 Subject: [PATCH 39/51] Clarify both request ceilings and link storage tuning guidance --- reference/configuration/options.md | 2 +- reference/database/storage-tuning.md | 6 ++++++ reference/resources/query-optimization.md | 2 +- release-notes/v5-lincoln/5.4.md | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index d8cccbebb..5e3424d78 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -314,7 +314,7 @@ storage: - `engine` — The database storage engine to use for new databases. Currently supported engines are `rocksdb` and `lmdb`. The default is `rocksdb`. Existing databases will use the engine that was used when they were created. - `fullText` — Full-text search work and native process limits. Sub-options: - `maxRawHitsPerRequest` — Maximum fetched hits per request, including bounded prefix replay when traversal is needed; default `10000`. - - `maxResultsPerRequest` — Maximum retained offset plus limit, independent of fetched-hit work; default `10000`. + - `maxResultsPerRequest` — Maximum retained offset plus limit. A request must also fit the separate raw-hit budget; default `10000`. - `requestBudgetMilliseconds` — Execution budget across native batches, source checks and highlights, capped by the native execution limit; default `5000` ms. Reader acquisition and freshness waiting have separate bounds. - `runtime.maxResidentIndexes` — Maximum resident native indexes; default `128`. - `runtime.maxIndexingThreads` — Maximum native indexing threads; default `128`. diff --git a/reference/database/storage-tuning.md b/reference/database/storage-tuning.md index 9cade5b28..1a0b6fd32 100644 --- a/reference/database/storage-tuning.md +++ b/reference/database/storage-tuning.md @@ -403,3 +403,9 @@ storage: - [Compaction](./compaction.md) — reclaiming space inside existing database files - [Resource API — `sourcedFrom`](../resources/resource-api.md#sourcedfromresource-options) — caching tables that interact with reclamation - [Database API — `createBlob`](./api.md) — creating blobs that live under `blobPaths` + +### `storage.fullText` + +Type: `object` + +The [storage option reference](../configuration/options.md#storage) lists the full-text request and native process defaults. Raise the fetched-hit budget to allow more filtering work; raising it does not raise the separate retained-result limit. Larger limits can increase CPU, source reads and memory use for every permitted query. Native process budgets also share capacity across indexes, so tune them with concurrent workloads rather than a single query. See [full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging) for paging costs, restart requirements and failure recovery. diff --git a/reference/resources/query-optimization.md b/reference/resources/query-optimization.md index 64229296b..2a934ab72 100644 --- a/reference/resources/query-optimization.md +++ b/reference/resources/query-optimization.md @@ -91,7 +91,7 @@ Within one request, Harper uses one native reader to fetch ranked batches. It ch Use a bounded `limit` for large result sets. An unbounded search that exceeds its native window or configured result/work budget throws 400; use a bounded query. `offset + limit` must not exceed either `storage.fullText.maxResultsPerRequest` or `storage.fullText.maxRawHitsPerRequest`; replayed or rejected hits can exhaust the raw-work budget earlier. Raising only the raw-hit budget allows more filtering work without raising the retained-record target. Offset requests revisit earlier matches and can retain up to `offset + limit` accepted records. Raising work limits can increase CPU, reads and memory use. -The fetched-hit budget includes the initial batch replay when Harper switches to native traversal. It counts hits returned by the native library, not every record Tantivy visits while scoring. Exhausting that budget throws a 400 error; exhausting the execution budget throws 503. The adapter does not return a shortened result as if the search had completed. If a custom resource has already started streaming a response, a later query error ends that stream instead of changing its HTTP status. An exhausted search returns its remaining eligible records, which may be empty. +The fetched-hit budget includes the initial batch replay when Harper switches to native traversal. It counts hits returned by the native library, not every record Tantivy visits while scoring. Exhausting that budget throws a 400 error; exhausting the execution budget throws 503. The adapter does not return a shortened result as if the search had completed. If a custom resource has already started streaming a response, a later query error ends that stream instead of changing its HTTP status. When there are no further native matches, Harper returns the eligible records found, which may be an empty result. This differs from reaching a budget while more matches remain. Configure the three request limits and six native process budgets under [`storage.fullText`](../configuration/options.md#storage). Work-limit changes through `set_configuration` apply to new queries. The execution budget is not a hard response-time guarantee: reader acquisition, query construction and some native operations can take additional time. Freshness waiting and reader acquisition use separate bounds. diff --git a/release-notes/v5-lincoln/5.4.md b/release-notes/v5-lincoln/5.4.md index b0b203621..cd676e60a 100644 --- a/release-notes/v5-lincoln/5.4.md +++ b/release-notes/v5-lincoln/5.4.md @@ -68,6 +68,6 @@ The new `threads.v8Flags` setting takes a V8 flag, or a list of them, and applie ### Paging and Search Work Limits -Full-text and autocomplete queries use the existing `limit` and `offset` paging options. Harper reads additional native batches when needed after filtering, so the initial native result window no longer silently cuts off eligible results. The new `storage.fullText.maxRawHitsPerRequest` and `storage.fullText.requestBudgetMilliseconds` settings bound request work. Requests that exceed those budgets return an error. Separate page requests run independently and can see changes to records or index contents. +Full-text and autocomplete queries use the existing `limit` and `offset` paging options. Harper reads additional native batches when needed after filtering, so the initial native result window no longer silently cuts off eligible results. The new `storage.fullText.maxRawHitsPerRequest`, `storage.fullText.maxResultsPerRequest` and `storage.fullText.requestBudgetMilliseconds` settings bound fetched hits, retained results and execution time. Requests that exceed those budgets return an error. Separate page requests run independently and can see changes to records or index contents. See [Full-text query execution](/reference/v5/resources/query-optimization#full-text-search-work-and-paging) and [storage configuration](/reference/v5/configuration/options#storage) for limits, restart requirements and recovery guidance. From 518f4ff7018b1439e8622c8eb5860f549257518e Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 22:57:00 -0600 Subject: [PATCH 40/51] Match the documented audit default to the shipped configuration --- reference/configuration/options.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reference/configuration/options.md b/reference/configuration/options.md index 5e3424d78..37da9eeb3 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -232,7 +232,7 @@ logging: level: warn root: ~/hdb/log stdStreams: false - auditLog: false + auditLog: true rotation: interval: 1D maxSize: 100M @@ -244,7 +244,7 @@ logging: - `path` — Explicit log file path (overrides `root`) - `stdStreams` — Write to stdout/stderr; _Default_: `false` - `console` — Include `console.*` output; _Default_: `true` -- `auditLog` — Enable table transaction audit logging; _Default_: `false` +- `auditLog` — Enable table transaction audit logging; _Default_: `true` - `auditRetention` — Audit log retention duration; _Default_: `3d` - `external` — Logging for components using the logger API; sub-options: `level`, `path` - `rotation.enabled` / `rotation.compress` / `rotation.interval` / `rotation.maxSize` / `rotation.path` — Log file rotation (activates when `interval` or `maxSize` is set) From 72f5e4e415070e512b6513eb85d910e3c5b264de Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 20 Oct 2026 23:00:18 -0600 Subject: [PATCH 41/51] Use existing worker restarts to apply full-text request limits --- reference/configuration/operations.md | 4 ++-- reference/configuration/options.md | 2 +- reference/configuration/overview.md | 2 +- reference/operations-api/operations.md | 2 +- reference/resources/query-optimization.md | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/reference/configuration/operations.md b/reference/configuration/operations.md index 11c0dee50..f49beffad 100644 --- a/reference/configuration/operations.md +++ b/reference/configuration/operations.md @@ -18,7 +18,7 @@ For the full list of configurable options, see [Configuration Options](./options Modifies one or more Harper configuration parameters. **Most changes require a [restart](../operations-api/operations.md#restart) or [restart_service](../operations-api/operations.md#restart_service) to take effect** (a [restart](../operations-api/operations.md#restart) for `threads.v8Flags`). A change to `replication.threads` takes effect only on a full `restart`. -Full-text request work limits (`storage.fullText.maxRawHitsPerRequest`, `storage.fullText.maxResultsPerRequest` and `storage.fullText.requestBudgetMilliseconds`) apply to new searches without a restart. Changes to `storage.fullText.runtime` require a full Harper process restart; `restart_service` does not replace the native process budget. See [Full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging). +Full-text request work limits (`storage.fullText.maxRawHitsPerRequest`, `storage.fullText.maxResultsPerRequest` and `storage.fullText.requestBudgetMilliseconds`) take effect in HTTP workers after `restart_service` or a full restart. Changes to `storage.fullText.runtime` require a full Harper process restart; `restart_service` does not replace the native process budget. See [Full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging). `operation` _(required)_ — must be `set_configuration` @@ -48,7 +48,7 @@ A replicated call sends the exact same values to every node. Do not include node } ``` -The response uses a generic restart message. The full-text request limits listed above apply to new searches without a restart. +For full-text request limits, restarting the HTTP workers with `restart_service` is sufficient; the native process budgets require a full restart. ### Replicated body diff --git a/reference/configuration/options.md b/reference/configuration/options.md index 37da9eeb3..97108aab4 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -323,7 +323,7 @@ storage: - `runtime.maxQueuedBytes` — Native queued-write admission budget; default `8589934592` (8 GiB). - `runtime.maxExpensiveSearches` — Maximum concurrent expensive native searches; default `8`. - The runtime budgets apply together and are not hard bounds on total process memory. Work-limit changes through `set_configuration` apply to new searches; runtime-budget changes require a full Harper process restart. Use the usual underscore-separated parameter names, such as `storage_fullText_maxRawHitsPerRequest`. See [full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging) for costs and failure recovery. + The runtime budgets apply together and are not hard bounds on total process memory. After changing request limits through `set_configuration`, use `restart_service` or restart Harper to apply them to HTTP workers. Native runtime-budget changes require a full Harper process restart. Use the usual underscore-separated parameter names, such as `storage_fullText_maxRawHitsPerRequest`. See [full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging) for costs and failure recovery. - `writeAsync` — Disable disk sync for higher throughput (**disables durability guarantees**); _Default_: `false` - `caching` — In-memory record caching; _Default_: `true` diff --git a/reference/configuration/overview.md b/reference/configuration/overview.md index 309ef0726..3358fc228 100644 --- a/reference/configuration/overview.md +++ b/reference/configuration/overview.md @@ -21,7 +21,7 @@ For a complete reference of all available configuration options, see [Configurat ## The Configuration File -To change a configuration value, edit `harper-config.yaml` and save. **Restart Harper to apply file edits.** To change full-text request work limits without a restart, use [`set_configuration`](./operations.md#set-configuration). Native full-text runtime budgets still require a full process restart. +To change a configuration value, edit `harper-config.yaml` and save. **Restart Harper to apply file edits.** For full-text request limits, `restart_service` reloads the HTTP workers. Native full-text runtime budgets require a full process restart. Configuration keys use camelCase (e.g. `operationsApi`). Nested keys use dot notation conceptually (e.g. `operationsApi.network.port`). diff --git a/reference/operations-api/operations.md b/reference/operations-api/operations.md index b0179399a..7a6c80d4a 100644 --- a/reference/operations-api/operations.md +++ b/reference/operations-api/operations.md @@ -1686,7 +1686,7 @@ Detailed documentation: [Configuration Overview](../configuration/overview.md) ### `set_configuration` -Updates configuration parameters in `harper-config.yaml`. Most changes require a restart (`restart` or `restart_service`) to take effect. Full-text request work limits apply to new searches without a restart; changes to the native process budgets require a full `restart`. See [Full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging). +Updates configuration parameters in `harper-config.yaml`. Most changes require a restart (`restart` or `restart_service`) to take effect. Full-text request limits apply to HTTP workers after `restart_service` or a full restart; changes to native process budgets require a full `restart`. See [Full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging). Supports `"replicated": true` to apply the same change to all cluster nodes in one call; per-node outcomes are returned in the response's `replicated` array. Only send cluster-appropriate parameters when replicating — node-local parameters (ports, `node.hostname`, file paths, TLS material, `replication.hostname`/`url`/`routes`) would overwrite every peer's local values. To apply the change cluster-wide, follow with `restart_service` using `"replicated": true` (which restarts nodes one at a time); `restart_service` restarts only worker threads, though, so a change to [`threads.v8Flags`](../configuration/options.md#applying-v8-flags) takes effect only after a `restart` sent to each node in turn (`restart` does not replicate). See [Configuration Operations](../configuration/operations.md#set-configuration) for details. diff --git a/reference/resources/query-optimization.md b/reference/resources/query-optimization.md index 2a934ab72..d86ba21c3 100644 --- a/reference/resources/query-optimization.md +++ b/reference/resources/query-optimization.md @@ -93,7 +93,7 @@ Use a bounded `limit` for large result sets. An unbounded search that exceeds it The fetched-hit budget includes the initial batch replay when Harper switches to native traversal. It counts hits returned by the native library, not every record Tantivy visits while scoring. Exhausting that budget throws a 400 error; exhausting the execution budget throws 503. The adapter does not return a shortened result as if the search had completed. If a custom resource has already started streaming a response, a later query error ends that stream instead of changing its HTTP status. When there are no further native matches, Harper returns the eligible records found, which may be an empty result. This differs from reaching a budget while more matches remain. -Configure the three request limits and six native process budgets under [`storage.fullText`](../configuration/options.md#storage). Work-limit changes through `set_configuration` apply to new queries. The execution budget is not a hard response-time guarantee: reader acquisition, query construction and some native operations can take additional time. Freshness waiting and reader acquisition use separate bounds. +Configure the three request limits and six native process budgets under [`storage.fullText`](../configuration/options.md#storage). After changing request limits through `set_configuration`, use `restart_service` or restart Harper to apply them to HTTP workers. The execution budget is not a hard response-time guarantee: reader acquisition, query construction and some native operations can take additional time. Freshness waiting and reader acquisition use separate bounds. ### Native runtime configuration From ee1862d1f468f035658bfeb64d4181405f459d0a Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Wed, 21 Oct 2026 00:01:37 -0600 Subject: [PATCH 42/51] Clarify full-text admission limits and error recovery --- reference/configuration/operations.md | 2 -- reference/configuration/options.md | 10 +++++----- reference/configuration/overview.md | 2 +- reference/database/storage-tuning.md | 16 ++++++++++------ reference/database/transaction.md | 2 +- reference/operations-api/operations.md | 2 +- reference/resources/query-optimization.md | 12 +++++++++--- 7 files changed, 27 insertions(+), 19 deletions(-) diff --git a/reference/configuration/operations.md b/reference/configuration/operations.md index f49beffad..8da1c0403 100644 --- a/reference/configuration/operations.md +++ b/reference/configuration/operations.md @@ -48,8 +48,6 @@ A replicated call sends the exact same values to every node. Do not include node } ``` -For full-text request limits, restarting the HTTP workers with `restart_service` is sufficient; the native process budgets require a full restart. - ### Replicated body ```json diff --git a/reference/configuration/options.md b/reference/configuration/options.md index 97108aab4..e1fd7cbe7 100644 --- a/reference/configuration/options.md +++ b/reference/configuration/options.md @@ -317,13 +317,13 @@ storage: - `maxResultsPerRequest` — Maximum retained offset plus limit. A request must also fit the separate raw-hit budget; default `10000`. - `requestBudgetMilliseconds` — Execution budget across native batches, source checks and highlights, capped by the native execution limit; default `5000` ms. Reader acquisition and freshness waiting have separate bounds. - `runtime.maxResidentIndexes` — Maximum resident native indexes; default `128`. - - `runtime.maxIndexingThreads` — Maximum native indexing threads; default `128`. - - `runtime.maxSearchThreads` — Maximum native search threads; default `256`. - - `runtime.maxWriterMemoryBytes` — Native writer-memory admission budget; default `4294967296` (4 GiB). - - `runtime.maxQueuedBytes` — Native queued-write admission budget; default `8589934592` (8 GiB). + - `runtime.maxIndexingThreads` — Maximum aggregate indexing-thread reservations across resident native indexes; default `128`. + - `runtime.maxSearchThreads` — Maximum aggregate search-thread reservations across resident native indexes; default `256`. + - `runtime.maxWriterMemoryBytes` — Aggregate writer-memory reservations across resident native indexes; default `4294967296` (4 GiB). + - `runtime.maxQueuedBytes` — Aggregate queued-write reservations across resident native indexes; default `8589934592` (8 GiB). - `runtime.maxExpensiveSearches` — Maximum concurrent expensive native searches; default `8`. - The runtime budgets apply together and are not hard bounds on total process memory. After changing request limits through `set_configuration`, use `restart_service` or restart Harper to apply them to HTTP workers. Native runtime-budget changes require a full Harper process restart. Use the usual underscore-separated parameter names, such as `storage_fullText_maxRawHitsPerRequest`. See [full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging) for costs and failure recovery. + The runtime budgets apply together and are not hard bounds on total process memory. See [Set Configuration](./operations.md#set-configuration) for when request and native runtime changes take effect. Use the usual underscore-separated parameter names, such as `storage_fullText_maxRawHitsPerRequest`. See [full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging) for costs and failure recovery. - `writeAsync` — Disable disk sync for higher throughput (**disables durability guarantees**); _Default_: `false` - `caching` — In-memory record caching; _Default_: `true` diff --git a/reference/configuration/overview.md b/reference/configuration/overview.md index 3358fc228..033c8006c 100644 --- a/reference/configuration/overview.md +++ b/reference/configuration/overview.md @@ -21,7 +21,7 @@ For a complete reference of all available configuration options, see [Configurat ## The Configuration File -To change a configuration value, edit `harper-config.yaml` and save. **Restart Harper to apply file edits.** For full-text request limits, `restart_service` reloads the HTTP workers. Native full-text runtime budgets require a full process restart. +To change a configuration value, edit `harper-config.yaml` and save. **Restart Harper to apply file edits.** See [Set Configuration](./operations.md#set-configuration) for settings that require a full process restart. Configuration keys use camelCase (e.g. `operationsApi`). Nested keys use dot notation conceptually (e.g. `operationsApi.network.port`). diff --git a/reference/database/storage-tuning.md b/reference/database/storage-tuning.md index 1a0b6fd32..8d22dbc8f 100644 --- a/reference/database/storage-tuning.md +++ b/reference/database/storage-tuning.md @@ -395,6 +395,16 @@ storage: compression: true ``` +### `storage.fullText` + + + +Type: `object` + +Defaults: See the [storage option reference](../configuration/options.md#storage). + +The [storage option reference](../configuration/options.md#storage) lists the full-text request and native process defaults. Raise the fetched-hit budget to allow more filtering work; raising it does not raise the separate retained-result limit. Larger limits can increase CPU, source reads and memory use for every permitted query. Native process budgets also share capacity across indexes, so tune them with concurrent workloads rather than a single query. See [full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging) for paging costs, restart requirements and failure recovery. + ## Related - [Configuration Options](../configuration/options.md) — full list of `storage` options @@ -403,9 +413,3 @@ storage: - [Compaction](./compaction.md) — reclaiming space inside existing database files - [Resource API — `sourcedFrom`](../resources/resource-api.md#sourcedfromresource-options) — caching tables that interact with reclamation - [Database API — `createBlob`](./api.md) — creating blobs that live under `blobPaths` - -### `storage.fullText` - -Type: `object` - -The [storage option reference](../configuration/options.md#storage) lists the full-text request and native process defaults. Raise the fetched-hit budget to allow more filtering work; raising it does not raise the separate retained-result limit. Larger limits can increase CPU, source reads and memory use for every permitted query. Native process budgets also share capacity across indexes, so tune them with concurrent workloads rather than a single query. See [full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging) for paging costs, restart requirements and failure recovery. diff --git a/reference/database/transaction.md b/reference/database/transaction.md index baba4612e..82f891561 100644 --- a/reference/database/transaction.md +++ b/reference/database/transaction.md @@ -23,7 +23,7 @@ On RocksDB (the default storage engine) the transaction log is physically a sing The transaction log is **enabled by default**. To disable it, set [`logging.auditLog`](../logging/configuration.md) to `false` in `harper-config.yaml` and restart Harper. -> The transaction log is required for real-time messaging (WebSocket and MQTT subscriptions) and replication. Do not disable it if real-time features or replication are in use. +> The transaction log is required for real-time messaging (WebSocket and MQTT subscriptions), replication and full-text indexing. Do not disable it while any of these features are in use. ## Operations diff --git a/reference/operations-api/operations.md b/reference/operations-api/operations.md index 7a6c80d4a..8a41f2aeb 100644 --- a/reference/operations-api/operations.md +++ b/reference/operations-api/operations.md @@ -1686,7 +1686,7 @@ Detailed documentation: [Configuration Overview](../configuration/overview.md) ### `set_configuration` -Updates configuration parameters in `harper-config.yaml`. Most changes require a restart (`restart` or `restart_service`) to take effect. Full-text request limits apply to HTTP workers after `restart_service` or a full restart; changes to native process budgets require a full `restart`. See [Full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging). +Updates configuration parameters in `harper-config.yaml`. Most changes require a restart (`restart` or `restart_service`) to take effect. See [Set Configuration](../configuration/operations.md#set-configuration) for full-text restart requirements. Supports `"replicated": true` to apply the same change to all cluster nodes in one call; per-node outcomes are returned in the response's `replicated` array. Only send cluster-appropriate parameters when replicating — node-local parameters (ports, `node.hostname`, file paths, TLS material, `replication.hostname`/`url`/`routes`) would overwrite every peer's local values. To apply the change cluster-wide, follow with `restart_service` using `"replicated": true` (which restarts nodes one at a time); `restart_service` restarts only worker threads, though, so a change to [`threads.v8Flags`](../configuration/options.md#applying-v8-flags) takes effect only after a `restart` sent to each node in turn (`restart` does not replicate). See [Configuration Operations](../configuration/operations.md#set-configuration) for details. diff --git a/reference/resources/query-optimization.md b/reference/resources/query-optimization.md index d86ba21c3..6a0e658f1 100644 --- a/reference/resources/query-optimization.md +++ b/reference/resources/query-optimization.md @@ -93,17 +93,17 @@ Use a bounded `limit` for large result sets. An unbounded search that exceeds it The fetched-hit budget includes the initial batch replay when Harper switches to native traversal. It counts hits returned by the native library, not every record Tantivy visits while scoring. Exhausting that budget throws a 400 error; exhausting the execution budget throws 503. The adapter does not return a shortened result as if the search had completed. If a custom resource has already started streaming a response, a later query error ends that stream instead of changing its HTTP status. When there are no further native matches, Harper returns the eligible records found, which may be an empty result. This differs from reaching a budget while more matches remain. -Configure the three request limits and six native process budgets under [`storage.fullText`](../configuration/options.md#storage). After changing request limits through `set_configuration`, use `restart_service` or restart Harper to apply them to HTTP workers. The execution budget is not a hard response-time guarantee: reader acquisition, query construction and some native operations can take additional time. Freshness waiting and reader acquisition use separate bounds. +Configure the three request limits and six native process budgets under [`storage.fullText`](../configuration/options.md#storage). See [Set Configuration](../configuration/operations.md#set-configuration) for restart requirements. The execution budget is not a hard response-time guarantee: reader acquisition, query construction and some native operations can take additional time. Freshness waiting and reader acquisition use separate bounds. ### Native runtime configuration Harper captures native process budgets before loading application components, or on an earlier main-thread native load or managed worker spawn. Application-created workers inherit that configuration. Create workers from a Harper application component after initialization. A worker created before capture fails on its first native full-text load with `FullTextWorkerInitializationError` (500, non-retryable); recreate that worker after Harper initializes. -Replacement workers keep the captured budgets after a live configuration change. Apply new native runtime budgets with a full Harper process restart; `restart_service` only replaces workers. +Replacement workers keep the captured budgets after a configuration change. See [Set Configuration](../configuration/operations.md#set-configuration) before changing these budgets. ### Readiness and recovery -Full-text indexes require audit logging. A format-incompatible native upgrade rebuilds the local index from stored records, and queries return 503 while it rebuilds. Harper 5.4 uses `@harperfast/fulltext` 0.6.0. +Full-text indexes require audit logging; a table declared with `@table(audit: false)` cannot register a full-text index. A format-incompatible native upgrade rebuilds the local index from stored records, and queries return 503 while it rebuilds. Harper 5.4 uses `@harperfast/fulltext` 0.6.0. | Failure | HTTP status | Code and retry guidance | | -------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------- | @@ -113,6 +113,12 @@ Full-text indexes require audit logging. A format-incompatible native upgrade re | Index readiness or coverage | 503 | `INDEX_REBUILDING` or `DERIVED_INDEX_LAGGING`, with `retryable: true`. | | Worker loaded native search before runtime capture | 500 | `FullTextWorkerInitializationError`, with `retryable: false`; recreate that worker after initialization. | +A native result that cannot fit even a single-hit response returns 413. Other native failures and invalid query configuration can return 500; inspect the error and server logs rather than assuming they are retryable. The table lists common failures, not every possible error. + +For read-after-write queries, `maxIndexLagMilliseconds: 0` requires current coverage. `waitForIndexMilliseconds` lets a query wait for coverage (default `0`, maximum `30000` ms); without an override, the allowed index lag is `3000` ms. These options belong to the full-text condition. + +Concurrent requests each have their own work budget. These limits do not cap the number of concurrent Harper queries or prevent brief event-loop delays during synchronous source reads. Measure deep offsets and selective filters under the expected concurrency before raising budgets. + `INDEX_REBUILDING` and `DERIVED_INDEX_LAGGING` identify retryable readiness or coverage failures. Retry with backoff while the index makes progress. Transaction history retained under [`logging.auditRetention`](../logging/configuration.md#loggingauditretention) (default `3d`) is used for catch-up. Lost history can require a rebuild; retries alone do not repair an index that cannot catch up. Check index readiness and logs if it stays unavailable. Budget and native-busy failures do not carry those readiness codes. A native busy response also returns 503; reduce concurrency or retry with backoff. For an execution-budget failure, narrow the query, reduce the offset or adjust the work budget instead of repeatedly submitting the same expensive query. Native page, response-byte and highlight limits still apply. Fulltext 0.6.0 also limits each analyzed query leaf to 64 terms and the compiled query to 256 clauses. Raising Harper's work budgets does not raise those native limits. See the [published native limits](https://github.com/HarperFast/fulltext/blob/v0.6.0/docs/limits.md) for page, byte and highlight bounds. From ab2ae9791eed3b18a485f9fcdc1120e88fbbef6f Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Wed, 21 Oct 2026 07:21:06 -0600 Subject: [PATCH 43/51] Clarify filtered full-text paging and partial-page behavior --- reference/resources/query-optimization.md | 2 ++ reference/rest/querying.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/reference/resources/query-optimization.md b/reference/resources/query-optimization.md index 6a0e658f1..34b31ad2c 100644 --- a/reference/resources/query-optimization.md +++ b/reference/resources/query-optimization.md @@ -89,6 +89,8 @@ Full-text and autocomplete queries use Harper's existing `limit` and `offset` op Within one request, Harper uses one native reader to fetch ranked batches. It checks source versions and record filters before counting records toward the requested offset and limit. Prefix and fuzzy-prefix searches can return more than the native ordinary autocomplete window. The native search window is not a total-result cutoff for bounded Harper queries. +For example, with `offset: 20` and `limit: 10`, Harper skips the first 20 eligible records and returns the next 10. Hits rejected by source-version checks or record filters do not count toward those 30 records, but fetching them still consumes the raw-hit budget. Harper fetches additional batches as needed until it fills the page, exhausts the matches or reaches a request limit. + Use a bounded `limit` for large result sets. An unbounded search that exceeds its native window or configured result/work budget throws 400; use a bounded query. `offset + limit` must not exceed either `storage.fullText.maxResultsPerRequest` or `storage.fullText.maxRawHitsPerRequest`; replayed or rejected hits can exhaust the raw-work budget earlier. Raising only the raw-hit budget allows more filtering work without raising the retained-record target. Offset requests revisit earlier matches and can retain up to `offset + limit` accepted records. Raising work limits can increase CPU, reads and memory use. The fetched-hit budget includes the initial batch replay when Harper switches to native traversal. It counts hits returned by the native library, not every record Tantivy visits while scoring. Exhausting that budget throws a 400 error; exhausting the execution budget throws 503. The adapter does not return a shortened result as if the search had completed. If a custom resource has already started streaming a response, a later query error ends that stream instead of changing its HTTP status. When there are no further native matches, Harper returns the eligible records found, which may be an empty result. This differs from reaching a budget while more matches remain. diff --git a/reference/rest/querying.md b/reference/rest/querying.md index 44201c153..e8674701c 100644 --- a/reference/rest/querying.md +++ b/reference/rest/querying.md @@ -183,7 +183,7 @@ Use `limit(start,end)` to page through a collection, and opt in to a total match Counting is opt-in: without the header, no count is computed and no count headers are returned. It applies only to `GET`/`HEAD` requests and requires a bounded page — the `limit()` must be a non-negative integer no larger than **10,000**, and the requested window (offset + limit) no larger than **1,000,000**. A request with no `limit()`, one outside those bounds, or a non-numeric one is served normally with no count headers, since counting an unbounded page would defeat the point of paging. -Full-text and autocomplete queries use the same paging syntax. See [Full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging) for search work limits and behavior when records change between requests. +Full-text and autocomplete queries use the same paging syntax. The page window counts eligible records after record filtering, rather than raw full-text hits. Harper fetches more ranked batches to fill that window when needed. A page can contain fewer records when no further matches remain; reaching a search work or time limit produces an error instead of a successful partial page. Each page request runs a new query, so concurrent changes can cause repeats or omissions between pages. See [Full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging) for search work limits and recovery guidance. The count-header guardrails do not override query-specific limits such as the full-text work budget. From a95ce001a5b721f8caff9f45fbe9c960a2e577f4 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Wed, 21 Oct 2026 10:33:03 -0600 Subject: [PATCH 44/51] Clarify full-text configuration leaf updates --- reference/configuration/operations.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/reference/configuration/operations.md b/reference/configuration/operations.md index 8da1c0403..c10e66715 100644 --- a/reference/configuration/operations.md +++ b/reference/configuration/operations.md @@ -26,6 +26,8 @@ Full-text request work limits (`storage.fullText.maxRawHitsPerRequest`, `storage Additional properties correspond to configuration keys in underscore-separated format (e.g. `logging_level` for `logging.level`, `clustering_enabled` for `clustering.enabled`). +For full-text settings, use individual parameter names such as `storage_fullText_maxRawHitsPerRequest` or `storage_fullText_runtime_maxSearchThreads`. `set_configuration` does not accept a whole `storage_fullText` object; use `storage.fullText` as the section name only in the YAML configuration file. + :::warning Replicate cluster-appropriate parameters only A replicated call sends the exact same values to every node. Do not include node-local parameters — ports, `node.hostname`, `rootPath`, storage/log file paths, TLS certificate or key paths, or `replication.hostname`/`url`/`routes` — as they would overwrite each peer's local values. ::: From afabe8dccf316e1df679c45516426e8436d5a238 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Wed, 21 Oct 2026 16:50:40 -0600 Subject: [PATCH 45/51] Clarify the pre-v5.4 prefix paging boundary --- reference/resources/query-optimization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference/resources/query-optimization.md b/reference/resources/query-optimization.md index 34b31ad2c..d6a6d3495 100644 --- a/reference/resources/query-optimization.md +++ b/reference/resources/query-optimization.md @@ -87,7 +87,7 @@ This is a transient condition — the index finishes building in the background, Full-text and autocomplete queries use Harper's existing `limit` and `offset` options. Each page runs a new query: changed records, permissions or index contents can cause records to repeat or be omitted between pages. There is no retained search across requests. See [REST paging and counts](../rest/querying.md#pagination-and-total-count) for URL syntax and count behavior. -Within one request, Harper uses one native reader to fetch ranked batches. It checks source versions and record filters before counting records toward the requested offset and limit. Prefix and fuzzy-prefix searches can return more than the native ordinary autocomplete window. The native search window is not a total-result cutoff for bounded Harper queries. +Within one request, Harper uses one native reader to fetch ranked batches. It checks source versions and record filters before counting records toward the requested offset and limit. Prefix and fuzzy-prefix searches can return more than the native ordinary autocomplete window. Before v5.4, prefix and fuzzy-prefix queries restrict `offset + limit` to 100. In v5.4, the native search window is not a total-result cutoff for bounded Harper queries. For example, with `offset: 20` and `limit: 10`, Harper skips the first 20 eligible records and returns the next 10. Hits rejected by source-version checks or record filters do not count toward those 30 records, but fetching them still consumes the raw-hit budget. Harper fetches additional batches as needed until it fills the page, exhausts the matches or reaches a request limit. From d0754c261221d63e7617469ac34ddec966140027 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Wed, 21 Oct 2026 17:17:47 -0600 Subject: [PATCH 46/51] Clarify native response overflow and audit requirements --- reference/database/transaction.md | 2 +- reference/resources/query-optimization.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/reference/database/transaction.md b/reference/database/transaction.md index 82f891561..5bed6afb7 100644 --- a/reference/database/transaction.md +++ b/reference/database/transaction.md @@ -204,7 +204,7 @@ type Dog @table(audit: true) { } ``` -This overrides the [`logging.auditLog`](../logging/configuration.md) global configuration for that specific table. +This overrides the [`logging.auditLog`](../logging/configuration.md) global configuration for that specific table. Keep auditing enabled for tables with full-text indexes: disabling it prevents full-text index registration. ## Related Documentation diff --git a/reference/resources/query-optimization.md b/reference/resources/query-optimization.md index d6a6d3495..bbdd158bc 100644 --- a/reference/resources/query-optimization.md +++ b/reference/resources/query-optimization.md @@ -115,7 +115,7 @@ Full-text indexes require audit logging; a table declared with `@table(audit: fa | Index readiness or coverage | 503 | `INDEX_REBUILDING` or `DERIVED_INDEX_LAGGING`, with `retryable: true`. | | Worker loaded native search before runtime capture | 500 | `FullTextWorkerInitializationError`, with `retryable: false`; recreate that worker after initialization. | -A native result that cannot fit even a single-hit response returns 413. Other native failures and invalid query configuration can return 500; inspect the error and server logs rather than assuming they are retryable. The table lists common failures, not every possible error. +A native result that exceeds the response-byte envelope can return 413, including an oversized ordinary batch even when each individual hit would fit. Other native failures and invalid query configuration can return 500; inspect the error and server logs rather than assuming they are retryable. The table lists common failures, not every possible error. For read-after-write queries, `maxIndexLagMilliseconds: 0` requires current coverage. `waitForIndexMilliseconds` lets a query wait for coverage (default `0`, maximum `30000` ms); without an override, the allowed index lag is `3000` ms. These options belong to the full-text condition. From df5264e6be75e769c8af7bcf1d850d0450c7d5c7 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Wed, 21 Oct 2026 17:47:05 -0600 Subject: [PATCH 47/51] Clarify worker error codes and native capacity recovery --- reference/resources/query-optimization.md | 16 ++++++++-------- release-notes/v5-lincoln/5.4.md | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/reference/resources/query-optimization.md b/reference/resources/query-optimization.md index bbdd158bc..2df0d1870 100644 --- a/reference/resources/query-optimization.md +++ b/reference/resources/query-optimization.md @@ -107,13 +107,13 @@ Replacement workers keep the captured budgets after a configuration change. See Full-text indexes require audit logging; a table declared with `@table(audit: false)` cannot register a full-text index. A format-incompatible native upgrade rebuilds the local index from stored records, and queries return 503 while it rebuilds. Harper 5.4 uses `@harperfast/fulltext` 0.6.0. -| Failure | HTTP status | Code and retry guidance | -| -------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------- | -| Raw-hit or retained-result budget | 400 | No readiness code. Narrow the query or adjust the named budget. | -| Execution budget | 503 | No readiness code. Narrow the query; the same expensive query can fail again. | -| Native busy | 503 | No readiness code. Reduce concurrency or retry with backoff. | -| Index readiness or coverage | 503 | `INDEX_REBUILDING` or `DERIVED_INDEX_LAGGING`, with `retryable: true`. | -| Worker loaded native search before runtime capture | 500 | `FullTextWorkerInitializationError`, with `retryable: false`; recreate that worker after initialization. | +| Failure | HTTP status | Code and retry guidance | +| -------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| Raw-hit or retained-result budget | 400 | No readiness code. Narrow the query or adjust the named budget. | +| Execution budget | 503 | No readiness code. Narrow the query; the same expensive query can fail again. | +| Native busy | 503 | No readiness code. Reduce concurrency or retry with backoff. | +| Index readiness or coverage | 503 | `INDEX_REBUILDING` or `DERIVED_INDEX_LAGGING`, with `retryable: true`. | +| Worker loaded native search before runtime capture | 500 | REST reports `FullTextWorkerInitializationError`; the Operations API can omit that code. Non-retryable; recreate the worker after initialization. | A native result that exceeds the response-byte envelope can return 413, including an oversized ordinary batch even when each individual hit would fit. Other native failures and invalid query configuration can return 500; inspect the error and server logs rather than assuming they are retryable. The table lists common failures, not every possible error. @@ -121,7 +121,7 @@ For read-after-write queries, `maxIndexLagMilliseconds: 0` requires current cove Concurrent requests each have their own work budget. These limits do not cap the number of concurrent Harper queries or prevent brief event-loop delays during synchronous source reads. Measure deep offsets and selective filters under the expected concurrency before raising budgets. -`INDEX_REBUILDING` and `DERIVED_INDEX_LAGGING` identify retryable readiness or coverage failures. Retry with backoff while the index makes progress. Transaction history retained under [`logging.auditRetention`](../logging/configuration.md#loggingauditretention) (default `3d`) is used for catch-up. Lost history can require a rebuild; retries alone do not repair an index that cannot catch up. Check index readiness and logs if it stays unavailable. Budget and native-busy failures do not carry those readiness codes. A native busy response also returns 503; reduce concurrency or retry with backoff. For an execution-budget failure, narrow the query, reduce the offset or adjust the work budget instead of repeatedly submitting the same expensive query. +`INDEX_REBUILDING` and `DERIVED_INDEX_LAGGING` identify retryable readiness or coverage failures. Retry with backoff while the index makes progress. Transaction history retained under [`logging.auditRetention`](../logging/configuration.md#loggingauditretention) (default `3d`) is used for catch-up. Lost history can require a rebuild; retries alone do not repair an index that cannot catch up. Use [`describe_table`](../operations-api/operations.md#describe_table) to inspect `full_text_indexes[].readiness`, including its state and reason. Correct the reported configuration or storage problem before retrying an unavailable index. Budget and native-busy failures do not carry those readiness codes. A native busy response also returns 503; reduce concurrency or retry with backoff. For an execution-budget failure, narrow the query, reduce the offset or adjust the work budget instead of repeatedly submitting the same expensive query. Native page, response-byte and highlight limits still apply. Fulltext 0.6.0 also limits each analyzed query leaf to 64 terms and the compiled query to 256 clauses. Raising Harper's work budgets does not raise those native limits. See the [published native limits](https://github.com/HarperFast/fulltext/blob/v0.6.0/docs/limits.md) for page, byte and highlight bounds. diff --git a/release-notes/v5-lincoln/5.4.md b/release-notes/v5-lincoln/5.4.md index cd676e60a..c5c9f04ab 100644 --- a/release-notes/v5-lincoln/5.4.md +++ b/release-notes/v5-lincoln/5.4.md @@ -68,6 +68,6 @@ The new `threads.v8Flags` setting takes a V8 flag, or a list of them, and applie ### Paging and Search Work Limits -Full-text and autocomplete queries use the existing `limit` and `offset` paging options. Harper reads additional native batches when needed after filtering, so the initial native result window no longer silently cuts off eligible results. The new `storage.fullText.maxRawHitsPerRequest`, `storage.fullText.maxResultsPerRequest` and `storage.fullText.requestBudgetMilliseconds` settings bound fetched hits, retained results and execution time. Requests that exceed those budgets return an error. Separate page requests run independently and can see changes to records or index contents. +Full-text and autocomplete queries use the existing `limit` and `offset` paging options. Harper reads additional native batches when needed after filtering, so the initial native result window no longer silently cuts off eligible results. The new `storage.fullText.maxRawHitsPerRequest`, `storage.fullText.maxResultsPerRequest` and `storage.fullText.requestBudgetMilliseconds` settings bound fetched hits, retained results and execution time. Requests that exceed those budgets return an error. The `storage.fullText.runtime` section also provides process-wide native-capacity budgets; changes to those budgets require a full Harper process restart. Separate page requests run independently and can see changes to records or index contents. See [Full-text query execution](/reference/v5/resources/query-optimization#full-text-search-work-and-paging) and [storage configuration](/reference/v5/configuration/options#storage) for limits, restart requirements and recovery guidance. From f1f55daf9632009b7d39e344487bea7b0c37ecd0 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Sat, 24 Oct 2026 08:14:31 -0600 Subject: [PATCH 48/51] docs: address paging restart and error review feedback --- reference/logging/configuration.md | 6 ++-- reference/operations-api/operations.md | 39 ++++++++++++++++++++--- reference/resources/query-optimization.md | 27 +++++++++------- reference/resources/resource-api.md | 2 +- reference/rest/querying.md | 2 +- 5 files changed, 55 insertions(+), 21 deletions(-) diff --git a/reference/logging/configuration.md b/reference/logging/configuration.md index f204b9325..5ef541370 100644 --- a/reference/logging/configuration.md +++ b/reference/logging/configuration.md @@ -102,11 +102,11 @@ Default: `true` Enables transaction logging. When enabled, Harper records every insert, update, and delete to the database's transaction log. This history is read with the [`read_audit_log`](../database/transaction.md#read_audit_log) operation — a legacy name; there is a single transaction log, not a separate audit table. -See [Database / Transaction Logging](../database/transaction.md) for details on the transaction log. +Full-text indexing requires transaction logging. Disabling this setting prevents new tables from inheriting that prerequisite; a full-text table must also have its per-table audit setting enabled. See [Database / Transaction Logging](../database/transaction.md) for details on the transaction log. ```yaml logging: - auditLog: false + auditLog: true ``` ### `logging.auditRetention` @@ -339,7 +339,7 @@ logging: file: true stdStreams: false console: false - auditLog: false + auditLog: true auditRetention: 3d rotation: enabled: true diff --git a/reference/operations-api/operations.md b/reference/operations-api/operations.md index 8a41f2aeb..56f789b94 100644 --- a/reference/operations-api/operations.md +++ b/reference/operations-api/operations.md @@ -47,7 +47,7 @@ Detailed documentation: [Database Overview](../database/overview.md) ### `describe_all` -Returns the definitions of all databases and tables within the database. Record counts above 5000 records are estimated; the response includes `estimated_record_range` when estimated. To force an exact count (requires full table scan), include `"exact_count": true`. Each table definition includes the [record-structure dictionary fields](#describe_table) described for `describe_table`. +Returns the definitions of all databases and tables within the database. Record counts above 5000 records are estimated; the response includes `estimated_record_range` when estimated. To force an exact count (requires full table scan), include `"exact_count": true`. Each table definition includes the [record-structure dictionary fields](#record-structure-dictionaries) described for `describe_table`. ```json { "operation": "describe_all" } @@ -55,7 +55,7 @@ Returns the definitions of all databases and tables within the database. Record ### `describe_database` -Returns all table definitions within the specified database. Each table definition includes the [record-structure dictionary fields](#describe_table) described for `describe_table`. +Returns all table definitions within the specified database. Each table definition includes the [record-structure dictionary fields](#record-structure-dictionaries) described for `describe_table`. ```json { "operation": "describe_database", "database": "dev" } @@ -63,12 +63,43 @@ Returns all table definitions within the specified database. Each table definiti ### `describe_table` -Returns the definition of a specific table. +Returns the definition of a specific table, including its schema and storage metadata. ```json { "operation": "describe_table", "table": "dog", "database": "dev" } ``` +#### Full-text indexes + + + +A table with native full-text indexes includes `full_text_indexes`; each entry reports its query-only field name, source fields, analyzer options, enabled query modes, and `readiness` (`unknown`, `ready`, `needs-rebuild`, `rebuilding`, or `unavailable`). The readiness object also includes `owner_epoch`, `rebuild_attempts`, and an optional `reason`. Query-only `FullText` fields do not appear in the ordinary `attributes` list. + +Requesting a table with full-text indexes: + +```json +{ "operation": "describe_table", "table": "Product", "database": "catalog" } +``` + +The response also includes the table's ordinary schema and storage metadata. The relevant full-text fragment is: + +```json +{ + "name": "Product", + "full_text_indexes": [ + { + "name": "catalogSearch", + "fields": [{ "name": "name", "weight": 3 }], + "analyzer": "english@2", + "query_modes": ["any", "all", "fuzzy", "phrase", "prefix", "fuzzy-prefix"], + "readiness": { "state": "ready", "owner_epoch": "4", "rebuild_attempts": 0 } + } + ] +} +``` + +#### Record-structure dictionaries + Alongside the schema, the response carries the size of the table's **record-structure dictionaries** @@ -1688,7 +1719,7 @@ Detailed documentation: [Configuration Overview](../configuration/overview.md) Updates configuration parameters in `harper-config.yaml`. Most changes require a restart (`restart` or `restart_service`) to take effect. See [Set Configuration](../configuration/operations.md#set-configuration) for full-text restart requirements. -Supports `"replicated": true` to apply the same change to all cluster nodes in one call; per-node outcomes are returned in the response's `replicated` array. Only send cluster-appropriate parameters when replicating — node-local parameters (ports, `node.hostname`, file paths, TLS material, `replication.hostname`/`url`/`routes`) would overwrite every peer's local values. To apply the change cluster-wide, follow with `restart_service` using `"replicated": true` (which restarts nodes one at a time); `restart_service` restarts only worker threads, though, so a change to [`threads.v8Flags`](../configuration/options.md#applying-v8-flags) takes effect only after a `restart` sent to each node in turn (`restart` does not replicate). See [Configuration Operations](../configuration/operations.md#set-configuration) for details. +Supports `"replicated": true` to apply the same change to all cluster nodes in one call; per-node outcomes are returned in the response's `replicated` array. Only send cluster-appropriate parameters when replicating — node-local parameters (ports, `node.hostname`, file paths, TLS material, `replication.hostname`/`url`/`routes`) would overwrite every peer's local values. To apply the change cluster-wide, follow with `restart_service` using `"replicated": true` (which restarts nodes one at a time); `restart_service` restarts only worker threads, though, so changes to [`threads.v8Flags`](../configuration/options.md#applying-v8-flags) or `storage.fullText.runtime.*` take effect only after a `restart` sent to each node in turn (`restart` does not replicate). See [Configuration Operations](../configuration/operations.md#set-configuration) for details. ```json { diff --git a/reference/resources/query-optimization.md b/reference/resources/query-optimization.md index 2df0d1870..9d837065e 100644 --- a/reference/resources/query-optimization.md +++ b/reference/resources/query-optimization.md @@ -87,11 +87,11 @@ This is a transient condition — the index finishes building in the background, Full-text and autocomplete queries use Harper's existing `limit` and `offset` options. Each page runs a new query: changed records, permissions or index contents can cause records to repeat or be omitted between pages. There is no retained search across requests. See [REST paging and counts](../rest/querying.md#pagination-and-total-count) for URL syntax and count behavior. -Within one request, Harper uses one native reader to fetch ranked batches. It checks source versions and record filters before counting records toward the requested offset and limit. Prefix and fuzzy-prefix searches can return more than the native ordinary autocomplete window. Before v5.4, prefix and fuzzy-prefix queries restrict `offset + limit` to 100. In v5.4, the native search window is not a total-result cutoff for bounded Harper queries. +Within one request, Harper uses one native reader to fetch ranked batches. It checks source versions and record filters before counting records toward the requested offset and limit. In v5.4, prefix and fuzzy-prefix searches can return more than the native ordinary autocomplete window. Before v5.4, prefix and fuzzy-prefix requests whose `offset + limit` exceeded 100 were rejected with 400; requests within that window could still return short pages after filtering. In v5.4, the native search window is not a total-result cutoff for bounded Harper queries. For example, with `offset: 20` and `limit: 10`, Harper skips the first 20 eligible records and returns the next 10. Hits rejected by source-version checks or record filters do not count toward those 30 records, but fetching them still consumes the raw-hit budget. Harper fetches additional batches as needed until it fills the page, exhausts the matches or reaches a request limit. -Use a bounded `limit` for large result sets. An unbounded search that exceeds its native window or configured result/work budget throws 400; use a bounded query. `offset + limit` must not exceed either `storage.fullText.maxResultsPerRequest` or `storage.fullText.maxRawHitsPerRequest`; replayed or rejected hits can exhaust the raw-work budget earlier. Raising only the raw-hit budget allows more filtering work without raising the retained-record target. Offset requests revisit earlier matches and can retain up to `offset + limit` accepted records. Raising work limits can increase CPU, reads and memory use. +Use a bounded `limit` for large result sets. An unbounded search that exceeds its native window, fetched-hit limit or retained-result limit throws 400; use a bounded query. Exhausting the execution-time budget instead throws 503. `offset + limit` must not exceed either `storage.fullText.maxResultsPerRequest` or `storage.fullText.maxRawHitsPerRequest`; replayed or rejected hits can exhaust the raw-work budget earlier. Raising only the raw-hit budget allows more filtering work without raising the retained-record target. Offset requests revisit earlier matches and can retain up to `offset + limit` accepted records. Raising work limits can increase CPU, reads and memory use. The fetched-hit budget includes the initial batch replay when Harper switches to native traversal. It counts hits returned by the native library, not every record Tantivy visits while scoring. Exhausting that budget throws a 400 error; exhausting the execution budget throws 503. The adapter does not return a shortened result as if the search had completed. If a custom resource has already started streaming a response, a later query error ends that stream instead of changing its HTTP status. When there are no further native matches, Harper returns the eligible records found, which may be an empty result. This differs from reaching a budget while more matches remain. @@ -105,25 +105,28 @@ Replacement workers keep the captured budgets after a configuration change. See ### Readiness and recovery -Full-text indexes require audit logging; a table declared with `@table(audit: false)` cannot register a full-text index. A format-incompatible native upgrade rebuilds the local index from stored records, and queries return 503 while it rebuilds. Harper 5.4 uses `@harperfast/fulltext` 0.6.0. +Full-text indexes require audit logging; a table declared with `@table(audit: false)` cannot register a full-text index. A format-incompatible native upgrade rebuilds the local index from stored records, and queries return 503 while it rebuilds. The v5.4 paging implementation targets `@harperfast/fulltext` 0.6.0. -| Failure | HTTP status | Code and retry guidance | -| -------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| Raw-hit or retained-result budget | 400 | No readiness code. Narrow the query or adjust the named budget. | -| Execution budget | 503 | No readiness code. Narrow the query; the same expensive query can fail again. | -| Native busy | 503 | No readiness code. Reduce concurrency or retry with backoff. | -| Index readiness or coverage | 503 | `INDEX_REBUILDING` or `DERIVED_INDEX_LAGGING`, with `retryable: true`. | -| Worker loaded native search before runtime capture | 500 | REST reports `FullTextWorkerInitializationError`; the Operations API can omit that code. Non-retryable; recreate the worker after initialization. | +| Failure | HTTP status | Code and retry guidance | +| --------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| Raw-hit or retained-result budget | 400 | No readiness code. Narrow the query or adjust the named budget. | +| Execution budget | 503 | No readiness code. Narrow the query; the same expensive query can fail again. | +| Native busy | 503 | No readiness code. Reduce concurrency or retry with backoff. | +| Index unknown, needs-rebuild or rebuilding | 503 | `INDEX_REBUILDING`, with `retryable: true`, for a not-ready readiness check. Retry with backoff while the index rebuilds. | +| Coverage lag or wait timeout | 503 | `DERIVED_INDEX_LAGGING`, with `retryable: true`. Retry with backoff while the index catches up. | +| Index unavailable | 503 | No readiness-specific code or retryable flag. Inspect readiness and logs; fix the reported problem before retrying. | +| Query host becomes non-ready during a coverage wait | 503 | Can return no readiness-specific code or retryable flag. Inspect readiness before deciding whether to retry. | +| Worker loaded native search before runtime capture | 500 | REST reports `FullTextWorkerInitializationError`; the Operations API can omit that code. Non-retryable; recreate the worker after initialization. | A native result that exceeds the response-byte envelope can return 413, including an oversized ordinary batch even when each individual hit would fit. Other native failures and invalid query configuration can return 500; inspect the error and server logs rather than assuming they are retryable. The table lists common failures, not every possible error. -For read-after-write queries, `maxIndexLagMilliseconds: 0` requires current coverage. `waitForIndexMilliseconds` lets a query wait for coverage (default `0`, maximum `30000` ms); without an override, the allowed index lag is `3000` ms. These options belong to the full-text condition. +For read-after-write queries, `maxIndexLagMilliseconds: 0` requires current coverage. `waitForIndexMilliseconds` lets a query wait for coverage (default `0`, maximum `30000` ms); without an override, the allowed index lag is `3000` ms. These options belong to the full-text condition. They wait for coverage, not for an unavailable index to recover. Readiness is checked first; a query host that becomes rebuilding or unavailable during wait setup can fail immediately with a bare 503. Concurrent requests each have their own work budget. These limits do not cap the number of concurrent Harper queries or prevent brief event-loop delays during synchronous source reads. Measure deep offsets and selective filters under the expected concurrency before raising budgets. `INDEX_REBUILDING` and `DERIVED_INDEX_LAGGING` identify retryable readiness or coverage failures. Retry with backoff while the index makes progress. Transaction history retained under [`logging.auditRetention`](../logging/configuration.md#loggingauditretention) (default `3d`) is used for catch-up. Lost history can require a rebuild; retries alone do not repair an index that cannot catch up. Use [`describe_table`](../operations-api/operations.md#describe_table) to inspect `full_text_indexes[].readiness`, including its state and reason. Correct the reported configuration or storage problem before retrying an unavailable index. Budget and native-busy failures do not carry those readiness codes. A native busy response also returns 503; reduce concurrency or retry with backoff. For an execution-budget failure, narrow the query, reduce the offset or adjust the work budget instead of repeatedly submitting the same expensive query. -Native page, response-byte and highlight limits still apply. Fulltext 0.6.0 also limits each analyzed query leaf to 64 terms and the compiled query to 256 clauses. Raising Harper's work budgets does not raise those native limits. See the [published native limits](https://github.com/HarperFast/fulltext/blob/v0.6.0/docs/limits.md) for page, byte and highlight bounds. +Native page, response-byte and highlight limits still apply. That native version also limits each analyzed query leaf to 64 terms and the compiled query to 256 clauses. Raising Harper's work budgets does not raise those native limits. See the [published native limits](https://github.com/HarperFast/fulltext/blob/v0.6.0/docs/limits.md) for page, byte and highlight bounds. ## Relationships and Joins diff --git a/reference/resources/resource-api.md b/reference/resources/resource-api.md index 202bf521c..36561e720 100644 --- a/reference/resources/resource-api.md +++ b/reference/resources/resource-api.md @@ -1035,7 +1035,7 @@ Array of condition objects to filter records. Each condition: | `conditions` | Nested conditions array | | `operator` | `and` (default) or `or` for the nested `conditions` | -Full-text conditions use `matches`, `matches_all`, `matches_phrase`, `matches_prefix`, `matches_fuzzy`, or `matches_fuzzy_prefix` against a declared full-text index. See [Full-text query execution](./query-optimization.md#full-text-search-work-and-paging) for paging costs and work limits. +Full-text conditions use `matches`, `matches_all`, `matches_phrase`, `matches_prefix`, `matches_fuzzy`, or `matches_fuzzy_prefix` against a declared full-text index. Each comparator also has a `not_` form: `not_matches`, `not_matches_all`, `not_matches_phrase`, `not_matches_prefix`, `not_matches_fuzzy`, and `not_matches_fuzzy_prefix`. A negated condition requires a positive full-text condition on the same index. See [Full-text query execution](./query-optimization.md#full-text-search-work-and-paging) for paging costs and work limits. Example with nested conditions: diff --git a/reference/rest/querying.md b/reference/rest/querying.md index e8674701c..94b300ec4 100644 --- a/reference/rest/querying.md +++ b/reference/rest/querying.md @@ -55,7 +55,7 @@ Harper uses [FIQL](https://datatracker.ietf.org/doc/html/draft-nottingham-atompu | `=`, `===` | Strict equality (no type conversion) | | `!==` | Strict inequality (no type conversion) | -Full-text indexes support `=matches=`, `=matches_all=`, `=matches_phrase=`, `=matches_prefix=`, `=matches_fuzzy=`, and `=matches_fuzzy_prefix=`. These use the full-text index rather than ordinary string comparisons. See [Full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging). +Full-text indexes support `=matches=`, `=matches_all=`, `=matches_phrase=`, `=matches_prefix=`, `=matches_fuzzy=`, and `=matches_fuzzy_prefix=`. Each has a negated form: `=not_matches=`, `=not_matches_all=`, `=not_matches_phrase=`, `=not_matches_prefix=`, `=not_matches_fuzzy=`, and `=not_matches_fuzzy_prefix=`. Combine a negated condition with a positive full-text condition on the same index; a structured filter alone does not qualify. These use the full-text index rather than ordinary string comparisons. See [Full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging). **Examples**: From 2c084ecf0235d12f98c5b029397405e8c9ea2a02 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 27 Oct 2026 20:33:03 -0600 Subject: [PATCH 49/51] Document Tantivy 0.27 versions and rebuild requirements --- reference/resources/query-optimization.md | 4 ++-- release-notes/v5-lincoln/5.4.md | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/reference/resources/query-optimization.md b/reference/resources/query-optimization.md index 9d837065e..b8ea4fd29 100644 --- a/reference/resources/query-optimization.md +++ b/reference/resources/query-optimization.md @@ -105,7 +105,7 @@ Replacement workers keep the captured budgets after a configuration change. See ### Readiness and recovery -Full-text indexes require audit logging; a table declared with `@table(audit: false)` cannot register a full-text index. A format-incompatible native upgrade rebuilds the local index from stored records, and queries return 503 while it rebuilds. The v5.4 paging implementation targets `@harperfast/fulltext` 0.6.0. +Full-text indexes require audit logging; a table declared with `@table(audit: false)` cannot register a full-text index. A format-incompatible native upgrade rebuilds the local index from stored records, and queries return 503 while it rebuilds. Harper v5.4 uses `@harperfast/fulltext` 0.7.0 with Tantivy 0.27.0. Indexes from fulltext 0.5.0 (shipped in Harper 5.3.4), as well as prerelease 0.6 indexes, require a full local rebuild. Drain the node's search traffic and stop old processes before starting the new version; do not mix engine versions against one local index. Verify readiness and current coverage before restoring search traffic. Replay after a long rebuild can also cause retryable write-503 responses; retry with bounded backoff while it catches up. Direct downgrade to fulltext 0.5.x is unsupported. | Failure | HTTP status | Code and retry guidance | | --------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -126,7 +126,7 @@ Concurrent requests each have their own work budget. These limits do not cap the `INDEX_REBUILDING` and `DERIVED_INDEX_LAGGING` identify retryable readiness or coverage failures. Retry with backoff while the index makes progress. Transaction history retained under [`logging.auditRetention`](../logging/configuration.md#loggingauditretention) (default `3d`) is used for catch-up. Lost history can require a rebuild; retries alone do not repair an index that cannot catch up. Use [`describe_table`](../operations-api/operations.md#describe_table) to inspect `full_text_indexes[].readiness`, including its state and reason. Correct the reported configuration or storage problem before retrying an unavailable index. Budget and native-busy failures do not carry those readiness codes. A native busy response also returns 503; reduce concurrency or retry with backoff. For an execution-budget failure, narrow the query, reduce the offset or adjust the work budget instead of repeatedly submitting the same expensive query. -Native page, response-byte and highlight limits still apply. That native version also limits each analyzed query leaf to 64 terms and the compiled query to 256 clauses. Raising Harper's work budgets does not raise those native limits. See the [published native limits](https://github.com/HarperFast/fulltext/blob/v0.6.0/docs/limits.md) for page, byte and highlight bounds. +Native page, response-byte and highlight limits still apply. That native version also limits each analyzed query leaf to 64 terms and the compiled query to 256 clauses. Raising Harper's work budgets does not raise those native limits. See the [published native limits](https://github.com/HarperFast/fulltext/blob/v0.7.0/docs/limits.md) for page, byte and highlight bounds. ## Relationships and Joins diff --git a/release-notes/v5-lincoln/5.4.md b/release-notes/v5-lincoln/5.4.md index 9251d902c..de85f0611 100644 --- a/release-notes/v5-lincoln/5.4.md +++ b/release-notes/v5-lincoln/5.4.md @@ -70,6 +70,12 @@ The new `threads.v8Flags` setting takes a V8 flag, or a list of them, and applie ## Full-Text Search +### Tantivy 0.27 and Existing Index Rebuilds + +Harper now uses `@harperfast/fulltext` 0.7.0 with Tantivy 0.27.0. Existing fulltext 0.5.x indexes, including those created by Harper 5.3.4, and prerelease 0.6 indexes rebuild automatically from authoritative records. Plan for local full-text unavailability, disk headroom and possible retryable write failures while replay catches up. Stop old processes before starting the new version against the same storage; direct downgrade to fulltext 0.5.x is unsupported. + +English stemming changes despite the unchanged `english@2` analyzer name: `added` now stems to `add` rather than `ad`. Matching and ranking can change. Query modes and schema options are unchanged; benchmark results from the wrapper do not establish application performance parity. Review representative searches and see [readiness and recovery](/reference/v5/resources/query-optimization#readiness-and-recovery) before restoring traffic. + ### Paging and Search Work Limits Full-text and autocomplete queries use the existing `limit` and `offset` paging options. Harper reads additional native batches when needed after filtering, so the initial native result window no longer silently cuts off eligible results. The new `storage.fullText.maxRawHitsPerRequest`, `storage.fullText.maxResultsPerRequest` and `storage.fullText.requestBudgetMilliseconds` settings bound fetched hits, retained results and execution time. Requests that exceed those budgets return an error. The `storage.fullText.runtime` section also provides process-wide native-capacity budgets; changes to those budgets require a full Harper process restart. Separate page requests run independently and can see changes to records or index contents. From 47e6ae450da7d446ab7d84d4bf9c5a01e5be72f4 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 27 Oct 2026 20:53:01 -0600 Subject: [PATCH 50/51] Clarify streamed search failures and upgrade requirements --- reference/database/transaction.md | 2 +- reference/logging/configuration.md | 2 +- reference/operations-api/operations.md | 2 +- reference/resources/query-optimization.md | 26 +++++++++++------------ reference/rest/querying.md | 2 +- release-notes/v5-lincoln/5.4.md | 6 +++++- 6 files changed, 22 insertions(+), 18 deletions(-) diff --git a/reference/database/transaction.md b/reference/database/transaction.md index 5bed6afb7..7672351a3 100644 --- a/reference/database/transaction.md +++ b/reference/database/transaction.md @@ -204,7 +204,7 @@ type Dog @table(audit: true) { } ``` -This overrides the [`logging.auditLog`](../logging/configuration.md) global configuration for that specific table. Keep auditing enabled for tables with full-text indexes: disabling it prevents full-text index registration. +This overrides the [`logging.auditLog`](../logging/configuration.md) global configuration for that specific table. New schema tables with `@fullText` must explicitly declare `@table(audit: true)`, even when global audit logging is enabled. Existing audited tables can add an index without restating `audit`. Keep auditing enabled while any full-text index is declared; disabling it is rejected. ## Related Documentation diff --git a/reference/logging/configuration.md b/reference/logging/configuration.md index 5ef541370..f5f82ea96 100644 --- a/reference/logging/configuration.md +++ b/reference/logging/configuration.md @@ -102,7 +102,7 @@ Default: `true` Enables transaction logging. When enabled, Harper records every insert, update, and delete to the database's transaction log. This history is read with the [`read_audit_log`](../database/transaction.md#read_audit_log) operation — a legacy name; there is a single transaction log, not a separate audit table. -Full-text indexing requires transaction logging. Disabling this setting prevents new tables from inheriting that prerequisite; a full-text table must also have its per-table audit setting enabled. See [Database / Transaction Logging](../database/transaction.md) for details on the transaction log. +Full-text indexing requires transaction logging. New schema tables with `@fullText` must explicitly declare `@table(audit: true)`, even when this global setting is enabled. Existing audited tables can add an index without restating `audit`; disabling table auditing while an index is declared is rejected. See [Database / Transaction Logging](../database/transaction.md) for details on the transaction log. ```yaml logging: diff --git a/reference/operations-api/operations.md b/reference/operations-api/operations.md index 56f789b94..56a5559f3 100644 --- a/reference/operations-api/operations.md +++ b/reference/operations-api/operations.md @@ -73,7 +73,7 @@ Returns the definition of a specific table, including its schema and storage met -A table with native full-text indexes includes `full_text_indexes`; each entry reports its query-only field name, source fields, analyzer options, enabled query modes, and `readiness` (`unknown`, `ready`, `needs-rebuild`, `rebuilding`, or `unavailable`). The readiness object also includes `owner_epoch`, `rebuild_attempts`, and an optional `reason`. Query-only `FullText` fields do not appear in the ordinary `attributes` list. +A table with native full-text indexes includes `full_text_indexes`; each entry reports its query-only field name, source fields, analyzer options, enabled query modes, and `readiness` (`unknown`, `ready`, `needs-rebuild`, `rebuilding`, or `unavailable`). The readiness object also includes `owner_epoch`, `rebuild_attempts`, and an optional `reason`. Query-only `FullText` fields do not appear in the ordinary `attributes` list. Metadata respects attribute permissions: unreadable source fields are omitted, and an index with no readable source fields is omitted entirely. Use an account with access to every indexed source field when checking upgrade readiness; a missing entry does not prove that no index exists. Requesting a table with full-text indexes: diff --git a/reference/resources/query-optimization.md b/reference/resources/query-optimization.md index b8ea4fd29..46e9e6821 100644 --- a/reference/resources/query-optimization.md +++ b/reference/resources/query-optimization.md @@ -93,7 +93,7 @@ For example, with `offset: 20` and `limit: 10`, Harper skips the first 20 eligib Use a bounded `limit` for large result sets. An unbounded search that exceeds its native window, fetched-hit limit or retained-result limit throws 400; use a bounded query. Exhausting the execution-time budget instead throws 503. `offset + limit` must not exceed either `storage.fullText.maxResultsPerRequest` or `storage.fullText.maxRawHitsPerRequest`; replayed or rejected hits can exhaust the raw-work budget earlier. Raising only the raw-hit budget allows more filtering work without raising the retained-record target. Offset requests revisit earlier matches and can retain up to `offset + limit` accepted records. Raising work limits can increase CPU, reads and memory use. -The fetched-hit budget includes the initial batch replay when Harper switches to native traversal. It counts hits returned by the native library, not every record Tantivy visits while scoring. Exhausting that budget throws a 400 error; exhausting the execution budget throws 503. The adapter does not return a shortened result as if the search had completed. If a custom resource has already started streaming a response, a later query error ends that stream instead of changing its HTTP status. When there are no further native matches, Harper returns the eligible records found, which may be an empty result. This differs from reaching a budget while more matches remain. +The fetched-hit budget includes the initial batch replay when Harper switches to native traversal. It counts hits returned by the native library, not every record Tantivy visits while scoring. Exhausting that budget throws a 400 error; exhausting the execution budget throws 503. The adapter does not return a shortened result as if the search had completed. If a REST response has already started streaming, a later query error ends that stream instead of changing its HTTP status. JSON-array clients must check for a terminal `error` element and treat the response as failed. For other formats, detect stream or decoding failures; status 200 and count headers alone do not prove completion. See [REST paging](../rest/querying.md#pagination-and-total-count). When there are no further native matches, Harper returns the eligible records found, which may be an empty result. This differs from reaching a budget while more matches remain. Configure the three request limits and six native process budgets under [`storage.fullText`](../configuration/options.md#storage). See [Set Configuration](../configuration/operations.md#set-configuration) for restart requirements. The execution budget is not a hard response-time guarantee: reader acquisition, query construction and some native operations can take additional time. Freshness waiting and reader acquisition use separate bounds. @@ -105,24 +105,24 @@ Replacement workers keep the captured budgets after a configuration change. See ### Readiness and recovery -Full-text indexes require audit logging; a table declared with `@table(audit: false)` cannot register a full-text index. A format-incompatible native upgrade rebuilds the local index from stored records, and queries return 503 while it rebuilds. Harper v5.4 uses `@harperfast/fulltext` 0.7.0 with Tantivy 0.27.0. Indexes from fulltext 0.5.0 (shipped in Harper 5.3.4), as well as prerelease 0.6 indexes, require a full local rebuild. Drain the node's search traffic and stop old processes before starting the new version; do not mix engine versions against one local index. Verify readiness and current coverage before restoring search traffic. Replay after a long rebuild can also cause retryable write-503 responses; retry with bounded backoff while it catches up. Direct downgrade to fulltext 0.5.x is unsupported. +Full-text indexes require audit logging. New schema tables must explicitly declare `@table(audit: true)` when adding `@fullText`, even if global audit logging is enabled; an existing audited table can add an index without restating `audit`. Disabling audit logging while an index is declared is rejected. A format-incompatible native upgrade rebuilds the local index from stored records, and queries return 503 while it rebuilds. Harper v5.4 uses `@harperfast/fulltext` 0.7.0 with Tantivy 0.27.0. Indexes from fulltext 0.5.x using current field-based declarations (including fulltext 0.5.0 shipped in Harper 5.3.4), as well as prerelease 0.6 indexes, require a full local rebuild. Earlier beta declarations and indexes have no supported migration path. Drain the node's search traffic and stop old processes before starting the new version; do not mix engine versions against one local index. Verify readiness and current coverage before restoring search traffic. Replay after a long rebuild can also reject ordinary local writes with 503, `code: "DERIVED_INDEX_LAGGING"`, and `retryable: true`; retry with bounded backoff while it catches up. Replication and crash-recovery replay continue. Direct downgrade to fulltext 0.5.x is unsupported: that engine refuses opening or resetting a 0.7 index identity. Stop an accidentally started old version and restore the 0.7.0-based Harper version, keeping search traffic drained until readiness and coverage recover. Do not edit native index files. Missing retained transaction history can require a full rebuild, as described below; this is current-engine recovery, not a general Harper downgrade procedure. English stemming also changes: `added` now stems to `add` rather than `ad`, so review matching and ranking against representative searches. -| Failure | HTTP status | Code and retry guidance | -| --------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| Raw-hit or retained-result budget | 400 | No readiness code. Narrow the query or adjust the named budget. | -| Execution budget | 503 | No readiness code. Narrow the query; the same expensive query can fail again. | -| Native busy | 503 | No readiness code. Reduce concurrency or retry with backoff. | -| Index unknown, needs-rebuild or rebuilding | 503 | `INDEX_REBUILDING`, with `retryable: true`, for a not-ready readiness check. Retry with backoff while the index rebuilds. | -| Coverage lag or wait timeout | 503 | `DERIVED_INDEX_LAGGING`, with `retryable: true`. Retry with backoff while the index catches up. | -| Index unavailable | 503 | No readiness-specific code or retryable flag. Inspect readiness and logs; fix the reported problem before retrying. | -| Query host becomes non-ready during a coverage wait | 503 | Can return no readiness-specific code or retryable flag. Inspect readiness before deciding whether to retry. | -| Worker loaded native search before runtime capture | 500 | REST reports `FullTextWorkerInitializationError`; the Operations API can omit that code. Non-retryable; recreate the worker after initialization. | +| Failure | HTTP status | Code and retry guidance | +| ------------------------------------------------------ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| Raw-hit or retained-result budget | 400 | No readiness code. Narrow the query or adjust the named budget. | +| Execution budget | 503 | No readiness code. Narrow the query; the same expensive query can fail again. | +| Native busy | 503 | No readiness code. Reduce concurrency or retry with backoff. | +| Index unknown, needs-rebuild or rebuilding | 503 | `INDEX_REBUILDING`, with `retryable: true`, for a not-ready readiness check. Retry with backoff while the index rebuilds. | +| Coverage lag, wait timeout or local write backpressure | 503 | `DERIVED_INDEX_LAGGING`, with `retryable: true`. Retry with backoff while the index catches up. | +| Index unavailable | 503 | No readiness-specific code or retryable flag. Inspect readiness and logs; fix the reported problem before retrying. | +| Query host becomes non-ready during a coverage wait | 503 | Can return no readiness-specific code or retryable flag. Inspect readiness before deciding whether to retry. | +| Worker loaded native search before runtime capture | 500 | REST reports `FullTextWorkerInitializationError`; the Operations API can omit that code. Non-retryable; recreate the worker after initialization. | A native result that exceeds the response-byte envelope can return 413, including an oversized ordinary batch even when each individual hit would fit. Other native failures and invalid query configuration can return 500; inspect the error and server logs rather than assuming they are retryable. The table lists common failures, not every possible error. For read-after-write queries, `maxIndexLagMilliseconds: 0` requires current coverage. `waitForIndexMilliseconds` lets a query wait for coverage (default `0`, maximum `30000` ms); without an override, the allowed index lag is `3000` ms. These options belong to the full-text condition. They wait for coverage, not for an unavailable index to recover. Readiness is checked first; a query host that becomes rebuilding or unavailable during wait setup can fail immediately with a bare 503. -Concurrent requests each have their own work budget. These limits do not cap the number of concurrent Harper queries or prevent brief event-loop delays during synchronous source reads. Measure deep offsets and selective filters under the expected concurrency before raising budgets. +Concurrent requests each have their own work budget. These per-request budgets do not cap the number of concurrent Harper queries or prevent brief event-loop delays during synchronous source reads. Measure deep offsets and selective filters under the expected concurrency before raising budgets. `INDEX_REBUILDING` and `DERIVED_INDEX_LAGGING` identify retryable readiness or coverage failures. Retry with backoff while the index makes progress. Transaction history retained under [`logging.auditRetention`](../logging/configuration.md#loggingauditretention) (default `3d`) is used for catch-up. Lost history can require a rebuild; retries alone do not repair an index that cannot catch up. Use [`describe_table`](../operations-api/operations.md#describe_table) to inspect `full_text_indexes[].readiness`, including its state and reason. Correct the reported configuration or storage problem before retrying an unavailable index. Budget and native-busy failures do not carry those readiness codes. A native busy response also returns 503; reduce concurrency or retry with backoff. For an execution-budget failure, narrow the query, reduce the offset or adjust the work budget instead of repeatedly submitting the same expensive query. diff --git a/reference/rest/querying.md b/reference/rest/querying.md index 94b300ec4..15021891f 100644 --- a/reference/rest/querying.md +++ b/reference/rest/querying.md @@ -183,7 +183,7 @@ Use `limit(start,end)` to page through a collection, and opt in to a total match Counting is opt-in: without the header, no count is computed and no count headers are returned. It applies only to `GET`/`HEAD` requests and requires a bounded page — the `limit()` must be a non-negative integer no larger than **10,000**, and the requested window (offset + limit) no larger than **1,000,000**. A request with no `limit()`, one outside those bounds, or a non-numeric one is served normally with no count headers, since counting an unbounded page would defeat the point of paging. -Full-text and autocomplete queries use the same paging syntax. The page window counts eligible records after record filtering, rather than raw full-text hits. Harper fetches more ranked batches to fill that window when needed. A page can contain fewer records when no further matches remain; reaching a search work or time limit produces an error instead of a successful partial page. Each page request runs a new query, so concurrent changes can cause repeats or omissions between pages. See [Full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging) for search work limits and recovery guidance. +Full-text and autocomplete queries use the same paging syntax. The page window counts eligible records after record filtering, rather than raw full-text hits. Harper fetches more ranked batches to fill that window when needed. A page can contain fewer records when no further matches remain; reaching a search work or time limit fails the query. A failure detected before response headers can set an error HTTP status. After streaming starts, the response can retain HTTP 200 and end with an error. For JSON-array responses, check for a terminal `error` element and treat the response as failed; do not render that element as a record. For other formats, detect stream or decoding failures as well. Neither status 200 nor count headers prove that a page completed successfully. Each page request runs a new query, so concurrent changes can cause repeats or omissions between pages. See [Full-text query execution](../resources/query-optimization.md#full-text-search-work-and-paging) for search work limits and recovery guidance. The count-header guardrails do not override query-specific limits such as the full-text work budget. diff --git a/release-notes/v5-lincoln/5.4.md b/release-notes/v5-lincoln/5.4.md index de85f0611..3e77b78a3 100644 --- a/release-notes/v5-lincoln/5.4.md +++ b/release-notes/v5-lincoln/5.4.md @@ -8,6 +8,10 @@ title: '5.4' All patch release notes for 5.4.x are available on the [releases page](https://github.com/HarperFast/harper/releases?q=v5.4&expanded=true). +## Upgrade Notes + +Full-text indexes using the current 0.5.x declarations or prerelease 0.6 rebuild locally after this upgrade. Plan for search unavailability and review the [Tantivy upgrade requirements](#tantivy-027-and-existing-index-rebuilds) before deploying; direct native-engine downgrade is unsupported. + ## Replication ### Dedicated Replication Threads @@ -72,7 +76,7 @@ The new `threads.v8Flags` setting takes a V8 flag, or a list of them, and applie ### Tantivy 0.27 and Existing Index Rebuilds -Harper now uses `@harperfast/fulltext` 0.7.0 with Tantivy 0.27.0. Existing fulltext 0.5.x indexes, including those created by Harper 5.3.4, and prerelease 0.6 indexes rebuild automatically from authoritative records. Plan for local full-text unavailability, disk headroom and possible retryable write failures while replay catches up. Stop old processes before starting the new version against the same storage; direct downgrade to fulltext 0.5.x is unsupported. +Harper now uses `@harperfast/fulltext` 0.7.0 with Tantivy 0.27.0. Existing fulltext 0.5.x indexes using current field-based declarations, including fulltext 0.5.0 shipped in Harper 5.3.4, and prerelease 0.6 indexes rebuild automatically from authoritative records. Earlier beta declarations and indexes have no supported migration path. Plan for local full-text unavailability, disk headroom and possible retryable write failures while replay catches up. Stop old processes before starting the new version against the same storage; direct downgrade to fulltext 0.5.x is unsupported. English stemming changes despite the unchanged `english@2` analyzer name: `added` now stems to `add` rather than `ad`. Matching and ranking can change. Query modes and schema options are unchanged; benchmark results from the wrapper do not establish application performance parity. Review representative searches and see [readiness and recovery](/reference/v5/resources/query-optimization#readiness-and-recovery) before restoring traffic. From 3640af2652ae1357f22824992a0207a8de80dbf7 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Tue, 27 Oct 2026 20:54:50 -0600 Subject: [PATCH 51/51] Scope freshness waits and unsupported downgrade guidance --- reference/resources/query-optimization.md | 2 +- release-notes/v5-lincoln/5.4.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/reference/resources/query-optimization.md b/reference/resources/query-optimization.md index 46e9e6821..b52a37288 100644 --- a/reference/resources/query-optimization.md +++ b/reference/resources/query-optimization.md @@ -120,7 +120,7 @@ Full-text indexes require audit logging. New schema tables must explicitly decla A native result that exceeds the response-byte envelope can return 413, including an oversized ordinary batch even when each individual hit would fit. Other native failures and invalid query configuration can return 500; inspect the error and server logs rather than assuming they are retryable. The table lists common failures, not every possible error. -For read-after-write queries, `maxIndexLagMilliseconds: 0` requires current coverage. `waitForIndexMilliseconds` lets a query wait for coverage (default `0`, maximum `30000` ms); without an override, the allowed index lag is `3000` ms. These options belong to the full-text condition. They wait for coverage, not for an unavailable index to recover. Readiness is checked first; a query host that becomes rebuilding or unavailable during wait setup can fail immediately with a bare 503. +For read-after-write queries, `maxIndexLagMilliseconds: 0` requires coverage of writes already committed on the queried node. It does not wait for replication to deliver writes from another node. `waitForIndexMilliseconds` lets a query wait for coverage (default `0`, maximum `30000` ms); without an override, the allowed index lag is `3000` ms. These options belong to the full-text condition. They wait for coverage, not for an unavailable index to recover. Readiness is checked first; a query host that becomes rebuilding or unavailable during wait setup can fail immediately with a bare 503. Concurrent requests each have their own work budget. These per-request budgets do not cap the number of concurrent Harper queries or prevent brief event-loop delays during synchronous source reads. Measure deep offsets and selective filters under the expected concurrency before raising budgets. diff --git a/release-notes/v5-lincoln/5.4.md b/release-notes/v5-lincoln/5.4.md index 3e77b78a3..d39a10eb6 100644 --- a/release-notes/v5-lincoln/5.4.md +++ b/release-notes/v5-lincoln/5.4.md @@ -10,7 +10,7 @@ All patch release notes for 5.4.x are available on the [releases page](https://g ## Upgrade Notes -Full-text indexes using the current 0.5.x declarations or prerelease 0.6 rebuild locally after this upgrade. Plan for search unavailability and review the [Tantivy upgrade requirements](#tantivy-027-and-existing-index-rebuilds) before deploying; direct native-engine downgrade is unsupported. +Full-text indexes using the current 0.5.x declarations or prerelease 0.6 rebuild locally after this upgrade. Plan for search unavailability and review the [Tantivy upgrade requirements](#tantivy-027-and-existing-index-rebuilds) before deploying; direct downgrade to fulltext 0.5.x is unsupported. ## Replication