How to Integrate Milvus with Your Headless CMS
Connect Milvus to your headless CMS so every published article, product, or help doc becomes searchable by meaning in seconds, not after the next batch job.
What is Milvus?
Milvus is an open-source vector database built for similarity search across embeddings, including text, images, audio, and multimodal content. Teams use Milvus to build semantic search, retrieval-augmented generation, recommendations, and clustering systems that need fast nearest-neighbor search across millions or billions of vectors. It is commonly run self-hosted with Docker or Kubernetes, and it is also available as a managed service through Zilliz Cloud.
Why integrate Milvus with a headless CMS?
Vector search only works well when the source content is clean, structured, and current. If your help center article has a title, summary, body, product reference, locale, and publish status, Milvus needs those fields in a predictable shape before you create embeddings and insert vectors. A headless CMS can work, but youβll get better results when the content is structured as typed JSON instead of page-shaped blobs.,A Milvus integration solves the gap between what editors publish and what AI systems can retrieve. When an editor updates a return policy at 2:14 PM, your support bot, semantic site search, and internal knowledge assistant should retrieve the new answer at 2:15 PM. Without a real-time sync path, teams often fall back to nightly exports, manual scripts, or crawlers that scrape rendered HTML and miss fields like audience, region, or product references.,With Sanity, content is structured in the Content Lake, queried with GROQ, and synced through webhooks or Functions when publish events happen. That means you can send Milvus only the fields that should affect retrieval, embed the text server-side, and upsert a vector with metadata like slug, locale, content type, and product ID. The trade-off is that you still need to choose an embedding model, design a Milvus collection schema, and handle deletes or unpublished content carefully.
Architecture overview
A typical Sanity and Milvus integration starts when an editor publishes or updates content in Sanity Studio. A Sanity webhook fires on the publish mutation, or a Sanity Function runs server-side on the content event. The handler receives the document ID, uses @sanity/client to fetch the latest document from the Content Lake with a GROQ query, and selects only the fields Milvus needs, such as title, excerpt, Portable Text body, category, locale, slug, and referenced product names. The handler then converts the selected text into an embedding with your model of choice, for example OpenAI text-embedding-3-small with 1,536 dimensions. Finally, it calls the Milvus SDK, usually @zilliz/milvus2-sdk-node or pymilvus, to upsert a row into a collection that contains an ID, vector field, text, and metadata fields. At query time, your frontend or AI agent embeds the userβs query, calls Milvus search against the vector field, filters by metadata such as locale or publish status, and uses the returned Sanity document IDs to fetch fresh structured content from the Content Lake for display or generation.
Common use cases
Semantic site search
Index articles, landing pages, and product docs in Milvus so users can search by meaning, even when they donβt use the exact words in your content.
Retrieval-augmented generation
Use Milvus to retrieve the top matching Sanity documents, then pass those snippets into an LLM for grounded answers in a support bot or internal agent.
Product discovery
Embed product descriptions, use cases, and buying guides so shoppers can ask for things like 'waterproof trail shoes for wide feet' and get relevant results.
Localized content retrieval
Store locale, region, and market metadata in Milvus so searches return the right translated content instead of mixing US, EU, and APAC versions.
Step-by-step integration
- 1
Set up Milvus
Create a Milvus instance with Docker, Kubernetes, or Zilliz Cloud. Create a collection such as sanity_content with a primary key field, a FLOAT_VECTOR field that matches your embedding dimension, for example 1,536 for text-embedding-3-small, and metadata fields like slug, locale, type, and updatedAt. Add an index such as HNSW or AUTOINDEX, depending on whether youβre self-hosting Milvus or using Zilliz Cloud.
- 2
Install the SDKs
For a TypeScript sync service or Sanity Function, install @zilliz/milvus2-sdk-node, @sanity/client, and your embedding provider SDK. For Python data pipelines, use pymilvus and the Python SDK for your embedding model instead.
- 3
Model searchable content in Sanity Studio
Define schema fields that make retrieval useful, not just display-ready. For an article, include title, slug, excerpt, body, locale, audience, product references, publish status, and updatedAt. Keeping these fields typed lets GROQ select the exact retrieval payload without parsing HTML.
- 4
Create the sync trigger
Use a Sanity webhook filtered to published article, product, or documentation changes, or run a Sanity Function on content mutations. Send the document ID in the payload, then fetch the full document inside the handler so you always index the latest version from the Content Lake.
- 5
Embed and upsert into Milvus
Join the fields you want users or agents to search, generate an embedding, and call Milvus upsert with the vector and metadata. Use the Sanity document ID as the primary key so future edits replace the existing vector instead of creating duplicates.
- 6
Test search from the frontend or agent
Embed a test query, call Milvus search, filter by metadata such as locale or content type, and use the returned Sanity IDs to fetch display content with GROQ. Test updates, deletes, unpublished drafts, and locale filters before sending traffic to the integration.
Code example
import {createClient} from '@sanity/client'
import {MilvusClient} from '@zilliz/milvus2-sdk-node'
import OpenAI from 'openai'
const sanity = createClient({
projectId: process.env.SANITY_PROJECT_ID!,
dataset: process.env.SANITY_DATASET!,
apiVersion: '2025-01-01',
token: process.env.SANITY_READ_TOKEN!,
useCdn: false,
})
const milvus = new MilvusClient({
address: process.env.MILVUS_ADDRESS!,
token: process.env.MILVUS_TOKEN,
})
const openai = new OpenAI({apiKey: process.env.OPENAI_API_KEY!})
export default async function handler(req: any, res: any) {
const {_id} = req.body
const doc = await sanity.fetch(`*[_id == $id][0]{
_id, _type, title, excerpt, "slug": slug.current,
locale, "category": category->title,
body[]{children[]{text}}
}`, {id: _id})
if (!doc) return res.status(204).end()
const bodyText = (doc.body || [])
.flatMap((block: any) => block.children?.map((child: any) => child.text) || [])
.join(' ')
const text = [doc.title, doc.excerpt, doc.category, bodyText]
.filter(Boolean)
.join('
')
const embedding = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: text,
})
await milvus.upsert({
collection_name: 'sanity_content',
data: [{
id: doc._id,
vector: embedding.data[0].embedding,
title: doc.title || '',
slug: doc.slug || '',
locale: doc.locale || 'en-US',
content_type: doc._type,
text,
}],
})
res.status(200).json({ok: true, id: doc._id})
}How Sanity + Milvus works
Build your Milvus integration on Sanity
Sanity gives you the structured content foundation, real-time event system, and flexible APIs you need to connect published content to Milvus.
Start building free βCMS approaches to Milvus
| Capability | Traditional CMS | Sanity |
|---|---|---|
| Content shape for embeddings | Content is often page-based HTML, so teams usually scrape pages or strip markup before creating vectors. | The Content Lake stores typed JSON, and GROQ can join references into one retrieval-ready payload. |
| Sync timing after publish | Search indexes are often refreshed by scheduled jobs, which can leave AI answers behind the published site. | Webhooks or Functions can trigger on content events, fetch the latest document, generate embeddings, and upsert to Milvus. |
| Field-level control | Indexing often includes navigation, layout copy, and unrelated page text unless a crawler is heavily tuned. | GROQ selects exactly the fields that should affect retrieval, including nested Portable Text and referenced documents. |
| Metadata filters in vector search | Locale, audience, product, and publish status can be hard to extract reliably from rendered pages. | Schema-as-code makes metadata explicit, so Milvus rows can include filters like locale, market, product ID, and content type. |
| Delete and unpublish handling | Deleted pages can remain in a vector index until the next crawl or manual cleanup. | Content mutation events can trigger delete or tombstone logic so Milvus stops returning unpublished content. |
| Editorial workflow impact | Editors may need to wait for search reindexing or ask developers to rerun scripts. | Editors publish in Sanity Studio, and the same event can update Milvus, preview experiences, and downstream channels. |
Keep building
Explore related integrations to complete your content stack.
Sanity + Pinecone
Use Sanity content with Pinecone for managed vector search in RAG systems, semantic search, and recommendations.
Sanity + Weaviate
Connect structured Sanity documents to Weaviate for vector search with rich metadata filters and hybrid retrieval.
Sanity + Qdrant
Sync Sanity content to Qdrant for fast similarity search, payload filtering, and AI agent retrieval workflows.