GrooveSeek

Semantic search over a Markdown knowledge base, served over MCP.

View the Project on GitHub alphabet-h/grooveseek

22. Put embedding behind a provider boundary, and keep FastEmbed the default

Context and problem

Until v1.12.0 embedding inference was the concrete FastEmbed wrapper. Commands, configuration and the index compatibility check all spoke in terms of ModelChoice, the enum of the two local BGE models GrooveSeek can download and run in-process. Nothing else could produce a vector, and every place that decided whether an index was usable decided it by asking that enum.

Operators asked to point GrooveSeek at an embedding service they already run — a local inference server, or a hosted one — without GrooveSeek learning each vendor (issue #312). Most such services speak the same POST /v1/embeddings request shape that OpenAI published, so one client covers many of them.

Two things make this more than adding a variant. An external endpoint receives every indexed chunk and every query: the KB’s text leaves the process, and possibly the machine. And the index records which model produced its vectors and refuses to open under a different one, so whatever identifies “the model” now has to cover a service GrooveSeek does not control.

The question this record answers is where the seam between GrooveSeek and an embedding implementation sits, what identifies an index built through it, and who may turn outbound embedding on.

Decision drivers

Options considered

  1. A small private provider trait with separate document and query calls; FastEmbed as its default implementation; a vendor-neutral OpenAI-compatible HTTP client as the second. Taken.

  2. Keep the concrete FastEmbed type and add external models as variants of ModelChoice. Rejected. Command and config plumbing and the compatibility check already depended on ModelChoice (PR #314’s reason for the refactor), and a remote model is not something that enum can describe: it has no fixed dimension, no cache directory and no download. Every new variant would widen one vendor’s model list into the place that decides whether an index opens.

  3. Bundle or manage a sidecar, or depend on a vendor SDK (a Python server, MLX, Jina’s client). Rejected; these were issue #312’s non-goals. GrooveSeek would own another process’s lifecycle — start, health, restart, upgrade — and would grow vendor-specific request fields and task conventions. The operator already runs the service; GrooveSeek only needs to reach it.

  4. Probe the endpoint to discover the model and its dimension. Rejected. Opening an index would then depend on a network round trip: startup and Config::validate would stop being deterministic, a service that is down would look like a configuration error, and document text or a probe request would leave the machine before the index had even been opened. The operator declares dimension instead, and GrooveSeek checks every response against it.

    The condition that would reopen it: a protocol that returns the model’s identity and dimension from a metadata call carrying no user text, supported widely enough to be relied on.

  5. Honour [embedding] from any config GrooveSeek finds. Rejected. A config discovered in the current directory or its git root may have been put there by whoever wrote that repository, archive or shared drive. Honouring it would let a file found on disk send indexed document text and every query to an address it names. This is the reason R5 exists for [parsers] (ADR-0016), and the same boundary is applied here as R7.

  6. Fold the endpoint into the identity. Rejected. A change of hostname, port or scheme, or a load-balanced pair of servers, would then force a full reindex although the vectors are unchanged. And the endpoint string still would not identify the model actually deployed behind it: a redeploy at the same URL is invisible either way. It buys a rebuild on every move and no guarantee in return.

    The condition that would reopen it: a server-side identifier of the embedding space, such as a model fingerprint in the response, that GrooveSeek could record instead of the URL.

Decision

Embedding inference sits behind a private EmbeddingProvider trait. FastEmbed stays the default implementation; the second is a vendor-neutral client for POST /v1/embeddings.

Consequences

References