Repository navigation
Document native full-text search #691
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
kylebernhardy
wants to merge
26
commits into
main
Choose a base branch
from
docs/full-text-search
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+931
β45
Open
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 02dbfe3
Clarify full-text prefix result limits
nizzlenitz 5bb4233
docs(fulltext): clarify runtime and recovery contracts
nizzlenitz b5e9b21
docs(fulltext): address final review
nizzlenitz 639e80c
docs(fulltext): clarify operational behavior
nizzlenitz b7ad217
docs(release): keep patch releases at heading level two
nizzlenitz e3379b8
docs(fulltext): define query edge cases
nizzlenitz 0954d11
docs(fulltext): resolve final contract gaps
nizzlenitz 6855bed
docs(fulltext): align with field-only schema
nizzlenitz 1d3792d
docs(fulltext): address review feedback
nizzlenitz 2fd2564
docs(fulltext): align operational caveats
nizzlenitz 5334c2a
docs(fulltext): complete security and storage guidance
nizzlenitz e50fc56
docs(fulltext): clarify REST count behavior
nizzlenitz 2c4ab6d
docs(fulltext): correct runtime behavior
nizzlenitz 8bbaa43
docs(fulltext): align Blob troubleshooting
nizzlenitz 25599cb
docs(fulltext): clarify search iteration
nizzlenitz 505a706
Document full-text filter metadata
nizzlenitz a035305
Merge remote-tracking branch 'origin/main' into docs/full-text-search
nizzlenitz 4d7df3f
Reconcile native full-text guidance with v5.4 paging
nizzlenitz ee4436e
Remove unsupported full-text metadata examples and reconcile release β¦
nizzlenitz 1c86ef0
Keep structured-filter guidance focused on supported options
nizzlenitz c319f8c
Clarify paging recovery, offset costs and condition ownership
nizzlenitz a629e79
Make paging errors explicit and keep badge styling unchanged
nizzlenitz 06b8ff0
docs: address native full-text review feedback
nizzlenitz a047778
docs: clarify streaming errors and REST range semantics
nizzlenitz 51713cc
docs: tighten full-text count guidance and scope
nizzlenitz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) { | ||
| 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'], | ||
|
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) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.