Home / Learning

LearningLearningBeginner

Learning: Milvus vector database for beginners - run Milvus, create a collection, insert vectors, search with filters and explore it in Attu

A hands-on learning session. Learn how Milvus is organised (collections, fields, segments, indexes, loading), start it with Milvus Lite or Docker, create a collection, insert embedded text, run vector searches with scalar filters, query, delete and look around with the Attu web UI. Exercises and a knowledge check included.

Deepak Bhardwaj10 min read

Applies toMilvus 2.5/2.6 (Milvus Lite on Linux/macOS, or Milvus standalone in Docker on Linux, macOS or Windows), pymilvus 2.5+ MilvusClient API, Python 3.10+

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:Milvus is 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

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:

  1. Browse the runbooks collection, its schema and index.
  2. Run a vector search from the UI using an entity's vector.
  3. 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

  1. Add 20 of your own runbook snippets with category, severity and owner.
  2. Write five realistic questions; record top results and scores; find a sensible min_score.
  3. Create a second collection with metric_type="L2" and compare rankings and score meaning.
  4. Release the collection, try a search, read the error, then load it again.
  5. Use Attu to find the number of entities and the index type of your collection.
  6. Write a filter that returns only severity-1 database runbooks owned by your team.

Knowledge check

  1. What must match between your embedding model and the collection?
  2. With COSINE, is a higher or lower score better? And with L2?
  3. What happens if you search a collection that isn't loaded?
  4. What are dynamic fields, and when are they enabled?
  5. Write a filter for "category is linux or azure and severity is 1".
  6. What is the difference between query and search?
  7. Which ports does standalone Milvus use for the API and for health/metrics?
  8. When would you choose Milvus Lite, standalone or distributed?
Answers
  1. The vector dimension (and you must use the same model for inserts and queries).
  2. COSINE: higher is more similar. L2: lower is more similar.
  3. It fails with a "collection not loaded" error; call load_collection first.
  4. Extra keys not defined in the schema, stored in a hidden JSON field; enabled by quick setup or enable_dynamic_field=True.
  5. category in ["linux", "azure"] and severity == 1
  6. query filters by scalar expressions only; search finds nearest vectors (optionally filtered).
  7. 19530 for the API, 9091 for health and metrics.
  8. 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 MilvusClient API as a full cluster.
  • insert, search, query, get, upsert and delete plus filter expressions cover everyday work.
  • Know your metric's score direction before setting RAG thresholds.

Further reading