npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aiconnect/easy-rag

v2.4.2

Published

A TypeScript CLI tool for local RAG indexing and querying

Downloads

1,112

Readme

@aiconnect/easy-rag

A TypeScript CLI for local RAG (Retrieval-Augmented Generation) — index documents and query them with natural language.

Uses OpenAI embeddings and Zvec (embedded vector database) for storage. No external infrastructure or server needed.

Quick Start

Prerequisites: Node.js >= 18 and an OpenAI API key.

# Install
npm install -g @aiconnect/easy-rag

# Set up your API key (interactive)
easy-rag init

# Index a folder and query it
easy-rag index ./my-docs
easy-rag query "What is the refund policy?"

Example output:

$ easy-rag index ./my-docs
Scanning ./my-docs...
Found 3 files (2 .md, 1 .pdf)
Chunking documents... 12 chunks created
Generating embeddings... done
Indexed 12 chunks into collection "my-docs"

$ easy-rag query "What is the refund policy?"
Result 1:
  Refunds are available within 30 days of purchase.
  Contact [email protected] to initiate a refund request.

Result 2:
  All subscription plans include a 30-day money-back guarantee...

Commands

easy-rag init

Interactive setup — prompts for your OpenAI API key, embedding model, and advanced search settings. Saves to $XDG_CONFIG_HOME/easy-rag/config.json (default ~/.config/easy-rag/config.json).

Basic Setup:

  • OpenAI API key
  • Embedding model (text-embedding-3-large, -small, or -ada-002)

Advanced Settings (optional):

  • Search mode: auto, hybrid, vector, or bm25 (default: auto)
  • Vector weight: 0.0-1.0 (default: 0.6)
  • BM25 weight: 0.0-1.0 (default: 0.4)

You can also skip init and set an environment variable instead:

export OPENAI_API_KEY="sk-..."

easy-rag index <folder>

Scans a folder recursively and indexes all supported files into Zvec.

easy-rag index ./knowledge-base
  • Supported formats: .pdf, .md, .csv
  • Chunking: Automatic — Markdown by heading, PDF by paragraph, CSV by row
  • Collection name: Derived from the folder name
  • Re-indexing the same folder creates a new collection with a unique suffix

easy-rag query <question>

Search indexed documents using auto mode (RRF fusion) by default.

easy-rag query "How do I configure API?"
easy-rag query --top 3 --metadata "quarterly revenue"
easy-rag query --collection my-docs "setup instructions"
easy-rag query --mode vector "semantic search only"
easy-rag query --mode bm25 "exact keyword match"

| Option | Description | Default | |--------|-------------|---------| | -t, --top <n> | Number of results | 5 | | -m, --metadata | Show source file, score, chunk index | off | | -c, --collection <name> | Search specific collection | all | | --mode <mode> | Search mode: auto, hybrid, vector, or bm25 | auto |

easy-rag collections

List all indexed collections with document counts.

easy-rag delete <collection>

Delete a collection. Use --force to skip confirmation.

easy-rag delete my-docs --force

easy-rag parse <file>

Parse a single file and print extracted text. Useful for debugging.

How It Works

Documents (.pdf, .md, .csv)
    │
    ▼
┌─────────┐    ┌─────────┐    ┌──────────────┐    ┌────────┐
│  Parse   │ →  │  Chunk  │ →  │  Embed       │ →  │  Store │
│          │    │         │    │  (OpenAI)    │    │  (Zvec) │
└─────────┘    └─────────┘    └──────────────┘    └────────┘
                                   │                  │
                                   │    Dense + Sparse vectors
                                   │
                    Query ─── Embed ───┘
                    │
    ┌───────────────┼───────────────┐
    ▼               ▼               ▼
Dense Search   Sparse Search   RRF Merge
    │               │               │
    └───────────────┼───────────────┘
                    ▼
            Relevant chunks
  1. Parse — Extract text from PDF, Markdown, or CSV
  2. Chunk — Split intelligently by heading, paragraph, or row
  3. Embed — Generate dense vectors via OpenAI (text-embedding-3-large) + sparse vectors (TF-hash)
  4. Store — Save in Zvec (embedded, no server needed)
  5. Query — Embed the question and find the closest chunks via hybrid search

Hybrid Search

EasyRAG uses hybrid search combining dense vector similarity and sparse keyword matching via Reciprocal Rank Fusion (RRF).

Why Hybrid Search?

  • Dense search captures semantic meaning but can miss exact keywords
  • Sparse search excels at keyword matches but lacks semantic understanding
  • Hybrid search gives the best of both worlds — like production RAG systems

How It Works

score(d) = vector_weight / (60 + vector_rank) + bm25_weight / (60 + bm25_rank)

Search Modes

| Mode | Description | Use Case | |------|-------------|----------| | auto (default) | Dense + Sparse via RRF, no weights needed | General queries | | hybrid | Dense + Sparse with configurable weights | Fine-tuned relevance | | vector | Dense vector similarity only | Semantic/abstract queries | | bm25 | Sparse keyword only | Exact terms, acronyms, names |

In hybrid mode, weights are configurable: vector=0.6, bm25=0.4 (via config.json). In auto mode, weights are fixed at 0.5/0.5 (RRF with equal weights, no configuration needed).

Note: auto currently uses manual RRF fusion because the Zvec Node SDK (@zvec/zvec) does not yet expose native multi-vector queries or rerankers. When the SDK adds this support, auto will delegate fusion to the engine.

Example:

# Auto search (default — RRF fusion, no config needed)
easy-rag query "refund policy"

# Hybrid with custom weights from config
easy-rag query --mode hybrid "refund policy"

# Vector-only for semantic queries
easy-rag query --mode vector "how does it work?"

# BM25-only for exact keywords
easy-rag query --mode bm25 "API endpoint /v1/users"

Configuration

API Key

| Method | How | Priority | |--------|-----|----------| | easy-rag init | Interactive prompt, saved to $XDG_CONFIG_HOME/easy-rag/config.json (default ~/.config/easy-rag/config.json) | 2nd | | Environment variable | export OPENAI_API_KEY="sk-..." | 1st (overrides config) |

Embedding Model

Set during easy-rag init or via EMBEDDING_MODEL env var.

| Model | Notes | |-------|-------| | text-embedding-3-large | Default, best performance | | text-embedding-3-small | Faster, cheaper | | text-embedding-ada-002 | Legacy |

Advanced Search Settings

Configure during easy-rag init or edit $XDG_CONFIG_HOME/easy-rag/config.json (default ~/.config/easy-rag/config.json).

| Setting | Description | Default | |---------|-------------|---------| | search_mode | Search mode: auto, hybrid, vector, or bm25 | auto | | vector_weight | Weight for vector results in hybrid mode (0.0-1.0) | 0.6 | | bm25_weight | Weight for sparse results in hybrid mode (0.0-1.0) | 0.4 |

Weights don't need to sum to 1.0 — they are used independently in the RRF formula.

Zvec

Zvec is the embedded vector store — no server or external process needed.

  • Data is stored in $XDG_DATA_HOME/easy-rag/collections/ (default ~/.local/share/easy-rag/collections/), auto-created on first use
  • Each collection is a directory on disk, fully portable

Storage Paths (XDG Base Directory Specification)

EasyRAG follows the XDG Base Directory Specification on every platform (Linux and macOS alike — no ~/Library/Application Support special-case).

| Resource | Path | Override | |----------|------|----------| | Config (config.json) | ~/.config/easy-rag/ | XDG_CONFIG_HOME (must be an absolute path) | | Data (collections/) | ~/.local/share/easy-rag/ | XDG_DATA_HOME (must be an absolute path) |

If XDG_CONFIG_HOME / XDG_DATA_HOME are unset, empty, whitespace-only, or set to a relative path, EasyRAG falls back to the defaults above.

Library Usage (Programmatic API)

Easy RAG can also be used as a library in Node.js applications (Express.js, Fastify, etc.):

import { EasyRAG } from '@aiconnect/easy-rag';

const rag = new EasyRAG({
  apiKey: process.env.OPENAI_API_KEY,
  collectionsDir: '/data/collections',
  readOnly: true,
});

const results = await rag.query('What is the refund policy?', {
  collection: 'docs',
  top: 5,
  mode: 'auto',
});

for (const result of results) {
  console.log(result.content);
  console.log(`Score: ${result.hybridScore.toFixed(4)}`);
}

await rag.close();

EasyRAGConfig

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | collectionsDir | string | Yes | — | Path to collections directory | | apiKey | string | No | OPENAI_API_KEY env var | OpenAI API key | | model | string | No | text-embedding-3-large | Embedding model | | readOnly | boolean | No | true | Open collections read-only | | logger | EasyRAGLogger | No | Silent (no output) | Custom logger | | vectorWeight | number | No | 0.6 | RRF vector weight | | bm25Weight | number | No | 0.4 | RRF BM25 weight |

EasyRAG Methods

| Method | Returns | Description | |--------|---------|-------------| | query(question, options?) | Promise<HybridQueryResult[]> | Query a collection | | listCollections() | CollectionInfo[] | List loaded collections | | hasCollection(name) | boolean | Check if collection exists | | close() | void | Close all collections (idempotent) |

QueryRequestOptions

| Option | Type | Default | Description | |--------|------|---------|-------------| | collection | string | — | Collection name (required for query) | | top | number | 5 | Number of results | | mode | 'auto' \| 'hybrid' \| 'vector' \| 'bm25' | 'auto' | Search mode |

Express.js Integration Example

import express from 'express';
import { EasyRAG } from '@aiconnect/easy-rag';

const app = express();
app.use(express.json());

const rag = new EasyRAG({
  apiKey: process.env.OPENAI_API_KEY,
  collectionsDir: process.env.COLLECTIONS_DIR!,
  readOnly: true,
});

app.get('/query', async (req, res) => {
  try {
    const { q, collection = 'docs', top = 5 } = req.query;
    const results = await rag.query(q as string, {
      collection: collection as string,
      top: Number(top),
    });
    res.json(results);
  } catch (error: any) {
    const status = error.message?.includes('not found') ? 404 : 500;
    res.status(status).json({ error: error.message });
  }
});

app.listen(3000, () => console.log('RAG API on :3000'));

process.on('SIGTERM', () => { rag.close(); process.exit(0); });

Logger Injection

By default, the library is silent (no console output). Inject a custom logger:

import { EasyRAG, type EasyRAGLogger } from '@aiconnect/easy-rag';

const logger: EasyRAGLogger = {
  info: (msg) => myLogger.info(msg),
  warn: (msg) => myLogger.warn(msg),
  error: (msg) => myLogger.error(msg),
};

const rag = new EasyRAG({ collectionsDir: '/data', logger });

Use consoleLogger for CLI-like output:

import { EasyRAG, consoleLogger } from '@aiconnect/easy-rag';
const rag = new EasyRAG({ collectionsDir: '/data', logger: consoleLogger });

Development

git clone https://github.com/johnjohn-aic/easy-rag.git
cd easy-rag
npm install

npm run build       # Compile TypeScript
npm test            # Run tests
npx tsx src/cli.ts <command>  # Dev mode (no build needed)

Project Structure

src/
  cli.ts            # CLI entrypoint (commander, shebang)
  index.ts          # Library API exports
  lib/              # Programmatic API (EasyRAG class)
  parsers/          # PDF, Markdown, CSV text extraction
  chunker/          # Content-aware text splitting
  embeddings/       # OpenAI embedding generation
  vector-store/     # Zvec embedded vector store + sparse tokenizer
  query/            # Dense, sparse, and hybrid search
  indexer/          # Orchestrates parse → chunk → embed → store
  config/           # Global config (XDG_CONFIG_HOME/easy-rag/config.json)
  paths/            # XDG-compliant config/data path resolution
  commands/         # CLI command implementations (init, serve)

License

MIT