chore: import clean Agent Desk snapshot (2/7)

This commit is contained in:
2026-10-16 11:57:21 +00:00
parent 79176541c7
commit 89c5231c4c
461 changed files with 60092 additions and 0 deletions

View File

@@ -0,0 +1,489 @@
//go:build lancedb
package vectordb
import (
"context"
"fmt"
"math"
"os"
"strconv"
"strings"
"agent-desk/internal/pkg/config"
"github.com/apache/arrow/go/v17/arrow"
"github.com/apache/arrow/go/v17/arrow/array"
"github.com/apache/arrow/go/v17/arrow/memory"
"github.com/lancedb/lancedb-go/pkg/contracts"
"github.com/lancedb/lancedb-go/pkg/lancedb"
)
const lanceDBVectorColumn = "vector"
type LanceDBProvider struct {
conn contracts.IConnection
}
func NewLanceDBProvider(cfg *config.LanceDBVectorDBConfig) (Provider, error) {
if cfg == nil {
return nil, fmt.Errorf("lancedb config is nil")
}
path := strings.TrimSpace(cfg.Path)
if path == "" {
path = "data/lancedb"
}
if err := os.MkdirAll(path, 0o755); err != nil {
return nil, fmt.Errorf("failed to create lancedb directory %s: %w", path, err)
}
conn, err := lancedb.Connect(context.Background(), path, nil)
if err != nil {
return nil, err
}
return &LanceDBProvider{conn: conn}, nil
}
func (p *LanceDBProvider) Close() error {
if p.conn == nil || p.conn.IsClosed() {
return nil
}
return p.conn.Close()
}
func (p *LanceDBProvider) CreateCollection(ctx context.Context, name string, dimension int) error {
if dimension <= 0 {
return fmt.Errorf("invalid lancedb vector dimension: %d", dimension)
}
if err := p.ensureOpen(); err != nil {
return err
}
schema, err := newLanceDBSchema(dimension)
if err != nil {
return err
}
table, err := p.conn.CreateTable(ctx, name, schema)
if err != nil {
return fmt.Errorf("failed to create lancedb table %s: %w", name, err)
}
return table.Close()
}
func (p *LanceDBProvider) DeleteCollection(ctx context.Context, name string) error {
if err := p.ensureOpen(); err != nil {
return err
}
if err := p.conn.DropTable(ctx, name); err != nil {
return fmt.Errorf("failed to delete lancedb table %s: %w", name, err)
}
return nil
}
func (p *LanceDBProvider) GetCollection(ctx context.Context, name string) (*CollectionInfo, error) {
table, err := p.openTable(ctx, name)
if err != nil {
return nil, err
}
defer table.Close()
schema, err := table.Schema(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get lancedb table schema %s: %w", name, err)
}
count, err := table.Count(ctx)
if err != nil {
return nil, fmt.Errorf("failed to count lancedb table %s: %w", name, err)
}
return &CollectionInfo{
Name: name,
Dimension: lanceDBVectorDimension(schema),
PointCount: int(count),
Status: "ok",
}, nil
}
func (p *LanceDBProvider) ListCollections(ctx context.Context) ([]string, error) {
if err := p.ensureOpen(); err != nil {
return nil, err
}
names, err := p.conn.TableNames(ctx)
if err != nil {
return nil, fmt.Errorf("failed to list lancedb tables: %w", err)
}
return names, nil
}
func (p *LanceDBProvider) UpsertVectors(ctx context.Context, collectionName string, vectors []Vector) error {
if len(vectors) == 0 {
return nil
}
table, err := p.openTable(ctx, collectionName)
if err != nil {
return err
}
defer table.Close()
ids := make([]string, 0, len(vectors))
for _, vector := range vectors {
if strings.TrimSpace(vector.ID) != "" {
ids = append(ids, vector.ID)
}
}
if len(ids) > 0 {
if err := table.Delete(ctx, lanceDBStringInFilter("id", ids)); err != nil {
return fmt.Errorf("failed to delete existing lancedb vectors from %s: %w", collectionName, err)
}
}
record, release, err := newLanceDBVectorRecord(vectors)
if err != nil {
return err
}
defer release()
if err := table.AddRecords(ctx, []arrow.Record{record}, nil); err != nil {
return fmt.Errorf("failed to add lancedb vectors to %s: %w", collectionName, err)
}
return nil
}
func (p *LanceDBProvider) DeleteVectors(ctx context.Context, collectionName string, ids []string) error {
if len(ids) == 0 {
return nil
}
table, err := p.openTable(ctx, collectionName)
if err != nil {
return err
}
defer table.Close()
if err := table.Delete(ctx, lanceDBStringInFilter("id", ids)); err != nil {
return fmt.Errorf("failed to delete lancedb vectors from %s: %w", collectionName, err)
}
return nil
}
func (p *LanceDBProvider) Search(ctx context.Context, req *SearchRequest) ([]SearchResult, error) {
table, err := p.openTable(ctx, req.CollectionName)
if err != nil {
return nil, err
}
defer table.Close()
filter := lanceDBSearchFilter(req.Filter)
var rows []map[string]interface{}
if filter == "" {
rows, err = table.VectorSearch(ctx, lanceDBVectorColumn, req.Vector, req.TopK)
} else {
rows, err = table.VectorSearchWithFilter(ctx, lanceDBVectorColumn, req.Vector, req.TopK, filter)
}
if err != nil {
return nil, fmt.Errorf("failed to search lancedb table %s: %w", req.CollectionName, err)
}
results := make([]SearchResult, 0, len(rows))
for _, row := range rows {
score := lanceDBScoreFromRow(row)
if req.ScoreThreshold > 0 && score < req.ScoreThreshold {
continue
}
results = append(results, SearchResult{
ID: valueToString(row["id"]),
Score: score,
Payload: lanceDBPayloadFromRow(row),
})
}
return results, nil
}
func (p *LanceDBProvider) ensureOpen() error {
if p == nil || p.conn == nil || p.conn.IsClosed() {
return fmt.Errorf("lancedb provider is closed")
}
return nil
}
func (p *LanceDBProvider) openTable(ctx context.Context, name string) (contracts.ITable, error) {
if err := p.ensureOpen(); err != nil {
return nil, err
}
table, err := p.conn.OpenTable(ctx, name)
if err != nil {
return nil, fmt.Errorf("failed to open lancedb table %s: %w", name, err)
}
return table, nil
}
func newLanceDBSchema(dimension int) (contracts.ISchema, error) {
schema := arrow.NewSchema([]arrow.Field{
{Name: "id", Type: arrow.BinaryTypes.String, Nullable: false},
{Name: lanceDBVectorColumn, Type: arrow.FixedSizeListOf(int32(dimension), arrow.PrimitiveTypes.Float32), Nullable: false},
{Name: "knowledge_base_id", Type: arrow.PrimitiveTypes.Int64, Nullable: false},
{Name: "document_id", Type: arrow.PrimitiveTypes.Int64, Nullable: false},
{Name: "document_title", Type: arrow.BinaryTypes.String, Nullable: true},
{Name: "faq_id", Type: arrow.PrimitiveTypes.Int64, Nullable: false},
{Name: "faq_question", Type: arrow.BinaryTypes.String, Nullable: true},
{Name: "chunk_no", Type: arrow.PrimitiveTypes.Int32, Nullable: false},
{Name: "chunk_type", Type: arrow.BinaryTypes.String, Nullable: true},
{Name: "section_path", Type: arrow.BinaryTypes.String, Nullable: true},
{Name: "title", Type: arrow.BinaryTypes.String, Nullable: true},
{Name: "content", Type: arrow.BinaryTypes.String, Nullable: true},
{Name: "provider", Type: arrow.BinaryTypes.String, Nullable: true},
}, nil)
return lancedb.NewSchema(schema)
}
func newLanceDBVectorRecord(vectors []Vector) (arrow.Record, func(), error) {
dimension := 0
for _, item := range vectors {
if len(item.Vector) > 0 {
dimension = len(item.Vector)
break
}
}
if dimension <= 0 {
return nil, nil, fmt.Errorf("lancedb vector dimension is empty")
}
for _, item := range vectors {
if len(item.Vector) != dimension {
return nil, nil, fmt.Errorf("inconsistent lancedb vector dimension for %s: got %d, want %d", item.ID, len(item.Vector), dimension)
}
}
pool := memory.NewGoAllocator()
idBuilder := array.NewStringBuilder(pool)
kbIDBuilder := array.NewInt64Builder(pool)
documentIDBuilder := array.NewInt64Builder(pool)
documentTitleBuilder := array.NewStringBuilder(pool)
faqIDBuilder := array.NewInt64Builder(pool)
faqQuestionBuilder := array.NewStringBuilder(pool)
chunkNoBuilder := array.NewInt32Builder(pool)
chunkTypeBuilder := array.NewStringBuilder(pool)
sectionPathBuilder := array.NewStringBuilder(pool)
titleBuilder := array.NewStringBuilder(pool)
contentBuilder := array.NewStringBuilder(pool)
providerBuilder := array.NewStringBuilder(pool)
vectorBuilder := array.NewFloat32Builder(pool)
for _, item := range vectors {
payload := item.Payload
idBuilder.Append(item.ID)
vectorBuilder.AppendValues(item.Vector, nil)
kbIDBuilder.Append(payload.KnowledgeBaseID)
documentIDBuilder.Append(payload.DocumentID)
documentTitleBuilder.Append(payload.DocumentTitle)
faqIDBuilder.Append(payload.FaqID)
faqQuestionBuilder.Append(payload.FaqQuestion)
chunkNoBuilder.Append(int32(payload.ChunkNo))
chunkTypeBuilder.Append(payload.ChunkType)
sectionPathBuilder.Append(payload.SectionPath)
titleBuilder.Append(payload.Title)
contentBuilder.Append(payload.Content)
providerBuilder.Append(payload.Provider)
}
idArray := idBuilder.NewArray()
vectorValues := vectorBuilder.NewArray()
kbIDArray := kbIDBuilder.NewArray()
documentIDArray := documentIDBuilder.NewArray()
documentTitleArray := documentTitleBuilder.NewArray()
faqIDArray := faqIDBuilder.NewArray()
faqQuestionArray := faqQuestionBuilder.NewArray()
chunkNoArray := chunkNoBuilder.NewArray()
chunkTypeArray := chunkTypeBuilder.NewArray()
sectionPathArray := sectionPathBuilder.NewArray()
titleArray := titleBuilder.NewArray()
contentArray := contentBuilder.NewArray()
providerArray := providerBuilder.NewArray()
vectorType := arrow.FixedSizeListOf(int32(dimension), arrow.PrimitiveTypes.Float32)
vectorArray := array.NewFixedSizeListData(
array.NewData(vectorType, len(vectors), []*memory.Buffer{nil}, []arrow.ArrayData{vectorValues.Data()}, 0, 0),
)
schema := arrow.NewSchema([]arrow.Field{
{Name: "id", Type: arrow.BinaryTypes.String, Nullable: false},
{Name: lanceDBVectorColumn, Type: vectorType, Nullable: false},
{Name: "knowledge_base_id", Type: arrow.PrimitiveTypes.Int64, Nullable: false},
{Name: "document_id", Type: arrow.PrimitiveTypes.Int64, Nullable: false},
{Name: "document_title", Type: arrow.BinaryTypes.String, Nullable: true},
{Name: "faq_id", Type: arrow.PrimitiveTypes.Int64, Nullable: false},
{Name: "faq_question", Type: arrow.BinaryTypes.String, Nullable: true},
{Name: "chunk_no", Type: arrow.PrimitiveTypes.Int32, Nullable: false},
{Name: "chunk_type", Type: arrow.BinaryTypes.String, Nullable: true},
{Name: "section_path", Type: arrow.BinaryTypes.String, Nullable: true},
{Name: "title", Type: arrow.BinaryTypes.String, Nullable: true},
{Name: "content", Type: arrow.BinaryTypes.String, Nullable: true},
{Name: "provider", Type: arrow.BinaryTypes.String, Nullable: true},
}, nil)
columns := []arrow.Array{
idArray,
vectorArray,
kbIDArray,
documentIDArray,
documentTitleArray,
faqIDArray,
faqQuestionArray,
chunkNoArray,
chunkTypeArray,
sectionPathArray,
titleArray,
contentArray,
providerArray,
}
record := array.NewRecord(schema, columns, int64(len(vectors)))
release := func() {
record.Release()
for _, column := range columns {
column.Release()
}
vectorValues.Release()
}
return record, release, nil
}
func lanceDBVectorDimension(schema *arrow.Schema) int {
if schema == nil {
return 0
}
for i := 0; i < schema.NumFields(); i++ {
field := schema.Field(i)
if field.Name != lanceDBVectorColumn {
continue
}
listType, ok := field.Type.(*arrow.FixedSizeListType)
if !ok {
return 0
}
return int(listType.Len())
}
return 0
}
func lanceDBSearchFilter(filter *SearchFilter) string {
if filter == nil {
return ""
}
parts := make([]string, 0, 2)
if len(filter.KnowledgeBaseIDs) > 0 {
parts = append(parts, lanceDBIntInFilter("knowledge_base_id", filter.KnowledgeBaseIDs))
}
if len(filter.DocumentIDs) > 0 {
parts = append(parts, lanceDBIntInFilter("document_id", filter.DocumentIDs))
}
return strings.Join(parts, " AND ")
}
func lanceDBIntInFilter(column string, values []int64) string {
items := make([]string, 0, len(values))
for _, value := range values {
items = append(items, strconv.FormatInt(value, 10))
}
return fmt.Sprintf("%s IN (%s)", column, strings.Join(items, ","))
}
func lanceDBStringInFilter(column string, values []string) string {
items := make([]string, 0, len(values))
for _, value := range values {
items = append(items, "'"+strings.ReplaceAll(value, "'", "''")+"'")
}
return fmt.Sprintf("%s IN (%s)", column, strings.Join(items, ","))
}
func lanceDBScoreFromRow(row map[string]interface{}) float32 {
for _, key := range []string{"_distance", "distance"} {
if value, ok := row[key]; ok {
distance := valueToFloat64(value)
if math.IsNaN(distance) {
break
}
score := 1 - distance
if score < 0 {
return 0
}
if score > 1 {
return 1
}
return float32(score)
}
}
for _, key := range []string{"_score", "score"} {
if value, ok := row[key]; ok {
score := valueToFloat64(value)
if !math.IsNaN(score) {
return float32(score)
}
}
}
return 0
}
func lanceDBPayloadFromRow(row map[string]interface{}) ChunkPayload {
return ChunkPayload{
KnowledgeBaseID: valueToInt64(row["knowledge_base_id"]),
DocumentID: valueToInt64(row["document_id"]),
DocumentTitle: valueToString(row["document_title"]),
FaqID: valueToInt64(row["faq_id"]),
FaqQuestion: valueToString(row["faq_question"]),
ChunkNo: int(valueToInt64(row["chunk_no"])),
ChunkType: valueToString(row["chunk_type"]),
SectionPath: valueToString(row["section_path"]),
Title: valueToString(row["title"]),
Content: valueToString(row["content"]),
Provider: valueToString(row["provider"]),
}
}
func valueToString(value interface{}) string {
switch v := value.(type) {
case nil:
return ""
case string:
return v
case []byte:
return string(v)
default:
return fmt.Sprint(value)
}
}
func valueToInt64(value interface{}) int64 {
switch v := value.(type) {
case int:
return int64(v)
case int32:
return int64(v)
case int64:
return v
case uint64:
return int64(v)
case float32:
return int64(v)
case float64:
return int64(v)
case string:
ret, _ := strconv.ParseInt(v, 10, 64)
return ret
default:
return 0
}
}
func valueToFloat64(value interface{}) float64 {
switch v := value.(type) {
case float32:
return float64(v)
case float64:
return v
case int:
return float64(v)
case int32:
return float64(v)
case int64:
return float64(v)
case string:
ret, err := strconv.ParseFloat(v, 64)
if err == nil {
return ret
}
}
return math.NaN()
}

View File

@@ -0,0 +1,13 @@
//go:build !lancedb
package vectordb
import (
"fmt"
"agent-desk/internal/pkg/config"
)
func NewLanceDBProvider(_ *config.LanceDBVectorDBConfig) (Provider, error) {
return nil, fmt.Errorf("LanceDB provider is not built. Rebuild with -tags lancedb and configure LanceDB native libraries")
}

View File

@@ -0,0 +1,102 @@
//go:build lancedb
package vectordb
import (
"context"
"testing"
"agent-desk/internal/pkg/config"
)
func TestLanceDBProviderVectorLifecycle(t *testing.T) {
ctx := context.Background()
provider, err := NewLanceDBProvider(&config.LanceDBVectorDBConfig{Path: t.TempDir()})
if err != nil {
t.Fatalf("NewLanceDBProvider() error = %v", err)
}
defer provider.Close()
const collectionName = "knowledge_chunks"
if err := provider.CreateCollection(ctx, collectionName, 3); err != nil {
t.Fatalf("CreateCollection() error = %v", err)
}
vectors := []Vector{
{
ID: "a",
Vector: []float32{1, 0, 0},
Payload: ChunkPayload{
KnowledgeBaseID: 10,
DocumentID: 100,
Title: "A",
Content: "alpha",
},
},
{
ID: "b",
Vector: []float32{0, 1, 0},
Payload: ChunkPayload{
KnowledgeBaseID: 20,
DocumentID: 200,
Title: "B",
Content: "beta",
},
},
}
if err := provider.UpsertVectors(ctx, collectionName, vectors); err != nil {
t.Fatalf("UpsertVectors() error = %v", err)
}
info, err := provider.GetCollection(ctx, collectionName)
if err != nil {
t.Fatalf("GetCollection() error = %v", err)
}
if info.Dimension != 3 {
t.Fatalf("CollectionInfo.Dimension = %d, want 3", info.Dimension)
}
if info.PointCount != 2 {
t.Fatalf("CollectionInfo.PointCount = %d, want 2", info.PointCount)
}
results, err := provider.Search(ctx, &SearchRequest{
CollectionName: collectionName,
Vector: []float32{1, 0, 0},
TopK: 5,
ScoreThreshold: 0,
Filter: &SearchFilter{
KnowledgeBaseIDs: []int64{10},
},
})
if err != nil {
t.Fatalf("Search() error = %v", err)
}
if len(results) != 1 {
t.Fatalf("Search() returned %d results, want 1: %#v", len(results), results)
}
if results[0].ID != "a" {
t.Fatalf("Search()[0].ID = %q, want %q", results[0].ID, "a")
}
if results[0].Payload.KnowledgeBaseID != 10 {
t.Fatalf("Search()[0].Payload.KnowledgeBaseID = %d, want 10", results[0].Payload.KnowledgeBaseID)
}
if err := provider.DeleteVectors(ctx, collectionName, []string{"a"}); err != nil {
t.Fatalf("DeleteVectors() error = %v", err)
}
results, err = provider.Search(ctx, &SearchRequest{
CollectionName: collectionName,
Vector: []float32{1, 0, 0},
TopK: 5,
ScoreThreshold: 0,
Filter: &SearchFilter{
KnowledgeBaseIDs: []int64{10},
},
})
if err != nil {
t.Fatalf("Search() after delete error = %v", err)
}
if len(results) != 0 {
t.Fatalf("Search() after delete returned %d results, want 0: %#v", len(results), results)
}
}

View File

@@ -0,0 +1,43 @@
package vectordb
import (
"github.com/mlogclub/simple/common/structs"
"github.com/spf13/cast"
)
type ChunkPayload struct {
KnowledgeBaseID int64 `json:"knowledge_base_id"`
DocumentID int64 `json:"document_id"`
DocumentTitle string `json:"document_title"`
FaqID int64 `json:"faq_id"`
FaqQuestion string `json:"faq_question"`
ChunkNo int `json:"chunk_no"`
ChunkType string `json:"chunk_type"`
SectionPath string `json:"section_path"`
Title string `json:"title"`
Content string `json:"content"`
Provider string `json:"provider"`
}
func (p ChunkPayload) ToMap() map[string]any {
return structs.StructToMap(p)
}
func ChunkPayloadFromMap(data map[string]any) ChunkPayload {
if data == nil {
return ChunkPayload{}
}
return ChunkPayload{
KnowledgeBaseID: cast.ToInt64(data["knowledge_base_id"]),
DocumentID: cast.ToInt64(data["document_id"]),
DocumentTitle: cast.ToString(data["document_title"]),
FaqID: cast.ToInt64(data["faq_id"]),
FaqQuestion: cast.ToString(data["faq_question"]),
ChunkNo: cast.ToInt(data["chunk_no"]),
ChunkType: cast.ToString(data["chunk_type"]),
SectionPath: cast.ToString(data["section_path"]),
Title: cast.ToString(data["title"]),
Content: cast.ToString(data["content"]),
Provider: cast.ToString(data["provider"]),
}
}

View File

@@ -0,0 +1,88 @@
package vectordb
import (
"context"
"fmt"
"agent-desk/internal/pkg/config"
"agent-desk/internal/pkg/enums"
)
var defaultProvider Provider
func Init(cfg *config.VectorDBConfig) error {
if cfg == nil || cfg.Type == "" {
return nil
}
var err error
switch enums.VectorDBType(cfg.Type) {
case enums.VectorDBTypeQdrant:
defaultProvider, err = NewQdrantProvider(&cfg.Qdrant)
case enums.VectorDBTypeLanceDB:
defaultProvider, err = NewLanceDBProvider(&cfg.LanceDB)
default:
return fmt.Errorf("unsupported vectordb type: %s", cfg.Type)
}
return err
}
func GetProvider() Provider {
return defaultProvider
}
func Close() error {
if defaultProvider != nil {
return defaultProvider.Close()
}
return nil
}
func CreateCollection(ctx context.Context, name string, dimension int) error {
if defaultProvider == nil {
return fmt.Errorf("vectordb provider not initialized")
}
return defaultProvider.CreateCollection(ctx, name, dimension)
}
func DeleteCollection(ctx context.Context, name string) error {
if defaultProvider == nil {
return fmt.Errorf("vectordb provider not initialized")
}
return defaultProvider.DeleteCollection(ctx, name)
}
func GetCollection(ctx context.Context, name string) (*CollectionInfo, error) {
if defaultProvider == nil {
return nil, fmt.Errorf("vectordb provider not initialized")
}
return defaultProvider.GetCollection(ctx, name)
}
func ListCollections(ctx context.Context) ([]string, error) {
if defaultProvider == nil {
return nil, fmt.Errorf("vectordb provider not initialized")
}
return defaultProvider.ListCollections(ctx)
}
func UpsertVectors(ctx context.Context, collectionName string, vectors []Vector) error {
if defaultProvider == nil {
return fmt.Errorf("vectordb provider not initialized")
}
return defaultProvider.UpsertVectors(ctx, collectionName, vectors)
}
func DeleteVectors(ctx context.Context, collectionName string, ids []string) error {
if defaultProvider == nil {
return fmt.Errorf("vectordb provider not initialized")
}
return defaultProvider.DeleteVectors(ctx, collectionName, ids)
}
func Search(ctx context.Context, req *SearchRequest) ([]SearchResult, error) {
if defaultProvider == nil {
return nil, fmt.Errorf("vectordb provider not initialized")
}
return defaultProvider.Search(ctx, req)
}

View File

@@ -0,0 +1,25 @@
//go:build !lancedb
package vectordb
import (
"strings"
"testing"
"agent-desk/internal/pkg/config"
)
func TestInitLanceDBWithoutBuildTagReturnsActionableError(t *testing.T) {
err := Init(&config.VectorDBConfig{
Type: "lancedb",
LanceDB: config.LanceDBVectorDBConfig{
Path: "data/lancedb",
},
})
if err == nil {
t.Fatal("Init(lancedb) error = nil, want actionable build tag error")
}
if !strings.Contains(err.Error(), "LanceDB provider is not built") {
t.Fatalf("Init(lancedb) error = %q, want build tag guidance", err.Error())
}
}

View File

@@ -0,0 +1,247 @@
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
}
}

View File

@@ -0,0 +1,48 @@
package vectordb
import "context"
type Vector struct {
ID string `json:"id"`
Vector []float32 `json:"vector"`
Payload ChunkPayload `json:"payload"`
}
type SearchRequest struct {
CollectionName string `json:"collectionName"`
Vector []float32 `json:"vector"`
TopK int `json:"topK"`
ScoreThreshold float32 `json:"scoreThreshold"`
Filter *SearchFilter `json:"filter,omitempty"`
}
type SearchFilter struct {
KnowledgeBaseIDs []int64 `json:"knowledgeBaseIds,omitempty"`
DocumentIDs []int64 `json:"documentIds,omitempty"`
}
type SearchResult struct {
ID string `json:"id"`
Score float32 `json:"score"`
Payload ChunkPayload `json:"payload"`
}
type CollectionInfo struct {
Name string `json:"name"`
Dimension int `json:"dimension"`
PointCount int `json:"pointCount"`
Status string `json:"status"`
}
type Provider interface {
CreateCollection(ctx context.Context, name string, dimension int) error
DeleteCollection(ctx context.Context, name string) error
GetCollection(ctx context.Context, name string) (*CollectionInfo, error)
ListCollections(ctx context.Context) ([]string, error)
UpsertVectors(ctx context.Context, collectionName string, vectors []Vector) error
DeleteVectors(ctx context.Context, collectionName string, ids []string) error
Search(ctx context.Context, req *SearchRequest) ([]SearchResult, error)
Close() error
}