@@ -537,20 +537,40 @@ type indexStats struct {
537537
538538func queryIndexStats (ctx context.Context , db * sql.DB ) indexStats {
539539 var s indexStats
540- _ = db .QueryRowContext (ctx , "SELECT COALESCE(SUM(wp_composer_installs_total), 0) FROM packages WHERE type = 'plugin'" ).Scan (& s .PluginInstalls )
541- _ = db .QueryRowContext (ctx , "SELECT COALESCE(SUM(wp_composer_installs_total), 0) FROM packages WHERE type = 'theme'" ).Scan (& s .ThemeInstalls )
540+ _ = db .QueryRowContext (ctx , "SELECT plugin_installs, theme_installs FROM package_stats WHERE id = 1" ).Scan (& s .PluginInstalls , & s .ThemeInstalls )
542541 return s
543542}
544543
544+ // collapseSlug strips hyphens, underscores, and spaces to produce a
545+ // compact form suitable for LIKE-matching against similarly collapsed names.
546+ func collapseSlug (s string ) string {
547+ s = strings .ToLower (s )
548+ s = strings .ReplaceAll (s , "-" , "" )
549+ s = strings .ReplaceAll (s , "_" , "" )
550+ s = strings .ReplaceAll (s , " " , "" )
551+ return s
552+ }
553+
554+ // ftsQuery converts a user search string into an FTS5 query.
555+ // Each token becomes a prefix search, joined with AND.
556+ // e.g. "woo commerce" -> "woo* AND commerce*"
557+ func ftsQuery (s string ) string {
558+ words := strings .Fields (s )
559+ for i , w := range words {
560+ // Escape double quotes to prevent FTS5 syntax injection
561+ w = strings .ReplaceAll (w , `"` , `""` )
562+ words [i ] = `"` + w + `"` + "*"
563+ }
564+ return strings .Join (words , " AND " )
565+ }
566+
545567func queryPackages (ctx context.Context , db * sql.DB , f publicFilters , page , limit int ) ([]packageRow , int , error ) {
546568 where := "is_active = 1"
547569 args := []any {}
548570
549- if f .Search != "" {
550- normalized := "%" + strings .NewReplacer ("-" , "" , " " , "" ).Replace (f .Search ) + "%"
551- s := "%" + f .Search + "%"
552- where += " AND (REPLACE(REPLACE(name, '-', ''), ' ', '') LIKE ? OR display_name LIKE ? OR description LIKE ?)"
553- args = append (args , normalized , s , s )
571+ if q := ftsQuery (f .Search ); q != "" {
572+ where += " AND (id IN (SELECT rowid FROM packages_fts WHERE packages_fts MATCH ?) OR REPLACE(name, '-', '') LIKE ?)"
573+ args = append (args , q , "%" + collapseSlug (f .Search )+ "%" )
554574 }
555575 if f .Type != "" {
556576 where += " AND type = ?"
@@ -568,9 +588,13 @@ func queryPackages(ctx context.Context, db *sql.DB, f publicFilters, page, limit
568588 }
569589
570590 var total int
571- countQ := "SELECT COUNT(*) FROM packages WHERE " + where
572- if err := db .QueryRowContext (ctx , countQ , args ... ).Scan (& total ); err != nil {
573- return nil , 0 , err
591+ if f .Search == "" && f .Type == "" {
592+ _ = db .QueryRowContext (ctx , "SELECT active_plugins + active_themes FROM package_stats WHERE id = 1" ).Scan (& total )
593+ } else {
594+ countQ := "SELECT COUNT(*) FROM packages WHERE " + where
595+ if err := db .QueryRowContext (ctx , countQ , args ... ).Scan (& total ); err != nil {
596+ return nil , 0 , err
597+ }
574598 }
575599
576600 offset := (page - 1 ) * limit
@@ -693,11 +717,9 @@ func queryDashboardStats(ctx context.Context, db *sql.DB) map[string]any {
693717 CurrentBuild string
694718 }
695719
696- _ = db .QueryRowContext (ctx , "SELECT COUNT(*) FROM packages WHERE is_active = 1" ).Scan (& s .TotalPackages )
697- _ = db .QueryRowContext (ctx , "SELECT COUNT(*) FROM packages WHERE is_active = 1 AND type = 'plugin'" ).Scan (& s .ActivePlugins )
698- _ = db .QueryRowContext (ctx , "SELECT COUNT(*) FROM packages WHERE is_active = 1 AND type = 'theme'" ).Scan (& s .ActiveThemes )
699- _ = db .QueryRowContext (ctx , "SELECT COALESCE(SUM(wp_composer_installs_total), 0) FROM packages" ).Scan (& s .TotalInstalls )
700- _ = db .QueryRowContext (ctx , "SELECT COALESCE(SUM(wp_composer_installs_30d), 0) FROM packages" ).Scan (& s .Installs30d )
720+ _ = db .QueryRowContext (ctx , `SELECT active_plugins, active_themes, active_plugins + active_themes,
721+ plugin_installs + theme_installs, installs_30d FROM package_stats WHERE id = 1` ).Scan (
722+ & s .ActivePlugins , & s .ActiveThemes , & s .TotalPackages , & s .TotalInstalls , & s .Installs30d )
701723
702724 stats ["Stats" ] = s
703725 return stats
@@ -707,11 +729,9 @@ func queryAdminPackages(ctx context.Context, db *sql.DB, f adminFilters, page, l
707729 where := "1=1"
708730 args := []any {}
709731
710- if f .Search != "" {
711- normalized := "%" + strings .NewReplacer ("-" , "" , " " , "" ).Replace (f .Search ) + "%"
712- s := "%" + f .Search + "%"
713- where += " AND (REPLACE(REPLACE(name, '-', ''), ' ', '') LIKE ? OR display_name LIKE ? OR description LIKE ?)"
714- args = append (args , normalized , s , s )
732+ if q := ftsQuery (f .Search ); q != "" {
733+ where += " AND (id IN (SELECT rowid FROM packages_fts WHERE packages_fts MATCH ?) OR REPLACE(name, '-', '') LIKE ?)"
734+ args = append (args , q , "%" + collapseSlug (f .Search )+ "%" )
715735 }
716736 if f .Type != "" {
717737 where += " AND type = ?"
0 commit comments