Enabling semantic search
Turn on vector-ranked search: add the Workers AI binding, create a semantic index, rebuild, and query.
#Enabling semantic search
Semantic search re-ranks full-text results by vector similarity, so "warm coat" can surface a record that only says "insulated winter jacket". It runs entirely on your gateway using Cloudflare Workers AI — no external service. This guide is for app builders consuming the SDK against a deployed EmuView instance.
#1. Check whether your instance has the AI binding
Semantic search requires the gateway worker's Workers AI binding (AI). Probe it before offering semantic features in your app:
const health = await fetch('https://your-api.example.com/health').then((r) => r.json());
const semanticAvailable = health.bindings.AI === true;
// When absent, 'AI' is listed in health.missing.optional
If AI is missing, everything else in this guide still works except semantic: true queries — indexes build (FTS-only) and lexical queries return normally.
#2. Deploy with the binding (instance operators)
The standard wrangler.template.toml already declares the binding:
[ai]
binding = "AI"
If your instance was deployed from an older config without it, add the block to the gateway's wrangler.toml and redeploy (wrangler deploy). There is nothing to provision — Workers AI is available to every Cloudflare account (usage is billed per inference). Note: under wrangler dev the binding exists but inference still runs remotely on Cloudflare; if the call fails (e.g. offline), semantic queries silently fall back to full-text results.
#3. Create a semantic index
Set semanticSearch: true on the index config. Give the fields you want embedded the vector role (they are concatenated per record and embedded at build time):
await sdk.search.create({
indexName: 'articles',
public: true,
semanticSearch: true,
// Optional — defaults to '@cf/baai/bge-small-en-v1.5'
// embeddingModel: '@cf/baai/bge-base-en-v1.5',
sources: [
{
collection: 'articles',
url: '/articles/{id}',
fields: [
{ name: 'title', roles: ['search', 'display', 'vector'] },
{ name: 'body', roles: ['search', 'vector'], contentProcessor: 'strip_html' },
{ name: 'category', roles: ['filter'] }
]
}
]
});
#4. Rebuild to generate embeddings
Embeddings are computed during the index build, not at query time:
await sdk.search.rebuild('articles');
// Poll until the build completes
let status;
do {
await new Promise((r) => setTimeout(r, 1000));
status = await sdk.search.status('articles');
} while (status.status === 'building');
Rebuild again after significant content changes — the compiled index (and its vectors) is a snapshot.
#5. Query with semantic: true
const { results } = await sdk.search.query('articles', {
q: 'warm coat',
semantic: true,
limit: 20
});
The gateway runs the normal full-text query, embeds your query string, ranks stored vectors by cosine similarity, and fuses the two rankings (Reciprocal Rank Fusion). Omit semantic for plain lexical search.
#Error handling
On an instance without the AI binding, a semantic: true query returns a typed error instead of silently degrading:
// HTTP 503
{ "error": "semantic_unavailable", "message": "Semantic search is unavailable: ..." }
Catch it and fall back to a lexical query (or hide the semantic toggle up front using the /health probe from step 1):
try {
return await sdk.search.query('articles', { q, semantic: true });
} catch (err) {
if ((err as any)?.error === 'semantic_unavailable') {
return await sdk.search.query('articles', { q }); // lexical fallback
}
throw err;
}
Two related states are intentionally not errors:
- Binding present, index built without embeddings (e.g. built while AI was absent, or no
vector-role fields): the query returns FTS results unchanged. Rebuild to generate vectors. - Transient AI failure at query time: the gateway logs it and returns FTS results — semantic search degrades, requests don't fail.
#What you learned
- Semantic search needs the gateway's Workers AI binding; probe
GET /health(bindings.AI/missing.optional) to detect it - Deploys use the standard template's
[ai]block; older instances add it and redeploy semanticSearch: true+vector-role fields at index creation; embeddings generate onrebuild()- Query with
semantic: true; handle the typedsemantic_unavailable503 on instances without the binding