Skip to content

Feat/mxbai embeddings and hardening - #1

Merged
AdamRussak merged 3 commits into
mainfrom
feat/mxbai-embeddings-and-hardening
Jul 21, 2026
Merged

Feat/mxbai embeddings and hardening#1
AdamRussak merged 3 commits into
mainfrom
feat/mxbai-embeddings-and-hardening

Conversation

@ulielitay

Copy link
Copy Markdown
Collaborator

Summary

Makes the embedding model selectable from a registry, where picking a model auto-derives everything downstream (vector dimension, container memory limits, query/passage prompts, the pre-baked image layer), and upgrades the default to a stronger model. Also adds the lint/type gate the repo was missing, hardens sitemap XML parsing, and adds parity guards against the duplicated-code drift the codebase already warned about in comments.

Embedding model

config/models.yaml is the single source of truth. make configure MODEL=<name> resolves the selection into .env and re-renders db/init/01_schema.sql from a template so vector(N) always matches the model.

Model dim ingestion / mcp memory
mixedbread-ai/mxbai-embed-large-v1 (new default) 1024 2g / 2g
intfloat/multilingual-e5-large 1024 2g / 2g
BAAI/bge-base-en-v1.5 768 1500m / 1g
BAAI/bge-small-en-v1.5 (previous default, used by CI) 384 1500m / 1g

Still FastEmbed / ONNX / CPU — no torch, no GPU.

Bug found while implementing

The previous code (and ADR-001) assumed FastEmbed's passage_embed()/query_embed() apply the BGE passage:/query: prefixes. They don't — in FastEmbed 0.8 both delegate straight to embed() for every non-multitask model (bge, mxbai, e5), so queries were being embedded with no instruction prefix at all. Prompts are now applied explicitly from the registry (mxbai/bge instruct the query only; e5 prefixes both sides). The chunker's tokenizer also follows the embedding model now, so chunk sizing matches the model that actually embeds the text.

Other changes

  • Lint/type gate: ruff + mypy, make lint / make typecheck, new CI job. mypy quarantines a documented pre-existing typing backlog so the gate enforces types on new/changed code without a risky refactor. Coverage added to make test.
  • CI cost: CI runs make configure MODEL=BAAI/bge-small-en-v1.5 so the suite doesn't pull 1.2GB on every run — which also continuously exercises the configure path.
  • Security: sitemap XML now parsed with defusedxml (forbids DTD/entity expansion/XXE); these documents come from untrusted upstream sources.

Other changes

  • Lint/type gate: ruff + mypy, make lint / make typecheck, new CI job. mypy quarantines a documented pre-existing typing backlog so the gate enforces types on new/changed code without a risky refactor. Coverage added to make test.
  • CI cost: CI runs make configure MODEL=BAAI/bge-small-en-v1.5 so the suite doesn't pull 1.2GB on every run — which also continuously exercises the configure path.
  • Security: sitemap XML now parsed with defusedxml (forbids DTD/entity expansion/XXE); these documents come from untrusted upstream sources.
  • Parity guards: tests assert the committed schema equals the rendered template, both services' defaults equal the registry default, and the hand-duplicated SSRF _addr_is_private helper stays byte-identical across services.
  • Housekeeping: LICENSE added; small B904/B905 fixes surfaced by the new lint gate.
  • Docs: README/runbook corrected (they still described the model as fixed), new ADR-004, ADR-001 given an inline supersede note in its existing style, and the false prefix claim in IMPLEMENTATION_PLAN.md corrected.

⚠️ Upgrading an existing deployment

The default dimension changes 384 → 1024, so stored vectors are invalid and the column width changes. Existing deployments need a rebuild + re-embed:

make configure                          # or MODEL=<your choice>
docker compose build ingestion mcp-server   # model is baked in at build time
docker compose down -v db && docker compose up -d db
make up && make sync

Same-dimension model swaps can use make reindex instead. Full procedure: Runbook → switch the embedding model.

Verification

  • make lint (ruff) clean; make typecheck (mypy) clean across both packages + scripts.
  • ingestion suite 250 passed, 44 skipped; mcp-server 39 passed, 4 skipped (skips are DB-gated).
  • Parity suite: 9 passed.
  • make configure round-trip verified: selecting bge-small yields dim 384 + 1500m/1g in .env and vector(384) in the schema; the default yields 1024 + 2g/2g and vector(1024).
  • make configure round-trip verified: selecting bge-small yields dim 384 + 1500m/1g in .env and vector(384) in the schema; the default yields 1024 + 2g/2g and vector(1024).
  • docker compose --profile full config confirms both services resolve to the same model with the derived memory limits and prompts.
  • XXE smoke: an entity-expansion sitemap is rejected (EntitiesForbidden) while a normal sitemap still parses.

One pre-existing test (test_startup_fails_fast_on_unreachable_db) fails locally under WSL2 — it expects a fast ECONNREFUSED from 127.0.0.1:1, which this sandbox hangs instead. Unrelated to these changes; it should pass in CI.

Not done deliberately: MCP host_origin_protection is left disabled — enabling it risks breaking Traefik-proxied access, which is out of scope here.

Make the embedding model selectable from a registry where choosing a model
auto-derives its vector dimension and the services' Docker memory limits, and
upgrade the default to a stronger model. Also add lint/type tooling, harden
sitemap parsing, and add parity guards.

Embedding model registry
- config/models.yaml is the single source of truth (model -> dim, memory,
  query/passage prompts). Default is mixedbread-ai/mxbai-embed-large-v1 (1024d),
  a quality upgrade over bge-small (384d).
- `make configure MODEL=<name>` resolves the selection into .env
  (EMBEDDING_*, {INGESTION,MCP}_MEM_LIMIT) and renders db/init/01_schema.sql's
  vector(N) from a template. `make reindex` re-embeds after a switch.
- Services read the model/dim/prompts from env with defaults matching the
  registry default; docker-compose derives memory limits and build-bakes the
  model. FastEmbed applies no query/passage prefix for these models, so the
  per-model prompt is applied manually around embed() (mxbai: query-only; e5:
  both sides). The chunker's tokenizer now follows the embedding model.

Tooling + CI
- ruff (lint) and mypy configured; `make lint` / `make typecheck` and new CI
  jobs. mypy quarantines a documented pre-existing typing backlog so the gate
  enforces types on new/changed code. Coverage reporting added to `make test`.
- CI configures the small 384d model so the suite doesn't pull the 1.2GB
  default on every run (and exercises `make configure`).

Security / robustness
- Sitemap XML now parsed with defusedxml (forbids DTD/entity expansion/XXE)
  against untrusted upstream sources.

Parity guards
- tests assert the committed schema matches the rendered template, the service
  defaults match the registry, and the duplicated SSRF _addr_is_private helper
  stays byte-identical across the two services.

Housekeeping
- Add LICENSE; small B904/B905 fixes surfaced by the new lint gate.
Bring the docs in line with the registry-driven model selection: README and the
runbook still described the model as fixed at BAAI/bge-small-en-v1.5 / 384-dim.

- README: embeddings are selectable via config/models.yaml (default
  mxbai-embed-large-v1, 1024d); add `make configure` to the quickstart and
  `make lint`/`make typecheck` to development; link the LICENSE file.
- Runbook: the offline-model FAQ now refers to the configured model and notes
  that switching it requires rebuilding both images (the model is baked in via
  the EMBEDDING_MODEL_NAME build arg).
- ADR-004: new record for the registry decision — single source of truth,
  derived dimension/memory, the default upgrade, manual prompting, and the
  re-embed consequence.
- ADR-001: inline supersede note pointing at ADR-004, matching the file's
  existing convention for the n8n note. The decision itself is left intact.
- IMPLEMENTATION_PLAN: correct the claim that FastEmbed applies the BGE
  passage:/query: prefixes — it does not for any non-multitask model, so
  queries were previously embedded with no instruction prefix.
The schema parity test asserted db/init/01_schema.sql always equals the
registry-DEFAULT render, but CI legitimately runs
`make configure MODEL=BAAI/bge-small-en-v1.5` (to avoid pulling the 1.2GB
default model on every run), which rewrites that file to vector(384). The test
therefore contradicted CI's own setup step and failed.

Split it into the two invariants that actually matter:

- The working-tree schema must be a faithful render of the template at the
  ACTIVE dimension (EMBEDDING_DIM, which `make configure` writes to .env and
  the Makefile exports; falls back to the registry default). This still catches
  hand-editing — verified by temporarily editing the SQL and watching it fail —
  while tolerating a reconfigured checkout.
- The schema COMMITTED TO GIT must be the default-model render, so a fresh
  clone gets the advertised default. Read via `git show HEAD:...` so a rewritten
  working copy is never mistaken for drift; skips if git is unavailable.

Verified at both dimensions: parity suite passes with EMBEDDING_DIM=1024 and,
after configuring bge-small, with EMBEDDING_DIM=384. Full e2e suite: 22 passed,
2 skipped.
@AdamRussak
AdamRussak merged commit 5afbc46 into main Jul 21, 2026
2 checks passed
@AdamRussak
AdamRussak deleted the feat/mxbai-embeddings-and-hardening branch July 21, 2026 21:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants