> ## Documentation Index
> Fetch the complete documentation index at: https://actianvectorai-ml-crtx-1153-academy-tutorial-rewrites.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Build your first application

> Step-by-step guide to building a semantic search app with Actian VectorAI DB: install, connect, create collections, embed, store, search, filter, update, and delete.

In this tutorial, you build a complete application on Actian VectorAI DB from scratch. By the end, you have a working movie recommendation engine that can store movie descriptions as dense vectors, find semantically similar movies using natural language queries, filter results by genre, year, or rating, update movie information after ingestion, delete outdated records, and inspect collection health and statistics.

No prior vector database experience is required. Each step introduces a concept, explains why it matters, and shows the code you need.

***

## What you build

A user describes what they want to watch in natural language — "a suspenseful space movie" — and the system finds the best matches from the database, optionally filtered by genre, year, or minimum rating. The diagram below shows how data flows from raw movie records through embedding and into a searchable vector store.

***

## Prerequisites

Before starting, make sure the following are in place.

* Python 3.10 or later.
* `pip` available in your environment (verify with `pip --version`).
* A virtual environment activated (recommended: `python -m venv .venv && source .venv/bin/activate`).
* An Actian VectorAI DB server running (default: `localhost:6574`).
* Internet access on first run — `sentence-transformers` downloads the embedding model (`all-MiniLM-L6-v2`, approximately 90 MB) from Hugging Face when you first call `SentenceTransformer(EMBED_MODEL)`.
* At least 512 MB of free memory to load the embedding model.

***

## Step 1: Install dependencies

The following command installs the Actian VectorAI SDK and the sentence embedding library. Run it inside your virtual environment.

```bash theme={null}
pip install actian-vectorai-client sentence-transformers
```

The two packages serve distinct roles in the application.

| Package                  | Purpose                                                               |
| ------------------------ | --------------------------------------------------------------------- |
| `actian-vectorai-client` | Official Python SDK — async/sync clients, Filter DSL, gRPC transport. |
| `sentence-transformers`  | Open-source library for generating text embeddings.                   |

***

## Step 2: Import libraries and configure

The following snippet imports every class needed for this tutorial and sets three constants that identify the server address, collection name, and embedding model. Running it loads the model into memory and prints the resolved configuration so you can confirm the values before proceeding.

```python theme={null}
import asyncio
from sentence_transformers import SentenceTransformer

from actian_vectorai import (
    AsyncVectorAIClient,
    Distance,
    Field,
    FilterBuilder,
    PointStruct,
    VectorParams,
)
from actian_vectorai.models.collections import HnswConfigDiff

# Connection and collection settings
SERVER     = "localhost:6574"
COLLECTION = "Movies"

# Embedding model settings — model name and its output dimension must match
EMBED_MODEL = "all-MiniLM-L6-v2"
EMBED_DIM   = 384

# Load the embedding model into memory (downloads on first run)
model = SentenceTransformer(EMBED_MODEL)

print(f"Server:     {SERVER}")
print(f"Collection: {COLLECTION}")
print(f"Model:      {EMBED_MODEL} ({EMBED_DIM} dimensions)")
```

The table below describes what each import provides.

| Import                | Purpose                                                   |
| --------------------- | --------------------------------------------------------- |
| `AsyncVectorAIClient` | Manages the gRPC connection to VectorAI DB.               |
| `Distance`            | Enum for similarity metrics (Cosine, Dot, Euclid).        |
| `Field`               | Builds type-safe conditions on payload fields.            |
| `FilterBuilder`       | Combines conditions with boolean logic (AND / OR / NOT).  |
| `PointStruct`         | A data point: ID + vector + payload (metadata).           |
| `VectorParams`        | Configuration for the vector space: dimension + distance. |
| `HnswConfigDiff`      | Tuning parameters for the HNSW search index.              |

### Expected output

```
Server:     localhost:6574
Collection: Movies
Model:      all-MiniLM-L6-v2 (384 dimensions)
```

> The model loader may print a `BertModel LOAD REPORT` warning about `embeddings.position_ids` marked as `UNEXPECTED`. This can be safely ignored — it is a known artefact of loading sentence-transformer weights and does not affect embedding quality.

***

## Step 3: Connect to the server

The following snippet opens a gRPC connection to the server, calls `health_check()`, and prints the server's version information. If the connection fails, an exception is raised inside the `async with` block and the error message identifies the problem.

```python theme={null}
async def check_connection():
    async with AsyncVectorAIClient(url=SERVER) as client:
        health = await client.health_check()
        print(f"Server health: {health}")

asyncio.run(check_connection())
```

### Expected output

```
Server health: {'title': 'Actian VectorAI DB', 'version': 'Actian VectorAI DB 1.0.0 / VDE 1.0.0'}
```

If you see a connection error, verify that the VectorAI DB server is running on `localhost:6574`.

When `check_connection()` runs, the `async with AsyncVectorAIClient(...)` block manages the gRPC connection lifecycle. The client opens a channel to `SERVER`, runs the coroutine body including `health_check()`, and closes the channel when the block exits, so resources are released even if something fails. The sequence is as follows.

1. `AsyncVectorAIClient(url=SERVER)` creates a client instance.
2. `async with` opens a gRPC channel and verifies the server is reachable.
3. `health_check()` pings the server and returns status information.
4. When the `async with` block exits, the connection is closed cleanly.

***

## Step 4: Create a collection

A *collection* is a named container for vectors. Think of it as a table in a relational database, but optimized for similarity search.

The following snippet calls `get_or_create`, which creates the collection if it does not already exist. On subsequent runs it reuses the existing collection without error.

```python theme={null}
async def create_collection():
    async with AsyncVectorAIClient(url=SERVER) as client:
        # Delete any stale collection from previous runs before creating fresh
        try:
            await client.collections.delete(COLLECTION)
        except Exception:
            pass
        await client.collections.get_or_create(
            name=COLLECTION,
            vectors_config=VectorParams(
                size=EMBED_DIM,
                distance=Distance.Cosine,
            ),
            hnsw_config=HnswConfigDiff(m=16, ef_construct=128),
        )
        # get_info confirms the collection is fully committed on the server
        # before the next script step opens a new connection to write into it
        await client.collections.get_info(COLLECTION)
    print(f"Collection '{COLLECTION}' ready.")

asyncio.run(create_collection())
```

| Parameter                  | Value                   | Meaning                                                          |
| -------------------------- | ----------------------- | ---------------------------------------------------------------- |
| `size=384`                 | Vector dimension        | Must match the embedding model's output dimension.               |
| `distance=Distance.Cosine` | Similarity metric       | Cosine similarity is ideal for sentence transformers.            |
| `m=16`                     | HNSW graph connections  | Each node connects to 16 neighbours — balances speed and recall. |
| `ef_construct=128`         | Build-time search width | Higher values improve index quality at the cost of build time.   |

### Why use `get_or_create`

`get_or_create` is safe to call repeatedly. When the collection does not yet exist, the SDK creates it and returns `True`. When the collection already exists, the SDK skips creation and returns `False`. This boolean return value lets you log whether a new collection was provisioned, and your scripts become idempotent — safe to re-run without side effects.

> **Sync barrier:** Always call `collections.get_info()` immediately after `get_or_create()` in the same connection block. This confirms the collection is fully committed on the server before the next step opens a new connection to write into it. Without it, a subsequent `points.upsert()` may raise `CollectionNotFoundError` even though creation appeared to succeed.

### Expected output

```
Collection 'Movies' ready.
```

***

## Step 5: Create embedding helpers

```python theme={null}
def embed_text(text: str) -> list[float]:
    """Convert a single text string to a 384-dimensional vector."""
    return model.encode(text).tolist()

def embed_texts(texts: list[str]) -> list[list[float]]:
    """Convert a batch of text strings to vectors in a single forward pass."""
    return model.encode(texts).tolist()

test_vec = embed_text("A thrilling adventure in space")
print(f"Vector dimension: {len(test_vec)}")
print(f"First 5 values:   {[round(v, 4) for v in test_vec[:5]]}")
```

### Expected output

```
Vector dimension: 384
First 5 values:   [-0.0234, 0.0891, -0.0567, 0.0123, -0.0456]
```

The exact values vary by platform. What matters is that the dimension is `384`.

Batching matters for three reasons.

* Speed: `embed_texts` processes all texts in a single forward pass through the model, which is significantly faster than calling `embed_text` in a loop.
* Efficiency: Batching reduces CPU and memory overhead compared to encoding one string at a time.
* Best practice: Always batch when embedding more than a few texts.

***

## Step 6: Prepare your data

Each movie becomes a point in the collection. A point has three parts.

* ID — A unique identifier (integer or UUID string).
* Vector — An embedding of the movie's plot description.
* Payload — Structured metadata (genre, year, rating, and so on).

The following list defines ten movies that will be embedded and stored in the next step. Each entry includes a plot description that the embedding model will encode into a 384-dimensional vector.

```python theme={null}
movies = [
    {
        "title": "Interstellar",
        "plot": "A team of explorers travel through a wormhole in space to ensure humanity's survival on a dying Earth.",
        "genre": "sci-fi", "year": 2014, "rating": 8.7, "director": "Christopher Nolan",
    },
    {
        "title": "The Shawshank Redemption",
        "plot": "A banker sentenced to life in prison forms an unlikely friendship and finds hope through acts of common decency.",
        "genre": "drama", "year": 1994, "rating": 9.3, "director": "Frank Darabont",
    },
    {
        "title": "Inception",
        "plot": "A thief who steals corporate secrets through dream-sharing technology is given the task of planting an idea in a target's mind.",
        "genre": "sci-fi", "year": 2010, "rating": 8.8, "director": "Christopher Nolan",
    },
    {
        "title": "The Dark Knight",
        "plot": "Batman faces the Joker, a criminal mastermind who plunges Gotham City into anarchy and forces the Dark Knight to confront his beliefs.",
        "genre": "action", "year": 2008, "rating": 9.0, "director": "Christopher Nolan",
    },
    {
        "title": "Pulp Fiction",
        "plot": "The lives of two mob hitmen, a boxer, a gangster, and his wife intertwine in four tales of violence and redemption.",
        "genre": "crime", "year": 1994, "rating": 8.9, "director": "Quentin Tarantino",
    },
    {
        "title": "The Matrix",
        "plot": "A computer hacker discovers that reality is a simulation created by machines and joins a rebellion to free humanity.",
        "genre": "sci-fi", "year": 1999, "rating": 8.7, "director": "The Wachowskis",
    },
    {
        "title": "Forrest Gump",
        "plot": "A slow-witted but kind-hearted man from Alabama witnesses and unwittingly influences several historical events in the 20th century.",
        "genre": "drama", "year": 1994, "rating": 8.8, "director": "Robert Zemeckis",
    },
    {
        "title": "Alien",
        "plot": "The crew of a commercial spaceship encounters a deadly extraterrestrial creature that begins hunting them one by one.",
        "genre": "horror", "year": 1979, "rating": 8.5, "director": "Ridley Scott",
    },
    {
        "title": "Goodfellas",
        "plot": "The story of Henry Hill and his life in the mob, covering his relationship with his wife and his mob partners.",
        "genre": "crime", "year": 1990, "rating": 8.7, "director": "Martin Scorsese",
    },
    {
        "title": "Blade Runner 2049",
        "plot": "A young blade runner discovers a long-buried secret that leads him to track down a former blade runner who has been missing for thirty years.",
        "genre": "sci-fi", "year": 2017, "rating": 8.0, "director": "Denis Villeneuve",
    },
]

print(f"Loaded {len(movies)} movies.")
```

***

## Step 7: Embed and store the data

```python theme={null}
async def ingest_movies():
    plots   = [m["plot"] for m in movies]
    vectors = embed_texts(plots)

    points = []
    for i, (movie, vector) in enumerate(zip(movies, vectors)):
        points.append(PointStruct(
            id=i,
            vector=vector,
            payload={
                "title":    movie["title"],
                "plot":     movie["plot"],
                "genre":    movie["genre"],
                "year":     movie["year"],
                "rating":   movie["rating"],
                "director": movie["director"],
            },
        ))

    async with AsyncVectorAIClient(url=SERVER) as client:
        await client.points.upsert(COLLECTION, points=points)
        await client.vde.flush(COLLECTION)
        count = await client.vde.get_vector_count(COLLECTION)

    print(f"Stored {len(points)} movies. Total in collection: {count}")

asyncio.run(ingest_movies())
```

### Expected output

After a successful upsert and flush, the stored count matches the number of points sent. The total reported by `get_vector_count` confirms all ten movies were persisted.

```
Stored 10 movies. Total in collection: 10
```

The ingestion pipeline runs through five stages.

1. `embed_texts` converts all 10 plots into 384-dimensional vectors in one batch.
2. Each movie becomes a `PointStruct` with an integer ID, the plot vector, and the full metadata as payload.
3. `points.upsert` sends the points to the server ("upsert" means insert-or-update).
4. `vde.flush` ensures the data is persisted to disk immediately.
5. `vde.get_vector_count` confirms how many vectors are stored.

***

## Step 8: Run your first semantic search

```python theme={null}
async def search_movies(query: str, top_k: int = 5):
    query_vector = embed_text(query)

    async with AsyncVectorAIClient(url=SERVER) as client:
        results = await client.points.search(
            COLLECTION,
            vector=query_vector,
            limit=top_k,
            with_payload=True,
        ) or []

    return results

query   = "a suspenseful movie set in outer space"
results = asyncio.run(search_movies(query))

print(f"Query: \"{query}\"\n")
for r in results:
    p = r.payload
    print(f"  {r.score:.4f}  {p['title']} ({p['year']}) — {p['genre']} — ★{p['rating']}")
```

| Parameter           | Value            | Purpose                                                        |
| ------------------- | ---------------- | -------------------------------------------------------------- |
| `vector`            | Query embedding  | The search finds vectors closest to this one.                  |
| `limit=5`           | Top 5 results    | Maximum number of results to return.                           |
| `with_payload=True` | Include metadata | Returns title, genre, year, and other fields with each result. |

### Expected output

```
Query: "a suspenseful movie set in outer space"

  0.4244  Alien (1979) — horror — ★8.5
  0.3576  Interstellar (2014) — sci-fi — ★8.7
  0.1724  Blade Runner 2049 (2017) — sci-fi — ★8.0
  0.1622  The Dark Knight (2008) — action — ★9.0
```

In this example, the embedding model captures semantic similarity rather than exact keyword matching. The query "suspenseful movie set in outer space" returns "Alien" and "Interstellar" even though none of the exact query words appear in their plot descriptions. Search quality depends on the model and dataset.

> `limit` is a maximum, not a guarantee. The number of results returned depends on how many points in the collection score above the internal threshold. Scores reflect the specific model and dataset — do not compare absolute score values across different models or collections.

***

## Step 9: Filter by metadata

Filters restrict the candidate set before vector ranking. Actian VectorAI DB provides the `Field` and `FilterBuilder` classes for this purpose.

### Filter by genre

```python theme={null}
async def search_by_genre(query: str, genre: str, top_k: int = 5):
    query_vector = embed_text(query)

    filter_obj = (
        FilterBuilder()
        .must(Field("genre").eq(genre))
        .build()
    )

    async with AsyncVectorAIClient(url=SERVER) as client:
        results = await client.points.search(
            COLLECTION,
            vector=query_vector,
            limit=top_k,
            filter=filter_obj,
            with_payload=True,
        ) or []

    return results

results = asyncio.run(search_by_genre("an exciting adventure", "sci-fi"))

print("Genre filter: sci-fi\n")
for r in results:
    p = r.payload
    print(f"  {r.score:.4f}  {p['title']} ({p['year']})")
```

#### Expected output

```
Genre filter: sci-fi

  0.2818  Interstellar (2014)
  0.2295  The Matrix (1999)
  0.2080  Blade Runner 2049 (2017)
```

### Filter by minimum rating

```python theme={null}
async def search_highly_rated(query: str, min_rating: float, top_k: int = 5):
    query_vector = embed_text(query)

    filter_obj = (
        FilterBuilder()
        .must(Field("rating").gte(min_rating))
        .build()
    )

    async with AsyncVectorAIClient(url=SERVER) as client:
        results = await client.points.search(
            COLLECTION,
            vector=query_vector,
            limit=top_k,
            filter=filter_obj,
            with_payload=True,
        ) or []

    return results

results = asyncio.run(search_highly_rated("intense crime story", 8.8))

print("Filter: rating >= 8.8\n")
for r in results:
    p = r.payload
    print(f"  {r.score:.4f}  {p['title']} — ★{p['rating']}")
```

#### Expected output

```
Filter: rating >= 8.8

  0.4255  Pulp Fiction — ★8.9
  0.3292  The Shawshank Redemption — ★9.3
  0.3026  Inception — ★8.8
  0.2964  Forrest Gump — ★8.8
  0.2887  The Dark Knight — ★9.0
```

***

## Step 10: Combine multiple filters

`FilterBuilder` supports three types of boolean logic.

| Method        | Meaning                              | SQL equivalent |
| ------------- | ------------------------------------ | -------------- |
| `.must()`     | All conditions must match.           | `AND`          |
| `.should()`   | At least one condition should match. | `OR`           |
| `.must_not()` | Exclude any points that match.       | `NOT`          |

```python theme={null}
async def advanced_search(query: str, top_k: int = 5):
    query_vector = embed_text(query)

    filter_obj = (
        FilterBuilder()
        .must(Field("year").gte(2000))
        .must(Field("rating").gte(8.5))
        .must_not(Field("genre").eq("drama"))
        .build()
    )

    async with AsyncVectorAIClient(url=SERVER) as client:
        results = await client.points.search(
            COLLECTION,
            vector=query_vector,
            limit=top_k,
            filter=filter_obj,
            with_payload=True,
        ) or []

    return results

results = asyncio.run(advanced_search("mind-bending thriller"))

print("Filters: year >= 2000, rating >= 8.5, NOT drama\n")
for r in results:
    p = r.payload
    print(f"  {r.score:.4f}  {p['title']} ({p['year']}) — {p['genre']} — ★{p['rating']}")
```

### Expected output

```
Filters: year >= 2000, rating >= 8.5, NOT drama

  0.2724  Inception (2010) — sci-fi — ★8.8
```

***

## Step 11: Retrieve a specific movie by ID

```python theme={null}
async def get_movie(movie_id: int):
    async with AsyncVectorAIClient(url=SERVER) as client:
        points = await client.points.get(
            COLLECTION,
            ids=[movie_id],
            with_payload=True,
        )

    if not points:
        print(f"Movie {movie_id} not found.")
        return None

    p = points[0].payload
    print(f"ID {movie_id}: {p['title']} ({p['year']}) — {p['genre']} — ★{p['rating']}")
    print(f"  Plot: {p['plot']}")
    return points[0]

asyncio.run(get_movie(0))
```

### Expected output

```
ID 0: Interstellar (2014) — sci-fi — ★8.7
  Plot: A team of explorers travel through a wormhole in space to ensure humanity's survival on a dying Earth.
```

***

## Step 12: Update movie metadata

Payload fields can be updated without re-embedding the vector.

```python theme={null}
async def update_movie_rating(movie_id: int, new_rating: float):
    async with AsyncVectorAIClient(url=SERVER) as client:
        await client.points.set_payload(
            COLLECTION,
            payload={"rating": new_rating},
            ids=[movie_id],
        )
    print(f"Updated movie {movie_id} rating to ★{new_rating}")

asyncio.run(update_movie_rating(0, 8.8))
asyncio.run(get_movie(0))
```

### Expected output

```
Updated movie 0 rating to ★8.8
ID 0: Interstellar (2014) — sci-fi — ★8.8
  Plot: A team of explorers travel through a wormhole in space to ensure humanity's survival on a dying Earth.
```

`set_payload` merges the provided fields into the existing payload. Three properties define its behaviour.

* Merge behaviour: Only the specified fields are updated. All other fields in the existing payload remain unchanged.
* No re-embedding: The vector stays the same — only the metadata is modified, so there is no reprocessing cost.
* Immediate effect: Subsequent searches and retrievals reflect the updated values right away.

### Add new fields

`set_payload` can also add entirely new keys to a point.

```python theme={null}
async def add_tags(movie_id: int, tags: list[str]):
    async with AsyncVectorAIClient(url=SERVER) as client:
        await client.points.set_payload(
            COLLECTION,
            payload={"tags": tags},
            ids=[movie_id],
        )
    print(f"Added tags to movie {movie_id}: {tags}")

asyncio.run(add_tags(0, ["space", "wormhole", "survival", "time-dilation"]))
asyncio.run(get_movie(0))
```

### Expected output

```
Added tags to movie 0: ['space', 'wormhole', 'survival', 'time-dilation']
ID 0: Interstellar (2014) — sci-fi — ★8.8
  Plot: A team of explorers travel through a wormhole in space to ensure humanity's survival on a dying Earth.
```

This code calls `add_tags` with movie ID `0` and a list of four descriptive tags. The `set_payload` call merges the new `tags` field into the existing payload, leaving all previously stored fields — title, plot, genre, year, rating, and director — unchanged.

***

## Step 13: Delete points

### Delete by ID

```python theme={null}
async def delete_movie(movie_id: int):
    async with AsyncVectorAIClient(url=SERVER) as client:
        await client.points.delete(COLLECTION, ids=[movie_id])
        await client.vde.flush(COLLECTION)
        count = await client.vde.get_vector_count(COLLECTION)
    print(f"Deleted movie {movie_id}. Remaining: {count}")

asyncio.run(delete_movie(9))
```

### Expected output

The vector count drops from 10 to 9, confirming that movie ID `9` (Blade Runner 2049) was removed from the collection.

```
Deleted movie 9. Remaining: 9
```

This code passes ID `9` — corresponding to "Blade Runner 2049", the last movie in the dataset — to `points.delete()`. After the deletion, `vde.get_vector_count` reads the updated total and prints it so you can confirm the point was removed.

### Delete by filter

```python theme={null}
async def delete_low_rated(min_rating: float):
    filter_obj = (
        FilterBuilder()
        .must(Field("rating").lt(min_rating))
        .build()
    )

    async with AsyncVectorAIClient(url=SERVER) as client:
        count_before = await client.vde.get_vector_count(COLLECTION)
        await client.points.delete(COLLECTION, filter=filter_obj)
        await client.vde.flush(COLLECTION)
        count_after = await client.vde.get_vector_count(COLLECTION)

    print(f"Deleted movies with rating < {min_rating}. Before: {count_before}, After: {count_after}")

# Uncomment the line below to run — this permanently removes points from the collection
# asyncio.run(delete_low_rated(8.6))
```

***

## Step 14: Count points

The following snippet counts the total number of points in the collection, then runs filtered counts to check how many sci-fi movies exist, how many have a rating of 8.8 or higher, and how many were directed by Christopher Nolan.

```python theme={null}
async def count_movies():
    async with AsyncVectorAIClient(url=SERVER) as client:
        total = await client.vde.get_vector_count(COLLECTION)
        all_points, _ = await client.points.scroll(
            COLLECTION, limit=1000, with_payload=True, with_vectors=False,
        )

    sci_fi       = sum(1 for p in all_points if p.payload.get("genre") == "sci-fi")
    highly_rated = sum(1 for p in all_points if p.payload.get("rating", 0) >= 8.8)
    nolan        = sum(1 for p in all_points if p.payload.get("director") == "Christopher Nolan")

    print(f"Total movies: {total}")
    print(f"Sci-fi movies: {sci_fi}")
    print(f"Movies with rating >= 8.8: {highly_rated}")
    print(f"Christopher Nolan movies: {nolan}")

asyncio.run(count_movies())
```

### Expected output

Counts reflect the state after Step 12 (Interstellar's rating updated to 8.8) and Step 13 (Blade Runner 2049 deleted).

```
Total movies: 9
Sci-fi movies: 3
Movies with rating >= 8.8: 6
Christopher Nolan movies: 3
```

> The `rating >= 8.8` count is **6**, not 5 as you might expect from the raw dataset. This is because Interstellar's rating was updated from 8.7 to 8.8 in Step 12 before this count runs.

***

## Step 15: Inspect collection status

```python theme={null}
async def inspect_collection():
    # Map raw integer enum values returned by the server to human-readable strings
    STATUS_MAP    = {1: "green", 2: "yellow", 3: "red"}
    VDE_STATE_MAP = {0: "active", 1: "inactive"}

    async with AsyncVectorAIClient(url=SERVER) as client:
        info  = await client.collections.get_info(COLLECTION)
        state = await client.vde.get_state(COLLECTION)
        count = await client.vde.get_vector_count(COLLECTION)

    print(f"Collection: {COLLECTION}")
    print(f"  Status:       {STATUS_MAP.get(info.status, info.status)}")
    print(f"  VDE state:    {VDE_STATE_MAP.get(state, state)}")
    print(f"  Vector count: {count}")

asyncio.run(inspect_collection())
```

### Expected output

```
Collection: Movies
  Status:       green
  VDE state:    active
  Vector count: 9
```

> `collections.get_info()` and `vde.get_state()` return raw integer enum values, not strings. Use the `STATUS_MAP` and `VDE_STATE_MAP` dictionaries above to convert them. A `VDE state` of `active` (integer `0`) means the collection is ready for searches regardless of the `Status` value. A `Status` of `red` after deletions is normal and does not affect search quality.

This code connects to the server, calls `collections.get_info` to retrieve the collection's operational status and vector configuration, then calls `vde.get_state` to read the current VDE lifecycle state, and finally calls `vde.get_vector_count` to confirm the number of stored vectors. All three values are printed together so you can verify the collection is healthy and correctly configured before running searches.

***

## Step 16: List all collections

```python theme={null}
async def list_collections():
    async with AsyncVectorAIClient(url=SERVER) as client:
        names = await client.collections.list()

    print(f"Collections on server ({len(names)}):")
    for name in names:
        print(f"  - {name}")

asyncio.run(list_collections())
```

### Expected output

Because only one collection was created in this tutorial, `collections.list()` returns a single entry. The count in the header updates automatically as collections are added or removed.

```
Collections on server (1):
  - Movies
```

This code calls `collections.list()`, which returns the names of all collections currently provisioned on the server. In this tutorial only one collection has been created, so the output lists `Movies` as the single entry.

***

## Step 17: Put it all together — a complete search function

```python theme={null}
async def recommend_movies(
    query: str,
    genre:         str | None   = None,
    min_year:      int | None   = None,
    min_rating:    float | None = None,
    exclude_genre: str | None   = None,
    top_k: int = 5,
):
    """Recommend movies using semantic search with optional filters."""
    query_vector = embed_text(query)

    fb = FilterBuilder()
    if genre:
        fb = fb.must(Field("genre").eq(genre))
    if min_year is not None:       # explicit None check — 0 is a valid year
        fb = fb.must(Field("year").gte(min_year))
    if min_rating is not None:     # explicit None check — 0.0 is a valid rating
        fb = fb.must(Field("rating").gte(min_rating))
    if exclude_genre:
        fb = fb.must_not(Field("genre").eq(exclude_genre))
    filter_obj = fb.build()

    async with AsyncVectorAIClient(url=SERVER) as client:
        results = await client.points.search(
            COLLECTION,
            vector=query_vector,
            limit=top_k,
            filter=filter_obj,
            with_payload=True,
        ) or []

    filters_desc = []
    if genre:         filters_desc.append(f"genre={genre}")
    if min_year:      filters_desc.append(f"year>={min_year}")
    if min_rating:    filters_desc.append(f"rating>={min_rating}")
    if exclude_genre: filters_desc.append(f"NOT {exclude_genre}")

    print(f"\n  Query: \"{query}\"")
    print(f"  Filters: {', '.join(filters_desc) or 'none'}  |  results: {len(results)}\n")

    for r in results:
        p = r.payload
        print(f"    {r.score:.4f}  {p['title']} ({p['year']}) — {p['genre']} — ★{p['rating']}")
        print(f"           {p['plot'][:90]}…")
    print()

asyncio.run(recommend_movies("a mind-bending sci-fi movie"))

asyncio.run(recommend_movies(
    "an intense crime story",
    min_rating=8.8,
))

asyncio.run(recommend_movies(
    "a feel-good movie about life",
    exclude_genre="crime",
    min_year=1990,
))
```

### Expected output

Three calls are made with different queries and filter combinations. Each block shows the active filters and how many results matched before the ranked list is printed.

```
  Query: "a mind-bending sci-fi movie"
  Filters: none  |  results: 5

    0.3180  The Matrix (1999) — sci-fi — ★8.7
           A computer hacker discovers that reality is a simulation created by machines and joins a r…
    0.2747  Alien (1979) — horror — ★8.5
           The crew of a commercial spaceship encounters a deadly extraterrestrial creature that begi…
    0.2609  Inception (2010) — sci-fi — ★8.8
           A thief who steals corporate secrets through dream-sharing technology is given the task of…

  Query: "an intense crime story"
  Filters: rating>=8.8  |  results: 2

    0.4317  Pulp Fiction (1994) — crime — ★8.9
           The lives of two mob hitmen, a boxer, a gangster, and his wife intertwine in four tales of…
    0.3621  The Shawshank Redemption (1994) — drama — ★9.3
           A banker sentenced to life in prison forms an unlikely friendship and finds hope through a…

  Query: "a feel-good movie about life"
  Filters: NOT crime, year>=1990  |  results: 5

    0.3058  The Shawshank Redemption (1994) — drama — ★9.3
           A banker sentenced to life in prison forms an unlikely friendship and finds hope through a…
    0.1866  Forrest Gump (1994) — drama — ★8.8
           A slow-witted but kind-hearted man from Alabama witnesses and unwittingly influences sever…
    …
```

The first call searches without any filters. The second applies `min_rating >= 8.8`. The third combines `exclude_genre="crime"` with `min_year=1990`. Each call prints the query, active filters, result count, and ranked movies with truncated plot descriptions.

***

## Step 18: Cleanup

```python theme={null}
async def cleanup():
    async with AsyncVectorAIClient(url=SERVER) as client:
        count = await client.vde.get_vector_count(COLLECTION)
        print(f"Collection '{COLLECTION}' contains {count} movies.")

        await client.vde.flush(COLLECTION)
        print("Data flushed to disk.")

        # Uncomment the next two lines to permanently delete the collection:
        # await client.collections.delete(COLLECTION)
        # print(f"Collection '{COLLECTION}' deleted.")

asyncio.run(cleanup())
```

### Expected output

The vector count reflects the state of the collection after all previous steps. The flush confirmation line indicates that any pending writes have been safely persisted to disk.

```
Collection 'Movies' contains 9 movies.
Data flushed to disk.
```

This code reads the current vector count from the collection, prints it, then calls `vde.flush` to ensure any pending writes are persisted to disk. The two lines that delete the collection are commented out — they are safe to uncomment when the tutorial data is no longer needed, but the collection is preserved by default so the data remains available for further experimentation.

***

## What you learned

| Concept           | API                                                           | What it does                                              |
| ----------------- | ------------------------------------------------------------- | --------------------------------------------------------- |
| Connect           | `AsyncVectorAIClient(url=...)`                                | Open a gRPC connection to VectorAI DB.                    |
| Health check      | `client.health_check()`                                       | Verify the server is reachable.                           |
| Create collection | `collections.get_or_create(vectors_config=VectorParams(...))` | Define a vector space with dimension and distance metric. |
| Embed text        | `SentenceTransformer.encode()`                                | Convert text to a numerical vector.                       |
| Store data        | `points.upsert(collection, points=[PointStruct(...)])`        | Insert or update points with vectors and metadata.        |
| Persist           | `vde.flush(collection)`                                       | Write pending data to disk.                               |
| Semantic search   | `points.search(collection, vector=..., limit=5)`              | Find the most similar vectors.                            |
| Filter (equality) | `Field("genre").eq("sci-fi")`                                 | Match a specific value.                                   |
| Filter (range)    | `Field("rating").gte(8.5)`                                    | Numeric comparison.                                       |
| Filter (exclude)  | `FilterBuilder().must_not(...)`                               | Exclude matching points.                                  |
| Combine filters   | `FilterBuilder().must(...).must(...).build()`                 | Boolean AND/OR/NOT logic.                                 |
| Get by ID         | `points.get(collection, ids=[0])`                             | Retrieve specific points.                                 |
| Update metadata   | `points.set_payload(collection, payload={...}, ids=[0])`      | Merge new fields into existing payloads.                  |
| Delete by ID      | `points.delete(collection, ids=[0])`                          | Remove specific points.                                   |
| Delete by filter  | `points.delete(collection, filter=...)`                       | Remove points matching conditions.                        |
| Count             | `vde.get_vector_count()` + `points.scroll()`                  | Total and filtered counts.                                |
| Collection info   | `collections.get_info(collection)`                            | Status and configuration.                                 |
| Collection state  | `vde.get_state(collection)`                                   | VDE lifecycle state.                                      |
| List collections  | `collections.list()`                                          | All collection names on the server.                       |
| Delete collection | `collections.delete(collection)`                              | Remove a collection entirely.                             |

***

## Common patterns quick reference

### Pattern 1: Search with optional filters

Use `is not None` rather than a truthiness check to avoid silently skipping valid falsy values such as `0.0`.

```python theme={null}
fb = FilterBuilder()
if genre:
    fb = fb.must(Field("genre").eq(genre))
if min_rating is not None:
    fb = fb.must(Field("rating").gte(min_rating))
filter_obj = fb.build()
```

### Pattern 2: Upsert is idempotent

Calling `upsert` with the same ID replaces the existing point, so ingestion scripts can be re-run safely without creating duplicates. This makes bulk ingestion pipelines robust to restarts.

### Pattern 3: Always flush after writes

Call `vde.flush()` immediately after `points.upsert()` to ensure data survives server restarts. Without it, recent writes may be lost if the server crashes.

```python theme={null}
await client.points.upsert(COLLECTION, points=points)
await client.vde.flush(COLLECTION)
```

### Pattern 4: Use `get_or_create` for collections

`get_or_create` is safe to run on every application startup. It creates the collection if it does not exist and does nothing if it already does, so startup code does not need a separate existence check.

```python theme={null}
await client.collections.get_or_create(name=COLLECTION, vectors_config=...)
```

***

## Next steps

* [Predicate filters](/academy/tutorials/predicate-filters) — Master the full Filter DSL with all field types and operators.
* [Similarity search fundamentals](/academy/tutorials/similarity-search) — Explore search parameters, score thresholds, and pagination.
* [Use open-source embedding models](/academy/tutorials/leverage-open-source-embedding-models) — Choose the right model for production.
* [Optimizing retrieval quality](/academy/tutorials/retrieval-quality) — Tune HNSW parameters and search settings.

<CardGroup cols={2}>
  <Card title="Predicate filters" href="/academy/tutorials/predicate-filters">
    Master the full Filter DSL with all field types and operators.
  </Card>

  <Card title="Similarity search fundamentals" href="/academy/tutorials/similarity-search">
    Explore search parameters, score thresholds, and pagination.
  </Card>

  <Card title="Use open-source embedding models" href="/academy/tutorials/leverage-open-source-embedding-models">
    Choose the right model and configure quantization for production.
  </Card>

  <Card title="Optimizing retrieval quality" href="/academy/tutorials/retrieval-quality">
    Tune HNSW parameters, quantization, and search settings.
  </Card>
</CardGroup>
