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, ascore > 0.8filter, 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_scorecarries the same raw distance.
Unsupported filters are loud; unknown fields are silent. Both wires reject a filter clause they can’t lower with a
400naming 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 + path | qdrant-client call |
|---|---|
PUT /collections/{name} | create_collection / recreate_collection |
GET /collections/{name} | get_collection |
DELETE /collections/{name} | delete_collection |
PUT /collections/{name}/points | upsert |
POST /collections/{name}/points | retrieve |
POST /collections/{name}/points/search | search |
POST /collections/{name}/points/delete | delete |
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 withclient.get_collections()throws on its first call. Address collections by name.
qdrant-client
# 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}_qmetaholds the distance metric and dimension;{name}_payloadholds(id, payload JSON). All three are visible on the Postgres and GraphQL wires. - The metric round-trips honestly.
_qmetapersists the distance you created the collection with, and bothget_collectionandsearchread it back. Distances:Cosine,Euclid(Euclideanalso accepted) andDot. - Response envelopes are wire-faithful —
{"result":…,"status":"ok","time":0.0}, errors understatus.error. - A match-all delete is safe by construction.
{"filter":{}}(the delete-everything idiom) becomes aTRUNCATE;{"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 hnswand ignores the failure: the HNSW build needs an integer primary key and Qdrant collections useVARCHAR(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 inget_collectionreveals it. Vectors that genuinely need ANN belong in a table with an integer PK, reached over SQL or the vector API. get_collectionreports hardcoded config.hnsw_config(m: 16,ef_construct: 100,full_scan_threshold: 10000),optimizer_configandwal_configare Qdrant’s defaults, emitted only because the client’s pydantic model requires the blocks — nothing you send changes them.statusis always"green",segments_countalways1,payload_schemaalways{}, andvectors_count/indexed_vectors_count/points_countare all the same number. Onlyconfig.params.vectors.{size,distance}are real (from_qmeta).- Tuning parameters are parsed and dropped. On search,
params.hnsw_efandparams.exactare read and ignored;score_threshold,offset,consistencyandshard_keyaren’t modelled at all. On create,hnsw_config,optimizers_config,quantization_config,on_disk_payload,shard_numberandreplication_factorare silently discarded. A non-standardparams.metricis 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-vectorvectors_configfails JSON parsing (422), not the Qdrant error envelope.Manhattandistance is a400. - Filters are
mustonly. Amustarray of{key, match: {value}}(exact) and{key, range: {gt|gte|lt|lte}}(numeric) conditions, ANDed.should,must_not, nested filters,has_id,geo_*andmatch.any/match.except/match.textare each a400naming 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_payloadis all-or-nothing. An include/exclude selector is read as “yes, attach the payload” and the whole payload comes back.delete_collectionleaks its companions. It drops only{name}—{name}_qmetaand{name}_payloadsurvive, andcreate_collection’sIF NOT EXISTSreuses them, so arecreate_collectioncan 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-collectionis a400, 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 + path | SDK call |
|---|---|
POST /vectors/upsert | index.upsert |
POST /query | index.query |
POST /vectors/delete | index.delete |
GET and POST /describe_index_stats | index.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-faithful —
upsertedCount;matches[].{id,score,values,metadata}, withmetadataomitted when absent. Rememberscoreis a distance. deleteAlltruncates the namespace’s table; a delete by id list is a keyedDELETE.- 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-Indexpicks the backing table; without it every request lands on a table literally calledvectors. 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, defaultcosine) 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_. Somy-indexandmy_indexcollide onto one table, and there is no namespace-level API. describe_index_statsis thin.namespacesis always{},totalVectorCountcounts only the default-namespace table (data under a namespace reports0), and there is nodimensionorindexFullness.- 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 a400naming the operator. - No sparse vectors, no hybrid search, no reranking. A
sparseVectoron a query or upsert is dropped by the JSON parser without comment. - Query by
idis not supported —/queryrequires avectorand atopK; omitting either is a422. - 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).
upsertedCountcounts 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:
| Header | Wire | Effect | Default |
|---|---|---|---|
X-Dodil-Index | Pinecone | Picks the backing table for the request | table named vectors |
X-Dodil-Metric | Pinecone | Distance metric for this request | cosine |
params.metric (body) | Qdrant | Per-search metric override | the 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 expect | On K3 |
|---|---|
score where higher is better | score 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 filters | 400 — must only |
Pinecone create_index / describe_index | 404 — the table is created on first upsert; pass X-Dodil-Index |
| Pinecone namespaces as a first-class API | Table-name mangling {index}__{namespace}; no namespace API |
Pinecone $in / $or / sparse vectors / rerank | 400 (filters) or silently dropped (sparse) |
| ANN by default | Only with an integer PK — Qdrant/Pinecone ids are strings, so exact scan |
See also
- Connect & wire adapters — endpoints, the two-header credential story, ready-to-run client snippets
- Vector → Overview and Core Concepts — the collection / destination model
- SQL → SQL Compatibility — pgvector KNN operators over the same vectors
dodil data vsearch— KNN over a table’s vector column (gRPC facet, not the vendor wires)