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

# Native search core

> Install the C core that searches vector fields, confirm that the engine loaded it, see how much faster it builds and searches, and keep a bundler from leaving it out in production.

URL: https://narsil.sondelali.com/docs/native-core
Section: Operations
Version: 0.3 (@delali/narsil@0.3.0, latest)

Narsil searches vector fields, builds and compacts their graphs, and writes their codes in a core written in C. Where that core is missing, the engine uses the same search compiled to WebAssembly. The core handles vector fields alone, so a keyword search takes the same path on every installation. You build `check-core.ts` here, a script that exits with an error on a host where the engine would search through WebAssembly.

## Install the core for your platform

`@delali/narsil` lists six platform packages as optional dependencies, so your package manager installs the one that matches the machine along with the engine:

| Platform | Package |
| --- | --- |
| macOS on Apple silicon | `@delali/narsil-native-darwin-arm64` |
| macOS on Intel | `@delali/narsil-native-darwin-x64` |
| Linux on ARM, with glibc or musl | `@delali/narsil-native-linux-arm64` |
| Linux on x64, with glibc or musl | `@delali/narsil-native-linux-x64` |
| Windows on ARM | `@delali/narsil-native-win32-arm64` |
| Windows on x64 | `@delali/narsil-native-win32-x64` |

The Linux packages each hold a second build for musl, the C library that Alpine uses, which the engine loads where the process has no glibc. On every other platform, and in every browser, the engine searches through WebAssembly. Where you install with optional dependencies left out, as `npm install --omit=optional` does, the package manager skips the platform package, so the engine falls back without an error.

## Confirm that the engine loaded the core

By default, the engine loads the core at the first vector search. Where it cannot load the core, it writes one line to standard error at that moment and then searches through WebAssembly:

```txt
Narsil searches through WebAssembly on this thread, because this installation has no "@delali/narsil-native-darwin-arm64" package.
```

With `NARSIL_REQUIRE_NATIVE_CORE=1` in the environment, `createNarsil` loads the core at start-up and fails with `CONFIG_INVALID` where it cannot load it, so `check-core.ts` only has to start the engine and shut it down.

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

const narsil = await createNarsil()

await narsil.shutdown()
```

Add it to your deployment with the variable set, as a step before the host takes traffic:

```bash
NARSIL_REQUIRE_NATIVE_CORE=1 node check-core.ts
```

The script exits with 0 where the engine loaded the core. Where the package is missing, it exits with 1 and prints an error whose message reads `NARSIL_REQUIRE_NATIVE_CORE is 1, yet this installation has no "@delali/narsil-native-darwin-arm64" package`, with the platform of that host in the package name.

You can steer the choice with two more variables. Under `NARSIL_SEARCH_BACKEND=wasm`, the engine searches through WebAssembly without the notice, which helps when you compare the two paths. Under any value other than `native` or `wasm`, however, `createNarsil` fails with `CONFIG_INVALID`. With `NARSIL_NATIVE_CORE_PATH` set to the absolute path of a core binary, the engine loads that binary in place of the platform package, although it falls back with a notice where no file exists at that path. The engine also refuses a binary built for a different version of its interface and falls back, naming both versions in the notice.

## See what the core changes

The engine returns the same hits from an exact scan on both paths, which a continuous integration job checks on each of the six platforms and on both musl builds. The two paths differ in how long the work takes. The table below lists the times from a measured run on an 11-core Apple silicon machine under Node 24, with 20,000 vectors of 384 dimensions and worker threads turned off.

| Work | WebAssembly | Native core |
| --- | --- | --- |
| An exact scan while the graph builds, for each query | 3.50 ms | 3.24 ms |
| Building the graph | 13.4 to 13.7 s | 5.3 to 5.4 s |
| A graph search, for each query | 0.16 ms | 0.12 ms |

The native core builds the graph in less than half the time. Since the engine answers from the exact scan while it builds the graph, the engine with the core also moves searches onto the graph sooner. [Vector search](/docs/vector-search) covers the graph, the coding modes, and the `memory` and `disk` storage modes.

## Keep Narsil out of your bundle

The engine resolves the platform package at run time, starting from its own module, so a bundler that follows your imports never includes the package. Where a deployment holds the bundle alone, the engine finds no platform package and falls back to WebAssembly. The bundle also leaves out the file from which the engine starts each worker thread, so a bundled engine answers every query on the main thread and writes a notice to standard error as it starts. Where the configuration sets `workers.enabled: true`, `createNarsil` fails with `CONFIG_INVALID` in place of that notice.

Mark `@delali/narsil` as external in your bundler, so that the application loads it from `node_modules` at run time. In Next.js, name it in `serverExternalPackages`:

```ts title="next.config.ts"
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  serverExternalPackages: ['@delali/narsil'],
}

export default nextConfig
```

With esbuild, pass `--external:@delali/narsil`; with webpack, list the package under `externals`.

## Where to go next

- [Vector search](/docs/vector-search) covers the graph that the core builds and the storage modes of a vector field.
- [Configuration](/docs/configuration) covers the worker settings, including the thread budget.
