{"_id":"@amjadkhan-dev/ragify","_rev":"2-0655de9282ac45ebc6db91ba33381684","name":"@amjadkhan-dev/ragify","dist-tags":{"latest":"0.1.2"},"versions":{"0.1.0":{"name":"@amjadkhan-dev/ragify","version":"0.1.0","keywords":["rag","retrieval-augmented-generation","vector-database","embeddings","llm","ai","nodejs","typescript"],"author":{"url":"https://github.com/AmjadKhan88","name":"Amjad Ullah","email":"amjadfast87@gmail.com"},"license":"MIT","_id":"@amjadkhan-dev/ragify@0.1.0","maintainers":[{"name":"amjadkhan88","email":"amjadfast87@gmail.com"}],"homepage":"https://github.com/AmjadKhan88/ragify#readme","bugs":{"url":"https://github.com/AmjadKhan88/ragify/issues"},"dist":{"shasum":"7d71a2e40688042c3f5092a5fb5ca8db2fe4c3ea","tarball":"https://registry.npmjs.org/@amjadkhan-dev/ragify/-/ragify-0.1.0.tgz","fileCount":9,"integrity":"sha512-4P+6p0h/rgudv9orLoNG25aMUqUDovvex85cS6CszG9rjcIhN9UdscVlMtTRK1OOrDMVissAbQ5UYiHZM8LD8A==","signatures":[{"sig":"MEUCIGqLBJaJU1u5uQNMG51HteOVOPA7u4+6lzPIvUoC4nDzAiEA9602f+++eNLzJUA7zXnCDQ9rUZ7xvMuPpD7vEKcuCKk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":216065},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"d4ec2f77d593604e6b70c923c4d3fb4d37c7d187","scripts":{"dev":"tsup --watch","lint":"eslint src","test":"vitest","build":"tsup","format":"prettier --write .","test:run":"vitest run","typecheck":"tsc --noEmit"},"_npmUser":{"name":"amjadkhan88","email":"amjadfast87@gmail.com"},"repository":{"url":"git+https://github.com/AmjadKhan88/ragify.git","type":"git"},"_npmVersion":"11.9.0","description":"Unified RAG Pipeline Builder for Node.js — eliminates boilerplate across chunking, embedding, retrieval, and vector storage.","directories":{},"_nodeVersion":"24.14.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.23.11","tsup":"^8.5.1","dotenv":"^17.4.2","eslint":"^10.8.1","openai":"^7.4.0","vitest":"^4.1.10","chromadb":"^3.5.0","groq-sdk":"^1.5.0","prettier":"^3.9.6","@eslint/js":"^10.0.1","typescript":"^5.7.3","@types/node":"^26.2.0","typescript-eslint":"^8.66.0","@google/generative-ai":"^0.24.1","@qdrant/js-client-rest":"^1.19.0","eslint-config-prettier":"^10.1.8","@pinecone-database/pinecone":"^8.2.0"},"peerDependencies":{"openai":"^7.4.0","chromadb":"^3.5.0","groq-sdk":"^1.5.0","@google/generative-ai":"^0.24.1","@qdrant/js-client-rest":"^1.19.0","@pinecone-database/pinecone":"^8.2.0"},"peerDependenciesMeta":{"openai":{"optional":true},"chromadb":{"optional":true},"groq-sdk":{"optional":true},"@google/generative-ai":{"optional":true},"@qdrant/js-client-rest":{"optional":true},"@pinecone-database/pinecone":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/ragify_0.1.0_1786622038768_0.7391493058759104","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@amjadkhan-dev/ragify","version":"0.1.2","description":"Unified RAG Pipeline Builder for Node.js — eliminates boilerplate across chunking, embedding, retrieval, and vector storage.","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"publishConfig":{"access":"public"},"scripts":{"build":"tsup","dev":"tsup --watch","test":"vitest","test:run":"vitest run","lint":"eslint src","format":"prettier --write .","typecheck":"tsc --noEmit"},"keywords":["rag","retrieval-augmented-generation","vector-database","embeddings","llm","ai","nodejs","typescript"],"author":{"name":"Amjad Ullah","email":"amjadfast87@gmail.com","url":"https://github.com/AmjadKhan88"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/AmjadKhan88/ragify.git"},"bugs":{"url":"https://github.com/AmjadKhan88/ragify/issues"},"homepage":"https://github.com/AmjadKhan88/ragify#readme","engines":{"node":">=20"},"devDependencies":{"@eslint/js":"^10.0.1","@google/generative-ai":"^0.24.1","@pinecone-database/pinecone":"^8.2.0","@qdrant/js-client-rest":"^1.19.0","@types/node":"^26.2.0","chromadb":"^3.5.0","dotenv":"^17.4.2","eslint":"^10.8.1","eslint-config-prettier":"^10.1.8","groq-sdk":"^1.5.0","openai":"^7.4.0","prettier":"^3.9.6","tsup":"^8.5.1","tsx":"^4.23.11","typescript":"^5.7.3","typescript-eslint":"^8.66.0","vitest":"^4.1.10"},"peerDependencies":{"@google/generative-ai":"^0.24.1","@pinecone-database/pinecone":"^8.2.0","chromadb":"^3.5.0","groq-sdk":"^1.5.0","openai":"^7.4.0","@qdrant/js-client-rest":"^1.19.0"},"peerDependenciesMeta":{"openai":{"optional":true},"@google/generative-ai":{"optional":true},"groq-sdk":{"optional":true},"@pinecone-database/pinecone":{"optional":true},"chromadb":{"optional":true},"@qdrant/js-client-rest":{"optional":true}},"gitHead":"ad3726a6b768f293660d5c6c62ece289091b3d61","_id":"@amjadkhan-dev/ragify@0.1.2","_nodeVersion":"24.14.0","_npmVersion":"11.9.0","dist":{"integrity":"sha512-yfse6t8uuueF1k4d58iDh/V5pDNd3DoeFx0M9TK/un34gY3j3bDJahMSJLGk9yiZmL58c5sn6XUznhm9Fq3McQ==","shasum":"74873893f36235bfec2e757b8a19f84361c7e87e","tarball":"https://registry.npmjs.org/@amjadkhan-dev/ragify/-/ragify-0.1.2.tgz","fileCount":9,"unpackedSize":245947,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDT1suf/U9Nc0recKJIhYElXQJPEMul769fJR7034vzwwIhAJCYtoheEFDYtANILKaCKsGnd7yV3CuTV4kG4yJhwali"}]},"_npmUser":{"name":"amjadkhan88","email":"amjadfast87@gmail.com"},"directories":{},"maintainers":[{"name":"amjadkhan88","email":"amjadfast87@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/ragify_0.1.2_1786674413890_0.7013491297740062"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-13T11:53:58.652Z","modified":"2026-08-14T02:26:54.284Z","0.1.0":"2026-08-13T11:53:58.919Z","0.1.2":"2026-08-14T02:26:54.095Z"},"bugs":{"url":"https://github.com/AmjadKhan88/ragify/issues"},"author":{"name":"Amjad Ullah","email":"amjadfast87@gmail.com","url":"https://github.com/AmjadKhan88"},"license":"MIT","homepage":"https://github.com/AmjadKhan88/ragify#readme","keywords":["rag","retrieval-augmented-generation","vector-database","embeddings","llm","ai","nodejs","typescript"],"repository":{"type":"git","url":"git+https://github.com/AmjadKhan88/ragify.git"},"description":"Unified RAG Pipeline Builder for Node.js — eliminates boilerplate across chunking, embedding, retrieval, and vector storage.","maintainers":[{"name":"amjadkhan88","email":"amjadfast87@gmail.com"}],"readme":"# Ragify\r\n\r\n[![CI](https://github.com/AmjadKhan88/Ragify/actions/workflows/ci.yml/badge.svg)](https://github.com/AmjadKhan88/Ragify/actions/workflows/ci.yml)\r\n[![npm version](https://img.shields.io/npm/v/@amjadkhan-dev/ragify.svg)](https://www.npmjs.com/package/@amjadkhan-dev/ragify)\r\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\r\n[![Node](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)\r\n\r\n> Unified RAG Pipeline Builder for Node.js — eliminate the repetitive boilerplate of chunking, embedding, retrieval, and vector storage behind one clean, swappable API.\r\n\r\nRagify gives you a single, cohesive interface to manage the entire Retrieval-Augmented Generation workflow — from document ingestion to grounded answer generation — without hand-wiring five different SDKs every time you start a new RAG project.\r\n\r\nThis document is the complete reference for Ragify: installation, every component, every vector database setup from scratch, every environment variable, common errors and their fixes, and how to extend the package with your own adapters. It's written so you can hand this entire file to an AI assistant (or a new team member) and they'll have everything needed to build a working RAG application on top of Ragify without any other source of truth.\r\n\r\n---\r\n\r\n## Table of Contents\r\n\r\n1. [Why Ragify](#1-why-ragify)\r\n2. [Installation](#2-installation)\r\n3. [Quick Start](#3-quick-start)\r\n4. [Core Architecture](#4-core-architecture)\r\n5. [RagifyPipeline API](#5-ragifypipeline-api)\r\n6. [Chunkers](#6-chunkers)\r\n7. [Embedders](#7-embedders)\r\n8. [Vector Stores — Full Setup Guides](#8-vector-stores--full-setup-guides)\r\n9. [Retrieval Strategies](#9-retrieval-strategies)\r\n10. [LLM Wrappers (Generation)](#10-llm-wrappers-generation)\r\n11. [Re-ranking](#11-re-ranking)\r\n12. [Caching](#12-caching)\r\n13. [Using Components Standalone (Without the Pipeline)](#13-using-components-standalone-without-the-pipeline)\r\n14. [Complete Configuration Reference](#14-complete-configuration-reference)\r\n15. [Environment Variables — Full Reference](#15-environment-variables--full-reference)\r\n16. [Error Handling](#16-error-handling)\r\n17. [Extending Ragify — Writing Custom Adapters](#17-extending-ragify--writing-custom-adapters)\r\n18. [Troubleshooting](#18-troubleshooting)\r\n19. [Compatibility](#19-compatibility)\r\n20. [End-to-End Example: Production RAG App](#20-end-to-end-example-production-rag-app)\r\n21. [FAQ](#21-faq)\r\n22. [Contributing](#22-contributing)\r\n23. [License](#23-license)\r\n\r\n---\r\n\r\n## 1. Why Ragify\r\n\r\nMost RAG tutorials wire one embedding API directly to one vector store, hardcoded together. The moment you want to swap providers, add hybrid search, cache embeddings to cut API costs, or re-rank results, you're rewriting plumbing from scratch.\r\n\r\nRagify is built around one idea: **every stage of the pipeline is a TypeScript interface, and every implementation is swappable**, without touching the rest of your code.\r\n\r\n```typescript\r\nconst pipeline = new RagifyPipeline({\r\n  chunker: new RecursiveChunker(),\r\n  embedder: new GeminiEmbedder(),        // swap for OpenAIEmbedder — nothing else changes\r\n  vectorStore: new QdrantVectorStore(),  // swap for Pinecone, Chroma, InMemory — nothing else changes\r\n  llm: new GroqLLM(),                    // swap for GeminiLLM — nothing else changes\r\n});\r\n```\r\n\r\n### Features\r\n\r\n- **Plug-and-play architecture** — swap chunkers, embedders, vector stores, retrievers, and LLMs via one config object\r\n- **Multiple embedding providers** — Gemini, OpenAI, or a zero-cost mock for local dev/testing\r\n- **Multiple vector stores** — in-memory, Pinecone, Chroma, Qdrant\r\n- **Smart chunking** — fixed-size or boundary-aware recursive chunking (paragraph → sentence → word → character)\r\n- **Hybrid retrieval** — dense (vector) + sparse (BM25) search fused via Reciprocal Rank Fusion\r\n- **LLM-based re-ranking** — improves precision on top of hybrid retrieval, no dedicated re-ranking API required\r\n- **Embedding cache** — content-hash-keyed caching (in-memory or file-based) skips re-embedding unchanged text\r\n- **Resilience built in** — automatic retry-with-backoff and request batching on every network-calling adapter\r\n- **Optional peer dependencies** — only install the SDK for the provider(s) you actually use; nothing else is downloaded\r\n- **Fully typed** — written in TypeScript, ships with complete `.d.ts` declarations\r\n- **Dual module format** — works in both ESM and CommonJS projects\r\n- **Every component works standalone** — use just `GroqLLM` for plain text generation, or just an embedder, without the rest of the pipeline\r\n\r\n---\r\n\r\n## 2. Installation\r\n\r\n```bash\r\nnpm install @amjadkhan-dev/ragify\r\n```\r\n\r\nRagify's provider SDKs are **optional peer dependencies** — install only what you use. If you call an adapter without its SDK installed, Ragify throws a clear error telling you exactly which package to install, rather than a cryptic module-not-found crash.\r\n\r\n```bash\r\n# Embedding providers\r\nnpm install @google/generative-ai       # GeminiEmbedder / GeminiLLM\r\nnpm install openai                      # OpenAIEmbedder\r\n\r\n# LLM generation\r\nnpm install groq-sdk                    # GroqLLM\r\n\r\n# Vector stores\r\nnpm install @pinecone-database/pinecone # PineconeVectorStore\r\nnpm install chromadb                    # ChromaVectorStore\r\nnpm install @qdrant/js-client-rest      # QdrantVectorStore\r\n```\r\n\r\n**Requirements:** Node.js ≥ 20.0.0 (required by several underlying SDKs and the test tooling; the package will not run correctly on Node 18 or earlier).\r\n\r\n---\r\n\r\n## 3. Quick Start\r\n\r\n### Zero-cost, zero-setup (recommended first step)\r\n\r\nUses the built-in mock embedder and in-memory store — no API keys, no signups, no cost. Good for confirming the package works before committing to any real provider.\r\n\r\n```typescript\r\nimport {\r\n  RagifyPipeline,\r\n  FixedSizeChunker,\r\n  InMemoryVectorStore,\r\n  MockEmbedder,\r\n} from '@amjadkhan-dev/ragify';\r\n\r\nconst pipeline = new RagifyPipeline({\r\n  chunker: new FixedSizeChunker(),\r\n  embedder: new MockEmbedder(),\r\n  vectorStore: new InMemoryVectorStore(),\r\n});\r\n\r\nawait pipeline.addDocuments([\r\n  { id: 'doc1', content: 'Node.js is a JavaScript runtime built on Chrome V8.' },\r\n  { id: 'doc2', content: 'Bananas are a great source of potassium.' },\r\n]);\r\n\r\nconst results = await pipeline.query('Tell me about JavaScript runtimes');\r\nconsole.log(results);\r\n```\r\n\r\n### With real embeddings and grounded generation\r\n\r\n```typescript\r\nimport {\r\n  RagifyPipeline,\r\n  RecursiveChunker,\r\n  InMemoryVectorStore,\r\n  GeminiEmbedder,\r\n  GroqLLM,\r\n} from '@amjadkhan-dev/ragify';\r\n\r\nconst pipeline = new RagifyPipeline({\r\n  chunker: new RecursiveChunker(),\r\n  embedder: new GeminiEmbedder(),   // reads GEMINI_API_KEY from env\r\n  vectorStore: new InMemoryVectorStore(),\r\n  llm: new GroqLLM(),               // reads GROQ_API_KEY from env\r\n});\r\n\r\nawait pipeline.addDocuments([\r\n  { id: 'doc1', content: 'Your document content here...' },\r\n]);\r\n\r\nconst answer = await pipeline.generate('What does this document say about X?');\r\nconsole.log(answer);\r\n```\r\n\r\n---\r\n\r\n## 4. Core Architecture\r\n\r\nRagify has **seven** swappable component types, all consumed through one `RagifyConfig` object passed to `RagifyPipeline`:\r\n\r\n| Component | Interface | Required? | Purpose |\r\n|---|---|---|---|\r\n| `chunker` | `Chunker` | ✅ Yes | Splits documents into indexable pieces |\r\n| `embedder` | `Embedder` | ✅ Yes | Converts text into vector embeddings |\r\n| `vectorStore` | `VectorStore` | ✅ Yes | Stores and searches embedded chunks |\r\n| `retriever` | `Retriever` | optional | Custom retrieval strategy (e.g. hybrid search) — overrides default `vectorStore.query()` |\r\n| `llm` | `LLMWrapper` | optional | Generates answers from retrieved context — required only for `.generate()` |\r\n| `cache` | `EmbeddingCache` | optional | Skips re-embedding unchanged content |\r\n| `reranker` | `Reranker` | optional | Reorders retrieved results for higher precision |\r\n\r\n### Data flow\r\n\r\n```\r\nRagifyDocument (raw text + metadata)\r\n      │\r\n      ▼  chunker.chunk()\r\n   Chunk[] (split pieces, no embeddings yet)\r\n      │\r\n      ▼  embedder.embed()  [checked against cache first, if configured]\r\n   Chunk[] (now with .embedding vectors attached)\r\n      │\r\n      ▼  vectorStore.upsert()  +  retriever.index() [if configured]\r\n   Stored in vector database / BM25 index\r\n      │\r\n      │  ... later, at query time ...\r\n      ▼  retriever.retrieve()  OR  embedder.embed() + vectorStore.query()\r\n   VectorSearchResult[] (ranked chunks + scores)\r\n      │\r\n      ▼  reranker.rerank()  [if configured]\r\n   VectorSearchResult[] (re-ordered, trimmed to topK)\r\n      │\r\n      ▼  llm.generate(question, context)  [only for .generate()]\r\n   string (final grounded answer)\r\n```\r\n\r\n---\r\n\r\n## 5. RagifyPipeline API\r\n\r\n```typescript\r\nclass RagifyPipeline {\r\n  constructor(config: RagifyConfig);\r\n\r\n  addDocuments(documents: RagifyDocument[]): Promise<void>;\r\n  query(text: string, topK?: number): Promise<VectorSearchResult[]>;\r\n  generate(question: string, topK?: number): Promise<string>;\r\n}\r\n```\r\n\r\n### `addDocuments(documents)`\r\n\r\nFor each document: chunks it, embeds each chunk (using the cache if configured — unchanged content is skipped), upserts into the vector store, and indexes into the retriever if one is configured.\r\n\r\n```typescript\r\nawait pipeline.addDocuments([\r\n  { id: 'doc1', content: 'First document text...', metadata: { source: 'manual.pdf', page: 1 } },\r\n  { id: 'doc2', content: 'Second document text...' },\r\n]);\r\n```\r\n\r\n`metadata` is optional and passed through to every chunk derived from that document — useful for filtering or displaying source information later.\r\n\r\n### `query(text, topK = 5)`\r\n\r\nReturns the `topK` most relevant chunks for `text`, each with a `score` (higher = more relevant, consistent across every vector store adapter).\r\n\r\n- If `retriever` is configured, delegates to it (e.g. `HybridRetriever`).\r\n- Otherwise, embeds `text` directly and calls `vectorStore.query()`.\r\n- If `reranker` is configured, internally fetches `topK * 3` candidates first, then re-ranks down to `topK` — giving the re-ranker a wider pool to actually improve on.\r\n\r\n```typescript\r\nconst results = await pipeline.query('How do I reset my password?', 3);\r\n// [{ score: 0.91, chunk: { id: '...', content: '...', documentId: '...' } }, ...]\r\n```\r\n\r\n### `generate(question, topK = 5)`\r\n\r\nCalls `query()` internally, then passes the retrieved chunks' content as `context` to `llm.generate()`.\r\n\r\n**Throws** `No LLM configured. Pass an llm (e.g. GroqLLM, GeminiLLM) in RagifyConfig to use generate().` if `config.llm` was not set.\r\n\r\n```typescript\r\nconst answer = await pipeline.generate('How do I reset my password?');\r\n// \"To reset your password, go to Settings > Security and click 'Reset Password'...\"\r\n```\r\n\r\n---\r\n\r\n## 6. Chunkers\r\n\r\n### `Chunker` interface\r\n\r\n```typescript\r\ninterface Chunker {\r\n  chunk(document: RagifyDocument, options?: ChunkerOptions): Promise<Chunk[]> | Chunk[];\r\n}\r\n\r\ninterface ChunkerOptions {\r\n  chunkSize?: number;    // default: 500 (characters)\r\n  chunkOverlap?: number; // default: 50 (characters)\r\n}\r\n```\r\n\r\n### `FixedSizeChunker`\r\n\r\nSplits `content` by raw character count, with no awareness of sentence or paragraph boundaries. Simple and predictable, but can cut a sentence — or a word — in half.\r\n\r\n```typescript\r\nimport { FixedSizeChunker } from '@amjadkhan-dev/ragify';\r\n\r\nconst chunker = new FixedSizeChunker();\r\nconst chunks = chunker.chunk(document, { chunkSize: 500, chunkOverlap: 50 });\r\n```\r\n\r\n**When to use:** quick prototyping, or content where sentence boundaries genuinely don't matter (e.g. structured logs, code).\r\n\r\n### `RecursiveChunker` (recommended default)\r\n\r\nTries to split on natural boundaries in priority order — paragraph breaks, then line breaks, then sentence endings, then word boundaries — only falling back to a hard character split as an absolute last resort. Small adjacent pieces are merged back up toward `chunkSize`, carrying `chunkOverlap` characters from the end of one chunk into the start of the next.\r\n\r\n```typescript\r\nimport { RecursiveChunker } from '@amjadkhan-dev/ragify';\r\n\r\nconst chunker = new RecursiveChunker();\r\nconst chunks = chunker.chunk(document, {\r\n  chunkSize: 500,\r\n  chunkOverlap: 50,\r\n  separators: ['\\n\\n', '\\n', '. ', ' ', ''], // optional, this is the default priority order\r\n});\r\n```\r\n\r\n**When to use:** almost always — this is the better default for real prose/document content, since it avoids severing sentences mid-thought, which directly improves retrieval and generation quality.\r\n\r\n---\r\n\r\n## 7. Embedders\r\n\r\n### `Embedder` interface\r\n\r\n```typescript\r\ninterface Embedder {\r\n  embed(texts: string[]): Promise<number[][]>;\r\n  readonly dimensions: number;\r\n}\r\n```\r\n\r\nAll real (non-mock) embedders include automatic retry-with-exponential-backoff and request batching internally — you never need to implement rate-limit handling yourself.\r\n\r\n### `MockEmbedder`\r\n\r\n```typescript\r\nimport { MockEmbedder } from '@amjadkhan-dev/ragify';\r\nconst embedder = new MockEmbedder();\r\n```\r\n\r\nNo options, no API key, no network calls. `dimensions = 8`. Produces deterministic vectors from a character-hash of the input text — **not semantically meaningful**. Use only for testing the plumbing of your pipeline, never in production, since it cannot actually judge meaning or relevance.\r\n\r\n### `GeminiEmbedder`\r\n\r\n```typescript\r\nimport { GeminiEmbedder } from '@amjadkhan-dev/ragify';\r\n\r\nconst embedder = new GeminiEmbedder({\r\n  apiKey: 'your-key',            // optional — defaults to process.env.GEMINI_API_KEY\r\n  model: 'text-embedding-004',   // optional — defaults to process.env.GEMINI_EMBEDDING_MODEL ?? 'text-embedding-004'\r\n  batchSize: 100,                // optional — texts per internal batch\r\n  dimensions: 768,               // optional — MUST match your actual model's real output size (see note below)\r\n});\r\n```\r\n\r\n**Getting an API key (free tier):** https://aistudio.google.com/apikey — no credit card required for the free tier.\r\n\r\n**⚠️ Important — dimension varies by model.** Different Gemini embedding models output different vector sizes:\r\n- `text-embedding-004` → 768 dimensions\r\n- `gemini-embedding-001` → 3072 dimensions by default (some preview models too)\r\n- `gemini-embedding-2-preview` → 3072 dimensions\r\n\r\nThe `dimensions` constructor option is **not automatically verified against the real API response** — if you set it incorrectly, or leave the default while using a model with a different real output size, you won't see an error from `GeminiEmbedder` itself; you'll only find out when your vector store rejects the upsert due to a dimension mismatch (see [§18 Troubleshooting](#18-troubleshooting)). Always check your specific model's actual output dimension in Google's docs and set `dimensions` (and your vector store's index dimension) to match exactly.\r\n\r\nRequires `@google/generative-ai` installed.\r\n\r\n### `OpenAIEmbedder`\r\n\r\n```typescript\r\nimport { OpenAIEmbedder } from '@amjadkhan-dev/ragify';\r\n\r\nconst embedder = new OpenAIEmbedder({\r\n  apiKey: 'your-key',                 // optional — defaults to process.env.OPENAI_API_KEY\r\n  model: 'text-embedding-3-small',    // optional — default shown\r\n  batchSize: 100,                     // optional\r\n});\r\n```\r\n\r\n`dimensions` is derived automatically: 1536 for `text-embedding-3-small`, 3072 for `text-embedding-3-large`.\r\n\r\nRequires `openai` installed. OpenAI embeddings are **paid** (no free tier) — billed per your OpenAI account.\r\n\r\n---\r\n\r\n## 8. Vector Stores — Full Setup Guides\r\n\r\nEvery vector store implements this interface:\r\n\r\n```typescript\r\ninterface VectorStore {\r\n  upsert(chunks: Chunk[]): Promise<void>;\r\n  query(embedding: number[], topK: number): Promise<VectorSearchResult[]>;\r\n  delete(ids: string[]): Promise<void>;\r\n}\r\n```\r\n\r\n### 8.1 `InMemoryVectorStore` — zero setup\r\n\r\n```typescript\r\nimport { InMemoryVectorStore } from '@amjadkhan-dev/ragify';\r\nconst vectorStore = new InMemoryVectorStore();\r\n```\r\n\r\nNo account, no config, no options. Stores everything in a JavaScript `Map` in the current process — **data is lost when the process exits**. Cosine similarity search.\r\n\r\n**Use for:** local development, testing, short-lived scripts. **Do not use for:** anything that needs to persist data or scale beyond a single process's memory.\r\n\r\n---\r\n\r\n### 8.2 `PineconeVectorStore` — managed cloud, free tier\r\n\r\n**Full setup, from zero:**\r\n\r\n1. Sign up free at https://app.pinecone.io — no credit card required for the free tier.\r\n2. Create a new index. **Critical step — choose the right creation flow:**\r\n   - Pinecone's UI offers two paths: **\"Pinecone-hosted model\"** (an integrated embedding model, with a dimension *dropdown* limited to fixed values like 1024/2048/768/512/384) and **\"Custom\"** / **bring-your-own-vectors** (a plain numeric *input field* for dimension).\r\n   - You want **Custom** — because Ragify generates embeddings itself (via `GeminiEmbedder`/`OpenAIEmbedder`), Pinecone should just store and search vectors you provide, not generate its own.\r\n   - If you only see the dropdown with fixed values, you're on the wrong creation path — look for a toggle near the top of the creation flow, or an earlier step asking \"How do you want to add data?\" and choose \"I have my own vectors.\"\r\n3. Set the index's dimension to **exactly match your embedder's real output size** (see §7 for how this varies by model — this is the single most common setup error, covered in detail in §18).\r\n4. Metric: `cosine`.\r\n5. Copy your API key from the Pinecone dashboard.\r\n\r\n**Environment variables:**\r\n```\r\nPINECONE_API_KEY=your-real-key-here\r\nPINECONE_INDEX_NAME=your-index-name\r\n```\r\n\r\n**Usage:**\r\n```typescript\r\nimport { PineconeVectorStore } from '@amjadkhan-dev/ragify';\r\n\r\nconst vectorStore = new PineconeVectorStore({\r\n  apiKey: process.env.PINECONE_API_KEY,       // optional, this is the default source\r\n  indexName: process.env.PINECONE_INDEX_NAME, // optional, this is the default source\r\n  namespace: 'my-namespace',                   // optional — isolate data within one index\r\n});\r\n```\r\n\r\n**Internal implementation notes** (relevant if you hit errors, or are on a different SDK major version):\r\n- Requires `@pinecone-database/pinecone`, tested and built against **v8.x**.\r\n- Content and metadata are stored in Pinecone's `metadata` field (Pinecone itself only natively stores vectors + metadata, not a first-class \"text\" field) and reconstructed into a `Chunk` shape when you query.\r\n- The v8 SDK requires `upsert({ records: [...] })` — a wrapped object, not a bare array (this changed from earlier SDK versions).\r\n- The v8 SDK removed legacy string-based index targeting (`pc.index('name')`); this adapter resolves the index host via `describeIndex()` first, then targets it by host.\r\n\r\n---\r\n\r\n### 8.3 `ChromaVectorStore` — open-source, self-hosted or cloud\r\n\r\nTwo connection modes, pick one:\r\n\r\n**Option A — Self-hosted (local, via Docker):**\r\n```bash\r\ndocker run -p 8000:8000 chromadb/chroma\r\n```\r\n```\r\nCHROMA_PATH=http://localhost:8000\r\n```\r\n\r\n**Option B — Chroma Cloud (managed, free starter credits, no local server needed):**\r\n1. Sign up free at https://trychroma.com/cloud\r\n2. Create a database, grab your API key, tenant ID, and database name from the dashboard.\r\n```\r\nCHROMA_API_KEY=ck-your-key\r\nCHROMA_TENANT=your-tenant-id\r\nCHROMA_DATABASE=your-database-name\r\n```\r\n\r\n**Usage** (auto-detects mode based on whether `apiKey` is present):\r\n```typescript\r\nimport { ChromaVectorStore } from '@amjadkhan-dev/ragify';\r\n\r\nconst vectorStore = new ChromaVectorStore({\r\n  collectionName: 'my-collection', // optional, default: 'ragify-collection'\r\n  // self-hosted:\r\n  path: process.env.CHROMA_PATH,\r\n  // OR Chroma Cloud:\r\n  apiKey: process.env.CHROMA_API_KEY,\r\n  tenant: process.env.CHROMA_TENANT,\r\n  database: process.env.CHROMA_DATABASE,\r\n});\r\n```\r\n\r\n**Internal implementation notes:**\r\n- Requires `chromadb` (v3 client) installed.\r\n- The collection is created automatically on first use, with `hnsw:space: cosine` set explicitly — Chroma defaults to L2 distance otherwise, which would make scores incomparable to the other vector store adapters in this package.\r\n- **Chroma's metadata only supports flat string/number/boolean values.** If a `chunk.metadata` field contains a nested object or array, it is silently dropped (not stored, not errored) when upserting into Chroma specifically — this is a Chroma limitation, not a Ragify bug. Keep metadata flat if you need it preserved.\r\n\r\n---\r\n\r\n### 8.4 `QdrantVectorStore` — open-source, self-hosted or cloud\r\n\r\nTwo connection modes, pick one:\r\n\r\n**Option A — Self-hosted (local, via Docker):**\r\n```bash\r\ndocker run -p 6333:6333 qdrant/qdrant\r\n```\r\n```\r\nQDRANT_URL=http://localhost:6333\r\n```\r\n\r\n**Option B — Qdrant Cloud (managed, free tier cluster):**\r\n1. Sign up free at https://cloud.qdrant.io\r\n2. Create a free cluster, grab the cluster URL and API key.\r\n```\r\nQDRANT_URL=https://your-cluster-id.your-region.aws.cloud.qdrant.io\r\nQDRANT_API_KEY=your-api-key\r\n```\r\n\r\n**Usage:**\r\n```typescript\r\nimport { QdrantVectorStore } from '@amjadkhan-dev/ragify';\r\n\r\nconst vectorStore = new QdrantVectorStore({\r\n  url: process.env.QDRANT_URL,          // required (constructor throws without it)\r\n  apiKey: process.env.QDRANT_API_KEY,    // optional (needed for Cloud, not for local)\r\n  collectionName: 'my-collection',       // optional, default: 'ragify-collection'\r\n  dimensions: 768,                       // optional — inferred automatically from the first embedding if omitted\r\n});\r\n```\r\n\r\n**Internal implementation notes:**\r\n- Requires `@qdrant/js-client-rest` installed.\r\n- The collection is **auto-created on first use** — unlike Pinecone, you do not need to manually create it in a dashboard first. Dimension is inferred from the actual embedding vector's length if you don't pass `dimensions` explicitly.\r\n- Uses the current, non-deprecated `.query()` method internally (not the older `.search()` method).\r\n- **Qdrant only accepts unsigned integers or UUIDs as point IDs** — it rejects arbitrary strings like `doc1-chunk-0`, which is what Ragify's chunkers normally produce. This adapter automatically maps every chunk ID to a **deterministic UUID** internally (same input string always produces the same UUID, so re-indexing a chunk overwrites rather than duplicates it) and stores your original chunk ID in the payload, restoring it correctly on query. You never need to think about this — it's fully transparent — but it's worth knowing about if you inspect Qdrant's dashboard directly and see UUIDs instead of your original chunk IDs.\r\n\r\n---\r\n\r\n### 8.5 Choosing a vector store\r\n\r\n| Store | Setup effort | Persistence | Cost | Best for |\r\n|---|---|---|---|---|\r\n| `InMemoryVectorStore` | None | ❌ Lost on exit | Free | Local dev, testing, short scripts |\r\n| `PineconeVectorStore` | Medium (manual index + dimension setup) | ✅ | Free tier | Managed production, simplest ops |\r\n| `ChromaVectorStore` | Low–Medium (Docker or Cloud signup) | ✅ | Free (self-hosted) / free tier (Cloud) | Open-source preference, local-first dev |\r\n| `QdrantVectorStore` | Low (auto-creates collection) | ✅ | Free (self-hosted) / free tier (Cloud) | Least manual setup among the persistent options |\r\n\r\n---\r\n\r\n## 9. Retrieval Strategies\r\n\r\n### `Retriever` interface\r\n\r\n```typescript\r\ninterface Retriever {\r\n  index(chunks: Chunk[]): Promise<void> | void;\r\n  retrieve(query: string, topK: number): Promise<VectorSearchResult[]>;\r\n}\r\n```\r\n\r\nIf no `retriever` is configured in `RagifyConfig`, `RagifyPipeline` falls back to a plain embed-and-query against `vectorStore` directly — this is fine for many use cases, and you don't need a `Retriever` at all unless you want hybrid search.\r\n\r\n### `BM25Retriever`\r\n\r\nPure keyword-statistics retrieval — no embeddings, no network calls, catches exact terms and rare keywords that dense embedding search can blur together (e.g. product codes, exact names, technical jargon).\r\n\r\n```typescript\r\nimport { BM25Retriever } from '@amjadkhan-dev/ragify';\r\n\r\nconst bm25 = new BM25Retriever({ k1: 1.5, b: 0.75 }); // both optional, these are the defaults\r\n```\r\nRarely used standalone in `RagifyConfig` — usually consumed internally by `HybridRetriever` instead.\r\n\r\n### `HybridRetriever` (recommended for production)\r\n\r\nCombines dense (vector similarity via your `embedder` + `vectorStore`) and sparse (BM25 keyword) search, fused using **Reciprocal Rank Fusion** — a chunk that ranks well in *both* dense and sparse search naturally floats to the top, giving you the best of semantic understanding and exact-keyword precision.\r\n\r\n```typescript\r\nimport { HybridRetriever, RagifyPipeline } from '@amjadkhan-dev/ragify';\r\n\r\nconst retriever = new HybridRetriever(vectorStore, embedder, {\r\n  rrfK: 60, // optional — higher = less aggressive fusion weighting, 60 is a well-established default\r\n});\r\n\r\nconst pipeline = new RagifyPipeline({\r\n  chunker,\r\n  embedder,\r\n  vectorStore,\r\n  retriever, // pipeline.query() and pipeline.generate() now use hybrid search automatically\r\n});\r\n```\r\n\r\n**When to use:** almost always for production RAG — hybrid search consistently outperforms either dense-only or sparse-only retrieval on real-world queries.\r\n\r\n---\r\n\r\n## 10. LLM Wrappers (Generation)\r\n\r\n### `LLMWrapper` interface\r\n\r\n```typescript\r\ninterface LLMWrapper {\r\n  generate(prompt: string, context?: string[]): Promise<string>;\r\n}\r\n```\r\n\r\nWhen `context` is provided, both built-in wrappers instruct the model to answer **only** from that context, and to honestly say so if the context doesn't contain the answer — reducing hallucination by default. When `context` is omitted, it's a normal, ungrounded generation call (see [§13](#13-using-components-standalone-without-the-pipeline) for standalone usage without RAG at all).\r\n\r\n### `GroqLLM`\r\n\r\n```typescript\r\nimport { GroqLLM } from '@amjadkhan-dev/ragify';\r\n\r\nconst llm = new GroqLLM({\r\n  apiKey: 'your-key',                       // optional — defaults to process.env.GROQ_API_KEY\r\n  model: 'llama-3.3-70b-versatile',         // optional — defaults to process.env.GROQ_MODEL ?? this value\r\n  temperature: 0.3,                          // optional — lower = more focused/deterministic\r\n});\r\n```\r\n\r\n**Getting an API key (free tier):** https://console.groq.com/keys — free, fast inference, no credit card required for the free tier. Requires `groq-sdk` installed.\r\n\r\n### `GeminiLLM`\r\n\r\n```typescript\r\nimport { GeminiLLM } from '@amjadkhan-dev/ragify';\r\n\r\nconst llm = new GeminiLLM({\r\n  apiKey: 'your-key',                // optional — defaults to process.env.GEMINI_API_KEY\r\n  model: 'gemini-1.5-flash',         // optional — defaults to process.env.GEMINI_LLM_MODEL ?? this value\r\n});\r\n```\r\n\r\nSame API key as `GeminiEmbedder` (https://aistudio.google.com/apikey). Requires `@google/generative-ai` installed.\r\n\r\n---\r\n\r\n## 11. Re-ranking\r\n\r\nRe-ranking runs *after* retrieval to reorder the top candidates using a more precise (but more expensive) relevance judgment than embeddings or BM25 alone provide.\r\n\r\n### `Reranker` interface\r\n\r\n```typescript\r\ninterface Reranker {\r\n  rerank(query: string, results: VectorSearchResult[], topK: number): Promise<VectorSearchResult[]>;\r\n}\r\n```\r\n\r\n### `LLMReranker`\r\n\r\nReuses your existing `GroqLLM`/`GeminiLLM` wrapper — no dedicated re-ranking API or extra cost/dependency needed. Scores each candidate 0–10 for relevance to the query, in concurrent batches, then re-sorts.\r\n\r\n```typescript\r\nimport { LLMReranker, RagifyPipeline } from '@amjadkhan-dev/ragify';\r\n\r\nconst pipeline = new RagifyPipeline({\r\n  chunker,\r\n  embedder,\r\n  vectorStore,\r\n  reranker: new LLMReranker(new GroqLLM(), {\r\n    concurrency: 5, // optional — how many candidates to score simultaneously\r\n  }),\r\n});\r\n```\r\n\r\n**Cost/latency note:** re-ranking makes one extra LLM call per candidate scored. `RagifyPipeline` automatically over-fetches `topK * 3` candidates before re-ranking down, to give the re-ranker real room to improve on the initial ranking — reasonable for `topK` in the 3–10 range. If a candidate's LLM response can't be parsed as a number, that candidate silently falls back to its original retrieval score rather than breaking the whole re-rank.\r\n\r\n**When to use:** when retrieval precision genuinely matters more than latency/cost — e.g. a support bot answering from a large knowledge base, where surfacing the *exact right* passage matters more than shaving off a few hundred milliseconds.\r\n\r\n---\r\n\r\n## 12. Caching\r\n\r\nAvoids re-embedding unchanged content, keyed by a hash of the chunk's content (salted with the embedder's class name, so switching providers never returns a stale, wrong-provider vector).\r\n\r\n### `EmbeddingCache` interface\r\n\r\n```typescript\r\ninterface EmbeddingCache {\r\n  get(key: string): Promise<number[] | undefined> | number[] | undefined;\r\n  set(key: string, embedding: number[]): Promise<void> | void;\r\n}\r\n```\r\n\r\n### `InMemoryCache`\r\n\r\n```typescript\r\nimport { InMemoryCache } from '@amjadkhan-dev/ragify';\r\nconst cache = new InMemoryCache();\r\n```\r\nNo options. Fast, but cleared when the process exits — only useful within a single long-running process (e.g. a server), not across separate script runs.\r\n\r\n### `FileCache`\r\n\r\n```typescript\r\nimport { FileCache } from '@amjadkhan-dev/ragify';\r\nconst cache = new FileCache({ filePath: '.ragify-cache.json' }); // optional, this is the default\r\n```\r\nPersists to a JSON file on disk — survives across separate script runs, which is where the real cost savings show up during development (re-running the same ingestion script twice only embeds once). Rewrites the entire file on every `set()` call — fine for development or moderate-scale use, not designed for high-throughput production write patterns.\r\n\r\n**Usage in the pipeline:**\r\n```typescript\r\nconst pipeline = new RagifyPipeline({\r\n  chunker,\r\n  embedder,\r\n  vectorStore,\r\n  cache: new FileCache(),\r\n});\r\n```\r\nRemember to add your cache file to `.gitignore` if using `FileCache` — it's generated local data, not something to commit.\r\n\r\n---\r\n\r\n## 13. Using Components Standalone (Without the Pipeline)\r\n\r\n**Every adapter in Ragify works independently** — you are not required to use `RagifyPipeline` at all if you only need one piece of functionality.\r\n\r\n### Just want text generation, no RAG at all?\r\n\r\n```typescript\r\nimport { GroqLLM } from '@amjadkhan-dev/ragify';\r\n\r\nconst llm = new GroqLLM();\r\nconst answer = await llm.generate('What is the capital of France?');\r\nconsole.log(answer);\r\n```\r\nNo documents, no chunking, no embedding, no vector store — this is a complete, working LLM call, functionally equivalent to calling the Groq API directly, but with Ragify's built-in retry/backoff handling.\r\n\r\n### Just want embeddings, without storing/searching them?\r\n\r\n```typescript\r\nimport { GeminiEmbedder } from '@amjadkhan-dev/ragify';\r\n\r\nconst embedder = new GeminiEmbedder();\r\nconst vectors = await embedder.embed(['some text', 'more text']);\r\nconsole.log(vectors); // number[][]\r\n```\r\n\r\n### Just want to chunk text, without embedding it?\r\n\r\n```typescript\r\nimport { RecursiveChunker } from '@amjadkhan-dev/ragify';\r\n\r\nconst chunker = new RecursiveChunker();\r\nconst chunks = chunker.chunk({ id: 'doc1', content: 'A long document...' }, { chunkSize: 500 });\r\n```\r\n\r\n### Just want to store/search vectors you've already generated elsewhere?\r\n\r\n```typescript\r\nimport { QdrantVectorStore } from '@amjadkhan-dev/ragify';\r\n\r\nconst vectorStore = new QdrantVectorStore({ url: process.env.QDRANT_URL });\r\nawait vectorStore.upsert([\r\n  { id: 'c1', content: 'text', documentId: 'd1', embedding: [/* your own vector */] },\r\n]);\r\nconst results = await vectorStore.query([/* query vector */], 5);\r\n```\r\n\r\nThis modularity is deliberate — adopt only the pieces you actually need rather than buying into the whole framework at once.\r\n\r\n---\r\n\r\n## 14. Complete Configuration Reference\r\n\r\n```typescript\r\ninterface RagifyConfig {\r\n  chunker: Chunker;              // required\r\n  embedder: Embedder;            // required\r\n  vectorStore: VectorStore;      // required\r\n  retriever?: Retriever;         // optional — enables hybrid/custom retrieval\r\n  llm?: LLMWrapper;              // optional — required only to use .generate()\r\n  cache?: EmbeddingCache;        // optional — enables embedding caching\r\n  reranker?: Reranker;           // optional — enables re-ranking on query()/generate()\r\n}\r\n\r\ninterface RagifyDocument {\r\n  id: string;\r\n  content: string;\r\n  metadata?: Record<string, unknown>;\r\n}\r\n\r\ninterface Chunk {\r\n  id: string;\r\n  content: string;\r\n  documentId: string;\r\n  metadata?: Record<string, unknown>;\r\n  embedding?: number[];\r\n}\r\n\r\ninterface VectorSearchResult {\r\n  chunk: Chunk;\r\n  score: number; // higher = more relevant, consistent across all vector store adapters\r\n}\r\n```\r\n\r\n---\r\n\r\n## 15. Environment Variables — Full Reference\r\n\r\n```bash\r\n# ── Gemini (embeddings + generation) ──────────────────────────\r\nGEMINI_API_KEY=\r\nGEMINI_EMBEDDING_MODEL=        # optional, defaults to text-embedding-004\r\nGEMINI_LLM_MODEL=              # optional, defaults to gemini-1.5-flash\r\n\r\n# ── Groq (generation) ──────────────────────────────────────────\r\nGROQ_API_KEY=\r\nGROQ_MODEL=                    # optional, defaults to llama-3.3-70b-versatile\r\n\r\n# ── OpenAI (embeddings) ────────────────────────────────────────\r\nOPENAI_API_KEY=\r\n\r\n# ── Pinecone ───────────────────────────────────────────────────\r\nPINECONE_API_KEY=\r\nPINECONE_INDEX_NAME=\r\n\r\n# ── Chroma — self-hosted OR Chroma Cloud, choose one ───────────\r\nCHROMA_PATH=                   # self-hosted, e.g. http://localhost:8000\r\nCHROMA_API_KEY=                # Chroma Cloud\r\nCHROMA_TENANT=                 # Chroma Cloud\r\nCHROMA_DATABASE=               # Chroma Cloud\r\n\r\n# ── Qdrant ─────────────────────────────────────────────────────\r\nQDRANT_URL=                    # required if using QdrantVectorStore\r\nQDRANT_API_KEY=                # required for Qdrant Cloud, omit for local\r\n```\r\n\r\nEvery constructor option can also be passed explicitly in code instead of via environment variable — explicit constructor options always take priority over environment variables, which take priority over hardcoded defaults.\r\n\r\n---\r\n\r\n## 16. Error Handling\r\n\r\nRagify throws descriptive `Error`s rather than failing silently or with opaque stack traces.\r\n\r\n| Error message | Cause | Fix |\r\n|---|---|---|\r\n| `<Provider> API key missing...` | No API key passed or set in env | Pass `apiKey` in constructor options, or set the relevant env var |\r\n| `Missing optional dependency \"X\". Install it with: npm install X` | Using an adapter without installing its peer SDK | Run the exact `npm install` command shown |\r\n| `Chunk <id> has no embedding — embed before upserting.` | Calling `vectorStore.upsert()` directly with unembedded chunks | Use `pipeline.addDocuments()` (embeds automatically), or call `embedder.embed()` yourself first |\r\n| `No LLM configured. Pass an llm...` | Calling `.generate()` without an `llm` in config | Add `llm: new GroqLLM()` (or `GeminiLLM`) to `RagifyConfig` |\r\n| `Qdrant URL missing...` | No `url` passed or `QDRANT_URL` set | Pass `url` explicitly or set the env var |\r\n| `Pinecone index name missing...` | No `indexName` passed or `PINECONE_INDEX_NAME` set | Pass `indexName` explicitly or set the env var |\r\n\r\nAll network-calling adapters (embedders, LLMs, vector stores) retry transient failures automatically — 3 attempts with exponential backoff — before throwing.\r\n\r\n---\r\n\r\n## 17. Extending Ragify — Writing Custom Adapters\r\n\r\nEvery component is just a TypeScript interface. Implement it to plug in a provider Ragify doesn't ship out of the box.\r\n\r\n### Custom embedder\r\n\r\n```typescript\r\nimport type { Embedder } from '@amjadkhan-dev/ragify';\r\n\r\nclass MyCustomEmbedder implements Embedder {\r\n  readonly dimensions = 512;\r\n\r\n  async embed(texts: string[]): Promise<number[][]> {\r\n    // call your own embedding service, return one vector per input text\r\n    return texts.map((t) => myEmbeddingService.embed(t));\r\n  }\r\n}\r\n```\r\n\r\n### Custom vector store\r\n\r\n```typescript\r\nimport type { VectorStore, VectorSearchResult, Chunk } from '@amjadkhan-dev/ragify';\r\n\r\nclass MyCustomVectorStore implements VectorStore {\r\n  async upsert(chunks: Chunk[]): Promise<void> { /* ... */ }\r\n  async query(embedding: number[], topK: number): Promise<VectorSearchResult[]> { /* ... */ }\r\n  async delete(ids: string[]): Promise<void> { /* ... */ }\r\n}\r\n```\r\n\r\n### Custom chunker\r\n\r\n```typescript\r\nimport type { Chunker, ChunkerOptions, RagifyDocument, Chunk } from '@amjadkhan-dev/ragify';\r\n\r\nclass MyCustomChunker implements Chunker {\r\n  chunk(document: RagifyDocument, options?: ChunkerOptions): Chunk[] {\r\n    // your splitting logic\r\n  }\r\n}\r\n```\r\n\r\n### Custom LLM wrapper\r\n\r\n```typescript\r\nimport type { LLMWrapper } from '@amjadkhan-dev/ragify';\r\n\r\nclass MyCustomLLM implements LLMWrapper {\r\n  async generate(prompt: string, context?: string[]): Promise<string> {\r\n    // call your own LLM API\r\n  }\r\n}\r\n```\r\n\r\nThe same pattern applies to `Retriever`, `EmbeddingCache`, and `Reranker` — implement the interface, pass an instance into `RagifyConfig`, and `RagifyPipeline` uses it exactly like a built-in adapter, since it only ever talks to these interfaces, never to a specific provider's SDK directly.\r\n\r\n---\r\n\r\n## 18. Troubleshooting\r\n\r\n### \"Vector dimension X does not match the dimension of the index Y\" (Pinecone)\r\n\r\nYour vector store's index was created with a fixed dimension that doesn't match your embedder's actual output size. This is the single most common setup error.\r\n\r\n**Fix:** Check your embedder's real output dimension (see §7 — this varies by specific model, not just by provider), then either:\r\n- Recreate the Pinecone index with the correct dimension, or\r\n- If using Gemini's newer models with `outputDimensionality` truncation support, configure the embedder to truncate to match your existing index (requires passing that option through — not enabled by default in this package as of the current version).\r\n\r\n### \"Must pass in at least 1 record to upsert\" (Pinecone) despite having real data\r\n\r\nAlmost always an SDK version mismatch — this specific error appears when a newer Pinecone SDK major version's `upsert()` call signature has changed. This package's `PineconeVectorStore` is built for SDK v8.x's `{ records: [...] }` format. If you're on a different major version, check Pinecone's changelog for that version's `upsert()` signature.\r\n\r\n### \"value X is not a valid point ID, valid values are either an unsigned integer or a UUID\" (Qdrant)\r\n\r\nYou should not see this from `QdrantVectorStore` — the built-in adapter handles ID mapping internally (see §8.4). If you see this, you're likely calling Qdrant's own SDK directly with raw Ragify chunk IDs rather than going through `QdrantVectorStore`.\r\n\r\n### Chroma metadata fields missing after upsert\r\n\r\nChroma only supports flat string/number/boolean metadata values. Nested objects/arrays in `chunk.metadata` are silently dropped when using `ChromaVectorStore` specifically (not a bug — a Chroma platform limitation). Flatten your metadata before passing it in if you need those fields preserved.\r\n\r\n### `vi.fn()` / mock-related errors when writing your own tests against Ragify\r\n\r\nIf you're writing tests that mock a provider SDK Ragify calls internally (e.g. mocking `groq-sdk` in your own app's tests), remember that classes instantiated with `new` cannot be arrow functions in JavaScript. Use `vi.fn().mockImplementation(function (this: any) { ... })`, not an arrow function, when mocking any SDK client class.\r\n\r\n### Slow re-ranking, or hitting provider rate limits during re-rank\r\n\r\nLower `LLMReranker`'s `concurrency` option (default 5), or reduce `topK` — re-ranking cost/latency scales roughly linearly with the number of candidates scored.\r\n\r\n### `SyntaxError: The requested module 'node:util' does not provide an export named 'styleText'`\r\n\r\nThis means you're running on Node 18 or earlier. Several of Ragify's dev/test tooling dependencies require Node ≥ 20. Upgrade your Node version.\r\n\r\n### `TypeError: () => (...) is not a constructor` in your own mocked tests\r\n\r\nSame root cause as the `vi.fn()` note above — you mocked a class-like export with an arrow function. Arrow functions cannot be used with `new`. Switch to a `function` declaration in the mock.\r\n\r\n---\r\n\r\n## 19. Compatibility\r\n\r\n| | Supported |\r\n|---|---|\r\n| Node.js | ≥ 20.0.0 |\r\n| Module formats | ESM and CommonJS (dual build — `import` and `require` both work) |\r\n| TypeScript | ≥ 5.0 (ships its own `.d.ts`; also works fine from plain JavaScript) |\r\n| Operating systems | Cross-platform (developed and tested on Windows/PowerShell, Linux CI via GitHub Actions on Node 20 & 22) |\r\n\r\n---\r\n\r\n## 20. End-to-End Example: Production RAG App\r\n\r\nA complete, realistic setup combining hybrid retrieval, caching, and re-ranking:\r\n\r\n```typescript\r\nimport 'dotenv/config';\r\nimport {\r\n  RagifyPipeline,\r\n  RecursiveChunker,\r\n  QdrantVectorStore,\r\n  GeminiEmbedder,\r\n  GroqLLM,\r\n  HybridRetriever,\r\n  FileCache,\r\n  LLMReranker,\r\n} from '@amjadkhan-dev/ragify';\r\n\r\n// Shared instances — embedder and vectorStore are reused by both the pipeline and the retriever\r\nconst embedder = new GeminiEmbedder();\r\nconst vectorStore = new QdrantVectorStore({ url: process.env.QDRANT_URL });\r\nconst llm = new GroqLLM();\r\n\r\nconst pipeline = new RagifyPipeline({\r\n  chunker: new RecursiveChunker(),\r\n  embedder,\r\n  vectorStore,\r\n  llm,\r\n  retriever: new HybridRetriever(vectorStore, embedder),\r\n  cache: new FileCache(),\r\n  reranker: new LLMReranker(llm),\r\n});\r\n\r\n// Ingest once — re-running this with the same content will skip re-embedding thanks to the cache\r\nawait pipeline.addDocuments([\r\n  { id: 'faq-1', content: 'To reset your password, go to Settings > Security...', metadata: { category: 'account' } },\r\n  { id: 'faq-2', content: 'Refunds are processed within 5-7 business days...', metadata: { category: 'billing' } },\r\n  // ... more documents\r\n]);\r\n\r\n// Query with the full pipeline: hybrid retrieval -> re-ranking -> grounded generation\r\nconst answer = await pipeline.generate('How long do refunds take?');\r\nconsole.log(answer);\r\n```\r\n\r\n---\r\n\r\n## 21. FAQ\r\n\r\n**Do I have to use all seven config options?**\r\nNo — only `chunker`, `embedder`, and `vectorStore` are required. Everything else (`retriever`, `llm`, `cache`, `reranker`) is optional and additive.\r\n\r\n**Can I mix providers — e.g. Gemini for embeddings, Groq for generation?**\r\nYes, this is the normal/expected usage pattern, not an edge case. Every adapter is independent.\r\n\r\n**Does Ragify support streaming responses from the LLM?**\r\nNot currently — `LLMWrapper.generate()` returns a complete string, not a stream. This is a known gap for a future version.\r\n\r\n**Can I use Ragify from plain JavaScript, not TypeScript?**\r\nYes — it's fully usable from JS; TypeScript just gets full autocomplete/type-checking for free via the shipped `.d.ts` files.\r\n\r\n**What happens if I call `pipeline.query()` before calling `addDocuments()`?**\r\nYou'll get an empty results array (or an error from some vector stores if the collection/index doesn't exist yet) — there's nothing indexed to search.\r\n\r\n**Is my data sent anywhere by Ragify itself?**\r\nNo — Ragify only makes network calls to the specific provider SDKs you configure (e.g. Gemini's API when you use `GeminiEmbedder`). It has no telemetry or third-party calls of its own.\r\n\r\n---\r\n\r\n## 22. Contributing\r\n\r\nContributions are welcome — see [CONTRIBUTING.md](./CONTRIBUTING.md) for setup, coding conventions, and how to add a new adapter.\r\n\r\n---\r\n\r\n## 23. License\r\n\r\nMIT © [Amjad Ullah](https://github.com/AmjadKhan88)","readmeFilename":"README.md"}