@wirecat/cli-core 0.433.0 → 0.435.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/CHANGELOG.md +43 -1
- package/dist/cli/messenger/context.js +1 -0
- package/dist/cli/messenger/conversation-answers.d.ts +1 -0
- package/dist/cli/messenger/conversations-command.js +2 -2
- package/dist/cli/messenger/message-answers.d.ts +1 -0
- package/dist/cli/messenger/message-writes.d.ts +2 -0
- package/dist/cli/messenger/messages-search-command.js +6 -10
- package/dist/cli/messenger/models-command.js +34 -7
- package/dist/cli/messenger/search-answers.d.ts +3 -0
- package/dist/cli/messenger/search-answers.js +40 -8
- package/dist/cli/messenger/search-command.js +3 -1
- package/dist/cli/messenger/store-maintenance-command.js +11 -5
- package/dist/cli/model-catalogue-command.js +1 -1
- package/dist/cli/settings.d.ts +4 -0
- package/dist/cli/settings.js +5 -0
- package/dist/cli/shared-command-exports.d.ts +4 -0
- package/dist/embeddings/child.js +15 -5
- package/dist/embeddings/embed.d.ts +16 -2
- package/dist/embeddings/embed.js +57 -28
- package/dist/embeddings/models.d.ts +18 -0
- package/dist/embeddings/models.js +69 -0
- package/dist/embeddings/process.d.ts +6 -1
- package/dist/embeddings/process.js +23 -7
- package/dist/embeddings/rerank.d.ts +10 -0
- package/dist/embeddings/rerank.js +31 -0
- package/dist/entities/conversation-results.d.ts +2 -0
- package/dist/entities/evidence.d.ts +8 -0
- package/dist/entities/index.d.ts +37 -4
- package/dist/entities/listener.d.ts +4 -0
- package/dist/entities/message-context.d.ts +3 -0
- package/dist/entities/message-write.d.ts +4 -0
- package/dist/entities/message.d.ts +2 -0
- package/dist/entities/message.js +2 -0
- package/dist/entities/model.d.ts +5 -2
- package/dist/entities/model.js +5 -2
- package/dist/entities/store-maintenance.d.ts +11 -3
- package/dist/entities/store-maintenance.js +18 -2
- package/dist/entities/store-records.d.ts +1 -0
- package/dist/errors/reasons.js +7 -0
- package/dist/mcp/tool.d.ts +1 -1
- package/dist/mcp/tool.js +1 -0
- package/dist/services/deps.d.ts +3 -0
- package/dist/services/embeddings-maintenance.d.ts +10 -2
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.js +1 -0
- package/dist/services/messages-archive-search.d.ts +2 -2
- package/dist/services/messages-archive-search.js +4 -4
- package/dist/services/messages-discovery.d.ts +33 -4
- package/dist/services/messages-discovery.js +88 -3
- package/dist/services/messages-meaning.d.ts +2 -1
- package/dist/services/messages-meaning.js +7 -3
- package/dist/services/messages-rerank.d.ts +7 -0
- package/dist/services/messages-rerank.js +42 -0
- package/dist/services/messages-search.d.ts +5 -0
- package/dist/services/messages.d.ts +6 -2
- package/dist/services/messages.js +4 -3
- package/dist/services/model-downloads.d.ts +3 -1
- package/dist/services/model-downloads.js +20 -1
- package/dist/services/notes-search.d.ts +7 -2
- package/dist/services/notes-search.js +7 -4
- package/dist/services/search-all-meetings.d.ts +3 -2
- package/dist/services/search-all-meetings.js +8 -8
- package/dist/services/search-all.d.ts +7 -1
- package/dist/services/search-all.js +109 -45
- package/dist/store/drivers/bun-sqlite.d.ts +6 -0
- package/dist/store/drivers/bun-sqlite.js +16 -1
- package/dist/store/drivers/node-sqlite.d.ts +5 -1
- package/dist/store/drivers/node-sqlite.js +15 -1
- package/dist/store/exports.d.ts +1 -0
- package/dist/store/exports.js +1 -0
- package/dist/store/sqlite/accounts.js +2 -2
- package/dist/store/sqlite/chats.d.ts +1 -1
- package/dist/store/sqlite/chats.js +11 -4
- package/dist/store/sqlite/document-pictures.js +5 -7
- package/dist/store/sqlite/email-index.d.ts +1 -3
- package/dist/store/sqlite/email-index.js +5 -21
- package/dist/store/sqlite/emails.js +4 -3
- package/dist/store/sqlite/image-chunks.d.ts +3 -3
- package/dist/store/sqlite/image-chunks.js +55 -25
- package/dist/store/sqlite/manifest.js +2 -0
- package/dist/store/sqlite/meeting-vectors.js +11 -9
- package/dist/store/sqlite/message-chunks.d.ts +5 -2
- package/dist/store/sqlite/message-chunks.js +44 -20
- package/dist/store/sqlite/migrations.generated.js +22 -0
- package/dist/store/sqlite/note-index.js +2 -7
- package/dist/store/sqlite/open-store.js +4 -0
- package/dist/store/sqlite/open.js +4 -6
- package/dist/store/sqlite/schema.d.ts +118 -1
- package/dist/store/sqlite/schema.js +18 -1
- package/dist/store/sqlite/vector-index.d.ts +76 -0
- package/dist/store/sqlite/vector-index.js +205 -0
- package/dist/store/sqlite/vectors.d.ts +8 -5
- package/dist/store/sqlite/vectors.js +82 -39
- package/dist/store/vector-extension.d.ts +10 -0
- package/dist/store/vector-extension.js +53 -0
- package/package.json +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { sql } from "./drizzle/core.js";
|
|
2
|
+
import { dropUnusedVectors, searchIndex } from "./vector-index.js";
|
|
2
3
|
import { dot } from "./vectors.js";
|
|
3
4
|
const IS_IMAGE = sql `(a.kind = 'photo' OR a.mime LIKE 'image/%')`;
|
|
4
5
|
const neighbours = ({ orm }, chatKey, messagePk, sentAt, { messages, withinMs }, side) => orm.all(sql `SELECT sender_name AS sender, text, sent_at AS sentAt FROM messages
|
|
@@ -26,7 +27,7 @@ export const imagesOfChat = (context, chatKey, window) => context.orm
|
|
|
26
27
|
* Makes these the chunks of their images, and drops the chunks of images that are gone or whose message was
|
|
27
28
|
* deleted, with the vectors nothing else uses any more.
|
|
28
29
|
*/
|
|
29
|
-
export const saveImageChunks = ({ orm, now }, accountPk, chatKey, chunks, keep) => {
|
|
30
|
+
export const saveImageChunks = ({ orm, now, database }, accountPk, chatKey, chunks, keep) => {
|
|
30
31
|
const at = now();
|
|
31
32
|
const scope = orm.get(sql `SELECT scope FROM accounts WHERE id = ${accountPk}`)?.scope ?? null;
|
|
32
33
|
const old = new Set();
|
|
@@ -50,9 +51,7 @@ export const saveImageChunks = ({ orm, now }, accountPk, chatKey, chunks, keep)
|
|
|
50
51
|
account_id, occurred_at, created_at, updated_at)
|
|
51
52
|
VALUES ('image', ${attachmentPk}, 0, 0, ${length}, ${hash}, ${scope}, ${accountPk}, ${occurredAt}, ${at}, ${at})`);
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
-
orm.run(sql `DELETE FROM embeddings WHERE content_hash = ${hash}
|
|
55
|
-
AND NOT EXISTS (SELECT 1 FROM chunks WHERE content_hash = ${hash})`);
|
|
54
|
+
dropUnusedVectors(database, old);
|
|
56
55
|
};
|
|
57
56
|
/** The hashes among these that already have a vector of `model`. */
|
|
58
57
|
export const embeddedHashes = ({ orm }, model, hashes) => new Set(hashes.length === 0
|
|
@@ -62,32 +61,64 @@ export const embeddedHashes = ({ orm }, model, hashes) => new Set(hashes.length
|
|
|
62
61
|
WHERE model = ${model} AND content_hash IN (SELECT value FROM json_each(${JSON.stringify(hashes)}))`)
|
|
63
62
|
.map(({ hash }) => hash));
|
|
64
63
|
const SCAN_PAGE = 5_000;
|
|
64
|
+
/** The scan's rows: one page after `after`, or the `ids` the index picked. */
|
|
65
|
+
const imageRows = ({ orm }, accountPk, { chatKey, model, since, position }, rows) => orm.all(sql `SELECT k.id, k.content_hash AS hash, ch.external_id AS chatId, m.external_id AS messageId, v.vector
|
|
66
|
+
FROM chunks k CROSS JOIN attachments a ON a.id = k.chunkable_id AND a.attachable_type = 'message'
|
|
67
|
+
JOIN messages m ON m.id = a.attachable_id AND m.deleted_at IS NULL
|
|
68
|
+
JOIN chats ch ON ch.id = m.chat_id
|
|
69
|
+
JOIN embeddings v ON v.model = ${model} AND v.content_hash = k.content_hash
|
|
70
|
+
WHERE k.chunkable_type = 'image' AND k.position = ${position} AND ch.account_id = ${accountPk}
|
|
71
|
+
${chatKey === undefined ? sql `AND ch.searchable = 1` : sql `AND m.chat_id = ${chatKey}`}
|
|
72
|
+
${since === undefined ? sql `` : sql `AND k.occurred_at >= ${since}`}
|
|
73
|
+
${"ids" in rows
|
|
74
|
+
? sql `AND k.id IN (SELECT value FROM json_each(${JSON.stringify(rows.ids)})) ORDER BY k.id`
|
|
75
|
+
: sql `AND k.id > ${rows.after} ORDER BY k.id LIMIT ${SCAN_PAGE}`}`);
|
|
65
76
|
/**
|
|
66
77
|
* The images of the account nearest to `query` by the vector of `model`, best first. Position 0 of an image is
|
|
67
78
|
* its context text, position 1 its picture (`PICTURE_POSITION`): a model's vectors only ever sit at one.
|
|
68
79
|
*/
|
|
69
|
-
export const nearestImages = (
|
|
80
|
+
export const nearestImages = (context, accountPk, { chatKey, model, since, limit, query, position = 0, }) => {
|
|
81
|
+
const filter = {
|
|
82
|
+
model,
|
|
83
|
+
position,
|
|
84
|
+
...(chatKey === undefined ? {} : { chatKey }),
|
|
85
|
+
...(since === undefined ? {} : { since }),
|
|
86
|
+
};
|
|
87
|
+
const score = (row) => ({
|
|
88
|
+
id: row.id,
|
|
89
|
+
chatId: row.chatId,
|
|
90
|
+
messageId: row.messageId,
|
|
91
|
+
score: dot(query, row.vector),
|
|
92
|
+
});
|
|
93
|
+
// Stable, so equal scores keep the scan's order, by chunk id.
|
|
94
|
+
const best = (found) => found.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
95
|
+
const indexed = searchIndex(context.database, model, {
|
|
96
|
+
kind: "image",
|
|
97
|
+
account: accountPk,
|
|
98
|
+
...(chatKey === undefined ? {} : { chat: chatKey }),
|
|
99
|
+
...(since === undefined ? {} : { since }),
|
|
100
|
+
}, query, limit, (candidates) => {
|
|
101
|
+
const hashes = new Map(candidates.map(({ id, hash }) => [id, hash]));
|
|
102
|
+
const results = best(imageRows(context, accountPk, filter, { ids: [...hashes.keys()] })
|
|
103
|
+
.filter((row) => hashes.get(row.id) === row.hash)
|
|
104
|
+
.map(score));
|
|
105
|
+
return { results, best: results.map(({ id }) => id) };
|
|
106
|
+
});
|
|
107
|
+
const plain = (found) => found.map(({ chatId, messageId, score }) => ({ chatId, messageId, score }));
|
|
108
|
+
if (indexed)
|
|
109
|
+
return plain(indexed);
|
|
70
110
|
const found = [];
|
|
71
111
|
let after = 0;
|
|
72
112
|
for (;;) {
|
|
73
|
-
const rows =
|
|
74
|
-
FROM chunks k CROSS JOIN attachments a ON a.id = k.chunkable_id AND a.attachable_type = 'message'
|
|
75
|
-
JOIN messages m ON m.id = a.attachable_id AND m.deleted_at IS NULL
|
|
76
|
-
JOIN chats ch ON ch.id = m.chat_id
|
|
77
|
-
JOIN embeddings v ON v.model = ${model} AND v.content_hash = k.content_hash
|
|
78
|
-
WHERE k.chunkable_type = 'image' AND k.position = ${position} AND ch.account_id = ${accountPk}
|
|
79
|
-
AND k.id > ${after}
|
|
80
|
-
${chatKey === undefined ? sql `AND ch.searchable = 1` : sql `AND m.chat_id = ${chatKey}`}
|
|
81
|
-
${since === undefined ? sql `` : sql `AND k.occurred_at >= ${since}`}
|
|
82
|
-
ORDER BY k.id LIMIT ${SCAN_PAGE}`);
|
|
113
|
+
const rows = imageRows(context, accountPk, filter, { after });
|
|
83
114
|
for (const row of rows)
|
|
84
|
-
found.push(
|
|
115
|
+
found.push(score(row));
|
|
85
116
|
const last = rows.at(-1);
|
|
86
117
|
if (!last || rows.length < SCAN_PAGE)
|
|
87
118
|
break;
|
|
88
119
|
after = last.id;
|
|
89
120
|
}
|
|
90
|
-
return
|
|
121
|
+
return plain(best(found));
|
|
91
122
|
};
|
|
92
123
|
/** An image's own vector is its chunk at this position; position 0 is the text around it. */
|
|
93
124
|
export const PICTURE_POSITION = 1;
|
|
@@ -102,24 +133,23 @@ export const picturesToEmbed = ({ orm }, accountPk, { chatKey, after, limit }) =
|
|
|
102
133
|
AND k.position = ${PICTURE_POSITION})
|
|
103
134
|
ORDER BY a.id LIMIT ${limit}`);
|
|
104
135
|
/** Makes each hash the picture chunk of its image; the vectors are saved apart, by `saveVectors`. */
|
|
105
|
-
export const savePictureChunks = ({ orm, now }, accountPk, pictures) => {
|
|
136
|
+
export const savePictureChunks = ({ orm, now, database }, accountPk, pictures) => {
|
|
106
137
|
const at = now();
|
|
107
138
|
const scope = orm.get(sql `SELECT scope FROM accounts WHERE id = ${accountPk}`)?.scope ?? null;
|
|
108
139
|
// A deleted message keeps no trace of its picture, as it keeps none of its text.
|
|
109
140
|
const gone = sql `chunkable_type = 'image' AND position = ${PICTURE_POSITION} AND account_id = ${accountPk}
|
|
110
141
|
AND NOT EXISTS (SELECT 1 FROM attachments a JOIN messages m ON a.attachable_type = 'message'
|
|
111
142
|
AND m.id = a.attachable_id WHERE a.id = chunkable_id AND m.deleted_at IS NULL)`;
|
|
112
|
-
const
|
|
113
|
-
orm.run(sql `DELETE FROM chunks WHERE ${gone}`);
|
|
114
|
-
for (const { hash } of dropped)
|
|
115
|
-
orm.run(sql `DELETE FROM embeddings WHERE content_hash = ${hash}
|
|
116
|
-
AND NOT EXISTS (SELECT 1 FROM chunks WHERE content_hash = ${hash})`);
|
|
143
|
+
const released = orm.all(sql `DELETE FROM chunks WHERE ${gone} RETURNING content_hash AS hash`);
|
|
117
144
|
for (const { attachmentPk, hash, bytes, occurredAt } of pictures) {
|
|
118
|
-
orm.
|
|
119
|
-
AND position = ${PICTURE_POSITION}`);
|
|
145
|
+
const replaced = orm.get(sql `DELETE FROM chunks WHERE chunkable_type = 'image' AND chunkable_id = ${attachmentPk}
|
|
146
|
+
AND position = ${PICTURE_POSITION} RETURNING content_hash AS hash`);
|
|
147
|
+
if (replaced)
|
|
148
|
+
released.push(replaced);
|
|
120
149
|
orm.run(sql `INSERT INTO chunks (chunkable_type, chunkable_id, position, start_offset, end_offset, content_hash, scope,
|
|
121
150
|
account_id, occurred_at, created_at, updated_at)
|
|
122
151
|
VALUES ('image', ${attachmentPk}, ${PICTURE_POSITION}, 0, ${bytes}, ${hash}, ${scope}, ${accountPk},
|
|
123
152
|
${occurredAt}, ${at}, ${at})`);
|
|
124
153
|
}
|
|
154
|
+
dropUnusedVectors(database, released.map(({ hash }) => hash));
|
|
125
155
|
};
|
|
@@ -43,6 +43,8 @@ export const MANIFEST = [
|
|
|
43
43
|
{ name: "20261025071850_version-32-files-data", version: 32, minCompatible: 32 },
|
|
44
44
|
{ name: "20261025072643_version-33-calendars", version: 33, minCompatible: 32 },
|
|
45
45
|
{ name: "20261025072644_version-33-calendars-triggers", version: 33, minCompatible: 32 },
|
|
46
|
+
{ name: "20261025182500_version-34-vector-index", version: 34, minCompatible: 34 },
|
|
47
|
+
{ name: "20261025182501_version-34-vector-index-triggers", version: 34, minCompatible: 34 },
|
|
46
48
|
];
|
|
47
49
|
export const generatedMigrations = (manifest = MANIFEST, generated = GENERATED) => {
|
|
48
50
|
const migrations = [];
|
|
@@ -3,6 +3,7 @@ import { chunkHash, cutChunks } from "../../conversations/chunks.js";
|
|
|
3
3
|
import { positiveReadInteger } from "./meeting-reads.js";
|
|
4
4
|
import { meetingSelectionSql } from "./meeting-selection.js";
|
|
5
5
|
import { read, write } from "./transaction.js";
|
|
6
|
+
import { dropUnusedVectors } from "./vector-index.js";
|
|
6
7
|
import { dot, saveVectors } from "./vectors.js";
|
|
7
8
|
/** Vectors read per query while ranking, so a store of any size stays within a few megabytes at a time. */
|
|
8
9
|
const RANK_BATCH = 512;
|
|
@@ -129,11 +130,13 @@ export const meetingVectorsOver = (context) => ({
|
|
|
129
130
|
text: load(Number(row.id)),
|
|
130
131
|
}));
|
|
131
132
|
let count = 0;
|
|
133
|
+
const released = [];
|
|
132
134
|
for (const { row, text } of prepared) {
|
|
133
135
|
input.signal?.throwIfAborted();
|
|
134
|
-
context.database
|
|
135
|
-
.prepare("DELETE FROM chunks WHERE chunkable_type='meeting_transcript' AND chunkable_id=?")
|
|
136
|
-
.
|
|
136
|
+
for (const { hash } of context.database
|
|
137
|
+
.prepare("DELETE FROM chunks WHERE chunkable_type='meeting_transcript' AND chunkable_id=? RETURNING content_hash AS hash")
|
|
138
|
+
.all(Number(row.id)))
|
|
139
|
+
released.push(String(hash));
|
|
137
140
|
const chunks = cutChunks([{ id: String(row.id), sender: null, text: text.text }], undefined, () => input.signal?.throwIfAborted());
|
|
138
141
|
for (let index = 0; index < chunks.length; index++) {
|
|
139
142
|
const chunk = chunks[index];
|
|
@@ -145,12 +148,11 @@ export const meetingVectorsOver = (context) => ({
|
|
|
145
148
|
count++;
|
|
146
149
|
}
|
|
147
150
|
}
|
|
148
|
-
context.database
|
|
149
|
-
.prepare("DELETE FROM
|
|
150
|
-
.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
.run(input.accountId, input.accountId);
|
|
151
|
+
for (const { hash } of context.database
|
|
152
|
+
.prepare("DELETE FROM chunks WHERE chunkable_type='meeting_transcript' AND account_id=? AND NOT EXISTS (SELECT 1 FROM meeting_transcripts t JOIN meetings m ON m.id=t.meeting_id WHERE t.id=chunks.chunkable_id AND t.deleted_at IS NULL AND t.superseded_at IS NULL AND m.deleted_at IS NULL AND m.account_id=?) RETURNING content_hash AS hash")
|
|
153
|
+
.all(input.accountId, input.accountId))
|
|
154
|
+
released.push(String(hash));
|
|
155
|
+
dropUnusedVectors(context.database, released);
|
|
154
156
|
input.signal?.throwIfAborted();
|
|
155
157
|
const hasMore = found.length > limit;
|
|
156
158
|
return {
|
|
@@ -33,12 +33,15 @@ export interface NearestMessage {
|
|
|
33
33
|
messageId: string;
|
|
34
34
|
score: number;
|
|
35
35
|
}
|
|
36
|
-
|
|
37
|
-
export declare const nearestMessages: ({ orm }: StoreContext, accountPk: number, { chatKey, model, since, before, limit, query, }: {
|
|
36
|
+
type MessageFilter = {
|
|
38
37
|
chatKey?: number;
|
|
39
38
|
model: string;
|
|
40
39
|
since?: number;
|
|
41
40
|
before?: number;
|
|
41
|
+
};
|
|
42
|
+
/** The account's live messages nearest to `query` by their vector of `model`, best first. */
|
|
43
|
+
export declare const nearestMessages: (context: StoreContext, accountPk: number, { limit, query, ...filter }: MessageFilter & {
|
|
42
44
|
limit: number;
|
|
43
45
|
query: Float32Array;
|
|
44
46
|
}) => NearestMessage[];
|
|
47
|
+
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { sql } from "./drizzle/core.js";
|
|
2
|
+
import { dropUnusedVectors, searchIndex } from "./vector-index.js";
|
|
2
3
|
import { dot } from "./vectors.js";
|
|
3
4
|
/** Live messages with text of the account, by pk after `after`; the caller compares hashes. */
|
|
4
5
|
export const messagesToEmbed = ({ orm }, accountPk, { chatKey, model, after, limit }) => orm
|
|
@@ -10,11 +11,6 @@ export const messagesToEmbed = ({ orm }, accountPk, { chatKey, model, after, lim
|
|
|
10
11
|
${chatKey === undefined ? sql `` : sql `AND m.chat_id = ${chatKey}`}
|
|
11
12
|
ORDER BY m.id LIMIT ${limit}`)
|
|
12
13
|
.map((row) => ({ ...row, embedded: row.embedded === 1 }));
|
|
13
|
-
const dropUnshared = ({ orm }, hashes) => {
|
|
14
|
-
for (const hash of hashes)
|
|
15
|
-
orm.run(sql `DELETE FROM embeddings WHERE content_hash = ${hash}
|
|
16
|
-
AND NOT EXISTS (SELECT 1 FROM chunks WHERE content_hash = ${hash})`);
|
|
17
|
-
};
|
|
18
14
|
/** Makes each hash its message's chunk; the vectors are saved apart, by `saveVectors`. */
|
|
19
15
|
export const saveMessageChunks = (context, accountPk, chunks) => {
|
|
20
16
|
const { orm, now } = context;
|
|
@@ -33,7 +29,7 @@ export const saveMessageChunks = (context, accountPk, chunks) => {
|
|
|
33
29
|
account_id, occurred_at, created_at, updated_at)
|
|
34
30
|
VALUES ('message', ${messagePk}, 0, 0, ${length}, ${hash}, ${scope}, ${accountPk}, ${occurredAt}, ${at}, ${at})`);
|
|
35
31
|
}
|
|
36
|
-
|
|
32
|
+
dropUnusedVectors(context.database, old);
|
|
37
33
|
};
|
|
38
34
|
/**
|
|
39
35
|
* Drops the chunks of the account's messages that are gone, deleted or emptied, with the vectors nothing else
|
|
@@ -44,33 +40,61 @@ export const dropGoneMessageChunks = (context, accountPk) => {
|
|
|
44
40
|
WHERE m.id = chunkable_id AND m.deleted_at IS NULL AND trim(m.text) <> '')`;
|
|
45
41
|
const dropped = context.orm.all(sql `SELECT content_hash AS hash FROM chunks WHERE ${gone}`);
|
|
46
42
|
context.orm.run(sql `DELETE FROM chunks WHERE ${gone}`);
|
|
47
|
-
|
|
43
|
+
dropUnusedVectors(context.database, dropped.map(({ hash }) => hash));
|
|
48
44
|
return dropped.length;
|
|
49
45
|
};
|
|
50
46
|
/** Whether any message of the account has a vector of `model`. */
|
|
51
47
|
export const hasMessageVectors = ({ orm }, accountPk, model) => orm.get(sql `SELECT 1 FROM chunks k JOIN embeddings v ON v.model = ${model} AND v.content_hash = k.content_hash
|
|
52
48
|
WHERE k.chunkable_type = 'message' AND k.account_id = ${accountPk} LIMIT 1`) !== undefined;
|
|
53
49
|
const SCAN_PAGE = 5_000;
|
|
54
|
-
/** The
|
|
55
|
-
|
|
50
|
+
/** The scan's rows: one page after `after`, or the `ids` the index picked. */
|
|
51
|
+
const messageRows = ({ orm }, accountPk, { chatKey, model, since, before }, rows) => orm.all(sql `SELECT k.id, k.content_hash AS hash, ch.external_id AS chatId, m.external_id AS messageId, v.vector
|
|
52
|
+
FROM chunks k CROSS JOIN messages m ON m.id = k.chunkable_id AND m.deleted_at IS NULL
|
|
53
|
+
JOIN chats ch ON ch.id = m.chat_id
|
|
54
|
+
JOIN embeddings v ON v.model = ${model} AND v.content_hash = k.content_hash
|
|
55
|
+
WHERE k.chunkable_type = 'message' AND k.position = 0 AND k.account_id = ${accountPk}
|
|
56
|
+
${chatKey === undefined ? sql `AND ch.searchable = 1` : sql `AND m.chat_id = ${chatKey}`}
|
|
57
|
+
${since === undefined ? sql `` : sql `AND m.sent_at >= ${since}`}
|
|
58
|
+
${before === undefined ? sql `` : sql `AND m.sent_at < ${before}`}
|
|
59
|
+
${"ids" in rows
|
|
60
|
+
? sql `AND k.id IN (SELECT value FROM json_each(${JSON.stringify(rows.ids)})) ORDER BY k.id`
|
|
61
|
+
: sql `AND k.id > ${rows.after} ORDER BY k.id LIMIT ${SCAN_PAGE}`}`);
|
|
62
|
+
/** The account's live messages nearest to `query` by their vector of `model`, best first. */
|
|
63
|
+
export const nearestMessages = (context, accountPk, { limit, query, ...filter }) => {
|
|
64
|
+
const score = (row) => ({
|
|
65
|
+
id: row.id,
|
|
66
|
+
chatId: row.chatId,
|
|
67
|
+
messageId: row.messageId,
|
|
68
|
+
score: dot(query, row.vector),
|
|
69
|
+
});
|
|
70
|
+
// Stable, so equal scores keep the scan's order, by chunk id.
|
|
71
|
+
const best = (found) => found.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
72
|
+
const indexed = searchIndex(context.database, filter.model, {
|
|
73
|
+
kind: "message",
|
|
74
|
+
account: accountPk,
|
|
75
|
+
...(filter.chatKey === undefined ? {} : { chat: filter.chatKey }),
|
|
76
|
+
...(filter.since === undefined ? {} : { since: filter.since }),
|
|
77
|
+
...(filter.before === undefined ? {} : { before: filter.before }),
|
|
78
|
+
}, query, limit, (candidates) => {
|
|
79
|
+
const hashes = new Map(candidates.map(({ id, hash }) => [id, hash]));
|
|
80
|
+
const results = best(messageRows(context, accountPk, filter, { ids: [...hashes.keys()] })
|
|
81
|
+
.filter((row) => hashes.get(row.id) === row.hash)
|
|
82
|
+
.map(score));
|
|
83
|
+
return { results, best: results.map(({ id }) => id) };
|
|
84
|
+
});
|
|
85
|
+
const plain = (found) => found.map(({ chatId, messageId, score }) => ({ chatId, messageId, score }));
|
|
86
|
+
if (indexed)
|
|
87
|
+
return plain(indexed);
|
|
56
88
|
const found = [];
|
|
57
89
|
let after = 0;
|
|
58
90
|
for (;;) {
|
|
59
|
-
const rows =
|
|
60
|
-
FROM chunks k CROSS JOIN messages m ON m.id = k.chunkable_id AND m.deleted_at IS NULL
|
|
61
|
-
JOIN chats ch ON ch.id = m.chat_id
|
|
62
|
-
JOIN embeddings v ON v.model = ${model} AND v.content_hash = k.content_hash
|
|
63
|
-
WHERE k.chunkable_type = 'message' AND k.position = 0 AND k.account_id = ${accountPk} AND k.id > ${after}
|
|
64
|
-
${chatKey === undefined ? sql `AND ch.searchable = 1` : sql `AND m.chat_id = ${chatKey}`}
|
|
65
|
-
${since === undefined ? sql `` : sql `AND m.sent_at >= ${since}`}
|
|
66
|
-
${before === undefined ? sql `` : sql `AND m.sent_at < ${before}`}
|
|
67
|
-
ORDER BY k.id LIMIT ${SCAN_PAGE}`);
|
|
91
|
+
const rows = messageRows(context, accountPk, filter, { after });
|
|
68
92
|
for (const row of rows)
|
|
69
|
-
found.push(
|
|
93
|
+
found.push(score(row));
|
|
70
94
|
const last = rows.at(-1);
|
|
71
95
|
if (!last || rows.length < SCAN_PAGE)
|
|
72
96
|
break;
|
|
73
97
|
after = last.id;
|
|
74
98
|
}
|
|
75
|
-
return
|
|
99
|
+
return plain(best(found));
|
|
76
100
|
};
|
|
@@ -739,5 +739,27 @@ export const GENERATED = [
|
|
|
739
739
|
"INSERT OR IGNORE INTO search_index_state (name, watermark, filled_through, terms_through, normalizer_version, built_at, analyzer)\n VALUES ('event_index', 0, 0, 0, 1, NULL, NULL);",
|
|
740
740
|
"INSERT OR IGNORE INTO event_index_pending (indexable_type, id) SELECT 'event', id FROM events;"
|
|
741
741
|
]
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
"name": "20261025182500_version-34-vector-index",
|
|
745
|
+
"statements": [
|
|
746
|
+
"CREATE TABLE `vector_index_redo` (\n\t`chunk_id` integer NOT NULL\n);",
|
|
747
|
+
"CREATE TABLE `vector_indexes` (\n\t`id` integer PRIMARY KEY,\n\t`model` text NOT NULL UNIQUE,\n\t`dims` integer NOT NULL,\n\t`ready` integer NOT NULL,\n\t`filled_through` text NOT NULL\n);"
|
|
748
|
+
]
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
"name": "20261025182501_version-34-vector-index-triggers",
|
|
752
|
+
"statements": [
|
|
753
|
+
"-- Every change that can add, drop or move a chunk's vector passes the chunk's id through vector_index_redo, where\n-- each model's own trigger (made with its vec0 table, `vector-index.ts`) re-derives that row. Writes in TypeScript\n-- and the schema's own delete triggers (document_bd, note_bd, …) all reach the index this way.\n-- A chunk usually comes before its vector; the vector's own trigger indexes it then.\n-- CROSS JOIN: led by the vectors, SQLite read every one of them per chunk (7 ms each at 100k).\nCREATE TRIGGER vector_index_chunk_ai AFTER INSERT ON chunks WHEN EXISTS (SELECT 1 FROM vector_indexes i\n CROSS JOIN embeddings v ON v.model = i.model AND v.content_hash = new.content_hash) BEGIN\n INSERT INTO vector_index_redo (chunk_id) VALUES (new.id);\n DELETE FROM vector_index_redo;\nEND;",
|
|
754
|
+
"CREATE TRIGGER vector_index_chunk_ad AFTER DELETE ON chunks BEGIN\n INSERT INTO vector_index_redo (chunk_id) VALUES (old.id);\n DELETE FROM vector_index_redo;\nEND;",
|
|
755
|
+
"CREATE TRIGGER vector_index_chunk_au AFTER UPDATE OF content_hash, chunkable_type, chunkable_id, account_id, occurred_at\n ON chunks WHEN old.content_hash IS NOT new.content_hash OR old.chunkable_type IS NOT new.chunkable_type\n OR old.chunkable_id IS NOT new.chunkable_id OR old.account_id IS NOT new.account_id\n OR old.occurred_at IS NOT new.occurred_at BEGIN\n INSERT INTO vector_index_redo (chunk_id) VALUES (new.id);\n DELETE FROM vector_index_redo;\nEND;",
|
|
756
|
+
"CREATE TRIGGER vector_index_embedding_ai AFTER INSERT ON embeddings\n WHEN EXISTS (SELECT 1 FROM vector_indexes WHERE model = new.model) BEGIN\n INSERT INTO vector_index_redo (chunk_id) SELECT id FROM chunks WHERE content_hash = new.content_hash;\n DELETE FROM vector_index_redo;\nEND;",
|
|
757
|
+
"CREATE TRIGGER vector_index_embedding_ad AFTER DELETE ON embeddings\n WHEN EXISTS (SELECT 1 FROM vector_indexes WHERE model = old.model) BEGIN\n INSERT INTO vector_index_redo (chunk_id) SELECT id FROM chunks WHERE content_hash = old.content_hash;\n DELETE FROM vector_index_redo;\nEND;",
|
|
758
|
+
"CREATE TRIGGER vector_index_embedding_au AFTER UPDATE OF model, content_hash, vector ON embeddings BEGIN\n INSERT INTO vector_index_redo (chunk_id)\n SELECT id FROM chunks WHERE content_hash = old.content_hash OR content_hash = new.content_hash;\n DELETE FROM vector_index_redo;\nEND;",
|
|
759
|
+
"-- A message's chunk and its images' chunks are indexed under its chat and send time; a chat merge moves messages.\nCREATE TRIGGER vector_index_message_au AFTER UPDATE OF chat_id, sent_at ON messages\n WHEN old.chat_id IS NOT new.chat_id OR old.sent_at IS NOT new.sent_at BEGIN\n INSERT INTO vector_index_redo (chunk_id)\n SELECT id FROM chunks WHERE chunkable_type = 'message' AND chunkable_id = new.id\n UNION ALL SELECT k.id FROM attachments a JOIN chunks k ON k.chunkable_type = 'image' AND k.chunkable_id = a.id\n WHERE a.attachable_type = 'message' AND a.attachable_id = new.id;\n DELETE FROM vector_index_redo;\nEND;",
|
|
760
|
+
"-- An image's chat and account come from its message; a chat merge moves attachments to the kept copy.\nCREATE TRIGGER vector_index_attachment_au AFTER UPDATE OF attachable_type, attachable_id ON attachments\n WHEN old.attachable_type IS NOT new.attachable_type OR old.attachable_id IS NOT new.attachable_id BEGIN\n INSERT INTO vector_index_redo (chunk_id) SELECT id FROM chunks WHERE chunkable_type = 'image' AND chunkable_id = new.id;\n DELETE FROM vector_index_redo;\nEND;",
|
|
761
|
+
"CREATE TRIGGER vector_index_conversation_au AFTER UPDATE OF chat_id ON conversations\n WHEN old.chat_id IS NOT new.chat_id BEGIN\n INSERT INTO vector_index_redo (chunk_id)\n SELECT id FROM chunks WHERE chunkable_type = 'conversation' AND chunkable_id = new.id;\n DELETE FROM vector_index_redo;\nEND;",
|
|
762
|
+
"-- Images and conversations are indexed under their chat's account; messages carry their own.\nCREATE TRIGGER vector_index_chat_au AFTER UPDATE OF account_id ON chats WHEN old.account_id IS NOT new.account_id BEGIN\n INSERT INTO vector_index_redo (chunk_id)\n SELECT k.id FROM messages m JOIN attachments a ON a.attachable_type = 'message' AND a.attachable_id = m.id\n JOIN chunks k ON k.chunkable_type = 'image' AND k.chunkable_id = a.id WHERE m.chat_id = new.id\n UNION ALL SELECT k.id FROM conversations c JOIN chunks k ON k.chunkable_type = 'conversation' AND k.chunkable_id = c.id\n WHERE c.chat_id = new.id;\n DELETE FROM vector_index_redo;\nEND;"
|
|
763
|
+
]
|
|
742
764
|
}
|
|
743
765
|
];
|
|
@@ -8,6 +8,7 @@ import { indexRow } from "./index-state.js";
|
|
|
8
8
|
import { speech } from "./lucene-fields.js";
|
|
9
9
|
import { savedStemmers } from "./stems.js";
|
|
10
10
|
import { write } from "./transaction.js";
|
|
11
|
+
import { dropUnusedVectors } from "./vector-index.js";
|
|
11
12
|
export const CORPORA = {
|
|
12
13
|
document: {
|
|
13
14
|
type: "document",
|
|
@@ -114,8 +115,6 @@ const writer = (database, corpus, stemmer) => {
|
|
|
114
115
|
stems: database.prepare(`INSERT INTO ${corpus.type}_stems (rowid, stems, scope) VALUES (?, ?, ?)`),
|
|
115
116
|
chunk: database.prepare("INSERT INTO chunks (chunkable_type, chunkable_id, position, start_offset, end_offset, content_hash, scope, account_id, created_at, updated_at) " +
|
|
116
117
|
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"),
|
|
117
|
-
purge: database.prepare(`DELETE FROM embeddings WHERE content_hash = ?
|
|
118
|
-
AND NOT EXISTS (SELECT 1 FROM chunks WHERE content_hash = ?)`),
|
|
119
118
|
dequeue: database.prepare(`DELETE FROM ${corpus.type}_index_pending WHERE indexable_type = ? AND id = ?`),
|
|
120
119
|
};
|
|
121
120
|
return (id) => {
|
|
@@ -134,7 +133,6 @@ const writer = (database, corpus, stemmer) => {
|
|
|
134
133
|
const stems = terms ? terms.stems : text ? stemmer.indexText(text) : "";
|
|
135
134
|
if (stems !== "")
|
|
136
135
|
statements.stems.run(id, stems, scope);
|
|
137
|
-
const after = new Set();
|
|
138
136
|
if (text.trim() && corpus.pieces !== false) {
|
|
139
137
|
const at = Date.now();
|
|
140
138
|
const sections = row?.metadata == null ? [] : storedSections(String(row.metadata));
|
|
@@ -143,13 +141,10 @@ const writer = (database, corpus, stemmer) => {
|
|
|
143
141
|
: splitText(text, CHUNK_CHARS);
|
|
144
142
|
ranges.forEach(({ start, end }, position) => {
|
|
145
143
|
const hash = chunkHash(text.slice(start, end));
|
|
146
|
-
after.add(hash);
|
|
147
144
|
statements.chunk.run(corpus.type, id, position, start, end, hash, scope, int(row?.account_id), at, at);
|
|
148
145
|
});
|
|
149
146
|
}
|
|
150
|
-
|
|
151
|
-
if (!after.has(hash))
|
|
152
|
-
statements.purge.run(hash, hash);
|
|
147
|
+
dropUnusedVectors(database, before);
|
|
153
148
|
if (!live && corpus.type === "document")
|
|
154
149
|
dropDocumentPicture(database, id);
|
|
155
150
|
statements.dequeue.run(queuedAs(corpus), id);
|
|
@@ -15,6 +15,7 @@ import { loadProviders } from "./providers.js";
|
|
|
15
15
|
import { assertReadCompatible } from "./read-compatibility.js";
|
|
16
16
|
import { stemsState } from "./stems.js";
|
|
17
17
|
import { fillStems, followDefaultStemmers } from "./stems-fill.js";
|
|
18
|
+
import { fillVectorIndexes } from "./vector-index.js";
|
|
18
19
|
export const BACKFILL_ON_OPEN = 5_000;
|
|
19
20
|
const NOT_WRITABLE = new Set(["EACCES", "EPERM", "EROFS"]);
|
|
20
21
|
const createWritable = (file) => {
|
|
@@ -62,6 +63,9 @@ export const openStoreContext = async ({ path, env, now = Date.now, command, ver
|
|
|
62
63
|
stemming.watermark - stemming.filledThrough + stemming.pending <= BACKFILL_ON_OPEN)
|
|
63
64
|
fillStems(database, { now });
|
|
64
65
|
logPruner(database, now)();
|
|
66
|
+
fillVectorIndexes(database, {
|
|
67
|
+
notice: (model) => warn(`indexing the stored vectors of ${model} for search by meaning — once; a large archive takes minutes`),
|
|
68
|
+
});
|
|
65
69
|
}
|
|
66
70
|
loadProviders(database);
|
|
67
71
|
return { ...opened, now, sharedConfig: sharedConfigPath(file, env ?? process.env) };
|
|
@@ -20,10 +20,9 @@ export const openSqlite = async (path, { readOnly = false } = {}) => {
|
|
|
20
20
|
};
|
|
21
21
|
const openUnderNode = async (path, readOnly) => {
|
|
22
22
|
quietSqliteWarning();
|
|
23
|
-
const { DatabaseSync } = await import("node:sqlite");
|
|
24
23
|
const { drizzle } = await import("./drizzle/node.js");
|
|
25
|
-
const { cacheOverNodeSqlite } = await import("../drivers/node-sqlite.js");
|
|
26
|
-
const client =
|
|
24
|
+
const { cacheOverNodeSqlite, nodeClient } = await import("../drivers/node-sqlite.js");
|
|
25
|
+
const client = await nodeClient(path, { readOnly });
|
|
27
26
|
try {
|
|
28
27
|
return { database: cacheOverNodeSqlite(client), orm: drizzle({ client }) };
|
|
29
28
|
}
|
|
@@ -33,10 +32,9 @@ const openUnderNode = async (path, readOnly) => {
|
|
|
33
32
|
}
|
|
34
33
|
};
|
|
35
34
|
const openUnderBun = async (path, readOnly) => {
|
|
36
|
-
const {
|
|
37
|
-
const Database = await bunDatabase();
|
|
35
|
+
const { bunClient, cacheOverBunSqlite } = await import("../drivers/bun-sqlite.js");
|
|
38
36
|
const { drizzle } = (await import("./drizzle/bun.js"));
|
|
39
|
-
const client =
|
|
37
|
+
const client = await bunClient(path, readOnly ? { readonly: true, create: false } : undefined);
|
|
40
38
|
try {
|
|
41
39
|
return { database: cacheOverBunSqlite(client), orm: drizzle({ client }) };
|
|
42
40
|
}
|
|
@@ -15535,7 +15535,7 @@ export declare const conversationState: import("drizzle-orm/sqlite-core").SQLite
|
|
|
15535
15535
|
dialect: 'sqlite';
|
|
15536
15536
|
isAlias: false;
|
|
15537
15537
|
}>;
|
|
15538
|
-
/** One model's vector for one chunk text; no owner
|
|
15538
|
+
/** One model's vector for one chunk text; no owner: chunks of any kind share it, and it goes once none holds the text. */
|
|
15539
15539
|
export declare const embeddings: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
15540
15540
|
name: "embeddings";
|
|
15541
15541
|
schema: undefined;
|
|
@@ -16637,6 +16637,123 @@ export declare const mentionPending: import("drizzle-orm/sqlite-core").SQLiteTab
|
|
|
16637
16637
|
dialect: 'sqlite';
|
|
16638
16638
|
isAlias: false;
|
|
16639
16639
|
}>;
|
|
16640
|
+
/**
|
|
16641
|
+
* One sqlite-vec `vec0` table per embedding model, `vector_index_<id>`, created at run time with its trigger since
|
|
16642
|
+
* its width is the model's: a row per chunk with a vector of that model, rowid `chunks.id` (`vector-index.ts`).
|
|
16643
|
+
*/
|
|
16644
|
+
export declare const vectorIndexes: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
16645
|
+
name: "vector_indexes";
|
|
16646
|
+
schema: undefined;
|
|
16647
|
+
columns: {
|
|
16648
|
+
id: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
16649
|
+
name: string;
|
|
16650
|
+
tableName: "vector_indexes";
|
|
16651
|
+
dataType: "number int53";
|
|
16652
|
+
data: number;
|
|
16653
|
+
driverParam: number;
|
|
16654
|
+
notNull: true;
|
|
16655
|
+
hasDefault: true;
|
|
16656
|
+
isPrimaryKey: true;
|
|
16657
|
+
isAutoincrement: false;
|
|
16658
|
+
hasRuntimeDefault: false;
|
|
16659
|
+
enumValues: undefined;
|
|
16660
|
+
baseColumn: never;
|
|
16661
|
+
identity: undefined;
|
|
16662
|
+
generated: undefined;
|
|
16663
|
+
}, {}>;
|
|
16664
|
+
model: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
16665
|
+
name: string;
|
|
16666
|
+
tableName: "vector_indexes";
|
|
16667
|
+
dataType: "string";
|
|
16668
|
+
data: string;
|
|
16669
|
+
driverParam: string;
|
|
16670
|
+
notNull: true;
|
|
16671
|
+
hasDefault: false;
|
|
16672
|
+
isPrimaryKey: false;
|
|
16673
|
+
isAutoincrement: false;
|
|
16674
|
+
hasRuntimeDefault: false;
|
|
16675
|
+
enumValues: [string, ...string[]];
|
|
16676
|
+
baseColumn: never;
|
|
16677
|
+
identity: undefined;
|
|
16678
|
+
generated: undefined;
|
|
16679
|
+
}, {}>;
|
|
16680
|
+
dims: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
16681
|
+
name: string;
|
|
16682
|
+
tableName: "vector_indexes";
|
|
16683
|
+
dataType: "number int53";
|
|
16684
|
+
data: number;
|
|
16685
|
+
driverParam: number;
|
|
16686
|
+
notNull: true;
|
|
16687
|
+
hasDefault: false;
|
|
16688
|
+
isPrimaryKey: false;
|
|
16689
|
+
isAutoincrement: false;
|
|
16690
|
+
hasRuntimeDefault: false;
|
|
16691
|
+
enumValues: undefined;
|
|
16692
|
+
baseColumn: never;
|
|
16693
|
+
identity: undefined;
|
|
16694
|
+
generated: undefined;
|
|
16695
|
+
}, {}>;
|
|
16696
|
+
ready: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
16697
|
+
name: string;
|
|
16698
|
+
tableName: "vector_indexes";
|
|
16699
|
+
dataType: "number int53";
|
|
16700
|
+
data: number;
|
|
16701
|
+
driverParam: number;
|
|
16702
|
+
notNull: true;
|
|
16703
|
+
hasDefault: false;
|
|
16704
|
+
isPrimaryKey: false;
|
|
16705
|
+
isAutoincrement: false;
|
|
16706
|
+
hasRuntimeDefault: false;
|
|
16707
|
+
enumValues: undefined;
|
|
16708
|
+
baseColumn: never;
|
|
16709
|
+
identity: undefined;
|
|
16710
|
+
generated: undefined;
|
|
16711
|
+
}, {}>;
|
|
16712
|
+
filledThrough: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
16713
|
+
name: string;
|
|
16714
|
+
tableName: "vector_indexes";
|
|
16715
|
+
dataType: "string";
|
|
16716
|
+
data: string;
|
|
16717
|
+
driverParam: string;
|
|
16718
|
+
notNull: true;
|
|
16719
|
+
hasDefault: false;
|
|
16720
|
+
isPrimaryKey: false;
|
|
16721
|
+
isAutoincrement: false;
|
|
16722
|
+
hasRuntimeDefault: false;
|
|
16723
|
+
enumValues: [string, ...string[]];
|
|
16724
|
+
baseColumn: never;
|
|
16725
|
+
identity: undefined;
|
|
16726
|
+
generated: undefined;
|
|
16727
|
+
}, {}>;
|
|
16728
|
+
};
|
|
16729
|
+
dialect: 'sqlite';
|
|
16730
|
+
isAlias: false;
|
|
16731
|
+
}>;
|
|
16732
|
+
/** Always empty: each changed chunk's id passes through it so that every model's index re-derives its row. */
|
|
16733
|
+
export declare const vectorIndexRedo: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
16734
|
+
name: "vector_index_redo";
|
|
16735
|
+
schema: undefined;
|
|
16736
|
+
columns: {
|
|
16737
|
+
chunkId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
16738
|
+
name: string;
|
|
16739
|
+
tableName: "vector_index_redo";
|
|
16740
|
+
dataType: "number int53";
|
|
16741
|
+
data: number;
|
|
16742
|
+
driverParam: number;
|
|
16743
|
+
notNull: true;
|
|
16744
|
+
hasDefault: false;
|
|
16745
|
+
isPrimaryKey: false;
|
|
16746
|
+
isAutoincrement: false;
|
|
16747
|
+
hasRuntimeDefault: false;
|
|
16748
|
+
enumValues: undefined;
|
|
16749
|
+
baseColumn: never;
|
|
16750
|
+
identity: undefined;
|
|
16751
|
+
generated: undefined;
|
|
16752
|
+
}, {}>;
|
|
16753
|
+
};
|
|
16754
|
+
dialect: 'sqlite';
|
|
16755
|
+
isAlias: false;
|
|
16756
|
+
}>;
|
|
16640
16757
|
/** Settings of the store file itself, shared by every profile, tg and MAX — unlike a profile's config file. */
|
|
16641
16758
|
export declare const storeSettings: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
16642
16759
|
name: "store_settings";
|
|
@@ -1556,7 +1556,7 @@ export const conversationState = sqliteTable("conversation_state", {
|
|
|
1556
1556
|
/** The build readers see; a higher one is being written, or failed. */
|
|
1557
1557
|
currentBuild: integer("current_build"),
|
|
1558
1558
|
});
|
|
1559
|
-
/** One model's vector for one chunk text; no owner
|
|
1559
|
+
/** One model's vector for one chunk text; no owner: chunks of any kind share it, and it goes once none holds the text. */
|
|
1560
1560
|
export const embeddings = sqliteTable("embeddings", {
|
|
1561
1561
|
/** `<provider>:<model>:<dims>` — vectors of different models never mix. */
|
|
1562
1562
|
model: text("model").notNull(),
|
|
@@ -1684,6 +1684,23 @@ export const mentionPending = sqliteTable("mention_pending", {
|
|
|
1684
1684
|
sourceType: text("source_type").notNull(),
|
|
1685
1685
|
sourceId: integer("source_id").notNull(),
|
|
1686
1686
|
}, (table) => [primaryKey({ columns: [table.sourceType, table.sourceId] })]);
|
|
1687
|
+
/**
|
|
1688
|
+
* One sqlite-vec `vec0` table per embedding model, `vector_index_<id>`, created at run time with its trigger since
|
|
1689
|
+
* its width is the model's: a row per chunk with a vector of that model, rowid `chunks.id` (`vector-index.ts`).
|
|
1690
|
+
*/
|
|
1691
|
+
export const vectorIndexes = sqliteTable("vector_indexes", {
|
|
1692
|
+
id: integer("id").primaryKey(),
|
|
1693
|
+
model: text("model").notNull().unique(),
|
|
1694
|
+
dims: integer("dims").notNull(),
|
|
1695
|
+
/** 1 once it holds every vector; until then searches scan. */
|
|
1696
|
+
ready: integer("ready").notNull(),
|
|
1697
|
+
/** The last `content_hash` the fill of older vectors reached. */
|
|
1698
|
+
filledThrough: text("filled_through").notNull(),
|
|
1699
|
+
});
|
|
1700
|
+
/** Always empty: each changed chunk's id passes through it so that every model's index re-derives its row. */
|
|
1701
|
+
export const vectorIndexRedo = sqliteTable("vector_index_redo", {
|
|
1702
|
+
chunkId: integer("chunk_id").notNull(),
|
|
1703
|
+
});
|
|
1687
1704
|
/** Settings of the store file itself, shared by every profile, tg and MAX — unlike a profile's config file. */
|
|
1688
1705
|
export const storeSettings = sqliteTable("store_settings", {
|
|
1689
1706
|
key: text("key").primaryKey(),
|