package cmd import ( "context" "encoding/json" "fmt" "strings" "github.com/spf13/cobra" appconfig "github.com/yourorg/devour/internal/config" "github.com/yourorg/devour/internal/search" ) var queryCmd = &cobra.Command{ Use: "query ", Short: "Search indexed documentation", Long: `Search indexed documentation using semantic similarity. Returns the most relevant document chunks based on vector similarity to the query text. Examples: devour query "how to authenticate" devour query "API rate limiting" --limit 10 devour query "deployment" --format json`, Args: cobra.MinimumNArgs(1), RunE: runQuery, } var ( queryLimit int queryFormat string queryThreshold float64 ) func init() { queryCmd.Flags().IntVarP(&queryLimit, "limit", "n", 5, "maximum number of results") queryCmd.Flags().StringVarP(&queryFormat, "format", "f", "text", "output format (text, json, markdown)") queryCmd.Flags().Float64Var(&queryThreshold, "threshold", 0, "minimum lexical score threshold") } func runQuery(cmd *cobra.Command, args []string) error { query := strings.TrimSpace(strings.Join(args, " ")) if query == "" { return fmt.Errorf("query cannot be empty") } cfg, err := loadAppConfig() if err != nil { return err } engine := search.NewEngine(cfg) results, stats, err := engine.Search(context.Background(), query, search.SearchOptions{ Limit: queryLimit, Threshold: queryThreshold, }) if err != nil { return fmt.Errorf("query failed: %w", err) } switch strings.ToLower(queryFormat) { case "json": resp := map[string]any{ "query": query, "limit": queryLimit, "threshold": queryThreshold, "count": len(results), "results": results, "indexed_at": stats.LastIndexedAt, "documents": stats.Documents, } enc := json.NewEncoder(cmd.OutOrStdout()) enc.SetIndent("", " ") return enc.Encode(resp) case "markdown": return printQueryMarkdown(cmd, query, cfg, results, stats) case "text": return printQueryText(cmd, query, cfg, results, stats) default: return fmt.Errorf("unsupported format: %s (supported: text, json, markdown)", queryFormat) } } func printQueryText(cmd *cobra.Command, query string, cfg *appconfig.Config, results []search.Result, stats *search.IndexStats) error { fmt.Fprintf(cmd.OutOrStdout(), "Searching: %q\n", query) fmt.Fprintf(cmd.OutOrStdout(), " Limit: %d\n", queryLimit) fmt.Fprintf(cmd.OutOrStdout(), " Threshold: %.2f\n", queryThreshold) fmt.Fprintf(cmd.OutOrStdout(), " Indexed docs: %d\n", stats.Documents) fmt.Fprintf(cmd.OutOrStdout(), " Docs dir: %s\n\n", cfg.Storage.DocsDir) if len(results) == 0 { fmt.Fprintln(cmd.OutOrStdout(), "No results found.") return nil } fmt.Fprintln(cmd.OutOrStdout(), "Results:") fmt.Fprintln(cmd.OutOrStdout(), "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") for i, r := range results { fmt.Fprintf(cmd.OutOrStdout(), "%d. %s\n", i+1, r.Title) fmt.Fprintf(cmd.OutOrStdout(), " Score: %.3f | Type: %s | Source: %s\n", r.Score, r.Type, defaultSource(r.Source)) if r.URL != "" { fmt.Fprintf(cmd.OutOrStdout(), " URL: %s\n", r.URL) } fmt.Fprintf(cmd.OutOrStdout(), " Snippet: %s\n\n", r.Snippet) } return nil } func printQueryMarkdown(cmd *cobra.Command, query string, cfg *appconfig.Config, results []search.Result, stats *search.IndexStats) error { fmt.Fprintf(cmd.OutOrStdout(), "# Query Results\n\n") fmt.Fprintf(cmd.OutOrStdout(), "- Query: `%s`\n", query) fmt.Fprintf(cmd.OutOrStdout(), "- Limit: `%d`\n", queryLimit) fmt.Fprintf(cmd.OutOrStdout(), "- Threshold: `%.2f`\n", queryThreshold) fmt.Fprintf(cmd.OutOrStdout(), "- Indexed docs: `%d`\n", stats.Documents) fmt.Fprintf(cmd.OutOrStdout(), "- Docs dir: `%s`\n\n", cfg.Storage.DocsDir) if len(results) == 0 { fmt.Fprintln(cmd.OutOrStdout(), "_No results found._") return nil } for i, r := range results { fmt.Fprintf(cmd.OutOrStdout(), "## %d. %s\n\n", i+1, r.Title) fmt.Fprintf(cmd.OutOrStdout(), "- Score: `%.3f`\n", r.Score) fmt.Fprintf(cmd.OutOrStdout(), "- Type: `%s`\n", r.Type) fmt.Fprintf(cmd.OutOrStdout(), "- Source: `%s`\n", defaultSource(r.Source)) if r.URL != "" { fmt.Fprintf(cmd.OutOrStdout(), "- URL: %s\n", r.URL) } fmt.Fprintf(cmd.OutOrStdout(), "\n%s\n\n", r.Snippet) } return nil } func defaultSource(source string) string { source = strings.TrimSpace(source) if source == "" { return "unknown" } return source }