Enabling semantic search

Turn on vector-ranked search: add the Workers AI binding, create a semantic index, rebuild, and query.

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 on rebuild()
  • Query with semantic: true; handle the typed semantic_unavailable 503 on instances without the binding