From 4c9f00d5dd5124e4a0380141c515ba52397d465e Mon Sep 17 00:00:00 2001 From: Siddharth Thevaril Date: Mon, 23 Feb 2026 01:08:00 +0530 Subject: [PATCH 1/4] add filter hook to filter post types --- .../components/facets/post-type-facet.js | 6 +++- assets/js/instant-results/config.js | 2 ++ .../Feature/InstantResults/InstantResults.php | 31 ++++++++++++++++++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/assets/js/instant-results/components/facets/post-type-facet.js b/assets/js/instant-results/components/facets/post-type-facet.js index a04f714120..5a92024ae9 100644 --- a/assets/js/instant-results/components/facets/post-type-facet.js +++ b/assets/js/instant-results/components/facets/post-type-facet.js @@ -8,7 +8,7 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies. */ import { useApiSearch } from '../../../api-search'; -import { postTypeLabels } from '../../config'; +import { excludedPostTypes, postTypeLabels } from '../../config'; import CheckboxList from '../common/checkbox-list'; import Panel from '../common/panel'; import { ActiveConstraint } from '../tools/active-constraints'; @@ -44,6 +44,10 @@ export default ({ defaultIsOpen, label }) => { return options; } + if (excludedPostTypes?.includes(key)) { + return options; + } + options.push({ checked: selectedPostTypes.includes(key), count: doc_count, diff --git a/assets/js/instant-results/config.js b/assets/js/instant-results/config.js index 17185aa62c..f9d4a36790 100644 --- a/assets/js/instant-results/config.js +++ b/assets/js/instant-results/config.js @@ -11,6 +11,7 @@ const { apiHost, argsSchema, currencyCode, + excludedPostTypes, facets, isWooCommerce, locale, @@ -71,6 +72,7 @@ export { apiHost, argsSchema, currencyCode, + excludedPostTypes, facets, isWooCommerce, isNumberedPagination, diff --git a/includes/classes/Feature/InstantResults/InstantResults.php b/includes/classes/Feature/InstantResults/InstantResults.php index ebf5985964..b9240b9d52 100644 --- a/includes/classes/Feature/InstantResults/InstantResults.php +++ b/includes/classes/Feature/InstantResults/InstantResults.php @@ -232,6 +232,7 @@ public function enqueue_frontend_assets() { 'apiHost' => ( 0 !== strpos( $api_endpoint, 'http' ) ) ? esc_url_raw( $this->host ) : '', 'argsSchema' => $this->get_args_schema(), 'currencyCode' => $this->is_woocommerce ? get_woocommerce_currency() : false, + 'excludedPostTypes' => $this->get_excluded_post_types(), 'facets' => $this->get_facets_for_frontend(), 'highlightTag' => $this->settings['highlight_tag'], 'isWooCommerce' => $this->is_woocommerce, @@ -278,7 +279,10 @@ public function get_template_endpoint(): string { * @return string The search template as JSON. */ public function get_search_template(): string { - $post_types = Features::factory()->get_registered_feature( 'search' )->get_searchable_post_types(); + $post_types = Features::factory()->get_registered_feature( 'search' )->get_searchable_post_types(); + $excluded_post_types = $this->get_excluded_post_types(); + $post_types = array_values( array_diff( $post_types, $excluded_post_types ) ); + $post_statuses = get_post_stati( [ 'public' => true, @@ -584,6 +588,31 @@ public function get_post_type_labels() { return $labels; } + /** + * Get post type slugs to exclude from the Instant Results post type filter. + * + * @since 5.4.0 + * @return array Array of post type slugs to exclude. + */ + public function get_excluded_post_types() { + /** + * Filter post type slugs to exclude from Instant Results. + * + * Excluded post types are hidden from the Post Type facet and + * removed from the search template so their content does not + * appear in results. The search template is generated and + * uploaded to Elasticsearch when Instant Results settings are + * saved, so after changing this filter, re-save the Instant + * Results or Weighting settings to regenerate the template. + * + * @hook ep_instant_results_excluded_post_types + * @since 5.4.0 + * @param {string[]} $excluded Post type slugs to exclude (e.g., ['page']). + * @return {string[]} Filtered exclusions. + */ + return apply_filters( 'ep_instant_results_excluded_post_types', [] ); + } + /** * Get available facets. * From d8683b25972e462e1768d228ab6ffaa25e528813 Mon Sep 17 00:00:00 2001 From: Siddharth Thevaril Date: Mon, 23 Feb 2026 01:28:56 +0530 Subject: [PATCH 2/4] add filter hook to filter exclude taxonomy terms --- .../components/facets/taxonomy-terms-facet.js | 6 ++++- assets/js/instant-results/config.js | 2 ++ .../Feature/InstantResults/InstantResults.php | 23 +++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/assets/js/instant-results/components/facets/taxonomy-terms-facet.js b/assets/js/instant-results/components/facets/taxonomy-terms-facet.js index c2d7f7df8e..3f39d35339 100644 --- a/assets/js/instant-results/components/facets/taxonomy-terms-facet.js +++ b/assets/js/instant-results/components/facets/taxonomy-terms-facet.js @@ -10,7 +10,7 @@ import { applyFilters } from '@wordpress/hooks'; * Internal dependencies. */ import { useApiSearch } from '../../../api-search'; -import { facets, postTypeLabels } from '../../config'; +import { excludedTermIds, facets, postTypeLabels } from '../../config'; import CheckboxList from '../common/checkbox-list'; import Panel from '../common/panel'; import { ActiveConstraint } from '../tools/active-constraints'; @@ -64,6 +64,10 @@ export default ({ defaultIsOpen, label, postTypes, name }) => { (options, { doc_count, key }) => { const { name: label, parent, term_id, term_order } = JSON.parse(key); + if (excludedTermIds?.includes(term_id)) { + return options; + } + options.push({ checked: selectedTerms.includes(term_id), count: doc_count, diff --git a/assets/js/instant-results/config.js b/assets/js/instant-results/config.js index f9d4a36790..dff86fe869 100644 --- a/assets/js/instant-results/config.js +++ b/assets/js/instant-results/config.js @@ -12,6 +12,7 @@ const { argsSchema, currencyCode, excludedPostTypes, + excludedTermIds, facets, isWooCommerce, locale, @@ -73,6 +74,7 @@ export { argsSchema, currencyCode, excludedPostTypes, + excludedTermIds, facets, isWooCommerce, isNumberedPagination, diff --git a/includes/classes/Feature/InstantResults/InstantResults.php b/includes/classes/Feature/InstantResults/InstantResults.php index b9240b9d52..b58da81f84 100644 --- a/includes/classes/Feature/InstantResults/InstantResults.php +++ b/includes/classes/Feature/InstantResults/InstantResults.php @@ -233,6 +233,7 @@ public function enqueue_frontend_assets() { 'argsSchema' => $this->get_args_schema(), 'currencyCode' => $this->is_woocommerce ? get_woocommerce_currency() : false, 'excludedPostTypes' => $this->get_excluded_post_types(), + 'excludedTermIds' => $this->get_excluded_term_ids(), 'facets' => $this->get_facets_for_frontend(), 'highlightTag' => $this->settings['highlight_tag'], 'isWooCommerce' => $this->is_woocommerce, @@ -613,6 +614,28 @@ public function get_excluded_post_types() { return apply_filters( 'ep_instant_results_excluded_post_types', [] ); } + /** + * Get term IDs to exclude from all Instant Results taxonomy filters. + * + * @since 5.4.0 + * @return int[] Term IDs to exclude. + */ + public function get_excluded_term_ids() { + /** + * Filter term IDs to exclude from all Instant Results taxonomy filters. + * + * Excluded terms are hidden from every taxonomy facet + * (Category, Tag, etc.) but posts associated with those + * terms still appear in search results. + * + * @hook ep_instant_results_excluded_term_ids + * @since 5.4.0 + * @param {int[]} $excluded Term IDs to exclude (e.g., [1, 120]). + * @return {int[]} Filtered exclusions. + */ + return apply_filters( 'ep_instant_results_excluded_term_ids', [] ); + } + /** * Get available facets. * From 5f364346140c41aabf1e65df291552c90ca72a4d Mon Sep 17 00:00:00 2001 From: Siddharth Thevaril Date: Wed, 29 Apr 2026 00:10:43 +0530 Subject: [PATCH 3/4] add unit tests --- tests/php/features/TestInstantResults.php | 47 ++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/tests/php/features/TestInstantResults.php b/tests/php/features/TestInstantResults.php index 5d5f046c38..bb222643b4 100644 --- a/tests/php/features/TestInstantResults.php +++ b/tests/php/features/TestInstantResults.php @@ -221,6 +221,51 @@ function () { $feature->after_update_feature( 'instant-results', [], [ 'active' => false ] ); } + /** + * Test that excluded post types are removed from the search template. + * + * @group instant-results + * @since 5.4.0 + */ + public function test_search_template_excludes_post_types() { + $feature = \ElasticPress\Features::factory()->get_registered_feature( 'instant-results' ); + + $baseline = $feature->get_search_template(); + $this->assertStringContainsString( '"page"', $baseline ); + + $post_types_filter = function () { + return [ 'page' ]; + }; + add_filter( 'ep_instant_results_excluded_post_types', $post_types_filter ); + + $template = $feature->get_search_template(); + + remove_filter( 'ep_instant_results_excluded_post_types', $post_types_filter ); + + $this->assertStringNotContainsString( '"page"', $template ); + } + + /** + * Test the get_excluded_term_ids method. + * + * @group instant-results + * @since 5.4.0 + */ + public function test_get_excluded_term_ids() { + $feature = \ElasticPress\Features::factory()->get_registered_feature( 'instant-results' ); + + $term_ids_filter = function () { + return [ 1, 42 ]; + }; + add_filter( 'ep_instant_results_excluded_term_ids', $term_ids_filter ); + + $excluded = $feature->get_excluded_term_ids(); + + remove_filter( 'ep_instant_results_excluded_term_ids', $term_ids_filter ); + + $this->assertSame( [ 1, 42 ], $excluded ); + } + /** * Ensure invalid taxonomy values returned from ep_facet_include_taxonomies * are skipped and logged via _doing_it_wrong(). @@ -266,7 +311,7 @@ public function test_invalid_taxonomy_from_facet_filter_triggers_doing_it_wrong_ $calls[0]['function'] ); - $message = html_entity_decode( $calls[0]['message'] ); + $message = html_entity_decode( $calls[0]['message'], ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 ); $this->assertStringContainsString( 'not-a-real-taxonomy', $message ); $this->assertStringContainsString( 'Invalid taxonomy', $message ); From 03f7a1675f1ea2fb1c3e0cd6bb2553011f3ea1cd Mon Sep 17 00:00:00 2001 From: Siddharth Thevaril Date: Wed, 29 Apr 2026 00:50:05 +0530 Subject: [PATCH 4/4] add e2e tests --- .wp-env.json | 2 + tests/e2e/src/specs/instant-results.spec.ts | 85 +++++++++++++++++++ ...er-instant-results-excluded-post-types.php | 18 ++++ ...lter-instant-results-excluded-term-ids.php | 21 +++++ 4 files changed, 126 insertions(+) create mode 100644 tests/e2e/src/wordpress-files/test-plugins/filter-instant-results-excluded-post-types.php create mode 100644 tests/e2e/src/wordpress-files/test-plugins/filter-instant-results-excluded-term-ids.php diff --git a/.wp-env.json b/.wp-env.json index 9fcd523a6a..e9d2ba905f 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -33,6 +33,8 @@ "wp-content/plugins/fake-log-messages.php": "./tests/e2e/src/wordpress-files/test-plugins/fake-log-messages.php", "wp-content/plugins/fake-new-activation.php": "./tests/e2e/src/wordpress-files/test-plugins/fake-new-activation.php", "wp-content/plugins/filter-instant-results-category-terms.php": "./tests/e2e/src/wordpress-files/test-plugins/filter-instant-results-category-terms.php", + "wp-content/plugins/filter-instant-results-excluded-post-types.php": "./tests/e2e/src/wordpress-files/test-plugins/filter-instant-results-excluded-post-types.php", + "wp-content/plugins/filter-instant-results-excluded-term-ids.php": "./tests/e2e/src/wordpress-files/test-plugins/filter-instant-results-excluded-term-ids.php", "wp-content/plugins/filter-instant-results-per-page.php": "./tests/e2e/src/wordpress-files/test-plugins/filter-instant-results-per-page.php", "wp-content/plugins/filter-instant-results-args-schema.php": "./tests/e2e/src/wordpress-files/test-plugins/filter-instant-results-args-schema.php", "wp-content/plugins/filter-autosuggest-navigate-callback.php": "./tests/e2e/src/wordpress-files/test-plugins/filter-autosuggest-navigate-callback.php", diff --git a/tests/e2e/src/specs/instant-results.spec.ts b/tests/e2e/src/specs/instant-results.spec.ts index 6a40945cbc..1197f1d4e9 100644 --- a/tests/e2e/src/specs/instant-results.spec.ts +++ b/tests/e2e/src/specs/instant-results.spec.ts @@ -598,6 +598,91 @@ test.describe('Instant Results Feature', { tag: '@group1' }, () => { 'wpCli', ); }); + + test('Is possible to exclude post types via ep_instant_results_excluded_post_types', async ({ + loggedInPage, + }) => { + /** + * Activate test plugin and regenerate the search template so the + * excluded post type is dropped from the template. + */ + await maybeEnableFeature('instant-results'); + await setCustomPostTypes(); + await activatePlugin( + loggedInPage, + 'filter-instant-results-excluded-post-types', + 'wpCli', + ); + await wpCli('elasticpress put-search-template', true); + + /** + * Perform a search. + */ + const responsePromise = instantResultRequestPromise(loggedInPage, 'search=post'); + await loggedInPage.goto('/'); + await searchFor(loggedInPage, 'post'); + await responsePromise; + + /** + * The excluded post type should not appear in the Post Type facet. + */ + await expect(loggedInPage.locator('#ep-search-post-type-page')).toHaveCount(0); + + await deactivatePlugin( + loggedInPage, + 'filter-instant-results-excluded-post-types', + 'wpCli', + ); + await wpCli('elasticpress put-search-template', true); + }); + + test('Is possible to exclude term IDs via ep_instant_results_excluded_term_ids', async ({ + loggedInPage, + }) => { + /** + * Activate test plugin. + */ + await maybeEnableFeature('instant-results'); + await setCustomPostTypes(); + await activatePlugin( + loggedInPage, + 'filter-instant-results-excluded-term-ids', + 'wpCli', + ); + + await goToAdminPage(loggedInPage, 'admin.php?page=elasticpress'); + const apiResponsePromise = loggedInPage.waitForResponse( + '**/wp-json/elasticpress/v1/features*', + ); + + await loggedInPage.getByRole('button', { name: 'Live Search' }).click(); + await loggedInPage.getByRole('button', { name: 'Instant Results' }).click(); + await addInstantResultFilter(loggedInPage, '(category)'); + await loggedInPage.getByRole('button', { name: 'Save changes' }).click(); + + await apiResponsePromise; + + /** + * Perform a search. + */ + const responsePromise = instantResultRequestPromise(loggedInPage, 'search=block'); + await loggedInPage.goto('/'); + await searchFor(loggedInPage, 'block'); + await responsePromise; + + /** + * The excluded term should not appear in the category facet. + */ + await expect( + loggedInPage.locator('[id^="ep-search-tax-category-"]', { hasText: 'Markup' }), + ).toHaveCount(0); + + await deactivatePlugin( + loggedInPage, + 'filter-instant-results-excluded-term-ids', + 'wpCli', + ); + }); }); test('Is possible to filter the arguments schema', async ({ loggedInPage }) => { diff --git a/tests/e2e/src/wordpress-files/test-plugins/filter-instant-results-excluded-post-types.php b/tests/e2e/src/wordpress-files/test-plugins/filter-instant-results-excluded-post-types.php new file mode 100644 index 0000000000..eccd15ac04 --- /dev/null +++ b/tests/e2e/src/wordpress-files/test-plugins/filter-instant-results-excluded-post-types.php @@ -0,0 +1,18 @@ +term_id; + } + return $excluded; + } +);