@nano-step/nano-brain 2026.1.14
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/.opencode/command/nano-brain-init.md +13 -0
- package/.opencode/command/nano-brain-reindex.md +11 -0
- package/.opencode/command/nano-brain-status.md +12 -0
- package/AGENTS.md +41 -0
- package/AGENTS_SNIPPET.md +44 -0
- package/CHANGELOG.md +186 -0
- package/README.md +298 -0
- package/SKILL.md +109 -0
- package/bin/cli.js +29 -0
- package/commands/nano-brain-init.md +36 -0
- package/commands/nano-brain-reindex.md +31 -0
- package/commands/nano-brain-status.md +32 -0
- package/index.html +929 -0
- package/nano-brain +4 -0
- package/opencode-mcp.json +9 -0
- package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/design.md +68 -0
- package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/proposal.md +27 -0
- package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/specs/mcp-integration-testing/spec.md +50 -0
- package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/specs/mcp-server/spec.md +40 -0
- package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/specs/search-pipeline/spec.md +29 -0
- package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/tasks.md +37 -0
- package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/design.md +111 -0
- package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/proposal.md +30 -0
- package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/specs/mcp-server/spec.md +33 -0
- package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/specs/storage-limits/spec.md +90 -0
- package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/specs/workspace-scoping/spec.md +66 -0
- package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/tasks.md +199 -0
- package/openspec/changes/codebase-indexing/.openspec.yaml +2 -0
- package/openspec/changes/codebase-indexing/design.md +169 -0
- package/openspec/changes/codebase-indexing/proposal.md +30 -0
- package/openspec/changes/codebase-indexing/specs/codebase-collection/spec.md +187 -0
- package/openspec/changes/codebase-indexing/specs/mcp-server/spec.md +36 -0
- package/openspec/changes/codebase-indexing/tasks.md +56 -0
- package/openspec/changes/fix-session-harvest-workspace-scoping/.openspec.yaml +2 -0
- package/openspec/changes/fix-session-harvest-workspace-scoping/design.md +84 -0
- package/openspec/changes/fix-session-harvest-workspace-scoping/proposal.md +26 -0
- package/openspec/changes/fix-session-harvest-workspace-scoping/specs/workspace-scoping/spec.md +65 -0
- package/openspec/changes/fix-session-harvest-workspace-scoping/tasks.md +33 -0
- package/openspec/changes/performance-and-search-quality/.openspec.yaml +2 -0
- package/openspec/changes/performance-and-search-quality/proposal.md +37 -0
- package/openspec/specs/mcp-integration-testing/spec.md +50 -0
- package/openspec/specs/mcp-server/spec.md +75 -0
- package/openspec/specs/search-pipeline/spec.md +29 -0
- package/openspec/specs/storage-limits/spec.md +94 -0
- package/openspec/specs/workspace-scoping/spec.md +70 -0
- package/package.json +37 -0
- package/site/build.js +66 -0
- package/site/partials/_api.html +83 -0
- package/site/partials/_compare.html +100 -0
- package/site/partials/_config.html +23 -0
- package/site/partials/_features.html +43 -0
- package/site/partials/_footer.html +6 -0
- package/site/partials/_hero.html +9 -0
- package/site/partials/_how-it-works.html +26 -0
- package/site/partials/_models.html +18 -0
- package/site/partials/_quick-start.html +15 -0
- package/site/partials/_stats.html +1 -0
- package/site/partials/_tech-stack.html +13 -0
- package/site/script.js +12 -0
- package/site/shell.html +44 -0
- package/site/styles.css +548 -0
- package/src/chunker.ts +427 -0
- package/src/codebase.ts +425 -0
- package/src/collections.ts +217 -0
- package/src/embeddings.ts +325 -0
- package/src/expansion.ts +79 -0
- package/src/harvester.ts +306 -0
- package/src/index.ts +778 -0
- package/src/reranker.ts +103 -0
- package/src/search.ts +294 -0
- package/src/server.ts +876 -0
- package/src/storage.ts +221 -0
- package/src/store.ts +653 -0
- package/src/types.ts +215 -0
- package/src/watcher.ts +389 -0
- package/test/chunker.test.ts +479 -0
- package/test/cli.test.ts +309 -0
- package/test/codebase-chunker.test.ts +446 -0
- package/test/codebase.test.ts +678 -0
- package/test/collections.test.ts +571 -0
- package/test/harvester.test.ts +636 -0
- package/test/integration.test.ts +219 -0
- package/test/llm.test.ts +322 -0
- package/test/search.test.ts +572 -0
- package/test/server.test.ts +541 -0
- package/test/storage.test.ts +302 -0
- package/test/store.test.ts +530 -0
- package/test/watcher.test.ts +717 -0
- package/test/workspace.test.ts +239 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +16 -0
package/src/reranker.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { getLlama } from 'node-llama-cpp';
|
|
2
|
+
import { cpus } from 'os';
|
|
3
|
+
import { resolveModelPath } from './embeddings.js';
|
|
4
|
+
import type { RerankResult, RerankDocument } from './types.js';
|
|
5
|
+
|
|
6
|
+
export interface Reranker {
|
|
7
|
+
rerank(query: string, documents: RerankDocument[]): Promise<RerankResult>;
|
|
8
|
+
dispose(): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface RerankerOptions {
|
|
12
|
+
modelPath?: string;
|
|
13
|
+
cacheDir?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const DEFAULT_MODEL_URI = 'hf:gpustack/bge-reranker-v2-m3-GGUF/bge-reranker-v2-m3-Q4_K_M.gguf';
|
|
17
|
+
const MODEL_NAME = 'bge-reranker-v2-m3';
|
|
18
|
+
const CONTEXT_SIZE = 8192;
|
|
19
|
+
|
|
20
|
+
function sigmoid(x: number): number {
|
|
21
|
+
return 1 / (1 + Math.exp(-x));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class RerankerImpl implements Reranker {
|
|
25
|
+
private contexts: any[] = [];
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
private model: any,
|
|
29
|
+
private parallelism: number
|
|
30
|
+
) {}
|
|
31
|
+
|
|
32
|
+
async initialize(): Promise<void> {
|
|
33
|
+
for (let i = 0; i < this.parallelism; i++) {
|
|
34
|
+
const context = await this.model.createContext({
|
|
35
|
+
contextSize: CONTEXT_SIZE,
|
|
36
|
+
});
|
|
37
|
+
this.contexts.push(context);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async rerank(query: string, documents: RerankDocument[]): Promise<RerankResult> {
|
|
42
|
+
const scoredDocs: Array<{ file: string; score: number; index: number }> = [];
|
|
43
|
+
|
|
44
|
+
const batchSize = Math.min(4, this.parallelism);
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < documents.length; i += batchSize) {
|
|
47
|
+
const batch = documents.slice(i, i + batchSize);
|
|
48
|
+
const batchPromises = batch.map(async (doc, idx) => {
|
|
49
|
+
const contextIdx = idx % this.contexts.length;
|
|
50
|
+
const context = this.contexts[contextIdx];
|
|
51
|
+
|
|
52
|
+
const prompt = `Query: ${query}\nDocument: ${doc.text}`;
|
|
53
|
+
|
|
54
|
+
const result = await context.evaluate([prompt]);
|
|
55
|
+
const rawScore = result?.logits?.[0] || 0;
|
|
56
|
+
const normalizedScore = sigmoid(rawScore);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
file: doc.file,
|
|
60
|
+
score: normalizedScore,
|
|
61
|
+
index: doc.index,
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const batchResults = await Promise.all(batchPromises);
|
|
66
|
+
scoredDocs.push(...batchResults);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
scoredDocs.sort((a, b) => b.score - a.score);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
results: scoredDocs,
|
|
73
|
+
model: MODEL_NAME,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
dispose(): void {
|
|
78
|
+
this.contexts = [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function createReranker(
|
|
83
|
+
options?: RerankerOptions
|
|
84
|
+
): Promise<Reranker | null> {
|
|
85
|
+
try {
|
|
86
|
+
const modelUri = options?.modelPath || DEFAULT_MODEL_URI;
|
|
87
|
+
const modelPath = await resolveModelPath(modelUri, options?.cacheDir);
|
|
88
|
+
|
|
89
|
+
const llama = await getLlama();
|
|
90
|
+
const model = await llama.loadModel({ modelPath });
|
|
91
|
+
|
|
92
|
+
const cpuCount = cpus().length;
|
|
93
|
+
const parallelism = Math.max(1, Math.min(4, Math.floor(cpuCount / 4)));
|
|
94
|
+
|
|
95
|
+
const reranker = new RerankerImpl(model, parallelism);
|
|
96
|
+
await reranker.initialize();
|
|
97
|
+
|
|
98
|
+
return reranker;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.warn('Failed to load reranker model:', error instanceof Error ? error.message : String(error));
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/search.ts
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import type { SearchResult, Store } from './types.js';
|
|
2
|
+
import { computeHash } from './store.js';
|
|
3
|
+
|
|
4
|
+
export interface SearchOptions {
|
|
5
|
+
query: string;
|
|
6
|
+
limit?: number;
|
|
7
|
+
collection?: string;
|
|
8
|
+
useVec?: boolean;
|
|
9
|
+
rerank?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface HybridSearchOptions {
|
|
13
|
+
query: string;
|
|
14
|
+
limit?: number;
|
|
15
|
+
collection?: string;
|
|
16
|
+
minScore?: number;
|
|
17
|
+
useExpansion?: boolean;
|
|
18
|
+
useReranking?: boolean;
|
|
19
|
+
topK?: number;
|
|
20
|
+
projectHash?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SearchProviders {
|
|
24
|
+
embedder?: { embed(text: string): Promise<{ embedding: number[] }> } | null;
|
|
25
|
+
reranker?: { rerank(query: string, docs: any[]): Promise<{ results: Array<{ file: string; score: number; index: number }> }> } | null;
|
|
26
|
+
expander?: { expand(query: string): Promise<string[]> } | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function searchFTS(
|
|
30
|
+
store: Store,
|
|
31
|
+
query: string,
|
|
32
|
+
options?: { limit?: number; collection?: string }
|
|
33
|
+
): SearchResult[] {
|
|
34
|
+
const limit = options?.limit;
|
|
35
|
+
const collection = options?.collection;
|
|
36
|
+
return store.searchFTS(query, limit, collection);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function searchVec(
|
|
40
|
+
store: Store,
|
|
41
|
+
query: string,
|
|
42
|
+
embedding: number[],
|
|
43
|
+
options?: { limit?: number; collection?: string }
|
|
44
|
+
): SearchResult[] {
|
|
45
|
+
const limit = options?.limit;
|
|
46
|
+
const collection = options?.collection;
|
|
47
|
+
return store.searchVec(query, embedding, limit, collection);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function rrfFuse(
|
|
51
|
+
resultSets: SearchResult[][],
|
|
52
|
+
k: number = 60,
|
|
53
|
+
weights?: number[]
|
|
54
|
+
): SearchResult[] {
|
|
55
|
+
const scoreMap = new Map<string, { result: SearchResult; score: number }>();
|
|
56
|
+
|
|
57
|
+
resultSets.forEach((results, setIndex) => {
|
|
58
|
+
const weight = weights?.[setIndex] ?? 1;
|
|
59
|
+
|
|
60
|
+
results.forEach((result, rank) => {
|
|
61
|
+
const rrfScore = weight / (k + rank + 1);
|
|
62
|
+
|
|
63
|
+
const existing = scoreMap.get(result.id);
|
|
64
|
+
if (existing) {
|
|
65
|
+
existing.score += rrfScore;
|
|
66
|
+
} else {
|
|
67
|
+
scoreMap.set(result.id, {
|
|
68
|
+
result: { ...result },
|
|
69
|
+
score: rrfScore,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const merged = Array.from(scoreMap.values()).map(({ result, score }) => ({
|
|
76
|
+
...result,
|
|
77
|
+
score,
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
return merged.sort((a, b) => b.score - a.score);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function applyTopRankBonus(
|
|
84
|
+
results: SearchResult[],
|
|
85
|
+
originalFtsResults: SearchResult[]
|
|
86
|
+
): SearchResult[] {
|
|
87
|
+
const bonusMap = new Map<string, number>();
|
|
88
|
+
|
|
89
|
+
if (originalFtsResults.length > 0) {
|
|
90
|
+
bonusMap.set(originalFtsResults[0].id, 0.05);
|
|
91
|
+
}
|
|
92
|
+
if (originalFtsResults.length > 1) {
|
|
93
|
+
bonusMap.set(originalFtsResults[1].id, 0.02);
|
|
94
|
+
}
|
|
95
|
+
if (originalFtsResults.length > 2) {
|
|
96
|
+
bonusMap.set(originalFtsResults[2].id, 0.02);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const boosted = results.map(r => ({
|
|
100
|
+
...r,
|
|
101
|
+
score: r.score + (bonusMap.get(r.id) ?? 0),
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
return boosted.sort((a, b) => b.score - a.score);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function positionAwareBlend(
|
|
108
|
+
rrfResults: SearchResult[],
|
|
109
|
+
rerankScores: Map<string, number>
|
|
110
|
+
): SearchResult[] {
|
|
111
|
+
const blended = rrfResults.map((result, index) => {
|
|
112
|
+
const rerankScore = rerankScores.get(result.id);
|
|
113
|
+
|
|
114
|
+
if (rerankScore === undefined) {
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let rrfWeight: number;
|
|
119
|
+
let rerankWeight: number;
|
|
120
|
+
|
|
121
|
+
if (index <= 2) {
|
|
122
|
+
rrfWeight = 0.75;
|
|
123
|
+
rerankWeight = 0.25;
|
|
124
|
+
} else if (index <= 9) {
|
|
125
|
+
rrfWeight = 0.60;
|
|
126
|
+
rerankWeight = 0.40;
|
|
127
|
+
} else {
|
|
128
|
+
rrfWeight = 0.40;
|
|
129
|
+
rerankWeight = 0.60;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const finalScore = rrfWeight * result.score + rerankWeight * rerankScore;
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
...result,
|
|
136
|
+
score: finalScore,
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return blended.sort((a, b) => b.score - a.score);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function formatSnippet(text: string, maxLength: number = 700): string {
|
|
144
|
+
if (text.length <= maxLength) {
|
|
145
|
+
return text;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const truncated = text.substring(0, maxLength);
|
|
149
|
+
const lastSpace = truncated.lastIndexOf(' ');
|
|
150
|
+
|
|
151
|
+
if (lastSpace > maxLength * 0.8) {
|
|
152
|
+
return truncated.substring(0, lastSpace) + '...';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return truncated + '...';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function cacheHash(prefix: string, ...parts: string[]): string {
|
|
159
|
+
return computeHash(prefix + ':' + parts.join(':'));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function hybridSearch(
|
|
163
|
+
store: Store,
|
|
164
|
+
options: HybridSearchOptions,
|
|
165
|
+
providers: SearchProviders = {}
|
|
166
|
+
): Promise<SearchResult[]> {
|
|
167
|
+
const {
|
|
168
|
+
query,
|
|
169
|
+
limit = 10,
|
|
170
|
+
collection,
|
|
171
|
+
minScore = 0,
|
|
172
|
+
useExpansion = true,
|
|
173
|
+
useReranking = true,
|
|
174
|
+
topK = 30,
|
|
175
|
+
projectHash,
|
|
176
|
+
} = options;
|
|
177
|
+
|
|
178
|
+
const { embedder, reranker, expander } = providers;
|
|
179
|
+
|
|
180
|
+
let queries: string[] = [query];
|
|
181
|
+
|
|
182
|
+
if (useExpansion && expander) {
|
|
183
|
+
const expansionCacheKey = cacheHash('expand', query);
|
|
184
|
+
const cached = store.getCachedResult(expansionCacheKey);
|
|
185
|
+
|
|
186
|
+
if (cached) {
|
|
187
|
+
try {
|
|
188
|
+
const variants = JSON.parse(cached) as string[];
|
|
189
|
+
queries = [query, ...variants];
|
|
190
|
+
} catch {
|
|
191
|
+
queries = [query];
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
try {
|
|
195
|
+
const variants = await expander.expand(query);
|
|
196
|
+
store.setCachedResult(expansionCacheKey, JSON.stringify(variants));
|
|
197
|
+
queries = [query, ...variants];
|
|
198
|
+
} catch {
|
|
199
|
+
queries = [query];
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const allResultSets: SearchResult[][] = [];
|
|
205
|
+
const weights: number[] = [];
|
|
206
|
+
|
|
207
|
+
for (let i = 0; i < queries.length; i++) {
|
|
208
|
+
const q = queries[i];
|
|
209
|
+
const isOriginal = i === 0;
|
|
210
|
+
const weight = isOriginal ? 2 : 1;
|
|
211
|
+
|
|
212
|
+
const ftsResults = store.searchFTS(q, topK, collection, projectHash);
|
|
213
|
+
allResultSets.push(ftsResults);
|
|
214
|
+
weights.push(weight);
|
|
215
|
+
|
|
216
|
+
if (embedder) {
|
|
217
|
+
try {
|
|
218
|
+
const { embedding } = await embedder.embed(q);
|
|
219
|
+
const vecResults = store.searchVec(q, embedding, topK, collection, projectHash);
|
|
220
|
+
allResultSets.push(vecResults);
|
|
221
|
+
weights.push(weight);
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const originalFtsResults = allResultSets[0] || [];
|
|
228
|
+
|
|
229
|
+
let fusedResults = rrfFuse(allResultSets, 60, weights);
|
|
230
|
+
|
|
231
|
+
fusedResults = applyTopRankBonus(fusedResults, originalFtsResults);
|
|
232
|
+
|
|
233
|
+
const candidates = fusedResults.slice(0, topK);
|
|
234
|
+
|
|
235
|
+
if (useReranking && reranker && candidates.length > 0) {
|
|
236
|
+
const candidateIds = candidates.map(c => c.id).join(',');
|
|
237
|
+
const rerankCacheKey = cacheHash('rerank', query, candidateIds);
|
|
238
|
+
const cachedRerank = store.getCachedResult(rerankCacheKey);
|
|
239
|
+
|
|
240
|
+
let rerankScores = new Map<string, number>();
|
|
241
|
+
|
|
242
|
+
if (cachedRerank) {
|
|
243
|
+
try {
|
|
244
|
+
const parsed = JSON.parse(cachedRerank) as Array<{ file: string; score: number }>;
|
|
245
|
+
parsed.forEach(r => rerankScores.set(r.file, r.score));
|
|
246
|
+
} catch {
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
try {
|
|
250
|
+
const docs = candidates.map((c, index) => ({
|
|
251
|
+
text: c.snippet,
|
|
252
|
+
file: c.id,
|
|
253
|
+
index,
|
|
254
|
+
}));
|
|
255
|
+
|
|
256
|
+
const rerankResult = await reranker.rerank(query, docs);
|
|
257
|
+
|
|
258
|
+
rerankResult.results.forEach(r => {
|
|
259
|
+
rerankScores.set(r.file, r.score);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const cacheData = rerankResult.results.map(r => ({
|
|
263
|
+
file: r.file,
|
|
264
|
+
score: r.score,
|
|
265
|
+
}));
|
|
266
|
+
store.setCachedResult(rerankCacheKey, JSON.stringify(cacheData));
|
|
267
|
+
} catch {
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
fusedResults = positionAwareBlend(candidates, rerankScores);
|
|
272
|
+
} else {
|
|
273
|
+
fusedResults = candidates;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let filtered = fusedResults;
|
|
277
|
+
if (minScore > 0) {
|
|
278
|
+
filtered = fusedResults.filter(r => r.score >= minScore);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const final = filtered.slice(0, limit);
|
|
282
|
+
|
|
283
|
+
return final.map(r => ({
|
|
284
|
+
...r,
|
|
285
|
+
snippet: formatSnippet(r.snippet, 700),
|
|
286
|
+
}));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export async function search(
|
|
290
|
+
store: Store,
|
|
291
|
+
options: SearchOptions
|
|
292
|
+
): Promise<SearchResult[]> {
|
|
293
|
+
return store.searchFTS(options.query, options.limit, options.collection);
|
|
294
|
+
}
|