@wirecat/cli-core 0.500.0 → 0.501.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 +28 -0
- package/dist/calendar/mcp/index.d.ts +12 -12
- package/dist/cli/envelope.d.ts +11 -1
- package/dist/cli/envelope.js +15 -3
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/meeting-preparation-answer.d.ts +8 -0
- package/dist/cli/meeting-preparation-projects.d.ts +8 -0
- package/dist/cli/meeting-preparation-read.d.ts +8 -0
- package/dist/cli/meeting-series-inventory-read.d.ts +8 -0
- package/dist/cli/messenger/messages-search-command.js +4 -1
- package/dist/cli/messenger/search-answers.d.ts +3 -0
- package/dist/cli/messenger/search-answers.js +6 -0
- package/dist/cli/messenger/search-command.js +4 -1
- package/dist/entities/index.d.ts +24 -0
- package/dist/entities/meeting-preparation.d.ts +8 -0
- package/dist/entities/meeting-series-inventory.d.ts +8 -0
- package/dist/mcp/meeting-preparation.d.ts +8 -0
- package/dist/mcp/meeting-series-inventory.d.ts +8 -0
- package/dist/meetings/mcp/index.d.ts +15 -15
- package/dist/meetings/project-evidence-answer.d.ts +8 -0
- package/dist/search/total.d.ts +13 -0
- package/dist/search/total.js +14 -0
- package/dist/services/index.d.ts +2 -1
- package/dist/services/index.js +2 -1
- package/dist/services/messages-archive-search.js +13 -3
- package/dist/services/messages-discovery.d.ts +6 -0
- package/dist/services/messages-discovery.js +14 -11
- package/dist/services/messages-search.d.ts +5 -0
- package/dist/services/messages-search.js +22 -0
- package/dist/services/messages.d.ts +3 -0
- package/dist/services/notes-search.d.ts +14 -2
- package/dist/services/notes-search.js +101 -19
- package/dist/store/queries.d.ts +2 -0
- package/dist/store/queries.js +1 -0
- package/dist/store/sqlite/lucene.d.ts +5 -0
- package/dist/store/sqlite/lucene.js +17 -0
- package/dist/store/sqlite/note-search.d.ts +18 -0
- package/dist/store/sqlite/note-search.js +40 -4
- package/package.json +3 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CliError } from "@wirecat/cli-base";
|
|
1
|
+
import { CliError, isCliError } from "@wirecat/cli-base";
|
|
2
2
|
import { dateRange, parseLucene, timezoneOf } from "@wirecat/cli-base/query";
|
|
3
3
|
import { INTERNAL_NOTES } from "../domain/providers.js";
|
|
4
4
|
import { normalizeTag } from "../domain/tags.js";
|
|
@@ -10,6 +10,7 @@ import { validateFields } from "../search/lucene/registry.js";
|
|
|
10
10
|
import { hasStems } from "../search/lucene/resolved.js";
|
|
11
11
|
import { SEARCH_QUERY } from "../search/lucene/types.js";
|
|
12
12
|
import { createStemmer, DEFAULT_STEMMERS } from "../search/stem.js";
|
|
13
|
+
import { TOTAL_CAP, totalOf } from "../search/total.js";
|
|
13
14
|
import { openPictureEmbedderFor } from "./document-pictures.js";
|
|
14
15
|
import { documentSource } from "./document-source.js";
|
|
15
16
|
import { plainQueryWords, titleQuery, titleTiers } from "./notes-title-tier.js";
|
|
@@ -122,7 +123,17 @@ export const embedEvents = (store, options = {}) => embedChunks(store, store.cal
|
|
|
122
123
|
/** Embeds mail's chunks as notes' are embedded: same model and key, so search by meaning reads both. */
|
|
123
124
|
export const embedMail = (store, options = {}) => embedChunks(store, store.mail, options);
|
|
124
125
|
/** Notes nearest in meaning to the query, best first; only notes already embedded can be found. */
|
|
125
|
-
export const nearestNotes = async (store, query,
|
|
126
|
+
export const nearestNotes = async (store, query, options) => {
|
|
127
|
+
const { key, vector, floor } = await queryVector(store, query, options);
|
|
128
|
+
const nearest = await store.notes.nearest(key, vector, {
|
|
129
|
+
limit: options.limit,
|
|
130
|
+
...(options.folderIds ? { folderIds: options.folderIds } : {}),
|
|
131
|
+
...(options.withInternal ? { withInternal: true } : {}),
|
|
132
|
+
...(options.source ? { source: options.source } : {}),
|
|
133
|
+
});
|
|
134
|
+
return nearest.filter(({ score }) => score > floor);
|
|
135
|
+
};
|
|
136
|
+
const queryVector = async (store, query, { model: choice, folderIds, withInternal, source, env, threads, command, embedders }) => {
|
|
126
137
|
if (!query.trim())
|
|
127
138
|
throw new CliError("validation_error", "say what to find in the notes");
|
|
128
139
|
const { model, directory, key } = localModel(choice, env, command);
|
|
@@ -134,14 +145,12 @@ export const nearestNotes = async (store, query, { model: choice, limit, folderI
|
|
|
134
145
|
if (!(await store.notes.embedded(key, scope)))
|
|
135
146
|
throw new CliError("not_found", `nothing searched here has vectors of ${model.id} yet, so only words were matched`);
|
|
136
147
|
const [vector] = await withLocal(model, directory, threads, (embedder) => embedder.embed([query], "query"), embedders);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
});
|
|
144
|
-
return nearest.filter(({ score }) => score > floor);
|
|
148
|
+
return { key, vector: vector, floor: meaningFloor(key) };
|
|
149
|
+
};
|
|
150
|
+
/** Every note of `refs` nearer in meaning than the model's floor, best first. */
|
|
151
|
+
const nearestAmong = async (store, query, refs, options) => {
|
|
152
|
+
const { key, vector, floor } = await queryVector(store, query, options);
|
|
153
|
+
return (await store.notes.scoreNotes(key, vector, refs)).filter(({ score }) => score > floor);
|
|
145
154
|
};
|
|
146
155
|
const MAX_LINE = 200;
|
|
147
156
|
const MAX_NOTES_OFFSET = 1000;
|
|
@@ -289,6 +298,62 @@ const matching = async (store, refs, root, scope) => {
|
|
|
289
298
|
});
|
|
290
299
|
return new Map(found.items.map(({ ref, note, at }) => [ref, { note, at }]));
|
|
291
300
|
};
|
|
301
|
+
/**
|
|
302
|
+
* The most notes a filter may hold for meaning to rank all of them, each by its best chunk. Above it, meaning ranks
|
|
303
|
+
* the nearest notes overall and keeps those inside the filter, so a narrow filter over a large store loses some. On
|
|
304
|
+
* the owner's store (1,454 documents, 23,308 chunks) scoring all of them took ~0.5 s against ~0.17 s for the
|
|
305
|
+
* nearest 50 overall, and the cost grows with chunks, so 2,000 notes keeps it under a second at that density.
|
|
306
|
+
*/
|
|
307
|
+
export const RANK_WITHIN = 2000;
|
|
308
|
+
/** The notes of a filter, newest first, when there are at most `RANK_WITHIN`; `undefined` when more, or too costly. */
|
|
309
|
+
const filterRefs = async (store, narrowing, scope) => {
|
|
310
|
+
const resolved = await resolvedQuery(store, narrowing, scope.timezone);
|
|
311
|
+
try {
|
|
312
|
+
const found = await store.notes.matchingRefs({
|
|
313
|
+
root: resolved.root,
|
|
314
|
+
limit: RANK_WITHIN,
|
|
315
|
+
...(scope.folderIds ? { folderIds: scope.folderIds } : {}),
|
|
316
|
+
...(scope.withInternal ? { withInternal: true } : {}),
|
|
317
|
+
...(scope.source ? { source: scope.source } : {}),
|
|
318
|
+
...(resolved.stemmer ? { stemmer: resolved.stemmer } : {}),
|
|
319
|
+
});
|
|
320
|
+
return found.hasMore ? undefined : found.refs;
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
if (tooLarge(error))
|
|
324
|
+
return undefined;
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
const tooLarge = (error) => isCliError(error) && error.details.reason === "query.too-large";
|
|
329
|
+
/**
|
|
330
|
+
* The notes the words find, with those found by meaning: every note the search would list, paged to the end. Never
|
|
331
|
+
* below what the page already showed; a count the store refuses as too costly is that lower bound.
|
|
332
|
+
*/
|
|
333
|
+
const notesTotal = async (store, text, { exact, scope, shown, hasMore, }, others, othersComplete) => {
|
|
334
|
+
let words;
|
|
335
|
+
try {
|
|
336
|
+
const { root, stemmer } = await storeQuery(store, text, {
|
|
337
|
+
exact,
|
|
338
|
+
...(scope.timezone ? { timezone: scope.timezone } : {}),
|
|
339
|
+
});
|
|
340
|
+
words = await store.notes.matchingRefs({
|
|
341
|
+
root,
|
|
342
|
+
limit: TOTAL_CAP,
|
|
343
|
+
...(scope.folderIds ? { folderIds: scope.folderIds } : {}),
|
|
344
|
+
...(scope.withInternal ? { withInternal: true } : {}),
|
|
345
|
+
...(scope.source ? { source: scope.source } : {}),
|
|
346
|
+
...(stemmer ? { stemmer } : {}),
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
if (tooLarge(error))
|
|
351
|
+
return totalOf(undefined, shown, hasMore);
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
const count = new Set([...words.refs, ...others]).size + (words.hasMore ? 1 : 0);
|
|
355
|
+
return totalOf(count, shown, hasMore, othersComplete);
|
|
356
|
+
};
|
|
292
357
|
/**
|
|
293
358
|
* `search notes`: words and stems, and meaning when the local model is there, merged by reciprocal rank;
|
|
294
359
|
* each hit says which half found it, and the answer lists what the notes found link to. Every field of the query and
|
|
@@ -317,24 +382,39 @@ export const searchNotes = async (store, query, { limit = 20, offset = 0, tag, f
|
|
|
317
382
|
};
|
|
318
383
|
let meaningSkipped;
|
|
319
384
|
let meaning;
|
|
385
|
+
/** Every note found by meaning; `meaningComplete` is false when the nearest overall were cut at the depth. */
|
|
386
|
+
let meaningRefs;
|
|
387
|
+
let meaningComplete = true;
|
|
320
388
|
if (exact)
|
|
321
389
|
meaningSkipped = "--exact searches words as written";
|
|
322
390
|
else if (words) {
|
|
391
|
+
const lane = {
|
|
392
|
+
command,
|
|
393
|
+
...(env === undefined ? {} : { env }),
|
|
394
|
+
...(embedders === undefined ? {} : { embedders }),
|
|
395
|
+
...(folderIds === undefined ? {} : { folderIds }),
|
|
396
|
+
...(withInternal ? { withInternal: true } : {}),
|
|
397
|
+
...(source === undefined ? {} : { source }),
|
|
398
|
+
};
|
|
399
|
+
const depth = Math.max(window, CANDIDATES);
|
|
323
400
|
try {
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
401
|
+
const within = narrowing === undefined ? undefined : await filterRefs(store, narrowing, scope);
|
|
402
|
+
let nearest;
|
|
403
|
+
if (within === undefined) {
|
|
404
|
+
nearest = await nearestNotes(store, words, { ...lane, limit: depth });
|
|
405
|
+
meaningComplete = nearest.length < depth;
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
const ranked = within.length ? await nearestAmong(store, words, within, lane) : [];
|
|
409
|
+
meaningRefs = new Set(ranked.map(({ ref }) => ref));
|
|
410
|
+
nearest = ranked.slice(0, depth);
|
|
411
|
+
}
|
|
333
412
|
const allowed = await matching(store, nearest.map(({ ref }) => ref), narrowing, scope);
|
|
334
413
|
meaning = nearest.flatMap(({ ref }) => {
|
|
335
414
|
const found = allowed.get(ref);
|
|
336
415
|
return found ? [{ ref, ...found }] : [];
|
|
337
416
|
});
|
|
417
|
+
meaningRefs ??= new Set(meaning.map(({ ref }) => ref));
|
|
338
418
|
}
|
|
339
419
|
catch (error) {
|
|
340
420
|
if (!(error instanceof CliError && error.code === "not_found"))
|
|
@@ -378,6 +458,7 @@ export const searchNotes = async (store, query, { limit = 20, offset = 0, tag, f
|
|
|
378
458
|
];
|
|
379
459
|
const page = slice ? ordered : ordered.slice(offset, window);
|
|
380
460
|
const hasMore = slice ? matched.hasMore : ordered.length > window || matched.hasMore || titled.hasMore;
|
|
461
|
+
const total = await notesTotal(store, text, { exact, scope, shown: offset + page.length, hasMore }, [...tier.map(({ ref }) => ref), ...(meaningRefs ?? [])], meaningComplete);
|
|
381
462
|
const hits = [];
|
|
382
463
|
const counts = new Map();
|
|
383
464
|
const accounts = await store.accounts.storedAccounts();
|
|
@@ -441,6 +522,7 @@ export const searchNotes = async (store, query, { limit = 20, offset = 0, tag, f
|
|
|
441
522
|
by: meaning === undefined ? "words" : "words and meaning",
|
|
442
523
|
...(meaningSkipped === undefined ? {} : { meaning_skipped: meaningSkipped }),
|
|
443
524
|
hits,
|
|
525
|
+
total,
|
|
444
526
|
has_more: hasMore,
|
|
445
527
|
...(hasMore && window <= MAX_NOTES_OFFSET ? { next_offset: window } : {}),
|
|
446
528
|
linked: [...counts.values()].sort((a, b) => b.notes - a.notes || (a.name ?? a.ref ?? "").localeCompare(b.name ?? b.ref ?? "")),
|
package/dist/store/queries.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface QueriesStore {
|
|
|
13
13
|
matchQuery(execution: QueryExecution): Promise<Page<ScoredHit>>;
|
|
14
14
|
/** The same matches as `matchQuery`, each counted once, grouped by chat, sender or quarter hour. */
|
|
15
15
|
countQuery(execution: QueryExecution, by: QueryGrouping): Promise<QueryGroup[]>;
|
|
16
|
+
/** How many messages match, counted no further than `cap + 1`; `undefined` when only a full search could tell. */
|
|
17
|
+
countMatches(execution: QueryExecution, cap: number): Promise<number | undefined>;
|
|
16
18
|
conversationEligibility(execution: QueryExecution): Promise<ConversationEligibility>;
|
|
17
19
|
rankQuery(execution: QueryExecution, request: RankingRequest): Promise<RankedStoreFound>;
|
|
18
20
|
rankingEvidence(execution: QueryExecution, request: RankingEvidenceRequest): Promise<RankedEvidence>;
|
package/dist/store/queries.js
CHANGED
|
@@ -13,6 +13,7 @@ export const queriesStoreOver = (context, stemmerFor) => {
|
|
|
13
13
|
return {
|
|
14
14
|
matchQuery: async (execution) => lucene.matchQuery(context, mailIndexed(execution)),
|
|
15
15
|
countQuery: async (execution, by) => lucene.countQuery(context, mailIndexed(execution), by),
|
|
16
|
+
countMatches: async (execution, cap) => lucene.countMatches(context, mailIndexed(execution), cap),
|
|
16
17
|
conversationEligibility: async (execution) => conversationEligibility(context, execution),
|
|
17
18
|
rankQuery: async (execution, request) => rankQuery(context, execution, request),
|
|
18
19
|
rankingEvidence: async (execution, request) => rankingEvidence(context, execution, request),
|
|
@@ -18,6 +18,11 @@ export declare const matchQuery: (context: StoreContext, execution: QueryExecuti
|
|
|
18
18
|
export declare const countQuery: (context: StoreContext, execution: QueryExecution, by: QueryGrouping) => Promise<QueryGroup[]>;
|
|
19
19
|
/** All matching message keys, without materializing message bodies or imposing a hit-page limit. */
|
|
20
20
|
export declare const queryMessagePks: (context: StoreContext, execution: QueryExecution) => Promise<number[]>;
|
|
21
|
+
/**
|
|
22
|
+
* How many messages match, counting no further than `cap + 1`: one SQL count, unranked. `undefined` when a leaf is
|
|
23
|
+
* checked row by row (a preset, a pattern), where a count would cost as much as the search itself.
|
|
24
|
+
*/
|
|
25
|
+
export declare const countMatches: (context: StoreContext, execution: QueryExecution, cap: number) => number | undefined;
|
|
21
26
|
interface QuerySelection {
|
|
22
27
|
sql: string;
|
|
23
28
|
params: SqlValue[];
|
|
@@ -48,6 +48,23 @@ export const matchQuery = (context, execution) => runQuery(context, execution);
|
|
|
48
48
|
export const countQuery = (context, execution, by) => runQuery(context, execution, by);
|
|
49
49
|
/** All matching message keys, without materializing message bodies or imposing a hit-page limit. */
|
|
50
50
|
export const queryMessagePks = (context, execution) => runQuery(context, execution, "pks");
|
|
51
|
+
/**
|
|
52
|
+
* How many messages match, counting no further than `cap + 1`: one SQL count, unranked. `undefined` when a leaf is
|
|
53
|
+
* checked row by row (a preset, a pattern), where a count would cost as much as the search itself.
|
|
54
|
+
*/
|
|
55
|
+
export const countMatches = (context, execution, cap) => {
|
|
56
|
+
const { database, check, where, from, withRanking, ranked, leaves } = compileQuery(context, {
|
|
57
|
+
...execution,
|
|
58
|
+
newest: true,
|
|
59
|
+
});
|
|
60
|
+
if (leaves.some(({ test }) => test))
|
|
61
|
+
return undefined;
|
|
62
|
+
const row = database
|
|
63
|
+
.prepare(`SELECT count(*) AS n FROM (${withRanking}SELECT m.id ${from} WHERE ${where.sql} LIMIT ?)`)
|
|
64
|
+
.get(...ranked.params, ...where.params, cap + 1);
|
|
65
|
+
check();
|
|
66
|
+
return Number(row?.n ?? 0);
|
|
67
|
+
};
|
|
51
68
|
const compileQuery = (context, execution, boundedAttachments = false) => {
|
|
52
69
|
const { database } = context;
|
|
53
70
|
const corpus = execution.corpus === "mail" ? MAIL : MESSAGES;
|
|
@@ -79,6 +79,11 @@ export declare const searchNotes: (context: StoreContext, query: NoteQuery) => {
|
|
|
79
79
|
items: NoteHit[];
|
|
80
80
|
hasMore: boolean;
|
|
81
81
|
};
|
|
82
|
+
/** The refs of the newest `limit` notes matching a query, without reading the notes themselves. */
|
|
83
|
+
export declare const matchingNoteRefs: (context: StoreContext, query: Omit<NoteQuery, "offset" | "newest">) => {
|
|
84
|
+
refs: string[];
|
|
85
|
+
hasMore: boolean;
|
|
86
|
+
};
|
|
82
87
|
export declare const searchCorpus: (context: StoreContext, query: CorpusQuery, corpus: Corpus) => CorpusRow[];
|
|
83
88
|
export interface NoteChunkToEmbed {
|
|
84
89
|
hash: string;
|
|
@@ -101,6 +106,11 @@ export interface NoteSearch extends DocumentPictureStore {
|
|
|
101
106
|
items: NoteHit[];
|
|
102
107
|
hasMore: boolean;
|
|
103
108
|
}>;
|
|
109
|
+
/** The refs of the newest `limit` notes matching the query; `hasMore` when more match. */
|
|
110
|
+
matchingRefs(query: Omit<NoteQuery, "offset" | "newest">): Promise<{
|
|
111
|
+
refs: string[];
|
|
112
|
+
hasMore: boolean;
|
|
113
|
+
}>;
|
|
104
114
|
indexState(): Promise<NoteIndexState | undefined>;
|
|
105
115
|
/** Live notes' chunks with no vector of `model`, by hash from `after`, each with its text cut again. */
|
|
106
116
|
chunksToEmbed(model: string, options: {
|
|
@@ -120,6 +130,14 @@ export interface NoteSearch extends DocumentPictureStore {
|
|
|
120
130
|
withInternal?: boolean;
|
|
121
131
|
source?: Note["source"];
|
|
122
132
|
}): Promise<NearestNote[]>;
|
|
133
|
+
/**
|
|
134
|
+
* Each of `refs` scored by its best chunk, best first, every chunk compared: a filter's own notes ranked, not the
|
|
135
|
+
* nearest overall. A note with no vector of `model` is left out; the notes themselves are not read.
|
|
136
|
+
*/
|
|
137
|
+
scoreNotes(model: string, query: Float32Array, refs: string[]): Promise<{
|
|
138
|
+
ref: string;
|
|
139
|
+
score: number;
|
|
140
|
+
}[]>;
|
|
123
141
|
}
|
|
124
142
|
export declare const noteSearchOver: (context: StoreContext) => NoteSearch;
|
|
125
143
|
export {};
|
|
@@ -63,9 +63,7 @@ const CORPORA = [
|
|
|
63
63
|
tagged: (tag) => bound("n.id IN (SELECT g.taggable_id FROM taggings g JOIN resolved_tags t ON t.id=g.tag_id WHERE t.name = ? AND g.taggable_type = 'note')", tag),
|
|
64
64
|
},
|
|
65
65
|
];
|
|
66
|
-
|
|
67
|
-
export const searchNotes = (context, query) => {
|
|
68
|
-
const offset = query.offset ?? 0;
|
|
66
|
+
const matchedRows = (context, query) => {
|
|
69
67
|
const chosen = CORPORA.filter(({ type }) => (query.source === undefined || (query.source === "file") === (type === "document")) &&
|
|
70
68
|
(!query.folderIds?.length || query.withInternal === true || type === "document"));
|
|
71
69
|
const idsOf = (type) => query.refs?.flatMap((ref) => (ref.startsWith(`${type}:`) ? [Number(ref.slice(type.length + 1))] : []));
|
|
@@ -83,6 +81,12 @@ export const searchNotes = (context, query) => {
|
|
|
83
81
|
(a.relevance ?? 0) - (b.relevance ?? 0) ||
|
|
84
82
|
(b.at ?? 0) - (a.at ?? 0) ||
|
|
85
83
|
a.id - b.id);
|
|
84
|
+
return rows;
|
|
85
|
+
};
|
|
86
|
+
/** Notes matching a parsed query, by the same language as messages: files and written notes, merged. */
|
|
87
|
+
export const searchNotes = (context, query) => {
|
|
88
|
+
const offset = query.offset ?? 0;
|
|
89
|
+
const rows = matchedRows(context, query);
|
|
86
90
|
const read = {
|
|
87
91
|
document: context.database.prepare("SELECT * FROM documents WHERE id = ?"),
|
|
88
92
|
note: context.database.prepare("SELECT * FROM notes WHERE id = ?"),
|
|
@@ -102,6 +106,11 @@ export const searchNotes = (context, query) => {
|
|
|
102
106
|
hasMore: rows.length > offset + query.limit,
|
|
103
107
|
};
|
|
104
108
|
};
|
|
109
|
+
/** The refs of the newest `limit` notes matching a query, without reading the notes themselves. */
|
|
110
|
+
export const matchingNoteRefs = (context, query) => {
|
|
111
|
+
const rows = matchedRows(context, { ...query, newest: true });
|
|
112
|
+
return { refs: rows.slice(0, query.limit).map(({ type, id }) => `${type}:${id}`), hasMore: rows.length > query.limit };
|
|
113
|
+
};
|
|
105
114
|
export const searchCorpus = (context, query, corpus) => {
|
|
106
115
|
const { database } = context;
|
|
107
116
|
const wordMatch = (match) => ({
|
|
@@ -282,7 +291,7 @@ export const searchCorpus = (context, query, corpus) => {
|
|
|
282
291
|
: "";
|
|
283
292
|
const window = tests.size ? QUERY_LIMITS.candidates + 1 : query.limit + offset + 1;
|
|
284
293
|
const rows = database
|
|
285
|
-
.prepare(`${ctes.length ? `WITH ${ctes.join(", ")} ` : ""}SELECT n
|
|
294
|
+
.prepare(`${ctes.length ? `WITH ${ctes.join(", ")} ` : ""}SELECT ${tests.size ? "n.*" : "n.id, n.at"}, ${ranking ? "f.rank" : "NULL"} AS relevance${exactTier ? ", n.id IN (SELECT id FROM x) AS exact" : ""}${projection}
|
|
286
295
|
FROM ${corpus.view} n${ranking ? " LEFT JOIN f ON f.id = n.id" : ""} WHERE ${where.sql} ORDER BY ${order} LIMIT ?`)
|
|
287
296
|
.all(...cteParams, ...leaves.flatMap(([, fragment]) => (tests.size ? fragment.params : [])), ...where.params, window);
|
|
288
297
|
if (tests.size && rows.length > QUERY_LIMITS.candidates)
|
|
@@ -346,6 +355,10 @@ export const noteSearchOver = (context) => {
|
|
|
346
355
|
drain();
|
|
347
356
|
return searchNotes(context, query);
|
|
348
357
|
},
|
|
358
|
+
matchingRefs: async (query) => {
|
|
359
|
+
drain();
|
|
360
|
+
return matchingNoteRefs(context, query);
|
|
361
|
+
},
|
|
349
362
|
indexState: async () => noteIndexState(database),
|
|
350
363
|
chunksToEmbed: async (model, { after, limit }) => {
|
|
351
364
|
drain();
|
|
@@ -456,5 +469,28 @@ export const noteSearchOver = (context) => {
|
|
|
456
469
|
return note ? [{ ref: note.ref, note, score, range: { start, end } }] : [];
|
|
457
470
|
});
|
|
458
471
|
},
|
|
472
|
+
scoreNotes: async (model, query, refs) => {
|
|
473
|
+
drain();
|
|
474
|
+
const scope = live(undefined, undefined, true);
|
|
475
|
+
const owned = database.prepare(`SELECT k.chunkable_type AS type, k.chunkable_id AS owner, v.vector
|
|
476
|
+
FROM chunks k JOIN embeddings v ON v.model = ? AND v.content_hash = k.content_hash
|
|
477
|
+
WHERE k.chunkable_type = ? AND k.chunkable_id IN (SELECT value FROM json_each(?))${scope.sql}`);
|
|
478
|
+
const best = new Map();
|
|
479
|
+
for (const type of ["document", "note"]) {
|
|
480
|
+
const ids = refs.flatMap((ref) => (ref.startsWith(`${type}:`) ? [Number(ref.slice(type.length + 1))] : []));
|
|
481
|
+
if (!ids.length)
|
|
482
|
+
continue;
|
|
483
|
+
for (const row of owned.all(model, type, JSON.stringify(ids), ...scope.params)) {
|
|
484
|
+
const key = `${row.type}:${row.owner}`;
|
|
485
|
+
const score = dot(query, row.vector);
|
|
486
|
+
if (score > (best.get(key) ?? Number.NEGATIVE_INFINITY))
|
|
487
|
+
best.set(key, score);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const order = new Map(refs.map((ref, index) => [ref, index]));
|
|
491
|
+
return [...best]
|
|
492
|
+
.map(([ref, score]) => ({ ref, score }))
|
|
493
|
+
.sort((a, b) => b.score - a.score || (order.get(a.ref) ?? 0) - (order.get(b.ref) ?? 0));
|
|
494
|
+
},
|
|
459
495
|
};
|
|
460
496
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.501.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|
|
@@ -247,7 +247,7 @@
|
|
|
247
247
|
"@types/node": "^22.20.5",
|
|
248
248
|
"@types/pngjs": "6.0.5",
|
|
249
249
|
"@vitest/coverage-v8": "^5.0.5",
|
|
250
|
-
"@wirecat/cli-base": "0.
|
|
250
|
+
"@wirecat/cli-base": "0.67.0",
|
|
251
251
|
"@wirecat/cli-core-legacy-tags": "npm:@wirecat/cli-core@0.414.0",
|
|
252
252
|
"commander": "15.0.0",
|
|
253
253
|
"cspell": "10.4.0",
|
|
@@ -267,7 +267,7 @@
|
|
|
267
267
|
},
|
|
268
268
|
"peerDependencies": {
|
|
269
269
|
"@napi-rs/canvas": ">=0.1.69 <2",
|
|
270
|
-
"@wirecat/cli-base": ">=0.
|
|
270
|
+
"@wirecat/cli-base": ">=0.67.0 <0.68.0",
|
|
271
271
|
"commander": ">=15"
|
|
272
272
|
},
|
|
273
273
|
"bin": {
|