Spaces:
Sleeping
Sleeping
| // ---------------------------------------------------------------------------- | |
| // In-memory document store: recursive chunking + cosine retrieval. | |
| // | |
| // For a single-instance deployment this is fine and fast. If you scale to | |
| // multiple instances or want persistence, swap this Map for Redis / a vector DB | |
| // (Qdrant, pgvector). The interface (put/get/retrieve) stays the same. | |
| // ---------------------------------------------------------------------------- | |
| const docs = new Map(); // docId -> { chunks: string[], vectors: number[][], at: number } | |
| // --- recursive character splitter (LangChain-style) ------------------------ | |
| export function recursiveSplit( | |
| text, | |
| chunkSize = 1000, | |
| chunkOverlap = 150, | |
| separators = ['\n\n', '\n', '. ', ' ', ''], | |
| ) { | |
| const splitRec = (input, seps) => { | |
| if (input.length <= chunkSize) return input.trim() ? [input] : []; | |
| const [sep, ...rest] = seps.length ? seps : ['']; | |
| const parts = sep === '' ? input.split('') : input.split(sep); | |
| const chunks = []; | |
| let current = ''; | |
| for (const part of parts) { | |
| const piece = sep === '' ? part : part + sep; | |
| if ((current + piece).length > chunkSize) { | |
| if (current) chunks.push(current); | |
| if (piece.length > chunkSize && rest.length) { | |
| chunks.push(...splitRec(piece, rest)); | |
| current = ''; | |
| } else { | |
| current = piece; | |
| } | |
| } else { | |
| current += piece; | |
| } | |
| } | |
| if (current) chunks.push(current); | |
| return chunks; | |
| }; | |
| const raw = splitRec(text, separators).filter((c) => c.trim().length > 0); | |
| if (chunkOverlap <= 0) return raw; | |
| const out = []; | |
| for (let i = 0; i < raw.length; i++) { | |
| const prevTail = i > 0 ? raw[i - 1].slice(-chunkOverlap) : ''; | |
| out.push((prevTail + raw[i]).trim()); | |
| } | |
| return out; | |
| } | |
| export function cosine(a, b) { | |
| let dot = 0; | |
| let na = 0; | |
| let nb = 0; | |
| for (let i = 0; i < a.length; i++) { | |
| dot += a[i] * b[i]; | |
| na += a[i] * a[i]; | |
| nb += b[i] * b[i]; | |
| } | |
| return dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-8); | |
| } | |
| export function hasDoc(docId) { | |
| return docs.has(docId); | |
| } | |
| export function putDoc(docId, chunks, vectors) { | |
| docs.set(docId, { chunks, vectors, at: Date.now() }); | |
| } | |
| /** Retrieve top-k chunks across one or more docs for a query vector. */ | |
| export function retrieve(docIds, queryVec, k = 4) { | |
| const scored = []; | |
| for (const id of docIds) { | |
| const doc = docs.get(id); | |
| if (!doc) continue; | |
| for (let i = 0; i < doc.chunks.length; i++) { | |
| scored.push({ text: doc.chunks[i], score: cosine(queryVec, doc.vectors[i]) }); | |
| } | |
| } | |
| return scored.sort((a, b) => b.score - a.score).slice(0, k).map((s) => s.text); | |
| } | |
| // Optional: evict docs older than N ms to cap memory (call periodically). | |
| export function evictOlderThan(ms) { | |
| const cutoff = Date.now() - ms; | |
| for (const [id, doc] of docs) { | |
| if (doc.at < cutoff) docs.delete(id); | |
| } | |
| } | |