package memory import ( "database/sql" "strings" "time" ) type SearchResult struct { Memory Score float64 `json:"score"` } func (s *Store) Search(query string, limit int) ([]SearchResult, error) { s.mu.RLock() defer s.mu.RUnlock() if limit <= 0 { limit = 10 } if limit > 50 { limit = 50 } normalizedQuery := normalizeQuery(query) rows, err := s.db.Query(` SELECT m.id, m.type, m.key, m.content, m.tags, m.source, m.confidence, m.access_count, m.created_at, m.updated_at, bm25(memories_fts) as score FROM memories_fts f JOIN memories m ON m.rowid = f.rowid WHERE memories_fts MATCH ? ORDER BY score LIMIT ? `, normalizedQuery, limit) if err != nil { return fallbackSearch(s.db, query, limit) } defer rows.Close() return scanSearchResults(rows) } func (s *Store) Recall(query string, limit int) ([]Memory, error) { results, err := s.Search(query, limit) if err != nil { return nil, err } memories := make([]Memory, len(results)) for i, r := range results { memories[i] = r.Memory } return memories, nil } func (s *Store) RecallByType(memType MemoryType, limit int) ([]Memory, error) { s.mu.RLock() defer s.mu.RUnlock() if limit <= 0 { limit = 20 } rows, err := s.db.Query(` SELECT id, type, key, content, tags, source, confidence, access_count, created_at, updated_at FROM memories WHERE type = ? ORDER BY access_count DESC, updated_at DESC LIMIT ? `, string(memType), limit) if err != nil { return nil, err } defer rows.Close() return scanMemories(rows) } func (s *Store) RecallRecent(since time.Time, limit int) ([]Memory, error) { s.mu.RLock() defer s.mu.RUnlock() if limit <= 0 { limit = 20 } rows, err := s.db.Query(` SELECT id, type, key, content, tags, source, confidence, access_count, created_at, updated_at FROM memories WHERE updated_at >= ? ORDER BY updated_at DESC LIMIT ? `, since, limit) if err != nil { return nil, err } defer rows.Close() return scanMemories(rows) } func (s *Store) RecallPreferences() ([]Memory, error) { return s.RecallByType(TypePreference, 50) } func (s *Store) RecallFacts() ([]Memory, error) { return s.RecallByType(TypeFact, 50) } func (s *Store) StorePreference(key, content string) error { return s.Store(&Memory{ Type: TypePreference, Key: key, Content: content, Source: "user", Confidence: 0.9, }) } func (s *Store) StoreContext(key, content string) error { return s.Store(&Memory{ Type: TypeContext, Key: key, Content: content, Source: "conversation", Confidence: 0.7, }) } func (s *Store) StoreSummary(sessionID, summary string) error { return s.Store(&Memory{ Type: TypeSummary, Key: "session:" + sessionID, Content: summary, Source: "auto", Confidence: 0.8, }) } func (s *Store) StoreFact(key, content string) error { return s.Store(&Memory{ Type: TypeFact, Key: key, Content: content, Source: "auto", Confidence: 0.85, }) } func normalizeQuery(query string) string { words := strings.Fields(strings.ToLower(query)) var escaped []string for _, w := range words { if len(w) > 0 { escaped = append(escaped, w+"*") } } return strings.Join(escaped, " OR ") } func fallbackSearch(db *sql.DB, query string, limit int) ([]SearchResult, error) { likePattern := "%" + strings.ToLower(query) + "%" rows, err := db.Query(` SELECT id, type, key, content, tags, source, confidence, access_count, created_at, updated_at FROM memories WHERE LOWER(key) LIKE ? OR LOWER(content) LIKE ? OR LOWER(tags) LIKE ? ORDER BY updated_at DESC LIMIT ? `, likePattern, likePattern, likePattern, limit) if err != nil { return nil, err } defer rows.Close() var results []SearchResult for rows.Next() { var m Memory err := rows.Scan(&m.ID, &m.Type, &m.Key, &m.Content, &m.Tags, &m.Source, &m.Confidence, &m.AccessCount, &m.CreatedAt, &m.UpdatedAt) if err != nil { return results, err } score := computeFallbackScore(m, query) results = append(results, SearchResult{Memory: m, Score: score}) } return results, nil } func computeFallbackScore(m Memory, query string) float64 { score := m.Confidence * 0.5 lower := strings.ToLower(query) if strings.Contains(strings.ToLower(m.Key), lower) { score += 0.3 } if strings.Contains(strings.ToLower(m.Content), lower) { score += 0.2 } score += float64(m.AccessCount) * 0.01 return score } func scanSearchResults(rows *sql.Rows) ([]SearchResult, error) { var results []SearchResult for rows.Next() { var m Memory var score float64 err := rows.Scan(&m.ID, &m.Type, &m.Key, &m.Content, &m.Tags, &m.Source, &m.Confidence, &m.AccessCount, &m.CreatedAt, &m.UpdatedAt, &score) if err != nil { return results, err } results = append(results, SearchResult{Memory: m, Score: score}) } return results, nil }