Files
st-pierre/qdrant.go
T
didier 39c6185b70 feat: sqlite persistence with goose migrations, flashcard schema, zap logger
- Add SQLite as authoritative store alongside Qdrant (best-effort indexing)
- Use goose for embedded SQL migrations
- Schema matches flashcard format: id(INTEGER PK), note, repos, host, tags, user, context, created
- Replace std log with zap structured logger
- Split registerTools into registerStoreKnowledge + registerSearchKnowledge
- Qdrant index uses SQLite rowid directly as point ID (no SHA256 hash)
- Search enriches Qdrant results from SQLite by ID
- Lowercase auth header before prefix check
- Always handle json.MarshalIndent errors
- NixOS: StateDirectory, sqlitePath to /var/lib/st-pierre/st-pierre.sqlite
2026-06-02 10:01:28 -04:00

128 lines
2.8 KiB
Go

package main
import (
"context"
"fmt"
"strconv"
"github.com/qdrant/go-client/qdrant"
)
const collectionName = "knowledge"
type entry struct {
ID int64 `json:"id"`
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"`
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 int64, 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)),
"content": qdrant.NewValueString(note),
}
_, err = s.client.Upsert(ctx, &qdrant.UpsertPoints{
CollectionName: collectionName,
Points: []*qdrant.PointStruct{
{
Id: qdrant.NewIDNum(uint64(id)),
Vectors: qdrant.NewVectors(vec...),
Payload: payload,
},
},
})
if err != nil {
return fmt.Errorf("upsert 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
}