Documentation
¶
Overview ¶
Package qdrant is a focused HTTP client for the Qdrant vector database (https://qdrant.tech).
Unlike drops/pg and drops/clickhouse, Qdrant doesn't speak SQL — it has a JSON API for collection management and vector search. This package exposes that API as plain Go types and methods, with no dependency on the upstream Go SDK (net/http + encoding/json only).
Quick start:
cli, _ := qdrant.NewClient("http://localhost:6333",
qdrant.WithAPIKey(os.Getenv("QDRANT_API_KEY")))
_ = cli.CreateCollection(ctx, "embeddings", qdrant.CollectionConfig{
Vectors: qdrant.VectorParams{Size: 384, Distance: qdrant.DistanceCosine},
})
_ = cli.Upsert(ctx, "embeddings", []qdrant.Point{
{ID: "doc-1", Vector: vec1, Payload: map[string]any{"topic": "go"}},
{ID: "doc-2", Vector: vec2, Payload: map[string]any{"topic": "rust"}},
})
hits, _ := cli.Search(ctx, "embeddings", qdrant.SearchRequest{
Vector: query,
Limit: 10,
Filter: qdrant.Must(qdrant.Eq("topic", "go")),
WithPayload: true,
})
Surface (current):
- Collection ops: CreateCollection, DeleteCollection, CollectionExists, CollectionInfo, ListCollections
- Point ops: Upsert, Delete, Retrieve, Count
- Search ops: Search, Scroll, Recommend
- Filter DSL: Must / Should / MustNot blocks with Eq / In / Range / IsEmpty / HasID / GeoBoundingBox conditions
Out of scope (drop down to raw HTTP via Do for these): gRPC, snapshots, sharding management, payload indexes, cluster topology.
Index ¶
- Variables
- func F(v float64) *float64
- type Client
- func (c *Client) BaseURL() string
- func (c *Client) CollectionExists(ctx context.Context, name string) (bool, error)
- func (c *Client) CollectionInfo(ctx context.Context, name string) (*CollectionInfo, error)
- func (c *Client) Count(ctx context.Context, collection string, f *Filter, exact bool) (int64, error)
- func (c *Client) CreateCollection(ctx context.Context, name string, cfg CollectionConfig) error
- func (c *Client) DeleteByFilter(ctx context.Context, collection string, f *Filter, opts ...WriteOptions) error
- func (c *Client) DeleteByIDs(ctx context.Context, collection string, ids []any, opts ...WriteOptions) error
- func (c *Client) DeleteCollection(ctx context.Context, name string) error
- func (c *Client) Do(ctx context.Context, method, path string, body, out any) (err error)
- func (c *Client) HTTPClient() *http.Client
- func (c *Client) Hook() drops.Hook
- func (c *Client) ListCollections(ctx context.Context) ([]string, error)
- func (c *Client) Ping(ctx context.Context) error
- func (c *Client) Recommend(ctx context.Context, collection string, req RecommendRequest) ([]Hit, error)
- func (c *Client) Retrieve(ctx context.Context, collection string, ids []any, opts ...RetrieveOptions) ([]Point, error)
- func (c *Client) Scroll(ctx context.Context, collection string, req ScrollRequest) (*ScrollPage, error)
- func (c *Client) Search(ctx context.Context, collection string, req SearchRequest) ([]Hit, error)
- func (c *Client) Store(collection string, opts ...StoreOption) *Store
- func (c *Client) Upsert(ctx context.Context, collection string, points []Point, opts ...WriteOptions) error
- func (c *Client) WithHookFn(hook drops.Hook) *Client
- type ClientOption
- type CollectionConfig
- type CollectionInfo
- type Condition
- func Eq(field string, value any) Condition
- func GeoIn(field string, topLeft, bottomRight GeoPoint) Condition
- func HasID(ids ...any) Condition
- func In(field string, values ...any) Condition
- func IsEmpty(field string) Condition
- func IsNull(field string) Condition
- func MatchText(field, text string) Condition
- func Nest(f *Filter) Condition
- func NotIn(field string, values ...any) Condition
- func Range(field string, opts RangeOpts) Condition
- type Distance
- type Filter
- type GeoBoundingBox
- type GeoPoint
- type HNSWConfig
- type HTTPError
- type Hit
- type IsEmptyCondition
- type IsNullCondition
- type MatchCondition
- type Point
- type RangeCondition
- type RangeOpts
- type RecommendRequest
- type RetrieveOptions
- type ScrollPage
- type ScrollRequest
- type SearchRequest
- type Store
- type StoreOption
- type VectorParams
- type WriteOptions
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var All = Must
All / Any are aliases that read naturally at call sites.
var Any = Should
var ErrCollectionMissing = errors.New("qdrant: collection not found")
ErrCollectionMissing is returned when a 404 references a missing collection. Use errors.Is to branch on it.
var ErrInvalidIdentifier = errors.New("qdrant: invalid identifier")
ErrInvalidIdentifier is returned when a collection / vector name fails validation. errors.Is(err, ErrInvalidIdentifier) is true for every wrapped instance.
var ErrNoPoints = errors.New("qdrant: no points supplied")
ErrNoPoints is returned by Upsert / DeleteByIDs when called with an empty batch.
Functions ¶
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client wraps Qdrant's HTTP API.
Safe for concurrent use by multiple goroutines. The optional Hook fires after every HTTP request with method + path + duration + error so the same observability story (drops.LoggerHook, OTel adapter, metrics emitter) works against Qdrant just like it does against pg and clickhouse.
func NewClient ¶
func NewClient(baseURL string, opts ...ClientOption) (*Client, error)
NewClient returns a Client for the supplied Qdrant base URL — e.g. "http://localhost:6333" or "https://my-cluster.eu.cloud.qdrant.io". Trailing slashes are trimmed.
Example ¶
ExampleNewClient shows the minimal happy-path: connect, create a collection, upsert a point, search.
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"github.com/bernardoforcillo/drops/qdrant"
)
// exampleServer returns an httptest.Server that mimics a tiny subset
// of the Qdrant API so the example tests stay self-contained and
// deterministic (no real Qdrant instance required).
func exampleServer() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
respond := func(result any) {
_ = json.NewEncoder(w).Encode(map[string]any{
"result": result, "status": "ok", "time": 0.0,
})
}
switch {
case r.Method == http.MethodPut && r.URL.Path == "/collections/docs":
respond(true)
case r.Method == http.MethodPut && r.URL.Path == "/collections/docs/points":
respond(map[string]any{"operation_id": 1, "status": "completed"})
case r.Method == http.MethodPost && r.URL.Path == "/collections/docs/points/search":
respond([]map[string]any{
{"id": "doc-1", "score": 0.93, "payload": map[string]any{"topic": "go"}},
})
default:
http.NotFound(w, r)
}
}))
}
func main() {
srv := exampleServer()
defer srv.Close()
cli, _ := qdrant.NewClient(srv.URL)
ctx := context.Background()
if err := cli.CreateCollection(ctx, "docs", qdrant.CollectionConfig{
Vectors: qdrant.VectorParams{Size: 3, Distance: qdrant.DistanceCosine},
}); err != nil {
fmt.Println("create:", err)
return
}
if err := cli.Upsert(ctx, "docs", []qdrant.Point{
{ID: "doc-1", Vector: []float32{1, 0, 0},
Payload: map[string]any{"topic": "go"}},
}); err != nil {
fmt.Println("upsert:", err)
return
}
hits, _ := cli.Search(ctx, "docs", qdrant.SearchRequest{
Vector: []float32{1, 0, 0},
Limit: 1,
WithPayload: true,
})
fmt.Printf("found %d hit(s): %v at %.2f\n",
len(hits), hits[0].ID, hits[0].Score)
}
Output: found 1 hit(s): doc-1 at 0.93
func (*Client) CollectionExists ¶
CollectionExists reports whether the collection exists.
func (*Client) CollectionInfo ¶
CollectionInfo returns metadata about a collection.
func (*Client) Count ¶
func (c *Client) Count(ctx context.Context, collection string, f *Filter, exact bool) (int64, error)
Count returns the number of points matching an optional filter. Pass nil for a full count.
func (*Client) CreateCollection ¶
CreateCollection creates a new collection. Returns nil if the operation succeeds (Qdrant responds with `{"result": true}`).
func (*Client) DeleteByFilter ¶
func (c *Client) DeleteByFilter(ctx context.Context, collection string, f *Filter, opts ...WriteOptions) error
DeleteByFilter deletes every point that matches the filter.
func (*Client) DeleteByIDs ¶
func (c *Client) DeleteByIDs(ctx context.Context, collection string, ids []any, opts ...WriteOptions) error
DeleteByIDs deletes the listed points.
func (*Client) DeleteCollection ¶
DeleteCollection drops a collection. Idempotent on the server side.
func (*Client) Do ¶
Do issues a request to <base>+path with optional JSON body and decodes the response into out (if non-nil). The Qdrant convention is `{"result": ..., "status": "ok", "time": float}`; out is decoded against the result field.
Every Do call fires the observability hook (if installed) exactly once after the request completes, with Kind="http", SQL set to "<METHOD> <path>", the elapsed duration, and any error.
Callers usually don't need Do; the typed methods (CreateCollection, Upsert, Search, …) wrap it.
func (*Client) HTTPClient ¶
HTTPClient returns the underlying http.Client. Useful for tests that want to swap out a transport.
func (*Client) ListCollections ¶
ListCollections returns every collection's name.
func (*Client) Ping ¶
Ping issues a request to /healthz to confirm the Qdrant instance is reachable and willing to serve traffic. Suitable as a Kubernetes readiness probe shape.
func (*Client) Recommend ¶
func (c *Client) Recommend(ctx context.Context, collection string, req RecommendRequest) ([]Hit, error)
Recommend returns points similar to the positive examples and dissimilar to the negative examples.
func (*Client) Retrieve ¶
func (c *Client) Retrieve(ctx context.Context, collection string, ids []any, opts ...RetrieveOptions) ([]Point, error)
Retrieve fetches the points with the given IDs.
func (*Client) Scroll ¶
func (c *Client) Scroll(ctx context.Context, collection string, req ScrollRequest) (*ScrollPage, error)
Scroll iterates through a collection in deterministic order, optionally filtered. Pass the previous response's NextPageOffset as the next call's Offset until it comes back nil.
func (*Client) Store ¶ added in v0.6.0
func (c *Client) Store(collection string, opts ...StoreOption) *Store
Store returns a portable vector.Store view of a collection.
store := cli.Store("embeddings", qdrant.WithMetric(vector.Cosine))
res, err := store.Search(ctx, q)
type ClientOption ¶
type ClientOption func(*Client)
ClientOption configures a Client.
func WithAPIKey ¶
func WithAPIKey(key string) ClientOption
WithAPIKey sets the Authorization header sent with every request. Qdrant Cloud expects the api-key header; the package wires both `api-key` and `Authorization: Bearer` so either deployment style works without further config.
func WithHTTPClient ¶
func WithHTTPClient(h *http.Client) ClientOption
WithHTTPClient overrides the default http.Client (useful for custom transports, timeouts, instrumentation).
func WithHook ¶
func WithHook(h drops.Hook) ClientOption
WithHook installs an observability hook that fires after every HTTP request the client makes. The same drops.Hook contract used by pg.DB and clickhouse.DB — compose with drops.ChainHooks and pair with drops.LoggerHook for instant request logging.
func WithTimeout ¶
func WithTimeout(d time.Duration) ClientOption
WithTimeout sets the request timeout on the underlying http.Client. Ignored if WithHTTPClient was used.
type CollectionConfig ¶
type CollectionConfig struct {
Vectors VectorParams `json:"vectors"`
ShardNumber *int `json:"shard_number,omitempty"`
ReplicationFactor *int `json:"replication_factor,omitempty"`
WriteConsistency *int `json:"write_consistency_factor,omitempty"`
OnDiskPayload *bool `json:"on_disk_payload,omitempty"`
HNSW *HNSWConfig `json:"hnsw_config,omitempty"`
}
CollectionConfig is the payload for CreateCollection.
type CollectionInfo ¶
type CollectionInfo struct {
Status string `json:"status"`
VectorsCount int `json:"vectors_count"`
PointsCount int `json:"points_count"`
SegmentsCount int `json:"segments_count"`
Config map[string]any `json:"config"`
}
CollectionInfo is the response from GET /collections/{name}.
type Condition ¶
type Condition struct {
Key string `json:"key,omitempty"`
Match *MatchCondition `json:"match,omitempty"`
Range *RangeCondition `json:"range,omitempty"`
HasID []any `json:"has_id,omitempty"`
IsEmpty *IsEmptyCondition `json:"is_empty,omitempty"`
IsNull *IsNullCondition `json:"is_null,omitempty"`
Geo *GeoBoundingBox `json:"geo_bounding_box,omitempty"`
Nested *Filter `json:"filter,omitempty"`
}
Condition is a single filter clause — either a key/value test, a range, an ID membership test, or a nested Filter.
func Nest ¶
Nest wraps a Filter as a Condition so it can sit inside another block — useful for OR-of-ANDs and similar shapes.
type Distance ¶
type Distance string
Distance enumerates the supported similarity metrics. The values are the exact strings Qdrant expects in JSON.
func MetricDistance ¶ added in v0.6.0
MetricDistance maps a portable vector.Metric onto the Qdrant Distance constant that names the same function. The second result is false for metrics Qdrant has no equivalent for (Hamming, Jaccard).
type Filter ¶
type Filter struct {
Must []Condition `json:"must,omitempty"`
Should []Condition `json:"should,omitempty"`
MustNot []Condition `json:"must_not,omitempty"`
}
Filter is Qdrant's predicate object: a tree of Must / Should / MustNot blocks containing Condition leaves.
func CompileFilter ¶ added in v0.6.0
CompileFilter turns a portable vector.Filter into the Qdrant filter object. The zero Filter compiles to nil — no filter at all rather than an empty one, which Qdrant would reject.
Exported because it is useful on its own: it lets code that already builds portable filters drive the Client's own Scroll, Recommend and DeleteByFilter methods.
func Must ¶
Must builds a Filter where every condition is required.
Example ¶
ExampleMust shows the filter DSL: AND-of conditions on a payload.
package main
import (
"encoding/json"
"fmt"
"github.com/bernardoforcillo/drops/qdrant"
)
func main() {
filter := qdrant.Must(
qdrant.Eq("topic", "go"),
qdrant.Range("created_at", qdrant.RangeOpts{Gte: qdrant.F(1700000000)}),
)
body, _ := json.Marshal(filter)
fmt.Println(string(body))
}
Output: {"must":[{"key":"topic","match":{"value":"go"}},{"key":"created_at","range":{"gte":1700000000}}]}
type GeoBoundingBox ¶
type GeoBoundingBox struct {
TopLeft GeoPoint `json:"top_left"`
BottomRight GeoPoint `json:"bottom_right"`
}
GeoBoundingBox is the rectangle-shape geo filter.
type HNSWConfig ¶
type HNSWConfig struct {
M *int `json:"m,omitempty"`
EFConstruct *int `json:"ef_construct,omitempty"`
FullScanThreshold *int `json:"full_scan_threshold,omitempty"`
OnDisk *bool `json:"on_disk,omitempty"`
}
HNSWConfig is the HNSW-index tuning bag. Pointer fields let zero- valued integers reach Qdrant as "not set" rather than 0.
type HTTPError ¶
HTTPError is returned for non-2xx responses. The body is captured so callers can extract Qdrant's error payload when needed.
type Hit ¶
type Hit struct {
ID any `json:"id"`
Score float32 `json:"score"`
Vector []float32 `json:"vector,omitempty"`
Payload map[string]any `json:"payload,omitempty"`
}
Hit is one search result.
type IsEmptyCondition ¶
type IsEmptyCondition struct {
Key string `json:"key"`
}
IsEmptyCondition / IsNullCondition test for missing / null payloads.
type IsNullCondition ¶
type IsNullCondition struct {
Key string `json:"key"`
}
type MatchCondition ¶
type MatchCondition struct {
Value any `json:"value,omitempty"`
Any []any `json:"any,omitempty"`
Except []any `json:"except,omitempty"`
Text string `json:"text,omitempty"`
}
MatchCondition expresses payload value equality / set membership / text-search. Exactly one of Value / Any / Text / Except should be non-zero.
type Point ¶
type Point struct {
ID any `json:"id"`
Vector []float32 `json:"vector"`
Payload map[string]any `json:"payload,omitempty"`
}
Point is a single vector + payload + ID record.
ID may be an int (uint64) or a string (UUID); Qdrant accepts either and the package passes the value through to JSON as-is. Vector is the dense embedding; for named-vector collections use NamedVectors instead via a custom payload type.
type RangeCondition ¶
type RangeCondition struct {
Lt *float64 `json:"lt,omitempty"`
Lte *float64 `json:"lte,omitempty"`
Gt *float64 `json:"gt,omitempty"`
Gte *float64 `json:"gte,omitempty"`
}
RangeCondition expresses numeric range tests. All bounds are optional; nil-pointer means "open end".
type RecommendRequest ¶
type RecommendRequest struct {
Positive []any `json:"positive,omitempty"`
Negative []any `json:"negative,omitempty"`
Limit int `json:"limit"`
Offset int `json:"offset,omitempty"`
Filter *Filter `json:"filter,omitempty"`
ScoreThreshold *float32 `json:"score_threshold,omitempty"`
WithVector bool `json:"with_vector,omitempty"`
WithPayload bool `json:"with_payload,omitempty"`
Params map[string]any `json:"params,omitempty"`
Strategy string `json:"strategy,omitempty"` // "average_vector" | "best_score"
}
RecommendRequest is the body for /collections/{name}/points/recommend.
type RetrieveOptions ¶
RetrieveOptions configures Retrieve.
type ScrollPage ¶
type ScrollPage struct {
Points []Point `json:"points"`
NextPageOffset any `json:"next_page_offset"`
}
ScrollPage is the response from Scroll.
type ScrollRequest ¶
type ScrollRequest struct {
Filter *Filter `json:"filter,omitempty"`
Limit int `json:"limit,omitempty"`
Offset any `json:"offset,omitempty"` // page cursor; pass last page's NextPageOffset
WithVector bool `json:"with_vector,omitempty"`
WithPayload bool `json:"with_payload,omitempty"`
}
ScrollRequest is the body for /collections/{name}/points/scroll.
type SearchRequest ¶
type SearchRequest struct {
Vector []float32 `json:"vector"`
Limit int `json:"limit"`
Offset int `json:"offset,omitempty"`
Filter *Filter `json:"filter,omitempty"`
ScoreThreshold *float32 `json:"score_threshold,omitempty"`
WithVector bool `json:"with_vector,omitempty"`
WithPayload bool `json:"with_payload,omitempty"`
Params map[string]any `json:"params,omitempty"` // hnsw_ef, exact, etc.
}
SearchRequest is the body for /collections/{name}/points/search.
type Store ¶ added in v0.6.0
type Store struct {
// contains filtered or unexported fields
}
Store adapts a Qdrant collection to github.com/bernardoforcillo/drops/vector.Store, so the same portable query that runs against a pgvector table runs here.
Build one with Client.Store. The Client's own typed methods (Search, Scroll, Recommend) stay available and lose nothing — Store is a second, portable door onto the same collection, not a replacement for the first.
func (*Store) Collection ¶ added in v0.6.0
Collection returns the collection name the store reads from.
func (*Store) Search ¶ added in v0.6.0
Search implements vector.Store.
Pagination is by offset, because Qdrant's search endpoint offers no keyset: the cursor carries the number of hits to skip. The adapter asks for TopK+1 hits and trims, so HasMore is exact and costs no second request.
type StoreOption ¶ added in v0.6.0
type StoreOption func(*Store)
StoreOption configures a Store.
func WithMetric ¶ added in v0.6.0
func WithMetric(m vector.Metric) StoreOption
WithMetric declares the distance function the collection was created with.
This matters more than it looks. Qdrant fixes the metric at collection-creation time and a search cannot override it, so the Metric on a vector.Query is not a request — it is an assertion about the collection. Store checks the two agree and returns vector.ErrUnsupportedMetric when they don't, rather than ranking by cosine while the caller believes it asked for L2.
It is also what tells the adapter how to read Qdrant's score, whose meaning changes with the metric: a similarity for Cosine and Dot, a raw distance for Euclid and Manhattan.
Defaults to vector.Cosine, matching Qdrant's most common setup.
type VectorParams ¶
type VectorParams struct {
Size int `json:"size"`
Distance Distance `json:"distance"`
// OnDisk stores the vector data on disk instead of in memory.
OnDisk *bool `json:"on_disk,omitempty"`
}
VectorParams configures a single named vector or the (default) unnamed vector of a collection.
type WriteOptions ¶
type WriteOptions struct {
Wait bool
// Ordering hint; "weak" (default), "medium", "strong".
Ordering string
}
WriteOptions tune the consistency of write operations. The default (zero value) is "fire and forget" — return as soon as Qdrant has queued the operation. Wait=true blocks until the data is durable in the local segments.