@theromans/convex-dobty 0.1.9 → 0.1.11
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/index.d.ts +36 -12
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +33 -12
- package/dist/client/index.js.map +1 -1
- package/dist/component/knowledge.d.ts +61 -16
- package/dist/component/knowledge.d.ts.map +1 -1
- package/dist/component/knowledge.js +91 -21
- package/dist/component/knowledge.js.map +1 -1
- package/dist/component/knowledgeSearch.d.ts +32 -7
- package/dist/component/knowledgeSearch.d.ts.map +1 -1
- package/dist/component/knowledgeSearch.js +99 -39
- package/dist/component/knowledgeSearch.js.map +1 -1
- package/dist/component/schema.d.ts +30 -7
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +22 -4
- package/dist/component/schema.js.map +1 -1
- package/knowledge-chunks.json +247 -0
- package/package.json +5 -3
- package/src/client/index.ts +55 -16
- package/src/component/knowledge.ts +104 -21
- package/src/component/knowledgeSearch.ts +154 -48
- package/src/component/schema.ts +22 -4
|
@@ -13,8 +13,10 @@ export const seedKnowledgeChunks = mutation({
|
|
|
13
13
|
v.object({
|
|
14
14
|
topic: v.string(),
|
|
15
15
|
chunkKey: v.string(),
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
title_English: v.string(),
|
|
17
|
+
title_Deutsch: v.string(),
|
|
18
|
+
content_English: v.string(),
|
|
19
|
+
content_Deutsch: v.string(),
|
|
18
20
|
}),
|
|
19
21
|
),
|
|
20
22
|
},
|
|
@@ -23,6 +25,9 @@ export const seedKnowledgeChunks = mutation({
|
|
|
23
25
|
let updated = 0;
|
|
24
26
|
let skipped = 0;
|
|
25
27
|
|
|
28
|
+
// Collect all existing chunkKeys for this topic to detect deletions
|
|
29
|
+
const incomingKeys = new Set(chunks.map((c) => `${c.topic}::${c.chunkKey}`));
|
|
30
|
+
|
|
26
31
|
for (const chunk of chunks) {
|
|
27
32
|
const existing = await ctx.db
|
|
28
33
|
.query("knowledgeChunks")
|
|
@@ -32,10 +37,21 @@ export const seedKnowledgeChunks = mutation({
|
|
|
32
37
|
.unique();
|
|
33
38
|
|
|
34
39
|
if (existing) {
|
|
35
|
-
|
|
40
|
+
const contentChanged =
|
|
41
|
+
existing.content_English !== chunk.content_English ||
|
|
42
|
+
existing.content_Deutsch !== chunk.content_Deutsch ||
|
|
43
|
+
existing.title_English !== chunk.title_English ||
|
|
44
|
+
existing.title_Deutsch !== chunk.title_Deutsch;
|
|
45
|
+
|
|
46
|
+
if (contentChanged) {
|
|
36
47
|
await ctx.db.patch(existing._id, {
|
|
37
|
-
|
|
38
|
-
|
|
48
|
+
title_English: chunk.title_English,
|
|
49
|
+
title_Deutsch: chunk.title_Deutsch,
|
|
50
|
+
content_English: chunk.content_English,
|
|
51
|
+
content_Deutsch: chunk.content_Deutsch,
|
|
52
|
+
// Invalidate both embeddings so they get re-embedded
|
|
53
|
+
embeddedAt_en: undefined,
|
|
54
|
+
embeddedAt_de: undefined,
|
|
39
55
|
updatedAt: Date.now(),
|
|
40
56
|
});
|
|
41
57
|
updated++;
|
|
@@ -46,15 +62,27 @@ export const seedKnowledgeChunks = mutation({
|
|
|
46
62
|
await ctx.db.insert("knowledgeChunks", {
|
|
47
63
|
topic: chunk.topic,
|
|
48
64
|
chunkKey: chunk.chunkKey,
|
|
49
|
-
|
|
50
|
-
|
|
65
|
+
title_English: chunk.title_English,
|
|
66
|
+
title_Deutsch: chunk.title_Deutsch,
|
|
67
|
+
content_English: chunk.content_English,
|
|
68
|
+
content_Deutsch: chunk.content_Deutsch,
|
|
51
69
|
updatedAt: Date.now(),
|
|
52
70
|
});
|
|
53
71
|
inserted++;
|
|
54
72
|
}
|
|
55
73
|
}
|
|
56
74
|
|
|
57
|
-
|
|
75
|
+
// Remove chunks that are no longer in the incoming set (full replacement semantics)
|
|
76
|
+
const allExisting = await ctx.db.query("knowledgeChunks").collect();
|
|
77
|
+
let deleted = 0;
|
|
78
|
+
for (const existing of allExisting) {
|
|
79
|
+
if (!incomingKeys.has(`${existing.topic}::${existing.chunkKey}`)) {
|
|
80
|
+
await ctx.db.delete(existing._id);
|
|
81
|
+
deleted++;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { inserted, updated, skipped, deleted };
|
|
58
86
|
},
|
|
59
87
|
});
|
|
60
88
|
|
|
@@ -79,49 +107,104 @@ export const listTopics = query({
|
|
|
79
107
|
},
|
|
80
108
|
});
|
|
81
109
|
|
|
82
|
-
/** List chunks that need (re-)embedding
|
|
83
|
-
export const
|
|
110
|
+
/** List chunks that need (re-)embedding for English. */
|
|
111
|
+
export const listStaleChunksEn = query({
|
|
84
112
|
args: {},
|
|
85
113
|
handler: async (ctx) => {
|
|
86
114
|
const all = await ctx.db.query("knowledgeChunks").collect();
|
|
87
115
|
return all
|
|
88
|
-
.filter(
|
|
116
|
+
.filter(
|
|
117
|
+
(c) =>
|
|
118
|
+
!c.embedding_en ||
|
|
119
|
+
!c.embeddedAt_en ||
|
|
120
|
+
c.embeddedAt_en < c.updatedAt,
|
|
121
|
+
)
|
|
89
122
|
.map((c) => ({
|
|
90
123
|
_id: c._id,
|
|
91
124
|
topic: c.topic,
|
|
92
125
|
chunkKey: c.chunkKey,
|
|
93
|
-
title: c.
|
|
94
|
-
content: c.
|
|
126
|
+
title: c.title_English,
|
|
127
|
+
content: c.content_English,
|
|
95
128
|
}));
|
|
96
129
|
},
|
|
97
130
|
});
|
|
98
131
|
|
|
99
|
-
/**
|
|
100
|
-
export const
|
|
132
|
+
/** List chunks that need (re-)embedding for Deutsch. */
|
|
133
|
+
export const listStaleChunksDe = query({
|
|
134
|
+
args: {},
|
|
135
|
+
handler: async (ctx) => {
|
|
136
|
+
const all = await ctx.db.query("knowledgeChunks").collect();
|
|
137
|
+
return all
|
|
138
|
+
.filter(
|
|
139
|
+
(c) =>
|
|
140
|
+
!c.embedding_de ||
|
|
141
|
+
!c.embeddedAt_de ||
|
|
142
|
+
c.embeddedAt_de < c.updatedAt,
|
|
143
|
+
)
|
|
144
|
+
.map((c) => ({
|
|
145
|
+
_id: c._id,
|
|
146
|
+
topic: c.topic,
|
|
147
|
+
chunkKey: c.chunkKey,
|
|
148
|
+
title: c.title_Deutsch,
|
|
149
|
+
content: c.content_Deutsch,
|
|
150
|
+
}));
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
/** Update a chunk's English embedding by ID. */
|
|
155
|
+
export const updateChunkEmbeddingEn = mutation({
|
|
101
156
|
args: {
|
|
102
157
|
chunkId: v.id("knowledgeChunks"),
|
|
103
158
|
embedding: v.array(v.float64()),
|
|
104
159
|
},
|
|
105
160
|
handler: async (ctx, { chunkId, embedding }) => {
|
|
106
161
|
await ctx.db.patch(chunkId, {
|
|
107
|
-
embedding,
|
|
108
|
-
|
|
162
|
+
embedding_en: embedding,
|
|
163
|
+
embeddedAt_en: Date.now(),
|
|
164
|
+
});
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
/** Update a chunk's Deutsch embedding by ID. */
|
|
169
|
+
export const updateChunkEmbeddingDe = mutation({
|
|
170
|
+
args: {
|
|
171
|
+
chunkId: v.id("knowledgeChunks"),
|
|
172
|
+
embedding: v.array(v.float64()),
|
|
173
|
+
},
|
|
174
|
+
handler: async (ctx, { chunkId, embedding }) => {
|
|
175
|
+
await ctx.db.patch(chunkId, {
|
|
176
|
+
embedding_de: embedding,
|
|
177
|
+
embeddedAt_de: Date.now(),
|
|
109
178
|
});
|
|
110
179
|
},
|
|
111
180
|
});
|
|
112
181
|
|
|
113
182
|
// ── Internal functions (called by knowledgeSearch.ts) ──
|
|
114
183
|
|
|
115
|
-
/** Patch embedding
|
|
116
|
-
export const
|
|
184
|
+
/** Patch English embedding for a single chunk. */
|
|
185
|
+
export const updateEmbeddingEn = internalMutation({
|
|
186
|
+
args: {
|
|
187
|
+
chunkId: v.id("knowledgeChunks"),
|
|
188
|
+
embedding: v.array(v.float64()),
|
|
189
|
+
},
|
|
190
|
+
handler: async (ctx, { chunkId, embedding }) => {
|
|
191
|
+
await ctx.db.patch(chunkId, {
|
|
192
|
+
embedding_en: embedding,
|
|
193
|
+
embeddedAt_en: Date.now(),
|
|
194
|
+
});
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
/** Patch Deutsch embedding for a single chunk. */
|
|
199
|
+
export const updateEmbeddingDe = internalMutation({
|
|
117
200
|
args: {
|
|
118
201
|
chunkId: v.id("knowledgeChunks"),
|
|
119
202
|
embedding: v.array(v.float64()),
|
|
120
203
|
},
|
|
121
204
|
handler: async (ctx, { chunkId, embedding }) => {
|
|
122
205
|
await ctx.db.patch(chunkId, {
|
|
123
|
-
embedding,
|
|
124
|
-
|
|
206
|
+
embedding_de: embedding,
|
|
207
|
+
embeddedAt_de: Date.now(),
|
|
125
208
|
});
|
|
126
209
|
},
|
|
127
210
|
});
|
|
@@ -8,6 +8,7 @@ const EMBEDDING_MODEL = "BAAI/bge-m3";
|
|
|
8
8
|
const EMBEDDING_DIMENSIONS = 1024;
|
|
9
9
|
|
|
10
10
|
const EMBEDDING_TIMEOUT_MS = 10_000;
|
|
11
|
+
const DEFAULT_SEARCH_LIMIT = 5;
|
|
11
12
|
|
|
12
13
|
async function getEmbedding(
|
|
13
14
|
text: string,
|
|
@@ -48,110 +49,215 @@ async function getEmbedding(
|
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
interface KnowledgeResult {
|
|
52
|
+
export interface KnowledgeResult {
|
|
52
53
|
title: string;
|
|
53
54
|
content: string;
|
|
55
|
+
title_English: string;
|
|
56
|
+
title_Deutsch: string;
|
|
57
|
+
content_English: string;
|
|
58
|
+
content_Deutsch: string;
|
|
54
59
|
score: number;
|
|
60
|
+
language: "en" | "de";
|
|
61
|
+
embeddingLanguage: "en" | "de";
|
|
55
62
|
}
|
|
56
63
|
|
|
57
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Search knowledge chunks by embedding similarity.
|
|
66
|
+
*
|
|
67
|
+
* - language: the requested language for returned title/content ("en" | "de")
|
|
68
|
+
* - Uses the embedding index for the requested language.
|
|
69
|
+
* - Falls back to the other language's index if the primary has no vectors.
|
|
70
|
+
* - Returns up to `limit` results (default 5).
|
|
71
|
+
*/
|
|
58
72
|
export const searchKnowledge = action({
|
|
59
73
|
args: {
|
|
60
74
|
topic: v.string(),
|
|
61
75
|
query: v.string(),
|
|
76
|
+
language: v.union(v.literal("en"), v.literal("de")),
|
|
62
77
|
limit: v.optional(v.number()),
|
|
63
78
|
apiToken: v.string(),
|
|
64
79
|
},
|
|
65
|
-
handler: async (
|
|
80
|
+
handler: async (
|
|
81
|
+
ctx,
|
|
82
|
+
{ topic, query, language, limit = DEFAULT_SEARCH_LIMIT, apiToken },
|
|
83
|
+
): Promise<KnowledgeResult[]> => {
|
|
66
84
|
const embedding = await getEmbedding(query, apiToken);
|
|
67
85
|
|
|
68
|
-
|
|
69
|
-
vector: embedding,
|
|
70
|
-
limit,
|
|
71
|
-
filter: (q) => q.eq("topic", topic),
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
if (results.length === 0) return [];
|
|
75
|
-
|
|
76
|
-
const docs: (KnowledgeResult | null)[] = await Promise.all(
|
|
77
|
-
results.map(async (r): Promise<KnowledgeResult | null> => {
|
|
78
|
-
const doc: any = await ctx.runQuery(internal.knowledge.getChunk, {
|
|
79
|
-
chunkId: r._id,
|
|
80
|
-
});
|
|
81
|
-
return doc ? { title: doc.title, content: doc.content, score: r._score } : null;
|
|
82
|
-
}),
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
return docs.filter((d): d is KnowledgeResult => d !== null);
|
|
86
|
+
return _searchByVector(ctx, { topic, embedding, language, limit });
|
|
86
87
|
},
|
|
87
88
|
});
|
|
88
89
|
|
|
89
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* Search knowledge chunks by pre-computed embedding vector (no outbound fetch needed).
|
|
92
|
+
*
|
|
93
|
+
* - language: the requested language for returned title/content ("en" | "de")
|
|
94
|
+
* - Falls back to the other language's index if primary has no vectors.
|
|
95
|
+
* - Returns up to `limit` results (default 5).
|
|
96
|
+
*/
|
|
90
97
|
export const searchByVector = action({
|
|
91
98
|
args: {
|
|
92
99
|
topic: v.string(),
|
|
93
100
|
embedding: v.array(v.float64()),
|
|
101
|
+
language: v.union(v.literal("en"), v.literal("de")),
|
|
94
102
|
limit: v.optional(v.number()),
|
|
95
103
|
},
|
|
96
|
-
handler: async (
|
|
97
|
-
|
|
104
|
+
handler: async (
|
|
105
|
+
ctx,
|
|
106
|
+
{ topic, embedding, language, limit = DEFAULT_SEARCH_LIMIT },
|
|
107
|
+
): Promise<KnowledgeResult[]> => {
|
|
108
|
+
return _searchByVector(ctx, { topic, embedding, language, limit });
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
async function _searchByVector(
|
|
113
|
+
ctx: any,
|
|
114
|
+
{
|
|
115
|
+
topic,
|
|
116
|
+
embedding,
|
|
117
|
+
language,
|
|
118
|
+
limit,
|
|
119
|
+
}: {
|
|
120
|
+
topic: string;
|
|
121
|
+
embedding: number[];
|
|
122
|
+
language: "en" | "de";
|
|
123
|
+
limit: number;
|
|
124
|
+
},
|
|
125
|
+
): Promise<KnowledgeResult[]> {
|
|
126
|
+
const primaryIndex = language === "en" ? "by_embedding_en" : "by_embedding_de";
|
|
127
|
+
const fallbackIndex = language === "en" ? "by_embedding_de" : "by_embedding_en";
|
|
128
|
+
const fallbackLanguage: "en" | "de" = language === "en" ? "de" : "en";
|
|
129
|
+
|
|
130
|
+
// Try primary language index first
|
|
131
|
+
let results = await ctx.vectorSearch("knowledgeChunks", primaryIndex, {
|
|
132
|
+
vector: embedding,
|
|
133
|
+
limit,
|
|
134
|
+
filter: (q: any) => q.eq("topic", topic),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
let embeddingLanguage: "en" | "de" = language;
|
|
138
|
+
|
|
139
|
+
// Fall back to the other language's index if primary returned nothing
|
|
140
|
+
if (results.length === 0) {
|
|
141
|
+
results = await ctx.vectorSearch("knowledgeChunks", fallbackIndex, {
|
|
98
142
|
vector: embedding,
|
|
99
143
|
limit,
|
|
100
|
-
filter: (q) => q.eq("topic", topic),
|
|
144
|
+
filter: (q: any) => q.eq("topic", topic),
|
|
101
145
|
});
|
|
146
|
+
embeddingLanguage = fallbackLanguage;
|
|
147
|
+
}
|
|
102
148
|
|
|
103
|
-
|
|
149
|
+
if (results.length === 0) return [];
|
|
104
150
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}),
|
|
112
|
-
);
|
|
151
|
+
const docs: (KnowledgeResult | null)[] = await Promise.all(
|
|
152
|
+
results.map(async (r: any): Promise<KnowledgeResult | null> => {
|
|
153
|
+
const doc: any = await ctx.runQuery(internal.knowledge.getChunk, {
|
|
154
|
+
chunkId: r._id,
|
|
155
|
+
});
|
|
156
|
+
if (!doc) return null;
|
|
113
157
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
158
|
+
const title = language === "en" ? doc.title_English : doc.title_Deutsch;
|
|
159
|
+
const content =
|
|
160
|
+
language === "en" ? doc.content_English : doc.content_Deutsch;
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
title,
|
|
164
|
+
content,
|
|
165
|
+
title_English: doc.title_English,
|
|
166
|
+
title_Deutsch: doc.title_Deutsch,
|
|
167
|
+
content_English: doc.content_English,
|
|
168
|
+
content_Deutsch: doc.content_Deutsch,
|
|
169
|
+
score: r._score,
|
|
170
|
+
language,
|
|
171
|
+
embeddingLanguage,
|
|
172
|
+
};
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
return docs.filter((d): d is KnowledgeResult => d !== null);
|
|
177
|
+
}
|
|
117
178
|
|
|
118
|
-
/**
|
|
179
|
+
/**
|
|
180
|
+
* Re-embed stale chunks for both languages.
|
|
181
|
+
* Embeds English and Deutsch separately, storing embedding_en and embedding_de.
|
|
182
|
+
*/
|
|
119
183
|
export const refreshEmbeddings = action({
|
|
120
184
|
args: {
|
|
121
185
|
apiToken: v.string(),
|
|
122
186
|
},
|
|
123
|
-
handler: async (
|
|
187
|
+
handler: async (
|
|
188
|
+
ctx,
|
|
189
|
+
{ apiToken },
|
|
190
|
+
): Promise<{
|
|
191
|
+
total: number;
|
|
192
|
+
staleEn: number;
|
|
193
|
+
staleDe: number;
|
|
194
|
+
embeddedEn: number;
|
|
195
|
+
embeddedDe: number;
|
|
196
|
+
errors: number;
|
|
197
|
+
}> => {
|
|
124
198
|
const allChunks: any[] = await ctx.runQuery(
|
|
125
199
|
internal.knowledge.listAllChunks,
|
|
126
200
|
{},
|
|
127
201
|
);
|
|
128
202
|
|
|
129
|
-
const
|
|
130
|
-
(c) =>
|
|
203
|
+
const staleEn = allChunks.filter(
|
|
204
|
+
(c) =>
|
|
205
|
+
!c.embedding_en || !c.embeddedAt_en || c.embeddedAt_en < c.updatedAt,
|
|
206
|
+
);
|
|
207
|
+
const staleDe = allChunks.filter(
|
|
208
|
+
(c) =>
|
|
209
|
+
!c.embedding_de || !c.embeddedAt_de || c.embeddedAt_de < c.updatedAt,
|
|
131
210
|
);
|
|
132
211
|
|
|
133
|
-
let
|
|
212
|
+
let embeddedEn = 0;
|
|
213
|
+
let embeddedDe = 0;
|
|
134
214
|
let errors = 0;
|
|
135
215
|
|
|
136
|
-
for (const chunk of
|
|
216
|
+
for (const chunk of staleEn) {
|
|
217
|
+
try {
|
|
218
|
+
const embedding = await getEmbedding(
|
|
219
|
+
`${chunk.title_English}\n\n${chunk.content_English}`,
|
|
220
|
+
apiToken,
|
|
221
|
+
);
|
|
222
|
+
await ctx.runMutation(internal.knowledge.updateEmbeddingEn, {
|
|
223
|
+
chunkId: chunk._id,
|
|
224
|
+
embedding,
|
|
225
|
+
});
|
|
226
|
+
embeddedEn++;
|
|
227
|
+
} catch (e: any) {
|
|
228
|
+
console.error(
|
|
229
|
+
`Failed to embed EN chunk ${chunk.chunkKey}: ${e.message}`,
|
|
230
|
+
);
|
|
231
|
+
errors++;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
for (const chunk of staleDe) {
|
|
137
236
|
try {
|
|
138
237
|
const embedding = await getEmbedding(
|
|
139
|
-
`${chunk.
|
|
238
|
+
`${chunk.title_Deutsch}\n\n${chunk.content_Deutsch}`,
|
|
140
239
|
apiToken,
|
|
141
240
|
);
|
|
142
|
-
await ctx.runMutation(internal.knowledge.
|
|
241
|
+
await ctx.runMutation(internal.knowledge.updateEmbeddingDe, {
|
|
143
242
|
chunkId: chunk._id,
|
|
144
243
|
embedding,
|
|
145
244
|
});
|
|
146
|
-
|
|
245
|
+
embeddedDe++;
|
|
147
246
|
} catch (e: any) {
|
|
148
247
|
console.error(
|
|
149
|
-
`Failed to embed chunk ${chunk.chunkKey}: ${e.message}`,
|
|
248
|
+
`Failed to embed DE chunk ${chunk.chunkKey}: ${e.message}`,
|
|
150
249
|
);
|
|
151
250
|
errors++;
|
|
152
251
|
}
|
|
153
252
|
}
|
|
154
253
|
|
|
155
|
-
return {
|
|
254
|
+
return {
|
|
255
|
+
total: allChunks.length,
|
|
256
|
+
staleEn: staleEn.length,
|
|
257
|
+
staleDe: staleDe.length,
|
|
258
|
+
embeddedEn,
|
|
259
|
+
embeddedDe,
|
|
260
|
+
errors,
|
|
261
|
+
};
|
|
156
262
|
},
|
|
157
263
|
});
|
package/src/component/schema.ts
CHANGED
|
@@ -215,16 +215,34 @@ export default defineSchema({
|
|
|
215
215
|
knowledgeChunks: defineTable({
|
|
216
216
|
topic: v.string(),
|
|
217
217
|
chunkKey: v.string(),
|
|
218
|
-
|
|
219
|
-
|
|
218
|
+
// New bilingual fields (required going forward)
|
|
219
|
+
title_English: v.optional(v.string()),
|
|
220
|
+
title_Deutsch: v.optional(v.string()),
|
|
221
|
+
content_English: v.optional(v.string()),
|
|
222
|
+
content_Deutsch: v.optional(v.string()),
|
|
223
|
+
// Legacy single-language fields — kept optional to allow migration deploy
|
|
224
|
+
title: v.optional(v.string()),
|
|
225
|
+
content: v.optional(v.string()),
|
|
226
|
+
/** Embedding of title_English + content_English */
|
|
227
|
+
embedding_en: v.optional(v.array(v.float64())),
|
|
228
|
+
/** Embedding of title_Deutsch + content_Deutsch */
|
|
229
|
+
embedding_de: v.optional(v.array(v.float64())),
|
|
230
|
+
// Legacy single embedding — kept optional for migration
|
|
220
231
|
embedding: v.optional(v.array(v.float64())),
|
|
232
|
+
embeddedAt_en: v.optional(v.number()),
|
|
233
|
+
embeddedAt_de: v.optional(v.number()),
|
|
221
234
|
embeddedAt: v.optional(v.number()),
|
|
222
235
|
updatedAt: v.number(),
|
|
223
236
|
})
|
|
224
237
|
.index("by_topic", ["topic"])
|
|
225
238
|
.index("by_topic_chunkKey", ["topic", "chunkKey"])
|
|
226
|
-
.vectorIndex("
|
|
227
|
-
vectorField: "
|
|
239
|
+
.vectorIndex("by_embedding_en", {
|
|
240
|
+
vectorField: "embedding_en",
|
|
241
|
+
dimensions: 1024,
|
|
242
|
+
filterFields: ["topic"],
|
|
243
|
+
})
|
|
244
|
+
.vectorIndex("by_embedding_de", {
|
|
245
|
+
vectorField: "embedding_de",
|
|
228
246
|
dimensions: 1024,
|
|
229
247
|
filterFields: ["topic"],
|
|
230
248
|
}),
|