Venn

Store adapters#

Connect an existing vector store to Venn without copying it into a new database. Adapters read application-owned vectors and outcomes; the application owns the client, credentials, schema, writes and connection lifetime.

Supported stores#

Backend Batch reader Outcome search Search and visibility
FAISS FaissAdapter FaissOutcomeAdapter App-supplied squared-L2 index; app serializes reads and writes.
PostgreSQL / pgvector PgVectorAdapter PgVectorOutcomeAdapter Filtered squared L2; visibility follows the cursor's transaction.
Qdrant QdrantAdapter QdrantOutcomeAdapter Index-based filtered search by default; outcome collections require an unnamed Euclidean vector.
Weaviate WeaviateAdapter WeaviateOutcomeAdapter Filtered nearest vectors; backend metric and replication semantics apply.
LanceDB LanceDBAdapter LanceDBOutcomeAdapter Prefiltered squared L2; outcome queries check out the latest table version.
Microsoft Azure AI Search AzureSearchAdapter AzureSearchOutcomeAdapter Configured vector index with prefilters by default; indexing is eventually consistent.

Use a batch reader to stream stored vectors into a sweep or other vector-consuming workflow. Its batches(batch_size=...) method yields dictionaries with ids, float32 vectors, optional outcomes, profile and revision. Batch readers require the pinned 768-dimensional text profile.

Use an outcome adapter with OutcomeService to choose among actions using their nearest recorded cases. Each search applies the option and metadata constraints before top-k. Outcome adapters carry a complete VectorTag; they can use other supported representations when the stored and query tags match exactly. Registration does not copy the store into a snapshot.

Outcome searches should normally use the store's vector index as the corpus grows. An exhaustive scan can be reasonable below roughly 5,000-10,000 vectors or when measuring exact recall, but that is a workload guideline, not an automatic cutoff. Dimension, filter selectivity, hardware and latency targets also matter. Venn does not count the collection on every request or switch search modes behind the application's back.

AzureSearchOutcomeAdapter defaults to exhaustive=False, allowing the configured vector index to serve the query. Use an HNSW vector profile for indexed approximate search; an exhaustiveKnn profile still scans. Set exhaustive=True explicitly for a small corpus or an exact reference run. QdrantOutcomeAdapter similarly defaults to exact=False; set exact=True to force a full exact search. Both modes retain option and metadata prefilters.

For pgvector, Weaviate and LanceDB, the adapter leaves index use to the backend: provision a compatible vector index and tune its search parameters. Their planners may still scan a small or highly selective subset. FAISS uses the app-supplied index on unfiltered queries; its current metadata-filtered path builds an exact eligible subset instead. That filtered path is not a scalable ANN search; use smaller app-owned partitions or a filter-aware database for large filtered collections.

Indexed approximate search trades recall for speed and can change which cases, and therefore which outcome, Venn returns. Measure recall and decision quality against exact search on representative queries before deployment. Batch readers are different: they intentionally visit every stored row rather than perform nearest-neighbor search.

Install and prepare your data#

FAISS is included in the base installation. From a source checkout, install the other clients with:

python -m pip install -e ".[adapters]"

See installation for Python and PyTorch setup. Import adapter classes from venn.adapters.

The examples below open existing, populated stores. They do not create services, indexes or credentials. Each stored case has:

Field Example type Meaning
id String Unique case identifier.
embedding 768 floats Vector from the exact pinned text reader and protocol.
option String The action taken, such as billing or technical.
outcome Number The observed utility of that action. Higher is better in these examples.
tenant String Application metadata used by the example equality filter.

Change the fields, options and filters to match your application. A dimension match alone does not establish compatibility: adapters trust the profile/tag you declare; they do not prove which reader produced existing vectors. Follow profile compatibility before connecting a store. These adapters do not generate embeddings or write feedback.

Shared usage#

Run this setup before one of the backend examples. Export one compatible query vector to query-vector.npy, with shape (768,); it must use the same pinned reader and protocol as the stored cases. Do not query a production index with arbitrary synthetic vectors.

import numpy as np

from venn import OutcomeService, TEXT_TAG, VENN_1

pin = {"profile": VENN_1.id, "revision": VENN_1.revision}
query_vector = np.load("query-vector.npy", allow_pickle=False)


def scan(adapter):
    for batch in adapter.batches(batch_size=256):
        print(batch["ids"], batch["vectors"].shape)
        # Consume this batch before requesting the next one.


def decide(adapter):
    service = OutcomeService()
    store = service.register_store(adapter)
    result = service.decide_batch(
        [{"vector": query_vector.tolist(), "tag": TEXT_TAG.to_dict()}],
        {"route": {
            "type": "nearest_outcome",
            "store": store,
            "options": ["billing", "technical"],
            "filters": {"tenant": "demo"},
            "k": 3,
        }},
    )
    print(result["results"][0]["answers"]["route"])

decide runs without a trained head or reader download because the query is already embedded. Its answer includes per-option support, outcome means and evidence IDs. Empty or insufficiently supported options are not evidence. See outcome stores for min_support, minimizing costs, exploration and the HTTP setup.

FAISS#

For this example, case-vectors.npy holds an (N, 768) array and case-metadata.json holds an aligned list of id, option, outcome and tenant dictionaries in exactly the same row order.

import json

import faiss

from venn.adapters import FaissAdapter, FaissOutcomeAdapter

vectors = np.ascontiguousarray(
    np.load("case-vectors.npy", allow_pickle=False), dtype=np.float32
)
with open("case-metadata.json", encoding="utf-8") as source:
    cases = json.load(source)
if len(vectors) != len(cases):
    raise ValueError("Vectors and metadata must have the same row count")

index = faiss.IndexFlatL2(VENN_1.dimensions)
index.add(vectors)
scan(FaissAdapter(
    index, [case["id"] for case in cases],
    [case["outcome"] for case in cases], **pin,
))

partitions = {}
for option in ("billing", "technical"):
    positions = [i for i, case in enumerate(cases) if case["option"] == option]
    option_index = faiss.IndexFlatL2(VENN_1.dimensions)
    option_index.add(np.ascontiguousarray(vectors[positions]))
    partitions[option] = {
        "index": option_index,
        "ids": [cases[i]["id"] for i in positions],
        "outcomes": [cases[i]["outcome"] for i in positions],
        "metadata": [{"tenant": cases[i]["tenant"]} for i in positions],
    }

decide(FaissOutcomeAdapter(partitions, tag=TEXT_TAG))

Batch indexes must support vector reconstruction. Outcome indexes must use squared L2 and have aligned ID, outcome and optional metadata lists per option. The application must update those lists and indexes together and serialize reads and writes. Filtered reads build an exact eligible subset before search. The flat indexes in this example are intended for small collections. For large unfiltered collections, supply a suitable ANN index instead; see indexed or exact search for the filtered-path limit.

PostgreSQL and pgvector#

Provision PostgreSQL with the vector extension and a populated cases table whose embedding column is vector(768). Set DATABASE_URL to your connection string. The example uses a server-side cursor for bounded batch reads and a regular cursor for outcome queries.

import os

import psycopg
from pgvector.psycopg import register_vector

from venn.adapters import PgVectorAdapter, PgVectorOutcomeAdapter

with psycopg.connect(os.environ["DATABASE_URL"]) as connection:
    register_vector(connection)
    with connection.cursor(name="venn_scan") as cursor:
        scan(PgVectorAdapter(cursor, "cases", outcome_column="outcome", **pin))
    with connection.cursor() as cursor:
        decide(PgVectorOutcomeAdapter(cursor, "cases", tag=TEXT_TAG))

Use id_column, vector_column and outcome_column to rename fields; the outcome adapter also accepts option_column. Table and column names must be simple identifiers, not SQL expressions or dotted schema paths. Filter values are SQL parameters. Choose transaction isolation and commit boundaries to provide the visibility your application needs; a retained snapshot can hide external writes.

Qdrant#

Set QDRANT_URL and, if required, QDRANT_API_KEY. The populated cases collection must use an unnamed, 768-dimensional Euclidean vector for outcome searches, with option, outcome and tenant in each point's payload.

import os
from contextlib import closing

from qdrant_client import QdrantClient

from venn.adapters import QdrantAdapter, QdrantOutcomeAdapter

with closing(QdrantClient(
    url=os.environ["QDRANT_URL"],
    api_key=os.environ.get("QDRANT_API_KEY"),
)) as client:
    scan(QdrantAdapter(client, "cases", outcome_key="outcome", **pin))
    decide(QdrantOutcomeAdapter(client, "cases", tag=TEXT_TAG))

The batch reader also supports named vectors through vector_name. QdrantOutcomeAdapter does not: it checks for an unnamed Euclidean collection matching the tag. Complete application writes with wait=True before depending on their visibility. Outcomes permit the backend vector index by default and report squared distances. Set exact=True on the outcome adapter to force exhaustive search for small collections or an exact reference measurement.

Weaviate#

This example connects to a local Weaviate instance at the SDK's default HTTP and gRPC ports. Use an application-configured authenticated client for a remote deployment. The existing Cases collection must hold a single default vector from the pinned reader, with option, outcome and tenant properties.

import weaviate

from venn.adapters import WeaviateAdapter, WeaviateOutcomeAdapter

with weaviate.connect_to_local() as client:
    collection = client.collections.use("Cases")
    scan(WeaviateAdapter(collection, outcome_key="outcome", **pin))
    decide(WeaviateOutcomeAdapter(collection, tag=TEXT_TAG))

The batch reader can select a named vector with vector_name. The outcome adapter uses the collection's default vector search and has no named-vector selector. Change property names through outcome_key and, for outcomes, option_key. Distances retain the collection's metric semantics. Replication consistency controls when writes become visible.

LanceDB#

Set LANCEDB_URI to your database. The existing cases table needs an embedding vector column containing 768-element vectors, plus the shared metadata fields above.

import os

import lancedb

from venn.adapters import LanceDBAdapter, LanceDBOutcomeAdapter

table = lancedb.connect(os.environ["LANCEDB_URI"]).open_table("cases")
scan(LanceDBAdapter(
    table, vector_column="embedding", outcome_column="outcome", **pin,
))
decide(LanceDBOutcomeAdapter(table, vector_column="embedding", tag=TEXT_TAG))

Use id_column, vector_column, outcome_column and, for outcomes, option_column to match your schema. Outcome queries check out the latest table version before a fresh prefiltered L2 search. Complete the app's writes first; the adapter does not write or manage transactions.

Set AZURE_SEARCH_ENDPOINT, AZURE_SEARCH_INDEX and AZURE_SEARCH_KEY. Supply an existing synchronous SearchClient; an application-owned Microsoft Entra credential can also be used instead of the API key shown here.

import os

from azure.core.credentials import AzureKeyCredential
from azure.search.documents import SearchClient

from venn.adapters import AzureSearchAdapter, AzureSearchOutcomeAdapter

with SearchClient(
    endpoint=os.environ["AZURE_SEARCH_ENDPOINT"],
    index_name=os.environ["AZURE_SEARCH_INDEX"],
    credential=AzureKeyCredential(os.environ["AZURE_SEARCH_KEY"]),
) as client:
    scan(AzureSearchAdapter(client, outcome_field="outcome", **pin))
    decide(AzureSearchOutcomeAdapter(client, tag=TEXT_TAG))

Default fields are id, embedding, option and outcome. Rename them with id_field, vector_field, outcome_field and, for outcomes, option_field. Only simple top-level field names are supported.

For batch scans, vectors must be retrievable and the unique string key must be filterable and sortable. Scans advance by key, not skip offset, return at most 1,000 documents per batch, and are not snapshots.

For outcomes, the vector field's dimensions and reader must match the declared tag. ID and outcome fields must be retrievable; option and metadata fields must be filterable scalars. Equality filters support strings, numbers, booleans and nulls. The adapter sends one vector query per option and state, with preFilter, no text search and no hybrid score fusion. It does not configure or inspect the index schema. The default exhaustive=False uses the index's configured algorithm; exhaustive=True explicitly opts into brute-force search.

Distances use Azure's inverse vector score, (1 - score) / score: this is cosine distance for a cosine index; other metrics retain the backend's scale, not a promised squared-L2 unit. Indexing is eventually consistent. A successful upload is not a search-visibility guarantee: poll filtered search until the expected cases are visible before relying on them for subsequent decisions.

Operational limits and validation#

Batch readers are not snapshot exporters. Keep the underlying corpus stable or record its version when using scans for an evaluation. Do not compare distance magnitudes across backends with different metric conventions.

Only the FAISS outcome adapter implements the optional float16 exact-key exact_match utility override, and only when configured with float16 query keys. This override is different from the Azure exhaustive and Qdrant exact nearest-neighbor search controls. Compiling an outcome store into a head additionally requires an adapter with export(); the provided FAISS outcome adapter supplies it.

From a development checkout, run adapter contracts with:

python -m pip install -e ".[dev,adapters]"
python -m pytest -q tests/test_adapters_mcp.py tests/test_outcome_api.py tests/test_azure_search_adapter.py

These include optional local Qdrant and LanceDB checks and Azure's real SDK against a test HTTP transport. They do not replace integration validation against a provisioned PostgreSQL, Weaviate or Azure AI Search service. Validate your deployment's schema, permissions, filters and write visibility before using its results.

View this page on GitHub

On this page