Query features compose: one query can combine a search term with filters, facets, sorting, grouping, pinned results, and a pagination cursor. This page builds one file, search.ts, step by step. You start from a small products catalogue, run a plain search, then add filters, facets, sorting, grouping, and pagination. Each step prints its result so that you can watch the data change as you go.
Set up the catalogue
Every step queries the same products index, so start search.ts with an engine, a schema, and a handful of products to search against.
import { createNarsil } from '@delali/narsil'
const narsil = await createNarsil()
await narsil.createIndex('products', {
schema: {
title: 'string',
description: 'string',
price: 'number',
inStock: 'boolean',
category: 'enum',
tags: 'string[]',
},
language: 'english',
})
await narsil.insertBatch('products', [
{ id: 'kb-mech', title: 'Mechanical Keyboard', description: 'Cherry MX Brown switches with PBT keycaps for tactile typing.', price: 129.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-lite', title: 'Wireless Keyboard', description: 'Slim low-profile board with a long battery life.', price: 59.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-ergo', title: 'Ergonomic Split Keyboard for Typists', description: 'Split layout that eases wrist strain over long typing sessions.', price: 89.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-compact', title: 'Compact Wireless Keyboard', description: 'Tenkeyless board that frees desk space.', price: 74.99, inStock: false, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'case-kb', title: 'Folio Keyboard Case for Tablets and Phones', description: 'Bluetooth folio case that turns a tablet into a laptop.', price: 49.99, inStock: true, category: 'accessories', tags: ['bluetooth', 'travel'] },
{ id: 'rest-wrist', title: 'Keyboard Wrist Rest', description: 'Memory-foam rest that supports your wrists at the keyboard.', price: 24.99, inStock: true, category: 'accessories', tags: ['ergonomic'] },
])Run a plain search
With the catalogue loaded, add a query for keyboard and print the ranked hits. Every hit carries the stored document and its relevance score.
import { createNarsil } from '@delali/narsil'
const narsil = await createNarsil()
await narsil.createIndex('products', {
schema: {
title: 'string',
description: 'string',
price: 'number',
inStock: 'boolean',
category: 'enum',
tags: 'string[]',
},
language: 'english',
})
await narsil.insertBatch('products', [
{ id: 'kb-mech', title: 'Mechanical Keyboard', description: 'Cherry MX Brown switches with PBT keycaps for tactile typing.', price: 129.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-lite', title: 'Wireless Keyboard', description: 'Slim low-profile board with a long battery life.', price: 59.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-ergo', title: 'Ergonomic Split Keyboard for Typists', description: 'Split layout that eases wrist strain over long typing sessions.', price: 89.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-compact', title: 'Compact Wireless Keyboard', description: 'Tenkeyless board that frees desk space.', price: 74.99, inStock: false, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'case-kb', title: 'Folio Keyboard Case for Tablets and Phones', description: 'Bluetooth folio case that turns a tablet into a laptop.', price: 49.99, inStock: true, category: 'accessories', tags: ['bluetooth', 'travel'] },
{ id: 'rest-wrist', title: 'Keyboard Wrist Rest', description: 'Memory-foam rest that supports your wrists at the keyboard.', price: 24.99, inStock: true, category: 'accessories', tags: ['ergonomic'] },
])
const results = await narsil.query('products', {
term: 'keyboard',
})
console.log(JSON.stringify(results, (k, v) => (typeof v === 'number' ? Number(v.toFixed(2)) : v), 2))All six products mention keyboards, so all six come back. The wrist rest ranks first because keyboard appears in both its title and its description, so the term matches twice. The two shortest titles, Mechanical Keyboard and Wireless Keyboard, tie next, because BM25 weighs a match against field length and gives a shorter field more weight. The longer titles score below them.
Narrow with filters
Filters restrict which candidates the search scores. Filter on any indexed field with comparison operators (eq, ne, gt, lt, gte, lte, between), string operators (in, nin, startsWith, endsWith), array operators (containsAll, matchesAny, size), and presence checks (exists, notExists, isEmpty, isNotEmpty). Field conditions go under fields, and the and, or, and not combinators nest whole filter expressions, so any boolean shape is expressible.
Add a filters block so that the query keeps only in-stock electronics or accessories priced between 10 and 100 that carry a bluetooth tag.
import { createNarsil } from '@delali/narsil'
const narsil = await createNarsil()
await narsil.createIndex('products', {
schema: {
title: 'string',
description: 'string',
price: 'number',
inStock: 'boolean',
category: 'enum',
tags: 'string[]',
},
language: 'english',
})
await narsil.insertBatch('products', [
{ id: 'kb-mech', title: 'Mechanical Keyboard', description: 'Cherry MX Brown switches with PBT keycaps for tactile typing.', price: 129.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-lite', title: 'Wireless Keyboard', description: 'Slim low-profile board with a long battery life.', price: 59.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-ergo', title: 'Ergonomic Split Keyboard for Typists', description: 'Split layout that eases wrist strain over long typing sessions.', price: 89.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-compact', title: 'Compact Wireless Keyboard', description: 'Tenkeyless board that frees desk space.', price: 74.99, inStock: false, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'case-kb', title: 'Folio Keyboard Case for Tablets and Phones', description: 'Bluetooth folio case that turns a tablet into a laptop.', price: 49.99, inStock: true, category: 'accessories', tags: ['bluetooth', 'travel'] },
{ id: 'rest-wrist', title: 'Keyboard Wrist Rest', description: 'Memory-foam rest that supports your wrists at the keyboard.', price: 24.99, inStock: true, category: 'accessories', tags: ['ergonomic'] },
])
const results = await narsil.query('products', {
term: 'keyboard',
filters: {
or: [
{ fields: { category: { eq: 'electronics' } } },
{ fields: { category: { eq: 'accessories' } } },
],
fields: {
inStock: { eq: true },
price: { between: [10, 100] },
tags: { containsAll: ['bluetooth'] },
},
},
})
console.log(JSON.stringify(results, (k, v) => (typeof v === 'number' ? Number(v.toFixed(2)) : v), 2))That narrows six hits to three. The mechanical keyboard drops out on price, because 129.99 is above the range. The compact keyboard drops out because it is out of stock. The wrist rest drops out because it carries no bluetooth tag. In vector and hybrid modes the same filters restrict which documents the vector search considers, not only which scored hits survive.
Count values with facets
Facets return value counts alongside the hits, which is how a filter sidebar shows how many products belong to each option. String and enum facets take a limit and a sort direction, and numeric facets take explicit ranges. Add a facets block that counts categories and buckets prices.
import { createNarsil } from '@delali/narsil'
const narsil = await createNarsil()
await narsil.createIndex('products', {
schema: {
title: 'string',
description: 'string',
price: 'number',
inStock: 'boolean',
category: 'enum',
tags: 'string[]',
},
language: 'english',
})
await narsil.insertBatch('products', [
{ id: 'kb-mech', title: 'Mechanical Keyboard', description: 'Cherry MX Brown switches with PBT keycaps for tactile typing.', price: 129.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-lite', title: 'Wireless Keyboard', description: 'Slim low-profile board with a long battery life.', price: 59.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-ergo', title: 'Ergonomic Split Keyboard for Typists', description: 'Split layout that eases wrist strain over long typing sessions.', price: 89.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-compact', title: 'Compact Wireless Keyboard', description: 'Tenkeyless board that frees desk space.', price: 74.99, inStock: false, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'case-kb', title: 'Folio Keyboard Case for Tablets and Phones', description: 'Bluetooth folio case that turns a tablet into a laptop.', price: 49.99, inStock: true, category: 'accessories', tags: ['bluetooth', 'travel'] },
{ id: 'rest-wrist', title: 'Keyboard Wrist Rest', description: 'Memory-foam rest that supports your wrists at the keyboard.', price: 24.99, inStock: true, category: 'accessories', tags: ['ergonomic'] },
])
const results = await narsil.query('products', {
term: 'keyboard',
filters: {
or: [
{ fields: { category: { eq: 'electronics' } } },
{ fields: { category: { eq: 'accessories' } } },
],
fields: {
inStock: { eq: true },
price: { between: [10, 100] },
tags: { containsAll: ['bluetooth'] },
},
},
facets: {
category: { limit: 10, sort: 'desc' },
price: {
ranges: [{ from: 0, to: 50 }, { from: 50, to: 100 }],
},
},
})
console.log(JSON.stringify(results, (k, v) => (typeof v === 'number' ? Number(v.toFixed(2)) : v), 2))The counts arrive under results.facets, keyed by field. Two of the three hits are electronics and one is an accessory, and by price two fall in the 50 to 100 bucket and one below 50. A sidebar reads these counts straight into its checkboxes and price ranges.
Sort and group hits
sort orders hits by field values instead of by score. Multiple entries apply in order, so the second field breaks ties in the first. group then merges hits that share field values, maxPerGroup limits how many hits each group keeps, and an optional reducer combines every grouped document into a single value. Sort cheapest first and group the results by category.
import { createNarsil } from '@delali/narsil'
const narsil = await createNarsil()
await narsil.createIndex('products', {
schema: {
title: 'string',
description: 'string',
price: 'number',
inStock: 'boolean',
category: 'enum',
tags: 'string[]',
},
language: 'english',
})
await narsil.insertBatch('products', [
{ id: 'kb-mech', title: 'Mechanical Keyboard', description: 'Cherry MX Brown switches with PBT keycaps for tactile typing.', price: 129.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-lite', title: 'Wireless Keyboard', description: 'Slim low-profile board with a long battery life.', price: 59.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-ergo', title: 'Ergonomic Split Keyboard for Typists', description: 'Split layout that eases wrist strain over long typing sessions.', price: 89.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-compact', title: 'Compact Wireless Keyboard', description: 'Tenkeyless board that frees desk space.', price: 74.99, inStock: false, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'case-kb', title: 'Folio Keyboard Case for Tablets and Phones', description: 'Bluetooth folio case that turns a tablet into a laptop.', price: 49.99, inStock: true, category: 'accessories', tags: ['bluetooth', 'travel'] },
{ id: 'rest-wrist', title: 'Keyboard Wrist Rest', description: 'Memory-foam rest that supports your wrists at the keyboard.', price: 24.99, inStock: true, category: 'accessories', tags: ['ergonomic'] },
])
const results = await narsil.query('products', {
term: 'keyboard',
filters: {
or: [
{ fields: { category: { eq: 'electronics' } } },
{ fields: { category: { eq: 'accessories' } } },
],
fields: {
inStock: { eq: true },
price: { between: [10, 100] },
tags: { containsAll: ['bluetooth'] },
},
},
sort: { price: 'asc', title: 'asc' },
group: {
fields: ['category'],
maxPerGroup: 3,
},
})
console.log(JSON.stringify(results, (k, v) => (typeof v === 'number' ? Number(v.toFixed(2)) : v), 2))results.hits now runs cheapest first, from the folio case at 49.99 up to the split keyboard at 89.99. results.groups collects the same hits by category, at most three per group: the electronics group holds the two keyboards, and the accessories group holds the folio case.
Paginate a page at a time
A results page requires a size and a way to reach the next one. Shallow pagination sets limit for the page size and offset to skip into the results. Ask for two hits at a time so that the three filtered products span two pages.
import { createNarsil } from '@delali/narsil'
const narsil = await createNarsil()
await narsil.createIndex('products', {
schema: {
title: 'string',
description: 'string',
price: 'number',
inStock: 'boolean',
category: 'enum',
tags: 'string[]',
},
language: 'english',
})
await narsil.insertBatch('products', [
{ id: 'kb-mech', title: 'Mechanical Keyboard', description: 'Cherry MX Brown switches with PBT keycaps for tactile typing.', price: 129.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-lite', title: 'Wireless Keyboard', description: 'Slim low-profile board with a long battery life.', price: 59.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-ergo', title: 'Ergonomic Split Keyboard for Typists', description: 'Split layout that eases wrist strain over long typing sessions.', price: 89.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-compact', title: 'Compact Wireless Keyboard', description: 'Tenkeyless board that frees desk space.', price: 74.99, inStock: false, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'case-kb', title: 'Folio Keyboard Case for Tablets and Phones', description: 'Bluetooth folio case that turns a tablet into a laptop.', price: 49.99, inStock: true, category: 'accessories', tags: ['bluetooth', 'travel'] },
{ id: 'rest-wrist', title: 'Keyboard Wrist Rest', description: 'Memory-foam rest that supports your wrists at the keyboard.', price: 24.99, inStock: true, category: 'accessories', tags: ['ergonomic'] },
])
const results = await narsil.query('products', {
term: 'keyboard',
filters: {
or: [
{ fields: { category: { eq: 'electronics' } } },
{ fields: { category: { eq: 'accessories' } } },
],
fields: {
inStock: { eq: true },
price: { between: [10, 100] },
tags: { containsAll: ['bluetooth'] },
},
},
limit: 2,
})
console.log(JSON.stringify(results, (k, v) => (typeof v === 'number' ? Number(v.toFixed(2)) : v), 2))The first page holds the top two hits by relevance. count still reports 3, the total that match, and the page carries a cursor for the next step. To reach the second page with offset, skip the two you have already read.
import { createNarsil } from '@delali/narsil'
const narsil = await createNarsil()
await narsil.createIndex('products', {
schema: {
title: 'string',
description: 'string',
price: 'number',
inStock: 'boolean',
category: 'enum',
tags: 'string[]',
},
language: 'english',
})
await narsil.insertBatch('products', [
{ id: 'kb-mech', title: 'Mechanical Keyboard', description: 'Cherry MX Brown switches with PBT keycaps for tactile typing.', price: 129.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-lite', title: 'Wireless Keyboard', description: 'Slim low-profile board with a long battery life.', price: 59.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-ergo', title: 'Ergonomic Split Keyboard for Typists', description: 'Split layout that eases wrist strain over long typing sessions.', price: 89.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-compact', title: 'Compact Wireless Keyboard', description: 'Tenkeyless board that frees desk space.', price: 74.99, inStock: false, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'case-kb', title: 'Folio Keyboard Case for Tablets and Phones', description: 'Bluetooth folio case that turns a tablet into a laptop.', price: 49.99, inStock: true, category: 'accessories', tags: ['bluetooth', 'travel'] },
{ id: 'rest-wrist', title: 'Keyboard Wrist Rest', description: 'Memory-foam rest that supports your wrists at the keyboard.', price: 24.99, inStock: true, category: 'accessories', tags: ['ergonomic'] },
])
const results = await narsil.query('products', {
term: 'keyboard',
filters: {
or: [
{ fields: { category: { eq: 'electronics' } } },
{ fields: { category: { eq: 'accessories' } } },
],
fields: {
inStock: { eq: true },
price: { between: [10, 100] },
tags: { containsAll: ['bluetooth'] },
},
},
limit: 2,
offset: 2,
})
console.log(JSON.stringify(results, (k, v) => (typeof v === 'number' ? Number(v.toFixed(2)) : v), 2))offset reads well for the first few pages. It grows more expensive the deeper a reader scrolls, though, because the engine walks every skipped hit before it reaches the page you asked for.
Switch to deep cursor pagination
searchAfter cursors keep latency flat at any depth by tracking a position in the ranking instead of counting from the start. Every page carries a cursor string, so you pass it straight back as searchAfter to fetch the next page. A cursor is only valid for the exact query parameters it came from. Lift the shared parameters into a page object and pass it to each call so that the second query matches the first and the cursor stays valid. Annotate that object as QueryParams, because between takes a pair of numbers and an unannotated object literal loosens [10, 100] into a list of any length. Fetch the first page, then use its cursor to fetch the second.
import { createNarsil } from '@delali/narsil'
import type { QueryParams } from '@delali/narsil'
const narsil = await createNarsil()
await narsil.createIndex('products', {
schema: {
title: 'string',
description: 'string',
price: 'number',
inStock: 'boolean',
category: 'enum',
tags: 'string[]',
},
language: 'english',
})
await narsil.insertBatch('products', [
{ id: 'kb-mech', title: 'Mechanical Keyboard', description: 'Cherry MX Brown switches with PBT keycaps for tactile typing.', price: 129.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-lite', title: 'Wireless Keyboard', description: 'Slim low-profile board with a long battery life.', price: 59.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-ergo', title: 'Ergonomic Split Keyboard for Typists', description: 'Split layout that eases wrist strain over long typing sessions.', price: 89.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-compact', title: 'Compact Wireless Keyboard', description: 'Tenkeyless board that frees desk space.', price: 74.99, inStock: false, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'case-kb', title: 'Folio Keyboard Case for Tablets and Phones', description: 'Bluetooth folio case that turns a tablet into a laptop.', price: 49.99, inStock: true, category: 'accessories', tags: ['bluetooth', 'travel'] },
{ id: 'rest-wrist', title: 'Keyboard Wrist Rest', description: 'Memory-foam rest that supports your wrists at the keyboard.', price: 24.99, inStock: true, category: 'accessories', tags: ['ergonomic'] },
])
const page: QueryParams = {
term: 'keyboard',
filters: {
or: [
{ fields: { category: { eq: 'electronics' } } },
{ fields: { category: { eq: 'accessories' } } },
],
fields: {
inStock: { eq: true },
price: { between: [10, 100] },
tags: { containsAll: ['bluetooth'] },
},
},
limit: 2,
}
const firstPage = await narsil.query('products', page)
const secondPage = await narsil.query('products', {
...page,
searchAfter: firstPage.cursor,
})
console.log(JSON.stringify(secondPage, (k, v) => (typeof v === 'number' ? Number(v.toFixed(2)) : v), 2))firstPage returns the same top two hits and its cursor. Passing that cursor as searchAfter returns the third hit, the folio case, and the page comes back with no cursor, which signals the end of the results. A malformed or mismatched cursor fails with SEARCH_INVALID_CURSOR, which is why reusing the same page object matters.
Browse an index without a search term
A searchAfter cursor marks a position in a ranking, so it works only for a query that carries a term. Say an operator opens an admin screen to review the whole catalogue, one page at a time. listDocuments covers that case: it pages through the stored documents without ranking them. It takes the same filter expression and the same sort object a query takes. Narrow the listing to the products in stock. Order it by price, and set document so that each entry carries the two fields a browse table shows.
import { createNarsil } from '@delali/narsil'
const narsil = await createNarsil()
await narsil.createIndex('products', {
schema: {
title: 'string',
description: 'string',
price: 'number',
inStock: 'boolean',
category: 'enum',
tags: 'string[]',
},
language: 'english',
})
await narsil.insertBatch('products', [
{ id: 'kb-mech', title: 'Mechanical Keyboard', description: 'Cherry MX Brown switches with PBT keycaps for tactile typing.', price: 129.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-lite', title: 'Wireless Keyboard', description: 'Slim low-profile board with a long battery life.', price: 59.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-ergo', title: 'Ergonomic Split Keyboard for Typists', description: 'Split layout that eases wrist strain over long typing sessions.', price: 89.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-compact', title: 'Compact Wireless Keyboard', description: 'Tenkeyless board that frees desk space.', price: 74.99, inStock: false, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'case-kb', title: 'Folio Keyboard Case for Tablets and Phones', description: 'Bluetooth folio case that turns a tablet into a laptop.', price: 49.99, inStock: true, category: 'accessories', tags: ['bluetooth', 'travel'] },
{ id: 'rest-wrist', title: 'Keyboard Wrist Rest', description: 'Memory-foam rest that supports your wrists at the keyboard.', price: 24.99, inStock: true, category: 'accessories', tags: ['ergonomic'] },
])
const inStock = await narsil.listDocuments('products', {
limit: 3,
filters: { fields: { inStock: { eq: true } } },
sort: { price: 'desc' },
document: { include: ['title', 'price'] },
})
console.log(JSON.stringify(inStock, (k, v) => (typeof v === 'number' ? Number(v.toFixed(2)) : v), 2)) The filter accepts five products, which is the count total reports. The page carries the three most expensive of them. Pass its cursor back on the next call to reach the other two.
The sort fields apply in the order the object lists them, and document id breaks a tie. A document missing a sorted field comes last, under asc and desc alike. Narsil compares strings without case or accents, so apple precedes Banana. It sorts by at most eight fields, because the cursor carries one value for each of them.
A cursor belongs to the sort that produced it. Send it back with the same sort object, since Narsil answers SEARCH_INVALID_CURSOR for a cursor sent under a different order. Leaving sort out returns the listing to document-id order.
Narsil reads every document the listing covers to build a sorted page, so a sorted listing costs more than the default order on a large index. It holds one page at a time while it selects, and memory follows the page size rather than the index size. The elapsed above includes the one-time setup of the string collator, which the first sorted call in a process pays for. Later sorted pages come back faster.
document sets how much of each stored document comes back. include keeps the named fields. exclude drops them and keeps the rest. false empties every entry's document, so only the ids come back. Each entry keeps its own id, whatever the projection does to the document. The three entries above still identify their products. query takes the same document option, and it matters most on a vector search. The engine otherwise reads every hit's vector back out of the index and writes it into the response.
Indexes and documents covers what a listing guarantees while writes continue.
Pin editorial results
pinned places specific documents at fixed positions in the ranked results, which serves sponsored or editorial placements. Positions are zero-based and apply on top of whichever ranking, sort, or pagination the query already uses. Pin the mechanical keyboard to the top slot.
import { createNarsil } from '@delali/narsil'
const narsil = await createNarsil()
await narsil.createIndex('products', {
schema: {
title: 'string',
description: 'string',
price: 'number',
inStock: 'boolean',
category: 'enum',
tags: 'string[]',
},
language: 'english',
})
await narsil.insertBatch('products', [
{ id: 'kb-mech', title: 'Mechanical Keyboard', description: 'Cherry MX Brown switches with PBT keycaps for tactile typing.', price: 129.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-lite', title: 'Wireless Keyboard', description: 'Slim low-profile board with a long battery life.', price: 59.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-ergo', title: 'Ergonomic Split Keyboard for Typists', description: 'Split layout that eases wrist strain over long typing sessions.', price: 89.99, inStock: true, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'kb-compact', title: 'Compact Wireless Keyboard', description: 'Tenkeyless board that frees desk space.', price: 74.99, inStock: false, category: 'electronics', tags: ['bluetooth', 'typing'] },
{ id: 'case-kb', title: 'Folio Keyboard Case for Tablets and Phones', description: 'Bluetooth folio case that turns a tablet into a laptop.', price: 49.99, inStock: true, category: 'accessories', tags: ['bluetooth', 'travel'] },
{ id: 'rest-wrist', title: 'Keyboard Wrist Rest', description: 'Memory-foam rest that supports your wrists at the keyboard.', price: 24.99, inStock: true, category: 'accessories', tags: ['ergonomic'] },
])
const results = await narsil.query('products', {
term: 'keyboard',
pinned: [{ docId: 'kb-mech', position: 0 }],
})
console.log(JSON.stringify(results, (k, v) => (typeof v === 'number' ? Number(v.toFixed(2)) : v), 2))Narsil places kb-mech at position zero with a score of 0, whether or not the search would have ranked it there, and the rest of the hits follow below in their usual order.