Documentation/Libraries

Qdrant vector database client

On this page

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
NEXT

The 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
WEND

ScrollResult 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 TRY

QdrantError 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.

From the BASIL documentation. View source ↗

Find your way around BASIL

Type to search the manual.