Agent Engineering

Hands-on Milvus

Use one collection name and real text embeddings from insert through delete. This example assumes local Milvus is already listening on port 19530.

Start the service with the official Docker Standalone guide, then install pymilvus and sentence-transformers. The dimension is derived from actual model output—no random vectors posing as semantic search.
Connect, define, and batch insert
from pymilvus import MilvusClient, DataType
from sentence_transformers import SentenceTransformer
COLLECTION = "support_kb"
client = MilvusClient(uri="http://localhost:19530")
print(client.list_collections())  # Verify connectivity; health is commonly exposed at 9091/healthz
RESET_LAB = False  # Set True only after confirming old lab data is disposable
if client.has_collection(collection_name=COLLECTION):
    if not RESET_LAB:
        raise RuntimeError("support_kb exists; rename it or explicitly enable RESET_LAB")
    client.drop_collection(collection_name=COLLECTION)  # Deletes the whole collection
encoder = SentenceTransformer("BAAI/bge-m3")
docs = [{"id": 1, "text": "Refunds arrive within three business days", "category": "refund"},
        {"id": 2, "text": "Sign in again after changing your password", "category": "account"}]
vectors = encoder.encode([d["text"] for d in docs], normalize_embeddings=True).tolist()
schema = MilvusClient.create_schema(auto_id=False, enable_dynamic_field=False)
schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True)
schema.add_field(field_name="vector", datatype=DataType.FLOAT_VECTOR, dim=len(vectors[0]))
schema.add_field(field_name="text", datatype=DataType.VARCHAR, max_length=1000)
schema.add_field(field_name="category", datatype=DataType.VARCHAR, max_length=64)
client.create_collection(collection_name=COLLECTION, schema=schema)
client.insert(collection_name=COLLECTION, data=[{**d, "vector": v} for d, v in zip(docs, vectors)])
Index, load, and filtered Top-K search
index = client.prepare_index_params()
index.add_index(field_name="vector", index_type="HNSW", metric_type="COSINE",
                params={"M": 16, "efConstruction": 200})
client.create_index(collection_name=COLLECTION, index_params=index)
client.load_collection(collection_name=COLLECTION)
query_vector = encoder.encode(["When will my refund arrive?"], normalize_embeddings=True).tolist()
hits = client.search(collection_name=COLLECTION, data=query_vector, anns_field="vector", limit=3,
    filter='category == "refund"', output_fields=["text", "category"],
    search_params={"metric_type": "COSINE", "params": {"ef": 64}})
Query and delete
rows = client.query(collection_name=COLLECTION, filter='category == "account"', output_fields=["id", "text"])
client.delete(collection_name=COLLECTION, filter="id == 2")
# Destructive; use only in a disposable lab:
# client.drop_collection(collection_name=COLLECTION)

Writes

Insert in batches; use stable IDs plus upsert or application deduplication for idempotency—insert alone does not deduplicate. Record content, embedding-model, and model-version metadata.

Deletes

For reversible deletion, mark active=false first. Physical deletes reclaim storage later through Compaction.

Checklist Same model, dimension, and COSINE metric; real embeddings; load before search. Use FLAT as a small-scale correctness baseline before tuning HNSW.