About this session
| Session | Details |
|---|---|
| Duration | 2–3 hours, including exercises |
| You will build | A Milvus collection of IT runbook snippets with embeddings and metadata, searchable by meaning and filterable by category and severity |
| You will learn | Vectors and similarity, Milvus terms (collection, schema, field, primary key, index, metric, load), Milvus Lite vs standalone, MilvusClient create/insert/search/query/delete, filter expressions, Attu |
| Prerequisites | Basic Python; for the Docker path, Docker Desktop or Docker Engine with about 4 GB free RAM |
If you completed the Chroma beginner session, the ideas are the same; Milvus adds an explicit schema and a load step, and scales out to clusters. The advanced session covers schemas, index tuning, partitions, hybrid search, security and Kubernetes.
Module 1: the ideas you need first
Vectors and similarity (60-second recap)
An embedding model turns text into a vector of numbers. Similar meaning → nearby vectors. A vector database finds the nearest vectors to a query vector quickly, using an index (an approximate nearest-neighbour structure such as HNSW).
Milvus vocabulary
| Term | Meaning |
|---|---|
| Collection | Like a table: holds entities with the same schema |
| Schema / fields | Columns: a primary key, one or more vector fields, and scalar fields (numbers, strings, JSON) |
| Entity | One row |
| Metric type | How similarity is measured: COSINE, L2 or IP for dense vectors |
| Index | Makes vector search fast; AUTOINDEX lets Milvus choose |
| Load / release | A collection must be loaded into memory before it can be searched |
| Segment | Internal chunk of data; growing segments receive inserts, sealed ones get indexed |
| Partition | Optional subdivision of a collection |
Ways to run Milvus
| Option | What it is | Use it for |
|---|---|---|
| Milvus Lite | A Python library; data in a local file. Linux and macOS only | Learning, notebooks, tests |
| Standalone | One Milvus process in Docker with embedded or bundled dependencies | Dev/test, small production |
| Distributed | Many components on Kubernetes with etcd, object storage and a message queue | Production at scale |
| Zilliz Cloud | Managed Milvus | No servers to run |
The same MilvusClient code works against all of them; only the connection URI changes.
Module 2: start Milvus
Option A: Milvus Lite (Linux or macOS)
python -m venv .venv && source .venv/bin/activate
pip install -U pymilvus sentence-transformers
from pymilvus import MilvusClient
client = MilvusClient("./milvus_lab.db") # a local file = Milvus Lite
Option B: Milvus standalone in Docker (any OS)
Milvus publishes Docker Compose files with each release. Download the one for your version from the Milvus GitHub releases page (file name like milvus-standalone-docker-compose.yml), save it as docker-compose.yml, then:
docker compose up -d
docker compose ps # milvus-standalone, milvus-etcd, milvus-minio should be healthy
curl -s http://localhost:9091/healthz # prints OK when ready
Milvus listens on 19530 (gRPC/HTTP API) and 9091 (health and metrics).
On Linux there is also a single-container script (standalone_embed.sh) and on Windows a batch script (standalone.bat) in the Milvus docs; any of these is fine for the lab.
Install the client:
pip install -U pymilvus sentence-transformers
from pymilvus import MilvusClient
client = MilvusClient(uri="http://localhost:19530", token="root:Milvus") # default credentials
print(client.list_collections())
Warning
root:Milvusis the well-known default. Authentication may be disabled by default in a fresh install, in which case any token works. Never expose a lab instance to a network; the advanced session turns on authentication and changes the password.
Module 3: create your first collection
The quick-setup form creates an id primary key, a vector field, enables dynamic fields (extra keys are stored automatically) and builds an index:
client.create_collection(
collection_name="runbooks",
dimension=384, # must match your embedding model
metric_type="COSINE",
auto_id=False,
)
print(client.describe_collection("runbooks"))
print(client.get_load_state("runbooks"))
Quick setup also loads the collection. Look at describe_collection output: you'll see the fields, the dimension and enable_dynamic_field: True.
Module 4: embed and insert data
We use all-MiniLM-L6-v2 (384 dimensions), which runs locally.
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("all-MiniLM-L6-v2")
docs = [
("SQL Server transaction log is full, error 9002. Take a log backup and check log_reuse_wait_desc.", "databases", 1),
("PostgreSQL refuses connections: too many clients already. Use a pooler such as PgBouncer.", "databases", 2),
("Linux root filesystem at 100 percent. Find large files with du and clear old logs.", "linux", 1),
("Azure VM will not boot after a kernel update. Use the serial console and a repair VM.", "azure", 1),
("Kubernetes pods stuck in ImagePullBackOff because registry credentials expired.", "kubernetes", 2),
("Reset a forgotten Windows local administrator password with the VM access extension.", "windows", 3),
]
vectors = model.encode([d[0] for d in docs], normalize_embeddings=True)
rows = [
{"id": i, "vector": vectors[i].tolist(), "text": text, "category": cat, "severity": sev}
for i, (text, cat, sev) in enumerate(docs)
]
res = client.insert(collection_name="runbooks", data=rows)
print(res) # {'insert_count': 6, 'ids': [0, 1, 2, 3, 4, 5]}
text, category and severity aren't in the schema; they're stored as dynamic fields because quick setup enabled them. That is convenient for learning; the advanced session uses an explicit schema.
Module 5: search by meaning
q = model.encode(["my disk ran out of space"], normalize_embeddings=True)
hits = client.search(
collection_name="runbooks",
data=q.tolist(),
limit=3,
output_fields=["text", "category", "severity"],
)
for h in hits[0]:
print(f"{h['distance']:.3f} {h['entity']['category']:<11} {h['entity']['text'][:70]}")
With COSINE, the score is a similarity: higher is better (up to 1.0). With L2 it is a distance: lower is better. Always know which metric your collection uses before setting thresholds.
Several queries at once:
qs = model.encode(["database won't accept connections", "forgot admin password"], normalize_embeddings=True)
res = client.search("runbooks", data=qs.tolist(), limit=1, output_fields=["text"])
print([r[0]["entity"]["text"][:40] for r in res])
Module 6: filters, query, get and delete
Filtered search
Milvus filters use a boolean expression string:
hits = client.search(
collection_name="runbooks",
data=model.encode(["urgent outage"], normalize_embeddings=True).tolist(),
limit=5,
filter='severity <= 1 and category in ["linux", "azure"]',
output_fields=["text", "category", "severity"],
)
| Expression | Meaning |
|---|---|
category == "databases" | Equal (strings in double quotes) |
severity >= 2 | Comparison |
category in ["linux", "azure"] | In a list |
text like "Kubernetes%" | Prefix match |
a and b, a or b, not a | Combine |
Use a template with parameters to avoid building strings from user input:
client.search("runbooks", data=q.tolist(), limit=3,
filter="category == {cat}", filter_params={"cat": "databases"},
output_fields=["text"])
(filter_params needs a recent pymilvus/Milvus; with older versions build the string carefully.)
Query and get (no vector involved)
print(client.query("runbooks", filter="severity == 1", output_fields=["text", "category"]))
print(client.get("runbooks", ids=[0, 2], output_fields=["text"]))
Upsert and delete
v = model.encode(["Redis OOM command not allowed: maxmemory reached. Check big keys and eviction policy."],
normalize_embeddings=True)[0].tolist()
client.upsert("runbooks", data=[{"id": 6, "vector": v, "text": "Redis OOM ... eviction policy.",
"category": "databases", "severity": 2}])
client.delete("runbooks", ids=[5])
client.delete("runbooks", filter='category == "windows"')
print(client.query("runbooks", filter="id >= 0", output_fields=["count(*)"]))
Deletes in Milvus are logical at first; space is reclaimed later by compaction. For lab sizes, you won't notice.
Module 7: load and release
Searching a released collection fails with a "collection not loaded" error.
client.release_collection("runbooks")
print(client.get_load_state("runbooks")) # NotLoad
client.load_collection("runbooks")
print(client.get_load_state("runbooks")) # Loaded
Loaded collections use memory on the query nodes. In real deployments you release collections that aren't in use, and size memory for the ones that are. More: Milvus "collection not loaded" and query node memory.
Module 8: explore with Attu
Attu is the Milvus web UI. With standalone in Docker:
docker run -d --name attu -p 8000:3000 -e MILVUS_URL=host.docker.internal:19530 zilliz/attu:latest
Use a pinned tag that matches your Milvus version in real use. On Linux, replace host.docker.internal with the host IP or run Attu on the same Docker network as Milvus.
Open http://localhost:8000 and:
- Browse the
runbookscollection, its schema and index. - Run a vector search from the UI using an entity's vector.
- Check load state and segment information.
Module 9: a mini RAG retrieval step
def retrieve(question, k=3, min_score=0.35, category=None):
vec = model.encode([question], normalize_embeddings=True).tolist()
flt = f'category == "{category}"' if category else ""
res = client.search("runbooks", data=vec, limit=k, filter=flt, output_fields=["text", "category"])
return [{"text": h["entity"]["text"], "score": round(h["distance"], 3)}
for h in res[0] if h["distance"] >= min_score] # COSINE: higher is better
hits = retrieve("postgres says too many clients")
context = "\n".join(f"- {h['text']}" for h in hits) or "(no relevant runbook)"
print(f"Answer using only these notes:\n{context}\n\nQuestion: postgres says too many clients")
As with any RAG app, the score cut-off prevents weak matches from becoming "context". Pick it from your own data.
Module 10: clean up
client.drop_collection("runbooks")
docker compose down # stop Milvus, keep data volumes
docker compose down -v # stop and delete data (lab only)
For Milvus Lite, delete the .db file.
Exercises
- Add 20 of your own runbook snippets with
category,severityandowner. - Write five realistic questions; record top results and scores; find a sensible
min_score. - Create a second collection with
metric_type="L2"and compare rankings and score meaning. - Release the collection, try a search, read the error, then load it again.
- Use Attu to find the number of entities and the index type of your collection.
- Write a filter that returns only severity-1 database runbooks owned by your team.
Knowledge check
- What must match between your embedding model and the collection?
- With
COSINE, is a higher or lower score better? And withL2? - What happens if you search a collection that isn't loaded?
- What are dynamic fields, and when are they enabled?
- Write a filter for "category is linux or azure and severity is 1".
- What is the difference between
queryandsearch? - Which ports does standalone Milvus use for the API and for health/metrics?
- When would you choose Milvus Lite, standalone or distributed?
Answers
- The vector dimension (and you must use the same model for inserts and queries).
COSINE: higher is more similar.L2: lower is more similar.- It fails with a "collection not loaded" error; call
load_collectionfirst. - Extra keys not defined in the schema, stored in a hidden JSON field; enabled by quick setup or
enable_dynamic_field=True. category in ["linux", "azure"] and severity == 1queryfilters by scalar expressions only;searchfinds nearest vectors (optionally filtered).- 19530 for the API, 9091 for health and metrics.
- Lite for learning/tests on Linux/macOS; standalone for dev/test and small production; distributed for scale and HA.
Summary
- Milvus stores entities in collections with a schema; vectors need a metric and an index; collections must be loaded to search.
- Milvus Lite and standalone Docker give you the same
MilvusClientAPI as a full cluster. insert,search,query,get,upsertanddeleteplus filter expressions cover everyday work.- Know your metric's score direction before setting RAG thresholds.
Further reading
- Milvus index types cheat sheet
- Milvus "collection not loaded" and query node memory
- Learning: Milvus advanced
- Learning: Chroma for beginners
- Milvus documentation: milvus.io/docs