You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by il...@apache.org on 2026/02/24 15:32:48 UTC

(syncope) branch master updated: [SYNCOPE-1951] Realm search based on plain attrs (#1300)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 377f1cd775 [SYNCOPE-1951] Realm search based on plain attrs (#1300)
377f1cd775 is described below

commit 377f1cd775471f960858b7fea2d68b250d6b7379
Author: Matteo Tatoni <ta...@gmail.com>
AuthorDate: Tue Feb 24 11:52:46 2026 +0100

    [SYNCOPE-1951] Realm search based on plain attrs (#1300)
---
 .../clientapps/ClientAppModalPanelBuilder.java     |   2 +-
 .../console/panels/ConnObjectListViewPanel.java    |   3 +-
 .../client/console/status/ReconTaskPanel.java      |   2 +-
 .../wizards/resources/ConnectorDetailsPanel.java   |   2 +-
 .../client/console/commons/RealmsUtils.java        |   4 +-
 .../client/console/panels/RealmChoicePanel.java    |   4 +-
 .../console/tasks/SchedTaskWizardBuilder.java      |   2 +-
 .../client/console/wizards/any/Details.java        |   2 +-
 .../console/wizards/role/RoleWizardBuilder.java    |   2 +-
 .../apache/syncope/client/lib/SyncopeClient.java   |  10 +
 .../common/lib/search/RealmCompleteCondition.java  |  22 +
 .../search/RealmFiqlSearchConditionBuilder.java    |  50 ++
 .../common/lib/search/RealmPartialCondition.java   |  22 +
 .../syncope/common/lib/search/RealmProperty.java   |  22 +
 .../syncope/common/rest/api/beans/AnyQuery.java    |   4 +-
 .../syncope/common/rest/api/beans/RealmQuery.java  |  30 +-
 .../syncope/core/logic/ReconciliationLogic.java    |   2 +-
 .../syncope/core/rest/cxf/IdMRESTCXFContext.java   |   4 +-
 .../cxf/service/ReconciliationServiceImpl.java     |  10 +-
 .../org/apache/syncope/core/logic/GroupLogic.java  |   2 +-
 .../org/apache/syncope/core/logic/RealmLogic.java  |  11 +-
 .../org/apache/syncope/core/logic/UserLogic.java   |   2 +-
 .../core/rest/cxf/IdRepoRESTCXFContext.java        |  13 +-
 .../core/rest/cxf/SyncopeOpenApiCustomizer.java    |   4 +-
 .../core/rest/cxf/service/AbstractAnyService.java  |   9 +-
 .../rest/cxf/service/AbstractSearchService.java    |  11 +-
 .../rest/cxf/service/AnyObjectServiceImpl.java     |   4 +-
 .../core/rest/cxf/service/GroupServiceImpl.java    |   4 +-
 .../core/rest/cxf/service/RealmServiceImpl.java    |  30 +-
 .../core/rest/cxf/service/UserServiceImpl.java     |   4 +-
 .../rest/cxf/service/AnyObjectServiceTest.java     |   4 +-
 .../syncope/core/persistence/api/dao/AnyDAO.java   |  13 -
 .../core/persistence/api/dao/AnySearchDAO.java     |  36 +-
 .../core/persistence/api/dao/RealmSearchDAO.java   |  36 +-
 .../api/search/AnySearchCondVisitor.java           |  31 +
 .../api/search/RealmSearchCondVisitor.java         |  31 +
 .../api/search/SearchCondConverter.java            |   7 +-
 ....java => SyncopeAbstractSearchCondVisitor.java} |  22 +-
 .../api/search/SearchCondConverterTest.java        |   2 +-
 .../common/CommonPersistenceContext.java           |  21 +-
 .../common/dao/AbstractAnySearchDAO.java           | 193 +------
 .../common/dao/AbstractRealmSearchDAO.java         | 110 ++++
 .../persistence/common/dao/AbstractSearchDAO.java  | 312 ++++++++++
 .../core/persistence/common/dao/AnyFinder.java     | 155 -----
 .../persistence/jpa/MariaDBPersistenceContext.java |  26 +-
 .../persistence/jpa/MySQLPersistenceContext.java   |  26 +-
 .../persistence/jpa/OraclePersistenceContext.java  |  26 +-
 .../core/persistence/jpa/PGPersistenceContext.java |  26 +-
 .../core/persistence/jpa/PersistenceContext.java   |  26 +-
 .../jpa/content/XMLContentExporter.java            |   3 +-
 .../jpa/dao/AbstractJPAAnySearchDAO.java           | 171 +-----
 .../jpa/dao/AbstractJPARealmSearchDAO.java         | 541 ++++++++++++++++++
 .../core/persistence/jpa/dao/JPARealmDAO.java      |   2 +-
 .../persistence/jpa/dao/JPARealmSearchDAO.java     | 190 -------
 .../core/persistence/jpa/dao/JPATaskDAO.java       |   2 +-
 .../jpa/dao/MariaDBJPAAnySearchDAO.java            |   3 -
 ...earchDAO.java => MariaDBJPARealmSearchDAO.java} |  30 +-
 .../persistence/jpa/dao/MySQLJPAAnySearchDAO.java  |   3 -
 ...ySearchDAO.java => MySQLJPARealmSearchDAO.java} | 170 +++---
 .../persistence/jpa/dao/OracleJPAAnySearchDAO.java |   3 -
 ...SearchDAO.java => OracleJPARealmSearchDAO.java} | 190 ++-----
 .../persistence/jpa/dao/PGJPAAnySearchDAO.java     |   3 -
 ...AAnySearchDAO.java => PGJPARealmSearchDAO.java} | 187 +++---
 .../core/persistence/jpa/dao/RealmSearchNode.java  | 141 +++++
 .../jpa/dao/repo/AbstractAnyRepoExt.java           |  11 -
 .../jpa/dao/repo/AnyObjectRepoExtImpl.java         |   5 +-
 .../core/persistence/jpa/dao/repo/AnyRepoExt.java  |   3 -
 .../persistence/jpa/dao/repo/GroupRepoExtImpl.java |   5 +-
 .../persistence/jpa/dao/repo/UserRepoExtImpl.java  |   5 +-
 .../core/persistence/jpa/entity/JPASRARoute.java   |   4 +-
 .../core/persistence/jpa/inner/AnySearchTest.java  |  37 +-
 .../persistence/jpa/inner/MultitenancyTest.java    |   5 +-
 .../core/persistence/jpa/inner/RealmTest.java      |   4 +-
 .../core/persistence/jpa/inner/UserTest.java       |  31 -
 .../core/persistence/jpa/outer/AnySearchTest.java  |  42 ++
 .../core/persistence/jpa/outer/RealmTest.java      | 130 +++++
 .../core/persistence/jpa/outer/UserTest.java       |  15 +-
 .../core/persistence/neo4j/PersistenceContext.java |  32 +-
 .../persistence/neo4j/dao/Neo4jAnySearchDAO.java   |  47 +-
 .../core/persistence/neo4j/dao/Neo4jRealmDAO.java  |   2 +-
 .../persistence/neo4j/dao/Neo4jRealmSearchDAO.java | 631 +++++++++++++++++++--
 .../core/persistence/neo4j/dao/Neo4jTaskDAO.java   |   2 +-
 .../neo4j/dao/repo/AbstractAnyRepoExt.java         |  10 -
 .../neo4j/dao/repo/AnyObjectRepoExtImpl.java       |   3 -
 .../persistence/neo4j/dao/repo/AnyRepoExt.java     |   2 -
 .../neo4j/dao/repo/GroupRepoExtImpl.java           |   5 +-
 .../neo4j/dao/repo/RoleRepoExtImpl.java            |   5 -
 .../neo4j/dao/repo/UserRepoExtImpl.java            |   3 -
 .../persistence/neo4j/entity/Neo4jSRARoute.java    |   4 +-
 .../persistence/neo4j/inner/AnySearchTest.java     |  37 +-
 .../persistence/neo4j/inner/MultitenancyTest.java  |   5 +-
 .../core/persistence/neo4j/inner/RealmTest.java    |  13 +-
 .../core/persistence/neo4j/inner/UserTest.java     |  31 -
 .../persistence/neo4j/outer/AnySearchTest.java     |  40 ++
 .../core/persistence/neo4j/outer/RealmTest.java    | 126 ++++
 .../core/persistence/neo4j/outer/UserTest.java     |  15 +-
 .../provisioning/java/ProvisioningContext.java     |  15 +-
 .../java/data/FIQLQueryDataBinderImpl.java         |   6 +-
 .../java/data/GroupDataBinderImpl.java             |   6 +-
 .../provisioning/java/data/RoleDataBinderImpl.java |   7 +-
 .../notification/DefaultNotificationManager.java   |   6 +-
 .../provisioning/java/pushpull/InboundMatcher.java |  86 ++-
 .../java/pushpull/PushJobDelegate.java             |  11 +-
 .../syncope/core/starter/SelfKeymasterContext.java |   2 +-
 .../elasticsearch/client/ElasticsearchUtils.java   |  25 +-
 .../ElasticsearchPersistenceContext.java           |  15 +-
 .../dao/ElasticsearchAnySearchDAO.java             |  12 +-
 .../dao/ElasticsearchRealmSearchDAO.java           | 314 ++++++++--
 .../dao/ElasticsearchAnySearchDAOTest.java         |   3 +-
 .../dao/ElasticsearchRealmSearchDAOTest.java       | 216 +++++++
 .../ext/opensearch/client/OpenSearchUtils.java     |  25 +-
 .../opensearch/OpenSearchPersistenceContext.java   |  15 +-
 .../opensearch/dao/OpenSearchAnySearchDAO.java     |  12 +-
 .../opensearch/dao/OpenSearchRealmSearchDAO.java   | 317 +++++++++--
 .../opensearch/dao/OpenSearchAnySearchDAOTest.java |   3 +-
 .../dao/OpenSearchRealmSearchDAOTest.java          | 216 +++++++
 .../syncope/fit/core/reference/TestCommand.java    |   2 +-
 .../org/apache/syncope/fit/AbstractITCase.java     |   6 +-
 .../syncope/fit/core/MultitenancyITCase.java       |   4 +-
 .../org/apache/syncope/fit/core/RealmITCase.java   | 165 +++++-
 .../org/apache/syncope/fit/core/UserITCase.java    |   2 +-
 .../apache/syncope/fit/core/UserIssuesITCase.java  |   7 +-
 122 files changed, 4228 insertions(+), 1615 deletions(-)

diff --git a/client/am/console/src/main/java/org/apache/syncope/client/console/clientapps/ClientAppModalPanelBuilder.java b/client/am/console/src/main/java/org/apache/syncope/client/console/clientapps/ClientAppModalPanelBuilder.java
index 7c2e2532b0..10044edcd2 100644
--- a/client/am/console/src/main/java/org/apache/syncope/client/console/clientapps/ClientAppModalPanelBuilder.java
+++ b/client/am/console/src/main/java/org/apache/syncope/client/console/clientapps/ClientAppModalPanelBuilder.java
@@ -196,7 +196,7 @@ public class ClientAppModalPanelBuilder<T extends ClientAppTO> extends AbstractM
                 protected Iterator<String> getChoices(final String input) {
                     return realmRestClient.search(fullRealmsTree
                             ? RealmsUtils.buildBaseQuery()
-                            : RealmsUtils.buildKeywordQuery(input)).getResult().stream().
+                            : RealmsUtils.buildNameQuery(input)).getResult().stream().
                             map(RealmTO::getFullPath).iterator();
                 }
             };
diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/panels/ConnObjectListViewPanel.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/panels/ConnObjectListViewPanel.java
index b305b96819..b059363ee5 100644
--- a/client/idm/console/src/main/java/org/apache/syncope/client/console/panels/ConnObjectListViewPanel.java
+++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/panels/ConnObjectListViewPanel.java
@@ -161,8 +161,7 @@ public abstract class ConnObjectListViewPanel extends Panel {
 
         List<ConnObject> listOfItems = reloadItems(resource.getKey(), anyType, null, null);
 
-        ListViewPanel.Builder<ConnObject> builder = new ListViewPanel.Builder<>(
-                ConnObject.class, pageRef) {
+        ListViewPanel.Builder<ConnObject> builder = new ListViewPanel.Builder<>(ConnObject.class, pageRef) {
 
             private static final long serialVersionUID = -8251750413385566738L;
 
diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ReconTaskPanel.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ReconTaskPanel.java
index 9851476317..0f656fd100 100644
--- a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ReconTaskPanel.java
+++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ReconTaskPanel.java
@@ -141,7 +141,7 @@ public class ReconTaskPanel extends MultilevelPanel.SecondLevel {
                     return (RealmsUtils.checkInput(input)
                             ? (realmRestClient.search(fullRealmsTree
                                     ? RealmsUtils.buildBaseQuery()
-                                    : RealmsUtils.buildKeywordQuery(input)).getResult())
+                                    : RealmsUtils.buildNameQuery(input)).getResult())
                             : List.<RealmTO>of()).stream().
                             map(RealmTO::getFullPath).iterator();
                 }
diff --git a/client/idm/console/src/main/java/org/apache/syncope/client/console/wizards/resources/ConnectorDetailsPanel.java b/client/idm/console/src/main/java/org/apache/syncope/client/console/wizards/resources/ConnectorDetailsPanel.java
index 1918fa55d6..90274ea233 100644
--- a/client/idm/console/src/main/java/org/apache/syncope/client/console/wizards/resources/ConnectorDetailsPanel.java
+++ b/client/idm/console/src/main/java/org/apache/syncope/client/console/wizards/resources/ConnectorDetailsPanel.java
@@ -69,7 +69,7 @@ public class ConnectorDetailsPanel extends WizardStep {
                 return (RealmsUtils.checkInput(input)
                         ? (realmRestClient.search(fullRealmsTree
                                 ? RealmsUtils.buildBaseQuery()
-                                : RealmsUtils.buildKeywordQuery(input)).getResult())
+                                : RealmsUtils.buildNameQuery(input)).getResult())
                         : List.<RealmTO>of()).stream().
                         map(RealmTO::getFullPath).iterator();
             }
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/RealmsUtils.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/RealmsUtils.java
index c578e59425..6a4375fd94 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/RealmsUtils.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/RealmsUtils.java
@@ -38,8 +38,8 @@ public final class RealmsUtils {
         return StringUtils.isNotBlank(input) && !"*".equals(input);
     }
 
-    public static RealmQuery buildKeywordQuery(final String input) {
-        return new RealmQuery.Builder().keyword(input.contains("*") ? input : "*" + input + "*").build();
+    public static RealmQuery buildNameQuery(final String input) {
+        return new RealmQuery.Builder().fiql("name=~" + (input.contains("*") ? input : "*" + input + "*")).build();
     }
 
     public static RealmQuery buildBaseQuery() {
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/RealmChoicePanel.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/RealmChoicePanel.java
index 9cfeb117fb..f0c88378dc 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/RealmChoicePanel.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/RealmChoicePanel.java
@@ -140,7 +140,7 @@ public class RealmChoicePanel extends Panel {
             String rootRealmName = StringUtils.substringAfterLast(rootRealm, "/");
 
             List<RealmTO> realmTOs = realmRestClient.search(
-                    RealmsUtils.buildKeywordQuery(SyncopeConstants.ROOT_REALM.equals(rootRealm)
+                    RealmsUtils.buildNameQuery(SyncopeConstants.ROOT_REALM.equals(rootRealm)
                             ? SyncopeConstants.ROOT_REALM : rootRealmName)).getResult();
 
             return realmTOs.stream().
@@ -377,7 +377,7 @@ public class RealmChoicePanel extends Panel {
     protected Map<String, Pair<RealmTO, List<RealmTO>>> reloadRealmParentMap() {
         List<RealmTO> realmsToList = realmRestClient.search(fullRealmsTree
                 ? RealmsUtils.buildBaseQuery()
-                : RealmsUtils.buildKeywordQuery(searchQuery)).getResult();
+                : RealmsUtils.buildNameQuery(searchQuery)).getResult();
 
         return reloadRealmParentMap(realmsToList.stream().
                 sorted(Comparator.comparing(RealmTO::getName)).
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder.java
index 6f612e41af..259f40aba6 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder.java
@@ -124,7 +124,7 @@ public class SchedTaskWizardBuilder<T extends SchedTaskTO> extends BaseAjaxWizar
     protected List<String> searchRealms(final String realmQuery) {
         return realmRestClient.search(fullRealmsTree
                 ? RealmsUtils.buildBaseQuery()
-                : RealmsUtils.buildKeywordQuery(realmQuery)).
+                : RealmsUtils.buildNameQuery(realmQuery)).
                 getResult().stream().map(RealmTO::getFullPath).collect(Collectors.toList());
     }
 
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Details.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Details.java
index 478d1d1fae..665e3f487e 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Details.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Details.java
@@ -96,7 +96,7 @@ public class Details<T extends AnyTO> extends WizardStep {
                             ? getRealmsFromLinks(realms.getRealmChoicePanel().getLinks())
                             : (fullRealmsTree
                                     ? realmRestClient.search(RealmsUtils.buildBaseQuery())
-                                    : realmRestClient.search(RealmsUtils.buildKeywordQuery(input))).getResult()).
+                                    : realmRestClient.search(RealmsUtils.buildNameQuery(input))).getResult()).
                             stream().map(RealmTO::getFullPath).
                             filter(fullPath -> authRealms.stream().anyMatch(
                             authRealm -> fullPath.startsWith(authRealm))).iterator();
diff --git a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/role/RoleWizardBuilder.java b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/role/RoleWizardBuilder.java
index d0b1eaf89c..11818c3251 100644
--- a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/role/RoleWizardBuilder.java
+++ b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/role/RoleWizardBuilder.java
@@ -160,7 +160,7 @@ public class RoleWizardBuilder extends BaseAjaxWizardBuilder<RoleWrapper> {
                 protected Iterator<String> getChoices(final String input) {
                     return realmRestClient.search(fullRealmsTree
                             ? RealmsUtils.buildBaseQuery()
-                            : RealmsUtils.buildKeywordQuery(input)).getResult().stream().
+                            : RealmsUtils.buildNameQuery(input)).getResult().stream().
                             map(RealmTO::getFullPath).iterator();
                 }
             };
diff --git a/client/idrepo/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClient.java b/client/idrepo/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClient.java
index 1d4b530059..7b64960003 100644
--- a/client/idrepo/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClient.java
+++ b/client/idrepo/lib/src/main/java/org/apache/syncope/client/lib/SyncopeClient.java
@@ -46,6 +46,7 @@ import org.apache.syncope.common.lib.search.AnyObjectFiqlSearchConditionBuilder;
 import org.apache.syncope.common.lib.search.ConnObjectTOFiqlSearchConditionBuilder;
 import org.apache.syncope.common.lib.search.GroupFiqlSearchConditionBuilder;
 import org.apache.syncope.common.lib.search.OrderByClauseBuilder;
+import org.apache.syncope.common.lib.search.RealmFiqlSearchConditionBuilder;
 import org.apache.syncope.common.lib.search.UserFiqlSearchConditionBuilder;
 import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.rest.api.Preference;
@@ -76,6 +77,15 @@ public class SyncopeClient {
 
     }
 
+    /**
+     * Returns a new instance of {@link RealmFiqlSearchConditionBuilder}, for assisted building of FIQL queries.
+     *
+     * @return default instance of {@link RealmFiqlSearchConditionBuilder}
+     */
+    public static RealmFiqlSearchConditionBuilder getRealmFiqlSearchConditionBuilder() {
+        return new RealmFiqlSearchConditionBuilder();
+    }
+
     /**
      * Returns a new instance of {@link UserFiqlSearchConditionBuilder}, for assisted building of FIQL queries.
      *
diff --git a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/RealmCompleteCondition.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/RealmCompleteCondition.java
new file mode 100644
index 0000000000..49976b2e77
--- /dev/null
+++ b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/RealmCompleteCondition.java
@@ -0,0 +1,22 @@
+/*
+ * 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.syncope.common.lib.search;
+
+public interface RealmCompleteCondition extends SyncopeCompleteCondition<RealmPartialCondition, RealmProperty> {
+}
diff --git a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/RealmFiqlSearchConditionBuilder.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/RealmFiqlSearchConditionBuilder.java
new file mode 100644
index 0000000000..7ee9d31938
--- /dev/null
+++ b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/RealmFiqlSearchConditionBuilder.java
@@ -0,0 +1,50 @@
+/*
+ * 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.syncope.common.lib.search;
+
+import java.util.Map;
+
+public class RealmFiqlSearchConditionBuilder
+        extends AbstractFiqlSearchConditionBuilder<RealmProperty, RealmPartialCondition, RealmCompleteCondition> {
+
+    private static final long serialVersionUID = 324753886224642253L;
+
+    protected static class Builder extends AbstractFiqlSearchConditionBuilder.Builder<
+            RealmProperty, RealmPartialCondition, RealmCompleteCondition>
+            implements RealmProperty, RealmPartialCondition, RealmCompleteCondition {
+
+        public Builder(final Map<String, String> properties) {
+            super(properties);
+        }
+
+        public Builder(final RealmFiqlSearchConditionBuilder.Builder parent) {
+            super(parent);
+        }
+    }
+
+    @Override
+    protected Builder newBuilderInstance() {
+        return new Builder(properties);
+    }
+
+    @Override
+    public RealmProperty is(final String property) {
+        return newBuilderInstance().is(property);
+    }
+}
diff --git a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/RealmPartialCondition.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/RealmPartialCondition.java
new file mode 100644
index 0000000000..dda92a61d9
--- /dev/null
+++ b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/RealmPartialCondition.java
@@ -0,0 +1,22 @@
+/*
+ * 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.syncope.common.lib.search;
+
+public interface RealmPartialCondition extends SyncopePartialCondition<RealmProperty, RealmCompleteCondition> {
+}
diff --git a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/RealmProperty.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/RealmProperty.java
new file mode 100644
index 0000000000..1cffb03f28
--- /dev/null
+++ b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/search/RealmProperty.java
@@ -0,0 +1,22 @@
+/*
+ * 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.syncope.common.lib.search;
+
+public interface RealmProperty extends SyncopeProperty<RealmCompleteCondition> {
+}
diff --git a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/AnyQuery.java b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/AnyQuery.java
index 5d13ea2b8b..893daba106 100644
--- a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/AnyQuery.java
+++ b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/AnyQuery.java
@@ -74,7 +74,7 @@ public class AnyQuery extends AbstractQuery {
             + "primarily meant for containing Users, Groups and Any Objects", schema =
             @Schema(implementation = String.class, defaultValue = SyncopeConstants.ROOT_REALM, externalDocs =
                     @ExternalDocumentation(description = "Apache Syncope Reference Guide",
-                            url = "https://syncope.apache.org/docs/3.0/reference-guide.html#realms")))
+                            url = "https://syncope.apache.org/docs/4.0/reference-guide.html#realms")))
     public String getRealm() {
         return realm;
     }
@@ -120,7 +120,7 @@ public class AnyQuery extends AbstractQuery {
             + "feed.", example = "username==rossini", schema =
             @Schema(implementation = String.class, externalDocs =
                     @ExternalDocumentation(description = "Apache Syncope Reference Guide",
-                            url = "https://syncope.apache.org/docs/3.0/reference-guide.html#search")))
+                            url = "https://syncope.apache.org/docs/4.0/reference-guide.html#search")))
     @QueryParam(JAXRSService.PARAM_FIQL)
     public void setFiql(final String fiql) {
         this.fiql = fiql;
diff --git a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/RealmQuery.java b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/RealmQuery.java
index badd154610..2d23920a84 100644
--- a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/RealmQuery.java
+++ b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/RealmQuery.java
@@ -18,6 +18,9 @@
  */
 package org.apache.syncope.common.rest.api.beans;
 
+import io.swagger.v3.oas.annotations.ExternalDocumentation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.ws.rs.QueryParam;
 import java.util.Collection;
 import java.util.HashSet;
@@ -27,6 +30,7 @@ import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.apache.commons.lang3.builder.EqualsBuilder;
 import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.syncope.common.rest.api.service.JAXRSService;
 
 public class RealmQuery extends AbstractQuery {
 
@@ -39,8 +43,8 @@ public class RealmQuery extends AbstractQuery {
             return new RealmQuery();
         }
 
-        public Builder keyword(final String keyword) {
-            getInstance().setKeyword(keyword);
+        public Builder fiql(final String fiql) {
+            getInstance().setFiql(fiql);
             return this;
         }
 
@@ -63,17 +67,23 @@ public class RealmQuery extends AbstractQuery {
         }
     }
 
-    private String keyword;
+    private String fiql;
 
     private Set<String> bases;
 
-    public String getKeyword() {
-        return keyword;
+    public String getFiql() {
+        return fiql;
     }
 
-    @QueryParam("keyword")
-    public void setKeyword(final String keyword) {
-        this.keyword = keyword;
+    @Parameter(name = JAXRSService.PARAM_FIQL, description = "Feed Item Query Language (FIQL, pronounced “fickle”) is "
+            + "a simple but flexible, URI-friendly syntax for expressing filters across the entries in a syndicated "
+            + "feed.", example = "name==department1", schema =
+            @Schema(implementation = String.class, externalDocs =
+                    @ExternalDocumentation(description = "Apache Syncope Reference Guide",
+                            url = "https://syncope.apache.org/docs/4.0/reference-guide.html#search")))
+    @QueryParam(JAXRSService.PARAM_FIQL)
+    public void setFiql(final String fiql) {
+        this.fiql = fiql;
     }
 
     public Set<String> getBases() {
@@ -99,7 +109,7 @@ public class RealmQuery extends AbstractQuery {
         RealmQuery other = (RealmQuery) obj;
         return new EqualsBuilder().
                 appendSuper(super.equals(obj)).
-                append(keyword, other.keyword).
+                append(fiql, other.fiql).
                 append(bases, other.bases).
                 build();
     }
@@ -108,7 +118,7 @@ public class RealmQuery extends AbstractQuery {
     public int hashCode() {
         return new HashCodeBuilder().
                 appendSuper(super.hashCode()).
-                append(keyword).
+                append(fiql).
                 append(bases).
                 build();
     }
diff --git a/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ReconciliationLogic.java b/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ReconciliationLogic.java
index ac1b977f30..34a7387592 100644
--- a/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ReconciliationLogic.java
+++ b/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ReconciliationLogic.java
@@ -610,7 +610,7 @@ public class ReconciliationLogic extends AbstractLogic<EntityTO> {
                 orElseThrow(() -> new NotFoundException("Realm " + realm));
 
         Set<String> adminRealms = RealmUtils.getEffective(AuthContextUtils.getAuthorizations().get(entitlement), realm);
-        SearchCond effectiveCond = searchCond == null ? anyUtils.dao().getAllMatchingCond() : searchCond;
+        SearchCond effectiveCond = searchCond == null ? anySearchDAO.getAllMatchingCond() : searchCond;
 
         List<Any> matching;
         if (spec.getIgnorePaging()) {
diff --git a/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/IdMRESTCXFContext.java b/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/IdMRESTCXFContext.java
index bf74c2ee2a..70d773f86b 100644
--- a/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/IdMRESTCXFContext.java
+++ b/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/IdMRESTCXFContext.java
@@ -27,7 +27,7 @@ import org.apache.syncope.core.logic.ReconciliationLogic;
 import org.apache.syncope.core.logic.RemediationLogic;
 import org.apache.syncope.core.logic.ResourceLogic;
 import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
-import org.apache.syncope.core.persistence.api.search.SearchCondVisitor;
+import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor;
 import org.apache.syncope.core.rest.cxf.service.ConnectorServiceImpl;
 import org.apache.syncope.core.rest.cxf.service.ReconciliationServiceImpl;
 import org.apache.syncope.core.rest.cxf.service.RemediationServiceImpl;
@@ -48,7 +48,7 @@ public class IdMRESTCXFContext {
     @ConditionalOnMissingBean
     @Bean
     public ReconciliationService reconciliationService(
-            final SearchCondVisitor searchCondVisitor,
+            final AnySearchCondVisitor searchCondVisitor,
             final ReconciliationLogic reconciliationLogic) {
 
         return new ReconciliationServiceImpl(searchCondVisitor, reconciliationLogic);
diff --git a/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ReconciliationServiceImpl.java b/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ReconciliationServiceImpl.java
index 43480eab2b..7cc4829b40 100644
--- a/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ReconciliationServiceImpl.java
+++ b/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ReconciliationServiceImpl.java
@@ -48,16 +48,18 @@ import org.apache.syncope.common.rest.api.beans.ReconQuery;
 import org.apache.syncope.common.rest.api.service.ReconciliationService;
 import org.apache.syncope.core.logic.ReconciliationLogic;
 import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
+import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor;
 import org.apache.syncope.core.persistence.api.search.FilterVisitor;
-import org.apache.syncope.core.persistence.api.search.SearchCondVisitor;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
 import org.identityconnectors.framework.common.objects.filter.Filter;
 
-public class ReconciliationServiceImpl extends AbstractSearchService implements ReconciliationService {
+public class ReconciliationServiceImpl
+        extends AbstractSearchService<AnySearchCondVisitor>
+        implements ReconciliationService {
 
     protected final ReconciliationLogic logic;
 
-    public ReconciliationServiceImpl(final SearchCondVisitor searchCondVisitor, final ReconciliationLogic logic) {
+    public ReconciliationServiceImpl(final AnySearchCondVisitor searchCondVisitor, final ReconciliationLogic logic) {
         super(searchCondVisitor);
         this.logic = logic;
     }
@@ -150,7 +152,7 @@ public class ReconciliationServiceImpl extends AbstractSearchService implements
 
         SearchCond searchCond = StringUtils.isBlank(query.getFiql())
                 ? null
-                : getSearchCond(query.getFiql(), realm);
+                : getSearchCond(query.getFiql());
 
         StreamingOutput sout = os -> logic.push(
                 searchCond,
diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/GroupLogic.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/GroupLogic.java
index e9b26d2528..cadacdb12d 100644
--- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/GroupLogic.java
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/GroupLogic.java
@@ -172,7 +172,7 @@ public class GroupLogic extends AbstractAnyLogic<GroupTO, GroupCR, GroupUR> {
         Set<String> authRealms = RealmUtils.getEffective(
                 AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.GROUP_SEARCH), realm);
 
-        SearchCond effectiveCond = searchCond == null ? groupDAO.getAllMatchingCond() : searchCond;
+        SearchCond effectiveCond = searchCond == null ? searchDAO.getAllMatchingCond() : searchCond;
 
         long count = searchDAO.count(base, recursive, authRealms, effectiveCond, AnyTypeKind.GROUP);
 
diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/RealmLogic.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/RealmLogic.java
index ae2cdeaf9c..bf297dac35 100644
--- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/RealmLogic.java
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/RealmLogic.java
@@ -121,8 +121,8 @@ public class RealmLogic extends AbstractTransactionalLogic<RealmTO> {
     @PreAuthorize("isAuthenticated()")
     @Transactional(readOnly = true)
     public Page<RealmTO> search(
-            final String keyword,
             final Set<String> bases,
+            final SearchCond searchCond,
             final Pageable pageable) {
 
         Set<String> baseRealms = new HashSet<>();
@@ -135,14 +135,15 @@ public class RealmLogic extends AbstractTransactionalLogic<RealmTO> {
             }
         }
 
-        long count = realmSearchDAO.countDescendants(baseRealms, keyword);
+        SearchCond effectiveCond = searchCond == null ? realmSearchDAO.getAllMatchingCond() : searchCond;
+
+        long count = realmSearchDAO.count(baseRealms, effectiveCond);
 
         Set<String> authorizations = AuthContextUtils.getAuthorizations().
                 getOrDefault(IdRepoEntitlement.REALM_SEARCH, Set.of());
-        List<RealmTO> result = realmSearchDAO.findDescendants(baseRealms, keyword, pageable).stream().
+        List<RealmTO> result = realmSearchDAO.search(baseRealms, effectiveCond, pageable).stream().
                 map(realm -> binder.getRealmTO(
-                realm, authorizations.stream().
-                        anyMatch(auth -> realm.getFullPath().startsWith(auth)))).
+                realm, authorizations.stream().anyMatch(auth -> realm.getFullPath().startsWith(auth)))).
                 sorted(Comparator.comparing(RealmTO::getFullPath)).
                 toList();
 
diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java
index 83c335bf37..c93a2d8ac1 100644
--- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java
@@ -179,7 +179,7 @@ public class UserLogic extends AbstractAnyLogic<UserTO, UserCR, UserUR> {
         Set<String> authRealms = RealmUtils.getEffective(
                 AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.USER_SEARCH), realm);
 
-        SearchCond effectiveCond = searchCond == null ? userDAO.getAllMatchingCond() : searchCond;
+        SearchCond effectiveCond = searchCond == null ? searchDAO.getAllMatchingCond() : searchCond;
 
         long count = searchDAO.count(base, recursive, authRealms, effectiveCond, AnyTypeKind.USER);
 
diff --git a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/IdRepoRESTCXFContext.java b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/IdRepoRESTCXFContext.java
index 2615ade450..806ec79c36 100644
--- a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/IdRepoRESTCXFContext.java
+++ b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/IdRepoRESTCXFContext.java
@@ -95,7 +95,8 @@ import org.apache.syncope.core.persistence.api.dao.BatchDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.core.persistence.api.entity.EntityFactory;
-import org.apache.syncope.core.persistence.api.search.SearchCondVisitor;
+import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor;
+import org.apache.syncope.core.persistence.api.search.RealmSearchCondVisitor;
 import org.apache.syncope.core.rest.cxf.service.AccessTokenServiceImpl;
 import org.apache.syncope.core.rest.cxf.service.AnyObjectServiceImpl;
 import org.apache.syncope.core.rest.cxf.service.AnyTypeClassServiceImpl;
@@ -324,7 +325,7 @@ public class IdRepoRESTCXFContext {
     @ConditionalOnMissingBean
     @Bean
     public AnyObjectService anyObjectService(final AnyObjectDAO anyObjectDAO, final AnyObjectLogic anyObjectLogic,
-            final SearchCondVisitor searchCondVisitor) {
+            final AnySearchCondVisitor searchCondVisitor) {
         return new AnyObjectServiceImpl(searchCondVisitor, anyObjectDAO, anyObjectLogic);
     }
 
@@ -367,7 +368,7 @@ public class IdRepoRESTCXFContext {
     @ConditionalOnMissingBean
     @Bean
     public GroupService groupService(final GroupDAO groupDAO, final GroupLogic groupLogic,
-            final SearchCondVisitor searchCondVisitor) {
+            final AnySearchCondVisitor searchCondVisitor) {
         return new GroupServiceImpl(searchCondVisitor, groupDAO, groupLogic);
     }
 
@@ -397,8 +398,8 @@ public class IdRepoRESTCXFContext {
 
     @ConditionalOnMissingBean
     @Bean
-    public RealmService realmService(final RealmLogic realmLogic) {
-        return new RealmServiceImpl(realmLogic);
+    public RealmService realmService(final RealmLogic realmLogic, final RealmSearchCondVisitor searchCondVisitor) {
+        return new RealmServiceImpl(realmLogic, searchCondVisitor);
     }
 
     @ConditionalOnMissingBean
@@ -463,7 +464,7 @@ public class IdRepoRESTCXFContext {
             final UserDAO userDAO,
             final UserLogic userLogic,
             final SyncopeLogic syncopeLogic,
-            final SearchCondVisitor searchCondVisitor) {
+            final AnySearchCondVisitor searchCondVisitor) {
 
         return new UserServiceImpl(searchCondVisitor, userDAO, userLogic, syncopeLogic);
     }
diff --git a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/SyncopeOpenApiCustomizer.java b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/SyncopeOpenApiCustomizer.java
index 9c1850e23d..3c0fe35c87 100644
--- a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/SyncopeOpenApiCustomizer.java
+++ b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/SyncopeOpenApiCustomizer.java
@@ -92,7 +92,7 @@ public class SyncopeOpenApiCustomizer extends OpenApiCustomizer {
 
             ExternalDocumentation extDoc = new ExternalDocumentation();
             extDoc.setDescription("Apache Syncope Reference Guide");
-            extDoc.setUrl("https://syncope.apache.org/docs/3.0/reference-guide.html#domains");
+            extDoc.setUrl("https://syncope.apache.org/docs/4.0/reference-guide.html#domains");
 
             Schema<String> schema = new Schema<>();
             schema.setDescription("Domains are built to facilitate multitenancy.");
@@ -113,7 +113,7 @@ public class SyncopeOpenApiCustomizer extends OpenApiCustomizer {
 
             ExternalDocumentation extDoc = new ExternalDocumentation();
             extDoc.setDescription("Apache Syncope Reference Guide");
-            extDoc.setUrl("https://syncope.apache.org/docs/3.0/reference-guide.html#delegation");
+            extDoc.setUrl("https://syncope.apache.org/docs/4.0/reference-guide.html#delegation");
 
             Schema<String> schema = new Schema<>();
             schema.setDescription("Acton behalf of someone else");
diff --git a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AbstractAnyService.java b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AbstractAnyService.java
index 5137b272e8..3e98e2e49f 100644
--- a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AbstractAnyService.java
+++ b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AbstractAnyService.java
@@ -54,15 +54,16 @@ import org.apache.syncope.core.logic.AbstractAnyLogic;
 import org.apache.syncope.core.persistence.api.dao.AnyDAO;
 import org.apache.syncope.core.persistence.api.dao.NotFoundException;
 import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
-import org.apache.syncope.core.persistence.api.search.SearchCondVisitor;
+import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor;
 import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
 import org.apache.syncope.core.spring.security.SecureRandomUtils;
 import org.springframework.data.domain.Page;
 
 public abstract class AbstractAnyService<TO extends AnyTO, CR extends AnyCR, UR extends AnyUR>
-        extends AbstractSearchService implements AnyService<TO> {
+        extends AbstractSearchService<AnySearchCondVisitor>
+        implements AnyService<TO> {
 
-    public AbstractAnyService(final SearchCondVisitor searchCondVisitor) {
+    public AbstractAnyService(final AnySearchCondVisitor searchCondVisitor) {
         super(searchCondVisitor);
     }
 
@@ -117,7 +118,7 @@ public abstract class AbstractAnyService<TO extends AnyTO, CR extends AnyCR, UR
         String realm = Strings.CS.prependIfMissing(anyQuery.getRealm(), SyncopeConstants.ROOT_REALM);
         SearchCond searchCond = StringUtils.isBlank(anyQuery.getFiql())
                 ? null
-                : getSearchCond(anyQuery.getFiql(), realm);
+                : getSearchCond(anyQuery.getFiql());
         try {
             Page<TO> result = getAnyLogic().search(
                     searchCond,
diff --git a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AbstractSearchService.java b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AbstractSearchService.java
index 97afca154d..4e6ce53835 100644
--- a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AbstractSearchService.java
+++ b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AbstractSearchService.java
@@ -24,19 +24,18 @@ import org.apache.cxf.jaxrs.ext.search.SearchCondition;
 import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
-import org.apache.syncope.core.persistence.api.search.SearchCondVisitor;
+import org.apache.syncope.core.persistence.api.search.SyncopeAbstractSearchCondVisitor;
 
-public abstract class AbstractSearchService extends AbstractService {
+public abstract class AbstractSearchService<V extends SyncopeAbstractSearchCondVisitor> extends AbstractService {
 
-    protected final SearchCondVisitor searchCondVisitor;
+    protected final V searchCondVisitor;
 
-    public AbstractSearchService(final SearchCondVisitor searchCondVisitor) {
+    public AbstractSearchService(final V searchCondVisitor) {
         this.searchCondVisitor = searchCondVisitor;
     }
 
-    protected SearchCond getSearchCond(final String fiql, final String realm) {
+    protected SearchCond getSearchCond(final String fiql) {
         try {
-            searchCondVisitor.setRealm(realm);
             SearchCondition<SearchBean> sc = searchContext.getCondition(fiql, SearchBean.class);
             sc.accept(searchCondVisitor);
 
diff --git a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AnyObjectServiceImpl.java b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AnyObjectServiceImpl.java
index 8a6adf8278..cb3e923470 100644
--- a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AnyObjectServiceImpl.java
+++ b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AnyObjectServiceImpl.java
@@ -34,7 +34,7 @@ import org.apache.syncope.core.logic.AbstractAnyLogic;
 import org.apache.syncope.core.logic.AnyObjectLogic;
 import org.apache.syncope.core.persistence.api.dao.AnyDAO;
 import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
-import org.apache.syncope.core.persistence.api.search.SearchCondVisitor;
+import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor;
 
 public class AnyObjectServiceImpl extends AbstractAnyService<AnyObjectTO, AnyObjectCR, AnyObjectUR>
         implements AnyObjectService {
@@ -44,7 +44,7 @@ public class AnyObjectServiceImpl extends AbstractAnyService<AnyObjectTO, AnyObj
     protected final AnyObjectLogic logic;
 
     public AnyObjectServiceImpl(
-            final SearchCondVisitor searchCondVisitor,
+            final AnySearchCondVisitor searchCondVisitor,
             final AnyObjectDAO anyObjectDAO,
             final AnyObjectLogic logic) {
 
diff --git a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/GroupServiceImpl.java b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/GroupServiceImpl.java
index ecac8f0f82..5faa4f5472 100644
--- a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/GroupServiceImpl.java
+++ b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/GroupServiceImpl.java
@@ -31,7 +31,7 @@ import org.apache.syncope.core.logic.AbstractAnyLogic;
 import org.apache.syncope.core.logic.GroupLogic;
 import org.apache.syncope.core.persistence.api.dao.AnyDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
-import org.apache.syncope.core.persistence.api.search.SearchCondVisitor;
+import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor;
 
 public class GroupServiceImpl extends AbstractAnyService<GroupTO, GroupCR, GroupUR> implements GroupService {
 
@@ -40,7 +40,7 @@ public class GroupServiceImpl extends AbstractAnyService<GroupTO, GroupCR, Group
     protected final GroupLogic logic;
 
     public GroupServiceImpl(
-            final SearchCondVisitor searchCondVisitor,
+            final AnySearchCondVisitor searchCondVisitor,
             final GroupDAO groupDAO,
             final GroupLogic logic) {
 
diff --git a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/RealmServiceImpl.java b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/RealmServiceImpl.java
index 866ca7bc24..7d393d70c4 100644
--- a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/RealmServiceImpl.java
+++ b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/RealmServiceImpl.java
@@ -20,33 +20,47 @@ package org.apache.syncope.core.rest.cxf.service;
 
 import jakarta.ws.rs.core.Response;
 import java.net.URI;
-import java.util.Optional;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.Strings;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.to.PagedResult;
 import org.apache.syncope.common.lib.to.ProvisioningResult;
 import org.apache.syncope.common.lib.to.RealmTO;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.common.rest.api.RESTHeaders;
 import org.apache.syncope.common.rest.api.beans.RealmQuery;
 import org.apache.syncope.common.rest.api.service.RealmService;
 import org.apache.syncope.core.logic.RealmLogic;
+import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
+import org.apache.syncope.core.persistence.api.search.RealmSearchCondVisitor;
 import org.springframework.data.domain.Page;
 
-public class RealmServiceImpl extends AbstractService implements RealmService {
+public class RealmServiceImpl extends AbstractSearchService<RealmSearchCondVisitor> implements RealmService {
 
     protected final RealmLogic logic;
 
-    public RealmServiceImpl(final RealmLogic logic) {
+    public RealmServiceImpl(final RealmLogic logic, final RealmSearchCondVisitor searchCondVisitor) {
+        super(searchCondVisitor);
         this.logic = logic;
     }
 
     @Override
     public PagedResult<RealmTO> search(final RealmQuery query) {
-        Page<RealmTO> result = logic.search(
-                Optional.ofNullable(query.getKeyword()).map(k -> k.replace('*', '%')).orElse(null),
-                query.getBases(),
-                pageable(query));
-        return buildPagedResult(result);
+        SearchCond searchCond = StringUtils.isBlank(query.getFiql()) ? null : getSearchCond(query.getFiql());
+        try {
+            Page<RealmTO> result = logic.search(
+                    query.getBases(),
+                    searchCond,
+                    pageable(query));
+            return buildPagedResult(result);
+        } catch (IllegalArgumentException e) {
+            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidSearchParameters);
+            sce.getElements().add(query.getFiql());
+            sce.getElements().add(ExceptionUtils.getRootCauseMessage(e));
+            throw sce;
+        }
     }
 
     @Override
diff --git a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/UserServiceImpl.java b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/UserServiceImpl.java
index 1d34a8ab15..12a89c52b6 100644
--- a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/UserServiceImpl.java
+++ b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/UserServiceImpl.java
@@ -33,7 +33,7 @@ import org.apache.syncope.core.logic.SyncopeLogic;
 import org.apache.syncope.core.logic.UserLogic;
 import org.apache.syncope.core.persistence.api.dao.AnyDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
-import org.apache.syncope.core.persistence.api.search.SearchCondVisitor;
+import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor;
 
 public class UserServiceImpl extends AbstractAnyService<UserTO, UserCR, UserUR> implements UserService {
 
@@ -44,7 +44,7 @@ public class UserServiceImpl extends AbstractAnyService<UserTO, UserCR, UserUR>
     protected final SyncopeLogic syncopeLogic;
 
     public UserServiceImpl(
-            final SearchCondVisitor searchCondVisitor,
+            final AnySearchCondVisitor searchCondVisitor,
             final UserDAO userDAO,
             final UserLogic logic,
             final SyncopeLogic syncopeLogic) {
diff --git a/core/idrepo/rest-cxf/src/test/java/org/apache/syncope/core/rest/cxf/service/AnyObjectServiceTest.java b/core/idrepo/rest-cxf/src/test/java/org/apache/syncope/core/rest/cxf/service/AnyObjectServiceTest.java
index eebbc66863..0714b29869 100644
--- a/core/idrepo/rest-cxf/src/test/java/org/apache/syncope/core/rest/cxf/service/AnyObjectServiceTest.java
+++ b/core/idrepo/rest-cxf/src/test/java/org/apache/syncope/core/rest/cxf/service/AnyObjectServiceTest.java
@@ -66,7 +66,7 @@ import org.apache.syncope.common.rest.api.service.AnyObjectService;
 import org.apache.syncope.core.logic.AnyObjectLogic;
 import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
-import org.apache.syncope.core.persistence.api.search.SearchCondVisitor;
+import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor;
 import org.apache.syncope.core.persistence.api.search.SyncopePage;
 import org.apache.syncope.core.rest.cxf.AddETagFilter;
 import org.apache.syncope.core.rest.cxf.RestServiceExceptionMapper;
@@ -145,7 +145,7 @@ public class AnyObjectServiceTest {
                 return result;
             });
 
-            SearchCondVisitor searchCondVisitor = mock(SearchCondVisitor.class);
+            AnySearchCondVisitor searchCondVisitor = mock(AnySearchCondVisitor.class);
             when(searchCondVisitor.getQuery()).thenReturn(new SearchCond());
 
             @SuppressWarnings("unchecked")
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AnyDAO.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AnyDAO.java
index 9049114eed..053c342951 100644
--- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AnyDAO.java
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AnyDAO.java
@@ -42,19 +42,6 @@ public interface AnyDAO<A extends Any> extends DAO<A> {
 
     A authFind(String key);
 
-    /**
-     * Find any objects by derived attribute value. This method could fail if one or more string literals contained
-     * into the derived attribute value provided derive from identifier (schema key) replacement. When you are going to
-     * specify a derived attribute expression you must be quite sure that string literals used to build the expression
-     * cannot be found into the attribute values used to replace attribute schema keys used as identifiers.
-     *
-     * @param expression JEXL expression
-     * @param value derived attribute value
-     * @param ignoreCaseMatch whether comparison for string values should take case into account or not
-     * @return list of any objects
-     */
-    List<A> findByDerAttrValue(String expression, String value, boolean ignoreCaseMatch);
-
     List<A> findByResourcesContaining(ExternalResource resource);
 
     Page<? extends A> findAll(Pageable pageable);
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AnySearchDAO.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AnySearchDAO.java
index 85dc4faf34..2e0e0b7d91 100644
--- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AnySearchDAO.java
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AnySearchDAO.java
@@ -21,6 +21,8 @@ package org.apache.syncope.core.persistence.api.dao;
 import java.util.List;
 import java.util.Set;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
+import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
 import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
 import org.apache.syncope.core.persistence.api.entity.Any;
 import org.apache.syncope.core.persistence.api.entity.Realm;
@@ -29,12 +31,31 @@ import org.springframework.data.domain.Sort;
 
 public interface AnySearchDAO {
 
+    /**
+     * Find any objects by derived attribute value. This method could fail if one or more string literals contained
+     * into the derived attribute value provided derive from identifier (schema key) replacement. When you are going to
+     * specify a derived attribute expression you must be quite sure that string literals used to build the expression
+     * cannot be found into the attribute values used to replace attribute schema keys used as identifiers.
+     *
+     * @param expression JEXL expression
+     * @param value derived attribute value
+     * @param ignoreCaseMatch whether comparison for string values should take case into account or not
+     * @param kind any type kind
+     * @param <T> any
+     * @return list of any objects
+     */
+    <T extends Any> List<T> findByDerAttrValue(
+            String expression,
+            String value,
+            boolean ignoreCaseMatch,
+            AnyTypeKind kind);
+
     /**
      * @param base Realm to start searching from
      * @param recursive whether search should recursively include results from child Realms
      * @param adminRealms realms for which the caller owns the proper entitlement(s)
      * @param searchCondition the search condition
-     * @param kind any object
+     * @param kind any type kind
      * @return size of search result
      */
     long count(
@@ -55,7 +76,7 @@ public interface AnySearchDAO {
     /**
      * @param searchCondition the search condition
      * @param orderBy list of ordering clauses
-     * @param kind any object
+     * @param kind any type kind
      * @param <T> any
      * @return the list of any objects matching the given search condition
      */
@@ -67,7 +88,7 @@ public interface AnySearchDAO {
      * @param adminRealms realms for which the caller owns the proper entitlement(s)
      * @param searchCondition the search condition
      * @param pageable paging information
-     * @param kind any object
+     * @param kind any type kind
      * @param <T> any
      * @return the list of any objects matching the given search condition (in the given page)
      */
@@ -78,4 +99,13 @@ public interface AnySearchDAO {
             SearchCond searchCondition,
             Pageable pageable,
             AnyTypeKind kind);
+
+    /**
+     * @return the search condition to match all entities
+     */
+    default SearchCond getAllMatchingCond() {
+        AnyCond idCond = new AnyCond(AttrCond.Type.ISNOTNULL);
+        idCond.setSchema("id");
+        return SearchCond.of(idCond);
+    }
 }
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/RealmSearchDAO.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/RealmSearchDAO.java
index 3b3a740b5f..e72e865653 100644
--- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/RealmSearchDAO.java
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/RealmSearchDAO.java
@@ -22,6 +22,9 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
+import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
+import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
+import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
 import org.apache.syncope.core.persistence.api.entity.Realm;
 import org.springframework.data.domain.Pageable;
 
@@ -33,15 +36,7 @@ public interface RealmSearchDAO {
 
     List<Realm> findChildren(Realm realm);
 
-    long countDescendants(String base, String keyword);
-
-    long countDescendants(Set<String> bases, String keyword);
-
-    List<Realm> findDescendants(String base, String keyword, Pageable pageable);
-
-    List<Realm> findDescendants(Set<String> bases, String keyword, Pageable pageable);
-
-    List<String> findDescendants(String base, String prefix);
+    List<Realm> findDescendants(String base, String prefix);
 
     default void findAncestors(final List<Realm> result, final Realm realm) {
         if (realm.getParent() != null && !result.contains(realm.getParent())) {
@@ -56,4 +51,27 @@ public interface RealmSearchDAO {
         findAncestors(result, realm);
         return result;
     }
+
+    /**
+     * Find realmss by derived attribute value. This method could fail if one or more string literals contained
+     * into the derived attribute value provided derive from identifier (schema key) replacement. When you are going to
+     * specify a derived attribute expression you must be quite sure that string literals used to build the expression
+     * cannot be found into the attribute values used to replace attribute schema keys used as identifiers.
+     *
+     * @param expression JEXL expression
+     * @param value derived attribute value
+     * @param ignoreCaseMatch whether comparison for string values should take case into account or not
+     * @return list of realms
+     */
+    List<Realm> findByDerAttrValue(String expression, String value, boolean ignoreCaseMatch);
+
+    long count(Set<String> bases, SearchCond cond);
+
+    List<Realm> search(Set<String> bases, SearchCond cond, Pageable pageable);
+
+    default SearchCond getAllMatchingCond() {
+        AnyCond idCond = new AnyCond(AttrCond.Type.ISNOTNULL);
+        idCond.setSchema("id");
+        return SearchCond.of(idCond);
+    }
 }
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/AnySearchCondVisitor.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/AnySearchCondVisitor.java
new file mode 100644
index 0000000000..d31f7f5995
--- /dev/null
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/AnySearchCondVisitor.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.syncope.core.persistence.api.search;
+
+import java.util.Optional;
+import org.apache.syncope.common.lib.search.SpecialAttr;
+
+public class AnySearchCondVisitor extends SyncopeAbstractSearchCondVisitor {
+
+
+    @Override
+    protected Optional<SpecialAttr> getSpecialAttrName(final String propertyName) {
+        return SpecialAttr.fromString(propertyName);
+    }
+}
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/RealmSearchCondVisitor.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/RealmSearchCondVisitor.java
new file mode 100644
index 0000000000..b31cd96c20
--- /dev/null
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/RealmSearchCondVisitor.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.syncope.core.persistence.api.search;
+
+import java.util.Optional;
+import org.apache.syncope.common.lib.search.SpecialAttr;
+
+public class RealmSearchCondVisitor extends SyncopeAbstractSearchCondVisitor {
+
+
+    @Override
+    protected Optional<SpecialAttr> getSpecialAttrName(final String propertyName) {
+        return Optional.empty();
+    }
+}
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SearchCondConverter.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SearchCondConverter.java
index 83c1486f12..3a8ada5572 100644
--- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SearchCondConverter.java
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SearchCondConverter.java
@@ -37,19 +37,16 @@ public final class SearchCondConverter {
     /**
      * Parses a FIQL expression into Syncope's {@link SearchCond}, using {@link SyncopeFiqlParser}.
      *
+     * @param <V> concrete {@link SyncopeAbstractSearchCondVisitor} implementation
      * @param visitor visitor instance
      * @param fiql FIQL string
-     * @param realms optional realm to provide to {@link SearchCondVisitor}
      * @return {@link SearchCond} instance for given FIQL expression
      */
-    public static SearchCond convert(final SearchCondVisitor visitor, final String fiql, final String... realms) {
+    public static <V extends SyncopeAbstractSearchCondVisitor> SearchCond convert(final V visitor, final String fiql) {
         SyncopeFiqlParser<SearchBean> parser = new SyncopeFiqlParser<>(
                 SearchBean.class, AbstractFiqlSearchConditionBuilder.CONTEXTUAL_PROPERTIES);
 
         try {
-            if (realms != null && realms.length > 0) {
-                visitor.setRealm(realms[0]);
-            }
             SearchCondition<SearchBean> sc = parser.parse(URLDecoder.decode(fiql, StandardCharsets.UTF_8));
             sc.accept(visitor);
 
diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SearchCondVisitor.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SyncopeAbstractSearchCondVisitor.java
similarity index 96%
rename from core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SearchCondVisitor.java
rename to core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SyncopeAbstractSearchCondVisitor.java
index 2f1a5adfae..dd27c91d03 100644
--- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SearchCondVisitor.java
+++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SyncopeAbstractSearchCondVisitor.java
@@ -47,20 +47,11 @@ import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
 /**
  * Visits CXF's {@link SearchBean} and produces {@link SearchCond}.
  */
-public class SearchCondVisitor extends AbstractSearchConditionVisitor<SearchBean, SearchCond> {
-
-    protected static final ThreadLocal<String> REALM = new ThreadLocal<>();
+public abstract class SyncopeAbstractSearchCondVisitor
+        extends AbstractSearchConditionVisitor<SearchBean, SearchCond> {
 
     protected static final ThreadLocal<SearchCond> SEARCH_COND = new ThreadLocal<>();
 
-    public SearchCondVisitor() {
-        super(null);
-    }
-
-    public void setRealm(final String realm) {
-        REALM.set(realm);
-    }
-
     protected static AttrCond createAttrCond(final String schema) {
         AttrCond attrCond = SearchableFields.contains(schema)
                 ? new AnyCond()
@@ -83,6 +74,7 @@ public class SearchCondVisitor extends AbstractSearchConditionVisitor<SearchBean
         ConditionType ct = sc.getConditionType();
         if (sc instanceof final SyncopeFiqlSearchCondition<SearchBean> sfsc
                 && sc.getConditionType() == ConditionType.CUSTOM) {
+
             switch (sfsc.getOperator()) {
                 case SyncopeFiqlParser.IEQ:
                     ct = ConditionType.EQUALS;
@@ -101,10 +93,16 @@ public class SearchCondVisitor extends AbstractSearchConditionVisitor<SearchBean
         return ct;
     }
 
+    protected SyncopeAbstractSearchCondVisitor() {
+        super(null);
+    }
+
+    protected abstract Optional<SpecialAttr> getSpecialAttrName(String propertyName);
+
     @SuppressWarnings("ConvertToStringSwitch")
     protected SearchCond visitPrimitive(final SearchCondition<SearchBean> sc) {
         String name = getRealPropertyName(sc.getStatement().getProperty());
-        Optional<SpecialAttr> specialAttrName = SpecialAttr.fromString(name);
+        Optional<SpecialAttr> specialAttrName = getSpecialAttrName(name);
 
         String value = getValue(sc);
         Optional<SpecialAttr> specialAttrValue = SpecialAttr.fromString(value);
diff --git a/core/persistence-api/src/test/java/org/apache/syncope/core/persistence/api/search/SearchCondConverterTest.java b/core/persistence-api/src/test/java/org/apache/syncope/core/persistence/api/search/SearchCondConverterTest.java
index 34dc473efc..72ce4b59b3 100644
--- a/core/persistence-api/src/test/java/org/apache/syncope/core/persistence/api/search/SearchCondConverterTest.java
+++ b/core/persistence-api/src/test/java/org/apache/syncope/core/persistence/api/search/SearchCondConverterTest.java
@@ -39,7 +39,7 @@ import org.junit.jupiter.api.Test;
 
 public class SearchCondConverterTest {
 
-    private static final SearchCondVisitor VISITOR = new SearchCondVisitor();
+    private static final AnySearchCondVisitor VISITOR = new AnySearchCondVisitor();
 
     @Test
     public void eq() {
diff --git a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/CommonPersistenceContext.java b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/CommonPersistenceContext.java
index 18e524347a..14de720545 100644
--- a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/CommonPersistenceContext.java
+++ b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/CommonPersistenceContext.java
@@ -23,7 +23,6 @@ import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
 import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
-import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmChecker;
@@ -33,11 +32,11 @@ import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
 import org.apache.syncope.core.persistence.api.entity.EntityFactory;
 import org.apache.syncope.core.persistence.api.entity.am.ClientAppUtilsFactory;
 import org.apache.syncope.core.persistence.api.entity.policy.PolicyUtilsFactory;
-import org.apache.syncope.core.persistence.api.search.SearchCondVisitor;
+import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor;
+import org.apache.syncope.core.persistence.api.search.RealmSearchCondVisitor;
 import org.apache.syncope.core.persistence.api.utils.RealmUtils;
 import org.apache.syncope.core.persistence.common.attrvalue.DefaultPlainAttrValidationManager;
 import org.apache.syncope.core.persistence.common.content.KeymasterConfParamLoader;
-import org.apache.syncope.core.persistence.common.dao.AnyFinder;
 import org.apache.syncope.core.persistence.common.entity.DefaultAnyUtils;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -52,8 +51,14 @@ public class CommonPersistenceContext {
 
     @ConditionalOnMissingBean
     @Bean
-    public SearchCondVisitor searchCondVisitor() {
-        return new SearchCondVisitor();
+    public AnySearchCondVisitor anySearchCondVisitor() {
+        return new AnySearchCondVisitor();
+    }
+
+    @ConditionalOnMissingBean
+    @Bean
+    public RealmSearchCondVisitor realmSearchCondVisitor() {
+        return new RealmSearchCondVisitor();
     }
 
     @Bean
@@ -158,12 +163,6 @@ public class CommonPersistenceContext {
         return new KeymasterConfParamLoader(confParamOps, ctx);
     }
 
-    @ConditionalOnMissingBean
-    @Bean
-    public AnyFinder anyFinder(final @Lazy PlainSchemaDAO plainSchemaDAO, final @Lazy AnySearchDAO anySearchDAO) {
-        return new AnyFinder(plainSchemaDAO, anySearchDAO);
-    }
-
     @ConditionalOnMissingBean
     @Bean
     public RealmChecker realmChecker(final @Lazy PlainSchemaDAO plainSchemaDAO) {
diff --git a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractAnySearchDAO.java b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractAnySearchDAO.java
index da73786c60..106ee3bc6b 100644
--- a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractAnySearchDAO.java
+++ b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractAnySearchDAO.java
@@ -18,22 +18,14 @@
  */
 package org.apache.syncope.core.persistence.common.dao;
 
-import jakarta.validation.ValidationException;
-import jakarta.validation.constraints.Max;
-import jakarta.validation.constraints.Min;
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Set;
 import java.util.stream.Collectors;
-import org.apache.commons.lang3.ArrayUtils;
-import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
-import org.apache.syncope.common.lib.types.AttrSchemaType;
 import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
 import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
@@ -48,11 +40,8 @@ import org.apache.syncope.core.persistence.api.dao.search.MembershipCond;
 import org.apache.syncope.core.persistence.api.dao.search.RelationshipCond;
 import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
 import org.apache.syncope.core.persistence.api.entity.Any;
-import org.apache.syncope.core.persistence.api.entity.AnyUtils;
 import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
 import org.apache.syncope.core.persistence.api.entity.EntityFactory;
-import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
-import org.apache.syncope.core.persistence.api.entity.PlainSchema;
 import org.apache.syncope.core.persistence.api.entity.Realm;
 import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
 import org.slf4j.Logger;
@@ -60,23 +49,17 @@ import org.slf4j.LoggerFactory;
 import org.springframework.data.domain.PageRequest;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Sort;
+import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
 
-public abstract class AbstractAnySearchDAO implements AnySearchDAO {
-
-    protected record CheckResult<C extends AttrCond>(PlainSchema schema, PlainAttrValue value, C cond) {
-
-    }
+public abstract class AbstractAnySearchDAO extends AbstractSearchDAO implements AnySearchDAO {
 
     protected static final Logger LOG = LoggerFactory.getLogger(AnySearchDAO.class);
 
-    protected static final String ALWAYS_FALSE_CLAUSE = "1=2";
-
-    private static final String[] ORDER_BY_NOT_ALLOWED = {
-        "serialVersionUID", "password", "securityQuestion", "securityAnswer", "token", "tokenExpireTime"
-    };
+    private static final Set<String> ORDER_BY_NOT_ALLOWED = Set.of(
+            "serialVersionUID", "password", "securityQuestion", "securityAnswer", "token", "tokenExpireTime");
 
-    protected static final String[] RELATIONSHIP_FIELDS = { "realm", "uManager", "gManager" };
+    protected static final Set<String> RELATIONSHIP_FIELDS = Set.of("realm", "uManager", "gManager");
 
     protected static SearchCond buildEffectiveCond(
             final SearchCond cond,
@@ -106,52 +89,12 @@ public abstract class AbstractAnySearchDAO implements AnySearchDAO {
         return SearchCond.and(result);
     }
 
-    public static String key(final AttrSchemaType schemaType) {
-        String key;
-        switch (schemaType) {
-            case Boolean:
-                key = "booleanValue";
-                break;
-
-            case Date:
-                key = "dateValue";
-                break;
-
-            case Double:
-                key = "doubleValue";
-                break;
-
-            case Long:
-                key = "longValue";
-                break;
-
-            case Binary:
-                key = "binaryValue";
-                break;
-
-            default:
-                key = "stringValue";
-        }
-
-        return key;
-    }
-
     protected final RealmSearchDAO realmSearchDAO;
 
-    protected final UserDAO userDAO;
-
-    protected final GroupDAO groupDAO;
-
     protected final AnyObjectDAO anyObjectDAO;
 
-    protected final PlainSchemaDAO plainSchemaDAO;
-
-    protected final EntityFactory entityFactory;
-
     protected final AnyUtilsFactory anyUtilsFactory;
 
-    protected final PlainAttrValidationManager validator;
-
     public AbstractAnySearchDAO(
             final RealmSearchDAO realmSearchDAO,
             final UserDAO userDAO,
@@ -162,14 +105,25 @@ public abstract class AbstractAnySearchDAO implements AnySearchDAO {
             final AnyUtilsFactory anyUtilsFactory,
             final PlainAttrValidationManager validator) {
 
+        super(plainSchemaDAO, userDAO, groupDAO, entityFactory, validator);
         this.realmSearchDAO = realmSearchDAO;
-        this.userDAO = userDAO;
-        this.groupDAO = groupDAO;
         this.anyObjectDAO = anyObjectDAO;
-        this.plainSchemaDAO = plainSchemaDAO;
-        this.entityFactory = entityFactory;
         this.anyUtilsFactory = anyUtilsFactory;
-        this.validator = validator;
+    }
+
+    @Transactional(readOnly = true)
+    @Override
+    public <A extends Any> List<A> findByDerAttrValue(
+            final String expression,
+            final String value,
+            final boolean ignoreCaseMatch,
+            final AnyTypeKind anyTypeKind) {
+
+        List<SearchCond> conditions = buildDerAttrValueConditions(expression, value, ignoreCaseMatch);
+
+        LOG.debug("Generated search {} conditions: {}", anyTypeKind, conditions);
+
+        return conditions.isEmpty() ? List.of() : search(SearchCond.and(conditions), anyTypeKind);
     }
 
     protected abstract long doCount(
@@ -223,109 +177,6 @@ public abstract class AbstractAnySearchDAO implements AnySearchDAO {
             Pageable pageable,
             AnyTypeKind kind);
 
-    protected CheckResult<AttrCond> check(final AttrCond cond) {
-        PlainSchema schema = plainSchemaDAO.findById(cond.getSchema()).
-                orElseThrow(() -> new IllegalArgumentException("Invalid schema " + cond.getSchema()));
-
-        PlainAttrValue attrValue = new PlainAttrValue();
-
-        if (AttrSchemaType.Encrypted == schema.getType()) {
-            throw new IllegalArgumentException("Cannot search by encrypted schema " + cond.getSchema());
-        }
-
-        try {
-            if (cond.getType() != AttrCond.Type.LIKE
-                    && cond.getType() != AttrCond.Type.ILIKE
-                    && cond.getType() != AttrCond.Type.ISNULL
-                    && cond.getType() != AttrCond.Type.ISNOTNULL) {
-
-                validator.validate(schema, cond.getExpression(), attrValue);
-            }
-        } catch (ValidationException e) {
-            throw new IllegalArgumentException("Could not validate expression " + cond.getExpression());
-        }
-
-        return new CheckResult<>(schema, attrValue, cond);
-    }
-
-    protected CheckResult<AnyCond> check(final AnyCond cond, final AnyTypeKind kind) {
-        AnyCond computed = new AnyCond(cond.getType());
-        computed.setSchema(cond.getSchema());
-        computed.setExpression(cond.getExpression());
-
-        AnyUtils anyUtils = anyUtilsFactory.getInstance(kind);
-
-        Field anyField = anyUtils.getField(computed.getSchema()).
-                orElseThrow(() -> new IllegalArgumentException("Invalid schema " + computed.getSchema()));
-
-        // Keeps track of difference between entity's getKey() and JPA @Id fields
-        if ("key".equals(computed.getSchema())) {
-            computed.setSchema("id");
-        }
-
-        PlainSchema schema = entityFactory.newEntity(PlainSchema.class);
-        schema.setKey(anyField.getName());
-        for (AttrSchemaType attrSchemaType : AttrSchemaType.values()) {
-            if (anyField.getType().isAssignableFrom(attrSchemaType.getType())) {
-                schema.setType(attrSchemaType);
-            }
-        }
-        if (schema.getType() == null || schema.getType() == AttrSchemaType.Dropdown) {
-            schema.setType(AttrSchemaType.String);
-        }
-
-        // Deal with any Integer fields logically mapping to boolean values
-        boolean foundBooleanMin = false;
-        boolean foundBooleanMax = false;
-        if (Integer.class.equals(anyField.getType())) {
-            for (Annotation annotation : anyField.getAnnotations()) {
-                if (Min.class.equals(annotation.annotationType())) {
-                    foundBooleanMin = ((Min) annotation).value() == 0;
-                } else if (Max.class.equals(annotation.annotationType())) {
-                    foundBooleanMax = ((Max) annotation).value() == 1;
-                }
-            }
-        }
-        if (foundBooleanMin && foundBooleanMax) {
-            schema.setType(AttrSchemaType.Boolean);
-        }
-
-        // Deal with any fields representing relationships to other entities
-        if (ArrayUtils.contains(RELATIONSHIP_FIELDS, computed.getSchema())) {
-            computed.setSchema(computed.getSchema() + "_id");
-            schema.setType(AttrSchemaType.String);
-
-            if (!SyncopeConstants.UUID_PATTERN.matcher(computed.getExpression()).matches()) {
-                switch (StringUtils.substringBefore(computed.getSchema(), "_id")) {
-                    case "uManager" ->
-                        userDAO.findKey(computed.getExpression()).ifPresent(computed::setExpression);
-
-                    case "gManager" ->
-                        groupDAO.findKey(computed.getExpression()).ifPresent(computed::setExpression);
-
-                    default -> {
-                    }
-                }
-            }
-        }
-
-        PlainAttrValue attrValue = new PlainAttrValue();
-        if (computed.getType() != AttrCond.Type.LIKE
-                && computed.getType() != AttrCond.Type.ILIKE
-                && computed.getType() != AttrCond.Type.ISNULL
-                && computed.getType() != AttrCond.Type.ISNOTNULL) {
-
-            try {
-                validator.validate(schema, computed.getExpression(), attrValue);
-            } catch (ValidationException e) {
-                LOG.error("Could not validate expression {}", computed.getExpression(), e);
-                throw new IllegalArgumentException("Could not validate expression " + computed.getExpression());
-            }
-        }
-
-        return new CheckResult<>(schema, attrValue, computed);
-    }
-
     protected boolean isPatternMatch(final String clause) {
         return clause.indexOf('%') != -1;
     }
@@ -417,7 +268,7 @@ public abstract class AbstractAnySearchDAO implements AnySearchDAO {
                     new Sort.Order(Sort.Direction.ASC, kind == AnyTypeKind.USER ? "username" : "name"));
         } else {
             effectiveOrderBy = pageable.getSort().stream().
-                    filter(clause -> !ArrayUtils.contains(ORDER_BY_NOT_ALLOWED, clause.getProperty())).
+                    filter(clause -> !ORDER_BY_NOT_ALLOWED.contains(clause.getProperty())).
                     toList();
         }
 
diff --git a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractRealmSearchDAO.java b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractRealmSearchDAO.java
new file mode 100644
index 0000000000..10fdb3e243
--- /dev/null
+++ b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractRealmSearchDAO.java
@@ -0,0 +1,110 @@
+/*
+ * 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.syncope.core.persistence.common.dao;
+
+import java.util.List;
+import java.util.Set;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
+import org.apache.syncope.core.persistence.api.dao.GroupDAO;
+import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
+import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO;
+import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.Realm;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.transaction.annotation.Transactional;
+
+public abstract class AbstractRealmSearchDAO extends AbstractSearchDAO implements RealmSearchDAO {
+
+    private static final Set<String> ORDER_BY_NOT_ALLOWED = Set.of(
+            "serialVersionUID", "parent");
+
+    protected static final Set<String> RELATIONSHIP_FIELDS = Set.of(
+            "parent", "passwordPolicy", "accountPolicy", "authPolicy", "accessPolicy", "attrReleasePolicy",
+            "ticketExpirationPolicy");
+
+    public AbstractRealmSearchDAO(
+            final PlainSchemaDAO plainSchemaDAO,
+            final UserDAO userDAO,
+            final GroupDAO groupDAO,
+            final EntityFactory entityFactory,
+            final PlainAttrValidationManager validator) {
+
+        super(plainSchemaDAO, userDAO, groupDAO, entityFactory, validator);
+    }
+
+    @Transactional(readOnly = true)
+    @Override
+    public List<Realm> findByDerAttrValue(
+            final String expression,
+            final String value,
+            final boolean ignoreCaseMatch) {
+
+        List<SearchCond> conditions = buildDerAttrValueConditions(expression, value, ignoreCaseMatch);
+
+        LOG.debug("Generated search Realm conditions: {}", conditions);
+
+        return conditions.isEmpty() ? List.of() : search(
+                Set.of(SyncopeConstants.ROOT_REALM), SearchCond.and(conditions), Pageable.unpaged());
+    }
+
+    protected abstract long doCount(Set<String> bases, SearchCond cond);
+
+    @Override
+    public long count(final Set<String> bases, final SearchCond cond) {
+        LOG.debug("Search condition:\n{}", cond);
+        if (cond == null || !cond.isValid()) {
+            LOG.error("Invalid search condition:\n{}", cond);
+            return 0;
+        }
+
+        return doCount(bases, cond);
+    }
+
+    protected abstract List<Realm> doSearch(Set<String> bases, SearchCond cond, Pageable pageable);
+
+    @Override
+    public List<Realm> search(final Set<String> bases, final SearchCond cond, final Pageable pageable) {
+        LOG.debug("Search condition:\n{}", cond);
+        if (cond == null || !cond.isValid()) {
+            LOG.error("Invalid search condition:\n{}", cond);
+            return List.of();
+        }
+
+        List<Sort.Order> effectiveOrderBy;
+        if (pageable.getSort().isEmpty()) {
+            effectiveOrderBy = List.of(new Sort.Order(Sort.Direction.ASC, "name"));
+        } else {
+            effectiveOrderBy = pageable.getSort().stream().
+                    filter(clause -> !ORDER_BY_NOT_ALLOWED.contains(clause.getProperty())).
+                    toList();
+        }
+
+        return doSearch(
+                bases,
+                cond,
+                pageable.isUnpaged()
+                ? Pageable.unpaged(Sort.by(effectiveOrderBy))
+                : PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by(effectiveOrderBy)));
+    }
+}
diff --git a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractSearchDAO.java b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractSearchDAO.java
new file mode 100644
index 0000000000..03c8dad854
--- /dev/null
+++ b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractSearchDAO.java
@@ -0,0 +1,312 @@
+/*
+ * 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.syncope.core.persistence.common.dao;
+
+import jakarta.validation.ValidationException;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Supplier;
+import java.util.regex.Pattern;
+import org.apache.commons.jexl3.parser.Parser;
+import org.apache.commons.jexl3.parser.ParserConstants;
+import org.apache.commons.jexl3.parser.Token;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.types.AttrSchemaType;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
+import org.apache.syncope.core.persistence.api.dao.GroupDAO;
+import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
+import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
+import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
+import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
+import org.apache.syncope.core.persistence.api.entity.PlainSchema;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class AbstractSearchDAO {
+
+    public record CheckResult<C extends AttrCond>(PlainSchema schema, PlainAttrValue value, C cond) {
+
+    }
+
+    protected static final Logger LOG = LoggerFactory.getLogger(AbstractSearchDAO.class);
+
+    protected static final String ALWAYS_FALSE_CLAUSE = "1=2";
+
+    public static String key(final AttrSchemaType schemaType) {
+        return switch (schemaType) {
+            case Boolean ->
+                "booleanValue";
+
+            case Date ->
+                "dateValue";
+
+            case Double ->
+                "doubleValue";
+
+            case Long ->
+                "longValue";
+
+            case Binary ->
+                "binaryValue";
+
+            default ->
+                "stringValue";
+        };
+    }
+
+    protected static final Comparator<String> LITERAL_COMPARATOR = (l1, l2) -> {
+        if (l1 == null && l2 == null) {
+            return 0;
+        } else if (l1 != null && l2 == null) {
+            return -1;
+        } else if (l1 == null) {
+            return 1;
+        } else if (l1.length() == l2.length()) {
+            return 0;
+        } else if (l1.length() > l2.length()) {
+            return -1;
+        } else {
+            return 1;
+        }
+    };
+
+    /**
+     * Split an attribute value recurring on provided literals/tokens.
+     *
+     * @param attrValue value to be split
+     * @param literals literals/tokens
+     * @return split value
+     */
+    protected static List<String> split(final String attrValue, final List<String> literals) {
+        final List<String> attrValues = new ArrayList<>();
+
+        if (literals.isEmpty()) {
+            attrValues.add(attrValue);
+        } else {
+            for (String token : attrValue.split(Pattern.quote(literals.getFirst()))) {
+                if (!token.isEmpty()) {
+                    attrValues.addAll(split(token, literals.subList(1, literals.size())));
+                }
+            }
+        }
+
+        return attrValues;
+    }
+
+    protected static Supplier<SyncopeClientException> syncopeClientException(final String message) {
+        return () -> {
+            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidSearchParameters);
+            sce.getElements().add(message);
+            return sce;
+        };
+    }
+
+    protected final PlainSchemaDAO plainSchemaDAO;
+
+    protected final UserDAO userDAO;
+
+    protected final GroupDAO groupDAO;
+
+    protected final EntityFactory entityFactory;
+
+    protected final PlainAttrValidationManager validator;
+
+    protected AbstractSearchDAO(
+            final PlainSchemaDAO plainSchemaDAO,
+            final UserDAO userDAO,
+            final GroupDAO groupDAO,
+            final EntityFactory entityFactory,
+            final PlainAttrValidationManager validator) {
+
+        this.plainSchemaDAO = plainSchemaDAO;
+        this.userDAO = userDAO;
+        this.groupDAO = groupDAO;
+        this.entityFactory = entityFactory;
+        this.validator = validator;
+    }
+
+    protected List<SearchCond> buildDerAttrValueConditions(
+            final String expression,
+            final String value,
+            final boolean ignoreCaseMatch) {
+
+        Parser parser = new Parser(expression);
+
+        // Schema keys
+        List<String> identifiers = new ArrayList<>();
+
+        // Literals
+        List<String> literals = new ArrayList<>();
+
+        // Get schema keys and literals
+        for (Token token = parser.getNextToken(); token != null && StringUtils.isNotBlank(token.toString());
+                token = parser.getNextToken()) {
+
+            if (token.kind == ParserConstants.STRING_LITERAL) {
+                literals.add(token.toString().substring(1, token.toString().length() - 1));
+            }
+
+            if (token.kind == ParserConstants.IDENTIFIER) {
+                identifiers.add(token.toString());
+            }
+        }
+
+        // Sort literals in order to process later literals included into others
+        literals.sort(LITERAL_COMPARATOR);
+
+        // Split value on provided literals
+        List<String> attrValues = split(value, literals);
+
+        if (attrValues.size() != identifiers.size()) {
+            LOG.error("Ambiguous JEXL expression resolution: literals and values have different size");
+            return List.of();
+        }
+
+        List<SearchCond> conditions = new ArrayList<>();
+
+        // Contains used identifiers in order to avoid replications
+        Set<String> used = new HashSet<>();
+
+        // Create several clauses: one for eanch identifiers
+        for (int i = 0; i < identifiers.size() && !used.contains(identifiers.get(i)); i++) {
+            used.add(identifiers.get(i));
+
+            AttrCond cond = plainSchemaDAO.findById(identifiers.get(i)).
+                    map(schema -> new AttrCond()).
+                    orElseGet(() -> new AnyCond());
+            cond.setType(ignoreCaseMatch ? AttrCond.Type.IEQ : AttrCond.Type.EQ);
+            cond.setSchema(identifiers.get(i));
+            cond.setExpression(attrValues.get(i));
+            conditions.add(SearchCond.of(cond));
+        }
+
+        return conditions;
+    }
+
+    protected CheckResult<AttrCond> check(final AttrCond cond) {
+        PlainSchema schema = plainSchemaDAO.findById(cond.getSchema()).
+                orElseThrow(() -> new IllegalArgumentException("Invalid schema " + cond.getSchema()));
+
+        PlainAttrValue attrValue = new PlainAttrValue();
+
+        if (AttrSchemaType.Encrypted == schema.getType()) {
+            throw new IllegalArgumentException("Cannot search by encrypted schema " + cond.getSchema());
+        }
+
+        try {
+            if (cond.getType() != AttrCond.Type.LIKE
+                    && cond.getType() != AttrCond.Type.ILIKE
+                    && cond.getType() != AttrCond.Type.ISNULL
+                    && cond.getType() != AttrCond.Type.ISNOTNULL) {
+
+                validator.validate(schema, cond.getExpression(), attrValue);
+            }
+        } catch (ValidationException e) {
+            throw new IllegalArgumentException("Could not validate expression " + cond.getExpression());
+        }
+
+        return new CheckResult<>(schema, attrValue, cond);
+    }
+
+    protected CheckResult<AnyCond> check(final AnyCond cond, final Field field, final Set<String> relationshipsFields) {
+        AnyCond computed = new AnyCond(cond.getType());
+        computed.setSchema(cond.getSchema());
+        computed.setExpression(cond.getExpression());
+
+        // Keeps track of difference between entity's getKey() and @Id fields
+        if ("key".equals(computed.getSchema())) {
+            computed.setSchema("id");
+        }
+
+        PlainSchema schema = entityFactory.newEntity(PlainSchema.class);
+        schema.setKey(field.getName());
+        for (AttrSchemaType attrSchemaType : AttrSchemaType.values()) {
+            if (field.getType().isAssignableFrom(attrSchemaType.getType())) {
+                schema.setType(attrSchemaType);
+            }
+        }
+        if (schema.getType() == null || schema.getType() == AttrSchemaType.Dropdown) {
+            schema.setType(AttrSchemaType.String);
+        }
+
+        // Deal with any Integer fields logically mapping to boolean values
+        boolean foundBooleanMin = false;
+        boolean foundBooleanMax = false;
+        if (Integer.class.equals(field.getType())) {
+            for (Annotation annotation : field.getAnnotations()) {
+                if (Min.class.equals(annotation.annotationType())) {
+                    foundBooleanMin = ((Min) annotation).value() == 0;
+                } else if (Max.class.equals(annotation.annotationType())) {
+                    foundBooleanMax = ((Max) annotation).value() == 1;
+                }
+            }
+        }
+        if (foundBooleanMin && foundBooleanMax) {
+            schema.setType(AttrSchemaType.Boolean);
+        }
+
+        // Deal with fields representing relationships to other entities
+        if (relationshipsFields.contains(computed.getSchema())) {
+            computed.setSchema(computed.getSchema() + "_id");
+            schema.setType(AttrSchemaType.String);
+
+            if (!SyncopeConstants.UUID_PATTERN.matcher(computed.getExpression()).matches()) {
+                switch (StringUtils.substringBefore(computed.getSchema(), "_id")) {
+                    case "uManager" ->
+                        userDAO.findKey(computed.getExpression()).ifPresent(computed::setExpression);
+
+                    case "gManager" ->
+                        groupDAO.findKey(computed.getExpression()).ifPresent(computed::setExpression);
+
+                    default -> {
+                    }
+                }
+            }
+        }
+
+        PlainAttrValue attrValue = new PlainAttrValue();
+        if (computed.getType() != AttrCond.Type.LIKE
+                && computed.getType() != AttrCond.Type.ILIKE
+                && computed.getType() != AttrCond.Type.ISNULL
+                && computed.getType() != AttrCond.Type.ISNOTNULL) {
+
+            try {
+                validator.validate(schema, computed.getExpression(), attrValue);
+            } catch (ValidationException e) {
+                LOG.error("Could not validate expression {}", computed.getExpression(), e);
+                throw new IllegalArgumentException("Could not validate expression " + computed.getExpression());
+            }
+        }
+
+        return new CheckResult<>(schema, attrValue, computed);
+    }
+}
diff --git a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AnyFinder.java b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AnyFinder.java
deleted file mode 100644
index 7297bd7f27..0000000000
--- a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AnyFinder.java
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * 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.syncope.core.persistence.common.dao;
-
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.regex.Pattern;
-import org.apache.commons.jexl3.parser.Parser;
-import org.apache.commons.jexl3.parser.ParserConstants;
-import org.apache.commons.jexl3.parser.Token;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.syncope.common.lib.types.AnyTypeKind;
-import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
-import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
-import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
-import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
-import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
-import org.apache.syncope.core.persistence.api.entity.Any;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.transaction.annotation.Transactional;
-
-public class AnyFinder {
-
-    protected static final Logger LOG = LoggerFactory.getLogger(AnyFinder.class);
-
-    protected static final Comparator<String> LITERAL_COMPARATOR = (l1, l2) -> {
-        if (l1 == null && l2 == null) {
-            return 0;
-        } else if (l1 != null && l2 == null) {
-            return -1;
-        } else if (l1 == null) {
-            return 1;
-        } else if (l1.length() == l2.length()) {
-            return 0;
-        } else if (l1.length() > l2.length()) {
-            return -1;
-        } else {
-            return 1;
-        }
-    };
-
-    /**
-     * Split an attribute value recurring on provided literals/tokens.
-     *
-     * @param attrValue value to be split
-     * @param literals literals/tokens
-     * @return split value
-     */
-    protected static List<String> split(final String attrValue, final List<String> literals) {
-        final List<String> attrValues = new ArrayList<>();
-
-        if (literals.isEmpty()) {
-            attrValues.add(attrValue);
-        } else {
-            for (String token : attrValue.split(Pattern.quote(literals.getFirst()))) {
-                if (!token.isEmpty()) {
-                    attrValues.addAll(split(token, literals.subList(1, literals.size())));
-                }
-            }
-        }
-
-        return attrValues;
-    }
-
-    protected final PlainSchemaDAO plainSchemaDAO;
-
-    protected final AnySearchDAO anySearchDAO;
-
-    public AnyFinder(final PlainSchemaDAO plainSchemaDAO, final AnySearchDAO anySearchDAO) {
-        this.plainSchemaDAO = plainSchemaDAO;
-        this.anySearchDAO = anySearchDAO;
-    }
-
-    @Transactional(readOnly = true)
-    public <A extends Any> List<A> findByDerAttrValue(
-            final AnyTypeKind anyTypeKind,
-            final String expression,
-            final String value,
-            final boolean ignoreCaseMatch) {
-
-        Parser parser = new Parser(expression);
-
-        // Schema keys
-        List<String> identifiers = new ArrayList<>();
-
-        // Literals
-        List<String> literals = new ArrayList<>();
-
-        // Get schema keys and literals
-        for (Token token = parser.getNextToken(); token != null && StringUtils.isNotBlank(token.toString());
-                token = parser.getNextToken()) {
-
-            if (token.kind == ParserConstants.STRING_LITERAL) {
-                literals.add(token.toString().substring(1, token.toString().length() - 1));
-            }
-
-            if (token.kind == ParserConstants.IDENTIFIER) {
-                identifiers.add(token.toString());
-            }
-        }
-
-        // Sort literals in order to process later literals included into others
-        literals.sort(LITERAL_COMPARATOR);
-
-        // Split value on provided literals
-        List<String> attrValues = split(value, literals);
-
-        if (attrValues.size() != identifiers.size()) {
-            LOG.error("Ambiguous JEXL expression resolution: literals and values have different size");
-            return List.of();
-        }
-
-        List<SearchCond> andConditions = new ArrayList<>();
-
-        // Contains used identifiers in order to avoid replications
-        Set<String> used = new HashSet<>();
-
-        // Create several clauses: one for eanch identifiers
-        for (int i = 0; i < identifiers.size() && !used.contains(identifiers.get(i)); i++) {
-            used.add(identifiers.get(i));
-
-            AttrCond cond = plainSchemaDAO.existsById(identifiers.get(i))
-                    ? new AttrCond()
-                    : new AnyCond();
-            cond.setType(ignoreCaseMatch ? AttrCond.Type.IEQ : AttrCond.Type.EQ);
-            cond.setSchema(identifiers.get(i));
-            cond.setExpression(attrValues.get(i));
-            andConditions.add(SearchCond.of(cond));
-        }
-
-        LOG.debug("Generated search {} conditions: {}", anyTypeKind, andConditions);
-
-        return andConditions.isEmpty() ? List.of() : anySearchDAO.search(SearchCond.and(andConditions), anyTypeKind);
-    }
-}
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MariaDBPersistenceContext.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MariaDBPersistenceContext.java
index 4f3525942e..61a57aeaee 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MariaDBPersistenceContext.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MariaDBPersistenceContext.java
@@ -19,7 +19,6 @@
 package org.apache.syncope.core.persistence.jpa;
 
 import jakarta.persistence.EntityManager;
-import jakarta.persistence.EntityManagerFactory;
 import java.util.TimeZone;
 import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
 import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
@@ -31,7 +30,9 @@ import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
 import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.utils.RealmUtils;
 import org.apache.syncope.core.persistence.jpa.dao.MariaDBJPAAnySearchDAO;
+import org.apache.syncope.core.persistence.jpa.dao.MariaDBJPARealmSearchDAO;
 import org.apache.syncope.core.persistence.jpa.dao.repo.MariaDBPlainSchemaRepoExtImpl;
 import org.apache.syncope.core.persistence.jpa.dao.repo.PlainSchemaRepoExt;
 import org.apache.syncope.core.persistence.jpa.entity.MariaDBEntityFactory;
@@ -83,7 +84,6 @@ public class MariaDBPersistenceContext {
             final @Lazy EntityFactory entityFactory,
             final AnyUtilsFactory anyUtilsFactory,
             final PlainAttrValidationManager validator,
-            final EntityManagerFactory entityManagerFactory,
             final EntityManager entityManager) {
 
         return new MariaDBJPAAnySearchDAO(
@@ -95,10 +95,30 @@ public class MariaDBPersistenceContext {
                 entityFactory,
                 anyUtilsFactory,
                 validator,
-                entityManagerFactory,
                 entityManager);
     }
 
+    @ConditionalOnMissingBean
+    @Bean
+    public RealmSearchDAO realmSearchDAO(
+            final EntityManager entityManager,
+            final @Lazy PlainSchemaDAO plainSchemaDAO,
+            final @Lazy UserDAO userDAO,
+            final @Lazy GroupDAO groupDAO,
+            final @Lazy EntityFactory entityFactory,
+            final PlainAttrValidationManager validator,
+            final RealmUtils realmUtils) {
+
+        return new MariaDBJPARealmSearchDAO(
+                entityManager,
+                plainSchemaDAO,
+                userDAO,
+                groupDAO,
+                entityFactory,
+                validator,
+                realmUtils);
+    }
+
     @ConditionalOnMissingBean
     @Bean
     public PlainSchemaRepoExt plainSchemaRepoExt(
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MySQLPersistenceContext.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MySQLPersistenceContext.java
index 7544de272c..a90b1a575e 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MySQLPersistenceContext.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MySQLPersistenceContext.java
@@ -19,7 +19,6 @@
 package org.apache.syncope.core.persistence.jpa;
 
 import jakarta.persistence.EntityManager;
-import jakarta.persistence.EntityManagerFactory;
 import java.util.TimeZone;
 import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
 import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
@@ -31,7 +30,9 @@ import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
 import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.utils.RealmUtils;
 import org.apache.syncope.core.persistence.jpa.dao.MySQLJPAAnySearchDAO;
+import org.apache.syncope.core.persistence.jpa.dao.MySQLJPARealmSearchDAO;
 import org.apache.syncope.core.persistence.jpa.dao.repo.MySQLPlainSchemaRepoExtImpl;
 import org.apache.syncope.core.persistence.jpa.dao.repo.PlainSchemaRepoExt;
 import org.apache.syncope.core.persistence.jpa.entity.MySQLEntityFactory;
@@ -83,7 +84,6 @@ public class MySQLPersistenceContext {
             final @Lazy EntityFactory entityFactory,
             final AnyUtilsFactory anyUtilsFactory,
             final PlainAttrValidationManager validator,
-            final EntityManagerFactory entityManagerFactory,
             final EntityManager entityManager) {
 
         return new MySQLJPAAnySearchDAO(
@@ -95,10 +95,30 @@ public class MySQLPersistenceContext {
                 entityFactory,
                 anyUtilsFactory,
                 validator,
-                entityManagerFactory,
                 entityManager);
     }
 
+    @ConditionalOnMissingBean
+    @Bean
+    public RealmSearchDAO realmSearchDAO(
+            final EntityManager entityManager,
+            final @Lazy PlainSchemaDAO plainSchemaDAO,
+            final @Lazy UserDAO userDAO,
+            final @Lazy GroupDAO groupDAO,
+            final @Lazy EntityFactory entityFactory,
+            final PlainAttrValidationManager validator,
+            final RealmUtils realmUtils) {
+
+        return new MySQLJPARealmSearchDAO(
+                entityManager,
+                plainSchemaDAO,
+                userDAO,
+                groupDAO,
+                entityFactory,
+                validator,
+                realmUtils);
+    }
+
     @ConditionalOnMissingBean
     @Bean
     public PlainSchemaRepoExt plainSchemaRepoExt(
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/OraclePersistenceContext.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/OraclePersistenceContext.java
index ea26e5fc89..d49afa29b4 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/OraclePersistenceContext.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/OraclePersistenceContext.java
@@ -19,7 +19,6 @@
 package org.apache.syncope.core.persistence.jpa;
 
 import jakarta.persistence.EntityManager;
-import jakarta.persistence.EntityManagerFactory;
 import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
 import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
@@ -30,7 +29,9 @@ import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
 import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.utils.RealmUtils;
 import org.apache.syncope.core.persistence.jpa.dao.OracleJPAAnySearchDAO;
+import org.apache.syncope.core.persistence.jpa.dao.OracleJPARealmSearchDAO;
 import org.apache.syncope.core.persistence.jpa.dao.repo.OraclePlainSchemaRepoExtImpl;
 import org.apache.syncope.core.persistence.jpa.dao.repo.PlainSchemaRepoExt;
 import org.apache.syncope.core.persistence.jpa.entity.OracleEntityFactory;
@@ -62,7 +63,6 @@ public class OraclePersistenceContext {
             final @Lazy EntityFactory entityFactory,
             final AnyUtilsFactory anyUtilsFactory,
             final PlainAttrValidationManager validator,
-            final EntityManagerFactory entityManagerFactory,
             final EntityManager entityManager) {
 
         return new OracleJPAAnySearchDAO(
@@ -74,10 +74,30 @@ public class OraclePersistenceContext {
                 entityFactory,
                 anyUtilsFactory,
                 validator,
-                entityManagerFactory,
                 entityManager);
     }
 
+    @ConditionalOnMissingBean
+    @Bean
+    public RealmSearchDAO realmSearchDAO(
+            final EntityManager entityManager,
+            final @Lazy PlainSchemaDAO plainSchemaDAO,
+            final @Lazy UserDAO userDAO,
+            final @Lazy GroupDAO groupDAO,
+            final @Lazy EntityFactory entityFactory,
+            final PlainAttrValidationManager validator,
+            final RealmUtils realmUtils) {
+
+        return new OracleJPARealmSearchDAO(
+                entityManager,
+                plainSchemaDAO,
+                userDAO,
+                groupDAO,
+                entityFactory,
+                validator,
+                realmUtils);
+    }
+
     @ConditionalOnMissingBean
     @Bean
     public PlainSchemaRepoExt plainSchemaRepoExt(
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PGPersistenceContext.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PGPersistenceContext.java
index 4fbad459f6..9c35cfef8c 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PGPersistenceContext.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PGPersistenceContext.java
@@ -19,7 +19,6 @@
 package org.apache.syncope.core.persistence.jpa;
 
 import jakarta.persistence.EntityManager;
-import jakarta.persistence.EntityManagerFactory;
 import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
 import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
@@ -30,7 +29,9 @@ import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
 import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.utils.RealmUtils;
 import org.apache.syncope.core.persistence.jpa.dao.PGJPAAnySearchDAO;
+import org.apache.syncope.core.persistence.jpa.dao.PGJPARealmSearchDAO;
 import org.apache.syncope.core.persistence.jpa.dao.repo.PGPlainSchemaRepoExtImpl;
 import org.apache.syncope.core.persistence.jpa.dao.repo.PlainSchemaRepoExt;
 import org.apache.syncope.core.persistence.jpa.entity.PGEntityFactory;
@@ -63,7 +64,6 @@ public class PGPersistenceContext {
             final @Lazy EntityFactory entityFactory,
             final AnyUtilsFactory anyUtilsFactory,
             final PlainAttrValidationManager validator,
-            final EntityManagerFactory entityManagerFactory,
             final EntityManager entityManager) {
 
         return new PGJPAAnySearchDAO(
@@ -75,10 +75,30 @@ public class PGPersistenceContext {
                 entityFactory,
                 anyUtilsFactory,
                 validator,
-                entityManagerFactory,
                 entityManager);
     }
 
+    @ConditionalOnMissingBean
+    @Bean
+    public RealmSearchDAO realmSearchDAO(
+            final EntityManager entityManager,
+            final @Lazy PlainSchemaDAO plainSchemaDAO,
+            final @Lazy UserDAO userDAO,
+            final @Lazy GroupDAO groupDAO,
+            final @Lazy EntityFactory entityFactory,
+            final PlainAttrValidationManager validator,
+            final RealmUtils realmUtils) {
+
+        return new PGJPARealmSearchDAO(
+                entityManager,
+                plainSchemaDAO,
+                userDAO,
+                groupDAO,
+                entityFactory,
+                validator,
+                realmUtils);
+    }
+
     @ConditionalOnMissingBean
     @Bean
     public PlainSchemaRepoExt plainSchemaRepoExt(
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java
index ad9b5e6445..c0abfd5597 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java
@@ -85,7 +85,6 @@ import org.apache.syncope.core.persistence.api.entity.EntityFactory;
 import org.apache.syncope.core.persistence.api.entity.task.TaskUtilsFactory;
 import org.apache.syncope.core.persistence.common.CommonPersistenceContext;
 import org.apache.syncope.core.persistence.common.RuntimeDomainLoader;
-import org.apache.syncope.core.persistence.common.dao.AnyFinder;
 import org.apache.syncope.core.persistence.jpa.content.XMLContentExporter;
 import org.apache.syncope.core.persistence.jpa.content.XMLContentLoader;
 import org.apache.syncope.core.persistence.jpa.dao.JPAAnyMatchDAO;
@@ -97,7 +96,6 @@ import org.apache.syncope.core.persistence.jpa.dao.JPAOIDCJWKSDAO;
 import org.apache.syncope.core.persistence.jpa.dao.JPAPersistenceInfoDAO;
 import org.apache.syncope.core.persistence.jpa.dao.JPAPolicyDAO;
 import org.apache.syncope.core.persistence.jpa.dao.JPARealmDAO;
-import org.apache.syncope.core.persistence.jpa.dao.JPARealmSearchDAO;
 import org.apache.syncope.core.persistence.jpa.dao.JPATaskDAO;
 import org.apache.syncope.core.persistence.jpa.dao.JPATaskExecDAO;
 import org.apache.syncope.core.persistence.jpa.dao.repo.AccessTokenRepo;
@@ -408,16 +406,14 @@ public class PersistenceContext {
             final @Lazy UserDAO userDAO,
             final @Lazy GroupDAO groupDAO,
             final EntityManager entityManager,
-            final AnyChecker anyChecker,
-            final AnyFinder anyFinder) {
+            final AnyChecker anyChecker) {
 
         return new AnyObjectRepoExtImpl(
                 anyUtilsFactory,
                 userDAO,
                 groupDAO,
                 entityManager,
-                anyChecker,
-                anyFinder);
+                anyChecker);
     }
 
     @ConditionalOnMissingBean
@@ -625,8 +621,7 @@ public class PersistenceContext {
             final @Lazy UserDAO userDAO,
             final @Lazy AnyObjectDAO anyObjectDAO,
             final EntityManager entityManager,
-            final AnyChecker anyChecker,
-            final AnyFinder anyFinder) {
+            final AnyChecker anyChecker) {
 
         return new GroupRepoExtImpl(
                 anyUtilsFactory,
@@ -635,8 +630,7 @@ public class PersistenceContext {
                 userDAO,
                 anyObjectDAO,
                 entityManager,
-                anyChecker,
-                anyFinder);
+                anyChecker);
     }
 
     @ConditionalOnMissingBean
@@ -767,12 +761,6 @@ public class PersistenceContext {
         return new JPARealmDAO(roleDAO, realmSearchDAO, plainSchemaDAO, publisher, entityManager, realmChecker);
     }
 
-    @ConditionalOnMissingBean
-    @Bean
-    public RealmSearchDAO realmSearchDAO(final EntityManager entityManager) {
-        return new JPARealmSearchDAO(entityManager);
-    }
-
     @ConditionalOnMissingBean
     @Bean
     public RelationshipTypeRepoExt relationshipTypeRepoExt(final EntityManager entityManager) {
@@ -952,8 +940,7 @@ public class PersistenceContext {
             final DelegationDAO delegationDAO,
             final FIQLQueryDAO fiqlQueryDAO,
             final EntityManager entityManager,
-            final AnyChecker anyChecker,
-            final AnyFinder anyFinder) {
+            final AnyChecker anyChecker) {
 
         return new UserRepoExtImpl(
                 anyUtilsFactory,
@@ -964,8 +951,7 @@ public class PersistenceContext {
                 fiqlQueryDAO,
                 securityProperties,
                 entityManager,
-                anyChecker,
-                anyFinder);
+                anyChecker);
     }
 
     @ConditionalOnMissingBean
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/content/XMLContentExporter.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/content/XMLContentExporter.java
index 0d0e16570e..e08db492b6 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/content/XMLContentExporter.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/content/XMLContentExporter.java
@@ -77,7 +77,6 @@ import org.apache.syncope.core.persistence.jpa.entity.JPAAuditEvent;
 import org.apache.syncope.core.persistence.jpa.entity.JPAJobStatus;
 import org.apache.syncope.core.persistence.jpa.entity.JPARealm;
 import org.springframework.context.ConfigurableApplicationContext;
-import org.springframework.data.domain.Pageable;
 import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.jdbc.datasource.DataSourceUtils;
 import org.springframework.jdbc.support.JdbcUtils;
@@ -367,7 +366,7 @@ public class XMLContentExporter extends AbstractXMLContentExporter {
         if (tableName.equalsIgnoreCase(JPARealm.TABLE)) {
             List<Map<String, String>> realmRows = new ArrayList<>(rows);
             rows.clear();
-            realmSearchDAO.findDescendants(SyncopeConstants.ROOT_REALM, null, Pageable.unpaged()).
+            realmSearchDAO.findDescendants(SyncopeConstants.ROOT_REALM, null).
                     forEach(realm -> realmRows.stream().filter(row -> {
 
                 String id = Optional.ofNullable(row.get("ID")).orElseGet(() -> row.get("id"));
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractJPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractJPAAnySearchDAO.java
index 8db331780a..53892a8393 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractJPAAnySearchDAO.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractJPAAnySearchDAO.java
@@ -19,7 +19,6 @@
 package org.apache.syncope.core.persistence.jpa.dao;
 
 import jakarta.persistence.EntityManager;
-import jakarta.persistence.EntityManagerFactory;
 import jakarta.persistence.Query;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -28,10 +27,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.Supplier;
 import java.util.stream.Collectors;
-import org.apache.commons.lang3.ArrayUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.common.lib.SyncopeClientException;
@@ -66,9 +62,6 @@ import org.apache.syncope.core.persistence.api.entity.PlainSchema;
 import org.apache.syncope.core.persistence.api.entity.Realm;
 import org.apache.syncope.core.persistence.api.utils.RealmUtils;
 import org.apache.syncope.core.persistence.common.dao.AbstractAnySearchDAO;
-import org.apache.syncope.core.spring.security.AuthContextUtils;
-import org.hibernate.dialect.OracleDialect;
-import org.hibernate.internal.SessionFactoryImpl;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Sort;
 
@@ -94,8 +87,6 @@ abstract class AbstractJPAAnySearchDAO extends AbstractAnySearchDAO {
             + "lastChangeDate,lastModifier,status,changePwdDate,cipherAlgorithm,failedLogins,"
             + "lastLoginDate,mustChangePassword,suspended,username";
 
-    private static final Map<String, Boolean> IS_ORACLE = new ConcurrentHashMap<>();
-
     protected static int setParameter(final List<Object> parameters, final Object parameter) {
         parameters.add(parameter);
         return parameters.size();
@@ -111,16 +102,6 @@ abstract class AbstractJPAAnySearchDAO extends AbstractAnySearchDAO {
         }
     }
 
-    protected static Supplier<SyncopeClientException> syncopeClientException(final String message) {
-        return () -> {
-            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidSearchParameters);
-            sce.getElements().add(message);
-            return sce;
-        };
-    }
-
-    protected final EntityManagerFactory entityManagerFactory;
-
     protected final EntityManager entityManager;
 
     protected AbstractJPAAnySearchDAO(
@@ -132,7 +113,6 @@ abstract class AbstractJPAAnySearchDAO extends AbstractAnySearchDAO {
             final EntityFactory entityFactory,
             final AnyUtilsFactory anyUtilsFactory,
             final PlainAttrValidationManager validator,
-            final EntityManagerFactory entityManagerFactory,
             final EntityManager entityManager) {
 
         super(
@@ -144,17 +124,9 @@ abstract class AbstractJPAAnySearchDAO extends AbstractAnySearchDAO {
                 entityFactory,
                 anyUtilsFactory,
                 validator);
-        this.entityManagerFactory = entityManagerFactory;
         this.entityManager = entityManager;
     }
 
-    protected boolean isOracle() {
-        return IS_ORACLE.computeIfAbsent(
-                AuthContextUtils.getDomain(),
-                k -> entityManagerFactory.unwrap(SessionFactoryImpl.class).getJdbcServices().
-                        getDialect() instanceof OracleDialect);
-    }
-
     protected SearchSupport.SearchView defaultSV(final SearchSupport svs) {
         return svs.field();
     }
@@ -505,7 +477,7 @@ abstract class AbstractJPAAnySearchDAO extends AbstractAnySearchDAO {
             final boolean not,
             final List<Object> parameters) {
 
-        // activate ignoreCase only for EQ and LIKE operators
+        // activate ignoreCase only for ILIKE and IEQ operators
         boolean ignoreCase = AttrCond.Type.ILIKE == cond.getType() || AttrCond.Type.IEQ == cond.getType();
 
         String left = column;
@@ -528,7 +500,7 @@ abstract class AbstractJPAAnySearchDAO extends AbstractAnySearchDAO {
                     } else {
                         clause.append('?').append(setParameter(parameters, cond.getExpression()));
                     }
-                    if (isOracle()) {
+                    if (this instanceof OracleJPAAnySearchDAO) {
                         clause.append(" ESCAPE '\\'");
                     }
                 } else {
@@ -598,74 +570,12 @@ abstract class AbstractJPAAnySearchDAO extends AbstractAnySearchDAO {
                         : from.alias() + ".schema_id='" + schema.getKey() + "' AND " + clause);
     }
 
-    protected AttrCondQuery getQuery(
-            final AttrCond cond,
-            final boolean not,
-            final CheckResult<AttrCond> checked,
-            final List<Object> parameters,
-            final SearchSupport svs) {
-
-        // normalize NULL / NOT NULL checks
-        if (not) {
-            if (cond.getType() == AttrCond.Type.ISNULL) {
-                cond.setType(AttrCond.Type.ISNOTNULL);
-            } else if (cond.getType() == AttrCond.Type.ISNOTNULL) {
-                cond.setType(AttrCond.Type.ISNULL);
-            }
-        }
-
-        SearchSupport.SearchView sv = checked.schema().isUniqueConstraint()
-                ? svs.asSearchViewSupport().uniqueAttr()
-                : svs.asSearchViewSupport().attr();
-
-        switch (cond.getType()) {
-            case ISNOTNULL -> {
-                return new AttrCondQuery(true, new AnySearchNode.Leaf(
-                        sv,
-                        sv.alias() + ".schema_id='" + checked.schema().getKey() + "'"));
-            }
-
-            case ISNULL -> {
-                String clause = new StringBuilder(anyId(svs)).append(" NOT IN ").
-                        append('(').
-                        append("SELECT DISTINCT any_id FROM ").
-                        append(sv.name()).
-                        append(" WHERE schema_id=").append("'").append(checked.schema().getKey()).append("'").
-                        append(')').toString();
-                return new AttrCondQuery(true, new AnySearchNode.Leaf(defaultSV(svs), clause));
-            }
-
-            default -> {
-                AnySearchNode.Leaf node;
-                if (not && checked.schema().isMultivalue()) {
-                    AnySearchNode.Leaf notNode = fillAttrQuery(
-                            sv.alias() + "." + key(checked.schema().getType()),
-                            sv,
-                            checked.value(),
-                            checked.schema(),
-                            cond,
-                            false,
-                            parameters);
-                    node = new AnySearchNode.Leaf(
-                            sv,
-                            anyId(svs) + " NOT IN ("
-                            + "SELECT any_id FROM " + sv.name()
-                            + " WHERE " + notNode.getClause().replace(sv.alias() + ".", "")
-                            + ")");
-                } else {
-                    node = fillAttrQuery(
-                            sv.alias() + "." + key(checked.schema().getType()),
-                            sv,
-                            checked.value(),
-                            checked.schema(),
-                            cond,
-                            not,
-                            parameters);
-                }
-                return new AttrCondQuery(true, node);
-            }
-        }
-    }
+    protected abstract AttrCondQuery getQuery(
+            AttrCond cond,
+            boolean not,
+            CheckResult<AttrCond> checked,
+            List<Object> parameters,
+            SearchSupport svs);
 
     protected AnySearchNode getQuery(
             final AnyCond cond,
@@ -681,7 +591,11 @@ abstract class AbstractJPAAnySearchDAO extends AbstractAnySearchDAO {
             cond.setExpression(realm.getKey());
         }
 
-        CheckResult<AnyCond> checked = check(cond, svs.anyTypeKind);
+        CheckResult<AnyCond> checked = check(
+                cond,
+                anyUtilsFactory.getInstance(svs.anyTypeKind).getField(cond.getSchema()).
+                        orElseThrow(() -> new IllegalArgumentException("Invalid schema " + cond.getSchema())),
+                RELATIONSHIP_FIELDS);
 
         return switch (checked.cond().getType()) {
             case ISNULL ->
@@ -742,7 +656,8 @@ abstract class AbstractJPAAnySearchDAO extends AbstractAnySearchDAO {
                             return noRealm;
                         });
 
-                        realmKeys.addAll(realmSearchDAO.findDescendants(realm.getFullPath(), base.getFullPath()));
+                        realmKeys.addAll(realmSearchDAO.findDescendants(realm.getFullPath(), base.getFullPath()).
+                                stream().map(Realm::getKey).toList());
                     }));
         } else {
             if (adminRealms.stream().anyMatch(r -> r.startsWith(base.getFullPath()))) {
@@ -810,7 +725,7 @@ abstract class AbstractJPAAnySearchDAO extends AbstractAnySearchDAO {
     protected String buildWhere(final List<String> where, final AnySearchNode root) {
         return where.stream().
                 map(w -> "(" + w + ")").
-                collect(Collectors.joining(" " + root.getType().name() + " "));
+                collect(Collectors.joining(' ' + root.getType().name() + ' '));
     }
 
     protected String buildCountQuery(
@@ -833,15 +748,14 @@ abstract class AbstractJPAAnySearchDAO extends AbstractAnySearchDAO {
         Map<SearchSupport.SearchView, Boolean> counters = new HashMap<>();
         visitNode(root, counters, from, where, svs);
 
-        StringBuilder queryString = new StringBuilder("SELECT COUNT(DISTINCT ").append(anyId(svs)).append(") ");
-
-        queryString.append("FROM ").append(buildFrom(from, queryInfo.plainSchemas(), null));
-
-        queryString.append(" WHERE ").append(buildWhere(where, root));
+        String queryString = new StringBuilder("SELECT COUNT(DISTINCT ").append(anyId(svs)).append(") ").
+                append("FROM ").append(buildFrom(from, queryInfo.plainSchemas(), null)).
+                append(" WHERE ").append(buildWhere(where, root)).
+                toString();
 
         LOG.debug("Query: {}, parameters: {}", queryString, parameters);
 
-        return queryString.toString();
+        return queryString;
     }
 
     @Override
@@ -879,40 +793,13 @@ abstract class AbstractJPAAnySearchDAO extends AbstractAnySearchDAO {
         return ((Number) countQuery.getSingleResult()).intValue();
     }
 
-    protected void parseOrderByForPlainSchema(
-            final SearchSupport svs,
-            final OrderBySupport obs,
-            final OrderBySupport.Item item,
-            final Sort.Order clause,
-            final PlainSchema schema,
-            final String fieldName) {
-
-        // keep track of involvement of non-mandatory schemas in the order by clauses
-        obs.nonMandatorySchemas = !"true".equals(schema.getMandatoryCondition());
-
-        if (schema.isUniqueConstraint()) {
-            obs.views.add(svs.asSearchViewSupport().uniqueAttr());
-
-            item.select = new StringBuilder().
-                    append(svs.asSearchViewSupport().uniqueAttr().alias()).append('.').
-                    append(key(schema.getType())).
-                    append(" AS ").append(fieldName).toString();
-            item.where = new StringBuilder().
-                    append(svs.asSearchViewSupport().uniqueAttr().alias()).
-                    append(".schema_id='").append(fieldName).append("'").toString();
-            item.orderBy = fieldName + ' ' + clause.getDirection().name();
-        } else {
-            obs.views.add(svs.asSearchViewSupport().attr());
-
-            item.select = new StringBuilder().
-                    append(svs.asSearchViewSupport().attr().alias()).append('.').append(key(schema.getType())).
-                    append(" AS ").append(fieldName).toString();
-            item.where = new StringBuilder().
-                    append(svs.asSearchViewSupport().attr().alias()).
-                    append(".schema_id='").append(fieldName).append("'").toString();
-            item.orderBy = fieldName + ' ' + clause.getDirection().name();
-        }
-    }
+    protected abstract void parseOrderByForPlainSchema(
+            SearchSupport svs,
+            OrderBySupport obs,
+            OrderBySupport.Item item,
+            Sort.Order clause,
+            PlainSchema schema,
+            String fieldName);
 
     protected void parseOrderByForField(
             final SearchSupport svs,
@@ -956,7 +843,7 @@ abstract class AbstractJPAAnySearchDAO extends AbstractAnySearchDAO {
                             String fieldName = "key".equals(clause.getProperty()) ? "id" : clause.getProperty();
 
                             // Adjust field name to column name
-                            if (ArrayUtils.contains(RELATIONSHIP_FIELDS, fieldName)) {
+                            if (RELATIONSHIP_FIELDS.contains(fieldName)) {
                                 fieldName += "_id";
                             }
 
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractJPARealmSearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractJPARealmSearchDAO.java
new file mode 100644
index 0000000000..a6ce4cbdfb
--- /dev/null
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractJPARealmSearchDAO.java
@@ -0,0 +1,541 @@
+/*
+ * 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.syncope.core.persistence.jpa.dao;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.NoResultException;
+import jakarta.persistence.Query;
+import jakarta.persistence.TypedQuery;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.types.AttrSchemaType;
+import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
+import org.apache.syncope.core.persistence.api.dao.GroupDAO;
+import org.apache.syncope.core.persistence.api.dao.MalformedPathException;
+import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
+import org.apache.syncope.core.persistence.api.dao.RealmDAO;
+import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
+import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
+import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
+import org.apache.syncope.core.persistence.api.entity.PlainSchema;
+import org.apache.syncope.core.persistence.api.entity.Realm;
+import org.apache.syncope.core.persistence.api.utils.RealmUtils;
+import org.apache.syncope.core.persistence.common.dao.AbstractRealmSearchDAO;
+import org.apache.syncope.core.persistence.jpa.entity.JPARealm;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.transaction.annotation.Transactional;
+
+public abstract class AbstractJPARealmSearchDAO extends AbstractRealmSearchDAO {
+
+    protected record QueryInfo(RealmSearchNode node, Set<String> plainSchemas) {
+
+    }
+
+    protected record AttrCondQuery(Boolean addPlainSchemas, RealmSearchNode node) {
+
+    }
+
+    protected static int setParameter(final List<Object> parameters, final Object parameter) {
+        parameters.add(parameter);
+        return parameters.size();
+    }
+
+    protected static void fillWithParameters(final Query query, final List<Object> parameters) {
+        for (int i = 0; i < parameters.size(); i++) {
+            if (parameters.get(i) instanceof Boolean aBoolean) {
+                query.setParameter(i + 1, aBoolean ? 1 : 0);
+            } else {
+                query.setParameter(i + 1, parameters.get(i));
+            }
+        }
+    }
+
+    protected final EntityManager entityManager;
+
+    protected final RealmUtils realmUtils;
+
+    protected AbstractJPARealmSearchDAO(
+            final EntityManager entityManager,
+            final PlainSchemaDAO plainSchemaDAO,
+            final UserDAO userDAO,
+            final GroupDAO groupDAO,
+            final EntityFactory entityFactory,
+            final PlainAttrValidationManager validator,
+            final RealmUtils realmUtils) {
+
+        super(plainSchemaDAO, userDAO, groupDAO, entityFactory, validator);
+
+        this.entityManager = entityManager;
+        this.realmUtils = realmUtils;
+    }
+
+    @Transactional(readOnly = true)
+    @Override
+    public Optional<Realm> findByFullPath(final String fullPath) {
+        if (StringUtils.isBlank(fullPath)
+                || (!SyncopeConstants.ROOT_REALM.equals(fullPath)
+                && !RealmDAO.PATH_PATTERN.matcher(fullPath).matches())) {
+
+            throw new MalformedPathException(fullPath);
+        }
+
+        TypedQuery<Realm> query = entityManager.createQuery(
+                "SELECT e FROM " + JPARealm.class.getSimpleName() + " e WHERE e.fullPath=:fullPath", Realm.class);
+        query.setParameter("fullPath", fullPath);
+
+        Realm result = null;
+        try {
+            result = query.getSingleResult();
+        } catch (NoResultException e) {
+            LOG.debug("Realm with fullPath {} not found", fullPath, e);
+        }
+
+        return Optional.ofNullable(result);
+    }
+
+    @Override
+    public List<Realm> findByName(final String name) {
+        TypedQuery<Realm> query = entityManager.createQuery(
+                "SELECT e FROM " + JPARealm.class.getSimpleName() + " e WHERE e.name=:name", Realm.class);
+        query.setParameter("name", name);
+
+        return query.getResultList();
+    }
+
+    @Override
+    public List<Realm> findChildren(final Realm realm) {
+        TypedQuery<Realm> query = entityManager.createQuery(
+                "SELECT e FROM " + JPARealm.class.getSimpleName() + " e WHERE e.parent=:realm", Realm.class);
+        query.setParameter("realm", realm);
+
+        return query.getResultList();
+    }
+
+    @Override
+    public List<Realm> findDescendants(final String base, final String prefix) {
+        List<Object> parameters = new ArrayList<>();
+
+        StringBuilder queryString = new StringBuilder("SELECT e FROM ").
+                append(JPARealm.class.getSimpleName()).
+                append(" e WHERE (e.fullPath=?").append(setParameter(parameters, base)).
+                append(" OR e.fullPath LIKE ?").append(setParameter(
+                parameters,
+                SyncopeConstants.ROOT_REALM.equals(base) ? "/%" : base + "/%")).
+                append(") ");
+        if (prefix != null) {
+            queryString.append("AND (e.fullPath=?").append(setParameter(parameters, prefix)).
+                    append(" OR e.fullPath LIKE ?").append(setParameter(
+                    parameters,
+                    SyncopeConstants.ROOT_REALM.equals(prefix) ? "/%" : prefix + "/%")).
+                    append(") ");
+        }
+        queryString.append("ORDER BY e.fullPath");
+
+        TypedQuery<Realm> query = entityManager.createQuery(queryString.toString(), Realm.class);
+
+        fillWithParameters(query, parameters);
+
+        return query.getResultList();
+    }
+
+    // ------------------------------------------ //
+    protected Optional<RealmSearchNode> getQueryForCustomConds(
+            final SearchCond cond,
+            final boolean not,
+            final List<Object> parameters) {
+
+        return Optional.empty();
+    }
+
+    protected Optional<QueryInfo> getQuery(final SearchCond cond, final List<Object> parameters) {
+        if (cond == null) {
+            return Optional.empty();
+        }
+
+        boolean not = cond.getType() == SearchCond.Type.NOT_LEAF;
+
+        Optional<RealmSearchNode> node = Optional.empty();
+        Set<String> plainSchemas = new HashSet<>();
+
+        switch (cond.getType()) {
+            case LEAF:
+            case NOT_LEAF:
+                node = cond.asLeaf(AnyCond.class).
+                        map(anyCond -> getQuery(anyCond, not, parameters)).
+                        or(() -> cond.asLeaf(AttrCond.class).
+                        map(attrCond -> {
+                            CheckResult<AttrCond> checked = check(attrCond);
+                            AttrCondQuery query = getQuery(attrCond, not, checked, parameters);
+                            if (query.addPlainSchemas()) {
+                                plainSchemas.add(checked.schema().getKey());
+                            }
+                            return query.node();
+                        }));
+
+                if (node.isEmpty()) {
+                    node = getQueryForCustomConds(cond, not, parameters);
+                }
+                break;
+
+            case AND:
+                RealmSearchNode andNode = new RealmSearchNode(RealmSearchNode.Type.AND);
+
+                getQuery(cond.getLeft(), parameters).ifPresent(left -> {
+                    andNode.add(left.node());
+                    plainSchemas.addAll(left.plainSchemas());
+                });
+
+                getQuery(cond.getRight(), parameters).ifPresent(right -> {
+                    andNode.add(right.node());
+                    plainSchemas.addAll(right.plainSchemas());
+                });
+
+                if (!andNode.getChildren().isEmpty()) {
+                    node = Optional.of(andNode);
+                }
+                break;
+
+            case OR:
+                RealmSearchNode orNode = new RealmSearchNode(RealmSearchNode.Type.OR);
+
+                getQuery(cond.getLeft(), parameters).ifPresent(left -> {
+                    orNode.add(left.node());
+                    plainSchemas.addAll(left.plainSchemas());
+                });
+
+                getQuery(cond.getRight(), parameters).ifPresent(right -> {
+                    orNode.add(right.node());
+                    plainSchemas.addAll(right.plainSchemas());
+                });
+
+                if (!orNode.getChildren().isEmpty()) {
+                    node = Optional.of(orNode);
+                }
+                break;
+
+            default:
+        }
+
+        return node.map(n -> new QueryInfo(n, plainSchemas));
+    }
+
+    protected abstract AttrCondQuery getQuery(
+            AttrCond cond,
+            boolean not,
+            CheckResult<AttrCond> checked,
+            List<Object> parameters);
+
+    protected RealmSearchNode getQuery(final AnyCond cond, final boolean not, final List<Object> parameters) {
+        CheckResult<AnyCond> checked = check(
+                cond,
+                realmUtils.getField(cond.getSchema()).
+                        orElseThrow(() -> new IllegalArgumentException("Invalid schema " + cond.getSchema())),
+                RELATIONSHIP_FIELDS);
+
+        return switch (checked.cond().getType()) {
+            case ISNULL ->
+                new RealmSearchNode.Leaf("r." + checked.cond().getSchema() + (not ? " IS NOT NULL" : " IS NULL"));
+
+            case ISNOTNULL ->
+                new RealmSearchNode.Leaf("r." + checked.cond().getSchema() + (not ? " IS NULL" : " IS NOT NULL"));
+
+            default ->
+                fillAttrQuery(
+                "r." + checked.cond().getSchema(),
+                checked.value(),
+                checked.schema(),
+                checked.cond(),
+                not,
+                parameters);
+        };
+    }
+
+    protected RealmSearchNode.Leaf fillAttrQuery(
+            final String column,
+            final PlainAttrValue attrValue,
+            final PlainSchema schema,
+            final AttrCond cond,
+            final boolean not,
+            final List<Object> parameters) {
+
+        boolean ignoreCase = AttrCond.Type.ILIKE == cond.getType() || AttrCond.Type.IEQ == cond.getType();
+
+        String left = column;
+        if (ignoreCase && (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum)) {
+            left = "LOWER(" + left + ')';
+        }
+
+        StringBuilder clause = new StringBuilder(left);
+        switch (cond.getType()) {
+
+            case ILIKE:
+            case LIKE:
+                if (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum) {
+                    if (not) {
+                        clause.append(" NOT");
+                    }
+                    clause.append(" LIKE ");
+                    if (ignoreCase) {
+                        clause.append("LOWER(?").append(setParameter(parameters, cond.getExpression())).append(')');
+                    } else {
+                        clause.append('?').append(setParameter(parameters, cond.getExpression()));
+                    }
+                    if (this instanceof OracleJPARealmSearchDAO) {
+                        clause.append(" ESCAPE '\\'");
+                    }
+                } else {
+                    LOG.error("LIKE is only compatible with string or enum schemas");
+                    return new RealmSearchNode.Leaf(ALWAYS_FALSE_CLAUSE);
+                }
+                break;
+
+            case IEQ:
+            case EQ:
+            default:
+                clause.append(not ? "<>" : "=");
+                if (ignoreCase
+                        && (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum)) {
+                    clause.append("LOWER(?").append(setParameter(parameters, attrValue.getValue())).append(')');
+                } else {
+                    clause.append('?').append(setParameter(parameters, attrValue.getValue()));
+                }
+                break;
+
+            case GE:
+                clause.append(not ? "<" : ">=");
+                clause.append('?').append(setParameter(parameters, attrValue.getValue()));
+                break;
+
+            case GT:
+                clause.append(not ? "<=" : ">");
+                clause.append('?').append(setParameter(parameters, attrValue.getValue()));
+                break;
+
+            case LE:
+                clause.append(not ? ">" : "<=");
+                clause.append('?').append(setParameter(parameters, attrValue.getValue()));
+                break;
+
+            case LT:
+                clause.append(not ? ">=" : "<");
+                clause.append('?').append(setParameter(parameters, attrValue.getValue()));
+                break;
+        }
+
+        return new RealmSearchNode.Leaf(
+                cond instanceof AnyCond
+                        ? clause.toString()
+                        : "r.schema_id='" + schema.getKey() + "' AND " + clause);
+    }
+
+    protected void visitNode(final RealmSearchNode node, final List<String> where) {
+        node.asLeaf().ifPresentOrElse(
+                leaf -> where.add(leaf.getClause()),
+                () -> {
+                    List<String> nodeWhere = new ArrayList<>();
+                    node.getChildren().forEach(child -> visitNode(child, nodeWhere));
+                    String op = " " + node.getType().name() + " ";
+                    where.add(nodeWhere.stream().
+                            map(w -> w.contains(" AND ") || w.contains(" OR ") ? "(" + w + ")" : w).
+                            collect(Collectors.joining(op)));
+                });
+    }
+
+    protected String buildFrom(final Set<String> plainSchemas, final OrderBySupport obs) {
+        return JPARealm.TABLE + " r";
+    }
+
+    protected String buildWhere(final Set<String> bases, final QueryInfo queryInfo, final List<Object> parameters) {
+        String fullPaths = bases.stream().
+                map(base -> "r.fullPath=?" + setParameter(parameters, base)
+                + " OR r.fullPath LIKE ?" + setParameter(
+                        parameters, SyncopeConstants.ROOT_REALM.equals(base) ? "/%" : base + "/%")).
+                collect(Collectors.joining(" OR "));
+
+        RealmSearchNode root;
+        if (queryInfo.node().getType() == RealmSearchNode.Type.AND) {
+            root = queryInfo.node();
+        } else {
+            root = new RealmSearchNode(RealmSearchNode.Type.AND);
+            root.add(queryInfo.node());
+        }
+
+        List<String> where = new ArrayList<>();
+        visitNode(root, where);
+
+        return "(" + fullPaths + ')'
+                + " AND (" + where.stream().
+                        map(w -> w.contains(" AND ") || w.contains(" OR ") ? "(" + w + ")" : w).
+                        collect(Collectors.joining(' ' + root.getType().name() + ' '))
+                + ')';
+    }
+
+    @Override
+    protected long doCount(final Set<String> bases, final SearchCond cond) {
+        List<Object> parameters = new ArrayList<>();
+
+        QueryInfo queryInfo = getQuery(cond, parameters).orElse(null);
+        if (queryInfo == null) {
+            LOG.error("Invalid search condition: {}", cond);
+            return 0;
+        }
+
+        String queryString = new StringBuilder("SELECT COUNT(DISTINCT r.id)").
+                append(" FROM ").append(buildFrom(queryInfo.plainSchemas(), null)).
+                append(" WHERE ").append(buildWhere(bases, queryInfo, parameters)).
+                toString();
+
+        LOG.debug("Query: {}, parameters: {}", queryString, parameters);
+
+        Query query = entityManager.createNativeQuery(queryString);
+        fillWithParameters(query, parameters);
+
+        return ((Number) query.getSingleResult()).longValue();
+    }
+
+    protected abstract void parseOrderByForPlainSchema(
+            OrderBySupport obs,
+            OrderBySupport.Item item,
+            Sort.Order clause,
+            PlainSchema schema,
+            String fieldName);
+
+    protected void parseOrderByForField(
+            final OrderBySupport.Item item,
+            final String fieldName,
+            final Sort.Order clause) {
+
+        item.select = "r." + fieldName;
+        item.where = StringUtils.EMPTY;
+        item.orderBy = "r." + fieldName + ' ' + clause.getDirection().name();
+    }
+
+    protected void parseOrderByForCustom(
+            final Sort.Order clause,
+            final OrderBySupport.Item item,
+            final OrderBySupport obs) {
+
+        // do nothing by default, meant for subclasses
+    }
+
+    protected OrderBySupport parseOrderBy(final List<Sort.Order> orderBy) {
+        OrderBySupport obs = new OrderBySupport();
+
+        Set<String> orderByUniquePlainSchemas = new HashSet<>();
+        Set<String> orderByNonUniquePlainSchemas = new HashSet<>();
+        orderBy.forEach(clause -> {
+            OrderBySupport.Item item = new OrderBySupport.Item();
+
+            parseOrderByForCustom(clause, item, obs);
+
+            if (item.isEmpty()) {
+                realmUtils.getField(clause.getProperty()).ifPresentOrElse(
+                        field -> {
+                            // Manage difference among external key attribute and internal JPA @Id
+                            String fieldName = "key".equals(clause.getProperty()) ? "id" : clause.getProperty();
+
+                            // Adjust field name to column name
+                            if (RELATIONSHIP_FIELDS.contains(fieldName)) {
+                                fieldName += "_id";
+                            }
+
+                            parseOrderByForField(item, fieldName, clause);
+                        },
+                        () -> {
+                            plainSchemaDAO.findById(clause.getProperty()).ifPresent(schema -> {
+                                if (schema.isUniqueConstraint()) {
+                                    orderByUniquePlainSchemas.add(schema.getKey());
+                                } else {
+                                    orderByNonUniquePlainSchemas.add(schema.getKey());
+                                }
+                                if (orderByUniquePlainSchemas.size() > 1 || orderByNonUniquePlainSchemas.size() > 1) {
+                                    throw syncopeClientException("Order by more than one attribute is not allowed; "
+                                            + "remove one from " + (orderByUniquePlainSchemas.size() > 1
+                                            ? orderByUniquePlainSchemas : orderByNonUniquePlainSchemas)).get();
+                                }
+                                parseOrderByForPlainSchema(obs, item, clause, schema, clause.getProperty());
+                            });
+                        });
+            }
+
+            if (item.isEmpty()) {
+                LOG.warn("Cannot build any valid clause from {}", clause);
+            } else {
+                obs.items.add(item);
+            }
+        });
+
+        return obs;
+    }
+
+    @Override
+    protected List<Realm> doSearch(final Set<String> bases, final SearchCond cond, final Pageable pageable) {
+        List<Object> parameters = new ArrayList<>();
+
+        QueryInfo queryInfo = getQuery(cond, parameters).orElse(null);
+        if (queryInfo == null) {
+            LOG.error("Invalid search condition: {}", cond);
+            return List.of();
+        }
+
+        OrderBySupport obs = parseOrderBy(pageable.getSort().toList());
+
+        StringBuilder queryString = new StringBuilder("SELECT DISTINCT r.id");
+        obs.items.forEach(item -> queryString.append(',').append(item.select));
+
+        queryString.append(" FROM ").append(buildFrom(queryInfo.plainSchemas(), obs)).
+                append(" WHERE ").append(buildWhere(bases, queryInfo, parameters)).
+                toString();
+
+        if (!obs.items.isEmpty()) {
+            queryString.append(" ORDER BY ").
+                    append(obs.items.stream().map(item -> item.orderBy).collect(Collectors.joining(",")));
+        }
+
+        LOG.debug("Query: {}, parameters: {}", queryString, parameters);
+
+        Query query = entityManager.createNativeQuery(queryString.toString());
+
+        if (pageable.isPaged()) {
+            query.setFirstResult(pageable.getPageSize() * pageable.getPageNumber());
+            query.setMaxResults(pageable.getPageSize());
+        }
+
+        fillWithParameters(query, parameters);
+
+        @SuppressWarnings("unchecked")
+        List<String> keys = query.getResultList().stream().
+                map(key -> key instanceof Object[] array ? (String) (array)[0] : ((String) key)).
+                toList();
+
+        return keys.stream().map(k -> entityManager.find(JPARealm.class, k)).
+                filter(Objects::nonNull).map(Realm.class::cast).toList();
+    }
+}
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmDAO.java
index 8228f71fdc..70b53a9937 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmDAO.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmDAO.java
@@ -246,7 +246,7 @@ public class JPARealmDAO implements RealmDAO {
             return;
         }
 
-        realmSearchDAO.findDescendants(realm.getFullPath(), null, Pageable.unpaged()).forEach(toBeDeleted -> {
+        realmSearchDAO.findDescendants(realm.getFullPath(), null).forEach(toBeDeleted -> {
             roleDAO.findByRealms(toBeDeleted).forEach(role -> role.getRealms().remove(toBeDeleted));
 
             toBeDeleted.setParent(null);
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmSearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmSearchDAO.java
deleted file mode 100644
index d3aef9eaa9..0000000000
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmSearchDAO.java
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * 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.syncope.core.persistence.jpa.dao;
-
-import jakarta.persistence.EntityManager;
-import jakarta.persistence.NoResultException;
-import jakarta.persistence.Query;
-import jakarta.persistence.TypedQuery;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.Strings;
-import org.apache.syncope.common.lib.SyncopeConstants;
-import org.apache.syncope.core.persistence.api.dao.MalformedPathException;
-import org.apache.syncope.core.persistence.api.dao.RealmDAO;
-import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO;
-import org.apache.syncope.core.persistence.api.entity.Realm;
-import org.apache.syncope.core.persistence.jpa.entity.JPARealm;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.data.domain.Pageable;
-import org.springframework.transaction.annotation.Transactional;
-
-public class JPARealmSearchDAO implements RealmSearchDAO {
-
-    protected static final Logger LOG = LoggerFactory.getLogger(RealmSearchDAO.class);
-
-    protected static int setParameter(final List<Object> parameters, final Object parameter) {
-        parameters.add(parameter);
-        return parameters.size();
-    }
-
-    protected static StringBuilder buildDescendantsQuery(
-            final Set<String> bases,
-            final String keyword,
-            final List<Object> parameters) {
-
-        String basesClause = bases.stream().
-                map(base -> "e.fullPath=?" + setParameter(parameters, base)
-                + " OR e.fullPath LIKE ?" + setParameter(
-                        parameters, SyncopeConstants.ROOT_REALM.equals(base) ? "/%" : base + "/%")).
-                collect(Collectors.joining(" OR "));
-
-        StringBuilder queryString = new StringBuilder("SELECT e FROM ").
-                append(JPARealm.class.getSimpleName()).append(" e ").
-                append("WHERE (").append(basesClause).append(')');
-
-        if (keyword != null) {
-            queryString.append(" AND LOWER(e.name) LIKE ?").
-                    append(setParameter(parameters, "%" + keyword.replaceAll("_", "\\\\_").toLowerCase() + "%"));
-        }
-
-        return queryString;
-    }
-
-    protected final EntityManager entityManager;
-
-    public JPARealmSearchDAO(final EntityManager entityManager) {
-        this.entityManager = entityManager;
-    }
-
-    @Transactional(readOnly = true)
-    @Override
-    public Optional<Realm> findByFullPath(final String fullPath) {
-        if (StringUtils.isBlank(fullPath)
-                || (!SyncopeConstants.ROOT_REALM.equals(fullPath)
-                && !RealmDAO.PATH_PATTERN.matcher(fullPath).matches())) {
-
-            throw new MalformedPathException(fullPath);
-        }
-
-        TypedQuery<Realm> query = entityManager.createQuery(
-                "SELECT e FROM " + JPARealm.class.getSimpleName() + " e WHERE e.fullPath=:fullPath", Realm.class);
-        query.setParameter("fullPath", fullPath);
-
-        Realm result = null;
-        try {
-            result = query.getSingleResult();
-        } catch (NoResultException e) {
-            LOG.debug("Realm with fullPath {} not found", fullPath, e);
-        }
-
-        return Optional.ofNullable(result);
-    }
-
-    @Override
-    public List<Realm> findByName(final String name) {
-        TypedQuery<Realm> query = entityManager.createQuery(
-                "SELECT e FROM " + JPARealm.class.getSimpleName() + " e WHERE e.name=:name", Realm.class);
-        query.setParameter("name", name);
-
-        return query.getResultList();
-    }
-
-    @Override
-    public List<Realm> findChildren(final Realm realm) {
-        TypedQuery<Realm> query = entityManager.createQuery(
-                "SELECT e FROM " + JPARealm.class.getSimpleName() + " e WHERE e.parent=:realm", Realm.class);
-        query.setParameter("realm", realm);
-
-        return query.getResultList();
-    }
-
-    @Override
-    public long countDescendants(final String base, final String keyword) {
-        return countDescendants(Set.of(base), keyword);
-    }
-
-    @Override
-    public long countDescendants(final Set<String> bases, final String keyword) {
-        List<Object> parameters = new ArrayList<>();
-
-        StringBuilder queryString = buildDescendantsQuery(bases, keyword, parameters);
-        Query query = entityManager.createQuery(Strings.CS.replaceOnce(
-                queryString.toString(),
-                "SELECT e ",
-                "SELECT COUNT(e) "));
-
-        for (int i = 1; i <= parameters.size(); i++) {
-            query.setParameter(i, parameters.get(i - 1));
-        }
-
-        return ((Number) query.getSingleResult()).longValue();
-    }
-
-    @Override
-    public List<Realm> findDescendants(final String base, final String keyword, final Pageable pageable) {
-        return findDescendants(Set.of(base), keyword, pageable);
-    }
-
-    @Override
-    public List<Realm> findDescendants(final Set<String> bases, final String keyword, final Pageable pageable) {
-        List<Object> parameters = new ArrayList<>();
-
-        StringBuilder queryString = buildDescendantsQuery(bases, keyword, parameters);
-        TypedQuery<Realm> query = entityManager.createQuery(
-                queryString.append(" ORDER BY e.fullPath").toString(), Realm.class);
-
-        for (int i = 1; i <= parameters.size(); i++) {
-            query.setParameter(i, parameters.get(i - 1));
-        }
-
-        if (pageable.isPaged()) {
-            query.setFirstResult(pageable.getPageSize() * pageable.getPageNumber());
-            query.setMaxResults(pageable.getPageSize());
-        }
-
-        return query.getResultList();
-    }
-
-    @Override
-    public List<String> findDescendants(final String base, final String prefix) {
-        List<Object> parameters = new ArrayList<>();
-
-        StringBuilder queryString = buildDescendantsQuery(Set.of(base), null, parameters);
-        TypedQuery<Realm> query = entityManager.createQuery(queryString.
-                append(" AND (e.fullPath=?").
-                append(setParameter(parameters, prefix)).
-                append(" OR e.fullPath LIKE ?").
-                append(setParameter(parameters, SyncopeConstants.ROOT_REALM.equals(prefix) ? "/%" : prefix + "/%")).
-                append(')').
-                append(" ORDER BY e.fullPath").toString(),
-                Realm.class);
-
-        for (int i = 1; i <= parameters.size(); i++) {
-            query.setParameter(i, parameters.get(i - 1));
-        }
-
-        return query.getResultList().stream().map(Realm::getKey).toList();
-    }
-}
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPATaskDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPATaskDAO.java
index 21c90912cf..c94ed7853a 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPATaskDAO.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPATaskDAO.java
@@ -339,7 +339,7 @@ public class JPATaskDAO implements TaskDAO {
             String realmKeysArg = AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.TASK_LIST).stream().
                     map(realmSearchDAO::findByFullPath).
                     filter(Optional::isPresent).
-                    flatMap(r -> realmSearchDAO.findDescendants(r.get().getFullPath(), null, Pageable.unpaged()).
+                    flatMap(r -> realmSearchDAO.findDescendants(r.get().getFullPath(), null).
                     stream()).
                     map(Realm::getKey).
                     distinct().
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPAAnySearchDAO.java
index ab768e1d75..09647e3ed6 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPAAnySearchDAO.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPAAnySearchDAO.java
@@ -19,7 +19,6 @@
 package org.apache.syncope.core.persistence.jpa.dao;
 
 import jakarta.persistence.EntityManager;
-import jakarta.persistence.EntityManagerFactory;
 import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
 import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
@@ -40,7 +39,6 @@ public class MariaDBJPAAnySearchDAO extends MySQLJPAAnySearchDAO {
             final EntityFactory entityFactory,
             final AnyUtilsFactory anyUtilsFactory,
             final PlainAttrValidationManager validator,
-            final EntityManagerFactory entityManagerFactory,
             final EntityManager entityManager) {
 
         super(
@@ -52,7 +50,6 @@ public class MariaDBJPAAnySearchDAO extends MySQLJPAAnySearchDAO {
                 entityFactory,
                 anyUtilsFactory,
                 validator,
-                entityManagerFactory,
                 entityManager);
     }
 }
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPARealmSearchDAO.java
similarity index 59%
copy from core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPAAnySearchDAO.java
copy to core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPARealmSearchDAO.java
index ab768e1d75..1f734aa3e7 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPAAnySearchDAO.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPARealmSearchDAO.java
@@ -19,40 +19,24 @@
 package org.apache.syncope.core.persistence.jpa.dao;
 
 import jakarta.persistence.EntityManager;
-import jakarta.persistence.EntityManagerFactory;
 import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
-import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
-import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
-import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
 import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.utils.RealmUtils;
 
-public class MariaDBJPAAnySearchDAO extends MySQLJPAAnySearchDAO {
+public class MariaDBJPARealmSearchDAO extends MySQLJPARealmSearchDAO {
 
-    public MariaDBJPAAnySearchDAO(
-            final RealmSearchDAO realmSearchDAO,
+    public MariaDBJPARealmSearchDAO(
+            final EntityManager entityManager,
+            final PlainSchemaDAO plainSchemaDAO,
             final UserDAO userDAO,
             final GroupDAO groupDAO,
-            final AnyObjectDAO anyObjectDAO,
-            final PlainSchemaDAO schemaDAO,
             final EntityFactory entityFactory,
-            final AnyUtilsFactory anyUtilsFactory,
             final PlainAttrValidationManager validator,
-            final EntityManagerFactory entityManagerFactory,
-            final EntityManager entityManager) {
+            final RealmUtils realmUtils) {
 
-        super(
-                realmSearchDAO,
-                userDAO,
-                groupDAO,
-                anyObjectDAO,
-                schemaDAO,
-                entityFactory,
-                anyUtilsFactory,
-                validator,
-                entityManagerFactory,
-                entityManager);
+        super(entityManager, plainSchemaDAO, userDAO, groupDAO, entityFactory, validator, realmUtils);
     }
 }
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPAAnySearchDAO.java
index e9c4f007b3..3aa62156eb 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPAAnySearchDAO.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPAAnySearchDAO.java
@@ -19,7 +19,6 @@
 package org.apache.syncope.core.persistence.jpa.dao;
 
 import jakarta.persistence.EntityManager;
-import jakarta.persistence.EntityManagerFactory;
 import java.time.format.DateTimeFormatter;
 import java.util.List;
 import java.util.Optional;
@@ -50,7 +49,6 @@ public class MySQLJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
             final EntityFactory entityFactory,
             final AnyUtilsFactory anyUtilsFactory,
             final PlainAttrValidationManager validator,
-            final EntityManagerFactory entityManagerFactory,
             final EntityManager entityManager) {
 
         super(
@@ -62,7 +60,6 @@ public class MySQLJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
                 entityFactory,
                 anyUtilsFactory,
                 validator,
-                entityManagerFactory,
                 entityManager);
     }
 
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPARealmSearchDAO.java
similarity index 57%
copy from core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPAAnySearchDAO.java
copy to core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPARealmSearchDAO.java
index e9c4f007b3..46d5cf4f8c 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPAAnySearchDAO.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPARealmSearchDAO.java
@@ -4,14 +4,14 @@
  * 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
+ * \"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
+ * \"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.
@@ -19,56 +19,71 @@
 package org.apache.syncope.core.persistence.jpa.dao;
 
 import jakarta.persistence.EntityManager;
-import jakarta.persistence.EntityManagerFactory;
 import java.time.format.DateTimeFormatter;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.lib.types.AttrSchemaType;
 import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
-import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
-import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
-import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
 import org.apache.syncope.core.persistence.api.entity.EntityFactory;
 import org.apache.syncope.core.persistence.api.entity.PlainAttr;
 import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
 import org.apache.syncope.core.persistence.api.entity.PlainSchema;
+import org.apache.syncope.core.persistence.api.utils.RealmUtils;
+import org.apache.syncope.core.persistence.jpa.entity.JPARealm;
 import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
 import org.springframework.data.domain.Sort;
 
-public class MySQLJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
+public class MySQLJPARealmSearchDAO extends AbstractJPARealmSearchDAO {
 
-    public MySQLJPAAnySearchDAO(
-            final RealmSearchDAO realmSearchDAO,
+    protected static String from(final PlainSchema schema) {
+        String sqlType = switch (schema.getType()) {
+            case Long ->
+                "BIGINT";
+
+            case Double ->
+                "DOUBLE";
+
+            case Boolean ->
+                "VARCHAR(8)";
+
+            default ->
+                "VARCHAR(255)";
+        };
+
+        if (schema.isUniqueConstraint()) {
+            return "JSON_TABLE(r.plainAttrs, '$[*]' COLUMNS ("
+                    + "schemaz VARCHAR(255) PATH '$.schema', "
+                    + "uniqueValue " + sqlType + " PATH '$.uniqueValue." + key(schema.getType()) + "'"
+                    + ")) AS " + schema.getKey();
+        }
+
+        return "JSON_TABLE(r.plainAttrs, '$[*]' COLUMNS ("
+                + "schemaz VARCHAR(255) PATH '$.schema', "
+                + "NESTED PATH '$.values[*]' COLUMNS ("
+                + "valuez " + sqlType + " PATH '$." + key(schema.getType()) + "'"
+                + "))) AS " + schema.getKey();
+    }
+
+    public MySQLJPARealmSearchDAO(
+            final EntityManager entityManager,
+            final PlainSchemaDAO plainSchemaDAO,
             final UserDAO userDAO,
             final GroupDAO groupDAO,
-            final AnyObjectDAO anyObjectDAO,
-            final PlainSchemaDAO schemaDAO,
             final EntityFactory entityFactory,
-            final AnyUtilsFactory anyUtilsFactory,
             final PlainAttrValidationManager validator,
-            final EntityManagerFactory entityManagerFactory,
-            final EntityManager entityManager) {
-
-        super(
-                realmSearchDAO,
-                userDAO,
-                groupDAO,
-                anyObjectDAO,
-                schemaDAO,
-                entityFactory,
-                anyUtilsFactory,
-                validator,
-                entityManagerFactory,
-                entityManager);
+            final RealmUtils realmUtils) {
+
+        super(entityManager, plainSchemaDAO, userDAO, groupDAO, entityFactory, validator, realmUtils);
     }
 
     @Override
     protected void parseOrderByForPlainSchema(
-            final SearchSupport svs,
             final OrderBySupport obs,
             final OrderBySupport.Item item,
             final Sort.Order clause,
@@ -78,53 +93,45 @@ public class MySQLJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
         // keep track of involvement of non-mandatory schemas in the order by clauses
         obs.nonMandatorySchemas = !"true".equals(schema.getMandatoryCondition());
 
-        obs.views.add(svs.field());
-
-        item.select = new StringBuilder().append(schema.isUniqueConstraint()
-                ? "( SELECT JSON_UNQUOTE(JSON_EXTRACT(usa.attrUniqueValue, '$." + key(schema.getType()) + "')) "
-                : "( SELECT usa." + key(schema.getType())).
-                append(" FROM ").
-                append(svs.field().name()).
-                append(" usa WHERE usa.any_id = ").
-                append(svs.field().alias()).
-                append(".any_id").
-                append(" AND usa.plainSchema ='").append(fieldName).append("'").
-                append(" LIMIT 1").
-                append(") AS ").append(fieldName).toString();
-        item.where = "plainSchema = '" + fieldName + '\'';
+        item.select = schema.getKey() + "."
+                + (schema.isUniqueConstraint() ? "uniqueValue" : "valuez")
+                + " AS " + schema.getKey();
+        item.where = StringUtils.EMPTY;
         item.orderBy = fieldName + ' ' + clause.getDirection().name();
     }
 
-    protected AnySearchNode.Leaf filJSONAttrQuery(
-            final SearchSupport.SearchView from,
+    protected RealmSearchNode.Leaf filJSONAttrQuery(
             final PlainAttrValue attrValue,
             final PlainSchema schema,
             final AttrCond cond,
             final boolean not,
             final List<Object> parameters) {
 
-        String key = key(schema.getType());
-
         String value = Optional.ofNullable(attrValue.getDateValue()).
                 map(DateTimeFormatter.ISO_OFFSET_DATE_TIME::format).
                 orElseGet(cond::getExpression);
+        Object typedValue = schema.getType() == AttrSchemaType.Date ? value : attrValue.getValue();
 
-        boolean lower = (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum)
-                && (cond.getType() == AttrCond.Type.IEQ || cond.getType() == AttrCond.Type.ILIKE);
+        boolean isString = schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum;
+        boolean lower = isString && (cond.getType() == AttrCond.Type.IEQ || cond.getType() == AttrCond.Type.ILIKE);
+        boolean binary = isString && !lower;
 
-        StringBuilder clause = new StringBuilder("plainSchema=?").append(setParameter(parameters, cond.getSchema())).
+        StringBuilder clause = new StringBuilder(schema.getKey()).
+                append(".schemaz=?").append(setParameter(parameters, cond.getSchema())).
                 append(" AND ").
                 append(lower ? "LOWER(" : "").
+                append(binary ? "BINARY " : "").
+                append(schema.getKey()).append('.').
                 append(schema.isUniqueConstraint()
-                        ? "JSON_UNQUOTE(JSON_EXTRACT(attrUniqueValue, '$." + key + "'))"
-                        : key).
+                        ? "uniqueValue"
+                        : "valuez").
                 append(lower ? ')' : "");
 
         switch (cond.getType()) {
             case LIKE:
             case ILIKE:
                 if (not) {
-                    clause.append("NOT ");
+                    clause.append(" NOT");
                 }
                 clause.append(" LIKE ");
                 break;
@@ -171,10 +178,13 @@ public class MySQLJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
         }
 
         clause.append(lower ? "LOWER(" : "").
-                append('?').append(setParameter(parameters, value)).
+                append('?').append(setParameter(parameters,
+                cond.getType() == AttrCond.Type.LIKE || cond.getType() == AttrCond.Type.ILIKE
+                ? value
+                : typedValue)).
                 append(lower ? ")" : "");
 
-        return new AnySearchNode.Leaf(from, clause.toString());
+        return new RealmSearchNode.Leaf(clause.toString());
     }
 
     @Override
@@ -182,10 +192,8 @@ public class MySQLJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
             final AttrCond cond,
             final boolean not,
             final CheckResult<AttrCond> checked,
-            final List<Object> parameters,
-            final SearchSupport svs) {
+            final List<Object> parameters) {
 
-        // normalize NULL / NOT NULL checks
         if (not) {
             if (cond.getType() == AttrCond.Type.ISNULL) {
                 cond.setType(AttrCond.Type.ISNOTNULL);
@@ -196,18 +204,16 @@ public class MySQLJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
 
         switch (cond.getType()) {
             case ISNOTNULL -> {
-                return new AttrCondQuery(true, new AnySearchNode.Leaf(
-                        svs.field(),
+                return new AttrCondQuery(true, new RealmSearchNode.Leaf(
                         "JSON_SEARCH("
-                        + "plainAttrs, 'one', '" + checked.schema().getKey() + "', NULL, '$[*].schema'"
+                        + "r.plainAttrs, 'one', '" + checked.schema().getKey() + "', NULL, '$[*].schema'"
                         + ") IS NOT NULL"));
             }
 
             case ISNULL -> {
-                return new AttrCondQuery(true, new AnySearchNode.Leaf(
-                        svs.field(),
+                return new AttrCondQuery(true, new RealmSearchNode.Leaf(
                         "JSON_SEARCH("
-                        + "plainAttrs, 'one', '" + checked.schema().getKey() + "', NULL, '$[*].schema'"
+                        + "r.plainAttrs, 'one', '" + checked.schema().getKey() + "', NULL, '$[*].schema'"
                         + ") IS NULL"));
             }
 
@@ -221,30 +227,26 @@ public class MySQLJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
                         container.add(checked.value());
                     }
 
-                    return new AttrCondQuery(true, new AnySearchNode.Leaf(
-                            svs.field(),
+                    return new AttrCondQuery(true, new RealmSearchNode.Leaf(
                             "JSON_CONTAINS("
-                            + "plainAttrs, '" + POJOHelper.serialize(List.of(container)).replace("'", "''")
+                            + "r.plainAttrs, '" + POJOHelper.serialize(List.of(container)).replace("'", "''")
                             + "')"));
                 } else {
-                    AnySearchNode.Leaf node;
+                    RealmSearchNode.Leaf node;
                     if (not && checked.schema().isMultivalue()) {
-                        AnySearchNode.Leaf notNode = filJSONAttrQuery(
-                                svs.field(),
+                        RealmSearchNode.Leaf notNode = filJSONAttrQuery(
                                 checked.value(),
                                 checked.schema(),
                                 cond,
                                 false,
                                 parameters);
-                        node = new AnySearchNode.Leaf(
-                                notNode.getFrom(),
-                                "sv.any_id NOT IN ("
-                                + "SELECT any_id FROM " + notNode.getFrom().name()
-                                + " WHERE " + notNode.getClause().replace(notNode.getFrom().alias() + ".", "")
+                        node = new RealmSearchNode.Leaf(
+                                "r.id NOT IN ("
+                                + "SELECT r.id FROM " + JPARealm.TABLE + " r, " + from(checked.schema())
+                                + " WHERE " + notNode.getClause()
                                 + ")");
                     } else {
                         node = filJSONAttrQuery(
-                                svs.field(),
                                 checked.value(),
                                 checked.schema(),
                                 cond,
@@ -256,4 +258,24 @@ public class MySQLJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
             }
         }
     }
+
+    @Override
+    protected String buildFrom(final Set<String> plainSchemas, final OrderBySupport obs) {
+        StringBuilder clause = new StringBuilder(super.buildFrom(plainSchemas, obs));
+
+        plainSchemas.forEach(schema -> plainSchemaDAO.findById(schema).
+                ifPresent(pschema -> clause.append(",").append(from(pschema))));
+
+        if (obs != null) {
+            obs.items.forEach(item -> {
+                String schema = StringUtils.substringBefore(item.orderBy, ' ');
+                if (StringUtils.isNotBlank(schema) && !plainSchemas.contains(schema)) {
+                    plainSchemaDAO.findById(schema).ifPresent(
+                            pschema -> clause.append(" LEFT OUTER JOIN ").append(from(pschema)).append(" ON 1=1"));
+                }
+            });
+        }
+
+        return clause.toString();
+    }
 }
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleJPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleJPAAnySearchDAO.java
index 6ee2ebf412..bf2dedc071 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleJPAAnySearchDAO.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleJPAAnySearchDAO.java
@@ -19,7 +19,6 @@
 package org.apache.syncope.core.persistence.jpa.dao;
 
 import jakarta.persistence.EntityManager;
-import jakarta.persistence.EntityManagerFactory;
 import java.time.format.DateTimeFormatter;
 import java.util.List;
 import java.util.Map;
@@ -67,7 +66,6 @@ public class OracleJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
             final EntityFactory entityFactory,
             final AnyUtilsFactory anyUtilsFactory,
             final PlainAttrValidationManager validator,
-            final EntityManagerFactory entityManagerFactory,
             final EntityManager entityManager) {
 
         super(
@@ -79,7 +77,6 @@ public class OracleJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
                 entityFactory,
                 anyUtilsFactory,
                 validator,
-                entityManagerFactory,
                 entityManager);
     }
 
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleJPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleJPARealmSearchDAO.java
similarity index 51%
copy from core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleJPAAnySearchDAO.java
copy to core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleJPARealmSearchDAO.java
index 6ee2ebf412..7ae1fd9f42 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleJPAAnySearchDAO.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleJPARealmSearchDAO.java
@@ -19,38 +19,27 @@
 package org.apache.syncope.core.persistence.jpa.dao;
 
 import jakarta.persistence.EntityManager;
-import jakarta.persistence.EntityManagerFactory;
 import java.time.format.DateTimeFormatter;
 import java.util.List;
-import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.lib.types.AttrSchemaType;
 import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
-import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
-import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
-import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
 import org.apache.syncope.core.persistence.api.entity.EntityFactory;
 import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
 import org.apache.syncope.core.persistence.api.entity.PlainSchema;
+import org.apache.syncope.core.persistence.api.utils.RealmUtils;
+import org.apache.syncope.core.persistence.jpa.entity.JPARealm;
 import org.springframework.data.domain.Sort;
 
-public class OracleJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
+public class OracleJPARealmSearchDAO extends AbstractJPARealmSearchDAO {
 
-    /**
-     *
-     * @param schema
-     * @return JSON_TABLE(plainAttrs, '$[*]?(@.schema == "fullname").uniqueValue' \
-     * COLUMNS uniqueValue PATH '$.stringValue') AS fullname
-     * or JSON_TABLE(plainAttrs, '$[*]?(@.schema == "loginDate").values[*]' \
-     * COLUMNS valuez PATH '$.dateValue') AS loginDate
-     */
-    public static String from(final PlainSchema schema) {
+    protected static String from(final PlainSchema schema) {
         return new StringBuilder("JSON_TABLE(plainAttrs, '$[*]?(@.schema == \"").append(schema.getKey()).append("\").").
                 append(schema.isUniqueConstraint() ? "uniqueValue" : "values[*]").
                 append("' COLUMNS ").append(schema.isUniqueConstraint() ? "uniqueValue" : "valuez").
@@ -58,44 +47,20 @@ public class OracleJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
                 toString();
     }
 
-    public OracleJPAAnySearchDAO(
-            final RealmSearchDAO realmSearchDAO,
+    public OracleJPARealmSearchDAO(
+            final EntityManager entityManager,
+            final PlainSchemaDAO plainSchemaDAO,
             final UserDAO userDAO,
             final GroupDAO groupDAO,
-            final AnyObjectDAO anyObjectDAO,
-            final PlainSchemaDAO schemaDAO,
             final EntityFactory entityFactory,
-            final AnyUtilsFactory anyUtilsFactory,
             final PlainAttrValidationManager validator,
-            final EntityManagerFactory entityManagerFactory,
-            final EntityManager entityManager) {
+            final RealmUtils realmUtils) {
 
-        super(
-                realmSearchDAO,
-                userDAO,
-                groupDAO,
-                anyObjectDAO,
-                schemaDAO,
-                entityFactory,
-                anyUtilsFactory,
-                validator,
-                entityManagerFactory,
-                entityManager);
-    }
-
-    @Override
-    protected SearchSupport.SearchView defaultSV(final SearchSupport svs) {
-        return svs.table();
-    }
-
-    @Override
-    protected String anyId(final SearchSupport.SearchView sv) {
-        return sv.alias() + ".id";
+        super(entityManager, plainSchemaDAO, userDAO, groupDAO, entityFactory, validator, realmUtils);
     }
 
     @Override
     protected void parseOrderByForPlainSchema(
-            final SearchSupport svs,
             final OrderBySupport obs,
             final OrderBySupport.Item item,
             final Sort.Order clause,
@@ -105,8 +70,6 @@ public class OracleJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
         // keep track of involvement of non-mandatory schemas in the order by clauses
         obs.nonMandatorySchemas = !"true".equals(schema.getMandatoryCondition());
 
-        obs.views.add(svs.table());
-
         item.select = schema.getKey() + "."
                 + (schema.isUniqueConstraint() ? "uniqueValue" : "valuez")
                 + " AS " + schema.getKey();
@@ -114,20 +77,7 @@ public class OracleJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
         item.orderBy = fieldName + ' ' + clause.getDirection().name();
     }
 
-    @Override
-    protected void parseOrderByForField(
-            final SearchSupport svs,
-            final OrderBySupport.Item item,
-            final String fieldName,
-            final Sort.Order clause) {
-
-        item.select = svs.table().alias() + '.' + fieldName;
-        item.where = StringUtils.EMPTY;
-        item.orderBy = svs.table().alias() + '.' + fieldName + ' ' + clause.getDirection().name();
-    }
-
-    protected AnySearchNode.Leaf filJSONAttrQuery(
-            final SearchSupport.SearchView from,
+    protected RealmSearchNode.Leaf filJSONAttrQuery(
             final PlainAttrValue attrValue,
             final PlainSchema schema,
             final AttrCond cond,
@@ -137,6 +87,7 @@ public class OracleJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
         String value = Optional.ofNullable(attrValue.getDateValue()).
                 map(DateTimeFormatter.ISO_OFFSET_DATE_TIME::format).
                 orElseGet(cond::getExpression);
+        Object typedValue = schema.getType() == AttrSchemaType.Date ? value : attrValue.getValue();
 
         boolean lower = (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum)
                 && (cond.getType() == AttrCond.Type.IEQ || cond.getType() == AttrCond.Type.ILIKE);
@@ -149,61 +100,45 @@ public class OracleJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
             case LIKE:
             case ILIKE:
                 if (not) {
-                    clause.append("NOT ");
+                    clause.append(" NOT");
                 }
-                clause.append(" LIKE ");
+                clause.append(" LIKE ").
+                        append(lower ? "LOWER(" : "").
+                        append('?').append(setParameter(parameters, value)).
+                        append(lower ? ')' : "").append(" ESCAPE '\\'");
                 break;
 
             case GE:
-                if (not) {
-                    clause.append('<');
-                } else {
-                    clause.append(">=");
-                }
+                clause.append(not ? "<" : ">=").
+                        append('?').append(setParameter(parameters, typedValue));
                 break;
 
             case GT:
-                if (not) {
-                    clause.append("<=");
-                } else {
-                    clause.append('>');
-                }
+                clause.append(not ? "<=" : ">").
+                        append('?').append(setParameter(parameters, typedValue));
                 break;
 
             case LE:
-                if (not) {
-                    clause.append('>');
-                } else {
-                    clause.append("<=");
-                }
+                clause.append(not ? ">" : "<=").
+                        append('?').append(setParameter(parameters, typedValue));
                 break;
 
             case LT:
-                if (not) {
-                    clause.append(">=");
-                } else {
-                    clause.append('<');
-                }
+                clause.append(not ? ">=" : "<").
+                        append('?').append(setParameter(parameters, typedValue));
                 break;
 
-            case EQ:
             case IEQ:
+            case EQ:
             default:
-                if (not) {
-                    clause.append('!');
-                }
-                clause.append('=');
+                clause.append(not ? " != " : " = ").
+                        append(lower ? "LOWER(" : "").
+                        append('?').append(setParameter(parameters, typedValue)).
+                        append(lower ? ")" : "");
+                break;
         }
 
-        clause.append(lower ? "LOWER(" : "").
-                append('?').append(setParameter(parameters, value)).
-                append(lower ? ")" : "");
-
-        // workaround for Oracle DB adding explicit escaping string, to search for literal _ (underscore)
-        if (cond.getType() == AttrCond.Type.ILIKE || cond.getType() == AttrCond.Type.LIKE) {
-            clause.append(" ESCAPE '\\'");
-        }
-        return new AnySearchNode.Leaf(from, clause.toString());
+        return new RealmSearchNode.Leaf(clause.toString());
     }
 
     @Override
@@ -211,10 +146,8 @@ public class OracleJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
             final AttrCond cond,
             final boolean not,
             final CheckResult<AttrCond> checked,
-            final List<Object> parameters,
-            final SearchSupport svs) {
+            final List<Object> parameters) {
 
-        // normalize NULL / NOT NULL checks
         if (not) {
             if (cond.getType() == AttrCond.Type.ISNULL) {
                 cond.setType(AttrCond.Type.ISNOTNULL);
@@ -225,67 +158,40 @@ public class OracleJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
 
         switch (cond.getType()) {
             case ISNOTNULL -> {
-                return new AttrCondQuery(false, new AnySearchNode.Leaf(
-                        svs.table(),
-                        "JSON_EXISTS(plainAttrs, '$[*]?(@.schema == \"" + checked.schema().getKey() + "\")')"));
+                return new AttrCondQuery(false, new RealmSearchNode.Leaf(
+                        "JSON_EXISTS(r.plainAttrs, '$[*]?(@.schema == \""
+                        + checked.schema().getKey() + "\")')"));
             }
 
             case ISNULL -> {
-                return new AttrCondQuery(false, new AnySearchNode.Leaf(
-                        svs.table(),
-                        "NOT JSON_EXISTS(plainAttrs, '$[*]?(@.schema == \"" + checked.schema().getKey() + "\")')"));
+                return new AttrCondQuery(false, new RealmSearchNode.Leaf(
+                        "NOT JSON_EXISTS(r.plainAttrs, '$[*]?(@.schema == \""
+                        + checked.schema().getKey() + "\")')"));
             }
 
             default -> {
-                AnySearchNode.Leaf node;
+                RealmSearchNode.Leaf node;
                 if (not && checked.schema().isMultivalue()) {
-                    AnySearchNode.Leaf notNode = filJSONAttrQuery(
-                            svs.table(),
-                            checked.value(),
-                            checked.schema(),
-                            cond,
-                            false,
-                            parameters);
-                    node = new AnySearchNode.Leaf(
-                            notNode.getFrom(),
+                    RealmSearchNode.Leaf notNode = filJSONAttrQuery(
+                            checked.value(), checked.schema(), cond, false, parameters);
+                    node = new RealmSearchNode.Leaf(
                             "id NOT IN ("
-                            + "SELECT id FROM " + notNode.getFrom().name() + "," + from(checked.schema())
-                            + " WHERE " + notNode.getClause().replace(notNode.getFrom().alias() + ".", "")
+                            + "SELECT id FROM " + JPARealm.TABLE + " e, " + from(checked.schema())
+                            + " WHERE " + notNode.getClause()
                             + ")");
                     return new AttrCondQuery(false, node);
                 } else {
-                    node = filJSONAttrQuery(
-                            svs.table(),
-                            checked.value(),
-                            checked.schema(),
-                            cond,
-                            not,
-                            parameters);
+                    node = filJSONAttrQuery(checked.value(), checked.schema(), cond, not, parameters);
                 }
                 return new AttrCondQuery(true, node);
             }
-        }
-    }
 
-    @Override
-    protected void visitNode(
-            final AnySearchNode node,
-            final Map<SearchSupport.SearchView, Boolean> counters,
-            final Set<SearchSupport.SearchView> from,
-            final List<String> where,
-            final SearchSupport svs) {
-
-        counters.clear();
-        super.visitNode(node, counters, from, where, svs);
+        }
     }
 
     @Override
-    protected String buildFrom(
-            final Set<SearchSupport.SearchView> from,
-            final Set<String> plainSchemas,
-            final OrderBySupport obs) {
-
-        StringBuilder clause = new StringBuilder(super.buildFrom(from, plainSchemas, obs));
+    protected String buildFrom(final Set<String> plainSchemas, final OrderBySupport obs) {
+        StringBuilder clause = new StringBuilder(super.buildFrom(plainSchemas, obs));
 
         plainSchemas.forEach(schema -> plainSchemaDAO.findById(schema).
                 ifPresent(pschema -> clause.append(",").append(from(pschema))));
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAAnySearchDAO.java
index 01a6a25d79..c1dae44bb8 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAAnySearchDAO.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAAnySearchDAO.java
@@ -19,7 +19,6 @@
 package org.apache.syncope.core.persistence.jpa.dao;
 
 import jakarta.persistence.EntityManager;
-import jakarta.persistence.EntityManagerFactory;
 import java.time.format.DateTimeFormatter;
 import java.util.HashSet;
 import java.util.List;
@@ -68,7 +67,6 @@ public class PGJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
             final EntityFactory entityFactory,
             final AnyUtilsFactory anyUtilsFactory,
             final PlainAttrValidationManager validator,
-            final EntityManagerFactory entityManagerFactory,
             final EntityManager entityManager) {
 
         super(
@@ -80,7 +78,6 @@ public class PGJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
                 entityFactory,
                 anyUtilsFactory,
                 validator,
-                entityManagerFactory,
                 entityManager);
     }
 
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPARealmSearchDAO.java
similarity index 59%
copy from core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAAnySearchDAO.java
copy to core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPARealmSearchDAO.java
index 01a6a25d79..7bac97a495 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAAnySearchDAO.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPARealmSearchDAO.java
@@ -19,29 +19,25 @@
 package org.apache.syncope.core.persistence.jpa.dao;
 
 import jakarta.persistence.EntityManager;
-import jakarta.persistence.EntityManagerFactory;
 import java.time.format.DateTimeFormatter;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.lib.types.AttrSchemaType;
 import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
-import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
-import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
-import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
 import org.apache.syncope.core.persistence.api.entity.EntityFactory;
 import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
 import org.apache.syncope.core.persistence.api.entity.PlainSchema;
+import org.apache.syncope.core.persistence.api.utils.RealmUtils;
 import org.springframework.data.domain.Sort;
 
-public class PGJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
+public class PGJPARealmSearchDAO extends AbstractJPARealmSearchDAO {
 
     protected static final String REGEX_CHARS = "!$()*+.:<=>?[\\]^{|}-";
 
@@ -54,49 +50,23 @@ public class PGJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
     }
 
     protected static String escapeIfString(final String value, final boolean isStr) {
-        return isStr
-                ? new StringBuilder().append('"').append(value.replace("'", "''")).append('"').toString()
-                : value;
+        return isStr ? '"' + value.replace("'", "''") + '"' : value;
     }
 
-    public PGJPAAnySearchDAO(
-            final RealmSearchDAO realmSearchDAO,
+    public PGJPARealmSearchDAO(
+            final EntityManager entityManager,
+            final PlainSchemaDAO plainSchemaDAO,
             final UserDAO userDAO,
             final GroupDAO groupDAO,
-            final AnyObjectDAO anyObjectDAO,
-            final PlainSchemaDAO schemaDAO,
             final EntityFactory entityFactory,
-            final AnyUtilsFactory anyUtilsFactory,
             final PlainAttrValidationManager validator,
-            final EntityManagerFactory entityManagerFactory,
-            final EntityManager entityManager) {
-
-        super(
-                realmSearchDAO,
-                userDAO,
-                groupDAO,
-                anyObjectDAO,
-                schemaDAO,
-                entityFactory,
-                anyUtilsFactory,
-                validator,
-                entityManagerFactory,
-                entityManager);
-    }
+            final RealmUtils realmUtils) {
 
-    @Override
-    protected SearchSupport.SearchView defaultSV(final SearchSupport svs) {
-        return svs.table();
-    }
-
-    @Override
-    protected String anyId(final SearchSupport.SearchView sv) {
-        return sv.alias() + ".id";
+        super(entityManager, plainSchemaDAO, userDAO, groupDAO, entityFactory, validator, realmUtils);
     }
 
     @Override
     protected void parseOrderByForPlainSchema(
-            final SearchSupport svs,
             final OrderBySupport obs,
             final OrderBySupport.Item item,
             final Sort.Order clause,
@@ -106,33 +76,19 @@ public class PGJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
         // keep track of involvement of non-mandatory schemas in the order by clauses
         obs.nonMandatorySchemas = !"true".equals(schema.getMandatoryCondition());
 
-        obs.views.add(svs.table());
-
         item.select = fieldName + " -> 0 AS " + fieldName;
         item.where = StringUtils.EMPTY;
         item.orderBy = fieldName + ' ' + clause.getDirection().name();
     }
 
-    @Override
-    protected void parseOrderByForField(
-            final SearchSupport svs,
-            final OrderBySupport.Item item,
-            final String fieldName,
-            final Sort.Order clause) {
-
-        item.select = svs.table().alias() + '.' + fieldName;
-        item.where = StringUtils.EMPTY;
-        item.orderBy = svs.table().alias() + '.' + fieldName + ' ' + clause.getDirection().name();
-    }
-
-    protected AnySearchNode.Leaf filJSONAttrQuery(
-            final SearchSupport.SearchView from,
+    protected RealmSearchNode.Leaf filJSONAttrQuery(
             final PlainAttrValue attrValue,
             final PlainSchema schema,
             final AttrCond cond,
             final boolean not) {
 
         String key = key(schema.getType());
+        String valuesPath = schema.isUniqueConstraint() ? "uniqueValue" : "values[*]";
 
         String value = Optional.ofNullable(attrValue.getDateValue()).
                 map(DateTimeFormatter.ISO_OFFSET_DATE_TIME::format).
@@ -141,30 +97,25 @@ public class PGJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
         boolean isStr = true;
         boolean lower = false;
         if (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum) {
-            lower = (cond.getType() == AttrCond.Type.IEQ || cond.getType() == AttrCond.Type.ILIKE);
+            lower = cond.getType() == AttrCond.Type.IEQ || cond.getType() == AttrCond.Type.ILIKE;
         } else if (schema.getType() != AttrSchemaType.Date) {
-            lower = false;
             try {
                 switch (schema.getType()) {
                     case Long ->
                         Long.valueOf(value);
-
                     case Double ->
                         Double.valueOf(value);
-
                     case Boolean -> {
                         if (!("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value))) {
                             throw new IllegalArgumentException();
                         }
                     }
-
                     default -> {
                     }
                 }
-
                 isStr = false;
             } catch (Exception nfe) {
-                // ignore
+                // ignore — treat as string
             }
         }
 
@@ -172,57 +123,63 @@ public class PGJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
         switch (cond.getType()) {
             case ILIKE:
             case LIKE:
-                // jsonb_path_exists(Nome, '$[*] ? (@.stringValue like_regex "EL.*" flag "i")')
                 if (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum) {
-                    clause.append("jsonb_path_exists(").append(schema.getKey()).append(", '$[*] ? ").
-                            append("(@.").append(key).append(" like_regex \"").
-                            append(escapeForLikeRegex(value).replace("%", ".*")).
-                            append("\"").
+                    clause.append("jsonb_path_exists(r.plainAttrs, '$[*] ? (@.schema == \"").
+                            append(schema.getKey()).append("\").").append(valuesPath).
+                            append(" ? (@.").append(key).append(" like_regex \"").
+                            append(escapeForLikeRegex(value).replace("%", ".*")).append("\"").
                             append(lower ? " flag \"i\"" : "").append(")')");
                 } else {
                     LOG.error("LIKE is only compatible with string or enum schemas");
-                    clause.append(' ').append(ALWAYS_FALSE_CLAUSE);
+                    clause.append(ALWAYS_FALSE_CLAUSE);
                 }
                 break;
 
             case IEQ:
             case EQ:
             default:
-                clause.append("jsonb_path_exists(").append(schema.getKey()).append(", '$[*] ? ").
-                        append("(@.").append(key);
-
+                clause.append("jsonb_path_exists(r.plainAttrs, '$[*] ? (@.schema == \"").
+                        append(schema.getKey()).append("\").").append(valuesPath).
+                        append(" ? (");
                 if (StringUtils.containsAny(value, REGEX_CHARS) || lower) {
-                    clause.append(" like_regex \"^").
-                            append(escapeForLikeRegex(value).replace("'", "''")).
-                            append("$\"");
+                    clause.append("@.").append(key).append(" like_regex \"^").
+                            append(escapeForLikeRegex(value).replace("'", "''")).append("$\"");
+                } else if (isStr) {
+                    clause.append("@.").append(key).append(" == ").append(escapeIfString(value, true));
                 } else {
-                    clause.append(" == ").append(escapeIfString(value, isStr));
+                    // Some datasets can store scalar values as JSON strings; accept both representations.
+                    clause.append("@.").append(key).append(" == ").append(escapeIfString(value, false)).
+                            append(" || @.").append(key).append(" == ").append(escapeIfString(value, true)).
+                            append(" || @.stringValue == ").append(escapeIfString(value, true));
                 }
-
                 clause.append(lower ? " flag \"i\"" : "").append(")')");
                 break;
 
             case GE:
-                clause.append("jsonb_path_exists(").append(schema.getKey()).append(", '$[*] ? ").
-                        append("(@.").append(key).append(" >= ").
+                clause.append("jsonb_path_exists(r.plainAttrs, '$[*] ? (@.schema == \"").
+                        append(schema.getKey()).append("\").").append(valuesPath).
+                        append(" ? (@.").append(key).append(" >= ").
                         append(escapeIfString(value, isStr)).append(")')");
                 break;
 
             case GT:
-                clause.append("jsonb_path_exists(").append(schema.getKey()).append(", '$[*] ? ").
-                        append("(@.").append(key).append(" > ").
+                clause.append("jsonb_path_exists(r.plainAttrs, '$[*] ? (@.schema == \"").
+                        append(schema.getKey()).append("\").").append(valuesPath).
+                        append(" ? (@.").append(key).append(" > ").
                         append(escapeIfString(value, isStr)).append(")')");
                 break;
 
             case LE:
-                clause.append("jsonb_path_exists(").append(schema.getKey()).append(", '$[*] ? ").
-                        append("(@.").append(key).append(" <= ").
+                clause.append("jsonb_path_exists(r.plainAttrs, '$[*] ? (@.schema == \"").
+                        append(schema.getKey()).append("\").").append(valuesPath).
+                        append(" ? (@.").append(key).append(" <= ").
                         append(escapeIfString(value, isStr)).append(")')");
                 break;
 
             case LT:
-                clause.append("jsonb_path_exists(").append(schema.getKey()).append(", '$[*] ? ").
-                        append("(@.").append(key).append(" < ").
+                clause.append("jsonb_path_exists(r.plainAttrs, '$[*] ? (@.schema == \"").
+                        append(schema.getKey()).append("\").").append(valuesPath).
+                        append(" ? (@.").append(key).append(" < ").
                         append(escapeIfString(value, isStr)).append(")')");
                 break;
         }
@@ -231,7 +188,7 @@ public class PGJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
             clause.insert(0, "NOT ");
         }
 
-        return new AnySearchNode.Leaf(from, clause.toString());
+        return new RealmSearchNode.Leaf(clause.toString());
     }
 
     @Override
@@ -239,10 +196,8 @@ public class PGJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
             final AttrCond cond,
             final boolean not,
             final CheckResult<AttrCond> checked,
-            final List<Object> parameters,
-            final SearchSupport svs) {
+            final List<Object> parameters) {
 
-        // normalize NULL / NOT NULL checks
         if (not) {
             if (cond.getType() == AttrCond.Type.ISNULL) {
                 cond.setType(AttrCond.Type.ISNOTNULL);
@@ -251,46 +206,40 @@ public class PGJPAAnySearchDAO extends AbstractJPAAnySearchDAO {
             }
         }
 
-        return switch (cond.getType()) {
-            case ISNOTNULL ->
-                new AttrCondQuery(true, new AnySearchNode.Leaf(
-                svs.table(),
-                "jsonb_path_exists(" + checked.schema().getKey() + ",'$[*]')"));
-
-            case ISNULL ->
-                new AttrCondQuery(true, new AnySearchNode.Leaf(
-                svs.table(),
-                "NOT jsonb_path_exists(" + checked.schema().getKey() + ",'$[*]')"));
-
-            default ->
-                new AttrCondQuery(true, filJSONAttrQuery(
-                svs.table(),
-                checked.value(),
-                checked.schema(),
-                cond,
-                not));
-        };
-    }
+        switch (cond.getType()) {
+            case ISNOTNULL -> {
+                return new AttrCondQuery(true, new RealmSearchNode.Leaf(
+                        "jsonb_path_exists(r.plainAttrs, '$[*] ? (@.schema == \""
+                        + checked.schema().getKey() + "\")')"));
+            }
 
-    @Override
-    protected void visitNode(
-            final AnySearchNode node,
-            final Map<SearchSupport.SearchView, Boolean> counters,
-            final Set<SearchSupport.SearchView> from,
-            final List<String> where,
-            final SearchSupport svs) {
-
-        counters.clear();
-        super.visitNode(node, counters, from, where, svs);
+            case ISNULL -> {
+                return new AttrCondQuery(true, new RealmSearchNode.Leaf(
+                        "NOT jsonb_path_exists(r.plainAttrs, '$[*] ? (@.schema == \""
+                        + checked.schema().getKey() + "\")')"));
+            }
+
+            default -> {
+                RealmSearchNode.Leaf node;
+                if (not && checked.schema().isMultivalue()) {
+                    // negate multivalue by wrapping the positive expression in NOT
+                    RealmSearchNode.Leaf notNode = filJSONAttrQuery(
+                            checked.value(), checked.schema(), cond, false);
+                    node = new RealmSearchNode.Leaf("NOT " + notNode.getClause());
+                } else {
+                    node = filJSONAttrQuery(checked.value(), checked.schema(), cond, not);
+                }
+                return new AttrCondQuery(true, node);
+            }
+        }
     }
 
     @Override
     protected String buildFrom(
-            final Set<SearchSupport.SearchView> from,
             final Set<String> plainSchemas,
             final OrderBySupport obs) {
 
-        StringBuilder clause = new StringBuilder(super.buildFrom(from, plainSchemas, obs));
+        StringBuilder clause = new StringBuilder(super.buildFrom(plainSchemas, obs));
 
         Set<String> schemas = new HashSet<>(plainSchemas);
 
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/RealmSearchNode.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/RealmSearchNode.java
new file mode 100644
index 0000000000..b9b5c08b6b
--- /dev/null
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/RealmSearchNode.java
@@ -0,0 +1,141 @@
+/*
+ * 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.syncope.core.persistence.jpa.dao;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+
+public class RealmSearchNode {
+
+    public enum Type {
+        AND,
+        OR,
+        LEAF
+    }
+
+    public static class Leaf extends RealmSearchNode {
+
+        private final String clause;
+
+        protected Leaf(final String clause) {
+            super(Type.LEAF);
+            this.clause = clause;
+        }
+
+        public String getClause() {
+            return clause;
+        }
+
+        @Override
+        public int hashCode() {
+            return new HashCodeBuilder().
+                    appendSuper(super.hashCode()).
+                    append(clause).
+                    build();
+        }
+
+        @Override
+        public boolean equals(final Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null) {
+                return false;
+            }
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+            final Leaf other = (Leaf) obj;
+
+            return new EqualsBuilder().
+                    appendSuper(super.equals(obj)).
+                    append(clause, other.clause).
+                    build();
+        }
+
+        @Override
+        public String toString() {
+            return "LeafNode{clause=" + clause + '}';
+        }
+    }
+
+    private final Type type;
+
+    private final List<RealmSearchNode> children = new ArrayList<>();
+
+    public RealmSearchNode(final Type type) {
+        this.type = type;
+    }
+
+    protected Type getType() {
+        return type;
+    }
+
+    protected boolean add(final RealmSearchNode child) {
+        if (type == Type.LEAF) {
+            throw new IllegalArgumentException("Cannot add children to a leaf node");
+        }
+        return children.add(child);
+    }
+
+    protected List<RealmSearchNode> getChildren() {
+        return children;
+    }
+
+    protected Optional<Leaf> asLeaf() {
+        return type == Type.LEAF
+                ? Optional.of((Leaf) this)
+                : Optional.empty();
+    }
+
+    @Override
+    public int hashCode() {
+        return new HashCodeBuilder().
+                append(type).
+                append(children).
+                build();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final RealmSearchNode other = (RealmSearchNode) obj;
+
+        return new EqualsBuilder().
+                append(type, other.type).
+                append(children, other.children).
+                build();
+    }
+
+    @Override
+    public String toString() {
+        return "Node{" + "type=" + type + ", children=" + children + '}';
+    }
+}
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AbstractAnyRepoExt.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AbstractAnyRepoExt.java
index e08a2f1578..a7eb723478 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AbstractAnyRepoExt.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AbstractAnyRepoExt.java
@@ -22,7 +22,6 @@ import jakarta.persistence.EntityManager;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.time.OffsetDateTime;
-import java.util.List;
 import java.util.Optional;
 import org.apache.syncope.core.persistence.api.dao.AnyChecker;
 import org.apache.syncope.core.persistence.api.dao.NotFoundException;
@@ -30,7 +29,6 @@ import org.apache.syncope.core.persistence.api.entity.Any;
 import org.apache.syncope.core.persistence.api.entity.AnyUtils;
 import org.apache.syncope.core.persistence.api.entity.Relationship;
 import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
-import org.apache.syncope.core.persistence.common.dao.AnyFinder;
 import org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAAnyObject;
 import org.apache.syncope.core.persistence.jpa.entity.group.JPAGroup;
 import org.apache.syncope.core.persistence.jpa.entity.user.JPAUser;
@@ -48,8 +46,6 @@ public abstract class AbstractAnyRepoExt<A extends Any> implements AnyRepoExt<A>
 
     protected final AnyChecker anyChecker;
 
-    protected final AnyFinder anyFinder;
-
     protected final AnyUtils anyUtils;
 
     protected final String table;
@@ -57,12 +53,10 @@ public abstract class AbstractAnyRepoExt<A extends Any> implements AnyRepoExt<A>
     protected AbstractAnyRepoExt(
             final EntityManager entityManager,
             final AnyChecker anyChecker,
-            final AnyFinder anyFinder,
             final AnyUtils anyUtils) {
 
         this.entityManager = entityManager;
         this.anyChecker = anyChecker;
-        this.anyFinder = anyFinder;
         this.anyUtils = anyUtils;
         switch (anyUtils.anyTypeKind()) {
             case ANY_OBJECT:
@@ -133,11 +127,6 @@ public abstract class AbstractAnyRepoExt<A extends Any> implements AnyRepoExt<A>
         return any;
     }
 
-    @Override
-    public List<A> findByDerAttrValue(final String expression, final String value, final boolean ignoreCaseMatch) {
-        return anyFinder.findByDerAttrValue(anyUtils.anyTypeKind(), expression, value, ignoreCaseMatch);
-    }
-
     @Override
     public void deleteRelationship(final Relationship<? extends A, AnyObject> relationship) {
         entityManager.remove(relationship);
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AnyObjectRepoExtImpl.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AnyObjectRepoExtImpl.java
index 39f771f323..4d80469dd7 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AnyObjectRepoExtImpl.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AnyObjectRepoExtImpl.java
@@ -44,7 +44,6 @@ import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
 import org.apache.syncope.core.persistence.api.entity.group.Group;
 import org.apache.syncope.core.persistence.api.entity.user.URelationship;
 import org.apache.syncope.core.persistence.api.utils.RealmUtils;
-import org.apache.syncope.core.persistence.common.dao.AnyFinder;
 import org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAARelationship;
 import org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAAnyObject;
 import org.apache.syncope.core.persistence.jpa.entity.user.JPAURelationship;
@@ -63,13 +62,11 @@ public class AnyObjectRepoExtImpl extends AbstractAnyRepoExt<AnyObject> implemen
             final UserDAO userDAO,
             final GroupDAO groupDAO,
             final EntityManager entityManager,
-            final AnyChecker anyChecker,
-            final AnyFinder anyFinder) {
+            final AnyChecker anyChecker) {
 
         super(
                 entityManager,
                 anyChecker,
-                anyFinder,
                 anyUtilsFactory.getInstance(AnyTypeKind.ANY_OBJECT));
         this.userDAO = userDAO;
         this.groupDAO = groupDAO;
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AnyRepoExt.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AnyRepoExt.java
index 9ae2f80b5e..cb30b760f5 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AnyRepoExt.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AnyRepoExt.java
@@ -20,7 +20,6 @@ package org.apache.syncope.core.persistence.jpa.dao.repo;
 
 import java.time.OffsetDateTime;
 import java.util.Collection;
-import java.util.List;
 import java.util.Optional;
 import org.apache.syncope.core.persistence.api.entity.Any;
 import org.apache.syncope.core.persistence.api.entity.Relationship;
@@ -32,8 +31,6 @@ public interface AnyRepoExt<A extends Any> {
 
     A authFind(String key);
 
-    List<A> findByDerAttrValue(String expression, String value, boolean ignoreCaseMatch);
-
     Collection<String> findAllResourceKeys(String key);
 
     void deleteRelationship(Relationship<? extends A, AnyObject> relationship);
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/GroupRepoExtImpl.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/GroupRepoExtImpl.java
index 8d074434bb..146a2ed96c 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/GroupRepoExtImpl.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/GroupRepoExtImpl.java
@@ -43,7 +43,6 @@ import org.apache.syncope.core.persistence.api.entity.group.GroupTypeExtension;
 import org.apache.syncope.core.persistence.api.entity.user.UMembership;
 import org.apache.syncope.core.persistence.api.entity.user.User;
 import org.apache.syncope.core.persistence.api.utils.RealmUtils;
-import org.apache.syncope.core.persistence.common.dao.AnyFinder;
 import org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAAMembership;
 import org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAAnyObject;
 import org.apache.syncope.core.persistence.jpa.entity.group.JPAGroup;
@@ -75,13 +74,11 @@ public class GroupRepoExtImpl extends AbstractAnyRepoExt<Group> implements Group
             final UserDAO userDAO,
             final AnyObjectDAO anyObjectDAO,
             final EntityManager entityManager,
-            final AnyChecker anyChecker,
-            final AnyFinder anyFinder) {
+            final AnyChecker anyChecker) {
 
         super(
                 entityManager,
                 anyChecker,
-                anyFinder,
                 anyUtilsFactory.getInstance(AnyTypeKind.GROUP));
         this.publisher = publisher;
         this.realmDAO = realmDAO;
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/UserRepoExtImpl.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/UserRepoExtImpl.java
index 51e350f750..cfdfe0d0cf 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/UserRepoExtImpl.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/UserRepoExtImpl.java
@@ -47,7 +47,6 @@ import org.apache.syncope.core.persistence.api.entity.group.Group;
 import org.apache.syncope.core.persistence.api.entity.user.UMembership;
 import org.apache.syncope.core.persistence.api.entity.user.User;
 import org.apache.syncope.core.persistence.api.utils.RealmUtils;
-import org.apache.syncope.core.persistence.common.dao.AnyFinder;
 import org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAAnyObject;
 import org.apache.syncope.core.persistence.jpa.entity.group.JPAGroup;
 import org.apache.syncope.core.persistence.jpa.entity.user.JPALinkedAccount;
@@ -81,13 +80,11 @@ public class UserRepoExtImpl extends AbstractAnyRepoExt<User> implements UserRep
             final FIQLQueryDAO fiqlQueryDAO,
             final SecurityProperties securityProperties,
             final EntityManager entityManager,
-            final AnyChecker anyChecker,
-            final AnyFinder anyFinder) {
+            final AnyChecker anyChecker) {
 
         super(
                 entityManager,
                 anyChecker,
-                anyFinder,
                 anyUtilsFactory.getInstance(AnyTypeKind.USER));
         this.roleDAO = roleDAO;
         this.accessTokenDAO = accessTokenDAO;
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPASRARoute.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPASRARoute.java
index 00ee143dea..2a92e79213 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPASRARoute.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/JPASRARoute.java
@@ -157,7 +157,7 @@ public class JPASRARoute extends AbstractGeneratedKeyEntity implements SRARoute
     public List<SRARouteFilter> getFilters() {
         return Optional.ofNullable(filters).
                 map(f -> List.of(POJOHelper.deserialize(f, SRARouteFilter[].class))).
-            orElseGet(List::of);
+                orElseGet(List::of);
     }
 
     @Override
@@ -169,7 +169,7 @@ public class JPASRARoute extends AbstractGeneratedKeyEntity implements SRARoute
     public List<SRARoutePredicate> getPredicates() {
         return Optional.ofNullable(predicates).
                 map(f -> List.of(POJOHelper.deserialize(f, SRARoutePredicate[].class))).
-            orElseGet(List::of);
+                orElseGet(List::of);
     }
 
     @Override
diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java
index 03c1ce7f3a..39cf78fb16 100644
--- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java
+++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java
@@ -39,6 +39,7 @@ import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationMana
 import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
 import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
+import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmDAO;
@@ -107,6 +108,9 @@ public class AnySearchTest extends AbstractTest {
     @Autowired
     private PlainSchemaDAO plainSchemaDAO;
 
+    @Autowired
+    private DerSchemaDAO derSchemaDAO;
+
     @Autowired
     private PlainAttrValidationManager validator;
 
@@ -730,13 +734,13 @@ public class AnySearchTest extends AbstractTest {
             assertEquals(
                     1,
                     searchDAO.count(
-                            realmDAO.getRoot(), true, authRealms, groupDAO.getAllMatchingCond(), AnyTypeKind.GROUP));
+                            realmDAO.getRoot(), true, authRealms, searchDAO.getAllMatchingCond(), AnyTypeKind.GROUP));
 
             List<Group> groups = searchDAO.search(
                     realmDAO.getRoot(),
                     true,
                     authRealms,
-                    groupDAO.getAllMatchingCond(),
+                    searchDAO.getAllMatchingCond(),
                     PageRequest.of(0, 10),
                     AnyTypeKind.GROUP);
             assertEquals(1, groups.size());
@@ -763,6 +767,35 @@ public class AnySearchTest extends AbstractTest {
         assertEquals(5, users.size());
     }
 
+    @Test
+    public void findByDerAttrValue() {
+        List<User> list = searchDAO.findByDerAttrValue(
+                derSchemaDAO.findById("cn").orElseThrow().getExpression(), "Vivaldi, Antonio", false, AnyTypeKind.USER);
+        assertEquals(1, list.size());
+
+        list = searchDAO.findByDerAttrValue(
+                derSchemaDAO.findById("cn").orElseThrow().getExpression(), "VIVALDI, ANTONIO", false, AnyTypeKind.USER);
+        assertEquals(0, list.size());
+
+        list = searchDAO.findByDerAttrValue(
+                derSchemaDAO.findById("cn").orElseThrow().getExpression(), "VIVALDI, ANTONIO", true, AnyTypeKind.USER);
+        assertEquals(1, list.size());
+    }
+
+    @Test
+    public void findByInvalidDerAttrValue() {
+        assertTrue(searchDAO.findByDerAttrValue(
+                derSchemaDAO.findById("cn").orElseThrow().getExpression(),
+                "Antonio, Maria, Rossi", false, AnyTypeKind.USER).isEmpty());
+    }
+
+    @Test
+    public void findByInvalidDerAttrExpression() {
+        assertThrows(IllegalArgumentException.class, () -> searchDAO.findByDerAttrValue(
+                derSchemaDAO.findById("noschema").orElseThrow().getExpression(),
+                "Antonio, Maria", false, AnyTypeKind.USER).isEmpty());
+    }
+
     @Test
     public void issue202() {
         ResourceCond ws2 = new ResourceCond();
diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/MultitenancyTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/MultitenancyTest.java
index d1531ffd8b..ab83a95e8c 100644
--- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/MultitenancyTest.java
+++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/MultitenancyTest.java
@@ -35,7 +35,6 @@ import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.data.domain.Pageable;
 import org.springframework.test.annotation.DirtiesContext;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -74,10 +73,10 @@ public class MultitenancyTest extends AbstractTest {
     public void readRealm() {
         assertEquals(
                 1,
-                realmSearchDAO.findDescendants(realmDAO.getRoot().getFullPath(), null, Pageable.unpaged()).size());
+                realmSearchDAO.findDescendants(realmDAO.getRoot().getFullPath(), null).size());
         assertEquals(
                 realmDAO.getRoot(),
-                realmSearchDAO.findDescendants(realmDAO.getRoot().getFullPath(), null, Pageable.unpaged()).getFirst());
+                realmSearchDAO.findDescendants(realmDAO.getRoot().getFullPath(), null).getFirst());
     }
 
     @Test
diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/RealmTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/RealmTest.java
index 5dddbdb6cb..55453d5d9a 100644
--- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/RealmTest.java
+++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/RealmTest.java
@@ -98,14 +98,14 @@ public class RealmTest extends AbstractTest {
 
     @Test
     public void findAll() {
-        List<Realm> list = realmSearchDAO.findDescendants(realmDAO.getRoot().getFullPath(), null, Pageable.unpaged());
+        List<Realm> list = realmSearchDAO.findDescendants(realmDAO.getRoot().getFullPath(), null);
         assertNotNull(list);
         assertFalse(list.isEmpty());
         list.forEach(Assertions::assertNotNull);
 
         assertEquals(4, realmDAO.findAll(Pageable.unpaged()).stream().count());
 
-        list = realmSearchDAO.findDescendants(Set.of("/even", "/odd"), null, Pageable.unpaged());
+        list = realmSearchDAO.search(Set.of("/even", "/odd"), realmSearchDAO.getAllMatchingCond(), Pageable.unpaged());
         assertEquals(3, list.size());
         assertNotNull(list.stream().filter(realm -> "even".equals(realm.getName())).findFirst().orElseThrow());
         assertNotNull(list.stream().filter(realm -> "two".equals(realm.getName())).findFirst().orElseThrow());
diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/UserTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/UserTest.java
index 37477b2100..29dc05d80d 100644
--- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/UserTest.java
+++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/UserTest.java
@@ -30,7 +30,6 @@ import java.time.OffsetDateTime;
 import java.util.List;
 import org.apache.syncope.common.lib.types.CipherAlgorithm;
 import org.apache.syncope.core.persistence.api.EncryptorManager;
-import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO;
 import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO;
@@ -61,9 +60,6 @@ public class UserTest extends AbstractTest {
     @Autowired
     private ExternalResourceDAO resourceDAO;
 
-    @Autowired
-    private DerSchemaDAO derSchemaDAO;
-
     @Autowired
     private SecurityQuestionDAO securityQuestionDAO;
 
@@ -109,33 +105,6 @@ public class UserTest extends AbstractTest {
         assertEquals(5, count);
     }
 
-    @Test
-    public void findByDerAttrValue() {
-        List<User> list = userDAO.findByDerAttrValue(
-                derSchemaDAO.findById("cn").orElseThrow().getExpression(), "Vivaldi, Antonio", false);
-        assertEquals(1, list.size());
-
-        list = userDAO.findByDerAttrValue(
-                derSchemaDAO.findById("cn").orElseThrow().getExpression(), "VIVALDI, ANTONIO", false);
-        assertEquals(0, list.size());
-
-        list = userDAO.findByDerAttrValue(
-                derSchemaDAO.findById("cn").orElseThrow().getExpression(), "VIVALDI, ANTONIO", true);
-        assertEquals(1, list.size());
-    }
-
-    @Test
-    public void findByInvalidDerAttrValue() {
-        assertTrue(userDAO.findByDerAttrValue(
-                derSchemaDAO.findById("cn").orElseThrow().getExpression(), "Antonio, Maria, Rossi", false).isEmpty());
-    }
-
-    @Test
-    public void findByInvalidDerAttrExpression() {
-        assertThrows(IllegalArgumentException.class, () -> userDAO.findByDerAttrValue(
-                derSchemaDAO.findById("noschema").orElseThrow().getExpression(), "Antonio, Maria", false).isEmpty());
-    }
-
     @Test
     public void findByKey() {
         assertTrue(userDAO.findById("1417acbe-cbf6-4277-9372-e75e04f97000").isPresent());
diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/AnySearchTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/AnySearchTest.java
index cedcdaeff4..c87034b9da 100644
--- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/AnySearchTest.java
+++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/AnySearchTest.java
@@ -33,12 +33,14 @@ import org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
 import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
+import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
 import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
 import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
+import org.apache.syncope.core.persistence.api.entity.DerSchema;
 import org.apache.syncope.core.persistence.api.entity.PlainAttr;
 import org.apache.syncope.core.persistence.api.entity.anyobject.AMembership;
 import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
@@ -71,6 +73,9 @@ public class AnySearchTest extends AbstractTest {
     @Autowired
     private RealmDAO realmDAO;
 
+    @Autowired
+    private DerSchemaDAO derSchemaDAO;
+
     @Autowired
     private PlainAttrValidationManager validator;
 
@@ -163,6 +168,43 @@ public class AnySearchTest extends AbstractTest {
         assertEquals("c9b2dec2-00a7-4855-97c0-d854842b4b24", users.getFirst().getKey());
     }
 
+    @Test
+    public void issueSYNCOPE800() {
+        // create derived attribute (literal as prefix)
+        DerSchema prefix = entityFactory.newEntity(DerSchema.class);
+        prefix.setKey("kprefix");
+        prefix.setExpression("'k' + firstname");
+
+        derSchemaDAO.save(prefix);
+        entityManager.flush();
+
+        // create derived attribute (literal as suffix)
+        DerSchema suffix = entityFactory.newEntity(DerSchema.class);
+        suffix.setKey("ksuffix");
+        suffix.setExpression("firstname + 'k'");
+
+        derSchemaDAO.save(suffix);
+        entityManager.flush();
+
+        // add derived attributes to user
+        User owner = userDAO.findByUsername("vivaldi").orElseThrow();
+
+        String firstname = owner.getPlainAttr("firstname").get().getValuesAsStrings().getFirst();
+        assertNotNull(firstname);
+
+        // search by ksuffix derived attribute
+        List<User> list = searchDAO.findByDerAttrValue(
+                derSchemaDAO.findById("ksuffix").orElseThrow().getExpression(),
+                firstname + 'k', false, AnyTypeKind.USER);
+        assertEquals(1, list.size());
+
+        // search by kprefix derived attribute
+        list = searchDAO.findByDerAttrValue(
+                derSchemaDAO.findById("kprefix").orElseThrow().getExpression(),
+                'k' + firstname, false, AnyTypeKind.USER);
+        assertEquals(1, list.size());
+    }
+
     @Test
     public void issueSYNCOPE1417() {
         AnyCond usernameLeafCond = new AnyCond(AnyCond.Type.EQ);
diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/RealmTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/RealmTest.java
index ad9bb45f72..273846cc13 100644
--- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/RealmTest.java
+++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/RealmTest.java
@@ -18,15 +18,23 @@
  */
 package org.apache.syncope.core.persistence.jpa.outer;
 
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.util.List;
+import java.util.Set;
+import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
 import org.apache.syncope.core.persistence.api.dao.AnyTypeClassDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO;
 import org.apache.syncope.core.persistence.api.dao.RoleDAO;
+import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
+import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
+import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
 import org.apache.syncope.core.persistence.api.entity.PlainAttr;
 import org.apache.syncope.core.persistence.api.entity.Realm;
 import org.apache.syncope.core.persistence.api.entity.Role;
@@ -34,6 +42,8 @@ import org.apache.syncope.core.persistence.api.entity.group.Group;
 import org.apache.syncope.core.persistence.jpa.AbstractTest;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
 import org.springframework.transaction.annotation.Transactional;
 
 @Transactional
@@ -105,4 +115,124 @@ public class RealmTest extends AbstractTest {
         role = roleDAO.findById("User reviewer").orElseThrow();
         assertEquals(beforeSize - 1, role.getRealms().size());
     }
+
+    @Test
+    public void search() {
+        Realm two = realmSearchDAO.findByFullPath("/even/two").orElseThrow();
+        two.add(anyTypeClassDAO.findById("other").orElseThrow());
+        two = realmDAO.save(two);
+        entityManager.flush();
+
+        two = realmDAO.findById(two.getKey()).orElseThrow();
+        PlainAttr aLong = new PlainAttr();
+        aLong.setSchema("aLong");
+        aLong.add(validator, "42");
+        two.add(aLong);
+        two = realmDAO.save(two);
+        entityManager.flush();
+
+        Realm odd = realmSearchDAO.findByFullPath("/odd").orElseThrow();
+        odd.add(anyTypeClassDAO.findById("other").orElseThrow());
+        odd = realmDAO.save(odd);
+        entityManager.flush();
+
+        odd = realmDAO.findById(odd.getKey()).orElseThrow();
+        PlainAttr oddLong = new PlainAttr();
+        oddLong.setSchema("aLong");
+        oddLong.add(validator, "99");
+        odd.add(oddLong);
+        realmDAO.save(odd);
+        entityManager.flush();
+
+        two = realmDAO.findById(two.getKey()).orElseThrow();
+        assertEquals(anyTypeClassDAO.findById("other").orElseThrow(), two.getAnyTypeClasses().iterator().next());
+        assertEquals(1, two.getPlainAttrs().size());
+        assertEquals(42, two.getPlainAttr("aLong").orElseThrow().getValues().getFirst().getLongValue());
+
+        odd = realmDAO.findById(odd.getKey()).orElseThrow();
+        assertEquals(anyTypeClassDAO.findById("other").orElseThrow(), odd.getAnyTypeClasses().iterator().next());
+        assertEquals(1, odd.getPlainAttrs().size());
+        assertEquals(99, odd.getPlainAttr("aLong").orElseThrow().getValues().getFirst().getLongValue());
+
+        AnyCond name = new AnyCond(AttrCond.Type.EQ);
+        name.setSchema("name");
+        name.setExpression("two");
+
+        List<Realm> result = realmSearchDAO.search(
+                Set.of(SyncopeConstants.ROOT_REALM),
+                SearchCond.of(name),
+                Pageable.unpaged());
+        assertEquals(1, result.size());
+        assertEquals("two", result.getFirst().getName());
+
+        AttrCond attrEq = new AttrCond(AttrCond.Type.EQ);
+        attrEq.setSchema("aLong");
+        attrEq.setExpression("42");
+
+        result = realmSearchDAO.search(
+                Set.of(SyncopeConstants.ROOT_REALM),
+                SearchCond.of(attrEq),
+                Pageable.unpaged());
+        assertEquals(1, result.size());
+        assertEquals("two", result.getFirst().getName());
+
+        result = realmSearchDAO.search(
+                Set.of(SyncopeConstants.ROOT_REALM),
+                SearchCond.and(SearchCond.of(name), SearchCond.of(attrEq)),
+                Pageable.unpaged());
+        assertEquals(1, result.size());
+        assertEquals("two", result.getFirst().getName());
+
+        AttrCond attrEq2 = new AttrCond(AttrCond.Type.EQ);
+        attrEq2.setSchema("aLong");
+        attrEq2.setExpression("99");
+
+        result = realmSearchDAO.search(
+                Set.of(SyncopeConstants.ROOT_REALM),
+                SearchCond.or(SearchCond.of(attrEq), SearchCond.of(attrEq2)),
+                Pageable.unpaged());
+        assertTrue(result.stream().anyMatch(r -> "two".equals(r.getName())));
+        assertTrue(result.stream().anyMatch(r -> "odd".equals(r.getName())));
+
+        AttrCond attrIsNull = new AttrCond(AttrCond.Type.ISNULL);
+        attrIsNull.setSchema("aLong");
+
+        result = realmSearchDAO.search(
+                Set.of(SyncopeConstants.ROOT_REALM),
+                SearchCond.of(attrIsNull),
+                Pageable.unpaged());
+        result.forEach(r -> assertFalse("two".equals(r.getName()) || "odd".equals(r.getName())));
+
+        AttrCond attrIsNotNull = new AttrCond(AttrCond.Type.ISNOTNULL);
+        attrIsNotNull.setSchema("aLong");
+
+        assertEquals(2, realmSearchDAO.count(
+                Set.of(SyncopeConstants.ROOT_REALM),
+                SearchCond.of(attrIsNotNull)));
+
+        result = realmSearchDAO.search(
+                Set.of(SyncopeConstants.ROOT_REALM),
+                SearchCond.of(attrIsNotNull),
+                Pageable.unpaged(Sort.by(new Sort.Order(Sort.Direction.ASC, "name"))));
+        assertEquals("odd", result.get(0).getName());
+        assertEquals("two", result.get(1).getName());
+
+        result = realmSearchDAO.search(
+                Set.of(SyncopeConstants.ROOT_REALM),
+                SearchCond.of(attrIsNotNull),
+                Pageable.unpaged(Sort.by(new Sort.Order(Sort.Direction.ASC, "aLong"))));
+        assertEquals("two", result.get(0).getName());
+        assertEquals("odd", result.get(1).getName());
+
+        AttrCond cond1 = new AttrCond(AttrCond.Type.EQ);
+        cond1.setSchema("aLong");
+        cond1.setExpression("42");
+        AttrCond cond2 = new AttrCond(AttrCond.Type.IEQ);
+        cond2.setSchema("ctype");
+        cond2.setExpression("string");
+        assertDoesNotThrow(() -> realmSearchDAO.search(
+                Set.of(SyncopeConstants.ROOT_REALM),
+                SearchCond.and(SearchCond.of(cond1), SearchCond.of(cond2)),
+                Pageable.unpaged()));
+    }
 }
diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/UserTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/UserTest.java
index 7f1a110492..a9764f8780 100644
--- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/UserTest.java
+++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/UserTest.java
@@ -30,10 +30,12 @@ import java.time.OffsetDateTime;
 import java.util.List;
 import java.util.Objects;
 import java.util.UUID;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.CipherAlgorithm;
 import org.apache.syncope.core.persistence.api.attrvalue.InvalidEntityException;
 import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
 import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
+import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
 import org.apache.syncope.core.persistence.api.dao.AnyTypeClassDAO;
 import org.apache.syncope.core.persistence.api.dao.DelegationDAO;
 import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO;
@@ -97,6 +99,9 @@ public class UserTest extends AbstractTest {
     @Autowired
     private AnyTypeClassDAO anyTypeClassDAO;
 
+    @Autowired
+    private AnySearchDAO anySearchDAO;
+
     @Autowired
     private PlainAttrValidationManager validator;
 
@@ -306,13 +311,15 @@ public class UserTest extends AbstractTest {
         assertNotNull(firstname);
 
         // search by ksuffix derived attribute
-        List<User> list = userDAO.findByDerAttrValue(
-                derSchemaDAO.findById("ksuffix").orElseThrow().getExpression(), firstname + 'k', false);
+        List<User> list = anySearchDAO.findByDerAttrValue(
+                derSchemaDAO.findById("ksuffix").orElseThrow().getExpression(),
+                firstname + 'k', false, AnyTypeKind.USER);
         assertEquals(1, list.size());
 
         // search by kprefix derived attribute
-        list = userDAO.findByDerAttrValue(
-                derSchemaDAO.findById("kprefix").orElseThrow().getExpression(), 'k' + firstname, false);
+        list = anySearchDAO.findByDerAttrValue(
+                derSchemaDAO.findById("kprefix").orElseThrow().getExpression(),
+                'k' + firstname, false, AnyTypeKind.USER);
         assertEquals(1, list.size());
     }
 }
diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/PersistenceContext.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/PersistenceContext.java
index 72b23a7634..c1e50e6cef 100644
--- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/PersistenceContext.java
+++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/PersistenceContext.java
@@ -86,10 +86,9 @@ import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
 import org.apache.syncope.core.persistence.api.entity.EntityFactory;
 import org.apache.syncope.core.persistence.api.entity.PlainAttr;
 import org.apache.syncope.core.persistence.api.entity.task.TaskUtilsFactory;
-import org.apache.syncope.core.persistence.api.search.SearchCondVisitor;
+import org.apache.syncope.core.persistence.api.utils.RealmUtils;
 import org.apache.syncope.core.persistence.common.CommonPersistenceContext;
 import org.apache.syncope.core.persistence.common.RuntimeDomainLoader;
-import org.apache.syncope.core.persistence.common.dao.AnyFinder;
 import org.apache.syncope.core.persistence.neo4j.content.XMLContentExporter;
 import org.apache.syncope.core.persistence.neo4j.content.XMLContentLoader;
 import org.apache.syncope.core.persistence.neo4j.dao.Neo4jAnyChecker;
@@ -502,7 +501,6 @@ public class PersistenceContext {
             final @Lazy DerSchemaDAO derSchemaDAO,
             final @Lazy UserDAO userDAO,
             final @Lazy GroupDAO groupDAO,
-            final @Lazy AnyFinder anyFinder,
             final AnyChecker anyChecker,
             final Neo4jTemplate neo4jTemplate,
             final Neo4jClient neo4jClient,
@@ -517,7 +515,6 @@ public class PersistenceContext {
                 userDAO,
                 groupDAO,
                 anyChecker,
-                anyFinder,
                 neo4jTemplate,
                 neo4jClient,
                 nodeValidator,
@@ -935,7 +932,6 @@ public class PersistenceContext {
             final @Lazy UserDAO userDAO,
             final @Lazy AnyObjectDAO anyObjectDAO,
             final AnyChecker anyChecker,
-            final @Lazy AnyFinder anyFinder,
             final Neo4jTemplate neo4jTemplate,
             final Neo4jClient neo4jClient,
             final NodeValidator nodeValidator,
@@ -953,7 +949,6 @@ public class PersistenceContext {
                 userDAO,
                 anyObjectDAO,
                 anyChecker,
-                anyFinder,
                 neo4jTemplate,
                 neo4jClient,
                 nodeValidator,
@@ -1185,11 +1180,26 @@ public class PersistenceContext {
     @ConditionalOnMissingBean
     @Bean
     public RealmSearchDAO realmSearchDAO(
+            final @Lazy RealmDAO realmDAO,
+            final PlainSchemaDAO plainSchemaDAO,
+            final UserDAO userDAO,
+            final GroupDAO groupDAO,
+            final EntityFactory entityFactory,
+            final PlainAttrValidationManager validator,
+            final RealmUtils realmUtils,
             final Neo4jTemplate neo4jTemplate,
-            final Neo4jClient neo4jClient,
-            final Cache<EntityCacheKey, Neo4jRealm> realmCache) {
+            final Neo4jClient neo4jClient) {
 
-        return new Neo4jRealmSearchDAO(neo4jTemplate, neo4jClient, realmCache);
+        return new Neo4jRealmSearchDAO(
+                realmDAO,
+                plainSchemaDAO,
+                userDAO,
+                groupDAO,
+                entityFactory,
+                validator,
+                realmUtils,
+                neo4jTemplate,
+                neo4jClient);
     }
 
     @ConditionalOnMissingBean
@@ -1307,7 +1317,6 @@ public class PersistenceContext {
             final @Lazy AnyMatchDAO anyMatchDAO,
             final @Lazy AnySearchDAO anySearchDAO,
             final DelegationDAO delegationDAO,
-            final SearchCondVisitor searchCondVisitor,
             final Neo4jTemplate neo4jTemplate,
             final Neo4jClient neo4jClient,
             final NodeValidator nodeValidator,
@@ -1318,7 +1327,6 @@ public class PersistenceContext {
                 anyMatchDAO,
                 anySearchDAO,
                 delegationDAO,
-                searchCondVisitor,
                 neo4jTemplate,
                 neo4jClient,
                 nodeValidator,
@@ -1441,7 +1449,6 @@ public class PersistenceContext {
             final DelegationDAO delegationDAO,
             final FIQLQueryDAO fiqlQueryDAO,
             final AnyChecker anyChecker,
-            final @Lazy AnyFinder anyFinder,
             final Neo4jTemplate neo4jTemplate,
             final Neo4jClient neo4jClient,
             final NodeValidator nodeValidator,
@@ -1460,7 +1467,6 @@ public class PersistenceContext {
                 delegationDAO,
                 fiqlQueryDAO,
                 anyChecker,
-                anyFinder,
                 securityProperties,
                 neo4jTemplate,
                 neo4jClient,
diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAnySearchDAO.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAnySearchDAO.java
index 9631c5252f..39c976238c 100644
--- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAnySearchDAO.java
+++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAnySearchDAO.java
@@ -28,7 +28,6 @@ import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
-import org.apache.commons.lang3.ArrayUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.Strings;
 import org.apache.commons.lang3.tuple.Pair;
@@ -133,6 +132,20 @@ public class Neo4jAnySearchDAO extends AbstractAnySearchDAO {
                 : value;
     }
 
+    protected static void queryOp(
+            final TextStringBuilder query,
+            final String op,
+            final QueryInfo leftInfo,
+            final QueryInfo rightInfo) {
+
+        query.append("WHERE EXISTS { ").
+                append(Strings.CS.prependIfMissing(leftInfo.query().toString(), "MATCH (n) ")).
+                append(" } ").
+                append(op).append(" EXISTS { ").
+                append(Strings.CS.prependIfMissing(rightInfo.query().toString(), "MATCH (n) ")).
+                append(" }");
+    }
+
     protected final Neo4jTemplate neo4jTemplate;
 
     protected final Neo4jClient neo4jClient;
@@ -198,7 +211,8 @@ public class Neo4jAnySearchDAO extends AbstractAnySearchDAO {
                             return noRealm;
                         });
 
-                        realmKeys.addAll(realmSearchDAO.findDescendants(realm.getFullPath(), base.getFullPath()));
+                        realmKeys.addAll(realmSearchDAO.findDescendants(realm.getFullPath(), base.getFullPath()).
+                                stream().map(Realm::getKey).toList());
                     }));
         } else {
             if (adminRealms.stream().anyMatch(r -> r.startsWith(base.getFullPath()))) {
@@ -605,12 +619,13 @@ public class Neo4jAnySearchDAO extends AbstractAnySearchDAO {
                     null);
         }
 
-        CheckResult<AnyCond> checked = check(cond, kind);
-
-        if (ArrayUtils.contains(
-                RELATIONSHIP_FIELDS,
-                StringUtils.substringBefore(checked.cond().getSchema(), "_id"))) {
+        CheckResult<AnyCond> checked = check(
+                cond,
+                anyUtilsFactory.getInstance(kind).getField(cond.getSchema()).
+                        orElseThrow(() -> new IllegalArgumentException("Invalid schema " + cond.getSchema())),
+                RELATIONSHIP_FIELDS);
 
+        if (RELATIONSHIP_FIELDS.contains(StringUtils.substringBefore(checked.cond().getSchema(), "_id"))) {
             String field = StringUtils.substringBefore(checked.cond().getSchema(), "_id");
             switch (field) {
                 case "uManager" -> {
@@ -673,20 +688,6 @@ public class Neo4jAnySearchDAO extends AbstractAnySearchDAO {
         // do nothing by default, leave it open for subclasses
     }
 
-    protected void queryOp(
-            final TextStringBuilder query,
-            final String op,
-            final QueryInfo leftInfo,
-            final QueryInfo rightInfo) {
-
-        query.append("WHERE EXISTS { ").
-                append(Strings.CS.prependIfMissing(leftInfo.query().toString(), "MATCH (n) ")).
-                append(" } ").
-                append(op).append(" EXISTS { ").
-                append(Strings.CS.prependIfMissing(rightInfo.query().toString(), "MATCH (n) ")).
-                append(" }");
-    }
-
     protected QueryInfo getQuery(final AnyTypeKind kind, final SearchCond cond, final Map<String, Object> parameters) {
         boolean not = cond.getType() == SearchCond.Type.NOT_LEAF;
 
@@ -925,6 +926,8 @@ public class Neo4jAnySearchDAO extends AbstractAnySearchDAO {
         // 4. prepare the count query
         query.append("RETURN COUNT(id)");
 
+        LOG.debug("Query: {}, parameters: {}", query, parameters);
+
         return neo4jTemplate.count(query.toString(), parameters);
     }
 
@@ -1001,7 +1004,7 @@ public class Neo4jAnySearchDAO extends AbstractAnySearchDAO {
                     append(" LIMIT ").append(pageable.getPageSize());
         }
 
-        LOG.debug("Query with auth and order by statements: {}, parameters: {}", query, parameters);
+        LOG.debug("Query: {}, parameters: {}", query, parameters);
 
         // 5. Prepare the result (avoiding duplicates)
         return buildResult(neo4jClient.query(query.toString()).bindAll(parameters).fetch().all().stream().
diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmDAO.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmDAO.java
index 8c9bfd9d10..2701d0850f 100644
--- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmDAO.java
+++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmDAO.java
@@ -266,7 +266,7 @@ public class Neo4jRealmDAO extends AbstractDAO implements RealmDAO {
             return;
         }
 
-        realmSearchDAO.findDescendants(realm.getFullPath(), null, Pageable.unpaged()).forEach(toBeDeleted -> {
+        realmSearchDAO.findDescendants(realm.getFullPath(), null).forEach(toBeDeleted -> {
             roleDAO.findByRealms(toBeDeleted).forEach(role -> role.getRealms().remove(toBeDeleted));
 
             cascadeDelete(
diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmSearchDAO.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmSearchDAO.java
index 753879b62d..6302c3970f 100644
--- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmSearchDAO.java
+++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmSearchDAO.java
@@ -18,62 +18,129 @@
  */
 package org.apache.syncope.core.persistence.neo4j.dao;
 
+import java.time.format.DateTimeFormatter;
+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.Set;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
-import javax.cache.Cache;
+import java.util.stream.Stream;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Strings;
+import org.apache.commons.text.TextStringBuilder;
+import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.types.AttrSchemaType;
+import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
+import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.MalformedPathException;
+import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmDAO;
-import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO;
+import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
+import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
+import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
+import org.apache.syncope.core.persistence.api.entity.PlainSchema;
 import org.apache.syncope.core.persistence.api.entity.Realm;
-import org.apache.syncope.core.persistence.neo4j.entity.EntityCacheKey;
+import org.apache.syncope.core.persistence.api.utils.RealmUtils;
+import org.apache.syncope.core.persistence.common.dao.AbstractRealmSearchDAO;
+import org.apache.syncope.core.persistence.neo4j.dao.repo.AnyRepoExt;
 import org.apache.syncope.core.persistence.neo4j.entity.Neo4jRealm;
 import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.domain.Sort.Order;
 import org.springframework.data.neo4j.core.Neo4jClient;
 import org.springframework.data.neo4j.core.Neo4jTemplate;
+import org.springframework.data.util.Streamable;
 import org.springframework.transaction.annotation.Transactional;
 
-public class Neo4jRealmSearchDAO extends AbstractDAO implements RealmSearchDAO {
+public class Neo4jRealmSearchDAO extends AbstractRealmSearchDAO {
 
-    protected static StringBuilder buildDescendantsQuery(
-            final Set<String> bases,
-            final String keyword,
-            final Map<String, Object> parameters) {
+    protected record AnyCondQuery(String query, String field) {
 
-        AtomicInteger index = new AtomicInteger(0);
-        String basesClause = bases.stream().map(base -> {
-            int idx = index.incrementAndGet();
-            parameters.put("base" + idx, base);
-            parameters.put("like" + idx, SyncopeConstants.ROOT_REALM.equals(base) ? "/.*" : base + "/.*");
-            return "n.fullPath = $base" + idx + " OR n.fullPath =~ $like" + idx;
-        }).collect(Collectors.joining(" OR "));
+    }
 
-        StringBuilder queryString = new StringBuilder("MATCH (n:").append(Neo4jRealm.NODE).append(") ").
-                append("WHERE (").append(basesClause).append(')');
+    protected record AttrCondQuery(String query, PlainSchema schema) {
 
-        if (keyword != null) {
-            queryString.append(" AND toLower(n.name) =~ $name");
-            parameters.put("name", keyword.replace("%", ".*").replaceAll("_", "\\\\_").toLowerCase() + ".*");
+    }
+
+    protected record QueryInfo(
+            TextStringBuilder query,
+            Set<String> fields,
+            Set<PlainSchema> plainSchemas) {
+
+    }
+
+    protected static String setParameter(final Map<String, Object> parameters, final Object parameter) {
+        String name = "param" + parameters.size();
+        parameters.put(name, parameter);
+        return name;
+    }
+
+    protected static void appendPlainAttrCond(
+            final TextStringBuilder query, final PlainSchema schema, final String cond) {
+
+        if (schema.isUniqueConstraint()) {
+            query.append(schema.getKey()).append('.').append(key(schema.getType())).append(cond);
+        } else {
+            query.append("any(k IN ").append(schema.getKey()).
+                    append(" WHERE k").append('.').append(key(schema.getType())).append(cond).
+                    append(")");
         }
+    }
 
-        return queryString;
+    protected static String escapeIfString(final String value, final boolean isStr) {
+        return isStr
+                ? new StringBuilder().append('"').append(value).append('"').toString()
+                : value;
     }
 
-    protected final Cache<EntityCacheKey, Neo4jRealm> cache;
+    protected static void queryOp(
+            final TextStringBuilder query,
+            final String op,
+            final QueryInfo leftInfo,
+            final QueryInfo rightInfo) {
+
+        query.append("WHERE EXISTS { ").
+                append(Strings.CS.prependIfMissing(leftInfo.query().toString(), "MATCH (n) ")).
+                append(" } ").
+                append(op).append(" EXISTS { ").
+                append(Strings.CS.prependIfMissing(rightInfo.query().toString(), "MATCH (n) ")).
+                append(" }");
+    }
+
+    protected final RealmDAO realmDAO;
+
+    protected final RealmUtils realmUtils;
+
+    protected final Neo4jTemplate neo4jTemplate;
+
+    protected final Neo4jClient neo4jClient;
 
     public Neo4jRealmSearchDAO(
+            final RealmDAO realmDAO,
+            final PlainSchemaDAO plainSchemaDAO,
+            final UserDAO userDAO,
+            final GroupDAO groupDAO,
+            final EntityFactory entityFactory,
+            final PlainAttrValidationManager validator,
+            final RealmUtils realmUtils,
             final Neo4jTemplate neo4jTemplate,
-            final Neo4jClient neo4jClient,
-            final Cache<EntityCacheKey, Neo4jRealm> cache) {
+            final Neo4jClient neo4jClient) {
 
-        super(neo4jTemplate, neo4jClient);
-        this.cache = cache;
+        super(plainSchemaDAO, userDAO, groupDAO, entityFactory, validator);
+        this.realmDAO = realmDAO;
+        this.realmUtils = realmUtils;
+        this.neo4jTemplate = neo4jTemplate;
+        this.neo4jClient = neo4jClient;
     }
 
     @Transactional(readOnly = true)
@@ -89,68 +156,518 @@ public class Neo4jRealmSearchDAO extends AbstractDAO implements RealmSearchDAO {
         return neo4jClient.query(
                 "MATCH (n:" + Neo4jRealm.NODE + ") WHERE n.fullPath = $fullPath RETURN n.id").
                 bindAll(Map.of("fullPath", fullPath)).fetch().one().
-                flatMap(toOptional("n.id", Neo4jRealm.class, cache));
+                flatMap(found -> realmDAO.findById(found.get("n.id").toString()).map(n -> (Realm) n));
+    }
+
+    protected List<Realm> toList(
+            final Collection<Map<String, Object>> result,
+            final String property) {
+
+        return result.stream().
+                map(found -> realmDAO.findById(found.get(property).toString())).
+                flatMap(Optional::stream).map(n -> (Realm) n).toList();
     }
 
     @Override
     public List<Realm> findByName(final String name) {
         return toList(neo4jClient.query(
                 "MATCH (n:" + Neo4jRealm.NODE + ") WHERE n.name = $name RETURN n.id").
-                bindAll(Map.of("name", name)).fetch().all(), "n.id", Neo4jRealm.class, cache);
+                bindAll(Map.of("name", name)).fetch().all(), "n.id");
     }
 
     @Override
     public List<Realm> findChildren(final Realm realm) {
         return toList(neo4jClient.query(
                 "MATCH (n:" + Neo4jRealm.NODE + " {id: $id})<-[r:" + Neo4jRealm.PARENT_REL + "]-(c) RETURN c.id").
-                bindAll(Map.of("id", realm.getKey())).fetch().all(), "c.id", Neo4jRealm.class, cache);
+                bindAll(Map.of("id", realm.getKey())).fetch().all(), "c.id");
     }
 
     @Override
-    public long countDescendants(final String base, final String keyword) {
-        return countDescendants(Set.of(base), keyword);
+    public List<Realm> findDescendants(final String base, final String prefix) {
+        Map<String, Object> parameters = new HashMap<>();
+
+        StringBuilder query = new StringBuilder("MATCH (n:").append(Neo4jRealm.NODE).append(") ").
+                append("WHERE (").append("n.fullPath = $base OR n.fullPath =~ $like").append(')');
+        parameters.put("base", base);
+        parameters.put("like", SyncopeConstants.ROOT_REALM.equals(base) ? "/.*" : base + "/.*");
+
+        if (prefix != null) {
+            query.append(" AND (n.fullPath = $prefix OR n.fullPath =~ $likePrefix)");
+            parameters.put("prefix", prefix);
+            parameters.put("likePrefix", SyncopeConstants.ROOT_REALM.equals(prefix) ? "/.*" : prefix + "/.*");
+        }
+
+        query.append(" RETURN n.id ORDER BY n.fullPath");
+
+        return toList(neo4jClient.query(
+                query.toString()).bindAll(parameters).fetch().all(), "n.id");
     }
 
-    @Override
-    public long countDescendants(final Set<String> bases, final String keyword) {
-        Map<String, Object> parameters = new HashMap<>();
+    protected QueryInfo getQuery(final SearchCond cond, final Map<String, Object> parameters) {
+        boolean not = cond.getType() == SearchCond.Type.NOT_LEAF;
+
+        TextStringBuilder query = new TextStringBuilder();
+        Set<String> involvedFields = new HashSet<>();
+        Set<PlainSchema> involvedPlainSchemas = new HashSet<>();
 
-        StringBuilder queryString = buildDescendantsQuery(bases, keyword, parameters).append(" RETURN COUNT(n)");
-        return neo4jTemplate.count(queryString.toString(), parameters);
+        switch (cond.getType()) {
+            case LEAF, NOT_LEAF -> {
+                cond.asLeaf(AnyCond.class).ifPresentOrElse(
+                        anyCond -> {
+                            AnyCondQuery anyCondQuery = getQuery(anyCond, not, parameters);
+                            query.append(anyCondQuery.query());
+                            Optional.ofNullable(anyCondQuery.field()).ifPresent(involvedFields::add);
+                        },
+                        () -> cond.asLeaf(AttrCond.class).ifPresent(leaf -> {
+                            AttrCondQuery attrCondQuery = getQuery(leaf, not, parameters);
+                            query.append(attrCondQuery.query());
+                            involvedPlainSchemas.add(attrCondQuery.schema());
+                        }));
+
+                // allow for additional search conditions
+                getQueryForCustomConds(cond, parameters, not, query);
+            }
+            case AND -> {
+                QueryInfo leftAndInfo = getQuery(cond.getLeft(), parameters);
+                involvedFields.addAll(leftAndInfo.fields());
+                involvedPlainSchemas.addAll(leftAndInfo.plainSchemas());
+
+                QueryInfo rigthAndInfo = getQuery(cond.getRight(), parameters);
+                involvedFields.addAll(rigthAndInfo.fields());
+                involvedPlainSchemas.addAll(rigthAndInfo.plainSchemas());
+
+                queryOp(query, "AND", leftAndInfo, rigthAndInfo);
+            }
+
+            case OR -> {
+                QueryInfo leftOrInfo = getQuery(cond.getLeft(), parameters);
+                involvedFields.addAll(leftOrInfo.fields());
+                involvedPlainSchemas.addAll(leftOrInfo.plainSchemas());
+
+                QueryInfo rigthOrInfo = getQuery(cond.getRight(), parameters);
+                involvedFields.addAll(rigthOrInfo.fields());
+                involvedPlainSchemas.addAll(rigthOrInfo.plainSchemas());
+
+                queryOp(query, "OR", leftOrInfo, rigthOrInfo);
+            }
+
+            default -> {
+            }
+        }
+
+        return new QueryInfo(query, involvedFields, involvedPlainSchemas);
     }
 
-    @Override
-    public List<Realm> findDescendants(final String base, final String keyword, final Pageable pageable) {
-        return findDescendants(Set.of(base), keyword, pageable);
+    protected void wrapQuery(
+            final Set<String> bases,
+            final QueryInfo queryInfo,
+            final Streamable<Order> orderBy,
+            final Map<String, Object> parameters) {
+
+        TextStringBuilder match = new TextStringBuilder("MATCH (n:").append(Neo4jRealm.NODE).append(") ").
+                append("WITH n.id AS id");
+
+        // take fields into account
+        queryInfo.fields().remove("id");
+        Stream.concat(
+                queryInfo.fields().stream(),
+                orderBy.stream().filter(clause -> !"id".equals(clause.getProperty())
+                && realmUtils.getField(clause.getProperty()).isPresent()).map(Order::getProperty)).
+                distinct().forEach(field -> match.append(", n.").append(field).append(" AS ").append(field));
+
+        // take plain schemas into account
+        Stream.concat(
+                queryInfo.plainSchemas().stream(),
+                orderBy.stream().map(clause -> plainSchemaDAO.findById(clause.getProperty())).
+                        flatMap(Optional::stream)).distinct().forEach(schema -> {
+
+            match.append(", apoc.convert.getJsonProperty(n, 'plainAttrs.").append(schema.getKey());
+            if (schema.isUniqueConstraint()) {
+                match.append("', '$.uniqueValue')");
+            } else {
+                match.append("', '$.values')");
+            }
+            match.append(" AS ").append(schema.getKey());
+        });
+
+        TextStringBuilder query = queryInfo.query();
+
+        // take bases into account
+        AtomicInteger index = new AtomicInteger(0);
+        String basesClause = bases.stream().map(base -> {
+            int idx = index.incrementAndGet();
+            parameters.put("base" + idx, base);
+            parameters.put("like" + idx, SyncopeConstants.ROOT_REALM.equals(base) ? "/.*" : base + "/.*");
+            return "n.fullPath = $base" + idx + " OR n.fullPath =~ $like" + idx;
+        }).collect(Collectors.joining(" OR "));
+        if (query.startsWith("MATCH (n)")) {
+            query.replaceFirst("MATCH (n)", match + " WHERE (EXISTS { MATCH (n)");
+            query.append("} ");
+        } else {
+            query.replaceFirst("WHERE EXISTS", "WHERE (EXISTS");
+            query.insert(0, match.append(' '));
+        }
+        query.append(") AND EXISTS { ").append("(n) WHERE (").append(basesClause).append(")").append(" } ");
+    }
+
+    protected AttrCondQuery getQuery(
+            final AttrCond cond,
+            final boolean not,
+            final Map<String, Object> parameters) {
+
+        CheckResult<AttrCond> checked = check(cond);
+
+        TextStringBuilder query = new TextStringBuilder("MATCH (n) ");
+        switch (cond.getType()) {
+            case ISNOTNULL ->
+                query.append("WHERE n.`plainAttrs.").append(checked.schema().getKey()).append("` IS NOT NULL");
+
+            case ISNULL ->
+                query.append("WHERE n.`plainAttrs.").append(checked.schema().getKey()).append("` IS NULL");
+
+            default ->
+                fillAttrQuery(query, checked.value(), checked.schema(), cond, not, parameters);
+        }
+
+        return new AttrCondQuery(query.toString(), checked.schema());
+    }
+
+    protected void getQueryForCustomConds(
+            final SearchCond cond,
+            final Map<String, Object> parameters,
+            final boolean not,
+            final TextStringBuilder query) {
+
+        // do nothing by default, leave it open for subclasses
+    }
+
+    protected void fillAttrQuery(
+            final TextStringBuilder query,
+            final PlainAttrValue attrValue,
+            final PlainSchema schema,
+            final AttrCond cond,
+            final boolean not,
+            final Map<String, Object> parameters) {
+
+        if (not && cond.getType() == AttrCond.Type.ISNULL) {
+            cond.setType(AttrCond.Type.ISNOTNULL);
+            fillAttrQuery(query, attrValue, schema, cond, true, parameters);
+            return;
+        }
+        if (not) {
+            if (schema.isUniqueConstraint()) {
+                fillAttrQuery(query, attrValue, schema, cond, false, parameters);
+                query.replaceFirst("WHERE", "WHERE NOT(");
+                query.append(')');
+            } else {
+                fillAttrQuery(query, attrValue, schema, cond, false, parameters);
+                query.replaceAll("any(", schema.getKey() + " IS NULL OR none(");
+            }
+            return;
+        }
+
+        String value = Optional.ofNullable(attrValue.getDateValue()).
+                map(DateTimeFormatter.ISO_OFFSET_DATE_TIME::format).
+                orElseGet(cond::getExpression);
+
+        boolean isStr = true;
+        boolean lower = false;
+        if (schema.getType().isStringClass()) {
+            lower = (cond.getType() == AttrCond.Type.IEQ || cond.getType() == AttrCond.Type.ILIKE);
+        } else if (schema.getType() != AttrSchemaType.Date) {
+            lower = false;
+            try {
+                switch (schema.getType()) {
+                    case Long ->
+                        Long.valueOf(value);
+
+                    case Double ->
+                        Double.valueOf(value);
+
+                    case Boolean -> {
+                        if (!("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value))) {
+                            throw new IllegalArgumentException();
+                        }
+                    }
+
+                    default -> {
+                    }
+                }
+
+                isStr = false;
+            } catch (Exception nfe) {
+                // ignore
+            }
+        }
+
+        query.append("WHERE ");
+
+        switch (cond.getType()) {
+            case ISNULL -> {
+            }
+
+            case ISNOTNULL ->
+                query.append(schema.getKey()).append(" IS NOT NULL");
+
+            case ILIKE, LIKE -> {
+                if (schema.getType().isStringClass()) {
+                    appendPlainAttrCond(
+                            query,
+                            schema,
+                            " =~ \"" + (lower ? "(?i)" : "")
+                            + AnyRepoExt.escapeForLikeRegex(value).replace("%", ".*") + '"');
+                } else {
+                    query.append(ALWAYS_FALSE_CLAUSE);
+                    LOG.error("LIKE is only compatible with string or enum schemas");
+                }
+            }
+
+            case IEQ, EQ -> {
+                if (StringUtils.containsAny(value, AnyRepoExt.REGEX_CHARS) || lower) {
+                    appendPlainAttrCond(
+                            query,
+                            schema,
+                            " =~ \"^" + (lower ? "(?i)" : "")
+                            + AnyRepoExt.escapeForLikeRegex(value).replace("%", ".*") + "$\"");
+                } else {
+                    appendPlainAttrCond(
+                            query,
+                            schema,
+                            " = " + escapeIfString(value, isStr));
+                }
+            }
+
+            case GE ->
+                appendPlainAttrCond(
+                        query,
+                        schema,
+                        " >= " + escapeIfString(value, isStr));
+
+            case GT ->
+                appendPlainAttrCond(
+                        query,
+                        schema,
+                        " > " + escapeIfString(value, isStr));
+
+            case LE ->
+                appendPlainAttrCond(
+                        query,
+                        schema,
+                        " <= " + escapeIfString(value, isStr));
+
+            case LT ->
+                appendPlainAttrCond(
+                        query,
+                        schema,
+                        " < " + escapeIfString(value, isStr));
+
+            default -> {
+            }
+        }
+        // shouldn't occour: processed before
+    }
+
+    protected void fillAttrQuery(
+            final TextStringBuilder query,
+            final PlainAttrValue attrValue,
+            final PlainSchema schema,
+            final AnyCond cond,
+            final boolean not,
+            final Map<String, Object> parameters) {
+
+        if (not && cond.getType() == AttrCond.Type.ISNULL) {
+            cond.setType(AttrCond.Type.ISNOTNULL);
+            fillAttrQuery(query, attrValue, schema, cond, true, parameters);
+            return;
+        }
+        if (not) {
+            query.append("NOT (");
+            fillAttrQuery(query, attrValue, schema, cond, false, parameters);
+            query.append(')');
+            return;
+        }
+        if (not && cond.getType() == AttrCond.Type.ISNULL) {
+            cond.setType(AttrCond.Type.ISNOTNULL);
+            fillAttrQuery(query, attrValue, schema, cond, true, parameters);
+            return;
+        }
+
+        boolean lower = schema.getType().isStringClass()
+                && (cond.getType() == AttrCond.Type.IEQ || cond.getType() == AttrCond.Type.ILIKE);
+
+        String property = "n." + cond.getSchema();
+        if (lower) {
+            property = "toLower (" + property + ')';
+        }
+
+        switch (cond.getType()) {
+
+            case ISNULL ->
+                query.append(property).append(" IS NULL");
+
+            case ISNOTNULL ->
+                query.append(property).append(" IS NOT NULL");
+
+            case ILIKE, LIKE -> {
+                if (schema.getType().isStringClass()) {
+                    query.append(property).append(" =~ ");
+                    if (lower) {
+                        query.append("toLower($").
+                                append(setParameter(parameters, cond.getExpression().replace("%", ".*"))).
+                                append(')');
+                    } else {
+                        query.append('$').append(setParameter(parameters, cond.getExpression().replace("%", ".*")));
+                    }
+                } else {
+                    query.append(' ').append(ALWAYS_FALSE_CLAUSE);
+                    LOG.error("LIKE is only compatible with string or enum schemas");
+                }
+            }
+
+            case IEQ, EQ -> {
+                query.append(property).append('=');
+
+                if (lower) {
+                    query.append("toLower($").append(setParameter(parameters, attrValue.getValue())).append(')');
+                } else {
+                    query.append('$').append(setParameter(parameters, attrValue.getValue()));
+                }
+            }
+
+            case GE -> {
+                query.append(property);
+                if (not) {
+                    query.append('<');
+                } else {
+                    query.append(">=");
+                }
+                query.append('$').append(setParameter(parameters, attrValue.getValue()));
+            }
+
+            case GT -> {
+                query.append(property);
+                if (not) {
+                    query.append("<=");
+                } else {
+                    query.append('>');
+                }
+                query.append('$').append(setParameter(parameters, attrValue.getValue()));
+            }
+
+            case LE -> {
+                query.append(property);
+                if (not) {
+                    query.append('>');
+                } else {
+                    query.append("<=");
+                }
+                query.append('$').append(setParameter(parameters, attrValue.getValue()));
+            }
+
+            case LT -> {
+                query.append(property);
+                if (not) {
+                    query.append(">=");
+                } else {
+                    query.append('<');
+                }
+                query.append('$').append(setParameter(parameters, attrValue.getValue()));
+            }
+
+            default -> {
+            }
+        }
+    }
+
+    protected AnyCondQuery getQuery(
+            final AnyCond cond,
+            final boolean not,
+            final Map<String, Object> parameters) {
+
+        CheckResult<AnyCond> checked = check(
+                cond,
+                realmUtils.getField(cond.getSchema()).
+                        orElseThrow(() -> new IllegalArgumentException("Invalid schema " + cond.getSchema())),
+                RELATIONSHIP_FIELDS);
+
+        TextStringBuilder query = new TextStringBuilder("MATCH (n) WHERE ");
+
+        fillAttrQuery(query, checked.value(), checked.schema(), checked.cond(), not, parameters);
+
+        return new AnyCondQuery(query.toString(), checked.cond().getSchema());
     }
 
     @Override
-    public List<Realm> findDescendants(final Set<String> bases, final String keyword, final Pageable pageable) {
+    protected long doCount(final Set<String> bases, final SearchCond cond) {
         Map<String, Object> parameters = new HashMap<>();
 
-        StringBuilder queryString = buildDescendantsQuery(bases, keyword, parameters).
-                append(" RETURN n.id ORDER BY n.fullPath");
-        if (pageable.isPaged()) {
-            queryString.append(" SKIP ").append(pageable.getPageSize() * pageable.getPageNumber()).
-                    append(" LIMIT ").append(pageable.getPageSize());
-        }
+        QueryInfo queryInfo = getQuery(cond, parameters);
 
-        return toList(neo4jClient.query(
-                queryString.toString()).bindAll(parameters).fetch().all(), "n.id", Neo4jRealm.class, cache);
+        wrapQuery(bases, queryInfo, Streamable.empty(), parameters);
+        TextStringBuilder query = queryInfo.query();
+
+        query.append("RETURN COUNT(id)");
+
+        LOG.debug("Query: {}, parameters: {}", query, parameters);
+
+        return neo4jTemplate.count(query.toString(), parameters);
+    }
+
+    protected List<String> parseOrderBy(final Streamable<Sort.Order> orderBy) {
+        List<String> clauses = new ArrayList<>();
+
+        Set<String> orderByUniquePlainSchemas = new HashSet<>();
+        Set<String> orderByNonUniquePlainSchemas = new HashSet<>();
+        orderBy.forEach(clause -> {
+            if (realmUtils.getField(clause.getProperty()).isPresent()) {
+                clauses.add(clause.getProperty() + " " + clause.getDirection().name());
+            } else {
+                plainSchemaDAO.findById(clause.getProperty()).ifPresent(schema -> {
+                    if (schema.isUniqueConstraint()) {
+                        orderByUniquePlainSchemas.add(schema.getKey());
+                    } else {
+                        orderByNonUniquePlainSchemas.add(schema.getKey());
+                    }
+                    if (orderByUniquePlainSchemas.size() > 1 || orderByNonUniquePlainSchemas.size() > 1) {
+                        SyncopeClientException invalidSearch =
+                                SyncopeClientException.build(ClientExceptionType.InvalidSearchParameters);
+                        invalidSearch.getElements().add("Order by more than one attribute is not allowed; "
+                                + "remove one from " + (orderByUniquePlainSchemas.size() > 1
+                                ? orderByUniquePlainSchemas : orderByNonUniquePlainSchemas));
+                        throw invalidSearch;
+                    }
+
+                    clauses.add(schema.getKey() + " " + clause.getDirection().name());
+                });
+            }
+        });
+
+        return clauses;
     }
 
     @Override
-    public List<String> findDescendants(final String base, final String prefix) {
+    protected List<Realm> doSearch(final Set<String> bases, final SearchCond cond, final Pageable pageable) {
         Map<String, Object> parameters = new HashMap<>();
 
-        StringBuilder queryString = buildDescendantsQuery(Set.of(base), null, parameters).
-                append(" AND (n.fullPath = $prefix OR n.fullPath =~ $likePrefix)").
-                append(" RETURN n.id ORDER BY n.fullPath");
-        parameters.put("prefix", prefix);
-        parameters.put("likePrefix", SyncopeConstants.ROOT_REALM.equals(prefix) ? "/.*" : prefix + "/.*");
+        QueryInfo queryInfo = getQuery(cond, parameters);
 
-        return neo4jClient.query(queryString.toString()).
-                bindAll(parameters).fetch().all().stream().
-                map(found -> (String) found.values().iterator().next()).toList();
+        wrapQuery(bases, queryInfo, pageable.getSort(), parameters);
+        TextStringBuilder query = queryInfo.query();
+
+        List<String> orderBy = parseOrderBy(pageable.getSort());
+        String orderByStmt = String.join(", ", orderBy);
+
+        query.append("RETURN id ").
+                append("ORDER BY ").append(orderByStmt);
+
+        if (pageable.isPaged()) {
+            query.append(" SKIP ").append(pageable.getPageSize() * pageable.getPageNumber()).
+                    append(" LIMIT ").append(pageable.getPageSize());
+        }
+
+        LOG.debug("Query: {}, parameters: {}", query, parameters);
+
+        return toList(neo4jClient.query(
+                query.toString()).bindAll(parameters).fetch().all(), "id");
     }
 }
diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jTaskDAO.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jTaskDAO.java
index 0251ab76c2..0bbea61c08 100644
--- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jTaskDAO.java
+++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jTaskDAO.java
@@ -320,7 +320,7 @@ public class Neo4jTaskDAO extends AbstractDAO implements TaskDAO {
             Stream<String> realmKeys = AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.TASK_LIST).stream().
                     map(realmSearchDAO::findByFullPath).
                     filter(Optional::isPresent).
-                    flatMap(r -> realmSearchDAO.findDescendants(r.get().getFullPath(), null, Pageable.unpaged()).
+                    flatMap(r -> realmSearchDAO.findDescendants(r.get().getFullPath(), null).
                     stream()).
                     map(Realm::getKey).
                     distinct();
diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AbstractAnyRepoExt.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AbstractAnyRepoExt.java
index b82ded261f..ea7569315a 100644
--- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AbstractAnyRepoExt.java
+++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AbstractAnyRepoExt.java
@@ -38,7 +38,6 @@ import org.apache.syncope.core.persistence.api.entity.Relationship;
 import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
 import org.apache.syncope.core.persistence.api.entity.group.GRelationship;
 import org.apache.syncope.core.persistence.api.entity.user.URelationship;
-import org.apache.syncope.core.persistence.common.dao.AnyFinder;
 import org.apache.syncope.core.persistence.neo4j.dao.AbstractDAO;
 import org.apache.syncope.core.persistence.neo4j.entity.AbstractAny;
 import org.apache.syncope.core.persistence.neo4j.entity.EntityCacheKey;
@@ -84,8 +83,6 @@ public abstract class AbstractAnyRepoExt<A extends Any, N extends AbstractAny>
 
     protected final AnyChecker anyChecker;
 
-    protected final AnyFinder anyFinder;
-
     protected final AnyUtils anyUtils;
 
     protected AbstractAnyRepoExt(
@@ -93,7 +90,6 @@ public abstract class AbstractAnyRepoExt<A extends Any, N extends AbstractAny>
             final AnyTypeClassDAO anyTypeClassDAO,
             final DerSchemaDAO derSchemaDAO,
             final AnyChecker anyChecker,
-            final AnyFinder anyFinder,
             final AnyUtils anyUtils,
             final Neo4jTemplate neo4jTemplate,
             final Neo4jClient neo4jClient) {
@@ -103,7 +99,6 @@ public abstract class AbstractAnyRepoExt<A extends Any, N extends AbstractAny>
         this.anyTypeClassDAO = anyTypeClassDAO;
         this.derSchemaDAO = derSchemaDAO;
         this.anyChecker = anyChecker;
-        this.anyFinder = anyFinder;
         this.anyUtils = anyUtils;
     }
 
@@ -150,11 +145,6 @@ public abstract class AbstractAnyRepoExt<A extends Any, N extends AbstractAny>
         return any;
     }
 
-    @Override
-    public List<A> findByDerAttrValue(final String expression, final String value, final boolean ignoreCaseMatch) {
-        return anyFinder.findByDerAttrValue(anyUtils.anyTypeKind(), expression, value, ignoreCaseMatch);
-    }
-
     @Override
     public List<A> findByResourcesContaining(final ExternalResource resource) {
         return findByRelationship(
diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AnyObjectRepoExtImpl.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AnyObjectRepoExtImpl.java
index 6504b21a99..606f8efddd 100644
--- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AnyObjectRepoExtImpl.java
+++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AnyObjectRepoExtImpl.java
@@ -47,7 +47,6 @@ import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
 import org.apache.syncope.core.persistence.api.entity.group.Group;
 import org.apache.syncope.core.persistence.api.entity.user.URelationship;
 import org.apache.syncope.core.persistence.api.utils.RealmUtils;
-import org.apache.syncope.core.persistence.common.dao.AnyFinder;
 import org.apache.syncope.core.persistence.neo4j.entity.EntityCacheKey;
 import org.apache.syncope.core.persistence.neo4j.entity.Neo4jAnyType;
 import org.apache.syncope.core.persistence.neo4j.entity.Neo4jAnyTypeClass;
@@ -85,7 +84,6 @@ public class AnyObjectRepoExtImpl extends AbstractAnyRepoExt<AnyObject, Neo4jAny
             final UserDAO userDAO,
             final GroupDAO groupDAO,
             final AnyChecker anyChecker,
-            final AnyFinder anyFinder,
             final Neo4jTemplate neo4jTemplate,
             final Neo4jClient neo4jClient,
             final NodeValidator nodeValidator,
@@ -96,7 +94,6 @@ public class AnyObjectRepoExtImpl extends AbstractAnyRepoExt<AnyObject, Neo4jAny
                 anyTypeClassDAO,
                 derSchemaDAO,
                 anyChecker,
-                anyFinder,
                 anyUtilsFactory.getInstance(AnyTypeKind.ANY_OBJECT),
                 neo4jTemplate,
                 neo4jClient);
diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AnyRepoExt.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AnyRepoExt.java
index 6fd68e4add..9b0ae140a7 100644
--- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AnyRepoExt.java
+++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AnyRepoExt.java
@@ -75,8 +75,6 @@ public interface AnyRepoExt<A extends Any> {
 
     A authFind(String key);
 
-    List<A> findByDerAttrValue(String expression, String value, boolean ignoreCaseMatch);
-
     Collection<String> findAllResourceKeys(String key);
 
     List<A> findByResourcesContaining(ExternalResource resource);
diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/GroupRepoExtImpl.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/GroupRepoExtImpl.java
index 95bcb2704b..6fcb2ac443 100644
--- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/GroupRepoExtImpl.java
+++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/GroupRepoExtImpl.java
@@ -47,7 +47,6 @@ import org.apache.syncope.core.persistence.api.entity.group.GroupTypeExtension;
 import org.apache.syncope.core.persistence.api.entity.user.UMembership;
 import org.apache.syncope.core.persistence.api.entity.user.User;
 import org.apache.syncope.core.persistence.api.utils.RealmUtils;
-import org.apache.syncope.core.persistence.common.dao.AnyFinder;
 import org.apache.syncope.core.persistence.neo4j.entity.AbstractAny;
 import org.apache.syncope.core.persistence.neo4j.entity.EntityCacheKey;
 import org.apache.syncope.core.persistence.neo4j.entity.Neo4jAnyTypeClass;
@@ -98,7 +97,6 @@ public class GroupRepoExtImpl extends AbstractAnyRepoExt<Group, Neo4jGroup> impl
             final UserDAO userDAO,
             final AnyObjectDAO anyObjectDAO,
             final AnyChecker anyChecker,
-            final AnyFinder anyFinder,
             final Neo4jTemplate neo4jTemplate,
             final Neo4jClient neo4jClient,
             final NodeValidator nodeValidator,
@@ -111,11 +109,10 @@ public class GroupRepoExtImpl extends AbstractAnyRepoExt<Group, Neo4jGroup> impl
                 anyTypeClassDAO,
                 derSchemaDAO,
                 anyChecker,
-                anyFinder,
                 anyUtilsFactory.getInstance(AnyTypeKind.GROUP),
                 neo4jTemplate,
                 neo4jClient);
-        
+
         this.publisher = publisher;
         this.realmDAO = realmDAO;
         this.userDAO = userDAO;
diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/RoleRepoExtImpl.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/RoleRepoExtImpl.java
index fee8671f33..67e1b6dd39 100644
--- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/RoleRepoExtImpl.java
+++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/RoleRepoExtImpl.java
@@ -26,7 +26,6 @@ import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
 import org.apache.syncope.core.persistence.api.dao.DelegationDAO;
 import org.apache.syncope.core.persistence.api.entity.Realm;
 import org.apache.syncope.core.persistence.api.entity.Role;
-import org.apache.syncope.core.persistence.api.search.SearchCondVisitor;
 import org.apache.syncope.core.persistence.neo4j.dao.AbstractDAO;
 import org.apache.syncope.core.persistence.neo4j.entity.EntityCacheKey;
 import org.apache.syncope.core.persistence.neo4j.entity.Neo4jRealm;
@@ -50,8 +49,6 @@ public class RoleRepoExtImpl extends AbstractDAO implements RoleRepoExt {
 
     protected final DelegationDAO delegationDAO;
 
-    protected final SearchCondVisitor searchCondVisitor;
-
     protected final NodeValidator nodeValidator;
 
     protected final Cache<EntityCacheKey, Neo4jRole> cache;
@@ -61,7 +58,6 @@ public class RoleRepoExtImpl extends AbstractDAO implements RoleRepoExt {
             final AnyMatchDAO anyMatchDAO,
             final AnySearchDAO anySearchDAO,
             final DelegationDAO delegationDAO,
-            final SearchCondVisitor searchCondVisitor,
             final Neo4jTemplate neo4jTemplate,
             final Neo4jClient neo4jClient,
             final NodeValidator nodeValidator,
@@ -72,7 +68,6 @@ public class RoleRepoExtImpl extends AbstractDAO implements RoleRepoExt {
         this.anyMatchDAO = anyMatchDAO;
         this.anySearchDAO = anySearchDAO;
         this.delegationDAO = delegationDAO;
-        this.searchCondVisitor = searchCondVisitor;
         this.nodeValidator = nodeValidator;
         this.cache = cache;
     }
diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/UserRepoExtImpl.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/UserRepoExtImpl.java
index 4cd720ded8..3dcb0997f8 100644
--- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/UserRepoExtImpl.java
+++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/UserRepoExtImpl.java
@@ -51,7 +51,6 @@ import org.apache.syncope.core.persistence.api.entity.user.UMembership;
 import org.apache.syncope.core.persistence.api.entity.user.URelationship;
 import org.apache.syncope.core.persistence.api.entity.user.User;
 import org.apache.syncope.core.persistence.api.utils.RealmUtils;
-import org.apache.syncope.core.persistence.common.dao.AnyFinder;
 import org.apache.syncope.core.persistence.neo4j.entity.AbstractAny;
 import org.apache.syncope.core.persistence.neo4j.entity.EntityCacheKey;
 import org.apache.syncope.core.persistence.neo4j.entity.Neo4jAnyTypeClass;
@@ -107,7 +106,6 @@ public class UserRepoExtImpl extends AbstractAnyRepoExt<User, Neo4jUser> impleme
             final DelegationDAO delegationDAO,
             final FIQLQueryDAO fiqlQueryDAO,
             final AnyChecker anyChecker,
-            final AnyFinder anyFinder,
             final SecurityProperties securityProperties,
             final Neo4jTemplate neo4jTemplate,
             final Neo4jClient neo4jClient,
@@ -121,7 +119,6 @@ public class UserRepoExtImpl extends AbstractAnyRepoExt<User, Neo4jUser> impleme
                 anyTypeClassDAO,
                 derSchemaDAO,
                 anyChecker,
-                anyFinder,
                 anyUtilsFactory.getInstance(AnyTypeKind.USER),
                 neo4jTemplate,
                 neo4jClient);
diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/Neo4jSRARoute.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/Neo4jSRARoute.java
index e000ea4280..eaed66f437 100644
--- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/Neo4jSRARoute.java
+++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/Neo4jSRARoute.java
@@ -148,7 +148,7 @@ public class Neo4jSRARoute extends AbstractGeneratedKeyNode implements SRARoute
     public List<SRARouteFilter> getFilters() {
         return Optional.ofNullable(filters).
                 map(f -> List.of(POJOHelper.deserialize(f, SRARouteFilter[].class))).
-            orElseGet(List::of);
+                orElseGet(List::of);
     }
 
     @Override
@@ -160,7 +160,7 @@ public class Neo4jSRARoute extends AbstractGeneratedKeyNode implements SRARoute
     public List<SRARoutePredicate> getPredicates() {
         return Optional.ofNullable(predicates).
                 map(f -> List.of(POJOHelper.deserialize(f, SRARoutePredicate[].class))).
-            orElseGet(List::of);
+                orElseGet(List::of);
     }
 
     @Override
diff --git a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/AnySearchTest.java b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/AnySearchTest.java
index 8e50996632..693ff9a01a 100644
--- a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/AnySearchTest.java
+++ b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/AnySearchTest.java
@@ -39,6 +39,7 @@ import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationMana
 import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
 import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
+import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmDAO;
@@ -111,6 +112,9 @@ public class AnySearchTest extends AbstractTest {
     @Autowired
     private PlainSchemaDAO plainSchemaDAO;
 
+    @Autowired
+    private DerSchemaDAO derSchemaDAO;
+
     @Autowired
     private PlainAttrValidationManager validator;
 
@@ -761,13 +765,13 @@ public class AnySearchTest extends AbstractTest {
             assertEquals(
                     1,
                     searchDAO.count(
-                            realmDAO.getRoot(), true, authRealms, groupDAO.getAllMatchingCond(), AnyTypeKind.GROUP));
+                            realmDAO.getRoot(), true, authRealms, searchDAO.getAllMatchingCond(), AnyTypeKind.GROUP));
 
             List<Group> groups = searchDAO.search(
                     realmDAO.getRoot(),
                     true,
                     authRealms,
-                    groupDAO.getAllMatchingCond(),
+                    searchDAO.getAllMatchingCond(),
                     PageRequest.of(0, 10),
                     AnyTypeKind.GROUP);
             assertEquals(1, groups.size());
@@ -794,6 +798,35 @@ public class AnySearchTest extends AbstractTest {
         assertEquals(5, users.size());
     }
 
+    @Test
+    public void findByDerAttrValue() {
+        List<User> list = searchDAO.findByDerAttrValue(
+                derSchemaDAO.findById("cn").orElseThrow().getExpression(), "Vivaldi, Antonio", false, AnyTypeKind.USER);
+        assertEquals(1, list.size());
+
+        list = searchDAO.findByDerAttrValue(
+                derSchemaDAO.findById("cn").orElseThrow().getExpression(), "VIVALDI, ANTONIO", false, AnyTypeKind.USER);
+        assertEquals(0, list.size());
+
+        list = searchDAO.findByDerAttrValue(
+                derSchemaDAO.findById("cn").orElseThrow().getExpression(), "VIVALDI, ANTONIO", true, AnyTypeKind.USER);
+        assertEquals(1, list.size());
+    }
+
+    @Test
+    public void findByInvalidDerAttrValue() {
+        assertTrue(searchDAO.findByDerAttrValue(
+                derSchemaDAO.findById("cn").orElseThrow().getExpression(),
+                "Antonio, Maria, Rossi", false, AnyTypeKind.USER).isEmpty());
+    }
+
+    @Test
+    public void findByInvalidDerAttrExpression() {
+        assertThrows(IllegalArgumentException.class, () -> searchDAO.findByDerAttrValue(
+                derSchemaDAO.findById("noschema").orElseThrow().getExpression(),
+                "Antonio, Maria", false, AnyTypeKind.USER).isEmpty());
+    }
+
     @Test
     public void issue202() {
         ResourceCond ws2 = new ResourceCond();
diff --git a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/MultitenancyTest.java b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/MultitenancyTest.java
index 594124a0ad..dfc7ee33e1 100644
--- a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/MultitenancyTest.java
+++ b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/MultitenancyTest.java
@@ -34,7 +34,6 @@ import org.apache.syncope.core.persistence.neo4j.PersistenceTestContext;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.data.domain.Pageable;
 import org.springframework.transaction.annotation.Transactional;
 
 @Transactional
@@ -70,10 +69,10 @@ public class MultitenancyTest extends AbstractTest {
     public void readRealm() {
         assertEquals(
                 1,
-                realmSearchDAO.findDescendants(realmDAO.getRoot().getFullPath(), null, Pageable.unpaged()).size());
+                realmSearchDAO.findDescendants(realmDAO.getRoot().getFullPath(), null).size());
         assertEquals(
                 realmDAO.getRoot(),
-                realmSearchDAO.findDescendants(realmDAO.getRoot().getFullPath(), null, Pageable.unpaged()).getFirst());
+                realmSearchDAO.findDescendants(realmDAO.getRoot().getFullPath(), null).getFirst());
     }
 
     @Test
diff --git a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/RealmTest.java b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/RealmTest.java
index 578cb3f891..ad53b741b5 100644
--- a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/RealmTest.java
+++ b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/RealmTest.java
@@ -86,14 +86,11 @@ public class RealmTest extends AbstractTest {
 
     @Test
     public void findDescendants() {
-        List<String> found = realmSearchDAO.findDescendants(SyncopeConstants.ROOT_REALM, SyncopeConstants.ROOT_REALM);
+        List<Realm> found = realmSearchDAO.findDescendants(SyncopeConstants.ROOT_REALM, SyncopeConstants.ROOT_REALM);
         assertEquals(4, found.size());
-        assertTrue(found.stream().allMatch(f -> SyncopeConstants.UUID_PATTERN.matcher(f).matches()));
+        assertTrue(found.stream().allMatch(f -> SyncopeConstants.UUID_PATTERN.matcher(f.getKey()).matches()));
 
-        assertEquals(
-                found,
-                realmSearchDAO.findDescendants(SyncopeConstants.ROOT_REALM, null, Pageable.unpaged()).stream().
-                        map(Realm::getKey).toList());
+        assertEquals(found, realmSearchDAO.findDescendants(SyncopeConstants.ROOT_REALM, null));
     }
 
     @Test
@@ -110,14 +107,14 @@ public class RealmTest extends AbstractTest {
 
     @Test
     public void findAll() {
-        List<Realm> list = realmSearchDAO.findDescendants(realmDAO.getRoot().getFullPath(), null, Pageable.unpaged());
+        List<Realm> list = realmSearchDAO.findDescendants(realmDAO.getRoot().getFullPath(), null);
         assertNotNull(list);
         assertFalse(list.isEmpty());
         list.forEach(Assertions::assertNotNull);
 
         assertEquals(4, realmDAO.findAll(Pageable.ofSize(100)).stream().count());
 
-        list = realmSearchDAO.findDescendants(Set.of("/even", "/odd"), null, Pageable.unpaged());
+        list = realmSearchDAO.search(Set.of("/even", "/odd"), realmSearchDAO.getAllMatchingCond(), Pageable.unpaged());
         assertEquals(3, list.size());
         assertNotNull(list.stream().filter(realm -> "even".equals(realm.getName())).findFirst().orElseThrow());
         assertNotNull(list.stream().filter(realm -> "two".equals(realm.getName())).findFirst().orElseThrow());
diff --git a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/UserTest.java b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/UserTest.java
index 8286c5f2b7..b49c015746 100644
--- a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/UserTest.java
+++ b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/UserTest.java
@@ -30,7 +30,6 @@ import java.time.OffsetDateTime;
 import java.util.List;
 import org.apache.syncope.common.lib.types.CipherAlgorithm;
 import org.apache.syncope.core.persistence.api.EncryptorManager;
-import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO;
 import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO;
@@ -61,9 +60,6 @@ public class UserTest extends AbstractTest {
     @Autowired
     private ExternalResourceDAO resourceDAO;
 
-    @Autowired
-    private DerSchemaDAO derSchemaDAO;
-
     @Autowired
     private SecurityQuestionDAO securityQuestionDAO;
 
@@ -109,33 +105,6 @@ public class UserTest extends AbstractTest {
         assertEquals(5, count);
     }
 
-    @Test
-    public void findByDerAttrValue() {
-        List<User> list = userDAO.findByDerAttrValue(
-                derSchemaDAO.findById("cn").orElseThrow().getExpression(), "Vivaldi, Antonio", false);
-        assertEquals(1, list.size());
-
-        list = userDAO.findByDerAttrValue(
-                derSchemaDAO.findById("cn").orElseThrow().getExpression(), "VIVALDI, ANTONIO", false);
-        assertEquals(0, list.size());
-
-        list = userDAO.findByDerAttrValue(
-                derSchemaDAO.findById("cn").orElseThrow().getExpression(), "VIVALDI, ANTONIO", true);
-        assertEquals(1, list.size());
-    }
-
-    @Test
-    public void findByInvalidDerAttrValue() {
-        assertTrue(userDAO.findByDerAttrValue(
-                derSchemaDAO.findById("cn").orElseThrow().getExpression(), "Antonio, Maria, Rossi", false).isEmpty());
-    }
-
-    @Test
-    public void findByInvalidDerAttrExpression() {
-        assertThrows(IllegalArgumentException.class, () -> userDAO.findByDerAttrValue(
-                derSchemaDAO.findById("noschema").orElseThrow().getExpression(), "Antonio, Maria", false).isEmpty());
-    }
-
     @Test
     public void findByKey() {
         assertTrue(userDAO.findById("1417acbe-cbf6-4277-9372-e75e04f97000").isPresent());
diff --git a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/outer/AnySearchTest.java b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/outer/AnySearchTest.java
index 96ec720672..a89ebc2523 100644
--- a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/outer/AnySearchTest.java
+++ b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/outer/AnySearchTest.java
@@ -33,6 +33,7 @@ import org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
 import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
+import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO;
@@ -41,6 +42,7 @@ import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
 import org.apache.syncope.core.persistence.api.dao.search.AnyTypeCond;
 import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
 import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
+import org.apache.syncope.core.persistence.api.entity.DerSchema;
 import org.apache.syncope.core.persistence.api.entity.PlainAttr;
 import org.apache.syncope.core.persistence.api.entity.anyobject.AMembership;
 import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
@@ -76,6 +78,9 @@ public class AnySearchTest extends AbstractTest {
     @Autowired
     private RealmSearchDAO realmSearchDAO;
 
+    @Autowired
+    private DerSchemaDAO derSchemaDAO;
+
     @Autowired
     private PlainAttrValidationManager validator;
 
@@ -182,6 +187,41 @@ public class AnySearchTest extends AbstractTest {
         assertEquals("c9b2dec2-00a7-4855-97c0-d854842b4b24", users.getFirst().getKey());
     }
 
+    @Test
+    public void issueSYNCOPE800() {
+        // create derived attribute (literal as prefix)
+        DerSchema prefix = entityFactory.newEntity(DerSchema.class);
+        prefix.setKey("kprefix");
+        prefix.setExpression("'k' + firstname");
+
+        derSchemaDAO.save(prefix);
+
+        // create derived attribute (literal as suffix)
+        DerSchema suffix = entityFactory.newEntity(DerSchema.class);
+        suffix.setKey("ksuffix");
+        suffix.setExpression("firstname + 'k'");
+
+        derSchemaDAO.save(suffix);
+
+        // add derived attributes to user
+        User owner = userDAO.findByUsername("vivaldi").orElseThrow();
+
+        String firstname = owner.getPlainAttr("firstname").get().getValuesAsStrings().getFirst();
+        assertNotNull(firstname);
+
+        // search by ksuffix derived attribute
+        List<User> list = searchDAO.findByDerAttrValue(
+                derSchemaDAO.findById("ksuffix").orElseThrow().getExpression(),
+                firstname + 'k', false, AnyTypeKind.USER);
+        assertEquals(1, list.size());
+
+        // search by kprefix derived attribute
+        list = searchDAO.findByDerAttrValue(
+                derSchemaDAO.findById("kprefix").orElseThrow().getExpression(),
+                'k' + firstname, false, AnyTypeKind.USER);
+        assertEquals(1, list.size());
+    }
+
     @Test
     public void issueSYNCOPE1417() {
         AnyCond usernameLeafCond = new AnyCond(AnyCond.Type.EQ);
diff --git a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/outer/RealmTest.java b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/outer/RealmTest.java
index 941889f3cb..cd457b900b 100644
--- a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/outer/RealmTest.java
+++ b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/outer/RealmTest.java
@@ -18,10 +18,15 @@
  */
 package org.apache.syncope.core.persistence.neo4j.outer;
 
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.util.List;
+import java.util.Set;
 import java.util.UUID;
+import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.types.IdRepoImplementationType;
 import org.apache.syncope.common.lib.types.ImplementationEngine;
 import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
@@ -31,6 +36,9 @@ import org.apache.syncope.core.persistence.api.dao.ImplementationDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO;
 import org.apache.syncope.core.persistence.api.dao.RoleDAO;
+import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
+import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
+import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
 import org.apache.syncope.core.persistence.api.entity.Implementation;
 import org.apache.syncope.core.persistence.api.entity.PlainAttr;
 import org.apache.syncope.core.persistence.api.entity.Realm;
@@ -39,6 +47,8 @@ import org.apache.syncope.core.persistence.api.entity.group.Group;
 import org.apache.syncope.core.persistence.neo4j.AbstractTest;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
 import org.springframework.transaction.annotation.Transactional;
 
 @Transactional
@@ -136,4 +146,120 @@ public class RealmTest extends AbstractTest {
         realm = realmDAO.findById("722f3d84-9c2b-4525-8f6e-e4b82c55a36c").orElseThrow();
         assertTrue(realm.getActions().isEmpty());
     }
+
+    @Test
+    public void search() {
+        Realm two = realmSearchDAO.findByFullPath("/even/two").orElseThrow();
+        two.add(anyTypeClassDAO.findById("other").orElseThrow());
+        two = realmDAO.save(two);
+
+        two = realmDAO.findById(two.getKey()).orElseThrow();
+        PlainAttr aLong = new PlainAttr();
+        aLong.setSchema("aLong");
+        aLong.add(validator, "42");
+        two.add(aLong);
+        two = realmDAO.save(two);
+
+        Realm odd = realmSearchDAO.findByFullPath("/odd").orElseThrow();
+        odd.add(anyTypeClassDAO.findById("other").orElseThrow());
+        odd = realmDAO.save(odd);
+
+        odd = realmDAO.findById(odd.getKey()).orElseThrow();
+        PlainAttr oddLong = new PlainAttr();
+        oddLong.setSchema("aLong");
+        oddLong.add(validator, "99");
+        odd.add(oddLong);
+        realmDAO.save(odd);
+
+        two = realmDAO.findById(two.getKey()).orElseThrow();
+        assertEquals(anyTypeClassDAO.findById("other").orElseThrow(), two.getAnyTypeClasses().iterator().next());
+        assertEquals(1, two.getPlainAttrs().size());
+        assertEquals(42, two.getPlainAttr("aLong").orElseThrow().getValues().getFirst().getLongValue());
+
+        odd = realmDAO.findById(odd.getKey()).orElseThrow();
+        assertEquals(anyTypeClassDAO.findById("other").orElseThrow(), odd.getAnyTypeClasses().iterator().next());
+        assertEquals(1, odd.getPlainAttrs().size());
+        assertEquals(99, odd.getPlainAttr("aLong").orElseThrow().getValues().getFirst().getLongValue());
+
+        AnyCond name = new AnyCond(AttrCond.Type.EQ);
+        name.setSchema("name");
+        name.setExpression("two");
+
+        List<Realm> result = realmSearchDAO.search(
+                Set.of(SyncopeConstants.ROOT_REALM),
+                SearchCond.of(name),
+                Pageable.unpaged());
+        assertEquals(1, result.size());
+        assertEquals("two", result.getFirst().getName());
+
+        AttrCond attrEq = new AttrCond(AttrCond.Type.EQ);
+        attrEq.setSchema("aLong");
+        attrEq.setExpression("42");
+
+        result = realmSearchDAO.search(
+                Set.of(SyncopeConstants.ROOT_REALM),
+                SearchCond.of(attrEq),
+                Pageable.unpaged());
+        assertEquals(1, result.size());
+        assertEquals("two", result.getFirst().getName());
+
+        result = realmSearchDAO.search(
+                Set.of(SyncopeConstants.ROOT_REALM),
+                SearchCond.and(SearchCond.of(name), SearchCond.of(attrEq)),
+                Pageable.unpaged());
+        assertEquals(1, result.size());
+        assertEquals("two", result.getFirst().getName());
+
+        AttrCond attrEq2 = new AttrCond(AttrCond.Type.EQ);
+        attrEq2.setSchema("aLong");
+        attrEq2.setExpression("99");
+
+        result = realmSearchDAO.search(
+                Set.of(SyncopeConstants.ROOT_REALM),
+                SearchCond.or(SearchCond.of(attrEq), SearchCond.of(attrEq2)),
+                Pageable.unpaged());
+        assertTrue(result.stream().anyMatch(r -> "two".equals(r.getName())));
+        assertTrue(result.stream().anyMatch(r -> "odd".equals(r.getName())));
+
+        AttrCond attrIsNull = new AttrCond(AttrCond.Type.ISNULL);
+        attrIsNull.setSchema("aLong");
+
+        result = realmSearchDAO.search(
+                Set.of(SyncopeConstants.ROOT_REALM),
+                SearchCond.of(attrIsNull),
+                Pageable.unpaged());
+        result.forEach(r -> assertFalse("two".equals(r.getName()) || "odd".equals(r.getName())));
+
+        AttrCond attrIsNotNull = new AttrCond(AttrCond.Type.ISNOTNULL);
+        attrIsNotNull.setSchema("aLong");
+
+        assertEquals(2, realmSearchDAO.count(
+                Set.of(SyncopeConstants.ROOT_REALM),
+                SearchCond.of(attrIsNotNull)));
+
+        result = realmSearchDAO.search(
+                Set.of(SyncopeConstants.ROOT_REALM),
+                SearchCond.of(attrIsNotNull),
+                Pageable.unpaged(Sort.by(new Sort.Order(Sort.Direction.ASC, "name"))));
+        assertEquals("odd", result.get(0).getName());
+        assertEquals("two", result.get(1).getName());
+
+        result = realmSearchDAO.search(
+                Set.of(SyncopeConstants.ROOT_REALM),
+                SearchCond.of(attrIsNotNull),
+                Pageable.unpaged(Sort.by(new Sort.Order(Sort.Direction.ASC, "aLong"))));
+        assertEquals("two", result.get(0).getName());
+        assertEquals("odd", result.get(1).getName());
+
+        AttrCond cond1 = new AttrCond(AttrCond.Type.EQ);
+        cond1.setSchema("aLong");
+        cond1.setExpression("42");
+        AttrCond cond2 = new AttrCond(AttrCond.Type.IEQ);
+        cond2.setSchema("ctype");
+        cond2.setExpression("string");
+        assertDoesNotThrow(() -> realmSearchDAO.search(
+                Set.of(SyncopeConstants.ROOT_REALM),
+                SearchCond.and(SearchCond.of(cond1), SearchCond.of(cond2)),
+                Pageable.unpaged()));
+    }
 }
diff --git a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/outer/UserTest.java b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/outer/UserTest.java
index 1de4247081..5c8fed9aed 100644
--- a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/outer/UserTest.java
+++ b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/outer/UserTest.java
@@ -28,10 +28,12 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 import java.time.OffsetDateTime;
 import java.util.List;
 import java.util.UUID;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.CipherAlgorithm;
 import org.apache.syncope.core.persistence.api.attrvalue.InvalidEntityException;
 import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
 import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
+import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
 import org.apache.syncope.core.persistence.api.dao.AnyTypeClassDAO;
 import org.apache.syncope.core.persistence.api.dao.DelegationDAO;
 import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO;
@@ -94,6 +96,9 @@ public class UserTest extends AbstractTest {
     @Autowired
     private AnyTypeClassDAO anyTypeClassDAO;
 
+    @Autowired
+    private AnySearchDAO anySearchDAO;
+
     @Autowired
     private PlainAttrValidationManager validator;
 
@@ -285,13 +290,15 @@ public class UserTest extends AbstractTest {
         assertNotNull(firstname);
 
         // search by ksuffix derived attribute
-        List<User> list = userDAO.findByDerAttrValue(
-                derSchemaDAO.findById("ksuffix").orElseThrow().getExpression(), firstname + 'k', false);
+        List<User> list = anySearchDAO.findByDerAttrValue(
+                derSchemaDAO.findById("ksuffix").orElseThrow().getExpression(),
+                firstname + 'k', false, AnyTypeKind.USER);
         assertEquals(1, list.size());
 
         // search by kprefix derived attribute
-        list = userDAO.findByDerAttrValue(
-                derSchemaDAO.findById("kprefix").orElseThrow().getExpression(), 'k' + firstname, false);
+        list = anySearchDAO.findByDerAttrValue(
+                derSchemaDAO.findById("kprefix").orElseThrow().getExpression(),
+                'k' + firstname, false, AnyTypeKind.USER);
         assertEquals(1, list.size());
     }
 }
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/ProvisioningContext.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/ProvisioningContext.java
index 4d7a6849d6..1458cfb449 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/ProvisioningContext.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/ProvisioningContext.java
@@ -64,7 +64,7 @@ import org.apache.syncope.core.persistence.api.dao.WAConfigDAO;
 import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
 import org.apache.syncope.core.persistence.api.entity.EntityFactory;
 import org.apache.syncope.core.persistence.api.entity.task.TaskUtilsFactory;
-import org.apache.syncope.core.persistence.api.search.SearchCondVisitor;
+import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor;
 import org.apache.syncope.core.persistence.api.utils.RealmUtils;
 import org.apache.syncope.core.provisioning.api.AnyObjectProvisioningManager;
 import org.apache.syncope.core.provisioning.api.AuditEventProcessor;
@@ -581,7 +581,7 @@ public class ProvisioningContext {
     @Bean
     public NotificationManager notificationManager(
             final EntityFactory entityFactory,
-            final SearchCondVisitor searchCondVisitor,
+            final AnySearchCondVisitor searchCondVisitor,
             final DerSchemaDAO derSchemaDAO,
             final NotificationDAO notificationDAO,
             final AnyObjectDAO anyObjectDAO,
@@ -857,7 +857,7 @@ public class ProvisioningContext {
     @ConditionalOnMissingBean
     @Bean
     public FIQLQueryDataBinder fiqlQueryDataBinder(
-            final SearchCondVisitor searchCondVisitor,
+            final AnySearchCondVisitor searchCondVisitor,
             final UserDAO userDAO,
             final EntityFactory entityFactory) {
 
@@ -868,7 +868,7 @@ public class ProvisioningContext {
     @Bean
     public GroupDataBinder groupDataBinder(
             final EntityFactory entityFactory,
-            final SearchCondVisitor searchCondVisitor,
+            final AnySearchCondVisitor searchCondVisitor,
             final AnyUtilsFactory anyUtilsFactory,
             final AnyTypeDAO anyTypeDAO,
             final RealmSearchDAO realmSearchDAO,
@@ -1044,15 +1044,10 @@ public class ProvisioningContext {
     @Bean
     public RoleDataBinder roleDataBinder(
             final EntityFactory entityFactory,
-            final SearchCondVisitor searchCondVisitor,
             final RealmSearchDAO realmSearchDAO,
             final RoleDAO roleDAO) {
 
-        return new RoleDataBinderImpl(
-                realmSearchDAO,
-                roleDAO,
-                entityFactory,
-                searchCondVisitor);
+        return new RoleDataBinderImpl(realmSearchDAO, roleDAO, entityFactory);
     }
 
     @ConditionalOnMissingBean
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/FIQLQueryDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/FIQLQueryDataBinderImpl.java
index 61ba15b93f..748d0008e2 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/FIQLQueryDataBinderImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/FIQLQueryDataBinderImpl.java
@@ -27,8 +27,8 @@ import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
 import org.apache.syncope.core.persistence.api.entity.EntityFactory;
 import org.apache.syncope.core.persistence.api.entity.FIQLQuery;
+import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor;
 import org.apache.syncope.core.persistence.api.search.SearchCondConverter;
-import org.apache.syncope.core.persistence.api.search.SearchCondVisitor;
 import org.apache.syncope.core.provisioning.api.data.FIQLQueryDataBinder;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
 import org.slf4j.Logger;
@@ -38,14 +38,14 @@ public class FIQLQueryDataBinderImpl implements FIQLQueryDataBinder {
 
     protected static final Logger LOG = LoggerFactory.getLogger(FIQLQueryDataBinder.class);
 
-    protected final SearchCondVisitor searchCondVisitor;
+    protected final AnySearchCondVisitor searchCondVisitor;
 
     protected final UserDAO userDAO;
 
     protected final EntityFactory entityFactory;
 
     public FIQLQueryDataBinderImpl(
-            final SearchCondVisitor searchCondVisitor,
+            final AnySearchCondVisitor searchCondVisitor,
             final UserDAO userDAO,
             final EntityFactory entityFactory) {
 
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GroupDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GroupDataBinderImpl.java
index a13e93d8d0..86fb42b166 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GroupDataBinderImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GroupDataBinderImpl.java
@@ -54,7 +54,7 @@ import org.apache.syncope.core.persistence.api.entity.Groupable;
 import org.apache.syncope.core.persistence.api.entity.Realm;
 import org.apache.syncope.core.persistence.api.entity.group.Group;
 import org.apache.syncope.core.persistence.api.entity.group.GroupTypeExtension;
-import org.apache.syncope.core.persistence.api.search.SearchCondVisitor;
+import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor;
 import org.apache.syncope.core.provisioning.api.DerAttrHandler;
 import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
 import org.apache.syncope.core.provisioning.api.MappingManager;
@@ -68,7 +68,7 @@ import org.springframework.transaction.annotation.Transactional;
 @Transactional(rollbackFor = { Throwable.class })
 public class GroupDataBinderImpl extends AnyDataBinder implements GroupDataBinder {
 
-    protected final SearchCondVisitor searchCondVisitor;
+    protected final AnySearchCondVisitor searchCondVisitor;
 
     public GroupDataBinderImpl(
             final AnyTypeDAO anyTypeDAO,
@@ -87,7 +87,7 @@ public class GroupDataBinderImpl extends AnyDataBinder implements GroupDataBinde
             final MappingManager mappingManager,
             final IntAttrNameParser intAttrNameParser,
             final OutboundMatcher outboundMatcher,
-            final SearchCondVisitor searchCondVisitor,
+            final AnySearchCondVisitor searchCondVisitor,
             final PlainAttrValidationManager validator,
             final JexlTools jexlTools) {
 
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/RoleDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/RoleDataBinderImpl.java
index 42eacc4f52..2ba900ef10 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/RoleDataBinderImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/RoleDataBinderImpl.java
@@ -24,7 +24,6 @@ import org.apache.syncope.core.persistence.api.dao.RoleDAO;
 import org.apache.syncope.core.persistence.api.entity.EntityFactory;
 import org.apache.syncope.core.persistence.api.entity.Realm;
 import org.apache.syncope.core.persistence.api.entity.Role;
-import org.apache.syncope.core.persistence.api.search.SearchCondVisitor;
 import org.apache.syncope.core.provisioning.api.data.RoleDataBinder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -39,18 +38,14 @@ public class RoleDataBinderImpl implements RoleDataBinder {
 
     protected final EntityFactory entityFactory;
 
-    protected final SearchCondVisitor searchCondVisitor;
-
     public RoleDataBinderImpl(
             final RealmSearchDAO realmSearchDAO,
             final RoleDAO roleDAO,
-            final EntityFactory entityFactory,
-            final SearchCondVisitor searchCondVisitor) {
+            final EntityFactory entityFactory) {
 
         this.realmSearchDAO = realmSearchDAO;
         this.roleDAO = roleDAO;
         this.entityFactory = entityFactory;
-        this.searchCondVisitor = searchCondVisitor;
     }
 
     @Override
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/DefaultNotificationManager.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/DefaultNotificationManager.java
index cbf6229eb2..8f50156872 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/DefaultNotificationManager.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/DefaultNotificationManager.java
@@ -64,8 +64,8 @@ import org.apache.syncope.core.persistence.api.entity.task.TaskExec;
 import org.apache.syncope.core.persistence.api.entity.user.UMembership;
 import org.apache.syncope.core.persistence.api.entity.user.URelationship;
 import org.apache.syncope.core.persistence.api.entity.user.User;
+import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor;
 import org.apache.syncope.core.persistence.api.search.SearchCondConverter;
-import org.apache.syncope.core.persistence.api.search.SearchCondVisitor;
 import org.apache.syncope.core.provisioning.api.DerAttrHandler;
 import org.apache.syncope.core.provisioning.api.IntAttrName;
 import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
@@ -119,7 +119,7 @@ public class DefaultNotificationManager implements NotificationManager {
 
     protected final IntAttrNameParser intAttrNameParser;
 
-    protected final SearchCondVisitor searchCondVisitor;
+    protected final AnySearchCondVisitor searchCondVisitor;
 
     protected final JexlTools jexlTools;
 
@@ -142,7 +142,7 @@ public class DefaultNotificationManager implements NotificationManager {
             final ConfParamOps confParamOps,
             final EntityFactory entityFactory,
             final IntAttrNameParser intAttrNameParser,
-            final SearchCondVisitor searchCondVisitor,
+            final AnySearchCondVisitor searchCondVisitor,
             final JexlTools jexlTools) {
 
         this.derSchemaDAO = derSchemaDAO;
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/InboundMatcher.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/InboundMatcher.java
index c317631790..a5f6930f25 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/InboundMatcher.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/InboundMatcher.java
@@ -23,6 +23,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.stream.Collectors;
 import org.apache.commons.lang3.StringUtils;
@@ -143,11 +144,11 @@ public class InboundMatcher {
         // first, attempt to match the provided connObjectLinkValue against the configured connObjectLink
         // (if available) via internal search
         if (StringUtils.isNotBlank(provision.getMapping().getConnObjectLink())) {
-            List<? extends Any> found = anyUtilsFactory.getInstance(anyType.getKind()).dao().
-                    findByDerAttrValue(
-                            provision.getMapping().getConnObjectLink(),
-                            connObjectLinkValue,
-                            provision.isIgnoreCaseMatch());
+            List<? extends Any> found = anySearchDAO.findByDerAttrValue(
+                    provision.getMapping().getConnObjectLink(),
+                    connObjectLinkValue,
+                    provision.isIgnoreCaseMatch(),
+                    anyType.getKind());
             if (!found.isEmpty()) {
                 return Optional.of(new InboundMatch(MatchType.ANY, found.getFirst()));
             }
@@ -308,10 +309,11 @@ public class InboundMatcher {
                 }
 
                 case DERIVED ->
-                    anys.addAll(anyUtils.dao().findByDerAttrValue(
+                    anys.addAll(anySearchDAO.findByDerAttrValue(
                             ((DerSchema) intAttrName.getSchemaInfo().schema()).getExpression(),
                             finalConnObjectKeyValue,
-                            ignoreCaseMatch));
+                            ignoreCaseMatch,
+                            anyTypeKind));
 
                 default -> {
                 }
@@ -439,10 +441,9 @@ public class InboundMatcher {
     public List<Realm> match(final LiveSyncDelta syncDelta, final OrgUnit orgUnit) {
         String connObjectKey = null;
 
-        Optional<Item> connObjectKeyItem = orgUnit.getConnObjectKeyItem();
-        if (connObjectKeyItem.isPresent()) {
-            Attribute connObjectKeyAttr = syncDelta.getObject().
-                    getAttributeByName(connObjectKeyItem.get().getExtAttrName());
+        Item connObjectKeyItem = orgUnit.getConnObjectKeyItem().orElse(null);
+        if (connObjectKeyItem != null) {
+            Attribute connObjectKeyAttr = syncDelta.getObject().getAttributeByName(connObjectKeyItem.getExtAttrName());
             if (connObjectKeyAttr != null) {
                 connObjectKey = AttributeUtil.getStringValue(connObjectKeyAttr);
             }
@@ -452,10 +453,10 @@ public class InboundMatcher {
         }
 
         for (ItemTransformer transformer
-                : MappingUtils.getItemTransformers(connObjectKeyItem.get(), getTransformers(connObjectKeyItem.get()))) {
+                : MappingUtils.getItemTransformers(connObjectKeyItem, getTransformers(connObjectKeyItem))) {
 
             List<Object> output = transformer.beforePull(
-                    connObjectKeyItem.get(),
+                    connObjectKeyItem,
                     null,
                     List.of(connObjectKey));
             if (!CollectionUtils.isEmpty(output)) {
@@ -463,27 +464,58 @@ public class InboundMatcher {
             }
         }
 
+        IntAttrName intAttrName;
+        try {
+            intAttrName = intAttrNameParser.parse(connObjectKeyItem.getIntAttrName());
+        } catch (ParseException e) {
+            LOG.error("Invalid intAttrName '{}' specified, ignoring", connObjectKeyItem.getIntAttrName(), e);
+            return List.of();
+        }
+
         List<Realm> result = new ArrayList<>();
 
-        switch (connObjectKeyItem.get().getIntAttrName()) {
-            case "key" -> {
-                realmDAO.findById(connObjectKey).ifPresent(result::add);
-            }
+        if (intAttrName.getField() != null) {
+            switch (intAttrName.getField()) {
+                case "key" ->
+                    realmDAO.findById(connObjectKey).ifPresent(result::add);
 
-            case "name" -> {
-                if (orgUnit.isIgnoreCaseMatch()) {
-                    result.addAll(realmSearchDAO.findDescendants(
-                            SyncopeConstants.ROOT_REALM, connObjectKey, Pageable.unpaged()));
-                } else {
-                    result.addAll(realmSearchDAO.findByName(connObjectKey).stream().toList());
+                case "name" -> {
+                    if (orgUnit.isIgnoreCaseMatch()) {
+                        AnyCond cond = new AnyCond();
+                        cond.setType(AttrCond.Type.IEQ);
+                        cond.setSchema("name");
+                        cond.setExpression(connObjectKey);
+                        result.addAll(realmSearchDAO.search(
+                                Set.of(SyncopeConstants.ROOT_REALM), SearchCond.of(cond), Pageable.unpaged()));
+                    } else {
+                        result.addAll(realmSearchDAO.findByName(connObjectKey).stream().toList());
+                    }
                 }
-            }
 
-            case "fullpath" -> {
-                realmSearchDAO.findByFullPath(connObjectKey).ifPresent(result::add);
+                case "fullpath" ->
+                    realmSearchDAO.findByFullPath(connObjectKey).ifPresent(result::add);
+
+                default -> {
+                }
             }
+        } else if (intAttrName.getSchemaInfo() != null) {
+            switch (intAttrName.getSchemaInfo().type()) {
+                case PLAIN -> {
+                    AttrCond cond = new AttrCond(orgUnit.isIgnoreCaseMatch() ? AttrCond.Type.IEQ : AttrCond.Type.EQ);
+                    cond.setSchema(intAttrName.getSchemaInfo().schema().getKey());
+                    cond.setExpression(connObjectKey);
+                    result.addAll(realmSearchDAO.search(
+                            Set.of(SyncopeConstants.ROOT_REALM), SearchCond.of(cond), Pageable.unpaged()));
+                }
+
+                case DERIVED ->
+                    result.addAll(realmSearchDAO.findByDerAttrValue(
+                            ((DerSchema) intAttrName.getSchemaInfo().schema()).getExpression(),
+                            connObjectKey,
+                            orgUnit.isIgnoreCaseMatch()));
 
-            default -> {
+                default -> {
+                }
             }
         }
 
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PushJobDelegate.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PushJobDelegate.java
index a5548eadb4..fd2613e37a 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PushJobDelegate.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PushJobDelegate.java
@@ -42,8 +42,8 @@ import org.apache.syncope.core.persistence.api.entity.Implementation;
 import org.apache.syncope.core.persistence.api.entity.Realm;
 import org.apache.syncope.core.persistence.api.entity.policy.PushPolicy;
 import org.apache.syncope.core.persistence.api.entity.task.PushTask;
+import org.apache.syncope.core.persistence.api.search.AnySearchCondVisitor;
 import org.apache.syncope.core.persistence.api.search.SearchCondConverter;
-import org.apache.syncope.core.persistence.api.search.SearchCondVisitor;
 import org.apache.syncope.core.provisioning.api.ProvisionSorter;
 import org.apache.syncope.core.provisioning.api.job.JobExecutionContext;
 import org.apache.syncope.core.provisioning.api.job.JobExecutionException;
@@ -59,7 +59,6 @@ import org.apache.syncope.core.spring.implementation.ImplementationManager;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.ConfigurableApplicationContext;
 import org.springframework.data.domain.PageRequest;
-import org.springframework.data.domain.Pageable;
 
 public class PushJobDelegate
         extends AbstractProvisioningJobDelegate<PushTask>
@@ -72,7 +71,7 @@ public class PushJobDelegate
     protected RealmSearchDAO realmSearchDAO;
 
     @Autowired
-    protected SearchCondVisitor searchCondVisitor;
+    protected AnySearchCondVisitor searchCondVisitor;
 
     @Autowired
     protected ConfigurableApplicationContext ctx;
@@ -212,7 +211,7 @@ public class PushJobDelegate
 
             // Never push the root realm
             List<Realm> realms = realmSearchDAO.findDescendants(
-                    profile.getTask().getSourceRealm().getFullPath(), null, Pageable.unpaged()).stream().
+                    profile.getTask().getSourceRealm().getFullPath(), null).stream().
                     filter(realm -> realm.getParent() != null).toList();
             boolean result = true;
             for (int i = 0; i < realms.size() && result; i++) {
@@ -238,8 +237,6 @@ public class PushJobDelegate
             AnyType anyType = anyTypeDAO.findById(provision.getAnyType()).
                     orElseThrow(() -> new NotFoundException("AnyType" + provision.getAnyType()));
 
-            AnyDAO<?> anyDAO = anyUtilsFactory.getInstance(anyType.getKind()).dao();
-
             dispatcher.addHandlerSupplier(provision.getAnyType(), () -> {
                 SyncopePushResultHandler handler;
                 switch (anyType.getKind()) {
@@ -261,7 +258,7 @@ public class PushJobDelegate
 
             String filter = task.getFilter(anyType.getKey()).orElse(null);
             SearchCond cond = StringUtils.isBlank(filter)
-                    ? anyDAO.getAllMatchingCond()
+                    ? searchDAO.getAllMatchingCond()
                     : SearchCondConverter.convert(searchCondVisitor, filter);
             long count = searchDAO.count(
                     profile.getTask().getSourceRealm(),
diff --git a/core/self-keymaster-starter/src/main/java/org/apache/syncope/core/starter/SelfKeymasterContext.java b/core/self-keymaster-starter/src/main/java/org/apache/syncope/core/starter/SelfKeymasterContext.java
index 2aa43a642e..fed1700b25 100644
--- a/core/self-keymaster-starter/src/main/java/org/apache/syncope/core/starter/SelfKeymasterContext.java
+++ b/core/self-keymaster-starter/src/main/java/org/apache/syncope/core/starter/SelfKeymasterContext.java
@@ -180,7 +180,7 @@ public class SelfKeymasterContext {
 
                     ExternalDocumentation extDoc = new ExternalDocumentation();
                     extDoc.setDescription("Apache Syncope Reference Guide");
-                    extDoc.setUrl("https://syncope.apache.org/docs/3.0/reference-guide.html#domains");
+                    extDoc.setUrl("https://syncope.apache.org/docs/4.0/reference-guide.html#domains");
 
                     Schema<String> schema = new Schema<>();
                     schema.setDescription("Domains are built to facilitate multitenancy.");
diff --git a/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchUtils.java b/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchUtils.java
index 2018ac04b6..146abe8f0f 100644
--- a/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchUtils.java
+++ b/ext/elasticsearch/client-elasticsearch/src/main/java/org/apache/syncope/ext/elasticsearch/client/ElasticsearchUtils.java
@@ -90,6 +90,17 @@ public class ElasticsearchUtils {
         builder.put("relationshipTypes", relationshipTypes);
     }
 
+    protected void addPlainAttr(final Map<String, Object> builder, final List<PlainAttr> plainAttrs) {
+        for (PlainAttr plainAttr : plainAttrs) {
+            List<Object> values = plainAttr.getValues().stream().
+                    map(PlainAttrValue::getValue).collect(Collectors.toList());
+
+            Optional.ofNullable(plainAttr.getUniqueValue()).ifPresent(v -> values.add(v.getValue()));
+
+            builder.put(plainAttr.getSchema(), values.size() == 1 ? values.getFirst() : values);
+        }
+    }
+
     /**
      * Returns the document specialized with content from the provided any.
      *
@@ -165,14 +176,7 @@ public class ElasticsearchUtils {
             }
         }
 
-        for (PlainAttr plainAttr : any.getPlainAttrs()) {
-            List<Object> values = plainAttr.getValues().stream().
-                    map(PlainAttrValue::getValue).collect(Collectors.toList());
-
-            Optional.ofNullable(plainAttr.getUniqueValue()).ifPresent(v -> values.add(v.getValue()));
-
-            builder.put(plainAttr.getSchema(), values.size() == 1 ? values.getFirst() : values);
-        }
+        addPlainAttr(builder, any.getPlainAttrs());
 
         // add also flattened membership attributes
         if (any instanceof Groupable<?, ?, ?> groupable) {
@@ -210,6 +214,7 @@ public class ElasticsearchUtils {
         builder.put("name", realm.getName());
         builder.put("parent_id", realm.getParent() == null ? null : realm.getParent().getKey());
         builder.put("fullPath", realm.getFullPath());
+        addPlainAttr(builder, realm.getPlainAttrs());
 
         customizeDocument(builder, realm);
 
@@ -235,8 +240,6 @@ public class ElasticsearchUtils {
         return builder;
     }
 
-    protected void customizeDocument(
-            final Map<String, Object> builder,
-            final AuditEvent auditEvent) {
+    protected void customizeDocument(final Map<String, Object> builder, final AuditEvent auditEvent) {
     }
 }
diff --git a/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/ElasticsearchPersistenceContext.java b/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/ElasticsearchPersistenceContext.java
index cd95e10699..24aa529631 100644
--- a/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/ElasticsearchPersistenceContext.java
+++ b/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/ElasticsearchPersistenceContext.java
@@ -74,10 +74,23 @@ public class ElasticsearchPersistenceContext {
     @Bean
     public RealmSearchDAO realmSearchDAO(
             final @Lazy RealmDAO realmDAO,
+            final @Lazy PlainSchemaDAO plainSchemaDAO,
+            final @Lazy UserDAO userDAO,
+            final @Lazy GroupDAO groupDAO,
+            final EntityFactory entityFactory,
+            final PlainAttrValidationManager validator,
             final ElasticsearchClient client,
             final ElasticsearchProperties props) {
 
-        return new ElasticsearchRealmSearchDAO(realmDAO, client, props.getIndexMaxResultWindow());
+        return new ElasticsearchRealmSearchDAO(
+                realmDAO,
+                plainSchemaDAO,
+                userDAO,
+                groupDAO,
+                entityFactory,
+                validator,
+                client,
+                props.getIndexMaxResultWindow());
     }
 
     @ConditionalOnMissingBean(name = "elasticsearchAuditEventDAO")
diff --git a/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchAnySearchDAO.java b/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchAnySearchDAO.java
index 2e3ba71b3f..986176cc8b 100644
--- a/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchAnySearchDAO.java
+++ b/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchAnySearchDAO.java
@@ -143,7 +143,7 @@ public class ElasticsearchAnySearchDAO extends AbstractAnySearchDAO {
                         });
 
                         realmSearchDAO.findDescendants(realm.getFullPath(), base.getFullPath()).
-                                forEach(descendant -> queries.add(
+                                stream().map(Realm::getKey).forEach(descendant -> queries.add(
                                 new Query.Builder().term(QueryBuilders.term().
                                         field("realm").value(descendant).caseInsensitive(false).build()).
                                         build()));
@@ -570,8 +570,8 @@ public class ElasticsearchAnySearchDAO extends AbstractAnySearchDAO {
     }
 
     @Override
-    protected CheckResult<AnyCond> check(final AnyCond cond, final AnyTypeKind kind) {
-        CheckResult<AnyCond> checked = super.check(cond, kind);
+    protected CheckResult<AnyCond> check(final AnyCond cond, final Field field, final Set<String> relationshipsFields) {
+        CheckResult<AnyCond> checked = super.check(cond, field, relationshipsFields);
 
         // Manage difference between external id attribute and internal _id
         if ("id".equals(checked.cond().getSchema())) {
@@ -591,7 +591,11 @@ public class ElasticsearchAnySearchDAO extends AbstractAnySearchDAO {
             cond.setExpression(realm.getKey());
         }
 
-        CheckResult<AnyCond> checked = check(cond, kind);
+        CheckResult<AnyCond> checked = check(
+                cond,
+                anyUtilsFactory.getInstance(kind).getField(cond.getSchema()).
+                        orElseThrow(() -> new IllegalArgumentException("Invalid schema " + cond.getSchema())),
+                RELATIONSHIP_FIELDS);
 
         return fillAttrQuery(checked.schema(), checked.value(), checked.cond());
     }
diff --git a/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchRealmSearchDAO.java b/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchRealmSearchDAO.java
index df85297a61..395a475646 100644
--- a/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchRealmSearchDAO.java
+++ b/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchRealmSearchDAO.java
@@ -19,40 +19,65 @@
 package org.apache.syncope.core.persistence.elasticsearch.dao;
 
 import co.elastic.clients.elasticsearch.ElasticsearchClient;
+import co.elastic.clients.elasticsearch._types.FieldSort;
+import co.elastic.clients.elasticsearch._types.FieldValue;
 import co.elastic.clients.elasticsearch._types.ScriptLanguage;
 import co.elastic.clients.elasticsearch._types.ScriptSortType;
 import co.elastic.clients.elasticsearch._types.ScriptSource;
 import co.elastic.clients.elasticsearch._types.SearchType;
 import co.elastic.clients.elasticsearch._types.SortOptions;
 import co.elastic.clients.elasticsearch._types.SortOrder;
+import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
 import co.elastic.clients.elasticsearch._types.query_dsl.Query;
 import co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders;
+import co.elastic.clients.elasticsearch._types.query_dsl.RangeQuery;
 import co.elastic.clients.elasticsearch.core.CountRequest;
 import co.elastic.clients.elasticsearch.core.SearchRequest;
 import co.elastic.clients.elasticsearch.core.search.Hit;
 import co.elastic.clients.elasticsearch.core.search.SourceConfig;
+import co.elastic.clients.json.JsonData;
+import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
+import java.util.stream.Stream;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.types.AttrSchemaType;
+import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
+import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.MalformedPathException;
+import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO;
+import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
+import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
+import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
+import org.apache.syncope.core.persistence.api.entity.PlainSchema;
 import org.apache.syncope.core.persistence.api.entity.Realm;
+import org.apache.syncope.core.persistence.api.utils.FormatUtils;
+import org.apache.syncope.core.persistence.api.utils.RealmUtils;
+import org.apache.syncope.core.persistence.common.dao.AbstractRealmSearchDAO;
+import org.apache.syncope.core.persistence.common.dao.AbstractSearchDAO.CheckResult;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
 import org.apache.syncope.ext.elasticsearch.client.ElasticsearchUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
 import org.springframework.transaction.annotation.Transactional;
 
-public class ElasticsearchRealmSearchDAO implements RealmSearchDAO {
+public class ElasticsearchRealmSearchDAO extends AbstractRealmSearchDAO implements RealmSearchDAO {
 
-    protected static final Logger LOG = LoggerFactory.getLogger(RealmDAO.class);
+    protected static final Logger LOG = LoggerFactory.getLogger(RealmSearchDAO.class);
 
-    protected static final List<SortOptions> REALM_SORT_OPTIONS = List.of(
+    protected static final Set<String> ID_PROPS = Set.of("key", "id", "_id");
+
+    protected static final List<SortOptions> FULLPATH_SORT_OPTIONS = List.of(
             new SortOptions.Builder().
                     script(s -> s.type(ScriptSortType.Number).
                     script(t -> t.lang(ScriptLanguage.Painless).
@@ -63,16 +88,25 @@ public class ElasticsearchRealmSearchDAO implements RealmSearchDAO {
 
     protected final RealmDAO realmDAO;
 
+    protected final RealmUtils realmUtils;
+
     protected final ElasticsearchClient client;
 
     protected final int indexMaxResultWindow;
 
     public ElasticsearchRealmSearchDAO(
             final RealmDAO realmDAO,
+            final PlainSchemaDAO plainSchemaDAO,
+            final UserDAO userDAO,
+            final GroupDAO groupDAO,
+            final EntityFactory entityFactory,
+            final PlainAttrValidationManager validator,
             final ElasticsearchClient client,
             final int indexMaxResultWindow) {
 
+        super(plainSchemaDAO, userDAO, groupDAO, entityFactory, validator);
         this.realmDAO = realmDAO;
+        this.realmUtils = new RealmUtils(entityFactory);
         this.client = client;
         this.indexMaxResultWindow = indexMaxResultWindow;
     }
@@ -113,7 +147,7 @@ public class ElasticsearchRealmSearchDAO implements RealmSearchDAO {
                 index(ElasticsearchUtils.getRealmIndex(AuthContextUtils.getDomain())).
                 searchType(SearchType.QueryThenFetch).
                 query(query).
-                sort(REALM_SORT_OPTIONS).
+                sort(FULLPATH_SORT_OPTIONS).
                 fields(List.of()).source(new SourceConfig.Builder().fetch(false).build()).
                 build();
         LOG.debug("Search request: {}", request);
@@ -146,7 +180,7 @@ public class ElasticsearchRealmSearchDAO implements RealmSearchDAO {
                 flatMap(Optional::stream).map(Realm.class::cast).toList();
     }
 
-    protected Query buildDescendantsQuery(final Set<String> bases, final String keyword) {
+    protected Query buildDescendantsQuery(final Set<String> bases, final SearchCond searchCond) {
         List<Query> basesQueries = new ArrayList<>();
         bases.forEach(base -> {
             basesQueries.add(new Query.Builder().term(QueryBuilders.term().
@@ -157,28 +191,199 @@ public class ElasticsearchRealmSearchDAO implements RealmSearchDAO {
         });
         Query prefix = new Query.Builder().disMax(QueryBuilders.disMax().queries(basesQueries).build()).build();
 
-        if (keyword == null) {
-            return prefix;
+        BoolQuery.Builder boolBuilder = QueryBuilders.bool().filter(prefix);
+        if (searchCond != null) {
+            boolBuilder.filter(getQuery(searchCond));
         }
+        return new Query.Builder().bool(boolBuilder.build()).build();
+    }
 
-        return new Query.Builder().bool(QueryBuilders.bool().filter(
-                prefix,
-                new Query.Builder().wildcard(QueryBuilders.wildcard().
-                        field("name").value(keyword.replace('%', '*').replace("\\_", "_")).
-                        caseInsensitive(true).build()).build()).build()).
-                build();
+    protected Query getQuery(final SearchCond cond) {
+        Query query = null;
+
+        switch (cond.getType()) {
+            case LEAF, NOT_LEAF -> {
+                query = cond.asLeaf(AnyCond.class).map(this::getQuery).orElse(null);
+                if (query == null) {
+                    query = cond.asLeaf(AttrCond.class).map(this::getQuery).orElse(null);
+                }
+                if (query == null) {
+                    query = getQueryForCustomConds(cond);
+                }
+                if (query == null) {
+                    throw new IllegalArgumentException("Cannot construct QueryBuilder");
+                }
+                if (cond.getType() == SearchCond.Type.NOT_LEAF) {
+                    query = new Query.Builder().bool(QueryBuilders.bool().mustNot(query).build()).build();
+                }
+            }
+            case AND -> {
+                List<Query> andCompound = new ArrayList<>();
+                Query andLeft = getQuery(cond.getLeft());
+                if (andLeft.isBool() && !andLeft.bool().filter().isEmpty()) {
+                    andCompound.addAll(andLeft.bool().filter());
+                } else {
+                    andCompound.add(andLeft);
+                }
+                Query andRight = getQuery(cond.getRight());
+                if (andRight.isBool() && !andRight.bool().filter().isEmpty()) {
+                    andCompound.addAll(andRight.bool().filter());
+                } else {
+                    andCompound.add(andRight);
+                }
+                query = new Query.Builder().bool(QueryBuilders.bool().filter(andCompound).build()).build();
+            }
+            case OR -> {
+                List<Query> orCompound = new ArrayList<>();
+                Query orLeft = getQuery(cond.getLeft());
+                if (orLeft.isDisMax()) {
+                    orCompound.addAll(orLeft.disMax().queries());
+                } else {
+                    orCompound.add(orLeft);
+                }
+                Query orRight = getQuery(cond.getRight());
+                if (orRight.isDisMax()) {
+                    orCompound.addAll(orRight.disMax().queries());
+                } else {
+                    orCompound.add(orRight);
+                }
+                query = new Query.Builder().disMax(QueryBuilders.disMax().queries(orCompound).build()).build();
+            }
+            default -> {
+            }
+        }
+
+        return query;
     }
 
     @Override
-    public long countDescendants(final String base, final String keyword) {
-        return countDescendants(Set.of(base), keyword);
+    protected CheckResult<AnyCond> check(final AnyCond cond, final Field field, final Set<String> relationshipsFields) {
+        CheckResult<AnyCond> checked = super.check(cond, field, relationshipsFields);
+
+        // Manage difference between external id attribute and internal _id
+        if ("id".equals(checked.cond().getSchema())) {
+            checked.cond().setSchema("_id");
+        }
+        if ("id".equals(checked.schema().getKey())) {
+            checked.schema().setKey("_id");
+        }
+
+        return checked;
+    }
+
+    protected Query fillAttrQuery(
+            final PlainSchema schema,
+            final PlainAttrValue attrValue,
+            final AttrCond cond) {
+
+        Object value = schema.getType() == AttrSchemaType.Date && attrValue.getDateValue() != null
+                ? FormatUtils.format(attrValue.getDateValue())
+                : attrValue.getValue();
+
+        Query query = null;
+        switch (cond.getType()) {
+            case ISNOTNULL:
+                query = new Query.Builder().exists(QueryBuilders.exists().field(schema.getKey()).build()).build();
+                break;
+
+            case ISNULL:
+                query = new Query.Builder().bool(QueryBuilders.bool().mustNot(
+                        new Query.Builder().exists(QueryBuilders.exists().field(schema.getKey()).build())
+                                .build()).build()).build();
+                break;
+
+            case ILIKE:
+                query = new Query.Builder().wildcard(QueryBuilders.wildcard().
+                        field(schema.getKey()).value(cond.getExpression().replace('%', '*').replace("\\_", "_")).
+                        caseInsensitive(true).build()).build();
+                break;
+
+            case LIKE:
+                query = new Query.Builder().wildcard(QueryBuilders.wildcard().
+                        field(schema.getKey()).value(cond.getExpression().replace('%', '*').replace("\\_", "_")).
+                        caseInsensitive(false).build()).build();
+                break;
+
+            case IEQ:
+                query = new Query.Builder().term(QueryBuilders.term().
+                        field(schema.getKey()).value(FieldValue.of(cond.getExpression())).caseInsensitive(true).
+                        build()).build();
+                break;
+
+            case EQ:
+                FieldValue fieldValue = switch (value) {
+                    case Double aDouble ->
+                        FieldValue.of(aDouble);
+                    case Long aLong ->
+                        FieldValue.of(aLong);
+                    case Boolean aBoolean ->
+                        FieldValue.of(aBoolean);
+                    default ->
+                        FieldValue.of(value.toString());
+                };
+                query = new Query.Builder().term(QueryBuilders.term().
+                        field(schema.getKey()).value(fieldValue).caseInsensitive(false).build()).
+                        build();
+                break;
+
+            case GE:
+                query = new Query.Builder().range(RangeQuery.of(r -> r.untyped(n -> n.
+                        field(schema.getKey()).
+                        gte(JsonData.of(value))))).
+                        build();
+                break;
+
+            case GT:
+                query = new Query.Builder().range(RangeQuery.of(r -> r.untyped(n -> n.
+                        field(schema.getKey()).
+                        gt(JsonData.of(value))))).
+                        build();
+                break;
+
+            case LE:
+                query = new Query.Builder().range(RangeQuery.of(r -> r.untyped(n -> n.
+                        field(schema.getKey()).
+                        lte(JsonData.of(value))))).
+                        build();
+                break;
+
+            case LT:
+                query = new Query.Builder().range(RangeQuery.of(r -> r.untyped(n -> n.
+                        field(schema.getKey()).
+                        lt(JsonData.of(value))))).
+                        build();
+                break;
+
+            default:
+                break;
+        }
+
+        return query;
+    }
+
+    protected Query getQuery(final AttrCond cond) {
+        CheckResult<AttrCond> checked = check(cond);
+        return fillAttrQuery(checked.schema(), checked.value(), cond);
+    }
+
+    protected Query getQuery(final AnyCond cond) {
+        CheckResult<AnyCond> checked = check(
+                cond,
+                realmUtils.getField(cond.getSchema()).
+                        orElseThrow(() -> new IllegalArgumentException("Invalid schema " + cond.getSchema())),
+                RELATIONSHIP_FIELDS);
+        return fillAttrQuery(checked.schema(), checked.value(), checked.cond());
+    }
+
+    protected Query getQueryForCustomConds(final SearchCond cond) {
+        return null;
     }
 
     @Override
-    public long countDescendants(final Set<String> bases, final String keyword) {
+    protected long doCount(final Set<String> bases, final SearchCond searchCond) {
         CountRequest request = new CountRequest.Builder().
                 index(ElasticsearchUtils.getRealmIndex(AuthContextUtils.getDomain())).
-                query(buildDescendantsQuery(bases, keyword)).
+                query(buildDescendantsQuery(bases, searchCond)).
                 build();
         LOG.debug("Count request: {}", request);
 
@@ -190,20 +395,52 @@ public class ElasticsearchRealmSearchDAO implements RealmSearchDAO {
         }
     }
 
-    @Override
-    public List<Realm> findDescendants(final String base, final String keyword, final Pageable pageable) {
-        return findDescendants(Set.of(base), keyword, pageable);
+    protected List<SortOptions> sortBuilders(final Stream<Sort.Order> orderBy) {
+        List<SortOptions> options = new ArrayList<>();
+        orderBy.forEach(clause -> {
+            String sortName = null;
+
+            String fieldName = clause.getProperty();
+            // Cannot sort by internal _id
+            if (!ID_PROPS.contains(fieldName)) {
+                Field anyField = realmUtils.getField(fieldName).orElse(null);
+                if (anyField == null) {
+                    PlainSchema schema = plainSchemaDAO.findById(fieldName).orElse(null);
+                    if (schema != null) {
+                        sortName = fieldName;
+                    }
+                } else {
+                    sortName = fieldName;
+                }
+            }
+
+            if (sortName == null) {
+                LOG.warn("Cannot build any valid clause from {}", clause);
+            } else {
+                if ("fullPath".equals(sortName)) {
+                    options.addAll(FULLPATH_SORT_OPTIONS);
+                } else {
+                    options.add(new SortOptions.Builder().field(
+                            new FieldSort.Builder().
+                                    field(sortName).
+                                    order(clause.getDirection() == Sort.Direction.ASC ? SortOrder.Asc : SortOrder.Desc).
+                                    build()).
+                            build());
+                }
+            }
+        });
+        return options.isEmpty() ? FULLPATH_SORT_OPTIONS : options;
     }
 
     @Override
-    public List<Realm> findDescendants(final Set<String> bases, final String keyword, final Pageable pageable) {
+    protected List<Realm> doSearch(final Set<String> bases, final SearchCond searchCond, final Pageable pageable) {
         SearchRequest request = new SearchRequest.Builder().
                 index(ElasticsearchUtils.getRealmIndex(AuthContextUtils.getDomain())).
                 searchType(SearchType.QueryThenFetch).
-                query(buildDescendantsQuery(bases, keyword)).
+                query(buildDescendantsQuery(bases, searchCond)).
                 from(pageable.isUnpaged() ? 0 : pageable.getPageSize() * pageable.getPageNumber()).
                 size(pageable.isUnpaged() ? indexMaxResultWindow : pageable.getPageSize()).
-                sort(REALM_SORT_OPTIONS).
+                sort(sortBuilders(pageable.getSort().get())).
                 fields(List.of()).source(new SourceConfig.Builder().fetch(false).build()).
                 build();
         LOG.debug("Search request: {}", request);
@@ -222,17 +459,22 @@ public class ElasticsearchRealmSearchDAO implements RealmSearchDAO {
     }
 
     @Override
-    public List<String> findDescendants(final String base, final String prefix) {
-        Query prefixQuery = new Query.Builder().disMax(QueryBuilders.disMax().queries(
-                new Query.Builder().term(QueryBuilders.term().
-                        field("fullPath").value(prefix).caseInsensitive(false).build()).build(),
-                new Query.Builder().prefix(QueryBuilders.prefix().
-                        field("fullPath").value(SyncopeConstants.ROOT_REALM.equals(prefix) ? "/" : prefix + "/").
-                        build()).build()).build()).build();
-
-        Query query = new Query.Builder().bool(QueryBuilders.bool().filter(
-                buildDescendantsQuery(Set.of(base), (String) null), prefixQuery).build()).
-                build();
+    public List<Realm> findDescendants(final String base, final String prefix) {
+        Query descendantsQuery = buildDescendantsQuery(Set.of(base), null);
+        Query query;
+        if (prefix == null) {
+            query = descendantsQuery;
+        } else {
+            Query prefixQuery = new Query.Builder().disMax(QueryBuilders.disMax().queries(
+                    new Query.Builder().term(QueryBuilders.term().
+                            field("fullPath").value(prefix).caseInsensitive(false).build()).build(),
+                    new Query.Builder().prefix(QueryBuilders.prefix().
+                            field("fullPath").value(SyncopeConstants.ROOT_REALM.equals(prefix) ? "/" : prefix + "/").
+                            build()).build()).build()).build();
+            query = new Query.Builder().bool(QueryBuilders.bool().filter(
+                    descendantsQuery, prefixQuery).build()).
+                    build();
+        }
 
         SearchRequest request = new SearchRequest.Builder().
                 index(ElasticsearchUtils.getRealmIndex(AuthContextUtils.getDomain())).
@@ -240,7 +482,7 @@ public class ElasticsearchRealmSearchDAO implements RealmSearchDAO {
                 query(query).
                 from(0).
                 size(indexMaxResultWindow).
-                sort(REALM_SORT_OPTIONS).
+                sort(FULLPATH_SORT_OPTIONS).
                 fields(List.of()).source(new SourceConfig.Builder().fetch(false).build()).
                 build();
         LOG.debug("Search request: {}", request);
@@ -253,6 +495,6 @@ public class ElasticsearchRealmSearchDAO implements RealmSearchDAO {
         } catch (Exception e) {
             LOG.error("While searching in Elasticsearch with request {}", request, e);
         }
-        return result;
+        return result.stream().map(realmDAO::findById).flatMap(Optional::stream).map(Realm.class::cast).toList();
     }
 }
diff --git a/ext/elasticsearch/persistence/src/test/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchAnySearchDAOTest.java b/ext/elasticsearch/persistence/src/test/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchAnySearchDAOTest.java
index 729346452c..8507d071f7 100644
--- a/ext/elasticsearch/persistence/src/test/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchAnySearchDAOTest.java
+++ b/ext/elasticsearch/persistence/src/test/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchAnySearchDAOTest.java
@@ -110,11 +110,12 @@ public class ElasticsearchAnySearchDAOTest {
     public void getAdminRealmsFilter4realm() throws IOException {
         // 1. mock
         Realm root = mock(Realm.class);
+        when(root.getKey()).thenReturn("rootKey");
         when(root.getFullPath()).thenReturn(SyncopeConstants.ROOT_REALM);
 
         when(realmSearchDAO.findByFullPath(SyncopeConstants.ROOT_REALM)).thenAnswer(ic -> Optional.of(root));
         when(realmSearchDAO.findDescendants(eq(SyncopeConstants.ROOT_REALM), anyString())).
-                thenReturn(List.of("rootKey"));
+                thenReturn(List.of(root));
 
         // 2. test
         Set<String> adminRealms = Set.of(SyncopeConstants.ROOT_REALM);
diff --git a/ext/elasticsearch/persistence/src/test/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchRealmSearchDAOTest.java b/ext/elasticsearch/persistence/src/test/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchRealmSearchDAOTest.java
new file mode 100644
index 0000000000..ae38c45297
--- /dev/null
+++ b/ext/elasticsearch/persistence/src/test/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchRealmSearchDAOTest.java
@@ -0,0 +1,216 @@
+/*
+ * 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.syncope.core.persistence.elasticsearch.dao;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import co.elastic.clients.elasticsearch.ElasticsearchClient;
+import co.elastic.clients.elasticsearch._types.FieldValue;
+import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
+import co.elastic.clients.elasticsearch._types.query_dsl.DisMaxQuery;
+import co.elastic.clients.elasticsearch._types.query_dsl.Query;
+import co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders;
+import java.util.Optional;
+import java.util.Set;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.types.AttrSchemaType;
+import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
+import org.apache.syncope.core.persistence.api.dao.GroupDAO;
+import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
+import org.apache.syncope.core.persistence.api.dao.RealmDAO;
+import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
+import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
+import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
+import org.apache.syncope.core.persistence.api.entity.PlainSchema;
+import org.apache.syncope.core.persistence.jpa.entity.JPAPlainSchema;
+import org.apache.syncope.core.persistence.jpa.entity.JPARealm;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class ElasticsearchRealmSearchDAOTest {
+
+    @Mock
+    private RealmDAO realmDAO;
+
+    @Mock
+    private PlainSchemaDAO plainSchemaDAO;
+
+    @Mock
+    private UserDAO userDAO;
+
+    @Mock
+    private GroupDAO groupDAO;
+
+    @Mock
+    private EntityFactory entityFactory;
+
+    @Mock
+    private PlainAttrValidationManager validator;
+
+    @Mock
+    private ElasticsearchClient client;
+
+    private ElasticsearchRealmSearchDAO searchDAO;
+
+    @BeforeEach
+    protected void setupSearchDAO() {
+        doReturn(JPARealm.class).when(entityFactory).realmClass();
+        searchDAO = new ElasticsearchRealmSearchDAO(
+                realmDAO,
+                plainSchemaDAO,
+                userDAO,
+                groupDAO,
+                entityFactory,
+                validator,
+                client,
+                10000);
+    }
+
+    @Test
+    public void query4anyCond() {
+        when(entityFactory.newEntity(PlainSchema.class)).thenReturn(new JPAPlainSchema());
+        doAnswer(ic -> {
+            PlainAttrValue value = ic.getArgument(2);
+            value.setStringValue(ic.getArgument(1));
+            return null;
+        }).when(validator).validate(any(PlainSchema.class), anyString(), any(PlainAttrValue.class));
+
+        AnyCond name = new AnyCond(AttrCond.Type.EQ);
+        name.setSchema("name");
+        name.setExpression("two");
+
+        Query query = searchDAO.getQuery(SearchCond.of(name));
+        assertThat(
+                new Query.Builder().term(QueryBuilders.term().
+                        field("name").value(FieldValue.of("two")).caseInsensitive(false).build()).build()).
+                usingRecursiveComparison().isEqualTo(query);
+        verifyNoInteractions(plainSchemaDAO);
+    }
+
+    @Test
+    public void query4attrCond() {
+        PlainSchema aLong = new JPAPlainSchema();
+        aLong.setKey("aLong");
+        aLong.setType(AttrSchemaType.Long);
+        doReturn(Optional.of(aLong)).when(plainSchemaDAO).findById("aLong");
+        doAnswer(ic -> {
+            PlainAttrValue value = ic.getArgument(2);
+            value.setLongValue(Long.valueOf(ic.getArgument(1)));
+            return null;
+        }).when(validator).validate(any(PlainSchema.class), anyString(), any(PlainAttrValue.class));
+
+        AttrCond attrEq = new AttrCond(AttrCond.Type.EQ);
+        attrEq.setSchema("aLong");
+        attrEq.setExpression("42");
+
+        Query query = searchDAO.getQuery(SearchCond.of(attrEq));
+        assertEquals(Query.Kind.Term, query._kind());
+        assertEquals("aLong", query.term().field());
+        assertEquals(Boolean.FALSE, query.term().caseInsensitive());
+        assertEquals("42", query.term().value().anyValue().toString());
+    }
+
+    @Test
+    public void query4attrCondNullChecks() {
+        PlainSchema aLong = new JPAPlainSchema();
+        aLong.setKey("aLong");
+        aLong.setType(AttrSchemaType.Long);
+        doReturn(Optional.of(aLong)).when(plainSchemaDAO).findById("aLong");
+
+        AttrCond isNull = new AttrCond(AttrCond.Type.ISNULL);
+        isNull.setSchema("aLong");
+
+        Query query = searchDAO.getQuery(SearchCond.of(isNull));
+        assertThat(
+                new Query.Builder().bool(QueryBuilders.bool().mustNot(
+                        new Query.Builder().exists(QueryBuilders.exists().field("aLong").build())
+                                .build()).build()).build()).
+                usingRecursiveComparison().isEqualTo(query);
+
+        AttrCond isNotNull = new AttrCond(AttrCond.Type.ISNOTNULL);
+        isNotNull.setSchema("aLong");
+
+        query = searchDAO.getQuery(SearchCond.of(isNotNull));
+        assertThat(
+                new Query.Builder().exists(QueryBuilders.exists().field("aLong").build()).build()).
+                usingRecursiveComparison().isEqualTo(query);
+    }
+
+    @Test
+    public void descendantsQueryWithAndOrFilters() {
+        when(entityFactory.newEntity(PlainSchema.class)).thenReturn(new JPAPlainSchema());
+        doAnswer(ic -> {
+            PlainAttrValue value = ic.getArgument(2);
+            value.setStringValue(ic.getArgument(1));
+            return null;
+        }).when(validator).validate(any(PlainSchema.class), anyString(), any(PlainAttrValue.class));
+
+        AnyCond nameTwo = new AnyCond(AttrCond.Type.EQ);
+        nameTwo.setSchema("name");
+        nameTwo.setExpression("two");
+
+        AnyCond nameOdd = new AnyCond(AttrCond.Type.EQ);
+        nameOdd.setSchema("name");
+        nameOdd.setExpression("odd");
+
+        Query query = searchDAO.buildDescendantsQuery(
+                Set.of(SyncopeConstants.ROOT_REALM),
+                SearchCond.or(SearchCond.of(nameTwo), SearchCond.of(nameOdd)));
+
+        assertEquals(Query.Kind.Bool, query._kind());
+        assertEquals(2, ((BoolQuery) query._get()).filter().size());
+        Query right = ((BoolQuery) query._get()).filter().get(1);
+        assertEquals(Query.Kind.DisMax, right._kind());
+        assertEquals(2, ((DisMaxQuery) right._get()).queries().size());
+
+        assertThat(
+                new Query.Builder().bool(QueryBuilders.bool().
+                        filter(new Query.Builder().disMax(QueryBuilders.disMax().
+                                queries(new Query.Builder().term(QueryBuilders.term().
+                                        field("fullPath").value(FieldValue.of(SyncopeConstants.ROOT_REALM)).
+                                        caseInsensitive(false).build()).build()).
+                                queries(new Query.Builder().regexp(QueryBuilders.regexp().
+                                        field("fullPath").value("/.*").build()).build()).
+                                build()).build()).
+                        filter(new Query.Builder().disMax(QueryBuilders.disMax().
+                                queries(new Query.Builder().term(QueryBuilders.term().
+                                        field("name").value(FieldValue.of("two")).caseInsensitive(false).
+                                        build()).build()).
+                                queries(new Query.Builder().term(QueryBuilders.term().
+                                        field("name").value(FieldValue.of("odd")).caseInsensitive(false).
+                                        build()).build()).
+                                build()).build()).
+                        build()).build()).
+                usingRecursiveComparison().isEqualTo(query);
+    }
+}
diff --git a/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchUtils.java b/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchUtils.java
index d951980637..22c864f3a2 100644
--- a/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchUtils.java
+++ b/ext/opensearch/client-opensearch/src/main/java/org/apache/syncope/ext/opensearch/client/OpenSearchUtils.java
@@ -90,6 +90,17 @@ public class OpenSearchUtils {
         builder.put("relationshipTypes", relationshipTypes);
     }
 
+    protected void addPlainAttr(final Map<String, Object> builder, final List<PlainAttr> plainAttrs) {
+        for (PlainAttr plainAttr : plainAttrs) {
+            List<Object> values = plainAttr.getValues().stream().
+                    map(PlainAttrValue::getValue).collect(Collectors.toList());
+
+            Optional.ofNullable(plainAttr.getUniqueValue()).ifPresent(v -> values.add(v.getValue()));
+
+            builder.put(plainAttr.getSchema(), values.size() == 1 ? values.getFirst() : values);
+        }
+    }
+
     /**
      * Returns the document specialized with content from the provided any.
      *
@@ -165,14 +176,7 @@ public class OpenSearchUtils {
             }
         }
 
-        for (PlainAttr plainAttr : any.getPlainAttrs()) {
-            List<Object> values = plainAttr.getValues().stream().
-                    map(PlainAttrValue::getValue).collect(Collectors.toList());
-
-            Optional.ofNullable(plainAttr.getUniqueValue()).ifPresent(v -> values.add(v.getValue()));
-
-            builder.put(plainAttr.getSchema(), values.size() == 1 ? values.getFirst() : values);
-        }
+        addPlainAttr(builder, any.getPlainAttrs());
 
         // add also flattened membership attributes
         if (any instanceof Groupable<?, ?, ?> groupable) {
@@ -210,6 +214,7 @@ public class OpenSearchUtils {
         builder.put("name", realm.getName());
         builder.put("parent_id", realm.getParent() == null ? null : realm.getParent().getKey());
         builder.put("fullPath", realm.getFullPath());
+        addPlainAttr(builder, realm.getPlainAttrs());
 
         customizeDocument(builder, realm);
 
@@ -235,8 +240,6 @@ public class OpenSearchUtils {
         return builder;
     }
 
-    protected void customizeDocument(
-            final Map<String, Object> builder,
-            final AuditEvent auditEvent) {
+    protected void customizeDocument(final Map<String, Object> builder, final AuditEvent auditEvent) {
     }
 }
diff --git a/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/OpenSearchPersistenceContext.java b/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/OpenSearchPersistenceContext.java
index c58bb061bf..e184bc636e 100644
--- a/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/OpenSearchPersistenceContext.java
+++ b/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/OpenSearchPersistenceContext.java
@@ -74,10 +74,23 @@ public class OpenSearchPersistenceContext {
     @Bean
     public RealmSearchDAO realmSearchDAO(
             final @Lazy RealmDAO realmDAO,
+            final @Lazy PlainSchemaDAO plainSchemaDAO,
+            final @Lazy UserDAO userDAO,
+            final @Lazy GroupDAO groupDAO,
+            final EntityFactory entityFactory,
+            final PlainAttrValidationManager validator,
             final OpenSearchClient client,
             final OpenSearchProperties props) {
 
-        return new OpenSearchRealmSearchDAO(realmDAO, client, props.getIndexMaxResultWindow());
+        return new OpenSearchRealmSearchDAO(
+                realmDAO,
+                plainSchemaDAO,
+                userDAO,
+                groupDAO,
+                entityFactory,
+                validator,
+                client,
+                props.getIndexMaxResultWindow());
     }
 
     @ConditionalOnMissingBean(name = "openSearchAuditEventDAO")
diff --git a/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchAnySearchDAO.java b/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchAnySearchDAO.java
index d4e973f36c..d27fb819f1 100644
--- a/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchAnySearchDAO.java
+++ b/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchAnySearchDAO.java
@@ -142,7 +142,7 @@ public class OpenSearchAnySearchDAO extends AbstractAnySearchDAO {
                         });
 
                         realmSearchDAO.findDescendants(realm.getFullPath(), base.getFullPath()).
-                                forEach(descendant -> queries.add(
+                                stream().map(Realm::getKey).forEach(descendant -> queries.add(
                                 new Query.Builder().term(QueryBuilders.term().
                                         field("realm").value(FieldValue.of(descendant)).caseInsensitive(false).build()).
                                         build()));
@@ -565,8 +565,8 @@ public class OpenSearchAnySearchDAO extends AbstractAnySearchDAO {
     }
 
     @Override
-    protected CheckResult<AnyCond> check(final AnyCond cond, final AnyTypeKind kind) {
-        CheckResult<AnyCond> checked = super.check(cond, kind);
+    protected CheckResult<AnyCond> check(final AnyCond cond, final Field field, final Set<String> relationshipsFields) {
+        CheckResult<AnyCond> checked = super.check(cond, field, relationshipsFields);
 
         // Manage difference between external id attribute and internal _id
         if ("id".equals(checked.cond().getSchema())) {
@@ -586,7 +586,11 @@ public class OpenSearchAnySearchDAO extends AbstractAnySearchDAO {
             cond.setExpression(realm.getKey());
         }
 
-        CheckResult<AnyCond> checked = check(cond, kind);
+        CheckResult<AnyCond> checked = check(
+                cond,
+                anyUtilsFactory.getInstance(kind).getField(cond.getSchema()).
+                        orElseThrow(() -> new IllegalArgumentException("Invalid schema " + cond.getSchema())),
+                RELATIONSHIP_FIELDS);
 
         return fillAttrQuery(checked.schema(), checked.value(), checked.cond());
     }
diff --git a/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchRealmSearchDAO.java b/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchRealmSearchDAO.java
index 1f3618d6be..6b9415d383 100644
--- a/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchRealmSearchDAO.java
+++ b/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchRealmSearchDAO.java
@@ -18,26 +18,45 @@
  */
 package org.apache.syncope.core.persistence.opensearch.dao;
 
+import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
+import java.util.stream.Stream;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.types.AttrSchemaType;
+import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
+import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.MalformedPathException;
+import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmDAO;
 import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO;
+import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
+import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
+import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
+import org.apache.syncope.core.persistence.api.entity.PlainSchema;
 import org.apache.syncope.core.persistence.api.entity.Realm;
+import org.apache.syncope.core.persistence.api.utils.FormatUtils;
+import org.apache.syncope.core.persistence.api.utils.RealmUtils;
+import org.apache.syncope.core.persistence.common.dao.AbstractRealmSearchDAO;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
 import org.apache.syncope.ext.opensearch.client.OpenSearchUtils;
+import org.opensearch.client.json.JsonData;
 import org.opensearch.client.opensearch.OpenSearchClient;
 import org.opensearch.client.opensearch._types.BuiltinScriptLanguage;
+import org.opensearch.client.opensearch._types.FieldSort;
 import org.opensearch.client.opensearch._types.FieldValue;
 import org.opensearch.client.opensearch._types.ScriptLanguage;
 import org.opensearch.client.opensearch._types.ScriptSortType;
 import org.opensearch.client.opensearch._types.SearchType;
 import org.opensearch.client.opensearch._types.SortOptions;
 import org.opensearch.client.opensearch._types.SortOrder;
+import org.opensearch.client.opensearch._types.query_dsl.BoolQuery;
 import org.opensearch.client.opensearch._types.query_dsl.Query;
 import org.opensearch.client.opensearch._types.query_dsl.QueryBuilders;
 import org.opensearch.client.opensearch.core.CountRequest;
@@ -47,13 +66,16 @@ import org.opensearch.client.opensearch.core.search.SourceConfig;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
 import org.springframework.transaction.annotation.Transactional;
 
-public class OpenSearchRealmSearchDAO implements RealmSearchDAO {
+public class OpenSearchRealmSearchDAO extends AbstractRealmSearchDAO implements RealmSearchDAO {
 
-    protected static final Logger LOG = LoggerFactory.getLogger(RealmDAO.class);
+    protected static final Logger LOG = LoggerFactory.getLogger(RealmSearchDAO.class);
 
-    protected static final List<SortOptions> REALM_SORT_OPTIONS = List.of(
+    protected static final Set<String> ID_PROPS = Set.of("key", "id", "_id");
+
+    protected static final List<SortOptions> FULLPATH_SORT_OPTIONS = List.of(
             new SortOptions.Builder().
                     script(s -> s.type(ScriptSortType.Number).
                     script(t -> t.inline(i -> i.lang(ScriptLanguage.builder().
@@ -64,16 +86,25 @@ public class OpenSearchRealmSearchDAO implements RealmSearchDAO {
 
     protected final RealmDAO realmDAO;
 
+    protected final RealmUtils realmUtils;
+
     protected final OpenSearchClient client;
 
     protected final int indexMaxResultWindow;
 
     public OpenSearchRealmSearchDAO(
             final RealmDAO realmDAO,
+            final PlainSchemaDAO plainSchemaDAO,
+            final UserDAO userDAO,
+            final GroupDAO groupDAO,
+            final EntityFactory entityFactory,
+            final PlainAttrValidationManager validator,
             final OpenSearchClient client,
             final int indexMaxResultWindow) {
 
+        super(plainSchemaDAO, userDAO, groupDAO, entityFactory, validator);
         this.realmDAO = realmDAO;
+        this.realmUtils = new RealmUtils(entityFactory);
         this.client = client;
         this.indexMaxResultWindow = indexMaxResultWindow;
     }
@@ -103,7 +134,7 @@ public class OpenSearchRealmSearchDAO implements RealmSearchDAO {
             return client.search(request, Void.class).hits().hits().stream().findFirst().map(Hit::id).
                     flatMap(realmDAO::findById).map(Realm.class::cast);
         } catch (Exception e) {
-            LOG.error("While searching OpenSearch for Realm path {} with request {}", fullPath, request, e);
+            LOG.error("While searching Elasticsearch for Realm path {} with request {}", fullPath, request, e);
         }
 
         return Optional.empty();
@@ -114,7 +145,7 @@ public class OpenSearchRealmSearchDAO implements RealmSearchDAO {
                 index(OpenSearchUtils.getRealmIndex(AuthContextUtils.getDomain())).
                 searchType(SearchType.QueryThenFetch).
                 query(query).
-                sort(REALM_SORT_OPTIONS).
+                sort(FULLPATH_SORT_OPTIONS).
                 fields(List.of()).source(new SourceConfig.Builder().fetch(false).build()).
                 build();
         LOG.debug("Search request: {}", request);
@@ -124,7 +155,7 @@ public class OpenSearchRealmSearchDAO implements RealmSearchDAO {
                     map(Hit::id).
                     toList();
         } catch (Exception e) {
-            LOG.error("While searching in OpenSearch with request {}", request, e);
+            LOG.error("While searching in Elasticsearch with request {}", request, e);
             return List.of();
         }
     }
@@ -148,7 +179,7 @@ public class OpenSearchRealmSearchDAO implements RealmSearchDAO {
                 flatMap(Optional::stream).map(Realm.class::cast).toList();
     }
 
-    protected Query buildDescendantsQuery(final Set<String> bases, final String keyword) {
+    protected Query buildDescendantsQuery(final Set<String> bases, final SearchCond searchCond) {
         List<Query> basesQueries = new ArrayList<>();
         bases.forEach(base -> {
             basesQueries.add(new Query.Builder().term(QueryBuilders.term().
@@ -159,53 +190,252 @@ public class OpenSearchRealmSearchDAO implements RealmSearchDAO {
         });
         Query prefix = new Query.Builder().disMax(QueryBuilders.disMax().queries(basesQueries).build()).build();
 
-        if (keyword == null) {
-            return prefix;
+        BoolQuery.Builder boolBuilder = QueryBuilders.bool().filter(prefix);
+        if (searchCond != null) {
+            boolBuilder.filter(getQuery(searchCond));
         }
+        return new Query.Builder().bool(boolBuilder.build()).build();
+    }
 
-        return new Query.Builder().bool(QueryBuilders.bool().filter(
-                prefix,
-                new Query.Builder().wildcard(QueryBuilders.wildcard().
-                        field("name").value(keyword.replace('%', '*').replace("\\_", "_")).
-                        caseInsensitive(true).build()).build()).build()).
-                build();
+    protected Query getQuery(final SearchCond cond) {
+        Query query = null;
+
+        switch (cond.getType()) {
+            case LEAF, NOT_LEAF -> {
+                query = cond.asLeaf(AnyCond.class).map(this::getQuery).orElse(null);
+                if (query == null) {
+                    query = cond.asLeaf(AttrCond.class).map(this::getQuery).orElse(null);
+                }
+                if (query == null) {
+                    query = getQueryForCustomConds(cond);
+                }
+                if (query == null) {
+                    throw new IllegalArgumentException("Cannot construct QueryBuilder");
+                }
+                if (cond.getType() == SearchCond.Type.NOT_LEAF) {
+                    query = new Query.Builder().bool(QueryBuilders.bool().mustNot(query).build()).build();
+                }
+            }
+            case AND -> {
+                List<Query> andCompound = new ArrayList<>();
+                Query andLeft = getQuery(cond.getLeft());
+                if (andLeft.isBool() && !andLeft.bool().filter().isEmpty()) {
+                    andCompound.addAll(andLeft.bool().filter());
+                } else {
+                    andCompound.add(andLeft);
+                }
+                Query andRight = getQuery(cond.getRight());
+                if (andRight.isBool() && !andRight.bool().filter().isEmpty()) {
+                    andCompound.addAll(andRight.bool().filter());
+                } else {
+                    andCompound.add(andRight);
+                }
+                query = new Query.Builder().bool(QueryBuilders.bool().filter(andCompound).build()).build();
+            }
+            case OR -> {
+                List<Query> orCompound = new ArrayList<>();
+                Query orLeft = getQuery(cond.getLeft());
+                if (orLeft.isDisMax()) {
+                    orCompound.addAll(orLeft.disMax().queries());
+                } else {
+                    orCompound.add(orLeft);
+                }
+                Query orRight = getQuery(cond.getRight());
+                if (orRight.isDisMax()) {
+                    orCompound.addAll(orRight.disMax().queries());
+                } else {
+                    orCompound.add(orRight);
+                }
+                query = new Query.Builder().disMax(QueryBuilders.disMax().queries(orCompound).build()).build();
+            }
+            default -> {
+            }
+        }
+
+        return query;
     }
 
     @Override
-    public long countDescendants(final String base, final String keyword) {
-        return countDescendants(Set.of(base), keyword);
+    protected CheckResult<AnyCond> check(final AnyCond cond, final Field field, final Set<String> relationshipsFields) {
+        CheckResult<AnyCond> checked = super.check(cond, field, relationshipsFields);
+
+        // Manage difference between external id attribute and internal _id
+        if ("id".equals(checked.cond().getSchema())) {
+            checked.cond().setSchema("_id");
+        }
+        if ("id".equals(checked.schema().getKey())) {
+            checked.schema().setKey("_id");
+        }
+
+        return checked;
+    }
+
+    protected Query fillAttrQuery(
+            final PlainSchema schema,
+            final PlainAttrValue attrValue,
+            final AttrCond cond) {
+
+        Object value = schema.getType() == AttrSchemaType.Date && attrValue.getDateValue() != null
+                ? FormatUtils.format(attrValue.getDateValue())
+                : attrValue.getValue();
+
+        Query query = null;
+        switch (cond.getType()) {
+            case ISNOTNULL:
+                query = new Query.Builder().exists(QueryBuilders.exists().field(schema.getKey()).build()).build();
+                break;
+
+            case ISNULL:
+                query = new Query.Builder().bool(QueryBuilders.bool().mustNot(
+                        new Query.Builder().exists(QueryBuilders.exists().field(schema.getKey()).build())
+                                .build()).build()).build();
+                break;
+
+            case ILIKE:
+                query = new Query.Builder().wildcard(QueryBuilders.wildcard().
+                        field(schema.getKey()).value(cond.getExpression().replace('%', '*').replace("\\_", "_")).
+                        caseInsensitive(true).build()).build();
+                break;
+
+            case LIKE:
+                query = new Query.Builder().wildcard(QueryBuilders.wildcard().
+                        field(schema.getKey()).value(cond.getExpression().replace('%', '*').replace("\\_", "_")).
+                        caseInsensitive(false).build()).build();
+                break;
+
+            case IEQ:
+                query = new Query.Builder().term(QueryBuilders.term().
+                        field(schema.getKey()).value(FieldValue.of(cond.getExpression())).caseInsensitive(true).
+                        build()).build();
+                break;
+
+            case EQ:
+                FieldValue fieldValue = switch (value) {
+                    case Double aDouble ->
+                        FieldValue.of(aDouble);
+                    case Long aLong ->
+                        FieldValue.of(aLong);
+                    case Boolean aBoolean ->
+                        FieldValue.of(aBoolean);
+                    default ->
+                        FieldValue.of(value.toString());
+                };
+                query = new Query.Builder().term(QueryBuilders.term().
+                        field(schema.getKey()).value(fieldValue).caseInsensitive(false).build()).
+                        build();
+                break;
+
+            case GE:
+                query = new Query.Builder().range(QueryBuilders.range().
+                        field(schema.getKey()).gte(JsonData.of(value)).build()).
+                        build();
+                break;
+
+            case GT:
+                query = new Query.Builder().range(QueryBuilders.range().
+                        field(schema.getKey()).gt(JsonData.of(value)).build()).
+                        build();
+                break;
+
+            case LE:
+                query = new Query.Builder().range(QueryBuilders.range().
+                        field(schema.getKey()).lte(JsonData.of(value)).build()).
+                        build();
+                break;
+
+            case LT:
+                query = new Query.Builder().range(QueryBuilders.range().
+                        field(schema.getKey()).lt(JsonData.of(value)).build()).
+                        build();
+                break;
+
+            default:
+                break;
+        }
+
+        return query;
+    }
+
+    protected Query getQuery(final AttrCond cond) {
+        CheckResult<AttrCond> checked = check(cond);
+        return fillAttrQuery(checked.schema(), checked.value(), cond);
+    }
+
+    protected Query getQuery(final AnyCond cond) {
+        CheckResult<AnyCond> checked = check(
+                cond,
+                realmUtils.getField(cond.getSchema()).
+                        orElseThrow(() -> new IllegalArgumentException("Invalid schema " + cond.getSchema())),
+                RELATIONSHIP_FIELDS);
+        return fillAttrQuery(checked.schema(), checked.value(), checked.cond());
+    }
+
+    protected Query getQueryForCustomConds(final SearchCond cond) {
+        return null;
     }
 
     @Override
-    public long countDescendants(final Set<String> bases, final String keyword) {
+    protected long doCount(final Set<String> bases, final SearchCond searchCond) {
         CountRequest request = new CountRequest.Builder().
                 index(OpenSearchUtils.getRealmIndex(AuthContextUtils.getDomain())).
-                query(buildDescendantsQuery(bases, keyword)).
+                query(buildDescendantsQuery(bases, searchCond)).
                 build();
         LOG.debug("Count request: {}", request);
 
         try {
             return client.count(request).count();
         } catch (Exception e) {
-            LOG.error("While counting in OpenSearch with request {}", request, e);
+            LOG.error("While counting in Elasticsearch with request {}", request, e);
             return 0;
         }
     }
 
-    @Override
-    public List<Realm> findDescendants(final String base, final String keyword, final Pageable pageable) {
-        return findDescendants(Set.of(base), keyword, pageable);
+    protected List<SortOptions> sortBuilders(final Stream<Sort.Order> orderBy) {
+        List<SortOptions> options = new ArrayList<>();
+        orderBy.forEach(clause -> {
+            String sortName = null;
+
+            String fieldName = clause.getProperty();
+            // Cannot sort by internal _id
+            if (!ID_PROPS.contains(fieldName)) {
+                Field anyField = realmUtils.getField(fieldName).orElse(null);
+                if (anyField == null) {
+                    PlainSchema schema = plainSchemaDAO.findById(fieldName).orElse(null);
+                    if (schema != null) {
+                        sortName = fieldName;
+                    }
+                } else {
+                    sortName = fieldName;
+                }
+            }
+
+            if (sortName == null) {
+                LOG.warn("Cannot build any valid clause from {}", clause);
+            } else {
+                if ("fullPath".equals(sortName)) {
+                    options.addAll(FULLPATH_SORT_OPTIONS);
+                } else {
+                    options.add(new SortOptions.Builder().field(
+                            new FieldSort.Builder().
+                                    field(sortName).
+                                    order(clause.getDirection() == Sort.Direction.ASC ? SortOrder.Asc : SortOrder.Desc).
+                                    build()).
+                            build());
+                }
+            }
+        });
+        return options.isEmpty() ? FULLPATH_SORT_OPTIONS : options;
     }
 
     @Override
-    public List<Realm> findDescendants(final Set<String> bases, final String keyword, final Pageable pageable) {
+    protected List<Realm> doSearch(final Set<String> bases, final SearchCond searchCond, final Pageable pageable) {
         SearchRequest request = new SearchRequest.Builder().
                 index(OpenSearchUtils.getRealmIndex(AuthContextUtils.getDomain())).
                 searchType(SearchType.QueryThenFetch).
-                query(buildDescendantsQuery(bases, keyword)).
+                query(buildDescendantsQuery(bases, searchCond)).
                 from(pageable.isUnpaged() ? 0 : pageable.getPageSize() * pageable.getPageNumber()).
                 size(pageable.isUnpaged() ? indexMaxResultWindow : pageable.getPageSize()).
-                sort(REALM_SORT_OPTIONS).
+                sort(sortBuilders(pageable.getSort().get())).
                 fields(List.of()).source(new SourceConfig.Builder().fetch(false).build()).
                 build();
         LOG.debug("Search request: {}", request);
@@ -216,7 +446,7 @@ public class OpenSearchRealmSearchDAO implements RealmSearchDAO {
                     map(Hit::id).
                     toList();
         } catch (Exception e) {
-            LOG.error("While searching in OpenSearch with request {}", request, e);
+            LOG.error("While searching in Elasticsearch with request {}", request, e);
         }
 
         return result.stream().map(realmDAO::findById).
@@ -224,17 +454,22 @@ public class OpenSearchRealmSearchDAO implements RealmSearchDAO {
     }
 
     @Override
-    public List<String> findDescendants(final String base, final String prefix) {
-        Query prefixQuery = new Query.Builder().disMax(QueryBuilders.disMax().queries(
-                new Query.Builder().term(QueryBuilders.term().
-                        field("fullPath").value(FieldValue.of(prefix)).caseInsensitive(false).build()).build(),
-                new Query.Builder().prefix(QueryBuilders.prefix().
-                        field("fullPath").value(SyncopeConstants.ROOT_REALM.equals(prefix) ? "/" : prefix + "/").
-                        build()).build()).build()).build();
-
-        Query query = new Query.Builder().bool(QueryBuilders.bool().filter(
-                buildDescendantsQuery(Set.of(base), (String) null), prefixQuery).build()).
-                build();
+    public List<Realm> findDescendants(final String base, final String prefix) {
+        Query descendantsQuery = buildDescendantsQuery(Set.of(base), null);
+        Query query;
+        if (prefix == null) {
+            query = descendantsQuery;
+        } else {
+            Query prefixQuery = new Query.Builder().disMax(QueryBuilders.disMax().queries(
+                    new Query.Builder().term(QueryBuilders.term().
+                            field("fullPath").value(FieldValue.of(prefix)).caseInsensitive(false).build()).build(),
+                    new Query.Builder().prefix(QueryBuilders.prefix().
+                            field("fullPath").value(SyncopeConstants.ROOT_REALM.equals(prefix) ? "/" : prefix + "/").
+                            build()).build()).build()).build();
+            query = new Query.Builder().bool(QueryBuilders.bool().filter(
+                    descendantsQuery, prefixQuery).build()).
+                    build();
+        }
 
         SearchRequest request = new SearchRequest.Builder().
                 index(OpenSearchUtils.getRealmIndex(AuthContextUtils.getDomain())).
@@ -242,7 +477,7 @@ public class OpenSearchRealmSearchDAO implements RealmSearchDAO {
                 query(query).
                 from(0).
                 size(indexMaxResultWindow).
-                sort(REALM_SORT_OPTIONS).
+                sort(FULLPATH_SORT_OPTIONS).
                 fields(List.of()).source(new SourceConfig.Builder().fetch(false).build()).
                 build();
         LOG.debug("Search request: {}", request);
@@ -253,8 +488,8 @@ public class OpenSearchRealmSearchDAO implements RealmSearchDAO {
                     map(Hit::id).
                     toList();
         } catch (Exception e) {
-            LOG.error("While searching in OpenSearch", e);
+            LOG.error("While searching in Elasticsearch with request {}", request, e);
         }
-        return result;
+        return result.stream().map(realmDAO::findById).flatMap(Optional::stream).map(Realm.class::cast).toList();
     }
 }
diff --git a/ext/opensearch/persistence/src/test/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchAnySearchDAOTest.java b/ext/opensearch/persistence/src/test/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchAnySearchDAOTest.java
index 86f99f3ec9..20998de3ba 100644
--- a/ext/opensearch/persistence/src/test/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchAnySearchDAOTest.java
+++ b/ext/opensearch/persistence/src/test/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchAnySearchDAOTest.java
@@ -111,11 +111,12 @@ public class OpenSearchAnySearchDAOTest {
     public void getAdminRealmsFilter4realm() throws IOException {
         // 1. mock
         Realm root = mock(Realm.class);
+        when(root.getKey()).thenReturn("rootKey");
         when(root.getFullPath()).thenReturn(SyncopeConstants.ROOT_REALM);
 
         when(realmSearchDAO.findByFullPath(SyncopeConstants.ROOT_REALM)).thenAnswer(ic -> Optional.of(root));
         when(realmSearchDAO.findDescendants(eq(SyncopeConstants.ROOT_REALM), anyString())).
-                thenReturn(List.of("rootKey"));
+                thenReturn(List.of(root));
 
         // 2. test
         Set<String> adminRealms = Set.of(SyncopeConstants.ROOT_REALM);
diff --git a/ext/opensearch/persistence/src/test/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchRealmSearchDAOTest.java b/ext/opensearch/persistence/src/test/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchRealmSearchDAOTest.java
new file mode 100644
index 0000000000..90ff254376
--- /dev/null
+++ b/ext/opensearch/persistence/src/test/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchRealmSearchDAOTest.java
@@ -0,0 +1,216 @@
+/*
+ * 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.syncope.core.persistence.opensearch.dao;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import java.util.Optional;
+import java.util.Set;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.types.AttrSchemaType;
+import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager;
+import org.apache.syncope.core.persistence.api.dao.GroupDAO;
+import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
+import org.apache.syncope.core.persistence.api.dao.RealmDAO;
+import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
+import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
+import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
+import org.apache.syncope.core.persistence.api.entity.PlainSchema;
+import org.apache.syncope.core.persistence.jpa.entity.JPAPlainSchema;
+import org.apache.syncope.core.persistence.jpa.entity.JPARealm;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.opensearch.client.opensearch.OpenSearchClient;
+import org.opensearch.client.opensearch._types.FieldValue;
+import org.opensearch.client.opensearch._types.query_dsl.BoolQuery;
+import org.opensearch.client.opensearch._types.query_dsl.DisMaxQuery;
+import org.opensearch.client.opensearch._types.query_dsl.Query;
+import org.opensearch.client.opensearch._types.query_dsl.QueryBuilders;
+
+@ExtendWith(MockitoExtension.class)
+public class OpenSearchRealmSearchDAOTest {
+
+    @Mock
+    private RealmDAO realmDAO;
+
+    @Mock
+    private PlainSchemaDAO plainSchemaDAO;
+
+    @Mock
+    private UserDAO userDAO;
+
+    @Mock
+    private GroupDAO groupDAO;
+
+    @Mock
+    private EntityFactory entityFactory;
+
+    @Mock
+    private PlainAttrValidationManager validator;
+
+    @Mock
+    private OpenSearchClient client;
+
+    private OpenSearchRealmSearchDAO searchDAO;
+
+    @BeforeEach
+    protected void setupSearchDAO() {
+        doReturn(JPARealm.class).when(entityFactory).realmClass();
+        searchDAO = new OpenSearchRealmSearchDAO(
+                realmDAO,
+                plainSchemaDAO,
+                userDAO,
+                groupDAO,
+                entityFactory,
+                validator,
+                client,
+                10000);
+    }
+
+    @Test
+    public void query4anyCond() {
+        when(entityFactory.newEntity(PlainSchema.class)).thenReturn(new JPAPlainSchema());
+        doAnswer(ic -> {
+            PlainAttrValue value = ic.getArgument(2);
+            value.setStringValue(ic.getArgument(1));
+            return null;
+        }).when(validator).validate(any(PlainSchema.class), anyString(), any(PlainAttrValue.class));
+
+        AnyCond name = new AnyCond(AttrCond.Type.EQ);
+        name.setSchema("name");
+        name.setExpression("two");
+
+        Query query = searchDAO.getQuery(SearchCond.of(name));
+        assertThat(
+                new Query.Builder().term(QueryBuilders.term().
+                        field("name").value(FieldValue.of("two")).caseInsensitive(false).build()).build()).
+                usingRecursiveComparison().isEqualTo(query);
+        verifyNoInteractions(plainSchemaDAO);
+    }
+
+    @Test
+    public void query4attrCond() {
+        PlainSchema aLong = new JPAPlainSchema();
+        aLong.setKey("aLong");
+        aLong.setType(AttrSchemaType.Long);
+        doReturn(Optional.of(aLong)).when(plainSchemaDAO).findById("aLong");
+        doAnswer(ic -> {
+            PlainAttrValue value = ic.getArgument(2);
+            value.setLongValue(Long.valueOf(ic.getArgument(1)));
+            return null;
+        }).when(validator).validate(any(PlainSchema.class), anyString(), any(PlainAttrValue.class));
+
+        AttrCond attrEq = new AttrCond(AttrCond.Type.EQ);
+        attrEq.setSchema("aLong");
+        attrEq.setExpression("42");
+
+        Query query = searchDAO.getQuery(SearchCond.of(attrEq));
+        assertEquals(Query.Kind.Term, query._kind());
+        assertEquals("aLong", query.term().field());
+        assertEquals(Boolean.FALSE, query.term().caseInsensitive());
+        assertEquals(42, query.term().value().longValue());
+    }
+
+    @Test
+    public void query4attrCondNullChecks() {
+        PlainSchema aLong = new JPAPlainSchema();
+        aLong.setKey("aLong");
+        aLong.setType(AttrSchemaType.Long);
+        doReturn(Optional.of(aLong)).when(plainSchemaDAO).findById("aLong");
+
+        AttrCond isNull = new AttrCond(AttrCond.Type.ISNULL);
+        isNull.setSchema("aLong");
+
+        Query query = searchDAO.getQuery(SearchCond.of(isNull));
+        assertThat(
+                new Query.Builder().bool(QueryBuilders.bool().mustNot(
+                        new Query.Builder().exists(QueryBuilders.exists().field("aLong").build())
+                                .build()).build()).build()).
+                usingRecursiveComparison().isEqualTo(query);
+
+        AttrCond isNotNull = new AttrCond(AttrCond.Type.ISNOTNULL);
+        isNotNull.setSchema("aLong");
+
+        query = searchDAO.getQuery(SearchCond.of(isNotNull));
+        assertThat(
+                new Query.Builder().exists(QueryBuilders.exists().field("aLong").build()).build()).
+                usingRecursiveComparison().isEqualTo(query);
+    }
+
+    @Test
+    public void descendantsQueryWithAndOrFilters() {
+        when(entityFactory.newEntity(PlainSchema.class)).thenReturn(new JPAPlainSchema());
+        doAnswer(ic -> {
+            PlainAttrValue value = ic.getArgument(2);
+            value.setStringValue(ic.getArgument(1));
+            return null;
+        }).when(validator).validate(any(PlainSchema.class), anyString(), any(PlainAttrValue.class));
+
+        AnyCond nameTwo = new AnyCond(AttrCond.Type.EQ);
+        nameTwo.setSchema("name");
+        nameTwo.setExpression("two");
+
+        AnyCond nameOdd = new AnyCond(AttrCond.Type.EQ);
+        nameOdd.setSchema("name");
+        nameOdd.setExpression("odd");
+
+        Query query = searchDAO.buildDescendantsQuery(
+                Set.of(SyncopeConstants.ROOT_REALM),
+                SearchCond.or(SearchCond.of(nameTwo), SearchCond.of(nameOdd)));
+
+        assertEquals(Query.Kind.Bool, query._kind());
+        assertEquals(2, ((BoolQuery) query._get()).filter().size());
+        Query right = ((BoolQuery) query._get()).filter().get(1);
+        assertEquals(Query.Kind.DisMax, right._kind());
+        assertEquals(2, ((DisMaxQuery) right._get()).queries().size());
+
+        assertThat(
+                new Query.Builder().bool(QueryBuilders.bool().
+                        filter(new Query.Builder().disMax(QueryBuilders.disMax().
+                                queries(new Query.Builder().term(QueryBuilders.term().
+                                        field("fullPath").value(FieldValue.of(SyncopeConstants.ROOT_REALM)).
+                                        caseInsensitive(false).build()).build()).
+                                queries(new Query.Builder().regexp(QueryBuilders.regexp().
+                                        field("fullPath").value("/.*").build()).build()).
+                                build()).build()).
+                        filter(new Query.Builder().disMax(QueryBuilders.disMax().
+                                queries(new Query.Builder().term(QueryBuilders.term().
+                                        field("name").value(FieldValue.of("two")).caseInsensitive(false).
+                                        build()).build()).
+                                queries(new Query.Builder().term(QueryBuilders.term().
+                                        field("name").value(FieldValue.of("odd")).caseInsensitive(false).
+                                        build()).build()).
+                                build()).build()).
+                        build()).build()).
+                usingRecursiveComparison().isEqualTo(query);
+    }
+}
diff --git a/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/TestCommand.java b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/TestCommand.java
index a43bb8f095..8361c24869 100644
--- a/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/TestCommand.java
+++ b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/TestCommand.java
@@ -46,7 +46,7 @@ public class TestCommand implements Command<TestCommandArgs> {
     private AnyObjectLogic anyObjectLogic;
 
     private Optional<RealmTO> getRealm(final String fullPath) {
-        return realmLogic.search(null, Set.of(fullPath), Pageable.unpaged()).get().
+        return realmLogic.search(Set.of(fullPath), null, Pageable.unpaged()).get().
                 filter(realm -> fullPath.equals(realm.getFullPath())).findFirst();
     }
 
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
index 69c56addaf..039caf429b 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/AbstractITCase.java
@@ -518,11 +518,11 @@ public abstract class AbstractITCase {
         IS_FLOWABLE_ENABLED = uwfAdapter.get("resource").asString().contains("Flowable");
 
         JsonNode anySearchDAO = beans.findValues("anySearchDAO").getFirst();
-        boolean isElasticsearchEnabled = anySearchDAO.get("type").asString().contains("Elasticsearch");
-        boolean isOpenSearchEnabled = anySearchDAO.get("type").asString().contains("OpenSearch");
+        boolean isElasticsearchEnabled = anySearchDAO.get("resource").asString().contains("Elasticsearch");
+        boolean isOpenSearchEnabled = anySearchDAO.get("resource").asString().contains("OpenSearch");
         IS_EXT_SEARCH_ENABLED = isElasticsearchEnabled || isOpenSearchEnabled;
 
-        IS_NEO4J_PERSISTENCE = anySearchDAO.get("type").asString().contains("Neo4j");
+        IS_NEO4J_PERSISTENCE = anySearchDAO.get("resource").asString().contains("neo4j");
 
         if (!IS_EXT_SEARCH_ENABLED) {
             return;
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MultitenancyITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MultitenancyITCase.java
index 0e062f5e1c..a9f729ab1b 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MultitenancyITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/MultitenancyITCase.java
@@ -97,7 +97,7 @@ public class MultitenancyITCase extends AbstractITCase {
     @Test
     public void readRealm() {
         PagedResult<RealmTO> realms = ADMIN_CLIENT.getService(RealmService.class).
-                search(new RealmQuery.Builder().keyword("*").build());
+                search(new RealmQuery.Builder().build());
         assertEquals(1, realms.getTotalCount());
         assertEquals(1, realms.getResult().size());
         assertEquals(SyncopeConstants.ROOT_REALM, realms.getResult().getFirst().getName());
@@ -106,7 +106,7 @@ public class MultitenancyITCase extends AbstractITCase {
     @Test
     public void createUser() {
         assertNull(ADMIN_CLIENT.getService(RealmService.class).
-                search(new RealmQuery.Builder().keyword("*").build()).getResult().getFirst().getPasswordPolicy());
+                search(new RealmQuery.Builder().build()).getResult().getFirst().getPasswordPolicy());
 
         UserCR userCR = new UserCR();
         userCR.setRealm(SyncopeConstants.ROOT_REALM);
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/RealmITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/RealmITCase.java
index 987780c3ac..c7a8ddae5c 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/RealmITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/RealmITCase.java
@@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
@@ -68,8 +69,166 @@ public class RealmITCase extends AbstractITCase {
 
     @Test
     public void search() {
-        PagedResult<RealmTO> match = REALM_SERVICE.search(new RealmQuery.Builder().keyword("*o*").build());
+        PagedResult<RealmTO> match = REALM_SERVICE.search(new RealmQuery.Builder().fiql("name=~*o*").build());
         assertTrue(match.getResult().stream().allMatch(realm -> realm.getName().contains("o")));
+
+        RealmTO realm = new RealmTO();
+        realm.setName("searchTest1");
+        realm.getAnyTypeClasses().add("other");
+        realm.getPlainAttrs().add(new Attr.Builder("ctype").value("number1").build());
+        realm.getPlainAttrs().add(new Attr.Builder("aLong").value("42").build());
+        realm.getPlainAttrs().add(new Attr.Builder("loginDate").value("2008-05-26").build());
+        realm.getResources().add(RESOURCE_NAME_LDAP_ORGUNIT);
+        REALM_SERVICE.create(SyncopeConstants.ROOT_REALM, realm);
+
+        realm = new RealmTO();
+        realm.setName("searchTest2");
+        realm.getAnyTypeClasses().add("other");
+        realm.getPlainAttrs().add(new Attr.Builder("ctype").value("string2").build());
+        realm.getPlainAttrs().add(new Attr.Builder("aLong").value("90").build());
+        realm.getPlainAttrs().add(new Attr.Builder("loginDate").value("2009-05-26").build());
+        realm.getResources().add(RESOURCE_NAME_LDAP_ORGUNIT);
+        REALM_SERVICE.create("/even", realm);
+
+        realm = new RealmTO();
+        realm.setName("searchTest3");
+        realm.getAnyTypeClasses().add("other");
+        realm.getPlainAttrs().add(new Attr.Builder("ctype").value("string2").build());
+        realm.getPlainAttrs().add(new Attr.Builder("aLong").value("42").build());
+        realm.getPlainAttrs().add(new Attr.Builder("loginDate").value("2009-05-26").value("2010-05-26").build());
+        realm.getResources().add(RESOURCE_NAME_LDAP_ORGUNIT);
+        REALM_SERVICE.create("/even", realm);
+
+        realm = new RealmTO();
+        realm.setName("searchTest4");
+        realm.getAnyTypeClasses().add("other");
+        realm.getPlainAttrs().add(new Attr.Builder("aLong").value("4242").build());
+        realm.getResources().add(RESOURCE_NAME_LDAP_ORGUNIT);
+        REALM_SERVICE.create("/even/two", realm);
+
+        realm = new RealmTO();
+        realm.setName("searchTest5");
+        realm.getAnyTypeClasses().add("other");
+        realm.getPlainAttrs().add(new Attr.Builder("ctype").value("String5").build());
+        realm.getPlainAttrs().add(new Attr.Builder("aLong").value("5").build());
+        realm.getPlainAttrs().add(new Attr.Builder("loginDate").value("2011-05-26").build());
+        realm.getResources().add(RESOURCE_NAME_LDAP_ORGUNIT);
+        REALM_SERVICE.create("/even", realm);
+
+        // Numeric equality
+        match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").fiql("aLong==90").build());
+        assertEquals(1, match.getSize());
+        assertTrue(match.getResult().stream().allMatch(r -> r.getName().equals("searchTest2")));
+
+        match = REALM_SERVICE.search(new RealmQuery.Builder().fiql("aLong==42").orderBy("name DESC").build());
+        assertEquals(2, match.getSize());
+        assertEquals("searchTest3", match.getResult().get(0).getName());
+        assertEquals("searchTest1", match.getResult().get(1).getName());
+
+        // Mixed numeric + string filters
+        match = REALM_SERVICE.search(new RealmQuery.Builder().fiql("aLong==42;ctype=~string*").build());
+        assertEquals(1, match.getSize());
+        assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest3")));
+
+        match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").fiql("name==searchTest*;ctype==string*").
+                build());
+        assertEquals(2, match.getSize());
+        assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest2")));
+        assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest3")));
+
+        match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").fiql("name==searchTest*;ctype=~string*").
+                build());
+        assertEquals(3, match.getSize());
+        assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest2")));
+        assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest3")));
+        assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest5")));
+
+        // Negated LIKE
+        match = REALM_SERVICE.search(new RealmQuery.Builder().fiql("aLong==42;ctype!~string*").build());
+        assertEquals(1, match.getSize());
+        assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest1")));
+
+        match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").fiql("name==searchTest*;ctype=~string5").
+                build());
+        assertEquals(1, match.getSize());
+        assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest5")));
+
+        match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").fiql("aLong==4242;ctype==$null").build());
+        assertEquals(1, match.getSize());
+        assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest4")));
+
+        // Null / not-null checks on optional plain attrs
+        match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").fiql("name==searchTest*;ctype!=$null").
+                build());
+        assertEquals(3, match.getSize());
+        assertTrue(match.getResult().stream().noneMatch(r -> r.getName().equals("searchTest4")));
+
+        // Numeric ranges
+        match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").fiql("name==searchTest*;aLong=gt=42").
+                build());
+        assertEquals(2, match.getSize());
+        assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest2")));
+        assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest4")));
+
+        match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").
+                fiql("name==searchTest*;aLong=ge=80;aLong=le=100").build());
+        assertEquals(1, match.getSize());
+        assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest2")));
+
+        match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").fiql("name==searchTest*;aLong=lt=42").
+                build());
+        assertEquals(1, match.getSize());
+        assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest5")));
+
+        match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").fiql("name==searchTest*;aLong=le=42").
+                build());
+        assertEquals(2, match.getSize());
+        assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest3")));
+        assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest5")));
+
+        // Grouping / OR conditions
+        match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").
+                fiql("name==searchTest*;(aLong==90,ctype==String5)").build());
+        assertEquals(2, match.getSize());
+        assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest2")));
+        assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest5")));
+
+        // Date equality against multivalue date schema
+        match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").fiql("loginDate==2009-05-26").build());
+        assertEquals(2, match.getSize());
+        assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest2")));
+        assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest3")));
+
+        match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").fiql("loginDate==2010-05-26").build());
+        assertEquals(1, match.getSize());
+        assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest3")));
+
+        // Date range checks on multivalue schema
+        match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").fiql("loginDate=gt=2010-01-01").build());
+        assertEquals(2, match.getSize());
+        assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest3")));
+        assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest5")));
+
+        match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").
+                fiql("loginDate=ge=2010-05-26;loginDate=le=2010-05-26").build());
+        assertEquals(1, match.getSize());
+        assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest3")));
+
+        match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").
+                fiql("name==searchTest*;loginDate==$null").build());
+        assertEquals(1, match.getSize());
+        assertTrue(match.getResult().stream().anyMatch(r -> r.getName().equals("searchTest4")));
+
+        // Date nullability checks
+        match = REALM_SERVICE.search(new RealmQuery.Builder().base("/even").
+                fiql("name==searchTest*;loginDate!=$null").build());
+        assertEquals(3, match.getSize());
+        assertTrue(match.getResult().stream().noneMatch(r -> r.getName().equals("searchTest4")));
+
+        // Validation error for numeric schema with non-numeric value
+        SyncopeClientException exception = assertThrows(SyncopeClientException.class,
+                () -> REALM_SERVICE.search(new RealmQuery.Builder().fiql("aLong==notANumber").build()));
+        assertEquals(ClientExceptionType.InvalidSearchParameters, exception.getType());
     }
 
     @Test
@@ -406,7 +565,7 @@ public class RealmITCase extends AbstractITCase {
     @Test
     public void issueSYNCOPE1472() {
         // 1. assign twice resource-ldap-orgunit to /odd
-        RealmTO realmTO = REALM_SERVICE.search(new RealmQuery.Builder().base("/odd").build()).getResult().getFirst();
+        RealmTO realmTO = getRealm("/odd").orElseThrow();
         realmTO.getResources().clear();
         realmTO.getResources().add("resource-ldap-orgunit");
         realmTO.getResources().add("resource-ldap-orgunit");
@@ -456,7 +615,7 @@ public class RealmITCase extends AbstractITCase {
             Response response = REALM_SERVICE.create(SyncopeConstants.ROOT_REALM, realmTO);
             assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatusInfo().getStatusCode());
             childRealm = REALM_SERVICE.search(new RealmQuery.Builder().
-                    base(SyncopeConstants.ROOT_REALM).keyword("child").build()).getResult().getFirst();
+                    base(SyncopeConstants.ROOT_REALM).fiql("name=~child").build()).getResult().getFirst();
 
             // MANAGER CANNOT UPDATE /child
             try {
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java
index 5b163f911c..17d52b7eb9 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java
@@ -959,7 +959,7 @@ public class UserITCase extends AbstractITCase {
         passwordPolicy = createPolicy(PolicyType.PASSWORD, passwordPolicy);
         assertNotNull(passwordPolicy);
 
-        RealmTO realm = REALM_SERVICE.search(new RealmQuery.Builder().keyword("two").build()).getResult().getFirst();
+        RealmTO realm = REALM_SERVICE.search(new RealmQuery.Builder().fiql("name==two").build()).getResult().getFirst();
         String oldAccountPolicy = realm.getAccountPolicy();
         realm.setAccountPolicy(accountPolicy.getKey());
         String oldPasswordPolicy = realm.getPasswordPolicy();
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java
index 0b42e19f9e..e0321ed187 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java
@@ -794,7 +794,7 @@ public class UserIssuesITCase extends AbstractITCase {
         }
         assertNotNull(logicActions);
 
-        RealmTO realm = REALM_SERVICE.search(new RealmQuery.Builder().keyword("two").build()).getResult().getFirst();
+        RealmTO realm = REALM_SERVICE.search(new RealmQuery.Builder().fiql("name==two").build()).getResult().getFirst();
         assertNotNull(realm);
         realm.getActions().add(logicActions.getKey());
         REALM_SERVICE.update(realm);
@@ -1201,7 +1201,7 @@ public class UserIssuesITCase extends AbstractITCase {
         passwordPolicy = createPolicy(PolicyType.PASSWORD, passwordPolicy);
         assertNotNull(passwordPolicy);
 
-        RealmTO realm = REALM_SERVICE.search(new RealmQuery.Builder().keyword("two").build()).getResult().getFirst();
+        RealmTO realm = REALM_SERVICE.search(new RealmQuery.Builder().fiql("name==two").build()).getResult().getFirst();
         String oldPasswordPolicy = realm.getPasswordPolicy();
         realm.setPasswordPolicy(passwordPolicy.getKey());
         REALM_SERVICE.update(realm);
@@ -1378,7 +1378,8 @@ public class UserIssuesITCase extends AbstractITCase {
         PasswordPolicyTO pwdPolicy = POLICY_SERVICE.read(PolicyType.PASSWORD, "ce93fcda-dc3a-4369-a7b0-a6108c261c85");
         assertEquals(1, pwdPolicy.getHistoryLength());
 
-        RealmTO evenTwo = REALM_SERVICE.search(new RealmQuery.Builder().keyword("two").build()).getResult().getFirst();
+        RealmTO evenTwo = REALM_SERVICE.search(new RealmQuery.Builder().fiql("name==two").build())
+                .getResult().getFirst();
         evenTwo.setPasswordPolicy(pwdPolicy.getKey());
         REALM_SERVICE.update(evenTwo);