diff --git a/includes/class-algolia-search.php b/includes/class-algolia-search.php index b1234186..c06dc03f 100644 --- a/includes/class-algolia-search.php +++ b/includes/class-algolia-search.php @@ -2,10 +2,15 @@ class Algolia_Search { + /** + * @var array + */ + private $current_page_hits = []; + /** * @var int */ - private $nb_hits; + private $total_hits; /** * @var Algolia_Index @@ -18,7 +23,9 @@ class Algolia_Search { public function __construct( Algolia_Index $index ) { $this->index = $index; + add_action( 'loop_start', [ $this, 'begin_highlighting' ] ); add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) ); + add_action( 'wp_head', [ $this, 'output_highlighting_bundled_styles' ] ); } /** @@ -56,6 +63,8 @@ public function pre_get_posts( WP_Query $query ) { 'attributesToRetrieve' => 'post_id', 'hitsPerPage' => $posts_per_page, 'page' => $current_page - 1, // Algolia pages are zero indexed. + 'highlightPreTag' => '<em class="algolia-search-highlight">', + 'highlightPostTag' => '</em>', ) ); @@ -73,9 +82,14 @@ public function pre_get_posts( WP_Query $query ) { add_filter( 'found_posts', array( $this, 'found_posts' ), 10, 2 ); add_filter( 'posts_search', array( $this, 'posts_search' ), 10, 2 ); + // Store the current page hits, so that we can use them for highlighting later on. + foreach ( $results['hits'] as $hit ) { + $this->current_page_hits[ $hit['post_id'] ] = $hit; + } + // Store the total number of its, so that we can hook into the `found_posts`. // This is useful for pagination. - $this->nb_hits = $results['nbHits']; + $this->total_hits = $results['nbHits']; $post_ids = array(); foreach ( $results['hits'] as $result ) { @@ -117,7 +131,7 @@ public function pre_get_posts( WP_Query $query ) { * @return int */ public function found_posts( $found_posts, WP_Query $query ) { - return $this->should_filter_query( $query ) ? $this->nb_hits : $found_posts; + return $this->should_filter_query( $query ) ? $this->total_hits : $found_posts; } /** @@ -134,4 +148,108 @@ public function found_posts( $found_posts, WP_Query $query ) { public function posts_search( $search, WP_Query $query ) { return $this->should_filter_query( $query ) ? '' : $search; } + + /** + * Output the bundled styles for highlighting search result matches, if enabled. + */ + public function output_highlighting_bundled_styles() { + if ( ! $this->highlighting_enabled() ) { + return; + } + + if ( ! apply_filters( 'algolia_search_highlighting_enable_bundled_styles', true ) ) { + return; + } + + ?> + <style> + .algolia-search-highlight { + background-color: #fffbcc; + border-radius: 2px; + font-style: normal; + } + </style> + <?php + } + + /** + * Begin highlighting search result matches, if enabled. + * + * This method is called on the loop_start action, where we want to begin highlighting search result matches. + * + * @param WP_Query $query + */ + public function begin_highlighting( $query ) { + if ( ! $this->should_filter_query( $query ) ) { + return; + } + + if ( ! $this->highlighting_enabled() ) { + return; + } + + add_filter( 'the_title', [ $this, 'highlight_the_title' ], 10, 2 ); + add_filter( 'get_the_excerpt', [ $this, 'highlight_get_the_excerpt' ], 10, 2 ); + + add_action( 'loop_end', [ $this, 'end_highlighting' ] ); + } + + /** + * Stop highlighting search result matches. + * + * This method is called on the loop_end action, where we want to stop highlighting search result matches. + * + * @param WP_Query $query + */ + public function end_highlighting( $query ) { + remove_filter( 'the_title', [ $this, 'highlight_the_title' ], 10 ); + remove_filter( 'get_the_excerpt', [ $this, 'highlight_get_the_excerpt' ], 10 ); + + remove_action( 'loop_end', [ $this, 'end_highlighting' ] ); + } + + /** + * Filter the_title, replacing it with the highlighted title from the Algolia index. + * + * @param string $title + * @param int $post_id + * + * @return string + */ + public function highlight_the_title( $title, $post_id ) { + $highlighted_title = $this->current_page_hits[ $post_id ]['_highlightResult']['post_title']['value'] ?? null; + + if ( ! empty( $highlighted_title ) ) { + $title = $highlighted_title; + } + + return $title; + } + + /** + * Filter get_the_excerpt, replacing it with the highlighted excerpt from the Algolia index. + * + * @param string $excerpt + * @param WP_Post $post + * + * @return string + */ + public function highlight_get_the_excerpt( $excerpt, $post ) { + $highlighted_excerpt = $this->current_page_hits[ $post->ID ]['_snippetResult']['content']['value'] ?? null; + + if ( ! empty( $highlighted_excerpt ) ) { + $excerpt = $highlighted_excerpt; + } + + return $excerpt; + } + + /** + * Determine whether highlighting is enabled. + * + * @return bool + */ + private function highlighting_enabled() : bool { + return apply_filters( 'algolia_search_highlighting_enabled', true ); + } } diff --git a/includes/indices/class-algolia-searchable-posts-index.php b/includes/indices/class-algolia-searchable-posts-index.php index 1c664e4f..79ac85fe 100644 --- a/includes/indices/class-algolia-searchable-posts-index.php +++ b/includes/indices/class-algolia-searchable-posts-index.php @@ -201,7 +201,7 @@ protected function get_settings() { ), 'attributesToSnippet' => array( 'post_title:30', - 'content:30', + 'content:' . intval( apply_filters( 'excerpt_length', 55 ) ), ), 'snippetEllipsisText' => '…', );