Skip to main content
This tutorial covers every configuration lever in Actian VectorAI DB that affects how accurately similarity search returns relevant results. Getting search to work is straightforward. Getting it to return the right results consistently under real query load requires understanding the trade-offs between recall, precision, speed, and memory — and knowing which knobs to turn for each. Retrieval quality has two dimensions:
  • Recall — the fraction of truly relevant results that the system returns. If there are 10 relevant documents and the system finds 8, recall is 80%.
  • Precision — the fraction of returned results that are actually relevant. If the system returns 10 results and 7 are relevant, precision is 70%.
In approximate nearest-neighbour (ANN) search, there is always a trade-off between quality and speed. A brute-force scan over every vector gives perfect recall but is slow. An HNSW index is fast but may miss some neighbours. By the end of this tutorial, you will know how to:
  • Measure — Establish a ground truth baseline with exact search.
  • Tune HNSW — Adjust m, ef_construct, and hnsw_ef for the recall–speed trade-off.
  • Choose distance — Pick the right metric for your embeddings.
  • Tune search-time parameters — Use hnsw_ef and exact mode.
  • Use multistage prefetch — Widen the candidate pool then rerank.
  • Apply payload filters — Accelerate filtered search.
  • Set score thresholds — Cut noise at the right level.
  • Rebuild and compact — Keep index quality fresh after updates.

Environment setup

Run the following command to install the Python packages required for all code samples in this tutorial.
pip install actian-vectorai-client sentence-transformers numpy

Step 1: Create a test collection and ingest data

This step sets up the shared imports, constants, and embedding helpers used throughout the tutorial. Running this block loads the all-MiniLM-L6-v2 model, defines two encoding functions, and establishes the server address and collection name that all subsequent steps reference.
import asyncio
import time
import numpy as np
from sentence_transformers import SentenceTransformer

from actian_vectorai import (
    AsyncVectorAIClient,
    Distance,
    Field,
    FilterBuilder,
    PointStruct,
    PrefetchQuery,
    SearchParams,
    VectorParams,
)
from actian_vectorai.models.enums import Fusion
from actian_vectorai.models.collections import HnswConfigDiff
from actian_vectorai.models.points import ScoredPoint

SERVER     = "localhost:6574"
COLLECTION = "Retrieval-Quality"
EMBED_DIM  = 384

model = SentenceTransformer("all-MiniLM-L6-v2")

def embed_text(text: str) -> list:
    return model.encode(text).tolist()

def embed_texts(texts: list) -> list:
    return model.encode(texts).tolist()
The following block defines a 30-document corpus and ingests it into a new collection with cosine distance and default HNSW settings.
corpus = [
    {"text": "Python is a versatile programming language used in web development, data science, and automation.", "category": "programming"},
    {"text": "JavaScript runs in browsers and on servers with Node.js, powering interactive web applications.", "category": "programming"},
    {"text": "Rust provides memory safety without garbage collection through its ownership system.", "category": "programming"},
    {"text": "Go is designed for building scalable networked services and cloud infrastructure.", "category": "programming"},
    {"text": "TypeScript adds static types to JavaScript, improving code quality in large codebases.", "category": "programming"},
    {"text": "Machine learning models learn statistical patterns from labeled training data.", "category": "ml"},
    {"text": "Deep neural networks stack multiple layers to learn hierarchical representations of data.", "category": "ml"},
    {"text": "Transformers use self-attention to process sequences in parallel, enabling large language models.", "category": "ml"},
    {"text": "Gradient boosting combines weak decision trees into a strong ensemble predictor.", "category": "ml"},
    {"text": "Reinforcement learning trains agents by rewarding desired behaviors in an environment.", "category": "ml"},
    {"text": "Convolutional neural networks detect spatial patterns in images through learned filters.", "category": "ml"},
    {"text": "Transfer learning fine-tunes pretrained models on domain-specific data with less labeled examples.", "category": "ml"},
    {"text": "PostgreSQL is a relational database with strong ACID compliance and extensibility.", "category": "databases"},
    {"text": "MongoDB stores data as flexible JSON-like documents without a fixed schema.", "category": "databases"},
    {"text": "Redis is an in-memory key-value store used for caching and real-time applications.", "category": "databases"},
    {"text": "Vector databases store embeddings and find similar items using approximate nearest-neighbour search.", "category": "databases"},
    {"text": "Elasticsearch provides full-text search and analytics on structured and unstructured data.", "category": "databases"},
    {"text": "Docker containers package applications with their dependencies for consistent deployment.", "category": "devops"},
    {"text": "Kubernetes orchestrates containers across clusters, handling scaling and self-healing.", "category": "devops"},
    {"text": "CI/CD pipelines automate testing, building, and deploying code changes to production.", "category": "devops"},
    {"text": "Infrastructure as Code defines server configurations in version-controlled files.", "category": "devops"},
    {"text": "Prometheus collects metrics and Grafana visualizes them for monitoring distributed systems.", "category": "devops"},
    {"text": "Microservices decompose applications into small independently deployable services.", "category": "architecture"},
    {"text": "Event-driven architecture uses asynchronous messages to decouple producers and consumers.", "category": "architecture"},
    {"text": "API gateways route requests, handle authentication, and enforce rate limits for microservices.", "category": "architecture"},
    {"text": "The CAP theorem states that distributed systems cannot simultaneously guarantee consistency, availability, and partition tolerance.", "category": "architecture"},
    {"text": "CQRS separates read and write models to optimize performance for different workloads.", "category": "architecture"},
    {"text": "TLS encrypts network traffic between clients and servers to prevent eavesdropping.", "category": "security"},
    {"text": "OAuth 2.0 delegates authorization using access tokens without sharing credentials.", "category": "security"},
    {"text": "Zero-trust security verifies every request regardless of network location.", "category": "security"},
]

async def setup_and_ingest():
    # delete → get_or_create → get_info sync barrier → upsert in fresh connection
    async with AsyncVectorAIClient(url=SERVER) as client:
        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),
        )
        await client.collections.get_info(COLLECTION)  # sync barrier

    texts   = [d["text"] for d in corpus]
    vectors = embed_texts(texts)
    points  = [
        PointStruct(id=i, vector=vectors[i], payload=corpus[i])
        for i in range(len(corpus))
    ]
    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"Collection ready with {count} vectors.")

asyncio.run(setup_and_ingest())

Expected output

This block embeds all 30 corpus documents, constructs PointStruct objects pairing each vector with its text and category payload, upserts them into the Retrieval-Quality collection using cosine distance and default HNSW settings (m=16, ef_construct=128), flushes the writes to disk, and queries the server for the total stored vector count.
Collection ready with 30 vectors.

Before tuning anything, measure ground truth. An exact (brute-force) search scans every vector in the collection and returns the mathematically correct nearest neighbours with 100% recall. Every tuning step in this tutorial should be measured against this baseline. The following block defines three functions: exact_search, which runs a brute-force scan; approx_search, which uses the HNSW index with an optional hnsw_ef override; and compute_recall, which calculates what fraction of the exact top-K results the approximate search also returned. Running the block then queries both functions for the same input and prints a side-by-side comparison. Every tuning step in this tutorial should be measured against this baseline.
async def exact_search(query: str, top_k: int = 10):
    vec = embed_text(query)
    async with AsyncVectorAIClient(url=SERVER) as client:
        results = await client.points.search(
            COLLECTION,
            vector=vec,
            limit=top_k,
            with_payload=True,
            params=SearchParams(exact=True),  # disable HNSW and scan all vectors
        ) or []
    return results

async def approx_search(query: str, top_k: int = 10, hnsw_ef: int = None):
    vec = embed_text(query)
    params = SearchParams(hnsw_ef=hnsw_ef) if hnsw_ef else None
    async with AsyncVectorAIClient(url=SERVER) as client:
        results = await client.points.search(
            COLLECTION,
            vector=vec,
            limit=top_k,
            with_payload=True,
            params=params,
        ) or []
    return results

def compute_recall(exact_results, approx_results) -> float:
    """Compute recall@K: fraction of exact top-K results found by approximate search."""
    exact_ids  = {r.id for r in exact_results}
    approx_ids = {r.id for r in approx_results}
    if not exact_ids:
        return 1.0
    return len(exact_ids & approx_ids) / len(exact_ids)

query  = "How do neural networks learn from data?"
exact  = asyncio.run(exact_search(query, top_k=10))
approx = asyncio.run(approx_search(query, top_k=10))
recall = compute_recall(exact, approx)

print(f"Query: {query}")
print(f"Exact top-10 IDs:  {[r.id for r in exact]}")
print(f"Approx top-10 IDs: {[r.id for r in approx]}")
print(f"Recall@10: {recall:.2%}")

Expected output

This block queries the same sentence using both exact_search (brute-force scan) and approx_search (HNSW traversal), then computes recall@10. When both result sets share identical IDs, recall reaches 100%, confirming the HNSW index is producing no approximation error on this query.
Query: How do neural networks learn from data?
Exact top-10 IDs:  [6, 5, 10, 11, 9, 8, 15, 7, 13, 23]
Approx top-10 IDs: [6, 5, 10, 11, 9, 8, 15, 7, 13, 23]
Recall@10: 100.00%

Step 3: Tune HNSW index parameters

The HNSW index has two sets of parameters: build-time parameters that affect the quality of the graph structure stored on disk, and search-time parameters that affect how many nodes the query traverses at runtime.

Build-time parameters: m and ef_construct

m and ef_construct are set when creating the collection. Once set, changing them requires recreating the index. The following block creates four collections at different quality levels and measures recall across all of them.
async def create_with_hnsw(m: int, ef_construct: int, name_suffix: str):
    coll = f"HNSW-{name_suffix}"
    async with AsyncVectorAIClient(url=SERVER) as client:
        try:
            await client.collections.delete(coll)
        except Exception:
            pass
        await client.collections.get_or_create(
            name=coll,
            vectors_config=VectorParams(size=EMBED_DIM, distance=Distance.Cosine),
            hnsw_config=HnswConfigDiff(m=m, ef_construct=ef_construct),
        )
        await client.collections.get_info(coll)  # sync barrier

    texts   = [d["text"] for d in corpus]
    vectors = embed_texts(texts)
    points  = [PointStruct(id=i, vector=vectors[i], payload=corpus[i])
               for i in range(len(corpus))]
    async with AsyncVectorAIClient(url=SERVER) as client:
        await client.points.upsert(coll, points=points)
        await client.vde.flush(coll)

    print(f"Collection \'{coll}\' ready (m={m}, ef_construct={ef_construct}).")
    return coll

configs = [
    {"m": 4,  "ef_construct": 32,  "suffix": "low"},
    {"m": 16, "ef_construct": 128, "suffix": "default"},
    {"m": 32, "ef_construct": 256, "suffix": "high"},
    {"m": 64, "ef_construct": 512, "suffix": "max"},
]

collections = []
for cfg in configs:
    coll = asyncio.run(create_with_hnsw(cfg["m"], cfg["ef_construct"], cfg["suffix"]))
    collections.append(coll)
The following table summarizes how each parameter level affects build speed, memory, and the recall ceiling the index can reach.
ParameterLowDefaultHighMax
m4163264
ef_construct32128256512
Build speedFastestFastSlowerSlowest
Memory usageLowestModerateHigherHighest
Recall potentialLowerGoodBetterBest
The two parameters control different aspects of graph quality:
  • m — The number of bi-directional links created for each node. Higher values produce a denser graph with more traversal paths, which improves recall at the cost of memory and build time.
  • ef_construct — The search width used during index construction. Higher values produce a better-connected graph. Set this to at least 2 * m.

Measure recall across configurations

async def measure_recall_across_configs(query: str, top_k: int = 10):
    vec = embed_text(query)

    async with AsyncVectorAIClient(url=SERVER) as client:
        exact_results = await client.points.search(
            COLLECTION, vector=vec, limit=top_k,
            with_payload=True, params=SearchParams(exact=True),
        ) or []

    for coll_name in collections:
        async with AsyncVectorAIClient(url=SERVER) as client:
            approx_results = await client.points.search(
                coll_name, vector=vec, limit=top_k, with_payload=True,
            ) or []

        recall = compute_recall(exact_results, approx_results)
        print(f"  {coll_name}: recall@{top_k} = {recall:.2%}")

query = "How do neural networks learn from data?"
print(f"Query: {query}")
asyncio.run(measure_recall_across_configs(query))

Expected output

This block embeds the query, fetches exact ground-truth results from the baseline collection, then runs the same approximate search against each of the four HNSW collections and computes recall@10 for each. The low configuration uses a sparse graph that misses some traversal paths; default and above close that gap entirely.
Query: How do neural networks learn from data?
  HNSW-low: recall@10 = 100.00%
  HNSW-default: recall@10 = 100.00%
  HNSW-high: recall@10 = 100.00%
All configurations return 100% recall on this small 30-doc corpus. Differences become visible at larger scale (1M+ vectors).

Search-time parameter: hnsw_ef

hnsw_ef controls how many candidate nodes the search explores at query time. It can be set per request without rebuilding the index, which makes it the primary knob for trading latency against recall at runtime.
async def measure_ef_impact(query: str, ef_values: list, top_k: int = 10):
    vec = embed_text(query)

    async with AsyncVectorAIClient(url=SERVER) as client:
        exact_results = await client.points.search(
            COLLECTION, vector=vec, limit=top_k,
            params=SearchParams(exact=True), with_payload=True,
        ) or []

        for ef in ef_values:
            start = time.perf_counter()
            approx_results = await client.points.search(
                COLLECTION, vector=vec, limit=top_k,
                params=SearchParams(hnsw_ef=ef), with_payload=True,
            ) or []
            elapsed = (time.perf_counter() - start) * 1000

            recall = compute_recall(exact_results, approx_results)
            print(f"  hnsw_ef={ef:>4}  recall@{top_k}={recall:.2%}  latency={elapsed:.1f}ms")

print("Impact of hnsw_ef on recall and latency:")
asyncio.run(measure_ef_impact(
    "How do neural networks learn from data?",
    ef_values=[16, 32, 64, 128, 256, 512],
))

Expected output

This block sweeps six values of hnsw_ef and for each measures recall against the exact baseline and wall-clock latency.
Impact of hnsw_ef on recall and latency:
  hnsw_ef=  16  recall@10=100.00%  latency=2.0ms
  hnsw_ef=  32  recall@10=100.00%  latency=1.4ms
  hnsw_ef=  64  recall@10=100.00%  latency=1.3ms
  hnsw_ef= 128  recall@10=100.00%  latency=1.5ms
  hnsw_ef= 256  recall@10=100.00%  latency=1.7ms
  hnsw_ef= 512  recall@10=100.00%  latency=1.3ms
All hnsw_ef values return 100% recall on this small corpus. Latency differences are minimal; at larger scale higher ef values trade measurable latency for better recall.
The following table maps hnsw_ef ranges to their typical recall and latency characteristics.
hnsw_efRecallLatencyUse case
16–32LowerFastestReal-time autocomplete, high QPS
64–128GoodFastGeneral search, most applications
256–512ExcellentSlowerHigh-accuracy retrieval, RAG
Exact modePerfectSlowestEvaluation, ground truth

Step 4: Choose the right distance metric

The distance metric defines what the index considers “similar”. Choosing the wrong metric for your embedding model produces systematically lower recall regardless of any other tuning.
async def compare_metrics(query: str, top_k: int = 5):
    vec = embed_text(query)
    metrics = {
        "Cosine": Distance.Cosine,
        "Dot":    Distance.Dot,
        "Euclid": Distance.Euclid,
    }

    for name, distance in metrics.items():
        coll = f"Metric-{name}"
        async with AsyncVectorAIClient(url=SERVER) as client:
            try:
                await client.collections.delete(coll)
            except Exception:
                pass
            await client.collections.get_or_create(
                name=coll,
                vectors_config=VectorParams(size=EMBED_DIM, distance=distance),
                hnsw_config=HnswConfigDiff(m=16, ef_construct=128),
            )
            await client.collections.get_info(coll)  # sync barrier

        texts   = [d["text"] for d in corpus]
        vectors = embed_texts(texts)
        points  = [PointStruct(id=i, vector=vectors[i], payload=corpus[i])
                   for i in range(len(corpus))]
        async with AsyncVectorAIClient(url=SERVER) as client:
            await client.points.upsert(coll, points=points)
            await client.vde.flush(coll)
            results = await client.points.search(
                coll, vector=vec, limit=top_k, with_payload=True,
            ) or []
            print(f"\n=== {name} distance ===")
            for r in results:
                print(f"  id={r.id}  score={r.score:.4f}  {r.payload.get(\'text\', \'\')[:60]}...")
            await client.collections.delete(coll)

asyncio.run(compare_metrics("How do transformers work in deep learning?"))
The following table lists common embedding models and the distance metric each is designed to work with.
Embedding modelRecommended metricWhy
all-MiniLM-L6-v2CosineProduces normalized vectors.
text-embedding-3-smallCosineNormalized by default.
CLIP (ViT-B-32)CosineNormalized image/text embeddings.
Custom non-normalizedDot or EuclidMagnitude carries meaning.
Sparse/hybridDotStandard for TF-IDF/BM25.
Most pretrained embedding models produce unit-normalized vectors, where cosine similarity equals the dot product. If you are unsure which metric to use, start with cosine.

Expected output

  Cosine: top ids=[7, 6, 10]  scores=[0.4725, 0.448, 0.3752]
  Dot: top ids=[7, 6, 10]  scores=[0.4725, 0.448, 0.3752]
  Euclid: top ids=[7, 6, 10]  scores=[1.055, 1.104, 1.2496]
Cosine and Dot return identical rankings and identical scores for all-MiniLM-L6-v2 because it produces unit-normalized vectors (cosine = dot product for unit vectors). Euclid returns distance values — lower is more similar, which inverts the ranking intuition.

Step 5: Use multistage prefetch to widen the candidate pool

A single HNSW traversal only explores one path through the graph. If the most relevant documents sit in a different region of the vector space — for example, in a specific category — that path may never reach them. Multi-stage prefetch runs several candidate-gathering passes in parallel, then reranks the combined pool.
async def prefetch_quality_test(query: str, top_k: int = 5):
    vec = embed_text(query)

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

        ml_filter = FilterBuilder().must(Field("category").eq("ml")).build()
        db_filter = FilterBuilder().must(Field("category").eq("databases")).build()

        prefetch_results = await client.points.query(
            COLLECTION,
            query={"fusion": Fusion.RRF},   # required: multiple prefetch stages need fusion
            prefetch=[
                PrefetchQuery(query=vec, limit=15),
                PrefetchQuery(query=vec, filter=ml_filter, limit=15),
                PrefetchQuery(query=vec, filter=db_filter, limit=15),
            ],
            limit=top_k,
            with_payload=True,
        )

    print("=== Single-pass search ===")
    for r in single:
        print(f"  id={r.id}  score={r.score:.4f}  cat={r.payload.get(\'category\')}  {r.payload.get(\'text\', \'\')[:50]}...")

    print("\n=== Multi-stage prefetch + rerank ===")
    for r in (list(prefetch_results) if prefetch_results else []):
        print(f"  id={r.id}  score={r.score:.4f}  cat={r.payload.get(\'category\')}  {r.payload.get(\'text\', \'\')[:50]}...")

asyncio.run(prefetch_quality_test("storing and searching embeddings efficiently"))
The following table compares prefetch strategies by the size and composition of the candidate pool each one produces.
ApproachCandidate poolTrade-off
Single searchTop-K from one pass.May miss results outside the HNSW traversal path.
Prefetch (unfiltered)Broader initial pool, then rerank.Catches near-misses from the same vector region.
Prefetch (multifilter)Candidates from different payload categories.Ensures diversity across category boundaries.
Prefetch (multivector)Candidates from different embedding spaces.Enables cross-perspective matching.
The final limit=5 reranks from the union of all prefetched candidates. Even if one prefetch path misses a relevant result, another path may find it.

Expected output

Single-pass:
  id=15  score=0.6957  cat=databases     Vector databases store embeddings and find si...
  id=16  score=0.3429  cat=databases     Elasticsearch provides full-text search and a...
  id=6   score=0.2988  cat=ml            Deep neural networks stack multiple layers to...
  id=11  score=0.2569  cat=ml            Transfer learning fine-tunes pretrained model...
  id=14  score=0.2462  cat=databases     Redis is an in-memory key-value store used fo...

Prefetch RRF 3-stream:
  id=15  score=0.0328  cat=databases     Vector databases store embeddings and find si...
  id=6   score=0.0323  cat=ml            Deep neural networks stack multiple layers to...
  id=16  score=0.0323  cat=databases     Elasticsearch provides full-text search and a...
  id=11  score=0.0318  cat=ml            Transfer learning fine-tunes pretrained model...
  id=14  score=0.0313  cat=databases     Redis is an in-memory key-value store used fo...
The RRF fusion promotes id=6 (ml) from rank 3 to rank 2, reflecting that the ML-filtered prefetch stream also returned it as a top candidate — consensus across streams boosts its position.

Step 6: Set the right score threshold

A score threshold rejects any result whose similarity score falls below a minimum value. Setting it too low returns noisy, irrelevant results. Setting it too high discards valid matches. The right threshold depends on the score distribution of your specific embedding model and corpus.
async def threshold_analysis(query: str, relevant_ids: set):
    vec = embed_text(query)

    async with AsyncVectorAIClient(url=SERVER) as client:
        all_results = await client.points.search(
            COLLECTION, vector=vec, limit=30, with_payload=True,
            params=SearchParams(exact=True),
        ) or []

    thresholds = [0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]

    print(f"Query: {query}")
    print(f"Known relevant IDs: {sorted(relevant_ids)}\n")
    print(f"   Threshold  Returned    TP   Precision    Recall")
    print(f"  {\'─\'*50}")

    for t in thresholds:
        filtered     = [r for r in all_results if r.score >= t]
        returned_ids = {r.id for r in filtered}
        tp           = len(returned_ids & relevant_ids)
        precision    = tp / len(returned_ids) if returned_ids else 0.0
        recall       = tp / len(relevant_ids) if relevant_ids else 0.0
        print(f"  {t:>10.1f}  {len(filtered):>8}  {tp:>4}  {precision:>9.2%}  {recall:>8.2%}")

asyncio.run(threshold_analysis(
    "machine learning and neural network training",
    relevant_ids={5, 6, 7, 8, 9, 10, 11},
))

Expected output

This block runs threshold_analysis with the query “machine learning and neural network training” and the known-relevant set {5, 6, 7, 8, 9, 10, 11}. It applies seven score thresholds and prints precision and recall at each level.
Query: machine learning and neural network training
Known relevant IDs: [5, 6, 7, 8, 9, 10, 11]

   Threshold  Returned    TP   Precision    Recall
  ──────────────────────────────────────────────────
         0.2         8     7      87.50%   100.00%
         0.3         5     5     100.00%    71.43%
         0.4         2     2     100.00%    28.57%
         0.5         1     1     100.00%    14.29%
         0.6         0     0       0.00%     0.00%
         0.7         0     0       0.00%     0.00%
         0.8         0     0       0.00%     0.00%
Reading this output:
  • At threshold 0.2 — 87.5% precision, 100% recall — good balanced starting point.
  • At threshold 0.3 — 100% precision, 71% recall — trades some recall for clean results.
  • At threshold 0.4+ — fewer than 3 results remain; too aggressive for this corpus.
Run this analysis on multiple representative queries and pick the threshold that balances precision and recall across your query set.

Step 7: Rebuild and compact for sustained quality

After many updates and deletions, index quality degrades over time. Deleted vectors leave tombstones that waste memory and slow search. Segments accumulate and fragment, reducing scan locality. The following block runs a full rebuild, optimization, and compaction sequence.
async def maintenance_for_quality():
    async with AsyncVectorAIClient(url=SERVER) as client:
        count = await client.vde.get_vector_count(COLLECTION)
        state = await client.vde.get_state(COLLECTION)
        info  = await client.collections.get_info(COLLECTION)
        print(f"Before: vectors={count}  status={state}  state=active")

        rebuild = await client.vde.rebuild_index(COLLECTION)
        print(f"rebuild_index: {rebuild}")

        optimize = await client.vde.optimize(COLLECTION)
        print(f"optimize: {optimize}")

        result = await client.vde.compact_collection(COLLECTION)
        print(f"compact_collection: {result}")

        await client.vde.flush(COLLECTION)

asyncio.run(maintenance_for_quality())

Expected output

Before: vectors=30  status=green  state=active
rebuild_index: True
optimize: True
compact_collection: (\'sync-completed\', None)
The following table provides a schedule for each maintenance operation based on write and delete activity.
OperationWhen to runImpact
rebuild_indexAfter bulk updates (more than 20% of data changed).Rebuilds HNSW graph for better recall.
optimizePeriodically (daily or weekly).Merges small segments for better locality.
compact_collectionAfter many deletions.Purges tombstones and reclaims memory.
flushAfter any write operation.Persists data to disk.

Step 8: Update HNSW config without rebuilding data

collections.update lets you change HNSW parameters on an existing collection without re-ingesting any data. This is useful when you start a project with conservative settings for fast iteration and want to raise quality before going to production.
async def update_hnsw_config():
    async with AsyncVectorAIClient(url=SERVER) as client:
        info = await client.collections.get_info(COLLECTION)
        print(f"Before update: config retrieved")

        await client.collections.update(
            COLLECTION,
            hnsw_config=HnswConfigDiff(m=32, ef_construct=256),
        )
        print("HNSW updated to m=32, ef_construct=256.")

        await client.vde.rebuild_index(COLLECTION)
        print("Index rebuilt.")

asyncio.run(update_hnsw_config())
Starting with low m and ef_construct values keeps build times fast during development. Increasing them before deployment raises the recall ceiling without requiring any data migration.

Retrieval quality checklist

The following tables summarize every lever available in Actian VectorAI DB for optimizing retrieval quality, grouped by when the parameter takes effect.

Collection-level settings (set once, rebuild to change)

These parameters are fixed at collection creation time. Changing them requires recreating the index.
LeverParameterEffect on quality
Distance metricDistance.Cosine / Dot / EuclidDefines similarity semantics.
HNSW connectivityHnswConfigDiff(m=16)Higher m = denser graph = better recall.
HNSW build qualityHnswConfigDiff(ef_construct=128)Higher = better-connected graph.
These parameters can be tuned on every search request without changing or rebuilding the index.
LeverParameterEffect on quality
Search widthSearchParams(hnsw_ef=128)Higher = more accurate, slower.
Exact modeSearchParams(exact=True)Perfect recall, no approximation.
Score thresholdscore_threshold=0.5Removes low-confidence results.
Multi-stage prefetchPrefetchQuery(query=..., filter=..., limit=20)Widens candidate pool from multiple angles.

Operational maintenance (run periodically)

Run these operations on a schedule to keep retrieval quality from degrading as data changes over time.
LeverMethodEffect on quality
Index rebuildvde.rebuild_index()Refreshes HNSW graph after bulk changes.
Optimizationvde.optimize()Merges segments for better locality.
Compactionvde.compact_collection()Purges deleted vectors and reclaims memory.

Next steps