@convex-dev/rag 0.3.1 → 0.3.3-alpha.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.
Files changed (39) hide show
  1. package/README.md +327 -98
  2. package/dist/client/defaultChunker.d.ts.map +1 -1
  3. package/dist/client/defaultChunker.js +47 -16
  4. package/dist/client/defaultChunker.js.map +1 -1
  5. package/dist/client/fileUtils.d.ts +4 -2
  6. package/dist/client/fileUtils.d.ts.map +1 -1
  7. package/dist/client/fileUtils.js +5 -3
  8. package/dist/client/fileUtils.js.map +1 -1
  9. package/dist/client/hybridRank.d.ts +23 -0
  10. package/dist/client/hybridRank.d.ts.map +1 -0
  11. package/dist/client/hybridRank.js +21 -0
  12. package/dist/client/hybridRank.js.map +1 -0
  13. package/dist/client/index.d.ts +18 -35
  14. package/dist/client/index.d.ts.map +1 -1
  15. package/dist/client/index.js +12 -27
  16. package/dist/client/index.js.map +1 -1
  17. package/dist/component/_generated/api.d.ts +1 -0
  18. package/dist/component/chunks.d.ts +1 -0
  19. package/dist/component/chunks.d.ts.map +1 -1
  20. package/dist/component/chunks.js +31 -2
  21. package/dist/component/chunks.js.map +1 -1
  22. package/dist/component/entries.d.ts +2 -2
  23. package/dist/component/entries.d.ts.map +1 -1
  24. package/dist/component/entries.js +1 -1
  25. package/dist/component/entries.js.map +1 -1
  26. package/dist/shared.d.ts +2 -2
  27. package/dist/shared.d.ts.map +1 -1
  28. package/package.json +1 -1
  29. package/src/client/defaultChunker.test.ts +1 -1
  30. package/src/client/defaultChunker.ts +73 -17
  31. package/src/client/fileUtils.ts +8 -4
  32. package/src/client/hybridRank.ts +39 -0
  33. package/src/client/index.test.ts +11 -7
  34. package/src/client/index.ts +25 -58
  35. package/src/component/_generated/api.d.ts +1 -0
  36. package/src/component/chunks.test.ts +11 -1
  37. package/src/component/chunks.ts +33 -3
  38. package/src/component/entries.ts +3 -3
  39. package/src/shared.ts +2 -2
@@ -27,7 +27,7 @@ import {
27
27
  type Chunk,
28
28
  type CreateChunkArgs,
29
29
  type Entry,
30
- type EntryFilterValues,
30
+ type EntryFilter,
31
31
  type EntryId,
32
32
  type Namespace,
33
33
  type NamespaceId,
@@ -49,6 +49,7 @@ import {
49
49
  import type { NamedFilter } from "../component/filters.js";
50
50
  import { defaultChunker } from "./defaultChunker.js";
51
51
 
52
+ export { hybridRank } from "./hybridRank.js";
52
53
  export { defaultChunker, vEntryId, vNamespaceId };
53
54
  export type {
54
55
  ChunkerAction,
@@ -66,6 +67,7 @@ export type {
66
67
  export {
67
68
  type VEntry,
68
69
  type VSearchEntry,
70
+ type EntryFilter,
69
71
  vEntry,
70
72
  vSearchEntry,
71
73
  vSearchResult,
@@ -362,7 +364,7 @@ export class RAG<
362
364
  /**
363
365
  * The query to search for. Optional if embedding is provided.
364
366
  */
365
- query?: string;
367
+ query: string | Array<number>;
366
368
  } & SearchOptions<FitlerSchemas>
367
369
  ): Promise<{
368
370
  results: SearchResult[];
@@ -376,7 +378,7 @@ export class RAG<
376
378
  chunkContext = { before: 0, after: 0 },
377
379
  vectorScoreThreshold,
378
380
  } = args;
379
- let embedding = args.embedding;
381
+ let embedding = Array.isArray(args.query) ? args.query : undefined;
380
382
  if (!embedding) {
381
383
  const embedResult = await embed({
382
384
  model: this.options.textEmbeddingModel,
@@ -405,7 +407,7 @@ export class RAG<
405
407
  for (const range of ranges) {
406
408
  if (previousEnd !== 0) {
407
409
  if (range.startOrder !== previousEnd) {
408
- text += "\n...\n";
410
+ text += "\n\n...\n\n";
409
411
  } else {
410
412
  text += "\n";
411
413
  }
@@ -419,8 +421,8 @@ export class RAG<
419
421
  return {
420
422
  results: results as SearchResult[],
421
423
  text: entriesWithTexts
422
- .map((e) => (e.title ? `## ${e.title}:\n${e.text}` : e.text))
423
- .join(`\n---\n`),
424
+ .map((e) => (e.title ? `## ${e.title}:\n\n${e.text}` : e.text))
425
+ .join(`\n\n---\n\n`),
424
426
  entries: entriesWithTexts,
425
427
  };
426
428
  }
@@ -446,6 +448,11 @@ export class RAG<
446
448
  * The namespace to search in. e.g. a userId if entries are per-user.
447
449
  */
448
450
  namespace: string;
451
+ /**
452
+ * The text or embedding to search for. If provided, it will be used
453
+ * instead of the prompt for vector search.
454
+ */
455
+ query?: string | Array<number>;
449
456
  };
450
457
  /**
451
458
  * Required. The prompt to use for context search, as well as the final
@@ -552,14 +559,17 @@ export class RAG<
552
559
  ctx: RunQueryCtx,
553
560
  args: {
554
561
  namespaceId?: NamespaceId;
555
- paginationOpts: PaginationOptions;
556
562
  order?: "desc" | "asc";
557
563
  status?: Status;
558
- }
564
+ } & ({ paginationOpts: PaginationOptions } | { limit: number })
559
565
  ): Promise<PaginationResult<Entry<FitlerSchemas, EntryMetadata>>> {
566
+ const paginationOpts =
567
+ "paginationOpts" in args
568
+ ? args.paginationOpts
569
+ : { cursor: null, numItems: args.limit };
560
570
  const results = await ctx.runQuery(this.component.entries.list, {
561
571
  namespaceId: args.namespaceId,
562
- paginationOpts: args.paginationOpts,
572
+ paginationOpts,
563
573
  order: args.order ?? "asc",
564
574
  status: args.status ?? "ready",
565
575
  });
@@ -586,7 +596,7 @@ export class RAG<
586
596
  * new results into a new entry when migrating, or avoiding duplicating work
587
597
  * when updating content.
588
598
  */
589
- async findExistingEntryByContentHash(
599
+ async findEntryByContentHash(
590
600
  ctx: RunQueryCtx,
591
601
  args: {
592
602
  namespace: string;
@@ -679,11 +689,13 @@ export class RAG<
679
689
  args: {
680
690
  paginationOpts: PaginationOptions;
681
691
  entryId: EntryId;
692
+ order?: "desc" | "asc";
682
693
  }
683
694
  ): Promise<PaginationResult<Chunk>> {
684
695
  return ctx.runQuery(this.component.chunks.list, {
685
696
  entryId: args.entryId,
686
697
  paginationOpts: args.paginationOpts,
698
+ order: args.order ?? "asc",
687
699
  });
688
700
  }
689
701
 
@@ -927,7 +939,7 @@ async function createChunkArgsBatch(
927
939
  for (const batch of makeBatches(missingEmbeddingsWithIndex, 100)) {
928
940
  const { embeddings } = await embedMany({
929
941
  model: embedModel,
930
- values: batch.map((b) => b.text),
942
+ values: batch.map((b) => b.text.trim() || "<empty>"),
931
943
  });
932
944
  for (const [index, embedding] of embeddings.entries()) {
933
945
  argsMaybeMissingEmbeddings[batch[index].index].embedding = embedding;
@@ -941,46 +953,6 @@ async function createChunkArgsBatch(
941
953
  }) as CreateChunkArgs[];
942
954
  }
943
955
 
944
- /**
945
- * Rank results from multiple results, e.g. from vector search and text search.
946
- * Uses the "Recriprocal Rank Fusion" algorithm.
947
- * @param sortedResults The results arrays ordered by most important first.
948
- */
949
- export function hybridRank<T extends string>(
950
- sortedResults: T[][],
951
- opts?: {
952
- /**
953
- * A constant used to change the bias of the top results in each list vs.
954
- * results in the middle of multiple lists.
955
- * A higher k means less of a bias toward the top few results.
956
- */
957
- k: number;
958
- /**
959
- * The weights of each sortedResults array.
960
- * Used to prefer results from one sortedResults array over another.
961
- */
962
- weights: number[];
963
- /**
964
- * The cutoff score for a result to be returned.
965
- */
966
- cutoffScore?: number;
967
- }
968
- ): T[] {
969
- const k = opts?.k ?? 10;
970
- const scores: Map<T, number> = new Map();
971
- for (const [i, results] of sortedResults.entries()) {
972
- const weight = opts?.weights?.[i] ?? 1;
973
- for (let j = 0; j < results.length; j++) {
974
- const key = results[j];
975
- scores.set(key, (scores.get(key) ?? 0) + weight / (k + j));
976
- }
977
- }
978
- const sortedScores = Array.from(scores.entries()).sort((a, b) => b[1] - a[1]);
979
- return sortedScores
980
- .filter(([_, score]) => score >= (opts?.cutoffScore ?? 0))
981
- .map(([key]) => key);
982
- }
983
-
984
956
  type MastraChunk = {
985
957
  text: string;
986
958
  metadata: Record<string, Value>;
@@ -1060,7 +1032,7 @@ type EntryArgs<
1060
1032
  * and searching with the same value will return entries that match that
1061
1033
  * value exactly.
1062
1034
  */
1063
- filterValues?: EntryFilterValues<FitlerSchemas>[];
1035
+ filterValues?: EntryFilter<FitlerSchemas>[];
1064
1036
  /**
1065
1037
  * The importance of the entry. This is used to scale the vector search
1066
1038
  * score of each chunk.
@@ -1080,11 +1052,6 @@ type EntryArgs<
1080
1052
  };
1081
1053
 
1082
1054
  type SearchOptions<FitlerSchemas extends Record<string, Value>> = {
1083
- /**
1084
- * The embedding to search for. If provided, it will be used instead
1085
- * of the query for vector search.
1086
- */
1087
- embedding?: Array<number>;
1088
1055
  /**
1089
1056
  * Filters to apply to the search. These are OR'd together. To represent
1090
1057
  * AND logic, your filter can be an object or array with multiple values.
@@ -1097,7 +1064,7 @@ type SearchOptions<FitlerSchemas extends Record<string, Value>> = {
1097
1064
  * `{ team_user: { team: "team1", user: "user1" } }`, it will not match
1098
1065
  * `{ team_user: { team: "team1" } }` but it will match
1099
1066
  */
1100
- filters?: EntryFilterValues<FitlerSchemas>[];
1067
+ filters?: EntryFilter<FitlerSchemas>[];
1101
1068
  /**
1102
1069
  * The maximum number of messages to fetch. Default is 10.
1103
1070
  * This is the number *before* the chunkContext is applied.
@@ -62,6 +62,7 @@ export type Mounts = {
62
62
  "public",
63
63
  {
64
64
  entryId: string;
65
+ order: "desc" | "asc";
65
66
  paginationOpts: {
66
67
  cursor: string | null;
67
68
  endCursor?: string | null;
@@ -332,11 +332,12 @@ describe("chunks", () => {
332
332
  // Insert a large number of chunks
333
333
  const chunks = createTestChunks(10);
334
334
  await t.run(async (ctx) => {
335
- return insertChunks(ctx, {
335
+ const result = await insertChunks(ctx, {
336
336
  entryId,
337
337
  startOrder: 0,
338
338
  chunks,
339
339
  });
340
+ expect(result.status).toBe("ready");
340
341
  });
341
342
 
342
343
  // Verify chunks exist
@@ -375,8 +376,15 @@ describe("chunks", () => {
375
376
  const allContent = await t.run(async (ctx) => {
376
377
  return ctx.db.query("content").collect();
377
378
  });
379
+
378
380
  // Should have only 3 content records remaining (for the 3 remaining chunks)
379
381
  expect(allContent).toHaveLength(3);
382
+
383
+ // Verify embeddings were deleted
384
+ const allEmbeddings = await t.run(async (ctx) => {
385
+ return ctx.db.query("vectors_128").collect();
386
+ });
387
+ expect(allEmbeddings).toHaveLength(3);
380
388
  });
381
389
 
382
390
  test("listing chunks returns correct pagination", async () => {
@@ -397,6 +405,7 @@ describe("chunks", () => {
397
405
  // Test listing with pagination
398
406
  const result = await t.query(api.chunks.list, {
399
407
  entryId,
408
+ order: "asc",
400
409
  paginationOpts: { numItems: 3, cursor: null },
401
410
  });
402
411
 
@@ -417,6 +426,7 @@ describe("chunks", () => {
417
426
  // Get next page
418
427
  const nextResult = await t.query(api.chunks.list, {
419
428
  entryId,
429
+ order: "asc",
420
430
  paginationOpts: { numItems: 3, cursor: result.continueCursor },
421
431
  });
422
432
 
@@ -21,7 +21,7 @@ import {
21
21
  type QueryCtx,
22
22
  } from "./_generated/server.js";
23
23
  import { insertEmbedding } from "./embeddings/index.js";
24
- import { vVectorId } from "./embeddings/tables.js";
24
+ import { vVectorId, type VectorTableName } from "./embeddings/tables.js";
25
25
  import { schema, v } from "./schema.js";
26
26
  import { getPreviousEntry, publicEntry } from "./entries.js";
27
27
  import {
@@ -463,6 +463,7 @@ export const list = query({
463
463
  args: v.object({
464
464
  entryId: v.id("entries"),
465
465
  paginationOpts: paginationOptsValidator,
466
+ order: v.union(v.literal("desc"), v.literal("asc")),
466
467
  }),
467
468
  returns: vPaginationResult(vChunk),
468
469
  handler: async (ctx, args) => {
@@ -470,7 +471,7 @@ export const list = query({
470
471
  const chunks = await paginator(ctx.db, schema)
471
472
  .query("chunks")
472
473
  .withIndex("entryId_order", (q) => q.eq("entryId", entryId))
473
- .order("asc")
474
+ .order(args.order)
474
475
  .paginate(paginationOpts);
475
476
  return {
476
477
  ...chunks,
@@ -524,20 +525,49 @@ export async function deleteChunksPage(
524
525
  for await (const chunk of chunkStream) {
525
526
  dataUsedSoFar += await estimateChunkSize(chunk);
526
527
  await ctx.db.delete(chunk._id);
528
+ if (chunk.state.kind === "ready") {
529
+ const embedding = await ctx.db.get(chunk.state.embeddingId);
530
+ if (embedding) {
531
+ dataUsedSoFar += estimateEmbeddingSize(embedding);
532
+ await ctx.db.delete(chunk.state.embeddingId);
533
+ }
534
+ }
527
535
  dataUsedSoFar += await estimateContentSize(ctx, chunk.contentId);
528
536
  await ctx.db.delete(chunk.contentId);
529
537
  if (dataUsedSoFar > BANDWIDTH_PER_TRANSACTION_HARD_LIMIT) {
530
- // TODO: schedule follow-up - workpool?
531
538
  return { isDone: false, nextStartOrder: chunk.order };
532
539
  }
533
540
  }
534
541
  return { isDone: true, nextStartOrder: -1 };
535
542
  }
536
543
 
544
+ function estimateEmbeddingSize(embedding: Doc<VectorTableName>) {
545
+ let dataUsedSoFar =
546
+ embedding.vector.length * 8 +
547
+ embedding.namespaceId.length +
548
+ embedding._id.length +
549
+ 8;
550
+ for (const filter of [
551
+ embedding.filter0,
552
+ embedding.filter1,
553
+ embedding.filter2,
554
+ embedding.filter3,
555
+ ]) {
556
+ if (filter) {
557
+ dataUsedSoFar += JSON.stringify(convexToJson(filter[1])).length;
558
+ }
559
+ }
560
+ return dataUsedSoFar;
561
+ }
562
+
537
563
  async function estimateChunkSize(chunk: Doc<"chunks">) {
538
564
  let dataUsedSoFar = 100; // constant metadata - roughly
539
565
  if (chunk.state.kind === "pending") {
540
566
  dataUsedSoFar += chunk.state.embedding.length * 8;
567
+ dataUsedSoFar += chunk.state.pendingSearchableText?.length ?? 0;
568
+ } else if (chunk.state.kind === "replaced") {
569
+ dataUsedSoFar += chunk.state.vector.length * 8;
570
+ dataUsedSoFar += chunk.state.pendingSearchableText?.length ?? 0;
541
571
  }
542
572
  return dataUsedSoFar;
543
573
  }
@@ -1,7 +1,7 @@
1
1
  import { assert, omit } from "convex-helpers";
2
2
  import { createFunctionHandle, paginationOptsValidator } from "convex/server";
3
3
  import { v, type Value } from "convex/values";
4
- import type { ChunkerAction, EntryFilterValues, EntryId } from "../shared.js";
4
+ import type { ChunkerAction, EntryFilter, EntryId } from "../shared.js";
5
5
  import {
6
6
  statuses,
7
7
  vActiveStatus,
@@ -115,7 +115,7 @@ function workpoolName(
115
115
  key: string | undefined,
116
116
  entryId: Id<"entries">
117
117
  ) {
118
- return `async-chunker-${namespace}-${key ? key + "-" + entryId : entryId}`;
118
+ return `rag-async-${namespace}-${key ? key + "-" + entryId : entryId}`;
119
119
  }
120
120
 
121
121
  export const addAsyncOnComplete = internalMutation({
@@ -497,7 +497,7 @@ export function publicEntry(entry: {
497
497
  _id: Id<"entries">;
498
498
  key?: string | undefined;
499
499
  importance: number;
500
- filterValues: EntryFilterValues[];
500
+ filterValues: EntryFilter[];
501
501
  contentHash?: string | undefined;
502
502
  title?: string | undefined;
503
503
  metadata?: Record<string, Value> | undefined;
package/src/shared.ts CHANGED
@@ -99,7 +99,7 @@ export type SearchEntry<
99
99
  text: string;
100
100
  };
101
101
 
102
- export type EntryFilterValues<
102
+ export type EntryFilter<
103
103
  Filters extends Record<string, Value> = Record<string, Value>,
104
104
  > = {
105
105
  [K in keyof Filters & string]: NamedFilter<K, Filters[K]>;
@@ -126,7 +126,7 @@ export type Entry<
126
126
  /** Filters that can be used to search for this entry.
127
127
  * Up to 4 filters are supported, of any type.
128
128
  */
129
- filterValues: EntryFilterValues<Filters>[];
129
+ filterValues: EntryFilter<Filters>[];
130
130
  /** Hash of the entry contents.
131
131
  * If supplied, it will avoid adding if the hash is the same.
132
132
  */