@aiconnect/easy-rag
v2.7.2
Published
A TypeScript CLI tool for local RAG indexing and querying
Downloads
1,172
Maintainers
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"
easy-rag query --mode hybrid --vector-weight 0.8 --bm25-weight 0.2 "refund policy"| 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 | from config / auto |
| --vector-weight <0..1> | Dense (semantic) fusion weight; overrides config for this query | from config / 0.6 |
| --bm25-weight <0..1> | BM25 (keyword) fusion weight; overrides config for this query | from config / 0.4 |
Per-query overrides & precedence. --mode, --vector-weight, and --bm25-weight override the config.json values for that single invocation only (the config file is never modified). Each weight resolves independently — supplying just --vector-weight 0.7 leaves the BM25 weight at its config default. Invalid input fails fast: a weight outside [0, 1], a non-numeric weight, or an unknown --mode exits with a non-zero status and a clear message before any embedding request.
Weights only affect hybrid mode. auto fuses by native RRF (rank-based, weights ignored); vector and bm25 are single-arm. Passing weights in a non-hybrid mode is accepted but inert and emits a one-line warning.
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 --forceeasy-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 vectors + FTS (BM25) index on text
│
Query ─── Embed ───┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
Dense Search FTS Search Native rerank
│ │ │
└───────────────┼───────────────┘
▼
Relevant chunks- Parse — Extract text from PDF, Markdown, or CSV
- Chunk — Split intelligently by heading, paragraph, or row
- Embed — Generate dense vectors via OpenAI (
text-embedding-3-large); the chunk text is indexed by Zvec's native full-text (BM25) index — no client-side sparse vectors - Store — Save in Zvec (embedded, no server needed)
- Query — Embed the question and find the closest chunks via hybrid search
Hybrid Search
EasyRAG uses hybrid search combining dense vector similarity and Zvec's native full-text (BM25) keyword matching, fused by Zvec's native multi-query reranker.
Why Hybrid Search?
- Dense search captures semantic meaning but can miss exact keywords
- Full-text (BM25) search excels at keyword matches but lacks semantic understanding
- Hybrid search gives the best of both worlds — like production RAG systems
How It Works
# auto — RRF, rank-based, no weights:
score(d) = 1 / (60 + vector_rank) + 1 / (60 + bm25_rank)
# hybrid — weighted, score-based:
score(d) = vector_weight · vector_score(d) + bm25_weight · bm25_score(d)Search Modes
| Mode | Description | Use Case |
|------|-------------|----------|
| auto (default) | Dense + FTS via native RRF rerank, no weights needed | General queries |
| hybrid | Dense + FTS with configurable weights (native weighted rerank) | Fine-tuned relevance |
| vector | Dense vector similarity only | Semantic/abstract queries |
| bm25 | Native full-text (BM25) keyword only | Exact terms, acronyms, names |
In hybrid mode, weights are configurable: vector=0.6, bm25=0.4 (via config.json).
In auto mode, fusion uses Reciprocal Rank Fusion with rank constant 60 (no weights needed).
Note: Fusion is delegated to Zvec's native multi-query reranker (
@zvec/zvec≥ 0.5.0): RRF inauto, weighted inhybrid. If the native call fails, EasyRAG falls back to manual RRF fusion in JS.⚠️ Breaking change (native FTS schema): the collection schema now uses a native FTS index on the chunk text instead of a sparse-vector field, and records
schemaVersion: 2. Collections built by older versions are rejected on query/index with a "delete and re-index" error — there is no in-place migration. Delete and re-index your collections (easy-rag delete <name>theneasy-rag index <folder>).⚠️ BM25 arm requires a re-index (FTS commit fix): the native FTS index is only committed to a searchable on-disk segment when indexing finishes (
optimizeSync). Collections indexed before this fix have an empty FTS index, sobm25returns nothing andauto/hybridsilently degrade to vector-only. There is no in-place repair — delete and re-index any collection built before this version (easy-rag delete <name>theneasy-rag index <folder>).
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 BM25 (keyword) results in hybrid mode (0.0-1.0) | 0.4 |
Weights don't need to sum to 1.0 — they apply independently in hybrid mode's weighted rerank; auto ignores them and fuses by RRF.
These are the defaults; any single query can override search_mode and the weights via the CLI flags (--mode, --vector-weight, --bm25-weight) or the library query() options (mode, vectorWeight, bm25Weight) without editing this file. See easy-rag query for precedence and validation.
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 | Hybrid weighted vector weight |
| bm25Weight | number | No | 0.4 | Hybrid weighted 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' | constructor / 'auto' | Search mode for this call |
| vectorWeight | number | constructor / 0.6 | Dense fusion weight [0,1] for this call; hybrid only |
| bm25Weight | number | constructor / 0.4 | BM25 fusion weight [0,1] for this call; hybrid only |
Per-call mode / vectorWeight / bm25Weight override the values set on the EasyRAG constructor for that call only; each resolves independently. Out-of-range weights, a degenerate [0,0] hybrid weighting, or an unknown mode cause query() to throw before any embedding request. Weights supplied outside hybrid mode are inert and trigger one logger.warn.
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 (native FTS index)
query/ # Dense, FTS (BM25), 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)License
MIT
