From 86cdf0bc015ce90f6c62c53c2809765ee188dd06 Mon Sep 17 00:00:00 2001 From: Siddharth Thevaril Date: Sat, 21 Feb 2026 05:35:16 +0530 Subject: [PATCH] add Weight related posts by date setting --- .../Feature/RelatedPosts/RelatedPosts.php | 176 +++++++++++++++ tests/php/features/TestRelatedPosts.php | 209 ++++++++++++++++++ 2 files changed, 385 insertions(+) diff --git a/includes/classes/Feature/RelatedPosts/RelatedPosts.php b/includes/classes/Feature/RelatedPosts/RelatedPosts.php index 252715a6a4..586a7c48ce 100644 --- a/includes/classes/Feature/RelatedPosts/RelatedPosts.php +++ b/includes/classes/Feature/RelatedPosts/RelatedPosts.php @@ -30,6 +30,10 @@ public function __construct() { $this->requires_install_reindex = false; + $this->default_settings = [ + 'decaying_enabled' => '0', + ]; + parent::__construct(); } @@ -134,6 +138,7 @@ public function get_related_query( $post_id, $post_return = 5 ) { 'posts_per_page' => $post_return, 'ep_integrate' => true, 'ignore_sticky_posts' => true, + 'orderby' => 'relevance', ); /** @@ -167,6 +172,176 @@ public function find_related( $post_id, $post_return = 5 ) { return $query->posts; } + /** + * Set the `settings_schema` attribute. + * + * @since 5.4.0 + */ + protected function set_settings_schema() { + $this->settings_schema = [ + [ + 'default' => '0', + 'help' => __( 'When enabled, newer content will be favored over older content in related posts results.', 'elasticpress' ), + 'key' => 'decaying_enabled', + 'label' => __( 'Weight related posts by date', 'elasticpress' ), + 'type' => 'checkbox', + ], + ]; + } + + /** + * Returns whether date decay is enabled for related posts. + * + * @since 5.4.0 + * @param array $args WP_Query args. + * @return bool + */ + public function is_decaying_enabled( $args = [] ) { + $settings = $this->get_settings(); + + $is_decaying_enabled = ! empty( $settings['decaying_enabled'] ) && '0' !== $settings['decaying_enabled']; + + /** + * Filter to enable or disable decay by date for related posts. + * + * @hook ep_related_posts_is_decaying_enabled + * @since 5.4.0 + * @param {bool} $is_decaying_enabled Whether decay by date is enabled or not. + * @param {array} $settings Related Posts feature settings. + * @param {array} $args WP_Query args. + * @return {bool} New value. + */ + return apply_filters( 'ep_related_posts_is_decaying_enabled', $is_decaying_enabled, $settings, $args ); + } + + /** + * Weight more recent content in related posts results. + * + * @param array $formatted_args Formatted ES args. + * @param array $args WP_Query args. + * @since 5.4.0 + * @return array + */ + public function weight_recent( $formatted_args, $args ) { + if ( empty( $args['more_like'] ) ) { + return $formatted_args; + } + + if ( ! $this->is_decaying_enabled( $args ) ) { + return $formatted_args; + } + + /** + * Filter related posts date weighting decay function. + * + * @hook epwr_related_posts_decay_function + * @since 5.4.0 + * @param {string} $decay_function Current decay function. + * @param {array} $formatted_args Formatted Elasticsearch arguments. + * @param {array} $args WP_Query arguments. + * @return {string} New decay function. + */ + $decay_function = apply_filters( 'epwr_related_posts_decay_function', 'exp', $formatted_args, $args ); + + /** + * Filter related posts date weighting field. + * + * @hook epwr_related_posts_decay_field + * @since 5.4.0 + * @param {string} $field Current decay field. + * @param {array} $formatted_args Formatted Elasticsearch arguments. + * @param {array} $args WP_Query arguments. + * @return {string} New decay field. + */ + $field = apply_filters( 'epwr_related_posts_decay_field', 'post_date_gmt', $formatted_args, $args ); + + $date_score = array( + 'function_score' => array( + 'query' => $formatted_args['query'], + 'functions' => array( + array( + $decay_function => array( + $field => array( + /** + * Filter related posts date weighting scale. + * + * @hook epwr_related_posts_scale + * @since 5.4.0 + * @param {string} $scale Current scale. + * @param {array} $formatted_args Formatted Elasticsearch arguments. + * @param {array} $args WP_Query arguments. + * @return {string} New scale. + */ + 'scale' => apply_filters( 'epwr_related_posts_scale', '360d', $formatted_args, $args ), + /** + * Filter related posts date weighting decay. + * + * @hook epwr_related_posts_decay + * @since 5.4.0 + * @param {float} $decay Current decay. + * @param {array} $formatted_args Formatted Elasticsearch arguments. + * @param {array} $args WP_Query arguments. + * @return {float} New decay. + */ + 'decay' => apply_filters( 'epwr_related_posts_decay', 0.5, $formatted_args, $args ), + /** + * Filter related posts date weighting offset. + * + * @hook epwr_related_posts_offset + * @since 5.4.0 + * @param {string} $offset Current offset. + * @param {array} $formatted_args Formatted Elasticsearch arguments. + * @param {array} $args WP_Query arguments. + * @return {string} New offset. + */ + 'offset' => apply_filters( 'epwr_related_posts_offset', '30d', $formatted_args, $args ), + ), + ), + ), + array( + /** + * Filter related posts date weight. + * + * @hook epwr_related_posts_weight + * @since 5.4.0 + * @param {float} $weight Current weight. + * @param {array} $formatted_args Formatted Elasticsearch arguments. + * @param {array} $args WP_Query arguments. + * @return {float} New weight. + */ + 'weight' => apply_filters( 'epwr_related_posts_weight', 0.001, $formatted_args, $args ), + ), + ), + /** + * Filter related posts date weighting score mode. + * + * @hook epwr_related_posts_score_mode + * @since 5.4.0 + * @param {string} $score_mode Current score mode. + * @param {array} $formatted_args Formatted Elasticsearch arguments. + * @param {array} $args WP_Query arguments. + * @return {string} New score mode. + */ + 'score_mode' => apply_filters( 'epwr_related_posts_score_mode', 'sum', $formatted_args, $args ), + /** + * Filter related posts date weighting boost mode. + * + * @hook epwr_related_posts_boost_mode + * @since 5.4.0 + * @param {string} $boost_mode Current boost mode. + * @param {array} $formatted_args Formatted Elasticsearch arguments. + * @param {array} $args WP_Query arguments. + * @return {string} New boost mode. + */ + 'boost_mode' => apply_filters( 'epwr_related_posts_boost_mode', 'multiply', $formatted_args, $args ), + ), + ); + + $formatted_args['query'] = $date_score; + + return $formatted_args; + } + /** * Setup all feature filters * @@ -176,6 +351,7 @@ public function setup() { add_action( 'widgets_init', [ $this, 'register_widget' ] ); add_filter( 'widget_types_to_hide_from_legacy_widget_block', [ $this, 'hide_legacy_widget' ] ); add_filter( 'ep_formatted_args', [ $this, 'formatted_args' ], 10, 2 ); + add_filter( 'ep_formatted_args', [ $this, 'weight_recent' ], 11, 2 ); add_action( 'init', [ $this, 'register_block' ] ); add_action( 'rest_api_init', [ $this, 'setup_endpoint' ] ); } diff --git a/tests/php/features/TestRelatedPosts.php b/tests/php/features/TestRelatedPosts.php index 8c2d4a654b..3b1a4672c6 100644 --- a/tests/php/features/TestRelatedPosts.php +++ b/tests/php/features/TestRelatedPosts.php @@ -121,6 +121,215 @@ public function testGetRelatedQuery() { $this->assertEquals( $related_post_title, $query->posts[0]->post_title ); } + /** + * Test that date decay is disabled by default for related posts. + * + * @group related_posts + */ + public function testRelatedPostsDecayDisabledByDefault() { + $post_id = $this->ep_factory->post->create( array( 'post_content' => 'findme test 1' ) ); + $this->ep_factory->post->create( array( 'post_content' => 'findme test 2' ) ); + + ElasticPress\Elasticsearch::factory()->refresh_indices(); + + ElasticPress\Features::factory()->activate_feature( 'related_posts' ); + ElasticPress\Features::factory()->setup_features(); + + add_filter( 'ep_formatted_args', array( $this, 'catch_ep_formatted_args' ), 20 ); + + ElasticPress\Features::factory()->get_registered_feature( 'related_posts' )->find_related( $post_id ); + + $this->assertTrue( isset( $this->fired_actions['ep_formatted_args'] ) ); + + $query = $this->fired_actions['ep_formatted_args']['query']; + + // Should have more_like_this but NOT function_score. + $this->assertArrayHasKey( 'more_like_this', $query ); + $this->assertArrayNotHasKey( 'function_score', $query ); + } + + /** + * Test that date decay can be enabled for related posts. + * + * @group related_posts + */ + public function testRelatedPostsDecayEnabled() { + $post_id = $this->ep_factory->post->create( array( 'post_content' => 'findme test 1' ) ); + $this->ep_factory->post->create( array( 'post_content' => 'findme test 2' ) ); + + ElasticPress\Elasticsearch::factory()->refresh_indices(); + + ElasticPress\Features::factory()->activate_feature( 'related_posts' ); + ElasticPress\Features::factory()->update_feature( + 'related_posts', + array( + 'active' => true, + 'decaying_enabled' => '1', + ) + ); + ElasticPress\Features::factory()->setup_features(); + + add_filter( 'ep_formatted_args', array( $this, 'catch_ep_formatted_args' ), 20 ); + + ElasticPress\Features::factory()->get_registered_feature( 'related_posts' )->find_related( $post_id ); + + $this->assertTrue( isset( $this->fired_actions['ep_formatted_args'] ) ); + + $query = $this->fired_actions['ep_formatted_args']['query']; + + $this->assertDecayEnabled( $query ); + } + + /** + * Test the ep_related_posts_is_decaying_enabled filter. + * + * @group related_posts + */ + public function testRelatedPostsDecayFilter() { + ElasticPress\Features::factory()->activate_feature( 'related_posts' ); + ElasticPress\Features::factory()->setup_features(); + + $feature = ElasticPress\Features::factory()->get_registered_feature( 'related_posts' ); + + // Default is disabled. + $this->assertFalse( $feature->is_decaying_enabled() ); + + // Filter can enable it. + add_filter( 'ep_related_posts_is_decaying_enabled', '__return_true' ); + $this->assertTrue( $feature->is_decaying_enabled() ); + remove_filter( 'ep_related_posts_is_decaying_enabled', '__return_true' ); + + // Filter can disable it. + add_filter( 'ep_related_posts_is_decaying_enabled', '__return_false' ); + $this->assertFalse( $feature->is_decaying_enabled() ); + remove_filter( 'ep_related_posts_is_decaying_enabled', '__return_false' ); + } + + /** + * Test that without decay, related posts can return old posts first. + * + * @group related_posts + */ + public function testRelatedPostsWithoutDecayReturnsOldPosts() { + $shared_content = 'Elasticsearch search integration plugin relevance matching'; + + // The source post. + $source_id = $this->ep_factory->post->create( + array( + 'post_title' => 'Source Post', + 'post_content' => $shared_content, + 'post_date' => gmdate( 'Y-m-d H:i:s' ), + ) + ); + + // An old post with very similar content. + $old_post_id = $this->ep_factory->post->create( + array( + 'post_title' => 'Old Related Post', + 'post_content' => $shared_content . ' old content matching', + 'post_date' => gmdate( 'Y-m-d H:i:s', strtotime( '-3 years' ) ), + ) + ); + + // A recent post with very similar content. + $new_post_id = $this->ep_factory->post->create( + array( + 'post_title' => 'New Related Post', + 'post_content' => $shared_content . ' new content matching', + 'post_date' => gmdate( 'Y-m-d H:i:s', strtotime( '-1 day' ) ), + ) + ); + + ElasticPress\Elasticsearch::factory()->refresh_indices(); + + ElasticPress\Features::factory()->activate_feature( 'related_posts' ); + ElasticPress\Features::factory()->setup_features(); + + $related = ElasticPress\Features::factory()->get_registered_feature( 'related_posts' )->find_related( $source_id, 2 ); + + $this->assertCount( 2, $related ); + + // Without decay, both posts are returned (order based on content relevance only). + $related_ids = wp_list_pluck( $related, 'ID' ); + $this->assertContains( $old_post_id, $related_ids ); + $this->assertContains( $new_post_id, $related_ids ); + } + + /** + * Test that with decay enabled, newer related posts are favored. + * + * @group related_posts + */ + public function testRelatedPostsWithDecayFavorsNewerPosts() { + $shared_content = 'Elasticsearch search integration plugin relevance matching'; + + // The source post. + $source_id = $this->ep_factory->post->create( + array( + 'post_title' => 'Source Post', + 'post_content' => $shared_content, + 'post_date' => gmdate( 'Y-m-d H:i:s' ), + ) + ); + + // An old post with very similar content. + $this->ep_factory->post->create( + array( + 'post_title' => 'Old Related Post', + 'post_content' => $shared_content . ' old content matching', + 'post_date' => gmdate( 'Y-m-d H:i:s', strtotime( '-3 years' ) ), + ) + ); + + // A recent post with very similar content. + $new_post_id = $this->ep_factory->post->create( + array( + 'post_title' => 'New Related Post', + 'post_content' => $shared_content . ' new content matching', + 'post_date' => gmdate( 'Y-m-d H:i:s', strtotime( '-1 day' ) ), + ) + ); + + ElasticPress\Elasticsearch::factory()->refresh_indices(); + + ElasticPress\Features::factory()->activate_feature( 'related_posts' ); + ElasticPress\Features::factory()->update_feature( + 'related_posts', + array( + 'active' => true, + 'decaying_enabled' => '1', + ) + ); + ElasticPress\Features::factory()->setup_features(); + + // Use aggressive decay so the old post is clearly penalized. + add_filter( + 'epwr_related_posts_scale', + function () { + return '30d'; + } + ); + add_filter( + 'epwr_related_posts_offset', + function () { + return '7d'; + } + ); + add_filter( + 'epwr_related_posts_decay', + function () { + return 0.1; + } + ); + + $related = ElasticPress\Features::factory()->get_registered_feature( 'related_posts' )->find_related( $source_id, 2 ); + + $this->assertNotEmpty( $related ); + + // The newer post should be the first result. + $this->assertEquals( $new_post_id, $related[0]->ID ); + } + /** * Detect EP fire *