AI Vector & Retrieval8 min read

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.

Published April 29, 2026
01 β€” Overview

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.


02 β€” The case for integration

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.


03 β€” Architecture

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.


04 β€” Use cases

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.


05 β€” Implementation

Step-by-step integration

  1. 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. 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. 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. 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. 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. 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.


06 β€” Code

Code example

typescriptapi/sanity-to-milvus.ts
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})
}

07 β€” Why Sanity

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 β†’

08 β€” Comparison

CMS approaches to Milvus

CapabilityTraditional CMSSanity
Content shape for embeddingsContent 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 publishSearch 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 controlIndexing 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 searchLocale, 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 handlingDeleted 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 impactEditors 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.

09 β€” Next steps

Keep building

Explore related integrations to complete your content stack.

Ready to try Sanity?

See how Sanity's Content Operating System powers integrations with Milvus and 200+ other tools.