(
chunks: Chunk[],
embed: (texts: string[]) => Promise<number[][]>,
opts: { batchSize?: number; maxChars?: number; onProgress?: (msg: string) => void } = {},
)
| 129 | * unit-testable without a model. Pure aside from the in-place embedding assignment. |
| 130 | */ |
| 131 | export async function embedChunksResilient( |
| 132 | chunks: Chunk[], |
| 133 | embed: (texts: string[]) => Promise<number[][]>, |
| 134 | opts: { batchSize?: number; maxChars?: number; onProgress?: (msg: string) => void } = {}, |
| 135 | ): Promise<{ embedded: number; skipped: number }> { |
| 136 | const batchSize = opts.batchSize ?? 32; |
| 137 | const maxChars = opts.maxChars ?? EMBED_MAX_CHARS; |
| 138 | let embedded = 0; |
| 139 | let skipped = 0; |
| 140 | for (let i = 0; i < chunks.length; i += batchSize) { |
| 141 | const batch = chunks.slice(i, i + batchSize); |
| 142 | const texts = batch.map(c => capTextForEmbedding(c.text, maxChars)); |
| 143 | try { |
| 144 | const embs = await embed(texts); |
| 145 | for (let j = 0; j < batch.length; j++) { |
| 146 | if (embs[j] && embs[j]!.length > 0) { batch[j]!.embedding = embs[j]; embedded++; } |
| 147 | else skipped++; |
| 148 | } |
| 149 | } catch { |
| 150 | // Batch failed (e.g. one chunk still 400s) — degrade to per-chunk so a single |
| 151 | // bad chunk costs only itself, not every chunk in the batch. |
| 152 | for (let j = 0; j < batch.length; j++) { |
| 153 | try { |
| 154 | const [e] = await embed([capTextForEmbedding(batch[j]!.text, maxChars)]); |
| 155 | if (e && e.length > 0) { batch[j]!.embedding = e; embedded++; } |
| 156 | else skipped++; |
| 157 | } catch { skipped++; } |
| 158 | } |
| 159 | } |
| 160 | if (opts.onProgress && i % 256 === 0) { |
| 161 | opts.onProgress(` ${Math.min(i + batchSize, chunks.length)}/${chunks.length}`); |
| 162 | } |
| 163 | } |
| 164 | return { embedded, skipped }; |
| 165 | } |
| 166 | |
| 167 | export function cosineSim(a: number[], b: number[]): number { |
| 168 | let dot = 0, na = 0, nb = 0; |
no test coverage detected