Skip to content

feat(embeddings): batch ollama embeds via /api/embed - #300

Merged
Helweg merged 3 commits into
Helweg:mainfrom
dkhokhlov:feat/ollama-batched-embed
Aug 18, 2026
Merged

feat(embeddings): batch ollama embeds via /api/embed#300
Helweg merged 3 commits into
Helweg:mainfrom
dkhokhlov:feat/ollama-batched-embed

Conversation

@dkhokhlov

@dkhokhlov dkhokhlov commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Cuts per-chunk HTTP round-trips for ollama embeddings by switching the fast path to POST /api/embed with input: string[] (one request for N chunks instead of N requests). Amortizes N HTTP
round-trips into 1, which dominates indexing throughput for a remote or shared ollama host.

Closes #301.

Problem

OllamaEmbeddingProvider.embedBatch issued one POST /api/embeddings (single prompt) per chunk, and getDynamicBatchOptions pinned ollama maxBatchItems: 1, so the indexer's batching
machinery 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/embed endpoint accepts input: string[] and returns embeddings: number[][] (one vector per input, in order) in a single request. Ollama encodes each input
independently (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 bounds
each input.

Changes

  • src/embeddings/providers/ollama.ts: new embedMany (batched /api/embed); embedBatch now tries the batch path and falls back to the legacy per-text /api/embeddings loop on 404 (old
    ollama 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/embedSingleWithFallback unchanged.
  • src/indexer/index.ts: getDynamicBatchOptions ollama defaults to maxBatchItems: 16, maxBatchTokens: 65536. New forceSingleItemBatches option forces maxBatchItems: 1 on the two
    failed-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: new embedding.batch.{maxBatchItems,maxBatchTokens} knobs (parsed with Number.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 ×
    maxBatchItems chunks in flight; ollama concurrency is fixed at 5, not configurable).
  • Tests: new 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.ts pins per-text recovery tests to maxBatchItems: 1 and adds a batched isolation regression test; tests/custom-provider.test.ts
    updated to assert batching into a single /api/embed request; tests/config.test.ts covers the new knobs.

Verified

  • Affected suites green (npx vitest run); npm run typecheck clean; npm run build:ts exit 0.
  • Batched code confirmed in dist/cli.js + dist/index.js.

@dkhokhlov
dkhokhlov force-pushed the feat/ollama-batched-embed branch from 0852180 to e37b013 Compare August 17, 2026 04:46

@Helweg Helweg left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@dkhokhlov
dkhokhlov force-pushed the feat/ollama-batched-embed branch from e37b013 to 121d836 Compare August 17, 2026 06:36
@dkhokhlov
dkhokhlov marked this pull request as draft August 17, 2026 07:31
@Helweg Helweg added the feature New feature or capability label Aug 17, 2026
dkhokhlov added a commit to dkhokhlov/open-codebase-index that referenced this pull request Aug 17, 2026
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.
@Helweg

Helweg commented Aug 17, 2026

Copy link
Copy Markdown
Owner

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 main, and its head changed after the last review. Please rebase onto current main, resolve the conflicts, and mark it ready for review. Once pushed, we will re-review the final head and run the hosted validation.

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.
@dkhokhlov
dkhokhlov force-pushed the feat/ollama-batched-embed branch from 8102597 to 7dc5d21 Compare August 18, 2026 04:03
@dkhokhlov
dkhokhlov marked this pull request as ready for review August 18, 2026 04:06
@dkhokhlov
dkhokhlov requested a review from Helweg August 18, 2026 04:18
@dkhokhlov

dkhokhlov commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

@Helweg
Let me profile the indexing throughput.

@Helweg Helweg left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@Helweg

Helweg commented Aug 18, 2026

Copy link
Copy Markdown
Owner

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.

@Helweg
Helweg merged commit 077982c into Helweg:main Aug 18, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or capability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] batch ollama embeddings via /api/embed to cut per-chunk HTTP round-trips

2 participants