96 lines
3.1 KiB
Go
96 lines
3.1 KiB
Go
// Package application holds knowledge's use cases — the content of
|
|
// internal/rag/store.go's IndexFile/Search methods, now split from
|
|
// their SQL (internal/infrastructure/postgres) so this package depends
|
|
// only on domain.DocumentRepository/domain.Embedder.
|
|
package application
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
|
|
"enkly/services/knowledge/internal/domain"
|
|
)
|
|
|
|
type Service struct {
|
|
Documents domain.DocumentRepository
|
|
Embedder domain.Embedder
|
|
}
|
|
|
|
func hashContent(content string) string {
|
|
sum := sha256.Sum256([]byte(content))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
// upsertChunk replaces rag.Store.UpsertChunk: embeds a single chunk and
|
|
// stores it, skipping the embedding call entirely if the chunk's
|
|
// content hasn't changed since it was last indexed.
|
|
func (s *Service) upsertChunk(ctx context.Context, filePath string, chunkIndex int, content string) error {
|
|
hash := hashContent(content)
|
|
|
|
existing, err := s.Documents.ExistingContentHash(ctx, filePath, chunkIndex)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if existing == hash {
|
|
return nil // unchanged, skip re-embedding
|
|
}
|
|
|
|
vec, err := s.Embedder.Embed(ctx, content)
|
|
if err != nil {
|
|
return fmt.Errorf("knowledge: embed %s chunk %d: %w", filePath, chunkIndex, err)
|
|
}
|
|
if len(vec) != domain.EmbeddingDim {
|
|
return fmt.Errorf("knowledge: embedding model returned %d-dimensional vectors but the schema declares vector(%d) — "+
|
|
"use a %d-dim model (the default nomic-embed-text is) or change the column and re-index",
|
|
len(vec), domain.EmbeddingDim, domain.EmbeddingDim)
|
|
}
|
|
if err := s.Documents.Upsert(ctx, filePath, chunkIndex, content, hash, vec); err != nil {
|
|
return fmt.Errorf("knowledge: upsert %s chunk %d: %w", filePath, chunkIndex, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// IndexFile replaces rag.Store.IndexFile.
|
|
func (s *Service) IndexFile(ctx context.Context, filePath, content string) (indexed, skipped int, err error) {
|
|
chunks := domain.ChunkText(content)
|
|
if err := s.Documents.DeleteChunksFrom(ctx, filePath, len(chunks)); err != nil {
|
|
return 0, 0, fmt.Errorf("knowledge: prune stale chunks for %s: %w", filePath, err)
|
|
}
|
|
for i, chunk := range chunks {
|
|
before, _ := s.Documents.ExistingContentHash(ctx, filePath, i)
|
|
if err := s.upsertChunk(ctx, filePath, i, chunk); err != nil {
|
|
return indexed, skipped, err
|
|
}
|
|
if before == hashContent(chunk) {
|
|
skipped++
|
|
} else {
|
|
indexed++
|
|
}
|
|
}
|
|
return indexed, skipped, nil
|
|
}
|
|
|
|
// PruneMissing replaces rag.Store.pruneMissing.
|
|
func (s *Service) PruneMissing(ctx context.Context, keepFilePaths []string) (int, error) {
|
|
return s.Documents.PruneMissing(ctx, keepFilePaths)
|
|
}
|
|
|
|
// Search replaces rag.Store.Search.
|
|
func (s *Service) Search(ctx context.Context, query string, topK int) ([]domain.CodeChunk, error) {
|
|
if topK <= 0 {
|
|
topK = 6
|
|
}
|
|
vec, err := s.Embedder.Embed(ctx, query)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("knowledge: embed query: %w", err)
|
|
}
|
|
return s.Documents.Search(ctx, vec, topK)
|
|
}
|
|
|
|
// ListIndexedFiles replaces db.ListIndexedFiles.
|
|
func (s *Service) ListIndexedFiles(ctx context.Context, limit int) ([]string, error) {
|
|
return s.Documents.ListIndexedFiles(ctx, limit)
|
|
}
|