@@ -660,18 +660,75 @@ func (d *Database) SearchReports(ctx context.Context, searchQuery string, classi
660660 // Phase 2: Fetch report details only for matching IDs
661661 // This approach is ~5x faster than joining all tables for FULLTEXT match
662662
663- var seqArgs []interface {}
664- var seqQuery string
663+ var seqs []int
664+
665+ // Fast path for common single-word brand searches (e.g. "google"):
666+ // use indexed brand columns first and only fall back to FULLTEXT when needed.
667+ term := strings .TrimSpace (searchQuery )
668+ if strings .HasPrefix (term , "+" ) {
669+ term = strings .TrimPrefix (term , "+" )
670+ }
671+ if term != "" && ! strings .Contains (term , " " ) && ! strings .ContainsAny (term , "\" *~()<>" ) {
672+ prefixTerm := strings .ToLower (term ) + "%"
673+ var fastQuery string
674+ var fastArgs []interface {}
675+ if classification != "" {
676+ fastQuery = `
677+ SELECT DISTINCT seq
678+ FROM report_analysis
679+ WHERE is_valid = TRUE
680+ AND classification = ?
681+ AND brand_name LIKE ?
682+ LIMIT 200
683+ `
684+ fastArgs = []interface {}{classification , prefixTerm }
685+ } else {
686+ fastQuery = `
687+ SELECT DISTINCT seq
688+ FROM report_analysis
689+ WHERE is_valid = TRUE
690+ AND brand_name LIKE ?
691+ LIMIT 200
692+ `
693+ fastArgs = []interface {}{prefixTerm }
694+ }
695+
696+ fastRows , err := d .db .QueryContext (ctx , fastQuery , fastArgs ... )
697+ if err != nil {
698+ log .Printf ("SearchReports brand fast path failed, falling back to FULLTEXT: %v" , err )
699+ } else {
700+ defer fastRows .Close ()
701+ for fastRows .Next () {
702+ var seq int
703+ if err := fastRows .Scan (& seq ); err != nil {
704+ return nil , fmt .Errorf ("failed to scan fast-path seq: %w" , err )
705+ }
706+ seqs = append (seqs , seq )
707+ }
708+ if err := fastRows .Err (); err != nil {
709+ return nil , fmt .Errorf ("error iterating fast-path seqs: %w" , err )
710+ }
711+ }
712+ }
713+
714+ if len (seqs ) == 0 {
715+ if searchQuery == "" {
716+ // No search query - just return empty
717+ if ! full_data {
718+ return []models.ReportWithMinimalAnalysis {}, nil
719+ }
720+ return []models.ReportWithAnalysis {}, nil
721+ }
665722
666- if searchQuery != "" {
723+ var seqArgs []interface {}
724+ var seqQuery string
667725 if classification != "" {
668726 seqQuery = `
669727 SELECT DISTINCT seq
670728 FROM report_analysis
671729 WHERE is_valid = TRUE
672730 AND classification = ?
673731 AND MATCH(title, description, brand_name, brand_display_name, summary) AGAINST (? IN BOOLEAN MODE)
674- ORDER BY seq DESC
675732 LIMIT 200
676733 `
677734 seqArgs = []interface {}{classification , searchQuery }
@@ -681,33 +738,28 @@ func (d *Database) SearchReports(ctx context.Context, searchQuery string, classi
681738 FROM report_analysis
682739 WHERE is_valid = TRUE
683740 AND MATCH(title, description, brand_name, brand_display_name, summary) AGAINST (? IN BOOLEAN MODE)
684- ORDER BY seq DESC
685741 LIMIT 200
686742 `
687743 seqArgs = []interface {}{searchQuery }
688744 }
689- } else {
690- // No search query - just return empty
691- if ! full_data {
692- return []models.ReportWithMinimalAnalysis {}, nil
693- }
694- return []models.ReportWithAnalysis {}, nil
695- }
696745
697- // Phase 1: Get matching seq IDs (fast - uses FULLTEXT index only)
698- seqRows , err := d .db .QueryContext (ctx , seqQuery , seqArgs ... )
699- if err != nil {
700- return nil , fmt .Errorf ("failed to query search seq IDs: %w" , err )
701- }
702- defer seqRows .Close ()
746+ // Phase 1 fallback: FULLTEXT search for seq IDs
747+ seqRows , err := d .db .QueryContext (ctx , seqQuery , seqArgs ... )
748+ if err != nil {
749+ return nil , fmt .Errorf ("failed to query search seq IDs: %w" , err )
750+ }
751+ defer seqRows .Close ()
703752
704- var seqs []int
705- for seqRows .Next () {
706- var seq int
707- if err := seqRows .Scan (& seq ); err != nil {
708- return nil , fmt .Errorf ("failed to scan seq: %w" , err )
753+ for seqRows .Next () {
754+ var seq int
755+ if err := seqRows .Scan (& seq ); err != nil {
756+ return nil , fmt .Errorf ("failed to scan seq: %w" , err )
757+ }
758+ seqs = append (seqs , seq )
759+ }
760+ if err := seqRows .Err (); err != nil {
761+ return nil , fmt .Errorf ("error iterating search seq IDs: %w" , err )
709762 }
710- seqs = append (seqs , seq )
711763 }
712764
713765 if len (seqs ) == 0 {
@@ -938,7 +990,7 @@ func (d *Database) GetReportBySeq(ctx context.Context, seq int) (*models.ReportW
938990 // Then, get all analyses for this report
939991 analysesQuery := `
940992 SELECT
941- ra.seq, ra.source, ra.analysis_text, ra. analysis_image,
993+ ra.seq, ra.source, ra.analysis_text, NULL as analysis_image,
942994 ra.title, ra.description, ra.brand_name, ra.brand_display_name,
943995 ra.litter_probability, ra.hazard_probability, ra.digital_bug_probability,
944996 ra.severity_level, ra.summary, ra.language, ra.classification,
0 commit comments