diff --git a/.gitignore b/.gitignore
index a30a3fa095b66e018388937f3ffb79eed4692301..767581f76153ad5ac9f16422ee895a14b7a67f32 100644
--- a/.gitignore
+++ b/.gitignore
@@ -65,7 +65,6 @@ docs/_build/
 aleksis/node_modules/
 aleksis/static/
 aleksis/whoosh_index/
-aleksis/xapian_index/
 
 .coverage
 .mypy_cache/
diff --git a/README.rst b/README.rst
index 5946b0d4eb15331b918e95446fddbc440250bfcb..de7d791c4b901e4b35f08bc3c2fa64ca9bbc2307 100644
--- a/README.rst
+++ b/README.rst
@@ -36,13 +36,14 @@ Licence
 
 ::
 
-  Copyright © 2017, 2018, 2019, 2020, 2021 Jonathan Weth <wethjo@katharineum.de>
+  Copyright © 2017, 2018, 2019, 2020, 2021 Jonathan Weth <dev@jonathanweth.de>
   Copyright © 2017, 2018, 2019, 2020 Frank Poetzsch-Heffter <p-h@katharineum.de>
   Copyright © 2018, 2019, 2020, 2021 Julian Leucker <leuckeju@katharineum.de>
   Copyright © 2018, 2019, 2020, 2021 Hangzhi Yu <yuha@katharineum.de>
   Copyright © 2019, 2020, 2021 Dominik George <dominik.george@teckids.org>
   Copyright © 2019, 2020, 2021 Tom Teichler <tom.teichler@teckids.org>
   Copyright © 2019 mirabilos <thorsten.glaser@teckids.org>
+  Copyright © 2021 Lloyd Meins <meinsll@katharineum.de>
   Copyright © 2021 magicfelix <felix@felix-zauberer.de>
 
   Licenced under the EUPL, version 1.2 or later, by Teckids e.V. (Bonn, Germany).
diff --git a/aleksis/core/apps.py b/aleksis/core/apps.py
index e5eaffcc5de2e9ad1a26adf758962b5050ab648e..16d30129631f574c6fa987612356a3481164c8ec 100644
--- a/aleksis/core/apps.py
+++ b/aleksis/core/apps.py
@@ -35,6 +35,7 @@ class CoreConfig(AppConfig):
         ([2019, 2020, 2021], "Dominik George", "dominik.george@teckids.org"),
         ([2019, 2020, 2021], "Tom Teichler", "tom.teichler@teckids.org"),
         ([2019], "mirabilos", "thorsten.glaser@teckids.org"),
+        ([2021], "Lloyd Meins", "meinsll@katharineum.de"),
         ([2021], "magicfelix", "felix@felix-zauberer.de"),
     )
 
diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py
index 821bdc79d31440cd30d1806c06480f38453d19b5..642364b1b331f37720bdbb5798ac20b33786b79b 100644
--- a/aleksis/core/forms.py
+++ b/aleksis/core/forms.py
@@ -381,8 +381,7 @@ class SchoolTermForm(ExtensibleForm):
 
 class DashboardWidgetOrderForm(ExtensibleForm):
     pk = forms.ModelChoiceField(
-        queryset=DashboardWidget.objects.all(),
-        widget=forms.HiddenInput(attrs={"class": "pk-input"}),
+        queryset=None, widget=forms.HiddenInput(attrs={"class": "pk-input"}),
     )
     order = forms.IntegerField(initial=0, widget=forms.HiddenInput(attrs={"class": "order-input"}))
 
@@ -390,6 +389,12 @@ class DashboardWidgetOrderForm(ExtensibleForm):
         model = DashboardWidget
         fields = []
 
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        # Set queryset here to prevent problems with not migrated database due to special queryset
+        self.fields["pk"].queryset = DashboardWidget.objects.all()
+
 
 DashboardWidgetOrderFormSet = forms.formset_factory(
     form=DashboardWidgetOrderForm, max_num=0, extra=0
diff --git a/aleksis/core/menus.py b/aleksis/core/menus.py
index ffc46dd99d6e0e8d92da6c0835df99027166e253..a1b6f32113129085ba390a2ed8a2cc84d3c3eef1 100644
--- a/aleksis/core/menus.py
+++ b/aleksis/core/menus.py
@@ -128,7 +128,7 @@ MENUS = {
                     "validators": [
                         (
                             "aleksis.core.util.predicates.permission_validator",
-                            "core.view_announcements",
+                            "core.view_announcements_rule",
                         ),
                     ],
                 },
@@ -139,7 +139,7 @@ MENUS = {
                     "validators": [
                         (
                             "aleksis.core.util.predicates.permission_validator",
-                            "core.view_schoolterm",
+                            "core.view_schoolterm_rule",
                         ),
                     ],
                 },
@@ -150,7 +150,7 @@ MENUS = {
                     "validators": [
                         (
                             "aleksis.core.util.predicates.permission_validator",
-                            "core.view_dashboardwidget",
+                            "core.view_dashboardwidget_rule",
                         ),
                     ],
                 },
@@ -159,7 +159,10 @@ MENUS = {
                     "url": "data_management",
                     "icon": "view_list",
                     "validators": [
-                        ("aleksis.core.util.predicates.permission_validator", "core.manage_data"),
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "core.manage_data_rule",
+                        ),
                     ],
                 },
                 {
@@ -169,7 +172,7 @@ MENUS = {
                     "validators": [
                         (
                             "aleksis.core.util.predicates.permission_validator",
-                            "core.view_system_status",
+                            "core.view_system_status_rule",
                         ),
                     ],
                 },
@@ -178,7 +181,10 @@ MENUS = {
                     "url": "impersonate-list",
                     "icon": "people",
                     "validators": [
-                        ("aleksis.core.util.predicates.permission_validator", "core.impersonate"),
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "core.impersonate_rule",
+                        ),
                     ],
                 },
                 {
@@ -188,7 +194,7 @@ MENUS = {
                     "validators": [
                         (
                             "aleksis.core.util.predicates.permission_validator",
-                            "core.change_site_preferences",
+                            "core.change_site_preferences_rule",
                         ),
                     ],
                 },
@@ -211,7 +217,7 @@ MENUS = {
                     "validators": [
                         (
                             "aleksis.core.util.predicates.permission_validator",
-                            "core.list_oauth_applications",
+                            "core.list_oauth_applications_rule",
                         ),
                     ],
                 },
@@ -223,7 +229,7 @@ MENUS = {
             "icon": "people",
             "root": True,
             "validators": [
-                ("aleksis.core.util.predicates.permission_validator", "core.view_people_menu")
+                ("aleksis.core.util.predicates.permission_validator", "core.view_people_menu_rule")
             ],
             "submenu": [
                 {
@@ -231,7 +237,10 @@ MENUS = {
                     "url": "persons",
                     "icon": "person",
                     "validators": [
-                        ("aleksis.core.util.predicates.permission_validator", "core.view_persons")
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "core.view_persons_rule",
+                        )
                     ],
                 },
                 {
@@ -239,7 +248,10 @@ MENUS = {
                     "url": "groups",
                     "icon": "group",
                     "validators": [
-                        ("aleksis.core.util.predicates.permission_validator", "core.view_groups")
+                        (
+                            "aleksis.core.util.predicates.permission_validator",
+                            "core.view_groups_rule",
+                        )
                     ],
                 },
                 {
@@ -249,7 +261,7 @@ MENUS = {
                     "validators": [
                         (
                             "aleksis.core.util.predicates.permission_validator",
-                            "core.view_grouptypes",
+                            "core.view_grouptypes_rule",
                         )
                     ],
                 },
@@ -260,7 +272,7 @@ MENUS = {
                     "validators": [
                         (
                             "aleksis.core.util.predicates.permission_validator",
-                            "core.link_persons_accounts",
+                            "core.link_persons_accounts_rule",
                         )
                     ],
                 },
@@ -271,7 +283,7 @@ MENUS = {
                     "validators": [
                         (
                             "aleksis.core.util.predicates.permission_validator",
-                            "core.assign_child_groups_to_groups",
+                            "core.assign_child_groups_to_groups_rule",
                         )
                     ],
                 },
@@ -282,7 +294,7 @@ MENUS = {
                     "validators": [
                         (
                             "aleksis.core.util.predicates.permission_validator",
-                            "core.view_additionalfields",
+                            "core.view_additionalfields_rule",
                         )
                     ],
                 },
@@ -296,7 +308,7 @@ MENUS = {
             "validators": [
                 (
                     "aleksis.core.util.predicates.permission_validator",
-                    "core.assign_child_groups_to_groups",
+                    "core.assign_child_groups_to_groups_rule",
                 )
             ],
         },
diff --git a/aleksis/core/rules.py b/aleksis/core/rules.py
index d36f54fe46f824cef63c860110c30eb9c4d8aaa8..68c7f5a7a4329ff4dc0f1093b49559f15b9e7ebd 100644
--- a/aleksis/core/rules.py
+++ b/aleksis/core/rules.py
@@ -17,32 +17,32 @@ rules.add_perm("core", rules.always_allow)
 
 # View dashboard
 view_dashboard_predicate = is_site_preference_set("general", "anonymous_dashboard") | has_person
-rules.add_perm("core.view_dashboard", view_dashboard_predicate)
+rules.add_perm("core.view_dashboard_rule", view_dashboard_predicate)
 
 # View notifications
-rules.add_perm("core.view_notifications", has_person)
+rules.add_perm("core.view_notifications_rule", has_person)
 
 # Use search
 search_predicate = has_person & has_global_perm("core.search")
-rules.add_perm("core.search", search_predicate)
+rules.add_perm("core.search_rule", search_predicate)
 
 # View persons
 view_persons_predicate = has_person & (
-    has_global_perm("core.view_person") | has_any_object("core.view_person", Person)
+    has_global_perm("core.view_person") | has_any_object("core.view_person_rule", Person)
 )
-rules.add_perm("core.view_persons", view_persons_predicate)
+rules.add_perm("core.view_persons_rule", view_persons_predicate)
 
 # View person
 view_person_predicate = has_person & (
     has_global_perm("core.view_person") | has_object_perm("core.view_person") | is_current_person
 )
-rules.add_perm("core.view_person", view_person_predicate)
+rules.add_perm("core.view_person_rule", view_person_predicate)
 
 # View person address
 view_address_predicate = has_person & (
     has_global_perm("core.view_address") | has_object_perm("core.view_address") | is_current_person
 )
-rules.add_perm("core.view_address", view_address_predicate)
+rules.add_perm("core.view_address_rule", view_address_predicate)
 
 # View person contact details
 view_contact_details_predicate = has_person & (
@@ -50,13 +50,13 @@ view_contact_details_predicate = has_person & (
     | has_object_perm("core.view_contact_details")
     | is_current_person
 )
-rules.add_perm("core.view_contact_details", view_contact_details_predicate)
+rules.add_perm("core.view_contact_details_rule", view_contact_details_predicate)
 
 # View person photo
 view_photo_predicate = has_person & (
     has_global_perm("core.view_photo") | has_object_perm("core.view_photo") | is_current_person
 )
-rules.add_perm("core.view_photo", view_photo_predicate)
+rules.add_perm("core.view_photo_rule", view_photo_predicate)
 
 # View persons groups
 view_groups_predicate = has_person & (
@@ -64,7 +64,7 @@ view_groups_predicate = has_person & (
     | has_object_perm("core.view_person_groups")
     | is_current_person
 )
-rules.add_perm("core.view_person_groups", view_groups_predicate)
+rules.add_perm("core.view_person_groups_rule", view_groups_predicate)
 
 # Edit person
 edit_person_predicate = has_person & (
@@ -72,91 +72,91 @@ edit_person_predicate = has_person & (
     | has_object_perm("core.change_person")
     | is_current_person & is_site_preference_set("account", "editable_fields_person")
 )
-rules.add_perm("core.edit_person", edit_person_predicate)
+rules.add_perm("core.edit_person_rule", edit_person_predicate)
 
 # Delete person
 delete_person_predicate = has_person & (
     has_global_perm("core.delete_person") | has_object_perm("core.delete_person")
 )
-rules.add_perm("core.delete_person", delete_person_predicate)
+rules.add_perm("core.delete_person_rule", delete_person_predicate)
 
 # Link persons with accounts
 link_persons_accounts_predicate = has_person & has_global_perm("core.link_persons_accounts")
-rules.add_perm("core.link_persons_accounts", link_persons_accounts_predicate)
+rules.add_perm("core.link_persons_accounts_rule", link_persons_accounts_predicate)
 
 # View groups
 view_groups_predicate = has_person & (
     has_global_perm("core.view_group") | has_any_object("core.view_group", Group)
 )
-rules.add_perm("core.view_groups", view_groups_predicate)
+rules.add_perm("core.view_groups_rule", view_groups_predicate)
 
 # View group
 view_group_predicate = has_person & (
     has_global_perm("core.view_group") | has_object_perm("core.view_group")
 )
-rules.add_perm("core.view_group", view_group_predicate)
+rules.add_perm("core.view_group_rule", view_group_predicate)
 
 # Edit group
 edit_group_predicate = has_person & (
     has_global_perm("core.change_group") | has_object_perm("core.change_group")
 )
-rules.add_perm("core.edit_group", edit_group_predicate)
+rules.add_perm("core.edit_group_rule", edit_group_predicate)
 
 # Delete group
 delete_group_predicate = has_person & (
     has_global_perm("core.delete_group") | has_object_perm("core.delete_group")
 )
-rules.add_perm("core.delete_group", delete_group_predicate)
+rules.add_perm("core.delete_group_rule", delete_group_predicate)
 
 # Assign child groups to groups
 assign_child_groups_to_groups_predicate = has_person & has_global_perm(
     "core.assign_child_groups_to_groups"
 )
-rules.add_perm("core.assign_child_groups_to_groups", assign_child_groups_to_groups_predicate)
+rules.add_perm("core.assign_child_groups_to_groups_rule", assign_child_groups_to_groups_predicate)
 
 # Edit school information
 edit_school_information_predicate = has_person & has_global_perm("core.change_school")
-rules.add_perm("core.edit_school_information", edit_school_information_predicate)
+rules.add_perm("core.edit_school_information_rule", edit_school_information_predicate)
 
 # Manage data
 manage_data_predicate = has_person & has_global_perm("core.manage_data")
-rules.add_perm("core.manage_data", manage_data_predicate)
+rules.add_perm("core.manage_data_rule", manage_data_predicate)
 
 # Mark notification as read
 mark_notification_as_read_predicate = has_person & is_notification_recipient
-rules.add_perm("core.mark_notification_as_read", mark_notification_as_read_predicate)
+rules.add_perm("core.mark_notification_as_read_rule", mark_notification_as_read_predicate)
 
 # View announcements
 view_announcements_predicate = has_person & (
     has_global_perm("core.view_announcement")
     | has_any_object("core.view_announcement", Announcement)
 )
-rules.add_perm("core.view_announcements", view_announcements_predicate)
+rules.add_perm("core.view_announcements_rule", view_announcements_predicate)
 
 # Create or edit announcement
 create_or_edit_announcement_predicate = has_person & (
     has_global_perm("core.add_announcement")
     & (has_global_perm("core.change_announcement") | has_object_perm("core.change_announcement"))
 )
-rules.add_perm("core.create_or_edit_announcement", create_or_edit_announcement_predicate)
+rules.add_perm("core.create_or_edit_announcement_rule", create_or_edit_announcement_predicate)
 
 # Delete announcement
 delete_announcement_predicate = has_person & (
     has_global_perm("core.delete_announcement") | has_object_perm("core.delete_announcement")
 )
-rules.add_perm("core.delete_announcement", delete_announcement_predicate)
+rules.add_perm("core.delete_announcement_rule", delete_announcement_predicate)
 
 # Use impersonate
 impersonate_predicate = has_person & has_global_perm("core.impersonate")
-rules.add_perm("core.impersonate", impersonate_predicate)
+rules.add_perm("core.impersonate_rule", impersonate_predicate)
 
 # View system status
 view_system_status_predicate = has_person & has_global_perm("core.view_system_status")
-rules.add_perm("core.view_system_status", view_system_status_predicate)
+rules.add_perm("core.view_system_status_rule", view_system_status_predicate)
 
 # View people menu (persons + objects)
 rules.add_perm(
-    "core.view_people_menu",
+    "core.view_people_menu_rule",
     has_person
     & (
         view_persons_predicate
@@ -172,14 +172,14 @@ view_personal_details_predicate = has_person & (
     | has_object_perm("core.view_personal_details")
     | is_current_person
 )
-rules.add_perm("core.view_personal_details", view_personal_details_predicate)
+rules.add_perm("core.view_personal_details_rule", view_personal_details_predicate)
 
 # Change site preferences
 change_site_preferences = has_person & (
     has_global_perm("core.change_site_preferences")
     | has_object_perm("core.change_site_preferences")
 )
-rules.add_perm("core.change_site_preferences", change_site_preferences)
+rules.add_perm("core.change_site_preferences_rule", change_site_preferences)
 
 # Change person preferences
 change_person_preferences = has_person & (
@@ -187,7 +187,7 @@ change_person_preferences = has_person & (
     | has_object_perm("core.change_person_preferences")
     | is_current_person
 )
-rules.add_perm("core.change_person_preferences", change_person_preferences)
+rules.add_perm("core.change_person_preferences_rule", change_person_preferences)
 
 # Change group preferences
 change_group_preferences = has_person & (
@@ -195,81 +195,81 @@ change_group_preferences = has_person & (
     | has_object_perm("core.change_group_preferences")
     | is_group_owner
 )
-rules.add_perm("core.change_group_preferences", change_group_preferences)
+rules.add_perm("core.change_group_preferences_rule", change_group_preferences)
 
 
 # Edit additional field
 change_additional_field_predicate = has_person & (
     has_global_perm("core.change_additionalfield") | has_object_perm("core.change_additionalfield")
 )
-rules.add_perm("core.change_additionalfield", change_additional_field_predicate)
+rules.add_perm("core.change_additionalfield_rule", change_additional_field_predicate)
 
 # Edit additional field
 create_additional_field_predicate = has_person & (
     has_global_perm("core.create_additionalfield") | has_object_perm("core.create_additionalfield")
 )
-rules.add_perm("core.create_additionalfield", create_additional_field_predicate)
+rules.add_perm("core.create_additionalfield_rule", create_additional_field_predicate)
 
 
 # Delete additional field
 delete_additional_field_predicate = has_person & (
     has_global_perm("core.delete_additionalfield") | has_object_perm("core.delete_additionalfield")
 )
-rules.add_perm("core.delete_additionalfield", delete_additional_field_predicate)
+rules.add_perm("core.delete_additionalfield_rule", delete_additional_field_predicate)
 
 # View additional fields
 view_additional_fields_predicate = has_person & (
     has_global_perm("core.view_additionalfield")
     | has_any_object("core.view_additionalfield", AdditionalField)
 )
-rules.add_perm("core.view_additionalfields", view_additional_fields_predicate)
+rules.add_perm("core.view_additionalfields_rule", view_additional_fields_predicate)
 
 # Edit group type
 change_group_type_predicate = has_person & (
     has_global_perm("core.change_grouptype") | has_object_perm("core.change_grouptype")
 )
-rules.add_perm("core.edit_grouptype", change_group_type_predicate)
+rules.add_perm("core.edit_grouptype_rule", change_group_type_predicate)
 
 # Create group type
 create_group_type_predicate = has_person & (
     has_global_perm("core.create_grouptype") | has_object_perm("core.change_grouptype")
 )
-rules.add_perm("core.create_grouptype", create_group_type_predicate)
+rules.add_perm("core.create_grouptype_rule", create_group_type_predicate)
 
 
 # Delete group type
 delete_group_type_predicate = has_person & (
     has_global_perm("core.delete_grouptype") | has_object_perm("core.delete_grouptype")
 )
-rules.add_perm("core.delete_grouptype", delete_group_type_predicate)
+rules.add_perm("core.delete_grouptype_rule", delete_group_type_predicate)
 
 # View group types
 view_group_types_predicate = has_person & (
     has_global_perm("core.view_grouptype") | has_any_object("core.view_grouptype", GroupType)
 )
-rules.add_perm("core.view_grouptypes", view_group_types_predicate)
+rules.add_perm("core.view_grouptypes_rule", view_group_types_predicate)
 
 # Create person
 create_person_predicate = has_person & (
     has_global_perm("core.create_person") | has_object_perm("core.create_person")
 )
-rules.add_perm("core.create_person", create_person_predicate)
+rules.add_perm("core.create_person_rule", create_person_predicate)
 
 # Create group
 create_group_predicate = has_person & (
     has_global_perm("core.create_group") | has_object_perm("core.create_group")
 )
-rules.add_perm("core.create_group", create_group_predicate)
+rules.add_perm("core.create_group_rule", create_group_predicate)
 
 # School years
 view_school_term_predicate = has_person & has_global_perm("core.view_schoolterm")
-rules.add_perm("core.view_schoolterm", view_school_term_predicate)
+rules.add_perm("core.view_schoolterm_rule", view_school_term_predicate)
 
 create_school_term_predicate = has_person & has_global_perm("core.add_schoolterm")
-rules.add_perm("core.create_schoolterm", create_school_term_predicate)
+rules.add_perm("core.create_schoolterm_rule", create_school_term_predicate)
 
 edit_school_term_predicate = has_person & has_global_perm("core.change_schoolterm")
-rules.add_perm("core.edit_schoolterm", edit_school_term_predicate)
+rules.add_perm("core.edit_schoolterm_rule", edit_school_term_predicate)
 
 # View admin menu
 view_admin_menu_predicate = has_person & (
@@ -279,47 +279,47 @@ view_admin_menu_predicate = has_person & (
     | view_system_status_predicate
     | view_announcements_predicate
 )
-rules.add_perm("core.view_admin_menu", view_admin_menu_predicate)
+rules.add_perm("core.view_admin_menu_rule", view_admin_menu_predicate)
 
 # View group stats
 view_group_stats_predicate = has_person & (
     has_global_perm("core.view_group_stats") | has_object_perm("core.view_group_stats")
 )
-rules.add_perm("core.view_group_stats", view_group_stats_predicate)
+rules.add_perm("core.view_group_stats_rule", view_group_stats_predicate)
 
 # View data check results
 view_data_check_results_predicate = has_person & has_global_perm("core.view_datacheckresult")
-rules.add_perm("core.view_datacheckresults", view_data_check_results_predicate)
+rules.add_perm("core.view_datacheckresults_rule", view_data_check_results_predicate)
 
 # Run data checks
 run_data_checks_predicate = (
     has_person & view_data_check_results_predicate & has_global_perm("core.run_data_checks")
 )
-rules.add_perm("core.run_data_checks", run_data_checks_predicate)
+rules.add_perm("core.run_data_checks_rule", run_data_checks_predicate)
 
 # Solve data problems
 solve_data_problem_predicate = (
     has_person & view_data_check_results_predicate & has_global_perm("core.solve_data_problem")
 )
-rules.add_perm("core.solve_data_problem", solve_data_problem_predicate)
+rules.add_perm("core.solve_data_problem_rule", solve_data_problem_predicate)
 
 view_dashboard_widget_predicate = has_person & has_global_perm("core.view_dashboardwidget")
-rules.add_perm("core.view_dashboardwidget", view_dashboard_widget_predicate)
+rules.add_perm("core.view_dashboardwidget_rule", view_dashboard_widget_predicate)
 
 create_dashboard_widget_predicate = has_person & has_global_perm("core.add_dashboardwidget")
-rules.add_perm("core.create_dashboardwidget", create_dashboard_widget_predicate)
+rules.add_perm("core.create_dashboardwidget_rule", create_dashboard_widget_predicate)
 
 edit_dashboard_widget_predicate = has_person & has_global_perm("core.change_dashboardwidget")
-rules.add_perm("core.edit_dashboardwidget", edit_dashboard_widget_predicate)
+rules.add_perm("core.edit_dashboardwidget_rule", edit_dashboard_widget_predicate)
 
 delete_dashboard_widget_predicate = has_person & has_global_perm("core.delete_dashboardwidget")
-rules.add_perm("core.delete_dashboardwidget", delete_dashboard_widget_predicate)
+rules.add_perm("core.delete_dashboardwidget_rule", delete_dashboard_widget_predicate)
 
 edit_dashboard_predicate = is_site_preference_set("general", "dashboard_editing") & has_person
-rules.add_perm("core.edit_dashboard", edit_dashboard_predicate)
+rules.add_perm("core.edit_dashboard_rule", edit_dashboard_predicate)
 
 edit_default_dashboard_predicate = has_person & has_global_perm("core.edit_default_dashboard")
-rules.add_perm("core.edit_default_dashboard", edit_default_dashboard_predicate)
+rules.add_perm("core.edit_default_dashboard_rule", edit_default_dashboard_predicate)
 
 # django-allauth
 can_register_predicate = is_site_preference_set(section="auth", pref="signup_enabled")
@@ -330,26 +330,26 @@ rules.add_perm("core.can_change_password", can_change_password_predicate)
 
 # OAuth2 permissions
 add_oauth_applications_predicate = has_person & has_global_perm("core.add_oauth_applications")
-rules.add_perm("core.add_oauth_applications", add_oauth_applications_predicate)
+rules.add_perm("core.add_oauth_applications_rule", add_oauth_applications_predicate)
 
 list_oauth_applications_predicate = has_person & has_global_perm("core.list_oauth_applications")
-rules.add_perm("core.list_oauth_applications", list_oauth_applications_predicate)
+rules.add_perm("core.list_oauth_applications_rule", list_oauth_applications_predicate)
 
 view_oauth_applications_predicate = has_person & has_global_perm("core.view_oauth_applications")
-rules.add_perm("core.view_oauth_applications", view_oauth_applications_predicate)
+rules.add_perm("core.view_oauth_applications_rule", view_oauth_applications_predicate)
 
 update_oauth_applications_predicate = has_person & has_global_perm("core.update_oauth_applications")
-rules.add_perm("core.update_oauth_applications", update_oauth_applications_predicate)
+rules.add_perm("core.update_oauth_applications_rule", update_oauth_applications_predicate)
 
 delete_oauth_applications_predicate = has_person & has_global_perm("core.delete_oauth_applications")
-rules.add_perm("core.delete_oauth_applications", delete_oauth_applications_predicate)
+rules.add_perm("core.delete_oauth_applications_rule", delete_oauth_applications_predicate)
 
 # Upload and browse files via CKEditor
 upload_files_ckeditor_predicate = has_person & has_global_perm("core.upload_files_ckeditor")
-rules.add_perm("core.upload_files_ckeditor", upload_files_ckeditor_predicate)
+rules.add_perm("core.upload_files_ckeditor_rule", upload_files_ckeditor_predicate)
 
 test_pdf_generation_predicate = has_person & has_global_perm("core.test_pdf")
-rules.add_perm("core.test_pdf", test_pdf_generation_predicate)
+rules.add_perm("core.test_pdf_rule", test_pdf_generation_predicate)
 
 # Generate rules for syncable fields
 for field in Person._meta.fields:
@@ -361,4 +361,4 @@ for field in Person._meta.fields:
             & contains_site_preference_value("account", "editable_fields_person", field.name)
         )
     )
-    rules.add_perm(f"core.change_person_field_{field.name}", perm)
+    rules.add_perm(f"core.change_person_field_{field.name}_rule", perm)
diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py
index c20bdfe033647fd849f0df33165fd0507674ad8c..118560120edca43baf2cd62021bc6d73b16dc516 100644
--- a/aleksis/core/settings.py
+++ b/aleksis/core/settings.py
@@ -235,8 +235,6 @@ if _settings.get("caching.redis.enabled", not IN_PYTEST):
     }
     if REDIS_PASSWORD:
         CACHES["default"]["OPTIONS"]["PASSWORD"] = REDIS_PASSWORD
-    DJANGO_REDIS_IGNORE_EXCEPTIONS = True
-    DJANGO_REDIS_LOG_IGNORED_EXCEPTIONS = True
 else:
     CACHES = {
         "default": {
@@ -817,26 +815,9 @@ SILENCED_SYSTEM_CHECKS.append("guardian.W001")
 # Append authentication backends
 AUTHENTICATION_BACKENDS.append("rules.permissions.ObjectPermissionBackend")
 
-HAYSTACK_BACKEND_SHORT = _settings.get("search.backend", "simple")
-
-if HAYSTACK_BACKEND_SHORT == "simple":
-    HAYSTACK_CONNECTIONS = {
-        "default": {"ENGINE": "haystack.backends.simple_backend.SimpleEngine",},
-    }
-elif HAYSTACK_BACKEND_SHORT == "xapian":
-    HAYSTACK_CONNECTIONS = {
-        "default": {
-            "ENGINE": "xapian_backend.XapianEngine",
-            "PATH": _settings.get("search.index", os.path.join(BASE_DIR, "xapian_index")),
-        },
-    }
-elif HAYSTACK_BACKEND_SHORT == "whoosh":
-    HAYSTACK_CONNECTIONS = {
-        "default": {
-            "ENGINE": "haystack.backends.whoosh_backend.WhooshEngine",
-            "PATH": _settings.get("search.index", os.path.join(BASE_DIR, "whoosh_index")),
-        },
-    }
+HAYSTACK_CONNECTIONS = {
+    "default": {"ENGINE": "haystack_redis.RedisEngine", "PATH": REDIS_URL,},
+}
 
 HAYSTACK_SIGNAL_PROCESSOR = "celery_haystack.signals.CelerySignalProcessor"
 CELERY_HAYSTACK_IGNORE_RESULT = True
diff --git a/aleksis/core/templates/search/search.html b/aleksis/core/templates/search/search.html
index caff1cc6f62f30c67dcb0e2fe082dfa08db389a8..514661df42edecac7ca223c1c41fe4dae440b5ee 100644
--- a/aleksis/core/templates/search/search.html
+++ b/aleksis/core/templates/search/search.html
@@ -27,7 +27,7 @@
 
     {% if query %}
       <div class="collection">
-        {% for result in page.object_list %}
+        {% for result in page_obj.object_list %}
           <a href="{{ result.object.get_absolute_url|default:"#" }}" class="collection-item">
             <i class="material-icons left">{{ result.object.icon_ }}</i>
             {{ result.object }}
@@ -41,11 +41,11 @@
         {% endfor %}
       </div>
 
-      {% if page.has_other_pages %}
+      {% if page_obj.has_other_pages %}
         <ul class="pagination">
-          {% if page.has_previous %}
+          {% if page_obj.has_previous %}
             <li class="waves-effect">
-              <a href="?q={{ query }}&amp;page={{ page.previous_page_number }}">
+              <a href="?q={{ query }}&amp;page={{ page_obj.previous_page_number }}">
                 <i class="material-icons">chevron_left</i>
               </a>
             </li>
@@ -55,8 +55,8 @@
             </li>
           {% endif %}
 
-          {% for page_num in page.paginator.page_range %}
-            {% if page.number == page_num %}
+          {% for page_num in page_obj.paginator.page_range %}
+            {% if page_obj.number == page_num %}
               <li class="active">
                 <a href="#">{{ page_num }}</a>
               </li>
@@ -67,9 +67,9 @@
             {% endif %}
           {% endfor %}
 
-          {% if page.has_next %}
+          {% if page_obj.has_next %}
             <li class="waves-effect">
-              <a href="?q={{ query }}&amp;page={{ page.next_page_number }}">
+              <a href="?q={{ query }}&amp;page={{ page_obj.next_page_number }}">
                 <i class="material-icons">chevron_right</i>
               </a>
             </li>
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index 54e6c75b5874326daa8262a863da52f6e61f5173..7aa74920384822a3d38728d8f8e4e1925b31e6f9 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -84,7 +84,7 @@ urlpatterns = [
     path("announcement/edit/<int:id_>/", views.announcement_form, name="edit_announcement"),
     path("announcement/delete/<int:id_>/", views.delete_announcement, name="delete_announcement"),
     path("search/searchbar/", views.searchbar_snippets, name="searchbar_snippets"),
-    path("search/", views.PermissionSearchView(), name="haystack_search"),
+    path("search/", views.PermissionSearchView.as_view(), name="haystack_search"),
     path("maintenance-mode/", include("maintenance_mode.urls")),
     path("impersonate/", include("impersonate.urls")),
     path(
diff --git a/aleksis/core/util/core_helpers.py b/aleksis/core/util/core_helpers.py
index a1423bd5b4c15dd50d0665e1257b822bf55115c0..1124ad65ca344a0ea2e849f40011c0674ad4ec41 100644
--- a/aleksis/core/util/core_helpers.py
+++ b/aleksis/core/util/core_helpers.py
@@ -4,8 +4,10 @@ from importlib import import_module, metadata
 from itertools import groupby
 from operator import itemgetter
 from typing import Any, Callable, Optional, Sequence, Union
+from warnings import warn
 
 from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
 from django.core.files import File
 from django.db.models import Model, QuerySet
 from django.http import HttpRequest
@@ -130,6 +132,12 @@ def get_or_create_favicon(title: str, default: str, is_favicon: bool = False) ->
     """Ensure that there is always a favicon object."""
     from favicon.models import Favicon  # noqa
 
+    if not os.path.exists(default):
+        warn("staticfiles are not ready yet, not creating default icons")
+        return
+    elif os.path.isdir(default):
+        raise ImproperlyConfigured(f"staticfiles are broken: unexpected directory at {default}")
+
     favicon, created = Favicon.on_site.get_or_create(
         title=title, defaults={"isFavicon": is_favicon}
     )
@@ -248,12 +256,6 @@ def objectgetter_optional(
     return get_object
 
 
-def handle_uploaded_file(f, filename: str):
-    with open(filename, "wb+") as destination:
-        for chunk in f.chunks():
-            destination.write(chunk)
-
-
 @cache_memoize(3600)
 def get_content_type_by_perm(perm: str) -> Union["ContentType", None]:
     from django.contrib.contenttypes.models import ContentType  # noqa
@@ -300,3 +302,23 @@ def monkey_patch() -> None:  # noqa
             return super().default(o)
 
     json.DjangoJSONEncoder = DjangoJSONEncoder
+
+
+def get_allowed_object_ids(request: HttpRequest, models: list) -> list:
+    """Get all objects of all given models the user of a given request is allowed to view."""
+    allowed_object_ids = []
+
+    for model in models:
+        app_label = model._meta.app_label
+        model_name = model.__name__.lower()
+
+        # Loop through the pks of all objects of the current model the user is allowed to view
+        # and put the corresponding ids into a django-haystack-style-formatted list
+        allowed_object_ids += [
+            f"{app_label}.{model_name}.{pk}"
+            for pk in queryset_rules_filter(
+                request, model.objects.all(), f"{app_label}.view_{model_name}_rule"
+            ).values_list("pk", flat=True)
+        ]
+
+    return allowed_object_ids
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index 266bdb3a093475f95e4f8e4e3ad92c3b88438bd3..9492d0a6eabbd95edd7cb54002f118c4f7560993 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -25,9 +25,10 @@ from django_celery_results.models import TaskResult
 from django_tables2 import RequestConfig, SingleTableView
 from dynamic_preferences.forms import preference_form_builder
 from guardian.shortcuts import get_objects_for_user
+from haystack.generic_views import SearchView
 from haystack.inputs import AutoQuery
 from haystack.query import SearchQuerySet
-from haystack.views import SearchView
+from haystack.utils.loading import UnifiedIndex
 from health_check.views import MainView
 from oauth2_provider.models import Application
 from reversion import set_user
@@ -85,7 +86,12 @@ from .tables import (
 from .util import messages
 from .util.apps import AppConfig
 from .util.celery_progress import render_progress_page
-from .util.core_helpers import get_site_preferences, has_person, objectgetter_optional
+from .util.core_helpers import (
+    get_allowed_object_ids,
+    get_site_preferences,
+    has_person,
+    objectgetter_optional,
+)
 from .util.forms import PreferenceLayout
 from .util.pdf import render_pdf
 
@@ -101,7 +107,7 @@ class RenderPDFView(TemplateView):
         return render_pdf(request, self.template_name, context)
 
 
-@permission_required("core.view_dashboard")
+@permission_required("core.view_dashboard_rule")
 def index(request: HttpRequest) -> HttpResponse:
     """View for dashboard."""
     context = {}
@@ -138,7 +144,7 @@ def index(request: HttpRequest) -> HttpResponse:
 
 
 class NotificationsListView(PermissionRequiredMixin, ListView):
-    permission_required = "core.view_notifications"
+    permission_required = "core.view_notifications_rule"
     template_name = "core/notifications.html"
 
     def get_queryset(self) -> QuerySet:
@@ -165,7 +171,7 @@ class SchoolTermListView(PermissionRequiredMixin, SingleTableView):
 
     model = SchoolTerm
     table_class = SchoolTermTable
-    permission_required = "core.view_schoolterm"
+    permission_required = "core.view_schoolterm_rule"
     template_name = "core/school_term/list.html"
 
 
@@ -175,7 +181,7 @@ class SchoolTermCreateView(PermissionRequiredMixin, AdvancedCreateView):
 
     model = SchoolTerm
     form_class = SchoolTermForm
-    permission_required = "core.add_schoolterm"
+    permission_required = "core.add_schoolterm_rule"
     template_name = "core/school_term/create.html"
     success_url = reverse_lazy("school_terms")
     success_message = _("The school term has been created.")
@@ -193,7 +199,7 @@ class SchoolTermEditView(PermissionRequiredMixin, AdvancedEditView):
     success_message = _("The school term has been saved.")
 
 
-@permission_required("core.view_persons")
+@permission_required("core.view_persons_rule")
 def persons(request: HttpRequest) -> HttpResponse:
     """List view listing all persons."""
     context = {}
@@ -216,7 +222,7 @@ def persons(request: HttpRequest) -> HttpResponse:
 
 
 @permission_required(
-    "core.view_person", fn=objectgetter_optional(Person, "request.user.person", True)
+    "core.view_person_rule", fn=objectgetter_optional(Person, "request.user.person", True)
 )
 def person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
     """Detail view for one person; defaulting to logged-in person."""
@@ -236,7 +242,7 @@ def person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
     return render(request, "core/person/full.html", context)
 
 
-@permission_required("core.view_group", fn=objectgetter_optional(Group, None, False))
+@permission_required("core.view_group_rule", fn=objectgetter_optional(Group, None, False))
 def group(request: HttpRequest, id_: int) -> HttpResponse:
     """Detail view for one group."""
     context = {}
@@ -269,7 +275,7 @@ def group(request: HttpRequest, id_: int) -> HttpResponse:
     return render(request, "core/group/full.html", context)
 
 
-@permission_required("core.view_groups")
+@permission_required("core.view_groups_rule")
 def groups(request: HttpRequest) -> HttpResponse:
     """List view for listing all groups."""
     context = {}
@@ -290,7 +296,7 @@ def groups(request: HttpRequest) -> HttpResponse:
 
 
 @never_cache
-@permission_required("core.link_persons_accounts")
+@permission_required("core.link_persons_accounts_rule")
 def persons_accounts(request: HttpRequest) -> HttpResponse:
     """View allowing to batch-process linking of users to persons."""
     context = {}
@@ -311,7 +317,7 @@ def persons_accounts(request: HttpRequest) -> HttpResponse:
 
 
 @never_cache
-@permission_required("core.assign_child_groups_to_groups")
+@permission_required("core.assign_child_groups_to_groups_rule")
 def groups_child_groups(request: HttpRequest) -> HttpResponse:
     """View for batch-processing assignment from child groups to groups."""
     context = {}
@@ -349,7 +355,7 @@ def groups_child_groups(request: HttpRequest) -> HttpResponse:
 
 
 @never_cache
-@permission_required("core.edit_person", fn=objectgetter_optional(Person))
+@permission_required("core.edit_person_rule", fn=objectgetter_optional(Person))
 def edit_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
     """Edit view for a single person, defaulting to logged-in person."""
     context = {}
@@ -364,7 +370,7 @@ def edit_person(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse
         )
     else:
         # Empty form to create a new group
-        if request.user.has_perm("core.create_person"):
+        if request.user.has_perm("core.create_person_rule"):
             edit_person_form = EditPersonForm(request, request.POST or None, request.FILES or None)
         else:
             raise PermissionDenied()
@@ -411,7 +417,7 @@ def get_group_by_id(request: HttpRequest, id_: Optional[int] = None):
 
 
 @never_cache
-@permission_required("core.edit_group", fn=objectgetter_optional(Group, None, False))
+@permission_required("core.edit_group_rule", fn=objectgetter_optional(Group, None, False))
 def edit_group(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
     """View to edit or create a group."""
     context = {}
@@ -424,7 +430,7 @@ def edit_group(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
         edit_group_form = EditGroupForm(request.POST or None, instance=group)
     else:
         # Empty form to create a new group
-        if request.user.has_perm("core.create_group"):
+        if request.user.has_perm("core.create_group_rule"):
             edit_group_form = EditGroupForm(request.POST or None)
         else:
             raise PermissionDenied()
@@ -444,7 +450,7 @@ def edit_group(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
     return render(request, "core/group/edit.html", context)
 
 
-@permission_required("core.manage_data")
+@permission_required("core.manage_data_rule")
 def data_management(request: HttpRequest) -> HttpResponse:
     """View with special menu for data management."""
     context = {}
@@ -455,7 +461,7 @@ class SystemStatus(PermissionRequiredMixin, MainView):
     """View giving information about the system status."""
 
     template_name = "core/pages/system_status.html"
-    permission_required = "core.view_system_status"
+    permission_required = "core.view_system_status_rule"
     context = {}
 
     def get(self, request, *args, **kwargs):
@@ -480,11 +486,11 @@ class SystemStatus(PermissionRequiredMixin, MainView):
 
 class TestPDFGenerationView(PermissionRequiredMixin, RenderPDFView):
     template_name = "core/pages/test_pdf.html"
-    permission_required = "core.test_pdf"
+    permission_required = "core.test_pdf_rule"
 
 
 @permission_required(
-    "core.mark_notification_as_read", fn=objectgetter_optional(Notification, None, False)
+    "core.mark_notification_as_read_rule", fn=objectgetter_optional(Notification, None, False)
 )
 def notification_mark_read(request: HttpRequest, id_: int) -> HttpResponse:
     """Mark a notification read."""
@@ -497,7 +503,7 @@ def notification_mark_read(request: HttpRequest, id_: int) -> HttpResponse:
     return redirect("index")
 
 
-@permission_required("core.view_announcements")
+@permission_required("core.view_announcements_rule")
 def announcements(request: HttpRequest) -> HttpResponse:
     """List view of announcements."""
     context = {}
@@ -511,7 +517,7 @@ def announcements(request: HttpRequest) -> HttpResponse:
 
 @never_cache
 @permission_required(
-    "core.create_or_edit_announcement", fn=objectgetter_optional(Announcement, None, False)
+    "core.create_or_edit_announcement_rule", fn=objectgetter_optional(Announcement, None, False)
 )
 def announcement_form(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
     """View to create or edit an announcement."""
@@ -541,7 +547,7 @@ def announcement_form(request: HttpRequest, id_: Optional[int] = None) -> HttpRe
 
 
 @permission_required(
-    "core.delete_announcement", fn=objectgetter_optional(Announcement, None, False)
+    "core.delete_announcement_rule", fn=objectgetter_optional(Announcement, None, False)
 )
 def delete_announcement(request: HttpRequest, id_: int) -> HttpResponse:
     """View to delete an announcement."""
@@ -553,13 +559,16 @@ def delete_announcement(request: HttpRequest, id_: int) -> HttpResponse:
     return redirect("announcements")
 
 
-@permission_required("core.search")
+@permission_required("core.search_rule")
 def searchbar_snippets(request: HttpRequest) -> HttpResponse:
     """View to return HTML snippet with searchbar autocompletion results."""
     query = request.GET.get("q", "")
     limit = int(request.GET.get("limit", "5"))
-
-    results = SearchQuerySet().filter(text=AutoQuery(query))[:limit]
+    indexed_models = UnifiedIndex().get_indexed_models()
+    allowed_object_ids = get_allowed_object_ids(request, indexed_models)
+    results = (
+        SearchQuerySet().filter(id__in=allowed_object_ids).filter(text=AutoQuery(query))[:limit]
+    )
     context = {"results": results}
 
     return render(request, "search/searchbar_snippets.html", context)
@@ -568,13 +577,15 @@ def searchbar_snippets(request: HttpRequest) -> HttpResponse:
 class PermissionSearchView(PermissionRequiredMixin, SearchView):
     """Wrapper to apply permission to haystack's search view."""
 
-    permission_required = "core.search"
+    permission_required = "core.search_rule"
+
+    def get_context_data(self, *, object_list=None, **kwargs):
+        queryset = object_list if object_list is not None else self.object_list
+        indexed_models = UnifiedIndex().get_indexed_models()
+        allowed_object_ids = get_allowed_object_ids(self.request, indexed_models)
+        queryset = queryset.filter(id__in=allowed_object_ids)
 
-    def create_response(self):
-        context = self.get_context()
-        if not self.has_permission():
-            return self.handle_no_permission()
-        return render(self.request, self.template, context)
+        return super().get_context_data(object_list=queryset, **kwargs)
 
 
 @never_cache
@@ -593,21 +604,21 @@ def preferences(
         instance = request.site
         form_class = SitePreferenceForm
 
-        if not request.user.has_perm("core.change_site_preferences", instance):
+        if not request.user.has_perm("core.change_site_preferences_rule", instance):
             raise PermissionDenied()
     elif registry_name == "person":
         registry = person_preferences_registry
         instance = objectgetter_optional(Person, "request.user.person", True)(request, pk)
         form_class = PersonPreferenceForm
 
-        if not request.user.has_perm("core.change_person_preferences", instance):
+        if not request.user.has_perm("core.change_person_preferences_rule", instance):
             raise PermissionDenied()
     elif registry_name == "group":
         registry = group_preferences_registry
         instance = objectgetter_optional(Group, None, False)(request, pk)
         form_class = GroupPreferenceForm
 
-        if not request.user.has_perm("core.change_group_preferences", instance):
+        if not request.user.has_perm("core.change_group_preferences_rule", instance):
             raise PermissionDenied()
     else:
         # Invalid registry name passed from URL
@@ -641,7 +652,7 @@ def preferences(
     return render(request, "dynamic_preferences/form.html", context)
 
 
-@permission_required("core.delete_person", fn=objectgetter_optional(Person))
+@permission_required("core.delete_person_rule", fn=objectgetter_optional(Person))
 def delete_person(request: HttpRequest, id_: int) -> HttpResponse:
     """View to delete an person."""
     person = objectgetter_optional(Person)(request, id_)
@@ -656,7 +667,7 @@ def delete_person(request: HttpRequest, id_: int) -> HttpResponse:
     return redirect("persons")
 
 
-@permission_required("core.delete_group", fn=objectgetter_optional(Group))
+@permission_required("core.delete_group_rule", fn=objectgetter_optional(Group))
 def delete_group(request: HttpRequest, id_: int) -> HttpResponse:
     """View to delete an group."""
     group = objectgetter_optional(Group)(request, id_)
@@ -672,7 +683,7 @@ def delete_group(request: HttpRequest, id_: int) -> HttpResponse:
 
 @never_cache
 @permission_required(
-    "core.change_additionalfield", fn=objectgetter_optional(AdditionalField, None, False)
+    "core.change_additionalfield_rule", fn=objectgetter_optional(AdditionalField, None, False)
 )
 def edit_additional_field(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
     """View to edit or create a additional_field."""
@@ -687,7 +698,7 @@ def edit_additional_field(request: HttpRequest, id_: Optional[int] = None) -> Ht
             request.POST or None, instance=additional_field
         )
     else:
-        if request.user.has_perm("core.create_additionalfield"):
+        if request.user.has_perm("core.create_additionalfield_rule"):
             # Empty form to create a new additional_field
             edit_additional_field_form = EditAdditionalFieldForm(request.POST or None)
         else:
@@ -706,7 +717,7 @@ def edit_additional_field(request: HttpRequest, id_: Optional[int] = None) -> Ht
     return render(request, "core/additional_field/edit.html", context)
 
 
-@permission_required("core.view_additionalfields")
+@permission_required("core.view_additionalfields_rule")
 def additional_fields(request: HttpRequest) -> HttpResponse:
     """List view for listing all additional fields."""
     context = {}
@@ -725,7 +736,7 @@ def additional_fields(request: HttpRequest) -> HttpResponse:
 
 
 @permission_required(
-    "core.delete_additionalfield", fn=objectgetter_optional(AdditionalField, None, False)
+    "core.delete_additionalfield_rule", fn=objectgetter_optional(AdditionalField, None, False)
 )
 def delete_additional_field(request: HttpRequest, id_: int) -> HttpResponse:
     """View to delete an additional field."""
@@ -737,7 +748,7 @@ def delete_additional_field(request: HttpRequest, id_: int) -> HttpResponse:
 
 
 @never_cache
-@permission_required("core.change_grouptype", fn=objectgetter_optional(GroupType, None, False))
+@permission_required("core.change_grouptype_rule", fn=objectgetter_optional(GroupType, None, False))
 def edit_group_type(request: HttpRequest, id_: Optional[int] = None) -> HttpResponse:
     """View to edit or create a group_type."""
     context = {}
@@ -765,7 +776,7 @@ def edit_group_type(request: HttpRequest, id_: Optional[int] = None) -> HttpResp
     return render(request, "core/group_type/edit.html", context)
 
 
-@permission_required("core.view_grouptypes")
+@permission_required("core.view_grouptypes_rule")
 def group_types(request: HttpRequest) -> HttpResponse:
     """List view for listing all group types."""
     context = {}
@@ -781,7 +792,7 @@ def group_types(request: HttpRequest) -> HttpResponse:
     return render(request, "core/group_type/list.html", context)
 
 
-@permission_required("core.delete_grouptype", fn=objectgetter_optional(GroupType, None, False))
+@permission_required("core.delete_grouptype_rule", fn=objectgetter_optional(GroupType, None, False))
 def delete_group_type(request: HttpRequest, id_: int) -> HttpResponse:
     """View to delete an group_type."""
     group_type = objectgetter_optional(GroupType, None, False)(request, id_)
@@ -792,7 +803,7 @@ def delete_group_type(request: HttpRequest, id_: int) -> HttpResponse:
 
 
 class DataCheckView(PermissionRequiredMixin, ListView):
-    permission_required = "core.view_datacheckresults"
+    permission_required = "core.view_datacheckresults_rule"
     model = DataCheckResult
     template_name = "core/data_check/list.html"
     context_object_name = "results"
@@ -811,7 +822,7 @@ class DataCheckView(PermissionRequiredMixin, ListView):
 
 
 class RunDataChecks(PermissionRequiredMixin, View):
-    permission_required = "core.run_data_checks"
+    permission_required = "core.run_data_checks_rule"
 
     def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
         result = check_data.delay()
@@ -829,7 +840,7 @@ class RunDataChecks(PermissionRequiredMixin, View):
 
 class SolveDataCheckView(PermissionRequiredMixin, RevisionMixin, DetailView):
     queryset = DataCheckResult.objects.all()
-    permission_required = "core.solve_data_problem"
+    permission_required = "core.solve_data_problem_rule"
 
     def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
         solve_option = self.kwargs["solve_option"]
@@ -856,7 +867,7 @@ class DashboardWidgetListView(PermissionRequiredMixin, SingleTableView):
 
     model = DashboardWidget
     table_class = DashboardWidgetTable
-    permission_required = "core.view_dashboardwidget"
+    permission_required = "core.view_dashboardwidget_rule"
     template_name = "core/dashboard_widget/list.html"
 
     def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
@@ -877,7 +888,7 @@ class DashboardWidgetEditView(PermissionRequiredMixin, AdvancedEditView):
 
     model = DashboardWidget
     fields = "__all__"
-    permission_required = "core.edit_dashboardwidget"
+    permission_required = "core.edit_dashboardwidget_rule"
     template_name = "core/dashboard_widget/edit.html"
     success_url = reverse_lazy("dashboard_widgets")
     success_message = _("The dashboard widget has been saved.")
@@ -907,7 +918,7 @@ class DashboardWidgetCreateView(PermissionRequiredMixin, AdvancedCreateView):
         return super().post(request, *args, **kwargs)
 
     fields = "__all__"
-    permission_required = "core.add_dashboardwidget"
+    permission_required = "core.add_dashboardwidget_rule"
     template_name = "core/dashboard_widget/create.html"
     success_url = reverse_lazy("dashboard_widgets")
     success_message = _("The dashboard widget has been created.")
@@ -917,7 +928,7 @@ class DashboardWidgetDeleteView(PermissionRequiredMixin, AdvancedDeleteView):
     """Delete view for dashboard widgets."""
 
     model = DashboardWidget
-    permission_required = "core.delete_dashboardwidget"
+    permission_required = "core.delete_dashboardwidget_rule"
     template_name = "core/pages/delete.html"
     success_url = reverse_lazy("dashboard_widgets")
     success_message = _("The dashboard widget has been deleted.")
@@ -926,13 +937,13 @@ class DashboardWidgetDeleteView(PermissionRequiredMixin, AdvancedDeleteView):
 class EditDashboardView(PermissionRequiredMixin, View):
     """View for editing dashboard widget order."""
 
-    permission_required = "core.edit_dashboard"
+    permission_required = "core.edit_dashboard_rule"
 
     def get_context_data(self, request, **kwargs):
         context = {}
         self.default_dashboard = kwargs.get("default", False)
 
-        if self.default_dashboard and not request.user.has_perm("core.edit_default_dashboard"):
+        if self.default_dashboard and not request.user.has_perm("core.edit_default_dashboard_rule"):
             raise PermissionDenied()
 
         context["default_dashboard"] = self.default_dashboard
@@ -1002,7 +1013,7 @@ class EditDashboardView(PermissionRequiredMixin, View):
 class OAuth2List(PermissionRequiredMixin, ListView):
     """List view for all the applications."""
 
-    permission_required = "core.list_oauth_applications"
+    permission_required = "core.list_oauth_applications_rule"
     context_object_name = "applications"
     template_name = "oauth2_provider/application_list.html"
 
@@ -1014,7 +1025,7 @@ class OAuth2Detail(PermissionRequiredMixin, DetailView):
     """Detail view for an application instance."""
 
     context_object_name = "application"
-    permission_required = "core.view_oauth_applications"
+    permission_required = "core.view_oauth_applications_rule"
     template_name = "oauth2_provider/application_detail.html"
 
     def get_queryset(self):
@@ -1024,7 +1035,7 @@ class OAuth2Detail(PermissionRequiredMixin, DetailView):
 class OAuth2Delete(PermissionRequiredMixin, DeleteView):
     """View used to delete an application."""
 
-    permission_required = "core.delete_oauth_applications"
+    permission_required = "core.delete_oauth_applications_rule"
     context_object_name = "application"
     success_url = reverse_lazy("oauth_list")
     template_name = "oauth2_provider/application_confirm_delete.html"
@@ -1036,7 +1047,7 @@ class OAuth2Delete(PermissionRequiredMixin, DeleteView):
 class OAuth2Update(PermissionRequiredMixin, UpdateView):
     """View used to update an application."""
 
-    permission_required = "core.update_oauth_applications"
+    permission_required = "core.update_oauth_applications_rule"
     context_object_name = "application"
     template_name = "oauth2_provider/application_form.html"
 
diff --git a/poetry.lock b/poetry.lock
index e7fc4cce47d1ee3cd54efa7b97800aac12adde9b..b8adc22252e52d9502edcb4204db335473f9b339 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -214,20 +214,20 @@ python-versions = "*"
 
 [[package]]
 name = "boto3"
-version = "1.17.74"
+version = "1.17.76"
 description = "The AWS SDK for Python"
 category = "main"
 optional = true
 python-versions = ">= 2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
 
 [package.dependencies]
-botocore = ">=1.20.74,<1.21.0"
+botocore = ">=1.20.76,<1.21.0"
 jmespath = ">=0.7.1,<1.0.0"
 s3transfer = ">=0.4.0,<0.5.0"
 
 [[package]]
 name = "botocore"
-version = "1.20.74"
+version = "1.20.76"
 description = "Low-level, data-driven core of boto 3."
 category = "main"
 optional = true
@@ -1455,6 +1455,19 @@ python-versions = ">=3.5"
 [package.dependencies]
 gitdb = ">=4.0.1,<5"
 
+[[package]]
+name = "haystack-redis"
+version = "0.0.1"
+description = "Use redis as a persistence layer for Whoosh and Haystack"
+category = "main"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+django-haystack = "*"
+redis = "*"
+whoosh = "*"
+
 [[package]]
 name = "html2text"
 version = "2020.1.16"
@@ -1557,14 +1570,14 @@ testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"]
 
 [[package]]
 name = "jinja2"
-version = "3.0.0"
+version = "3.0.1"
 description = "A very fast and expressive template engine."
 category = "dev"
 optional = false
 python-versions = ">=3.6"
 
 [package.dependencies]
-MarkupSafe = ">=2.0.0rc2"
+MarkupSafe = ">=2.0"
 
 [package.extras]
 i18n = ["Babel (>=2.7)"]
@@ -1769,7 +1782,7 @@ ptyprocess = ">=0.5"
 
 [[package]]
 name = "pg8000"
-version = "1.19.4"
+version = "1.19.5"
 description = "PostgreSQL interface library"
 category = "dev"
 optional = false
@@ -2629,21 +2642,9 @@ name = "whoosh"
 version = "2.7.4"
 description = "Fast, pure-Python full text indexing, search, and spell checking library."
 category = "main"
-optional = true
-python-versions = "*"
-
-[[package]]
-name = "xapian-haystack"
-version = "2.1.1"
-description = "A Xapian backend for Haystack"
-category = "main"
-optional = true
+optional = false
 python-versions = "*"
 
-[package.dependencies]
-django = ">=1.8"
-django-haystack = ">=2.5.1"
-
 [[package]]
 name = "yubiotp"
 version = "1.0.0.post1"
@@ -2658,13 +2659,11 @@ pycryptodome = "*"
 [extras]
 ldap = ["django-auth-ldap"]
 s3 = ["boto3", "django-storages"]
-whoosh = ["Whoosh"]
-xapian = ["xapian-haystack"]
 
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.9"
-content-hash = "beabb80d3caf7c756cf28689c2c13d10e09ae12b9bc6a65723797f694e635e68"
+content-hash = "13d988ca4c812fa1cd0c68e210f6d8eb58bd512ab40b47e0830b3d0d714eef3f"
 
 [metadata.files]
 alabaster = [
@@ -2673,6 +2672,7 @@ alabaster = [
 ]
 aleksis-builddeps = [
     {file = "AlekSIS-Builddeps-3.tar.gz", hash = "sha256:04597e29a861e576d78adc068c9f1bf85450c81dc43cb1f7db0ed43e975b64a2"},
+    {file = "AlekSIS_Builddeps-3-py3-none-any.whl", hash = "sha256:0de7fdf007afade836a6dcdbf47eb9fb702786572a8e511c6b6ae33ddce1dda6"},
 ]
 amqp = [
     {file = "amqp-5.0.6-py3-none-any.whl", hash = "sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb"},
@@ -2736,12 +2736,12 @@ bleach = [
     {file = "boolean.py-3.8.tar.gz", hash = "sha256:cc24e20f985d60cd4a3a5a1c0956dd12611159d32a75081dabd0c9ab981acaa4"},
 ]
 boto3 = [
-    {file = "boto3-1.17.74-py2.py3-none-any.whl", hash = "sha256:c83a33fff7d20027386552967355508ce71fb7406ab0cc8e627e257c94754d43"},
-    {file = "boto3-1.17.74.tar.gz", hash = "sha256:0a21893db156c0938d0a06b622c3dd3d2da2dcd9d06d343c8f9536ac9de4ec7f"},
+    {file = "boto3-1.17.76-py2.py3-none-any.whl", hash = "sha256:2421478269219131d58e1a7c49f535d32442a4de9f1b9022bffb3269cc6a46cb"},
+    {file = "boto3-1.17.76.tar.gz", hash = "sha256:b46ce7eaeb5a388b7a4a48999c3bb9e4276677bb0c1ee4b9f2bf7a1250d96e5e"},
 ]
 botocore = [
-    {file = "botocore-1.20.74-py2.py3-none-any.whl", hash = "sha256:2061cf3d17615aa4114c91dbed8917adc5287a88354a7693c96aa8e9f9dedd6e"},
-    {file = "botocore-1.20.74.tar.gz", hash = "sha256:6937954ce6dabc00eb157e9fbd21edd45b4dfe3de738e68dbca4c042bfda0954"},
+    {file = "botocore-1.20.76-py2.py3-none-any.whl", hash = "sha256:277456d5eb993bc5ba13bb66e09875e9e8dab846b8722ed0370c7420858dcfbc"},
+    {file = "botocore-1.20.76.tar.gz", hash = "sha256:208d9d2cc7ec725892c0afcab3c76b42049a7f96309b4286f160b527cd64170f"},
 ]
 bs4 = [
     {file = "bs4-0.0.1.tar.gz", hash = "sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a"},
@@ -2782,24 +2782,36 @@ cffi = [
     {file = "cffi-1.14.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058"},
     {file = "cffi-1.14.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5"},
     {file = "cffi-1.14.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132"},
+    {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24ec4ff2c5c0c8f9c6b87d5bb53555bf267e1e6f70e52e5a9740d32861d36b6f"},
+    {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c3f39fa737542161d8b0d680df2ec249334cd70a8f420f71c9304bd83c3cbed"},
+    {file = "cffi-1.14.5-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:681d07b0d1e3c462dd15585ef5e33cb021321588bebd910124ef4f4fb71aef55"},
     {file = "cffi-1.14.5-cp36-cp36m-win32.whl", hash = "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53"},
     {file = "cffi-1.14.5-cp36-cp36m-win_amd64.whl", hash = "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813"},
     {file = "cffi-1.14.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73"},
     {file = "cffi-1.14.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06"},
     {file = "cffi-1.14.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1"},
     {file = "cffi-1.14.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49"},
+    {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06d7cd1abac2ffd92e65c0609661866709b4b2d82dd15f611e602b9b188b0b69"},
+    {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f861a89e0043afec2a51fd177a567005847973be86f709bbb044d7f42fc4e05"},
+    {file = "cffi-1.14.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc5a8e069b9ebfa22e26d0e6b97d6f9781302fe7f4f2b8776c3e1daea35f1adc"},
     {file = "cffi-1.14.5-cp37-cp37m-win32.whl", hash = "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62"},
     {file = "cffi-1.14.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4"},
     {file = "cffi-1.14.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053"},
     {file = "cffi-1.14.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0"},
     {file = "cffi-1.14.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e"},
     {file = "cffi-1.14.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827"},
+    {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04c468b622ed31d408fea2346bec5bbffba2cc44226302a0de1ade9f5ea3d373"},
+    {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:06db6321b7a68b2bd6df96d08a5adadc1fa0e8f419226e25b2a5fbf6ccc7350f"},
+    {file = "cffi-1.14.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:293e7ea41280cb28c6fcaaa0b1aa1f533b8ce060b9e701d78511e1e6c4a1de76"},
     {file = "cffi-1.14.5-cp38-cp38-win32.whl", hash = "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e"},
     {file = "cffi-1.14.5-cp38-cp38-win_amd64.whl", hash = "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396"},
     {file = "cffi-1.14.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea"},
     {file = "cffi-1.14.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322"},
     {file = "cffi-1.14.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c"},
     {file = "cffi-1.14.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee"},
+    {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf1ac1984eaa7675ca8d5745a8cb87ef7abecb5592178406e55858d411eadc0"},
+    {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:df5052c5d867c1ea0b311fb7c3cd28b19df469c056f7fdcfe88c7473aa63e333"},
+    {file = "cffi-1.14.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24a570cd11895b60829e941f2613a4f79df1a27344cbbb82164ef2e0116f09c7"},
     {file = "cffi-1.14.5-cp39-cp39-win32.whl", hash = "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396"},
     {file = "cffi-1.14.5-cp39-cp39-win_amd64.whl", hash = "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d"},
     {file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"},
@@ -3217,6 +3229,10 @@ gitpython = [
     {file = "GitPython-3.1.17-py3-none-any.whl", hash = "sha256:29fe82050709760081f588dd50ce83504feddbebdc4da6956d02351552b1c135"},
     {file = "GitPython-3.1.17.tar.gz", hash = "sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e"},
 ]
+haystack-redis = [
+    {file = "haystack-redis-0.0.1.tar.gz", hash = "sha256:ccfea88bdc1387c9f7f6f19e9bc062a3612039ef94cfd3e78cf59a96ddd269b2"},
+    {file = "haystack_redis-0.0.1-py3-none-any.whl", hash = "sha256:4fdeee5a9d8daadb1fed4584fd2ffbb25b1ed2315dacb97b53093756d6b54467"},
+]
 html2text = [
     {file = "html2text-2020.1.16-py3-none-any.whl", hash = "sha256:c7c629882da0cf377d66f073329ccf34a12ed2adf0169b9285ae4e63ef54c82b"},
     {file = "html2text-2020.1.16.tar.gz", hash = "sha256:e296318e16b059ddb97f7a8a1d6a5c1d7af4544049a01e261731d2d5cc277bbb"},
@@ -3250,8 +3266,8 @@ jedi = [
     {file = "jedi-0.18.0.tar.gz", hash = "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"},
 ]
 jinja2 = [
-    {file = "Jinja2-3.0.0-py3-none-any.whl", hash = "sha256:2f2de5285cf37f33d33ecd4a9080b75c87cd0c1994d5a9c6df17131ea1f049c6"},
-    {file = "Jinja2-3.0.0.tar.gz", hash = "sha256:ea8d7dd814ce9df6de6a761ec7f1cac98afe305b8cdc4aaae4e114b8d8ce24c5"},
+    {file = "Jinja2-3.0.1-py3-none-any.whl", hash = "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4"},
+    {file = "Jinja2-3.0.1.tar.gz", hash = "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4"},
 ]
 jmespath = [
     {file = "jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"},
@@ -3388,8 +3404,8 @@ pexpect = [
     {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"},
 ]
 pg8000 = [
-    {file = "pg8000-1.19.4-py3-none-any.whl", hash = "sha256:510db12be8ad85488289bd0b97bc340202cf6749da08cf8588032a047fb5d0ba"},
-    {file = "pg8000-1.19.4.tar.gz", hash = "sha256:7c9c6d57541b0f2153e23a8d759ceb3d7cb9308918fba6ad9e92f8e0e5a90bc8"},
+    {file = "pg8000-1.19.5-py3-none-any.whl", hash = "sha256:f750fc6c7acab10a399d27fed2732b0f380f0012eeb054ba1637519c190255b3"},
+    {file = "pg8000-1.19.5.tar.gz", hash = "sha256:dcb9afb9cb87acc79388ae633c45cf7f806aabe3bb5d8cf154572b699393f937"},
 ]
 phonenumbers = [
     {file = "phonenumbers-8.12.23-py2.py3-none-any.whl", hash = "sha256:4b9d2f2165309613f32fe5057ff0604eb8e4bbb7be44f7ba77baef760d7d60e2"},
@@ -3937,9 +3953,6 @@ whoosh = [
     {file = "Whoosh-2.7.4.tar.gz", hash = "sha256:7ca5633dbfa9e0e0fa400d3151a8a0c4bec53bd2ecedc0a67705b17565c31a83"},
     {file = "Whoosh-2.7.4.zip", hash = "sha256:e0857375f63e9041e03fedd5b7541f97cf78917ac1b6b06c1fcc9b45375dda69"},
 ]
-xapian-haystack = [
-    {file = "xapian-haystack-2.1.1.tar.gz", hash = "sha256:1e2fc4d84dc32f132e5b143266e4d4a32e6abac1d363742d0ebc212fac8ade70"},
-]
 yubiotp = [
     {file = "YubiOTP-1.0.0.post1-py2.py3-none-any.whl", hash = "sha256:7ad57011866e0bc6c6d179ffbc3926fcc0e82d410178a6d01ba4da0f88332878"},
     {file = "YubiOTP-1.0.0.post1.tar.gz", hash = "sha256:c13825f7b76a69afb92f19521f4dea9f5031d70f45123b505dc2e0ac03132065"},
diff --git a/pyproject.toml b/pyproject.toml
index f7afc33f84bf53d9701915fabc360a21e2af34e3..7cabb6beda0f755a92179ab6d0b132e8790d36ae 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -98,15 +98,13 @@ django-storages = {version = "^1.11.1", optional = true}
 boto3 = {version = "^1.17.33", optional = true}
 django-cleanup = "^5.1.0"
 djangorestframework = "^3.12.4"
-Whoosh = {version = "^2.7.4", optional = true}
-xapian-haystack = {version = "^2.1.1", optional = true}
+Whoosh = "^2.7.4"
 django-titofisto = "^0.1.0"
+haystack-redis = "^0.0.1"
 
 [tool.poetry.extras]
 ldap = ["django-auth-ldap"]
 s3 = ["boto3", "django-storages"]
-whoosh = ["whoosh"]
-xapian = ["xapian-haystack"]
 
 [tool.poetry.dev-dependencies]
 aleksis-builddeps = "*"