@zenera/rag 1.1.9 → 1.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/README.md +195 -10
- package/dist/command.js +2 -1
- package/dist/common/cache.d.ts +66 -0
- package/dist/common/cache.js +172 -0
- package/dist/common/embedder.d.ts +11 -1
- package/dist/common/embedder.js +6 -17
- package/dist/common/manifest.d.ts +7 -1
- package/dist/common/progress.d.ts +16 -1
- package/dist/common/progress.js +35 -3
- package/dist/common/prose.d.ts +7 -0
- package/dist/common/prose.js +13 -0
- package/dist/docs/assemble.d.ts +52 -0
- package/dist/docs/assemble.js +127 -0
- package/dist/docs/build.d.ts +54 -0
- package/dist/docs/build.js +134 -0
- package/dist/docs/chunk.d.ts +73 -0
- package/dist/docs/chunk.js +586 -0
- package/dist/docs/command.d.ts +3 -0
- package/dist/docs/command.js +575 -0
- package/dist/docs/files.d.ts +94 -0
- package/dist/docs/files.js +80 -0
- package/dist/docs/index.d.ts +13 -0
- package/dist/docs/index.js +13 -0
- package/dist/docs/load.d.ts +42 -0
- package/dist/docs/load.js +203 -0
- package/dist/docs/lookup.d.ts +80 -0
- package/dist/docs/lookup.js +147 -0
- package/dist/docs/outline.d.ts +11 -0
- package/dist/docs/outline.js +55 -0
- package/dist/docs/parse-cache.d.ts +20 -0
- package/dist/docs/parse-cache.js +60 -0
- package/dist/docs/parse-worker.d.ts +13 -0
- package/dist/docs/parse-worker.js +21 -0
- package/dist/docs/parse.d.ts +95 -0
- package/dist/docs/parse.js +372 -0
- package/dist/docs/pool.d.ts +27 -0
- package/dist/docs/pool.js +133 -0
- package/dist/docs/readme.d.ts +6 -0
- package/dist/docs/readme.js +142 -0
- package/dist/docs/render.d.ts +13 -0
- package/dist/docs/render.js +46 -0
- package/dist/docs/repl.d.ts +7 -0
- package/dist/docs/repl.js +130 -0
- package/dist/docs/search.d.ts +92 -0
- package/dist/docs/search.js +251 -0
- package/dist/docs/store.d.ts +79 -0
- package/dist/docs/store.js +214 -0
- package/dist/docs/tools.d.ts +10 -0
- package/dist/docs/tools.js +300 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/schema/build.d.ts +16 -2
- package/dist/schema/build.js +37 -25
- package/dist/schema/command.js +39 -10
- package/dist/schema/query.js +1 -0
- package/dist/schema/readme.js +6 -2
- package/dist/schema/search.d.ts +2 -0
- package/dist/schema/search.js +18 -2
- package/dist/schema/store.d.ts +13 -3
- package/dist/schema/store.js +73 -24
- package/dist/schema/tools.js +21 -2
- package/package.json +17 -4
package/dist/common/progress.js
CHANGED
|
@@ -46,8 +46,18 @@ export function beginBuild(plan) {
|
|
|
46
46
|
let summary;
|
|
47
47
|
let done = 0;
|
|
48
48
|
let total = 0;
|
|
49
|
+
let pending = [];
|
|
49
50
|
let wroteAt = 0;
|
|
50
51
|
let closed = false;
|
|
52
|
+
const timings = [];
|
|
53
|
+
let phaseAt = started;
|
|
54
|
+
/** Closes the running phase off. Called on every transition, and at the end. */
|
|
55
|
+
const mark = () => {
|
|
56
|
+
const at = Date.now();
|
|
57
|
+
timings.push({ name: phase, ms: at - phaseAt });
|
|
58
|
+
phaseAt = at;
|
|
59
|
+
};
|
|
60
|
+
const sofar = () => [...timings, { name: phase, ms: Date.now() - phaseAt }];
|
|
51
61
|
const write = (body) => {
|
|
52
62
|
// Through a temp name, so a reader never catches half a file.
|
|
53
63
|
const target = join(plan.dir, README_FILE);
|
|
@@ -62,8 +72,11 @@ export function beginBuild(plan) {
|
|
|
62
72
|
started,
|
|
63
73
|
now: Date.now(),
|
|
64
74
|
step: plan.phases[phase],
|
|
75
|
+
phase,
|
|
65
76
|
done,
|
|
66
77
|
total,
|
|
78
|
+
pending,
|
|
79
|
+
timings: sofar(),
|
|
67
80
|
summary,
|
|
68
81
|
}));
|
|
69
82
|
const maybe = () => {
|
|
@@ -87,7 +100,9 @@ export function beginBuild(plan) {
|
|
|
87
100
|
report();
|
|
88
101
|
return {
|
|
89
102
|
phase(name) {
|
|
103
|
+
mark();
|
|
90
104
|
phase = name;
|
|
105
|
+
pending = [];
|
|
91
106
|
maybe();
|
|
92
107
|
},
|
|
93
108
|
read(seen, count) {
|
|
@@ -95,16 +110,33 @@ export function beginBuild(plan) {
|
|
|
95
110
|
total = count;
|
|
96
111
|
maybe();
|
|
97
112
|
},
|
|
98
|
-
progress(at, of) {
|
|
113
|
+
progress(at, of, waiting) {
|
|
99
114
|
done = at;
|
|
100
115
|
total = of;
|
|
116
|
+
pending = waiting ?? [];
|
|
101
117
|
maybe();
|
|
102
118
|
},
|
|
103
119
|
finish(manifest) {
|
|
104
|
-
|
|
120
|
+
mark();
|
|
121
|
+
close(plan.report.complete({
|
|
122
|
+
dir: plan.dir,
|
|
123
|
+
manifest,
|
|
124
|
+
ms: Date.now() - started,
|
|
125
|
+
timings,
|
|
126
|
+
}));
|
|
105
127
|
},
|
|
106
128
|
fail(reason) {
|
|
107
|
-
|
|
129
|
+
mark();
|
|
130
|
+
close(plan.report.failed({
|
|
131
|
+
documents,
|
|
132
|
+
step: plan.phases[phase],
|
|
133
|
+
reason,
|
|
134
|
+
started,
|
|
135
|
+
timings,
|
|
136
|
+
}));
|
|
137
|
+
},
|
|
138
|
+
get timings() {
|
|
139
|
+
return closed ? timings : sofar();
|
|
108
140
|
},
|
|
109
141
|
};
|
|
110
142
|
}
|
package/dist/common/prose.d.ts
CHANGED
|
@@ -4,6 +4,13 @@ export declare function fields(rows: readonly string[][]): string[];
|
|
|
4
4
|
export declare function grid(headers: readonly string[], rows: readonly (readonly string[])[]): string[];
|
|
5
5
|
export declare function plural(n: number, one: string, many?: string): string;
|
|
6
6
|
export declare function duration(ms: number): string;
|
|
7
|
+
/** Sub-second, because a phase breakdown is read to compare phases against each other. */
|
|
8
|
+
export declare function span(ms: number): string;
|
|
9
|
+
/** Where the time went, which is the only way to know what is worth making faster. */
|
|
10
|
+
export declare const breakdown: (timings: readonly {
|
|
11
|
+
name: string;
|
|
12
|
+
ms: number;
|
|
13
|
+
}[]) => string;
|
|
7
14
|
/** The first line only: a stack trace in a README helps nobody. */
|
|
8
15
|
export declare function message(reason: unknown): string;
|
|
9
16
|
export declare function searched(indexes: {
|
package/dist/common/prose.js
CHANGED
|
@@ -36,6 +36,19 @@ export function duration(ms) {
|
|
|
36
36
|
? `${minutes}m${seconds % 60}s`
|
|
37
37
|
: `${Math.floor(minutes / 60)}h${minutes % 60}m`;
|
|
38
38
|
}
|
|
39
|
+
/** Sub-second, because a phase breakdown is read to compare phases against each other. */
|
|
40
|
+
export function span(ms) {
|
|
41
|
+
if (ms < 1000) {
|
|
42
|
+
return `${Math.round(ms)}ms`;
|
|
43
|
+
}
|
|
44
|
+
if (ms < 60_000) {
|
|
45
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
46
|
+
}
|
|
47
|
+
const seconds = Math.round(ms / 1000);
|
|
48
|
+
return `${Math.floor(seconds / 60)}m${String(seconds % 60).padStart(2, '0')}s`;
|
|
49
|
+
}
|
|
50
|
+
/** Where the time went, which is the only way to know what is worth making faster. */
|
|
51
|
+
export const breakdown = (timings) => timings.map((t) => `${t.name} ${span(t.ms)}`).join(' · ');
|
|
39
52
|
/** The first line only: a stack trace in a README helps nobody. */
|
|
40
53
|
export function message(reason) {
|
|
41
54
|
const text = reason instanceof Error ? reason.message : String(reason);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { DocsIndex, Match } from './search.ts';
|
|
2
|
+
export interface AssembleOptions {
|
|
3
|
+
/** extra lines quoted before each matching body */
|
|
4
|
+
before?: number;
|
|
5
|
+
after?: number;
|
|
6
|
+
/** a ceiling on the whole answer, so one long section cannot eat it */
|
|
7
|
+
maxLines?: number;
|
|
8
|
+
/** two ranges closer than this are joined rather than marked */
|
|
9
|
+
mergeGap?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface Segment {
|
|
12
|
+
start: number;
|
|
13
|
+
end: number;
|
|
14
|
+
/** the lines themselves, verbatim */
|
|
15
|
+
lines: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface Omission {
|
|
18
|
+
start: number;
|
|
19
|
+
end: number;
|
|
20
|
+
count: number;
|
|
21
|
+
/** the headings the skipped lines covered, so the gap has a name */
|
|
22
|
+
sections: string[];
|
|
23
|
+
}
|
|
24
|
+
export type Piece = ({
|
|
25
|
+
type: 'segment';
|
|
26
|
+
} & Segment) | ({
|
|
27
|
+
type: 'omission';
|
|
28
|
+
} & Omission);
|
|
29
|
+
export interface Excerpt {
|
|
30
|
+
path: string;
|
|
31
|
+
title: string;
|
|
32
|
+
score: number;
|
|
33
|
+
/** what landed in this document, best first */
|
|
34
|
+
matches: Match[];
|
|
35
|
+
pieces: Piece[];
|
|
36
|
+
/** lines actually quoted */
|
|
37
|
+
shown: number;
|
|
38
|
+
/** the document's length, so a reader knows what fraction this is */
|
|
39
|
+
lines: number;
|
|
40
|
+
}
|
|
41
|
+
export interface Assembly {
|
|
42
|
+
files: Excerpt[];
|
|
43
|
+
shown: number;
|
|
44
|
+
/** true when the line budget cut something that had matched */
|
|
45
|
+
truncated: boolean;
|
|
46
|
+
}
|
|
47
|
+
export declare const DEFAULT_BEFORE = 0;
|
|
48
|
+
export declare const DEFAULT_AFTER = 0;
|
|
49
|
+
export declare const DEFAULT_MAX_LINES = 400;
|
|
50
|
+
export declare const DEFAULT_MERGE_GAP = 3;
|
|
51
|
+
export declare function assemble(index: DocsIndex, matches: readonly Match[], options?: AssembleOptions): Promise<Assembly>;
|
|
52
|
+
//# sourceMappingURL=assemble.d.ts.map
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
export const DEFAULT_BEFORE = 0;
|
|
2
|
+
export const DEFAULT_AFTER = 0;
|
|
3
|
+
export const DEFAULT_MAX_LINES = 400;
|
|
4
|
+
export const DEFAULT_MERGE_GAP = 3;
|
|
5
|
+
export async function assemble(index, matches, options = {}) {
|
|
6
|
+
const before = options.before ?? DEFAULT_BEFORE;
|
|
7
|
+
const after = options.after ?? DEFAULT_AFTER;
|
|
8
|
+
const gap = options.mergeGap ?? DEFAULT_MERGE_GAP;
|
|
9
|
+
let budget = options.maxLines ?? DEFAULT_MAX_LINES;
|
|
10
|
+
const files = [];
|
|
11
|
+
let truncated = false;
|
|
12
|
+
let shown = 0;
|
|
13
|
+
for (const [path, group] of byFile(matches)) {
|
|
14
|
+
const outline = index.file(path);
|
|
15
|
+
const source = await index.lines(path);
|
|
16
|
+
const total = outline?.lines ?? source.length;
|
|
17
|
+
const wanted = ranges(lineSet(group, before, after, total), gap);
|
|
18
|
+
const pieces = [];
|
|
19
|
+
let quoted = 0;
|
|
20
|
+
let last;
|
|
21
|
+
for (const range of wanted) {
|
|
22
|
+
if (budget <= 0) {
|
|
23
|
+
truncated = true;
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
const end = Math.min(range.end, range.start + budget - 1);
|
|
27
|
+
if (last) {
|
|
28
|
+
pieces.push(omission(last.end + 1, range.start - 1, outline?.headings ?? []));
|
|
29
|
+
}
|
|
30
|
+
pieces.push({
|
|
31
|
+
type: 'segment',
|
|
32
|
+
start: range.start,
|
|
33
|
+
end,
|
|
34
|
+
lines: source.slice(range.start - 1, end),
|
|
35
|
+
});
|
|
36
|
+
const count = end - range.start + 1;
|
|
37
|
+
quoted += count;
|
|
38
|
+
budget -= count;
|
|
39
|
+
if (end < range.end) {
|
|
40
|
+
truncated = true;
|
|
41
|
+
}
|
|
42
|
+
last = { start: range.start, end };
|
|
43
|
+
}
|
|
44
|
+
if (last && last.end < total) {
|
|
45
|
+
pieces.push(omission(last.end + 1, total, outline?.headings ?? []));
|
|
46
|
+
}
|
|
47
|
+
if (pieces.length === 0) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
shown += quoted;
|
|
51
|
+
files.push({
|
|
52
|
+
path,
|
|
53
|
+
title: outline?.title ?? path,
|
|
54
|
+
score: group[0].score,
|
|
55
|
+
matches: group,
|
|
56
|
+
pieces,
|
|
57
|
+
shown: quoted,
|
|
58
|
+
lines: total,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return { files, shown, truncated };
|
|
62
|
+
}
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
/** Documents in the order their best match placed, matches within them likewise. */
|
|
65
|
+
function byFile(matches) {
|
|
66
|
+
const groups = new Map();
|
|
67
|
+
for (const item of matches) {
|
|
68
|
+
const group = groups.get(item.path);
|
|
69
|
+
if (group) {
|
|
70
|
+
group.push(item);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
groups.set(item.path, [item]);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return [...groups.entries()];
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Every line the answer wants. A chunk carries its own render set — its
|
|
80
|
+
* headings, a table's header row, the line its body started on — and `before`
|
|
81
|
+
* and `after` widen the body only, so context is padding around the match and
|
|
82
|
+
* never around a heading quoted from elsewhere in the file.
|
|
83
|
+
*/
|
|
84
|
+
function lineSet(matches, before, after, total) {
|
|
85
|
+
const wanted = new Set();
|
|
86
|
+
const add = (line) => {
|
|
87
|
+
if (line >= 1 && line <= total) {
|
|
88
|
+
wanted.add(line);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
for (const item of matches) {
|
|
92
|
+
for (const line of item.lineNumbers) {
|
|
93
|
+
add(line);
|
|
94
|
+
}
|
|
95
|
+
for (let line = item.bodyStart - before; line <= item.bodyEnd + after; line++) {
|
|
96
|
+
add(line);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return wanted;
|
|
100
|
+
}
|
|
101
|
+
/** Contiguous runs, with runs closer together than `gap` joined into one. */
|
|
102
|
+
function ranges(wanted, gap) {
|
|
103
|
+
const sorted = [...wanted].sort((a, b) => a - b);
|
|
104
|
+
const out = [];
|
|
105
|
+
for (const line of sorted) {
|
|
106
|
+
const last = out.at(-1);
|
|
107
|
+
if (last && line - last.end <= gap + 1) {
|
|
108
|
+
last.end = line;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
out.push({ start: line, end: line });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
function omission(start, end, headings) {
|
|
117
|
+
const named = headings.filter((h) => h.line >= start && h.line <= end).map((h) => h.title);
|
|
118
|
+
return {
|
|
119
|
+
type: 'omission',
|
|
120
|
+
start,
|
|
121
|
+
end,
|
|
122
|
+
count: end - start + 1,
|
|
123
|
+
// A gap covering thirty headings is a gap; naming all of them is noise.
|
|
124
|
+
sections: named.slice(0, 6),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=assemble.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Embedder } from '@zenera/neo';
|
|
2
|
+
import { type PhaseTiming } from '../common/progress.ts';
|
|
3
|
+
import { type ChunkOptions } from './chunk.ts';
|
|
4
|
+
import { type Counts, type DocRecord, type Manifest } from './files.ts';
|
|
5
|
+
import { type Corpus } from './load.ts';
|
|
6
|
+
import { type ChunkRecord } from './store.ts';
|
|
7
|
+
export interface BuildOptions {
|
|
8
|
+
/** files, directories or patterns, as they were named */
|
|
9
|
+
files: readonly string[];
|
|
10
|
+
cwd: string;
|
|
11
|
+
out: string;
|
|
12
|
+
embedder: Embedder;
|
|
13
|
+
/** the reference as it was written, which is what a later search will type */
|
|
14
|
+
embeddingRef?: string;
|
|
15
|
+
/** told the manifest, so a store can say what wrote it */
|
|
16
|
+
indexer: string;
|
|
17
|
+
chunk?: ChunkOptions;
|
|
18
|
+
/**
|
|
19
|
+
* The width asked of the embedder, when one was asked for. Part of the cache
|
|
20
|
+
* key, because a truncated vector is a different vector; left undefined when
|
|
21
|
+
* nobody asked, because that is a different key again from asking for the
|
|
22
|
+
* number the model would have chosen anyway.
|
|
23
|
+
*/
|
|
24
|
+
dimensions?: number;
|
|
25
|
+
/** reuse vectors and parses this machine already has; on by default */
|
|
26
|
+
cache?: boolean;
|
|
27
|
+
/** keep them somewhere other than the shared store */
|
|
28
|
+
cacheDir?: string;
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
/** what the documents turned out to hold, before a vector has been paid for */
|
|
31
|
+
onRead?: (summary: BuildSummary) => void;
|
|
32
|
+
/** documents parsed so far, and what the pool is still on */
|
|
33
|
+
onReading?: (done: number, total: number, pending: readonly string[]) => void;
|
|
34
|
+
onProgress?: (done: number, total: number) => void;
|
|
35
|
+
}
|
|
36
|
+
export interface BuildSummary {
|
|
37
|
+
sources: DocRecord[];
|
|
38
|
+
counts: Counts;
|
|
39
|
+
skipped: Corpus['skipped'];
|
|
40
|
+
}
|
|
41
|
+
export interface BuildResult {
|
|
42
|
+
manifest: Manifest;
|
|
43
|
+
chunks: ChunkRecord[];
|
|
44
|
+
/** what each phase cost, so a slow build can say which part was slow */
|
|
45
|
+
timings: readonly PhaseTiming[];
|
|
46
|
+
/** what came out of the shared cache instead of being done again */
|
|
47
|
+
reused: Reused;
|
|
48
|
+
}
|
|
49
|
+
export interface Reused {
|
|
50
|
+
parses: number;
|
|
51
|
+
vectors: number;
|
|
52
|
+
}
|
|
53
|
+
export declare function buildIndex(options: BuildOptions): Promise<BuildResult>;
|
|
54
|
+
//# sourceMappingURL=build.d.ts.map
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { embedStream, NO_CACHE, openCache } from "../common/cache.js";
|
|
2
|
+
import { beginBuild } from "../common/progress.js";
|
|
3
|
+
import { formatLines } from "./chunk.js";
|
|
4
|
+
import { INDEX_VERSION, SOURCES_DIR, writeIndex, } from "./files.js";
|
|
5
|
+
import { loadDocuments } from "./load.js";
|
|
6
|
+
import { DOCS_REPORT, PHASES } from "./readme.js";
|
|
7
|
+
import { openChunks } from "./store.js";
|
|
8
|
+
export async function buildIndex(options) {
|
|
9
|
+
const journal = beginBuild({
|
|
10
|
+
dir: options.out,
|
|
11
|
+
files: options.files,
|
|
12
|
+
embedding: options.embeddingRef ?? options.embedder.id,
|
|
13
|
+
indexer: options.indexer,
|
|
14
|
+
phases: PHASES,
|
|
15
|
+
report: DOCS_REPORT,
|
|
16
|
+
});
|
|
17
|
+
const ref = options.embeddingRef ?? options.embedder.id;
|
|
18
|
+
const cache = options.cache === false
|
|
19
|
+
? NO_CACHE
|
|
20
|
+
: openCache(options.embedder, {
|
|
21
|
+
ref,
|
|
22
|
+
dir: options.cacheDir,
|
|
23
|
+
dimensions: options.dimensions,
|
|
24
|
+
});
|
|
25
|
+
let writer;
|
|
26
|
+
try {
|
|
27
|
+
const corpus = await loadDocuments(options.files, options.cwd, {
|
|
28
|
+
chunk: options.chunk,
|
|
29
|
+
cache: options.cache !== false,
|
|
30
|
+
cacheDir: options.cacheDir,
|
|
31
|
+
onProgress: (done, total, pending) => {
|
|
32
|
+
journal.progress(done, total, pending);
|
|
33
|
+
options.onReading?.(done, total, pending);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
const chunks = recordsOf(corpus);
|
|
37
|
+
const sources = corpus.docs.map((doc) => ({
|
|
38
|
+
name: doc.name,
|
|
39
|
+
file: doc.file,
|
|
40
|
+
path: `${SOURCES_DIR}/${doc.name}`,
|
|
41
|
+
sha256: doc.sha256,
|
|
42
|
+
format: doc.format,
|
|
43
|
+
title: doc.outline.title,
|
|
44
|
+
bytes: doc.bytes,
|
|
45
|
+
lines: doc.outline.lines,
|
|
46
|
+
sections: doc.outline.headings.length,
|
|
47
|
+
tables: doc.outline.tables.length,
|
|
48
|
+
chunks: doc.chunks.length,
|
|
49
|
+
}));
|
|
50
|
+
const counts = {
|
|
51
|
+
documents: sources.length,
|
|
52
|
+
chunks: chunks.length,
|
|
53
|
+
lines: sources.reduce((n, s) => n + s.lines, 0),
|
|
54
|
+
sections: sources.reduce((n, s) => n + s.sections, 0),
|
|
55
|
+
tables: sources.reduce((n, s) => n + s.tables, 0),
|
|
56
|
+
};
|
|
57
|
+
journal.read(counts, chunks.length);
|
|
58
|
+
options.onRead?.({ sources, counts, skipped: corpus.skipped });
|
|
59
|
+
journal.phase('embedding');
|
|
60
|
+
writer = await openChunks(options.out);
|
|
61
|
+
const dimensions = await embedAll(chunks, options, journal, cache, writer);
|
|
62
|
+
journal.phase('writing');
|
|
63
|
+
const written = await writer.finish();
|
|
64
|
+
const manifest = {
|
|
65
|
+
version: INDEX_VERSION,
|
|
66
|
+
kind: 'docs',
|
|
67
|
+
createdAt: new Date().toISOString(),
|
|
68
|
+
indexer: options.indexer,
|
|
69
|
+
embedding: {
|
|
70
|
+
ref,
|
|
71
|
+
id: options.embedder.id,
|
|
72
|
+
dimensions,
|
|
73
|
+
requested: options.dimensions,
|
|
74
|
+
},
|
|
75
|
+
sources,
|
|
76
|
+
counts,
|
|
77
|
+
indexes: { fts: written.fts, vector: written.vector },
|
|
78
|
+
};
|
|
79
|
+
const outline = { files: corpus.docs.map((doc) => doc.outline) };
|
|
80
|
+
const documents = Object.fromEntries(corpus.docs.map((doc) => [doc.name, doc.text]));
|
|
81
|
+
await writeIndex(options.out, { manifest, outline, documents });
|
|
82
|
+
cache.commit();
|
|
83
|
+
journal.finish(manifest);
|
|
84
|
+
return {
|
|
85
|
+
manifest,
|
|
86
|
+
chunks,
|
|
87
|
+
timings: journal.timings,
|
|
88
|
+
reused: { parses: corpus.cached, vectors: cache.hits },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
writer?.close();
|
|
93
|
+
cache.abandon();
|
|
94
|
+
journal.fail(err);
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** One row per chunk, with the render set encoded and the document name on it. */
|
|
99
|
+
function recordsOf(corpus) {
|
|
100
|
+
return corpus.docs.flatMap((doc) => doc.chunks.map((chunk) => ({
|
|
101
|
+
id: `${doc.name}#c${chunk.index}`,
|
|
102
|
+
path: doc.name,
|
|
103
|
+
ordinal: chunk.index,
|
|
104
|
+
kind: chunk.kind,
|
|
105
|
+
text: chunk.text,
|
|
106
|
+
embedText: chunk.embedText,
|
|
107
|
+
lineSpec: formatLines(chunk.lineNumbers),
|
|
108
|
+
bodyStart: chunk.bodyStart,
|
|
109
|
+
bodyEnd: chunk.bodyEnd,
|
|
110
|
+
structureId: chunk.structureId,
|
|
111
|
+
structurePath: chunk.structurePath,
|
|
112
|
+
headings: chunk.headings,
|
|
113
|
+
tokens: chunk.tokens,
|
|
114
|
+
})));
|
|
115
|
+
}
|
|
116
|
+
async function embedAll(chunks, options, journal, cache, writer) {
|
|
117
|
+
// A window at a time, rather than the whole corpus in one call. How many
|
|
118
|
+
// texts fit in a request, and how many requests may be in flight, are still
|
|
119
|
+
// the embedder's to answer — it knows the model's caps and it is the one
|
|
120
|
+
// that sees the 429s. What the window decides is only how much is resident.
|
|
121
|
+
return embedStream({
|
|
122
|
+
embedder: options.embedder,
|
|
123
|
+
cache,
|
|
124
|
+
records: chunks,
|
|
125
|
+
textOf: (chunk) => chunk.embedText,
|
|
126
|
+
signal: options.signal,
|
|
127
|
+
onProgress: (done, total) => {
|
|
128
|
+
journal.progress(done, total);
|
|
129
|
+
options.onProgress?.(done, total);
|
|
130
|
+
},
|
|
131
|
+
onWindow: (window, vectors) => writer.add(window, vectors),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=build.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type ParsedDoc } from './parse.ts';
|
|
2
|
+
/** Soft target, hard ceiling, and the width at which one table row is too wide. */
|
|
3
|
+
export declare const CHUNK_TOKENS = 384;
|
|
4
|
+
export declare const MAX_CHUNK_TOKENS = 512;
|
|
5
|
+
export declare const TABLE_SLICE_TOKENS = 128;
|
|
6
|
+
/** How much of a table its descriptor stands for when no row of it matched. */
|
|
7
|
+
export declare const TABLE_PREVIEW_ROWS = 3;
|
|
8
|
+
/**
|
|
9
|
+
* And how many rows may share one. The token budget alone would put a narrow
|
|
10
|
+
* sixteen-row table in a single chunk, so matching one row of it quotes all
|
|
11
|
+
* sixteen — the rows are independent facts, and a reader asking about one is
|
|
12
|
+
* not asking about the other fifteen.
|
|
13
|
+
*/
|
|
14
|
+
export declare const TABLE_ROWS_PER_CHUNK = 4;
|
|
15
|
+
/**
|
|
16
|
+
* Below this a chunk is merged into its neighbour rather than retrieved alone.
|
|
17
|
+
*
|
|
18
|
+
* BM25 normalises by document length, so a nine-word chunk that happens to
|
|
19
|
+
* contain two query terms outscores a real answer that contains them among a
|
|
20
|
+
* hundred other words. Measured on this repository, a one-line aside about
|
|
21
|
+
* markdown link syntax took full-text rank 0 for "how to create docs index"
|
|
22
|
+
* while the vector leg — correctly — put it 185th. It is not that the chunk is
|
|
23
|
+
* wrong; it is that alone it is not a passage, and a passage is what the
|
|
24
|
+
* lexical index is scoring.
|
|
25
|
+
*/
|
|
26
|
+
export declare const MIN_CHUNK_TOKENS = 48;
|
|
27
|
+
/**
|
|
28
|
+
* How much of the block before a chunk may carry for continuity. It is one
|
|
29
|
+
* line, which is a whisper until the line is the whole of a README's challenge
|
|
30
|
+
* table on one row: 92kB of badge markup, none of it cited by a line number,
|
|
31
|
+
* outweighing the seven lines the chunk actually stands for by eighty to one.
|
|
32
|
+
*/
|
|
33
|
+
export declare const CARRY_TOKENS = 32;
|
|
34
|
+
/**
|
|
35
|
+
* Four characters to a token, which is within about 15% for English prose and
|
|
36
|
+
* wrong for CJK, for dense numeric cells and for long identifiers. It cannot
|
|
37
|
+
* cause a request to fail — 512 estimated tokens sits far below any embedding
|
|
38
|
+
* model's limit, so even a threefold underestimate has headroom. Injectable so
|
|
39
|
+
* a real tokenizer is a one-line swap if the corpus ever needs one.
|
|
40
|
+
*/
|
|
41
|
+
export declare const tokenCount: (text: string) => number;
|
|
42
|
+
/** The kinds a chunk can be, which is what `--kind` filters on. */
|
|
43
|
+
export declare const CHUNK_KINDS: readonly ["paragraph", "list", "table", "table_row", "code", "frontmatter", "html"];
|
|
44
|
+
export type ChunkKind = (typeof CHUNK_KINDS)[number];
|
|
45
|
+
export interface ChunkOptions {
|
|
46
|
+
chunkTokens?: number;
|
|
47
|
+
minChunkTokens?: number;
|
|
48
|
+
maxChunkTokens?: number;
|
|
49
|
+
tableSliceTokens?: number;
|
|
50
|
+
tokenCount?: (text: string) => number;
|
|
51
|
+
}
|
|
52
|
+
export interface Chunk {
|
|
53
|
+
index: number;
|
|
54
|
+
kind: ChunkKind;
|
|
55
|
+
/** the innermost structure node containing the whole body */
|
|
56
|
+
structureId: string;
|
|
57
|
+
structurePath: string;
|
|
58
|
+
/** the breadcrumb, from the document name down to the nearest heading */
|
|
59
|
+
headings: string;
|
|
60
|
+
bodyStart: number;
|
|
61
|
+
bodyEnd: number;
|
|
62
|
+
/** everything that gets rendered: headings, prelude and body, sorted */
|
|
63
|
+
lineNumbers: number[];
|
|
64
|
+
/** the full-text document — wider */
|
|
65
|
+
text: string;
|
|
66
|
+
/** the vector's source — tighter */
|
|
67
|
+
embedText: string;
|
|
68
|
+
tokens: number;
|
|
69
|
+
}
|
|
70
|
+
export declare function chunkDocument(doc: ParsedDoc, options?: ChunkOptions): Chunk[];
|
|
71
|
+
export declare function formatLines(numbers: readonly number[]): string;
|
|
72
|
+
export declare function parseLines(spec: string): number[];
|
|
73
|
+
//# sourceMappingURL=chunk.d.ts.map
|