Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 90
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
SearchManager
0.00% covered (danger)
0.00%
0 / 90
0.00% covered (danger)
0.00%
0 / 6
240
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 search
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
20
 logSearch
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 getNoResultsQueries
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getPopularSearches
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getSearchAnalytics
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3namespace BuyerKiosk\Support;
4
5use \PDO;
6use \PDOException;
7
8/**
9 * SearchManager - Search and analytics for support articles
10 *
11 * Handles search functionality with logging for analytics,
12 * tracking no-results queries and popular searches for insights.
13 */
14class SearchManager
15{
16    /**
17     * @var PDO Database connection (kiosk_buykiosk)
18     */
19    private $db;
20
21    /**
22     * Constructor
23     *
24     * @param PDO $db Database connection (kiosk_buykiosk)
25     */
26    public function __construct(PDO $db)
27    {
28        $this->db = $db;
29    }
30
31    /**
32     * Perform search and log it
33     *
34     * Searches articles using FULLTEXT search and logs the query for analytics
35     *
36     * @param string $query Search query
37     * @param int|null $storeId Store ID (optional, for multi-tenant tracking)
38     * @param int|null $userId User/Employee ID (optional)
39     * @param int $limit Maximum number of results
40     * @return array Array with 'results' (Article objects) and 'count' keys
41     */
42    public function search(string $query, ?int $storeId = null, ?int $userId = null, int $limit = 20): array
43    {
44        try {
45            // Perform the search using FULLTEXT
46            $stmt = $this->db->prepare("
47                SELECT
48                    a.*,
49                    c.name as categoryName,
50                    c.slug as categorySlug,
51                    c.icon as categoryIcon,
52                    MATCH(a.title, a.content_md) AGAINST(:query IN NATURAL LANGUAGE MODE) as relevance
53                FROM support_articles a
54                LEFT JOIN support_categories c ON a.category_id = c.id
55                WHERE a.status = 'published'
56                  AND MATCH(a.title, a.content_md) AGAINST(:query IN NATURAL LANGUAGE MODE)
57                ORDER BY relevance DESC, a.view_count DESC, a.published_at DESC
58                LIMIT :limit
59            ");
60
61            $stmt->bindValue(':query', $query);
62            $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
63            $stmt->execute();
64
65            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
66            $articles = [];
67
68            foreach ($rows as $row) {
69                $article = Article::fromArray($row);
70
71                // Add category object if available
72                if ($row['categoryName']) {
73                    $category = new Category();
74                    $category->id = $article->categoryId;
75                    $category->name = $row['categoryName'];
76                    $category->slug = $row['categorySlug'];
77                    $category->icon = $row['categoryIcon'];
78                    $article->category = $category;
79                }
80
81                $articles[] = $article;
82            }
83
84            $resultsCount = count($articles);
85
86            // Log the search (async in real implementation, but keeping it simple)
87            $this->logSearch($storeId, $userId, $query, $results_count);
88
89            return [
90                'results' => $articles,
91                'count' => $resultsCount
92            ];
93        } catch (PDOException $e) {
94            error_log("SearchManager::search error: " . $e->getMessage());
95            return [
96                'results' => [],
97                'count' => 0
98            ];
99        }
100    }
101
102    /**
103     * Log a search query
104     *
105     * Records search queries for analytics and improvement insights
106     *
107     * @param int|null $storeId Store ID (optional)
108     * @param int|null $userId User/Employee ID (optional)
109     * @param string $query Search query
110     * @param int $resultsCount Number of results returned
111     * @return bool True if successful
112     */
113    public function logSearch(?int $storeId, ?int $userId, string $query, int $results_count): bool
114    {
115        try {
116            $stmt = $this->db->prepare("
117                INSERT INTO support_search_log
118                (store_id, user_id, query, results_count)
119                VALUES
120                (:storeId, :userId, :query, :results_count)
121            ");
122
123            return $stmt->execute([
124                ':storeId' => $storeId,
125                ':userId' => $userId,
126                ':query' => trim($query),
127                ':resultsCount' => $resultsCount
128            ]);
129        } catch (PDOException $e) {
130            // Don't fail the search if logging fails
131            error_log("SearchManager::logSearch error: " . $e->getMessage());
132            return false;
133        }
134    }
135
136    /**
137     * Get queries that returned no results
138     *
139     * Returns list of search queries that didn't find any articles,
140     * useful for identifying content gaps in the knowledge base
141     *
142     * @param int $days Number of days to look back
143     * @param int $limit Maximum number of queries to return
144     * @return array Array of queries with counts
145     */
146    public function getNoResultsQueries(int $days = 30, int $limit = 50): array
147    {
148        try {
149            $stmt = $this->db->prepare("
150                SELECT
151                    query,
152                    COUNT(*) as searchCount,
153                    MAX(created_at) as lastSearched
154                FROM support_search_log
155                WHERE results_count = 0
156                  AND created_at >= DATE_SUB(NOW(), INTERVAL :days DAY)
157                GROUP BY query
158                ORDER BY searchCount DESC, lastSearched DESC
159                LIMIT :limit
160            ");
161
162            $stmt->bindValue(':days', $days, PDO::PARAM_INT);
163            $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
164            $stmt->execute();
165
166            return $stmt->fetchAll(PDO::FETCH_ASSOC);
167        } catch (PDOException $e) {
168            error_log("SearchManager::getNoResultsQueries error: " . $e->getMessage());
169            return [];
170        }
171    }
172
173    /**
174     * Get popular search queries
175     *
176     * Returns most frequently searched queries with results,
177     * useful for identifying popular topics and trends
178     *
179     * @param int $days Number of days to look back
180     * @param int $limit Maximum number of queries to return
181     * @return array Array of queries with counts
182     */
183    public function getPopularSearches(int $days = 30, int $limit = 20): array
184    {
185        try {
186            $stmt = $this->db->prepare("
187                SELECT
188                    query,
189                    COUNT(*) as searchCount,
190                    AVG(results_count) as avgResults,
191                    MAX(created_at) as lastSearched
192                FROM support_search_log
193                WHERE results_count > 0
194                  AND created_at >= DATE_SUB(NOW(), INTERVAL :days DAY)
195                GROUP BY query
196                ORDER BY searchCount DESC, lastSearched DESC
197                LIMIT :limit
198            ");
199
200            $stmt->bindValue(':days', $days, PDO::PARAM_INT);
201            $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
202            $stmt->execute();
203
204            return $stmt->fetchAll(PDO::FETCH_ASSOC);
205        } catch (PDOException $e) {
206            error_log("SearchManager::getPopularSearches error: " . $e->getMessage());
207            return [];
208        }
209    }
210
211    /**
212     * Get search analytics summary
213     *
214     * Returns aggregated search statistics for a given time period
215     *
216     * @param int $days Number of days to look back
217     * @return array Associative array with analytics data
218     */
219    public function getSearchAnalytics(int $days = 30): array
220    {
221        try {
222            $stmt = $this->db->prepare("
223                SELECT
224                    COUNT(*) as totalSearches,
225                    COUNT(DISTINCT query) as uniqueQueries,
226                    SUM(CASE WHEN results_count = 0 THEN 1 ELSE 0 END) as noResultsCount,
227                    AVG(results_count) as avgResults
228                FROM support_search_log
229                WHERE createdAt >= DATE_SUB(NOW(), INTERVAL :days DAY)
230            ");
231
232            $stmt->bindValue(':days', $days, PDO::PARAM_INT);
233            $stmt->execute();
234
235            $row = $stmt->fetch(PDO::FETCH_ASSOC);
236
237            if (!$row) {
238                return [
239                    'totalSearches' => 0,
240                    'uniqueQueries' => 0,
241                    'noResultsCount' => 0,
242                    'noResultsPercentage' => 0,
243                    'avgResults' => 0
244                ];
245            }
246
247            $totalSearches = (int) $row['totalSearches'];
248            $noResultsCount = (int) $row['noResultsCount'];
249
250            return [
251                'totalSearches' => $totalSearches,
252                'uniqueQueries' => (int) $row['uniqueQueries'],
253                'noResultsCount' => $noResultsCount,
254                'noResultsPercentage' => $totalSearches > 0 ? round(($noResultsCount / $totalSearches) * 100, 2) : 0,
255                'avgResults' => round((float) $row['avgResults'], 2)
256            ];
257        } catch (PDOException $e) {
258            error_log("SearchManager::getSearchAnalytics error: " . $e->getMessage());
259            return [
260                'totalSearches' => 0,
261                'uniqueQueries' => 0,
262                'noResultsCount' => 0,
263                'noResultsPercentage' => 0,
264                'avgResults' => 0
265            ];
266        }
267    }
268}