Files
Devour/cmd/query.go
T
Tomas Dvorak 898a3c303f update
2026-02-24 10:33:59 +01:00

143 lines
4.4 KiB
Go

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 <text>",
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
}