ZBSearch
Usage

Create a new ZBSearch instance

Create a new ZBSearch instance to store and search documents.

We can create a new instance (from now on database) with an indexing schema.
The schema represents the searchable properties of the document to be inserted. Not all properties need to be indexed, but only those that we want to be able to search for.

If you want to learn more and see real-world examples, check out this blog post we wrote about schema optimization.

Schema properties and types

The schema is an object where the keys are the property names and the values are the property types.
ZBSearch supports the following types:

TypeDescriptionExample
stringA string of characters.'Hello world'
numberA numeric value, either float or integer.42
booleanA boolean value.true
enumAn enum value.'drama'
geopointA geopoint value.{ lat: 40.7128, lon: 74.0060 }
string[]An array of strings.['red', 'green', 'blue']
number[]An array of numbers.[42, 91, 28.5]
boolean[]An array of booleans.[true, false, false]
enum[]An array of enums.['comedy', 'action', 'romance']
vector[<size>]A vector of numbers to perform vector search on.[0.403, 0.192, 0.830]

A database can be as simple as:

import { create } from "zbsearch";

const db = create({
  schema: {
    word: "string",
  },
});

or more variegated:

import { create } from "zbsearch";

const movieDB = create({
  schema: {
    title: "string",
    director: "string",
    plot: "string",
    year: "number",
    isFavorite: "boolean",
  },
});

Nested properties

ZBSearch supports nested properties natively. Just add them as you would typically do in any JavaScript object:

const movieDB = create({
  schema: {
    title: "string",
    plot: "string",
    cast: {
      director: "string",
      leading: "string",
      supporting: "string[]",
    },
    year: "number",
    isFavorite: "boolean",
  },
});

insert(movieDB, {
  title: "The Godfather",
  plot: "The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.",
  cast: {
    director: "Francis Ford Coppola",
    leading: "Marlon Brando",
    supporting: ["Al Pacino", "James Caan", "Robert Duvall"],
  },
  year: 1972,
  isFavorite: true,
});

Vector properties

Since version 1.2.0, ZBSearch supports vector search.
To run vector queries, you first need to initialize a vector property in the schema:

const db = create({
  schema: {
    title: "string",
    embedding: "vector[10]", // replace 10 with the appropriate size of your vector
  },
});

insert(db, {
  title: "The Godfather",
  embedding: [
    -0.8469661901208547, 0.6762289692745016, 0.3294302068627739,
    -0.9269241187762711, -0.8340635986042049, -0.9940330715457502,
    -0.46761552816396046, 0.2818135926099674, -0.5812061227183709,
    0.6443446315273054,
  ],
});

Please note that the size of the vector must be specified in the schema.
The size of the vector is the number of elements that the vector contains, so make sure to specify the correct size, as performing search on vectors of different sizes will result in unpredictable and mostly wrong results. For performance reasons, we recommend using one vector property per database, even though it's possible to have multiple vector properties in the same ZBSearch instance.

If you're using vector properties to search through embeddings, we highly recommend using HuggingFace's gte-small model, which has a vector size of 384.

There is a great article written by Supabase explaining why it might be a better option than OpenAI's text-embedding-ada-002 model: https://supabase.com/blog/fewer-dimensions-are-better-pgvector.

Choosing an IVF index

By default, ZBSearch uses a flat vector index: every query compares your search vector against all stored embeddings. That is simple, exact, and ideal for small or medium datasets (roughly up to a few thousand vectors), prototypes, and cases where recall must be perfect.

For larger collections, switch to an IVF (Inverted File) index. IVF partitions vectors into clusters at insert time and only searches a subset of clusters at query time, trading a small amount of recall for much faster searches. Enable it when vector search latency grows with your dataset size and you can tolerate approximate nearest-neighbor results.

import { create } from "zbsearch";
import { ivf } from "zbsearch/trees/vector-ivf";

const db = create({
  schema: {
    title: "string",
    embedding: "vector[384]",
  },
  indexes: {
    embedding: ivf({
      nlist: 64,    // number of clusters (default: 64)
      nprobe: 16,   // clusters searched per query (default: nlist / 4, capped at 32)
      trainMin: 32, // minimum vectors before training centroids (default: min(nlist, 32))
    }),
  },
});

Pros: significantly faster vector search on large indexes, lower per-query CPU cost, and configurable speed/recall via nprobe (higher nprobe = more accurate but slower).

Cons: results are approximate-you may miss the true nearest neighbor-centroids must be trained once enough vectors are inserted (trainMin), tuning nlist and nprobe takes experimentation, and persistence requires passing the same indexes config when loading a saved database. Start with flat search; reach for IVF when profiling shows vector queries are a bottleneck.

Instance ID

Every ZBSearch instance has a unique id property, which can be used to identify a given instance when working with multiple databases.

You can customize it by passing an id property during the creation of the instance:

import { create } from "zbsearch";

const db = create({
  schema: {
    word: "string",
  },
  id: "my-orama-instance",
});

On this page