Skip to content

fix(cli): embed at 512 dimensions by default, and document embeddings for the Ask box - #170

Open
dgesteves wants to merge 3 commits into
mainfrom
fix/od-embed-defaults-512-and-ask-embeddings
Open

dgesteves wants to merge 3 commits into
mainfrom
fix/od-embed-defaults-512-and-ask-embeddings

Conversation

@dgesteves

@dgesteves dgesteves commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Roadmap item: od-embed-defaults (OD2-34 + OD2-40, wave 3).

What was wrong

OD2-34. With OPENAI_API_KEY set and no --embedding, ondocs index built text-embedding-3-small at its full 1,536 dimensions (src/cli/main.ts passed no size), while the plugins build it at 512 (DEFAULT_DIMENSIONS in src/integration/build.ts) and the docs assume 512. The README's Next.js line and the index commands ondocs init prints pass no --dimensions, so a site that followed them shipped the larger index. On main, against the local OpenAI stub (scripts/openai-stub.mjs), docusaurus.io's docs at the pinned commit (1,070 chunks):

ondocs index on docusaurus.io's docs Index file gzipped retrieve() p50
main, no flags (1,536 dimensions) 3,073,665 B 1,981,225 B 1.64 ms
--dimensions 512 (this branch's default) 1,614,214 B 839,297 B 0.60 ms
-e none (keyword-only), for scale 869,417 B 225,290 B

The p50 is 500 searches with a random query vector, two runs each, on a shared 10-core Mac (1-minute load average about 16 when it ran), so read it as a ratio.

OD2-40. The docs and the Worker template presented keyword-only as the default for the Ask box: the README's static-site command and the Worker template's README both said -e none. The plugins' "No embedding model" warning said nothing about what a keyword-only index means for the Ask box, or that the key has to be where the site builds and not only where the endpoint runs. And the mock gate (-e mock, the README's "Try it without a key") counted a similarity from any chunk that shared one word with the question, so on main a mock index of docusaurus.io's docs answered 10 of 15 off-topic Stack Overflow titles (git, Bash, SQL, Java) by quoting unrelated pages, and a mock index of the ondocs docs answered 7.

What changed

  • src/cli/main.ts: without --embedding or a config model, the default model (OpenAI, or the same model through AI Gateway) is built at DEFAULT_DIMENSIONS (512), imported from src/integration/build.ts, unless --dimensions gives a size. A model named with --embedding keeps its own size, as the plugins' embedding option does (their test says so). The help says it. Check mode is unchanged: --check without flags still names no model.
  • src/integration/build.ts: a build without a model key prints a second warning line: the Ask box then answers only questions that use enough of the docs' own words, the MCP endpoint ondocs init writes searches by keywords either way, and the key belongs where the site builds as well as where the endpoint runs (or embedding: 'none' to silence it). The first line is unchanged because troubleshooting.md quotes it and test/troubleshooting.test.tsx pins the quote, and neither file is in this bundle. No refusal percentage goes into the warning, as the brief's priority check advised.
  • src/search/retrieve.ts, src/mock/index.ts: over a mock index, a similarity counts only from a chunk that shares two of the question's words (it was one). similarityNeedsKeyword now also takes a number; true still means one and false none, so the type is only widened. Real-embedding indexes are unchanged (default false). The count comes from the BM25 search the gate already runs: Bm25Index.search now also returns matchedTerms, how many of the query's distinct terms each chunk contains, so no chunk is tokenized again. On docusaurus.io's 1,070-chunk mock index (430 questions, two runs), retrieve() takes p50 0.55 ms and p95 0.62 to 0.65 ms with two words, the same as with one; an earlier version of this branch, which tokenized each candidate chunk, took p95 1.73 to 1.76 ms.
  • Docs (cli.md, deployment.md, local-development.md, retrieval.md, workers-ai.md, synced to the three examples), the README and the Worker template's README: embeddings are the documented default for the Ask box, with OpenAI's text-embedding-3-small when there is a key and Workers AI's BGE-small for the Worker (as init --host github-pages already sets up). Keyword-only is what the MCP endpoint uses and the no-key fallback. cli.md gives the 512 default and the upgrade path, and local-development.md says that mock mode's gate is permissive. The README's "Any other site" block names the index command for the Workers AI Worker that init --host github-pages writes (-e workers-ai:@cf/baai/bge-small-en-v1.5 with CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN, or -e none), as code comments, because the README's prose is at its word limit (test/readme-claims.test.ts). The Worker template's README says beside its plugin snippets that the embedding line needs those two variables where the site builds.
  • getting-started.md is unchanged. A paragraph there took the Next.js example's /docs/getting-started to 180.1 KB of JavaScript against its 180 KB budget in CI (Next.js serializes the page into its inline RSC script, which scripts/js-weight.mjs counts), and cli.md, retrieval.md and deployment.md already say the same.
  • A minor changeset: the CLI's default index changes size, so the next build of every CLI user with a key embeds every chunk again (paid embedding calls), and similarityNeedsKeyword and Bm25Result are widened. The plugins' move to 512 shipped as a minor too (0.6.0).

Why two shared words, not keyword coverage of 0.3

The finding offered both. I measured both on the same sets:

Mock index gate docusaurus.io: off-topic SO titles answered docusaurus.io: audit's 22, hit@3 / refused ondocs docs: off-topic SO titles answered ondocs docs: the site's 5 suggested questions answered
main (one word) 10 / 15 16 / 0 7 / 15 5 / 5
two words (this PR) 6 / 15 17 / 1 4 / 15 (5 / 15 on this branch's docs) 5 / 5
coverage ≥ 0.3 5 / 15 17 / 2 0 / 15 5 / 5
keyword-only index, for comparison 6 / 15 19 / 1

Coverage ≥ 0.3 filters more, but it refuses "How much does the plan cost?" on a one-page site whose page says "The plan costs ten dollars a month." (coverage 0.29, because IDF over one page is flat). That is the case test/astro-route.test.ts covers, and a small folder is how people try mock mode. Coverage thresholds also change the CJK counts that limits.md quotes and test/docs-claims.test.ts checks, which are outside this bundle. Two words pass the whole suite unchanged. On the audit's 22 questions, two words refuse one: "How do I add a tip or warning callout box?". Main answered it from /docs/migration/v3, a page that does not answer it. The six off-topic titles two words still let through on docusaurus.io cover half their keyword weight on a page (four, the same rule every embedded index uses) or share two words with it ("git" and "branch"). So the gate now lets through as many as a keyword-only index does.

The 15 off-topic questions

Well-known Stack Overflow question titles, picked for this check as off-topic for both sites (not labelled by a person): How do I delete a Git branch locally and remotely? / How do I undo the most recent local commits in Git? / How do I check if a directory exists or not in a Bash shell script? / How do I tell if a file does not exist in Bash? / How do I return the response from an asynchronous call? / How can I remove a specific item from an array in JavaScript? / What does the "yield" keyword do in Python? / How do I make a time delay in Python? / How do I UPDATE from a SELECT in SQL Server? / How do I import an SQL file using the command line in MySQL? / How do I horizontally center an element? / How do I change the URI (URL) for a remote Git repository? / How do I efficiently iterate over each entry in a Java Map? / How do I check whether a checkbox is checked in jQuery? / Why is processing a sorted array faster than processing an unsorted array?

The docusaurus.io runs are ondocs eval on test/eval/docusaurus.io.yaml plus these 15, against ondocs index <docs> --framework docusaurus --base-url /docs -e mock. The ondocs-docs runs call retrieve() with hashEmbedding and MOCK_MIN_SIMILARITY, as ondocs dev does, on a -e mock index of examples/nextjs/content/docs. The ondocs-docs columns of the table were measured on main's docs. On the docs this branch ships, one word lets 7 of the 15 through and two words 5, and the 5 suggested questions are all answered; the changeset quotes 5 instead of 7.

What existing indexes must do

Nothing. I checked this on docusaurus.io's docs with this branch's CLI against the stub, starting from main's 1,536-dimension index:

  • ondocs index --check without flags: is up to date (1070 chunks).
  • The next ondocs index: 1070 chunks, 1070 embedded, 0 reused, at 512 dimensions, 1,614,214 B. The build after that: 0 embedded, 1070 reused.
  • --dimensions 1536 keeps the full size, and that build also embeds every chunk once (1070 embedded, 0 reused, 3,073,695 B), because the index now records the size as a setting.

The endpoints, ondocs dev and ondocs eval read the size from the index (queryProviderOptions in src/server/shared.ts), so either size is served without a config change. Only an endpoint that sets dimensions in embeddingProviderOptions by hand has to match the index it serves. The changeset says all of this.

How it was tested

New test files, so the shared test/cli.test.ts and test/mock.test.ts stay untouched (per the wave rules):

  • test/embedding-defaults.test.ts: the CLI's default at 512 through the real HTTP stub, --dimensions 1536 and a named model at its own size, the help, the upgrade path above (check passes, one full re-embed, then full reuse), AI Gateway at 512 when only its key is set, and the plugins' second warning line.
  • test/mock-gate.test.ts: a one-shared-word off-topic question is refused on a mock index, two-word and whole-question matches still answer, similarityNeedsKeyword takes true, a number or false, Bm25Index.search counts each distinct query word once in matchedTerms, and real-embedding indexes are unaffected.

On main, 8 of these 11 tests fail, for the reasons above: 1,536 in the index, no 512 in the help, 0 embedded, 3 reused on the rebuild, no providerOptions to AI Gateway, no second warning line, the off-topic question answered, and no matchedTerms. The 3 guards pass on main and on this branch.

Commands, all passing on this branch as it is now. js-weight ran from a copy of the script that starts next start on a free port in 4100 to 4999 (the script picks one from 3100 to 3599 at random); nothing else differs. Locally the Next.js page measures 180 KB and main's 179.9 KB: the external scripts are byte-identical (159,536 B gzipped), and the 61 B of inline script between them is the page's "Last updated" line, which main's copy, exported without git history, does not render.

pnpm install --frozen-lockfile --prefer-offline
pnpm format:check
pnpm lint
pnpm typecheck
pnpm test                      # 96 files, 1,294 tests passed, 10 skipped
ONDOCS_CORPUS=<fetched> pnpm vitest run test/corpus.test.ts   # 9 passed (skipped locally without the corpus)
pnpm build && pnpm examples:sync && pnpm examples:check
pnpm check:package
pnpm template:check
pnpm example:build
pnpm --filter ask-my-site-example-nextjs lint
pnpm --filter ask-my-site-example-docusaurus build
pnpm --filter ask-my-site-example-starlight build
pnpm --filter ask-my-site-example-vitepress build
node scripts/js-weight.mjs docusaurus starlight vitepress next embed --check
# docusaurus 177.7 KB (budget 182), starlight 36.1 (40), vitepress 61.6 (64), next 180 (180), embed 3.2 (4)

Left out, and follow-ups

  • Quality numbers for real embeddings. I had no model key, and the finder's real-question sets are gone, so I did not re-measure OpenAI or BGE refusal rates. The new text describes the mechanism ("answers only questions that use enough of the docs' own words") and links to retrieval.md, which already has the measured keyword-only rates. OD2-39 (real-question evals and the proof page) is where numbers for embeddings versus keyword-only belong.
  • src/cli/init.ts is unchanged. Its index commands get 512 from the new default, and its GitHub Pages flow already sets up BGE-small. Rewording its output would move test/__snapshots__/init.test.ts.snap, which is outside this bundle.
  • nextjs.md is unchanged: it already passes --dimensions 512.
  • troubleshooting.md (outside this bundle) quotes only the first warning line. It could quote the second.
  • docusaurus.md (outside this bundle) gives the plugin's dimensions default as "the model's", but the plugin builds its default model at 512.
  • The Next.js example's JavaScript budget. /docs/getting-started sits at 179.9 KB in main's CI against 180 KB, so about 100 B of gzipped prose on that page fails CI. Raising the budget in scripts/js-weight.mjs is for the owner to decide, with a measured reason.

… for the Ask box

ondocs index with OPENAI_API_KEY and no --embedding built
text-embedding-3-small at its full 1,536 dimensions, while the plugins
build it at 512 and the docs assume 512. The README's Next.js line and
the commands init prints pass no --dimensions, so they shipped the
larger index: 3,073,665 bytes against 1,614,214 for docusaurus.io's
1,070 chunks. The CLI's default model, with OpenAI or through AI
Gateway, is now built at 512 unless --dimensions gives a size; a model
named with --embedding keeps its own size, as in the plugins. An index
built at 1,536 keeps working, since the endpoint reads the size from
it, and the next build embeds every chunk again once.

A plugin build without a model key now adds a second warning line: the
Ask box then answers only questions that use enough of the docs' own
words, and the key belongs where the site builds as well as where the
endpoint runs. The first line stays as troubleshooting.md quotes it.

Over a mock index, a similarity counts only from a chunk that shares
two of the question's words, not one. On docusaurus.io's docs, 15
off-topic Stack Overflow titles got 6 quoted answers instead of 10; on
the ondocs docs, 4 instead of 7. similarityNeedsKeyword takes the
number of words; true is still one.

The docs present embeddings as the default for the Ask box (OpenAI with
a key, Workers AI's BGE-small for the Worker), keyword-only as what the
MCP endpoint uses and the no-key fallback, and say that mock mode's
gate is permissive. The Worker template's README embeds with Workers AI
in its commands and plugin snippets.
The paragraph this branch added to getting-started.md pushed the
Next.js example's /docs/getting-started to 180.1 KB of JavaScript
against its 180 KB budget, since Next.js serializes the page into its
inline RSC script. It is gone: cli.md, retrieval.md and deployment.md
already say the same. The four getting-started pages match main again.

The changeset is minor: the CLI's default index changes size, so the
next build of every CLI user with a key embeds every chunk again, and
similarityNeedsKeyword takes a number. On the ondocs docs this branch
ships, the two-word mock gate lets 5 of the 15 off-topic titles
through, not 4 (one word: 7).

The README names the Workers AI Worker's index command, with its
Cloudflare variables, or -e none, in the "Any other site" block. The
Worker template's README says beside its plugin snippets that the
embedding line needs CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN
where the site builds.

The mock gate counts shared words from the BM25 postings the search
already walks (Bm25Result.matchedTerms) instead of tokenizing each
candidate chunk again. Results are the same; on docusaurus.io's 1,070
chunk mock index, retrieve() p95 goes from 1.73 ms back to 0.62 ms.

This branch has not been deployed

No deployments
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.

1 participant