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

# Sorting, grouping, and pagination

> Order hits by field values, collapse them into groups with a reducer, page through them by offset or by cursor, browse an index in any order, and pin a result in place.

URL: https://narsil.sondelali.com/docs/sorting-grouping-and-pagination
Section: Core concepts
Version: 0.3 (@delali/narsil@0.3.0, latest)

The engine ranks a search's hits by score until you ask for another order. You build one file here, `search.ts`, in which you sort a bicycle catalogue by field values, collapse the hits into groups, and page through them by offset and by cursor. At the end, you use two short files to browse the catalogue without a search term and to pin a sponsored result in place. Each code block below holds a whole file, with green lines marking what each step adds and struck lines marking what it takes away.

## Order the hits by field values

With `sort`, the engine orders the hits by field values in place of their scores, and it uses each further field to break the ties that the one before it leaves. A query may sort on a `number`, `enum`, `boolean`, or `string:sortable` field. The schema declares `maker` as `string:sortable` for that reason, because a sort on a plain `string` field fails with `SEARCH_INVALID_FIELD`.

```ts title="search.ts"
import { createNarsil } from '@delali/narsil'

const narsil = await createNarsil()

await narsil.createIndex('bicycles', {
  schema: {
    name: 'string',
    summary: 'string',
    maker: 'string:sortable',
    category: 'enum',
    price: 'number',
    rating: 'number',
    inStock: 'boolean',
    tags: 'string[]',
  },
})

await narsil.insertBatch('bicycles', [
  { id: 'urban-commuter', name: 'Gomes Urban commuter', summary: 'A lightweight aluminium commuter bicycle with hydraulic disc brakes.', maker: 'Gomes Cycles', category: 'city', price: 749, rating: 4.5, inStock: true, tags: ['commuter', 'aluminium'] },
  { id: 'city-step', name: 'Gomes Step-through city bicycle', summary: 'A step-through city bicycle with a chain guard and a hub dynamo.', maker: 'Gomes Cycles', category: 'city', price: 549, rating: 4.0, inStock: true, tags: ['commuter', 'steel'] },
  { id: 'gravel-explorer', name: 'Mori Gravel explorer', summary: 'A steel gravel bicycle built for long unpaved rides.', maker: 'Mori Works', category: 'gravel', price: 1299, rating: 4.8, inStock: true, tags: ['gravel', 'steel'] },
  { id: 'gravel-allroad', name: 'Mori Allroad gravel bicycle', summary: 'An aluminium gravel bicycle with mounts for three bottles.', maker: 'Mori Works', category: 'gravel', price: 999, rating: 4.4, inStock: false, tags: ['gravel', 'aluminium'] },
  { id: 'folding-metro', name: 'Obi Metro folding bicycle', summary: 'A folding bicycle that fits under a train seat.', maker: 'Obi Mobility', category: 'folding', price: 899, rating: 4.2, inStock: true, tags: ['folding', 'commuter'] },
  { id: 'folding-tourer', name: 'Obi Tourer folding bicycle', summary: 'A folding bicycle with a rear rack and eight gears.', maker: 'Obi Mobility', category: 'folding', price: 1150, rating: 4.1, inStock: true, tags: ['folding', 'aluminium'] },
  { id: 'cargo-hauler', name: 'Larsson Cargo hauler', summary: 'An electric cargo bicycle rated to carry two children.', maker: 'Larsson Bikes', category: 'cargo', price: 3499, rating: 4.6, inStock: true, tags: ['cargo', 'electric'] },
  { id: 'cargo-trike', name: 'Larsson Cargo trike', summary: 'A three-wheeled cargo bicycle with a lockable box.', maker: 'Larsson Bikes', category: 'cargo', price: 4195, rating: 4.3, inStock: false, tags: ['cargo', 'electric'] },
  { id: 'road-racer', name: 'Khoury Road racer', summary: 'A carbon road bicycle with electronic shifting.', maker: 'Khoury Composites', category: 'road', price: 4250, rating: 4.9, inStock: true, tags: ['road', 'carbon'] },
  { id: 'road-endurance', name: 'Khoury Endurance road bicycle', summary: 'A carbon road bicycle tuned for comfort over long distances.', maker: 'Khoury Composites', category: 'road', price: 2799, rating: 4.7, inStock: true, tags: ['road', 'carbon'] },
  { id: 'kids-first', name: 'Adams First ride bicycle', summary: 'A first bicycle for a child, with stabilisers that come off in one minute.', maker: 'Adams Assembly', category: 'kids', price: 189, rating: 4.3, inStock: true, tags: ['kids'] },
  { id: 'kids-junior', name: 'Adams Junior mountain bicycle', summary: 'A junior mountain bicycle with a suspension fork and six gears.', maker: 'Adams Assembly', category: 'kids', price: 329, rating: 4.1, inStock: true, tags: ['kids', 'aluminium'] },
])

const results = await narsil.query('bicycles', {
  term: 'bicycle',
  filters: { fields: { inStock: { eq: true } } },
  sort: { maker: 'asc', price: 'asc' },
  document: { include: ['maker', 'price'] },
  limit: 6,
})

console.log(JSON.stringify(results.hits, null, 2))

await narsil.shutdown()
```

```json result open
[
  {
    "id": "kids-first",
    "document": {
      "maker": "Adams Assembly",
      "price": 189
    }
  },
  {
    "id": "kids-junior",
    "document": {
      "maker": "Adams Assembly",
      "price": 329
    }
  },
  {
    "id": "city-step",
    "document": {
      "maker": "Gomes Cycles",
      "price": 549
    }
  },
  {
    "id": "urban-commuter",
    "document": {
      "maker": "Gomes Cycles",
      "price": 749
    }
  },
  {
    "id": "road-endurance",
    "document": {
      "maker": "Khoury Composites",
      "price": 2799
    }
  },
  {
    "id": "road-racer",
    "document": {
      "maker": "Khoury Composites",
      "price": 4250
    }
  }
]
```

The engine orders the hits by maker and then by price within each maker. The hits hold no `score`, because the engine ranks a sorted query by its sort values alone, so set `includeScores: true` where you want the scores back. The engine compares text by its case-folded form, so apple sorts before Banana; it leaves accents unfolded, however, so Ärla and Ćevapi sort after cevapi. A document that lacks the sorted field comes last under `asc` and under `desc` alike.

A sort takes at most eight fields, because the cursor holds one value for each of them. Write the sort as a list of `{ field, direction }` entries wherever a field name is a whole number such as 2024, since JavaScript moves such keys to the front of an object and the engine would then apply that field first. A hybrid query takes no sort, because its fusion sets the order, so the engine fails a query that asks for both with `SEARCH_INVALID_MODE`.

## Collapse the hits into groups

With `group`, the engine gathers the hits that share values in the named fields. `maxPerGroup` sets how many hits the engine keeps in each group, while `reduce` gives the engine a function that folds each group's hits into one value. The engine groups the whole matched set before `limit` applies, so `limit: 0` returns every group with no hits beside them.

```ts title="search.ts"
import { createNarsil } from '@delali/narsil'

const narsil = await createNarsil()

await narsil.createIndex('bicycles', {
  schema: {
    name: 'string',
    summary: 'string',
    maker: 'string:sortable',
    category: 'enum',
    price: 'number',
    rating: 'number',
    inStock: 'boolean',
    tags: 'string[]',
  },
})

await narsil.insertBatch('bicycles', [
  { id: 'urban-commuter', name: 'Gomes Urban commuter', summary: 'A lightweight aluminium commuter bicycle with hydraulic disc brakes.', maker: 'Gomes Cycles', category: 'city', price: 749, rating: 4.5, inStock: true, tags: ['commuter', 'aluminium'] },
  { id: 'city-step', name: 'Gomes Step-through city bicycle', summary: 'A step-through city bicycle with a chain guard and a hub dynamo.', maker: 'Gomes Cycles', category: 'city', price: 549, rating: 4.0, inStock: true, tags: ['commuter', 'steel'] },
  { id: 'gravel-explorer', name: 'Mori Gravel explorer', summary: 'A steel gravel bicycle built for long unpaved rides.', maker: 'Mori Works', category: 'gravel', price: 1299, rating: 4.8, inStock: true, tags: ['gravel', 'steel'] },
  { id: 'gravel-allroad', name: 'Mori Allroad gravel bicycle', summary: 'An aluminium gravel bicycle with mounts for three bottles.', maker: 'Mori Works', category: 'gravel', price: 999, rating: 4.4, inStock: false, tags: ['gravel', 'aluminium'] },
  { id: 'folding-metro', name: 'Obi Metro folding bicycle', summary: 'A folding bicycle that fits under a train seat.', maker: 'Obi Mobility', category: 'folding', price: 899, rating: 4.2, inStock: true, tags: ['folding', 'commuter'] },
  { id: 'folding-tourer', name: 'Obi Tourer folding bicycle', summary: 'A folding bicycle with a rear rack and eight gears.', maker: 'Obi Mobility', category: 'folding', price: 1150, rating: 4.1, inStock: true, tags: ['folding', 'aluminium'] },
  { id: 'cargo-hauler', name: 'Larsson Cargo hauler', summary: 'An electric cargo bicycle rated to carry two children.', maker: 'Larsson Bikes', category: 'cargo', price: 3499, rating: 4.6, inStock: true, tags: ['cargo', 'electric'] },
  { id: 'cargo-trike', name: 'Larsson Cargo trike', summary: 'A three-wheeled cargo bicycle with a lockable box.', maker: 'Larsson Bikes', category: 'cargo', price: 4195, rating: 4.3, inStock: false, tags: ['cargo', 'electric'] },
  { id: 'road-racer', name: 'Khoury Road racer', summary: 'A carbon road bicycle with electronic shifting.', maker: 'Khoury Composites', category: 'road', price: 4250, rating: 4.9, inStock: true, tags: ['road', 'carbon'] },
  { id: 'road-endurance', name: 'Khoury Endurance road bicycle', summary: 'A carbon road bicycle tuned for comfort over long distances.', maker: 'Khoury Composites', category: 'road', price: 2799, rating: 4.7, inStock: true, tags: ['road', 'carbon'] },
  { id: 'kids-first', name: 'Adams First ride bicycle', summary: 'A first bicycle for a child, with stabilisers that come off in one minute.', maker: 'Adams Assembly', category: 'kids', price: 189, rating: 4.3, inStock: true, tags: ['kids'] },
  { id: 'kids-junior', name: 'Adams Junior mountain bicycle', summary: 'A junior mountain bicycle with a suspension fork and six gears.', maker: 'Adams Assembly', category: 'kids', price: 329, rating: 4.1, inStock: true, tags: ['kids', 'aluminium'] },
])

const results = await narsil.query('bicycles', {
  term: 'bicycle',
  filters: { fields: { inStock: { eq: true } } },
  sort: { maker: 'asc', price: 'asc' }, // [!code --]
  document: { include: ['maker', 'price'] }, // [!code --]
  limit: 6, // [!code --]
  sort: { price: 'asc' }, // [!code ++:7]
  group: {
    fields: ['category'],
    maxPerGroup: 2,
    reduce: { initialValue: () => 0, reducer: (count) => Number(count) + 1 },
  },
  limit: 0,
})

console.log(JSON.stringify(results.hits, null, 2)) // [!code --]
console.log( // [!code ++:7]
  JSON.stringify(
    results.groups?.map((group) => ({ ...group.values, hits: group.hits.map((hit) => hit.id), counted: group.reduced })),
    null,
    2
  )
)

await narsil.shutdown()
```

```json result open
[
  {
    "category": "folding",
    "hits": [
      "folding-metro",
      "folding-tourer"
    ],
    "counted": 2
  },
  {
    "category": "city",
    "hits": [
      "city-step",
      "urban-commuter"
    ],
    "counted": 2
  },
  {
    "category": "kids",
    "hits": [
      "kids-first",
      "kids-junior"
    ],
    "counted": 2
  },
  {
    "category": "road",
    "hits": [
      "road-endurance",
      "road-racer"
    ],
    "counted": 2
  },
  {
    "category": "cargo",
    "hits": [
      "cargo-hauler"
    ],
    "counted": 1
  },
  {
    "category": "gravel",
    "hits": [
      "gravel-explorer"
    ],
    "counted": 1
  }
]
```

The engine keeps the two cheapest bicycles in stock for each category, since it sorts the hits by price before it groups them. The engine passes the reducer every member of the group, whatever `maxPerGroup` keeps for display, so `counted` gives the full size of a category even where the category holds more bicycles than its group shows. Where a group query leaves `maxPerGroup` out, the engine keeps one hit for each group.

## Page through the results

A page comes from `limit` together with either `offset`, which skips that many hits, or `searchAfter`, which takes the `cursor` from the page before. You keep the shared options in one `QueryParams` object, so that both requests for the second page send the same query. The annotation also types `'asc'` as a sort direction in place of a plain string.

```ts title="search.ts"
import { createNarsil } from '@delali/narsil' // [!code --]
import { createNarsil, type QueryParams } from '@delali/narsil' // [!code ++]

const narsil = await createNarsil()

await narsil.createIndex('bicycles', {
  schema: {
    name: 'string',
    summary: 'string',
    maker: 'string:sortable',
    category: 'enum',
    price: 'number',
    rating: 'number',
    inStock: 'boolean',
    tags: 'string[]',
  },
})

await narsil.insertBatch('bicycles', [
  { id: 'urban-commuter', name: 'Gomes Urban commuter', summary: 'A lightweight aluminium commuter bicycle with hydraulic disc brakes.', maker: 'Gomes Cycles', category: 'city', price: 749, rating: 4.5, inStock: true, tags: ['commuter', 'aluminium'] },
  { id: 'city-step', name: 'Gomes Step-through city bicycle', summary: 'A step-through city bicycle with a chain guard and a hub dynamo.', maker: 'Gomes Cycles', category: 'city', price: 549, rating: 4.0, inStock: true, tags: ['commuter', 'steel'] },
  { id: 'gravel-explorer', name: 'Mori Gravel explorer', summary: 'A steel gravel bicycle built for long unpaved rides.', maker: 'Mori Works', category: 'gravel', price: 1299, rating: 4.8, inStock: true, tags: ['gravel', 'steel'] },
  { id: 'gravel-allroad', name: 'Mori Allroad gravel bicycle', summary: 'An aluminium gravel bicycle with mounts for three bottles.', maker: 'Mori Works', category: 'gravel', price: 999, rating: 4.4, inStock: false, tags: ['gravel', 'aluminium'] },
  { id: 'folding-metro', name: 'Obi Metro folding bicycle', summary: 'A folding bicycle that fits under a train seat.', maker: 'Obi Mobility', category: 'folding', price: 899, rating: 4.2, inStock: true, tags: ['folding', 'commuter'] },
  { id: 'folding-tourer', name: 'Obi Tourer folding bicycle', summary: 'A folding bicycle with a rear rack and eight gears.', maker: 'Obi Mobility', category: 'folding', price: 1150, rating: 4.1, inStock: true, tags: ['folding', 'aluminium'] },
  { id: 'cargo-hauler', name: 'Larsson Cargo hauler', summary: 'An electric cargo bicycle rated to carry two children.', maker: 'Larsson Bikes', category: 'cargo', price: 3499, rating: 4.6, inStock: true, tags: ['cargo', 'electric'] },
  { id: 'cargo-trike', name: 'Larsson Cargo trike', summary: 'A three-wheeled cargo bicycle with a lockable box.', maker: 'Larsson Bikes', category: 'cargo', price: 4195, rating: 4.3, inStock: false, tags: ['cargo', 'electric'] },
  { id: 'road-racer', name: 'Khoury Road racer', summary: 'A carbon road bicycle with electronic shifting.', maker: 'Khoury Composites', category: 'road', price: 4250, rating: 4.9, inStock: true, tags: ['road', 'carbon'] },
  { id: 'road-endurance', name: 'Khoury Endurance road bicycle', summary: 'A carbon road bicycle tuned for comfort over long distances.', maker: 'Khoury Composites', category: 'road', price: 2799, rating: 4.7, inStock: true, tags: ['road', 'carbon'] },
  { id: 'kids-first', name: 'Adams First ride bicycle', summary: 'A first bicycle for a child, with stabilisers that come off in one minute.', maker: 'Adams Assembly', category: 'kids', price: 189, rating: 4.3, inStock: true, tags: ['kids'] },
  { id: 'kids-junior', name: 'Adams Junior mountain bicycle', summary: 'A junior mountain bicycle with a suspension fork and six gears.', maker: 'Adams Assembly', category: 'kids', price: 329, rating: 4.1, inStock: true, tags: ['kids', 'aluminium'] },
])

const results = await narsil.query('bicycles', { // [!code --]
const page: QueryParams = { // [!code ++]
  term: 'bicycle',
  filters: { fields: { inStock: { eq: true } } },
  sort: { price: 'asc' },
  group: { // [!code --]
    fields: ['category'], // [!code --]
    maxPerGroup: 2, // [!code --]
    reduce: { initialValue: () => 0, reducer: (count) => Number(count) + 1 }, // [!code --]
  }, // [!code --]
  limit: 0, // [!code --]
}) // [!code --]
  document: false, // [!code ++:9]
  limit: 4,
}

const ids = (hits: Array<{ id: string }>): string[] => hits.map((hit) => hit.id)

const first = await narsil.query('bicycles', page)
const secondByOffset = await narsil.query('bicycles', { ...page, offset: 4 })
const secondByCursor = await narsil.query('bicycles', { ...page, searchAfter: first.cursor })

console.log(
  JSON.stringify(
    results.groups?.map((group) => ({ ...group.values, hits: group.hits.map((hit) => hit.id), counted: group.reduced })), // [!code --]
    { // [!code ++:7]
      count: first.count,
      first: ids(first.hits),
      secondByOffset: ids(secondByOffset.hits),
      secondByCursor: ids(secondByCursor.hits),
      cursorAfterSecond: secondByCursor.cursor !== undefined,
    },
    null,
    2
  )
)

await narsil.shutdown()
```

```json result open
{
  "count": 10,
  "first": [
    "kids-first",
    "kids-junior",
    "city-step",
    "urban-commuter"
  ],
  "secondByOffset": [
    "folding-metro",
    "folding-tourer",
    "gravel-explorer",
    "road-endurance"
  ],
  "secondByCursor": [
    "folding-metro",
    "folding-tourer",
    "gravel-explorer",
    "road-endurance"
  ],
  "cursorAfterSecond": true
}
```

Offset and cursor reach the same four bicycles, while `count` still reports all ten matches. The last page comes back with no `cursor` at all, so stop once `cursor` is `undefined`. The engine binds a cursor to the query that produced it, so it fails the cursor with `SEARCH_INVALID_CURSOR` under a different term, filter, or sort, although it accepts a different `limit`. `offset` and `limit` together reach 10,000 hits at most, and past that the engine fails the request with `SEARCH_RESULT_WINDOW_EXCEEDED`, which leaves the cursor as the way to page deeper.

## Browse an index in any order

`listDocuments` pages through an index without a search term, and it takes the same `filters`, `sort`, and `document` options that a query takes. [Indexes and documents](/docs/indexes-and-documents#page-through-every-document) covers the listing in id order and the rules that hold for it while writes continue, while here you list the bicycles in stock from the most expensive down.

```ts title="browse.ts"
import { createNarsil } from '@delali/narsil'

const narsil = await createNarsil()

await narsil.createIndex('bicycles', {
  schema: {
    name: 'string',
    summary: 'string',
    maker: 'string:sortable',
    category: 'enum',
    price: 'number',
    rating: 'number',
    inStock: 'boolean',
    tags: 'string[]',
  },
})

await narsil.insertBatch('bicycles', [
  { id: 'urban-commuter', name: 'Gomes Urban commuter', summary: 'A lightweight aluminium commuter bicycle with hydraulic disc brakes.', maker: 'Gomes Cycles', category: 'city', price: 749, rating: 4.5, inStock: true, tags: ['commuter', 'aluminium'] },
  { id: 'city-step', name: 'Gomes Step-through city bicycle', summary: 'A step-through city bicycle with a chain guard and a hub dynamo.', maker: 'Gomes Cycles', category: 'city', price: 549, rating: 4.0, inStock: true, tags: ['commuter', 'steel'] },
  { id: 'gravel-explorer', name: 'Mori Gravel explorer', summary: 'A steel gravel bicycle built for long unpaved rides.', maker: 'Mori Works', category: 'gravel', price: 1299, rating: 4.8, inStock: true, tags: ['gravel', 'steel'] },
  { id: 'gravel-allroad', name: 'Mori Allroad gravel bicycle', summary: 'An aluminium gravel bicycle with mounts for three bottles.', maker: 'Mori Works', category: 'gravel', price: 999, rating: 4.4, inStock: false, tags: ['gravel', 'aluminium'] },
  { id: 'folding-metro', name: 'Obi Metro folding bicycle', summary: 'A folding bicycle that fits under a train seat.', maker: 'Obi Mobility', category: 'folding', price: 899, rating: 4.2, inStock: true, tags: ['folding', 'commuter'] },
  { id: 'folding-tourer', name: 'Obi Tourer folding bicycle', summary: 'A folding bicycle with a rear rack and eight gears.', maker: 'Obi Mobility', category: 'folding', price: 1150, rating: 4.1, inStock: true, tags: ['folding', 'aluminium'] },
  { id: 'cargo-hauler', name: 'Larsson Cargo hauler', summary: 'An electric cargo bicycle rated to carry two children.', maker: 'Larsson Bikes', category: 'cargo', price: 3499, rating: 4.6, inStock: true, tags: ['cargo', 'electric'] },
  { id: 'cargo-trike', name: 'Larsson Cargo trike', summary: 'A three-wheeled cargo bicycle with a lockable box.', maker: 'Larsson Bikes', category: 'cargo', price: 4195, rating: 4.3, inStock: false, tags: ['cargo', 'electric'] },
  { id: 'road-racer', name: 'Khoury Road racer', summary: 'A carbon road bicycle with electronic shifting.', maker: 'Khoury Composites', category: 'road', price: 4250, rating: 4.9, inStock: true, tags: ['road', 'carbon'] },
  { id: 'road-endurance', name: 'Khoury Endurance road bicycle', summary: 'A carbon road bicycle tuned for comfort over long distances.', maker: 'Khoury Composites', category: 'road', price: 2799, rating: 4.7, inStock: true, tags: ['road', 'carbon'] },
  { id: 'kids-first', name: 'Adams First ride bicycle', summary: 'A first bicycle for a child, with stabilisers that come off in one minute.', maker: 'Adams Assembly', category: 'kids', price: 189, rating: 4.3, inStock: true, tags: ['kids'] },
  { id: 'kids-junior', name: 'Adams Junior mountain bicycle', summary: 'A junior mountain bicycle with a suspension fork and six gears.', maker: 'Adams Assembly', category: 'kids', price: 329, rating: 4.1, inStock: true, tags: ['kids', 'aluminium'] },
])

const listing = await narsil.listDocuments('bicycles', {
  filters: { fields: { inStock: { eq: true } } },
  sort: { price: 'desc' },
  document: { include: ['name', 'price'] },
  limit: 3,
})

console.log(JSON.stringify(listing, (key, value) => (key === 'elapsed' ? undefined : value), 2))

await narsil.shutdown()
```

```json result open
{
  "documents": [
    {
      "id": "road-racer",
      "document": {
        "name": "Khoury Road racer",
        "price": 4250
      }
    },
    {
      "id": "cargo-hauler",
      "document": {
        "name": "Larsson Cargo hauler",
        "price": 3499
      }
    },
    {
      "id": "road-endurance",
      "document": {
        "name": "Khoury Endurance road bicycle",
        "price": 2799
      }
    }
  ],
  "cursor": "eyJ2Ijo0LCJhIjoicm9hZC1lbmR1cmFuY2UiLCJrIjpbMjc5OV0sIm8iOiJbW1wicHJpY2VcIixcImRlc2NcIl1dIiwicSI6IjU3NzM1YTljIn0=",
  "total": 10
}
```

Ten bicycles pass the filter, which `total` reports, and the page holds the three most expensive of them with a `cursor` for the next three. The engine binds that cursor to the sort as well, so send it back with the same `sort` object. Where a listing leaves `sort` out, the engine returns to id order.

## Pin a result in place

With `pinned`, the engine places a document at a fixed position in the results, counting from 0, which is how a shop puts a sponsored or editorial pick at the top of a search. In `pinned.ts` you pin the cargo hauler first on a search for gravel.

```ts title="pinned.ts"
import { createNarsil } from '@delali/narsil'

const narsil = await createNarsil()

await narsil.createIndex('bicycles', {
  schema: {
    name: 'string',
    summary: 'string',
    maker: 'string:sortable',
    category: 'enum',
    price: 'number',
    rating: 'number',
    inStock: 'boolean',
    tags: 'string[]',
  },
})

await narsil.insertBatch('bicycles', [
  { id: 'urban-commuter', name: 'Gomes Urban commuter', summary: 'A lightweight aluminium commuter bicycle with hydraulic disc brakes.', maker: 'Gomes Cycles', category: 'city', price: 749, rating: 4.5, inStock: true, tags: ['commuter', 'aluminium'] },
  { id: 'city-step', name: 'Gomes Step-through city bicycle', summary: 'A step-through city bicycle with a chain guard and a hub dynamo.', maker: 'Gomes Cycles', category: 'city', price: 549, rating: 4.0, inStock: true, tags: ['commuter', 'steel'] },
  { id: 'gravel-explorer', name: 'Mori Gravel explorer', summary: 'A steel gravel bicycle built for long unpaved rides.', maker: 'Mori Works', category: 'gravel', price: 1299, rating: 4.8, inStock: true, tags: ['gravel', 'steel'] },
  { id: 'gravel-allroad', name: 'Mori Allroad gravel bicycle', summary: 'An aluminium gravel bicycle with mounts for three bottles.', maker: 'Mori Works', category: 'gravel', price: 999, rating: 4.4, inStock: false, tags: ['gravel', 'aluminium'] },
  { id: 'folding-metro', name: 'Obi Metro folding bicycle', summary: 'A folding bicycle that fits under a train seat.', maker: 'Obi Mobility', category: 'folding', price: 899, rating: 4.2, inStock: true, tags: ['folding', 'commuter'] },
  { id: 'folding-tourer', name: 'Obi Tourer folding bicycle', summary: 'A folding bicycle with a rear rack and eight gears.', maker: 'Obi Mobility', category: 'folding', price: 1150, rating: 4.1, inStock: true, tags: ['folding', 'aluminium'] },
  { id: 'cargo-hauler', name: 'Larsson Cargo hauler', summary: 'An electric cargo bicycle rated to carry two children.', maker: 'Larsson Bikes', category: 'cargo', price: 3499, rating: 4.6, inStock: true, tags: ['cargo', 'electric'] },
  { id: 'cargo-trike', name: 'Larsson Cargo trike', summary: 'A three-wheeled cargo bicycle with a lockable box.', maker: 'Larsson Bikes', category: 'cargo', price: 4195, rating: 4.3, inStock: false, tags: ['cargo', 'electric'] },
  { id: 'road-racer', name: 'Khoury Road racer', summary: 'A carbon road bicycle with electronic shifting.', maker: 'Khoury Composites', category: 'road', price: 4250, rating: 4.9, inStock: true, tags: ['road', 'carbon'] },
  { id: 'road-endurance', name: 'Khoury Endurance road bicycle', summary: 'A carbon road bicycle tuned for comfort over long distances.', maker: 'Khoury Composites', category: 'road', price: 2799, rating: 4.7, inStock: true, tags: ['road', 'carbon'] },
  { id: 'kids-first', name: 'Adams First ride bicycle', summary: 'A first bicycle for a child, with stabilisers that come off in one minute.', maker: 'Adams Assembly', category: 'kids', price: 189, rating: 4.3, inStock: true, tags: ['kids'] },
  { id: 'kids-junior', name: 'Adams Junior mountain bicycle', summary: 'A junior mountain bicycle with a suspension fork and six gears.', maker: 'Adams Assembly', category: 'kids', price: 329, rating: 4.1, inStock: true, tags: ['kids', 'aluminium'] },
])

const results = await narsil.query('bicycles', {
  term: 'gravel',
  filters: { fields: { inStock: { eq: true } } },
  pinned: [{ docId: 'cargo-hauler', position: 0 }],
  document: { include: ['name'] },
})

console.log(
  JSON.stringify(
    { count: results.count, hits: results.hits },
    (key, value) => (typeof value === 'number' ? Number(value.toFixed(2)) : value),
    2
  )
)

await narsil.shutdown()
```

```json result open
{
  "count": 2,
  "hits": [
    {
      "id": "cargo-hauler",
      "score": 0,
      "document": {
        "name": "Larsson Cargo hauler"
      }
    },
    {
      "id": "gravel-explorer",
      "score": 4.98,
      "document": {
        "name": "Mori Gravel explorer"
      }
    }
  ]
}
```

The engine places the cargo hauler first with a score of 0, although the cargo hauler holds no word gravel, and the gravel explorer follows at 4.98. `count` reports 2, because the engine counts a pinned document along with the documents that the search matched, so a pager that computes its pages from `count` includes the pin. A pin at a position beyond the page stays off that page, and the engine skips every pin on a request that sets `searchAfter`.

## Where to go next

- [Filters and facets](/docs/filters-and-facets) covers the filters that narrow the hits before you sort, group, or page them.
- [Indexes and documents](/docs/indexes-and-documents) covers the listing in id order.
- [Hybrid search](/docs/hybrid-search) covers the fused ranking, which takes no sort.
