@convex-dev/rag 0.7.0 → 0.7.2
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/dist/client/hybridRank.d.ts +1 -1
- package/dist/client/hybridRank.js +1 -1
- package/dist/client/index.d.ts +35 -3
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +32 -16
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +2 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +6 -1
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/chunks.d.ts +21 -14
- package/dist/component/chunks.d.ts.map +1 -1
- package/dist/component/chunks.js +67 -64
- package/dist/component/chunks.js.map +1 -1
- package/dist/component/entries.d.ts +18 -54
- package/dist/component/entries.d.ts.map +1 -1
- package/dist/component/entries.js +21 -64
- package/dist/component/entries.js.map +1 -1
- package/dist/component/helpers.d.ts +70 -0
- package/dist/component/helpers.d.ts.map +1 -0
- package/dist/component/helpers.js +83 -0
- package/dist/component/helpers.js.map +1 -0
- package/dist/component/namespaces.d.ts +45 -73
- package/dist/component/namespaces.d.ts.map +1 -1
- package/dist/component/namespaces.js +10 -48
- package/dist/component/namespaces.js.map +1 -1
- package/dist/component/schema.d.ts +34 -34
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +0 -1
- package/dist/component/schema.js.map +1 -1
- package/dist/component/search.d.ts +45 -2
- package/dist/component/search.d.ts.map +1 -1
- package/dist/component/search.js +188 -17
- package/dist/component/search.js.map +1 -1
- package/dist/shared.d.ts +2 -0
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +1 -0
- package/dist/shared.js.map +1 -1
- package/package.json +31 -29
- package/src/client/hybridRank.ts +1 -1
- package/src/client/index.ts +76 -16
- package/src/component/_generated/api.ts +2 -0
- package/src/component/_generated/component.ts +6 -1
- package/src/component/_generated/server.ts +0 -5
- package/src/component/chunks.ts +103 -93
- package/src/component/entries.ts +32 -84
- package/src/component/helpers.ts +124 -0
- package/src/component/namespaces.test.ts +97 -0
- package/src/component/namespaces.ts +25 -82
- package/src/component/schema.ts +0 -1
- package/src/component/search.test.ts +303 -1
- package/src/component/search.ts +266 -19
- package/src/shared.ts +7 -0
|
@@ -409,12 +409,17 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
|
|
|
409
409
|
"internal",
|
|
410
410
|
{
|
|
411
411
|
chunkContext?: { after: number; before: number };
|
|
412
|
-
|
|
412
|
+
dimension?: number;
|
|
413
|
+
embedding?: Array<number>;
|
|
413
414
|
filters: Array<{ name: string; value: any }>;
|
|
414
415
|
limit: number;
|
|
415
416
|
modelId: string;
|
|
416
417
|
namespace: string;
|
|
418
|
+
searchType?: "vector" | "text" | "hybrid";
|
|
419
|
+
textQuery?: string;
|
|
420
|
+
textWeight?: number;
|
|
417
421
|
vectorScoreThreshold?: number;
|
|
422
|
+
vectorWeight?: number;
|
|
418
423
|
},
|
|
419
424
|
{
|
|
420
425
|
entries: Array<{
|
|
@@ -107,11 +107,6 @@ export const internalAction: ActionBuilder<DataModel, "internal"> =
|
|
|
107
107
|
*/
|
|
108
108
|
export const httpAction: HttpActionBuilder = httpActionGeneric;
|
|
109
109
|
|
|
110
|
-
type GenericCtx =
|
|
111
|
-
| GenericActionCtx<DataModel>
|
|
112
|
-
| GenericMutationCtx<DataModel>
|
|
113
|
-
| GenericQueryCtx<DataModel>;
|
|
114
|
-
|
|
115
110
|
/**
|
|
116
111
|
* A set of services for use within Convex query functions.
|
|
117
112
|
*
|
package/src/component/chunks.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
import { insertEmbedding } from "./embeddings/index.js";
|
|
25
25
|
import { vVectorId, type VectorTableName } from "./embeddings/tables.js";
|
|
26
26
|
import { schema, v } from "./schema.js";
|
|
27
|
-
import { getPreviousEntry, publicEntry } from "./
|
|
27
|
+
import { getPreviousEntry, publicEntry } from "./helpers.js";
|
|
28
28
|
import {
|
|
29
29
|
filterFieldsFromNumbers,
|
|
30
30
|
numberedFilterFromNamedFilters,
|
|
@@ -311,6 +311,107 @@ export const vRangeResult = v.object({
|
|
|
311
311
|
),
|
|
312
312
|
});
|
|
313
313
|
|
|
314
|
+
export async function buildRanges(
|
|
315
|
+
ctx: QueryCtx,
|
|
316
|
+
chunks: (Doc<"chunks"> | null)[],
|
|
317
|
+
chunkContext: { before: number; after: number },
|
|
318
|
+
): Promise<{
|
|
319
|
+
ranges: (null | Infer<typeof vRangeResult>)[];
|
|
320
|
+
entries: Entry[];
|
|
321
|
+
}> {
|
|
322
|
+
// Note: This preserves order of entries as they first appeared.
|
|
323
|
+
const entryDocs = (
|
|
324
|
+
await Promise.all(
|
|
325
|
+
Array.from(
|
|
326
|
+
new Set(chunks.filter((c) => c !== null).map((c) => c.entryId)),
|
|
327
|
+
).map((id) => ctx.db.get(id)),
|
|
328
|
+
)
|
|
329
|
+
).filter((d): d is Doc<"entries"> => d !== null);
|
|
330
|
+
const entries = entryDocs.map(publicEntry);
|
|
331
|
+
const entryDocById = new Map(entryDocs.map((d) => [d._id, d]));
|
|
332
|
+
|
|
333
|
+
const entryOrders = chunks
|
|
334
|
+
.filter((c) => c !== null)
|
|
335
|
+
.map((c) => [c.entryId, c.order] as const)
|
|
336
|
+
.reduce(
|
|
337
|
+
(acc, [entryId, order]) => {
|
|
338
|
+
if (acc[entryId]?.includes(order)) {
|
|
339
|
+
// De-dupe orders
|
|
340
|
+
return acc;
|
|
341
|
+
}
|
|
342
|
+
acc[entryId] = [...(acc[entryId] ?? []), order].sort((a, b) => a - b);
|
|
343
|
+
return acc;
|
|
344
|
+
},
|
|
345
|
+
{} as Record<Id<"entries">, number[]>,
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
const result: Array<Infer<typeof vRangeResult> | null> = [];
|
|
349
|
+
|
|
350
|
+
for (const chunk of chunks) {
|
|
351
|
+
if (chunk === null) {
|
|
352
|
+
result.push(null);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
// Note: if we parallelize this in the future, we could have a race
|
|
356
|
+
// instead we'd check that other chunks are not the same doc/order
|
|
357
|
+
if (
|
|
358
|
+
result.find(
|
|
359
|
+
(r) => r?.entryId === chunk.entryId && r?.order === chunk.order,
|
|
360
|
+
)
|
|
361
|
+
) {
|
|
362
|
+
// De-dupe chunks
|
|
363
|
+
result.push(null);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const entryId = chunk.entryId;
|
|
367
|
+
const entry = entryDocById.get(entryId);
|
|
368
|
+
assert(entry, `Entry ${entryId} not found`);
|
|
369
|
+
const otherOrders = entryOrders[entryId] ?? [chunk.order];
|
|
370
|
+
const ourOrderIndex = otherOrders.indexOf(chunk.order);
|
|
371
|
+
const previousOrder = otherOrders[ourOrderIndex - 1] ?? -Infinity;
|
|
372
|
+
const nextOrder = otherOrders[ourOrderIndex + 1] ?? Infinity;
|
|
373
|
+
// We absorb all previous context up to the previous chunk.
|
|
374
|
+
const startOrder = Math.max(
|
|
375
|
+
chunk.order - chunkContext.before,
|
|
376
|
+
0,
|
|
377
|
+
Math.min(previousOrder + 1, chunk.order),
|
|
378
|
+
);
|
|
379
|
+
// We stop short if the next chunk order's "before" context will cover it.
|
|
380
|
+
const endOrder = Math.min(
|
|
381
|
+
chunk.order + chunkContext.after + 1,
|
|
382
|
+
Math.max(nextOrder - chunkContext.before, chunk.order + 1),
|
|
383
|
+
);
|
|
384
|
+
const contentIds: Id<"content">[] = [];
|
|
385
|
+
if (startOrder === chunk.order && endOrder === chunk.order + 1) {
|
|
386
|
+
contentIds.push(chunk.contentId);
|
|
387
|
+
} else {
|
|
388
|
+
const rangeChunks = await ctx.db
|
|
389
|
+
.query("chunks")
|
|
390
|
+
.withIndex("entryId_order", (q) =>
|
|
391
|
+
q
|
|
392
|
+
.eq("entryId", entryId)
|
|
393
|
+
.gte("order", startOrder)
|
|
394
|
+
.lt("order", endOrder),
|
|
395
|
+
)
|
|
396
|
+
.collect();
|
|
397
|
+
for (const c of rangeChunks) {
|
|
398
|
+
contentIds.push(c.contentId);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const content = await Promise.all(
|
|
402
|
+
contentIds.map(async (contentId) => {
|
|
403
|
+
const content = await ctx.db.get(contentId);
|
|
404
|
+
assert(content, `Content ${contentId} not found`);
|
|
405
|
+
return { text: content.text, metadata: content.metadata };
|
|
406
|
+
}),
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
result.push({ entryId, order: chunk.order, startOrder, content });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return { ranges: result, entries };
|
|
413
|
+
}
|
|
414
|
+
|
|
314
415
|
export const getRangesOfChunks = internalQuery({
|
|
315
416
|
args: {
|
|
316
417
|
embeddingIds: v.array(vVectorId),
|
|
@@ -339,98 +440,7 @@ export const getRangesOfChunks = internalQuery({
|
|
|
339
440
|
.first(),
|
|
340
441
|
),
|
|
341
442
|
);
|
|
342
|
-
|
|
343
|
-
// Note: This preserves order of entries as they first appeared.
|
|
344
|
-
const entries = (
|
|
345
|
-
await Promise.all(
|
|
346
|
-
Array.from(
|
|
347
|
-
new Set(chunks.filter((c) => c !== null).map((c) => c.entryId)),
|
|
348
|
-
).map((id) => ctx.db.get(id)),
|
|
349
|
-
)
|
|
350
|
-
)
|
|
351
|
-
.filter((d) => d !== null)
|
|
352
|
-
.map(publicEntry);
|
|
353
|
-
|
|
354
|
-
const entryOders = chunks
|
|
355
|
-
.filter((c) => c !== null)
|
|
356
|
-
.map((c) => [c.entryId, c.order] as const)
|
|
357
|
-
.reduce(
|
|
358
|
-
(acc, [entryId, order]) => {
|
|
359
|
-
if (acc[entryId]?.includes(order)) {
|
|
360
|
-
// De-dupe orders
|
|
361
|
-
return acc;
|
|
362
|
-
}
|
|
363
|
-
acc[entryId] = [...(acc[entryId] ?? []), order].sort((a, b) => a - b);
|
|
364
|
-
return acc;
|
|
365
|
-
},
|
|
366
|
-
{} as Record<Id<"entries">, number[]>,
|
|
367
|
-
);
|
|
368
|
-
|
|
369
|
-
const result: Array<Infer<typeof vRangeResult> | null> = [];
|
|
370
|
-
|
|
371
|
-
for (const chunk of chunks) {
|
|
372
|
-
if (chunk === null) {
|
|
373
|
-
result.push(null);
|
|
374
|
-
continue;
|
|
375
|
-
}
|
|
376
|
-
// Note: if we parallelize this in the future, we could have a race
|
|
377
|
-
// instead we'd check that other chunks are not the same doc/order
|
|
378
|
-
if (
|
|
379
|
-
result.find(
|
|
380
|
-
(r) => r?.entryId === chunk.entryId && r?.order === chunk.order,
|
|
381
|
-
)
|
|
382
|
-
) {
|
|
383
|
-
// De-dupe chunks
|
|
384
|
-
result.push(null);
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
const entryId = chunk.entryId;
|
|
388
|
-
const entry = await ctx.db.get(entryId);
|
|
389
|
-
assert(entry, `Entry ${entryId} not found`);
|
|
390
|
-
const otherOrders = entryOders[entryId] ?? [chunk.order];
|
|
391
|
-
const ourOrderIndex = otherOrders.indexOf(chunk.order);
|
|
392
|
-
const previousOrder = otherOrders[ourOrderIndex - 1] ?? -Infinity;
|
|
393
|
-
const nextOrder = otherOrders[ourOrderIndex + 1] ?? Infinity;
|
|
394
|
-
// We absorb all previous context up to the previous chunk.
|
|
395
|
-
const startOrder = Math.max(
|
|
396
|
-
chunk.order - chunkContext.before,
|
|
397
|
-
0,
|
|
398
|
-
Math.min(previousOrder + 1, chunk.order),
|
|
399
|
-
);
|
|
400
|
-
// We stop short if the next chunk order's "before" context will cover it.
|
|
401
|
-
const endOrder = Math.min(
|
|
402
|
-
chunk.order + chunkContext.after + 1,
|
|
403
|
-
Math.max(nextOrder - chunkContext.before, chunk.order + 1),
|
|
404
|
-
);
|
|
405
|
-
const contentIds: Id<"content">[] = [];
|
|
406
|
-
if (startOrder === chunk.order && endOrder === chunk.order + 1) {
|
|
407
|
-
contentIds.push(chunk.contentId);
|
|
408
|
-
} else {
|
|
409
|
-
const chunks = await ctx.db
|
|
410
|
-
.query("chunks")
|
|
411
|
-
.withIndex("entryId_order", (q) =>
|
|
412
|
-
q
|
|
413
|
-
.eq("entryId", entryId)
|
|
414
|
-
.gte("order", startOrder)
|
|
415
|
-
.lt("order", endOrder),
|
|
416
|
-
)
|
|
417
|
-
.collect();
|
|
418
|
-
for (const chunk of chunks) {
|
|
419
|
-
contentIds.push(chunk.contentId);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
const content = await Promise.all(
|
|
423
|
-
contentIds.map(async (contentId) => {
|
|
424
|
-
const content = await ctx.db.get(contentId);
|
|
425
|
-
assert(content, `Content ${contentId} not found`);
|
|
426
|
-
return { text: content.text, metadata: content.metadata };
|
|
427
|
-
}),
|
|
428
|
-
);
|
|
429
|
-
|
|
430
|
-
result.push({ entryId, order: chunk.order, startOrder, content });
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
return { ranges: result, entries };
|
|
443
|
+
return buildRanges(ctx, chunks, chunkContext);
|
|
434
444
|
},
|
|
435
445
|
});
|
|
436
446
|
|
package/src/component/entries.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
vResultValidator,
|
|
3
|
+
vWorkIdValidator,
|
|
4
|
+
Workpool,
|
|
5
|
+
} from "@convex-dev/workpool";
|
|
1
6
|
import { assert, omit } from "convex-helpers";
|
|
7
|
+
import { mergedStream, stream } from "convex-helpers/server/stream";
|
|
8
|
+
import { doc } from "convex-helpers/validators";
|
|
2
9
|
import {
|
|
3
10
|
createFunctionHandle,
|
|
4
11
|
paginationOptsValidator,
|
|
5
12
|
PaginationResult,
|
|
6
13
|
} from "convex/server";
|
|
7
|
-
import { v
|
|
8
|
-
import type { ChunkerAction,
|
|
14
|
+
import { v } from "convex/values";
|
|
15
|
+
import type { ChunkerAction, OnComplete } from "../shared.js";
|
|
9
16
|
import {
|
|
10
17
|
statuses,
|
|
11
18
|
vActiveStatus,
|
|
@@ -15,7 +22,7 @@ import {
|
|
|
15
22
|
vStatus,
|
|
16
23
|
type Entry,
|
|
17
24
|
} from "../shared.js";
|
|
18
|
-
import { api, internal } from "./_generated/api.js";
|
|
25
|
+
import { api, components, internal } from "./_generated/api.js";
|
|
19
26
|
import type { Doc, Id } from "./_generated/dataModel.js";
|
|
20
27
|
import {
|
|
21
28
|
action,
|
|
@@ -23,26 +30,19 @@ import {
|
|
|
23
30
|
internalQuery,
|
|
24
31
|
mutation,
|
|
25
32
|
query,
|
|
33
|
+
type ActionCtx,
|
|
26
34
|
type MutationCtx,
|
|
27
35
|
type QueryCtx,
|
|
28
36
|
} from "./_generated/server.js";
|
|
29
37
|
import { deleteChunksPageHandler, insertChunks } from "./chunks.js";
|
|
30
|
-
import schema, { type StatusWithOnComplete } from "./schema.js";
|
|
31
|
-
import { mergedStream } from "convex-helpers/server/stream";
|
|
32
|
-
import { stream } from "convex-helpers/server/stream";
|
|
33
38
|
import {
|
|
34
39
|
getCompatibleNamespaceHandler,
|
|
40
|
+
getPreviousEntry,
|
|
41
|
+
publicEntry,
|
|
35
42
|
publicNamespace,
|
|
36
43
|
vNamespaceLookupArgs,
|
|
37
|
-
} from "./
|
|
38
|
-
import
|
|
39
|
-
import {
|
|
40
|
-
vResultValidator,
|
|
41
|
-
vWorkIdValidator,
|
|
42
|
-
Workpool,
|
|
43
|
-
} from "@convex-dev/workpool";
|
|
44
|
-
import { components } from "./_generated/api.js";
|
|
45
|
-
import { doc } from "convex-helpers/validators";
|
|
44
|
+
} from "./helpers.js";
|
|
45
|
+
import schema, { type StatusWithOnComplete } from "./schema.js";
|
|
46
46
|
|
|
47
47
|
const workpool = new Workpool(components.workpool, {
|
|
48
48
|
retryActionsByDefault: true,
|
|
@@ -479,58 +479,6 @@ async function promoteToReadyHandler(
|
|
|
479
479
|
};
|
|
480
480
|
}
|
|
481
481
|
|
|
482
|
-
export async function getPreviousEntry(ctx: QueryCtx, entry: Doc<"entries">) {
|
|
483
|
-
if (!entry.key) {
|
|
484
|
-
return null;
|
|
485
|
-
}
|
|
486
|
-
const previousEntry = await ctx.db
|
|
487
|
-
.query("entries")
|
|
488
|
-
.withIndex("namespaceId_status_key_version", (q) =>
|
|
489
|
-
q
|
|
490
|
-
.eq("namespaceId", entry.namespaceId)
|
|
491
|
-
.eq("status.kind", "ready")
|
|
492
|
-
.eq("key", entry.key),
|
|
493
|
-
)
|
|
494
|
-
.unique();
|
|
495
|
-
if (previousEntry?._id === entry._id) return null;
|
|
496
|
-
return previousEntry;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
export function publicEntry(entry: {
|
|
500
|
-
_id: Id<"entries">;
|
|
501
|
-
key?: string | undefined;
|
|
502
|
-
importance: number;
|
|
503
|
-
filterValues: EntryFilter[];
|
|
504
|
-
contentHash?: string | undefined;
|
|
505
|
-
title?: string | undefined;
|
|
506
|
-
metadata?: Record<string, Value> | undefined;
|
|
507
|
-
status: StatusWithOnComplete;
|
|
508
|
-
}): Entry {
|
|
509
|
-
const { key, importance, filterValues, contentHash, title, metadata } = entry;
|
|
510
|
-
|
|
511
|
-
const fields = {
|
|
512
|
-
entryId: entry._id as unknown as EntryId,
|
|
513
|
-
key,
|
|
514
|
-
title,
|
|
515
|
-
metadata,
|
|
516
|
-
importance,
|
|
517
|
-
filterValues,
|
|
518
|
-
contentHash,
|
|
519
|
-
};
|
|
520
|
-
if (entry.status.kind === "replaced") {
|
|
521
|
-
return {
|
|
522
|
-
...fields,
|
|
523
|
-
status: "replaced" as const,
|
|
524
|
-
replacedAt: entry.status.replacedAt,
|
|
525
|
-
};
|
|
526
|
-
} else {
|
|
527
|
-
return {
|
|
528
|
-
...fields,
|
|
529
|
-
status: entry.status.kind,
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
482
|
export const deleteAsync = mutation({
|
|
535
483
|
args: v.object({
|
|
536
484
|
entryId: v.id("entries"),
|
|
@@ -563,22 +511,24 @@ async function deleteAsyncHandler(
|
|
|
563
511
|
export const deleteSync = action({
|
|
564
512
|
args: { entryId: v.id("entries") },
|
|
565
513
|
returns: v.null(),
|
|
566
|
-
handler: async (ctx, { entryId }) =>
|
|
567
|
-
let startOrder = 0;
|
|
568
|
-
while (true) {
|
|
569
|
-
const status = await ctx.runMutation(internal.chunks.deleteChunksPage, {
|
|
570
|
-
entryId,
|
|
571
|
-
startOrder,
|
|
572
|
-
});
|
|
573
|
-
if (status.isDone) {
|
|
574
|
-
await ctx.runMutation(internal.entries._del, { entryId });
|
|
575
|
-
break;
|
|
576
|
-
}
|
|
577
|
-
startOrder = status.nextStartOrder;
|
|
578
|
-
}
|
|
579
|
-
},
|
|
514
|
+
handler: async (ctx, { entryId }) => deleteEntrySync(ctx, entryId),
|
|
580
515
|
});
|
|
581
516
|
|
|
517
|
+
export async function deleteEntrySync(ctx: ActionCtx, entryId: Id<"entries">) {
|
|
518
|
+
let startOrder = 0;
|
|
519
|
+
while (true) {
|
|
520
|
+
const status = await ctx.runMutation(internal.chunks.deleteChunksPage, {
|
|
521
|
+
entryId,
|
|
522
|
+
startOrder,
|
|
523
|
+
});
|
|
524
|
+
if (status.isDone) {
|
|
525
|
+
await ctx.runMutation(internal.entries._del, { entryId });
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
startOrder = status.nextStartOrder;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
582
532
|
export const _del = internalMutation({
|
|
583
533
|
args: { entryId: v.id("entries") },
|
|
584
534
|
returns: v.null(),
|
|
@@ -656,9 +606,7 @@ export const deleteByKeySync = action({
|
|
|
656
606
|
{ namespaceId: args.namespaceId, key: args.key },
|
|
657
607
|
);
|
|
658
608
|
for await (const entry of entries) {
|
|
659
|
-
await ctx
|
|
660
|
-
entryId: entry._id,
|
|
661
|
-
});
|
|
609
|
+
await deleteEntrySync(ctx, entry._id);
|
|
662
610
|
}
|
|
663
611
|
if (entries.length <= 100) {
|
|
664
612
|
break;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { ObjectType, Value } from "convex/values";
|
|
2
|
+
import {
|
|
3
|
+
Entry,
|
|
4
|
+
EntryFilter,
|
|
5
|
+
EntryId,
|
|
6
|
+
filterNamesContain,
|
|
7
|
+
type Namespace,
|
|
8
|
+
type NamespaceId,
|
|
9
|
+
} from "../shared.js";
|
|
10
|
+
import type { Doc, Id } from "./_generated/dataModel.js";
|
|
11
|
+
import { type QueryCtx } from "./_generated/server.js";
|
|
12
|
+
import { StatusWithOnComplete, v } from "./schema.js";
|
|
13
|
+
|
|
14
|
+
export function publicNamespace(namespace: Doc<"namespaces">): Namespace {
|
|
15
|
+
const { _id, _creationTime, status, ...rest } = namespace;
|
|
16
|
+
return {
|
|
17
|
+
namespaceId: _id as unknown as NamespaceId,
|
|
18
|
+
createdAt: _creationTime,
|
|
19
|
+
...rest,
|
|
20
|
+
status: status.kind,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function publicEntry(entry: {
|
|
25
|
+
_id: Id<"entries">;
|
|
26
|
+
key?: string | undefined;
|
|
27
|
+
importance: number;
|
|
28
|
+
filterValues: EntryFilter[];
|
|
29
|
+
contentHash?: string | undefined;
|
|
30
|
+
title?: string | undefined;
|
|
31
|
+
metadata?: Record<string, Value> | undefined;
|
|
32
|
+
status: StatusWithOnComplete;
|
|
33
|
+
}): Entry {
|
|
34
|
+
const { key, importance, filterValues, contentHash, title, metadata } = entry;
|
|
35
|
+
|
|
36
|
+
const fields = {
|
|
37
|
+
entryId: entry._id as unknown as EntryId,
|
|
38
|
+
key,
|
|
39
|
+
title,
|
|
40
|
+
metadata,
|
|
41
|
+
importance,
|
|
42
|
+
filterValues,
|
|
43
|
+
contentHash,
|
|
44
|
+
};
|
|
45
|
+
if (entry.status.kind === "replaced") {
|
|
46
|
+
return {
|
|
47
|
+
...fields,
|
|
48
|
+
status: "replaced" as const,
|
|
49
|
+
replacedAt: entry.status.replacedAt,
|
|
50
|
+
};
|
|
51
|
+
} else {
|
|
52
|
+
return {
|
|
53
|
+
...fields,
|
|
54
|
+
status: entry.status.kind,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const vNamespaceLookupArgs = {
|
|
60
|
+
namespace: v.string(),
|
|
61
|
+
modelId: v.string(),
|
|
62
|
+
dimension: v.number(),
|
|
63
|
+
filterNames: v.array(v.string()),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export async function getCompatibleNamespaceHandler(
|
|
67
|
+
ctx: QueryCtx,
|
|
68
|
+
args: ObjectType<typeof vNamespaceLookupArgs>,
|
|
69
|
+
) {
|
|
70
|
+
const iter = ctx.db
|
|
71
|
+
.query("namespaces")
|
|
72
|
+
.withIndex("status_namespace_version", (q) =>
|
|
73
|
+
q.eq("status.kind", "ready").eq("namespace", args.namespace),
|
|
74
|
+
)
|
|
75
|
+
.order("desc");
|
|
76
|
+
for await (const existing of iter) {
|
|
77
|
+
if (namespaceIsCompatible(existing, args)) {
|
|
78
|
+
return existing;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function namespaceIsCompatible(
|
|
85
|
+
existing: Doc<"namespaces">,
|
|
86
|
+
args: {
|
|
87
|
+
modelId: string;
|
|
88
|
+
dimension: number;
|
|
89
|
+
filterNames: string[];
|
|
90
|
+
},
|
|
91
|
+
) {
|
|
92
|
+
// Check basic compatibility
|
|
93
|
+
if (
|
|
94
|
+
existing.modelId !== args.modelId ||
|
|
95
|
+
existing.dimension !== args.dimension
|
|
96
|
+
) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// For filter names, the namespace must support all requested filters
|
|
101
|
+
// but can support additional filters (superset is OK)
|
|
102
|
+
if (!filterNamesContain(existing.filterNames, args.filterNames)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function getPreviousEntry(ctx: QueryCtx, entry: Doc<"entries">) {
|
|
110
|
+
if (!entry.key) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const previousEntry = await ctx.db
|
|
114
|
+
.query("entries")
|
|
115
|
+
.withIndex("namespaceId_status_key_version", (q) =>
|
|
116
|
+
q
|
|
117
|
+
.eq("namespaceId", entry.namespaceId)
|
|
118
|
+
.eq("status.kind", "ready")
|
|
119
|
+
.eq("key", entry.key),
|
|
120
|
+
)
|
|
121
|
+
.unique();
|
|
122
|
+
if (previousEntry?._id === entry._id) return null;
|
|
123
|
+
return previousEntry;
|
|
124
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
4
|
+
import { type TestConvex } from "convex-test";
|
|
5
|
+
import schema from "./schema.js";
|
|
6
|
+
import { api } from "./_generated/api.js";
|
|
7
|
+
import { initConvexTest } from "./setup.test.js";
|
|
8
|
+
import type { Id } from "./_generated/dataModel.js";
|
|
9
|
+
|
|
10
|
+
type ConvexTest = TestConvex<typeof schema>;
|
|
11
|
+
|
|
12
|
+
describe("namespaces", () => {
|
|
13
|
+
async function setupTestNamespace(t: ConvexTest) {
|
|
14
|
+
const namespace = await t.mutation(api.namespaces.getOrCreate, {
|
|
15
|
+
namespace: "test-namespace",
|
|
16
|
+
status: "ready",
|
|
17
|
+
modelId: "test-model",
|
|
18
|
+
dimension: 128,
|
|
19
|
+
filterNames: [],
|
|
20
|
+
});
|
|
21
|
+
return namespace.namespaceId;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function testEntryArgs(namespaceId: Id<"namespaces">, key: string) {
|
|
25
|
+
return {
|
|
26
|
+
namespaceId,
|
|
27
|
+
key,
|
|
28
|
+
importance: 0.5,
|
|
29
|
+
filterValues: [],
|
|
30
|
+
contentHash: `hash-${key}`,
|
|
31
|
+
title: `Entry ${key}`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function testChunk() {
|
|
36
|
+
return {
|
|
37
|
+
content: { text: "test chunk" },
|
|
38
|
+
embedding: Array.from({ length: 128 }, () => Math.random()),
|
|
39
|
+
searchableText: "test chunk",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
beforeEach(async () => {
|
|
44
|
+
vi.useFakeTimers();
|
|
45
|
+
});
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
vi.useRealTimers();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("deleteNamespaceSync deletes all entries then the namespace", async () => {
|
|
51
|
+
const t = initConvexTest();
|
|
52
|
+
const namespaceId = await setupTestNamespace(t);
|
|
53
|
+
|
|
54
|
+
// Create multiple entries so the pagination loop must handle them all
|
|
55
|
+
const entryIds: Id<"entries">[] = [];
|
|
56
|
+
for (let i = 0; i < 3; i++) {
|
|
57
|
+
const result = await t.mutation(api.entries.add, {
|
|
58
|
+
entry: testEntryArgs(namespaceId, `key-${i}`),
|
|
59
|
+
allChunks: [testChunk()],
|
|
60
|
+
});
|
|
61
|
+
expect(result.status).toBe("ready");
|
|
62
|
+
entryIds.push(result.entryId);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Verify entries exist
|
|
66
|
+
const entriesBefore = await t.run(async (ctx) => {
|
|
67
|
+
return ctx.db
|
|
68
|
+
.query("entries")
|
|
69
|
+
.filter((q) => q.eq(q.field("namespaceId"), namespaceId))
|
|
70
|
+
.collect();
|
|
71
|
+
});
|
|
72
|
+
expect(entriesBefore).toHaveLength(3);
|
|
73
|
+
|
|
74
|
+
// This should delete all entries and then the namespace.
|
|
75
|
+
// Before the fix, this threw "cannot delete, has entries" because
|
|
76
|
+
// the pagination loop checked isDone before processing the page,
|
|
77
|
+
// skipping deletion when entries fit in a single page.
|
|
78
|
+
await t.action(api.namespaces.deleteNamespaceSync, {
|
|
79
|
+
namespaceId,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Verify all entries are deleted
|
|
83
|
+
const entriesAfter = await t.run(async (ctx) => {
|
|
84
|
+
return ctx.db
|
|
85
|
+
.query("entries")
|
|
86
|
+
.filter((q) => q.eq(q.field("namespaceId"), namespaceId))
|
|
87
|
+
.collect();
|
|
88
|
+
});
|
|
89
|
+
expect(entriesAfter).toHaveLength(0);
|
|
90
|
+
|
|
91
|
+
// Verify namespace is deleted
|
|
92
|
+
const namespaceAfter = await t.run(async (ctx) => {
|
|
93
|
+
return ctx.db.get(namespaceId);
|
|
94
|
+
});
|
|
95
|
+
expect(namespaceAfter).toBeNull();
|
|
96
|
+
});
|
|
97
|
+
});
|