> Complete content index: https://narsil.sondelali.com/llms.txt

# Filters, facets, and pagination

> Narrow results with typed filters, build filter interfaces with facets, sort and group hits, and paginate at any depth.

URL: https://narsil.sondelali.com/docs/filters-facets-and-pagination
Section: Core concepts
Version: 0.2 (@delali/narsil@0.2.2, latest)

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.

```ts title="search.ts"

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 <Tooltip tip="BM25 is the standard keyword ranking formula. It scores a document higher when the query terms appear often in it, are rare across the whole collection, and appear in shorter fields.">BM25</Tooltip> relevance score.

```ts title="search.ts"

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', { // [!code ++:5]
  term: 'keyboard',
})

console.log(JSON.stringify(results, (k, v) => (typeof v === 'number' ? Number(v.toFixed(2)) : v), 2))
```

```json result
{
  "hits": [
    {
      "id": "rest-wrist",
      "score": 0.15,
      "document": {
        "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"
        ]
      }
    },
    {
      "id": "kb-mech",
      "score": 0.09,
      "document": {
        "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",
      "score": 0.09,
      "document": {
        "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-compact",
      "score": 0.08,
      "document": {
        "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": "kb-ergo",
      "score": 0.07,
      "document": {
        "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": "case-kb",
      "score": 0.06,
      "document": {
        "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"
        ]
      }
    }
  ],
  "count": 6,
  "elapsed": 0.66
}
```

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.

```ts title="search.ts"

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: { // [!code ++:11]
    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))
```

```json result
{
  "hits": [
    {
      "id": "kb-lite",
      "score": 0.09,
      "document": {
        "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",
      "score": 0.07,
      "document": {
        "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": "case-kb",
      "score": 0.06,
      "document": {
        "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"
        ]
      }
    }
  ],
  "count": 3,
  "elapsed": 0.29
}
```

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.

```ts title="search.ts"

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: { // [!code ++:6]
    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))
```

```json result
{
  "hits": [
    {
      "id": "kb-lite",
      "score": 0.09,
      "document": {
        "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",
      "score": 0.07,
      "document": {
        "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": "case-kb",
      "score": 0.06,
      "document": {
        "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"
        ]
      }
    }
  ],
  "count": 3,
  "elapsed": 0.31,
  "facets": {
    "category": {
      "values": {
        "electronics": 2,
        "accessories": 1
      },
      "count": 2
    },
    "price": {
      "values": {
        "50-100": 2,
        "0-50": 1
      },
      "count": 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.

```ts title="search.ts"

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' }, // [!code ++:5]
  group: {
    fields: ['category'],
    maxPerGroup: 3,
  },
})

console.log(JSON.stringify(results, (k, v) => (typeof v === 'number' ? Number(v.toFixed(2)) : v), 2))
```

```json result
{
  "hits": [
    {
      "id": "case-kb",
      "score": 0.06,
      "document": {
        "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": "kb-lite",
      "score": 0.09,
      "document": {
        "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",
      "score": 0.07,
      "document": {
        "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"
        ]
      }
    }
  ],
  "count": 3,
  "elapsed": 0.42,
  "groups": [
    {
      "values": {
        "category": "electronics"
      },
      "hits": [
        {
          "id": "kb-lite",
          "score": 0.09,
          "document": {
            "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",
          "score": 0.07,
          "document": {
            "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"
            ]
          }
        }
      ]
    },
    {
      "values": {
        "category": "accessories"
      },
      "hits": [
        {
          "id": "case-kb",
          "score": 0.06,
          "document": {
            "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"
            ]
          }
        }
      ]
    }
  ]
}
```

`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.

```ts title="search.ts"

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, // [!code ++]
})

console.log(JSON.stringify(results, (k, v) => (typeof v === 'number' ? Number(v.toFixed(2)) : v), 2))
```

```json result
{
  "hits": [
    {
      "id": "kb-lite",
      "score": 0.09,
      "document": {
        "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",
      "score": 0.07,
      "document": {
        "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"
        ]
      }
    }
  ],
  "count": 3,
  "elapsed": 0.14,
  "cursor": "W3sicyI6MC4wNjY5MDUyNTM0Nzc4NzQxNCwiZCI6ImtiLWVyZ28iLCJwIjowfV0="
}
```

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.

```ts title="search.ts"

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, // [!code ++]
})

console.log(JSON.stringify(results, (k, v) => (typeof v === 'number' ? Number(v.toFixed(2)) : v), 2))
```

```json result
{
  "hits": [
    {
      "id": "case-kb",
      "score": 0.06,
      "document": {
        "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"
        ]
      }
    }
  ],
  "count": 3,
  "elapsed": 0.06
}
```

`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.

```ts title="search.ts"

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) // [!code ++:6]

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))
```

```json result
{
  "hits": [
    {
      "id": "case-kb",
      "score": 0.06,
      "document": {
        "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"
        ]
      }
    }
  ],
  "count": 3,
  "elapsed": 0.11
}
```

`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.

```ts title="search.ts"

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', { // [!code ++:6]
  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)) // [!code ++]
```

```json result
{
  "documents": [
    {
      "id": "kb-mech",
      "document": {
        "title": "Mechanical Keyboard",
        "price": 129.99
      }
    },
    {
      "id": "kb-ergo",
      "document": {
        "title": "Ergonomic Split Keyboard for Typists",
        "price": 89.99
      }
    },
    {
      "id": "kb-lite",
      "document": {
        "title": "Wireless Keyboard",
        "price": 59.99
      }
    }
  ],
  "cursor": "eyJ2IjoxLCJhIjoia2ItbGl0ZSIsImsiOls1OS45OV0sInMiOiJbW1wicHJpY2VcIixcImRlc2NcIl1dIn0=",
  "total": 5,
  "elapsed": 6.42
}
```

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](/docs/indexes-and-documents#page-through-every-document) 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.

```ts title="search.ts"

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 }], // [!code ++]
})

console.log(JSON.stringify(results, (k, v) => (typeof v === 'number' ? Number(v.toFixed(2)) : v), 2))
```

```json result
{
  "hits": [
    {
      "id": "kb-mech",
      "score": 0,
      "document": {
        "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": "rest-wrist",
      "score": 0.15,
      "document": {
        "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"
        ]
      }
    },
    {
      "id": "kb-lite",
      "score": 0.09,
      "document": {
        "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-compact",
      "score": 0.08,
      "document": {
        "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": "kb-ergo",
      "score": 0.07,
      "document": {
        "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": "case-kb",
      "score": 0.06,
      "document": {
        "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"
        ]
      }
    }
  ],
  "count": 6,
  "elapsed": 0.37
}
```

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.
