Skip to Content
We are live but in Staging 🎉
Data EnginesVectorWire Compatibility

Vector Wire Compatibility

K3 speaks two vendor vector wires — Qdrant and Pinecone — so a stock qdrant-client or Pinecone SDK can point at the bucket and go. Each adapter is a frontend, not a reimplementation: it translates the vendor’s calls onto the same vector plane every other wire reaches. That buys real drop-in behaviour for the core data-plane calls (create, upsert, search, delete) — and it means the control-plane and tuning surfaces of the original product either 404 or answer with placeholders. This page is the honest list.

For endpoints, the three credential forms, and how to wire the two required headers into each client, see Connect & wire adapters. The one-line version: on both wires the api-key header carries the db id (the bucket name), and the credential rides separately in Authorization.

Scores are distances, not similarities — lower is closer. Every K3 vector wire reports the plane’s raw KNN distance (cosine distance, L2 distance, or negative inner product), where a smaller number is a better match. Qdrant and Pinecone both return similarity scores where higher is better. The result ordering is correct on K3 — only the number’s sense is flipped — so a score_threshold, a score > 0.8 filter, or any cutoff ported from either product selects the wrong end of the list, with no error anywhere. This is the single most important thing to know about these wires. GraphQL’s _score carries the same raw distance.

Unsupported filters are loud; unknown fields are silent. Both wires reject a filter clause they can’t lower with a 400 naming the offending clause, rather than quietly returning unfiltered results. That guarantee covers filters only — unknown fields elsewhere in a request body (tuning knobs, sparse vectors, named-vector configs) are dropped by the JSON parser without comment. Each wire’s dropped-field list is below.

Qdrant

Endpoint https://qdrant.uk-lon-1.dodil.io, qdrant-client. The router is seven routes — this is the whole surface:

Method + pathqdrant-client call
PUT /collections/{name}create_collection / recreate_collection
GET /collections/{name}get_collection
DELETE /collections/{name}delete_collection
PUT /collections/{name}/pointsupsert
POST /collections/{name}/pointsretrieve
POST /collections/{name}/points/searchsearch
POST /collections/{name}/points/deletedelete

Everything else 404s, including GET /collections (list), GET / and /telemetry (the client’s startup version probe), /collections/{n}/exists, the 1.10+ universal /points/query, /points/scroll, /points/count, /points/recommend, the batch-search endpoints, the /points/payload set/overwrite/clear family, payload field /index, aliases, shards and snapshots.

get_collections() fails. The client’s list call and its startup version probe both hit routes that don’t exist, so a script that opens with client.get_collections() throws on its first call. Address collections by name.

# api_key = the DB ID (bucket); credential rides in Authorization — see /connect client.create_collection( "docs", vectors_config=VectorParams(size=384, distance=Distance.COSINE) ) client.upsert("docs", points=[ PointStruct(id=1, vector=[0.1] * 384, payload={"city": "LA", "year": 2021}), ]) hits = client.search( # `search`, NOT `query_points` collection_name="docs", query_vector=[0.1] * 384, limit=5, with_payload=True, query_filter=Filter(must=[ # `must` is the only clause FieldCondition(key="city", match=MatchValue(value="LA")) ]), ) # hits[i].score is a DISTANCE — smaller is closer, not larger.

Supported / verified

  • A collection is three tables. {name} holds the vectors as (id VARCHAR PRIMARY KEY, vector vector(N)); {name}_qmeta holds the distance metric and dimension; {name}_payload holds (id, payload JSON). All three are visible on the Postgres and GraphQL wires.
  • The metric round-trips honestly. _qmeta persists the distance you created the collection with, and both get_collection and search read it back. Distances: Cosine, Euclid (Euclidean also accepted) and Dot.
  • Response envelopes are wire-faithful{"result":…,"status":"ok","time":0.0}, errors under status.error.
  • A match-all delete is safe by construction. {"filter":{}} (the delete-everything idiom) becomes a TRUNCATE; {"filter":null} and {} are no-ops, never an accidental wipe.

Not supported / diverges

  • Collections usually serve exact scan, not ANN. Creation attempts a CREATE INDEX … USING hnsw and ignores the failure: the HNSW build needs an integer primary key and Qdrant collections use VARCHAR (Qdrant ids may be UUIDs). Search still works — it falls back to an exact KNN scan — but there is no error, no warning, and nothing in get_collection reveals it. Vectors that genuinely need ANN belong in a table with an integer PK, reached over SQL or the vector API.
  • get_collection reports hardcoded config. hnsw_config (m: 16, ef_construct: 100, full_scan_threshold: 10000), optimizer_config and wal_config are Qdrant’s defaults, emitted only because the client’s pydantic model requires the blocks — nothing you send changes them. status is always "green", segments_count always 1, payload_schema always {}, and vectors_count / indexed_vectors_count / points_count are all the same number. Only config.params.vectors.{size,distance} are real (from _qmeta).
  • Tuning parameters are parsed and dropped. On search, params.hnsw_ef and params.exact are read and ignored; score_threshold, offset, consistency and shard_key aren’t modelled at all. On create, hnsw_config, optimizers_config, quantization_config, on_disk_payload, shard_number and replication_factor are silently discarded. A non-standard params.metric is honoured, as a per-search metric override (a Dodil extension).
  • One unnamed vector per point. Named vectors, sparse vectors and multivectors don’t exist; the single column is always vector. A named-vector vectors_config fails JSON parsing (422), not the Qdrant error envelope. Manhattan distance is a 400.
  • Filters are must only. A must array of {key, match: {value}} (exact) and {key, range: {gt|gte|lt|lte}} (numeric) conditions, ANDed. should, must_not, nested filters, has_id, geo_* and match.any / match.except / match.text are each a 400 naming the clause. Match values must be a string, number or bool. Filtering is a scan of the payload companion then a restricted KNN — there is no payload index.
  • with_payload is all-or-nothing. An include/exclude selector is read as “yes, attach the payload” and the whole payload comes back.
  • delete_collection leaks its companions. It drops only {name}{name}_qmeta and {name}_payload survive, and create_collection’s IF NOT EXISTS reuses them, so a recreate_collection can resurrect stale payloads for reused ids. Drop the companions yourself with SQL.
  • Collection names must be [A-Za-z_][A-Za-z0-9_]* — they become SQL identifiers. my-collection is a 400, never silently rewritten.
  • Upserts write payloads one statement at a time — vectors go in one batched call, each non-empty payload is a separate round trip. Every hit reports "version": 0.

Pinecone

Endpoint https://pinecone.uk-lon-1.dodil.io, the Pinecone SDKs. Four routes:

Method + pathSDK call
POST /vectors/upsertindex.upsert
POST /queryindex.query
POST /vectors/deleteindex.delete
GET and POST /describe_index_statsindex.describe_index_stats

fetch, update, list, index management (/indexes, describe_index, create_index, configure_index), collections, imports and the integrated-inference endpoints all 404.

Supported / verified

  • Response shapes are wire-faithfulupsertedCount; matches[].{id,score,values,metadata}, with metadata omitted when absent. Remember score is a distance.
  • deleteAll truncates the namespace’s table; a delete by id list is a keyed DELETE.
  • Metadata filters are a flat AND{"field": scalar} shorthand, or {"field": {"$eq"|"$gt"|"$gte"|"$lt"|"$lte": v}}. Range bounds must be numeric.

Not supported / diverges

  • The index name rides a header, not the URL. X-Dodil-Index picks the backing table; without it every request lands on a table literally called vectors. The index name you pass to the SDK constructor is client-side only — the adapter never sees it.
  • The metric is a per-request header. X-Dodil-Metric (cosine | euclidean | dotproduct, default cosine) picks the distance per request, not as a property of the index — nothing stops two queries against one index using different metrics.
  • Namespaces are table-name mangling. (index, namespace) maps to the table {index}__{namespace}, and both halves are sanitized — every character outside [A-Za-z0-9_] becomes _. So my-index and my_index collide onto one table, and there is no namespace-level API.
  • describe_index_stats is thin. namespaces is always {}, totalVectorCount counts only the default-namespace table (data under a namespace reports 0), and there is no dimension or indexFullness.
  • The backing table is created on first upsert, dimension taken from the first vector in the batch. There is no create_index, so there is no point at which you declare a dimension or a metric up front.
  • Top-level $and / $or / $nor, and per-field $in / $nin / $ne / $exists, are each a 400 naming the operator.
  • No sparse vectors, no hybrid search, no reranking. A sparseVector on a query or upsert is dropped by the JSON parser without comment.
  • Query by id is not supported/query requires a vector and a topK; omitting either is a 422.
  • Upsert is one SQL statement per record, executed sequentially — a 100-vector batch is 100 round trips (plus one more per record that carries metadata). upsertedCount counts what was submitted.
  • No delete-by-metadata-filter.

The X-Dodil-* extension headers

Two proprietary headers have no vendor equivalent and are not optional in practice on the Pinecone wire:

HeaderWireEffectDefault
X-Dodil-IndexPineconePicks the backing table for the requesttable named vectors
X-Dodil-MetricPineconeDistance metric for this requestcosine
params.metric (body)QdrantPer-search metric overridethe collection’s stored metric

Without X-Dodil-Index, every Pinecone index on a bucket collapses onto one table. Set it explicitly on every request.

Coming from a vendor SDK

You expectOn K3
score where higher is betterscore is a distance — lower is closer. Invert any threshold.
Qdrant client.get_collections()404 — address collections by name; list over SQL (SHOW TABLES)
Qdrant query_points (1.10 universal API)404 — use the classic search
Qdrant HNSW tuning (hnsw_ef, ef_construct)Parsed and dropped; collections usually run exact scan anyway
Qdrant should / must_not filters400must only
Pinecone create_index / describe_index404 — the table is created on first upsert; pass X-Dodil-Index
Pinecone namespaces as a first-class APITable-name mangling {index}__{namespace}; no namespace API
Pinecone $in / $or / sparse vectors / rerank400 (filters) or silently dropped (sparse)
ANN by defaultOnly with an integer PK — Qdrant/Pinecone ids are strings, so exact scan

See also