Schema: public_id + version columns with UNIQUE constraint, delete_timestamp for soft-delete. Tools: store_flashcard accepts optional public_id, update_flashcard creates a new version preserving history, delete_flashcard inserts a tombstone row and removes the Qdrant point. Qdrant: point ID is public_id (only latest version indexed).
148 lines
3.5 KiB
Go
148 lines
3.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strconv"
|
|
|
|
"github.com/qdrant/go-client/qdrant"
|
|
)
|
|
|
|
const collectionName = "flashcard"
|
|
|
|
type entry struct {
|
|
ID int64 `json:"_id"`
|
|
PublicID int64 `json:"public_id"`
|
|
Version int `json:"version"`
|
|
Note string `json:"note"`
|
|
Repos []string `json:"repos,omitempty"`
|
|
Host string `json:"host,omitempty"`
|
|
Tags []string `json:"tags,omitempty"`
|
|
User string `json:"user,omitempty"`
|
|
Context string `json:"context,omitempty"`
|
|
DeleteTimestamp string `json:"delete_timestamp,omitempty"`
|
|
Created string `json:"created"`
|
|
}
|
|
|
|
type searchResult struct {
|
|
Entry entry `json:",inline"`
|
|
Score float32 `json:"score"`
|
|
}
|
|
|
|
type qdrantStore struct {
|
|
client *qdrant.Client
|
|
embedder *embedder
|
|
}
|
|
|
|
func newQdrantStore(client *qdrant.Client, embedder *embedder) *qdrantStore {
|
|
return &qdrantStore{client: client, embedder: embedder}
|
|
}
|
|
|
|
func (s *qdrantStore) ensureCollection(ctx context.Context) error {
|
|
exists, err := s.client.CollectionExists(ctx, collectionName)
|
|
if err != nil {
|
|
return fmt.Errorf("check collection exists: %w", err)
|
|
}
|
|
if exists {
|
|
return nil
|
|
}
|
|
|
|
err = s.client.CreateCollection(ctx, &qdrant.CreateCollection{
|
|
CollectionName: collectionName,
|
|
VectorsConfig: qdrant.NewVectorsConfig(&qdrant.VectorParams{
|
|
Size: 1536,
|
|
Distance: qdrant.Distance_Cosine,
|
|
}),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("create collection: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *qdrantStore) index(ctx context.Context, id, publicID int64, version int, note string) error {
|
|
vec, err := s.embedder.embedText(ctx, note)
|
|
if err != nil {
|
|
return fmt.Errorf("embed text: %w", err)
|
|
}
|
|
|
|
payload := map[string]*qdrant.Value{
|
|
"_id": qdrant.NewValueString(strconv.FormatInt(id, 10)),
|
|
"public_id": qdrant.NewValueString(strconv.FormatInt(publicID, 10)),
|
|
"version": qdrant.NewValueInt(int64(version)),
|
|
"content": qdrant.NewValueString(note),
|
|
}
|
|
|
|
_, err = s.client.Upsert(ctx, &qdrant.UpsertPoints{
|
|
CollectionName: collectionName,
|
|
Points: []*qdrant.PointStruct{
|
|
{
|
|
Id: qdrant.NewIDNum(uint64(publicID)),
|
|
Vectors: qdrant.NewVectors(vec...),
|
|
Payload: payload,
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("upsert point: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *qdrantStore) delete(ctx context.Context, publicID int64) error {
|
|
wait := true
|
|
_, err := s.client.Delete(ctx, &qdrant.DeletePoints{
|
|
CollectionName: collectionName,
|
|
Wait: &wait,
|
|
Points: qdrant.NewPointsSelector(
|
|
qdrant.NewIDNum(uint64(publicID)),
|
|
),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("delete point: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *qdrantStore) search(ctx context.Context, query string, limit uint64) ([]searchResult, error) {
|
|
vec, err := s.embedder.embedText(ctx, query)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("embed query: %w", err)
|
|
}
|
|
|
|
if limit == 0 {
|
|
limit = 5
|
|
}
|
|
|
|
results, err := s.client.Query(ctx, &qdrant.QueryPoints{
|
|
CollectionName: collectionName,
|
|
Query: qdrant.NewQueryDense(vec),
|
|
Limit: &limit,
|
|
WithPayload: qdrant.NewWithPayload(true),
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query points: %w", err)
|
|
}
|
|
|
|
var searchResults []searchResult
|
|
for _, pt := range results {
|
|
r := searchResult{
|
|
Score: pt.Score,
|
|
}
|
|
|
|
if payload := pt.GetPayload(); payload != nil {
|
|
if v, ok := payload["_id"]; ok {
|
|
if n, err := strconv.ParseInt(v.GetStringValue(), 10, 64); err == nil {
|
|
r.Entry.ID = n
|
|
}
|
|
}
|
|
}
|
|
|
|
searchResults = append(searchResults, r)
|
|
}
|
|
|
|
return searchResults, nil
|
|
}
|