GNL 0.17: early and moving fast — APIs can change between releasesJoin us

GNL
Docs menu
Core · Free@gnldev/rag

RAG pipeline — chunking, vector stores, GraphRAG

Deterministic chunking, durable vector stores (pgvector, Qdrant, Chroma, Weaviate) and similarity-graph (GraphRAG) retrieval; run inside createRagTool it's journaled → exactly-once / replayable

What it's for / when to use it#

@gnldev/ragturns a knowledge base into a tool the agent can call. The critical difference: when the RAG tool is used inside runDurable its result is journaled → on resume/replay the same documents come back, no new embed/query call is made (exactly-once RAG) — a retrieval that already ran is replayed from the journal rather than re-issued.

The pipeline has three parts: (1) chunking — splits raw text/documents into chunks; (2) vector store — in-memory, durable Postgres/pgvector, a Qdrant, Chroma or Weaviate server, or a similarity graph (GraphRAG); (3) createRagTool — packages embed + query + optional rerank into a single tool.

Chunking — chunkText / chunkDocuments#

chunkText splits raw text into chunks; three strategies: recursive (default — splits by preferring paragraph → line → sentence → word boundaries), markdown (sections by heading hierarchy; adds a heading breadcrumb metadata field to every chunk), and character (fixed window). Consecutive chunks overlap via overlap (context continuity).

chunkDocuments splits VectorDocs and produces a list ready for indexDocuments. The chunk id is <docId>#<i> — deterministic, so re-indexing upserts into the same ids instead of producing duplicates. A document that fits in a single chunk passes through unsplit (no pointless #0 derivation).

chunkDocuments → indexDocuments
import { chunkDocuments, indexDocuments, InMemoryVectorStore } from '@gnldev/rag';

const chunks = chunkDocuments(docs, {
  size: 1200, overlap: 120, strategy: 'markdown',
});
// chunks[i].id === 'guide#3', metadata: { source: 'guide', chunk: 3, heading: 'Kurulum > Docker' }

const store = new InMemoryVectorStore();
await indexDocuments(store, chunks, embed);

Durable store — PostgresVectorStore (pgvector)#

PostgresVectorStoreand InMemoryVectorStore expose the same interface — Postgres backs it durably with pgvector: it takes an injectable pool, lazy-loads the pg driver, and supports HNSW or ivfflat indexes. This means the knowledge base persists across restarts in real customer projects.

PostgresVectorStore
import { PostgresVectorStore, createRagTool } from '@gnldev/rag';
import { Pool } from 'pg';

const store = new PostgresVectorStore({
  pool: new Pool({ connectionString: process.env.DATABASE_URL }),
  dimension: 1536,
  index: 'hnsw',
});
await store.upsert(items); // the same signature as InMemory

Vector servers — Qdrant, Chroma, Weaviate, Elasticsearch, OpenSearch#

If your vectors live in a dedicated vector database, five stores keep them there through the server's REST API, with no extra package: QdrantVectorStore, ChromaVectorStore (Chroma 1.x), WeaviateVectorStore (Weaviate 1.x), ElasticsearchVectorStore (Elasticsearch 9.x) and OpenSearchVectorStore (OpenSearch 3.x). Every call has a deadline (timeoutMs, 30 s by default). The collection is created on the first write with that write's vector length (or dimension); the API key goes in a header and never into an error message.

Qdrant · Chroma · Weaviate · Elasticsearch · OpenSearch
import { QdrantVectorStore, ChromaVectorStore, WeaviateVectorStore, ElasticsearchVectorStore, OpenSearchVectorStore } from '@gnldev/rag';

const qdrant = new QdrantVectorStore({ url: 'https://qdrant.internal:6333', apiKey: process.env.QDRANT_API_KEY });
const chroma = new ChromaVectorStore({ url: 'http://chroma.internal:8000', apiKey: process.env.CHROMA_TOKEN });
const weaviate = new WeaviateVectorStore({ url: 'https://weaviate.internal:8080', apiKey: process.env.WEAVIATE_API_KEY });
const es = new ElasticsearchVectorStore({ url: 'https://es.internal:9200', apiKey: process.env.ES_API_KEY });
const os = new OpenSearchVectorStore({ url: 'https://opensearch.internal:9200', auth: { username: 'gnl', password: process.env.OPENSEARCH_PASSWORD! } });
// each: createRagTool({ store: weaviate, embed })

All of them keep the same rules as every other store, measured by the same 20-scenario vector kit: owner, namespace and sharing are part of each record's id, so a write cannot take over another owner's document, even when two processes race; a filter narrows before ranking; equal scores at the top-K cut are read whole and ordered by id. That last one is work on Chroma and Weaviate: their search is approximate and misses some copies of one vector (Weaviate: 2,974 of 3,000 found with a limit of 4,000), so the tie is read exactly.

GraphRAG — indirect-relevance retrieval#

GraphRagdoes retrieval over a similarity graph between chunks (the graph-retrieval pattern, over the same journal as everything else GraphRAG ). It catches what plain vector search misses: chunks that aren't directly similar to the query but are strongly connected to chunks that are (indirect relevance) also join the results. Since it implements the VectorStore interface, it's a drop-in for createRagTool.

Parameters: threshold (edge threshold, default 0.75), hops (neighbor-expansion depth, default 1; 0 = plain vector search), decay (per-hop score decay, default 0.7), and seeds (number of direct results seeding the expansion, default 4). If a node is reached by more than one path, the highest score is kept; ties break deterministically by id.

GraphRag — the graph as a query layer
import { GraphRag, createRagTool } from '@gnldev/rag';

const graph = new GraphRag({ threshold: 0.75, hops: 1, decay: 0.7, seeds: 4 });
await graph.upsert(items); // edges are built incrementally

const ragTool = createRagTool({ store: graph, embed, topK: 6 });

createRagTool — deterministic / exactly-once#

createRagTool packages embed + store.query + optional rerankinto a single tool. It works with any VectorStore (InMemory / Postgres / Qdrant / Chroma / Weaviate / GraphRag). Because runDurable journals the durableTool result, resume returns the same documents; no new embed/query/rerank call runs.

Guarantee
RAG being deterministic and exactly-once is unique to GNL: the same-context guarantee across replays makes debugging and evals reproducible — frameworks that embed live can't offer this consistency.

API reference#

fnchunkText

(text, opts?) → Chunk[]; recursive/markdown/character strategy, size/overlap, markdown heading breadcrumb.

fnchunkDocuments

(docs, opts?) → VectorDoc[]; deterministic <docId>#<i> chunk id → re-indexing upserts.

classPostgresVectorStore

Durable pgvector store; injectable pool, lazy pg, HNSW/ivfflat — same interface as InMemory.

classQdrantVectorStore / ChromaVectorStore / WeaviateVectorStore / ElasticsearchVectorStore / OpenSearchVectorStore

Vector stores on a Qdrant, Chroma, Weaviate, Elasticsearch or OpenSearch server over REST; same interface as InMemory, same vector kit; no extra package.

classGraphRag

Similarity-graph VectorStore; indirect-relevance retrieval via threshold/hops/decay/seeds, drop-in for createRagTool.

fncreateRagTool

({ store, embed, topK?, rerank?, ... }) → tool; journaled inside durable = exactly-once/replayable RAG.

fnInMemoryVectorStore / indexDocuments

In-memory store + bulk index helper (test/prototype).