Documentation
New to vector search? Start with the guides and demos just below — plain-language concepts, focused tutorials, and end-to-end builds. Looking for a specific call? The full Python SDK and HTTP API reference are further down, one runnable example per method.
Quickstart
Install the SDK, get a key, and run your first query.
Python SDK
One row per method. Signature, prose, copyable example.
HTTP API
The same surface area over REST, for non-Python clients.
Answers
Direct answers to the questions people actually ask, each on its own page: the complete answer up front, then a runnable example with its real output.
How do I filter vector search results by metadata?
Pass a filter predicate with your query — filtering happens during the search, not after. With the operator set and real output.
~3 min read AnswerHow do I pick the right index for my vector search?
You don't — candidates are measured on your actual vectors and the fastest one meeting your recall target serves. Your one knob is target_recall.
~4 min read AnswerHow do I get CSV, JSONL, or Parquet files into a vector database?
Pass the file path to upsert — the extension picks the parser. Ten formats, one call each.
~4 min read AnswerHow do I migrate from pgvector?
Dump with \copy, upsert the file — pgvector's vector text parses natively. Plus the distance-to-similarity and WHERE-to-filter translations.
~5 min read AnswerHow do I export all my vectors and data out?
One call writes ids, vectors, and metadata to parquet you can read anywhere. No fees, no ceilings, no tier restrictions.
~3 min read AnswerWhat score does vector search return, and what threshold should I use?
Raw cosine similarity in [-1, 1]. Why 0.7 is not a universal cutoff, and how to pick yours from your own score distribution.
~4 min read AnswerHow do I keep my embedding model consistent between writes and queries?
Declare an embedding contract on the collection — mismatched query vectors are rejected by name, and every client can discover the right model.
~4 min read AnswerHow does Vector Panda compare to pgvector, Pinecone, and Qdrant?
An honest comparison — what each tool is best at, what we do differently, and the cases where Vector Panda is not the right choice.
~7 min read AnswerWhich storage tier should I pick for my vector collection?
Pick by access pattern, not size: hot for anything a user waits on, warm at a tenth the cost, paused for archives. Billing follows immediately.
~3 min readConcepts
Short, plain-language reads on the ideas behind vector search. No SDK code. New to all of this? Start with the first one.
When to use vector search — and when not to
The starting point: which problems vectors are the right tool for, which ones they're the wrong tool for, and how to tell the difference. No code, no math.
~9 min read ConceptWhat's actually inside a vector
Build one by hand from a spreadsheet of animals and the shape stops being mysterious — similarity falls out on its own, and you see why it's a list of numbers.
~8 min read ConceptA search is just another row
The spreadsheet's payoff: turn your question into a row, drop it in, and read off the nearest rows. A vector search, done by hand.
~8 min read ConceptHow a computer learns what words mean
No dictionary, no rules — just a machine that read the internet and noticed which words keep the same company. The black magic behind embeddings, explained.
~8 min read ConceptWhy you can't read a vector
A real embedding looks like noise — no "meaning" column anywhere. Why the numbers are unreadable, and where the meaning actually hides: in directions, not columns.
~9 min read ConceptVector embeddings 101
What an embedding is, why it captures meaning, and how to choose a model for your data.
~10 min read ConceptEmbeddings beyond text: images, audio & video
The same coordinates trick works on photos, sound, and video — and when they share one map, you can search images by typing a sentence.
~11 min read ConceptCosine similarity vs Euclidean distance
Why cosine is the default for semantic search, and what changes if you switch metrics.
~6 min read ConceptWhy vector search is fast — and a bit approximate
How you find the closest match among a billion items in milliseconds: you don't check all billion. The shortcut, and the honest trade it makes.
~7 min read ConceptHow AI assistants answer questions about your docs
The AI was never trained on your files, so how does it answer accurately without making things up? The open-book trick, and where vectors fit.
~8 min read ConceptWhen good embeddings give weird results
Your search returns nonsense and nothing looks broken. Usually it's one of four common, fixable mismatches. How to spot each one.
~7 min read ConceptHow auto-optimize picks your index
Eleven indexing strategies, recall vs latency on a sample of your real vectors, and the Pareto frontier that decides which one wins.
~10 min read ConceptHot, warm, paused: a tier mental model
RAM vs SSD vs ZFS as physical layers, and how the tier you pick maps to query latency and cost.
~8 min readProjects
End-to-end builds (30–60 minutes). Pick a use case, follow it through schema design, ingest, and query.
Visual search over an image library
Embed images with CLIP locally, upload to Vector Panda, then query with a typed sentence or another image. Same collection answers both.
~30 min build ProjectSearch your own data
Embed a folder of text files locally with sentence-transformers, upload to Vector Panda, and search semantically. Pick the upload tab that matches your data shape.
~30 min build ProjectSearch a movie by scene description
Slice five public-domain films into one-frame-per-second JPEGs, embed each with CLIP, then type the scene you remember and jump to the exact archive.org timestamp.
~30 min build ProjectRecommendations from a watch list
Average the embedding vectors of a few items a user liked to make a "taste centroid," then query with that to get a "you might also like" feed. The trick that's vector-database-only.
~25 min buildTutorials
Focused walkthroughs (5–15 minutes) covering one SDK feature at a time.
The four authentication modes
When to use VP.login(), VP.from_creds(), explicit keys, and VEEP_API_KEY. Same client class, four ways to hand it credentials.
Filter syntax in depth
Every operator the query API supports ($eq, $ne, $gt/gte, $lt/lte, $in, $nin, $and, $or), with copy-pasteable examples that run end-to-end.
~8 min read TutorialBatching queries with query_batch()
When to send N queries in one round-trip, when to just loop over query(), and what the response shape looks like. With real timings.
Inline upserts and the WAL fast path
Single-row writes that land in milliseconds and are queryable immediately. The WAL, what it guarantees, and when you'd reach for it instead of a bulk file upload.
~7 min readInstallation
The SDK is published as veep on PyPI. Python 3.9+.
# Everything the quickstart needs
# (samples = bundled embedding model; pandas = DataFrame upload support)
pip install "veep[samples,pandas]"
Or just pip install veep for the client on its own.
Quickstart
A complete round-trip: log in, create a collection, upload the bundled corpus, run a query.
from veep import VP, samples
vp = VP.login() # browser device flow, saves credentials
vp.collections.create("movies", tier="hot")
vp.vectors.upsert("movies", dataframe=samples.dataframe()) # 5,000 movies, embedded
q = samples.encode("a hobbit destroys a magic ring")
results = vp.vectors.query("movies", vector=q, top_k=3)
for r in results:
print(f"{r.score:.3f} {r.metadata.get('title','?')}")
samples.dataframe() returns 5,000 Wikipedia movie plots already embedded with the all-MiniLM-L6-v2 ONNX model. samples.encode(text) uses the same model so query vectors land in the same space. Useful for verifying installation; for production, embed your own data with whichever model you prefer.
MCP server
Vector Panda publishes this documentation as an MCP server, so AI assistants and coding agents can search docs, look up API operations, and check current pricing directly. No account or API key required.
# Claude Code
claude mcp add --transport http vector-panda-docs https://www.vectorpanda.com/mcp
Or add it to any MCP client that supports Streamable HTTP:
{
"mcpServers": {
"vector-panda-docs": {
"type": "http",
"url": "https://www.vectorpanda.com/mcp"
}
}
}
The server exposes eight read-only tools: search_docs, read_doc, get_quickstart, get_api_operation, get_pricing, get_benchmark_methodology, compare_storage_tiers, and list_supported_formats. Every result cites its canonical URL on this site, and the answers come from the same sources that render these pages — the machine-readable spec behind get_api_operation is published at /openapi.yaml.
Operate your database over MCP
A second server, authenticated with your API key, lets agents work with your own collections: create and manage collections, upsert and search vectors with metadata filters, inspect schema and files, review measured index experiments (and activate a candidate explicitly), and export your data.
# Claude Code
claude mcp add --transport http vector-panda https://www.vectorpanda.com/api/mcp \
--header "Authorization: Bearer YOUR_API_KEY"
{
"mcpServers": {
"vector-panda": {
"type": "http",
"url": "https://www.vectorpanda.com/api/mcp",
"headers": { "Authorization": "Bearer YOUR_API_KEY" }
}
}
}
Every operation acts only on the account the key belongs to. Collections can declare an embedding contract — which model produced their vectors, at what dimensions — so agents and frameworks discover the right model to embed with and mismatched queries fail loudly instead of returning nonsense; embedding always happens on your side, never ours. Searches accept the same MongoDB-style metadata filters as the SDK, destructive tools name their targets exactly, and recommend_index is read-only — it reports the cheapest measured index candidate meeting your recall and latency constraints, with the evidence, and changes nothing until you call activate_index yourself.
Python SDK reference
Every public method, in the order a typical script calls them: authenticate, create a collection, upload, query.
Authentication
Four ways to get an authenticated VP instance, ranging from interactive (good for notebooks) to environment-driven (good for CI).
VP.login() — interactive device flow
Class method. Opens a browser-based device-code flow, prompts you to confirm, then returns an authenticated client. By default it writes the credentials to ~/.veep/credentials.json so subsequent scripts can use VP.from_creds().
from veep import VP
vp = VP.login() # prints a code, opens https://www.vectorpanda.com/cli-auth
# vp is now authenticated and ~/.veep/credentials.json is written
VP.from_creds() — reuse saved credentials
Class method. Reads ~/.veep/credentials.json (written by a prior VP.login() or vp.save()) and returns an authenticated client. The path used for non-interactive runs.
vp = VP.from_creds()
print(vp.host) # https://api.vectorpanda.com (or VEEP_HOST override)
VP(api_key=…) — explicit constructor
Direct constructor. Reads the API key from the VEEP_API_KEY environment variable (or pass api_key= explicitly). The host argument falls back to VEEP_HOST, then to https://api.vectorpanda.com. Raises AuthError when no key is available. This is the recommended shape for CI and other non-interactive environments.
# export VEEP_API_KEY=veep_live_... (set in your shell or CI secrets)
vp = VP()
vp.save() — persist credentials
Writes the current api_key and host to ~/.veep/credentials.json with mode 0600. Useful when you constructed VP programmatically and want subsequent shells to pick it up via VP.from_creds().
# vp constructed any of the ways above
vp.save()
# later, from a different process:
vp2 = VP.from_creds()
Health
vp.ping() — liveness check
Hits the cluster's /api/v1/health endpoint and returns True on a 2xx response. The endpoint is unauthenticated by design — it tells you whether the cluster is reachable, not whether your API key is valid. Use vp.whoami() for the auth check.
assert vp.ping() is True
vp.whoami() — identity / auth check
Authenticated equivalent of ping(). Returns the resolved client_id and client_name. Raises AuthError on a missing or invalid API key. Use this when you want to confirm a key is valid before doing real work, without the side effects of collections.list().
info = vp.whoami()
print(info["client_id"], info["client_name"])
Collections
A collection is a named bucket of vectors with a single schema. Tiers (hot / warm / paused) control where the data lives and what queries cost.
collections.create() — create a collection
Creates a new collection. tier defaults to "hot" (RAM-resident). The schema fields are optional — by default the SDK infers them from the first upload.
vp.collections.create("movies", tier="hot")
# with explicit schema hints
vp.collections.create(
"movies",
tier="warm",
id_field="id",
vector_field="vector",
)
target_recall sets how accurate this collection's searches have to be, between 0.50 and 0.999. It defaults to 0.95.
Recall is the share of the true nearest neighbours a search actually returns — at 0.95, a search asking for the closest 100 rows typically finds 95 of the true closest 100. Vector Panda serves the fastest configuration whose measured recall meets your target, so raising it buys accuracy and lowering it buys speed.
Leave it alone unless you have a reason. 0.95 is the right answer for most search and recommendation work. Raise it toward 0.999 when a missed row is expensive — deduplication, compliance lookups, anything where "close enough" isn't. Lower it toward 0.80 when you want the cheapest possible latency and an occasional missed row costs you nothing.
# accuracy matters more than speed
vp.collections.create("legal-docs", target_recall=0.99)
# speed matters more than the occasional missed row
vp.collections.create("related-posts", target_recall=0.85)
Change it later with collections.update().
if_exists controls what happens when a collection with this name already exists:
"ignore"(default) — log a warning and return the existing collection. Re-running a setup script doesn't fail. Thetierand schema args are not applied to the existing collection."replace"— DESTRUCTIVE. Delete the existing collection (and all its stored vectors) before creating the fresh one. Use only when you intentionally want a clean slate."error"— raiseCollectionAlreadyExistsError. Strict opt-in for callers that need fail-fast on conflict.
from veep.exceptions import CollectionAlreadyExistsError
# idempotent re-run (default) — returns existing if it's there
vp.collections.create("movies")
# wipe + recreate (DELETES ALL STORED VECTORS)
vp.collections.create("movies", if_exists="replace")
# strict mode — raises CollectionAlreadyExistsError on conflict
try:
vp.collections.create("movies", if_exists="error")
except CollectionAlreadyExistsError:
pass # the collection already exists — handle however you like
collections.list() — list every collection
Returns every collection visible to the API key. Each Collection exposes name, status, tier, target_recall, and tier-specific metadata.
for c in vp.collections.list():
print(c.name, c.status, c.tier)
collections.get() — fetch one
Returns a single Collection by name, or raises CollectionNotFoundError.
col = vp.collections.get("movies")
print(col.name, col.tier, col.status)
collections.status() — short status string
Returns just the lifecycle string: awaiting_artifacts (no data yet), processing, distributing, awaiting_workers, or ready. Cheap polling target after an upload.
import time
while vp.collections.status("movies") != "ready":
time.sleep(2)
collections.update() — change the recall target
Changes a collection's recall target and returns the updated Collection. Takes effect immediately: Vector Panda re-evaluates how it serves this collection against the new target, without you re-uploading anything.
Raising the target can make queries slower, because the configuration that meets the higher bar may be a slower one. Lowering it can make them faster. Your data is untouched either way.
# tighten the target after seeing a few misses you cared about
col = vp.collections.update("movies", target_recall=0.99)
print(col.target_recall) # 0.99
# loosen it when latency matters more
vp.collections.update("movies", target_recall=0.90)
Raises ValidationError outside 0.50–0.999, and CollectionNotFoundError if the collection doesn't exist.
collections.delete() — drop a collection
The example uses a throwaway "scratch" collection so the page's other examples (which all reference "movies") keep working. Substitute the name you actually want to drop.
Drops the collection and every vector in it. Irreversible.
vp.collections.create("scratch", if_exists="replace")
vp.collections.delete("scratch")
Schema
Each collection has one schema: which column holds the ID, which holds the vector, plus dimension and format. Most callers let the schema auto-confirm from the first upload; these methods are for explicit control.
schema.get() — current schema
Returns a SchemaInfo with state, id_field, vector_field, dimension, and format.
schema = vp.schema.get("movies")
print(schema.state, schema.id_field, schema.vector_field, schema.dimension)
schema.confirm() — pin the schema
Locks in the field names and (optionally) format/dimension. Only needed if you skipped id_field / vector_field in collections.create() and want to commit them before the first upload.
vp.schema.confirm(
"movies",
id_field="id",
vector_field="vector",
)
schema.update() — change a field name
Updates one or both schema fields after the schema is already confirmed. Pass reprocess=True to re-extract artifacts under the new schema.
Vectors
The data plane: how rows get in, how queries run, and how individual rows are amended or removed.
vectors.upsert() — four ways to upload
One method, four input shapes. Pick whichever fits your data layout.
import pyarrow as pa
from veep import samples
df = samples.dataframe().head(100) # pandas DataFrame, fully embedded
df.to_parquet("plots.parquet", index=False)
arrow_table = pa.Table.from_pandas(df)
# 1. From a pandas DataFrame
vp.vectors.upsert("movies", dataframe=df)
# 2. From a file on disk (.parquet, .csv, .npy, ...)
vp.vectors.upsert("movies", "plots.parquet")
# 3. From a pyarrow Table
vp.vectors.upsert("movies", table=arrow_table)
# 4. Inline rows (small batches, ~ms latency via WAL fast path)
vp.vectors.upsert("movies", vectors=[
{"id": "m-extra-1", "vector": samples.encode("Aliens"),
"metadata": {"title": "Aliens", "year": 1986, "genre": "horror"}},
])
Every mode returns an UploadResult with status (e.g. created / accepted) and filename (the server-side file the upload was stored as). Inline upserts go through a separate WAL fast path; the other three become parquet on the artifact server.
vectors.replace() — overwrite an upload
Like upsert() with a file path, but instead of adding rows it replaces the contents of the same server-side file. Idempotent for same-content reuploads.
vp.vectors.replace("movies", "plots.parquet")
vectors.query() — similarity search
Runs a vector similarity search. Results are returned in descending score order. Cosine similarity scores fall in [0, 1]: 1.0 is identical, 0.0 is orthogonal.
q = samples.encode("a hobbit destroys a magic ring")
results = vp.vectors.query(
"movies",
vector=q,
top_k=5,
min_score=0.4,
filter={"genre": "fantasy"},
)
for r in results:
print(f"{r.score:.3f} {r.metadata.get('title','?')}")
The filter argument accepts MongoDB-style predicates. Equality ({"genre": "fantasy"}), comparison ($eq, $ne, $gt, $gte, $lt, $lte), set membership ($in, $nin), and boolean composition ($and, $or). Filters apply to metadata fields stored alongside each vector.
The metric parameter accepts "cosine" today. Euclidean and dot-product variants are roadmap; the API will accept the strings but currently routes them through cosine.
vectors.query_batch() — many queries in one round trip
Sends a list of query specs in a single request. Each entry is a dict with the same fields as query(): collection, vector, top_k, etc. Returns one QueryResults per input, in order.
prompts = [
"a hobbit destroys a magic ring",
"scientists clone dinosaurs and the dinosaurs escape",
"an off-duty cop is trapped in a high-rise at christmas",
]
batch = vp.vectors.query_batch([
{"collection": "movies", "vector": samples.encode(p), "top_k": 1}
for p in prompts
])
for p, hits in zip(prompts, batch):
print(p, "->", hits[0].metadata)
vectors.fetch() — by ID
Looks up a single row by its ID. Returns a FetchResult with found, vector, and metadata.
row = vp.vectors.fetch("movies", "m-42")
if row.found:
print(row.metadata, len(row.vector))
vectors.update() — modify rows in place
Replaces existing rows by ID. Each entry has the same shape as inline upsert(). Returns a dict containing updated_count.
new_vec = samples.encode("Aliens, the director's cut")
vp.vectors.update("movies", [
{"id": "m-extra-1", "vector": new_vec,
"metadata": {"title": "Aliens (Director's Cut)", "year": 1986, "genre": "horror"}},
])
vectors.delete() — by file or by IDs
Deletes either an entire uploaded file or a specific list of row IDs. Pass exactly one of filename (positional) or ids=.
# by file
vp.vectors.delete("movies", "plots.parquet")
# by IDs
vp.vectors.delete("movies", ids=["m-42", "m-43"])
vectors.list_files() — what's been uploaded
Returns one FileInfo per uploaded file, with name, size, and metadata. Useful for confirming an upload landed before issuing queries.
for f in vp.vectors.list_files("movies"):
print(f.name, f.size)
Samples
A bundled toy corpus and a matching encoder. Useful for trying things end-to-end before you wire up your own data.
samples.dataframe() — bundled corpus
Returns a 5,000-row pandas DataFrame of Wikipedia movie plots, each with a 384-dimensional embedding from all-MiniLM-L6-v2. Columns: id, title, year, genre, plot, vector.
from veep import samples
df = samples.dataframe()
print(len(df), df.columns.tolist())
# 5000 ['id', 'title', 'year', 'genre', 'plot', 'vector']
samples.encode() — encoder for query text
Embeds a single string with the same all-MiniLM-L6-v2 ONNX model used by the bundled corpus. Returns a 384-dimensional list of floats. Useful for query vectors when you want them to land in the same vector space as samples.dataframe().
q = samples.encode("a hobbit destroys a magic ring")
assert len(q) == 384
HTTP API
The same surface area as the Python SDK, exposed as REST. Use this for non-Python clients or for environments where adding a dependency isn't possible.
Base URL & authentication
https://api.vectorpanda.com
Every request authenticates with an API key. Most endpoints accept the key via the Authorization header; POST /api/v1/query additionally accepts an api_key field in the JSON body.
| Endpoint group | Auth method |
|---|---|
All /api/v1/* | Authorization: Bearer <api_key> |
POST /api/v1/query | Header above, or api_key in JSON body |
GET /api/v1/health | None — public |
GET /api/v1/whoami | Header above (returns 401 on bad key) |
POST/api/v1/query
Run a similarity search. Same semantics as vp.vectors.query().
curl https://api.vectorpanda.com/api/v1/query \
-H "Content-Type: application/json" \
-d '{
"api_key": "veep_live_...",
"collection": "movies",
"vector": [0.12, 0.04, ...],
"top_k": 5,
"min_score": 0.4,
"filter": {"genre": "fantasy"}
}'
Response shape:
{
"results": [
{"id": "m-42", "score": 0.84, "metadata": {"title": "Aliens"}},
...
]
}
GET/api/v1/health
Cluster health. No authentication. Returns 200 OK when the coordinator is up. Use this for status pages and uptime probes.
curl https://api.vectorpanda.com/api/v1/health
GET/api/v1/whoami
Identity / auth check. Authenticated; returns the resolved client_id and client_name in JSON. Returns 401 with the standard error envelope when the key is missing or invalid.
curl https://api.vectorpanda.com/api/v1/whoami \
-H "Authorization: Bearer veep_live_..."
Collections
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/collections | List collections |
| POST | /api/v1/collections | Create a collection (JSON body: name, tier, optional schema fields) |
| GET | /api/v1/collections/:name | Get one collection |
| DELETE | /api/v1/collections/:name | Delete a collection |
Files (uploads)
| Method | Path | Purpose |
|---|---|---|
| POST | /api/v1/collections/:name/files/:filename | Upload a vector file (raw bytes, content-type application/octet-stream) |
| PUT | /api/v1/collections/:name/files/:filename | Replace an existing file |
| DELETE | /api/v1/collections/:name/files/:filename | Delete a file |
Schema
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/collections/:name/schema | Get the current schema |
| POST | /api/v1/collections/:name/schema/confirm | Pin schema fields |
Errors
Errors come back as JSON with an error field. The Python SDK translates each non-2xx response into a typed exception subclass of VeepError.
{ "error": "Invalid API key" }
| Status | SDK exception | Meaning |
|---|---|---|
| 400 | ValidationError | Malformed request body or arguments |
| 401 | AuthError | Missing or invalid API key |
| 404 | CollectionNotFoundError / NotFoundError | Collection or row does not exist |
| 409 | CollectionAlreadyExistsError / FileAlreadyExistsError | Name conflict |
| 422 | UploadError | Upload payload could not be parsed (bad parquet, mismatched dimension, etc.) |
| 425 / 503 | CollectionNotReadyError / ServerError | Collection is still distributing or workers are unavailable |
| 5xx | ServerError | Unexpected server-side failure |
The SDK exposes the full hierarchy under veep.exceptions:
from veep.exceptions import (
VeepError, # base class for everything below
AuthError,
ValidationError,
NotFoundError, CollectionNotFoundError,
CollectionNotReadyError,
CollectionAlreadyExistsError,
UploadError, FileAlreadyExistsError,
QueryError,
TimeoutError,
ServerError,
)
