You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by tu...@apache.org on 2020/04/02 19:55:29 UTC

[nifi] branch master updated: NIFI-7188 Extending UI search with filters and refactoring existing solution

This is an automated email from the ASF dual-hosted git repository.

turcsanyi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/master by this push:
     new c0f5fcb  NIFI-7188 Extending UI search with filters and refactoring existing solution
c0f5fcb is described below

commit c0f5fcb48437e4d3c148bd2742a9fc92eb49630a
Author: Bence Simon <si...@gmail.com>
AuthorDate: Thu Apr 2 17:15:04 2020 +0200

    NIFI-7188 Extending UI search with filters and refactoring existing solution
    
    This closes #4123.
    
    Signed-off-by: Peter Turcsanyi <tu...@apache.org>
---
 nifi-docs/src/main/asciidoc/user-guide.adoc        |  103 +-
 .../nifi-framework/nifi-web/nifi-web-api/pom.xml   |   12 +
 .../org/apache/nifi/web/NiFiServiceFacade.java     |    3 +-
 .../apache/nifi/web/StandardNiFiServiceFacade.java |    4 +-
 .../java/org/apache/nifi/web/api/FlowResource.java |    7 +-
 .../nifi/web/controller/ControllerFacade.java      |   34 +-
 .../web/controller/ControllerSearchService.java    |  731 +++----------
 .../web/search/AttributeBasedComponentMatcher.java |   60 ++
 .../apache/nifi/web/search/ComponentMatcher.java   |   41 +
 .../nifi/web/search/ComponentMatcherFactory.java   |   88 ++
 .../search/attributematchers/AttributeMatcher.java |   56 +
 .../attributematchers/BackPressureMatcher.java     |   56 +
 .../web/search/attributematchers/BasicMatcher.java |   37 +
 .../attributematchers/ConnectionMatcher.java       |   39 +
 .../ConnectionRelationshipMatcher.java             |   31 +
 .../attributematchers/ConnectivityMatcher.java     |   42 +
 .../ControllerServiceNodeMatcher.java              |   41 +
 .../search/attributematchers/ExecutionMatcher.java |   36 +
 .../attributematchers/ExpirationMatcher.java       |   49 +
 .../search/attributematchers/ExtendedMatcher.java  |   38 +
 .../web/search/attributematchers/LabelMatcher.java |   37 +
 .../attributematchers/ParameterContextMatcher.java |   39 +
 .../search/attributematchers/ParameterMatcher.java |   42 +
 .../PortScheduledStateMatcher.java                 |   54 +
 .../attributematchers/PrioritiesMatcher.java       |   33 +
 .../attributematchers/ProcessGroupMatcher.java     |   41 +
 .../ProcessorMetadataMatcher.java                  |   33 +
 .../search/attributematchers/PropertyMatcher.java  |   67 ++
 .../attributematchers/PublicPortMatcher.java       |   42 +
 .../attributematchers/RelationshipMatcher.java     |   31 +
 .../RemoteProcessGroupMatcher.java                 |   41 +
 .../attributematchers/ScheduledStateMatcher.java   |   59 +
 .../attributematchers/SchedulingMatcher.java       |   54 +
 .../attributematchers/SearchableMatcher.java       |   65 ++
 .../search/attributematchers/TargetUriMatcher.java |   31 +
 .../TransmissionStatusMatcher.java                 |   48 +
 .../attributematchers/VariableRegistryMatcher.java |   44 +
 .../nifi/web/search/query/MapBasedSearchQuery.java |   69 ++
 .../web/search/query/RegexSearchQueryParser.java   |   60 ++
 .../apache/nifi/web/search/query/SearchQuery.java  |   72 ++
 .../nifi/web/search/query/SearchQueryParser.java   |   38 +
 .../AbstractComponentSearchResultEnricher.java     |   85 ++
 .../ComponentSearchResultEnricher.java             |   33 +
 .../ComponentSearchResultEnricherFactory.java      |   42 +
 .../GeneralComponentSearchResultEnricher.java      |   36 +
 .../ParameterSearchResultEnricher.java             |   38 +
 .../ProcessGroupSearchResultEnricher.java          |   37 +
 .../src/main/resources/nifi-web-api-context.xml    |  156 ++-
 .../AbstractControllerSearchIntegrationTest.java   |  361 +++++++
 .../nifi/web/controller/ComponentMockUtil.java     |  470 ++++++++
 .../nifi/web/controller/ControllerFacadeTest.java  |  111 ++
 .../ControllerSearchServiceFilterTest.java         |  239 +++++
 .../ControllerSearchServiceIntegrationTest.java    |  603 +++++++++++
 .../ControllerSearchServiceRegressionTest.java     |  605 +++++++++++
 .../controller/ControllerSearchServiceTest.java    | 1129 ++++++++++----------
 .../nifi/web/controller/SearchResultMatcher.java   |  124 +++
 .../search/AttributeBasedComponentMatcherTest.java |  127 +++
 .../AbstractAttributeMatcherTest.java              |   65 ++
 .../attributematchers/AttributeMatcherTest.java    |  171 +++
 .../attributematchers/BackPressureMatcherTest.java |   92 ++
 .../search/attributematchers/BasicMatcherTest.java |   53 +
 .../attributematchers/ConnectionMatcherTest.java   |   51 +
 .../ConnectionRelationshipMatcherTest.java         |   71 ++
 .../attributematchers/ConnectivityMatcherTest.java |   95 ++
 .../ControllerServiceNodeMatcherTest.java          |   55 +
 .../attributematchers/ExecutionMatcherTest.java    |   79 ++
 .../attributematchers/ExpirationMatcherTest.java   |  106 ++
 .../attributematchers/ExtendedMatcherTest.java     |   58 +
 .../search/attributematchers/LabelMatcherTest.java |   48 +
 .../ParameterContextMatcherTest.java               |   49 +
 .../attributematchers/ParameterMatcherTest.java    |   76 ++
 .../PortScheduledStateMatcherTest.java             |  140 +++
 .../attributematchers/PrioritiesMatcherTest.java   |   81 ++
 .../attributematchers/ProcessGroupMatcherTest.java |   55 +
 .../ProcessorMetadataMatcherTest.java              |  100 ++
 .../attributematchers/PropertyMatcherTest.java     |  113 ++
 .../attributematchers/PublicPortMatcherTest.java   |   70 ++
 .../attributematchers/RelationshipMatcherTest.java |   71 ++
 .../RemoteProcessGroupMatcherTest.java             |   56 +
 .../ScheduledStateMatcherTest.java                 |  194 ++++
 .../attributematchers/SchedulingMatcherTest.java   |  103 ++
 .../attributematchers/SearchableMatcherTest.java   |  117 ++
 .../attributematchers/TargetUriMatcherTest.java    |   49 +
 .../TransmissionStatusMatcherTest.java             |   92 ++
 .../VariableRegistryMatcherTest.java               |   91 ++
 .../search/query/RegexSearchQueryParserTest.java   |   89 ++
 .../ComponentSearchResultEnricherTest.java         |  146 +++
 .../test/resources/nifi-web-api-test-context.xml   |   32 +
 .../nf-ng-canvas-flow-status-controller.js         |    3 +-
 89 files changed, 8171 insertions(+), 1204 deletions(-)

diff --git a/nifi-docs/src/main/asciidoc/user-guide.adoc b/nifi-docs/src/main/asciidoc/user-guide.adoc
index 7e173d1..10a1c22 100644
--- a/nifi-docs/src/main/asciidoc/user-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/user-guide.adoc
@@ -161,8 +161,7 @@ The Operate Palette sits to the left-hand side of the screen. It consists of but
 used by DFMs to manage the flow, as well as by administrators who manage user access
 and configure system properties, such as how many system resources should be provided to the application.
 
-On the right side of the canvas is Search, and the Global Menu. You can use Search to easily find components on the
-canvas and to search by component name, type, identifier, configuration properties, and their values. The Global Menu
+On the right side of the canvas is Search, and the Global Menu. For more information on search refer to <<search>>. The Global Menu
 contains options that allow you to manipulate existing components on the canvas:
 
 image::global-menu.png[NiFi Global Menu]
@@ -1652,6 +1651,106 @@ and select "Align horizontally" to achieve these results:
 
 image:align-horizontally-after.png["Align Horizontally Example Before"]
 
+[[search]]
+== Search Components in DataFlow
+
+NiFi UI provides searching functionality in order to help easily find components on the canvas. You can use search to find components by name, type, identifier, configuration properties, and their values. Search also makes it possible to refine and narrow the search result based on certain conditions using Filters and Keywords.
+
+[caption="Example 1: "]
+.The result will contain components that match for "processor1".
+=====================================================================
+processor1
+=====================================================================
+
+=== Filters
+
+Filters can be added to the search box as key-value pairs where the keys are predefined and check certain conditions based on the given value. The syntax is "key:value".
+
+[caption="Example 2: "]
+.The search will be executed under Process Groups (directly or via contained Process Groups) containing the string "myGroup" in their name or id. The result will contain components that match for "processor1".
+=====================================================================
+group:myGroup processor1
+=====================================================================
+
+Filters can be used together with other search terms and multiple filters can be used. The only constraint is that the search must start with the filters. Unknown filters or known filters with unknown values are ignored. If the same filter key appears multiple times, the first will be used. The order of different filters has no effect on the result.
+
+[caption="Example 3: "]
+.Search will be restricted to the currently active process group (and process groups within that). The result will contain components that match for "import" but property matches will be excluded.
+=====================================================================
+scope:here properties:exclude import
+=====================================================================
+
+The supported filters are the following:
+
+*scope*: This filter narrows the scope of the search based on the user's currently active Process Group. The only valid value is "here". The usage of this filter looks like "scope:here". Any other value is considered as invalid, thus the filter will be ignored during search.
+
+*group*: This filter narrows the scope of the search based on the provided Process Group name or id. Search will be restricted to groups (and their components - including subgroups and their components) the names or ids of which match the filter value. If no group matches the filter, the result list will be empty.
+
+*properties*: With this, users can prevent property matches to appear in the search result. Valid values are: "no", "none", "false", "exclude" and "0".
+
+=== Keywords
+
+Users can use pre-defined (case-insensitive) keywords in the search box that will check certain conditions.
+
+[caption="Example 4: "]
+."disabled" will be treated both as keyword and regular search term. The result will contain disabled Ports and Processors as all other components that match for "disabled" in any way.
+=====================================================================
+disabled
+=====================================================================
+
+Keywords can be used with filters (see below) but not with other search terms (otherwise they won't be treated as keywords) and only one keyword can be used at a time. Note however that keywords will also be treated as general search terms at the same time.
+
+[caption="Example 5: "]
+.Search will be restricted to the currently selected process group (and its sub process groups). "invalid" here (as it is alone after the filter) will be treated both as a keyword and a regular search term. The result will contain invalid Processors and Ports as well as all other components that match for "invalid" in any way.
+=====================================================================
+scope:here invalid
+=====================================================================
+
+The supported keywords are the following:
+
+- *Scheduled state*
+
+** *disabled*: Adds disabled Ports and Processors to the result list.
+
+** *invalid*: Adds Ports and Processors to the result list where the component is invalid.
+
+** *running*: Adds running Ports and Processors to the result list.
+
+** *stopped*: Adds stopped Ports and Processors to the result list.
+
+** *validating*: Adds Processors to the result list that are validating at the time.
+
+- *Scheduling strategy*
+
+** *event*: Adds Processors to the result list where the Scheduling Strategy is "Event Driven".
+
+** *timer*: Adds Processors to the result list where the Scheduling Strategy is "Timer Driven".
+
+- *Execution*
+
+** *primary:* Adds Processors to the result list that are set to run on the primary node only (whether if the Processor is currently running or not).
+
+- *Back pressure*
+
+** *back pressure*: Adds Connections to the result list that are applying back pressure at the time.
+
+** *pressure*: See "back pressure".
+
+- *Expiration*
+
+** *expiration*: Adds Connections to the result list that contain expired Flow Files.
+
+** *expires*: See "expiration".
+
+- *Transmission*
+
+** *not transmitting*: Adds Remote Process Groups to the result list that are not transmitting data at the time.
+
+** *transmitting*: Adds Remote Process Groups to the result list that are transmitting data at the time.
+
+** *transmission disabled*: See "not transmitting".
+
+** *transmitting enabled*: See "transmitting".
 
 [[monitoring]]
 == Monitoring of DataFlow
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/pom.xml
index 30e93a5..30bb589 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/pom.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/pom.xml
@@ -396,9 +396,21 @@
             <scope>provided</scope>
         </dependency>
         <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-test</artifactId>
+            <version>4.3.26.RELEASE</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>org.spockframework</groupId>
             <artifactId>spock-core</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.hamcrest</groupId>
+            <artifactId>hamcrest-all</artifactId>
+            <version>1.3</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 </project>
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
index 6897c90..5107451 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiServiceFacade.java
@@ -197,9 +197,10 @@ public interface NiFiServiceFacade {
      * Searches the controller for the specified query string.
      *
      * @param query query
+     * @param activeGroupId the id of the group currently selected in the editor
      * @return results
      */
-    SearchResultsDTO searchController(String query);
+    SearchResultsDTO searchController(String query, String activeGroupId);
 
     /**
      * Submits a provenance request.
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
index edaee66..c492123 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java
@@ -3106,8 +3106,8 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade {
     // -----------------------------------------
 
     @Override
-    public SearchResultsDTO searchController(final String query) {
-        return controllerFacade.search(query);
+    public SearchResultsDTO searchController(final String query, final String activeGroupId) {
+        return controllerFacade.search(query, activeGroupId);
     }
 
     @Override
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
index d67e455..7072214 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowResource.java
@@ -884,11 +884,14 @@ public class FlowResource extends ApplicationResource {
                     @ApiResponse(code = 409, message = "The request was valid but NiFi was not in the appropriate state to process it. Retrying the same request later may be successful.")
             }
     )
-    public Response searchFlow(@QueryParam("q") @DefaultValue(StringUtils.EMPTY) String value) throws InterruptedException {
+    public Response searchFlow(
+            @QueryParam("q") @DefaultValue(StringUtils.EMPTY) String value,
+            @QueryParam("a") @DefaultValue(StringUtils.EMPTY) String activeGroupId
+    ) throws InterruptedException {
         authorizeFlow();
 
         // query the controller
-        final SearchResultsDTO results = serviceFacade.searchController(value);
+        final SearchResultsDTO results = serviceFacade.searchController(value, activeGroupId);
 
         // create the entity
         final SearchResultsEntity entity = new SearchResultsEntity();
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java
index c4ac5eb..3859daa 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerFacade.java
@@ -78,7 +78,6 @@ import org.apache.nifi.provenance.search.QuerySubmission;
 import org.apache.nifi.provenance.search.SearchTerm;
 import org.apache.nifi.provenance.search.SearchTerms;
 import org.apache.nifi.provenance.search.SearchableField;
-import org.apache.nifi.registry.VariableRegistry;
 import org.apache.nifi.registry.flow.VersionedProcessGroup;
 import org.apache.nifi.remote.PublicPort;
 import org.apache.nifi.remote.RemoteGroupPort;
@@ -109,6 +108,8 @@ import org.apache.nifi.web.api.dto.search.SearchResultsDTO;
 import org.apache.nifi.web.api.dto.status.ControllerStatusDTO;
 import org.apache.nifi.web.api.dto.status.StatusHistoryDTO;
 import org.apache.nifi.web.api.entity.ControllerServiceEntity;
+import org.apache.nifi.web.search.query.SearchQuery;
+import org.apache.nifi.web.search.query.SearchQueryParser;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -147,7 +148,7 @@ public class ControllerFacade implements Authorizable {
     // properties
     private NiFiProperties properties;
     private DtoFactory dtoFactory;
-    private VariableRegistry variableRegistry;
+    private SearchQueryParser searchQueryParser;
     private ControllerSearchService controllerSearchService;
 
     private ProcessGroup getRootGroup() {
@@ -1614,15 +1615,22 @@ public class ControllerFacade implements Authorizable {
     /**
      * Searches this controller for the specified term.
      *
-     * @param search search
+     * @param searchLiteral search string specified by the user
+     * @param activeGroupId the identifier of the currently visited group
      * @return result
      */
-    public SearchResultsDTO search(final String search) {
+    public SearchResultsDTO search(final String searchLiteral, final String activeGroupId) {
         final ProcessGroup rootGroup = getRootGroup();
+        final ProcessGroup activeGroup = (activeGroupId == null)
+                ? rootGroup
+                : flowController.getFlowManager().getGroup(activeGroupId);
         final SearchResultsDTO results = new SearchResultsDTO();
+        final SearchQuery searchQuery = searchQueryParser.parse(searchLiteral, NiFiUserUtils.getNiFiUser(), rootGroup, activeGroup);
 
-        controllerSearchService.search(results, search, rootGroup);
-        controllerSearchService.searchParameters(results, search);
+        if (!StringUtils.isEmpty(searchQuery.getTerm())) {
+            controllerSearchService.search(searchQuery, results);
+            controllerSearchService.searchParameters(searchQuery, results);
+        }
 
         return results;
     }
@@ -1631,7 +1639,6 @@ public class ControllerFacade implements Authorizable {
         flowController.verifyComponentTypesInSnippet(versionedFlow);
     }
 
-
     public ProcessorDiagnosticsDTO getProcessorDiagnostics(final ProcessorNode processor, final ProcessorStatus processorStatus, final BulletinRepository bulletinRepository,
             final Function<String, ControllerServiceEntity> serviceEntityFactory) {
         return dtoFactory.createProcessorDiagnosticsDto(processor, processorStatus, bulletinRepository, flowController, serviceEntityFactory);
@@ -1640,28 +1647,29 @@ public class ControllerFacade implements Authorizable {
     /*
      * setters
      */
+
     public void setFlowController(FlowController flowController) {
         this.flowController = flowController;
     }
 
-    public void setProperties(NiFiProperties properties) {
-        this.properties = properties;
+    public void setFlowService(FlowService flowService) {
+        this.flowService = flowService;
     }
 
     public void setAuthorizer(Authorizer authorizer) {
         this.authorizer = authorizer;
     }
 
-    public void setFlowService(FlowService flowService) {
-        this.flowService = flowService;
+    public void setProperties(NiFiProperties properties) {
+        this.properties = properties;
     }
 
     public void setDtoFactory(DtoFactory dtoFactory) {
         this.dtoFactory = dtoFactory;
     }
 
-    public void setVariableRegistry(VariableRegistry variableRegistry) {
-        this.variableRegistry = variableRegistry;
+    public void setSearchQueryParser(SearchQueryParser searchQueryParser) {
+        this.searchQueryParser = searchQueryParser;
     }
 
     public void setControllerSearchService(ControllerSearchService controllerSearchService) {
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerSearchService.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerSearchService.java
index 7204762..ba25a98 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerSearchService.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/controller/ControllerSearchService.java
@@ -16,688 +16,221 @@
  */
 package org.apache.nifi.web.controller;
 
-import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.authorization.Authorizer;
 import org.apache.nifi.authorization.RequestAction;
+import org.apache.nifi.authorization.resource.Authorizable;
 import org.apache.nifi.authorization.user.NiFiUser;
-import org.apache.nifi.authorization.user.NiFiUserUtils;
-import org.apache.nifi.components.PropertyDescriptor;
-import org.apache.nifi.components.validation.ValidationStatus;
-import org.apache.nifi.connectable.Connectable;
 import org.apache.nifi.connectable.Connection;
 import org.apache.nifi.connectable.Funnel;
 import org.apache.nifi.connectable.Port;
 import org.apache.nifi.controller.FlowController;
 import org.apache.nifi.controller.ProcessorNode;
-import org.apache.nifi.controller.ScheduledState;
 import org.apache.nifi.controller.label.Label;
-import org.apache.nifi.controller.queue.FlowFileQueue;
 import org.apache.nifi.controller.service.ControllerServiceNode;
-import org.apache.nifi.flowfile.FlowFilePrioritizer;
 import org.apache.nifi.groups.ProcessGroup;
 import org.apache.nifi.groups.RemoteProcessGroup;
-import org.apache.nifi.nar.NarCloseable;
 import org.apache.nifi.parameter.Parameter;
 import org.apache.nifi.parameter.ParameterContext;
-import org.apache.nifi.parameter.ParameterContextManager;
-import org.apache.nifi.processor.DataUnit;
-import org.apache.nifi.processor.Processor;
-import org.apache.nifi.processor.Relationship;
-import org.apache.nifi.registry.ComponentVariableRegistry;
-import org.apache.nifi.registry.VariableDescriptor;
-import org.apache.nifi.registry.VariableRegistry;
-import org.apache.nifi.remote.PublicPort;
-import org.apache.nifi.scheduling.ExecutionNode;
-import org.apache.nifi.scheduling.SchedulingStrategy;
-import org.apache.nifi.search.SearchContext;
-import org.apache.nifi.search.SearchResult;
-import org.apache.nifi.search.Searchable;
 import org.apache.nifi.web.api.dto.search.ComponentSearchResultDTO;
-import org.apache.nifi.web.api.dto.search.SearchResultGroupDTO;
 import org.apache.nifi.web.api.dto.search.SearchResultsDTO;
+import org.apache.nifi.web.search.ComponentMatcher;
+import org.apache.nifi.web.search.query.SearchQuery;
+import org.apache.nifi.web.search.resultenrichment.ComponentSearchResultEnricher;
+import org.apache.nifi.web.search.resultenrichment.ComponentSearchResultEnricherFactory;
 
-import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
+import java.util.Optional;
 
 /**
  * NiFi web controller's helper service that implements component search.
  */
 public class ControllerSearchService {
+    private final static String FILTER_NAME_GROUP = "group";
+    private final static String FILTER_NAME_SCOPE = "scope";
+    private final static String FILTER_SCOPE_VALUE_HERE = "here";
+
     private FlowController flowController;
     private Authorizer authorizer;
-    private VariableRegistry variableRegistry;
+    private ComponentSearchResultEnricherFactory resultEnricherFactory;
+
+    private ComponentMatcher<ProcessorNode> matcherForProcessor;
+    private ComponentMatcher<ProcessGroup> matcherForProcessGroup;
+    private ComponentMatcher<Connection> matcherForConnection;
+    private ComponentMatcher<RemoteProcessGroup> matcherForRemoteProcessGroup;
+    private ComponentMatcher<Port> matcherForPort;
+    private ComponentMatcher<Funnel> matcherForFunnel;
+    private ComponentMatcher<ParameterContext> matcherForParameterContext;
+    private ComponentMatcher<Parameter> matcherForParameter;
+    private ComponentMatcher<Label> matcherForLabel;
+    private ComponentMatcher<ControllerServiceNode> matcherForControllerServiceNode;
 
     /**
-     * Searches term in the controller beginning from a given process group.
+     * Searches all parameter contexts and parameters.
      *
+     * @param searchQuery Details of the search
      * @param results Search results
-     * @param search  The search term
-     * @param group   The init process group
      */
-    public void search(final SearchResultsDTO results, final String search, final ProcessGroup group) {
-        final NiFiUser user = NiFiUserUtils.getNiFiUser();
-
-        if (group.isAuthorized(authorizer, RequestAction.READ, user)) {
-            final ComponentSearchResultDTO groupMatch = search(search, group);
-            if (groupMatch != null) {
-                // get the parent group, not the current one
-                groupMatch.setParentGroup(buildResultGroup(group.getParent(), user));
-                groupMatch.setVersionedGroup(buildVersionedGroup(group.getParent(), user));
-                results.getProcessGroupResults().add(groupMatch);
-            }
-        }
-
-        for (final ProcessorNode procNode : group.getProcessors()) {
-            if (procNode.isAuthorized(authorizer, RequestAction.READ, user)) {
-                final ComponentSearchResultDTO match = search(search, procNode);
-                if (match != null) {
-                    match.setGroupId(group.getIdentifier());
-                    match.setParentGroup(buildResultGroup(group, user));
-                    match.setVersionedGroup(buildVersionedGroup(group, user));
-                    results.getProcessorResults().add(match);
-                }
-            }
-        }
-
-        for (final Connection connection : group.getConnections()) {
-            if (connection.isAuthorized(authorizer, RequestAction.READ, user)) {
-                final ComponentSearchResultDTO match = search(search, connection);
-                if (match != null) {
-                    match.setGroupId(group.getIdentifier());
-                    match.setParentGroup(buildResultGroup(group, user));
-                    match.setVersionedGroup(buildVersionedGroup(group, user));
-                    results.getConnectionResults().add(match);
-                }
-            }
-        }
-
-        for (final RemoteProcessGroup remoteGroup : group.getRemoteProcessGroups()) {
-            if (remoteGroup.isAuthorized(authorizer, RequestAction.READ, user)) {
-                final ComponentSearchResultDTO match = search(search, remoteGroup);
-                if (match != null) {
-                    match.setGroupId(group.getIdentifier());
-                    match.setParentGroup(buildResultGroup(group, user));
-                    match.setVersionedGroup(buildVersionedGroup(group, user));
-                    results.getRemoteProcessGroupResults().add(match);
-                }
-            }
-        }
-
-        for (final Port port : group.getInputPorts()) {
-            if (port.isAuthorized(authorizer, RequestAction.READ, user)) {
-                final ComponentSearchResultDTO match = search(search, port);
-                if (match != null) {
-                    match.setGroupId(group.getIdentifier());
-                    match.setParentGroup(buildResultGroup(group, user));
-                    match.setVersionedGroup(buildVersionedGroup(group, user));
-                    results.getInputPortResults().add(match);
-                }
-            }
+    public void search(final SearchQuery searchQuery, final SearchResultsDTO results) {
+        if (searchQuery.hasFilter(FILTER_NAME_SCOPE) && FILTER_SCOPE_VALUE_HERE.equals(searchQuery.getFilter(FILTER_NAME_SCOPE))) {
+            searchInProcessGroup(results, searchQuery, searchQuery.getActiveGroup());
+        } else {
+            searchInProcessGroup(results, searchQuery, searchQuery.getRootGroup());
         }
+    }
 
-        for (final Port port : group.getOutputPorts()) {
-            if (port.isAuthorized(authorizer, RequestAction.READ, user)) {
-                final ComponentSearchResultDTO match = search(search, port);
-                if (match != null) {
-                    match.setGroupId(group.getIdentifier());
-                    match.setParentGroup(buildResultGroup(group, user));
-                    match.setVersionedGroup(buildVersionedGroup(group, user));
-                    results.getOutputPortResults().add(match);
-                }
-            }
-        }
+    private void searchInProcessGroup(final SearchResultsDTO results, final SearchQuery searchQuery, final ProcessGroup scope) {
+        final NiFiUser user = searchQuery.getUser();
+        final ComponentSearchResultEnricher resultEnricher = resultEnricherFactory.getComponentResultEnricher(scope, user);
+        final ComponentSearchResultEnricher groupResultEnricher = resultEnricherFactory.getProcessGroupResultEnricher(scope, user);
 
-        for (final Funnel funnel : group.getFunnels()) {
-            if (funnel.isAuthorized(authorizer, RequestAction.READ, user)) {
-                final ComponentSearchResultDTO match = search(search, funnel);
-                if (match != null) {
-                    match.setGroupId(group.getIdentifier());
-                    match.setParentGroup(buildResultGroup(group, user));
-                    match.setVersionedGroup(buildVersionedGroup(group, user));
-                    results.getFunnelResults().add(match);
-                }
+        if (appliesToGroupFilter(searchQuery, scope)) {
+            if (scope.getParent() != null) {
+                searchComponentType(Collections.singletonList(scope), user, searchQuery, matcherForProcessGroup, groupResultEnricher, results.getProcessGroupResults());
             }
-        }
 
-        for (final Label label : group.getLabels()) {
-            if (label.isAuthorized(authorizer, RequestAction.READ, user)) {
-                final ComponentSearchResultDTO match = search(search, label);
-                if (match != null) {
-                    match.setGroupId(group.getIdentifier());
-                    match.setParentGroup(buildResultGroup(group, user));
-                    match.setVersionedGroup(buildVersionedGroup(group, user));
-                    results.getLabelResults().add(match);
-                }
-            }
+            searchComponentType(scope.getProcessors(), user, searchQuery, matcherForProcessor, resultEnricher, results.getProcessorResults());
+            searchComponentType(scope.getConnections(), user, searchQuery, matcherForConnection, resultEnricher, results.getConnectionResults());
+            searchComponentType(scope.getRemoteProcessGroups(), user, searchQuery, matcherForRemoteProcessGroup, resultEnricher, results.getRemoteProcessGroupResults());
+            searchComponentType(scope.getInputPorts(), user, searchQuery, matcherForPort, resultEnricher, results.getInputPortResults());
+            searchComponentType(scope.getOutputPorts(), user, searchQuery, matcherForPort, resultEnricher, results.getOutputPortResults());
+            searchComponentType(scope.getFunnels(), user, searchQuery, matcherForFunnel, resultEnricher, results.getFunnelResults());
+            searchComponentType(scope.getLabels(), user, searchQuery, matcherForLabel, resultEnricher, results.getLabelResults());
+            searchComponentType(scope.getControllerServices(false), user, searchQuery, matcherForControllerServiceNode, resultEnricher, results.getControllerServiceNodeResults());
         }
 
-        for (final ControllerServiceNode controllerServiceNode : group.getControllerServices(false)) {
-            if (controllerServiceNode.isAuthorized(authorizer, RequestAction.READ, user)) {
-                final ComponentSearchResultDTO match = search(search, controllerServiceNode);
-                if (match != null) {
-                    match.setGroupId(group.getIdentifier());
-                    match.setParentGroup(buildResultGroup(group, user));
-                    match.setVersionedGroup(buildVersionedGroup(group, user));
-                    results.getControllerServiceNodeResults().add(match);
-                }
-            }
-        }
-
-        for (final ProcessGroup processGroup : group.getProcessGroups()) {
-            search(results, search, processGroup);
-        }
+        scope.getProcessGroups().forEach(processGroup -> searchInProcessGroup(results, searchQuery, processGroup));
     }
 
-    /**
-     * Searches controller service for the given search term
-     *
-     * @param search                the search term
-     * @param controllerServiceNode a group controller service node
-     */
-    private ComponentSearchResultDTO search(final String search, final ControllerServiceNode controllerServiceNode) {
-        final List<String> matches = new ArrayList<>();
-        addIfAppropriate(search, controllerServiceNode.getIdentifier(), "Id", matches);
-        addIfAppropriate(search, controllerServiceNode.getVersionedComponentId().orElse(null), "Version Control ID", matches);
-        addIfAppropriate(search, controllerServiceNode.getName(), "Name", matches);
-        addIfAppropriate(search, controllerServiceNode.getComments(), "Comments", matches);
-
-        // search property values
-        controllerServiceNode.getRawPropertyValues().forEach((property, propertyValue) -> {
-            addIfAppropriate(search, property.getName(), "Property Name", matches);
-            addIfAppropriate(search, property.getDescription(), "Property Description", matches);
-
-            // never include sensitive properties in search results
-            if (property.isSensitive()) {
-                return;
-            }
-
-            if (propertyValue != null) {
-                addIfAppropriate(search, propertyValue, "Property Value", matches);
-            } else {
-                addIfAppropriate(search, property.getDefaultValue(), "Property Value", matches);
-            }
-        });
-
-        if (matches.isEmpty()) {
-            return null;
-        }
-
-        final ComponentSearchResultDTO dto = new ComponentSearchResultDTO();
-        dto.setId(controllerServiceNode.getIdentifier());
-        dto.setName(controllerServiceNode.getName());
-        dto.setMatches(matches);
-        return dto;
+    private boolean appliesToGroupFilter(final SearchQuery searchQuery, final ProcessGroup scope) {
+        return !searchQuery.hasFilter(FILTER_NAME_GROUP) || eligibleForGroupFilter(scope, searchQuery.getFilter(FILTER_NAME_GROUP));
     }
 
     /**
-     * Searches all parameter contexts and parameters
+     * Check is the group is eligible for the filter value. It might be eligible based on name or id.
      *
-     * @param results Search results
-     * @param search  The search term
+     * @param scope The subject process group.
+     * @param filterValue The value to match against.
+     *
+     * @return True in case the scope process group or any parent is matching. A group is matching when it's name or it's id contains the filter value.
      */
-    public void searchParameters(final SearchResultsDTO results, final String search) {
-        final NiFiUser user = NiFiUserUtils.getNiFiUser();
-        ParameterContextManager parameterContextManager = flowController.getFlowManager().getParameterContextManager();
-
-        final Set<ParameterContext> parameterContexts = parameterContextManager.getParameterContexts();
-        for (final ParameterContext parameterContext : parameterContexts) {
-            if (parameterContext.isAuthorized(authorizer, RequestAction.READ, user)) {
-                ComponentSearchResultDTO parameterContextMatch = search(search, parameterContext);
-                if (parameterContextMatch != null) {
-                    results.getParameterContextResults().add(parameterContextMatch);
-                }
-
-                // search each parameter within the context as well
-                for (Parameter parameter : parameterContext.getParameters().values()) {
-                    ComponentSearchResultDTO parameterMatch = search(search, parameter);
-                    if (parameterMatch != null) {
-                        final SearchResultGroupDTO paramContextGroup = new SearchResultGroupDTO();
-                        paramContextGroup.setId(parameterContext.getIdentifier());
-                        paramContextGroup.setName(parameterContext.getName());
-                        parameterMatch.setParentGroup(paramContextGroup);
-
-                        results.getParameterResults().add(parameterMatch);
-                    }
-                }
-            }
-        }
-    }
-
-    private ComponentSearchResultDTO search(final String searchStr, final Port port) {
-        final List<String> matches = new ArrayList<>();
+    private boolean eligibleForGroupFilter(final ProcessGroup scope, final String filterValue) {
+        final List<ProcessGroup> lineage = getLineage(scope);
 
-        addIfAppropriate(searchStr, port.getIdentifier(), "Id", matches);
-        addIfAppropriate(searchStr, port.getVersionedComponentId().orElse(null), "Version Control ID", matches);
-        addIfAppropriate(searchStr, port.getName(), "Name", matches);
-        addIfAppropriate(searchStr, port.getComments(), "Comments", matches);
-
-        // consider scheduled state
-        if (ScheduledState.DISABLED.equals(port.getScheduledState())) {
-            if (StringUtils.containsIgnoreCase("disabled", searchStr)) {
-                matches.add("Run status: Disabled");
-            }
-        } else {
-            if (StringUtils.containsIgnoreCase("invalid", searchStr) && !port.isValid()) {
-                matches.add("Run status: Invalid");
-            } else if (ScheduledState.RUNNING.equals(port.getScheduledState()) && StringUtils.containsIgnoreCase("running", searchStr)) {
-                matches.add("Run status: Running");
-            } else if (ScheduledState.STOPPED.equals(port.getScheduledState()) && StringUtils.containsIgnoreCase("stopped", searchStr)) {
-                matches.add("Run status: Stopped");
-            }
-        }
-
-        if (port instanceof PublicPort) {
-            final PublicPort publicPort = (PublicPort) port;
-
-            // user access controls
-            for (final String userAccessControl : publicPort.getUserAccessControl()) {
-                addIfAppropriate(searchStr, userAccessControl, "User access control", matches);
+        for (final ProcessGroup group : lineage) {
+            if (StringUtils.containsIgnoreCase(group.getName(), filterValue) || StringUtils.containsIgnoreCase(group.getIdentifier(), filterValue)) {
+                return true;
             }
-
-            // group access controls
-            for (final String groupAccessControl : publicPort.getGroupAccessControl()) {
-                addIfAppropriate(searchStr, groupAccessControl, "Group access control", matches);
-            }
-        }
-
-        if (matches.isEmpty()) {
-            return null;
         }
 
-        final ComponentSearchResultDTO dto = new ComponentSearchResultDTO();
-        dto.setId(port.getIdentifier());
-        dto.setName(port.getName());
-        dto.setMatches(matches);
-        return dto;
+        return false;
     }
 
-    private ComponentSearchResultDTO search(final String searchStr, final ProcessorNode procNode) {
-        final List<String> matches = new ArrayList<>();
-        final Processor processor = procNode.getProcessor();
-
-        addIfAppropriate(searchStr, procNode.getIdentifier(), "Id", matches);
-        addIfAppropriate(searchStr, procNode.getVersionedComponentId().orElse(null), "Version Control ID", matches);
-        addIfAppropriate(searchStr, procNode.getName(), "Name", matches);
-        addIfAppropriate(searchStr, procNode.getComments(), "Comments", matches);
-
-        // consider scheduling strategy
-        if (SchedulingStrategy.EVENT_DRIVEN.equals(procNode.getSchedulingStrategy()) && StringUtils.containsIgnoreCase("event", searchStr)) {
-            matches.add("Scheduling strategy: Event driven");
-        } else if (SchedulingStrategy.TIMER_DRIVEN.equals(procNode.getSchedulingStrategy()) && StringUtils.containsIgnoreCase("timer", searchStr)) {
-            matches.add("Scheduling strategy: Timer driven");
-        } else if (SchedulingStrategy.PRIMARY_NODE_ONLY.equals(procNode.getSchedulingStrategy()) && StringUtils.containsIgnoreCase("primary", searchStr)) {
-            // PRIMARY_NODE_ONLY has been deprecated as a SchedulingStrategy and replaced by PRIMARY as an ExecutionNode.
-            matches.add("Scheduling strategy: On primary node");
-        }
-
-        // consider execution node
-        if (ExecutionNode.PRIMARY.equals(procNode.getExecutionNode()) && StringUtils.containsIgnoreCase("primary", searchStr)) {
-            matches.add("Execution node: primary");
-        }
-
-        // consider scheduled state
-        if (ScheduledState.DISABLED.equals(procNode.getScheduledState())) {
-            if (StringUtils.containsIgnoreCase("disabled", searchStr)) {
-                matches.add("Run status: Disabled");
-            }
-        } else {
-            if (StringUtils.containsIgnoreCase("invalid", searchStr) && procNode.getValidationStatus() == ValidationStatus.INVALID) {
-                matches.add("Run status: Invalid");
-            } else if (StringUtils.containsIgnoreCase("validating", searchStr) && procNode.getValidationStatus() == ValidationStatus.VALIDATING) {
-                matches.add("Run status: Validating");
-            } else if (ScheduledState.RUNNING.equals(procNode.getScheduledState()) && StringUtils.containsIgnoreCase("running", searchStr)) {
-                matches.add("Run status: Running");
-            } else if (ScheduledState.STOPPED.equals(procNode.getScheduledState()) && StringUtils.containsIgnoreCase("stopped", searchStr)) {
-                matches.add("Run status: Stopped");
-            }
-        }
+    private List<ProcessGroup> getLineage(final ProcessGroup group) {
+        final LinkedList<ProcessGroup> result = new LinkedList<>();
+        ProcessGroup current = group;
 
-        for (final Relationship relationship : procNode.getRelationships()) {
-            addIfAppropriate(searchStr, relationship.getName(), "Relationship", matches);
+        while (current != null) {
+            result.addLast(current);
+            current = current.getParent();
         }
 
-        // Add both the actual class name and the component type. This allows us to search for 'Ghost'
-        // to search for components that could not be instantiated.
-        addIfAppropriate(searchStr, processor.getClass().getSimpleName(), "Type", matches);
-        addIfAppropriate(searchStr, procNode.getComponentType(), "Type", matches);
-
-        for (final Map.Entry<PropertyDescriptor, String> entry : procNode.getRawPropertyValues().entrySet()) {
-            final PropertyDescriptor descriptor = entry.getKey();
-
-            addIfAppropriate(searchStr, descriptor.getName(), "Property name", matches);
-            addIfAppropriate(searchStr, descriptor.getDescription(), "Property description", matches);
-
-            // never include sensitive properties values in search results
-            if (descriptor.isSensitive()) {
-                continue;
-            }
-
-            String value = entry.getValue();
-
-            // if unset consider default value
-            if (value == null) {
-                value = descriptor.getDefaultValue();
-            }
-
-            // evaluate if the value matches the search criteria
-            if (StringUtils.containsIgnoreCase(value, searchStr)) {
-                matches.add("Property value: " + descriptor.getName() + " - " + value);
-            }
-        }
-
-        // consider searching the processor directly
-        if (processor instanceof Searchable) {
-            final Searchable searchable = (Searchable) processor;
-
-            final SearchContext context = new StandardSearchContext(searchStr, procNode, flowController.getControllerServiceProvider(), variableRegistry);
-
-            // search the processor using the appropriate thread context classloader
-            try (final NarCloseable x = NarCloseable.withComponentNarLoader(flowController.getExtensionManager(), processor.getClass(), processor.getIdentifier())) {
-                final Collection<SearchResult> searchResults = searchable.search(context);
-                if (CollectionUtils.isNotEmpty(searchResults)) {
-                    for (final SearchResult searchResult : searchResults) {
-                        matches.add(searchResult.getLabel() + ": " + searchResult.getMatch());
-                    }
-                }
-            } catch (final Throwable t) {
-                // log this as error
-            }
-        }
-
-        if (matches.isEmpty()) {
-            return null;
-        }
-
-        final ComponentSearchResultDTO result = new ComponentSearchResultDTO();
-        result.setId(procNode.getIdentifier());
-        result.setMatches(matches);
-        result.setName(procNode.getName());
         return result;
     }
 
-    private ComponentSearchResultDTO search(final String searchStr, final ProcessGroup group) {
-        final List<String> matches = new ArrayList<>();
-        final ProcessGroup parent = group.getParent();
-        if (parent == null) {
-            return null;
-        }
-
-        addIfAppropriate(searchStr, group.getIdentifier(), "Id", matches);
-        addIfAppropriate(searchStr, group.getVersionedComponentId().orElse(null), "Version Control ID", matches);
-        addIfAppropriate(searchStr, group.getName(), "Name", matches);
-        addIfAppropriate(searchStr, group.getComments(), "Comments", matches);
-
-        final ComponentVariableRegistry varRegistry = group.getVariableRegistry();
-        if (varRegistry != null) {
-            final Map<VariableDescriptor, String> variableMap = varRegistry.getVariableMap();
-            for (final Map.Entry<VariableDescriptor, String> entry : variableMap.entrySet()) {
-                addIfAppropriate(searchStr, entry.getKey().getName(), "Variable Name", matches);
-                addIfAppropriate(searchStr, entry.getValue(), "Variable Value", matches);
-            }
-        }
-
-        if (matches.isEmpty()) {
-            return null;
-        }
-
-        final ComponentSearchResultDTO result = new ComponentSearchResultDTO();
-        result.setId(group.getIdentifier());
-        result.setName(group.getName());
-        result.setGroupId(parent.getIdentifier());
-        result.setMatches(matches);
-        return result;
+    private <T extends Authorizable> void searchComponentType(
+               final Collection<T> components,
+               final NiFiUser user,
+               final SearchQuery searchQuery,
+               final ComponentMatcher<T> matcher,
+               final ComponentSearchResultEnricher resultEnricher,
+               final List<ComponentSearchResultDTO> resultAccumulator) {
+        components.stream()
+                .filter(component -> component.isAuthorized(authorizer, RequestAction.READ, user))
+                .map(component -> matcher.match(component, searchQuery))
+                .filter(Optional::isPresent)
+                .map(result -> resultEnricher.enrich(result.get()))
+                .forEach(result -> resultAccumulator.add(result));
     }
 
-    private ComponentSearchResultDTO search(final String searchStr, final Connection connection) {
-        final List<String> matches = new ArrayList<>();
-
-        // search id and name
-        addIfAppropriate(searchStr, connection.getIdentifier(), "Id", matches);
-        addIfAppropriate(searchStr, connection.getVersionedComponentId().orElse(null), "Version Control ID", matches);
-        addIfAppropriate(searchStr, connection.getName(), "Name", matches);
-
-        // search relationships
-        for (final Relationship relationship : connection.getRelationships()) {
-            addIfAppropriate(searchStr, relationship.getName(), "Relationship", matches);
-        }
-
-        // search prioritizers
-        final FlowFileQueue queue = connection.getFlowFileQueue();
-        for (final FlowFilePrioritizer comparator : queue.getPriorities()) {
-            addIfAppropriate(searchStr, comparator.getClass().getName(), "Prioritizer", matches);
-        }
-
-        // search expiration
-        if (StringUtils.containsIgnoreCase("expires", searchStr) || StringUtils.containsIgnoreCase("expiration", searchStr)) {
-            final int expirationMillis = connection.getFlowFileQueue().getFlowFileExpiration(TimeUnit.MILLISECONDS);
-            if (expirationMillis > 0) {
-                matches.add("FlowFile expiration: " + connection.getFlowFileQueue().getFlowFileExpiration());
-            }
-        }
-
-        // search back pressure
-        if (StringUtils.containsIgnoreCase("back pressure", searchStr) || StringUtils.containsIgnoreCase("pressure", searchStr)) {
-            final String backPressureDataSize = connection.getFlowFileQueue().getBackPressureDataSizeThreshold();
-            final Double backPressureBytes = DataUnit.parseDataSize(backPressureDataSize, DataUnit.B);
-            if (backPressureBytes > 0) {
-                matches.add("Back pressure data size: " + backPressureDataSize);
-            }
-
-            final long backPressureCount = connection.getFlowFileQueue().getBackPressureObjectThreshold();
-            if (backPressureCount > 0) {
-                matches.add("Back pressure count: " + backPressureCount);
-            }
-        }
-
-        // search the source
-        final Connectable source = connection.getSource();
-        addIfAppropriate(searchStr, source.getIdentifier(), "Source id", matches);
-        addIfAppropriate(searchStr, source.getName(), "Source name", matches);
-        addIfAppropriate(searchStr, source.getComments(), "Source comments", matches);
-
-        // search the destination
-        final Connectable destination = connection.getDestination();
-        addIfAppropriate(searchStr, destination.getIdentifier(), "Destination id", matches);
-        addIfAppropriate(searchStr, destination.getName(), "Destination name", matches);
-        addIfAppropriate(searchStr, destination.getComments(), "Destination comments", matches);
-
-        if (matches.isEmpty()) {
-            return null;
-        }
-
-        final ComponentSearchResultDTO result = new ComponentSearchResultDTO();
-        result.setId(connection.getIdentifier());
-
-        // determine the name of the search match
-        if (StringUtils.isNotBlank(connection.getName())) {
-            result.setName(connection.getName());
-        } else if (!connection.getRelationships().isEmpty()) {
-            final List<String> relationships = new ArrayList<>(connection.getRelationships().size());
-            for (final Relationship relationship : connection.getRelationships()) {
-                if (StringUtils.isNotBlank(relationship.getName())) {
-                    relationships.add(relationship.getName());
-                }
-            }
-            if (!relationships.isEmpty()) {
-                result.setName(StringUtils.join(relationships, ", "));
-            }
-        }
-
-        // ensure a name is added
-        if (result.getName() == null) {
-            result.setName("From source " + connection.getSource().getName());
-        }
-
-        result.setMatches(matches);
-        return result;
+    /**
+     * Searches all parameter contexts and parameters.
+     *
+     * @param searchQuery Details of the search
+     * @param results Search results
+     */
+    public void searchParameters(final SearchQuery searchQuery, final SearchResultsDTO results) {
+        flowController.getFlowManager()
+                .getParameterContextManager()
+                .getParameterContexts()
+                .stream()
+                .filter(component -> component.isAuthorized(authorizer, RequestAction.READ, searchQuery.getUser()))
+                .forEach(parameterContext -> {
+                    final ComponentSearchResultEnricher resultEnricher = resultEnricherFactory.getParameterResultEnricher(parameterContext);
+
+                    matcherForParameterContext.match(parameterContext, searchQuery)
+                            .ifPresent(match -> results.getParameterContextResults().add(match));
+
+                    parameterContext.getParameters().values().stream()
+                            .map(component -> matcherForParameter.match(component, searchQuery))
+                            .filter(Optional::isPresent)
+                            .map(result -> resultEnricher.enrich(result.get()))
+                            .forEach(result -> results.getParameterResults().add(result));
+                });
     }
 
-    private ComponentSearchResultDTO search(final String searchStr, final RemoteProcessGroup group) {
-        final List<String> matches = new ArrayList<>();
-        addIfAppropriate(searchStr, group.getIdentifier(), "Id", matches);
-        addIfAppropriate(searchStr, group.getVersionedComponentId().orElse(null), "Version Control ID", matches);
-        addIfAppropriate(searchStr, group.getName(), "Name", matches);
-        addIfAppropriate(searchStr, group.getComments(), "Comments", matches);
-        addIfAppropriate(searchStr, group.getTargetUris(), "URLs", matches);
-
-        // consider the transmission status
-        if ((StringUtils.containsIgnoreCase("transmitting", searchStr) || StringUtils.containsIgnoreCase("transmission enabled", searchStr)) && group.isTransmitting()) {
-            matches.add("Transmission: On");
-        } else if ((StringUtils.containsIgnoreCase("not transmitting", searchStr) || StringUtils.containsIgnoreCase("transmission disabled", searchStr)) && !group.isTransmitting()) {
-            matches.add("Transmission: Off");
-        }
-
-        if (matches.isEmpty()) {
-            return null;
-        }
-
-        final ComponentSearchResultDTO result = new ComponentSearchResultDTO();
-        result.setId(group.getIdentifier());
-        result.setName(group.getName());
-        result.setMatches(matches);
-        return result;
+    public void setFlowController(FlowController flowController) {
+        this.flowController = flowController;
     }
 
-    private ComponentSearchResultDTO search(final String searchStr, final Funnel funnel) {
-        final List<String> matches = new ArrayList<>();
-        addIfAppropriate(searchStr, funnel.getIdentifier(), "Id", matches);
-        addIfAppropriate(searchStr, funnel.getVersionedComponentId().orElse(null), "Version Control ID", matches);
-
-        if (matches.isEmpty()) {
-            return null;
-        }
-
-        final ComponentSearchResultDTO dto = new ComponentSearchResultDTO();
-        dto.setId(funnel.getIdentifier());
-        dto.setName(funnel.getName());
-        dto.setMatches(matches);
-        return dto;
+    public void setAuthorizer(Authorizer authorizer) {
+        this.authorizer = authorizer;
     }
 
-    private ComponentSearchResultDTO search(final String searchStr, final Label label) {
-        final List<String> matches = new ArrayList<>();
-        addIfAppropriate(searchStr, label.getIdentifier(), "Id", matches);
-        addIfAppropriate(searchStr, label.getValue(), "Value", matches);
-
-        if (matches.isEmpty()) {
-            return null;
-        }
-
-        final ComponentSearchResultDTO dto = new ComponentSearchResultDTO();
-        dto.setId(label.getIdentifier());
-        dto.setName(label.getValue());
-        dto.setMatches(matches);
-        return dto;
+    public void setResultEnricherFactory(ComponentSearchResultEnricherFactory resultEnricherFactory) {
+        this.resultEnricherFactory = resultEnricherFactory;
     }
 
-    private ComponentSearchResultDTO search(final String searchString, final ParameterContext parameterContext) {
-        final List<String> matches = new ArrayList<>();
-        addIfAppropriate(searchString, parameterContext.getIdentifier(), "Id", matches);
-        addIfAppropriate(searchString, parameterContext.getName(), "Name", matches);
-        addIfAppropriate(searchString, parameterContext.getDescription(), "Description", matches);
-
-        if (matches.isEmpty()) {
-            return null;
-        }
-
-        final ComponentSearchResultDTO dto = new ComponentSearchResultDTO();
-        dto.setId(parameterContext.getIdentifier());
-        dto.setName(parameterContext.getName());
-        dto.setMatches(matches);
-        return dto;
+    public void setMatcherForProcessor(ComponentMatcher<ProcessorNode> matcherForProcessor) {
+        this.matcherForProcessor = matcherForProcessor;
     }
 
-    private ComponentSearchResultDTO search(final String searchString, final Parameter parameter) {
-        final List<String> matches = new ArrayList<>();
-        addIfAppropriate(searchString, parameter.getDescriptor().getName(), "Name", matches);
-        addIfAppropriate(searchString, parameter.getDescriptor().getDescription(), "Description", matches);
-        if (!parameter.getDescriptor().isSensitive()) {
-            addIfAppropriate(searchString, parameter.getValue(), "Value", matches);
-        }
-
-        if (matches.isEmpty()) {
-            return null;
-        }
-
-        final ComponentSearchResultDTO dto = new ComponentSearchResultDTO();
-        dto.setId(parameter.getDescriptor().getName());
-        dto.setName(parameter.getDescriptor().getName());
-        dto.setMatches(matches);
-        return dto;
+    public void setMatcherForProcessGroup(ComponentMatcher<ProcessGroup> matcherForProcessGroup) {
+        this.matcherForProcessGroup = matcherForProcessGroup;
     }
 
-    /**
-     * Builds the nearest versioned parent result group for a given user.
-     *
-     * @param group The containing group
-     * @param user The current NiFi user
-     * @return Versioned parent group
-     */
-    private SearchResultGroupDTO buildVersionedGroup(final ProcessGroup group, final NiFiUser user) {
-        if (group == null) {
-            return null;
-        }
-
-        ProcessGroup tmpParent = group.getParent();
-        ProcessGroup tmpGroup = group;
-
-        // search for a versioned group by traversing the group tree up to the root
-        while (!tmpGroup.isRootGroup()) {
-            if (tmpGroup.getVersionControlInformation() != null) {
-                return buildResultGroup(tmpGroup, user);
-            }
-
-            tmpGroup = tmpParent;
-            tmpParent = tmpGroup.getParent();
-        }
-
-        // traversed all the way to the root
-        return null;
+    public void setMatcherForConnection(ComponentMatcher<Connection> matcherForConnection) {
+        this.matcherForConnection = matcherForConnection;
     }
 
-    /**
-     * Builds result group for a given user.
-     *
-     * @param group The containing group
-     * @param user The current NiFi user
-     * @return Result group
-     */
-    private SearchResultGroupDTO buildResultGroup(final ProcessGroup group, final NiFiUser user) {
-        if (group == null) {
-            return null;
-        }
-
-        final SearchResultGroupDTO resultGroup = new SearchResultGroupDTO();
-        resultGroup.setId(group.getIdentifier());
+    public void setMatcherForRemoteProcessGroup(ComponentMatcher<RemoteProcessGroup> matcherForRemoteProcessGroup) {
+        this.matcherForRemoteProcessGroup = matcherForRemoteProcessGroup;
+    }
 
-        // keep the group name confidential
-        if (group.isAuthorized(authorizer, RequestAction.READ, user)) {
-            resultGroup.setName(group.getName());
-        }
+    public void setMatcherForPort(ComponentMatcher<Port> matcherForPort) {
+        this.matcherForPort = matcherForPort;
+    }
 
-        return resultGroup;
+    public void setMatcherForFunnel(ComponentMatcher<Funnel> matcherForFunnel) {
+        this.matcherForFunnel = matcherForFunnel;
     }
 
-    private void addIfAppropriate(final String searchStr, final String value, final String label, final List<String> matches) {
-        if (StringUtils.containsIgnoreCase(value, searchStr)) {
-            matches.add(label + ": " + value);
-        }
+    public void setMatcherForParameterContext(ComponentMatcher<ParameterContext> matcherForParameterContext) {
+        this.matcherForParameterContext = matcherForParameterContext;
     }
 
-    public void setFlowController(FlowController flowController) {
-        this.flowController = flowController;
+    public void setMatcherForParameter(ComponentMatcher<Parameter> matcherForParameter) {
+        this.matcherForParameter = matcherForParameter;
     }
 
-    public void setAuthorizer(Authorizer authorizer) {
-        this.authorizer = authorizer;
+    public void setMatcherForLabel(ComponentMatcher<Label> matcherForLabel) {
+        this.matcherForLabel = matcherForLabel;
     }
 
-    public void setVariableRegistry(VariableRegistry variableRegistry) {
-        this.variableRegistry = variableRegistry;
+    public void setMatcherForControllerServiceNode(ComponentMatcher<ControllerServiceNode> matcherForControllerServiceNode) {
+        this.matcherForControllerServiceNode = matcherForControllerServiceNode;
     }
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/AttributeBasedComponentMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/AttributeBasedComponentMatcher.java
new file mode 100644
index 0000000..792e72b
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/AttributeBasedComponentMatcher.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search;
+
+import org.apache.nifi.web.api.dto.search.ComponentSearchResultDTO;
+import org.apache.nifi.web.search.attributematchers.AttributeMatcher;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+
+public class AttributeBasedComponentMatcher<T> implements ComponentMatcher<T> {
+    private final List<AttributeMatcher<T>> attributeMatchers = new ArrayList<>();
+    private final Function<T, String> getComponentIdentifier;
+    private final Function<T, String> getComponentName;
+
+    public AttributeBasedComponentMatcher(
+               final List<AttributeMatcher<T>> attributeMatchers,
+               final Function<T, String> getComponentIdentifier,
+               final Function<T, String> getComponentName) {
+        this.getComponentIdentifier = getComponentIdentifier;
+        this.getComponentName = getComponentName;
+        this.attributeMatchers.addAll(attributeMatchers);
+    }
+
+    @Override
+    public final Optional<ComponentSearchResultDTO> match(final T component, final SearchQuery query) {
+        final List<String> matches = new LinkedList<>();
+        attributeMatchers.forEach(matcher -> matcher.match(component, query, matches));
+
+        return matches.isEmpty()
+                ? Optional.empty()
+                : Optional.of(generateResult(component, matches));
+    }
+
+    private ComponentSearchResultDTO generateResult(final T component, final List<String> matches) {
+        final ComponentSearchResultDTO result = new ComponentSearchResultDTO();
+        result.setId(getComponentIdentifier.apply(component));
+        result.setName(getComponentName.apply(component));
+        result.setMatches(matches);
+        return result;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/ComponentMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/ComponentMatcher.java
new file mode 100644
index 0000000..d2cdc79
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/ComponentMatcher.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search;
+
+import org.apache.nifi.web.api.dto.search.ComponentSearchResultDTO;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.Optional;
+
+/**
+ * Service responsible to clamp all the possible matches for a given component type.
+ *
+ * @param <COMPONENT_TYPE> The component type.
+ */
+public interface ComponentMatcher<COMPONENT_TYPE> {
+
+    /**
+     * Tries to match the incoming search query against a given component.
+     *
+     * @param component The component to match against.
+     * @param query The search query to match.
+     *
+     * @return The result of the matching. Returns with {@link Optional#empty()} if there was no match, contains {@link ComponentSearchResultDTO}
+     * with the details of the results in case there was at least one match for the given component and query.
+     */
+    Optional<ComponentSearchResultDTO> match(COMPONENT_TYPE component, SearchQuery query);
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/ComponentMatcherFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/ComponentMatcherFactory.java
new file mode 100644
index 0000000..66ca01b
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/ComponentMatcherFactory.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.connectable.Connectable;
+import org.apache.nifi.connectable.Connection;
+import org.apache.nifi.controller.label.Label;
+import org.apache.nifi.controller.service.ControllerServiceNode;
+import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.groups.RemoteProcessGroup;
+import org.apache.nifi.parameter.Parameter;
+import org.apache.nifi.parameter.ParameterContext;
+import org.apache.nifi.web.search.attributematchers.AttributeMatcher;
+
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public class ComponentMatcherFactory {
+    public ComponentMatcher<Connectable> getInstanceForConnectable(final List<AttributeMatcher<Connectable>> attributeMatchers) {
+        return new AttributeBasedComponentMatcher<>(attributeMatchers, component -> component.getIdentifier(), component -> component.getName());
+    }
+
+    public ComponentMatcher<Connection> getInstanceForConnection(final List<AttributeMatcher<Connection>> attributeMatchers) {
+        return new AttributeBasedComponentMatcher<>(attributeMatchers, component -> component.getIdentifier(), new GetConnectionName());
+    }
+
+    public ComponentMatcher<Parameter> getInstanceForParameter(final List<AttributeMatcher<Parameter>> attributeMatchers) {
+        return new AttributeBasedComponentMatcher<>(attributeMatchers, component -> component.getDescriptor().getName(), component -> component.getDescriptor().getName());
+    }
+
+    public ComponentMatcher<ParameterContext> getInstanceForParameterContext(final List<AttributeMatcher<ParameterContext>> attributeMatchers) {
+        return new AttributeBasedComponentMatcher<>(attributeMatchers, component -> component.getIdentifier(), component -> component.getName());
+    }
+
+    public ComponentMatcher<ProcessGroup> getInstanceForProcessGroup(final List<AttributeMatcher<ProcessGroup>> attributeMatchers) {
+        return new AttributeBasedComponentMatcher<>(attributeMatchers, component -> component.getIdentifier(), component -> component.getName());
+    }
+
+    public ComponentMatcher<RemoteProcessGroup> getInstanceForRemoteProcessGroup(final List<AttributeMatcher<RemoteProcessGroup>> attributeMatchers) {
+        return new AttributeBasedComponentMatcher<>(attributeMatchers, component -> component.getIdentifier(), component -> component.getName());
+    }
+
+    public ComponentMatcher<Label> getInstanceForLabel(final List<AttributeMatcher<Label>> attributeMatchers) {
+        return new AttributeBasedComponentMatcher<>(attributeMatchers, component -> component.getIdentifier(), component -> component.getValue());
+    }
+
+    public ComponentMatcher<ControllerServiceNode> getInstanceForControllerServiceNode(final List<AttributeMatcher<ControllerServiceNode>> attributeMatchers) {
+        return new AttributeBasedComponentMatcher<>(attributeMatchers, component -> component.getIdentifier(), component -> component.getName());
+    }
+
+    private static class GetConnectionName implements Function<Connection, String> {
+        private static final String DEFAULT_NAME_PREFIX = "From source ";
+        private static final String SEPARATOR = ", ";
+
+        public String apply(final Connection component) {
+            String result = null;
+
+            if (StringUtils.isNotBlank(component.getName())) {
+                result = component.getName();
+            } else if (!component.getRelationships().isEmpty()) {
+                result = component.getRelationships().stream()
+                        .filter(relationship -> StringUtils.isNotBlank(relationship.getName()))
+                        .map(relationship -> relationship.getName())
+                        .collect(Collectors.joining(SEPARATOR));
+            }
+
+            return result == null
+                    ? DEFAULT_NAME_PREFIX + component.getSource().getName()
+                    : result;
+        }
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/AttributeMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/AttributeMatcher.java
new file mode 100644
index 0000000..6f7709b
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/AttributeMatcher.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.List;
+
+/**
+ * Represents an elementary match based on a given attribute, like name or description, depending on the implementation.
+ *
+ * @param <T> The component type.
+ */
+public interface AttributeMatcher<T> {
+    String SEPARATOR = ": ";
+
+    /**
+     * Executing the match.
+     *
+     * @param component The component to match against.
+     * @param query The search query to match.
+     * @param matches Aggregator for the match results.
+     */
+    void match(T component, SearchQuery query, List<String> matches);
+
+    /**
+     * Helper method for implementations to execute simple text based matches.
+     *
+     * @param searchTerm The search term to match.
+     * @param subject The component's textual attribute to match against.
+     * @param label The descriptor of the match's nature.
+     * @param matches Aggregator for the match results.
+     */
+    static void addIfMatching(final String searchTerm, final String subject, final String label, final List<String> matches) {
+        final String match = label + SEPARATOR + subject;
+
+        if (StringUtils.containsIgnoreCase(subject, searchTerm) && matches != null && !matches.contains(match)) {
+            matches.add(match);
+        }
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/BackPressureMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/BackPressureMatcher.java
new file mode 100644
index 0000000..7cc6a49
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/BackPressureMatcher.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.connectable.Connection;
+import org.apache.nifi.processor.DataUnit;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class BackPressureMatcher implements AttributeMatcher<Connection> {
+    private static final String MATCH_PREFIX_SIZE = "Back pressure data size: ";
+    private static final String MATCH_PREFIX_COUNT = "Back pressure count: ";
+    private static final Set<String> KEYWORDS = new HashSet<>(Arrays.asList(
+            "back pressure",
+            "pressure"));
+
+    @Override
+    public void match(final Connection component, final SearchQuery query, final List<String> matches) {
+        if (containsKeyword(query)) {
+            final String backPressureDataSize = component.getFlowFileQueue().getBackPressureDataSizeThreshold();
+            final Double backPressureBytes = DataUnit.parseDataSize(backPressureDataSize, DataUnit.B);
+            final long backPressureCount = component.getFlowFileQueue().getBackPressureObjectThreshold();
+
+            if (backPressureBytes > 0) {
+                matches.add(MATCH_PREFIX_SIZE + backPressureDataSize);
+            }
+
+            if (backPressureCount > 0) {
+                matches.add(MATCH_PREFIX_COUNT + backPressureCount);
+            }
+        }
+    }
+
+    private boolean containsKeyword(final SearchQuery query) {
+        return KEYWORDS.stream().anyMatch(keyword -> StringUtils.containsIgnoreCase(keyword, query.getTerm()));
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/BasicMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/BasicMatcher.java
new file mode 100644
index 0000000..35b6855
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/BasicMatcher.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.connectable.Connectable;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.List;
+
+import static org.apache.nifi.web.search.attributematchers.AttributeMatcher.addIfMatching;
+
+public class BasicMatcher<T extends Connectable> implements AttributeMatcher<T> {
+    private static final String LABEL_ID = "Id";
+    private static final String LABEL_VERSION_CONTROL_ID = "Version Control ID";
+
+    @Override
+    public void match(final T component, final SearchQuery query, final List<String> matches) {
+        final String searchTerm = query.getTerm();
+
+        addIfMatching(searchTerm, component.getIdentifier(), LABEL_ID, matches);
+        addIfMatching(searchTerm, component.getVersionedComponentId().orElse(null), LABEL_VERSION_CONTROL_ID, matches);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ConnectionMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ConnectionMatcher.java
new file mode 100644
index 0000000..7520598
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ConnectionMatcher.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.connectable.Connection;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.List;
+
+import static org.apache.nifi.web.search.attributematchers.AttributeMatcher.addIfMatching;
+
+public class ConnectionMatcher implements AttributeMatcher<Connection> {
+    private static final String LABEL_ID = "Id";
+    private static final String LABEL_VERSION_CONTROL_ID = "Version Control ID";
+    private static final String LABEL_NAME = "Name";
+
+    @Override
+    public void match(final Connection component, final SearchQuery query, final List<String> matches) {
+        final String searchTerm = query.getTerm();
+
+        addIfMatching(searchTerm, component.getIdentifier(), LABEL_ID, matches);
+        addIfMatching(searchTerm, component.getVersionedComponentId().orElse(null), LABEL_VERSION_CONTROL_ID, matches);
+        addIfMatching(searchTerm, component.getName(), LABEL_NAME, matches);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ConnectionRelationshipMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ConnectionRelationshipMatcher.java
new file mode 100644
index 0000000..2390598
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ConnectionRelationshipMatcher.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.connectable.Connection;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.List;
+
+public class ConnectionRelationshipMatcher implements AttributeMatcher<Connection> {
+    private static final String LABEL = "Relationship";
+
+    @Override
+    public void match(final Connection component, final SearchQuery query, final List<String> matches) {
+        component.getRelationships().forEach(r -> AttributeMatcher.addIfMatching(query.getTerm(), r.getName(), LABEL, matches));
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ConnectivityMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ConnectivityMatcher.java
new file mode 100644
index 0000000..def9245
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ConnectivityMatcher.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.connectable.Connectable;
+import org.apache.nifi.connectable.Connection;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.List;
+
+import static org.apache.nifi.web.search.attributematchers.AttributeMatcher.addIfMatching;
+
+public class ConnectivityMatcher implements AttributeMatcher<Connection> {
+    @Override
+    public void match(final Connection component, final SearchQuery query, final List<String> matches) {
+        final String searchTerm = query.getTerm();
+
+        final Connectable source = component.getSource();
+        addIfMatching(searchTerm, source.getIdentifier(), "Source id", matches);
+        addIfMatching(searchTerm, source.getName(), "Source name", matches);
+        addIfMatching(searchTerm, source.getComments(), "Source comments", matches);
+
+        final Connectable destination = component.getDestination();
+        addIfMatching(searchTerm, destination.getIdentifier(), "Destination id", matches);
+        addIfMatching(searchTerm, destination.getName(), "Destination name", matches);
+        addIfMatching(searchTerm, destination.getComments(), "Destination comments", matches);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ControllerServiceNodeMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ControllerServiceNodeMatcher.java
new file mode 100644
index 0000000..ab20d14
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ControllerServiceNodeMatcher.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.controller.service.ControllerServiceNode;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.List;
+
+import static org.apache.nifi.web.search.attributematchers.AttributeMatcher.addIfMatching;
+
+public class ControllerServiceNodeMatcher implements AttributeMatcher<ControllerServiceNode> {
+    private static final String LABEL_ID = "Id";
+    private static final String LABEL_VERSION_CONTROL_ID = "Version Control ID";
+    private static final String LABEL_NAME = "Name";
+    private static final String LABEL_COMMENTS = "Comments";
+
+    @Override
+    public void match(final ControllerServiceNode component, final SearchQuery query, final List<String> matches) {
+        final String searchTerm = query.getTerm();
+
+        addIfMatching(searchTerm, component.getIdentifier(), LABEL_ID, matches);
+        addIfMatching(searchTerm, component.getVersionedComponentId().orElse(null), LABEL_VERSION_CONTROL_ID, matches);
+        addIfMatching(searchTerm, component.getName(), LABEL_NAME, matches);
+        addIfMatching(searchTerm, component.getComments(), LABEL_COMMENTS, matches);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ExecutionMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ExecutionMatcher.java
new file mode 100644
index 0000000..3c82d46
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ExecutionMatcher.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.controller.ProcessorNode;
+import org.apache.nifi.scheduling.ExecutionNode;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.List;
+
+public class ExecutionMatcher implements AttributeMatcher<ProcessorNode>  {
+    private static final String SEARCH_TERM = "primary";
+    private static final String MATCH_LABEL = "Execution node: primary";
+
+    @Override
+    public void match(final ProcessorNode component, final SearchQuery query, final List<String> matches) {
+        if (ExecutionNode.PRIMARY.equals(component.getExecutionNode()) && StringUtils.containsIgnoreCase(SEARCH_TERM, query.getTerm())) {
+            matches.add(MATCH_LABEL);
+        }
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ExpirationMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ExpirationMatcher.java
new file mode 100644
index 0000000..0c6bc3f
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ExpirationMatcher.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.connectable.Connection;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+public class ExpirationMatcher implements AttributeMatcher<Connection> {
+    private static final String MATCH_PREFIX = "FlowFile expiration: ";
+    private static final Set<String> KEYWORDS = new HashSet<>(Arrays.asList(
+            "expires",
+            "expiration"));
+
+    @Override
+    public void match(final Connection component, final SearchQuery query, final List<String> matches) {
+        if (containsKeyword(query)) {
+            final int expirationMillis = component.getFlowFileQueue().getFlowFileExpiration(TimeUnit.MILLISECONDS);
+
+            if (expirationMillis > 0) {
+                matches.add(MATCH_PREFIX + component.getFlowFileQueue().getFlowFileExpiration());
+            }
+        }
+    }
+
+    private boolean containsKeyword(final SearchQuery query) {
+        return KEYWORDS.stream().anyMatch(keyword -> StringUtils.containsIgnoreCase(keyword, query.getTerm()));
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ExtendedMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ExtendedMatcher.java
new file mode 100644
index 0000000..8fc9ebd
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ExtendedMatcher.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.connectable.Connectable;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.List;
+
+import static org.apache.nifi.web.search.attributematchers.AttributeMatcher.addIfMatching;
+
+public class ExtendedMatcher<T extends Connectable> extends BasicMatcher<T> {
+    private static final String LABEL_NAME = "Name";
+    private static final String LABEL_COMMENTS = "Comments";
+
+    @Override
+    public void match(final T component, final SearchQuery query, final List<String> matches) {
+        super.match(component, query, matches);
+        final String searchTerm = query.getTerm();
+
+        addIfMatching(searchTerm, component.getName(), LABEL_NAME, matches);
+        addIfMatching(searchTerm, component.getComments(), LABEL_COMMENTS, matches);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/LabelMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/LabelMatcher.java
new file mode 100644
index 0000000..de78cd9
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/LabelMatcher.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.controller.label.Label;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.List;
+
+import static org.apache.nifi.web.search.attributematchers.AttributeMatcher.addIfMatching;
+
+public class LabelMatcher implements AttributeMatcher<Label>  {
+    private static final String LABEL_ID = "Id";
+    private static final String LABEL_VALUE = "Value";
+
+    @Override
+    public void match(final Label component, final SearchQuery query, final List<String> matches) {
+        final String searchTerm = query.getTerm();
+
+        addIfMatching(searchTerm, component.getIdentifier(), LABEL_ID, matches);
+        addIfMatching(searchTerm, component.getValue(), LABEL_VALUE, matches);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ParameterContextMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ParameterContextMatcher.java
new file mode 100644
index 0000000..b60a849
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ParameterContextMatcher.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.parameter.ParameterContext;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.List;
+
+import static org.apache.nifi.web.search.attributematchers.AttributeMatcher.addIfMatching;
+
+public class ParameterContextMatcher implements AttributeMatcher<ParameterContext> {
+    private static final String LABEL_ID = "Id";
+    private static final String LABEL_NAME = "Name";
+    private static final String LABEL_DESCRIPTION = "Description";
+
+    @Override
+    public void match(final ParameterContext component, final SearchQuery query, final List<String> matches) {
+        final String searchTerm = query.getTerm();
+
+        addIfMatching(searchTerm, component.getIdentifier(), LABEL_ID, matches);
+        addIfMatching(searchTerm, component.getName(), LABEL_NAME, matches);
+        addIfMatching(searchTerm, component.getDescription(), LABEL_DESCRIPTION, matches);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ParameterMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ParameterMatcher.java
new file mode 100644
index 0000000..37a21e5
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ParameterMatcher.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.parameter.Parameter;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.List;
+
+import static org.apache.nifi.web.search.attributematchers.AttributeMatcher.addIfMatching;
+
+public class ParameterMatcher implements AttributeMatcher<Parameter> {
+    private static final String LABEL_NAME = "Name";
+    private static final String LABEL_VALUE = "Value";
+    private static final String LABEL_DESCRIPTION = "Description";
+
+    @Override
+    public void match(final Parameter component, final SearchQuery query, final List<String> matches) {
+        final String searchTerm = query.getTerm();
+
+        addIfMatching(searchTerm, component.getDescriptor().getName(), LABEL_NAME, matches);
+        addIfMatching(searchTerm, component.getDescriptor().getDescription(), LABEL_DESCRIPTION, matches);
+
+        if (!component.getDescriptor().isSensitive()) {
+            addIfMatching(searchTerm, component.getValue(), LABEL_VALUE, matches);
+        }
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/PortScheduledStateMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/PortScheduledStateMatcher.java
new file mode 100644
index 0000000..7cb3af8
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/PortScheduledStateMatcher.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.connectable.Port;
+import org.apache.nifi.controller.ScheduledState;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.List;
+
+public class PortScheduledStateMatcher implements AttributeMatcher<Port> {
+    private static final String SEARCH_TERM_DISABLED = "disabled";
+    private static final String SEARCH_TERM_INVALID = "invalid";
+    private static final String SEARCH_TERM_RUNNING = "running";
+    private static final String SEARCH_TERM_STOPPED = "stopped";
+
+    private static final String MATCH_PREFIX = "Run status: ";
+    private static final String MATCH_DISABLED = "Disabled";
+    private static final String MATCH_INVALID = "Invalid";
+    private static final String MATCH_RUNNING = "Running";
+    private static final String MATCH_STOPPED = "Stopped";
+
+    @Override
+    public void match(final Port component, final SearchQuery query, final List<String> matches) {
+        final String searchTerm = query.getTerm();
+
+        if (ScheduledState.DISABLED.equals(component.getScheduledState())) {
+            if (StringUtils.containsIgnoreCase(SEARCH_TERM_DISABLED, searchTerm)) {
+                matches.add(MATCH_PREFIX + MATCH_DISABLED);
+            }
+        } else if (StringUtils.containsIgnoreCase(SEARCH_TERM_INVALID, searchTerm) && !component.isValid()) {
+            matches.add(MATCH_PREFIX + MATCH_INVALID);
+        } else if (ScheduledState.RUNNING.equals(component.getScheduledState()) && StringUtils.containsIgnoreCase(SEARCH_TERM_RUNNING, searchTerm)) {
+            matches.add(MATCH_PREFIX + MATCH_RUNNING);
+        } else if (ScheduledState.STOPPED.equals(component.getScheduledState()) && StringUtils.containsIgnoreCase(SEARCH_TERM_STOPPED, searchTerm)) {
+            matches.add(MATCH_PREFIX + MATCH_STOPPED);
+        }
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/PrioritiesMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/PrioritiesMatcher.java
new file mode 100644
index 0000000..8cc0af0
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/PrioritiesMatcher.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.connectable.Connection;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.List;
+
+import static org.apache.nifi.web.search.attributematchers.AttributeMatcher.addIfMatching;
+
+public class PrioritiesMatcher implements AttributeMatcher<Connection> {
+    private static final String LABEL = "Prioritizer";
+
+    @Override
+    public void match(final Connection component, final SearchQuery query, final List<String> matches) {
+        component.getFlowFileQueue().getPriorities().forEach(prioritizer -> addIfMatching(query.getTerm(), prioritizer.getClass().getName(), LABEL, matches));
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ProcessGroupMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ProcessGroupMatcher.java
new file mode 100644
index 0000000..e4cd276
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ProcessGroupMatcher.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.List;
+
+import static org.apache.nifi.web.search.attributematchers.AttributeMatcher.addIfMatching;
+
+public class ProcessGroupMatcher implements AttributeMatcher<ProcessGroup> {
+    private static final String LABEL_ID = "Id";
+    private static final String LABEL_VERSION_CONTROL_ID = "Version Control ID";
+    private static final String LABEL_NAME = "Name";
+    private static final String LABEL_COMMENTS = "Comments";
+
+    @Override
+    public void match(final ProcessGroup component, final SearchQuery query, final List<String> matches) {
+        final String searchTerm = query.getTerm();
+
+        addIfMatching(searchTerm, component.getIdentifier(), LABEL_ID, matches);
+        addIfMatching(searchTerm, component.getVersionedComponentId().orElse(null), LABEL_VERSION_CONTROL_ID, matches);
+        addIfMatching(searchTerm, component.getName(), LABEL_NAME, matches);
+        addIfMatching(searchTerm, component.getComments(), LABEL_COMMENTS, matches);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ProcessorMetadataMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ProcessorMetadataMatcher.java
new file mode 100644
index 0000000..9e358ef
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ProcessorMetadataMatcher.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.controller.ProcessorNode;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.List;
+
+public class ProcessorMetadataMatcher implements AttributeMatcher<ProcessorNode> {
+    private static final String LABEL = "Type";
+
+    @Override
+    public void match(final ProcessorNode component, final SearchQuery query, final List<String> matches) {
+        final String searchTerm = query.getTerm();
+        AttributeMatcher.addIfMatching(searchTerm, component.getProcessor().getClass().getSimpleName(), LABEL, matches);
+        AttributeMatcher.addIfMatching(searchTerm, component.getComponentType(), LABEL, matches);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/PropertyMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/PropertyMatcher.java
new file mode 100644
index 0000000..6d3c08f
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/PropertyMatcher.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.controller.ComponentNode;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.apache.nifi.web.search.attributematchers.AttributeMatcher.addIfMatching;
+
+public class PropertyMatcher implements AttributeMatcher<ComponentNode> {
+    private static final String LABEL_NAME = "Property name";
+    private static final String LABEL_VALUE = "Property value";
+    private static final String LABEL_DESCRIPTION = "Property description";
+
+    private final static String FILTER_NAME_PROPERTIES = "properties";
+    private final static Set<String> FILTER_VALUES_PROPERTIES_EXCLUSION = new HashSet<>(Arrays.asList("no", "none", "false", "exclude", "0"));
+
+    @Override
+    public void match(final ComponentNode component, final SearchQuery query, final List<String> matches) {
+        final String searchTerm = query.getTerm();
+
+        if (!propertiesAreFilteredOut(query)) {
+            for (final Map.Entry<PropertyDescriptor, String> entry : component.getRawPropertyValues().entrySet()) {
+                final PropertyDescriptor descriptor = entry.getKey();
+                addIfMatching(searchTerm, descriptor.getName(), LABEL_NAME, matches);
+                addIfMatching(searchTerm, descriptor.getDescription(), LABEL_DESCRIPTION, matches);
+
+                // never include sensitive properties values in search results
+                if (!descriptor.isSensitive()) {
+                    final String value = Optional.ofNullable(entry.getValue()).orElse(descriptor.getDefaultValue());
+
+                    // evaluate if the value matches the search criteria
+                    if (StringUtils.containsIgnoreCase(value, searchTerm)) {
+                        matches.add(LABEL_VALUE + SEPARATOR + descriptor.getName() + " - " + value);
+                    }
+                }
+            }
+        }
+    }
+
+    private boolean propertiesAreFilteredOut(final SearchQuery query) {
+        return query.hasFilter(FILTER_NAME_PROPERTIES) && FILTER_VALUES_PROPERTIES_EXCLUSION.contains(query.getFilter(FILTER_NAME_PROPERTIES));
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/PublicPortMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/PublicPortMatcher.java
new file mode 100644
index 0000000..417ee38
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/PublicPortMatcher.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.connectable.Port;
+import org.apache.nifi.remote.PublicPort;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.List;
+
+import static org.apache.nifi.web.search.attributematchers.AttributeMatcher.addIfMatching;
+
+public class PublicPortMatcher implements AttributeMatcher<Port> {
+    private static final String LABEL_USER = "User access control";
+    private static final String LABEL_GROUP = "Group access control";
+
+    @Override
+    public void match(final Port component, final SearchQuery query, final List<String> matches) {
+        final String searchTerm = query.getTerm();
+
+        if (component instanceof PublicPort) {
+            final PublicPort publicPort = (PublicPort) component;
+
+            publicPort.getUserAccessControl().forEach(control -> addIfMatching(searchTerm, control, LABEL_USER, matches));
+            publicPort.getGroupAccessControl().forEach(control -> addIfMatching(searchTerm, control, LABEL_GROUP, matches));
+        }
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/RelationshipMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/RelationshipMatcher.java
new file mode 100644
index 0000000..e0c4751
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/RelationshipMatcher.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.connectable.Connectable;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.List;
+
+public class RelationshipMatcher<T extends Connectable> implements AttributeMatcher<T> {
+    private static final String LABEL = "Relationship";
+
+    @Override
+    public void match(final T component, final SearchQuery query, final List<String> matches) {
+        component.getRelationships().forEach(r -> AttributeMatcher.addIfMatching(query.getTerm(), r.getName(), LABEL, matches));
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/RemoteProcessGroupMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/RemoteProcessGroupMatcher.java
new file mode 100644
index 0000000..573465d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/RemoteProcessGroupMatcher.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.groups.RemoteProcessGroup;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.List;
+
+import static org.apache.nifi.web.search.attributematchers.AttributeMatcher.addIfMatching;
+
+public class RemoteProcessGroupMatcher implements AttributeMatcher<RemoteProcessGroup> {
+    private static final String LABEL_ID = "Id";
+    private static final String LABEL_VERSION_CONTROL_ID = "Version Control ID";
+    private static final String LABEL_NAME = "Name";
+    private static final String LABEL_COMMENTS = "Comments";
+
+    @Override
+    public void match(final RemoteProcessGroup component, final SearchQuery query, final List<String> matches) {
+        final String searchTerm = query.getTerm();
+
+        addIfMatching(searchTerm, component.getIdentifier(), LABEL_ID, matches);
+        addIfMatching(searchTerm, component.getVersionedComponentId().orElse(null), LABEL_VERSION_CONTROL_ID, matches);
+        addIfMatching(searchTerm, component.getName(), LABEL_NAME, matches);
+        addIfMatching(searchTerm, component.getComments(), LABEL_COMMENTS, matches);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ScheduledStateMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ScheduledStateMatcher.java
new file mode 100644
index 0000000..44185a4
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/ScheduledStateMatcher.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.components.validation.ValidationStatus;
+import org.apache.nifi.controller.ProcessorNode;
+import org.apache.nifi.controller.ScheduledState;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.List;
+
+public class ScheduledStateMatcher implements AttributeMatcher<ProcessorNode>  {
+    private static final String SEARCH_TERM_DISABLED = "disabled";
+    private static final String SEARCH_TERM_INVALID = "invalid";
+    private static final String SEARCH_TERM_VALIDATING = "validating";
+    private static final String SEARCH_TERM_RUNNING = "running";
+    private static final String SEARCH_TERM_STOPPED = "stopped";
+
+    private static final String MATCH_PREFIX = "Run status: ";
+    private static final String MATCH_DISABLED = "Disabled";
+    private static final String MATCH_INVALID = "Invalid";
+    private static final String MATCH_VALIDATING = "Validating";
+    private static final String MATCH_RUNNING = "Running";
+    private static final String MATCH_STOPPED = "Stopped";
+
+    @Override
+    public void match(final ProcessorNode component, final SearchQuery query, final List<String> matches) {
+        final String searchTerm = query.getTerm();
+
+        if (ScheduledState.DISABLED.equals(component.getScheduledState())) {
+            if (StringUtils.containsIgnoreCase(SEARCH_TERM_DISABLED, searchTerm)) {
+                matches.add(MATCH_PREFIX + MATCH_DISABLED);
+            }
+        } else if (StringUtils.containsIgnoreCase(SEARCH_TERM_INVALID, searchTerm) && component.getValidationStatus() == ValidationStatus.INVALID) {
+            matches.add(MATCH_PREFIX + MATCH_INVALID);
+        } else if (StringUtils.containsIgnoreCase(SEARCH_TERM_VALIDATING, searchTerm) && component.getValidationStatus() == ValidationStatus.VALIDATING) {
+            matches.add(MATCH_PREFIX + MATCH_VALIDATING);
+        } else if (ScheduledState.RUNNING.equals(component.getScheduledState()) && StringUtils.containsIgnoreCase(SEARCH_TERM_RUNNING, searchTerm)) {
+            matches.add(MATCH_PREFIX + MATCH_RUNNING);
+        } else if (ScheduledState.STOPPED.equals(component.getScheduledState()) && StringUtils.containsIgnoreCase(SEARCH_TERM_STOPPED, searchTerm)) {
+            matches.add(MATCH_PREFIX + MATCH_STOPPED);
+        }
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/SchedulingMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/SchedulingMatcher.java
new file mode 100644
index 0000000..326ae64
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/SchedulingMatcher.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.controller.ProcessorNode;
+import org.apache.nifi.scheduling.SchedulingStrategy;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.List;
+
+import static org.apache.nifi.scheduling.SchedulingStrategy.EVENT_DRIVEN;
+import static org.apache.nifi.scheduling.SchedulingStrategy.PRIMARY_NODE_ONLY;
+import static org.apache.nifi.scheduling.SchedulingStrategy.TIMER_DRIVEN;
+
+public class SchedulingMatcher implements AttributeMatcher<ProcessorNode> {
+    private static final String SEARCH_TERM_EVENT = "event";
+    private static final String SEARCH_TERM_TIMER = "timer";
+    private static final String SEARCH_TERM_PRIMARY = "primary";
+
+    private static final String MATCH_PREFIX = "Scheduling strategy: ";
+    private static final String MATCH_EVENT = "Event driven";
+    private static final String MATCH_TIMER = "Timer driven";
+    private static final String MATCH_PRIMARY = "On primary node";
+
+    @Override
+    public void match(final ProcessorNode component, final SearchQuery query, final List<String> matches) {
+        final String searchTerm = query.getTerm();
+        final SchedulingStrategy schedulingStrategy = component.getSchedulingStrategy();
+
+        if (EVENT_DRIVEN.equals(schedulingStrategy) && StringUtils.containsIgnoreCase(SEARCH_TERM_EVENT, searchTerm)) {
+            matches.add(MATCH_PREFIX + MATCH_EVENT);
+        } else if (TIMER_DRIVEN.equals(schedulingStrategy) && StringUtils.containsIgnoreCase(SEARCH_TERM_TIMER, searchTerm)) {
+            matches.add(MATCH_PREFIX + MATCH_TIMER);
+        } else if (PRIMARY_NODE_ONLY.equals(schedulingStrategy) && StringUtils.containsIgnoreCase(SEARCH_TERM_PRIMARY, searchTerm)) {
+            // PRIMARY_NODE_ONLY has been deprecated as a SchedulingStrategy and replaced by PRIMARY as an ExecutionNode.
+            matches.add(MATCH_PREFIX + MATCH_PRIMARY);
+        }
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/SearchableMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/SearchableMatcher.java
new file mode 100644
index 0000000..b98911e
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/SearchableMatcher.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.controller.FlowController;
+import org.apache.nifi.controller.ProcessorNode;
+import org.apache.nifi.nar.NarCloseable;
+import org.apache.nifi.processor.Processor;
+import org.apache.nifi.registry.VariableRegistry;
+import org.apache.nifi.search.SearchContext;
+import org.apache.nifi.search.Searchable;
+import org.apache.nifi.web.controller.StandardSearchContext;
+import org.apache.nifi.web.search.query.SearchQuery;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+public class SearchableMatcher implements AttributeMatcher<ProcessorNode> {
+    private static final Logger LOGGER = LoggerFactory.getLogger(SearchableMatcher.class);
+
+    private FlowController flowController;
+    private VariableRegistry variableRegistry;
+
+    @Override
+    public void match(final ProcessorNode component, final SearchQuery query, final List<String> matches) {
+        final Processor processor = component.getProcessor();
+
+        if (processor instanceof Searchable) {
+            final Searchable searchable = (Searchable) processor;
+            final String searchTerm = query.getTerm();
+            final SearchContext context = new StandardSearchContext(searchTerm, component, flowController.getControllerServiceProvider(), variableRegistry);
+
+            // search the processor using the appropriate thread context classloader
+            try (final NarCloseable narCloseable = NarCloseable.withComponentNarLoader(flowController.getExtensionManager(), component.getClass(), component.getIdentifier())) {
+                searchable.search(context).stream().forEach(searchResult -> matches.add(searchResult.getLabel() + AttributeMatcher.SEPARATOR + searchResult.getMatch()));
+            } catch (final Throwable t) {
+                LOGGER.error("Error happened during searchable matching: {}", t.getMessage());
+                t.printStackTrace();
+            }
+        }
+    }
+
+    public void setFlowController(final FlowController flowController) {
+        this.flowController = flowController;
+    }
+
+    public void setVariableRegistry(final VariableRegistry variableRegistry) {
+        this.variableRegistry = variableRegistry;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/TargetUriMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/TargetUriMatcher.java
new file mode 100644
index 0000000..53b50b0
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/TargetUriMatcher.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.groups.RemoteProcessGroup;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.List;
+
+public class TargetUriMatcher implements AttributeMatcher<RemoteProcessGroup> {
+    private static final String LABEL = "URLs";
+
+    @Override
+    public void match(final RemoteProcessGroup component, final SearchQuery query, final List<String> matches) {
+        AttributeMatcher.addIfMatching(query.getTerm(), component.getTargetUris(), LABEL, matches);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/TransmissionStatusMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/TransmissionStatusMatcher.java
new file mode 100644
index 0000000..53e8252
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/TransmissionStatusMatcher.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.groups.RemoteProcessGroup;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class TransmissionStatusMatcher implements AttributeMatcher<RemoteProcessGroup> {
+    private static final Set<String> ON_KEYWORDS = new HashSet<>(Arrays.asList(
+            "transmitting",
+            "transmission enabled"));
+    private static final Set<String> OFF_KEYWORDS = new HashSet<>(Arrays.asList(
+            "not transmitting",
+            "transmission disabled"));
+
+    @Override
+    public void match(final RemoteProcessGroup component, final SearchQuery query, final List<String> matches) {
+        if (containsKeyword(query, ON_KEYWORDS) && component.isTransmitting()) {
+            matches.add("Transmission: On");
+        } else if (containsKeyword(query, OFF_KEYWORDS) && !containsKeyword(query, ON_KEYWORDS) && !component.isTransmitting()) {
+            matches.add("Transmission: Off");
+        }
+    }
+
+    private boolean containsKeyword(final SearchQuery query, final Set<String> keywords) {
+        return keywords.stream().anyMatch(keyword -> StringUtils.containsIgnoreCase(keyword, query.getTerm()));
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/VariableRegistryMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/VariableRegistryMatcher.java
new file mode 100644
index 0000000..f506ded
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/attributematchers/VariableRegistryMatcher.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.registry.ComponentVariableRegistry;
+import org.apache.nifi.registry.VariableDescriptor;
+import org.apache.nifi.web.search.query.SearchQuery;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.apache.nifi.web.search.attributematchers.AttributeMatcher.addIfMatching;
+
+public class VariableRegistryMatcher implements AttributeMatcher<ProcessGroup> {
+    private static final String LABEL_NAME = "Variable Name";
+    private static final String LABEL_VALUE = "Variable Value";
+
+    @Override
+    public void match(final ProcessGroup component, final SearchQuery query, final List<String> matches) {
+        final ComponentVariableRegistry variableRegistry = component.getVariableRegistry();
+
+        if (variableRegistry != null) {
+            for (final Map.Entry<VariableDescriptor, String> entry : variableRegistry.getVariableMap().entrySet()) {
+                addIfMatching(query.getTerm(), entry.getKey().getName(), LABEL_NAME, matches);
+                addIfMatching(query.getTerm(), entry.getValue(), LABEL_VALUE, matches);
+            }
+        }
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/query/MapBasedSearchQuery.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/query/MapBasedSearchQuery.java
new file mode 100644
index 0000000..194b84a
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/query/MapBasedSearchQuery.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.query;
+
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.groups.ProcessGroup;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class MapBasedSearchQuery implements SearchQuery {
+    private final String term;
+    private final Map<String, String> filters = new HashMap<>();
+    private final NiFiUser user;
+    private final ProcessGroup rootGroup;
+    private final ProcessGroup activeGroup;
+
+    public MapBasedSearchQuery(final String term, final Map<String, String> filters, final NiFiUser user, final ProcessGroup rootGroup, final ProcessGroup activeGroup) {
+        this.term = term;
+        this.filters.putAll(filters);
+        this.user = user;
+        this.rootGroup = rootGroup;
+        this.activeGroup = activeGroup;
+
+    }
+
+    public String getTerm() {
+        return term;
+    }
+
+    @Override
+    public boolean hasFilter(final String filterName) {
+        return filters.containsKey(filterName);
+    }
+
+    @Override
+    public String getFilter(final String filterName) {
+        return filters.get(filterName);
+    }
+
+    @Override
+    public NiFiUser getUser() {
+        return user;
+    }
+
+    @Override
+    public ProcessGroup getRootGroup() {
+        return rootGroup;
+    }
+
+    @Override
+    public ProcessGroup getActiveGroup() {
+        return activeGroup;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/query/RegexSearchQueryParser.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/query/RegexSearchQueryParser.java
new file mode 100644
index 0000000..05c9553
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/query/RegexSearchQueryParser.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.query;
+
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.groups.ProcessGroup;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+public class RegexSearchQueryParser implements SearchQueryParser {
+    private static final String REGEX = "(?<filter>(\\w+:\\w+\\s+)*(\\w+:\\w+)?)(?<term>.*)";
+    private static final String FILTER_TOKEN_SEPARATOR = "\\:";
+    private static final String FILTER_SEPARATOR = "[\\s]+";
+    private static final String FILTER_GROUP = "filter";
+    private static final String TERM_GROUP = "term";
+
+    private final Pattern pattern;
+
+    public RegexSearchQueryParser() {
+        this.pattern = Pattern.compile(REGEX);
+    }
+
+    @Override
+    public SearchQuery parse(final String searchLiteral, final NiFiUser user, final ProcessGroup rootGroup, final ProcessGroup activeGroup)  {
+        final Matcher matcher = pattern.matcher(searchLiteral);
+        if (matcher.matches()) {
+            final String term = matcher.group(TERM_GROUP);
+            final String filters = matcher.group(FILTER_GROUP);
+            return new MapBasedSearchQuery(term, processFilters(filters), user, rootGroup, activeGroup);
+        } else {
+            return new MapBasedSearchQuery(searchLiteral, Collections.emptyMap(), user, rootGroup, activeGroup);
+        }
+    }
+
+    private Map<String, String> processFilters(final String filters) {
+        return Arrays.stream(filters.split(FILTER_SEPARATOR))
+                .map(token -> token.split(FILTER_TOKEN_SEPARATOR))
+                .filter(filter -> filter.length == 2)
+                .collect(Collectors.toMap(filter -> filter[0].trim(), filter -> filter[1].trim(), (first, second) -> first));
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/query/SearchQuery.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/query/SearchQuery.java
new file mode 100644
index 0000000..3fb1214
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/query/SearchQuery.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.query;
+
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.groups.ProcessGroup;
+
+/**
+ * Represents the data set the search query executes based on.
+ */
+public interface SearchQuery {
+
+    /**
+     * The part of the query string not containing metadata (filters).
+     *
+     * @return The query string used for executing the search.
+     */
+    String getTerm();
+
+    /**
+     * Returns true if the query contains a given filter (regardless the value).
+     *
+     * @param filterName The name of the filter.
+     *
+     * @return True if the query contains the filter, false otherwise.
+     */
+    boolean hasFilter(String filterName);
+
+    /**
+     * Returns the value of the query filter. Should be checked with {@link #hasFilter} beforehand!
+     *
+     * @param filterName The name of the filter.
+     *
+     * @return The value of the filter if exists, otherwise it's null.
+     */
+    String getFilter(String filterName);
+
+    /**
+     * Returns the user who executes the query.
+     *
+     * @return Requesting user.
+     */
+    NiFiUser getUser();
+
+    /**
+     * References to the flow's root process group.
+     *
+     * @return Root group of the flow.
+     */
+    ProcessGroup getRootGroup();
+
+    /**
+     * References to the process group was active for the user during requesting the search. This might be the same as the root group.
+     *
+     * @return The user's active group.
+     */
+    ProcessGroup getActiveGroup();
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/query/SearchQueryParser.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/query/SearchQueryParser.java
new file mode 100644
index 0000000..3a27250
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/query/SearchQueryParser.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.query;
+
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.groups.ProcessGroup;
+
+/**
+ * Service responsible to translating incoming user and contextual information.
+ */
+public interface SearchQueryParser {
+
+    /**
+     * Parses the incoming and contextual data and returns with a query object.
+     *
+     * @param searchLiteral The original search string provided by the user.
+     * @param user The requesting user.
+     * @param rootGroup The root process group of the flow.
+     * @param activeGroup The process group was active for the user during the time of query.
+     *
+     * @return Returns a query object containing all the details needed for the search.
+     */
+    SearchQuery parse(String searchLiteral, NiFiUser user, ProcessGroup rootGroup, ProcessGroup activeGroup);
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/resultenrichment/AbstractComponentSearchResultEnricher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/resultenrichment/AbstractComponentSearchResultEnricher.java
new file mode 100644
index 0000000..1367883
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/resultenrichment/AbstractComponentSearchResultEnricher.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.resultenrichment;
+
+import org.apache.nifi.authorization.Authorizer;
+import org.apache.nifi.authorization.RequestAction;
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.web.api.dto.search.SearchResultGroupDTO;
+
+abstract class AbstractComponentSearchResultEnricher implements ComponentSearchResultEnricher {
+    protected final ProcessGroup processGroup;
+    protected final NiFiUser user;
+    protected final Authorizer authorizer;
+
+    AbstractComponentSearchResultEnricher(final ProcessGroup processGroup, final NiFiUser user, final Authorizer authorizer) {
+        this.processGroup = processGroup;
+        this.user = user;
+        this.authorizer = authorizer;
+    }
+
+    /**
+     * Builds the nearest versioned parent result group for a given user.
+     *
+     * @param group The containing group
+     * @param user The current NiFi user
+     * @return Versioned parent group
+     */
+    protected SearchResultGroupDTO buildVersionedGroup(final ProcessGroup group, final NiFiUser user) {
+        if (group == null) {
+            return null;
+        }
+
+        ProcessGroup current = group;
+
+        // search for a versioned group by traversing the group tree up to the root
+        while (!current.isRootGroup()) {
+            if (current.getVersionControlInformation() != null) {
+                return buildResultGroup(current, user);
+            }
+
+            current = current.getParent();
+        }
+
+        // traversed all the way to the root
+        return null;
+    }
+
+    /**
+     * Builds result group for a given user.
+     *
+     * @param group The containing group
+     * @param user The current NiFi user
+     * @return Result group
+     */
+    protected SearchResultGroupDTO buildResultGroup(final ProcessGroup group, final NiFiUser user) {
+        if (group == null) {
+            return null;
+        }
+
+        final SearchResultGroupDTO resultGroup = new SearchResultGroupDTO();
+        resultGroup.setId(group.getIdentifier());
+
+        // keep the group name confidential
+        if (group.isAuthorized(authorizer, RequestAction.READ, user)) {
+            resultGroup.setName(group.getName());
+        }
+
+        return resultGroup;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/resultenrichment/ComponentSearchResultEnricher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/resultenrichment/ComponentSearchResultEnricher.java
new file mode 100644
index 0000000..2b9a4e3
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/resultenrichment/ComponentSearchResultEnricher.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.resultenrichment;
+
+import org.apache.nifi.web.api.dto.search.ComponentSearchResultDTO;
+
+/**
+ * Responsible for enriching the query result based on the containing component (for example process group).
+ */
+public interface ComponentSearchResultEnricher {
+    /**
+     * Enriches the incoming result object. Might change it or set one or more unset parameter.
+     *
+     * @param componentSearchResult The non-enriched result.
+     *
+     * @return The enriched result.
+     */
+    ComponentSearchResultDTO enrich(ComponentSearchResultDTO componentSearchResult);
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/resultenrichment/ComponentSearchResultEnricherFactory.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/resultenrichment/ComponentSearchResultEnricherFactory.java
new file mode 100644
index 0000000..fa72192
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/resultenrichment/ComponentSearchResultEnricherFactory.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.resultenrichment;
+
+import org.apache.nifi.authorization.Authorizer;
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.parameter.ParameterContext;
+
+public class ComponentSearchResultEnricherFactory {
+    private Authorizer authorizer;
+
+    public ComponentSearchResultEnricher getComponentResultEnricher(final ProcessGroup processGroup, final NiFiUser user) {
+        return new GeneralComponentSearchResultEnricher(processGroup, user, authorizer);
+    }
+
+    public ComponentSearchResultEnricher getProcessGroupResultEnricher(final ProcessGroup processGroup, final NiFiUser user) {
+        return new ProcessGroupSearchResultEnricher(processGroup, user, authorizer);
+    }
+
+    public ComponentSearchResultEnricher getParameterResultEnricher(final ParameterContext parameterContext) {
+        return new ParameterSearchResultEnricher(parameterContext);
+    }
+
+    public void setAuthorizer(Authorizer authorizer) {
+        this.authorizer = authorizer;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/resultenrichment/GeneralComponentSearchResultEnricher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/resultenrichment/GeneralComponentSearchResultEnricher.java
new file mode 100644
index 0000000..5c6fac6
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/resultenrichment/GeneralComponentSearchResultEnricher.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.resultenrichment;
+
+import org.apache.nifi.authorization.Authorizer;
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.web.api.dto.search.ComponentSearchResultDTO;
+
+public class GeneralComponentSearchResultEnricher extends AbstractComponentSearchResultEnricher {
+    public GeneralComponentSearchResultEnricher(final ProcessGroup processGroup, final NiFiUser user, final Authorizer authorizer) {
+        super(processGroup, user, authorizer);
+    }
+
+    @Override
+    public ComponentSearchResultDTO enrich(final ComponentSearchResultDTO input) {
+        input.setGroupId(processGroup.getIdentifier());
+        input.setParentGroup(buildResultGroup(processGroup, user));
+        input.setVersionedGroup(buildVersionedGroup(processGroup, user));
+        return input;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/resultenrichment/ParameterSearchResultEnricher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/resultenrichment/ParameterSearchResultEnricher.java
new file mode 100644
index 0000000..abf720d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/resultenrichment/ParameterSearchResultEnricher.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.resultenrichment;
+
+import org.apache.nifi.parameter.ParameterContext;
+import org.apache.nifi.web.api.dto.search.ComponentSearchResultDTO;
+import org.apache.nifi.web.api.dto.search.SearchResultGroupDTO;
+
+public class ParameterSearchResultEnricher implements ComponentSearchResultEnricher {
+    private final ParameterContext parameterContext;
+
+    public ParameterSearchResultEnricher(final ParameterContext parameterContext) {
+        this.parameterContext = parameterContext;
+    }
+
+    @Override
+    public ComponentSearchResultDTO enrich(final ComponentSearchResultDTO input) {
+        final SearchResultGroupDTO parentGroup = new SearchResultGroupDTO();
+        parentGroup.setId(parameterContext.getIdentifier());
+        parentGroup.setName(parameterContext.getName());
+        input.setParentGroup(parentGroup);
+        return input;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/resultenrichment/ProcessGroupSearchResultEnricher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/resultenrichment/ProcessGroupSearchResultEnricher.java
new file mode 100644
index 0000000..3c0ee4c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/search/resultenrichment/ProcessGroupSearchResultEnricher.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.resultenrichment;
+
+import org.apache.nifi.authorization.Authorizer;
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.web.api.dto.search.ComponentSearchResultDTO;
+
+public class ProcessGroupSearchResultEnricher extends AbstractComponentSearchResultEnricher {
+
+    public ProcessGroupSearchResultEnricher(final ProcessGroup processGroup, final NiFiUser user, final Authorizer authorizer) {
+        super(processGroup, user, authorizer);
+    }
+
+    @Override
+    public ComponentSearchResultDTO enrich(final ComponentSearchResultDTO input) {
+        input.setGroupId(processGroup.getParent().getIdentifier());
+        input.setParentGroup(buildResultGroup(processGroup.getParent(), user));
+        input.setVersionedGroup(buildVersionedGroup(processGroup.getParent(), user));
+        return input;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
index ad8cf11..162560e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/resources/nifi-web-api-context.xml
@@ -75,6 +75,141 @@
         <property name="dtoFactory" ref="dtoFactory" />
     </bean>
 
+    <!-- search functionality -->
+    <bean id="searchQueryParser" class="org.apache.nifi.web.search.query.RegexSearchQueryParser" />
+    <bean id="resultEnricherFactory" class="org.apache.nifi.web.search.resultenrichment.ComponentSearchResultEnricherFactory">
+        <property name="authorizer" ref="authorizer" />
+    </bean>
+
+    <bean id="basicMatcher" class="org.apache.nifi.web.search.attributematchers.BasicMatcher" />
+    <bean id="extendedMatcher" class="org.apache.nifi.web.search.attributematchers.ExtendedMatcher" />
+    <bean id="schedulingMatcher" class="org.apache.nifi.web.search.attributematchers.SchedulingMatcher" />
+    <bean id="executionMatcher" class="org.apache.nifi.web.search.attributematchers.ExecutionMatcher" />
+    <bean id="scheduledStateMatcher" class="org.apache.nifi.web.search.attributematchers.ScheduledStateMatcher" />
+    <bean id="relationshipMatcher" class="org.apache.nifi.web.search.attributematchers.RelationshipMatcher" />
+    <bean id="connectionRelationshipMatcher" class="org.apache.nifi.web.search.attributematchers.ConnectionRelationshipMatcher" />
+    <bean id="processorMetadataMatcher" class="org.apache.nifi.web.search.attributematchers.ProcessorMetadataMatcher" />
+    <bean id="propertyMatcher" class="org.apache.nifi.web.search.attributematchers.PropertyMatcher" />
+    <bean id="searchableMatcher" class="org.apache.nifi.web.search.attributematchers.SearchableMatcher">
+        <property name="flowController" ref="flowController" />
+        <property name="variableRegistry" ref="variableRegistry" />
+    </bean>
+    <bean id="processGroupMatcher" class="org.apache.nifi.web.search.attributematchers.ProcessGroupMatcher" />
+    <bean id="variableRegistryMatcher" class="org.apache.nifi.web.search.attributematchers.VariableRegistryMatcher" />
+    <bean id="connectionMatcher" class="org.apache.nifi.web.search.attributematchers.ConnectionMatcher" />
+    <bean id="prioritiesMatcher" class="org.apache.nifi.web.search.attributematchers.PrioritiesMatcher" />
+    <bean id="expirationMatcher" class="org.apache.nifi.web.search.attributematchers.ExpirationMatcher" />
+    <bean id="backPressureMatcher" class="org.apache.nifi.web.search.attributematchers.BackPressureMatcher" />
+    <bean id="connectivityMatcher" class="org.apache.nifi.web.search.attributematchers.ConnectivityMatcher" />
+    <bean id="remoteProcessGroupMatcher" class="org.apache.nifi.web.search.attributematchers.RemoteProcessGroupMatcher" />
+    <bean id="targetUriMatcher" class="org.apache.nifi.web.search.attributematchers.TargetUriMatcher" />
+    <bean id="transmissionStatusMatcher" class="org.apache.nifi.web.search.attributematchers.TransmissionStatusMatcher" />
+    <bean id="portScheduledStateMatcher" class="org.apache.nifi.web.search.attributematchers.PortScheduledStateMatcher" />
+    <bean id="publicPortMatcher" class="org.apache.nifi.web.search.attributematchers.PublicPortMatcher" />
+    <bean id="parameterContextMatcher" class="org.apache.nifi.web.search.attributematchers.ParameterContextMatcher" />
+    <bean id="parameterMatcher" class="org.apache.nifi.web.search.attributematchers.ParameterMatcher" />
+    <bean id="labelMatcher" class="org.apache.nifi.web.search.attributematchers.LabelMatcher" />
+    <bean id="controllerServiceNodeMatcher" class="org.apache.nifi.web.search.attributematchers.ControllerServiceNodeMatcher" />
+
+    <bean id="componentMatcherFactory" class="org.apache.nifi.web.search.ComponentMatcherFactory" />
+
+    <bean id="matcherForProcessor" factory-bean="componentMatcherFactory" factory-method="getInstanceForConnectable">
+        <constructor-arg>
+            <util:list>
+                <ref bean="extendedMatcher" />
+                <ref bean="schedulingMatcher" />
+                <ref bean="executionMatcher" />
+                <ref bean="scheduledStateMatcher" />
+                <ref bean="relationshipMatcher" />
+                <ref bean="processorMetadataMatcher" />
+                <ref bean="propertyMatcher" />
+                <ref bean="searchableMatcher" />
+            </util:list>
+        </constructor-arg>
+    </bean>
+
+    <bean id="matcherForProcessGroup" factory-bean="componentMatcherFactory" factory-method="getInstanceForProcessGroup">
+        <constructor-arg>
+            <util:list>
+                <ref bean="processGroupMatcher" />
+                <ref bean="variableRegistryMatcher" />
+            </util:list>
+        </constructor-arg>
+    </bean>
+
+    <bean id="matcherForConnection" factory-bean="componentMatcherFactory" factory-method="getInstanceForConnection">
+        <constructor-arg>
+            <util:list>
+                <ref bean="connectionMatcher" />
+                <ref bean="connectionRelationshipMatcher" />
+                <ref bean="prioritiesMatcher" />
+                <ref bean="expirationMatcher" />
+                <ref bean="backPressureMatcher" />
+                <ref bean="connectivityMatcher" />
+            </util:list>
+        </constructor-arg>
+    </bean>
+
+    <bean id="matcherForRemoteProcessGroup" factory-bean="componentMatcherFactory" factory-method="getInstanceForRemoteProcessGroup">
+        <constructor-arg>
+            <util:list>
+                <ref bean="remoteProcessGroupMatcher" />
+                <ref bean="targetUriMatcher" />
+                <ref bean="transmissionStatusMatcher" />
+            </util:list>
+        </constructor-arg>
+    </bean>
+
+    <bean id="matcherForPort" factory-bean="componentMatcherFactory" factory-method="getInstanceForConnectable">
+        <constructor-arg>
+            <util:list>
+                <ref bean="extendedMatcher" />
+                <ref bean="portScheduledStateMatcher" />
+                <ref bean="publicPortMatcher" />
+            </util:list>
+        </constructor-arg>
+    </bean>
+
+    <bean id="matcherForFunnel" factory-bean="componentMatcherFactory" factory-method="getInstanceForConnectable">
+        <constructor-arg>
+            <util:list>
+                <ref bean="basicMatcher" />
+            </util:list>
+        </constructor-arg>
+    </bean>
+
+    <bean id="matcherForParameterContext" factory-bean="componentMatcherFactory" factory-method="getInstanceForParameterContext">
+        <constructor-arg>
+            <util:list>
+                <ref bean="parameterContextMatcher" />
+            </util:list>
+        </constructor-arg>
+    </bean>
+
+    <bean id="matcherForParameter" factory-bean="componentMatcherFactory" factory-method="getInstanceForParameter">
+        <constructor-arg>
+            <util:list>
+                <ref bean="parameterMatcher" />
+            </util:list>
+        </constructor-arg>
+    </bean>
+
+    <bean id="matcherForLabel" factory-bean="componentMatcherFactory" factory-method="getInstanceForLabel">
+        <constructor-arg>
+            <util:list>
+                <ref bean="labelMatcher" />
+            </util:list>
+        </constructor-arg>
+    </bean>
+
+    <bean id="matcherForControllerServiceNode" factory-bean="componentMatcherFactory" factory-method="getInstanceForControllerServiceNode">
+        <constructor-arg>
+            <util:list>
+                <ref bean="controllerServiceNodeMatcher" />
+                <ref bean="propertyMatcher" />
+            </util:list>
+        </constructor-arg>
+    </bean>
 
     <!-- nifi component dao initialization -->
     <bean id="processGroupDAO" class="org.apache.nifi.web.dao.impl.StandardProcessGroupDAO">
@@ -138,17 +273,28 @@
         <property name="flowController" ref="flowController" />
     </bean>
     <bean id="controllerSearchService" class="org.apache.nifi.web.controller.ControllerSearchService">
-        <property name="flowController" ref="flowController"/>
-        <property name="authorizer" ref="authorizer"/>
-        <property name="variableRegistry" ref="variableRegistry"/>
+        <property name="flowController" ref="flowController" />
+        <property name="authorizer" ref="authorizer" />
+        <property name="resultEnricherFactory" ref="resultEnricherFactory" />
+
+        <property name="matcherForProcessor" ref="matcherForProcessor" />
+        <property name="matcherForProcessGroup" ref="matcherForProcessGroup" />
+        <property name="matcherForConnection" ref="matcherForConnection" />
+        <property name="matcherForRemoteProcessGroup" ref="matcherForRemoteProcessGroup" />
+        <property name="matcherForPort" ref="matcherForPort" />
+        <property name="matcherForFunnel" ref="matcherForFunnel" />
+        <property name="matcherForParameterContext" ref="matcherForParameterContext" />
+        <property name="matcherForParameter" ref="matcherForParameter" />
+        <property name="matcherForLabel" ref="matcherForLabel" />
+        <property name="matcherForControllerServiceNode" ref="matcherForControllerServiceNode" />
     </bean>
     <bean id="controllerFacade" class="org.apache.nifi.web.controller.ControllerFacade">
-        <property name="properties" ref="nifiProperties"/>
         <property name="flowController" ref="flowController"/>
         <property name="flowService" ref="flowService"/>
         <property name="authorizer" ref="authorizer"/>
+        <property name="properties" ref="nifiProperties"/>
         <property name="dtoFactory" ref="dtoFactory"/>
-        <property name="variableRegistry" ref="variableRegistry"/>
+        <property name="searchQueryParser" ref="searchQueryParser"/>
         <property name="controllerSearchService" ref="controllerSearchService"/>
     </bean>
     <bean id="authorizableLookup" class="org.apache.nifi.authorization.StandardAuthorizableLookup">
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/AbstractControllerSearchIntegrationTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/AbstractControllerSearchIntegrationTest.java
new file mode 100644
index 0000000..61de4c1
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/AbstractControllerSearchIntegrationTest.java
@@ -0,0 +1,361 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.controller;
+
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.connectable.Connection;
+import org.apache.nifi.connectable.Funnel;
+import org.apache.nifi.connectable.Port;
+import org.apache.nifi.controller.FlowController;
+import org.apache.nifi.controller.ProcessorNode;
+import org.apache.nifi.controller.flow.FlowManager;
+import org.apache.nifi.controller.label.Label;
+import org.apache.nifi.controller.service.ControllerServiceNode;
+import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.groups.RemoteProcessGroup;
+import org.apache.nifi.nar.ExtensionManager;
+import org.apache.nifi.parameter.Parameter;
+import org.apache.nifi.parameter.ParameterContext;
+import org.apache.nifi.parameter.ParameterContextManager;
+import org.apache.nifi.parameter.ParameterDescriptor;
+import org.apache.nifi.web.api.dto.search.ComponentSearchResultDTO;
+import org.apache.nifi.web.api.dto.search.SearchResultGroupDTO;
+import org.apache.nifi.web.api.dto.search.SearchResultsDTO;
+import org.apache.nifi.web.search.query.RegexSearchQueryParser;
+import org.apache.nifi.web.search.query.SearchQuery;
+import org.apache.nifi.web.search.query.SearchQueryParser;
+import org.junit.Before;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.StringJoiner;
+import java.util.function.Function;
+
+import static org.apache.nifi.web.controller.ComponentMockUtil.getRootProcessGroup;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration(locations = {"classpath:nifi-web-api-test-context.xml", "classpath:nifi-web-api-context.xml"})
+public abstract class AbstractControllerSearchIntegrationTest {
+    protected static final String ROOT_PROCESSOR_GROUP_ID = "rootId";
+    protected static final String ROOT_PROCESSOR_GROUP_NAME = "rootName";
+
+    protected static final boolean AUTHORIZED = true;
+    protected static final boolean NOT_AUTHORIZED = false;
+
+    protected static final boolean UNDER_VERSION_CONTROL = true;
+    protected static final boolean NOT_UNDER_VERSION_CONTROL = false;
+
+    private NiFiUser user = Mockito.mock(NiFiUser.class);
+
+    private final SearchQueryParser searchQueryParser = new RegexSearchQueryParser();
+
+    protected SearchResultsDTO results;
+
+    private Set<ProcessGroupSetup> processGroups;
+
+    private Set<ParameterContext> parameterContexts;
+
+    @Autowired
+    private ControllerSearchService controllerSearchService;
+
+    @Autowired
+    private FlowController flowController;
+
+    @Before
+    public void setUp() {
+        resetResults();
+        processGroups = new HashSet<>();
+        parameterContexts = new HashSet<>();
+
+        final FlowManager flowManager = Mockito.mock(FlowManager.class);
+        final ParameterContextManager parameterContextManager = Mockito.mock(ParameterContextManager.class);
+
+        Mockito.when(flowController.getFlowManager()).thenReturn(flowManager);
+        Mockito.when(flowManager.getParameterContextManager()).thenReturn(parameterContextManager);
+        Mockito.when(parameterContextManager.getParameterContexts()).thenReturn(parameterContexts);
+
+        ExtensionManager extensionManager = Mockito.mock(ExtensionManager.class);
+        Mockito.when(flowController.getExtensionManager()).thenReturn(extensionManager);
+    }
+
+    // given
+
+    protected ProcessGroupSetup givenRootProcessGroup() {
+        return givenProcessGroup(getRootProcessGroup(ROOT_PROCESSOR_GROUP_ID, ROOT_PROCESSOR_GROUP_NAME, "", AUTHORIZED, NOT_UNDER_VERSION_CONTROL));
+    }
+
+    protected ProcessGroupSetup givenProcessGroup(final ProcessGroup processGroup) {
+        return new ProcessGroupSetup(processGroup);
+    }
+
+    protected void givenParameterContext(final ParameterContext parameterContext) {
+        parameterContexts.add(parameterContext);
+    }
+
+    protected Map<ParameterDescriptor, Parameter> givenParameters(final Parameter... parameters) {
+        final Map<ParameterDescriptor, Parameter> result = new HashMap<>();
+
+        for (final Parameter parameter : parameters) {
+            result.put(parameter.getDescriptor(), parameter);
+        }
+
+        return result;
+    }
+
+    // when
+
+    protected void whenExecuteSearch(final String searchString, final String activeGroupId) {
+        resetResults();
+        final SearchQuery searchQuery = searchQueryParser.parse(searchString, user, getProcessGroup(ROOT_PROCESSOR_GROUP_ID), getProcessGroup(activeGroupId));
+        controllerSearchService.search(searchQuery, results);
+        controllerSearchService.searchParameters(searchQuery, results);
+    }
+
+    protected void whenExecuteSearch(final String searchString) {
+        whenExecuteSearch(searchString, ROOT_PROCESSOR_GROUP_ID);
+    }
+
+    // then
+
+    protected void thenResultIsEmpty() {
+        thenResultConsists().validate(results);
+    }
+
+    protected SearchResultMatcher thenResultConsists() {
+        return new SearchResultMatcher();
+    }
+
+    // Helpers
+
+    void resetResults() {
+        results = new SearchResultsDTO();
+    }
+
+    protected ProcessGroup getProcessGroup(final String processGroupId) {
+        final ProcessGroupSetup result = getProcessGroupSetup(processGroupId);
+        return (result == null) ? null : result.getProcessGroup();
+    }
+
+    protected ProcessGroupSetup getProcessGroupSetup(final String processGroupId) {
+        for (final ProcessGroupSetup helper : processGroups) {
+            if (processGroupId.equals(helper.getProcessGroup().getIdentifier())) {
+                return helper;
+            }
+        }
+
+        return null;
+    }
+
+    protected ComponentSearchResultDTO getSimpleResultFromRoot(
+            final String id,
+            final String name,
+            final String... matches) {
+        return getSimpleResult(id, name, ROOT_PROCESSOR_GROUP_ID, ROOT_PROCESSOR_GROUP_ID, ROOT_PROCESSOR_GROUP_NAME, matches);
+    }
+
+    protected ComponentSearchResultDTO getSimpleResult(
+            final String id,
+            final String name,
+            final String groupId,
+            final String parentGroupId,
+            final String parentGroupName,
+            final String... matches) {
+        final ComponentSearchResultDTO result = new ComponentSearchResultDTO();
+        result.setId(id);
+        result.setName(name);
+        result.setGroupId(groupId);
+
+        if (parentGroupId != null || parentGroupName != null) {
+            final SearchResultGroupDTO parentGroup = new SearchResultGroupDTO();
+            parentGroup.setId(parentGroupId);
+            parentGroup.setName(parentGroupName);
+            result.setParentGroup(parentGroup);
+        }
+
+        result.setMatches(Arrays.asList(matches));
+        return result;
+    }
+
+    protected ComponentSearchResultDTO getVersionedResult(
+            final String id,
+            final String name,
+            final String groupId,
+            final String parentGroupId,
+            final String parentGroupName,
+            final String versionedGroupId,
+            final String versionedGroupName,
+            final String... matches) {
+        final SearchResultGroupDTO versionedGroup = new SearchResultGroupDTO();
+        versionedGroup.setId(versionedGroupId);
+        versionedGroup.setName(versionedGroupName);
+
+        final ComponentSearchResultDTO result = getSimpleResult(id, name, groupId, parentGroupId, parentGroupName, matches);
+
+        result.setVersionedGroup(versionedGroup);
+        return result;
+    }
+
+    protected class ProcessGroupSetup {
+        private final Set<Port> outputPorts = new HashSet<>();
+        private final Set<Port> inputPorts = new HashSet<>();
+        private final Set<ProcessorNode> processors = new HashSet<>();
+        private final Set<Label> labels = new HashSet<>();
+        private final Set<RemoteProcessGroup> remoteProcessGroups = new HashSet<>();
+        private final Set<Funnel> funnels = new HashSet<>();
+        private final Set<Connection> connections = new HashSet<>();
+        private final Set<ControllerServiceNode> controllerServiceNodes = new HashSet<>();
+        private final Set<ProcessGroup> children = new HashSet<>();
+
+        private final ProcessGroup processGroup;
+
+        private ProcessGroupSetup(final ProcessGroup processGroup) {
+            this.processGroup = processGroup;
+            processGroups.add(this);
+
+            if (processGroup.getParent() != null) {
+                getProcessGroupSetup(processGroup.getParent().getIdentifier()).withChild(processGroup);
+            }
+
+            Mockito.when(processGroup.getInputPorts()).thenReturn(inputPorts);
+            Mockito.when(processGroup.getOutputPorts()).thenReturn(outputPorts);
+            Mockito.when(processGroup.getProcessors()).thenReturn(processors);
+            Mockito.when(processGroup.getLabels()).thenReturn(labels);
+            Mockito.when(processGroup.getRemoteProcessGroups()).thenReturn(remoteProcessGroups);
+            Mockito.when(processGroup.getFunnels()).thenReturn(funnels);
+            Mockito.when(processGroup.getConnections()).thenReturn(connections);
+            Mockito.when(processGroup.getControllerServices(Mockito.anyBoolean())).thenReturn(controllerServiceNodes);
+            Mockito.when(processGroup.getProcessGroups()).thenReturn(children);
+        }
+
+        public ProcessGroup getProcessGroup() {
+            return processGroup;
+        }
+
+        public ProcessGroupSetup withOutputPort(final Port outputPort) {
+            outputPorts.add(outputPort);
+            return this;
+        }
+
+        public ProcessGroupSetup withInputPort(final Port inputPort) {
+            inputPorts.add(inputPort);
+            return this;
+        }
+
+        public ProcessGroupSetup withProcessor(final ProcessorNode processor) {
+            processors.add(processor);
+            return this;
+        }
+
+        public ProcessGroupSetup withLabel(final Label label) {
+            labels.add(label);
+            return this;
+        }
+
+        public ProcessGroupSetup withRemoteProcessGroup(final RemoteProcessGroup remoteProcessGroup) {
+            remoteProcessGroups.add(remoteProcessGroup);
+            return this;
+        }
+
+        public ProcessGroupSetup withFunnel(final Funnel funnel) {
+            funnels.add(funnel);
+            return this;
+        }
+
+        public ProcessGroupSetup withConnection(final Connection connection) {
+            connections.add(connection);
+            return this;
+        }
+
+        public ProcessGroupSetup withControllerServiceNode(final ControllerServiceNode controllerServiceNode) {
+            controllerServiceNodes.add(controllerServiceNode);
+            return this;
+        }
+
+        public ProcessGroupSetup withChild(final ProcessGroup child) {
+            children.add(child);
+            return this;
+        }
+    }
+
+    static class ComponentSearchResultDTOWrapper {
+        private final ComponentSearchResultDTO item;
+
+        private final List<Function<ComponentSearchResultDTO, Object>> propertyProvider = Arrays.asList(
+            ComponentSearchResultDTO::getId,
+            ComponentSearchResultDTO::getName,
+            ComponentSearchResultDTO::getGroupId,
+            _item -> new HashSet<>(_item.getMatches()),
+            _item -> _item.getMatches().size(),
+            _item -> Optional.ofNullable(_item.getParentGroup()).map(SearchResultGroupDTO::getId).orElse("NO_PARENT_GROUP_ID"),
+            _item -> Optional.ofNullable(_item.getParentGroup()).map(SearchResultGroupDTO::getName).orElse("NO_PARENT_GROUP_NAME"),
+            _item -> Optional.ofNullable(_item.getVersionedGroup()).map(SearchResultGroupDTO::getId).orElse("NO_VERSIONED_GROUP_ID"),
+            _item -> Optional.ofNullable(_item.getVersionedGroup()).map(SearchResultGroupDTO::getName).orElse("NO_VERSIONED_GROUP_NAME")
+        );
+
+        public ComponentSearchResultDTOWrapper(final ComponentSearchResultDTO item) {
+            this.item = item;
+        }
+
+        @Override
+        public boolean equals(final Object o) {
+            if (this == o) {
+                return true;
+            }
+
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+
+            final ComponentSearchResultDTOWrapper that = (ComponentSearchResultDTOWrapper) o;
+            final EqualsBuilder equalsBuilder = new EqualsBuilder();
+
+            propertyProvider.forEach(propertySupplier -> equalsBuilder.append(propertySupplier.apply(item), propertySupplier.apply(that.item)));
+
+            return equalsBuilder.isEquals();
+        }
+
+        @Override
+        public int hashCode() {
+            final HashCodeBuilder hashCodeBuilder = new HashCodeBuilder(17, 37);
+
+            propertyProvider.forEach(propertySupplier -> hashCodeBuilder.append(propertySupplier.apply(item)));
+
+            return hashCodeBuilder.toHashCode();
+        }
+
+        @Override
+        public String toString() {
+            final StringJoiner stringJoiner = new StringJoiner(",\n\t", "{\n\t", "\n}");
+
+            propertyProvider.forEach(propertySupplier -> stringJoiner.add(Optional.ofNullable(propertySupplier.apply(item)).orElse("N/A").toString()));
+
+            return stringJoiner.toString();
+        }
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/ComponentMockUtil.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/ComponentMockUtil.java
new file mode 100644
index 0000000..319e45c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/ComponentMockUtil.java
@@ -0,0 +1,470 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.controller;
+
+import org.apache.nifi.authorization.Authorizer;
+import org.apache.nifi.authorization.RequestAction;
+import org.apache.nifi.authorization.resource.Authorizable;
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.validation.ValidationStatus;
+import org.apache.nifi.connectable.Connectable;
+import org.apache.nifi.connectable.Connection;
+import org.apache.nifi.connectable.Funnel;
+import org.apache.nifi.connectable.Port;
+import org.apache.nifi.controller.ControllerService;
+import org.apache.nifi.controller.ProcessorNode;
+import org.apache.nifi.controller.ScheduledState;
+import org.apache.nifi.controller.label.Label;
+import org.apache.nifi.controller.queue.FlowFileQueue;
+import org.apache.nifi.controller.service.ControllerServiceNode;
+import org.apache.nifi.flowfile.FlowFilePrioritizer;
+import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.groups.RemoteProcessGroup;
+import org.apache.nifi.parameter.Parameter;
+import org.apache.nifi.parameter.ParameterContext;
+import org.apache.nifi.parameter.ParameterDescriptor;
+import org.apache.nifi.processor.Processor;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.registry.flow.VersionControlInformation;
+import org.apache.nifi.remote.PublicPort;
+import org.apache.nifi.scheduling.ExecutionNode;
+import org.apache.nifi.scheduling.SchedulingStrategy;
+import org.apache.nifi.search.SearchContext;
+import org.apache.nifi.search.SearchResult;
+import org.apache.nifi.search.Searchable;
+import org.mockito.AdditionalMatchers;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+public class ComponentMockUtil {
+
+    // Process group
+
+    public static ProcessGroup getRootProcessGroup(
+            final String id,
+            final String name,
+            final String comments,
+            final boolean isAuthorized,
+            final boolean isUnderVersionControl) {
+        return getProcessGroup(id, name, comments, Optional.empty(), Optional.empty(), isAuthorized, isUnderVersionControl);
+    }
+
+    public static ProcessGroup getRootProcessGroup(
+            final String id,
+            final String name,
+            final String comments,
+            final String versionedId,
+            final boolean isAuthorized,
+            final boolean isUnderVersionControl) {
+        return getProcessGroup(id, name, comments, Optional.of(versionedId), Optional.empty(), isAuthorized, isUnderVersionControl);
+    }
+
+    public static ProcessGroup getChildProcessGroup(
+            final String id,
+            final String name,
+            final String comments,
+            final ProcessGroup parent,
+            final boolean isAuthorized,
+            final boolean isUnderVersionControl) {
+        return getProcessGroup(id, name, comments, Optional.empty(), Optional.of(parent), isAuthorized, isUnderVersionControl);
+    }
+
+    public static ProcessGroup getChildProcessGroup(
+            final String id,
+            final String name,
+            final String comments,
+            final String versionedId,
+            final ProcessGroup parent,
+            final boolean isAuthorized,
+            final boolean isUnderVersionControl) {
+        return getProcessGroup(id, name, comments, Optional.of(versionedId), Optional.of(parent), isAuthorized, isUnderVersionControl);
+    }
+
+    private static ProcessGroup getProcessGroup(
+            final String id,
+            final String name,
+            final String comments,
+            final Optional<String> versionedId,
+            final Optional<ProcessGroup> parent,
+            final boolean isAuthorized,
+            final boolean isUnderVersionControl) {
+        final ProcessGroup result = Mockito.mock(ProcessGroup.class);
+        Mockito.when(result.getIdentifier()).thenReturn(id);
+        Mockito.when(result.getName()).thenReturn(name);
+        Mockito.when(result.getComments()).thenReturn(comments);
+        Mockito.when(result.getVersionedComponentId()).thenReturn(versionedId);
+
+        if (isUnderVersionControl) {
+            VersionControlInformation versionControlInformation = Mockito.mock(VersionControlInformation.class);
+            Mockito.when(result.getVersionControlInformation()).thenReturn(versionControlInformation);
+        }
+
+        if (parent.isPresent()) {
+            Mockito.when(result.isRootGroup()).thenReturn(false);
+            Mockito.when(result.getParent()).thenReturn(parent.get());
+        } else {
+            Mockito.when(result.isRootGroup()).thenReturn(true);
+            Mockito.when(result.getParent()).thenReturn(null);
+        }
+
+        setAuthorized(result, isAuthorized);
+        return result;
+    }
+
+    // Processor
+
+    public static <T extends Searchable & Processor> ProcessorNode getSearchableProcessorNode(
+            final String id,
+            final String name,
+            final String comments,
+            final Class<T> processorType,
+            final Collection<SearchResult> searchResults,
+            final boolean isAuthorized) {
+       final T processor = Mockito.mock(processorType);
+       Mockito.when(processor.search(Mockito.any(SearchContext.class))).thenReturn(searchResults);
+
+       return getProcessorNode(id, name, comments, Optional.empty(), SchedulingStrategy.TIMER_DRIVEN, ExecutionNode.ALL, ScheduledState.RUNNING,
+               ValidationStatus.VALID, new HashSet<>(), "Searchable", processor, new HashMap<>(), isAuthorized);
+    }
+
+    public static ProcessorNode getProcessorNode(final String id, final String name, final boolean isAuthorized) {
+        return getProcessorNode(id, name, "", Optional.empty(), SchedulingStrategy.TIMER_DRIVEN, ExecutionNode.ALL, ScheduledState.RUNNING,
+                ValidationStatus.VALID, getBasicRelationships(), "dummyProcessor", Mockito.mock(DummyProcessor.class), new HashMap<>(), isAuthorized);
+    }
+
+    public static ProcessorNode getProcessorNode(
+            final String id,
+            final String name,
+            final SchedulingStrategy schedulingStrategy,
+            final ExecutionNode executionNode,
+            final ScheduledState scheduledState,
+            final ValidationStatus validationStatus,
+            final boolean isAuthorized) {
+        return getProcessorNode(id, name, "", Optional.empty(), schedulingStrategy, executionNode, scheduledState, validationStatus,
+                new HashSet<>(), "Processor", Mockito.mock(Processor.class), new HashMap<>(), isAuthorized);
+    }
+
+    public static ProcessorNode getProcessorNode(
+            final String id,
+            final String name,
+            final String comments,
+            final Optional<String> versionedId,
+            final SchedulingStrategy schedulingStrategy,
+            final ExecutionNode executionNode,
+            final ScheduledState scheduledState,
+            final ValidationStatus validationStatus,
+            final Collection<Relationship> relationships,
+            final String componentType,
+            final Processor processor,
+            final Map<PropertyDescriptor, String> rawProperties,
+            final boolean isAuthorized) {
+
+        final ProcessorNode result = Mockito.mock(ProcessorNode.class);
+        Mockito.when(result.getIdentifier()).thenReturn(id);
+        Mockito.when(result.getName()).thenReturn(name);
+        Mockito.when(result.getComments()).thenReturn(comments);
+        Mockito.when(result.getVersionedComponentId()).thenReturn(versionedId);
+        Mockito.when(result.getSchedulingStrategy()).thenReturn(schedulingStrategy);
+        Mockito.when(result.getExecutionNode()).thenReturn(executionNode);
+        Mockito.when(result.getScheduledState()).thenReturn(scheduledState);
+        Mockito.when(result.getValidationStatus()).thenReturn(validationStatus);
+        Mockito.when(result.getRelationships()).thenReturn(relationships);
+        Mockito.when(result.getComponentType()).thenReturn(componentType);
+        Mockito.when(result.getProcessor()).thenReturn(processor);
+        Mockito.when(result.getRawPropertyValues()).thenReturn(rawProperties);
+
+        setAuthorized(result, isAuthorized);
+        return result;
+    }
+
+    public static Processor getSearchableProcessor(List<SearchResult> searchResults) {
+        Processor searchableProcessor = Mockito.mock(Processor.class, Mockito.withSettings().extraInterfaces(Searchable.class));
+
+        Mockito.when(((Searchable)searchableProcessor).search(Mockito.any(SearchContext.class))).thenReturn(searchResults);
+
+        return searchableProcessor;
+    }
+
+    // Connection
+
+    public static Connection getConnection(
+            final String id,
+            final String name,
+            final Collection<Relationship> relationships,
+            final Connectable source,
+            final Connectable destination,
+            final boolean isAuthorized) {
+        return getConnection(id, name, Optional.empty(), relationships, new ArrayList<>(), 0, "0", 0L, source, destination, isAuthorized);
+    }
+
+    public static Connection getConnection(
+            final String id,
+            final String name,
+            final Optional<String> versionedId,
+            final Collection<Relationship> relationships,
+            final List<FlowFilePrioritizer> flowFilePrioritizers,
+            final int flowFileExpirationInMs,
+            final String backPressureDataSize,
+            final long backPressureCount,
+            final Connectable source,
+            final Connectable destination,
+            final boolean isAuthorized) {
+        final Connection result = Mockito.mock(Connection.class);
+        final FlowFileQueue flowFileQueue = Mockito.mock(FlowFileQueue.class);
+
+        Mockito.when(flowFileQueue.getPriorities()).thenReturn(flowFilePrioritizers);
+        Mockito.when(flowFileQueue.getFlowFileExpiration()).thenReturn(String.valueOf(flowFileExpirationInMs));
+        Mockito.when(flowFileQueue.getFlowFileExpiration(TimeUnit.MILLISECONDS)).thenReturn(flowFileExpirationInMs);
+        Mockito.when(flowFileQueue.getBackPressureDataSizeThreshold()).thenReturn(backPressureDataSize);
+        Mockito.when(flowFileQueue.getBackPressureObjectThreshold()).thenReturn(backPressureCount);
+
+        Mockito.when(result.getIdentifier()).thenReturn(id);
+        Mockito.when(result.getName()).thenReturn(name);
+        Mockito.when(result.getVersionedComponentId()).thenReturn(versionedId);
+        Mockito.when(result.getRelationships()).thenReturn(relationships);
+        Mockito.when(result.getFlowFileQueue()).thenReturn(flowFileQueue);
+        Mockito.when(result.getSource()).thenReturn(source);
+        Mockito.when(result.getDestination()).thenReturn(destination);
+
+        setAuthorized(result, isAuthorized);
+        return result;
+    }
+
+    // Remote process group
+
+    public static RemoteProcessGroup getRemoteProcessGroup(
+            final String id,
+            final String name,
+            final Optional<String> versionedId,
+            final String comments,
+            final String targetUris,
+            final boolean isTransmitting,
+            final boolean isAuthorized) {
+        final RemoteProcessGroup result = Mockito.mock(RemoteProcessGroup.class);
+        Mockito.when(result.getIdentifier()).thenReturn(id);
+        Mockito.when(result.getName()).thenReturn(name);
+        Mockito.when(result.getComments()).thenReturn(comments);
+        Mockito.when(result.getVersionedComponentId()).thenReturn(versionedId);
+        Mockito.when(result.getTargetUris()).thenReturn(targetUris);
+        Mockito.when(result.isTransmitting()).thenReturn(isTransmitting);
+        setAuthorized(result, isAuthorized);
+
+        return result;
+    }
+
+    // Port
+
+    public static Port getPort(
+            final String id,
+            final String name,
+            final String comments,
+            final ScheduledState scheduledState,
+            final boolean isValid,
+            final boolean isAuthorized) {
+        return getPort(Port.class, id, name, Optional.empty(), comments, scheduledState, isValid, isAuthorized);
+    }
+
+    public static Port getPort(
+            final String id,
+            final String name,
+            final String versionedId,
+            final String comments,
+            final ScheduledState scheduledState,
+            final boolean isValid,
+            final boolean isAuthorized) {
+        return getPort(Port.class, id, name, Optional.of(versionedId), comments, scheduledState, isValid, isAuthorized);
+    }
+
+    private static <T extends Port> T getPort(
+            final Class<T> type,
+            final String id,
+            final String name,
+            final Optional<String> versionedId,
+            final String comments,
+            final ScheduledState scheduledState,
+            final boolean isValid,
+            final boolean isAuthorized) {
+        final T result = Mockito.mock(type);
+        Mockito.when(result.getIdentifier()).thenReturn(id);
+        Mockito.when(result.getName()).thenReturn(name);
+        Mockito.when(result.getComments()).thenReturn(comments);
+        Mockito.when(result.getVersionedComponentId()).thenReturn(versionedId);
+        Mockito.when(result.getIdentifier()).thenReturn(id);
+        Mockito.when(result.getScheduledState()).thenReturn(scheduledState);
+        Mockito.when(result.isValid()).thenReturn(isValid);
+        setAuthorized(result, isAuthorized);
+        return result;
+    }
+
+    public static Port getPublicPort(
+            final String id,
+            final String name,
+            final String comments,
+            final ScheduledState scheduledState,
+            final boolean isValid,
+            final boolean isAuthorized,
+            final List<String> userAccessControl,
+            final List<String> groupAccessControl) {
+        final PublicPort result = getPort(PublicPort.class, id, name, Optional.empty(), comments, scheduledState, isValid, isAuthorized);
+        Mockito.when(result.getUserAccessControl()).thenReturn(new HashSet<>(userAccessControl));
+        Mockito.when(result.getGroupAccessControl()).thenReturn(new HashSet<>(groupAccessControl));
+        return result;
+    }
+
+    public static Port getPublicPort(
+            final String id,
+            final String name,
+            final String comments,
+            final String versionedId,
+            final ScheduledState scheduledState,
+            final boolean isValid,
+            final boolean isAuthorized,
+            final List<String> userAccessControl,
+            final List<String> groupAccessControl) {
+        final PublicPort result = getPort(PublicPort.class, id, name, Optional.of(versionedId), comments, scheduledState, isValid, isAuthorized);
+        Mockito.when(result.getUserAccessControl()).thenReturn(new HashSet<>(userAccessControl));
+        Mockito.when(result.getGroupAccessControl()).thenReturn(new HashSet<>(groupAccessControl));
+        return result;
+    }
+
+    // Funnel
+
+    public static Funnel getFunnel(
+            final String id,
+            final Optional<String> versionedId,
+            final boolean isAuthorized) {
+     return getFunnel(id, versionedId.orElse(null), null, isAuthorized);
+    }
+
+    public static Funnel getFunnel(
+        final String id,
+        final String versionedId,
+        String comments,
+        final boolean isAuthorized
+    ) {
+        final Funnel result = Mockito.mock(Funnel.class);
+
+        Mockito.when(result.getIdentifier()).thenReturn(id);
+        Mockito.when(result.getVersionedComponentId()).thenReturn(Optional.ofNullable(versionedId));
+        Mockito.when(result.getComments()).thenReturn(comments);
+
+        setAuthorized(result, isAuthorized);
+        return result;
+    }
+
+    // Label
+
+    public static Label getLabel(
+            final String id,
+            final String value,
+            final boolean isAuthorized) {
+        final Label result = Mockito.mock(Label.class);
+
+        Mockito.when(result.getIdentifier()).thenReturn(id);
+        Mockito.when(result.getValue()).thenReturn(value);
+
+        setAuthorized(result, isAuthorized);
+        return result;
+    }
+
+    // Parameter context
+
+    public static ParameterContext getParameterContext(
+            final String id,
+            final String name,
+            final String description,
+            final Map<ParameterDescriptor, Parameter> parameters,
+            final boolean isAuthorized) {
+        final ParameterContext result = Mockito.mock(ParameterContext.class);
+
+        Mockito.when(result.getIdentifier()).thenReturn(id);
+        Mockito.when(result.getName()).thenReturn(name);
+        Mockito.when(result.getDescription()).thenReturn(description);
+        Mockito.when(result.getParameters()).thenReturn(parameters);
+
+        setAuthorized(result, isAuthorized);
+        return result;
+    }
+
+    public static Parameter getParameter(final String name, final String value, final boolean isSensitive, final String description) {
+        final ParameterDescriptor descriptor = new ParameterDescriptor.Builder().name(name).description(description).sensitive(isSensitive).build();
+        return new Parameter(descriptor, value);
+
+    }
+
+    // ControllerServiceNode
+
+    public static ControllerServiceNode getControllerServiceNode(
+            final String id,
+            final String name,
+            final String comments,
+            final Map<PropertyDescriptor, String> rawProperties,
+            final boolean isAuthorized) {
+        return getControllerServiceNode(id, name, comments, rawProperties, null, isAuthorized);
+    }
+
+    public static ControllerServiceNode getControllerServiceNode(
+            final String id,
+            final String name,
+            final String comments,
+            final Map<PropertyDescriptor, String> rawProperties,
+            final String versionedComponentId,
+            final boolean isAuthorized) {
+        final ControllerService controllerService = Mockito.mock(ControllerService.class);
+        final ControllerServiceNode result = Mockito.mock(ControllerServiceNode.class);
+        Mockito.doReturn(controllerService).when(result).getControllerServiceImplementation();
+
+        // set controller service node attributes
+        Mockito.when(result.getIdentifier()).thenReturn(id);
+        Mockito.when(result.getName()).thenReturn(name);
+        Mockito.when(result.getVersionedComponentId()).thenReturn(Optional.ofNullable(versionedComponentId));
+        Mockito.when(result.getComments()).thenReturn(comments);
+        Mockito.when(result.getRawPropertyValues()).thenReturn(rawProperties);
+
+        setAuthorized(result, isAuthorized);
+        return result;
+    }
+
+    // Helper
+
+    private static void setAuthorized(final Authorizable authorizable, final boolean isAuthorized) {
+        Mockito.when(authorizable.isAuthorized(
+                Mockito.any(Authorizer.class),
+                Mockito.any(RequestAction.class),
+                AdditionalMatchers.or(Mockito.any(NiFiUser.class), Mockito.isNull()))
+        ).thenReturn(isAuthorized);
+    }
+
+    public static Collection<Relationship> getBasicRelationships() {
+        final Collection<Relationship> result = new HashSet<>();
+        result.add(new Relationship.Builder().name("success").description("Success").build());
+        result.add(new Relationship.Builder().name("failure").description("Failure").build());
+        return result;
+    }
+
+     public interface DummyProcessor extends Processor {}
+     public interface DummyFlowFilePrioritizer extends FlowFilePrioritizer {}
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/ControllerFacadeTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/ControllerFacadeTest.java
new file mode 100644
index 0000000..3946fc6
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/ControllerFacadeTest.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.controller;
+
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.controller.FlowController;
+import org.apache.nifi.controller.flow.FlowManager;
+import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.web.api.dto.search.SearchResultsDTO;
+import org.apache.nifi.web.search.query.SearchQuery;
+import org.apache.nifi.web.search.query.SearchQueryParser;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.AdditionalMatchers;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class ControllerFacadeTest {
+    private static final String ACTIVE_GROUP_ID = "activeId";
+    private static final String SEARCH_LITERAL = "processor1";
+
+    @Mock
+    private FlowController flowController;
+
+    @Mock
+    private FlowManager flowManager;
+
+    @Mock
+    private ProcessGroup rootGroup;
+
+    @Mock
+    private ProcessGroup activeGroup;
+
+    @Mock
+    private SearchQueryParser searchQueryParser;
+
+    @Mock
+    private SearchQuery searchQuery;
+
+    @Mock
+    private ControllerSearchService controllerSearchService;
+
+    @Before
+    public void setUp() {
+        Mockito.when(flowController.getFlowManager()).thenReturn(flowManager);
+        Mockito.when(flowManager.getRootGroup()).thenReturn(rootGroup);
+        Mockito.when(flowManager.getGroup(ACTIVE_GROUP_ID)).thenReturn(activeGroup);
+        // The NiFi user is null due to the production code acquires it from a static call
+        Mockito.when(searchQueryParser.parse(
+                Mockito.anyString(),
+                AdditionalMatchers.or(Mockito.any(NiFiUser.class), Mockito.isNull()),
+                Mockito.any(ProcessGroup.class),
+                Mockito.any(ProcessGroup.class))
+        ).thenReturn(searchQuery);
+        Mockito.when(searchQuery.getTerm()).thenReturn(SEARCH_LITERAL);
+    }
+
+    @Test
+    public void testExistingActiveGroupIsSentDownToSearch() {
+        // given
+        final ControllerFacade testSubject = givenTestSubject();
+
+        // when
+        testSubject.search(SEARCH_LITERAL, ACTIVE_GROUP_ID);
+
+        // then
+        Mockito.verify(searchQueryParser, Mockito.times(1))
+                .parse(Mockito.eq(SEARCH_LITERAL), AdditionalMatchers.or(Mockito.any(NiFiUser.class), Mockito.isNull()), Mockito.same(rootGroup), Mockito.same(activeGroup));
+
+        Mockito.verify(controllerSearchService, Mockito.times(1)).search(Mockito.same(searchQuery), Mockito.any(SearchResultsDTO.class));
+        Mockito.verify(controllerSearchService, Mockito.times(1)).searchParameters(Mockito.same(searchQuery), Mockito.any(SearchResultsDTO.class));
+    }
+
+    @Test
+    public void testSearchUsesRootGroupAsActiveIfNotProvided() {
+        // given
+        final ControllerFacade testSubject = givenTestSubject();
+
+        // when
+        testSubject.search(SEARCH_LITERAL, null);
+
+        // then
+        Mockito.verify(searchQueryParser, Mockito.times(1))
+                .parse(Mockito.eq(SEARCH_LITERAL), AdditionalMatchers.or(Mockito.any(NiFiUser.class), Mockito.isNull()), Mockito.same(rootGroup), Mockito.same(rootGroup));
+    }
+
+    private ControllerFacade givenTestSubject() {
+        final ControllerFacade testSubject = new ControllerFacade();
+        testSubject.setFlowController(flowController);
+        testSubject.setSearchQueryParser(searchQueryParser);
+        testSubject.setControllerSearchService(controllerSearchService);
+        return testSubject;
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/ControllerSearchServiceFilterTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/ControllerSearchServiceFilterTest.java
new file mode 100644
index 0000000..e2ff4b3
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/ControllerSearchServiceFilterTest.java
@@ -0,0 +1,239 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */package org.apache.nifi.web.controller;
+
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.validation.ValidationStatus;
+import org.apache.nifi.controller.ScheduledState;
+import org.apache.nifi.processor.Processor;
+import org.apache.nifi.scheduling.ExecutionNode;
+import org.apache.nifi.scheduling.SchedulingStrategy;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Optional;
+
+import static org.apache.nifi.web.controller.ComponentMockUtil.getChildProcessGroup;
+import static org.apache.nifi.web.controller.ComponentMockUtil.getProcessorNode;
+
+public class ControllerSearchServiceFilterTest extends AbstractControllerSearchIntegrationTest {
+
+    @Test
+    public void testScopeWhenChild() {
+        // given
+        givenRootProcessGroup()
+                .withProcessor(getProcessorNode("workingProcessor1", "processor1Name", AUTHORIZED));
+
+        givenProcessGroup(getChildProcessGroup("child", "childName", "", getProcessGroup(ROOT_PROCESSOR_GROUP_ID), AUTHORIZED, NOT_UNDER_VERSION_CONTROL))
+                .withProcessor(getProcessorNode("workingProcessor2", "processor2Name", AUTHORIZED));
+
+        // when
+        whenExecuteSearch("scope:here workingProcessor", "child");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResult("workingProcessor2", "processor2Name", "child", "child", "childName", "Id: workingProcessor2"))
+                .validate(results);
+    }
+
+    @Test
+    public void testScopeWhenRoot() {
+        // given
+        givenRootProcessGroup()
+                .withProcessor(getProcessorNode("workingProcessor1", "processor1Name", AUTHORIZED));
+
+        givenProcessGroup(getChildProcessGroup("child", "childName", "", getProcessGroup(ROOT_PROCESSOR_GROUP_ID), AUTHORIZED, NOT_UNDER_VERSION_CONTROL))
+                .withProcessor(getProcessorNode("workingProcessor2", "processor2Name", AUTHORIZED));
+
+        // when
+        whenExecuteSearch("scope:here workingProcessor", ROOT_PROCESSOR_GROUP_ID);
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResultFromRoot("workingProcessor1", "processor1Name","Id: workingProcessor1"))
+                .ofProcessor(getSimpleResult("workingProcessor2", "processor2Name", "child", "child", "childName", "Id: workingProcessor2"))
+                .validate(results);
+    }
+
+    @Test
+    public void testScopeWhenInvalidFilter() {
+        // given
+        givenRootProcessGroup()
+                .withProcessor(getProcessorNode("workingProcessor1", "processor1Name", AUTHORIZED));
+
+        givenProcessGroup(getChildProcessGroup("child", "childName", "", getProcessGroup(ROOT_PROCESSOR_GROUP_ID), AUTHORIZED, NOT_UNDER_VERSION_CONTROL))
+                .withProcessor(getProcessorNode("workingProcessor2", "processor2Name", AUTHORIZED));
+
+        // when
+        whenExecuteSearch("scope:there workingProcessor", "child");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResultFromRoot("workingProcessor1", "processor1Name","Id: workingProcessor1"))
+                .ofProcessor(getSimpleResult("workingProcessor2", "processor2Name", "child", "child", "childName", "Id: workingProcessor2"))
+                .validate(results);
+    }
+
+    @Test
+    public void testGroupWhenHasChildGroup() {
+        // given
+        givenRootProcessGroup()
+                .withProcessor(getProcessorNode("workingProcessor1", "processor1Name", AUTHORIZED));
+
+        givenProcessGroup(getChildProcessGroup("child1", "child1Name", "", getProcessGroup(ROOT_PROCESSOR_GROUP_ID), AUTHORIZED, NOT_UNDER_VERSION_CONTROL))
+                .withProcessor(getProcessorNode("workingProcessor2", "processor2Name", AUTHORIZED));
+
+        givenProcessGroup(getChildProcessGroup("child2", "child2Name", "", getProcessGroup("child1"), AUTHORIZED, NOT_UNDER_VERSION_CONTROL))
+                .withProcessor(getProcessorNode("workingProcessor3", "processor3Name", AUTHORIZED));
+
+
+        // when
+        whenExecuteSearch("group:child1 workingProcessor");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResult("workingProcessor2", "processor2Name", "child1", "child1", "child1Name", "Id: workingProcessor2"))
+                .ofProcessor(getSimpleResult("workingProcessor3", "processor3Name", "child2", "child2", "child2Name", "Id: workingProcessor3"))
+                .validate(results);
+    }
+
+    @Test
+    public void testGroupWhenUnknown() {
+        // given
+        givenRootProcessGroup()
+                .withProcessor(getProcessorNode("workingProcessor1", "processor1Name", AUTHORIZED));
+
+        givenProcessGroup(getChildProcessGroup("child1", "child1Name", "", getProcessGroup(ROOT_PROCESSOR_GROUP_ID), AUTHORIZED, NOT_UNDER_VERSION_CONTROL))
+                .withProcessor(getProcessorNode("workingProcessor2", "processor2Name", AUTHORIZED));
+
+        givenProcessGroup(getChildProcessGroup("child2", "child2Name", "", getProcessGroup("child1"), AUTHORIZED, NOT_UNDER_VERSION_CONTROL))
+                .withProcessor(getProcessorNode("workingProcessor3", "processor3Name", AUTHORIZED));
+
+
+        // when
+        whenExecuteSearch("group:unknown workingProcessor");
+
+        // then
+        thenResultIsEmpty();
+    }
+
+    @Test
+    public void testGroupWhenNotAuthorized() {
+        // given
+        givenRootProcessGroup()
+                .withProcessor(getProcessorNode("workingProcessor1", "processor1Name", AUTHORIZED));
+
+        givenProcessGroup(getChildProcessGroup("child1", "child1Name", "", getProcessGroup(ROOT_PROCESSOR_GROUP_ID), AUTHORIZED, NOT_UNDER_VERSION_CONTROL))
+                .withProcessor(getProcessorNode("workingProcessor2", "processor2Name", NOT_AUTHORIZED));
+
+        givenProcessGroup(getChildProcessGroup("child2", "child2Name", "", getProcessGroup("child1"), AUTHORIZED, NOT_UNDER_VERSION_CONTROL))
+                .withProcessor(getProcessorNode("workingProcessor3", "processor3Name", AUTHORIZED));
+
+
+        // when
+        whenExecuteSearch("group:child1 workingProcessor");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResult("workingProcessor3", "processor3Name", "child2", "child2", "child2Name", "Id: workingProcessor3"))
+                .validate(results);
+    }
+
+    @Test
+    public void testGroupWithScopeWhenOverlapping() {
+        // given
+        givenRootProcessGroup()
+                .withProcessor(getProcessorNode("workingProcessor1", "processor1Name", AUTHORIZED));
+
+        givenProcessGroup(getChildProcessGroup("child1", "child1Name", "", getProcessGroup(ROOT_PROCESSOR_GROUP_ID), AUTHORIZED, NOT_UNDER_VERSION_CONTROL))
+                .withProcessor(getProcessorNode("workingProcessor2", "processor2Name", NOT_AUTHORIZED));
+
+        givenProcessGroup(getChildProcessGroup("child2", "child2Name", "", getProcessGroup("child1"), AUTHORIZED, NOT_UNDER_VERSION_CONTROL))
+                .withProcessor(getProcessorNode("workingProcessor3", "processor3Name", AUTHORIZED));
+
+
+        // when
+        whenExecuteSearch("scope:here group:child2 workingProcessor", "child1");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResult("workingProcessor3", "processor3Name", "child2", "child2", "child2Name", "Id: workingProcessor3"))
+                .validate(results);
+
+
+        // when
+        whenExecuteSearch("scope:here group:child1 workingProcessor", "child2");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResult("workingProcessor3", "processor3Name", "child2", "child2", "child2Name", "Id: workingProcessor3"))
+                .validate(results);
+    }
+
+    @Test
+    public void testGroupWithScopeWhenNotOverlapping() {
+        // given
+        givenRootProcessGroup()
+                .withProcessor(getProcessorNode("workingProcessor1", "processor1Name", AUTHORIZED));
+
+        givenProcessGroup(getChildProcessGroup("child1", "child1Name", "", getProcessGroup(ROOT_PROCESSOR_GROUP_ID), AUTHORIZED, NOT_UNDER_VERSION_CONTROL))
+                .withProcessor(getProcessorNode("workingProcessor2", "processor2Name", NOT_AUTHORIZED));
+
+        givenProcessGroup(getChildProcessGroup("child2", "child2Name", "", getProcessGroup(ROOT_PROCESSOR_GROUP_ID), AUTHORIZED, NOT_UNDER_VERSION_CONTROL))
+                .withProcessor(getProcessorNode("workingProcessor3", "processor3Name", AUTHORIZED));
+
+        // when
+        whenExecuteSearch("scope:here group:child2 workingProcessor", "child1");
+
+        // then
+        thenResultIsEmpty();
+    }
+
+    @Test
+    public void testPropertiesAreExcluded() {
+        // given
+        final Map<PropertyDescriptor, String> rawProperties = new HashMap<>();
+        final PropertyDescriptor descriptor = new PropertyDescriptor.Builder().name("property1").displayName("property1display").description("property1 description").sensitive(false).build();
+        rawProperties.put(descriptor, "working");
+
+        givenRootProcessGroup()
+                .withProcessor(getProcessorNode("workingProcessor1", "processor1Name", "", Optional.empty(), SchedulingStrategy.TIMER_DRIVEN, ExecutionNode.ALL, ScheduledState.RUNNING,
+                        ValidationStatus.VALID, new HashSet<>(), "Processor", Mockito.mock(Processor.class), new HashMap<>(), AUTHORIZED))
+                .withProcessor(getProcessorNode("processor2", "processor2Name", "", Optional.empty(), SchedulingStrategy.TIMER_DRIVEN, ExecutionNode.ALL, ScheduledState.RUNNING,
+                        ValidationStatus.VALID, new HashSet<>(), "Processor", Mockito.mock(Processor.class), rawProperties, AUTHORIZED));
+
+        // when
+        whenExecuteSearch("properties:exclude working");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResultFromRoot("workingProcessor1", "processor1Name", "Id: workingProcessor1"))
+                .validate(results);
+
+
+        // when
+        whenExecuteSearch("properties:invalid working");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResultFromRoot("workingProcessor1", "processor1Name", "Id: workingProcessor1"))
+                .ofProcessor(getSimpleResultFromRoot("processor2", "processor2Name", "Property value: property1 - working"))
+                .validate(results);
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/ControllerSearchServiceIntegrationTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/ControllerSearchServiceIntegrationTest.java
new file mode 100644
index 0000000..cb07e0d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/ControllerSearchServiceIntegrationTest.java
@@ -0,0 +1,603 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.controller;
+
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.validation.ValidationStatus;
+import org.apache.nifi.connectable.Connection;
+import org.apache.nifi.controller.ProcessorNode;
+import org.apache.nifi.controller.ScheduledState;
+import org.apache.nifi.controller.queue.FlowFileQueue;
+import org.apache.nifi.flowfile.FlowFilePrioritizer;
+import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.processor.Processor;
+import org.apache.nifi.registry.ComponentVariableRegistry;
+import org.apache.nifi.registry.VariableDescriptor;
+import org.apache.nifi.scheduling.ExecutionNode;
+import org.apache.nifi.scheduling.SchedulingStrategy;
+import org.apache.nifi.web.api.dto.search.ComponentSearchResultDTO;
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.nifi.web.controller.ComponentMockUtil.getBasicRelationships;
+import static org.apache.nifi.web.controller.ComponentMockUtil.getChildProcessGroup;
+import static org.apache.nifi.web.controller.ComponentMockUtil.getConnection;
+import static org.apache.nifi.web.controller.ComponentMockUtil.getFunnel;
+import static org.apache.nifi.web.controller.ComponentMockUtil.getPort;
+import static org.apache.nifi.web.controller.ComponentMockUtil.getProcessorNode;
+import static org.apache.nifi.web.controller.ComponentMockUtil.getPublicPort;
+import static org.apache.nifi.web.controller.ComponentMockUtil.getRemoteProcessGroup;
+
+public class ControllerSearchServiceIntegrationTest extends AbstractControllerSearchIntegrationTest {
+
+    @Test
+    public void testSearchBasedOnBasicAttributes() {
+        // given
+        givenRootProcessGroup()
+                .withProcessor(getProcessorNode("processor1", "name1", AUTHORIZED))
+                .withProcessor(getProcessorNode("processor2", "NAME2", AUTHORIZED))
+                .withProcessor(getProcessorNode("processor3", "name3", NOT_AUTHORIZED))
+                .withProcessor(getProcessorNode("processor4", "other", AUTHORIZED))
+                .withProcessor(getProcessorNode("processor5", "something", "The name of the processor is something",
+                        Optional.of("versionId"), SchedulingStrategy.TIMER_DRIVEN, ExecutionNode.ALL, ScheduledState.RUNNING, ValidationStatus.VALID,
+                        new HashSet<>(),"Processor", Mockito.mock(Processor.class), new HashMap<>(), AUTHORIZED));
+
+        givenProcessGroup(getChildProcessGroup("childId", "child", "", getProcessGroup(ROOT_PROCESSOR_GROUP_ID), AUTHORIZED, NOT_UNDER_VERSION_CONTROL))
+                .withInputPort(getPort("port1", "name4", "comment consisting name", ScheduledState.RUNNING, true, AUTHORIZED))
+                .withOutputPort(getPort("port2", "TheName5", "comment", ScheduledState.RUNNING, true, AUTHORIZED))
+                .withFunnel(getFunnel("hasNoName1", Optional.empty(), AUTHORIZED))
+                .withFunnel(getFunnel("hasNoName2", Optional.empty(), NOT_AUTHORIZED));
+
+        // when
+        whenExecuteSearch("name");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResultFromRoot("processor1", "name1", "Name: name1"))
+                .ofProcessor(getSimpleResultFromRoot("processor2", "NAME2", "Name: NAME2"))
+                .ofProcessor(getSimpleResultFromRoot("processor5", "something", "Comments: The name of the processor is something"))
+                .ofInputPort(getSimpleResult("port1", "name4", "childId", "childId", "child", "Name: name4", "Comments: comment consisting name"))
+                .ofOutputPort(getSimpleResult("port2", "TheName5", "childId", "childId", "child", "Name: TheName5"))
+                .ofFunnel(getSimpleResult("hasNoName1", null, "childId", "childId", "child", "Id: hasNoName1"))
+                .validate(results);
+    }
+
+    @Test
+    public void testSearchBasedOnScheduling() {
+        // given
+        givenRootProcessGroup()
+                .withProcessor(getProcessorNode("processor1", "processor1name", SchedulingStrategy.EVENT_DRIVEN, ExecutionNode.ALL, ScheduledState.RUNNING, ValidationStatus.VALID, AUTHORIZED))
+                .withProcessor(getProcessorNode("processor2", "processor2name", SchedulingStrategy.EVENT_DRIVEN, ExecutionNode.ALL, ScheduledState.DISABLED, ValidationStatus.INVALID, AUTHORIZED))
+                .withProcessor(getProcessorNode("processor3", "processor3name", SchedulingStrategy.EVENT_DRIVEN, ExecutionNode.ALL, ScheduledState.RUNNING, ValidationStatus.VALID, NOT_AUTHORIZED))
+                .withProcessor(getProcessorNode("processor4", "processor4name", SchedulingStrategy.TIMER_DRIVEN, ExecutionNode.ALL, ScheduledState.STOPPED, ValidationStatus.VALID, AUTHORIZED))
+                .withProcessor(getProcessorNode("processor5", "eventHandlerProcessor", SchedulingStrategy.CRON_DRIVEN, ExecutionNode.PRIMARY, ScheduledState.RUNNING, ValidationStatus.VALID,
+                        AUTHORIZED));
+
+        // when
+        whenExecuteSearch("event");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResultFromRoot("processor1", "processor1name", "Scheduling strategy: Event driven"))
+                .ofProcessor(getSimpleResultFromRoot("processor2", "processor2name", "Scheduling strategy: Event driven"))
+                .ofProcessor(getSimpleResultFromRoot("processor5", "eventHandlerProcessor", "Name: eventHandlerProcessor"))
+                .validate(results);
+
+
+        // when
+        whenExecuteSearch("timer");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResultFromRoot("processor4", "processor4name", "Scheduling strategy: Timer driven"))
+                .validate(results);
+
+
+        // when
+        whenExecuteSearch("primary");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResultFromRoot("processor5", "eventHandlerProcessor", "Execution node: primary"))
+                .validate(results);
+    }
+
+    @Test
+    public void testSearchBasedOnExecution() {
+        // given
+        givenRootProcessGroup()
+                .withProcessor(getProcessorNode("processor1", "processor1name", SchedulingStrategy.PRIMARY_NODE_ONLY, ExecutionNode.PRIMARY, ScheduledState.RUNNING, ValidationStatus.VALID,
+                        AUTHORIZED));
+
+        // when
+        whenExecuteSearch("primary");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResultFromRoot("processor1", "processor1name",  "Execution node: primary", "Scheduling strategy: On primary node"))
+                .validate(results);
+    }
+
+    @Test
+    public void testSearchBasedOnScheduledState() {
+        givenRootProcessGroup()
+                .withProcessor(getProcessorNode("processor1", "name1", SchedulingStrategy.TIMER_DRIVEN, ExecutionNode.ALL, ScheduledState.RUNNING, ValidationStatus.VALID, AUTHORIZED))
+                .withProcessor(getProcessorNode("processor2", "name2", SchedulingStrategy.TIMER_DRIVEN, ExecutionNode.ALL, ScheduledState.RUNNING, ValidationStatus.VALID, NOT_AUTHORIZED))
+                .withProcessor(getProcessorNode("processor3", "name3", SchedulingStrategy.TIMER_DRIVEN, ExecutionNode.ALL, ScheduledState.RUNNING, ValidationStatus.INVALID, AUTHORIZED))
+                .withProcessor(getProcessorNode("processor4", "name4", SchedulingStrategy.TIMER_DRIVEN, ExecutionNode.ALL, ScheduledState.STOPPING, ValidationStatus.VALID, AUTHORIZED))
+                .withProcessor(getProcessorNode("processor5", "name5notDisabled", SchedulingStrategy.TIMER_DRIVEN, ExecutionNode.ALL, ScheduledState.STOPPED, ValidationStatus.VALID, AUTHORIZED))
+                .withProcessor(getProcessorNode("processor6", "name6", SchedulingStrategy.TIMER_DRIVEN, ExecutionNode.ALL, ScheduledState.STARTING, ValidationStatus.VALIDATING, AUTHORIZED))
+                .withProcessor(getProcessorNode("processor7", "name7", SchedulingStrategy.TIMER_DRIVEN, ExecutionNode.ALL, ScheduledState.DISABLED, ValidationStatus.VALID, AUTHORIZED))
+                .withProcessor(getProcessorNode("processor8", "name8disabled", SchedulingStrategy.TIMER_DRIVEN, ExecutionNode.ALL, ScheduledState.DISABLED, ValidationStatus.INVALID, AUTHORIZED))
+                .withInputPort(getPort("port1", "portName1", "", ScheduledState.RUNNING, true, AUTHORIZED))
+                .withInputPort(getPort("port2", "portName2", "", ScheduledState.RUNNING, true, NOT_AUTHORIZED))
+                .withInputPort(getPort("port3", "portName3", "", ScheduledState.DISABLED, true, AUTHORIZED))
+                .withInputPort(getPort("port4", "portName4", "", ScheduledState.STOPPING, true, AUTHORIZED))
+                .withInputPort(getPort("port5", "portName5", "", ScheduledState.STOPPED, true, AUTHORIZED))
+                .withOutputPort(getPort("port6", "portName6", "", ScheduledState.RUNNING, true, AUTHORIZED))
+                .withOutputPort(getPort("port7", "portName7", "", ScheduledState.STARTING, false, AUTHORIZED));
+
+        // when
+        whenExecuteSearch("disabled");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResultFromRoot("processor5", "name5notDisabled", "Name: name5notDisabled"))
+                .ofProcessor(getSimpleResultFromRoot("processor7", "name7", "Run status: Disabled"))
+                .ofProcessor(getSimpleResultFromRoot("processor8", "name8disabled", "Run status: Disabled", "Name: name8disabled"))
+                .ofInputPort(getSimpleResultFromRoot("port3", "portName3", "Run status: Disabled"))
+                .validate(results);
+
+
+        // when
+        whenExecuteSearch("invalid");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResultFromRoot("processor3", "name3", "Run status: Invalid"))
+                .ofOutputPort(getSimpleResultFromRoot("port7", "portName7", "Run status: Invalid"))
+                .validate(results);
+
+
+        // when
+        whenExecuteSearch("validating");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResultFromRoot("processor6", "name6", "Run status: Validating"))
+                .validate(results);
+
+
+        // when
+        whenExecuteSearch("running");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResultFromRoot("processor1", "name1", "Run status: Running"))
+                .ofProcessor(getSimpleResultFromRoot("processor3", "name3", "Run status: Running"))
+                .ofInputPort(getSimpleResultFromRoot("port1", "portName1", "Run status: Running"))
+                .ofOutputPort(getSimpleResultFromRoot("port6", "portName6", "Run status: Running"))
+                .validate(results);
+
+
+        // when
+        whenExecuteSearch("stopped");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResultFromRoot("processor5", "name5notDisabled", "Run status: Stopped"))
+                .ofInputPort(getSimpleResultFromRoot("port5", "portName5", "Run status: Stopped"))
+                .validate(results);
+
+
+        // when
+        whenExecuteSearch("stopping");
+
+        // then
+        thenResultIsEmpty();
+
+
+        // when
+        whenExecuteSearch("starting");
+
+        // then
+        thenResultIsEmpty();
+    }
+
+    @Test
+    public void testSearchBasedOnRelationship() {
+        // given
+        final ProcessorNode processorNode1 = getProcessorNode("processor1", "name1", "", Optional.empty(), SchedulingStrategy.TIMER_DRIVEN,
+                ExecutionNode.ALL, ScheduledState.RUNNING, ValidationStatus.VALID, getBasicRelationships(), "Processor", Mockito.mock(Processor.class),
+                new HashMap<>(), AUTHORIZED);
+        final ProcessorNode processorNode2 = getProcessorNode("processor2", "name2", "", Optional.empty(), SchedulingStrategy.TIMER_DRIVEN,
+                ExecutionNode.ALL, ScheduledState.RUNNING, ValidationStatus.VALID, getBasicRelationships(), "Processor", Mockito.mock(Processor.class),
+                new HashMap<>(), AUTHORIZED);
+
+        givenRootProcessGroup()
+                .withProcessor(processorNode1)
+                .withProcessor(processorNode2)
+                .withConnection(getConnection("connection1", "connection1name", getBasicRelationships(), processorNode1, processorNode2, AUTHORIZED));
+
+        // when
+        whenExecuteSearch("success");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResultFromRoot("processor1", "name1", "Relationship: success"))
+                .ofProcessor(getSimpleResultFromRoot("processor2", "name2", "Relationship: success"))
+                .ofConnection(getSimpleResultFromRoot("connection1", "connection1name", "Relationship: success"))
+                .validate(results);
+    }
+
+    @Test
+    public void testSearchBasedOnConnectionMetadata() {
+        // given
+        final Processor processor = Mockito.mock(ComponentMockUtil.DummyProcessor.class);
+
+        givenRootProcessGroup()
+                .withProcessor(getProcessorNode("processor1", "name1", "", Optional.empty(), SchedulingStrategy.TIMER_DRIVEN, ExecutionNode.ALL,
+                        ScheduledState.RUNNING, ValidationStatus.VALID, new HashSet<>(), "DummyProcessorForTest", processor,
+                        new HashMap<>(), AUTHORIZED));
+
+        // when
+        whenExecuteSearch("dummy");
+
+        // then
+        Assert.assertEquals(1, results.getProcessorResults().size());
+
+        final ComponentSearchResultDTO componentSearchResultDTO = results.getProcessorResults().get(0);
+        Assert.assertEquals("processor1", componentSearchResultDTO.getId());
+        Assert.assertEquals("name1", componentSearchResultDTO.getName());
+        Assert.assertEquals(2, componentSearchResultDTO.getMatches().size());
+
+        final String firstMatch = componentSearchResultDTO.getMatches().get(0);
+        final String secondMatch = componentSearchResultDTO.getMatches().get(1);
+
+        if ((!firstMatch.equals("Type: DummyProcessorForTest") || !secondMatch.startsWith("Type: ComponentMockUtil$DummyProcessor$MockitoMock$"))
+                && (!secondMatch.equals("Type: DummyProcessorForTest") || !firstMatch.startsWith("Type: ComponentMockUtil$DummyProcessor$MockitoMock$"))) {
+            Assert.fail();
+        }
+    }
+
+    @Test
+    public void testSearchBasedOnProperty() {
+        // given
+        final Map<PropertyDescriptor, String> rawProperties = new HashMap<>();
+        final PropertyDescriptor descriptor1 = new PropertyDescriptor.Builder().name("property1").displayName("property1display").description("property1 description").sensitive(false).build();
+        final PropertyDescriptor descriptor2 = new PropertyDescriptor.Builder().name("property2").displayName("property2display").description("property2 description").sensitive(true).build();
+        rawProperties.put(descriptor1, "property1value");
+        rawProperties.put(descriptor2, "property2value");
+
+        final ProcessorNode processorNode = getProcessorNode("processor1", "name1", "", Optional.empty(), SchedulingStrategy.TIMER_DRIVEN,
+                ExecutionNode.ALL, ScheduledState.RUNNING, ValidationStatus.VALID, new HashSet<>(), "Processor", Mockito.mock(Processor.class),
+                rawProperties, AUTHORIZED);
+
+        givenRootProcessGroup()
+                .withProcessor(processorNode);
+
+        // when
+        whenExecuteSearch("property");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResultFromRoot("processor1", "name1", "Property name: property1", "Property value: property1 - property1value",
+                        "Property description: property1 description", "Property name: property2", "Property description: property2 description"))
+                .validate(results);
+    }
+
+    @Test
+    public void testSearchBasedOnProcessGroupAttribute() {
+        // given
+        givenRootProcessGroup();
+        givenProcessGroup(getChildProcessGroup("groupA", "groupAName", "groupA comment", getProcessGroup(ROOT_PROCESSOR_GROUP_ID), AUTHORIZED, NOT_UNDER_VERSION_CONTROL));
+        givenProcessGroup(getChildProcessGroup("groupB", "groupBName", "groupB comment but contains groupA", getProcessGroup(ROOT_PROCESSOR_GROUP_ID), AUTHORIZED, NOT_UNDER_VERSION_CONTROL));
+        givenProcessGroup(getChildProcessGroup("groupC", "groupCName", "groupC comment", getProcessGroup(ROOT_PROCESSOR_GROUP_ID), NOT_AUTHORIZED, NOT_UNDER_VERSION_CONTROL));
+        givenProcessGroup(getChildProcessGroup("groupD", "groupDName", "groupD comment", getProcessGroup("groupA"), AUTHORIZED, NOT_UNDER_VERSION_CONTROL));
+
+        // when
+        whenExecuteSearch("groupA");
+
+        // then
+        thenResultConsists()
+                .ofProcessGroup(getSimpleResultFromRoot("groupA","groupAName", "Id: groupA", "Name: groupAName", "Comments: groupA comment"))
+                .ofProcessGroup(getSimpleResultFromRoot("groupB", "groupBName", "Comments: groupB comment but contains groupA"))
+                .validate(results);
+
+
+        // when
+        whenExecuteSearch("name");
+
+        // then
+        thenResultConsists()
+                .ofProcessGroup(getSimpleResultFromRoot("groupA","groupAName", "Name: groupAName"))
+                .ofProcessGroup(getSimpleResultFromRoot("groupB","groupBName", "Name: groupBName"))
+                .ofProcessGroup(getSimpleResult("groupD", "groupDName", "groupA", "groupA", "groupAName", "Name: groupDName"))
+                .validate(results);
+    }
+
+    @Test
+    public void testSearchBasedOnVariableRegistry() {
+        // given
+        givenRootProcessGroup();
+
+        final Map<VariableDescriptor, String> variables = new HashMap<>();
+        variables.put(new VariableDescriptor.Builder("variableName").build(), "variableValue");
+
+        final ComponentVariableRegistry variableRegistry = Mockito.mock(ComponentVariableRegistry.class);
+        Mockito.when(variableRegistry.getVariableMap()).thenReturn(variables);
+
+        final ProcessGroup processGroup = getChildProcessGroup("childGroup", "childGroupName", "", getProcessGroup(ROOT_PROCESSOR_GROUP_ID), AUTHORIZED, NOT_UNDER_VERSION_CONTROL);
+        Mockito.when(processGroup.getVariableRegistry()).thenReturn(variableRegistry);
+
+        givenProcessGroup(processGroup);
+
+        // when
+        whenExecuteSearch("variable");
+
+        // then
+        thenResultConsists()
+                .ofProcessGroup(getSimpleResultFromRoot("childGroup", "childGroupName", "Variable Name: variableName", "Variable Value: variableValue"))
+                .validate(results);
+    }
+
+    @Test
+    public void testSearchBasedOnConnectionAttributes() {
+        // given
+        final ProcessorNode processor1 = getProcessorNode("processor1", "processor1Name", AUTHORIZED);
+        final ProcessorNode processor2 = getProcessorNode("processor2", "processor2Name", AUTHORIZED);
+
+        givenRootProcessGroup()
+                .withProcessor(processor1)
+                .withProcessor(processor2)
+                .withConnection(getConnection("connection", "connectionName", getBasicRelationships(), processor1, processor2, AUTHORIZED));
+
+        // when
+        whenExecuteSearch("connection");
+
+        // then
+        thenResultConsists()
+                .ofConnection(getSimpleResultFromRoot("connection", "connectionName", "Id: connection", "Name: connectionName"))
+                .validate(results);
+    }
+
+    @Test
+    public void testSearchBasedOnPriorities() {
+        // given
+        final ProcessorNode processor1 = getProcessorNode("processor1", "processor1Name", AUTHORIZED);
+        final ProcessorNode processor2 = getProcessorNode("processor2", "processor2Name", AUTHORIZED);
+        final Connection connection = getConnection("connection", "connectionName", getBasicRelationships(), processor1, processor2, AUTHORIZED);
+
+        final FlowFileQueue flowFileQueue = Mockito.mock(FlowFileQueue.class);
+        final List<FlowFilePrioritizer> prioritizers = new ArrayList<>();
+        prioritizers.add(Mockito.mock(ComponentMockUtil.DummyFlowFilePrioritizer.class));
+        Mockito.when(flowFileQueue.getPriorities()).thenReturn(prioritizers);
+        Mockito.when(connection.getFlowFileQueue()).thenReturn(flowFileQueue);
+
+        givenRootProcessGroup()
+                .withProcessor(processor1)
+                .withProcessor(processor2)
+                .withConnection(connection);
+
+        // when
+        whenExecuteSearch("dummy");
+
+        // then
+        Assert.assertEquals(1, results.getConnectionResults().size());
+        Assert.assertEquals(1, results.getConnectionResults().get(0).getMatches().size());
+        Assert.assertTrue(results.getConnectionResults().get(0).getMatches().get(0)
+                .startsWith("Prioritizer: org.apache.nifi.web.controller.ComponentMockUtil$DummyFlowFilePrioritizer$"));
+    }
+
+    @Test
+    public void testSearchBasedOnExpiration() {
+        // given
+        final ProcessorNode processor1 = getProcessorNode("processor1", "processor1Name", AUTHORIZED);
+        final ProcessorNode processor2 = getProcessorNode("processor2", "processor2Name", AUTHORIZED);
+        final Connection connection = getConnection("connection", "connectionName", getBasicRelationships(), processor1, processor2, AUTHORIZED);
+
+        final FlowFileQueue flowFileQueue = Mockito.mock(FlowFileQueue.class);
+        Mockito.when(flowFileQueue.getFlowFileExpiration(TimeUnit.MILLISECONDS)).thenReturn(5);
+        Mockito.when(flowFileQueue.getFlowFileExpiration()).thenReturn("5");
+        Mockito.when(connection.getFlowFileQueue()).thenReturn(flowFileQueue);
+
+        givenRootProcessGroup()
+                .withProcessor(processor1)
+                .withProcessor(processor2)
+                .withConnection(connection);
+
+        // when
+        whenExecuteSearch("expire");
+
+        // then
+        thenResultConsists()
+                .ofConnection(getSimpleResultFromRoot("connection", "connectionName", "FlowFile expiration: 5" ))
+                .validate(results);
+
+
+        // when
+        whenExecuteSearch("expires");
+
+        // then
+        thenResultConsists()
+                .ofConnection(getSimpleResultFromRoot("connection", "connectionName", "FlowFile expiration: 5" ))
+                .validate(results);
+    }
+
+    @Test
+    public void testSearchBasedOnBackPressure() {
+        // given
+        final ProcessorNode processor1 = getProcessorNode("processor1", "processor1Name", AUTHORIZED);
+        final ProcessorNode processor2 = getProcessorNode("processor2", "processor2Name", AUTHORIZED);
+        final Connection connection = getConnection("connection", "connectionName", getBasicRelationships(), processor1, processor2, AUTHORIZED);
+
+        final FlowFileQueue flowFileQueue = Mockito.mock(FlowFileQueue.class);
+        Mockito.when(flowFileQueue.getBackPressureDataSizeThreshold()).thenReturn("100 KB");
+        Mockito.when(flowFileQueue.getBackPressureObjectThreshold()).thenReturn(5L);
+        Mockito.when(connection.getFlowFileQueue()).thenReturn(flowFileQueue);
+
+        givenRootProcessGroup()
+                .withProcessor(processor1)
+                .withProcessor(processor2)
+                .withConnection(connection);
+
+        // when
+        whenExecuteSearch("pressure");
+
+        // then
+        thenResultConsists()
+                .ofConnection(getSimpleResultFromRoot("connection", "connectionName", "Back pressure data size: 100 KB", "Back pressure count: 5"))
+                .validate(results);
+
+
+        // when
+        whenExecuteSearch("back pressure");
+
+        // then
+        thenResultConsists()
+                .ofConnection(getSimpleResultFromRoot("connection", "connectionName", "Back pressure data size: 100 KB", "Back pressure count: 5"))
+                .validate(results);
+    }
+
+    @Test
+    public void testSearchBasedOnConnectivity() {
+        // given
+        final ProcessorNode processor1 = getProcessorNode("source", "sourceName", AUTHORIZED);
+        final ProcessorNode processor2 = getProcessorNode("destination", "destinationName", AUTHORIZED);
+        final Connection connection = getConnection("connection", "connectionName", getBasicRelationships(), processor1, processor2, AUTHORIZED);
+
+        givenRootProcessGroup()
+                .withProcessor(processor1)
+                .withProcessor(processor2)
+                .withConnection(connection);
+
+        // when
+        whenExecuteSearch("source");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResultFromRoot("source", "sourceName", "Id: source", "Name: sourceName"))
+                .ofConnection(getSimpleResultFromRoot("connection", "connectionName", "Source id: source", "Source name: sourceName"))
+                .validate(results);
+
+
+        // when
+        whenExecuteSearch("destination");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResultFromRoot("destination", "destinationName", "Id: destination", "Name: destinationName"))
+                .ofConnection(getSimpleResultFromRoot("connection", "connectionName", "Destination id: destination", "Destination name: destinationName"))
+                .validate(results);
+    }
+
+    @Test
+    public void testSearchBasedOnRemoteProcessGroupAttributes() {
+        // given
+        givenRootProcessGroup()
+                .withRemoteProcessGroup(getRemoteProcessGroup("remote", "remoteName", Optional.empty(), "", "localhost", true, AUTHORIZED));
+
+        // when
+        whenExecuteSearch("remote");
+
+        // then
+        thenResultConsists()
+                .ofRemoteProcessGroup(getSimpleResultFromRoot("remote", "remoteName", "Id: remote", "Name: remoteName"))
+                .validate(results);
+    }
+
+    @Test
+    public void testSearchBasedOnRemoteProcessGroupURLs() {
+        // given
+        givenRootProcessGroup()
+                .withRemoteProcessGroup(getRemoteProcessGroup("remote", "remoteName", Optional.empty(), "", "localhost", true, AUTHORIZED));
+
+        // when
+        whenExecuteSearch("localhost");
+
+        // then
+        thenResultConsists()
+                .ofRemoteProcessGroup(getSimpleResultFromRoot("remote", "remoteName", "URLs: localhost"))
+                .validate(results);
+    }
+
+    @Test
+    public void testSearchBasedOnRemoteProcessTransmission() {
+        // given
+        givenRootProcessGroup()
+                .withRemoteProcessGroup(getRemoteProcessGroup("remote1", "remoteName1", Optional.empty(), "", "localhost", true, AUTHORIZED))
+                .withRemoteProcessGroup(getRemoteProcessGroup("remote2", "remoteName2", Optional.empty(), "", "localhost", false, AUTHORIZED));
+
+        // when
+        whenExecuteSearch("transmitting");
+
+        // then
+        thenResultConsists()
+                .ofRemoteProcessGroup(getSimpleResultFromRoot("remote1", "remoteName1", "Transmission: On"))
+                .validate(results);
+
+        // when
+        whenExecuteSearch("transmission enabled");
+
+        // then
+        thenResultConsists()
+                .ofRemoteProcessGroup(getSimpleResultFromRoot("remote1", "remoteName1", "Transmission: On"))
+                .validate(results);
+
+        // when
+        whenExecuteSearch("not transmitting");
+
+        // then
+        thenResultConsists()
+                .ofRemoteProcessGroup(getSimpleResultFromRoot("remote2", "remoteName2", "Transmission: Off"))
+                .validate(results);
+
+        // when
+        whenExecuteSearch("transmission disabled");
+
+        // then
+        thenResultConsists()
+                .ofRemoteProcessGroup(getSimpleResultFromRoot("remote2", "remoteName2", "Transmission: Off"))
+                .validate(results);
+    }
+
+    @Test
+    public void testSearchBasedOnPortPublicity() {
+        // given
+        givenRootProcessGroup()
+                .withInputPort(getPublicPort("port", "portName", "", ScheduledState.RUNNING, true, AUTHORIZED,
+                        Arrays.asList("accessAllowed1"), Arrays.asList("accessAllowed2")));
+
+        // when
+        whenExecuteSearch("allowed");
+
+        // then
+        thenResultConsists()
+                .ofInputPort(getSimpleResultFromRoot("port", "portName", "User access control: accessAllowed1", "Group access control: accessAllowed2"))
+                .validate(results);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/ControllerSearchServiceRegressionTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/ControllerSearchServiceRegressionTest.java
new file mode 100644
index 0000000..dc57a51
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/ControllerSearchServiceRegressionTest.java
@@ -0,0 +1,605 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.controller;
+
+import org.apache.nifi.authorization.Authorizer;
+import org.apache.nifi.authorization.RequestAction;
+import org.apache.nifi.authorization.user.NiFiUser;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.validation.ValidationStatus;
+import org.apache.nifi.connectable.Connection;
+import org.apache.nifi.connectable.Funnel;
+import org.apache.nifi.connectable.Port;
+import org.apache.nifi.controller.ProcessorNode;
+import org.apache.nifi.controller.ScheduledState;
+import org.apache.nifi.controller.label.Label;
+import org.apache.nifi.controller.service.ControllerServiceNode;
+import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.groups.RemoteProcessGroup;
+import org.apache.nifi.parameter.Parameter;
+import org.apache.nifi.parameter.ParameterContext;
+import org.apache.nifi.parameter.ParameterDescriptor;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.registry.ComponentVariableRegistry;
+import org.apache.nifi.registry.VariableDescriptor;
+import org.apache.nifi.registry.flow.VersionControlInformation;
+import org.apache.nifi.scheduling.ExecutionNode;
+import org.apache.nifi.scheduling.SchedulingStrategy;
+import org.apache.nifi.search.SearchResult;
+import org.apache.nifi.web.api.dto.search.ComponentSearchResultDTO;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import static org.apache.nifi.web.controller.ComponentMockUtil.getControllerServiceNode;
+import static org.apache.nifi.web.controller.ComponentMockUtil.getChildProcessGroup;
+import static org.apache.nifi.web.controller.ComponentMockUtil.getConnection;
+import static org.apache.nifi.web.controller.ComponentMockUtil.getFunnel;
+import static org.apache.nifi.web.controller.ComponentMockUtil.getLabel;
+import static org.apache.nifi.web.controller.ComponentMockUtil.getParameter;
+import static org.apache.nifi.web.controller.ComponentMockUtil.getParameterContext;
+import static org.apache.nifi.web.controller.ComponentMockUtil.getPort;
+import static org.apache.nifi.web.controller.ComponentMockUtil.getProcessorNode;
+import static org.apache.nifi.web.controller.ComponentMockUtil.getPublicPort;
+import static org.apache.nifi.web.controller.ComponentMockUtil.getRemoteProcessGroup;
+import static org.apache.nifi.web.controller.ComponentMockUtil.getRootProcessGroup;
+import static org.apache.nifi.web.controller.ComponentMockUtil.getSearchableProcessor;
+
+public class ControllerSearchServiceRegressionTest extends AbstractControllerSearchIntegrationTest {
+    @Test
+    public void testTextOmniMatch() {
+        // given
+        final String omniMatch = "omniMatch";
+        final ProcessGroup rootProcessGroup = getRootProcessGroup(ROOT_PROCESSOR_GROUP_ID, ROOT_PROCESSOR_GROUP_NAME, "root_no_find_omniMatch", true, false);
+
+        final ProcessorNode processor1 = getProcessorNode(
+            "proc1_id_omniMatch",
+            "proc1_name_omniMatch",
+            "proc1_comments_omniMatch",
+            Optional.of("proc1_versionedId_omniMatch"),
+            SchedulingStrategy.TIMER_DRIVEN,
+            ExecutionNode.ALL,
+            ScheduledState.STOPPED,
+            ValidationStatus.VALID,
+            Arrays.asList(
+                new Relationship.Builder().autoTerminateDefault(false).name("proc1_rel1_name_omniMatch").description("no_find_omniMatch").build(),
+                new Relationship.Builder().autoTerminateDefault(false).name("proc1_rel2_name_omniMatch").description("no_find_omniMatch").build()
+            ),
+            "proc1_type_omniMatch",
+            getSearchableProcessor(Arrays.asList(
+                new SearchResult.Builder().label("proc1_search1_label_omniMatch").match("proc1_search1_match_omniMatch").build(),
+                new SearchResult.Builder().label("proc1_search2_label_omniMatch").match("proc1_search2_match_omniMatch").build()
+            )),
+            new HashMap<PropertyDescriptor, String>(){{
+                put(new PropertyDescriptor.Builder().name("proc1_prop1_name_omniMatch").description("proc1_prop1_description_omniMatch").build(), "proc1_prop1_value_omniMatch");
+                put(new PropertyDescriptor.Builder().name("proc1_prop2_name_omniMatch").description("proc1_prop2_description_omniMatch").build(), "proc1_prop2_value_omniMatch");
+            }},
+            AUTHORIZED
+        );
+
+        final ComponentSearchResultDTO proc1Result = getSimpleResultFromRoot("proc1_id_omniMatch", "proc1_name_omniMatch",
+            "Id: proc1_id_omniMatch",
+            "Version Control ID: proc1_versionedId_omniMatch",
+            "Name: proc1_name_omniMatch",
+            "Comments: proc1_comments_omniMatch",
+            "Relationship: proc1_rel1_name_omniMatch",
+            "Relationship: proc1_rel2_name_omniMatch",
+            "Type: proc1_type_omniMatch",
+            "Property name: proc1_prop1_name_omniMatch",
+            "Property description: proc1_prop1_description_omniMatch",
+            "Property value: proc1_prop1_name_omniMatch - proc1_prop1_value_omniMatch",
+            "Property name: proc1_prop2_name_omniMatch",
+            "Property description: proc1_prop2_description_omniMatch",
+            "Property value: proc1_prop2_name_omniMatch - proc1_prop2_value_omniMatch",
+            "proc1_search1_label_omniMatch: proc1_search1_match_omniMatch",
+            "proc1_search2_label_omniMatch: proc1_search2_match_omniMatch"
+        );
+
+        final Funnel funnel1 = getFunnel(
+            "funnel1_id_omniMatch",
+            "funnel1_versionedId_omniMatch",
+            "funnel1_comments_omniMatch",
+            AUTHORIZED
+        );
+
+        final ComponentSearchResultDTO funnel1Result = getSimpleResultFromRoot("funnel1_id_omniMatch", null,
+            "Id: funnel1_id_omniMatch",
+            "Version Control ID: funnel1_versionedId_omniMatch"
+        );
+
+        final Connection connection1 = getConnection(
+            "conn1_id_omniMatch",
+            "conn1_name_omniMatch",
+            Optional.of("conn1_versionedId_omniMatch"),
+            Arrays.asList(
+                new Relationship.Builder().autoTerminateDefault(false).name("conn1_rel1_name_omniMatch").description("no_find_omniMatch").build(),
+                new Relationship.Builder().autoTerminateDefault(false).name("conn1_rel2_name_omniMatch").description("no_find_omniMatch").build()
+            ),
+            Arrays.asList(),
+            1,
+            "1 MB",
+            1L,
+            processor1,
+            funnel1,
+            AUTHORIZED
+        );
+
+        final ComponentSearchResultDTO connection1Result = getSimpleResultFromRoot("conn1_id_omniMatch", "conn1_name_omniMatch",
+            "Id: conn1_id_omniMatch",
+            "Version Control ID: conn1_versionedId_omniMatch",
+            "Name: conn1_name_omniMatch",
+            "Relationship: conn1_rel1_name_omniMatch",
+            "Relationship: conn1_rel2_name_omniMatch",
+            "Source id: proc1_id_omniMatch",
+            "Source name: proc1_name_omniMatch",
+            "Source comments: proc1_comments_omniMatch",
+            "Destination id: funnel1_id_omniMatch",
+            "Destination comments: funnel1_comments_omniMatch"
+        );
+
+        final Port inputPort1 = getPort(
+            "inport1_id_omniMatch",
+            "inport1_name_omniMatch",
+            "inport1_versionedId_omniMatch",
+            "inport1_comments_omniMatch",
+            ScheduledState.STOPPED,
+            true,
+            AUTHORIZED
+        );
+
+        final ComponentSearchResultDTO inputPort1Result = getSimpleResultFromRoot("inport1_id_omniMatch", "inport1_name_omniMatch",
+            "Id: inport1_id_omniMatch",
+            "Version Control ID: inport1_versionedId_omniMatch",
+            "Name: inport1_name_omniMatch",
+            "Comments: inport1_comments_omniMatch"
+        );
+
+        final Port outpuPublicPort1 = getPublicPort(
+            "outpublicport1_id_omniMatch",
+            "outpublicport1_name_omniMatch",
+            "outpublicport1_comments_omniMatch",
+            "outpublicport1_versionedId_omniMatch",
+            ScheduledState.STOPPED,
+            true,
+            AUTHORIZED,
+            Arrays.asList("outpublicport1_userAccessControl1_omniMatch", "outpublicport1_userAccessControl2_omniMatch"),
+            Arrays.asList("outpublicport1_groupAccessControl1_omniMatch", "outpublicport1_groupAccessControl2_omniMatch")
+        );
+
+        final ComponentSearchResultDTO outputPublicPort1Result = getSimpleResultFromRoot("outpublicport1_id_omniMatch", "outpublicport1_name_omniMatch",
+            "Id: outpublicport1_id_omniMatch",
+            "Version Control ID: outpublicport1_versionedId_omniMatch",
+            "Name: outpublicport1_name_omniMatch",
+            "Comments: outpublicport1_comments_omniMatch",
+            "User access control: outpublicport1_userAccessControl1_omniMatch",
+            "User access control: outpublicport1_userAccessControl2_omniMatch",
+            "Group access control: outpublicport1_groupAccessControl1_omniMatch",
+            "Group access control: outpublicport1_groupAccessControl2_omniMatch"
+        );
+
+        final Label label1 = getLabel("label1_id_omniMatch", "label1_value_omniMatch", true);
+
+        final ComponentSearchResultDTO label1Result = getSimpleResultFromRoot("label1_id_omniMatch", "label1_value_omniMatch",
+            "Id: label1_id_omniMatch",
+            "Value: label1_value_omniMatch"
+        );
+
+        final ControllerServiceNode controllerServiceNode1 = getControllerServiceNode(
+                "controllerServiceNode1_id_omniMatch",
+                "controllerServiceNode1_name_omniMatch",
+                "controllerServiceNode1_comments_omniMatch",
+                new HashMap<PropertyDescriptor, String>(){{
+                    put(new PropertyDescriptor.Builder()
+                                .name("controllerServiceNode1_prop1_name_omniMatch")
+                                .description("controllerServiceNode1_prop1_description_omniMatch")
+                                .build(),
+                            "controllerServiceNode1_prop1_value_omniMatch");
+                    put(new PropertyDescriptor.Builder()
+                                .name("controllerServiceNode1_prop2_name_omniMatch")
+                                .description("controllerServiceNode1_prop2_description_omniMatch")
+                                .build(),
+                            "controllerServiceNode1_prop2_value_omniMatch");
+                }},
+                "controllerServiceNode1_versioned_id_omniMatch",
+                AUTHORIZED
+        );
+
+        final ComponentSearchResultDTO controllerServiceNode1Result = getSimpleResultFromRoot("controllerServiceNode1_id_omniMatch", "controllerServiceNode1_name_omniMatch",
+                "Id: controllerServiceNode1_id_omniMatch",
+                "Name: controllerServiceNode1_name_omniMatch",
+                "Comments: controllerServiceNode1_comments_omniMatch",
+                "Version Control ID: controllerServiceNode1_versioned_id_omniMatch",
+                "Property name: controllerServiceNode1_prop1_name_omniMatch",
+                "Property description: controllerServiceNode1_prop1_description_omniMatch",
+                "Property value: controllerServiceNode1_prop1_name_omniMatch - controllerServiceNode1_prop1_value_omniMatch",
+                "Property name: controllerServiceNode1_prop2_name_omniMatch",
+                "Property description: controllerServiceNode1_prop2_description_omniMatch",
+                "Property value: controllerServiceNode1_prop2_name_omniMatch - controllerServiceNode1_prop2_value_omniMatch"
+        );
+
+        final ProcessGroup processGroup1 = getChildProcessGroup(
+            "processgroup1_id_omniMatch",
+            "processgroup1_name_omniMatch",
+            "processgroup1_comments_omniMatch",
+            "processgroup1_versionedId_omniMatch",
+            rootProcessGroup,
+            AUTHORIZED,
+            UNDER_VERSION_CONTROL
+        );
+
+        final ComponentVariableRegistry variableRegistry = Mockito.mock(ComponentVariableRegistry.class);
+
+        Mockito.when(processGroup1.getVariableRegistry()).thenReturn(variableRegistry);
+        Mockito.when(variableRegistry.getVariableMap()).thenReturn(new HashMap<VariableDescriptor, String>(){{
+            put(new VariableDescriptor.Builder("processgroup1_variable1_key_omniMatch").description("no_find_omniMatch").build(), "processgroup1_variable1_value_omniMatch");
+            put(new VariableDescriptor.Builder("processgroup1_variable2_key_omniMatch").description("no_find_omniMatch").build(), "processgroup1_variable2_value_omniMatch");
+        }});
+
+        final ComponentSearchResultDTO processGroup1Result = getSimpleResultFromRoot("processgroup1_id_omniMatch", "processgroup1_name_omniMatch",
+            "Id: processgroup1_id_omniMatch",
+            "Version Control ID: processgroup1_versionedId_omniMatch",
+            "Name: processgroup1_name_omniMatch",
+            "Comments: processgroup1_comments_omniMatch",
+            "Variable Name: processgroup1_variable1_key_omniMatch",
+            "Variable Value: processgroup1_variable1_value_omniMatch",
+            "Variable Name: processgroup1_variable2_key_omniMatch",
+            "Variable Value: processgroup1_variable2_value_omniMatch"
+        );
+
+        final RemoteProcessGroup remoteProcessGroup1 = getRemoteProcessGroup(
+            "remoteprocessgroup1_id_omniMatch",
+            "remoteprocessgroup1_name_omniMatch",
+            Optional.of("remoteprocessgroup1_versionedId_omniMatch"),
+            "remoteprocessgroup1_comments_omniMatch",
+            "remoteprocessgroup1_targetUris_omniMatch",
+            false,
+            AUTHORIZED
+        );
+
+        final ComponentSearchResultDTO remoteProcessGroup1Result = getSimpleResultFromRoot("remoteprocessgroup1_id_omniMatch", "remoteprocessgroup1_name_omniMatch",
+            "Id: remoteprocessgroup1_id_omniMatch",
+            "Version Control ID: remoteprocessgroup1_versionedId_omniMatch",
+            "Name: remoteprocessgroup1_name_omniMatch",
+            "Comments: remoteprocessgroup1_comments_omniMatch",
+            "URLs: remoteprocessgroup1_targetUris_omniMatch"
+        );
+
+        final ParameterContext parameterContext1 = getParameterContext(
+            "parametercontext1_id_omniMatch",
+            "parametercontext1_name_omniMatch",
+            "parametercontext1_description_omniMatch",
+            givenParameters(
+                getParameter("parametercontext1_parameter1_name_omniMatch", "parametercontext1_parameter1_value_omniMatch", false, "parametercontext1_parameter1_description_omniMatch"),
+                getParameter("parametercontext1_parameter2_name_omniMatch", "sensitive_no_find_omniMatch", true, "parametercontext1_parameter2_description_omniMatch")
+            ),
+            AUTHORIZED
+        );
+
+        final ComponentSearchResultDTO parameterContext1Result = getSimpleResult("parametercontext1_id_omniMatch", "parametercontext1_name_omniMatch", null, null, null,
+            "Id: parametercontext1_id_omniMatch",
+            "Name: parametercontext1_name_omniMatch",
+            "Description: parametercontext1_description_omniMatch"
+        );
+
+        final Collection<ComponentSearchResultDTO> parameterResults1 = Arrays.asList(
+            getSimpleResult(
+                "parametercontext1_parameter1_name_omniMatch",
+                "parametercontext1_parameter1_name_omniMatch",
+                null,
+                "parametercontext1_id_omniMatch",
+                "parametercontext1_name_omniMatch",
+                "Name: parametercontext1_parameter1_name_omniMatch",
+                "Description: parametercontext1_parameter1_description_omniMatch",
+                "Value: parametercontext1_parameter1_value_omniMatch"
+            ),
+            getSimpleResult(
+                "parametercontext1_parameter2_name_omniMatch",
+                "parametercontext1_parameter2_name_omniMatch",
+                null,
+                "parametercontext1_id_omniMatch",
+                "parametercontext1_name_omniMatch",
+                "Name: parametercontext1_parameter2_name_omniMatch",
+                "Description: parametercontext1_parameter2_description_omniMatch"
+            )
+        );
+
+        givenParameterContext(parameterContext1);
+        givenProcessGroup(rootProcessGroup)
+            .withProcessor(processor1)
+            .withFunnel(funnel1)
+            .withConnection(connection1)
+            .withInputPort(inputPort1)
+            .withOutputPort(outpuPublicPort1)
+            .withLabel(label1)
+            .withControllerServiceNode(controllerServiceNode1)
+            .withChild(processGroup1)
+            .withRemoteProcessGroup(remoteProcessGroup1);
+
+        // when
+        whenExecuteSearch(omniMatch);
+
+        // then
+        thenResultConsists()
+            .ofProcessor(proc1Result)
+            .ofFunnel(funnel1Result)
+            .ofConnection(connection1Result)
+            .ofInputPort(inputPort1Result)
+            .ofOutputPort(outputPublicPort1Result)
+            .ofLabel(label1Result)
+            .ofControllerServiceNode(controllerServiceNode1Result)
+            .ofProcessGroup(processGroup1Result)
+            .ofRemoteProcessGroup(remoteProcessGroup1Result)
+            .ofParameterContext(parameterContext1Result)
+            .ofParameter(parameterResults1)
+            .validate(results);
+    }
+
+    @Test
+    public void testSearchInRootLevelAllAuthorizedNoVersionControl() {
+        // given
+        givenBasicStructure();
+        getProcessGroupSetup(ROOT_PROCESSOR_GROUP_ID).withProcessor(getProcessorNode("foobarId", "foobar", AUTHORIZED));
+
+        // when
+        whenExecuteSearch("foo");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResult("foobarId", "foobar", "rootId", "rootId", "rootName", "Id: foobarId", "Name: foobar"))
+                .validate(results);
+    }
+
+    @Test
+    public void testSearchInThirdLevelAllAuthorizedNoVersionControl() {
+        // given
+        givenBasicStructure();
+        getProcessGroupSetup("thirdLevelAId").withProcessor(getProcessorNode("foobarId", "foobar", AUTHORIZED));
+
+        // when
+        whenExecuteSearch("foo");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResult("foobarId", "foobar", "thirdLevelAId", "thirdLevelAId", "thirdLevelA", "Id: foobarId", "Name: foobar"))
+                .validate(results);
+    }
+
+    @Test
+    public void testSearchInThirdLevelParentNotAuthorizedNoVersionControl() {
+        // given
+        givenBasicStructure();
+        givenThirdLevelIsNotAuthorized();
+        getProcessGroupSetup("thirdLevelAId").withProcessor(getProcessorNode("foobarId", "foobar", AUTHORIZED));
+
+        // when
+        whenExecuteSearch("foo");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getSimpleResult("foobarId", "foobar", "thirdLevelAId", "thirdLevelAId", null, "Id: foobarId", "Name: foobar"))
+                .validate(results);
+    }
+
+    @Test
+    public void testSearchInThirdLevelParentNotAuthorizedWithVersionControl() {
+        // given
+        givenBasicStructure();
+        givenThirdLevelIsNotAuthorized();
+        givenProcessorGroupIsUnderVersionControl("firstLevelAId");
+        getProcessGroupSetup("thirdLevelAId").withProcessor(getProcessorNode("foobarId", "foobar", AUTHORIZED));
+
+        // when
+        whenExecuteSearch("foo");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getVersionedResult("foobarId", "foobar", "thirdLevelAId", "thirdLevelAId", null, "firstLevelAId", "firstLevelA", "Id: foobarId", "Name: foobar"))
+                .validate(results);
+    }
+
+    @Test
+    public void testSearchInThirdLevelParentNotAuthorizedWithVersionControlInTheGroup() {
+        // given
+        givenBasicStructure();
+        givenThirdLevelIsNotAuthorized();
+        givenProcessorGroupIsUnderVersionControl("thirdLevelAId");
+        getProcessGroupSetup("thirdLevelAId").withProcessor(getProcessorNode("foobarId", "foobar", AUTHORIZED));
+
+        // when
+        whenExecuteSearch("foo");
+
+        // then
+        thenResultConsists()
+                .ofProcessor(getVersionedResult("foobarId", "foobar", "thirdLevelAId", "thirdLevelAId", null, "thirdLevelAId", null, "Id: foobarId", "Name: foobar"))
+                .validate(results);
+    }
+
+    @Test
+    public void testSearchParameterContext() {
+        // given
+        givenProcessGroup(getRootProcessGroup(ROOT_PROCESSOR_GROUP_ID, ROOT_PROCESSOR_GROUP_NAME, "", AUTHORIZED, NOT_UNDER_VERSION_CONTROL));
+
+        final Map<ParameterDescriptor, Parameter> fooParameters = givenParameters(
+                getParameter("foo_1", "foo_1_value", false, "Description for foo_1"));
+
+        final Map<ParameterDescriptor, Parameter> barParameters = givenParameters(
+                getParameter("bar_1", "bar_1_value", false, "Description for bar_1"),
+                getParameter("bar_2", "bar_2_value", false, "Description for bar_2"));
+
+        givenParameterContext(getParameterContext("fooId", "foo", "description for parameter context foo", fooParameters, AUTHORIZED));
+        givenParameterContext(getParameterContext("barId", "bar", "description for parameter context bar", barParameters, AUTHORIZED));
+
+        // when
+        whenExecuteSearch("foo");
+
+        // then
+        thenResultConsists()
+                .ofParameterContext(getSimpleResult("fooId", "foo", null, null, null, "Id: fooId", "Name: foo", "Description: description for parameter context foo"))
+                .ofParameter(getSimpleResult("foo_1", "foo_1", null, "fooId", "foo", "Name: foo_1", "Value: foo_1_value", "Description: Description for foo_1"))
+                .validate(results);
+    }
+
+    @Test
+    public void testSearchParameterContextNotAuthorized() {
+        // given
+        givenProcessGroup(getRootProcessGroup(ROOT_PROCESSOR_GROUP_ID, ROOT_PROCESSOR_GROUP_NAME, "", AUTHORIZED, NOT_UNDER_VERSION_CONTROL));
+
+        final Map<ParameterDescriptor, Parameter> fooParameters = givenParameters(
+                getParameter("foo_1", "foo_1_value", false, "Description for foo_1"));
+
+        final Map<ParameterDescriptor, Parameter> barParameters = givenParameters(
+                getParameter("bar_1", "bar_1_value", false, "Description for bar_1"),
+                getParameter("bar_2", "bar_2_value", false, "Description for bar_2"));
+
+        givenParameterContext(getParameterContext("fooId", "foo", "description for parameter context foo", fooParameters, NOT_AUTHORIZED));
+        givenParameterContext(getParameterContext("barId", "bar", "description for parameter context bar", barParameters, AUTHORIZED));
+
+        // when
+        whenExecuteSearch("foo");
+
+        // then
+        thenResultIsEmpty();
+    }
+
+    @Test
+    public void testSearchLabels() {
+        // given
+        givenProcessGroup(getRootProcessGroup(ROOT_PROCESSOR_GROUP_ID, ROOT_PROCESSOR_GROUP_NAME, "", AUTHORIZED, NOT_UNDER_VERSION_CONTROL))
+                .withLabel(getLabel("foo", "Value for label foo", AUTHORIZED))
+                .withLabel(getLabel("bar", "Value for label bar, but FOO is in here too", NOT_AUTHORIZED));
+
+        // when
+        whenExecuteSearch("foo");
+
+        // then
+        thenResultConsists()
+                .ofLabel(getSimpleResult("foo", "Value for label foo", ROOT_PROCESSOR_GROUP_ID, ROOT_PROCESSOR_GROUP_ID, ROOT_PROCESSOR_GROUP_NAME, "Id: foo", "Value: Value for label foo"))
+                .validate(results);
+    }
+
+    @Test
+    public void testSearchControllerServices() {
+        // given
+        final String name = "controllerServiceName";
+        final String id = name + "Id";
+        final Map<PropertyDescriptor, String> rawProperties = new HashMap<PropertyDescriptor, String>(){{
+            put(new PropertyDescriptor.Builder().name("prop1-name").displayName("prop1-displayname").description("prop1 description").defaultValue("prop1-default").build(), "prop1-value");
+            put(new PropertyDescriptor.Builder().name("prop2-name").displayName("prop2-displayname").description("prop2 description").defaultValue("prop2-default").build(), null);
+        }};
+
+
+        givenRootProcessGroup()
+                .withControllerServiceNode(getControllerServiceNode(id, name, "foo comment", rawProperties, AUTHORIZED));
+
+        // when - search for name
+        whenExecuteSearch("controllerserv");
+
+        // then
+        thenResultConsists()
+                .ofControllerServiceNode(getSimpleResultFromRoot(id, name, "Name: controllerServiceName", "Id: controllerServiceNameId"))
+                .validate(results);
+
+
+        // when - search for comments
+        whenExecuteSearch("foo comment");
+
+        // then
+        thenResultConsists()
+                .ofControllerServiceNode(getSimpleResultFromRoot(id, name, "Comments: foo comment"))
+                .validate(results);
+
+
+        // when - search for properties
+        whenExecuteSearch("prop1-name");
+
+        // then
+        thenResultConsists()
+                .ofControllerServiceNode(getSimpleResultFromRoot(id, name, "Property name: prop1-name"))
+                .validate(results);
+
+
+       // when - property default value
+        whenExecuteSearch("prop2-def");
+
+        // then
+        thenResultConsists()
+                .ofControllerServiceNode(getSimpleResultFromRoot(id, name, "Property value: prop2-name - prop2-default"))
+                .validate(results);
+
+
+        // when - property description
+        whenExecuteSearch("desc");
+
+        // then
+        thenResultConsists()
+                .ofControllerServiceNode(getSimpleResultFromRoot(id, name, "Property description: prop1 description", "Property description: prop2 description"))
+                .validate(results);
+
+
+        // when - by specified value
+        whenExecuteSearch("prop1-value");
+
+        // then
+        thenResultConsists()
+                .ofControllerServiceNode(getSimpleResultFromRoot(id, name, "Property value: prop1-name - prop1-value"))
+                .validate(results);
+
+
+        // when - search finding no match
+        whenExecuteSearch("ZZZZZZZZZYYYYYY");
+
+        // then
+        thenResultIsEmpty();
+
+
+        // when - properties are filtered out
+        whenExecuteSearch("properties:exclude prop1");
+
+        // then
+        thenResultIsEmpty();
+    }
+
+
+    // Helper methods
+
+    private void givenBasicStructure() {
+        givenProcessGroup(getRootProcessGroup(ROOT_PROCESSOR_GROUP_ID, ROOT_PROCESSOR_GROUP_NAME, "", ROOT_PROCESSOR_GROUP_ID, AUTHORIZED, NOT_UNDER_VERSION_CONTROL));
+
+        givenProcessGroup(getChildProcessGroup("firstLevelAId", "firstLevelA", "", "firstLevelAId", getProcessGroup(ROOT_PROCESSOR_GROUP_ID), AUTHORIZED, NOT_UNDER_VERSION_CONTROL));
+        givenProcessGroup(getChildProcessGroup("firstLevelBId", "firstLevelB", "", "firstLevelBId", getProcessGroup(ROOT_PROCESSOR_GROUP_ID), AUTHORIZED, NOT_UNDER_VERSION_CONTROL));
+
+        givenProcessGroup(getChildProcessGroup("secondLevelAId", "secondLevelA", "", "secondLevelAId", getProcessGroup("firstLevelAId"), AUTHORIZED, NOT_UNDER_VERSION_CONTROL));
+        givenProcessGroup(getChildProcessGroup("secondLevelBId", "secondLevelB", "", "secondLevelBId", getProcessGroup("firstLevelBId"), AUTHORIZED, NOT_UNDER_VERSION_CONTROL));
+
+        givenProcessGroup(getChildProcessGroup("thirdLevelAId", "thirdLevelA", "", "thirdLevelAId", getProcessGroup("secondLevelAId"), AUTHORIZED, NOT_UNDER_VERSION_CONTROL));
+        givenProcessGroup(getChildProcessGroup("thirdLevelBId", "thirdLevelB", "", "thirdLevelBId", getProcessGroup("secondLevelBId"), AUTHORIZED, NOT_UNDER_VERSION_CONTROL));
+    }
+
+    private void givenThirdLevelIsNotAuthorized() {
+        Mockito.when(getProcessGroup("thirdLevelAId").isAuthorized(Mockito.any(Authorizer.class), Mockito.any(RequestAction.class), Mockito.any(NiFiUser.class))).thenReturn(false);
+        Mockito.when(getProcessGroup("thirdLevelBId").isAuthorized(Mockito.any(Authorizer.class), Mockito.any(RequestAction.class), Mockito.any(NiFiUser.class))).thenReturn(false);
+    }
+
+    private void givenProcessorGroupIsUnderVersionControl(final String processGroupId) {
+        Mockito.when(getProcessGroup(processGroupId).getVersionControlInformation()).thenReturn(Mockito.mock(VersionControlInformation.class));
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/ControllerSearchServiceTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/ControllerSearchServiceTest.java
index 66ed8a9..322d2da 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/ControllerSearchServiceTest.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/ControllerSearchServiceTest.java
@@ -19,648 +19,619 @@ package org.apache.nifi.web.controller;
 import org.apache.nifi.authorization.Authorizer;
 import org.apache.nifi.authorization.RequestAction;
 import org.apache.nifi.authorization.user.NiFiUser;
-import org.apache.nifi.components.PropertyDescriptor;
-import org.apache.nifi.controller.ControllerService;
+import org.apache.nifi.connectable.Connection;
+import org.apache.nifi.connectable.Funnel;
+import org.apache.nifi.connectable.Port;
 import org.apache.nifi.controller.FlowController;
 import org.apache.nifi.controller.ProcessorNode;
-import org.apache.nifi.controller.StandardProcessorNode;
 import org.apache.nifi.controller.flow.FlowManager;
 import org.apache.nifi.controller.label.Label;
-import org.apache.nifi.controller.service.ControllerServiceNode;
-import org.apache.nifi.controller.service.StandardControllerServiceNode;
 import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.groups.RemoteProcessGroup;
 import org.apache.nifi.parameter.Parameter;
 import org.apache.nifi.parameter.ParameterContext;
 import org.apache.nifi.parameter.ParameterContextManager;
 import org.apache.nifi.parameter.ParameterDescriptor;
-import org.apache.nifi.processor.Processor;
-import org.apache.nifi.registry.VariableRegistry;
-import org.apache.nifi.registry.flow.StandardVersionControlInformation;
-import org.apache.nifi.registry.flow.VersionControlInformation;
-import org.apache.nifi.registry.variable.MutableVariableRegistry;
+import org.apache.nifi.web.api.dto.search.ComponentSearchResultDTO;
 import org.apache.nifi.web.api.dto.search.SearchResultsDTO;
+import org.apache.nifi.web.search.ComponentMatcher;
+import org.apache.nifi.web.search.query.SearchQuery;
+import org.apache.nifi.web.search.resultenrichment.ComponentSearchResultEnricher;
+import org.apache.nifi.web.search.resultenrichment.ComponentSearchResultEnricherFactory;
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
-import org.mockito.AdditionalMatchers;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
 import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
 
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.isNull;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-
-public class ControllerSearchServiceTest {
-    private MutableVariableRegistry variableRegistry;
-    private ControllerSearchService service;
-    private SearchResultsDTO searchResultsDTO;
+@RunWith(MockitoJUnitRunner.class)
+public class ControllerSearchServiceTest  {
+
+    public static final String PROCESS_GROUP_SECOND_LEVEL_A = "secondLevelA";
+    public static final String PROCESS_GROUP_SECOND_LEVEL_B_1 = "secondLevelB1";
+    public static final String PROCESS_GROUP_SECOND_LEVEL_B_2 = "secondLevelB2";
+    public static final String PROCESS_GROUP_FIRST_LEVEL_A = "firstLevelA";
+    public static final String PROCESS_GROUP_FIRST_LEVEL_B = "firstLevelB";
+    public static final String PROCESS_GROUP_ROOT = "root";
+
+    @Mock
+    private SearchQuery searchQuery;
+
+    @Mock
+    private NiFiUser user;
+
+    @Mock
+    private Authorizer authorizer;
+
+    @Mock
+    private ComponentSearchResultEnricherFactory resultEnricherFactory;
+
+    @Mock
+    private ComponentSearchResultEnricher resultEnricher;
+
+    @Mock
     private FlowController flowController;
+
+    @Mock
+    private FlowManager flowManager;
+
+    @Mock
     private ParameterContextManager parameterContextManager;
 
+    @Mock
+    private ComponentMatcher<ProcessorNode> matcherForProcessor;
+
+    @Mock
+    private ComponentMatcher<ProcessGroup> matcherForProcessGroup;
+
+    @Mock
+    private ComponentMatcher<Connection> matcherForConnection;
+
+    @Mock
+    private ComponentMatcher<RemoteProcessGroup> matcherForRemoteProcessGroup;
+
+    @Mock
+    private ComponentMatcher<Port> matcherForPort;
+
+    @Mock
+    private ComponentMatcher<Funnel> matcherForFunnel;
+
+    @Mock
+    private ComponentMatcher<ParameterContext> matcherForParameterContext;
+
+    @Mock
+    private ComponentMatcher<Parameter> matcherForParameter;
+
+    @Mock
+    private ComponentMatcher<Label> matcherForLabel;
+
+    private HashMap<String, ProcessGroup> processGroups;
+
+    private ControllerSearchService testSubject;
+
+    private SearchResultsDTO results;
+
     @Before
     public void setUp() {
-        variableRegistry = mock(MutableVariableRegistry.class);
-        service = new ControllerSearchService();
-        searchResultsDTO = new SearchResultsDTO();
-        flowController = mock(FlowController.class);
+        Mockito.when(resultEnricherFactory.getComponentResultEnricher(Mockito.any(ProcessGroup.class), Mockito.any(NiFiUser.class))).thenReturn(resultEnricher);
+        Mockito.when(resultEnricherFactory.getProcessGroupResultEnricher(Mockito.any(ProcessGroup.class), Mockito.any(NiFiUser.class))).thenReturn(resultEnricher);
+        Mockito.when(resultEnricherFactory.getParameterResultEnricher(Mockito.any(ParameterContext.class))).thenReturn(resultEnricher);
+        Mockito.when(resultEnricher.enrich(Mockito.any(ComponentSearchResultDTO.class))).thenAnswer(invocationOnMock -> invocationOnMock.getArgument(0));
+
+        Mockito.when(matcherForProcessor.match(Mockito.any(ProcessorNode.class), Mockito.any(SearchQuery.class))).thenReturn(Optional.of(new ComponentSearchResultDTO()));
+        Mockito.when(matcherForProcessGroup.match(Mockito.any(ProcessGroup.class), Mockito.any(SearchQuery.class))).thenReturn(Optional.of(new ComponentSearchResultDTO()));
+        Mockito.when(matcherForConnection.match(Mockito.any(Connection.class), Mockito.any(SearchQuery.class))).thenReturn(Optional.of(new ComponentSearchResultDTO()));
+        Mockito.when(matcherForRemoteProcessGroup.match(Mockito.any(RemoteProcessGroup.class), Mockito.any(SearchQuery.class))).thenReturn(Optional.of(new ComponentSearchResultDTO()));
+        Mockito.when(matcherForPort.match(Mockito.any(Port.class), Mockito.any(SearchQuery.class))).thenReturn(Optional.of(new ComponentSearchResultDTO()));
+        Mockito.when(matcherForFunnel.match(Mockito.any(Funnel.class), Mockito.any(SearchQuery.class))).thenReturn(Optional.of(new ComponentSearchResultDTO()));
+        Mockito.when(matcherForParameterContext.match(Mockito.any(ParameterContext.class), Mockito.any(SearchQuery.class))).thenReturn(Optional.of(new ComponentSearchResultDTO()));
+        Mockito.when(matcherForParameter.match(Mockito.any(Parameter.class), Mockito.any(SearchQuery.class))).thenReturn(Optional.of(new ComponentSearchResultDTO()));
+        Mockito.when(matcherForLabel.match(Mockito.any(Label.class), Mockito.any(SearchQuery.class))).thenReturn(Optional.of(new ComponentSearchResultDTO()));
+
+        results = new SearchResultsDTO();
+        testSubject = givenTestSubject();
+        processGroups = new HashMap<>();
+    }
 
-        FlowManager mockFlowManager = mock(FlowManager.class);
-        parameterContextManager = mock(ParameterContextManager.class);
+    @Test
+    public void testSearchChecksEveryComponentType() {
+        // given
+        givenSingleProcessGroupIsSetUp();
+        givenSearchQueryIsSetUp();
+        givenNoFilters();
+
+        // when
+        testSubject.search(searchQuery, results);
+
+        // then
+        thenAllComponentTypeIsChecked();
+        thenAllComponentResultsAreCollected();
+    }
 
-        doReturn(mockFlowManager).when(flowController).getFlowManager();
-        doReturn(parameterContextManager).when(mockFlowManager).getParameterContextManager();
-        service.setFlowController(flowController);
+    @Test
+    public void testSearchChecksChildrenGroupsToo() {
+        // given
+        givenProcessGroupsAreSetUp();
+        givenSearchQueryIsSetUp();
+        givenNoFilters();
+
+        // when
+        testSubject.search(searchQuery, results);
+
+        // then
+        thenFollowingGroupsAreSearched(Arrays.asList(
+                PROCESS_GROUP_FIRST_LEVEL_A,
+                PROCESS_GROUP_SECOND_LEVEL_A,
+                PROCESS_GROUP_FIRST_LEVEL_B,
+                PROCESS_GROUP_SECOND_LEVEL_B_1,
+                PROCESS_GROUP_SECOND_LEVEL_B_2));
+        thenContentOfTheFollowingGroupsAreSearched(processGroups.keySet());
     }
 
     @Test
-    public void testSearchInRootLevelAllAuthorizedNoVersionControl() {
-        // root level PG
-        final ProcessGroup rootProcessGroup = setupMockedProcessGroup("root", null, true, variableRegistry, null);
-
-        // first level PGs
-        final ProcessGroup firstLevelAProcessGroup = setupMockedProcessGroup("firstLevelA", rootProcessGroup, true, variableRegistry, null);
-        final ProcessGroup firstLevelBProcessGroup = setupMockedProcessGroup("firstLevelB", rootProcessGroup, true, variableRegistry, null);
-
-        // second level PGs
-        final ProcessGroup secondLevelAProcessGroup = setupMockedProcessGroup("secondLevelA", firstLevelAProcessGroup, true, variableRegistry, null);
-        final ProcessGroup secondLevelBProcessGroup = setupMockedProcessGroup("secondLevelB", firstLevelBProcessGroup, true, variableRegistry, null);
-        // third level PGs
-        final ProcessGroup thirdLevelAProcessGroup = setupMockedProcessGroup("thirdLevelA", secondLevelAProcessGroup, true, variableRegistry, null);
-        final ProcessGroup thirdLevelBProcessGroup = setupMockedProcessGroup("thirdLevelB", secondLevelAProcessGroup, true, variableRegistry, null);
-
-        // link PGs together
-        Mockito.doReturn(new HashSet<ProcessGroup>() {
-            {
-                add(firstLevelAProcessGroup);
-                add(firstLevelBProcessGroup);
-            }
-        }).when(rootProcessGroup).getProcessGroups();
-
-        Mockito.doReturn(new HashSet<ProcessGroup>() {
-            {
-                add(secondLevelAProcessGroup);
-            }
-        }).when(firstLevelAProcessGroup).getProcessGroups();
-        Mockito.doReturn(new HashSet<ProcessGroup>() {
-            {
-                add(secondLevelBProcessGroup);
-            }
-        }).when(firstLevelBProcessGroup).getProcessGroups();
-
-        Mockito.doReturn(new HashSet<ProcessGroup>() {
-            {
-                add(thirdLevelAProcessGroup);
-                add(thirdLevelBProcessGroup);
-            }
-        }).when(secondLevelAProcessGroup).getProcessGroups();
-
-        // setup processor
-        setupMockedProcessor("foobar", rootProcessGroup, true, variableRegistry);
-
-        // perform search
-        service.search(searchResultsDTO, "foo", rootProcessGroup);
-
-        assertTrue(searchResultsDTO.getProcessorResults().size() == 1);
-        assertTrue(searchResultsDTO.getProcessorResults().get(0).getId().equals("foobarId"));
-        assertTrue(searchResultsDTO.getProcessorResults().get(0).getParentGroup().getId().equals("rootId"));
-        assertTrue(searchResultsDTO.getProcessorResults().get(0).getParentGroup().getName().equals("root"));
-        assertTrue(searchResultsDTO.getProcessorResults().get(0).getVersionedGroup() == null);
+    public void testSearchWhenGroupIsNotAuthorized() {
+        // given
+        givenProcessGroupsAreSetUp();
+        givenSearchQueryIsSetUp();
+        givenNoFilters();
+        givenProcessGroupIsNotAutorized(PROCESS_GROUP_FIRST_LEVEL_B);
+
+        // when
+        testSubject.search(searchQuery, results);
+        // The authorization is not transitive, children groups might be good candidates.
+        thenFollowingGroupsAreSearched(Arrays.asList(
+                PROCESS_GROUP_FIRST_LEVEL_A,
+                PROCESS_GROUP_SECOND_LEVEL_A,
+                PROCESS_GROUP_SECOND_LEVEL_B_1,
+                PROCESS_GROUP_SECOND_LEVEL_B_2));
+        thenContentOfTheFollowingGroupsAreSearched(Arrays.asList(
+                PROCESS_GROUP_ROOT,
+                PROCESS_GROUP_FIRST_LEVEL_A,
+                PROCESS_GROUP_SECOND_LEVEL_A,
+                PROCESS_GROUP_FIRST_LEVEL_B,
+                PROCESS_GROUP_SECOND_LEVEL_B_1,
+                PROCESS_GROUP_SECOND_LEVEL_B_2));
     }
 
     @Test
-    public void testSearchInThirdLevelAllAuthorizedNoVersionControl() {
-        // root level PG
-        final ProcessGroup rootProcessGroup = setupMockedProcessGroup("root", null, true, variableRegistry, null);
-
-        // first level PGs
-        final ProcessGroup firstLevelAProcessGroup = setupMockedProcessGroup("firstLevelA", rootProcessGroup, true, variableRegistry, null);
-        final ProcessGroup firstLevelBProcessGroup = setupMockedProcessGroup("firstLevelB", rootProcessGroup, true, variableRegistry, null);
-
-        // second level PGs
-        final ProcessGroup secondLevelAProcessGroup = setupMockedProcessGroup("secondLevelA", firstLevelAProcessGroup, true, variableRegistry, null);
-        final ProcessGroup secondLevelBProcessGroup = setupMockedProcessGroup("secondLevelB", firstLevelBProcessGroup, true, variableRegistry, null);
-        // third level PGs
-        final ProcessGroup thirdLevelAProcessGroup = setupMockedProcessGroup("thirdLevelA", secondLevelAProcessGroup, true, variableRegistry, null);
-        final ProcessGroup thirdLevelBProcessGroup = setupMockedProcessGroup("thirdLevelB", secondLevelAProcessGroup, true, variableRegistry, null);
-
-        // link PGs together
-        Mockito.doReturn(new HashSet<ProcessGroup>() {
-            {
-                add(firstLevelAProcessGroup);
-                add(firstLevelBProcessGroup);
-            }
-        }).when(rootProcessGroup).getProcessGroups();
-
-        Mockito.doReturn(new HashSet<ProcessGroup>() {
-            {
-                add(secondLevelAProcessGroup);
-            }
-        }).when(firstLevelAProcessGroup).getProcessGroups();
-
-        Mockito.doReturn(new HashSet<ProcessGroup>() {
-            {
-                add(secondLevelBProcessGroup);
-            }
-        }).when(firstLevelBProcessGroup).getProcessGroups();
-
-        Mockito.doReturn(new HashSet<ProcessGroup>() {
-            {
-                add(thirdLevelAProcessGroup);
-                add(thirdLevelBProcessGroup);
-            }
-        }).when(secondLevelAProcessGroup).getProcessGroups();
-
-        // setup processor
-        setupMockedProcessor("foobar", thirdLevelAProcessGroup, true, variableRegistry);
-
-        // perform search
-        service.search(searchResultsDTO, "foo", rootProcessGroup);
-
-        assertTrue(searchResultsDTO.getProcessorResults().size() == 1);
-        assertTrue(searchResultsDTO.getProcessorResults().get(0).getId().equals("foobarId"));
-        assertTrue(searchResultsDTO.getProcessorResults().get(0).getParentGroup().getId().equals("thirdLevelAId"));
-        assertTrue(searchResultsDTO.getProcessorResults().get(0).getParentGroup().getName().equals("thirdLevelA"));
-        assertTrue(searchResultsDTO.getProcessorResults().get(0).getVersionedGroup() == null);
+    public void testSearchWhenProcessNodeIsNotAuthorized() {
+        // given
+        givenSingleProcessGroupIsSetUp();
+        givenSearchQueryIsSetUp();
+        givenProcessorIsNotAuthorized();
+        givenNoFilters();
+
+        // when
+        testSubject.search(searchQuery, results);
+
+        // then
+        thenProcessorMatcherIsNotCalled();
     }
 
     @Test
-    public void testSearchInThirdLevelParentNotAuthorizedNoVersionControl() {
-        // root level PG
-        final ProcessGroup rootProcessGroup = setupMockedProcessGroup("root", null, true, variableRegistry, null);
-
-        // first level PGs
-        final ProcessGroup firstLevelAProcessGroup = setupMockedProcessGroup("firstLevelA", rootProcessGroup, true, variableRegistry, null);
-        final ProcessGroup firstLevelBProcessGroup = setupMockedProcessGroup("firstLevelB", rootProcessGroup, true, variableRegistry, null);
-
-        // second level PGs
-        final ProcessGroup secondLevelAProcessGroup = setupMockedProcessGroup("secondLevelA", firstLevelAProcessGroup, true, variableRegistry, null);
-        final ProcessGroup secondLevelBProcessGroup = setupMockedProcessGroup("secondLevelB", firstLevelBProcessGroup, true, variableRegistry, null);
-        // third level PGs - not authorized
-        final ProcessGroup thirdLevelAProcessGroup = setupMockedProcessGroup("thirdLevelA", secondLevelAProcessGroup, false, variableRegistry, null);
-        final ProcessGroup thirdLevelBProcessGroup = setupMockedProcessGroup("thirdLevelB", secondLevelAProcessGroup, false, variableRegistry, null);
-
-        // link PGs together
-        Mockito.doReturn(new HashSet<ProcessGroup>() {
-            {
-                add(firstLevelAProcessGroup);
-                add(firstLevelBProcessGroup);
-            }
-        }).when(rootProcessGroup).getProcessGroups();
-
-        Mockito.doReturn(new HashSet<ProcessGroup>() {
-            {
-                add(secondLevelAProcessGroup);
-            }
-        }).when(firstLevelAProcessGroup).getProcessGroups();
-
-        Mockito.doReturn(new HashSet<ProcessGroup>() {
-            {
-                add(secondLevelBProcessGroup);
-            }
-        }).when(firstLevelBProcessGroup).getProcessGroups();
-
-        Mockito.doReturn(new HashSet<ProcessGroup>() {
-            {
-                add(thirdLevelAProcessGroup);
-                add(thirdLevelBProcessGroup);
-            }
-        }).when(secondLevelAProcessGroup).getProcessGroups();
-
-        // setup processor
-        setupMockedProcessor("foobar", thirdLevelAProcessGroup, true, variableRegistry);
-
-        // perform search
-        service.search(searchResultsDTO, "foo", rootProcessGroup);
-
-        assertTrue(searchResultsDTO.getProcessorResults().size() == 1);
-        assertTrue(searchResultsDTO.getProcessorResults().get(0).getId().equals("foobarId"));
-        assertTrue(searchResultsDTO.getProcessorResults().get(0).getParentGroup().getId().equals("thirdLevelAId"));
-        assertTrue(searchResultsDTO.getProcessorResults().get(0).getParentGroup().getName() == null);
-        assertTrue(searchResultsDTO.getProcessorResults().get(0).getVersionedGroup() == null);
+    public void testSearchWithHereFilterShowsActualGroupAndSubgroupsOnly() {
+        // given
+        givenProcessGroupsAreSetUp();
+        givenSearchQueryIsSetUp(processGroups.get(PROCESS_GROUP_FIRST_LEVEL_A));
+        givenScopeFilterIsSet();
+
+        // when
+        testSubject.search(searchQuery, results);
+
+        // then
+        thenFollowingGroupsAreSearched(Arrays.asList(
+                PROCESS_GROUP_FIRST_LEVEL_A,
+                PROCESS_GROUP_SECOND_LEVEL_A));
     }
 
     @Test
-    public void testSearchInThirdLevelParentNotAuthorizedWithVersionControl() {
-        // root level PG
-        final ProcessGroup rootProcessGroup = setupMockedProcessGroup("root", null, true, variableRegistry, null);
-
-        // first level PGs
-        final VersionControlInformation versionControlInformation = setupVC();
-        final ProcessGroup firstLevelAProcessGroup = setupMockedProcessGroup("firstLevelA", rootProcessGroup, true, variableRegistry, versionControlInformation);
-        final ProcessGroup firstLevelBProcessGroup = setupMockedProcessGroup("firstLevelB", rootProcessGroup, true, variableRegistry, null);
-
-        // second level PGs
-        final ProcessGroup secondLevelAProcessGroup = setupMockedProcessGroup("secondLevelA", firstLevelAProcessGroup, true, variableRegistry, null);
-        final ProcessGroup secondLevelBProcessGroup = setupMockedProcessGroup("secondLevelB", firstLevelBProcessGroup, true, variableRegistry, null);
-        // third level PGs - not authorized
-        final ProcessGroup thirdLevelAProcessGroup = setupMockedProcessGroup("thirdLevelA", secondLevelAProcessGroup, false, variableRegistry, null);
-        final ProcessGroup thirdLevelBProcessGroup = setupMockedProcessGroup("thirdLevelB", secondLevelAProcessGroup, false, variableRegistry, null);
-
-        // link PGs together
-        Mockito.doReturn(new HashSet<ProcessGroup>() {
-            {
-                add(firstLevelAProcessGroup);
-                add(firstLevelBProcessGroup);
-            }
-        }).when(rootProcessGroup).getProcessGroups();
-
-        Mockito.doReturn(new HashSet<ProcessGroup>() {
-            {
-                add(secondLevelAProcessGroup);
-            }
-        }).when(firstLevelAProcessGroup).getProcessGroups();
-
-        Mockito.doReturn(new HashSet<ProcessGroup>() {
-            {
-                add(secondLevelBProcessGroup);
-            }
-        }).when(firstLevelBProcessGroup).getProcessGroups();
-
-        Mockito.doReturn(new HashSet<ProcessGroup>() {
-            {
-                add(thirdLevelAProcessGroup);
-                add(thirdLevelBProcessGroup);
-            }
-        }).when(secondLevelAProcessGroup).getProcessGroups();
-
-        // setup processor
-        setupMockedProcessor("foobar", thirdLevelAProcessGroup, true, variableRegistry);
-
-        // perform search
-        service.search(searchResultsDTO, "foo", rootProcessGroup);
-
-        assertTrue(searchResultsDTO.getProcessorResults().size() == 1);
-        assertTrue(searchResultsDTO.getProcessorResults().get(0).getId().equals("foobarId"));
-        assertTrue(searchResultsDTO.getProcessorResults().get(0).getParentGroup().getId().equals("thirdLevelAId"));
-        assertTrue(searchResultsDTO.getProcessorResults().get(0).getParentGroup().getName() == null);
-        assertTrue(searchResultsDTO.getProcessorResults().get(0).getVersionedGroup() != null);
-        assertTrue(searchResultsDTO.getProcessorResults().get(0).getVersionedGroup().getId().equals("firstLevelAId"));
-        assertTrue(searchResultsDTO.getProcessorResults().get(0).getVersionedGroup().getName().equals("firstLevelA"));
+    public void testSearchWithHereFilterAndInRoot() {
+        // given
+        givenProcessGroupsAreSetUp();
+        givenSearchQueryIsSetUp();
+        givenScopeFilterIsSet();
+
+        // when
+        testSubject.search(searchQuery, results);
+
+        // then
+        thenFollowingGroupsAreSearched(Arrays.asList(
+                PROCESS_GROUP_FIRST_LEVEL_A,
+                PROCESS_GROUP_SECOND_LEVEL_A,
+                PROCESS_GROUP_FIRST_LEVEL_B,
+                PROCESS_GROUP_SECOND_LEVEL_B_1,
+                PROCESS_GROUP_SECOND_LEVEL_B_2));
+        thenContentOfTheFollowingGroupsAreSearched(processGroups.keySet());
     }
 
+
     @Test
-    public void testSearchInThirdLevelParentNotAuthorizedWithVersionControlInTheGroup() {
-        // root level PG
-        final ProcessGroup rootProcessGroup = setupMockedProcessGroup("root", null, true, variableRegistry, null);
-
-        // first level PGs
-        final ProcessGroup firstLevelAProcessGroup = setupMockedProcessGroup("firstLevelA", rootProcessGroup, true, variableRegistry, null);
-        final ProcessGroup firstLevelBProcessGroup = setupMockedProcessGroup("firstLevelB", rootProcessGroup, true, variableRegistry, null);
-
-        // second level PGs
-        final ProcessGroup secondLevelAProcessGroup = setupMockedProcessGroup("secondLevelA", firstLevelAProcessGroup, true, variableRegistry, null);
-        final ProcessGroup secondLevelBProcessGroup = setupMockedProcessGroup("secondLevelB", firstLevelBProcessGroup, true, variableRegistry, null);
-        // third level PGs - not authorized
-        final VersionControlInformation versionControlInformation = setupVC();
-        final ProcessGroup thirdLevelAProcessGroup = setupMockedProcessGroup("thirdLevelA", secondLevelAProcessGroup, false, variableRegistry, versionControlInformation);
-        final ProcessGroup thirdLevelBProcessGroup = setupMockedProcessGroup("thirdLevelB", secondLevelAProcessGroup, false, variableRegistry, null);
-
-        // link PGs together
-        Mockito.doReturn(new HashSet<ProcessGroup>() {
-            {
-                add(firstLevelAProcessGroup);
-                add(firstLevelBProcessGroup);
-            }
-        }).when(rootProcessGroup).getProcessGroups();
-
-        Mockito.doReturn(new HashSet<ProcessGroup>() {
-            {
-                add(secondLevelAProcessGroup);
-            }
-        }).when(firstLevelAProcessGroup).getProcessGroups();
-
-        Mockito.doReturn(new HashSet<ProcessGroup>() {
-            {
-                add(secondLevelBProcessGroup);
-            }
-        }).when(firstLevelBProcessGroup).getProcessGroups();
-
-        Mockito.doReturn(new HashSet<ProcessGroup>() {
-            {
-                add(thirdLevelAProcessGroup);
-                add(thirdLevelBProcessGroup);
-            }
-        }).when(secondLevelAProcessGroup).getProcessGroups();
-
-        // setup processor
-        setupMockedProcessor("foobar", thirdLevelAProcessGroup, true, variableRegistry);
-
-        // perform search
-        service.search(searchResultsDTO, "foo", rootProcessGroup);
-
-        assertTrue(searchResultsDTO.getProcessorResults().size() == 1);
-        assertTrue(searchResultsDTO.getProcessorResults().get(0).getId().equals("foobarId"));
-        assertTrue(searchResultsDTO.getProcessorResults().get(0).getParentGroup().getId().equals("thirdLevelAId"));
-        assertTrue(searchResultsDTO.getProcessorResults().get(0).getParentGroup().getName() == null);
-        assertTrue(searchResultsDTO.getProcessorResults().get(0).getVersionedGroup() != null);
-        assertTrue(searchResultsDTO.getProcessorResults().get(0).getVersionedGroup().getId().equals("thirdLevelAId"));
-        assertTrue(searchResultsDTO.getProcessorResults().get(0).getVersionedGroup().getName() == null);
+    public void testSearchWithGroupFilterShowsPointedGroupAndSubgroupsOnly() {
+        // given
+        givenProcessGroupsAreSetUp();
+        givenSearchQueryIsSetUp();
+        givenGroupFilterIsSet(PROCESS_GROUP_FIRST_LEVEL_B + "Name");
+
+        // when
+        testSubject.search(searchQuery, results);
+
+        // then
+        thenFollowingGroupsAreSearched(Arrays.asList( //
+                PROCESS_GROUP_FIRST_LEVEL_B, //
+                PROCESS_GROUP_SECOND_LEVEL_B_1, //
+                PROCESS_GROUP_SECOND_LEVEL_B_2));
     }
 
     @Test
-    public void testSearchParameterContext() {
-        final ParameterContext paramContext1 = setupMockedParameterContext("foo", "description for parameter context foo", 1, "foo_param", true);
-        final ParameterContext paramContext2 = setupMockedParameterContext("bar", "description for parameter context bar", 2, "bar_param", true);
-        final Set<ParameterContext> mockedParameterContexts = new HashSet<ParameterContext>();
-        mockedParameterContexts.add(paramContext1);
-        mockedParameterContexts.add(paramContext2);
+    public void testSearchGroupWithLowerCase() {
+        // given
+        givenProcessGroupsAreSetUp();
+        givenSearchQueryIsSetUp();
+        givenGroupFilterIsSet((PROCESS_GROUP_FIRST_LEVEL_B + "Name").toLowerCase());
+
+        // when
+        testSubject.search(searchQuery, results);
+
+        // then
+        thenFollowingGroupsAreSearched(Arrays.asList( //
+                PROCESS_GROUP_FIRST_LEVEL_B, //
+                PROCESS_GROUP_SECOND_LEVEL_B_1, //
+                PROCESS_GROUP_SECOND_LEVEL_B_2));
+    }
+
+    @Test
+    public void testSearchGroupWithPartialMatch() {
+        // given
+        givenProcessGroupsAreSetUp();
+        givenSearchQueryIsSetUp();
+        givenGroupFilterIsSet((PROCESS_GROUP_FIRST_LEVEL_B + "Na"));
+
+        // when
+        testSubject.search(searchQuery, results);
+
+        // then
+        thenFollowingGroupsAreSearched(Arrays.asList( //
+                PROCESS_GROUP_FIRST_LEVEL_B, //
+                PROCESS_GROUP_SECOND_LEVEL_B_1, //
+                PROCESS_GROUP_SECOND_LEVEL_B_2));
+    }
 
-        Mockito.doReturn(mockedParameterContexts).when(parameterContextManager).getParameterContexts();
+    @Test
+    public void testSearchGroupBasedOnIdentifier() {
+        // given
+        givenProcessGroupsAreSetUp();
+        givenSearchQueryIsSetUp();
+        givenGroupFilterIsSet((PROCESS_GROUP_FIRST_LEVEL_B + "Id"));
+
+        // when
+        testSubject.search(searchQuery, results);
+
+        // then
+        thenFollowingGroupsAreSearched(Arrays.asList( //
+                PROCESS_GROUP_FIRST_LEVEL_B, //
+                PROCESS_GROUP_SECOND_LEVEL_B_1, //
+                PROCESS_GROUP_SECOND_LEVEL_B_2));
+    }
 
-        service.searchParameters(searchResultsDTO, "foo");
+    @Test
+    public void testSearchWithGroupWhenRoot() {
+        // given
+        givenProcessGroupsAreSetUp();
+        givenSearchQueryIsSetUp();
+        givenGroupFilterIsSet(PROCESS_GROUP_ROOT + "Name");
+
+        // when
+        testSubject.search(searchQuery, results);
+
+        // then
+        thenFollowingGroupsAreSearched(Arrays.asList(
+                PROCESS_GROUP_FIRST_LEVEL_A,
+                PROCESS_GROUP_SECOND_LEVEL_A,
+                PROCESS_GROUP_FIRST_LEVEL_B,
+                PROCESS_GROUP_SECOND_LEVEL_B_1,
+                PROCESS_GROUP_SECOND_LEVEL_B_2));
+        thenContentOfTheFollowingGroupsAreSearched(processGroups.keySet());
+    }
 
-        assertEquals(1, searchResultsDTO.getParameterContextResults().size());
-        assertEquals("fooId", searchResultsDTO.getParameterContextResults().get(0).getId());
-        assertEquals("foo", searchResultsDTO.getParameterContextResults().get(0).getName());
-        // should have a match for the name, id, description
-        assertEquals(3, searchResultsDTO.getParameterContextResults().get(0).getMatches().size());
+    @Test
+    public void testSearchWithGroupWhenValueIsNonExisting() {
+        // given
+        givenProcessGroupsAreSetUp();
+        givenSearchQueryIsSetUp();
+        givenGroupFilterIsSet("Unknown");
 
-        assertEquals(1, searchResultsDTO.getParameterResults().size());
+        // when
+        testSubject.search(searchQuery, results);
 
-        assertEquals("fooId", searchResultsDTO.getParameterResults().get(0).getParentGroup().getId());
-        assertEquals("foo_param_0", searchResultsDTO.getParameterResults().get(0).getName());
-        // and the parameter name, parameter description, and the parameter value
-        assertEquals(3, searchResultsDTO.getParameterResults().get(0).getMatches().size());
+        // then
+        thenFollowingGroupsAreSearched(Arrays.asList());
     }
 
     @Test
-    public void testSearchParameterContextNotAuthorized() {
-        final ParameterContext paramContext1 = setupMockedParameterContext("foo", "description for parameter context foo", 1, "foo_param", false);
-        final ParameterContext paramContext2 = setupMockedParameterContext("bar", "description for parameter context bar", 2, "bar_param", true);
-        final Set<ParameterContext> mockedParameterContexts = new HashSet<ParameterContext>();
-        mockedParameterContexts.add(paramContext1);
-        mockedParameterContexts.add(paramContext2);
+    public void testWhenBothFiltersPresentAndScopeIsMoreRestricting() {
+        // given
+        givenProcessGroupsAreSetUp();
+        givenSearchQueryIsSetUp(processGroups.get(PROCESS_GROUP_SECOND_LEVEL_B_1));
+        givenScopeFilterIsSet();
+        givenGroupFilterIsSet(PROCESS_GROUP_FIRST_LEVEL_B + "Name");
+
+        // when
+        testSubject.search(searchQuery, results);
+
+        // then
+        thenFollowingGroupsAreSearched(Arrays.asList(PROCESS_GROUP_SECOND_LEVEL_B_1));
+    }
 
-        Mockito.doReturn(mockedParameterContexts).when(parameterContextManager).getParameterContexts();
+    @Test
+    public void testWhenBothFiltersPresentAndGroupIsMoreRestricting() {
+        // given
+        givenProcessGroupsAreSetUp();
+        givenSearchQueryIsSetUp(processGroups.get(PROCESS_GROUP_FIRST_LEVEL_B));
+        givenScopeFilterIsSet();
+        givenGroupFilterIsSet(PROCESS_GROUP_SECOND_LEVEL_B_1 + "Name");
+
+        // when
+        testSubject.search(searchQuery, results);
+
+        // then
+        thenFollowingGroupsAreSearched(Arrays.asList(PROCESS_GROUP_SECOND_LEVEL_B_1));
+    }
 
-        service.searchParameters(searchResultsDTO, "foo");
+    @Test
+    public void testWhenBothFiltersPresentTheyAreNotOverlapping() {
+        // given
+        givenProcessGroupsAreSetUp();
+        givenSearchQueryIsSetUp(processGroups.get(PROCESS_GROUP_FIRST_LEVEL_B));
+        givenScopeFilterIsSet();
+        givenGroupFilterIsSet(PROCESS_GROUP_FIRST_LEVEL_A + "Name");
+
+        // when
+        testSubject.search(searchQuery, results);
+
+        // then
+        thenFollowingGroupsAreSearched(Arrays.asList());
+    }
 
-        // the matching parameter context is not readable by the user, so there should not be any results
-        assertEquals(0, searchResultsDTO.getParameterContextResults().size());
-        assertEquals(0, searchResultsDTO.getParameterResults().size());
+    @Test
+    public void testSearchParameterContext() {
+        // given
+        givenSingleProcessGroupIsSetUp();
+        givenSearchQueryIsSetUp();
+        givenParameterSearchIsSetUp(true);
+
+        // when
+        testSubject.searchParameters(searchQuery, results);
+
+        // then
+        thenParameterComponentTypesAreChecked();
+        thenAllParameterComponentResultsAreCollected();
     }
 
     @Test
-    public void testSearchLabels() {
-        // root level PG
-        final ProcessGroup rootProcessGroup = setupMockedProcessGroup("root", null, true, variableRegistry, null);
+    public void testSearchParameterContextWhenNotAuthorized() {
+        // given
+        givenSingleProcessGroupIsSetUp();
+        givenSearchQueryIsSetUp();
+        givenParameterSearchIsSetUp(false);
 
-        // setup labels
-        setupMockedLabels(rootProcessGroup);
+        // when
+        testSubject.searchParameters(searchQuery, results);
 
-        // perform search for foo
-        service.search(searchResultsDTO, "FOO", rootProcessGroup);
+        // then
+        thenParameterSpecificComponentTypesAreNotChecked();
+    }
 
-        assertTrue(searchResultsDTO.getLabelResults().size() == 1);
-        assertTrue(searchResultsDTO.getLabelResults().get(0).getId().equals("foo"));
-        assertTrue(searchResultsDTO.getLabelResults().get(0).getName().equals("Value for label foo"));
+    private ControllerSearchService givenTestSubject() {
+        final ControllerSearchService result = new ControllerSearchService();
+        result.setAuthorizer(authorizer);
+        result.setFlowController(flowController);
+        result.setMatcherForProcessor(matcherForProcessor);
+        result.setMatcherForProcessGroup(matcherForProcessGroup);
+        result.setMatcherForConnection(matcherForConnection);
+        result.setMatcherForRemoteProcessGroup(matcherForRemoteProcessGroup);
+        result.setMatcherForPort(matcherForPort);
+        result.setMatcherForFunnel(matcherForFunnel);
+        result.setMatcherForParameterContext(matcherForParameterContext);
+        result.setMatcherForParameter(matcherForParameter);
+        result.setMatcherForLabel(matcherForLabel);
+        result.setResultEnricherFactory(resultEnricherFactory);
+        return result;
     }
 
-    @Test
-    public void testSearchControllerServices() {
-        final ProcessGroup rootProcessGroup = setupMockedProcessGroup("root", null, true, variableRegistry, null);
-
-        final String controllerServiceName = "controllerServiceName";
-        final String controllerServiceId = controllerServiceName + "Id";
-
-        final Map<PropertyDescriptor, String> props = new HashMap<>();
-        final PropertyDescriptor prop1 = new PropertyDescriptor.Builder()
-            .name("prop1-name")
-            .displayName("prop1-displayname")
-            .description("prop1 description")
-            .defaultValue("prop1-default")
-            .build();
-            final PropertyDescriptor prop2 = new PropertyDescriptor.Builder()
-            .name("prop2-name")
-            .displayName("prop2-displayname")
-            .description("prop2 description")
-            .defaultValue("prop2-default")
-            .build();
-        props.put(prop1, "prop1-value");
-        props.put(prop2, null);
-
-        setupMockedControllerService(controllerServiceName, rootProcessGroup, true, props);
-
-        // search for name
-        service.search(searchResultsDTO, "controllerserv", rootProcessGroup);
-
-        assertEquals(1, searchResultsDTO.getControllerServiceNodeResults().size());
-        assertEquals(controllerServiceId, searchResultsDTO.getControllerServiceNodeResults().get(0).getId());
-        assertEquals(controllerServiceName, searchResultsDTO.getControllerServiceNodeResults().get(0).getName());
-
-        // search for comments
-        searchResultsDTO = new SearchResultsDTO();
-        service.search(searchResultsDTO, "foo comment", rootProcessGroup);
-
-        assertEquals(1, searchResultsDTO.getControllerServiceNodeResults().size());
-        assertEquals(controllerServiceId, searchResultsDTO.getControllerServiceNodeResults().get(0).getId());
-        assertEquals(controllerServiceName, searchResultsDTO.getControllerServiceNodeResults().get(0).getName());
-
-        // search for properties
-        searchResultsDTO = new SearchResultsDTO();
-        service.search(searchResultsDTO, "prop1-name", rootProcessGroup);
-
-        assertEquals(1, searchResultsDTO.getControllerServiceNodeResults().size());
-        assertEquals(controllerServiceId, searchResultsDTO.getControllerServiceNodeResults().get(0).getId());
-        assertEquals(controllerServiceName, searchResultsDTO.getControllerServiceNodeResults().get(0).getName());
-
-        // by default
-        searchResultsDTO = new SearchResultsDTO();
-        service.search(searchResultsDTO, "prop2-def", rootProcessGroup);
-
-        assertEquals(1, searchResultsDTO.getControllerServiceNodeResults().size());
-        assertEquals(controllerServiceId, searchResultsDTO.getControllerServiceNodeResults().get(0).getId());
-        assertEquals(controllerServiceName, searchResultsDTO.getControllerServiceNodeResults().get(0).getName());
-
-        // by description
-        searchResultsDTO = new SearchResultsDTO();
-        service.search(searchResultsDTO, "desc", rootProcessGroup);
-
-        // "desc" would typically match both props, but it's for the same controller service.
-        assertEquals(1, searchResultsDTO.getControllerServiceNodeResults().size());
-        assertEquals(controllerServiceId, searchResultsDTO.getControllerServiceNodeResults().get(0).getId());
-        assertEquals(controllerServiceName, searchResultsDTO.getControllerServiceNodeResults().get(0).getName());
-
-        // by specified value
-        searchResultsDTO = new SearchResultsDTO();
-        service.search(searchResultsDTO, "prop1-value", rootProcessGroup);
-
-        assertEquals(1, searchResultsDTO.getControllerServiceNodeResults().size());
-        assertEquals(controllerServiceId, searchResultsDTO.getControllerServiceNodeResults().get(0).getId());
-        assertEquals(controllerServiceName, searchResultsDTO.getControllerServiceNodeResults().get(0).getName());
-
-        // search finding no match
-        searchResultsDTO = new SearchResultsDTO();
-        service.search(searchResultsDTO, "ZZZZZZZZZYYYYYY", rootProcessGroup);
-
-        assertEquals(0, searchResultsDTO.getControllerServiceNodeResults().size());
-    }
-
-    /**
-     * Mocks Labels including isAuthorized() and their identifier and value
-     *
-     * @param containingProcessGroup The process group
-     */
-    private static void setupMockedLabels(final ProcessGroup containingProcessGroup) {
-        final Label label1 = mock(Label.class);
-        Mockito.doReturn(true).when(label1).isAuthorized(AdditionalMatchers.or(any(Authorizer.class), isNull()), eq(RequestAction.READ),
-                AdditionalMatchers.or(any(NiFiUser.class), isNull()));
-        Mockito.doReturn("foo").when(label1).getIdentifier();
-        Mockito.doReturn("Value for label foo").when(label1).getValue();
-
-        final Label label2 = mock(Label.class);
-        Mockito.doReturn(false).when(label2).isAuthorized(AdditionalMatchers.or(any(Authorizer.class), isNull()), eq(RequestAction.READ),
-                AdditionalMatchers.or(any(NiFiUser.class), isNull()));
-        Mockito.doReturn("bar").when(label2).getIdentifier();
-        Mockito.doReturn("Value for label bar, but FOO is in here too").when(label2).getValue();
-
-        // assign labels to the PG
-        Mockito.doReturn(new HashSet<Label>() {
-            {
-                add(label1);
-                add(label2);
-            }
-        }).when(containingProcessGroup).getLabels();
-    }
-
-    /**
-     * Sets up a mock Parameter Context including isAuthorized()
-     * @param name                     name of the parameter context
-     * @param description              description of the parameter context
-     * @param numberOfParams           number of parameters to include as part of this context
-     * @param parameterNamePrefix      a prefix for the parameter names
-     * @param authorizedToRead         whether or not the user can read the parameter context
-     * @return ParameterContext
-     */
-    private ParameterContext setupMockedParameterContext(String name, String description, int numberOfParams, String parameterNamePrefix, boolean authorizedToRead) {
-        final ParameterContext parameterContext = mock(ParameterContext.class);
-        Mockito.doReturn(name + "Id").when(parameterContext).getIdentifier();
-        Mockito.doReturn(name).when(parameterContext).getName();
-        Mockito.doReturn(description).when(parameterContext).getDescription();
-
-        Mockito.doReturn(authorizedToRead).when(parameterContext).isAuthorized(AdditionalMatchers.or(any(Authorizer.class), isNull()), eq(RequestAction.READ),
-                AdditionalMatchers.or(any(NiFiUser.class), isNull()));
-
-        Map<ParameterDescriptor, Parameter> parameters = new HashMap<>();
-        for (int i = 0; i < numberOfParams; i++) {
-            final ParameterDescriptor descriptor = new ParameterDescriptor.Builder()
-                    .name(parameterNamePrefix + "_" + i)
-                    .description("Description for " + parameterNamePrefix + "_" + i)
-                    .sensitive(false)
-                    .build();
-
-            final Parameter param = new Parameter(descriptor, parameterNamePrefix + "_" + i + " value");
-            parameters.put(descriptor, param);
+    private void givenSingleProcessGroupIsSetUp() {
+        final ProcessGroup root = givenProcessGroup(PROCESS_GROUP_ROOT, true, Collections.emptySet(), Collections.emptySet());
+
+        final ProcessorNode processorNode = Mockito.mock(ProcessorNode.class);
+        Mockito.when(processorNode.isAuthorized(authorizer, RequestAction.READ, user)).thenReturn(true);
+        Mockito.when(root.getProcessors()).thenReturn(Collections.singletonList(processorNode));
+
+        final Connection connection = Mockito.mock(Connection.class);
+        Mockito.when(connection.isAuthorized(authorizer, RequestAction.READ, user)).thenReturn(true);
+        Mockito.when(root.getConnections()).thenReturn(new HashSet<>(Arrays.asList(connection)));
+
+        final RemoteProcessGroup remoteProcessGroup = Mockito.mock(RemoteProcessGroup.class);
+        Mockito.when(remoteProcessGroup.isAuthorized(authorizer, RequestAction.READ, user)).thenReturn(true);
+        Mockito.when(root.getRemoteProcessGroups()).thenReturn(new HashSet<>(Arrays.asList(remoteProcessGroup)));
+
+        final Port port = Mockito.mock(Port.class);
+        Mockito.when(port.isAuthorized(authorizer, RequestAction.READ, user)).thenReturn(true);
+        Mockito.when(root.getInputPorts()).thenReturn(new HashSet<>(Arrays.asList(port)));
+        Mockito.when(root.getOutputPorts()).thenReturn(new HashSet<>(Arrays.asList(port)));
+
+        final Funnel funnel = Mockito.mock(Funnel.class);
+        Mockito.when(funnel.isAuthorized(authorizer, RequestAction.READ, user)).thenReturn(true);
+        Mockito.when(root.getFunnels()).thenReturn(new HashSet<>(Arrays.asList(funnel)));
+
+        final Label label = Mockito.mock(Label.class);
+        Mockito.when(label.isAuthorized(authorizer, RequestAction.READ, user)).thenReturn(true);
+        Mockito.when(root.getLabels()).thenReturn(new HashSet<>(Arrays.asList(label)));
+    }
+
+    private void givenProcessGroupsAreSetUp() {
+        final ProcessGroup secondLevelAProcessGroup = givenProcessGroup(PROCESS_GROUP_SECOND_LEVEL_A, true, Collections.emptySet(), Collections.emptySet());
+        final ProcessGroup secondLevelB1ProcessGroup = givenProcessGroup(PROCESS_GROUP_SECOND_LEVEL_B_1, true, Collections.emptySet(), Collections.emptySet());
+        final ProcessGroup secondLevelB2ProcessGroup = givenProcessGroup(PROCESS_GROUP_SECOND_LEVEL_B_2, true, Collections.emptySet(), Collections.emptySet());
+
+        final ProcessGroup firstLevelAProcessGroup = givenProcessGroup(PROCESS_GROUP_FIRST_LEVEL_A, //
+                true, Collections.emptySet(), Collections.singleton(secondLevelAProcessGroup));
+        final ProcessGroup firstLevelBProcessGroup = givenProcessGroup(PROCESS_GROUP_FIRST_LEVEL_B, //
+                true, Collections.emptySet(), new HashSet<>(Arrays.asList(secondLevelB1ProcessGroup, secondLevelB2ProcessGroup)));
+
+        final ProcessGroup root =  givenProcessGroup(PROCESS_GROUP_ROOT, //
+                true, Collections.emptySet(), new HashSet<>(Arrays.asList(firstLevelAProcessGroup, firstLevelBProcessGroup)));
+    }
+
+    private void givenSearchQueryIsSetUp() {
+        givenSearchQueryIsSetUp(processGroups.get(PROCESS_GROUP_ROOT));
+    }
+
+    private void givenSearchQueryIsSetUp(final ProcessGroup activeProcessGroup) {
+        Mockito.when(searchQuery.getUser()).thenReturn(user);
+        Mockito.when(searchQuery.getRootGroup()).thenReturn(processGroups.get(PROCESS_GROUP_ROOT));
+        Mockito.when(searchQuery.getActiveGroup()).thenReturn(activeProcessGroup);
+    }
+
+    private ProcessGroup givenProcessGroup( //
+            final String identifier, //
+            final boolean isAuthorized, //
+            final Set<ProcessorNode> processors, //
+            final Set<ProcessGroup> children) {
+        final ProcessGroup result = Mockito.mock(ProcessGroup.class);
+        final Funnel funnel = Mockito.mock(Funnel.class); // This is for testing if group content was searched
+        Mockito.when(funnel.isAuthorized(authorizer, RequestAction.READ, user)).thenReturn(isAuthorized);
+
+        Mockito.when(result.getName()).thenReturn(identifier + "Name");
+        Mockito.when(result.getIdentifier()).thenReturn(identifier + "Id");
+        Mockito.when(result.isAuthorized(authorizer, RequestAction.READ, user)).thenReturn(isAuthorized);
+
+        Mockito.when(result.getProcessGroups()).thenReturn(children);
+        Mockito.when(result.getProcessors()).thenReturn(processors);
+        Mockito.when(result.getConnections()).thenReturn(Collections.emptySet());
+        Mockito.when(result.getRemoteProcessGroups()).thenReturn(Collections.emptySet());
+        Mockito.when(result.getInputPorts()).thenReturn(Collections.emptySet());
+        Mockito.when(result.getOutputPorts()).thenReturn(Collections.emptySet());
+        Mockito.when(result.getFunnels()).thenReturn(Collections.singleton(funnel));
+        Mockito.when(result.getLabels()).thenReturn(Collections.emptySet());
+
+        children.forEach(child -> Mockito.when(child.getParent()).thenReturn(result));
+        processGroups.put(identifier, result);
+
+        return result;
+    }
+
+    private void givenProcessGroupIsNotAutorized(final String processGroupName) {
+        Mockito.when(processGroups.get(processGroupName).isAuthorized(authorizer, RequestAction.READ, user)).thenReturn(false);
+    }
+
+    private void givenNoFilters() {
+        Mockito.when(searchQuery.hasFilter(Mockito.anyString())).thenReturn(false);
+    }
+
+    private void givenScopeFilterIsSet() {
+        Mockito.when(searchQuery.hasFilter("scope")).thenReturn(true);
+        Mockito.when(searchQuery.getFilter("scope")).thenReturn("here");
+    }
+
+    private void givenGroupFilterIsSet(final String group) {
+        Mockito.when(searchQuery.hasFilter("group")).thenReturn(true);
+        Mockito.when(searchQuery.getFilter("group")).thenReturn(group);
+    }
+
+    private void givenProcessorIsNotAuthorized() {
+        final ProcessorNode processor = processGroups.get(PROCESS_GROUP_ROOT).getProcessors().iterator().next();
+        Mockito.when(processor.isAuthorized(authorizer, RequestAction.READ, user)).thenReturn(false);
+    }
+
+    private void givenParameterSearchIsSetUp(boolean isAuthorized) {
+        final ParameterContext parameterContext = Mockito.mock(ParameterContext.class);
+        final Parameter parameter = Mockito.mock(Parameter.class);
+        final ParameterDescriptor descriptor = Mockito.mock(ParameterDescriptor.class);
+        final Map<ParameterDescriptor, Parameter> parameters = new HashMap<>();
+        parameters.put(descriptor, parameter);
+        Mockito.when(flowController.getFlowManager()).thenReturn(flowManager);
+        Mockito.when(flowManager.getParameterContextManager()).thenReturn(parameterContextManager);
+        Mockito.when(parameterContextManager.getParameterContexts()).thenReturn(new HashSet<>(Arrays.asList(parameterContext)));
+        Mockito.when(parameterContext.getParameters()).thenReturn(parameters);
+        Mockito.when(parameterContext.isAuthorized(authorizer, RequestAction.READ, user)).thenReturn(isAuthorized);
+    }
+
+    private void thenProcessorMatcherIsNotCalled() {
+        final ProcessorNode processor = processGroups.get(PROCESS_GROUP_ROOT).getProcessors().iterator().next();
+        Mockito.verify(matcherForProcessor, Mockito.never()).match(processor, searchQuery);
+    }
+
+    private void thenAllComponentTypeIsChecked() {
+        Mockito.verify(matcherForProcessor, Mockito.times(1)).match(Mockito.any(ProcessorNode.class), Mockito.any(SearchQuery.class));
+        Mockito.verify(matcherForConnection, Mockito.times(1)).match(Mockito.any(Connection.class), Mockito.any(SearchQuery.class));
+        Mockito.verify(matcherForRemoteProcessGroup, Mockito.times(1)).match(Mockito.any(RemoteProcessGroup.class), Mockito.any(SearchQuery.class));
+        // Port needs to be used multiple times as input and output ports are handled separately
+        Mockito.verify(matcherForPort, Mockito.times(2)).match(Mockito.any(Port.class), Mockito.any(SearchQuery.class));
+        Mockito.verify(matcherForFunnel, Mockito.times(1)).match(Mockito.any(Funnel.class), Mockito.any(SearchQuery.class));
+        Mockito.verify(matcherForLabel, Mockito.times(1)).match(Mockito.any(Label.class), Mockito.any(SearchQuery.class));
+    }
+
+    private void thenAllComponentResultsAreCollected() {
+        Assert.assertEquals(1, results.getProcessorResults().size());
+        Assert.assertEquals(1, results.getConnectionResults().size());
+        Assert.assertEquals(1, results.getRemoteProcessGroupResults().size());
+        Assert.assertEquals(1, results.getInputPortResults().size());
+        Assert.assertEquals(1, results.getOutputPortResults().size());
+        Assert.assertEquals(1, results.getFunnelResults().size());
+        Assert.assertEquals(1, results.getLabelResults().size());
+        Assert.assertTrue(results.getParameterContextResults().isEmpty());
+        Assert.assertTrue(results.getParameterResults().isEmpty());
+    }
+
+    private void thenParameterComponentTypesAreChecked() {
+        Mockito.verify(matcherForParameterContext, Mockito.times(1)).match(Mockito.any(ParameterContext.class), Mockito.any(SearchQuery.class));
+        Mockito.verify(matcherForParameter, Mockito.times(1)).match(Mockito.any(Parameter.class), Mockito.any(SearchQuery.class));
+    }
+
+    private void thenAllParameterComponentResultsAreCollected() {
+        Assert.assertTrue(results.getProcessGroupResults().isEmpty());
+        Assert.assertTrue(results.getProcessorResults().isEmpty());
+        Assert.assertTrue(results.getConnectionResults().isEmpty());
+        Assert.assertTrue(results.getRemoteProcessGroupResults().isEmpty());
+        Assert.assertTrue(results.getInputPortResults().isEmpty());
+        Assert.assertTrue(results.getOutputPortResults().isEmpty());
+        Assert.assertTrue(results.getFunnelResults().isEmpty());
+        Assert.assertTrue(results.getLabelResults().isEmpty());
+        Assert.assertEquals(1, results.getParameterContextResults().size());
+        Assert.assertEquals(1, results.getParameterResults().size());
+    }
+
+    private void thenParameterSpecificComponentTypesAreNotChecked() {
+        Mockito.verify(matcherForParameterContext, Mockito.never()).match(Mockito.any(ParameterContext.class), Mockito.any(SearchQuery.class));
+        Mockito.verify(matcherForParameter, Mockito.never()).match(Mockito.any(Parameter.class), Mockito.any(SearchQuery.class));
+    }
+
+    private void thenFollowingGroupsAreSearched(final Collection<String> searchedProcessGroups) {
+        for (final String processGroup : searchedProcessGroups) {
+            Mockito.verify(matcherForProcessGroup, Mockito.times(1)).match(processGroups.get(processGroup), searchQuery);
         }
 
-        Mockito.doReturn(parameters).when(parameterContext).getParameters();
-
-        return parameterContext;
-    }
-
-    /**
-     * Mocks Processor including isAuthorized() and its name & id.
-     *
-     * @param processorName          Desired processor name
-     * @param containingProcessGroup The process group
-     * @param authorizedToRead       Can the processor data be read?
-     * @param variableRegistry       The variable registry
-     */
-    private static void setupMockedProcessor(final String processorName, final ProcessGroup containingProcessGroup, boolean authorizedToRead, final MutableVariableRegistry variableRegistry) {
-        final String processorId = processorName + "Id";
-        final Processor processor1 = mock(Processor.class);
-
-        final ProcessorNode processorNode1 = mock(StandardProcessorNode.class);
-        Mockito.doReturn(authorizedToRead).when(processorNode1).isAuthorized(AdditionalMatchers.or(any(Authorizer.class), isNull()), eq(RequestAction.READ),
-                AdditionalMatchers.or(any(NiFiUser.class), isNull()));
-        Mockito.doReturn(variableRegistry).when(processorNode1).getVariableRegistry();
-        Mockito.doReturn(processor1).when(processorNode1).getProcessor();
-        // set processor node's attributes
-        Mockito.doReturn(processorId).when(processorNode1).getIdentifier();
-        Mockito.doReturn(Optional.ofNullable(null)).when(processorNode1).getVersionedComponentId(); // not actually searching based on versioned component id
-        Mockito.doReturn(processorName).when(processorNode1).getName();
-
-        // assign processor node to its PG
-        Mockito.doReturn(new HashSet<ProcessorNode>() {
-            {
-                add(processorNode1);
-            }
-        }).when(containingProcessGroup).getProcessors();
-    }
-
-    private static void setupMockedControllerService(final String controllerServiceName, final ProcessGroup containingProcessGroup, boolean authorizedToRead,
-        Map<PropertyDescriptor, String> properties) {
-        final String controllerServiceId = controllerServiceName + "Id";
-        final ControllerService controllerService = mock(ControllerService.class);
-
-        final ControllerServiceNode controllerServiceNode1 = mock(StandardControllerServiceNode.class);
-        Mockito.doReturn(authorizedToRead).when(controllerServiceNode1)
-                .isAuthorized(AdditionalMatchers.or(any(Authorizer.class), isNull()), eq(RequestAction.READ), AdditionalMatchers.or(any(NiFiUser.class), isNull()));
-        Mockito.doReturn(controllerService).when(controllerServiceNode1).getControllerServiceImplementation();
-        // set controller service node attributes
-        Mockito.doReturn(controllerServiceId).when(controllerServiceNode1).getIdentifier();
-        Mockito.doReturn(controllerServiceName).when(controllerServiceNode1).getName();
-        Mockito.doReturn(Optional.ofNullable(null)).when(controllerServiceNode1).getVersionedComponentId();
-        Mockito.doReturn("foo comments").when(controllerServiceNode1).getComments();
-
-        //set properties
-        Mockito.doReturn(properties).when(controllerServiceNode1).getRawPropertyValues();
-
-        // assign controller service node to its PG
-        Mockito.doReturn(new HashSet<ControllerServiceNode>() {
-            {
-                add(controllerServiceNode1);
-            }
-        }).when(containingProcessGroup).getControllerServices(anyBoolean());
-    }
-
-    /**
-     * Mocks ProcessGroup due to isAuthorized(). The final class StandardProcessGroup can't be used.
-     *
-     * @param processGroupName Desired process group name
-     * @param parent           The parent process group
-     * @param authorizedToRead Can the process group data be read?
-     * @param variableRegistry The variable registry
-     * @param versionControlInformation The version control information
-     * @return Mocked process group
-     */
-    private static ProcessGroup setupMockedProcessGroup(final String processGroupName, final ProcessGroup parent, boolean authorizedToRead, final VariableRegistry variableRegistry,
-                                                        final VersionControlInformation versionControlInformation) {
-        final String processGroupId = processGroupName + "Id";
-        final ProcessGroup processGroup = mock(ProcessGroup.class);
-
-        Mockito.doReturn(processGroupId).when(processGroup).getIdentifier();
-        Mockito.doReturn(Optional.ofNullable(null)).when(processGroup).getVersionedComponentId(); // not actually searching based on versioned component id
-        Mockito.doReturn(processGroupName).when(processGroup).getName();
-        Mockito.doReturn(parent).when(processGroup).getParent();
-        Mockito.doReturn(versionControlInformation).when(processGroup).getVersionControlInformation();
-        Mockito.doReturn(variableRegistry).when(processGroup).getVariableRegistry();
-        Mockito.doReturn(parent == null).when(processGroup).isRootGroup();
-        // override process group's access rights
-        Mockito.doReturn(authorizedToRead).when(processGroup).isAuthorized(AdditionalMatchers.or(any(Authorizer.class), isNull()), eq(RequestAction.READ),
-                AdditionalMatchers.or(any(NiFiUser.class), isNull()));
-
-        return processGroup;
-    }
-
-    /**
-     * Creates a version control information using dummy attributes.
-     *
-     * @return Dummy version control information
-     */
-    private static VersionControlInformation setupVC() {
-        final StandardVersionControlInformation.Builder builder = new StandardVersionControlInformation.Builder();
-        builder.registryId("regId").bucketId("bucId").flowId("flowId").version(1);
-
-        return builder.build();
-    }
-}
+        Mockito.verifyNoMoreInteractions(matcherForProcessGroup);
+    }
+
+    private void thenContentOfTheFollowingGroupsAreSearched(final Collection<String> searchedProcessGroupIds) {
+        for (final String processGroupId : searchedProcessGroupIds) {
+            // Checking on funnels is arbitrary, any given component we expect to be searched would be a good candidate
+            final ProcessGroup processGroup = processGroups.get(processGroupId);
+            final Funnel funnel = processGroup.getFunnels().iterator().next();
+            Mockito.verify(matcherForFunnel, Mockito.times(1)).match(funnel, searchQuery);
+        }
+
+        Mockito.verifyNoMoreInteractions(matcherForFunnel);
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/SearchResultMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/SearchResultMatcher.java
new file mode 100644
index 0000000..58d0477
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/controller/SearchResultMatcher.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.controller;
+
+import org.apache.nifi.web.api.dto.search.ComponentSearchResultDTO;
+import org.apache.nifi.web.api.dto.search.SearchResultsDTO;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+
+public class SearchResultMatcher {
+    private final Set<ComponentSearchResultDTO> outputPortResults = new HashSet<>();
+    private final Set<ComponentSearchResultDTO> inputPortResults = new HashSet<>();
+    private final Set<ComponentSearchResultDTO> processorResults = new HashSet<>();
+    private final Set<ComponentSearchResultDTO> labelResults = new HashSet<>();
+    private final Set<ComponentSearchResultDTO> remoteProcessGroupResults = new HashSet<>();
+    private final Set<ComponentSearchResultDTO> funnelResults = new HashSet<>();
+    private final Set<ComponentSearchResultDTO> connectionResults = new HashSet<>();
+    private final Set<ComponentSearchResultDTO> processGroupResults = new HashSet<>();
+    private final Set<ComponentSearchResultDTO> parameterContextResults = new HashSet<>();
+    private final Set<ComponentSearchResultDTO> parameterResults = new HashSet<>();
+    private final Set<ComponentSearchResultDTO> controllerServiceNodeResults = new HashSet<>();
+
+    public SearchResultMatcher ofOutputPort(final ComponentSearchResultDTO result) {
+        outputPortResults.add(result);
+        return this;
+    }
+
+    public SearchResultMatcher ofInputPort(final ComponentSearchResultDTO result) {
+        inputPortResults.add(result);
+        return this;
+    }
+
+    public SearchResultMatcher ofProcessor(final ComponentSearchResultDTO result) {
+        processorResults.add(result);
+        return this;
+    }
+
+    public SearchResultMatcher ofLabel(final ComponentSearchResultDTO result) {
+        labelResults.add(result);
+        return this;
+    }
+
+    public SearchResultMatcher ofRemoteProcessGroup(final ComponentSearchResultDTO result) {
+        remoteProcessGroupResults.add(result);
+        return this;
+    }
+
+    public SearchResultMatcher ofFunnel(final ComponentSearchResultDTO result) {
+        funnelResults.add(result);
+        return this;
+    }
+
+    public SearchResultMatcher ofConnection(final ComponentSearchResultDTO result) {
+        connectionResults.add(result);
+        return this;
+    }
+
+    public SearchResultMatcher ofProcessGroup(final ComponentSearchResultDTO result) {
+        processGroupResults.add(result);
+        return this;
+    }
+
+    public SearchResultMatcher ofParameterContext(final ComponentSearchResultDTO result) {
+        parameterContextResults.add(result);
+        return this;
+    }
+
+    public SearchResultMatcher ofParameter(final ComponentSearchResultDTO result) {
+        parameterResults.add(result);
+        return this;
+    }
+
+    public SearchResultMatcher ofParameter(final Collection<ComponentSearchResultDTO> result) {
+        parameterResults.addAll(result);
+        return this;
+    }
+
+    public SearchResultMatcher ofControllerServiceNode(final ComponentSearchResultDTO result) {
+        controllerServiceNodeResults.add(result);
+        return this;
+    }
+
+    public void validate(final SearchResultsDTO results) {
+        validate(outputPortResults, results.getOutputPortResults());
+        validate(inputPortResults, results.getInputPortResults());
+        validate(processorResults, results.getProcessorResults());
+        validate(labelResults, results.getLabelResults());
+        validate(remoteProcessGroupResults, results.getRemoteProcessGroupResults());
+        validate(funnelResults, results.getFunnelResults());
+        validate(connectionResults, results.getConnectionResults());
+        validate(processGroupResults, results.getProcessGroupResults());
+        validate(parameterContextResults, results.getParameterContextResults());
+        validate(parameterResults, results.getParameterResults());
+        validate(controllerServiceNodeResults, results.getControllerServiceNodeResults());
+    }
+
+    private void validate(final Collection<ComponentSearchResultDTO> expected, final Collection<ComponentSearchResultDTO> actual) {
+        Set<AbstractControllerSearchIntegrationTest.ComponentSearchResultDTOWrapper> expectedConverted
+                = expected.stream().map(AbstractControllerSearchIntegrationTest.ComponentSearchResultDTOWrapper::new).collect(Collectors.toSet());
+        Set<AbstractControllerSearchIntegrationTest.ComponentSearchResultDTOWrapper> actualConverted
+                = actual.stream().map(AbstractControllerSearchIntegrationTest.ComponentSearchResultDTOWrapper::new).collect(Collectors.toSet());
+
+        assertEquals(expectedConverted, actualConverted);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/AttributeBasedComponentMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/AttributeBasedComponentMatcherTest.java
new file mode 100644
index 0000000..e44ba01
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/AttributeBasedComponentMatcherTest.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search;
+
+import org.apache.nifi.controller.ProcessorNode;
+import org.apache.nifi.web.api.dto.search.ComponentSearchResultDTO;
+import org.apache.nifi.web.search.attributematchers.AttributeMatcher;
+import org.apache.nifi.web.search.query.SearchQuery;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+
+@RunWith(MockitoJUnitRunner.class)
+public class AttributeBasedComponentMatcherTest {
+    private static final String IDENTIFIER = "lorem";
+    private static final String NAME = "ipsum";
+
+    @Mock
+    private ProcessorNode component;
+
+    @Mock
+    private SearchQuery searchQuery;
+
+    @Mock
+    private AttributeMatcher<ProcessorNode> attributeMatcher1;
+
+    @Mock
+    private AttributeMatcher<ProcessorNode> attributeMatcher2;
+
+    @Mock
+    private Function<ProcessorNode, String> getIdentifier;
+
+    @Mock
+    private Function<ProcessorNode, String> getName;
+
+    @Before
+    public void setUp() {
+        Mockito.when(getIdentifier.apply(Mockito.any(ProcessorNode.class))).thenReturn(IDENTIFIER);
+        Mockito.when(getName.apply(Mockito.any(ProcessorNode.class))).thenReturn(NAME);
+    }
+
+    @Test
+    public void testMatching() {
+        // given
+        final AttributeBasedComponentMatcher<ProcessorNode> testSubject = new AttributeBasedComponentMatcher<>(givenAttributeMatchers(), getIdentifier, getName);
+        givenAttributesAreMatching();
+
+        // when
+        final Optional<ComponentSearchResultDTO> result = testSubject.match(component, searchQuery);
+
+        // then
+        Assert.assertTrue(result.isPresent());
+        Assert.assertEquals(IDENTIFIER, result.get().getId());
+        Assert.assertEquals(NAME, result.get().getName());
+        Assert.assertEquals(2, result.get().getMatches().size());
+        Assert.assertTrue(result.get().getMatches().contains("matcher1"));
+        Assert.assertTrue(result.get().getMatches().contains("matcher2"));
+
+        Mockito.verify(attributeMatcher1, Mockito.atLeastOnce()).match(Mockito.any(ProcessorNode.class), Mockito.any(SearchQuery.class), Mockito.anyList());
+        Mockito.verify(attributeMatcher2, Mockito.atLeastOnce()).match(Mockito.any(ProcessorNode.class), Mockito.any(SearchQuery.class), Mockito.anyList());
+    }
+
+    @Test
+    public void testNotMatching() {
+        // given
+        final AttributeBasedComponentMatcher<ProcessorNode> testSubject = new AttributeBasedComponentMatcher<>(givenAttributeMatchers(), getIdentifier, getName);
+        givenAttributesAreNotMatching();
+
+        // when
+        final Optional<ComponentSearchResultDTO> result = testSubject.match(component, searchQuery);
+
+        // then
+        Assert.assertFalse(result.isPresent());
+    }
+
+    private List<AttributeMatcher<ProcessorNode>> givenAttributeMatchers() {
+        final List<AttributeMatcher<ProcessorNode>> result = new ArrayList<>();
+        result.add(attributeMatcher1);
+        result.add(attributeMatcher2);
+        return result;
+    }
+
+    private void givenAttributesAreMatching() {
+        Mockito.doAnswer(invocationOnMock -> {
+            final List<String> accumulator = invocationOnMock.getArgument(2, List.class);
+            accumulator.add("matcher1");
+            return accumulator;
+        }).when(attributeMatcher1).match(Mockito.any(ProcessorNode.class), Mockito.any(SearchQuery.class), Mockito.anyList());
+
+        Mockito.doAnswer(invocationOnMock -> {
+            final List<String> accumulator = invocationOnMock.getArgument(2, List.class);
+            accumulator.add("matcher2");
+            return accumulator;
+        }).when(attributeMatcher2).match(Mockito.any(ProcessorNode.class), Mockito.any(SearchQuery.class), Mockito.anyList());
+    }
+
+    private void givenAttributesAreNotMatching() {
+        Mockito.doAnswer(invocationOnMock -> invocationOnMock.getArgument(2, List.class))
+                .when(attributeMatcher1).match(Mockito.any(ProcessorNode.class), Mockito.any(SearchQuery.class), Mockito.anyList());
+
+        Mockito.doAnswer(invocationOnMock -> invocationOnMock.getArgument(2, List.class))
+                .when(attributeMatcher2).match(Mockito.any(ProcessorNode.class), Mockito.any(SearchQuery.class), Mockito.anyList());
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/AbstractAttributeMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/AbstractAttributeMatcherTest.java
new file mode 100644
index 0000000..99c2e0a
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/AbstractAttributeMatcherTest.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.web.search.query.SearchQuery;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(MockitoJUnitRunner.class)
+public abstract class AbstractAttributeMatcherTest {
+    private static final String SEARCH_TERM = "lorem";
+
+    protected List<String> matches;
+
+    @Mock
+    protected SearchQuery searchQuery;
+
+    @Before
+    public void setUp() {
+        matches = new ArrayList<>();
+        Mockito.when(searchQuery.getTerm()).thenReturn(SEARCH_TERM);
+    }
+
+    protected void givenSearchTerm(final String term) {
+        Mockito.when(searchQuery.getTerm()).thenReturn(term);
+    }
+
+    protected void givenFilter(final String filterName, final String filterValue) {
+        Mockito.when(searchQuery.hasFilter(filterName)).thenReturn(true);
+        Mockito.when(searchQuery.getFilter(filterName)).thenReturn(filterValue);
+    }
+
+    protected void thenNoMatches() {
+        Assert.assertTrue(matches.isEmpty());
+    }
+
+    protected void thenMatchConsistsOf(final String... expectedMatches) {
+        Assert.assertEquals(expectedMatches.length, matches.size());
+
+        for (final String expectedMatch : expectedMatches) {
+            Assert.assertTrue("Should contain: " + expectedMatch, matches.contains(expectedMatch));
+        }
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/AttributeMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/AttributeMatcherTest.java
new file mode 100644
index 0000000..63a6f49
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/AttributeMatcherTest.java
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class AttributeMatcherTest {
+    private final static String SEARCH_TERM = "lorem";
+    private final static String SUBJECT_PARTIAL = SEARCH_TERM;
+    private final static String SUBJECT_PARTIAL_UPPERCASE = SEARCH_TERM.toUpperCase();
+    private final static String SUBJECT_FULL = SUBJECT_PARTIAL + " ipsum";
+    private final static String LABEL = "label";
+    private final static String LABEL_2 = "label2";
+
+    @Test
+    public void testWhenEqualsThenAdded() {
+        // given
+        final List<String> matches = new ArrayList<>();
+
+        // when
+        AttributeMatcher.addIfMatching(SEARCH_TERM, SUBJECT_PARTIAL, LABEL, matches);
+
+        // then
+        Assert.assertEquals(1, matches.size());
+        Assert.assertTrue(matches.contains(LABEL + AttributeMatcher.SEPARATOR + SUBJECT_PARTIAL));
+    }
+
+    @Test
+    public void testWhenSubstringThenAdded() {
+        // given
+        final List<String> matches = new ArrayList<>();
+
+        // when
+        AttributeMatcher.addIfMatching(SEARCH_TERM, SUBJECT_FULL, LABEL, matches);
+
+        // then
+        Assert.assertEquals(1, matches.size());
+        Assert.assertTrue(matches.contains(LABEL + AttributeMatcher.SEPARATOR + SUBJECT_FULL));
+    }
+
+    @Test
+    public void testWhenOnlyCaseDifferenceThenAdded() {
+        // given
+        final List<String> matches = new ArrayList<>();
+
+        // when
+        AttributeMatcher.addIfMatching(SEARCH_TERM, SUBJECT_PARTIAL_UPPERCASE, LABEL, matches);
+
+        // then
+        Assert.assertEquals(1, matches.size());
+        Assert.assertTrue(matches.contains(LABEL + AttributeMatcher.SEPARATOR + SUBJECT_PARTIAL_UPPERCASE));
+    }
+
+    @Test
+    public void testWhenNonMatchingThenNotAdded() {
+        // given
+        final String nonMatchingSubject = "foobar";
+        final List<String> matches = new ArrayList<>();
+
+        // when
+        AttributeMatcher.addIfMatching(SEARCH_TERM, nonMatchingSubject, LABEL, matches);
+
+        // then
+        Assert.assertEquals(0, matches.size());
+    }
+
+    @Test
+    public void testWhenSubjectIsNullThenNotAdded() {
+        // given
+        final List<String> matches = new ArrayList<>();
+
+        // when
+        AttributeMatcher.addIfMatching(SEARCH_TERM, null, LABEL, matches);
+
+        // then
+        Assert.assertEquals(0, matches.size());
+    }
+
+    @Test
+    public void testWhenSearchTermIsNullThenNotAdded() {
+        // given
+        final List<String> matches = new ArrayList<>();
+
+        // when
+        AttributeMatcher.addIfMatching(null, SUBJECT_PARTIAL, LABEL, matches);
+
+        // then
+        Assert.assertEquals(0, matches.size());
+    }
+
+    @Test
+    public void testWhenSearchTermAndSubjectAreNullThenNotAdded() {
+        // given
+        final List<String> matches = new ArrayList<>();
+
+        // when
+        AttributeMatcher.addIfMatching(null, null, LABEL, matches);
+
+        // then
+        Assert.assertEquals(0, matches.size());
+    }
+
+    @Test
+    public void testWhenMatchesIsNullThenNoException() {
+        // when
+        AttributeMatcher.addIfMatching(SEARCH_TERM, SUBJECT_PARTIAL, LABEL, null);
+
+        // then - no exception
+    }
+
+    @Test
+    public void testWhenLabelIsNullThenSkipped() {
+        // Test to cover backward compatibility
+        // given
+        final List<String> matches = new ArrayList<>();
+
+        // when
+        AttributeMatcher.addIfMatching(SEARCH_TERM, SUBJECT_PARTIAL, null, matches);
+
+        // then
+        Assert.assertEquals(1, matches.size());
+        Assert.assertTrue(matches.contains("null: " + SUBJECT_PARTIAL));
+    }
+
+    @Test
+    public void testWhenAddingMultipleThenOrderIsPreserved() {
+        // given
+        final List<String> matches = new ArrayList<>();
+
+        // when
+        AttributeMatcher.addIfMatching(SEARCH_TERM, SUBJECT_PARTIAL, LABEL, matches);
+        AttributeMatcher.addIfMatching(SEARCH_TERM, SUBJECT_FULL, LABEL_2, matches);
+
+        // then
+        Assert.assertEquals(2, matches.size());
+        Assert.assertEquals(LABEL + AttributeMatcher.SEPARATOR + SUBJECT_PARTIAL, matches.get(0));
+        Assert.assertEquals(LABEL_2 + AttributeMatcher.SEPARATOR + SUBJECT_FULL, matches.get(1));
+    }
+
+    @Test
+    public void testWhenDuplicatedThenNotAddedTwice() {
+        // given
+        final List<String> matches = new ArrayList<>();
+
+        // when
+        AttributeMatcher.addIfMatching(SEARCH_TERM, SUBJECT_PARTIAL, LABEL, matches);
+        AttributeMatcher.addIfMatching(SEARCH_TERM, SUBJECT_PARTIAL, LABEL, matches);
+
+        // then
+        Assert.assertEquals(1, matches.size());
+        Assert.assertTrue(matches.contains(LABEL + AttributeMatcher.SEPARATOR + SUBJECT_PARTIAL));
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/BackPressureMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/BackPressureMatcherTest.java
new file mode 100644
index 0000000..0a9631b8
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/BackPressureMatcherTest.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.connectable.Connection;
+import org.apache.nifi.controller.queue.FlowFileQueue;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+public class BackPressureMatcherTest extends AbstractAttributeMatcherTest {
+
+    private final BackPressureMatcher testSubject = new BackPressureMatcher();
+
+    @Mock
+    private Connection connection;
+
+    @Mock
+    private FlowFileQueue flowFileQueue;
+
+    @Before
+    public void setUp() {
+        super.setUp();
+        Mockito.when(connection.getFlowFileQueue()).thenReturn(flowFileQueue);
+    }
+
+    @Test
+    public void testWhenNoKeywordThenNoMatching() {
+        // given
+        givenThereIsBackPressure();
+
+        // when
+        testSubject.match(connection, searchQuery, matches);
+
+        // then
+        thenNoMatches();
+        Mockito.verify(connection, Mockito.never()).getFlowFileQueue();
+    }
+
+    @Test
+    public void testKeywordMatchesAndThereIsBackPressure() {
+        // given
+        givenSearchTerm("presSURE");
+        givenThereIsBackPressure();
+
+        // when
+        testSubject.match(connection, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Back pressure data size: 100 KB", "Back pressure count: 5");
+        Mockito.verify(connection, Mockito.atLeastOnce()).getFlowFileQueue();
+    }
+
+    @Test
+    public void testKeywordMatchesAndThereIsNoBackPressure() {
+        // given
+        givenSearchTerm("back pressure");
+        givenThereIsNoBackPressure();
+
+        // when
+        testSubject.match(connection, searchQuery, matches);
+
+        // then
+        thenNoMatches();
+        Mockito.verify(connection, Mockito.atLeastOnce()).getFlowFileQueue();
+    }
+
+    private void givenThereIsBackPressure() {
+        Mockito.when(flowFileQueue.getBackPressureDataSizeThreshold()).thenReturn("100 KB");
+        Mockito.when(flowFileQueue.getBackPressureObjectThreshold()).thenReturn(5L);
+    }
+
+    private void givenThereIsNoBackPressure() {
+        Mockito.when(flowFileQueue.getBackPressureDataSizeThreshold()).thenReturn("0 B");
+        Mockito.when(flowFileQueue.getBackPressureObjectThreshold()).thenReturn(0L);
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/BasicMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/BasicMatcherTest.java
new file mode 100644
index 0000000..1a1ba56
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/BasicMatcherTest.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.connectable.Connectable;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.util.Optional;
+
+public class BasicMatcherTest extends AbstractAttributeMatcherTest {
+    private static final String VALUE = "lorem";
+
+    @Mock
+    private Connectable component;
+
+    @Before
+    public void setUp() {
+        super.setUp();
+        Mockito.when(component.getIdentifier()).thenReturn(VALUE + "Id");
+        Mockito.when(component.getVersionedComponentId()).thenReturn(Optional.of(VALUE + "VersionId"));
+    }
+
+    @Test
+    public void testMatchingAddsResultWhenNotExtended() {
+        // given
+        final BasicMatcher<Connectable> testSubject = new BasicMatcher<>();
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf(
+            "Id: loremId", //
+            "Version Control ID: loremVersionId");
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ConnectionMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ConnectionMatcherTest.java
new file mode 100644
index 0000000..f37ecb5
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ConnectionMatcherTest.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.connectable.Connection;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.util.Optional;
+
+public class ConnectionMatcherTest extends AbstractAttributeMatcherTest {
+
+    @Mock
+    private Connection component;
+
+    @Before
+    public void setUp() {
+        super.setUp();
+        Mockito.when(component.getIdentifier()).thenReturn("LoremId");
+        Mockito.when(component.getVersionedComponentId()).thenReturn(Optional.of("LoremVersionId"));
+        Mockito.when(component.getName()).thenReturn("LoremName");
+    }
+
+    @Test
+    public void testMatching() {
+        // given
+        final ConnectionMatcher testSubject = new ConnectionMatcher();
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Id: LoremId", "Version Control ID: LoremVersionId", "Name: LoremName");
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ConnectionRelationshipMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ConnectionRelationshipMatcherTest.java
new file mode 100644
index 0000000..c60d2de
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ConnectionRelationshipMatcherTest.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.connectable.Connection;
+import org.apache.nifi.processor.Relationship;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.util.Collection;
+import java.util.HashSet;
+
+public class ConnectionRelationshipMatcherTest extends AbstractAttributeMatcherTest {
+
+    @Mock
+    private Connection component;
+
+    @Test
+    public void testMatching() {
+        // given
+        final ConnectionRelationshipMatcher testSubject = new ConnectionRelationshipMatcher();
+        givenRelationships("incoming", "outgoing1", "outgoing2");
+        givenSearchTerm("outgoing");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Relationship: outgoing1", "Relationship: outgoing2");
+    }
+
+    @Test
+    public void testDoesNotMatchForDescription() {
+        // given
+        final ConnectionRelationshipMatcher testSubject = new ConnectionRelationshipMatcher();
+        givenRelationships("incoming", "outgoing1", "outgoing2");
+        givenSearchTerm("description");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenNoMatches();
+    }
+
+    private void givenRelationships(final String... relationshipNames) {
+        final Collection<Relationship> relationships = new HashSet<>();
+
+        for (final String relationshipName : relationshipNames) {
+            final Relationship relationship = new Relationship.Builder().name(relationshipName).description("description").build();
+            relationships.add(relationship);
+        }
+
+        Mockito.when(component.getRelationships()).thenReturn(relationships);
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ConnectivityMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ConnectivityMatcherTest.java
new file mode 100644
index 0000000..6fbb1fc
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ConnectivityMatcherTest.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.connectable.Connectable;
+import org.apache.nifi.connectable.Connection;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+public class ConnectivityMatcherTest extends AbstractAttributeMatcherTest {
+
+    @Mock
+    private Connection component;
+
+    @Mock
+    private Connectable source;
+
+    @Mock
+    private Connectable destination;
+
+    @Before
+    public void setUp() {
+        super.setUp();
+        Mockito.when(component.getSource()).thenReturn(source);
+        Mockito.when(component.getDestination()).thenReturn(destination);
+
+        Mockito.when(source.getIdentifier()).thenReturn("SourceId");
+        Mockito.when(source.getName()).thenReturn("SourceName");
+        Mockito.when(source.getComments()).thenReturn("SourceComment");
+
+        Mockito.when(destination.getIdentifier()).thenReturn("DestinationId");
+        Mockito.when(destination.getName()).thenReturn("DestinationName");
+        Mockito.when(destination.getComments()).thenReturn("DestinationComment");
+    }
+
+    @Test
+    public void testSourceMatching() {
+        // given
+        final ConnectivityMatcher testSubject = new ConnectivityMatcher();
+        givenSearchTerm("source");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Source id: SourceId", //
+                "Source name: SourceName", //
+                "Source comments: SourceComment");
+    }
+
+    @Test
+    public void testDestinationMatching() {
+        // given
+        final ConnectivityMatcher testSubject = new ConnectivityMatcher();
+        givenSearchTerm("destination");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Destination id: DestinationId", //
+                "Destination name: DestinationName", //
+                "Destination comments: DestinationComment");
+    }
+
+    @Test
+    public void testBothMatching() {
+        // given
+        final ConnectivityMatcher testSubject = new ConnectivityMatcher();
+        givenSearchTerm("Name");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Source name: SourceName", //
+                "Destination name: DestinationName");
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ControllerServiceNodeMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ControllerServiceNodeMatcherTest.java
new file mode 100644
index 0000000..216f8df
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ControllerServiceNodeMatcherTest.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.controller.service.ControllerServiceNode;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.util.Optional;
+
+public class ControllerServiceNodeMatcherTest extends AbstractAttributeMatcherTest {
+
+    @Mock
+    private ControllerServiceNode component;
+
+    @Before
+    public void setUp() {
+        super.setUp();
+        Mockito.when(component.getIdentifier()).thenReturn("LoremId");
+        Mockito.when(component.getVersionedComponentId()).thenReturn(Optional.of("LoremVersionId"));
+        Mockito.when(component.getName()).thenReturn("LoremName");
+        Mockito.when(component.getComments()).thenReturn("LoremComment");
+    }
+
+    @Test
+    public void testMatching() {
+        // given
+        final ControllerServiceNodeMatcher testSubject = new ControllerServiceNodeMatcher();
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Id: LoremId", //
+                "Version Control ID: LoremVersionId", //
+                "Name: LoremName", //
+                "Comments: LoremComment");
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ExecutionMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ExecutionMatcherTest.java
new file mode 100644
index 0000000..4aa7cc3
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ExecutionMatcherTest.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.controller.ProcessorNode;
+import org.apache.nifi.scheduling.ExecutionNode;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+public class ExecutionMatcherTest extends AbstractAttributeMatcherTest {
+
+    @Mock
+    private ProcessorNode component;
+
+    @Test
+    public void testWithKeywordWhenApplies() {
+        // given
+        final ExecutionMatcher testSubject = new ExecutionMatcher();
+        givenExecutionModeIsPrimary();
+        givenSearchTerm("primary");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Execution node: primary");
+    }
+
+    @Test
+    public void testWithKeywordWhenDoesNotApplies() {
+        // given
+        final ExecutionMatcher testSubject = new ExecutionMatcher();
+        givenExecutionModeIsNotPrimary();
+        givenSearchTerm("primary");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenNoMatches();
+    }
+
+    @Test
+    public void testWithoutKeywordWhenDoesNotApplies() {
+        // given
+        final ExecutionMatcher testSubject = new ExecutionMatcher();
+        givenExecutionModeIsPrimary();
+        givenSearchTerm("lorem");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenNoMatches();
+    }
+
+    private void givenExecutionModeIsPrimary() {
+        Mockito.when(component.getExecutionNode()).thenReturn(ExecutionNode.PRIMARY);
+    }
+
+    private void givenExecutionModeIsNotPrimary() {
+        Mockito.when(component.getExecutionNode()).thenReturn(ExecutionNode.ALL);
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ExpirationMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ExpirationMatcherTest.java
new file mode 100644
index 0000000..70cf9eb
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ExpirationMatcherTest.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.connectable.Connection;
+import org.apache.nifi.controller.queue.FlowFileQueue;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.util.concurrent.TimeUnit;
+
+public class ExpirationMatcherTest extends AbstractAttributeMatcherTest{
+
+    @Mock
+    private Connection component;
+
+    @Mock
+    private FlowFileQueue flowFileQueue;
+
+    @Before
+    public void setUp() {
+        super.setUp();
+        Mockito.when(component.getFlowFileQueue()).thenReturn(flowFileQueue);
+    }
+
+    @Test
+    public void testWhenKeywordExpiresAppearsAndExpired() {
+        // given
+        final ExpirationMatcher testSubject = new ExpirationMatcher();
+        givenSearchTerm("expires");
+        givenExpired();
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("FlowFile expiration: 5");
+    }
+
+    @Test
+    public void testWhenKeywordExpirationAppearsAndExpired() {
+        // given
+        final ExpirationMatcher testSubject = new ExpirationMatcher();
+        givenSearchTerm("expiration");
+        givenExpired();
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("FlowFile expiration: 5");
+    }
+
+    @Test
+    public void testWhenNoKeywordAppearsAndExpired() {
+        // given
+        final ExpirationMatcher testSubject = new ExpirationMatcher();
+        givenSearchTerm("lorem");
+        givenExpired();
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenNoMatches();
+    }
+
+    @Test
+    public void testWhenKeywordExpiresAppearsAndDoesNotExpired() {
+        // given
+        final ExpirationMatcher testSubject = new ExpirationMatcher();
+        givenSearchTerm("expires");
+        givenNotExpired();
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenNoMatches();
+    }
+
+    private void givenExpired() {
+        Mockito.when(flowFileQueue.getFlowFileExpiration(TimeUnit.MILLISECONDS)).thenReturn(5);
+        Mockito.when(flowFileQueue.getFlowFileExpiration()).thenReturn("5");
+    }
+
+    private void givenNotExpired() {
+        Mockito.when(flowFileQueue.getFlowFileExpiration(TimeUnit.MILLISECONDS)).thenReturn(0);
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ExtendedMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ExtendedMatcherTest.java
new file mode 100644
index 0000000..1656550
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ExtendedMatcherTest.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.connectable.Connectable;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.util.Optional;
+
+public class ExtendedMatcherTest extends AbstractAttributeMatcherTest {
+    private static final String VALUE = "lorem";
+
+    @Mock
+    private Connectable component;
+
+    @Before
+    public void setUp() {
+        super.setUp();
+        Mockito.when(component.getIdentifier()).thenReturn(VALUE + "Id");
+        Mockito.when(component.getVersionedComponentId()).thenReturn(Optional.of(VALUE + "VersionId"));
+        Mockito.when(component.getName()).thenReturn(VALUE + "Name");
+        Mockito.when(component.getComments()).thenReturn(VALUE + "Comments");
+    }
+
+    @Test
+    public void testMatchingAddsResultWhenExtended() {
+        // given
+        final ExtendedMatcher<Connectable> testSubject = new ExtendedMatcher<>();
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf(
+                "Id: loremId", //
+                "Version Control ID: loremVersionId", //
+                "Name: loremName", //
+                "Comments: loremComments");
+    }
+
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/LabelMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/LabelMatcherTest.java
new file mode 100644
index 0000000..3ebeee2
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/LabelMatcherTest.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.controller.label.Label;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+public class LabelMatcherTest extends AbstractAttributeMatcherTest {
+
+    @Mock
+    private Label component;
+
+    @Before
+    public void setUp() {
+        super.setUp();
+        Mockito.when(component.getIdentifier()).thenReturn("LoremId");
+        Mockito.when(component.getValue()).thenReturn("LoremValue");
+    }
+
+    @Test
+    public void testMatching() {
+        // given
+        final LabelMatcher testSubject = new LabelMatcher();
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Id: LoremId", "Value: LoremValue");
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ParameterContextMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ParameterContextMatcherTest.java
new file mode 100644
index 0000000..f2650c1
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ParameterContextMatcherTest.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.parameter.ParameterContext;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+public class ParameterContextMatcherTest extends AbstractAttributeMatcherTest {
+
+    @Mock
+    private ParameterContext component;
+
+    @Before
+    public void setUp() {
+        super.setUp();
+        Mockito.when(component.getIdentifier()).thenReturn("LoremId");
+        Mockito.when(component.getName()).thenReturn("LoremName");
+        Mockito.when(component.getDescription()).thenReturn("LoremDescription");
+    }
+
+    @Test
+    public void testMatches() {
+        // given
+        final ParameterContextMatcher testSubject = new ParameterContextMatcher();
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Id: LoremId", "Name: LoremName", "Description: LoremDescription");
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ParameterMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ParameterMatcherTest.java
new file mode 100644
index 0000000..167d4dd
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ParameterMatcherTest.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.parameter.Parameter;
+import org.apache.nifi.parameter.ParameterDescriptor;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+public class ParameterMatcherTest extends AbstractAttributeMatcherTest {
+
+    @Mock
+    private Parameter parameter;
+
+    @Mock
+    private ParameterDescriptor descriptor;
+
+    @Before
+    public void setUp() {
+        super.setUp();
+        Mockito.when(parameter.getDescriptor()).thenReturn(descriptor);
+        Mockito.when(parameter.getValue()).thenReturn("LoremValue");
+        Mockito.when(descriptor.getName()).thenReturn("LoremName");
+        Mockito.when(descriptor.getDescription()).thenReturn("LoremDescription");
+    }
+
+    @Test
+    public void testMatchingWhenNotSensitive() {
+        // given
+        final ParameterMatcher testSubject = new ParameterMatcher();
+        givenValueIsNotSensitive();
+
+        // when
+        testSubject.match(parameter, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Name: LoremName", "Value: LoremValue", "Description: LoremDescription");
+    }
+
+    @Test
+    public void testMatchingWhenSensitive() {
+        // given
+        final ParameterMatcher testSubject = new ParameterMatcher();
+        givenValueIsSensitive();
+
+        // when
+        testSubject.match(parameter, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Name: LoremName", "Description: LoremDescription");
+    }
+
+    private void givenValueIsSensitive() {
+        Mockito.when(descriptor.isSensitive()).thenReturn(true);
+    }
+
+    private void givenValueIsNotSensitive() {
+        Mockito.when(descriptor.isSensitive()).thenReturn(false);
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/PortScheduledStateMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/PortScheduledStateMatcherTest.java
new file mode 100644
index 0000000..6df3884
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/PortScheduledStateMatcherTest.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.connectable.Port;
+import org.apache.nifi.controller.ScheduledState;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+public class PortScheduledStateMatcherTest extends AbstractAttributeMatcherTest {
+
+    @Mock
+    private Port component;
+
+    @Test
+    public void testDisabledMatches() {
+        // given
+        final PortScheduledStateMatcher testSubject = new PortScheduledStateMatcher();
+        givenSearchTerm("disabled");
+        givenStatus(ScheduledState.DISABLED);
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Run status: Disabled");
+    }
+
+    @Test
+    public void testDisabledAndInvalidWhenSearchedForInvalid() {
+        // given
+        final PortScheduledStateMatcher testSubject = new PortScheduledStateMatcher();
+        givenSearchTerm("invalid");
+        givenInvalid();
+        givenStatus(ScheduledState.DISABLED);
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenNoMatches();
+    }
+
+    @Test
+    public void testRunningMatches() {
+        // given
+        final PortScheduledStateMatcher testSubject = new PortScheduledStateMatcher();
+        givenSearchTerm("running");
+        givenStatus(ScheduledState.RUNNING);
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Run status: Running");
+    }
+
+    @Test
+    public void testStoppedMatches() {
+        // given
+        final PortScheduledStateMatcher testSubject = new PortScheduledStateMatcher();
+        givenSearchTerm("stopped");
+        givenStatus(ScheduledState.STOPPED);
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Run status: Stopped");
+    }
+
+    @Test
+    public void testStatusDoesNotMatch() {
+        // given
+        final PortScheduledStateMatcher testSubject = new PortScheduledStateMatcher();
+        givenSearchTerm("stopped");
+        givenStatus(ScheduledState.RUNNING);
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenNoMatches();
+    }
+
+    @Test
+    public void testInvalidMatches() {
+        // given
+        final PortScheduledStateMatcher testSubject = new PortScheduledStateMatcher();
+        givenSearchTerm("invalid");
+        givenInvalid();
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Run status: Invalid");
+    }
+
+    @Test
+    public void testInvalidDoesNotMatch() {
+        // given
+        final PortScheduledStateMatcher testSubject = new PortScheduledStateMatcher();
+        givenSearchTerm("invalid");
+        givenValid();
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenNoMatches();
+    }
+
+    private void givenStatus(final ScheduledState status) {
+        Mockito.when(component.getScheduledState()).thenReturn(status);
+    }
+
+    private void givenValid() {
+        Mockito.when(component.isValid()).thenReturn(true);
+    }
+
+    private void givenInvalid() {
+        Mockito.when(component.isValid()).thenReturn(false);
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/PrioritiesMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/PrioritiesMatcherTest.java
new file mode 100644
index 0000000..c89a202
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/PrioritiesMatcherTest.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.connectable.Connection;
+import org.apache.nifi.controller.queue.FlowFileQueue;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.FlowFilePrioritizer;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PrioritiesMatcherTest extends AbstractAttributeMatcherTest {
+
+    @Mock
+    private Connection component;
+
+    @Mock
+    private FlowFileQueue flowFileQueue;
+
+    @Before
+    public void setUp() {
+        super.setUp();
+        Mockito.when(component.getFlowFileQueue()).thenReturn(flowFileQueue);
+        Mockito.when(flowFileQueue.getPriorities()).thenReturn(givenPriorizers());
+    }
+
+    @Test
+    public void testMatching() {
+        // given
+        final PrioritiesMatcher testSubject = new PrioritiesMatcher();
+        givenSearchTerm("FlowFilePrioritize");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf(
+                "Prioritizer: org.apache.nifi.web.search.attributematchers.PrioritiesMatcherTest$FlowFilePrioritizerOne",
+                "Prioritizer: org.apache.nifi.web.search.attributematchers.PrioritiesMatcherTest$FlowFilePrioritizerTwo");
+    }
+
+    private List<FlowFilePrioritizer> givenPriorizers() {
+        final List<FlowFilePrioritizer> result = new ArrayList<>();
+        result.add(new FlowFilePrioritizerOne());
+        result.add(new FlowFilePrioritizerTwo());
+        return result;
+    }
+
+    private static class FlowFilePrioritizerOne implements FlowFilePrioritizer {
+        @Override
+        public int compare(FlowFile o1, FlowFile o2) {
+            return 0;
+        }
+    }
+
+    private static class FlowFilePrioritizerTwo implements FlowFilePrioritizer {
+        @Override
+        public int compare(FlowFile o1, FlowFile o2) {
+            return 0;
+        }
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ProcessGroupMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ProcessGroupMatcherTest.java
new file mode 100644
index 0000000..ce00b81
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ProcessGroupMatcherTest.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.groups.ProcessGroup;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.util.Optional;
+
+public class ProcessGroupMatcherTest extends AbstractAttributeMatcherTest {
+
+    @Mock
+    private ProcessGroup component;
+
+    @Before
+    public void setUp() {
+        super.setUp();
+        Mockito.when(component.getIdentifier()).thenReturn("LoremId");
+        Mockito.when(component.getVersionedComponentId()).thenReturn(Optional.of("LoremVersionId"));
+        Mockito.when(component.getName()).thenReturn("LoremName");
+        Mockito.when(component.getComments()).thenReturn("LoremComment");
+    }
+
+    @Test
+    public void testMatching() {
+        // given
+        final ProcessGroupMatcher testSubject = new ProcessGroupMatcher();
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Id: LoremId", //
+                "Version Control ID: LoremVersionId", //
+                "Name: LoremName", //
+                "Comments: LoremComment");
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ProcessorMetadataMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ProcessorMetadataMatcherTest.java
new file mode 100644
index 0000000..43635f8
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ProcessorMetadataMatcherTest.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.ValidationContext;
+import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.controller.ProcessorNode;
+
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSessionFactory;
+import org.apache.nifi.processor.Processor;
+import org.apache.nifi.processor.ProcessorInitializationContext;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+public class ProcessorMetadataMatcherTest extends AbstractAttributeMatcherTest {
+
+    @Mock
+    private ProcessorNode processorNode;
+
+    @Test
+    public void testMatching() {
+        // given
+        final ProcessorMetadataMatcher testSubject = new ProcessorMetadataMatcher();
+        final Processor processor = new LoremProcessor();
+        Mockito.when(processorNode.getProcessor()).thenReturn(processor);
+        Mockito.when(processorNode.getComponentType()).thenReturn("Lorem");
+
+        // when
+        testSubject.match(processorNode, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Type: LoremProcessor", "Type: Lorem");
+    }
+
+    private static class LoremProcessor implements Processor {
+
+        @Override
+        public void initialize(ProcessorInitializationContext context) {
+            // noop
+        }
+
+        @Override
+        public Set<Relationship> getRelationships() {
+            return null;
+        }
+
+        @Override
+        public void onTrigger(ProcessContext context, ProcessSessionFactory sessionFactory) throws ProcessException {
+            // noop
+        }
+
+        @Override
+        public Collection<ValidationResult> validate(ValidationContext context) {
+            return null;
+        }
+
+        @Override
+        public PropertyDescriptor getPropertyDescriptor(String name) {
+            return null;
+        }
+
+        @Override
+        public void onPropertyModified(PropertyDescriptor descriptor, String oldValue, String newValue) {
+            // noop
+        }
+
+        @Override
+        public List<PropertyDescriptor> getPropertyDescriptors() {
+            return null;
+        }
+
+        @Override
+        public String getIdentifier() {
+            return null;
+        }
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/PropertyMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/PropertyMatcherTest.java
new file mode 100644
index 0000000..d23e3dc
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/PropertyMatcherTest.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.controller.ProcessorNode;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class PropertyMatcherTest extends AbstractAttributeMatcherTest {
+
+    @Mock
+    private ProcessorNode component;
+
+    @Before
+    public void setUp() {
+        super.setUp();
+    }
+
+    @Test
+    public void testMatchingAndNotFiltered() {
+        // given
+        final PropertyMatcher testSubject = new PropertyMatcher();
+        givenProperties(false);
+        givenSearchTerm("lorem");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Property name: loremName", //
+                "Property value: loremName - loremValue", //
+                "Property description: loremDescription");
+    }
+
+    @Test
+    public void testMatchingAndNotFilteredButSensitive() {
+        // given
+        final PropertyMatcher testSubject = new PropertyMatcher();
+        givenProperties(true);
+        givenSearchTerm("lorem");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Property name: loremName", //
+                "Property description: loremDescription");
+    }
+
+    @Test
+    public void testMatchingAndFiltered() {
+        // given
+        final PropertyMatcher testSubject = new PropertyMatcher();
+        givenProperties(false);
+        givenSearchTerm("lorem");
+        givenFilter("properties", "exclude");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenNoMatches();
+    }
+
+    @Test
+    public void testMatchingAndFilteredWithIncorrectValue() {
+        // given
+        final PropertyMatcher testSubject = new PropertyMatcher();
+        givenProperties(false);
+        givenSearchTerm("lorem");
+        givenFilter("properties", "foobar");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Property name: loremName", //
+                "Property value: loremName - loremValue", //
+                "Property description: loremDescription");
+    }
+
+    private void givenProperties(final boolean isSensitive) {
+        final Map<PropertyDescriptor, String> result = new HashMap<>();
+        final PropertyDescriptor descriptor = new PropertyDescriptor.Builder() //
+                .name("loremName") //
+                .description("loremDescription") //
+                .sensitive(isSensitive) //
+                .build();
+
+        result.put(descriptor, "loremValue");
+        Mockito.when(component.getRawPropertyValues()).thenReturn(result);
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/PublicPortMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/PublicPortMatcherTest.java
new file mode 100644
index 0000000..8349ecc
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/PublicPortMatcherTest.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.connectable.Port;
+import org.apache.nifi.remote.PublicPort;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+import java.util.HashSet;
+
+public class PublicPortMatcherTest extends AbstractAttributeMatcherTest {
+
+    @Mock
+    private Port port;
+
+    @Mock
+    private PublicPort publicPort;
+
+    @Before
+    public void setUp() {
+        super.setUp();
+        Mockito.when(publicPort.getUserAccessControl()).thenReturn(new HashSet<>(Arrays.asList("user1Lorem", "user2Lorem")));
+        Mockito.when(publicPort.getGroupAccessControl()).thenReturn(new HashSet<>(Arrays.asList("group1Lorem", "group2Lorem")));
+    }
+
+    @Test
+    public void testNonPublicPort() {
+        // given
+        final PublicPortMatcher testSubject = new PublicPortMatcher();
+
+        // when
+        testSubject.match(port, searchQuery, matches);
+
+        // then
+        thenNoMatches();
+    }
+
+    @Test
+    public void testPublicPort() {
+        // given
+        final PublicPortMatcher testSubject = new PublicPortMatcher();
+
+        // when
+        testSubject.match(publicPort, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("User access control: user1Lorem",
+                "User access control: user2Lorem",
+                "Group access control: group1Lorem",
+                "Group access control: group1Lorem");
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/RelationshipMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/RelationshipMatcherTest.java
new file mode 100644
index 0000000..3e89e2a
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/RelationshipMatcherTest.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.connectable.Connectable;
+import org.apache.nifi.processor.Relationship;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.util.Collection;
+import java.util.HashSet;
+
+public class RelationshipMatcherTest extends AbstractAttributeMatcherTest {
+
+    @Mock
+    private Connectable component;
+
+    @Test
+    public void testMatching() {
+        // given
+        final RelationshipMatcher<Connectable> testSubject = new RelationshipMatcher<>();
+        givenRelationships("incoming", "outgoing1", "outgoing2");
+        givenSearchTerm("outgoing");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Relationship: outgoing1", "Relationship: outgoing2");
+    }
+
+    @Test
+    public void testDoesNotMatchForDescription() {
+        // given
+        final RelationshipMatcher<Connectable> testSubject = new RelationshipMatcher<>();
+        givenRelationships("incoming", "outgoing1", "outgoing2");
+        givenSearchTerm("description");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenNoMatches();
+    }
+
+    private void givenRelationships(final String... relationshipNames) {
+        final Collection<Relationship> relationships = new HashSet<>();
+
+        for (final String relationshipName : relationshipNames) {
+            final Relationship relationship = new Relationship.Builder().name(relationshipName).description("description").build();
+            relationships.add(relationship);
+        }
+
+        Mockito.when(component.getRelationships()).thenReturn(relationships);
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/RemoteProcessGroupMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/RemoteProcessGroupMatcherTest.java
new file mode 100644
index 0000000..334b057
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/RemoteProcessGroupMatcherTest.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.groups.RemoteProcessGroup;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.util.Optional;
+
+public class RemoteProcessGroupMatcherTest extends AbstractAttributeMatcherTest {
+
+    @Mock
+    private RemoteProcessGroup component;
+
+    @Before
+    public void setUp() {
+        super.setUp();
+
+        Mockito.when(component.getIdentifier()).thenReturn("LoremId");
+        Mockito.when(component.getVersionedComponentId()).thenReturn(Optional.of("LoremVersionId"));
+        Mockito.when(component.getName()).thenReturn("LoremName");
+        Mockito.when(component.getComments()).thenReturn("LoremComment");
+    }
+
+    @Test
+    public void testMatching() {
+        // given
+        final RemoteProcessGroupMatcher testSubject = new RemoteProcessGroupMatcher();
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Id: LoremId",
+                "Version Control ID: LoremVersionId",
+                "Name: LoremName",
+                "Comments: LoremComment");
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ScheduledStateMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ScheduledStateMatcherTest.java
new file mode 100644
index 0000000..33d782e
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/ScheduledStateMatcherTest.java
@@ -0,0 +1,194 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.components.validation.ValidationStatus;
+import org.apache.nifi.controller.ProcessorNode;
+import org.apache.nifi.controller.ScheduledState;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+public class ScheduledStateMatcherTest extends AbstractAttributeMatcherTest {
+
+    @Mock
+    private ProcessorNode component;
+
+    @Test
+    public void testWhenKeywordAppearsAndDisabled() {
+        // given
+        final ScheduledStateMatcher testSubject = new ScheduledStateMatcher();
+        givenScheduledState(ScheduledState.DISABLED);
+        givenSearchTerm("disabled");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Run status: Disabled");
+    }
+
+    @Test
+    public void testWhenKeywordAppearsAndNotDisabled() {
+        // given
+        final ScheduledStateMatcher testSubject = new ScheduledStateMatcher();
+        givenScheduledState(ScheduledState.RUNNING);
+        givenSearchTerm("disabled");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenNoMatches();
+    }
+
+    @Test
+    public void testWhenKeywordDoesNotAppearAndDisabled() {
+        // given
+        final ScheduledStateMatcher testSubject = new ScheduledStateMatcher();
+        givenScheduledState(ScheduledState.DISABLED);
+        givenSearchTerm("somethingElse");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenNoMatches();
+    }
+
+    @Test
+    public void testWhenInvalidKeywordAppearsAndInvalid() {
+        // given
+        final ScheduledStateMatcher testSubject = new ScheduledStateMatcher();
+        givenValidationStatus(ValidationStatus.INVALID);
+        givenSearchTerm("invalid");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Run status: Invalid");
+    }
+
+    @Test
+    public void testWhenInvalidKeywordAppearsAnValid() {
+        // given
+        final ScheduledStateMatcher testSubject = new ScheduledStateMatcher();
+        givenValidationStatus(ValidationStatus.VALID);
+        givenSearchTerm("invalid");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenNoMatches();
+    }
+
+    @Test
+    public void testWhenKeywordAppearsAndValid() {
+        // given
+        final ScheduledStateMatcher testSubject = new ScheduledStateMatcher();
+        givenValidationStatus(ValidationStatus.VALIDATING);
+        givenSearchTerm("validating");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Run status: Validating");
+    }
+
+    @Test
+    public void testWhenKeywordDoesNotAppearsAndValid() {
+        // given
+        final ScheduledStateMatcher testSubject = new ScheduledStateMatcher();
+        givenValidationStatus(ValidationStatus.VALID);
+        givenSearchTerm("invalid");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenNoMatches();
+    }
+
+    @Test
+    public void testWhenLookingForInvalidButTheStatusIsDisabled() {
+        // given
+        final ScheduledStateMatcher testSubject = new ScheduledStateMatcher();
+        givenScheduledState(ScheduledState.DISABLED);
+        givenValidationStatus(ValidationStatus.INVALID);
+        givenSearchTerm("invalid");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenNoMatches();
+    }
+
+    @Test
+    public void testWhenLookingForValidatingButTheStatusIsDisabled() {
+        // given
+        final ScheduledStateMatcher testSubject = new ScheduledStateMatcher();
+        givenScheduledState(ScheduledState.DISABLED);
+        givenValidationStatus(ValidationStatus.VALIDATING);
+        givenSearchTerm("validating");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenNoMatches();
+    }
+
+    @Test
+    public void testWhenRunning() {
+        // given
+        final ScheduledStateMatcher testSubject = new ScheduledStateMatcher();
+        givenScheduledState(ScheduledState.RUNNING);
+        givenSearchTerm("running");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Run status: Running");
+    }
+
+    @Test
+    public void testWhenStopped() {
+        // given
+        final ScheduledStateMatcher testSubject = new ScheduledStateMatcher();
+        givenScheduledState(ScheduledState.STOPPED);
+        givenSearchTerm("stopped");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Run status: Stopped");
+    }
+
+    private void givenScheduledState(final ScheduledState scheduledState) {
+        Mockito.when(component.getScheduledState()).thenReturn(scheduledState);
+    }
+
+    private void givenValidationStatus(final ValidationStatus validationStatus) {
+        Mockito.when(component.getValidationStatus()).thenReturn(validationStatus);
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/SchedulingMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/SchedulingMatcherTest.java
new file mode 100644
index 0000000..97ada1a
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/SchedulingMatcherTest.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.controller.ProcessorNode;
+import org.apache.nifi.scheduling.SchedulingStrategy;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+public class SchedulingMatcherTest extends AbstractAttributeMatcherTest {
+
+    @Mock
+    private ProcessorNode component;
+
+    @Test
+    public void testWhenKeywordAppearsAndEvent() {
+        // given
+        final SchedulingMatcher testSubject = new SchedulingMatcher();
+        givenSchedulingStrategy(SchedulingStrategy.EVENT_DRIVEN);
+        givenSearchTerm("event");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Scheduling strategy: Event driven");
+    }
+
+    @Test
+    public void testWhenKeywordAppearsAndNotEvent() {
+        // given
+        final SchedulingMatcher testSubject = new SchedulingMatcher();
+        givenSchedulingStrategy(SchedulingStrategy.TIMER_DRIVEN);
+        givenSearchTerm("event");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenNoMatches();
+    }
+
+    @Test
+    public void testWhenKeywordDoesNotAppearAndEvent() {
+        // given
+        final SchedulingMatcher testSubject = new SchedulingMatcher();
+        givenSchedulingStrategy(SchedulingStrategy.TIMER_DRIVEN);
+        givenSearchTerm("event");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenNoMatches();
+    }
+
+    @Test
+    public void testWhenKeywordAppearsAndTimer() {
+        // given
+        final SchedulingMatcher testSubject = new SchedulingMatcher();
+        givenSchedulingStrategy(SchedulingStrategy.TIMER_DRIVEN);
+        givenSearchTerm("timer");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Scheduling strategy: Timer driven");
+    }
+
+    @Test
+    public void testWhenKeywordAppearsAndPrimaryNodeOnly() {
+        // given
+        final SchedulingMatcher testSubject = new SchedulingMatcher();
+        givenSchedulingStrategy(SchedulingStrategy.PRIMARY_NODE_ONLY);
+        givenSearchTerm("primary");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Scheduling strategy: On primary node");
+    }
+
+    private void givenSchedulingStrategy(final SchedulingStrategy schedulingStrategy) {
+        Mockito.when(component.getSchedulingStrategy()).thenReturn(schedulingStrategy);
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/SearchableMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/SearchableMatcherTest.java
new file mode 100644
index 0000000..498b0ad
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/SearchableMatcherTest.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.controller.FlowController;
+import org.apache.nifi.controller.ProcessorNode;
+import org.apache.nifi.controller.service.ControllerServiceProvider;
+import org.apache.nifi.nar.ExtensionManager;
+import org.apache.nifi.processor.Processor;
+import org.apache.nifi.registry.VariableRegistry;
+import org.apache.nifi.search.SearchContext;
+import org.apache.nifi.search.SearchResult;
+import org.apache.nifi.search.Searchable;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.util.Collection;
+import java.util.HashSet;
+
+public class SearchableMatcherTest extends AbstractAttributeMatcherTest {
+
+    @Mock
+    private ProcessorNode component;
+
+    @Mock
+    private Processor nonSearchableProcessor;
+
+    @Mock
+    private SearchableProcessor searchableProcessor;
+
+    @Mock
+    private VariableRegistry variableRegistry;
+
+    @Mock
+    private FlowController flowController;
+
+    @Mock
+    private ControllerServiceProvider controllerServiceProvider;
+
+    @Mock
+    private ExtensionManager extensionManager;
+
+    @Before
+    public void setUp() {
+        super.setUp();
+        Mockito.when(flowController.getControllerServiceProvider()).thenReturn(controllerServiceProvider);
+        Mockito.when(flowController.getExtensionManager()).thenReturn(extensionManager);
+    }
+
+    @Test
+    public void testNonSearchableProcessorHasNoMatch() {
+        // given
+        final SearchableMatcher testSubject = givenTestSubject();
+        givenProcessorIsNotSearchable();
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenNoMatches();
+    }
+
+    @Test
+    public void testSearchableProcessor() {
+        // given
+        final SearchableMatcher testSubject = givenTestSubject();
+        givenProcessorIsSearchable();
+        givenSearchResultsAreNotEmpty();
+        givenSearchTerm("bbb");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("aaa: bbb", "bbb: ccc");
+    }
+
+    private void givenSearchResultsAreNotEmpty() {
+        final Collection<SearchResult> searchResults = new HashSet<>();
+        searchResults.add(new SearchResult.Builder().label("aaa").match("bbb").build());
+        searchResults.add(new SearchResult.Builder().label("bbb").match("ccc").build());
+        Mockito.when(searchableProcessor.search(Mockito.any(SearchContext.class))).thenReturn(searchResults);
+    }
+
+    private SearchableMatcher givenTestSubject() {
+        final SearchableMatcher result = new SearchableMatcher();
+        result.setFlowController(flowController);
+        result.setVariableRegistry(variableRegistry);
+        return result;
+    }
+
+    private void givenProcessorIsSearchable() {
+        Mockito.when(component.getProcessor()).thenReturn(searchableProcessor);
+    }
+
+    private void givenProcessorIsNotSearchable() {
+        Mockito.when(component.getProcessor()).thenReturn(nonSearchableProcessor);
+    }
+
+    private interface SearchableProcessor extends Processor, Searchable { }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/TargetUriMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/TargetUriMatcherTest.java
new file mode 100644
index 0000000..ba57cd2
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/TargetUriMatcherTest.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.groups.RemoteProcessGroup;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+public class TargetUriMatcherTest extends AbstractAttributeMatcherTest {
+    private static final String TARGET_URIS = "www.lorem.ipsum.com";
+
+    @Mock
+    private RemoteProcessGroup component;
+
+    @Before
+    public void setUp() {
+        super.setUp();
+        Mockito.when(component.getTargetUris()).thenReturn(TARGET_URIS);
+    }
+
+    @Test
+    public void testMatching() {
+        // given
+        final TargetUriMatcher testSubject = new TargetUriMatcher();
+        givenSearchTerm("lorem");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("URLs: " + TARGET_URIS);
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/TransmissionStatusMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/TransmissionStatusMatcherTest.java
new file mode 100644
index 0000000..52d61fb
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/TransmissionStatusMatcherTest.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.groups.RemoteProcessGroup;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+public class TransmissionStatusMatcherTest extends AbstractAttributeMatcherTest{
+
+    @Mock
+    private RemoteProcessGroup component;
+
+    @Test
+    public void testWhenTransmittingKeywordAndIsTransmitting() {
+        // given
+        final TransmissionStatusMatcher testSubject = new TransmissionStatusMatcher();
+        givenTransmitting();
+        givenSearchTerm("transmitting");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Transmission: On");
+    }
+
+    @Test
+    public void testWhenTransmittingKeywordAndIsNotTransmitting() {
+        // given
+        final TransmissionStatusMatcher testSubject = new TransmissionStatusMatcher();
+        givenNotTransmitting();
+        givenSearchTerm("transmitting");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenNoMatches();
+    }
+
+    @Test
+    public void testWhenNotTransmittingKeywordAndIsNotTransmitting() {
+        // given
+        final TransmissionStatusMatcher testSubject = new TransmissionStatusMatcher();
+        givenNotTransmitting();
+        givenSearchTerm("not transmitting");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Transmission: Off");
+    }
+
+    @Test
+    public void testWhenNotTransmittingKeywordAndIsTransmitting() {
+        // given
+        final TransmissionStatusMatcher testSubject = new TransmissionStatusMatcher();
+        givenTransmitting();
+        givenSearchTerm("not transmitting");
+
+        // when
+        testSubject.match(component, searchQuery, matches);
+
+        // then
+        thenNoMatches();
+    }
+
+    private void givenTransmitting() {
+        Mockito.when(component.isTransmitting()).thenReturn(true);
+    }
+
+    private void givenNotTransmitting() {
+        Mockito.when(component.isTransmitting()).thenReturn(false);
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/VariableRegistryMatcherTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/VariableRegistryMatcherTest.java
new file mode 100644
index 0000000..f27aa02
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/search/attributematchers/VariableRegistryMatcherTest.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.web.search.attributematchers;
+
+import org.apache.nifi.groups.ProcessGroup;
+import org.apache.nifi.registry.ComponentVariableRegistry;
+import org.apache.nifi.registry.VariableDescriptor;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class VariableRegistryMatcherTest extends AbstractAttributeMatcherTest {
+
+    @Mock
+    private ComponentVariableRegistry variableRegistry;
+
+    @Mock
+    private ProcessGroup processGroup;
+
+    @Before
+    public void setUp() {
+        super.setUp();
+        Mockito.when(processGroup.getVariableRegistry()).thenReturn(variableRegistry);
+        Mockito.when(variableRegistry.getVariableMap()).thenReturn(givenVariables());
+    }
+
+    @Test
+    public void testMatchForOneVariable() {
+        // given
+        final VariableRegistryMatcher testSubject = new VariableRegistryMatcher();
+        givenSearchTerm("ccc");
+
+        // when
+        testSubject.match(processGroup, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Variable Name: ccc", "Variable Value: ccc");
+    }
+
+    @Test
+    public void testMatchForMultipleVariable() {
+        // given
+        final VariableRegistryMatcher testSubject = new VariableRegistryMatcher();
+        givenSearchTerm("aaa");
+
+        // when
+        testSubject.match(processGroup, searchQuery, matches);
+
+        // then
+        thenMatchConsistsOf("Variable Name: aaa", "Variable Value: aaa");
... 325 lines suppressed ...