Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/embedding-defaults.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'ondocs': minor
---

`ondocs index` without `--embedding` now embeds with OpenAI's `text-embedding-3-small` at 512 dimensions, as the plugins do, with `OPENAI_API_KEY` or through AI Gateway. Before, it built the model's full 1,536, so a site that followed the README's Next.js line shipped an index about twice the size: 2.93 MB against 1.54 MB for docusaurus.io's 1,070 chunks. `--dimensions` sets another size, and a model you name with `--embedding` is still built at its own size unless `--dimensions` gives one.

An index the CLI built at 1,536 dimensions needs nothing: the endpoints, `ondocs dev` and `ondocs eval` read the size from the index, and `ondocs index --check` without flags still passes on it. The next `ondocs index` embeds every chunk again, once, at 512 (as a first build does), and reuses those vectors after that. To keep the full size, pass `--dimensions 1536`; that build also embeds every chunk once. An endpoint that sets `dimensions` in `embeddingProviderOptions` by hand must set the size of the index it serves.

A plugin build without a model key now also says what its keyword-only index means: the Ask box answers only questions that use enough of the docs' own words, the MCP endpoint `init` writes searches by keywords either way, and the key belongs where the site builds as well as where the endpoint runs. `embedding: 'none'` builds the same index without the warning.

Over a mock index (`-e mock`), a similarity now counts only from a chunk that shares two of the question's words, not one: a single common word in a short section let off-topic questions through. On docusaurus.io's docs, 15 off-topic Stack Overflow titles about git, Bash, SQL and Java got 6 quoted answers instead of 10, as many as from a keyword-only index; on the ondocs docs, 5 instead of 7. Of the 22 answerable questions in the sample eval of docusaurus.io's docs, one more is refused, one that had been answered from a page that does not answer it, and the right page is in the top three for 17 instead of 16. `retrieval.similarityNeedsKeyword` now also takes the number of words: `true` is one, as before. The count comes from `Bm25Index.search`, whose result now also has `matchedTerms`: how many of the query's distinct terms each document contains.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,10 @@ import 'ondocs/embed/launcher.css';
**Any other site.** Index the HTML it builds, write the endpoint with `npx ondocs init`, and add the script tag:

```sh
npx ondocs index public -o public/ask-index.json -e none # keyword-only; leave out -e none to embed with OPENAI_API_KEY
npx ondocs init --out public # plain HTML needs --out; Hugo, Jekyll, MkDocs and Eleventy are detected
npx ondocs index public -o public/ask-index.json # embeds with OPENAI_API_KEY, best for the Ask box; -e none for keyword-only
npx ondocs init --out public # plain HTML needs --out; Hugo, Jekyll, MkDocs and Eleventy are detected
# For the Workers AI Worker (init --host github-pages), index with -e workers-ai:@cf/baai/bge-small-en-v1.5
# and CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN set, or -e none: it can't embed questions for an OpenAI index
```

On a GitHub Pages project site, at `https://user.github.io/repo`, add `--base-url /repo` to the index command.
Expand Down
4 changes: 2 additions & 2 deletions examples/docusaurus/docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The `ondocs` command has four subcommands: `index` builds or checks the index, `
| `--check` | off | Exit 1 if the index is stale. Never calls a model. |
| `--base-url <url>` | `/` | URL prefix for pages, such as `/docs` or a full origin. |
| `-e, --embedding <spec>` | OpenAI when a key is set | `openai:<model>`, `workers-ai:@cf/<model>`, `<provider>/<model>`, `mock[:<dims>]` or `none`. |
| `--dimensions <n>` | the model's | Vector size, for models that support it. With `--check`, the size the index must have. |
| `--dimensions <n>` | 512 for the default | Vector size, for models that support it. With `--check`, the size the index must have. |
| `--chunk-size <chars>` | `1200` | Maximum characters per chunk. |
| `--chunk-overlap <chars>` | `150` | Characters shared by consecutive chunks of a section. |
| `--ignore <glob>` | none | Skip matching files. Repeatable. |
Expand All @@ -32,7 +32,7 @@ The `ondocs` command has four subcommands: `index` builds or checks the index, `

## Choosing the embedding model

Without `--embedding`, the CLI uses `openai:text-embedding-3-small` when `OPENAI_API_KEY` is set, or `openai/text-embedding-3-small` through AI Gateway when only `AI_GATEWAY_API_KEY` is. With neither, it fails rather than quietly building a keyword-only index, and suggests `-e mock` to try it without a key. `-e none` builds a keyword-only index on purpose. `-e workers-ai:@cf/baai/bge-small-en-v1.5` embeds with Workers AI over Cloudflare's REST API, with `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN`, for the [Workers AI Worker](./workers-ai.md); its vector size is fixed, so `--dimensions` does not apply.
Without `--embedding`, the CLI uses `openai:text-embedding-3-small` when `OPENAI_API_KEY` is set, or `openai/text-embedding-3-small` through AI Gateway when only `AI_GATEWAY_API_KEY` is, at 512 dimensions, as the plugins do. That is a third of the model's 1,536, and makes the index about half the size: 1.54 MB against 2.93 MB for docusaurus.io's 1,070 chunks. `--dimensions` sets another size. A model you name with `--embedding` is built at its own size unless `--dimensions` gives one. The endpoint reads the size from the index, so an index built at 1,536 dimensions keeps working; the next build embeds every chunk again, once, at the new size. With neither key, the CLI fails rather than quietly building a keyword-only index, and suggests `-e mock` to try it without a key. `-e none` builds a keyword-only index on purpose. It suits agents' MCP search, which `init` writes keyword-only, but the Ask box then answers only questions that use enough of the docs' own words: embed the index for it ([How retrieval works](./retrieval.md#tuning)). `-e workers-ai:@cf/baai/bge-small-en-v1.5` embeds with Workers AI over Cloudflare's REST API, with `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN`, for the [Workers AI Worker](./workers-ai.md); its vector size is fixed, so `--dimensions` does not apply.

## Environment and exit codes

Expand Down
2 changes: 1 addition & 1 deletion examples/docusaurus/docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ For a static site on Cloudflare Workers, `init` writes `worker/ondocs.ts`, which

## Environment variables

Set your provider's key, such as `OPENAI_API_KEY`, in two places: in the build, if the build embeds the index, and in the function's runtime environment, which embeds questions and calls the model. `init` prints where each goes on your host. The key never reaches the browser: the dialog only talks to your endpoint. A plugin's build without the key writes a keyword-only index, which the endpoint answers from without embedding questions; `ondocs index` writes one with `-e none`. The endpoint still needs the key, because a model writes every answer: without it, the dialog shows "Answers aren’t set up on this site yet." for every question it does not refuse. See [Troubleshooting](./troubleshooting.md#answers-arent-set-up-on-this-site-yet), which also covers the other errors of a first deploy.
Set your provider's key, such as `OPENAI_API_KEY`, in two places: where the site or the index builds, which embeds the pages, and in the function's runtime environment, which embeds questions and calls the model. `init` prints where each goes on your host. The key never reaches the browser: the dialog only talks to your endpoint. Set it in the build too, not only where the endpoint runs: a plugin's build without the key writes a keyword-only index, and says so in a warning (`ondocs index` writes one only with `-e none`). The endpoint answers from it without embedding questions, so the Ask box answers only questions that use enough of the docs' own words, even with the key set where it runs. The MCP endpoint `init` writes searches by keywords either way. The endpoint still needs the key, because a model writes every answer: without it, the dialog shows "Answers aren’t set up on this site yet." for every question it does not refuse. See [Troubleshooting](./troubleshooting.md#answers-arent-set-up-on-this-site-yet), which also covers the other errors of a first deploy.

## Cold starts

Expand Down
2 changes: 1 addition & 1 deletion examples/docusaurus/docs/local-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ With React, render `<AskDialog endpoint="http://localhost:8787/api/ask" launcher

`mockEmbeddingModel()` hashes words and word pairs into 512-dimension vectors, so similarity tracks shared vocabulary. `mockLanguageModel()` answers extractively: it picks the sentences from the retrieved sources that best match the question and cites each one, and it never writes text of its own. Everything else is the production code path: chunking, the int8 index, hybrid retrieval, the relevance gate, streaming and citations.

Both models come from `ondocs/mock`. They are for demos, tests and local development. Retrieval over a mock index is lexical, so a question has to share words with the page that answers it.
Both models come from `ondocs/mock`. They are for demos, tests and local development. Retrieval over a mock index is lexical, so a question has to share words with the page that answers it. Its "I don't know" is lexical too, and more permissive than a real model's: an off-topic question that shares two words with a page, or half of its keyword weight, gets an answer quoted from that page. On docusaurus.io's docs, 6 of 15 off-topic Stack Overflow titles about git, Bash, SQL and Java still did, as many as from a keyword-only index. To see the gate your readers get, index with the embedding model you deploy, as in [Use a real model locally](#use-a-real-model-locally).

## Use a real model locally

Expand Down
4 changes: 2 additions & 2 deletions examples/docusaurus/docs/retrieval.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ The top 6 chunks are numbered as sources, and chunks from the same section share

## Keyword fallback

If the embedding provider fails while embedding a question, that request falls back to keyword retrieval instead of failing, and the error goes to `onError`. An index built without embeddings (`-e none`) always retrieves by keywords alone.
If the embedding provider fails while embedding a question, that request falls back to keyword retrieval instead of failing, and the error goes to `onError`. An index built without embeddings (`-e none`, or a plugin's build without a model key) always retrieves by keywords alone. That is how the MCP endpoint `init` writes searches anyway, so agents lose nothing, but the Ask box then has only the keyword threshold: embed the index for it, with OpenAI's `text-embedding-3-small` when the endpoint answers with OpenAI, or Workers AI's BGE-small for the [Workers AI Worker](./workers-ai.md#keywords-or-meaning-too).

## Tuning

Expand Down Expand Up @@ -77,4 +77,4 @@ Whatever the hooks return, each hit's chunk is measured again against the questi

## Retrieval in mock mode

The mock embedder's similarity comes from shared words, so a mock index uses `minSimilarity: 0.2` (`MOCK_MIN_SIMILARITY`) and counts similarity only from chunks that share a word with the question. Without that rule, a short question could match a short chunk through a hash collision. The `similarityNeedsKeyword` option turns the rule on or off; it is off for real embeddings, which match meaning without shared words.
The mock embedder's similarity comes from shared words, so a mock index uses `minSimilarity: 0.2` (`MOCK_MIN_SIMILARITY`) and counts similarity only from chunks that share two of the question's words. With none, a short question could match a short chunk through a hash collision. With one, a single common word in a short section was enough: on docusaurus.io's docs, a mock index answered 10 of 15 off-topic Stack Overflow titles about git, Bash, SQL and Java, and with two it answers 6, as many as a keyword-only index of the same docs. Those 6 cover half of their keyword weight on one page, or share two words with it, such as "git" and "branch". Of the 22 questions about those docs in the 2026-10-10 audit, two words refuse one, which one word had answered from a page that does not answer it. `similarityNeedsKeyword` sets the number of words: `true` for one, `false` for none. It is off for real embeddings, which match meaning without shared words.
2 changes: 1 addition & 1 deletion examples/docusaurus/docs/workers-ai.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ The Worker fetches the index from the live site and checks it again every five m

## Keywords, or meaning too

An index built without an embedding model, the default when the build has no model key, is searched by keywords, and needs no secret anywhere. To search by meaning as well, embed the index with Workers AI as the site builds:
For the Ask box, embed the index with Workers AI as the site builds, as `init --host github-pages` sets it up: the Worker then searches by meaning as well as by keywords. An index built without an embedding model, as the plugins build one with no `embedding` option and no model key, or `ondocs index` with `-e none`, is searched by keywords only and needs no secret anywhere. That is how the Worker's MCP endpoint searches either way, but the Ask box then answers only questions that use enough of the docs' own words.

```js
// The Docusaurus or Starlight plugin
Expand Down
4 changes: 2 additions & 2 deletions examples/nextjs/content/docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The `ondocs` command has four subcommands: `index` builds or checks the index, `
| `--check` | off | Exit 1 if the index is stale. Never calls a model. |
| `--base-url <url>` | `/` | URL prefix for pages, such as `/docs` or a full origin. |
| `-e, --embedding <spec>` | OpenAI when a key is set | `openai:<model>`, `workers-ai:@cf/<model>`, `<provider>/<model>`, `mock[:<dims>]` or `none`. |
| `--dimensions <n>` | the model's | Vector size, for models that support it. With `--check`, the size the index must have. |
| `--dimensions <n>` | 512 for the default | Vector size, for models that support it. With `--check`, the size the index must have. |
| `--chunk-size <chars>` | `1200` | Maximum characters per chunk. |
| `--chunk-overlap <chars>` | `150` | Characters shared by consecutive chunks of a section. |
| `--ignore <glob>` | none | Skip matching files. Repeatable. |
Expand All @@ -34,7 +34,7 @@ The `ondocs` command has four subcommands: `index` builds or checks the index, `

## Choosing the embedding model

Without `--embedding`, the CLI uses `openai:text-embedding-3-small` when `OPENAI_API_KEY` is set, or `openai/text-embedding-3-small` through AI Gateway when only `AI_GATEWAY_API_KEY` is. With neither, it fails rather than quietly building a keyword-only index, and suggests `-e mock` to try it without a key. `-e none` builds a keyword-only index on purpose. `-e workers-ai:@cf/baai/bge-small-en-v1.5` embeds with Workers AI over Cloudflare's REST API, with `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN`, for the [Workers AI Worker](/docs/workers-ai); its vector size is fixed, so `--dimensions` does not apply.
Without `--embedding`, the CLI uses `openai:text-embedding-3-small` when `OPENAI_API_KEY` is set, or `openai/text-embedding-3-small` through AI Gateway when only `AI_GATEWAY_API_KEY` is, at 512 dimensions, as the plugins do. That is a third of the model's 1,536, and makes the index about half the size: 1.54 MB against 2.93 MB for docusaurus.io's 1,070 chunks. `--dimensions` sets another size. A model you name with `--embedding` is built at its own size unless `--dimensions` gives one. The endpoint reads the size from the index, so an index built at 1,536 dimensions keeps working; the next build embeds every chunk again, once, at the new size. With neither key, the CLI fails rather than quietly building a keyword-only index, and suggests `-e mock` to try it without a key. `-e none` builds a keyword-only index on purpose. It suits agents' MCP search, which `init` writes keyword-only, but the Ask box then answers only questions that use enough of the docs' own words: embed the index for it ([How retrieval works](/docs/retrieval#tuning)). `-e workers-ai:@cf/baai/bge-small-en-v1.5` embeds with Workers AI over Cloudflare's REST API, with `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN`, for the [Workers AI Worker](/docs/workers-ai); its vector size is fixed, so `--dimensions` does not apply.

## Environment and exit codes

Expand Down
2 changes: 1 addition & 1 deletion examples/nextjs/content/docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ For a static site on Cloudflare Workers, `init` writes `worker/ondocs.ts`, which

## Environment variables

Set your provider's key, such as `OPENAI_API_KEY`, in two places: in the build, if the build embeds the index, and in the function's runtime environment, which embeds questions and calls the model. `init` prints where each goes on your host. The key never reaches the browser: the dialog only talks to your endpoint. A plugin's build without the key writes a keyword-only index, which the endpoint answers from without embedding questions; `ondocs index` writes one with `-e none`. The endpoint still needs the key, because a model writes every answer: without it, the dialog shows "Answers aren’t set up on this site yet." for every question it does not refuse. See [Troubleshooting](/docs/troubleshooting#answers-arent-set-up-on-this-site-yet), which also covers the other errors of a first deploy.
Set your provider's key, such as `OPENAI_API_KEY`, in two places: where the site or the index builds, which embeds the pages, and in the function's runtime environment, which embeds questions and calls the model. `init` prints where each goes on your host. The key never reaches the browser: the dialog only talks to your endpoint. Set it in the build too, not only where the endpoint runs: a plugin's build without the key writes a keyword-only index, and says so in a warning (`ondocs index` writes one only with `-e none`). The endpoint answers from it without embedding questions, so the Ask box answers only questions that use enough of the docs' own words, even with the key set where it runs. The MCP endpoint `init` writes searches by keywords either way. The endpoint still needs the key, because a model writes every answer: without it, the dialog shows "Answers aren’t set up on this site yet." for every question it does not refuse. See [Troubleshooting](/docs/troubleshooting#answers-arent-set-up-on-this-site-yet), which also covers the other errors of a first deploy.

## Cold starts

Expand Down
Loading
Loading