---
title: "Choosing a Local Vector Store: Vectra, Chroma, and hnswlib"
description: "Compare Vectra, Chroma, and hnswlib through ChatCrystal’s deployment constraints: integration, retrieval cost, backups, and recovery boundaries."
image: "https://zengliangyi.com/og/blog/en/local-vector-database-selection.png"
---

Choosing vector storage for [ChatCrystal](/en/projects/chatcrystal) starts with installation, backups, and recovery rather than a performance leaderboard. The project supports a Node.js CLI and a desktop entry point; individual users may not want to maintain another database service.

This article compares Vectra, Chroma, and hnswlib under those constraints. It explains an architectural choice rather than reporting a benchmark. Implementation details were revised on September 23, 2026, against ChatCrystal commit [`7642768`](https://github.com/ZengLiangYi/ChatCrystal/tree/764276814e5c5a33ecbfeae63754f74b9261a3a8) and Vectra’s [v0.15.0 retrieval implementation](https://github.com/Stevenic/vectra/blob/v0.15.0/src/LocalIndex.ts).

## How Vectra searches and where the work goes

**The Vectra `LocalIndex` used here is not an LSH index.** In v0.15.0, `queryItems()` loads the index, applies metadata filters, scores candidate vectors using cosine similarity, and retains the top K with a bounded min-heap.

This vector retrieval path scores every candidate retained after filtering, without LSH or HNSW approximate candidate search. Whether it finds useful notes still depends on the embedding model, chunking, query wording, and filters. Graph relationships can provide additional exploration paths, but they are not repairing LSH recall losses in an implementation that does not use LSH.

For N candidates, D dimensions, and K results, scoring work grows with N and D, with additional Top K maintenance. Quoting heap complexity alone ignores vector arithmetic. A millisecond latency guarantee also requires measurements on specified hardware and data.

## The three options operate at different layers

| Option | Integration model | Responsibilities left to the application |

| --- | --- | --- |

| Vectra `LocalIndex` | File-backed index inside the Node.js process | Data lifecycle, write coordination, recovery, capacity validation |

| Chroma | Database and clients; TypeScript can connect to a separate service | Deployment, connections, authentication, upgrades |

| hnswlib | C++ approximate-nearest-neighbor library; Node.js needs a binding | Native distribution plus documents, metadata, and application APIs |

These are not interchangeable database products. Comparing a library’s search operation with a service request that includes networking, filtering, and document loading can produce a misleading ranking.

### Vectra: reuse the existing Node.js process

ChatCrystal’s `getIndex()` creates `vectra-index` under its data directory, constructs a `LocalIndex`, and caches the object. That layer does not require a separate index-service process. [Project implementation](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/services/vector-index.ts)

This integrates naturally with the TypeScript service, and index files can participate in its backup workflow. However, not needing an additional database service does not make the whole application dependency-free. Copying files during a write does not automatically create a consistent backup either.

### Chroma: make retrieval a service boundary

Chroma documents a client-server deployment in which TypeScript clients connect to a separate Chroma server. That boundary is useful for multiple processes accessing a shared index or applications with an established service deployment model. [Official documentation](https://docs.trychroma.com/docs/run-chroma/client-server)

For a simple desktop installation, it adds service startup, ports, health checks, and upgrade management. Those are deployment and product costs, not proof that Chroma cannot run locally or that Python necessarily makes a query slower. Language labels cannot substitute for measuring the current architecture and access path.

### hnswlib: focus on approximate nearest neighbors

hnswlib supplies an HNSW index with query parameters for trading speed against recall, plus index persistence. It is not a complete document-management service. [Project documentation](https://github.com/nmslib/hnswlib)

Node.js and Electron applications must also evaluate the selected binding’s support for target operating systems, CPU architectures, and runtimes. Local compilation depends on that binding and available prebuilt binaries; not every user necessarily needs a compiler. The real cost is maintaining the compatibility matrix and adding the application storage layer.

## Why ChatCrystal uses an in-process index

The priorities are to stay within the Node.js stack, reduce extra process management, keep the index alongside user data, and regenerate it from notes. Vectra’s integration model fits those requirements.

It is not the only possible choice. A shift toward shared services, more concurrent access, or larger indexes can change the priorities. Local-first architecture also does not require every entry point to run on one machine: an application server can expose an embedded index to clients. The important question is which process owns writes.

## Measure scale in vector chunks

A note count is not an index-entry count. If each note produces multiple chunks, estimate the total number of chunks, their dimensions, and their metadata rather than the number displayed in the notes UI.

For 10,000 vectors with 1,536 dimensions, a calculation using four bytes per dimension gives about 58.6 MiB of vector payload. **This is a capacity illustration, not a measurement of Vectra’s memory usage.** JavaScript objects, arrays, metadata, parsing, and other service state add overhead.

An evaluation should hold these conditions explicit:

- Input vectors, dimensions, distance definition, and metadata filters.

- Dataset size, chunks per note, and Top K.

- Cold starts, warm queries, median latency, and high-percentile latency.

- Peak memory, index-loading time, update cost, and deletion cost.

- For approximate search, recall against the same ground truth.

A fast query may accompany a slow index load. An acceptable isolated query may become problematic when imports and queries run together. Without those conditions, a table promising that ten thousand vectors are always fine or a million vectors always return in milliseconds is unjustified.

## File-backed indexes still need recovery design

Notes in SQLite and vectors in Vectra are separate state stores. Successfully writing a note does not guarantee that every chunk’s vector has been committed.

ChatCrystal’s vector-deletion code uses `beginUpdate()` and `endUpdate()`, with an attempted `cancelUpdate()` on failure. This defines a boundary for an index update; it does not establish a shared transaction spanning SQLite and Vectra.

Recovery needs to answer which notes require new vectors, how failed work retries, whether deletion clears every chunk, how a model or dimensionality change invalidates the old index, and how backups avoid capturing a partial update.

Note content should remain a recoverable source, while vectors should be reproducible from an explicit model and chunking configuration. Adding graph relationships can provide additional exploration paths, but it does not replace consistency checks or guarantee recovery of every missing result.

## When to reconsider the choice

Revisit the options when measured loading time, memory, or query latency exceeds the budget. Multiple writers, centralized access control, and shared data may make a service boundary more useful than embedded integration. Conversely, a project with mature native-module distribution may find an hnswlib binding inexpensive to support.

Maintenance risk should be assessed through actual versions, releases, tests, and upgrade plans. A small codebase does not automatically imply low risk, and a larger dependency tree does not automatically make a tool unsuitable.

The value of this decision is matching deployment to the project’s needs and identifying conditions that would change it. The index must support real workflows, such as [exporting knowledge to Obsidian](/en/blog/chatcrystal-to-obsidian). For the boundaries of its upstream input, see [five AI conversation formats](/en/blog/ai-conversation-data-formats).

Share:

##  Related Posts

[##  Five AI conversation parsers: retained fields and loss boundaries

Build an acceptance matrix from Claude Code, Codex, Cursor, Trae, and Copilot parsers, checking tool flags, thinking, order, and snapshot coverage.          曾亮懿       October 7, 2026      4 min read

#ChatCrystal

#TypeScript

#Node.js](/en/blog/ai-conversation-parser-boundaries)    [##  AI SDK Provider Abstraction: Capabilities and Protocols

Test 7 ChatCrystal providers and AI SDK factories with offline mocks for Responses, Chat Completions, embeddings, and structured output rejection.          曾亮懿       October 7, 2026      4 min read

#ChatCrystal

#Node.js

#TypeScript](/en/blog/ai-sdk-provider-abstraction)    [##  SourceAdapter internals: identity, content hashes, and reimport

Trace ChatCrystal’s registry, importer, and remote payload to verify identity, canonical hashes, metadata shortcuts, and parser-version boundaries.          曾亮懿       October 7, 2026      4 min read

#ChatCrystal

#TypeScript

#Node.js](/en/blog/source-adapter-internals)

##  From writing to code

Explore ChatCrystal, developer tools and daily learning notes on my GitHub. Questions about the implementation are welcome.

[Visit my GitHub](https://github.com/ZengLiangYi)

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "ZengLiangYi Blog",
  "alternateName": [
    "Zeng Liangyi",
    "ZengLiangYi",
    "zengliangyi.com"
  ],
  "url": "https://zengliangyi.com",
  "description": "曾亮懿的个人网站。记录 AI Native 探索、开发者工具与全栈产品实践。"
}
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "Choosing a Local Vector Store: Vectra, Chroma, and hnswlib",
  "description": "Compare Vectra, Chroma, and hnswlib through ChatCrystal’s deployment constraints: integration, retrieval cost, backups, and recovery boundaries.",
  "url": "https://zengliangyi.com/en/blog/local-vector-database-selection/",
  "image": "https://zengliangyi.com/images/blog/local-vector-database-selection.svg",
  "datePublished": "2026-06-09T01:00:00.000Z",
  "dateModified": "2026-09-24T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "曾亮懿",
    "url": "https://zengliangyi.com/en/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "ZengLiangYi Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://zengliangyi.com/favicon-96x96.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://zengliangyi.com/en/blog/local-vector-database-selection/"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "https://zengliangyi.com/en"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "Blog",
      "item": "https://zengliangyi.com/en/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "Choosing a Local Vector Store: Vectra, Chroma, and hnswlib",
      "item": "https://zengliangyi.com/en/blog/local-vector-database-selection/"
    }
  ]
}
```
