// ingestion/internal/search/search.go package search import ( "bufio" "fmt" "log/slog" "os" "path/filepath" "sort" "strings" ) // Result is a single search hit from the brain wiki. type Result struct { Path string `json:"path"` Title string `json:"title"` Excerpt string `json:"excerpt"` Score int `json:"score"` } // Query searches all .md files under brainDir/wiki/ for pages containing // any of the whitespace-separated terms in query. Returns up to limit results // sorted by score descending. func Query(brainDir, query string, limit int) ([]Result, error) { if limit <= 0 { limit = 5 } terms := strings.Fields(strings.ToLower(query)) if len(terms) == 0 { return nil, nil } var results []Result for _, subdir := range []string{"knowledge", "wiki"} { dir := filepath.Join(brainDir, subdir) if _, statErr := os.Stat(dir); os.IsNotExist(statErr) { continue } err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { if err != nil { slog.Warn("search: skipping path", "path", path, "err", err) return nil } if d.IsDir() || !strings.HasSuffix(path, ".md") { return nil } content, err := os.ReadFile(path) if err != nil { slog.Warn("search: skipping unreadable file", "path", path, "err", err) return nil } lower := strings.ToLower(string(content)) score := 0 for _, term := range terms { score += strings.Count(lower, term) } if score == 0 { return nil } rel, err := filepath.Rel(brainDir, path) if err != nil { return fmt.Errorf("rel path: %w", err) } rel = filepath.ToSlash(rel) results = append(results, Result{ Path: rel, Title: extractTitle(string(content), d.Name()), Excerpt: excerpt(string(content), 300), Score: score, }) return nil }) if err != nil { return nil, err } } sort.Slice(results, func(i, j int) bool { return results[i].Score > results[j].Score }) if len(results) > limit { results = results[:limit] } return results, nil } func extractTitle(content, filename string) string { scanner := bufio.NewScanner(strings.NewReader(content)) inFrontmatter := false for scanner.Scan() { line := scanner.Text() if strings.TrimSpace(line) == "---" { if !inFrontmatter { inFrontmatter = true continue } break } if inFrontmatter { key, val, ok := strings.Cut(line, ":") if ok && strings.TrimSpace(key) == "title" { return strings.Trim(strings.TrimSpace(val), `"'`) } } } return strings.TrimSuffix(filename, ".md") } func excerpt(content string, maxLen int) string { // Skip frontmatter, return first maxLen chars of body. parts := strings.SplitN(content, "---", 3) body := content if len(parts) == 3 { body = strings.TrimSpace(parts[2]) } if len(body) > maxLen { return body[:maxLen] + "…" } return body }