feat(embeddings): batch ollama embeds via /api/embed - #300
Conversation
0852180 to
e37b013
Compare
Helweg
left a comment
There was a problem hiding this comment.
I found one correctness issue before this can be approved:
getDynamicBatchOptions() applies embedding.batch.maxBatchItems and maxBatchTokens to every provider, despite the new public config/docs explicitly stating that these options are currently Ollama-only. For example, a custom, OpenAI, or Google setup with embedding.batch.maxBatchItems: 1 will now be split by the indexer into one-item calls, changing existing request behavior and potentially defeating a provider's own batching. Please return {} immediately for non-Ollama providers, and add a regression test showing a non-Ollama provider ignores this Ollama-specific configuration.
The targeted implementation tests, config tests, typecheck, and lint otherwise pass locally on e37b013.
e37b013 to
121d836
Compare
Follow-up to 427d06f and 121d836 incorporating findings from an independent review (claude + codex) of PR Helweg#300. No happy-path behavior change; corrects docs/comments and hardens edge paths. - ollama.ts: cache a 404 from /api/embed per provider instance so a legacy ollama install is probed once, not once per multi-text batch. Soften the fallback comments to match the actual behavior: a malformed batch re-embeds each text cleanly, but a text that hard-fails per-text throws and fails the whole request batch. In-run per-text isolation is not provided; a poison text is isolated only on the recovery run, which re-embeds one text per request. - index.ts: guard the getDynamicBatchOptions embedding.batch overrides with Number.isFinite so a programmatic NaN/Infinity cannot poison createDynamicBatches. Correct the in-flight gloss (~80 texts, not chunks). - docs/configuration.md: state that embedding.batch.* is ollama-only (OpenAI, Google, and custom providers ignore it and keep their existing request behavior); describe batched items as embedding texts/parts rather than chunks; document the failed-batch recovery semantics so a one-shot indexer knows healthy co-batch chunks recover on the next index() run. - tests: add a regression test for the 404 cache (one /api/embed hit across two consecutive batches) and for non-finite override rejection; add a trailing newline to ollama-embed.test.ts. Verified: affected suites green (192 tests); typecheck, lint, and build:ts clean. The three watcher-snapshot-reconciler failures are pre-existing on the clean PR head and unrelated to this change.
|
Thanks for the follow-up. We are planning the next release and would like to include this Ollama batching feature. The PR is currently a draft and has conflicts with |
Send multiple chunks per POST /api/embed request (input: string[] ->
embeddings: number[][]) to amortize N per-chunk HTTP round-trips into one.
~10x per-chunk speedup measured against a remote ollama host (236 ms/chunk
-> ~22 ms/chunk for 32 inputs).
- OllamaEmbeddingProvider: new embedMany (batched /api/embed) and
embedOneByOne (shared legacy per-text /api/embeddings path). embedBatch:
0 -> empty, 1 -> legacy (no /api/embed probe), >1 -> batched with fallback
to per-text on context-length / 404 / malformed-batch errors so one bad
vector fails only its chunk. Malformed/non-JSON/null 200 bodies engage
the fallback.
- getDynamicBatchOptions: ollama defaults maxBatchItems 16, maxBatchTokens
65536; new embedding.batch.{maxBatchItems,maxBatchTokens} config knobs
override them. Number.isFinite guard rejects NaN/Infinity.
- Recovery path (index() retry loop and retryFailedBatches) re-embeds
previously-failed chunks one per request (ollama-scoped) so a
permanently-failing chunk is isolated instead of re-poisoning its
co-batched healthy chunks.
- docs/configuration.md: new "Batching Ollama embeddings" section and a
resource-pressure note (ollama concurrency is fixed at 5; ~80 chunks in
flight at the default 16).
Backward compatible: old ollama without /api/embed (404) and context-length
overflow fall back to the unchanged per-text path.
getDynamicBatchOptions applied embedding.batch.{maxBatchItems,maxBatchTokens}
to every provider via an unconditional spread outside the
provider === "ollama" ternary. The config schema and docs state these
options are ollama-only, so a non-ollama provider (openai/google/custom)
configured with embedding.batch.maxBatchItems: 1 was split into one-item
embedding requests, changing its existing behavior.
Return an empty options object immediately for non-ollama providers, and
move the embeddingBatch override spread inside the ollama branch. Export
the helper so the provider gate can be unit-tested directly.
Add a regression test asserting that non-ollama providers ignore
embedding.batch.* (an aggressive maxBatchItems: 1 must not split their
requests), while ollama still honors overrides and the documented defaults.
Follow-up to 427d06f and 121d836 incorporating findings from an independent review (claude + codex) of PR Helweg#300. No happy-path behavior change; corrects docs/comments and hardens edge paths. - ollama.ts: cache a 404 from /api/embed per provider instance so a legacy ollama install is probed once, not once per multi-text batch. Soften the fallback comments to match the actual behavior: a malformed batch re-embeds each text cleanly, but a text that hard-fails per-text throws and fails the whole request batch. In-run per-text isolation is not provided; a poison text is isolated only on the recovery run, which re-embeds one text per request. - index.ts: guard the getDynamicBatchOptions embedding.batch overrides with Number.isFinite so a programmatic NaN/Infinity cannot poison createDynamicBatches. Correct the in-flight gloss (~80 texts, not chunks). - docs/configuration.md: state that embedding.batch.* is ollama-only (OpenAI, Google, and custom providers ignore it and keep their existing request behavior); describe batched items as embedding texts/parts rather than chunks; document the failed-batch recovery semantics so a one-shot indexer knows healthy co-batch chunks recover on the next index() run. - tests: add a regression test for the 404 cache (one /api/embed hit across two consecutive batches) and for non-finite override rejection; add a trailing newline to ollama-embed.test.ts. Verified: affected suites green (192 tests); typecheck, lint, and build:ts clean. The three watcher-snapshot-reconciler failures are pre-existing on the clean PR head and unrelated to this change.
8102597 to
7dc5d21
Compare
|
@Helweg |
Helweg
left a comment
There was a problem hiding this comment.
Re-reviewed the current head . The prior non-Ollama batching concern is addressed: now returns empty indexer options for non-Ollama providers. Focused native-backed validation passed: 192 tests across Ollama batching, batch pooling, failed-batch recovery, custom-provider compatibility, and config parsing. The implementation is approved pending the required GitHub-hosted workflows.
|
Clarification to my approval: I reviewed head 7dc5d21. The prior non-Ollama batching concern is addressed because embedding.batch now returns empty indexer options for non-Ollama providers. Native-backed focused validation passed: 192 tests covering Ollama batching, batch pooling, failed-batch recovery, custom-provider compatibility, and config parsing. Approval remains conditional on the required GitHub-hosted workflows. |
Summary
Cuts per-chunk HTTP round-trips for ollama embeddings by switching the fast path to
POST /api/embedwithinput: string[](one request for N chunks instead of N requests). Amortizes N HTTPround-trips into 1, which dominates indexing throughput for a remote or shared ollama host.
Closes #301.
Problem
OllamaEmbeddingProvider.embedBatchissued onePOST /api/embeddings(singleprompt) per chunk, andgetDynamicBatchOptionspinned ollamamaxBatchItems: 1, so the indexer's batchingmachinery never packed texts — every chunk was its own HTTP round-trip. The cost is the embedding HTTP wait, not local disk or parse.
Ollama's newer
POST /api/embedendpoint acceptsinput: string[]and returnsembeddings: number[][](one vector per input, in order) in a single request. Ollama encodes each inputindependently (each element of
input[]produces its own vector, not a concatenation), so the model context length applies per input, not over the batch — the upstream splitter already boundseach input.
Changes
src/embeddings/providers/ollama.ts: newembedMany(batched/api/embed);embedBatchnow tries the batch path and falls back to the legacy per-text/api/embeddingsloop on 404 (oldollama without
/api/embed), context-length overflow (per-text truncation net), or a malformed batch response (per-chunk isolation — one bad vector fails only its own chunk).embedSingle/embedSingleWithFallbackunchanged.src/indexer/index.ts:getDynamicBatchOptionsollama defaults tomaxBatchItems: 16,maxBatchTokens: 65536. NewforceSingleItemBatchesoption forcesmaxBatchItems: 1on the twofailed-batch recovery paths (index retry loop +
retryFailedBatchesUnlocked) so a permanently-failing chunk is isolated from healthy chunks on the retry run; scoped to ollama.src/config/schema.ts: newembedding.batch.{maxBatchItems,maxBatchTokens}knobs (parsed withNumber.isFinite, clamped to ≥1).docs/configuration.md: "Batching Ollama embeddings" section documenting the endpoint, fallbacks, knobs, and the resource-pressure note (up to 5 concurrent ollama requests ×maxBatchItemschunks in flight; ollama concurrency is fixed at 5, not configurable).tests/ollama-embed.test.ts(batched success, empty, single-text-uses-legacy, 404 fallback, context-length fallback, malformed-batch fallback, non-JSON/null-body fallback,non-404 error propagation);
tests/indexer-failed-batches.test.tspins per-text recovery tests tomaxBatchItems: 1and adds a batched isolation regression test;tests/custom-provider.test.tsupdated to assert batching into a single
/api/embedrequest;tests/config.test.tscovers the new knobs.Verified
npx vitest run);npm run typecheckclean;npm run build:tsexit 0.dist/cli.js+dist/index.js.