Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1782051
Document native full-text search
nizzlenitz Sep 27, 2026
02dbfe3
Clarify full-text prefix result limits
nizzlenitz Sep 28, 2026
5bb4233
docs(fulltext): clarify runtime and recovery contracts
nizzlenitz Sep 28, 2026
b5e9b21
docs(fulltext): address final review
nizzlenitz Sep 28, 2026
639e80c
docs(fulltext): clarify operational behavior
nizzlenitz Sep 28, 2026
b7ad217
docs(release): keep patch releases at heading level two
nizzlenitz Sep 28, 2026
e3379b8
docs(fulltext): define query edge cases
nizzlenitz Sep 28, 2026
0954d11
docs(fulltext): resolve final contract gaps
nizzlenitz Sep 28, 2026
6855bed
docs(fulltext): align with field-only schema
nizzlenitz Sep 29, 2026
1d3792d
docs(fulltext): address review feedback
nizzlenitz Sep 29, 2026
2fd2564
docs(fulltext): align operational caveats
nizzlenitz Sep 29, 2026
5334c2a
docs(fulltext): complete security and storage guidance
nizzlenitz Sep 29, 2026
e50fc56
docs(fulltext): clarify REST count behavior
nizzlenitz Sep 29, 2026
2c4ab6d
docs(fulltext): correct runtime behavior
nizzlenitz Sep 29, 2026
8bbaa43
docs(fulltext): align Blob troubleshooting
nizzlenitz Sep 29, 2026
25599cb
docs(fulltext): clarify search iteration
nizzlenitz Sep 29, 2026
505a706
Document full-text filter metadata
nizzlenitz Oct 1, 2026
a035305
Merge remote-tracking branch 'origin/main' into docs/full-text-search
nizzlenitz Oct 1, 2026
4d7df3f
Reconcile native full-text guidance with v5.4 paging
nizzlenitz Oct 21, 2026
ee4436e
Remove unsupported full-text metadata examples and reconcile release …
nizzlenitz Oct 21, 2026
1c86ef0
Keep structured-filter guidance focused on supported options
nizzlenitz Oct 21, 2026
c319f8c
Clarify paging recovery, offset costs and condition ownership
nizzlenitz Oct 21, 2026
a629e79
Make paging errors explicit and keep badge styling unchanged
nizzlenitz Oct 21, 2026
06b8ff0
docs: address native full-text review feedback
nizzlenitz Oct 22, 2026
a047778
docs: clarify streaming errors and REST range semantics
nizzlenitz Oct 22, 2026
51713cc
docs: tighten full-text count guidance and scope
nizzlenitz Oct 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ The Plugin API is the primary way to implement additional functionality in Harpe
- `logger.level` β€” Log level; _Default_: `"info"` (Added in: v4.1.0)
```

For inline config option annotations inside list items, plain text `(Added in: vX.Y.Z)` is fine β€” using the component mid-sentence is awkward. Reserve `<VersionBadge>` for standalone placement after headings.
Use `<VersionBadge>` after headings or alongside the specific sentence, list item, or table entry it annotates. Use `<EngineBadge engines="RocksDB" />` the same way when behavior is storage-engine-specific; comma-separated values such as `engines="RocksDB, LMDB"` are supported. Keep adjacent version and engine badges together on one line. For dense prose where a component would interrupt the sentence, plain text such as `(Added in: vX.Y.Z)` is fine.

## Redirects

Expand Down
193 changes: 193 additions & 0 deletions learn/developers/full-text-search-product-catalog.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
---
title: Add Full-Text Search to a Product Catalog
description: Configure and query a BM25-ranked product search in Harper.
---

# Add Full-Text Search to a Product Catalog

<VersionBadge version="v5.3.0" />

This guide adds relevance-ranked text search to a product catalog, then exposes it through Harper's JavaScript and REST APIs.

## What you will build

- A BM25-ranked index over product names, descriptions, and tags
- A Harper application endpoint with highlights and a structured category filter
- REST queries for terms, phrases, and record autocomplete

## Prerequisites

- Harper 5.3 or later using RocksDB
- A component with a GraphQL schema, JavaScript resources, and `rest: true` in its `config.yaml`

## 1. Declare the index

Declare an `@fullText` index over the fields customers search on a RocksDB-backed table. Ordinary tables inherit enabled audit logging by default, but a new full-text table requires explicit `audit: true` for its transaction-log recovery source. See the [audit requirement](/reference/v5/full-text-search/configuration) for existing tables.

```graphql title="schema.graphql"
type Product @table(database: "catalog", audit: true) @export {
id: ID @primaryKey
name: String
description: String
tags: [String]
category: String @indexed
price: Float @indexed
available: Boolean
catalogSearch: FullText
@fullText(
fields: [
{ name: "name", weight: 3, highlight: true }
{ name: "description", weight: 1, highlight: true }
{ name: "tags" }
]
highlighting: { maxFragments: 2, fragmentLength: 120 }
)
}
```

The query-only field name `catalogSearch` identifies the index. It is not stored with each product. `weight: 3` makes a match in `name` contribute more to BM25 relevance than the same match in `description` or `tags`. Highlighting is enabled only for the fields shown to customers.

The `@indexed` declarations create ordinary Harper secondary indexes. Full-text searches can use them to narrow candidates, and Harper checks structured conditions against current product records before returning results.

## 2. Add records

Use the generated REST endpoint:

```http
POST /Product/
Content-Type: application/json

{
"id": "shoe-1",
"name": "Waterproof trail running shoe",
"description": "Lightweight shoe for wet mountain trails",
"tags": ["outdoor", "trail"],
"category": "footwear",
"price": 129,
"available": true
}
```

Harper commits the record first, then applies the committed change to the local derived full-text index. Every replica performs the same derivation from the transaction it receives.

If the index remains behind for more than 30 seconds, a local write can return retryable `503` code `DERIVED_INDEX_LAGGING`. Retry writes with backoff while the index catches up; freshness options apply only to searches. See [write backpressure](/reference/v5/full-text-search/overview#write-backpressure).

## 3. Search from a Harper application

```javascript title="resources.js"
export class SearchProducts extends Resource {
static async post(_target, data, context) {
Comment thread
kylebernhardy marked this conversation as resolved.
if (!context?.user) {
const error = new Error('Authentication required');
error.statusCode = 401;
throw error;
}
const body = (await data) ?? {};
const text = typeof body.q === 'string' ? body.q.trim() : '';
if (!text) {
const error = new Error('q is required');
error.statusCode = 400;
throw error;
}
const conditions = [
{
attribute: 'catalogSearch',
comparator: 'matches',
value: text,
fields: ['name', 'description', 'tags'],
Comment thread
kylebernhardy marked this conversation as resolved.
},
{ attribute: 'available', comparator: 'equals', value: true },
];
const category = body.category;
if (category) conditions.push({ attribute: 'category', comparator: 'equals', value: category });
return databases.catalog.Product.search(
{
conditions,
select: ['id', 'name', 'price', '$score', '$highlights'],
limit: 20,
checkPermission: true,
},
context
);
}
}
```

Call the resource with an authenticated request:

```http
POST /SearchProducts/
Content-Type: application/json

{ "q": "trail", "category": "footwear" }
```

This custom resource accepts a structured JSON search body through POST. The category filter is optional; `q` is required. For URL-based GET searches, use the exported table endpoint in step 4.

Harper applies the availability and optional category conditions while accepting current product records. Existing secondary indexes can narrow candidates; rejected records do not count toward the requested page.

Calls through `tables` and `databases` use Harper's [trusted server-side context](/reference/v5/components/javascript-environment#tables). Forwarding the resource's `context` preserves the authenticated caller, and `checkPermission: true` asks the table to enforce that caller's table and source-field permissions. The custom resource must do both. Highlights can expose searched source text even when those fields are absent from `select`.

Matches in `tags` contribute to search results and ranking, but `$highlights` does not include tag matches because `tags` is not configured with `highlight: true`.

## 4. Search through REST

The exported table supports the same match comparator:

```http
GET /Product/?catalogSearch=matches=waterproof%20trail&category=footwear&select(id,name,price,$score,$highlights)&limit(20)
```

For a phrase:

```http
GET /Product/?catalogSearch=matches_phrase=trail%20running&limit(20)
```

For record autocomplete:

```http
GET /Product/?catalogSearch=matches_prefix=waterproof%20tra&select(id,name)&limit(10)
```

Prefix search returns matching products. It does not return a dictionary of suggested terms. Before v5.4, prefix and fuzzy-prefix queries restrict `offset + limit` to 100. In v5.4, bounded pages can go beyond that window within request budgets; each page runs a new query rather than retaining a search. See [paging and prefix result windows](/reference/v5/full-text-search/querying#paging-and-prefix-result-windows).

REST searches every source field in the index, so the caller must be allowed to read `name`, `description`, and `tags` for these requests. Use `Table.search()` or `search_by_conditions` when a role should search only a subset of sources. See [REST full-text operators](/reference/v5/rest/querying#full-text-operators).

## 5. Choose consistency behavior

Most searches can use the default three-second lag tolerance. When the next request must find a record that was just written, use the JavaScript API and wait for current index coverage:

```javascript
for await (const product of databases.catalog.Product.search({
conditions: [
{
attribute: 'catalogSearch',
comparator: 'matches',
value: 'new seasonal product',
maxIndexLagMilliseconds: 0,
waitForIndexMilliseconds: 10000,
},
],
})) {
console.log(product);
}
```

Do not apply this wait to every catalog request without measuring it. The normal bounded-staleness path keeps search traffic independent of short indexing bursts.

If the index does not reach current coverage within ten seconds, iteration throws an error with `statusCode: 503`, `code: "DERIVED_INDEX_LAGGING"`, and `retryable: true`. See [freshness controls](/reference/v5/full-text-search/querying#freshness-controls).

## 6. Operate the index

Use `describe_table` to inspect readiness. Harper reuses compatible local index files after restart and replays changes after their checkpoint. A node rebuilds locally when the files are missing, incompatible, or corrupt; table traffic remains available, while full-text requests in `unknown`, `needs-rebuild`, or `rebuilding` state return `503` with `code: "INDEX_REBUILDING"` and `retryable: true`. Retry this code with backoff; do not treat every `503` as an index rebuild.

The custom resource in step 3 streams its returned iterable. HTTP error statuses apply before response headers are sent; a failure after streaming starts is a terminal stream error and cannot change the HTTP status. Consumers must also handle that terminal error. See [freshness and streaming errors](/reference/v5/full-text-search/querying#freshness-controls).

See the [full-text configuration reference](/reference/v5/full-text-search/configuration) and [query reference](/reference/v5/full-text-search/querying) for every option and match mode.

## Additional resources

- [Full-text search overview](/reference/v5/full-text-search/overview)
- [Resource API](/reference/v5/resources/resource-api)
- [Query optimization](/reference/v5/resources/query-optimization)
3 changes: 2 additions & 1 deletion reference/backups/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ title: Backups

<!-- Source: HarperFast/harper#1831 (RocksDB managed backup and restore operations) -->

Harper can back up and restore its databases natively. A backup is a whole-database copy β€” all tables, the audit/transaction log, and any file-backed blobs β€” so a restored database is a consistent point-in-time image of everything in it. (`get_backup` on an LMDB database is the exception: it can stream a subset of tables, and includes the audit store only when requested.)
Harper can back up and restore its databases natively. A backup is a whole-database copy of authoritative state β€” all tables, the audit/transaction log, and any file-backed blobs β€” so a restored database is a consistent point-in-time image. Local derived state, including full-text index files, is excluded and rebuilt after restore. (`get_backup` on an LMDB database is the exception: it can stream a subset of tables, and includes the audit store only when requested.)

Two complementary mechanisms are available:

Expand Down Expand Up @@ -38,6 +38,7 @@ The offline path matters for restore. RocksDB is single-writer, so an in-place r
- **Backups live on the node that created them.** The backup repository is a local directory. RocksDB shares files across backup IDs (a backup ID is not a self-contained folder), so disaster-recovery copies must take the entire per-database repository β€” `<backupPath>/<database>` β€” not an individual backup, and must do so while no backup operation is running, or from an atomic filesystem snapshot. A live recursive copy can race `create_backup`/`delete_backup`/`purge_backups` and produce an unrestorable copy. Alternatively, use `get_backup` to pull a snapshot from a running server.
- **`get_backup` always streams the current state.** It cannot download a historical managed backup; to move a retained backup off-host, copy its whole per-database repository as above.
- **A restore is a point-in-time rollback.** In a replicated cluster, coordinate a restore with replication before bringing the node back.
- **Full-text indexes rebuild locally after restore.** Ordinary table traffic remains available. While the rebuild is active, full-text requests return `503` with `code: "INDEX_REBUILDING"` and `retryable: true`; branch on the code because other `503` responses are not necessarily rebuilds.
- **A restore checks the backup before it deletes anything.** <VersionBadge type="changed" version="v5.4.0" /> A backup with a corrupt database file, or one this version of Harper cannot read, is refused and the database is left as it was (a database an earlier restore left incomplete stays blocked). The check reads the whole backup, which adds to how long an online restore keeps the database unavailable. Blob file contents are not checked. See [`restore_backup`](./operations.md#restore_backup).
- **An interrupted restore leaves the database unloadable.** If `restore_backup` is interrupted after it has started replacing the database's files (crash, power loss), Harper marks the database as incompletely restored and skips loading it on the next start, logging an incomplete-restore error. Rerun `restore_backup` for the same database and `backup_id` to recover; do not load or hand-repair the directory.

Expand Down
2 changes: 1 addition & 1 deletion reference/components/javascript-environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ An object whose properties are the tables in the default database (`data`). Each
See [Database API](../database/api.md) for full reference.

:::note Server-side table reads run in a trusted context
Calls to `tables.X.get()`, `tables.X.search()`, and equivalent `databases.<database>.<table>` methods from within resource code execute in a **trusted system context** and do **not** re-apply the target table's `allowRead` guard or role-level `attribute_permissions`. This is by design β€” server-side code is responsible for its own authorization checks. A computed attribute or custom resource that cross-reads a protected table can return raw data regardless of the caller's role. Do not expose protected data through a computed attribute when access depends on the caller. Instead, use a custom resource and apply authorization explicitly (e.g. call `this.getCurrentUser()` in a `Resource` method, or use `getUser()`, and enforce the relevant restrictions before returning the data).
Calls to `tables.X.get()`, `tables.X.search()`, and equivalent `databases.<database>.<table>` methods from within resource code execute in a **trusted system context** and do **not** re-apply the target table's `allowRead` guard or role-level `attribute_permissions` by default. This is by design β€” server-side code is responsible for its own authorization checks. A custom resource can explicitly re-enter table and source-field authorization for `Table.search()` by forwarding its authenticated `context` and setting `checkPermission: true` in search options. Never accept or copy that flag or a permission object from client data. Context propagation alone does not request the check, and other direct table operations still require explicit authorization. A computed attribute or custom resource that cross-reads a protected table can otherwise return raw data regardless of the caller's role. Do not expose protected data through a computed attribute when access depends on the caller. Instead, use a custom resource and enforce the relevant restrictions before returning the data.
:::

### `databases`
Expand Down
1 change: 1 addition & 0 deletions reference/database/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ It is best practice to define schemas for production tables. Dynamic schemas are
For deeper coverage of each database feature, see the dedicated pages in this section:

- **[Schema](./schema.md)** β€” Defining table structure, types, indexes, relationships, and computed properties using GraphQL schema syntax
- **[Full-Text Search](../full-text-search/overview.md)**: BM25-ranked search over stored text fields on audited RocksDB tables
- **[API](./api.md)** β€” The `tables`, `databases`, `transaction()`, and `createBlob()` globals for interacting with the database from code
- **[Data Loader](./data-loader.md)** β€” Loading seed or initial data into tables as part of component deployment
- **[Storage Algorithm](./storage-algorithm.md)** β€” How Harper stores data using LMDB with universal indexing and ACID compliance
Expand Down
Loading
Loading