@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.
- package/README.md +327 -98
- package/dist/client/defaultChunker.d.ts.map +1 -1
- package/dist/client/defaultChunker.js +47 -16
- package/dist/client/defaultChunker.js.map +1 -1
- package/dist/client/fileUtils.d.ts +4 -2
- package/dist/client/fileUtils.d.ts.map +1 -1
- package/dist/client/fileUtils.js +5 -3
- package/dist/client/fileUtils.js.map +1 -1
- package/dist/client/hybridRank.d.ts +23 -0
- package/dist/client/hybridRank.d.ts.map +1 -0
- package/dist/client/hybridRank.js +21 -0
- package/dist/client/hybridRank.js.map +1 -0
- package/dist/client/index.d.ts +18 -35
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +12 -27
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +1 -0
- package/dist/component/chunks.d.ts +1 -0
- package/dist/component/chunks.d.ts.map +1 -1
- package/dist/component/chunks.js +31 -2
- package/dist/component/chunks.js.map +1 -1
- package/dist/component/entries.d.ts +2 -2
- package/dist/component/entries.d.ts.map +1 -1
- package/dist/component/entries.js +1 -1
- package/dist/component/entries.js.map +1 -1
- package/dist/shared.d.ts +2 -2
- package/dist/shared.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/defaultChunker.test.ts +1 -1
- package/src/client/defaultChunker.ts +73 -17
- package/src/client/fileUtils.ts +8 -4
- package/src/client/hybridRank.ts +39 -0
- package/src/client/index.test.ts +11 -7
- package/src/client/index.ts +25 -58
- package/src/component/_generated/api.d.ts +1 -0
- package/src/component/chunks.test.ts +11 -1
- package/src/component/chunks.ts +33 -3
- package/src/component/entries.ts +3 -3
- package/src/shared.ts +2 -2
package/src/client/index.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
type Chunk,
|
|
28
28
|
type CreateChunkArgs,
|
|
29
29
|
type Entry,
|
|
30
|
-
type
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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?:
|
|
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?:
|
|
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.
|
|
@@ -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
|
-
|
|
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
|
|
package/src/component/chunks.ts
CHANGED
|
@@ -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(
|
|
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
|
}
|
package/src/component/entries.ts
CHANGED
|
@@ -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,
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
*/
|