query( $wpdb->prepare( 'INSERT INTO %i (term, event_day, hits, results) VALUES (%s, %s, 1, %d) ON DUPLICATE KEY UPDATE hits = hits + 1, results = %d', $table, $term, $day, (int) $results, (int) $results ) ); wp_cache_delete( 'last_changed', Database::CACHE_GROUP ); } /** * Normalize a query for storage: lowercase, collapse whitespace, trim length. * * @param string $term Raw query. * @return string */ private static function normalize( $term ) { $term = trim( preg_replace( '/\s+/u', ' ', (string) $term ) ); $term = function_exists( 'mb_strtolower' ) ? mb_strtolower( $term ) : strtolower( $term ); $len = function_exists( 'mb_strlen' ) ? mb_strlen( $term ) : strlen( $term ); if ( $len < 2 ) { return ''; } // Column is VARCHAR(120); keep it inside that bound. return function_exists( 'mb_substr' ) ? mb_substr( $term, 0, 120 ) : substr( $term, 0, 120 ); } /** * Top search terms over the last N days. * * @param int $limit Rows. * @param int $days Days back. * @return array[] [ term, hits, results ]. */ public static function top( $limit = 10, $days = 30 ) { return self::query( $limit, $days, false ); } /** * Top search terms that returned zero results (content gaps). * * @param int $limit Rows. * @param int $days Days back. * @return array[] [ term, hits, results ]. */ public static function no_results( $limit = 10, $days = 30 ) { return self::query( $limit, $days, true ); } /** * Shared aggregate query. * * @param int $limit Rows. * @param int $days Days back. * @param bool $zero_only Restrict to zero-result terms. * @return array[] */ private static function query( $limit, $days, $zero_only ) { $last_changed = wp_cache_get_last_changed( Database::CACHE_GROUP ); $cache_key = 'searches:' . ( $zero_only ? 'zero:' : 'all:' ) . (int) $limit . ':' . (int) $days . ':' . $last_changed; $cached = wp_cache_get( $cache_key, Database::CACHE_GROUP ); if ( false !== $cached ) { return $cached; } global $wpdb; $table = Database::searches_table(); $since = gmdate( 'Y-m-d', time() - ( (int) $days * DAY_IN_SECONDS ) ); $limit = max( 1, min( 100, (int) $limit ) ); if ( $zero_only ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- reading the plugin's own analytics table has no WordPress API alternative; results are object-cached above. $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT term, SUM(hits) AS hits, MAX(results) AS results FROM %i WHERE event_day >= %s GROUP BY term HAVING MAX(results) = 0 ORDER BY hits DESC LIMIT %d', $table, $since, $limit ), ARRAY_A ); } else { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- reading the plugin's own analytics table has no WordPress API alternative; results are object-cached above. $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT term, SUM(hits) AS hits, MAX(results) AS results FROM %i WHERE event_day >= %s GROUP BY term ORDER BY hits DESC LIMIT %d', $table, $since, $limit ), ARRAY_A ); } $out = array(); foreach ( (array) $rows as $row ) { $out[] = array( 'term' => (string) $row['term'], 'hits' => (int) $row['hits'], 'results' => (int) $row['results'], ); } wp_cache_set( $cache_key, $out, Database::CACHE_GROUP, HOUR_IN_SECONDS ); return $out; } }