diff --git a/.gitignore b/.gitignore
index ed90f2f7a841da90717ad986c7bb57dbc86a8876..70d2c1202e645fc31260a0fdd6bac90fdd25e15a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -64,6 +64,8 @@ docs/_build/
 # Generated files
 aleksis/node_modules/
 aleksis/static/
+aleksis/whoosh_index/
+aleksis/xapian_index/
 
 .coverage
 .mypy_cache/
diff --git a/aleksis/core/mixins.py b/aleksis/core/mixins.py
index 8ba220c1610321971f6b7c96ca20b7ce65fe0d5b..574cf185cc2350e3e7b45f7723c8e9313a631790 100644
--- a/aleksis/core/mixins.py
+++ b/aleksis/core/mixins.py
@@ -71,6 +71,13 @@ class ExtensibleModel(CRUDMixin):
         - Dominik George <dominik.george@teckids.org>
     """
 
+    # Defines a material design icon associated with this type of model
+    icon_ = "radio_button_unchecked"
+
+    def get_absolute_url(self) -> str:
+        """ Get the URL o a view representing this model instance """
+        pass
+
     @property
     def crud_event_create(self) -> Optional[CRUDEvent]:
         """ Return create event of this object """
diff --git a/aleksis/core/models.py b/aleksis/core/models.py
index d87465c83b12427b7a969997278df31f3f273268..b9b29ea5f7372e54a626081e3d79e516a9b863d8 100644
--- a/aleksis/core/models.py
+++ b/aleksis/core/models.py
@@ -8,6 +8,7 @@ from django.contrib.contenttypes.models import ContentType
 from django.db import models
 from django.db.models import QuerySet
 from django.forms.widgets import Media
+from django.urls import reverse
 from django.utils import timezone
 from django.utils.translation import gettext_lazy as _
 from image_cropping import ImageCropField, ImageRatioField
@@ -95,6 +96,8 @@ class Person(ExtensibleModel):
         verbose_name = _("Person")
         verbose_name_plural = _("Persons")
 
+    icon_ = "person"
+
     SEX_CHOICES = [("f", _("female")), ("m", _("male"))]
 
     user = models.OneToOneField(
@@ -136,6 +139,10 @@ class Person(ExtensibleModel):
 
     description = models.TextField(verbose_name=_("Description"), blank=True, null=True)
 
+
+    def get_absolute_url(self) -> str:
+        return reverse("person_by_id", args=[self.id])
+
     @property
     def primary_group_short_name(self) -> Optional[str]:
         """ Returns the short_name field of the primary
@@ -242,6 +249,8 @@ class Group(ExtensibleModel):
         verbose_name = _("Group")
         verbose_name_plural = _("Groups")
 
+    icon_ = "group"
+
     name = models.CharField(verbose_name=_("Long name of group"), max_length=255, unique=True)
     short_name = models.CharField(verbose_name=_("Short name of group"), max_length=255, unique=True, blank=True, null=True)
 
@@ -256,7 +265,12 @@ class Group(ExtensibleModel):
         blank=True,
     )
 
+<<<<<<< HEAD
+    def get_absolute_url(self) -> str:
+        return reverse("group_by_id", args=[self.id])
+=======
     type = models.ForeignKey("GroupType", on_delete=models.CASCADE, related_name="type", verbose_name=_("Type of group"), null=True, blank=True)
+>>>>>>> master
 
     @property
     def announcement_recipients(self):
diff --git a/aleksis/core/search_indexes.py b/aleksis/core/search_indexes.py
new file mode 100644
index 0000000000000000000000000000000000000000..5828e0c52391423cc6dd0bad43f6a15310a85e1f
--- /dev/null
+++ b/aleksis/core/search_indexes.py
@@ -0,0 +1,10 @@
+from .models import Person, Group
+from .util.search import Indexable, SearchIndex
+
+
+class PersonIndex(SearchIndex, Indexable):
+    model = Person
+
+
+class GroupIndex(SearchIndex, Indexable):
+    model = Group
diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py
index e0f652eca2754a12d0e60fc86ce0805071e97507..eb4b3e07401fae2a830c530212747877ced2af93 100644
--- a/aleksis/core/settings.py
+++ b/aleksis/core/settings.py
@@ -53,6 +53,7 @@ INSTALLED_APPS = [
     "django.contrib.sites",
     "django.contrib.staticfiles",
     "django.contrib.humanize",
+    "haystack",
     "polymorphic",
     "django_global_request",
     "dbbackup",
@@ -571,3 +572,34 @@ LOGGING = {
         'level': _settings.get("logging.level", "WARNING"),
     },
 }
+
+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")),
+        },
+    }
+
+if _settings.get("celery.enabled", False) and _settings.get("search.celery", True):
+    INSTALLED_APPS.append("celery_haystack")
+    HAYSTACK_SIGNAL_PROCESSOR = 'celery_haystack.signals.CelerySignalProcessor'
+else:
+    HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
+
+HAYSTACK_SEARCH_RESULTS_PER_PAGE = 10
diff --git a/aleksis/core/static/js/main.js b/aleksis/core/static/js/main.js
index 27b5c5ad8fb126391f98e61b75883f965e9970e2..403837da22d351d8e97ed3cf6c27c79e30514497 100644
--- a/aleksis/core/static/js/main.js
+++ b/aleksis/core/static/js/main.js
@@ -65,6 +65,10 @@ $(document).ready( function () {
         });
     });
 
+    // Initialise auto-completion for search bar
+    window.autocomplete = new Autocomplete({});
+    window.autocomplete.setup();
+
     // Initialize text collapsibles [MAT, own work]
     $(".text-collapsible").addClass("closed").removeClass("opened");
 
diff --git a/aleksis/core/static/js/search.js b/aleksis/core/static/js/search.js
new file mode 100644
index 0000000000000000000000000000000000000000..7943d2ebfaa07123bf0174b24c055884ceb0bd36
--- /dev/null
+++ b/aleksis/core/static/js/search.js
@@ -0,0 +1,121 @@
+/*
+ * Based on: https://django-haystack.readthedocs.io/en/master/autocomplete.html
+ *
+ * Š Copyright 2009-2016, Daniel Lindsley
+ * Licensed under the 3-clause BSD license
+ */
+
+var Autocomplete = function (options) {
+    this.form_selector = options.form_selector || '.autocomplete';
+    this.url = options.url || Urls.searchbarSnippets();
+    this.delay = parseInt(options.delay || 300);
+    this.minimum_length = parseInt(options.minimum_length || 3);
+    this.form_elem = null;
+    this.query_box = null;
+    this.selected_element = null;
+};
+
+Autocomplete.prototype.setup = function () {
+    var self = this;
+
+    this.form_elem = $(this.form_selector);
+    this.query_box = this.form_elem.find('input[name=q]');
+
+
+    $("#search-form").focusout(function (e) {
+        if (!$(e.relatedTarget).hasClass("search-item")) {
+            e.preventDefault();
+            $("#search-results").remove();
+        }
+    });
+
+    // Trigger the "keyup" event if input gets focused
+
+    this.query_box.focus(function () {
+        self.query_box.trigger("keydown");
+    });
+
+    // Watch the input box.
+    this.query_box.keydown(function (e) {
+        var query = self.query_box.val();
+
+        if (e.which === 38) { // Keypress Up
+            if (!self.selected_element) {
+                self.setSelectedResult($("#search-collection").children().last());
+                return false;
+            }
+
+            let prev = self.selected_element.prev();
+            if (prev.length > 0) {
+                self.setSelectedResult(prev);
+            }
+            return false;
+        }
+
+        if (e.which === 40) { // Keypress Down
+            if (!self.selected_element) {
+                self.setSelectedResult($("#search-collection").children().first());
+                return false;
+            }
+
+            let next = self.selected_element.next();
+            if (next.length > 0) {
+                self.setSelectedResult(next);
+            }
+            return false;
+        }
+
+        if (self.selected_element && e.which === 13) {
+            e.preventDefault();
+            window.location.href = self.selected_element.attr("href");
+        }
+
+        if (query.length < self.minimum_length) {
+            $("#search-results").remove();
+            return true;
+        }
+
+        self.fetch(query);
+        return true;
+    });
+
+    // // On selecting a result, remove result box
+    // this.form_elem.on('click', '#search-results', function (ev) {
+    //     $('#search-results').remove();
+    //     return true;
+    // });
+
+    // Disable browser's own autocomplete
+    // We do this here so users without JavaScript can keep it enabled
+    this.query_box.attr('autocomplete', 'off');
+};
+
+Autocomplete.prototype.fetch = function (query) {
+    var self = this;
+
+    $.ajax({
+        url: this.url
+        , data: {
+            'q': query
+        }
+        , success: function (data) {
+            self.show_results(data);
+        }
+    })
+};
+
+Autocomplete.prototype.show_results = function (data) {
+    $('#search-results').remove();
+    var results_wrapper = $('<div id="search-results">' + data + '</div>');
+    this.query_box.after(results_wrapper);
+    this.selected_element = null;
+};
+
+Autocomplete.prototype.setSelectedResult = function (element) {
+    if (this.selected_element) {
+        this.selected_element.removeClass("active");
+    }
+    element.addClass("active");
+    this.selected_element = element;
+    console.log("New element: ", element);
+};
diff --git a/aleksis/core/static/style.scss b/aleksis/core/static/style.scss
index e2cc69479f8cca34d7f2b95c2b70d61aba284174..e1edc75561fb4fe57568f1b148b8ab0f955c1b58 100644
--- a/aleksis/core/static/style.scss
+++ b/aleksis/core/static/style.scss
@@ -8,6 +8,14 @@
   color: $primary-color !important;
 }
 
+.secondary-color {
+  background-color: $secondary-color !important;
+}
+
+.secondary-color-text, .secondary-color-text a {
+  color: $secondary-color !important;
+}
+
 rect#background {
   fill: $primary-color !important;
 }
@@ -72,7 +80,7 @@ header, main, footer {
 
 #sidenav-logo {
   height: 70px;
-  width:auto;
+  width: auto;
 }
 
 @media only screen and (max-width: 993px) {
@@ -129,6 +137,66 @@ li.active > a > .sidenav-badge {
   color: $primary-color !important;
 }
 
+.sidenav li.search {
+  position: relative;
+  z-index: 2;
+}
+
+.sidenav li.search:hover {
+  background-color: #fff;
+}
+
+.sidenav li.search .search-wrapper {
+  color: #777;
+  margin-top: -1px;
+  border-top: 1px solid rgba(0, 0, 0, 0.14);
+  border-bottom: 1px solid rgba(0, 0, 0, 0.14);
+
+  -webkit-transition: margin .25s ease;
+  transition: margin .25s ease;
+}
+
+.sidenav li.search .search-wrapper input#search {
+  color: #777;
+  display: block;
+  font-size: 16px;
+  font-weight: 300;
+  width: 100%;
+  height: 62px;
+  margin: 0;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+  padding: 0 45px 0 30px;
+  border: 0;
+}
+
+.sidenav li.search .search-wrapper input#search:focus {
+  outline: none;
+  -webkit-box-shadow: none;
+  box-shadow: none;
+}
+
+.sidenav li.search .search-wrapper > i.material-icons {
+  position: absolute;
+  top: 21px;
+  right: 10px;
+  cursor: pointer;
+}
+
+a.collection-item.search-item {
+  padding: 20px 10px;
+}
+
+div#search-results {
+  position: absolute;
+  width: 100%;
+}
+
+.search-result-icon {
+  position: absolute;
+  right: 10px;
+}
+
 
 // Sidenav trigger
 
@@ -204,6 +272,43 @@ form .row {
   margin-bottom: 0;
 }
 
+label.chips-checkbox {
+  &.active {
+    outline: none;
+    background-color: $chip-selected-color;
+    color: #fff;
+  }
+
+  display: inline-block;
+  height: 32px;
+  font-size: 13px;
+  font-weight: 500;
+  color: rgba(0, 0, 0, .6);
+  line-height: 32px;
+  padding: 0 12px;
+  border-radius: 16px;
+  background-color: $chip-bg-color;
+  margin-bottom: $chip-margin;
+  margin-right: $chip-margin;
+
+  > img {
+    float: left;
+    margin: 0 8px 0 -12px;
+    height: 32px;
+    width: 32px;
+    border-radius: 50%;
+  }
+}
+
+input[type="checkbox"].chips-checkbox + span {
+  padding-left: 0;
+
+  &:before {
+    display: none;
+    width: 0;
+  }
+}
+
 // Badges
 
 span.badge.new::after {
@@ -261,7 +366,7 @@ span.badge .material-icons {
 
 /* Table*/
 
-table.striped > tbody > tr:nth-child(odd), table tr.striped, table tbody.striped tr  {
+table.striped > tbody > tr:nth-child(odd), table tr.striped, table tbody.striped tr {
   background-color: rgba(208, 208, 208, 0.5);
 }
 
@@ -507,4 +612,3 @@ main .alert p:first-child, main .alert div:first-child {
   overflow: visible;
   width: 100%;
 }
-
diff --git a/aleksis/core/static/theme.scss b/aleksis/core/static/theme.scss
index 36d3e3f55a0e37595f3dcc27230947875cd2648b..907c444f5bf99ef3616ae47a4c6df8b2e82b011e 100644
--- a/aleksis/core/static/theme.scss
+++ b/aleksis/core/static/theme.scss
@@ -127,7 +127,7 @@ $collapsible-border-color: #ddd !default;
 
 $chip-bg-color: #e4e4e4 !default;
 $chip-border-color: #9e9e9e !default;
-$chip-selected-color: #26a69a !default;
+$chip-selected-color: $primary-color !default;
 $chip-margin: 5px !default;
 
 
diff --git a/aleksis/core/templates/core/base.html b/aleksis/core/templates/core/base.html
index b75eb9ec30f7ac9a29be5bd11a72921ca2d08ad8..23bfdf57d69cfbf1b11347bf04af1c75f80f2f23 100644
--- a/aleksis/core/templates/core/base.html
+++ b/aleksis/core/templates/core/base.html
@@ -32,6 +32,8 @@
   {# Include jQuery to provide $(document).ready #}
   {% include_js "jQuery" %}
 
+  <script type="text/javascript" src="{% static 'js/search.js' %}"></script>
+
   {% block extra_head %}{% endblock %}
 </head>
 <body>
@@ -69,6 +71,14 @@
         </object>
       </a>
     </li>
+    <li class="search">
+      <form method="get" action="{% url "haystack_search" %}" id="search-form" class="autocomplete">
+        <div class="search-wrapper">
+          <input id="search" name="q" placeholder="{% trans "Search" %}">
+          <i class="material-icons">search</i>
+        </div>
+      </form>
+    </li>
     <li class="no-padding">
       {% include "core/sidenav.html" %}
     </li>
diff --git a/aleksis/core/templates/search/indexes/core/group_text.txt b/aleksis/core/templates/search/indexes/core/group_text.txt
new file mode 100644
index 0000000000000000000000000000000000000000..165c30e8c240ddecc872520626cccb598a6ad7a0
--- /dev/null
+++ b/aleksis/core/templates/search/indexes/core/group_text.txt
@@ -0,0 +1,2 @@
+{{ object.name }}
+{{ object.short_name }}
diff --git a/aleksis/core/templates/search/indexes/core/person_text.txt b/aleksis/core/templates/search/indexes/core/person_text.txt
new file mode 100644
index 0000000000000000000000000000000000000000..210e1755e071a6ced9807790f54403ee5bc85f19
--- /dev/null
+++ b/aleksis/core/templates/search/indexes/core/person_text.txt
@@ -0,0 +1,3 @@
+{{ object.full_name }}
+{{ object.user.username }}
+{{ object.email }}
diff --git a/aleksis/core/templates/search/search.html b/aleksis/core/templates/search/search.html
new file mode 100644
index 0000000000000000000000000000000000000000..babbd70d945f26cbf777ae46ca0add15a24e442d
--- /dev/null
+++ b/aleksis/core/templates/search/search.html
@@ -0,0 +1,108 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+
+{% load i18n material_form_internal %}
+
+{% block browser_title %}{% blocktrans %}Search{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Global Search{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  <form method="get">
+    {#    {% form form=form %}{% endform %}#}
+
+    <input type="text" name="{{ form.q.name }}" id="{{ form.q.id }}" value="{% firstof form.q.value "" %}"
+           placeholder="{% trans "Search Term" %}">
+
+    <h6>{{ form.models.label }}</h6>
+    <div>
+      {% for group, items in form.models|select_options %}
+        {% for choice, value, selected in items %}
+          <label class="{% if selected %} active{% endif %}">
+            <input type="checkbox"
+                   {% if value == None or value == '' %}disabled{% else %}value="{{ value }}"{% endif %}
+                    {% if selected %} checked="checked"{% endif %} name="{{ form.models.name }}">
+            <span> {{ choice }} </span>
+          </label>
+        {% endfor %}
+      {% endfor %}
+    </div>
+
+    <button type="submit" class="btn waves-effect waves-light green">
+      <i class="material-icons left">search</i>
+      {% blocktrans %}Search{% endblocktrans %}
+    </button>
+
+    <h5>{% trans "Results" %}</h5>
+
+    {% if query %}
+      <div class="collection">
+        {% for result in page.object_list %}
+          <a href="{{ result.object.get_absolute_url|default:"#" }}" class="collection-item">
+            <i class="material-icons left">{{ result.object.icon_ }}</i>
+            {{ result.object }}
+          </a>
+        {% empty %}
+          <li class="collection-item">
+            {% trans "No search results could be found to your search" %}
+          </li>
+        {% endfor %}
+      </div>
+
+      {% if page.has_other_pages %}
+        <ul class="pagination">
+          {% if page.has_previous %}
+            <li class="waves-effect">
+              <a href="?q={{ query }}&amp;page={{ page.previous_page_number }}">
+                <i class="material-icons">chevron_left</i>
+              </a>
+            </li>
+          {% else %}
+            <li class="disabled"><a href="#"><i class="material-icons">chevron_left</i></a></li>
+          {% endif %}
+
+          {% for page_num in page.paginator.page_range %}
+            {% if page.number == page_num %}
+              <li class="active"><a href="#">{{ page_num }}</a></li>
+            {% else %}
+              <li class="waves-effect"><a href="?q={{ query }}&amp;page={{ page_num }}">{{ page_num }}</a></li>
+            {% endif %}
+          {% endfor %}
+
+          {% if page.has_next %}
+            <li class="waves-effect">
+              <a href="?q={{ query }}&amp;page={{ page.next_page_number }}">
+                <i class="material-icons">chevron_right</i>
+              </a>
+            </li>
+          {% else %}
+            <li class="disabled"><a href="#"><i class="material-icons">chevron_right</i></a></li>
+          {% endif %}
+        </ul>
+      {% endif %}
+    {% else %}
+      <div class="collection">
+        <li class="collection-item">
+          {% trans "Please enter a search term above" %}
+        </li>
+      </div>
+    {% endif %}
+
+
+  </form>
+
+  <script>
+    $(document).ready(function () {
+      $("input[type='checkbox']").each(function () {
+        $(this).addClass("chips-checkbox");
+        $(this).parent("label").addClass("chips-checkbox");
+      });
+
+      $("label.chips-checkbox > span").click(function () {
+        $(this).parent("label.chips-checkbox").toggleClass("active");
+        let input = $(this).next("input[type='checkbox']");
+        input.prop("checked", !input.prop("checked"));
+      });
+    });
+  </script>
+{% endblock %}
diff --git a/aleksis/core/templates/search/searchbar_snippet.html b/aleksis/core/templates/search/searchbar_snippet.html
new file mode 100644
index 0000000000000000000000000000000000000000..b5b7188c6a1e0381fd30f41b6fccc7e587949a30
--- /dev/null
+++ b/aleksis/core/templates/search/searchbar_snippet.html
@@ -0,0 +1,4 @@
+<a href="{{ result.object.get_absolute_url|default:"#" }}" class="collection-item search-item">
+  {{ result.object }}
+  <i class="material-icons secondary-content search-result-icon">{{ result.object.icon_ }}</i>
+</a>
diff --git a/aleksis/core/templates/search/searchbar_snippets.html b/aleksis/core/templates/search/searchbar_snippets.html
new file mode 100644
index 0000000000000000000000000000000000000000..373c93a7ec5d9311167abab0655d3444b3e57e73
--- /dev/null
+++ b/aleksis/core/templates/search/searchbar_snippets.html
@@ -0,0 +1,5 @@
+<div class="collection" id="search-collection">
+  {% for result in results %}
+    {% include "search/searchbar_snippet.html" %}
+  {% endfor %}
+</div>
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index e5320dd02f05a884a8891c7793305f8c0672d7fc..4bb7d10fbbaa6774d5f5beed3a1d7e034f020260 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -39,6 +39,8 @@ urlpatterns = [
     path("announcement/create/", views.announcement_form, name="add_announcement"),
     path("announcement/edit/<int:pk>/", views.announcement_form, name="edit_announcement"),
     path("announcement/delete/<int:pk>/", views.delete_announcement, name="delete_announcement"),
+    path("search/searchbar/", views.searchbar_snippets, name="searchbar_snippets"),
+    path("search/", include("haystack.urls")),
     path("maintenance-mode/", include("maintenance_mode.urls")),
     path("impersonate/", include("impersonate.urls")),
     path("__i18n__/", include("django.conf.urls.i18n")),
diff --git a/aleksis/core/util/search.py b/aleksis/core/util/search.py
new file mode 100644
index 0000000000000000000000000000000000000000..6720fb6b4236f10d3bfb1932969483d4d4db6d8f
--- /dev/null
+++ b/aleksis/core/util/search.py
@@ -0,0 +1,23 @@
+from django.conf import settings
+
+from haystack import indexes
+
+# Not used here, but simplifies imports for apps
+Indexable = indexes.Indexable  # noqa
+
+if settings.HAYSTACK_SIGNAL_PROCESSOR == 'celery_haystack.signals.CelerySignalProcessor':
+    from haystack.indexes import SearchIndex as BaseSearchIndex
+else:
+    from celery_haystack.indexes import CelerySearchIndex as BaseSearchIndex
+
+class SearchIndex(BaseSearchIndex):
+    """ Base class for search indexes on AlekSIS models
+
+    It provides a default document field caleld text and exects
+    the related model in the model attribute.
+    """
+
+    text = indexes.EdgeNgramField(document=True, use_template=True)
+
+    def get_model(self):
+        return self.model
diff --git a/aleksis/core/views.py b/aleksis/core/views.py
index 8a81d701a2f38114b78353eae2a5f4106b06c91d..d3ead324d90b313ffeec3a5068df24c90bc379f4 100644
--- a/aleksis/core/views.py
+++ b/aleksis/core/views.py
@@ -7,6 +7,8 @@ from django.shortcuts import get_object_or_404, redirect, render
 from django.utils.translation import gettext_lazy as _
 
 from django_tables2 import RequestConfig
+from haystack.inputs import AutoQuery
+from haystack.query import SearchQuerySet
 
 from .decorators import admin_required, person_required
 from .forms import (
@@ -323,3 +325,14 @@ def delete_announcement(request: HttpRequest, pk: int) -> HttpResponse:
         messages.success(request, _("The announcement has been deleted."))
 
     return redirect("announcements")
+
+
+@login_required
+def searchbar_snippets(request: HttpRequest) -> HttpResponse:
+    query = request.GET.get('q', '')
+    limit = int(request.GET.get('limit', '5'))
+
+    results = SearchQuerySet().filter(text=AutoQuery(query))[:limit]
+    context = {"results": results}
+
+    return render(request, "search/searchbar_snippets.html", context)
diff --git a/pyproject.toml b/pyproject.toml
index 5ce4997beed06321f10b5f92dd1d42e32a7e43bc..02448d52ad00c58009c4ba7cb4244f2a3c7da808 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -68,11 +68,13 @@ django-otp = "0.7.5"
 django-colorfield = "^0.2.1"
 django-bleach = "^0.6.1"
 django-memoize = "^2.2.1"
+django-haystack = "^3.0"
+celery-haystack = {version="^0.3.1", optional=true}
 django-dbbackup = "^3.3.0"
 
 [tool.poetry.extras]
 ldap = ["django-auth-ldap"]
-celery = ["Celery", "django-celery-results", "django-celery-beat", "django-celery-email"]
+celery = ["Celery", "django-celery-results", "django-celery-beat", "django-celery-email", "celery-haystack"]
 
 [tool.poetry.dev-dependencies]
 sphinx = "^2.1"