Vectra

repository·main·Indexed 20 days ago

https://github.com/stevenic/vectra

A lightweight, file-backed, in-memory vector database for Node.js and browsers (version 0.15.0). It features Pinecone-compatible filtering, hybrid BM25 search, and MongoDB-style metadata filtering. Vectra can be used as a standalone library via LocalDocumentIndex and LocalIndex, or as a gRPC server for cross-language access. It supports custom storage backends via the FileStorage interface (e.g., SQLite) and provides a CLI for directory synchronization and index management.

Tokens
65.9K
Snippets
216
Records
281
Agent score
68%

What's inside vectra

  1. Overview of Vectra

    main

    Vectra is a local, file-backed vector database designed for high-performance similarity search with minimal infrastructure. It functions similarly to managed services like Pinecone or Qdrant, but stores each index as a local folder on disk.

    Key Features:

    • Zero Infrastructure: No servers or managed services required; everything is stored in local folders.
    • Fast Lookups: Achieves sub-millisecond to low-millisecond latency.
    • Metadata Filtering: Supports MongoDB-style query operators for filtering results.
    • Flexible Embeddings: Supports OpenAI, Azure, OSS endpoints, or local HuggingFace models (via LocalEmbeddings or TransformersEmbeddings) without requiring API keys.
    • Pluggable Storage: Supports Filesystem, IndexedDB (for browsers), in-memory, or custom backends.
    • Cross-Language Access: Includes a built-in gRPC server with bindings for Python, C#, Rust, Go, Java, and TypeScript.
    • Multiple Formats: Supports human-readable JSON or highly efficient Protocol Buffers (40-50% smaller).
  2. Overview of Vectra Samples

    main

    Vectra provides several runnable examples to demonstrate its core features, ranging from minimal local indexing to full RAG pipelines and browser-based search.

    Available samples include:

    • quickstart: Minimal LocalIndex and LocalDocumentIndex examples.
    • rag: An end-to-end Retrieval-Augmented Generation (RAG) pipeline (ingest → query → render → LLM).
    • browser: Browser-based semantic search using IndexedDB and TransformersEmbeddings (no API key required).
    • custom-storage: Implementation of the FileStorage interface using SQLite.
    • grpc-python: A Python gRPC client for cross-language access.
    • folder-watcher: Auto-syncing a folder to an index via CLI or library (supports an offline variant with LocalEmbeddings).
    • wikipedia: Building a document index from Wikipedia using the CLI.
  3. Compare Vectra Embeddings Providers

    main

    Vectra supports several providers depending on your environment and requirements:

    ProviderAPI KeyEnvironmentDimensionsInstall
    OpenAIEmbeddingsRequiredNode.js, BrowserModel-dependentIncluded
    OpenAIEmbeddings (Azure)RequiredNode.js, BrowserSame as OpenAIIncluded
    LocalEmbeddingsNoneNode.js, Browser384 (default)@huggingface/transformers
    TransformersEmbeddingsNoneNode.js, Browser, Electron384 (default)@huggingface/transformers
  4. What is an Embeddings Model in Vectra?

    main

    Vectra requires an embeddings provider to convert text into vectors for similarity search. You specify a provider when creating an index (for LocalDocumentIndex) or when generating vectors manually (for LocalIndex).

    All providers implement the EmbeddingsModel interface, which requires a maxTokens property and a createEmbeddings method:

    interface EmbeddingsModel {
      maxTokens: number;
      createEmbeddings(inputs: string | string[]): Promise<EmbeddingsResponse>;
    }
    interface EmbeddingsModel {
      maxTokens: number;
      createEmbeddings(inputs: string | string[]): Promise<EmbeddingsResponse>;
    }
  5. How Vectra storage works: The FileStorage abstraction

    main

    Vectra separates index logic from file I/O using the FileStorage interface. This abstraction allows you to swap storage backends (e.g., moving from local disk to S3 or IndexedDB) without modifying your core index code. Every index operation—reading vectors, writing metadata, or listing files—is routed through the chosen FileStorage implementation.

    Built-in Implementations

    ImplementationEnvironmentPersistence
    LocalFileStorageNode.jsDisk (filesystem)
    IndexedDBStorageBrowser, ElectronIndexedDB
    VirtualFileStorageAnyIn-memory (ephemeral)

    Behavioral Contract

    When implementing or using storage, note these key behaviors:

    • createFile must throw if the file already exists.
    • upsertFile should create the file if it doesn't exist or overwrite it if it does.
    • createFolder must create parent directories recursively.
    • deleteFolder must remove the folder and all its contents.
    • listFiles accepts an optional filter: 'files', 'folders', or 'all' (default).
    import { FileStorage } from 'vectra';
    
    interface FileStorage {
      createFile(filePath: string, content: Buffer | string): Promise<void>;
      createFolder(folderPath: string): Promise<void>;
      deleteFile(filePath: string): Promise<void>;
      deleteFolder(folderPath: string): Promise<void>;
      getDetails(fileOrFolderPath: string): Promise<FileDetails>;
      listFiles(folderPath: string, filter?: 'files' | 'folders' | 'all'): Promise<FileDetails[]>;
      pathExists(fileOrFolderPath: string): Promise<boolean>;
      readFile(filePath: string): Promise<Buffer>;
      upsertFile(filePath: string, content: Buffer | string): Promise<void>;
    }
    
    interface FileDetails {
      name: string;
      path: string;
      isFolder: boolean;
      fileType?: string;
    }
  6. Compare SQLite and LocalFileStorage for Vectra

    main

    When deciding between a custom SQLite storage and the default LocalFileStorage, consider these trade-offs:

    ScenarioSQLiteLocalFileStorage
    Single-file backup/transferBest — one .db fileRequires zipping a folder
    Concurrent readersGood — WAL modeGood — OS-level file locks
    Inspecting dataSQL queriesManual JSON/protobuf parsing
    Maximum I/O performanceGoodBest (direct fs)
    Browser/ElectronNot availableUse IndexedDBStorage instead
  7. Compare File-based vs SQLite storage in Vectra

    main

    When deciding whether to use the default file-based storage or a custom SQLite implementation, consider the following trade-offs:

    ConsiderationFile-based (default)SQLite
    Single-file portabilityNo (directory tree)Yes (one .db file)
    Concurrent readersOS-dependentBuilt-in WAL mode
    SQL queryabilityNoYes
    Setup complexityZeroRequires better-sqlite3
  8. Use Vectra in Electron

    main

    The vectra/browser entry point is compatible with Electron renderer processes.

    • Persistence: Use IndexedDBStorage in the renderer to save data.
    • Inference: TransformersEmbeddings works with WebGPU and WASM in Electron.
    • Filesystem Access: For security, it is recommended to use vectra/browser in the renderer. If you need filesystem access, use a contextBridge to expose LocalFileStorage from the main process to the renderer.

    If nodeIntegration is enabled, you can use the standard vectra package and LocalFileStorage directly, but vectra/browser is the safer, recommended approach.

  9. Design efficient Vectra indexes

    main

    When designing an index, follow these principles to optimize performance and storage:

    • Filter Optimization: Only include frequently used filter fields in metadata_config.indexed. This keeps index.json small. All other metadata is stored in individual JSON files on disk.
    • Namespacing: Use separate folders for different datasets or tenants to mimic Pinecone-style namespaces.
    • Consistency: Ensure all vectors in an index use the same embedding model and dimensions. If you change models, you must re-embed and rebuild the entire index.
    • Memory Management: The entire index is loaded into RAM. A 1536-dim float32 vector consumes approximately 12 KB in memory (as JS doubles) plus metadata overhead. For very large corpora, consider splitting data into multiple smaller indexes.
  10. Optimize upserts using the skip-if-unchanged behavior

    main

    Vectra supports a 'skip-if-unchanged' optimization during document upserts. When you upsert a document with a URI that already exists in the index, Vectra computes a hash of the content (text, docType, and metadata). If the new hash matches the existing hash stored in the catalog, the operation short-circuits: no new embeddings are created, and no document deletion/re-insertion occurs. This significantly reduces latency and embedding costs during synchronization tasks.

    Key Behaviors:

    • Identical Content: If text and metadata are identical, the upsert returns the existing LocalDocument without calling the embedding provider.
    • Metadata Sensitivity: Changes to metadata (even if text is identical) or changes to docType will trigger a full re-embed because they affect the content hash.
    • Canonical Serialization: Metadata key order does not affect the hash; identical values with different key orders will still trigger a short-circuit.
    • Force Re-embedding: If you need to bypass the hash check and force a re-embedding of an existing URI, use the force: true option.
    // Example conceptual usage of the upsert with force option
    await index.upsertDocument({
      uri: 'https://example.com/doc',
      text: 'some content',
      docType: 'text',
      metadata: { author: 'stevenic' },
      force: true // Forces re-embedding even if content is unchanged
    });
  11. How Vectra handles data serialization with Codecs

    main

    Vectra uses a codec abstraction to manage how index data, catalogs, and metadata are serialized to disk. This decoupling allows the engine to support multiple storage formats without changing the core index logic.

    There are two primary implementations:

    1. JsonCodec: The default implementation. It uses JSON.stringify() and JSON.parse(). It is backward-compatible and requires no extra dependencies.
    2. ProtobufCodec: An opt-in binary format using Protocol Buffers. It is more efficient for large indexes because vectors are stored as packed float arrays (reducing size from ~8-12 KB to ~6 KB for 1536-dimensional vectors) and offers faster I/O.

    Note: To use the ProtobufCodec, you must manually install the protobufjs package, as it is an optional dependency: npm install protobufjs.

    // Example of the IndexCodec interface structure
    export interface IndexCodec {
      readonly extension: string; // e.g., '.json' or '.pb'
      serializeIndex(data: IndexData): Buffer;
      deserializeIndex(buffer: Buffer): IndexData;
      serializeCatalog(catalog: DocumentCatalog): Buffer;
      deserializeCatalog(buffer: Buffer): DocumentCatalog;
      serializeMetadata(metadata: Record<string, MetadataTypes>): Buffer;
      deserializeMetadata(buffer: Buffer): Record<string, MetadataTypes>;
    }