# How did we add semantic search to a Sanity FAQ without a vector database?

Reviewed by Martin Dejnicki · facts verified 25 October 2026 · canonical: https://deploi.ca/sanity/builds/semantic-search-in-groq

## Short answer

Deploi added semantic search to its Sanity FAQ hub's assistant with one GROQ (Graph-Relational Object Queries) query ranked by `text::semanticSimilarity()`. No vector database is needed, because Dataset Embeddings add semantic search to GROQ ([Sanity Docs](https://www.sanity.io/docs/content-lake/dataset-embeddings), October 2026). A keyword query takes over when the semantic query errors. Sanity's Free plan includes 500 embeddings queries a month, Growth 1,000 ([Sanity pricing](https://www.sanity.io/pricing), October 2026).

- **Retrieval:** One GROQ query, no second system. text::semanticSimilarity() runs inside score() on an embeddings-enabled dataset, so filtering, ranking, slicing and projection share one request ([Sanity Docs, Dataset Embeddings](https://www.sanity.io/docs/content-lake/dataset-embeddings); [Sanity Docs, Migrate from the Embeddings Index API to Dataset Embeddings](https://www.sanity.io/docs/content-lake/migrate-from-embeddings-index-api))
- **Embeddings queries a month:** 500 on Free, 1,000 on Growth. Free lists 500 as a monthly limit; Growth bills overage at $1.50 per 1,000 queries; Enterprise is custom. Generating embeddings is included on all plans, as of October 2026 ([Sanity, Plans that scale with you](https://www.sanity.io/pricing); [Sanity Docs, Dataset Embeddings](https://www.sanity.io/docs/content-lake/dataset-embeddings))
- **The template's original path:** Embeddings Index API, now deprecated. The public FAQ Hub template queries a named index; Sanity deprecated that API, its CLI and its Studio plugin, with no new features or fixes ([Deploi, faq-hub-with-ai-search embeddingsIndex.js](https://raw.githubusercontent.com/Team-Deploi/faq-hub-with-ai-search/main/frontend/sanity/embeddingsIndex.js); [Sanity Docs, Migrate from the Embeddings Index API to Dataset Embeddings](https://www.sanity.io/docs/content-lake/migrate-from-embeddings-index-api); [Sanity Docs, Embeddings index introduction (deprecated)](https://www.sanity.io/docs/content-lake/embeddings-index-api-overview))
- **Relevance score:** Ranks within one query only. _score is opaque and unitless, so numeric thresholds such as score > 0.8 must go; bound results with a slice instead ([Sanity Docs, Dataset Embeddings](https://www.sanity.io/docs/content-lake/dataset-embeddings); [Sanity Docs, Migrate from the Embeddings Index API to Dataset Embeddings](https://www.sanity.io/docs/content-lake/migrate-from-embeddings-index-api))

## Deploi's call

Use Dataset Embeddings and one GROQ query for an FAQ-sized corpus, and keep a keyword query behind it. A separate vector database adds a sync job and another system to secure, and nothing in our build needed one. Spend the effort on answer capsules and a test set of real questions instead.

**REUSE: Reuse Sanity semantic search in GROQ, with no vector database, when**

- Your FAQ answers already live in a Sanity dataset you can enable Dataset Embeddings on, so one GROQ (Graph-Relational Object Queries) query can rank them with text::semanticSimilarity().
- Visitors and assistants ask in sentences, not in product codes, because meaning is what the embeddings capture.
- Monthly searches fit Sanity's 500 embeddings queries on Free or 1,000 on Growth as of October 2026, or the Growth overage is budgeted.
- You can collect real questions to test the ranking before and after launch.

**AVOID: Avoid it, and the template's Embeddings Index path, when**

- You'd build new search on the Embeddings Index API (application programming interface), which Sanity has deprecated as of October 2026.
- You need reference suggestions in AI Assist or Agent Actions: Sanity's migration guide does not cover them, and says it has no replacement for references with Agent Actions as of October 2026.
- Your ranking has to reject weak matches by a score threshold, since Sanity's semantic score only ranks results within one query.

## Why did we skip a vector database for our FAQ search?

Deploi skipped a vector database because Sanity's Dataset Embeddings put semantic search inside GROQ (Graph-Relational Object Queries), so "you don't need an external vector database or a separate search pipeline" ([Sanity Docs, Dataset Embeddings](https://www.sanity.io/docs/content-lake/dataset-embeddings), October 2026). The search behind the assistant on [deploi.ca's Shopify FAQ hub](https://deploi.ca/shopify/faq) is one GROQ query over the same Sanity dataset that serves the pages, which held 256 FAQ answers on 24 October 2026. A vector database would add a sync job. Here Sanity recomputes a document's embedding after each update, normally in under a minute.

The public template took a different route. The FAQ Hub with AI Chatbot template, which Martin Dejnicki published on Sanity Exchange ([Sanity Exchange](https://www.sanity.io/templates/faq-hub-with-ai-chatbot), October 2026), searches through the Embeddings Index API: a named index queried at its own endpoint ([the template's code](https://raw.githubusercontent.com/Team-Deploi/faq-hub-with-ai-search/main/frontend/sanity/embeddingsIndex.js), October 2026). Sanity has since deprecated that API ([Sanity's migration guide](https://www.sanity.io/docs/content-lake/migrate-from-embeddings-index-api), October 2026), and on 24 October 2026 our own project's plan returned Unauthorized when we listed embeddings indexes. So deploi.ca's assistant runs `text::semanticSimilarity()` instead. The [knowledge base use case](https://deploi.ca/sanity/use-cases/knowledge-base-with-ai-answers) covers the content model around the search, and the template build covers the starter itself.

## How does semantic search work in plain GROQ?

Semantic search in GROQ (Graph-Relational Object Queries) is a scoring function, not a filter. You enable embeddings once per dataset with the Sanity command-line interface (CLI), optionally with a projection that limits which fields get embedded. Then `text::semanticSimilarity()` turns the search text into a vector and ranks documents by how close they sit; Sanity says it's "only valid as an argument to `score()`" ([Sanity Docs, Dataset Embeddings](https://www.sanity.io/docs/content-lake/dataset-embeddings), October 2026). Our query filters to FAQ answers that have a slug, ranks them, and returns the top IDs:

**deploi.ca's assistant search query, from Deploi's frontend code (sanity/queries.js)**

```groq
*[_type == "faqArticle" && defined(slug.current)]
  | score(text::semanticSimilarity($query))
  | order(_score desc)
  [0...$maxResults]._id
```

Three rules shape it, and they apply to GROQ's other scoring functions too. Filter first, because semantic scoring only ranks what the filter returns. Always slice, because "There is no default limit in GROQ" ([Sanity's migration guide](https://www.sanity.io/docs/content-lake/migrate-from-embeddings-index-api), October 2026). And never compare `_score` to a fixed number: Sanity calls it "an opaque, unitless value" for ranking within one query ([Sanity Docs, Dataset Embeddings](https://www.sanity.io/docs/content-lake/dataset-embeddings)). Our test question about moving from Shopify Advanced to Plus scored 8.80, 8.51 and 8.44 on 24 October 2026. Those figures order three answers and mean nothing outside that query.

## Why keep a keyword fallback behind the semantic query?

The keyword fallback keeps the assistant answering when the semantic GROQ (Graph-Relational Object Queries) query fails. Sanity documents that a `text::semanticSimilarity()` query against a dataset without embeddings "will return an error" ([Sanity Docs, Dataset Embeddings](https://www.sanity.io/docs/content-lake/dataset-embeddings), October 2026), and embeddings queries draw on a metered monthly quota ([Sanity pricing](https://www.sanity.io/pricing), October 2026). Our code tries semantic search first and runs a keyword query on an error or an empty result:

**Semantic first, keyword second, from Deploi's frontend code (sanity/copilotActions.js)**

```js
async function searchArticleIds(query, maxResults) {
  try {
    const ids = await client.fetch(FAQ_SEMANTIC_SEARCH_QUERY, { query, maxResults });
    if (ids?.length) return ids;
    console.warn("FAQ semantic search returned nothing; using keyword search");
  } catch (error) {
    console.warn("FAQ semantic search unavailable; using keyword search:", error.message);
  }
  const terms = toPlainTerms(query);
  if (!terms) return [];
  return client.fetch(FAQ_KEYWORD_SEARCH_QUERY, { terms, maxResults });
}
```

The keyword query weights matches with `boost()`: title 4, the answer capsule and excerpt 2, and body text 1, read from Portable Text with `pt::text()`. Before it runs, `toPlainTerms` strips quotes, asterisks and leading minus signs from the assistant's search string. That matters because `text::query()` reads double quotes as an exact phrase and a leading minus as an exclusion ([Sanity Docs, Search text content with GROQ](https://www.sanity.io/docs/content-lake/search-content-with-groq), October 2026), so a model-written "-Plus" would hide the very answers it wants.

## How does the deploi.ca assistant use the search?

Deploi's on-site FAQ assistant calls the semantic search as a tool and answers from the ranked results it gets back. It runs on CopilotKit, a library for in-app AI assistants, inside a Next.js API (application programming interface) route. The knowledge-base action appears on the FAQ hub's home and category pages only; the Build vs. Buy and Builds sections get a company-information action.

**One question becomes one ranked list of answers and one grounded reply**

- Visitor question -> Assistant: the question, on FAQ hub home and category pages
- Assistant -> Semantic GROQ query: the search string, 5 results by default
- Semantic GROQ query -> Keyword GROQ query: only on an error or an empty result
- Either query -> Article query: answer IDs in rank order
- Article query -> Assistant: capsule, body text and page URLs

Each question costs two GROQ (Graph-Relational Object Queries) requests: one ranks IDs, one fetches the answers. Sanity's migration guide collapses that into one query that returns documents with their score ([Sanity's migration guide](https://www.sanity.io/docs/content-lake/migrate-from-embeddings-index-api), October 2026), and we'd adopt it on the next pass. The [Next.js integration](https://deploi.ca/sanity/integrations/nextjs) covers the framework side, and Sanity Context covers giving an outside AI agent GROQ access, where Sanity says to "enable dataset embeddings and stay in GROQ mode" for content between structured data and prose ([Sanity Docs, Context retrieval modes](https://www.sanity.io/docs/ai/sanity-context-retrieval-modes), October 2026).

## What went wrong in our semantic search build, and what would we change?

Three things went wrong or remain unproven in Deploi's Sanity semantic search: the public template still calls a deprecated API, keyword and semantic scoring never combine, and retrieval quality is unmeasured. Each has a known fix.

- **The template still calls a deprecated API (application programming interface).** It posts searches to `/embeddings-index/query/` ([the template's code](https://raw.githubusercontent.com/Team-Deploi/faq-hub-with-ai-search/main/frontend/sanity/embeddingsIndex.js), October 2026), an API Sanity has deprecated with its Studio plugin ([Sanity's migration guide](https://www.sanity.io/docs/content-lake/migrate-from-embeddings-index-api), October 2026). The fix: enable embeddings on the dataset, copy the index filter into the GROQ (Graph-Relational Object Queries) filter, and call `text::semanticSimilarity()` inside `score()`.
- **Keywords and meaning never meet.** Keyword search runs only when semantic search fails, and since semantic scoring ranks every filtered document, an empty result almost never happens (Deploi's reading of the docs). Sanity calls hybrid scoring in one `score()` "typically the right choice for production search" ([Sanity Docs, Search text content with GROQ](https://www.sanity.io/docs/content-lake/search-content-with-groq), October 2026). Sanity's embeddings guide says that for many use cases filtered semantic search "is sufficient on its own" and recommends adding keyword matching for proper nouns and identifiers ([Sanity Docs, Dataset Embeddings](https://www.sanity.io/docs/content-lake/dataset-embeddings)), so the second signal earns its place only where those terms matter. App and plan names are the kind of terms that "don't embed well as concepts" ([Sanity Docs, Dataset Embeddings](https://www.sanity.io/docs/content-lake/dataset-embeddings)), so the next version scores both signals in one query.
- **Quality is unmeasured.** No precision or answer-quality numbers exist. Sanity asks for "A set of real search queries to test against" before a cutover ([Sanity's migration guide](https://www.sanity.io/docs/content-lake/migrate-from-embeddings-index-api)), and building that set is our next step.

## When should you reuse this semantic search pattern?

Reuse Deploi's semantic search pattern when your content already lives in Sanity, searches arrive as questions, and monthly volume fits the embeddings query quota. Dataset Embeddings are available on every plan, generation is included, and only the reads are metered ([Sanity Docs, Dataset Embeddings](https://www.sanity.io/docs/content-lake/dataset-embeddings), October 2026). As of October 2026, Free stops at 500 embeddings queries a month and Growth includes 1,000, then bills overage ([Sanity pricing](https://www.sanity.io/pricing), October 2026). Queries count against your organization's monthly quota ([Sanity Docs, Dataset Embeddings](https://www.sanity.io/docs/content-lake/dataset-embeddings)). The [knowledge base use case](https://deploi.ca/sanity/use-cases/knowledge-base-with-ai-answers) prices that meter at chatbot volumes.

Plan the projection before you reuse it, because Sanity embeds at most 10 chunks per document, "currently 10 and subject to change", and drops the rest ([Sanity Docs, Dataset Embeddings](https://www.sanity.io/docs/content-lake/dataset-embeddings)). Long answers need a projection that keeps the question, the capsule and the body's opening.

> **When not to reuse it**
>
> - You need reference suggestions in AI Assist or Agent Actions: the migration guide does not cover them and has no replacement for references with Agent Actions ([Sanity's migration guide](https://www.sanity.io/docs/content-lake/migrate-from-embeddings-index-api), October 2026).
> - You need a relevance cutoff by score: the score only ranks, so bound results with a slice.
> - Visitors search by stock-keeping unit (SKU) or model number: start with keyword `match`, then add semantic scoring.

## Deploi's point of view

**Where we disagree.** The common advice says an AI assistant needs a vector database and an embedding pipeline next to the CMS. On Sanity that's now the long way round: the embeddings live in the dataset and update themselves. The second mistake is treating a similarity score as confidence. Sanity's score only ranks results within one query, so a rule like answer only above 0.8 does nothing useful. Bound the result count and let the answer step judge relevance.

**What we've seen.** Our public template launched on the Embeddings Index API, and on 24 October 2026 our own project's plan returned Unauthorized when we listed embeddings indexes. deploi.ca's assistant runs on text::semanticSimilarity() over the dataset that holds its 256 FAQ answers, and a test question about moving from Shopify Advanced to Plus returned three Advanced vs. Plus answers as its top three that day. We haven't measured retrieval quality beyond that check.

**What we refuse to promise.** We won't quote an accuracy figure for this search. No precision or answer-quality measurement exists for it, and the score Sanity returns can't stand in for one.

## When this answer changes

- Sanity sets a sunset date for the Embeddings Index API
- Sanity changes the embeddings query quotas or the $1.50 per 1,000 overage
- Sanity raises or removes the 10-chunk cap per document
- Sanity publishes a replacement for references with Agent Actions or reference suggestions in AI Assist
- Deploi ports the public template to text::semanticSimilarity()

## How do you build the knowledge base around it?

See the content model, the assistant and the plan requirements for a Sanity knowledge base with AI answers. [Read the knowledge base use case](https://deploi.ca/sanity/use-cases/knowledge-base-with-ai-answers)

## Sources

1. [Sanity Docs, Dataset Embeddings](https://www.sanity.io/docs/content-lake/dataset-embeddings), checked 24 October 2026
2. [Sanity Docs, Search text content with GROQ](https://www.sanity.io/docs/content-lake/search-content-with-groq), checked 24 October 2026
3. [Sanity Docs, Migrate from the Embeddings Index API to Dataset Embeddings](https://www.sanity.io/docs/content-lake/migrate-from-embeddings-index-api), checked 24 October 2026
4. [Sanity, Plans that scale with you](https://www.sanity.io/pricing), checked 24 October 2026
5. [Sanity Docs, Embeddings index introduction (deprecated)](https://www.sanity.io/docs/content-lake/embeddings-index-api-overview), checked 24 October 2026
6. [Sanity Docs, Context retrieval modes](https://www.sanity.io/docs/ai/sanity-context-retrieval-modes), checked 24 October 2026
7. [Sanity Exchange, FAQ Hub with AI Chatbot](https://www.sanity.io/templates/faq-hub-with-ai-chatbot), checked 24 October 2026
8. [Deploi, faq-hub-with-ai-search embeddingsIndex.js](https://raw.githubusercontent.com/Team-Deploi/faq-hub-with-ai-search/main/frontend/sanity/embeddingsIndex.js), checked 24 October 2026
