A pure BASIL REST client for Qdrant, tested against the local Qdrant 1.20.0 server. No C adapter, external SDK, Python, or Rust client is needed to build or run an application using it. Networking and JSON use BASIL’s standard library.
The library has two layers:
- Friendly helpers for collections, exact numeric/UUID IDs, points, dense/sparse vectors, filters, queries, retrieval, deletion and paged scrolling.
- All 69 operations in the pinned 1.20.0 REST schema, with named methods, unrestricted JSON bodies/query parameters, binary snapshot download/upload, and raw HTTP/JSON entry points for future or custom endpoints.
This is REST support on port 6333, not a gRPC client or a Qdrant Cloud account management SDK. Nested request schemas are not all represented as BASIL records; advanced operations accept JsonValue so new options are not blocked by the client.
Build and import
Build the library with the normal BASIL compiler:
cd ~/src/basic/libraries/qdrant
~/src/basic/build/selfhost/stage3 build
This produces build/qdrant.blib. Add it to your application’s basic.toml:
[dependencies]
Qdrant = {path = "../basic/libraries/qdrant/build/qdrant.blib"}
Then use IMPORT Qdrant and build your application normally. You can copy the
single .blib elsewhere and point at that copy; producer source files are not
needed. Compiled libraries must match the compiler’s BASIL ABI: rebuild the .blib
when updating a development compiler that changes its ABI.
A source dependency is also supported:
[dependencies]
Qdrant = {path = "../basic/libraries/qdrant"}
No Python build wrapper is required. The included Python scripts only regenerate checked-in endpoint source and run development tests.
First connection
IMPORT Qdrant
client = Qdrant.NewClient("http://localhost:6333")
PRINT JOIN(Qdrant.ListCollections(client), ", ")
PRINT StringifyJSON(Qdrant.Root(client))NewClient defaults to http://localhost:6333. An optional second argument supplies
an api-key header:
client = Qdrant.NewClient("https://example.qdrant.io:6333", "your-api-key")
client = Qdrant.WithTimeout(client, Seconds(20))
client = Qdrant.WithLimits(client, 16777216, 67108864)Clients are opaque, independent copyable configuration values. WithTimeout, WithLimits, WithTLS and WithHeader return new configurations; they do not change other copies. Base URLs may have a reverse-proxy prefix, but cannot contain query parameters, fragments or embedded credentials.
HTTPS verifies certificates. WithTLS accepts the standard TLSClientOptions, including custom PEM CACertificates, ServerName and client Certificate/PrivateKey. There is no insecure-certificate bypass. WithHeader can provide explicit headers such as Authorization for a proxy; framing and api-key headers are reserved.
Create, insert and query
The following example creates a new collection. It does not silently replace an existing collection:
IMPORT Qdrant
TYPE Document
Text AS STRING JSON NAME "text"
Topic AS STRING JSON NAME "topic"
END TYPE
client = Qdrant.NewClient()
IF NOT Qdrant.Exists(client, "basic_documents") THEN
Qdrant.CreateCollection(client, "basic_documents", 3, "Cosine")
END IF
points = [Qdrant.Point(1, [1.0, 0.0, 0.0], ToJSON(Document {Text="Learning BASIC", Topic="programming"})), Qdrant.Point(2, [0.0, 1.0, 0.0], ToJSON(Document {Text="Vector databases", Topic="databases"}))]
Qdrant.Upsert(client, "basic_documents", points)
FOR EACH hit IN Qdrant.Query(client, "basic_documents", [1.0, 0.0, 0.0], 5)
document = FromJSON(hit.Payload) AS Document
PRINT Qdrant.IdText(hit.Id); ": "; document.Text
IF LET score = hit.Score THEN
PRINT score
END IF
NEXTThe example’s upsert intentionally writes IDs 1 and 2 if run on an existing collection. The executable tutorial is more conservative: it requires an explicitly supplied fresh collection name and refuses an existing one. It leaves that newly created collection available for exploration.
~/src/basic/build/selfhost/stage3 build --manifest examples/tutorial.toml
./examples/build/qdrant-tutorial my_new_demo_collection
The quickstart only lists collections and is read-only.
Upsert and Delete helpers default to wait=TRUE, so a subsequent read does not
race an acknowledged background mutation. Their returned JsonValue contains the
operation result. Generated low-level methods use server defaults unless query
parameters explicitly override them. The client never silently retries writes.
Point IDs and results
numeric = Qdrant.Id(42)
wide = Qdrant.Id(UInt64Max())
uuid = Qdrant.Id("550e8400-e29b-41d4-a716-446655440000")
point = Qdrant.GetPoint(client, "basic_documents", numeric)
records = Qdrant.Retrieve(client, "basic_documents", [numeric, uuid], TRUE)
Qdrant.Delete(client, "basic_documents", [numeric])Numeric IDs preserve the entire unsigned 64-bit range: no DOUBLE conversion is involved. Negative numeric IDs and invalid UUID strings fail locally. A STRING passed to Id means a UUID, not a decimal integer spelling. IdValue returns the exact JSON representation; IdText is convenient for display/path use.
PointRecord has:
| Field | Meaning |
|---|---|
| Id | Opaque PointId |
| Payload | JsonValue, including null if absent |
| Vector | JsonValue, preserving dense/named/sparse shapes or null if omitted |
| Score | OPTIONAL OF DOUBLE; absent for unscored retrieval/scroll results |
| Raw | Complete original point JSON, retaining version/shard/other fields |
The GetPoint overload taking PointId returns PointRecord. The generated overload taking a STRING path ID returns raw JsonValue, like other low-level endpoints.
Filters and query options
filter = Qdrant.AllOf([Qdrant.MatchValue("topic", "programming"), Qdrant.Between("price", 10.0, 50.0)])
options = Qdrant.QueryOptions {Limit=20, Filter=SOME(filter), WithVectors=TRUE, Exact=TRUE}
hits = Qdrant.Query(client, "products", [1.0, 0.0, 0.0], options)QueryOptions supports Limit, Offset, Using, Filter, ScoreThreshold, WithPayload, WithVectors and Exact. Limit must be positive; Offset must be nonnegative. MatchValue supports string, INTEGER, BOOLEAN or arbitrary JsonValue input. AllOf/AnyOf/NoneOf build must/should/must_not groups. Between uses inclusive bounds. SelectIds and SelectFilter build mutation selectors.
For other filter forms—geo, datetime, nested, text, match-any, minimum-should, and so on—use the same JSON representation as Qdrant’s API:
match = Qdrant.WithField(Qdrant.Object(), "any", JsonArray([JsonString("red"), JsonString("blue")]))
condition = Qdrant.WithField(Qdrant.Object(), "key", JsonString("color"))
condition = Qdrant.WithField(condition, "match", match)
filter = Qdrant.AllOf([condition])Object makes an empty JsonValue object; WithField returns an independent updated object. Member reads a required field with a checked error. Standard JsonGet*, Get/Find, ToJSON/FromJSON and ParseJSON remain available. User ToJSON/FromJSON conversions have their normal JsonError/custom-converter contracts; the library does not conceal failures in application code evaluated before a call.
Named, sparse, multivector, hybrid and inference queries
DenseVector converts a nonempty finite LIST OF DOUBLE. SparseVector validates matching lengths, unique unsigned-32-bit indices and finite values. Empty sparse vectors are permitted.
vectors = Qdrant.WithField(Qdrant.Object(), "dense", Qdrant.DenseVector([1.0, 0.0, 0.0]))
vectors = Qdrant.WithField(vectors, "text", Qdrant.SparseVector([1, 7], [0.8, 0.2]))
point = Qdrant.Point(Qdrant.Id(1), vectors, Qdrant.Object())
options = Qdrant.QueryOptions {Using="text", Limit=10}
hits = Qdrant.Query(client, "named_collection", Qdrant.SparseVector([7], [1.0]), options)Use CreateCollection’s JSON overload to configure named/sparse/multivector collections. VectorConfig(size,distance) builds a dense configuration; supported distances are Cosine, Dot, Euclid and Manhattan. Raw collection bodies can specify all other tuning, storage, quantization, replication and shard options.
QueryPoints accepts an unrestricted query body: prefetch, RRF/DBSF fusion, recommend/discover/context, formula queries, lookup sources, inference documents, and server-specific extensions. Point’s JsonValue vector overload similarly passes vector shapes through without restricting them to one dense representation. See the advanced live test for a complete named/sparse/RRF example that was actually run against the server.
Scroll without loading the collection into memory
cursor = Qdrant.Cursor(client, "basic_documents", 100)
' Optional: cursor.Filter = SOME(filter)
done = FALSE
WHILE NOT done
IF LET page = Qdrant.NextPage(BYREF cursor) THEN
FOR EACH point IN page.Points
PRINT Qdrant.IdText(point.Id)
NEXT
ELSE
done = TRUE
END IF
WENDScrollResult retains Points, NextOffset and Raw. ScrollPage exposes manual paging with optional offset/filter. Cursor state advances only after a successful page; an unchanged continuation ID is rejected to prevent an endless loop. A cursor is not a server snapshot or transaction: concurrent collection changes retain Qdrant’s normal scroll semantics. Raw ScrollPoints supports advanced order-by and other pagination modes; the convenience cursor uses default point-ID ordering.
Complete REST access
ENDPOINTS.md lists all 69 generated methods and their exact verbs, paths, request body forms, response kinds and supported query parameter names. Examples include collection updates, vector names, all payload operations, batch updates/queries, grouped queries, search matrices, aliases, indexes, shards, cluster operations, quotas, telemetry, issues and snapshots.
Each named endpoint has an overload accepting a final query dictionary:
params = Qdrant.WaitParameters(TRUE)
Put(BYREF params, "timeout", "30")
result = Qdrant.UpsertPoints(client, "basic_documents", body, params)Path arguments in generated signatures are strings and are individually encoded. Query dictionary values are their wire spellings, such as “true”, “all” or “30”. No operation is invoked merely by constructing a client or importing the library.
Low-level entry points:
| Function | Result |
|---|---|
| RequestRaw(client,method,path,bytes,contentType,query) | HTTPResponse, including exact bytes/headers |
| RequestRaw(client,method,path) | Same, without a request body/query |
| RequestJSON(client,method,path[,body[,query]]) | Full JSON response envelope |
| Call(client,method,path[,body[,query]]) | Validated envelope’s result, or the full JSON value if result is absent |
| ResponseJSON(response) / Result(json) | Explicit parsing/unwrapping helpers |
Paths must start with one slash, stay on the configured origin, and contain no
literal dot segments. Pass query values separately; absolute external URLs and
fragments are rejected. JsonNull as a request body means no body; RequestRaw can
send an explicit JSON null or any other content when required. No redirect is
followed, which also prevents API keys being forwarded to a redirected host.
Snapshots and non-JSON responses
snapshot = Qdrant.CreateSnapshot(client, "basic_documents", Qdrant.WaitParameters())
name = JsonGetString(Qdrant.Member(snapshot, "name"))
response = Qdrant.GetSnapshot(client, "basic_documents", name)
PRINT "Snapshot bytes: "; LEN(response.Body)
params = Qdrant.WaitParameters()
Put(BYREF params, "priority", "snapshot")
Qdrant.RecoverFromUploadedSnapshot(client, "restore_target", response.Body, params)Recovery changes the target collection: choose it explicitly. Upload methods build multipart/form-data with a snapshot part, preserve arbitrary bytes, and select a boundary absent from the payload. SnapshotBody also exposes multipart bytes and ContentType for custom calls/filenames. Download methods, Metrics and health endpoints return HTTPResponse rather than assuming everything is JSON.
Transfers are buffered, not streaming. Defaults cap both request and response at 64 MiB. WithLimits can raise/lower these limits; multipart framing counts toward the request limit. Large snapshots need adequate memory or a future streaming transport. This implementation does not claim bounded-memory file streaming.
Error handling and request policy
TRY
Qdrant.GetCollection(client, "missing_collection")
CATCH err AS Qdrant.QdrantError
PRINT err.Kind; " / HTTP "; err.Status; ": "; err.Message
END TRYQdrantError exposes Kind, Status, Code, Method, Path, Message and Body.
- http: non-2xx response; Status is retained, Body contains bounded diagnostics.
- transport: connection/I/O/protocol/timeout/transport-limit failures; Code retains the underlying code where available, Status is zero.
- tls: certificate/handshake/TLS failures.
- protocol: malformed JSON or unexpected envelope status.
- validation: invalid client/builder inputs or incompatible typed field shapes.
Status may be zero when no HTTP response context is available. Diagnostics are limited to 16 KiB; non-UTF-8 bodies have a descriptive placeholder. The library has no logging facility and does not put the API key into constructed error text. RequestRaw preserves full successful bytes within the configured limit; error bodies are diagnostic excerpts, not full snapshot downloads.
One request uses one connection, closed by deterministic resource cleanup.
There is no connection pool, automatic mutation retry, or retry/backoff policy
for rate limiting. A transport failure does not prove a remote write failed.
WithTimeout configures per-phase header/body/write deadlines, not one overall
wall-clock request deadline. Qdrant’s query timeout parameter is separate and
uses server-side seconds. Cancellation uses normal BASIL task/HTTP cleanup;
errors are checked ordinary function failures, not an async/await API.
Development and verification
# Normal library and read-only example builds; no Python required:
~/src/basic/build/selfhost/stage3 build
~/src/basic/build/selfhost/stage3 build --manifest examples/basic.toml
./examples/build/qdrant-example
# Development only:
python3 generate.py
python3 tests/run.py
Generation is offline and deterministic from spec/openapi.json . Edit core.bas/builders.bas/convenience.bas, then regenerate qdrant.bas. Python is not a runtime or ordinary application-build dependency. Tests additionally use Python’s HTTP/SSL facilities, OpenSSL’s command for a temporary test certificate, and a live server. BASIC_COMPILER selects a compiler; QDRANT_URL selects the test server (default localhost:6333).
The test suite checks all named routes through source and copied .blib consumers, encoding, query/auth headers, binary/multipart bodies, error tags across package boundaries, limits, timeouts, refused redirects, and verified TLS. Live tests cover points, uint64/UUID IDs, dense/named/sparse vectors, hybrid fusion, filters, payload indexes/updates, facets, aliases, scrolling and snapshot restoration. Cluster/peer/global-admin operations are tested against a mock, not executed on your server. Live fixtures use unique temporary names and cleanup in finally; pre-existing collections are checked unchanged.
Results and compiler/source/schema fingerprints are in tests/build/report.json . The suite pins a compiler copy for each run because another development session may replace stage3 concurrently.
Schema provenance: Qdrant v1.20.0 OpenAPI , official REST reference , and official interface guide . The schema’s internal info.version is “master”; its pinned Git tag and recorded SHA256 identify the actual input. Qdrant schema license/attribution is retained under spec/ and NOTICE.txt. No server binary or native SDK is bundled.