mikoshi 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.
@@ -0,0 +1,90 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ const COMMON_IGNORES = [
5
+ ".git/",
6
+ "node_modules/",
7
+ "dist/",
8
+ "build/",
9
+ "coverage/",
10
+ ".pytest_cache/",
11
+ ".venv/",
12
+ ".mypy_cache/",
13
+ ".ruff_cache/",
14
+ ".idea/",
15
+ ".vscode/",
16
+ "*.pyc",
17
+ "*.pyo",
18
+ "*.pyd",
19
+ "*.so",
20
+ "*.dylib",
21
+ "*.dll",
22
+ "*.exe",
23
+ "*.DS_Store",
24
+ "*.log",
25
+ "*.tmp",
26
+ "*.swp",
27
+ "*.swo",
28
+ "*.zip",
29
+ "*.tar",
30
+ "*.gz",
31
+ "*.tgz",
32
+ "*.jpg",
33
+ "*.jpeg",
34
+ "*.png",
35
+ "*.gif",
36
+ "*.bmp",
37
+ "*.ico",
38
+ "*.mp3",
39
+ "*.mp4",
40
+ "*.mov",
41
+ "*.avi",
42
+ "*.pdf",
43
+ ];
44
+
45
+ function globToRegex(glob) {
46
+ const escaped = glob
47
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
48
+ .replace(/\*/g, ".*")
49
+ .replace(/\?/g, ".");
50
+ return new RegExp(`^${escaped}$`);
51
+ }
52
+
53
+ export class IgnoreMatcher {
54
+ constructor(repoRoot) {
55
+ this.repoRoot = repoRoot;
56
+ this.patterns = [];
57
+ this._loadPatterns();
58
+ }
59
+
60
+ _loadPatterns() {
61
+ const addPattern = (pattern) => {
62
+ const trimmed = pattern.trim();
63
+ if (!trimmed || trimmed.startsWith("#")) return;
64
+ this.patterns.push(trimmed);
65
+ };
66
+
67
+ COMMON_IGNORES.forEach(addPattern);
68
+
69
+ const gitignorePath = path.join(this.repoRoot, ".gitignore");
70
+ if (fs.existsSync(gitignorePath)) {
71
+ const content = fs.readFileSync(gitignorePath, "utf8");
72
+ content.split(/\r?\n/).forEach(addPattern);
73
+ }
74
+ }
75
+
76
+ ignores(relPath) {
77
+ for (const pattern of this.patterns) {
78
+ if (pattern.endsWith("/")) {
79
+ if (relPath.startsWith(pattern)) return true;
80
+ continue;
81
+ }
82
+ if (pattern.includes("*")) {
83
+ if (globToRegex(pattern).test(relPath)) return true;
84
+ continue;
85
+ }
86
+ if (relPath === pattern || relPath.endsWith(`/${pattern}`)) return true;
87
+ }
88
+ return false;
89
+ }
90
+ }
@@ -0,0 +1,39 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ function looksBinary(buffer) {
5
+ const sample = buffer.subarray(0, 8000);
6
+ for (const byte of sample) {
7
+ if (byte === 0) return true;
8
+ }
9
+ return false;
10
+ }
11
+
12
+ function scanDir(dir, repoRoot, matcher, maxBytes, out) {
13
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
14
+ for (const entry of entries) {
15
+ const fullPath = path.join(dir, entry.name);
16
+ const relPath = path.relative(repoRoot, fullPath).split(path.sep).join("/");
17
+ if (matcher.ignores(relPath)) continue;
18
+
19
+ if (entry.isDirectory()) {
20
+ scanDir(fullPath, repoRoot, matcher, maxBytes, out);
21
+ } else if (entry.isFile()) {
22
+ try {
23
+ const stat = fs.statSync(fullPath);
24
+ if (stat.size > maxBytes) continue;
25
+ const data = fs.readFileSync(fullPath);
26
+ if (looksBinary(data)) continue;
27
+ out.push(fullPath);
28
+ } catch {
29
+ continue;
30
+ }
31
+ }
32
+ }
33
+ }
34
+
35
+ export function scanRepo(repoRoot, matcher, maxBytes) {
36
+ const files = [];
37
+ scanDir(repoRoot, repoRoot, matcher, maxBytes, files);
38
+ return files;
39
+ }
@@ -0,0 +1,82 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { sha256Text } from "../hashing.js";
4
+
5
+ export class IndexStore {
6
+ constructor(repoRoot, indexRoot) {
7
+ this.repo_root = repoRoot;
8
+ this.repo_id = repoIdForPath(repoRoot);
9
+ this.store_dir = path.join(indexRoot, this.repo_id);
10
+ this.meta_path = path.join(this.store_dir, "meta.json");
11
+ this.chunks_path = path.join(this.store_dir, "chunks.jsonl");
12
+ this.embeddings_path = path.join(this.store_dir, "embeddings.bin");
13
+ }
14
+
15
+ exists() {
16
+ return fs.existsSync(this.meta_path) && fs.existsSync(this.chunks_path);
17
+ }
18
+
19
+ ensureDir() {
20
+ fs.mkdirSync(this.store_dir, { recursive: true });
21
+ }
22
+
23
+ clear() {
24
+ if (fs.existsSync(this.store_dir)) {
25
+ fs.rmSync(this.store_dir, { recursive: true, force: true });
26
+ }
27
+ }
28
+
29
+ loadMeta() {
30
+ if (!fs.existsSync(this.meta_path)) return null;
31
+ try {
32
+ const data = JSON.parse(fs.readFileSync(this.meta_path, "utf8"));
33
+ return data;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ saveMeta(meta) {
40
+ this.ensureDir();
41
+ fs.writeFileSync(this.meta_path, JSON.stringify(meta, null, 2));
42
+ }
43
+
44
+ loadChunks() {
45
+ if (!fs.existsSync(this.chunks_path)) return [];
46
+ const lines = fs.readFileSync(this.chunks_path, "utf8").split(/\r?\n/);
47
+ const chunks = [];
48
+ for (const line of lines) {
49
+ if (!line.trim()) continue;
50
+ try {
51
+ chunks.push(JSON.parse(line));
52
+ } catch {
53
+ continue;
54
+ }
55
+ }
56
+ return chunks;
57
+ }
58
+
59
+ saveChunks(chunks) {
60
+ this.ensureDir();
61
+ const lines = chunks.map((chunk) => JSON.stringify(chunk)).join("\n");
62
+ fs.writeFileSync(this.chunks_path, lines + (lines ? "\n" : ""));
63
+ }
64
+
65
+ loadEmbeddings(meta) {
66
+ if (!fs.existsSync(this.embeddings_path)) return null;
67
+ const buffer = fs.readFileSync(this.embeddings_path);
68
+ const embeddings = new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 4);
69
+ const dim = meta?.embedding_dim || 0;
70
+ return { embeddings, dim };
71
+ }
72
+
73
+ saveEmbeddings(embeddings) {
74
+ this.ensureDir();
75
+ const buffer = Buffer.from(embeddings.buffer);
76
+ fs.writeFileSync(this.embeddings_path, buffer);
77
+ }
78
+ }
79
+
80
+ export function repoIdForPath(repoRoot) {
81
+ return sha256Text(path.resolve(repoRoot)).slice(0, 16);
82
+ }
@@ -0,0 +1,198 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { chunkText } from "../chunking.js";
4
+ import { loadConfig } from "../config.js";
5
+ import { sha256Bytes, sha256Text } from "../hashing.js";
6
+ import { IgnoreMatcher } from "../ignore.js";
7
+ import { IndexStore } from "./index_store.js";
8
+ import { scanRepo } from "./file_scanner.js";
9
+ import { getEmbeddingsProvider, normalizeEmbeddings } from "../retrieval/semantic.js";
10
+ import { Timer } from "../utils/timer.js";
11
+
12
+ function prepareFile(filePath, repoRoot) {
13
+ const data = fs.readFileSync(filePath);
14
+ const fileHash = sha256Bytes(data);
15
+ const text = data.toString("utf8");
16
+ const relpath = path.relative(repoRoot, filePath).split(path.sep).join("/");
17
+ return { path: filePath, relpath, file_hash: fileHash, text };
18
+ }
19
+
20
+ function chunksFromText(relpath, fileHash, text, maxLines, overlap) {
21
+ const spans = chunkText(text, maxLines, overlap);
22
+ return spans.map((span) => {
23
+ const chunkId = sha256Text(`${relpath}:${span.start_line}:${span.end_line}:${span.text}`);
24
+ return {
25
+ id: chunkId,
26
+ relpath,
27
+ start_line: span.start_line,
28
+ end_line: span.end_line,
29
+ text: span.text,
30
+ file_hash: fileHash,
31
+ vector_idx: null,
32
+ };
33
+ });
34
+ }
35
+
36
+ function withVectorIdx(chunks) {
37
+ return chunks.map((chunk, idx) => ({ ...chunk, vector_idx: idx }));
38
+ }
39
+
40
+ function reuseChunks(prevChunks, prevEmbeddings) {
41
+ const chunksByFile = new Map();
42
+ const embeddingById = new Map();
43
+
44
+ if (prevEmbeddings) {
45
+ for (const chunk of prevChunks) {
46
+ if (chunk.vector_idx == null) continue;
47
+ const offset = chunk.vector_idx;
48
+ if (offset >= 0) embeddingById.set(chunk.id, offset);
49
+ }
50
+ }
51
+
52
+ for (const chunk of prevChunks) {
53
+ if (!chunksByFile.has(chunk.relpath)) chunksByFile.set(chunk.relpath, []);
54
+ chunksByFile.get(chunk.relpath).push(chunk);
55
+ }
56
+
57
+ return { chunksByFile, embeddingById };
58
+ }
59
+
60
+ export async function indexRepo(repoPath, configOverride = null) {
61
+ const config = configOverride || loadConfig();
62
+ const repoRoot = path.resolve(repoPath);
63
+
64
+ const matcher = new IgnoreMatcher(repoRoot);
65
+ const store = new IndexStore(repoRoot, config.index_root);
66
+
67
+ const prevMeta = store.loadMeta();
68
+ const prevChunks = store.loadChunks();
69
+ const prevEmbeddingsData = prevMeta ? store.loadEmbeddings(prevMeta) : null;
70
+ const prevFiles = prevMeta?.files || {};
71
+
72
+ let reset = false;
73
+ if (prevMeta) {
74
+ if (
75
+ prevMeta.chunk_lines !== config.chunk_lines ||
76
+ prevMeta.chunk_overlap !== config.chunk_overlap ||
77
+ prevMeta.max_bytes !== config.max_bytes ||
78
+ prevMeta.embedding_provider !== config.embeddings.provider ||
79
+ prevMeta.model !== config.embeddings.model
80
+ ) {
81
+ reset = true;
82
+ }
83
+ }
84
+
85
+ const prevChunksSafe = reset ? [] : prevChunks;
86
+ const prevEmbeddingsSafe = reset ? null : prevEmbeddingsData;
87
+ const prevFilesSafe = reset ? {} : prevFiles;
88
+
89
+ const { chunksByFile, embeddingById } = reuseChunks(prevChunksSafe, prevEmbeddingsSafe?.embeddings);
90
+
91
+ const timer = new Timer();
92
+ const files = scanRepo(repoRoot, matcher, config.max_bytes);
93
+ const indexedFiles = [];
94
+ for (const filePath of files) {
95
+ try {
96
+ indexedFiles.push(prepareFile(filePath, repoRoot));
97
+ } catch {
98
+ continue;
99
+ }
100
+ }
101
+
102
+ const fileHashes = {};
103
+ for (const item of indexedFiles) fileHashes[item.relpath] = item.file_hash;
104
+
105
+ const newChunks = [];
106
+ const embeddingSlots = [];
107
+ const pendingTexts = [];
108
+ const pendingIndices = [];
109
+
110
+ for (const item of indexedFiles) {
111
+ const relpath = item.relpath;
112
+ const unchanged = prevFilesSafe[relpath] === item.file_hash;
113
+ if (unchanged && chunksByFile.has(relpath) && embeddingById.size) {
114
+ const existing = chunksByFile.get(relpath);
115
+ if (existing.every((chunk) => embeddingById.has(chunk.id))) {
116
+ for (const chunk of existing) {
117
+ newChunks.push(chunk);
118
+ embeddingSlots.push(embeddingById.get(chunk.id));
119
+ }
120
+ continue;
121
+ }
122
+ }
123
+
124
+ const chunks = chunksFromText(
125
+ relpath,
126
+ item.file_hash,
127
+ item.text,
128
+ config.chunk_lines,
129
+ config.chunk_overlap
130
+ );
131
+ for (const chunk of chunks) {
132
+ pendingIndices.push(newChunks.length);
133
+ newChunks.push(chunk);
134
+ embeddingSlots.push(null);
135
+ pendingTexts.push(chunk.text);
136
+ }
137
+ }
138
+
139
+ let provider = null;
140
+ let pendingEmbeddings = [];
141
+ if (pendingTexts.length) {
142
+ provider = getEmbeddingsProvider(config);
143
+ pendingEmbeddings = await provider.embedTexts(pendingTexts);
144
+ }
145
+
146
+ const dimension =
147
+ provider?.dimension ||
148
+ prevEmbeddingsSafe?.dim ||
149
+ prevMeta?.embedding_dim ||
150
+ (pendingEmbeddings[0] ? pendingEmbeddings[0].length : 1);
151
+
152
+ const allEmbeddings = new Float32Array(newChunks.length * dimension);
153
+ for (let i = 0; i < embeddingSlots.length; i += 1) {
154
+ const slot = embeddingSlots[i];
155
+ let vector = null;
156
+ if (slot !== null && prevEmbeddingsSafe) {
157
+ const offset = slot * dimension;
158
+ vector = prevEmbeddingsSafe.embeddings.subarray(offset, offset + dimension);
159
+ }
160
+ if (!vector) {
161
+ const pendingIndex = pendingIndices.indexOf(i);
162
+ if (pendingIndex >= 0) vector = pendingEmbeddings[pendingIndex];
163
+ }
164
+ if (!vector) {
165
+ vector = new Float32Array(dimension);
166
+ }
167
+ allEmbeddings.set(vector, i * dimension);
168
+ }
169
+
170
+ normalizeEmbeddings(allEmbeddings, dimension);
171
+ const finalChunks = withVectorIdx(newChunks);
172
+
173
+ const now = new Date().toISOString();
174
+ const meta = {
175
+ repo_id: store.repo_id,
176
+ repo_path: repoRoot,
177
+ created_at: prevMeta?.created_at || now,
178
+ updated_at: now,
179
+ embedding_provider: config.embeddings.provider,
180
+ model: config.embeddings.model,
181
+ embedding_dim: dimension,
182
+ chunk_lines: config.chunk_lines,
183
+ chunk_overlap: config.chunk_overlap,
184
+ max_bytes: config.max_bytes,
185
+ files: fileHashes,
186
+ chunks: finalChunks.length,
187
+ };
188
+
189
+ store.saveChunks(finalChunks);
190
+ store.saveEmbeddings(allEmbeddings);
191
+ store.saveMeta(meta);
192
+
193
+ return {
194
+ repo_id: store.repo_id,
195
+ chunks_indexed: finalChunks.length,
196
+ took_ms: timer.ms,
197
+ };
198
+ }
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+ import readline from "node:readline";
3
+ import { indexRepo } from "../indexing/indexer.js";
4
+ import { searchRepo } from "../retrieval/hybrid.js";
5
+ import { loadConfig } from "../config.js";
6
+ import { IndexStore } from "../indexing/index_store.js";
7
+ import path from "node:path";
8
+
9
+ const TOOL_NAME = "codebase-retrieval";
10
+ const TOOL_SCHEMA = {
11
+ name: TOOL_NAME,
12
+ description: "Index (if needed) and search a local codebase.",
13
+ inputSchema: {
14
+ type: "object",
15
+ properties: {
16
+ path: { type: "string" },
17
+ query: { type: "string" },
18
+ k: { type: "integer", default: 8 },
19
+ },
20
+ required: ["path", "query"],
21
+ },
22
+ outputSchema: {
23
+ type: "object",
24
+ properties: {
25
+ result: { type: "array" },
26
+ },
27
+ required: ["result"],
28
+ },
29
+ };
30
+
31
+ function send(message) {
32
+ process.stdout.write(JSON.stringify(message) + "\n");
33
+ }
34
+
35
+ function sendError(id, code, message) {
36
+ send({ jsonrpc: "2.0", id, error: { code, message } });
37
+ }
38
+
39
+ async function handleToolCall(params) {
40
+ const { name, arguments: args } = params || {};
41
+ if (name !== TOOL_NAME) {
42
+ return { isError: true, content: [{ type: "text", text: `Unknown tool: ${name}` }] };
43
+ }
44
+ const repoPath = args?.path;
45
+ const query = args?.query;
46
+ const k = args?.k ?? 8;
47
+ if (!repoPath || !query) {
48
+ return {
49
+ isError: true,
50
+ content: [{ type: "text", text: "Missing required arguments." }],
51
+ };
52
+ }
53
+
54
+ const config = loadConfig();
55
+ const repoRoot = path.resolve(repoPath);
56
+ const store = new IndexStore(repoRoot, config.index_root);
57
+ if (!store.exists()) {
58
+ await indexRepo(repoRoot, config);
59
+ } else {
60
+ await indexRepo(repoRoot, config);
61
+ }
62
+ const results = await searchRepo(repoRoot, query, k);
63
+ return {
64
+ isError: false,
65
+ content: [{ type: "text", text: JSON.stringify(results) }],
66
+ structuredContent: { result: results },
67
+ };
68
+ }
69
+
70
+ const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
71
+
72
+ rl.on("line", async (line) => {
73
+ if (!line.trim()) return;
74
+ let payload;
75
+ try {
76
+ payload = JSON.parse(line);
77
+ } catch {
78
+ return;
79
+ }
80
+
81
+ const { id, method, params } = payload;
82
+
83
+ try {
84
+ if (method === "initialize") {
85
+ send({
86
+ jsonrpc: "2.0",
87
+ id,
88
+ result: {
89
+ protocolVersion: params?.protocolVersion || "2024-11-05",
90
+ capabilities: { tools: {} },
91
+ serverInfo: { name: "mikoshi", version: "0.1.0" },
92
+ },
93
+ });
94
+ return;
95
+ }
96
+
97
+ if (method === "tools/list") {
98
+ send({ jsonrpc: "2.0", id, result: { tools: [TOOL_SCHEMA] } });
99
+ return;
100
+ }
101
+
102
+ if (method === "tools/call") {
103
+ const result = await handleToolCall(params);
104
+ send({ jsonrpc: "2.0", id, result });
105
+ return;
106
+ }
107
+
108
+ if (method === "ping") {
109
+ send({ jsonrpc: "2.0", id, result: {} });
110
+ return;
111
+ }
112
+
113
+ if (id !== undefined) {
114
+ sendError(id, -32601, `Method not found: ${method}`);
115
+ }
116
+ } catch (err) {
117
+ if (id !== undefined) {
118
+ sendError(id, -32000, err?.message || "Internal error");
119
+ }
120
+ }
121
+ });
@@ -0,0 +1,85 @@
1
+ import path from "node:path";
2
+ import { loadConfig } from "../config.js";
3
+ import { IndexStore } from "../indexing/index_store.js";
4
+ import { LexicalIndex } from "./lexical.js";
5
+ import { rerank } from "./rerank.js";
6
+ import { getEmbeddingsProvider, SemanticSearcher } from "./semantic.js";
7
+
8
+ function normalize(scores) {
9
+ if (!scores.length) return new Map();
10
+ const values = scores.map((item) => item[1]);
11
+ const min = Math.min(...values);
12
+ const max = Math.max(...values);
13
+ const out = new Map();
14
+ for (const [idx, score] of scores) {
15
+ if (max === min) out.set(idx, 1.0);
16
+ else out.set(idx, (score - min) / (max - min));
17
+ }
18
+ return out;
19
+ }
20
+
21
+ export function mergeScores(lexical, semantic, alpha = 0.5) {
22
+ const lexNorm = normalize(lexical);
23
+ const semNorm = normalize(semantic);
24
+ const combined = new Map();
25
+
26
+ for (const [idx, score] of lexNorm.entries()) {
27
+ combined.set(idx, (combined.get(idx) || 0) + alpha * score);
28
+ }
29
+ for (const [idx, score] of semNorm.entries()) {
30
+ combined.set(idx, (combined.get(idx) || 0) + (1 - alpha) * score);
31
+ }
32
+ return combined;
33
+ }
34
+
35
+ function topK(combined, k) {
36
+ return [...combined.entries()].sort((a, b) => b[1] - a[1]).slice(0, k);
37
+ }
38
+
39
+ export function makeSnippet(text, maxLines = 6, maxChars = 400) {
40
+ const lines = text.split(/\r?\n/);
41
+ let snippet = lines.slice(0, maxLines).join("\n");
42
+ if (snippet.length > maxChars) snippet = `${snippet.slice(0, maxChars - 3)}...`;
43
+ return snippet;
44
+ }
45
+
46
+ export async function hybridSearch(query, chunks, lexical, semantic, k = 8, alpha = 0.5) {
47
+ if (!chunks.length) return [];
48
+ const expanded = Math.max(k * 3, k);
49
+ const lexicalHits = lexical.search(query, expanded);
50
+ const semanticHits = await semantic.search(query, expanded);
51
+ const combined = mergeScores(lexicalHits, semanticHits, alpha);
52
+ const merged = topK(combined, expanded);
53
+ const reranked = rerank(query, merged, chunks);
54
+ return reranked.slice(0, k);
55
+ }
56
+
57
+ export async function searchRepo(repoPath, query, k = 8) {
58
+ const config = loadConfig();
59
+ const repoRoot = path.resolve(repoPath);
60
+ const store = new IndexStore(repoRoot, config.index_root);
61
+ const meta = store.loadMeta();
62
+ if (!meta) throw new Error("Repository has not been indexed yet.");
63
+
64
+ if (meta.embedding_provider !== config.embeddings.provider || meta.model !== config.embeddings.model) {
65
+ throw new Error("Index was built with different embeddings settings. Re-index the repository.");
66
+ }
67
+
68
+ const chunks = store.loadChunks();
69
+ const embeddingsData = store.loadEmbeddings(meta);
70
+ if (!embeddingsData) throw new Error("Index data missing. Re-run indexing.");
71
+ if (!chunks.length) return [];
72
+
73
+ const lexical = new LexicalIndex(chunks.map((chunk) => chunk.text));
74
+ const provider = getEmbeddingsProvider(config);
75
+ const semantic = new SemanticSearcher(embeddingsData.embeddings, embeddingsData.dim, provider);
76
+ const hits = await hybridSearch(query, chunks, lexical, semantic, k);
77
+
78
+ return hits.map(([idx, score]) => ({
79
+ relpath: chunks[idx].relpath,
80
+ start_line: chunks[idx].start_line,
81
+ end_line: chunks[idx].end_line,
82
+ score: Number(score),
83
+ snippet: makeSnippet(chunks[idx].text),
84
+ }));
85
+ }
@@ -0,0 +1,53 @@
1
+ function tokenize(text) {
2
+ return text.toLowerCase().split(/[^a-z0-9_]+/).filter(Boolean);
3
+ }
4
+
5
+ export class LexicalIndex {
6
+ constructor(documents) {
7
+ this.documents = documents;
8
+ this.docTokens = documents.map(tokenize);
9
+ this.docLengths = this.docTokens.map((tokens) => tokens.length);
10
+ this.avgDocLength =
11
+ this.docLengths.reduce((a, b) => a + b, 0) / (this.docLengths.length || 1);
12
+
13
+ this.termDocFreq = new Map();
14
+ for (const tokens of this.docTokens) {
15
+ const seen = new Set();
16
+ for (const token of tokens) {
17
+ if (seen.has(token)) continue;
18
+ seen.add(token);
19
+ this.termDocFreq.set(token, (this.termDocFreq.get(token) || 0) + 1);
20
+ }
21
+ }
22
+ }
23
+
24
+ search(query, k = 8) {
25
+ const tokens = tokenize(query);
26
+ const scores = new Array(this.documents.length).fill(0);
27
+ const N = this.documents.length;
28
+ const k1 = 1.2;
29
+ const b = 0.75;
30
+
31
+ for (const term of tokens) {
32
+ const df = this.termDocFreq.get(term) || 0.5;
33
+ const idf = Math.log(1 + (N - df + 0.5) / (df + 0.5));
34
+ for (let i = 0; i < this.docTokens.length; i += 1) {
35
+ const tokensDoc = this.docTokens[i];
36
+ let tf = 0;
37
+ for (const t of tokensDoc) {
38
+ if (t === term) tf += 1;
39
+ }
40
+ if (!tf) continue;
41
+ const denom = tf + k1 * (1 - b + (b * this.docLengths[i]) / this.avgDocLength);
42
+ scores[i] += idf * ((tf * (k1 + 1)) / denom);
43
+ }
44
+ }
45
+
46
+ const results = [];
47
+ for (let i = 0; i < scores.length; i += 1) {
48
+ if (scores[i] > 0) results.push([i, scores[i]]);
49
+ }
50
+ results.sort((a, b) => b[1] - a[1]);
51
+ return results.slice(0, k);
52
+ }
53
+ }
@@ -0,0 +1,3 @@
1
+ export function rerank(_query, merged, _chunks) {
2
+ return merged;
3
+ }