@amsterdamdatalabs/enact-docs-search 0.1.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/NOTICE +50 -0
- package/README.md +35 -0
- package/dist/adapter/repo-walker.d.ts +22 -0
- package/dist/adapter/repo-walker.d.ts.map +1 -0
- package/dist/adapter/repo-walker.js +96 -0
- package/dist/adapter/repo-walker.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +310 -0
- package/dist/cli.js.map +1 -0
- package/dist/embeddings/embedding-service.d.ts +12 -0
- package/dist/embeddings/embedding-service.d.ts.map +1 -0
- package/dist/embeddings/embedding-service.js +67 -0
- package/dist/embeddings/embedding-service.js.map +1 -0
- package/dist/indexing/chunker.d.ts +52 -0
- package/dist/indexing/chunker.d.ts.map +1 -0
- package/dist/indexing/chunker.js +196 -0
- package/dist/indexing/chunker.js.map +1 -0
- package/dist/indexing/indexing-service.d.ts +29 -0
- package/dist/indexing/indexing-service.d.ts.map +1 -0
- package/dist/indexing/indexing-service.js +151 -0
- package/dist/indexing/indexing-service.js.map +1 -0
- package/dist/search/search-service.d.ts +53 -0
- package/dist/search/search-service.d.ts.map +1 -0
- package/dist/search/search-service.js +139 -0
- package/dist/search/search-service.js.map +1 -0
- package/dist/search/snippet.d.ts +65 -0
- package/dist/search/snippet.d.ts.map +1 -0
- package/dist/search/snippet.js +352 -0
- package/dist/search/snippet.js.map +1 -0
- package/dist/storage/document-store.d.ts +72 -0
- package/dist/storage/document-store.d.ts.map +1 -0
- package/dist/storage/document-store.js +134 -0
- package/dist/storage/document-store.js.map +1 -0
- package/dist/storage/persistence.d.ts +55 -0
- package/dist/storage/persistence.d.ts.map +1 -0
- package/dist/storage/persistence.js +132 -0
- package/dist/storage/persistence.js.map +1 -0
- package/dist/storage/vector-store.d.ts +63 -0
- package/dist/storage/vector-store.d.ts.map +1 -0
- package/dist/storage/vector-store.js +172 -0
- package/dist/storage/vector-store.js.map +1 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +24 -0
- package/dist/types.js.map +1 -0
- package/package.json +40 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapted from obsidian-qmd (MIT): https://github.com/thirteen37/obsidian-qmd
|
|
3
|
+
* Source: src/indexing/Chunker.ts — ported verbatim (no Obsidian dependency in
|
|
4
|
+
* the original); only the module doc-comment and export style were adjusted
|
|
5
|
+
* and `startLine` computation was added for path:line output.
|
|
6
|
+
*
|
|
7
|
+
* Document chunking for search indexing. Splits documents into overlapping
|
|
8
|
+
* chunks with awareness of markdown structure.
|
|
9
|
+
*/
|
|
10
|
+
export interface Chunk {
|
|
11
|
+
/** The text content of this chunk */
|
|
12
|
+
content: string;
|
|
13
|
+
/** Path to the source file */
|
|
14
|
+
filePath: string;
|
|
15
|
+
/** Zero-based index of this chunk within the document */
|
|
16
|
+
chunkIndex: number;
|
|
17
|
+
/** Start position in the original document (character offset) */
|
|
18
|
+
startPos: number;
|
|
19
|
+
/** End position in the original document (character offset) */
|
|
20
|
+
endPos: number;
|
|
21
|
+
/** 1-based line number in the original document where this chunk starts */
|
|
22
|
+
startLine: number;
|
|
23
|
+
/** Heading hierarchy at the start of this chunk (H1 to deepest level) */
|
|
24
|
+
headingBreadcrumb: string[];
|
|
25
|
+
}
|
|
26
|
+
export interface ChunkOptions {
|
|
27
|
+
/** Maximum chunk size in characters (default: 3200) */
|
|
28
|
+
maxChunkSize: number;
|
|
29
|
+
/** Overlap between chunks as a percentage (default: 0.15 = 15%) */
|
|
30
|
+
overlapPercent: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Chunker splits documents into overlapping chunks for search indexing.
|
|
34
|
+
* It attempts to break at natural boundaries (paragraphs, sentences, words)
|
|
35
|
+
* while respecting markdown structure (code blocks, headings).
|
|
36
|
+
*/
|
|
37
|
+
export declare class Chunker {
|
|
38
|
+
private options;
|
|
39
|
+
constructor(options?: Partial<ChunkOptions>);
|
|
40
|
+
/**
|
|
41
|
+
* Extract the heading hierarchy at a given position in the content.
|
|
42
|
+
*/
|
|
43
|
+
getHeadingBreadcrumb(content: string, position: number): string[];
|
|
44
|
+
private findFirstNonHeadingContent;
|
|
45
|
+
/**
|
|
46
|
+
* Split a document into chunks.
|
|
47
|
+
*/
|
|
48
|
+
chunk(content: string, filePath: string): Chunk[];
|
|
49
|
+
private findBreakPoint;
|
|
50
|
+
private findCodeBlockBoundary;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=chunker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunker.d.ts","sourceRoot":"","sources":["../../src/indexing/chunker.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,KAAK;IACpB,qCAAqC;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,8BAA8B;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,yDAAyD;IACzD,UAAU,EAAE,MAAM,CAAC;IACnB,iEAAiE;IACjE,QAAQ,EAAE,MAAM,CAAC;IACjB,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,YAAY,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,cAAc,EAAE,MAAM,CAAC;CACxB;AAOD;;;;GAIG;AACH,qBAAa,OAAO;IAClB,OAAO,CAAC,OAAO,CAAe;gBAElB,OAAO,GAAE,OAAO,CAAC,YAAY,CAAM;IAI/C;;OAEG;IACH,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE;IAqBjE,OAAO,CAAC,0BAA0B;IAiClC;;OAEG;IACH,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,KAAK,EAAE;IAuEjD,OAAO,CAAC,cAAc;IA8CtB,OAAO,CAAC,qBAAqB;CAoB9B"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapted from obsidian-qmd (MIT): https://github.com/thirteen37/obsidian-qmd
|
|
3
|
+
* Source: src/indexing/Chunker.ts — ported verbatim (no Obsidian dependency in
|
|
4
|
+
* the original); only the module doc-comment and export style were adjusted
|
|
5
|
+
* and `startLine` computation was added for path:line output.
|
|
6
|
+
*
|
|
7
|
+
* Document chunking for search indexing. Splits documents into overlapping
|
|
8
|
+
* chunks with awareness of markdown structure.
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_OPTIONS = {
|
|
11
|
+
maxChunkSize: 3200,
|
|
12
|
+
overlapPercent: 0.15,
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Chunker splits documents into overlapping chunks for search indexing.
|
|
16
|
+
* It attempts to break at natural boundaries (paragraphs, sentences, words)
|
|
17
|
+
* while respecting markdown structure (code blocks, headings).
|
|
18
|
+
*/
|
|
19
|
+
export class Chunker {
|
|
20
|
+
options;
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Extract the heading hierarchy at a given position in the content.
|
|
26
|
+
*/
|
|
27
|
+
getHeadingBreadcrumb(content, position) {
|
|
28
|
+
const scanEnd = this.findFirstNonHeadingContent(content, position);
|
|
29
|
+
const contentToScan = content.slice(0, scanEnd);
|
|
30
|
+
const headingRegex = /^(#{1,6})\s+(.+)$/gm;
|
|
31
|
+
const headings = [null, null, null, null, null, null];
|
|
32
|
+
let match;
|
|
33
|
+
while ((match = headingRegex.exec(contentToScan)) !== null) {
|
|
34
|
+
const level = match[1].length;
|
|
35
|
+
const text = match[2].trim();
|
|
36
|
+
headings[level - 1] = text;
|
|
37
|
+
for (let i = level; i < 6; i++) {
|
|
38
|
+
headings[i] = null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return headings.filter((h) => h !== null);
|
|
42
|
+
}
|
|
43
|
+
findFirstNonHeadingContent(content, position) {
|
|
44
|
+
let i = position;
|
|
45
|
+
const len = content.length;
|
|
46
|
+
while (i < len) {
|
|
47
|
+
while (i < len && /\s/.test(content[i])) {
|
|
48
|
+
i++;
|
|
49
|
+
}
|
|
50
|
+
if (i >= len)
|
|
51
|
+
break;
|
|
52
|
+
const lineEnd = content.indexOf("\n", i);
|
|
53
|
+
const line = content.slice(i, lineEnd === -1 ? len : lineEnd);
|
|
54
|
+
if (/^#{1,6}\s+/.test(line)) {
|
|
55
|
+
i = lineEnd === -1 ? len : lineEnd + 1;
|
|
56
|
+
}
|
|
57
|
+
else if (line.trim() === "") {
|
|
58
|
+
i = lineEnd === -1 ? len : lineEnd + 1;
|
|
59
|
+
}
|
|
60
|
+
else if (line.startsWith("---") && i === 0) {
|
|
61
|
+
const frontmatterEnd = content.indexOf("\n---", i + 3);
|
|
62
|
+
if (frontmatterEnd !== -1) {
|
|
63
|
+
i = frontmatterEnd + 4;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return i;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Split a document into chunks.
|
|
77
|
+
*/
|
|
78
|
+
chunk(content, filePath) {
|
|
79
|
+
const trimmed = content.trim();
|
|
80
|
+
if (!trimmed) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
84
|
+
if (normalized.length <= this.options.maxChunkSize) {
|
|
85
|
+
return [
|
|
86
|
+
{
|
|
87
|
+
content: normalized,
|
|
88
|
+
filePath,
|
|
89
|
+
chunkIndex: 0,
|
|
90
|
+
startPos: 0,
|
|
91
|
+
endPos: normalized.length,
|
|
92
|
+
startLine: 1,
|
|
93
|
+
headingBreadcrumb: this.getHeadingBreadcrumb(normalized, 0),
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
const chunks = [];
|
|
98
|
+
const overlapSize = Math.floor(this.options.maxChunkSize * this.options.overlapPercent);
|
|
99
|
+
const minChunkSize = Math.floor(this.options.maxChunkSize * 0.2);
|
|
100
|
+
let position = 0;
|
|
101
|
+
let chunkIndex = 0;
|
|
102
|
+
while (position < normalized.length) {
|
|
103
|
+
const remaining = normalized.length - position;
|
|
104
|
+
if (remaining < minChunkSize && chunks.length > 0) {
|
|
105
|
+
const lastChunk = chunks[chunks.length - 1];
|
|
106
|
+
lastChunk.content = normalized.slice(lastChunk.startPos);
|
|
107
|
+
lastChunk.endPos = normalized.length;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
const chunkEnd = Math.min(position + this.options.maxChunkSize, normalized.length);
|
|
111
|
+
let actualEnd;
|
|
112
|
+
if (remaining <= this.options.maxChunkSize) {
|
|
113
|
+
actualEnd = normalized.length;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
actualEnd = this.findBreakPoint(normalized, position, chunkEnd);
|
|
117
|
+
}
|
|
118
|
+
const chunkContent = normalized.slice(position, actualEnd);
|
|
119
|
+
chunks.push({
|
|
120
|
+
content: chunkContent,
|
|
121
|
+
filePath,
|
|
122
|
+
chunkIndex,
|
|
123
|
+
startPos: position,
|
|
124
|
+
endPos: actualEnd,
|
|
125
|
+
startLine: lineNumberAt(normalized, position),
|
|
126
|
+
headingBreadcrumb: this.getHeadingBreadcrumb(normalized, position),
|
|
127
|
+
});
|
|
128
|
+
const advance = actualEnd - position - overlapSize;
|
|
129
|
+
position += Math.max(advance, 1);
|
|
130
|
+
chunkIndex++;
|
|
131
|
+
if (chunkIndex > normalized.length) {
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return chunks;
|
|
136
|
+
}
|
|
137
|
+
findBreakPoint(content, start, maxEnd) {
|
|
138
|
+
const searchRange = content.slice(start, maxEnd);
|
|
139
|
+
const codeBlockBoundary = this.findCodeBlockBoundary(content, start, maxEnd);
|
|
140
|
+
if (codeBlockBoundary !== null) {
|
|
141
|
+
return codeBlockBoundary;
|
|
142
|
+
}
|
|
143
|
+
const searchStart = Math.floor(searchRange.length * 0.7);
|
|
144
|
+
const lateRange = searchRange.slice(searchStart);
|
|
145
|
+
const paragraphMatch = lateRange.lastIndexOf("\n\n");
|
|
146
|
+
if (paragraphMatch !== -1) {
|
|
147
|
+
return start + searchStart + paragraphMatch + 2;
|
|
148
|
+
}
|
|
149
|
+
const sentenceRegex = /[.!?]\s+/g;
|
|
150
|
+
let lastSentenceEnd = -1;
|
|
151
|
+
let match;
|
|
152
|
+
const fullSearchRange = searchRange.slice(searchStart);
|
|
153
|
+
while ((match = sentenceRegex.exec(fullSearchRange)) !== null) {
|
|
154
|
+
lastSentenceEnd = match.index + match[0].length;
|
|
155
|
+
}
|
|
156
|
+
if (lastSentenceEnd !== -1) {
|
|
157
|
+
return start + searchStart + lastSentenceEnd;
|
|
158
|
+
}
|
|
159
|
+
const wordSearchStart = Math.floor(searchRange.length * 0.8);
|
|
160
|
+
const wordRange = searchRange.slice(wordSearchStart);
|
|
161
|
+
let lastSpace = Math.max(wordRange.lastIndexOf(" "), wordRange.lastIndexOf("\n"));
|
|
162
|
+
if (lastSpace !== -1) {
|
|
163
|
+
return start + wordSearchStart + lastSpace + 1;
|
|
164
|
+
}
|
|
165
|
+
lastSpace = Math.max(searchRange.lastIndexOf(" "), searchRange.lastIndexOf("\n"), searchRange.lastIndexOf("\t"));
|
|
166
|
+
if (lastSpace !== -1) {
|
|
167
|
+
return start + lastSpace + 1;
|
|
168
|
+
}
|
|
169
|
+
return maxEnd;
|
|
170
|
+
}
|
|
171
|
+
findCodeBlockBoundary(content, start, maxEnd) {
|
|
172
|
+
const beforeChunk = content.slice(0, start);
|
|
173
|
+
const chunk = content.slice(start, maxEnd);
|
|
174
|
+
const openMarkersBefore = (beforeChunk.match(/```/g) || []).length;
|
|
175
|
+
const markersInChunk = (chunk.match(/```/g) || []).length;
|
|
176
|
+
const startedInsideBlock = openMarkersBefore % 2 === 1;
|
|
177
|
+
if (startedInsideBlock || markersInChunk % 2 === 1) {
|
|
178
|
+
const afterMaxEnd = content.slice(maxEnd);
|
|
179
|
+
const closingMarker = afterMaxEnd.indexOf("```");
|
|
180
|
+
if (closingMarker !== -1 && closingMarker < this.options.maxChunkSize) {
|
|
181
|
+
return maxEnd + closingMarker + 3;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** 1-based line number of a character offset within `content`. */
|
|
188
|
+
function lineNumberAt(content, offset) {
|
|
189
|
+
let line = 1;
|
|
190
|
+
for (let i = 0; i < offset; i++) {
|
|
191
|
+
if (content[i] === "\n")
|
|
192
|
+
line++;
|
|
193
|
+
}
|
|
194
|
+
return line;
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=chunker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunker.js","sourceRoot":"","sources":["../../src/indexing/chunker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Scope } from "../types.js";
|
|
2
|
+
export interface IndexProgress {
|
|
3
|
+
phase: "scanning" | "text-indexing" | "embedding" | "done";
|
|
4
|
+
filesProcessed: number;
|
|
5
|
+
totalFiles: number;
|
|
6
|
+
currentFile?: string;
|
|
7
|
+
}
|
|
8
|
+
export type ProgressCallback = (progress: IndexProgress) => void;
|
|
9
|
+
export interface IndexRunResult {
|
|
10
|
+
scope: Scope;
|
|
11
|
+
filesScanned: number;
|
|
12
|
+
filesChanged: number;
|
|
13
|
+
filesRemoved: number;
|
|
14
|
+
chunksIndexed: number;
|
|
15
|
+
embeddingsGenerated: number;
|
|
16
|
+
totalDurationMs: number;
|
|
17
|
+
}
|
|
18
|
+
export interface IndexOptions {
|
|
19
|
+
repoPath: string;
|
|
20
|
+
scope: Scope;
|
|
21
|
+
/** Skip vector embedding entirely (keyword-only index, much faster). */
|
|
22
|
+
skipEmbeddings?: boolean;
|
|
23
|
+
/** Ignore the manifest and rebuild every file in scope from scratch. */
|
|
24
|
+
force?: boolean;
|
|
25
|
+
exclude?: string[];
|
|
26
|
+
onProgress?: ProgressCallback;
|
|
27
|
+
}
|
|
28
|
+
export declare function runIndex(options: IndexOptions): Promise<IndexRunResult>;
|
|
29
|
+
//# sourceMappingURL=indexing-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexing-service.d.ts","sourceRoot":"","sources":["../../src/indexing/indexing-service.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,UAAU,GAAG,eAAe,GAAG,WAAW,GAAG,MAAM,CAAC;IAC3D,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,MAAM,gBAAgB,GAAG,CAAC,QAAQ,EAAE,aAAa,KAAK,IAAI,CAAC;AAEjE,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,KAAK,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;IACb,wEAAwE;IACxE,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,wEAAwE;IACxE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B;AAKD,wBAAsB,QAAQ,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC,CAoJ7E"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* indexing-service.ts — orchestrates indexing for one target repo.
|
|
3
|
+
*
|
|
4
|
+
* Loosely modelled on obsidian-qmd's IndexingService.ts (scan -> chunk ->
|
|
5
|
+
* index text -> embed), but rewritten: the original drives itself off
|
|
6
|
+
* in-memory Obsidian vault state and doesn't persist between runs (Obsidian
|
|
7
|
+
* keeps the plugin process alive). This CLI process exits after every
|
|
8
|
+
* command, so this version is built around a manifest-driven incremental
|
|
9
|
+
* diff (new/changed/removed files by mtime) against a store that is loaded
|
|
10
|
+
* from and saved back to disk on every run, scoped per target repo.
|
|
11
|
+
*/
|
|
12
|
+
import { DocumentStore } from "../storage/document-store.js";
|
|
13
|
+
import { VectorStore } from "../storage/vector-store.js";
|
|
14
|
+
import { EmbeddingService, EMBEDDING_DIMENSIONS } from "../embeddings/embedding-service.js";
|
|
15
|
+
import { Chunker } from "./chunker.js";
|
|
16
|
+
import { listMarkdownFiles, readRepoFile } from "../adapter/repo-walker.js";
|
|
17
|
+
import { ensureCacheDir, readManifest, writeManifest, emptyManifest, readDocumentStoreExport, writeDocumentStoreExport, readVectorSidecar, writeVectorSidecar, vectorIndexExists, repoCacheDir, paths, EMBEDDING_MODEL_ID, } from "../storage/persistence.js";
|
|
18
|
+
import { scopesToRoots } from "../types.js";
|
|
19
|
+
const CHUNK_SIZE = 3200;
|
|
20
|
+
const CHUNK_OVERLAP = 0.15;
|
|
21
|
+
export async function runIndex(options) {
|
|
22
|
+
const start = Date.now();
|
|
23
|
+
const { repoPath, scope, onProgress } = options;
|
|
24
|
+
const cacheDir = repoCacheDir(repoPath);
|
|
25
|
+
await ensureCacheDir(cacheDir);
|
|
26
|
+
const manifest = (options.force ? null : await readManifest(cacheDir)) ?? emptyManifest();
|
|
27
|
+
onProgress?.({ phase: "scanning", filesProcessed: 0, totalFiles: 0 });
|
|
28
|
+
const roots = scopesToRoots(scope);
|
|
29
|
+
const files = await listMarkdownFiles(repoPath, roots, { exclude: options.exclude });
|
|
30
|
+
const docStore = new DocumentStore();
|
|
31
|
+
await docStore.initialize((await readDocumentStoreExport(cacheDir)) ?? undefined);
|
|
32
|
+
const vectorStore = new VectorStore({ dimensions: EMBEDDING_DIMENSIONS });
|
|
33
|
+
const hasExistingVectors = await vectorIndexExists(cacheDir);
|
|
34
|
+
if (hasExistingVectors && !options.force) {
|
|
35
|
+
const sidecar = await readVectorSidecar(cacheDir);
|
|
36
|
+
if (sidecar) {
|
|
37
|
+
await vectorStore.loadFromDisk(paths(cacheDir).vectorIndex, sidecar);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
await vectorStore.initialize();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
await vectorStore.initialize();
|
|
45
|
+
}
|
|
46
|
+
const chunker = new Chunker({ maxChunkSize: CHUNK_SIZE, overlapPercent: CHUNK_OVERLAP });
|
|
47
|
+
const currentPaths = new Set(files.map((f) => f.path));
|
|
48
|
+
const changed = [];
|
|
49
|
+
for (const f of files) {
|
|
50
|
+
const entry = manifest.files[f.path];
|
|
51
|
+
if (!entry || entry.mtimeMs !== f.mtimeMs)
|
|
52
|
+
changed.push(f);
|
|
53
|
+
}
|
|
54
|
+
// Files that were indexed under this manifest for this scope but no longer exist on disk.
|
|
55
|
+
const removed = Object.keys(manifest.files).filter((p) => {
|
|
56
|
+
const inScope = roots.some((r) => p === r || p.startsWith(r + "/"));
|
|
57
|
+
return inScope && !currentPaths.has(p);
|
|
58
|
+
});
|
|
59
|
+
onProgress?.({
|
|
60
|
+
phase: "text-indexing",
|
|
61
|
+
filesProcessed: 0,
|
|
62
|
+
totalFiles: changed.length,
|
|
63
|
+
});
|
|
64
|
+
let chunksIndexed = 0;
|
|
65
|
+
const changedDocsForEmbedding = [];
|
|
66
|
+
for (const path of removed) {
|
|
67
|
+
await docStore.deleteByFilePath(path);
|
|
68
|
+
await vectorStore.deleteByFilePath(path);
|
|
69
|
+
delete manifest.files[path];
|
|
70
|
+
}
|
|
71
|
+
for (let i = 0; i < changed.length; i++) {
|
|
72
|
+
const file = changed[i];
|
|
73
|
+
onProgress?.({
|
|
74
|
+
phase: "text-indexing",
|
|
75
|
+
filesProcessed: i,
|
|
76
|
+
totalFiles: changed.length,
|
|
77
|
+
currentFile: file.path,
|
|
78
|
+
});
|
|
79
|
+
const content = await readRepoFile(file.absPath);
|
|
80
|
+
await docStore.deleteByFilePath(file.path);
|
|
81
|
+
await vectorStore.deleteByFilePath(file.path);
|
|
82
|
+
const chunks = chunker.chunk(content, file.path);
|
|
83
|
+
const docs = chunks.map((chunk, idx) => ({
|
|
84
|
+
id: `${file.path}:${idx}`,
|
|
85
|
+
filePath: file.path,
|
|
86
|
+
content: chunk.content,
|
|
87
|
+
chunkIndex: idx,
|
|
88
|
+
mtimeMs: file.mtimeMs,
|
|
89
|
+
startLine: chunk.startLine,
|
|
90
|
+
headingBreadcrumb: chunk.headingBreadcrumb,
|
|
91
|
+
}));
|
|
92
|
+
await docStore.indexDocuments(docs);
|
|
93
|
+
chunksIndexed += docs.length;
|
|
94
|
+
changedDocsForEmbedding.push(...docs);
|
|
95
|
+
manifest.files[file.path] = { mtimeMs: file.mtimeMs };
|
|
96
|
+
}
|
|
97
|
+
let embeddingsGenerated = 0;
|
|
98
|
+
if (!options.skipEmbeddings && changedDocsForEmbedding.length > 0) {
|
|
99
|
+
const embeddingService = new EmbeddingService();
|
|
100
|
+
await embeddingService.initialize();
|
|
101
|
+
onProgress?.({
|
|
102
|
+
phase: "embedding",
|
|
103
|
+
filesProcessed: 0,
|
|
104
|
+
totalFiles: changedDocsForEmbedding.length,
|
|
105
|
+
});
|
|
106
|
+
const BATCH = 16;
|
|
107
|
+
for (let i = 0; i < changedDocsForEmbedding.length; i += BATCH) {
|
|
108
|
+
const batch = changedDocsForEmbedding.slice(i, i + BATCH);
|
|
109
|
+
const embeddings = await embeddingService.embedBatch(batch.map((d) => d.content));
|
|
110
|
+
for (let j = 0; j < batch.length; j++) {
|
|
111
|
+
const doc = batch[j];
|
|
112
|
+
await vectorStore.add({
|
|
113
|
+
id: doc.id,
|
|
114
|
+
filePath: doc.filePath,
|
|
115
|
+
chunkIndex: doc.chunkIndex,
|
|
116
|
+
embedding: embeddings[j],
|
|
117
|
+
});
|
|
118
|
+
embeddingsGenerated++;
|
|
119
|
+
}
|
|
120
|
+
onProgress?.({
|
|
121
|
+
phase: "embedding",
|
|
122
|
+
filesProcessed: Math.min(i + BATCH, changedDocsForEmbedding.length),
|
|
123
|
+
totalFiles: changedDocsForEmbedding.length,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Persist.
|
|
128
|
+
await writeDocumentStoreExport(cacheDir, docStore.export());
|
|
129
|
+
if (!options.skipEmbeddings) {
|
|
130
|
+
const sidecar = await vectorStore.saveToDisk(paths(cacheDir).vectorIndex);
|
|
131
|
+
await writeVectorSidecar(cacheDir, sidecar);
|
|
132
|
+
}
|
|
133
|
+
const updatedManifest = {
|
|
134
|
+
...manifest,
|
|
135
|
+
embeddingModel: EMBEDDING_MODEL_ID,
|
|
136
|
+
indexedScopes: Array.from(new Set([...manifest.indexedScopes, scope])),
|
|
137
|
+
lastIndexedAt: Date.now(),
|
|
138
|
+
};
|
|
139
|
+
await writeManifest(cacheDir, updatedManifest);
|
|
140
|
+
onProgress?.({ phase: "done", filesProcessed: files.length, totalFiles: files.length });
|
|
141
|
+
return {
|
|
142
|
+
scope,
|
|
143
|
+
filesScanned: files.length,
|
|
144
|
+
filesChanged: changed.length,
|
|
145
|
+
filesRemoved: removed.length,
|
|
146
|
+
chunksIndexed,
|
|
147
|
+
embeddingsGenerated,
|
|
148
|
+
totalDurationMs: Date.now() - start,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=indexing-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexing-service.js","sourceRoot":"","sources":["../../src/indexing/indexing-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapted from obsidian-qmd (MIT): https://github.com/thirteen37/obsidian-qmd
|
|
3
|
+
* Source: src/search/SearchService.ts. Carried over: the core idea of
|
|
4
|
+
* Reciprocal Rank Fusion over independently-ranked BM25 and vector result
|
|
5
|
+
* lists, with small positional bonuses for top ranks (RRF_K=60, same
|
|
6
|
+
* constant as upstream). Dropped: `QueryExpansionService` and
|
|
7
|
+
* `RerankingService` integration — both call out to an LLM, which is out of
|
|
8
|
+
* scope for this offline CLI (the brief's three dependencies are minisearch,
|
|
9
|
+
* @xenova/transformers, and hnswlib-node; no LLM client). Also dropped:
|
|
10
|
+
* `groupByFile`/section dedup UI conveniences not needed by a line-oriented
|
|
11
|
+
* CLI, since callers get raw scored chunks that already carry a line number.
|
|
12
|
+
*/
|
|
13
|
+
import { DocumentStore } from "../storage/document-store.js";
|
|
14
|
+
import { VectorStore } from "../storage/vector-store.js";
|
|
15
|
+
import { EmbeddingService } from "../embeddings/embedding-service.js";
|
|
16
|
+
import type { Scope, SearchMode } from "../types.js";
|
|
17
|
+
import type { HighlightMode } from "./snippet.js";
|
|
18
|
+
export interface HybridResult {
|
|
19
|
+
id: string;
|
|
20
|
+
filePath: string;
|
|
21
|
+
chunkIndex: number;
|
|
22
|
+
startLine: number;
|
|
23
|
+
textScore?: number;
|
|
24
|
+
vectorScore?: number;
|
|
25
|
+
rrfScore?: number;
|
|
26
|
+
snippet?: string;
|
|
27
|
+
content?: string;
|
|
28
|
+
headingBreadcrumb?: string[];
|
|
29
|
+
source: "text" | "vector" | "hybrid";
|
|
30
|
+
}
|
|
31
|
+
export interface SearchOptions {
|
|
32
|
+
mode: SearchMode;
|
|
33
|
+
scope: Scope;
|
|
34
|
+
limit?: number;
|
|
35
|
+
/** How matched terms should be marked in generated snippets. Defaults to
|
|
36
|
+
* "plain" (no markup) when omitted. */
|
|
37
|
+
highlight?: HighlightMode;
|
|
38
|
+
/** Repo root used to read each result's full source file from disk for
|
|
39
|
+
* snippet generation. When omitted, snippets fall back to chunk-only
|
|
40
|
+
* generation. */
|
|
41
|
+
repoRoot?: string;
|
|
42
|
+
}
|
|
43
|
+
export declare class SearchService {
|
|
44
|
+
private docStore;
|
|
45
|
+
private vectorStore;
|
|
46
|
+
private embeddingService;
|
|
47
|
+
constructor(docStore: DocumentStore, vectorStore: VectorStore, embeddingService: EmbeddingService | null);
|
|
48
|
+
search(query: string, options: SearchOptions): Promise<HybridResult[]>;
|
|
49
|
+
private textSearch;
|
|
50
|
+
private semanticSearch;
|
|
51
|
+
private hybridSearch;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=search-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-service.d.ts","sourceRoot":"","sources":["../../src/search/search-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AACtE,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;CACtC;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;2CACuC;IACvC,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B;;qBAEiB;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAID,qBAAa,aAAa;IAEtB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,WAAW;IACnB,OAAO,CAAC,gBAAgB;gBAFhB,QAAQ,EAAE,aAAa,EACvB,WAAW,EAAE,WAAW,EACxB,gBAAgB,EAAE,gBAAgB,GAAG,IAAI;IAG7C,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;YAkB9D,UAAU;YAqBV,cAAc;YAwCd,YAAY;CAqF3B"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { extractQueryTerms, buildSnippet } from "./snippet.js";
|
|
2
|
+
const RRF_K = 60;
|
|
3
|
+
export class SearchService {
|
|
4
|
+
docStore;
|
|
5
|
+
vectorStore;
|
|
6
|
+
embeddingService;
|
|
7
|
+
constructor(docStore, vectorStore, embeddingService) {
|
|
8
|
+
this.docStore = docStore;
|
|
9
|
+
this.vectorStore = vectorStore;
|
|
10
|
+
this.embeddingService = embeddingService;
|
|
11
|
+
}
|
|
12
|
+
async search(query, options) {
|
|
13
|
+
const trimmed = query.trim();
|
|
14
|
+
if (!trimmed)
|
|
15
|
+
return [];
|
|
16
|
+
const limit = options.limit ?? 10;
|
|
17
|
+
const highlight = options.highlight ?? "plain";
|
|
18
|
+
const repoRoot = options.repoRoot;
|
|
19
|
+
switch (options.mode) {
|
|
20
|
+
case "keyword":
|
|
21
|
+
return this.textSearch(trimmed, options.scope, limit, highlight, repoRoot);
|
|
22
|
+
case "semantic":
|
|
23
|
+
return this.semanticSearch(trimmed, options.scope, limit, highlight, repoRoot);
|
|
24
|
+
case "hybrid":
|
|
25
|
+
return this.hybridSearch(trimmed, options.scope, limit, highlight, repoRoot);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async textSearch(query, scope, limit, highlight, repoRoot) {
|
|
29
|
+
const results = await this.docStore.search(query, { limit, scope, highlight, repoRoot });
|
|
30
|
+
return results.map((r) => ({
|
|
31
|
+
id: r.id,
|
|
32
|
+
filePath: r.filePath,
|
|
33
|
+
chunkIndex: r.chunkIndex,
|
|
34
|
+
startLine: r.startLine,
|
|
35
|
+
textScore: r.score,
|
|
36
|
+
snippet: r.snippet,
|
|
37
|
+
content: r.content,
|
|
38
|
+
headingBreadcrumb: r.headingBreadcrumb,
|
|
39
|
+
source: "text",
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
async semanticSearch(query, scope, limit, highlight, repoRoot) {
|
|
43
|
+
if (!this.embeddingService) {
|
|
44
|
+
throw new Error("Semantic search requires an embedding service");
|
|
45
|
+
}
|
|
46
|
+
const embedding = await this.embeddingService.embed(query);
|
|
47
|
+
const results = await this.vectorStore.search(embedding, limit, scope);
|
|
48
|
+
const queryTerms = extractQueryTerms(query);
|
|
49
|
+
const hybridResults = [];
|
|
50
|
+
for (const r of results) {
|
|
51
|
+
const doc = await this.docStore.getDocument(r.id);
|
|
52
|
+
hybridResults.push({
|
|
53
|
+
id: r.id,
|
|
54
|
+
filePath: r.filePath,
|
|
55
|
+
chunkIndex: r.chunkIndex,
|
|
56
|
+
startLine: doc?.startLine ?? 0,
|
|
57
|
+
vectorScore: r.score,
|
|
58
|
+
content: doc?.content,
|
|
59
|
+
headingBreadcrumb: doc?.headingBreadcrumb,
|
|
60
|
+
snippet: doc
|
|
61
|
+
? await buildSnippet({ filePath: r.filePath, content: doc.content, startLine: doc.startLine }, queryTerms, highlight, repoRoot)
|
|
62
|
+
: undefined,
|
|
63
|
+
source: "vector",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return hybridResults;
|
|
67
|
+
}
|
|
68
|
+
async hybridSearch(query, scope, limit, highlight, repoRoot) {
|
|
69
|
+
const [textResults, vectorResults] = await Promise.all([
|
|
70
|
+
this.docStore.search(query, { limit: limit * 2, scope, highlight, repoRoot }),
|
|
71
|
+
this.embeddingService
|
|
72
|
+
? this.vectorStore.search(await this.embeddingService.embed(query), limit * 2, scope)
|
|
73
|
+
: Promise.resolve([]),
|
|
74
|
+
]);
|
|
75
|
+
const byId = new Map();
|
|
76
|
+
textResults.forEach((r, index) => {
|
|
77
|
+
byId.set(r.id, {
|
|
78
|
+
id: r.id,
|
|
79
|
+
filePath: r.filePath,
|
|
80
|
+
chunkIndex: r.chunkIndex,
|
|
81
|
+
startLine: r.startLine,
|
|
82
|
+
textScore: r.score,
|
|
83
|
+
textRank: index + 1,
|
|
84
|
+
snippet: r.snippet,
|
|
85
|
+
content: r.content,
|
|
86
|
+
headingBreadcrumb: r.headingBreadcrumb,
|
|
87
|
+
source: "text",
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
const queryTerms = extractQueryTerms(query);
|
|
91
|
+
for (const [index, r] of vectorResults.entries()) {
|
|
92
|
+
const existing = byId.get(r.id);
|
|
93
|
+
if (existing) {
|
|
94
|
+
existing.vectorScore = r.score;
|
|
95
|
+
existing.vectorRank = index + 1;
|
|
96
|
+
existing.source = "hybrid";
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
const doc = await this.docStore.getDocument(r.id);
|
|
100
|
+
byId.set(r.id, {
|
|
101
|
+
id: r.id,
|
|
102
|
+
filePath: r.filePath,
|
|
103
|
+
chunkIndex: r.chunkIndex,
|
|
104
|
+
startLine: doc?.startLine ?? 0,
|
|
105
|
+
vectorScore: r.score,
|
|
106
|
+
vectorRank: index + 1,
|
|
107
|
+
content: doc?.content,
|
|
108
|
+
headingBreadcrumb: doc?.headingBreadcrumb,
|
|
109
|
+
snippet: doc
|
|
110
|
+
? await buildSnippet({ filePath: r.filePath, content: doc.content, startLine: doc.startLine }, queryTerms, highlight, repoRoot)
|
|
111
|
+
: undefined,
|
|
112
|
+
source: "vector",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const results = Array.from(byId.values()).map((r) => {
|
|
117
|
+
let rrfScore = 0;
|
|
118
|
+
if (r.textRank) {
|
|
119
|
+
rrfScore += 1 / (RRF_K + r.textRank);
|
|
120
|
+
if (r.textRank === 1)
|
|
121
|
+
rrfScore += 0.05;
|
|
122
|
+
else if (r.textRank <= 3)
|
|
123
|
+
rrfScore += 0.02;
|
|
124
|
+
}
|
|
125
|
+
if (r.vectorRank) {
|
|
126
|
+
rrfScore += 1 / (RRF_K + r.vectorRank);
|
|
127
|
+
if (r.vectorRank === 1)
|
|
128
|
+
rrfScore += 0.05;
|
|
129
|
+
else if (r.vectorRank <= 3)
|
|
130
|
+
rrfScore += 0.02;
|
|
131
|
+
}
|
|
132
|
+
const { textRank, vectorRank, ...clean } = r;
|
|
133
|
+
return { ...clean, rrfScore };
|
|
134
|
+
});
|
|
135
|
+
results.sort((a, b) => (b.rrfScore ?? 0) - (a.rrfScore ?? 0));
|
|
136
|
+
return results.slice(0, limit);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=search-service.js.map
|