@absolutejs/rag 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,59 @@
1
1
  # @absolutejs/rag
2
2
 
3
- Standalone RAG runtime extracted from AbsoluteJS.
3
+ A standalone RAG runtime for Bun and Elysia applications covering document ingestion, chunking, embeddings, hybrid retrieval, reranking, source synchronization, evaluation, client primitives, and framework bindings.
4
4
 
5
- This package currently contains retrieval, ingestion, sync, connector, and evaluation code.
6
- A later cleanup pass should extract the remaining shared AI substrate into @absolutejs/ai.
5
+ ## Installation
6
+
7
+ ```sh
8
+ bun add @absolutejs/rag
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```ts
14
+ import {
15
+ createInMemoryRAGStore,
16
+ createRAGCollection,
17
+ ingestRAGDocuments,
18
+ openaiEmbeddings,
19
+ searchDocuments
20
+ } from '@absolutejs/rag';
21
+
22
+ const collection = createRAGCollection({
23
+ embedding: openaiEmbeddings({
24
+ apiKey: process.env.OPENAI_API_KEY ?? '',
25
+ defaultModel: 'text-embedding-3-small'
26
+ }),
27
+ store: createInMemoryRAGStore()
28
+ });
29
+
30
+ await ingestRAGDocuments(collection, {
31
+ documents: [{ id: 'intro', text: 'AbsoluteJS ships typed Bun primitives.' }]
32
+ });
33
+
34
+ const results = await searchDocuments(collection, {
35
+ query: 'What does AbsoluteJS ship?',
36
+ topK: 3
37
+ });
38
+ ```
39
+
40
+ ## Retrieval and storage
41
+
42
+ The built-in memory store supports development and tests. Published adapters provide PostgreSQL with pgvector, SQLite with optional vec0 acceleration, and Pinecone behind the same `RAGVectorStore` contract. Lexical and vector results can be fused, transformed, and reranked with provider or heuristic rerankers.
43
+
44
+ ## Ingestion and source sync
45
+
46
+ The ingestion pipeline handles files, directories, uploads, URLs, PDFs, office documents, archives, images, and media transcripts. Scheduled connectors can keep collections synchronized from email, GitHub, sitemaps, feeds, directories, and S3-compatible storage.
47
+
48
+ ## Quality and evaluation
49
+
50
+ `@absolutejs/rag/quality` evaluates retrieval relevance and answer grounding, compares strategies and rerankers, and records runs against a baseline so retrieval changes can be tested before release.
51
+
52
+ ## Client and framework entry points
53
+
54
+ - `@absolutejs/rag/client` and `/client/ui` provide browser-side primitives.
55
+ - `@absolutejs/rag/react`, `/vue`, `/svelte`, and `/angular` provide framework bindings.
56
+ - `@absolutejs/rag/adapter-kit` exposes the contracts used by vector-store adapters.
57
+ - `@absolutejs/rag/ui` exposes presentation-neutral UI contracts.
58
+
59
+ Pair the retrieval runtime with `@absolutejs/ai` when retrieved context should feed a model or streaming assistant.
@@ -13403,6 +13403,7 @@ var resolveRAGEmbeddingProvider = (providerLike, fallbackEmbed, defaultModel) =>
13403
13403
  }
13404
13404
  const resolvedDefaultModel = provider.defaultModel ?? defaultModel;
13405
13405
  return {
13406
+ cacheNamespace: provider.cacheNamespace,
13406
13407
  defaultModel: resolvedDefaultModel,
13407
13408
  dimensions: provider.dimensions,
13408
13409
  embed: (input) => provider.embed({
@@ -20893,6 +20894,30 @@ var createRAGCollection = (options) => {
20893
20894
  validateRAGEmbeddingDimensions(vector, getExpectedDimensions(), context);
20894
20895
  return vector;
20895
20896
  };
20897
+ const throwIfAborted = (signal) => {
20898
+ if (signal?.aborted) {
20899
+ throw signal.reason ?? new DOMException("The operation was aborted", "AbortError");
20900
+ }
20901
+ };
20902
+ const mapConcurrentSettled = async (values, concurrency, mapper) => {
20903
+ const results = new Array(values.length);
20904
+ let nextIndex = 0;
20905
+ const workers = Array.from({ length: Math.min(values.length, Math.max(1, Math.floor(concurrency))) }, async () => {
20906
+ while (nextIndex < values.length) {
20907
+ const index = nextIndex++;
20908
+ try {
20909
+ results[index] = {
20910
+ status: "fulfilled",
20911
+ value: await mapper(values[index], index)
20912
+ };
20913
+ } catch (reason) {
20914
+ results[index] = { reason, status: "rejected" };
20915
+ }
20916
+ }
20917
+ });
20918
+ await Promise.all(workers);
20919
+ return results;
20920
+ };
20896
20921
  const searchWithTrace = async (input) => {
20897
20922
  const model = input.model ?? options.defaultModel;
20898
20923
  const topK = input.topK ?? defaultTopK;
@@ -20953,17 +20978,20 @@ var createRAGCollection = (options) => {
20953
20978
  stage: "input"
20954
20979
  }
20955
20980
  ];
20956
- const queryVector = runVector ? await embed({
20957
- model,
20958
- signal: input.signal,
20959
- text: input.query
20960
- }, "query") : [];
20981
+ const queryVectors = new Map;
20982
+ if (runVector) {
20983
+ for (const query of searchQueries) {
20984
+ queryVectors.set(query, embed({ model, signal: input.signal, text: query }, "query"));
20985
+ }
20986
+ }
20987
+ const primarySearchQuery = searchQueries[0] ?? input.query;
20988
+ const queryVector = runVector ? await queryVectors.get(primarySearchQuery) : [];
20961
20989
  if (runVector) {
20962
20990
  steps.push({
20963
20991
  label: "Embedded primary query",
20964
20992
  metadata: {
20965
20993
  dimensions: queryVector.length,
20966
- query: input.query
20994
+ query: primarySearchQuery
20967
20995
  },
20968
20996
  stage: "embed"
20969
20997
  });
@@ -21022,11 +21050,7 @@ var createRAGCollection = (options) => {
21022
21050
  }
21023
21051
  const resultGroups = await Promise.all(searchQueries.map(async (query, queryIndex) => {
21024
21052
  const [vectorResults2, lexicalResults2] = await Promise.all([
21025
- runVector ? embed({
21026
- model,
21027
- signal: input.signal,
21028
- text: query
21029
- }, "query").then((nextQueryVector) => options.store.query({
21053
+ runVector ? queryVectors.get(query).then((nextQueryVector) => options.store.query({
21030
21054
  filter: input.filter,
21031
21055
  candidateLimit: input.nativeCandidateLimit ?? nativeQueryProfile?.candidateLimit,
21032
21056
  fillPolicy: input.nativeFillPolicy ?? nativeQueryProfile?.fillPolicy,
@@ -21322,32 +21346,46 @@ var createRAGCollection = (options) => {
21322
21346
  return result.results;
21323
21347
  };
21324
21348
  const ingest = async (input) => {
21325
- const chunks = (await Promise.all(input.chunks.map(async (chunk) => {
21326
- const normalizedEmbedding = chunk.embedding ? (validateRAGEmbeddingDimensions(chunk.embedding, getExpectedDimensions(), "chunk"), chunk.embedding) : await embed({
21327
- model: options.defaultModel,
21328
- text: chunk.text
21329
- }, "chunk");
21330
- const normalizedVariants = chunk.embeddingVariants ? await Promise.all(chunk.embeddingVariants.map(async (variant) => {
21331
- const embedding = variant.embedding ? (validateRAGEmbeddingDimensions(variant.embedding, getExpectedDimensions(), "chunk"), variant.embedding) : await embed({
21349
+ const batchSize = Math.max(1, Math.floor(input.upsertBatchSize ?? 32));
21350
+ const concurrency = Math.max(1, Math.floor(input.embeddingConcurrency ?? 4));
21351
+ for (let start = 0;start < input.chunks.length; start += batchSize) {
21352
+ throwIfAborted(input.signal);
21353
+ const batch = input.chunks.slice(start, start + batchSize);
21354
+ const settled = await mapConcurrentSettled(batch, concurrency, async (chunk) => {
21355
+ throwIfAborted(input.signal);
21356
+ const normalizedEmbedding = chunk.embedding ? (validateRAGEmbeddingDimensions(chunk.embedding, getExpectedDimensions(), "chunk"), chunk.embedding) : await embed({
21332
21357
  model: options.defaultModel,
21333
- text: variant.text ?? chunk.text
21358
+ signal: input.signal,
21359
+ text: chunk.text
21334
21360
  }, "chunk");
21335
- return {
21336
- ...variant,
21337
- embedding
21338
- };
21339
- })) : undefined;
21340
- return expandChunkForMultivectorStorage({
21341
- ...chunk,
21342
- embedding: normalizedEmbedding,
21343
- embeddingVariants: normalizedVariants,
21344
- metadata: {
21345
- ...chunk.metadata ?? {},
21346
- [MULTIVECTOR_PRIMARY]: true
21347
- }
21361
+ const normalizedVariants = chunk.embeddingVariants ? await Promise.all(chunk.embeddingVariants.map(async (variant) => {
21362
+ const embedding = variant.embedding ? (validateRAGEmbeddingDimensions(variant.embedding, getExpectedDimensions(), "chunk"), variant.embedding) : await embed({
21363
+ model: options.defaultModel,
21364
+ signal: input.signal,
21365
+ text: variant.text ?? chunk.text
21366
+ }, "chunk");
21367
+ return {
21368
+ ...variant,
21369
+ embedding
21370
+ };
21371
+ })) : undefined;
21372
+ return expandChunkForMultivectorStorage({
21373
+ ...chunk,
21374
+ embedding: normalizedEmbedding,
21375
+ embeddingVariants: normalizedVariants,
21376
+ metadata: {
21377
+ ...chunk.metadata ?? {},
21378
+ [MULTIVECTOR_PRIMARY]: true
21379
+ }
21380
+ });
21348
21381
  });
21349
- }))).flat();
21350
- await options.store.upsert({ chunks });
21382
+ const chunks = settled.filter((result) => result.status === "fulfilled").flatMap((result) => result.value);
21383
+ if (chunks.length > 0)
21384
+ await options.store.upsert({ chunks });
21385
+ const failed = settled.find((result) => result.status === "rejected");
21386
+ if (failed)
21387
+ throw failed.reason;
21388
+ }
21351
21389
  };
21352
21390
  const buildSourceUpsertInput = async (sourceId, input) => {
21353
21391
  const sharedMetadata = input.metadata;
@@ -21425,33 +21463,34 @@ var createRAGCollection = (options) => {
21425
21463
  if (!sourceId) {
21426
21464
  throw new Error("ingestSource requires a non-empty sourceId.");
21427
21465
  }
21428
- if (input.replace !== false) {
21429
- await removeSource({
21430
- chunkCount: input.previousChunkCount,
21431
- sourceId
21432
- });
21433
- }
21434
21466
  const built = await buildSourceUpsertInput(sourceId, input);
21435
21467
  const embedKind = input.embedKind ?? "passage";
21436
- const chunks = await Promise.all(built.chunks.map(async (chunk, index) => {
21468
+ const chunks = built.chunks.map((chunk, index) => {
21437
21469
  const chunkId = `${sourceId}#${index}`;
21438
- const embedding = chunk.embedding ?? await embed({
21439
- kind: embedKind,
21440
- model: options.defaultModel,
21441
- text: chunk.text
21442
- }, "chunk");
21443
21470
  return {
21444
21471
  ...chunk,
21445
21472
  chunkId,
21446
- embedding,
21447
21473
  metadata: {
21448
21474
  ...chunk.metadata ?? {},
21475
+ embeddingKind: embedKind,
21449
21476
  sourceId
21450
21477
  },
21451
21478
  source: chunk.source ?? sourceId
21452
21479
  };
21453
- }));
21454
- await ingest({ chunks });
21480
+ });
21481
+ await ingest({
21482
+ chunks,
21483
+ embeddingConcurrency: input.embeddingConcurrency,
21484
+ signal: input.signal,
21485
+ upsertBatchSize: input.upsertBatchSize
21486
+ });
21487
+ if (input.replace !== false && typeof input.previousChunkCount === "number" && input.previousChunkCount > chunks.length) {
21488
+ await removeSource({
21489
+ chunkIds: Array.from({ length: input.previousChunkCount - chunks.length }, (_unused, index) => `${sourceId}#${chunks.length + index}`),
21490
+ filterDelete: false,
21491
+ sourceId
21492
+ });
21493
+ }
21455
21494
  return {
21456
21495
  chunkCount: chunks.length,
21457
21496
  chunkIds: chunks.map((chunk) => chunk.chunkId),
@@ -31365,5 +31404,5 @@ export {
31365
31404
  RAG_NATIVE_QUERY_CANDIDATE_LIMIT
31366
31405
  };
31367
31406
 
31368
- //# debugId=7097CF082CBBCC6664756E2164756E21
31407
+ //# debugId=F1E8AAD3FF52845464756E2164756E21
31369
31408
  //# sourceMappingURL=index.js.map