How I add semantic search to a Next.js site using Sanity Embeddings Sanity's native Embeddings feature enables semantic search in Next.js without managing external vector stores. A developer demonstrates wiring up the feature by creating an index in Sanity Studio, then querying it via a GROQ function in a Next.js API route. The approach requires a Growth or Enterprise plan and a read token, but eliminates the need for separate embedding services. Sanity Embeddings semantic search in Next.js is one of those features that looks complicated from the outside but is surprisingly lean to wire up once you understand the moving parts. This post covers the current native Embeddings feature built into Sanity datasets — not the older Embeddings Index API, which Sanity is sunsetting. If you found a guide that talks about a separate embeddings-index resource you have to provision via the Management API, it is stale; skip it. Sanity's native Embeddings feature lets you mark document types for vector indexing directly inside your dataset. Sanity handles the embedding model and the vector store; you never manage a separate service. Queries use a dedicated sanity.embeddings.query GROQ function that takes a natural-language string and returns documents ranked by semantic similarity. The feature is available on Growth and Enterprise plans as of mid-2026. The workflow has three parts: Go to Manage → your project → Embeddings or open the Embeddings pane inside Sanity Studio if your plan surfaces it there . Create an index, give it a name e.g. site search , and select which document types and fields to embed. For a blog you would typically pick post with fields title , excerpt , and body plain text extracted from Portable Text . Sanity backfills existing documents automatically. New and updated documents are re-embedded on publish via an internal webhook — you do not configure that yourself. There is no code required for the indexing step. The index name you choose here site search is what you will pass in the GROQ query. Create a route handler that accepts a search term, fires the semantic query against Sanity, and returns JSON. Using a server-side route handler keeps your Sanity read token out of the browser. js // app/api/search/route.ts import { NextRequest, NextResponse } from 'next/server'; import { createClient } from '@sanity/client'; const client = createClient { projectId: process.env.NEXT PUBLIC SANITY PROJECT ID , dataset: process.env.NEXT PUBLIC SANITY DATASET , apiVersion: '2024-09-01', useCdn: false, token: process.env.SANITY API READ TOKEN, } ; export async function GET req: NextRequest { const q = req.nextUrl.searchParams.get 'q' ?.trim ; if q return NextResponse.json { results: } ; // sanity::embeddings.query is the GROQ function for native semantic search. // 'site search' must match the index name you created in the Embeddings pane. const results = await client.fetch sanity::embeddings.query 'site search', $query, 4 { id, type, title, "slug": slug.current, excerpt } , { query: q } ; return NextResponse.json { results } ; } A few things worth noting here. The fourth argument to sanity::embeddings.query is the maximum number of results — keep it small 4–8 to stay within rate limits and keep the UI fast. useCdn: false is required because the CDN does not serve personalised query results. The read token is needed because embeddings queries run against the non-public API surface. If you are on a plan that does not include Embeddings, the GROQ function will throw; you will see a clear error in the logs rather than silent empty results. The UI is a client component so the user can type without a full page reload. It calls the route handler with a debounced fetch and renders results as they arrive. js // components/SemanticSearch.tsx 'use client'; import { useState, useTransition } from 'react'; type Result = { id: string; type: string; title: string; slug: string; excerpt?: string; }; export function SemanticSearch { const query, setQuery = useState '' ; const results, setResults = useState