package vectordb import ( "context" "fmt" "github.com/qdrant/go-client/qdrant" "agent-desk/internal/pkg/config" ) type QdrantProvider struct { client *qdrant.Client } func NewQdrantProvider(cfg *config.QdrantVectorDBConfig) (*QdrantProvider, error) { if cfg == nil { return nil, fmt.Errorf("vectordb config is nil") } host := cfg.Host if host == "" { host = "localhost" } port := cfg.GrpcPort if port <= 0 { port = 6334 } client, err := qdrant.NewClient(&qdrant.Config{ Host: host, Port: port, APIKey: cfg.APIKey, UseTLS: cfg.UseTLS, }) if err != nil { return nil, fmt.Errorf("failed to create qdrant client: %w", err) } return &QdrantProvider{client: client}, nil } func (p *QdrantProvider) Close() error { if p.client != nil { return p.client.Close() } return nil } func (p *QdrantProvider) CreateCollection(ctx context.Context, name string, dimension int) error { err := p.client.CreateCollection(ctx, &qdrant.CreateCollection{ CollectionName: name, VectorsConfig: qdrant.NewVectorsConfig(&qdrant.VectorParams{ Size: uint64(dimension), Distance: qdrant.Distance_Cosine, }), }) if err != nil { return fmt.Errorf("failed to create collection %s: %w", name, err) } return nil } func (p *QdrantProvider) DeleteCollection(ctx context.Context, name string) error { err := p.client.DeleteCollection(ctx, name) if err != nil { return fmt.Errorf("failed to delete collection %s: %w", name, err) } return nil } func (p *QdrantProvider) GetCollection(ctx context.Context, name string) (*CollectionInfo, error) { info, err := p.client.GetCollectionInfo(ctx, name) if err != nil { return nil, fmt.Errorf("failed to get collection %s: %w", name, err) } status := info.GetStatus().String() pointCount := int(info.GetPointsCount()) dimension := 0 if info.Config != nil && info.Config.Params != nil { vectorsConfig := info.Config.Params.VectorsConfig if vectorsConfig != nil { params := vectorsConfig.GetParams() if params != nil { dimension = int(params.Size) } } } return &CollectionInfo{ Name: name, Dimension: dimension, PointCount: pointCount, Status: status, }, nil } func (p *QdrantProvider) ListCollections(ctx context.Context) ([]string, error) { collections, err := p.client.ListCollections(ctx) if err != nil { return nil, fmt.Errorf("failed to list collections: %w", err) } return collections, nil } func (p *QdrantProvider) UpsertVectors(ctx context.Context, collectionName string, vectors []Vector) error { if len(vectors) == 0 { return nil } points := make([]*qdrant.PointStruct, 0, len(vectors)) for _, v := range vectors { points = append(points, &qdrant.PointStruct{ Id: qdrant.NewID(v.ID), Vectors: qdrant.NewVectors(v.Vector...), Payload: qdrant.NewValueMap(v.Payload.ToMap()), }) } _, err := p.client.Upsert(ctx, &qdrant.UpsertPoints{ CollectionName: collectionName, Points: points, }) if err != nil { return fmt.Errorf("failed to upsert vectors to collection %s: %w", collectionName, err) } return nil } func (p *QdrantProvider) DeleteVectors(ctx context.Context, collectionName string, ids []string) error { if len(ids) == 0 { return nil } pointIDs := make([]*qdrant.PointId, 0, len(ids)) for _, id := range ids { pointIDs = append(pointIDs, qdrant.NewID(id)) } _, err := p.client.Delete(ctx, &qdrant.DeletePoints{ CollectionName: collectionName, Points: &qdrant.PointsSelector{ PointsSelectorOneOf: &qdrant.PointsSelector_Points{ Points: &qdrant.PointsIdsList{ Ids: pointIDs, }, }, }, }) if err != nil { return fmt.Errorf("failed to delete vectors from collection %s: %w", collectionName, err) } return nil } func (p *QdrantProvider) Search(ctx context.Context, req *SearchRequest) ([]SearchResult, error) { filter := p.buildFilter(req.Filter) results, err := p.client.Query(ctx, &qdrant.QueryPoints{ CollectionName: req.CollectionName, Query: qdrant.NewQuery(req.Vector...), Limit: qdrant.PtrOf(uint64(req.TopK)), ScoreThreshold: &req.ScoreThreshold, Filter: filter, WithPayload: qdrant.NewWithPayload(true), }) if err != nil { return nil, fmt.Errorf("failed to search collection %s: %w", req.CollectionName, err) } searchResults := make([]SearchResult, 0, len(results)) for _, r := range results { payload := make(map[string]any) if r.Payload != nil { for k, v := range r.Payload { payload[k] = p.extractPayloadValue(v) } } id := "" if r.Id != nil { id = r.Id.GetUuid() } searchResults = append(searchResults, SearchResult{ ID: id, Score: r.Score, Payload: ChunkPayloadFromMap(payload), }) } return searchResults, nil } func (p *QdrantProvider) buildFilter(filter *SearchFilter) *qdrant.Filter { if filter == nil { return nil } must := make([]*qdrant.Condition, 0, 2) if len(filter.KnowledgeBaseIDs) > 0 { must = append(must, qdrant.NewMatchInts("knowledge_base_id", filter.KnowledgeBaseIDs...)) } if len(filter.DocumentIDs) > 0 { must = append(must, qdrant.NewMatchInts("document_id", filter.DocumentIDs...)) } if len(must) == 0 { return nil } return &qdrant.Filter{Must: must} } func (p *QdrantProvider) extractPayloadValue(v *qdrant.Value) interface{} { if v == nil { return nil } switch val := v.Kind.(type) { case *qdrant.Value_StringValue: return val.StringValue case *qdrant.Value_IntegerValue: return val.IntegerValue case *qdrant.Value_DoubleValue: return val.DoubleValue case *qdrant.Value_BoolValue: return val.BoolValue case *qdrant.Value_ListValue: list := make([]interface{}, 0, len(val.ListValue.Values)) for _, item := range val.ListValue.Values { list = append(list, p.extractPayloadValue(item)) } return list case *qdrant.Value_StructValue: m := make(map[string]interface{}) for k, v := range val.StructValue.Fields { m[k] = p.extractPayloadValue(v) } return m default: return nil } }