@convex-dev/rag 0.6.1 → 0.7.1
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 +4 -17
- package/dist/client/hybridRank.d.ts +1 -1
- package/dist/client/hybridRank.js +1 -1
- package/dist/client/index.d.ts +39 -7
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +33 -14
- package/dist/client/index.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/dataModel.d.ts +1 -1
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/chunks.d.ts +9 -2
- package/dist/component/chunks.d.ts.map +1 -1
- package/dist/component/chunks.js +66 -63
- package/dist/component/chunks.js.map +1 -1
- package/dist/component/embeddings/tables.d.ts +2 -2
- package/dist/component/embeddings/tables.d.ts.map +1 -1
- package/dist/component/schema.d.ts +87 -84
- 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 +44 -1
- 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 +40 -38
- package/src/client/hybridRank.ts +1 -1
- package/src/client/index.test.ts +1 -1
- package/src/client/index.ts +80 -18
- package/src/component/_generated/component.ts +6 -1
- package/src/component/_generated/dataModel.ts +1 -1
- package/src/component/_generated/server.ts +0 -5
- package/src/component/chunks.ts +102 -92
- 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
package/src/client/index.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
vEntryId,
|
|
35
35
|
vNamespaceId,
|
|
36
36
|
vOnCompleteArgs,
|
|
37
|
+
vSearchType,
|
|
37
38
|
type Chunk,
|
|
38
39
|
type ChunkerAction,
|
|
39
40
|
type CreateChunkArgs,
|
|
@@ -46,12 +47,13 @@ import {
|
|
|
46
47
|
type OnCompleteNamespace,
|
|
47
48
|
type SearchEntry,
|
|
48
49
|
type SearchResult,
|
|
50
|
+
type SearchType,
|
|
49
51
|
type Status,
|
|
50
52
|
} from "../shared.js";
|
|
51
53
|
import { defaultChunker } from "./defaultChunker.js";
|
|
52
54
|
|
|
53
55
|
export { hybridRank } from "./hybridRank.js";
|
|
54
|
-
export { defaultChunker, vEntryId, vNamespaceId };
|
|
56
|
+
export { defaultChunker, vEntryId, vNamespaceId, vSearchType };
|
|
55
57
|
export type {
|
|
56
58
|
ChunkerAction,
|
|
57
59
|
Entry,
|
|
@@ -61,6 +63,7 @@ export type {
|
|
|
61
63
|
OnCompleteNamespace,
|
|
62
64
|
SearchEntry,
|
|
63
65
|
SearchResult,
|
|
66
|
+
SearchType,
|
|
64
67
|
Status,
|
|
65
68
|
};
|
|
66
69
|
|
|
@@ -112,7 +115,7 @@ export class RAG<
|
|
|
112
115
|
public component: ComponentApi,
|
|
113
116
|
public options: {
|
|
114
117
|
embeddingDimension: number;
|
|
115
|
-
textEmbeddingModel: EmbeddingModel
|
|
118
|
+
textEmbeddingModel: EmbeddingModel;
|
|
116
119
|
filterNames?: FilterNames<FitlerSchemas>;
|
|
117
120
|
},
|
|
118
121
|
) {}
|
|
@@ -388,27 +391,56 @@ export class RAG<
|
|
|
388
391
|
limit = DEFAULT_SEARCH_LIMIT,
|
|
389
392
|
chunkContext = { before: 0, after: 0 },
|
|
390
393
|
vectorScoreThreshold,
|
|
394
|
+
searchType = "vector",
|
|
395
|
+
textWeight,
|
|
396
|
+
vectorWeight,
|
|
391
397
|
} = args;
|
|
392
|
-
|
|
398
|
+
|
|
399
|
+
const needsEmbedding = searchType !== "text";
|
|
400
|
+
let needsTextQuery = searchType !== "vector";
|
|
401
|
+
|
|
402
|
+
if (needsTextQuery && Array.isArray(args.query)) {
|
|
403
|
+
if (searchType === "text") {
|
|
404
|
+
throw new Error('searchType "text" requires a string query.');
|
|
405
|
+
}
|
|
406
|
+
console.warn(
|
|
407
|
+
`searchType "${searchType}" requires a string query. Falling back to vector-only search for embedding array queries.`,
|
|
408
|
+
);
|
|
409
|
+
needsTextQuery = false;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
let embedding: number[] | undefined;
|
|
393
413
|
let usage: EmbeddingModelUsage = { tokens: 0 };
|
|
394
|
-
if (
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
414
|
+
if (needsEmbedding) {
|
|
415
|
+
if (Array.isArray(args.query)) {
|
|
416
|
+
embedding = args.query;
|
|
417
|
+
} else {
|
|
418
|
+
const embedResult = await embed({
|
|
419
|
+
model: this.options.textEmbeddingModel,
|
|
420
|
+
value: args.query,
|
|
421
|
+
});
|
|
422
|
+
embedding = embedResult.embedding;
|
|
423
|
+
usage = embedResult.usage;
|
|
424
|
+
}
|
|
401
425
|
}
|
|
426
|
+
|
|
427
|
+
const textQuery =
|
|
428
|
+
needsTextQuery && typeof args.query === "string" ? args.query : undefined;
|
|
429
|
+
|
|
402
430
|
const { results, entries } = await ctx.runAction(
|
|
403
431
|
this.component.search.search,
|
|
404
432
|
{
|
|
405
433
|
embedding,
|
|
434
|
+
dimension: this.options.embeddingDimension,
|
|
406
435
|
namespace,
|
|
407
436
|
modelId: getModelId(this.options.textEmbeddingModel),
|
|
408
437
|
filters,
|
|
409
438
|
limit,
|
|
410
439
|
vectorScoreThreshold,
|
|
411
440
|
chunkContext,
|
|
441
|
+
textQuery,
|
|
442
|
+
textWeight,
|
|
443
|
+
vectorWeight,
|
|
412
444
|
},
|
|
413
445
|
);
|
|
414
446
|
const entriesWithTexts = entries.map((e) => {
|
|
@@ -975,27 +1007,27 @@ function makeBatches<T>(items: T[], batchSize: number): T[][] {
|
|
|
975
1007
|
}
|
|
976
1008
|
|
|
977
1009
|
async function createChunkArgsBatch(
|
|
978
|
-
embedModel: EmbeddingModel
|
|
1010
|
+
embedModel: EmbeddingModel,
|
|
979
1011
|
chunks: InputChunk[],
|
|
980
1012
|
): Promise<{ chunks: CreateChunkArgs[]; usage: EmbeddingModelUsage }> {
|
|
981
1013
|
const argsMaybeMissingEmbeddings: (Omit<CreateChunkArgs, "embedding"> & {
|
|
982
1014
|
embedding?: number[];
|
|
983
1015
|
})[] = chunks.map((chunk) => {
|
|
984
1016
|
if (typeof chunk === "string") {
|
|
985
|
-
return { content: { text: chunk } };
|
|
1017
|
+
return { content: { text: chunk }, searchableText: chunk };
|
|
986
1018
|
} else if ("text" in chunk) {
|
|
987
1019
|
const { text, metadata, keywords: searchableText } = chunk;
|
|
988
1020
|
return {
|
|
989
1021
|
content: { text, metadata },
|
|
990
1022
|
embedding: chunk.embedding,
|
|
991
|
-
searchableText,
|
|
1023
|
+
searchableText: searchableText ?? text,
|
|
992
1024
|
};
|
|
993
1025
|
} else if ("pageContent" in chunk) {
|
|
994
1026
|
const { pageContent: text, metadata, keywords: searchableText } = chunk;
|
|
995
1027
|
return {
|
|
996
1028
|
content: { text, metadata },
|
|
997
1029
|
embedding: chunk.embedding,
|
|
998
|
-
searchableText,
|
|
1030
|
+
searchableText: searchableText ?? text,
|
|
999
1031
|
};
|
|
1000
1032
|
} else {
|
|
1001
1033
|
throw new Error("Invalid chunk: " + JSON.stringify(chunk));
|
|
@@ -1033,22 +1065,24 @@ async function createChunkArgsBatch(
|
|
|
1033
1065
|
|
|
1034
1066
|
type MastraChunk = {
|
|
1035
1067
|
text: string;
|
|
1036
|
-
metadata
|
|
1068
|
+
metadata?: Record<string, Value>;
|
|
1037
1069
|
embedding?: Array<number>;
|
|
1038
1070
|
};
|
|
1039
1071
|
|
|
1040
1072
|
type LangChainChunk = {
|
|
1041
1073
|
id?: string;
|
|
1042
1074
|
pageContent: string;
|
|
1043
|
-
metadata
|
|
1075
|
+
metadata?: Record<string, Value>; //{ loc: { lines: { from: number; to: number } } };
|
|
1044
1076
|
embedding?: Array<number>;
|
|
1045
1077
|
};
|
|
1046
1078
|
|
|
1047
1079
|
export type InputChunk =
|
|
1048
1080
|
| string
|
|
1049
1081
|
| ((MastraChunk | LangChainChunk) & {
|
|
1050
|
-
|
|
1051
|
-
|
|
1082
|
+
/**
|
|
1083
|
+
* Text to use for full-text search. Defaults to the chunk's text content.
|
|
1084
|
+
* Provide a custom value to control what text is searchable.
|
|
1085
|
+
*/
|
|
1052
1086
|
keywords?: string;
|
|
1053
1087
|
// In the future we can add per-chunk metadata if it's useful.
|
|
1054
1088
|
// importance?: Importance;
|
|
@@ -1167,6 +1201,34 @@ type SearchOptions<FitlerSchemas extends Record<string, Value>> = {
|
|
|
1167
1201
|
* The minimum score to return a result.
|
|
1168
1202
|
*/
|
|
1169
1203
|
vectorScoreThreshold?: number;
|
|
1204
|
+
/**
|
|
1205
|
+
* The search mode to use.
|
|
1206
|
+
* - "vector": Vector similarity search only (default). Returns cosine
|
|
1207
|
+
* similarity scores.
|
|
1208
|
+
* - "text": Full-text search only. No embedding is computed. Returns
|
|
1209
|
+
* position-based scores.
|
|
1210
|
+
* - "hybrid": Combines vector and full-text search using Reciprocal Rank
|
|
1211
|
+
* Fusion. Returns position-based scores (1.0 for top result, decreasing
|
|
1212
|
+
* linearly).
|
|
1213
|
+
*
|
|
1214
|
+
* Text and hybrid modes require the query to be a string (not an embedding
|
|
1215
|
+
* array).
|
|
1216
|
+
*/
|
|
1217
|
+
searchType?: SearchType;
|
|
1218
|
+
/**
|
|
1219
|
+
* Weight for text search results in hybrid ranking (RRF).
|
|
1220
|
+
* Higher values give more influence to text search matches.
|
|
1221
|
+
* Only used when searchType is "hybrid".
|
|
1222
|
+
* Default: 1
|
|
1223
|
+
*/
|
|
1224
|
+
textWeight?: number;
|
|
1225
|
+
/**
|
|
1226
|
+
* Weight for vector search results in hybrid ranking (RRF).
|
|
1227
|
+
* Higher values give more influence to vector search matches.
|
|
1228
|
+
* Only used when searchType is "hybrid".
|
|
1229
|
+
* Default: 1
|
|
1230
|
+
*/
|
|
1231
|
+
vectorWeight?: number;
|
|
1170
1232
|
};
|
|
1171
1233
|
|
|
1172
1234
|
function getModelCategory(model: string | { provider: string }) {
|
|
@@ -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<{
|
|
@@ -38,7 +38,7 @@ export type Doc<TableName extends TableNames> = DocumentByName<
|
|
|
38
38
|
* Convex documents are uniquely identified by their `Id`, which is accessible
|
|
39
39
|
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
|
|
40
40
|
*
|
|
41
|
-
* Documents can be loaded using `db.get(id)` in query and mutation functions.
|
|
41
|
+
* Documents can be loaded using `db.get(tableName, id)` in query and mutation functions.
|
|
42
42
|
*
|
|
43
43
|
* IDs are just strings at runtime, but this type can be used to distinguish them from other
|
|
44
44
|
* strings when type checking.
|
|
@@ -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
|
@@ -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
|
|