Skip to content

Commit 74c667f

Browse files
authored
Merge pull request #12 from streed/copilot/fix-5193b28f-7bac-4324-960f-b56e1621d0d4
Fix: Ensure lil-rag documents are deleted when notes are removed
2 parents cc7a234 + 13643dd commit 74c667f

File tree

4 files changed

+158
-2
lines changed

4 files changed

+158
-2
lines changed

cmd/delete.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/spf13/cobra"
1111
"github.com/streed/ml-notes/internal/logger"
12+
"github.com/streed/ml-notes/internal/search"
1213
)
1314

1415
var deleteCmd = &cobra.Command{
@@ -118,8 +119,18 @@ func runDelete(_ *cobra.Command, args []string) error {
118119
fmt.Printf("✓ Deleted note %d: %s\n", id, notesToDelete[id])
119120
successCount++
120121

121-
// Vector search cleanup is handled by lil-rag service
122-
logger.Debug("Note %d removed", id)
122+
// Also delete from lil-rag vector index
123+
if vectorSearch != nil {
124+
if lilragSearch, ok := vectorSearch.(*search.LilRagSearch); ok && lilragSearch.IsAvailable() {
125+
projectNamespace := getCurrentProjectNamespace()
126+
if err := lilragSearch.DeleteNoteWithNamespace(id, "", projectNamespace); err != nil {
127+
logger.Error("Failed to delete note %d from lil-rag: %v", id, err)
128+
// Don't fail the overall deletion if lil-rag deletion fails
129+
} else {
130+
logger.Debug("Note %d removed from lil-rag index", id)
131+
}
132+
}
133+
}
123134
}
124135
}
125136

@@ -181,6 +192,17 @@ func deleteAllNotes() error {
181192
failCount++
182193
} else {
183194
successCount++
195+
196+
// Also delete from lil-rag vector index
197+
if vectorSearch != nil {
198+
if lilragSearch, ok := vectorSearch.(*search.LilRagSearch); ok && lilragSearch.IsAvailable() {
199+
projectNamespace := getCurrentProjectNamespace()
200+
if err := lilragSearch.DeleteNoteWithNamespace(note.ID, "", projectNamespace); err != nil {
201+
logger.Error("Failed to delete note %d from lil-rag: %v", note.ID, err)
202+
// Don't fail the overall deletion if lil-rag deletion fails
203+
}
204+
}
205+
}
184206
}
185207
}
186208

internal/lilrag/client.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ type SearchResponse struct {
4848
Results []SearchResult `json:"results"`
4949
}
5050

51+
type DeleteRequest struct {
52+
ID string `json:"id"`
53+
Namespace string `json:"namespace,omitempty"`
54+
}
55+
56+
type DeleteResponse struct {
57+
Success bool `json:"success"`
58+
ID string `json:"id"`
59+
Message string `json:"message"`
60+
Status string `json:"status"`
61+
}
62+
5163
func NewClient(cfg *config.Config) *Client {
5264
baseURL := cfg.LilRagURL
5365
if baseURL == "" {
@@ -187,3 +199,59 @@ func (c *Client) IsAvailable() bool {
187199
logger.Debug("Lil-rag service not available at %s", c.baseURL)
188200
return false
189201
}
202+
203+
func (c *Client) DeleteDocument(id string) error {
204+
return c.DeleteDocumentWithNamespace(id, "")
205+
}
206+
207+
func (c *Client) DeleteDocumentWithNamespace(id, namespace string) error {
208+
req := DeleteRequest{
209+
ID: id,
210+
Namespace: namespace,
211+
}
212+
213+
jsonData, err := json.Marshal(req)
214+
if err != nil {
215+
return fmt.Errorf("failed to marshal delete request: %w", err)
216+
}
217+
218+
url := c.baseURL + "/api/delete"
219+
if namespace != "" {
220+
logger.Debug("Deleting document %s from lil-rag at %s (namespace: %s)", id, url, namespace)
221+
} else {
222+
logger.Debug("Deleting document %s from lil-rag at %s", id, url)
223+
}
224+
225+
resp, err := c.httpClient.Post(url, "application/json", bytes.NewBuffer(jsonData))
226+
if err != nil {
227+
return fmt.Errorf("failed to send delete request: %w", err)
228+
}
229+
defer resp.Body.Close()
230+
231+
if resp.StatusCode != http.StatusOK {
232+
body, _ := io.ReadAll(resp.Body)
233+
return fmt.Errorf("lil-rag delete request failed with status %d: %s", resp.StatusCode, string(body))
234+
}
235+
236+
var deleteResp DeleteResponse
237+
if err := json.NewDecoder(resp.Body).Decode(&deleteResp); err != nil {
238+
return fmt.Errorf("failed to decode delete response: %w", err)
239+
}
240+
241+
// Check for success using either Success field (new format) or Status field (actual lil-rag format)
242+
success := deleteResp.Success || deleteResp.Status == "deleted"
243+
if !success {
244+
message := deleteResp.Message
245+
if message == "" && deleteResp.Status != "" {
246+
message = deleteResp.Status
247+
}
248+
return fmt.Errorf("lil-rag delete failed: %s", message)
249+
}
250+
251+
message := deleteResp.Message
252+
if message == "" && deleteResp.Status != "" {
253+
message = deleteResp.Status
254+
}
255+
logger.Debug("Successfully deleted document %s: %s", deleteResp.ID, message)
256+
return nil
257+
}

internal/search/lilrag_search.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,27 @@ func (lrs *LilRagSearch) IsAvailable() bool {
113113
return lrs.client.IsAvailable()
114114
}
115115

116+
func (lrs *LilRagSearch) DeleteNote(noteID int) error {
117+
return lrs.DeleteNoteWithNamespace(noteID, "", "default")
118+
}
119+
120+
func (lrs *LilRagSearch) DeleteNoteWithNamespace(noteID int, namespace, projectID string) error {
121+
// Use project-specific note ID as document ID for lil-rag
122+
docID := fmt.Sprintf("notes-%s-%d", projectID, noteID)
123+
124+
// Create namespace with ml-notes prefix
125+
mlNamespace := lrs.createNamespace(namespace)
126+
127+
err := lrs.client.DeleteDocumentWithNamespace(docID, mlNamespace)
128+
if err != nil {
129+
logger.Error("Failed to delete note %d from lil-rag: %v", noteID, err)
130+
return fmt.Errorf("failed to delete note from lil-rag: %w", err)
131+
}
132+
133+
logger.Debug("Successfully deleted note %d from lil-rag", noteID)
134+
return nil
135+
}
136+
116137
// extractNoteIDFromDocID extracts the note ID and project ID from a lil-rag document ID
117138
// Expected format: "notes-project-123" -> (123, "project")
118139
func extractNoteIDFromDocID(docID string) (int, string, error) {

internal/search/lilrag_search_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package search
22

33
import (
4+
"fmt"
45
"testing"
56

67
"github.com/streed/ml-notes/internal/config"
@@ -42,3 +43,47 @@ func TestCreateNamespace(t *testing.T) {
4243
})
4344
}
4445
}
46+
47+
func TestDeleteNoteDocID(t *testing.T) {
48+
// Test that note deletion uses the same document ID format as indexing
49+
tests := []struct {
50+
name string
51+
noteID int
52+
projectID string
53+
expected string
54+
}{
55+
{
56+
name: "simple note",
57+
noteID: 123,
58+
projectID: "default",
59+
expected: "notes-default-123",
60+
},
61+
{
62+
name: "complex project",
63+
noteID: 456,
64+
projectID: "my-awesome-project",
65+
expected: "notes-my-awesome-project-456",
66+
},
67+
}
68+
69+
for _, tt := range tests {
70+
t.Run(tt.name, func(t *testing.T) {
71+
// Test that both index and delete use the same document ID format
72+
indexDocID := getDocumentID(tt.noteID, tt.projectID)
73+
deleteDocID := getDocumentID(tt.noteID, tt.projectID)
74+
75+
if indexDocID != tt.expected {
76+
t.Errorf("getDocumentID(%d, %q) = %q, want %q", tt.noteID, tt.projectID, indexDocID, tt.expected)
77+
}
78+
if deleteDocID != tt.expected {
79+
t.Errorf("delete document ID should match index document ID: got %q, want %q", deleteDocID, tt.expected)
80+
}
81+
})
82+
}
83+
}
84+
85+
// Helper function to extract document ID generation logic for testing
86+
func getDocumentID(noteID int, projectID string) string {
87+
// This mirrors the logic in IndexNoteWithNamespace and DeleteNoteWithNamespace
88+
return fmt.Sprintf("notes-%s-%d", projectID, noteID)
89+
}

0 commit comments

Comments
 (0)