mirror of
https://github.com/hanzoai/vector-go.git
synced 2026-10-30 18:18:35 +00:00
206 lines
6.2 KiB
Go
206 lines
6.2 KiB
Go
package qdrant
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"strconv"
|
|
"time"
|
|
|
|
"google.golang.org/grpc"
|
|
codes "google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/credentials"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
"google.golang.org/grpc/keepalive"
|
|
"google.golang.org/grpc/metadata"
|
|
status "google.golang.org/grpc/status"
|
|
)
|
|
|
|
const (
|
|
apiKeyHeader = "api-key"
|
|
defaultHost = "localhost"
|
|
defaultPort = 6334
|
|
defaultVersionCheckTimeout = time.Minute
|
|
|
|
// Hanzo ZAP-protocol default port (used by qdrant/zap_client.go).
|
|
defaultZapPort_ = 6335
|
|
)
|
|
|
|
// Configuration options for the client.
|
|
type Config struct {
|
|
// Hostname of the Qdrant server. Defaults to "localhost".
|
|
Host string
|
|
// gRPC port of the Qdrant server. Defaults to 6334.
|
|
Port int
|
|
// API key to use for authentication. Defaults to "".
|
|
APIKey string
|
|
// Whether to use TLS for the connection. Defaults to false.
|
|
UseTLS bool
|
|
// TLS configuration to use for the connection.
|
|
// If not provided, uses default config with minimum TLS version set to 1.3
|
|
TLSConfig *tls.Config
|
|
// Additional gRPC options to use for the connection.
|
|
GrpcOptions []grpc.DialOption
|
|
// Whether to check compatibility between server's version and client's. Defaults to false.
|
|
SkipCompatibilityCheck bool
|
|
// PoolSize specifies the number of connections to create.
|
|
// If 0, the default of 3 will be used.
|
|
// If 1 a single connection is used (aka no pool).
|
|
// If greater than 1, a pool of connections is created and requests are distributed in a round-robin fashion.
|
|
PoolSize uint
|
|
// KeepAliveTime specifies the duration after which if the client does not see any activity (in seconds),
|
|
// it pings the server to check if the transport is still alive.
|
|
// If 0, the default is 10 seconds.
|
|
// If set to -1, keepalive is disabled.
|
|
KeepAliveTime int
|
|
// KeepAliveTimeout specifies the duration the client waits for a response from the server after
|
|
// sending a ping (in seconds).
|
|
// If the server does not respond within this timeout, the connection is closed.
|
|
// If set to 0, defaults to 2 seconds.
|
|
// This setting is only used if keepalive is active (see KeepAliveTime).
|
|
KeepAliveTimeout uint
|
|
// RetryConfig enables automatic retries with exponential backoff for
|
|
// transient gRPC errors (ResourceExhausted, Unavailable).
|
|
// If nil, no automatic retries are performed.
|
|
RetryConfig *RetryConfig
|
|
// VersionCheckTimeout specifies the timeout used when probing the server for its
|
|
// version during client construction (compatibility check).
|
|
// If 0, defaults to 1 minute.
|
|
VersionCheckTimeout time.Duration
|
|
// Headers specifies optional headers to send with every gRPC request.
|
|
Headers map[string]string
|
|
}
|
|
|
|
// Internal method.
|
|
func (c *Config) getVersionCheckTimeout() time.Duration {
|
|
if c.VersionCheckTimeout > 0 {
|
|
return c.VersionCheckTimeout
|
|
}
|
|
return defaultVersionCheckTimeout
|
|
}
|
|
|
|
// Internal method.
|
|
func (c *Config) getAddr() string {
|
|
host := c.Host
|
|
if host == "" {
|
|
host = defaultHost
|
|
}
|
|
port := c.Port
|
|
if port == 0 {
|
|
port = defaultPort
|
|
}
|
|
return fmt.Sprintf("%s:%d", host, port)
|
|
}
|
|
|
|
// getZapAddr returns the Hanzo ZAP-protocol address string.
|
|
// ZAP is a separate wire protocol port distinct from the gRPC port.
|
|
func (c *Config) getZapAddr() string {
|
|
host := c.Host
|
|
if host == "" {
|
|
host = defaultHost
|
|
}
|
|
port := c.Port
|
|
if port == 0 {
|
|
port = defaultZapPort_
|
|
}
|
|
return fmt.Sprintf("%s:%d", host, port)
|
|
}
|
|
|
|
// Internal method.
|
|
func (c *Config) getKeepAliveParams() []grpc.DialOption {
|
|
if c.KeepAliveTime == -1 {
|
|
// Disabled
|
|
return nil
|
|
}
|
|
// Default to 10 seconds
|
|
keepAliveTime := 10
|
|
if c.KeepAliveTime > 0 {
|
|
keepAliveTime = c.KeepAliveTime
|
|
}
|
|
keepAliveTimeout := 2
|
|
if c.KeepAliveTimeout > 0 {
|
|
keepAliveTimeout = int(c.KeepAliveTimeout)
|
|
}
|
|
return []grpc.DialOption{getClientKeepAliveParams(keepAliveTime, keepAliveTimeout)}
|
|
}
|
|
|
|
// Internal method.
|
|
func (c *Config) getTransportCreds() grpc.DialOption {
|
|
if c.UseTLS {
|
|
if c.TLSConfig == nil {
|
|
return grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
|
|
MinVersion: tls.VersionTLS13,
|
|
}))
|
|
}
|
|
return grpc.WithTransportCredentials(credentials.NewTLS(c.TLSConfig))
|
|
} else if c.APIKey != "" {
|
|
slog.Default().Warn("API key is being used without TLS(HTTPS). It will be transmitted in plaintext.")
|
|
}
|
|
return grpc.WithTransportCredentials(insecure.NewCredentials())
|
|
}
|
|
|
|
// Internal method.
|
|
//
|
|
//nolint:lll
|
|
func (c *Config) getMetadataInterceptor() grpc.DialOption {
|
|
return grpc.WithUnaryInterceptor(func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
|
var kvPairs []string
|
|
if c.APIKey != "" {
|
|
kvPairs = append(kvPairs, apiKeyHeader, c.APIKey)
|
|
}
|
|
for k, v := range c.Headers {
|
|
kvPairs = append(kvPairs, k, v)
|
|
}
|
|
newCtx := metadata.AppendToOutgoingContext(ctx, kvPairs...)
|
|
return invoker(newCtx, method, req, reply, cc, opts...)
|
|
})
|
|
}
|
|
|
|
// Internal method.
|
|
func (c *Config) getRateLimitInterceptor() grpc.DialOption {
|
|
return grpc.WithChainUnaryInterceptor(func(
|
|
ctx context.Context,
|
|
method string,
|
|
req,
|
|
reply any,
|
|
cc *grpc.ClientConn,
|
|
invoker grpc.UnaryInvoker,
|
|
opts ...grpc.CallOption) error {
|
|
var md metadata.MD
|
|
opts = append(opts, grpc.Trailer(&md))
|
|
err := invoker(ctx, method, req, reply, cc, opts...)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
st, ok := status.FromError(err)
|
|
if !ok || st.Code() != codes.ResourceExhausted {
|
|
return err
|
|
}
|
|
if values := md.Get("retry-after"); len(values) > 0 {
|
|
parsed, parseErr := strconv.Atoi(values[0])
|
|
if parseErr == nil {
|
|
return &QdrantResourceExhaustedError{
|
|
st.Message(),
|
|
parsed,
|
|
}
|
|
}
|
|
return errors.Join(fmt.Errorf("parse retry-after header %q: %w", values[0], parseErr), err)
|
|
}
|
|
return err
|
|
})
|
|
}
|
|
|
|
// Internal method.
|
|
func getClientKeepAliveParams(keepAliveTime, keepAliveTimeout int) grpc.DialOption {
|
|
return grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
|
// Send pings every keepAliveTime (default 10s) if no activity
|
|
Time: time.Duration(keepAliveTime) * time.Second,
|
|
// Wait keepAliveTimeout (default 2s) for ping ack before closing
|
|
Timeout: time.Duration(keepAliveTimeout) * time.Second,
|
|
// Send pings even with no active streams
|
|
PermitWithoutStream: true,
|
|
})
|
|
}
|