Algolia For Django
Tags: django, algolia, react, tutorial
Published: 2022-06-11 20:38:29



[**Algolia**](https://www.algolia.com/) is a hosted search engine, offering full-text, numerical, and faceted search, capable of delivering real-time results from the first keystroke. Algolia's powerful API lets you quickly and seamlessly implement search within your websites and mobile applications. Our search API powers billions of queries for thousands of companies every month, delivering relevant results in under 100ms anywhere in the world.

### Resources
- [Algolia Django](https://www.algolia.com/doc/framework-integration/django/setup/?client=python)
- [Algolia Repo](https://github.com/algolia/algoliasearch-django)

**Well** before continuing, i believe most of you are fimailiar with `django`, and won't be explaining much of it.

Demo:
{% embed https://algolia-search-frontend.vercel.app/ %}

### Setup
- create a project or clone from [github](https://github.com/lyamaa/algolia_search_backends)
- create Virtualenv. 
```
virtualenv venv
```
- create requirements.txt
```
Django==4.0.5
django-taggit==3.0.0
django-treebeard==4.5.1
djangorestframework==3.13.1
algoliasearch-django==2.0.0
django-environ==0.8.1
```
- create a new app `algolia_search` in the project     
```
python manage.py startapp algolia_search
```
- create `.env` file

```
DJANGO_SUPERUSER_PASSWORD=root
DJANGO_SUPERUSER_USERNAME=root
DJANGO_SUPERUSER_EMAIL=root@root.com

APPLICATION_ID=
ADMIN_API_KEY=
SEARCH_ONLY_API_KEY=
```
### Config Settings
configure the `settings.py` file
```
import os
import environ
from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent

env = environ.Env()
environ.Env.read_env(os.path.join(BASE_DIR, ".env"))

INSTALLED_APPS = [
  
    # *** 3rd party apps ***
    "rest_framework",
    "rest_framework.authtoken",
    "treebeard",
    "taggit",
    "algoliasearch_django",
    # ** apps **
    "algolia_search
]

ALGOLIA = {
    "APPLICATION_ID": os.environ.get("APPLICATION_ID"),
    "API_KEY": os.environ.get("ADMIN_API_KEY"),
    "SEARCH_API_KEY": os.environ.get("SEARCH_ONLY_API_KEY"),
}

```
### Creating Models
```
# algolia_search/models.py

from this import s
from unicodedata import category
from django.db import models
from django.db.models import Manager, QuerySet
from taggit.managers import TaggableManager
from treebeard.mp_tree import MP_Node

from django.contrib.auth import get_user_model

User = get_user_model()


class TimeStamp(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    update_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True


class Category(MP_Node):
    class Meta:
        db_table = "categories"
        verbose_name_plural = "Categories"

    name = models.CharField(max_length=30)

    node_order_by = ["name"]

    def __str__(self):
        return f"Category: {self.name}"


class ArticleQuerySet(QuerySet):
    def update(self, **kwargs):
        super(ArticleQuerySet, self).update(**kwargs)


class CustomManager(Manager):
    def get_queryset(self):
        return ArticleQuerySet(self.model, using=self._db)


class ArticleLike(TimeStamp):
    class Meta:
        db_table = "article_like"
        unique_together = ("user", "article")

    user = models.ForeignKey(User, on_delete=models.CASCADE)
    article = models.ForeignKey(
        "Article", on_delete=models.CASCADE, related_name="likes_article"
    )


class Article(TimeStamp):
    class Meta:
        db_table = "articles"

    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
    title = models.CharField(max_length=255)
    description = models.TextField(null=True, blank=True)
    is_published = models.BooleanField(default=False)
    tags = TaggableManager()
    likes = models.ManyToManyField(
        User, related_name="user_likes_article", through=ArticleLike, blank=True
    )

    objects = CustomManager()

    def __str__(self):
        return self.title

    def is_published_indexing(self):
        return self.is_published == True

    @property
    def likes_count(self):
        return int(self.likes.count())

    @property
    def tags_indexing(self):
        return [tag.name for tag in self.tags.all()]

    @property
    def category_indexing(self):
        return list(self.category.get_descendants())
```
### Modify `admin.py`

```
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
from django.db import models
from django.utils.safestring import mark_safe
from treebeard.admin import TreeAdmin
from treebeard.forms import movenodeform_factory

from algolia_search.models import Article, Category, ArticleLike


class UserAdmin(UserAdmin):
    model = User
    list_display = (
        "username",
        "email",
        "is_staff",
        "is_active",
    )
    list_filters = (
        "username",
        "email",
        "is_staff",
        "is_active",
    )
    fieldsets = (
        (None, {"fields": ("username", "email", "password")}),
        (
            "Permissions",
            {
                "fields": (
                    "is_staff",
                    (
                        "is_active",
                        "is_superuser",
                    ),
                )
            },
        ),
        ("Important dates", {"fields": ("last_login", "date_joined")}),
        ("Advanced options", {"classes": ("collapse",), "fields": ("groups",)}),
    )
    add_fieldsets = (
        (
            None,
            {
                "classes": ("wide",),
                "fields": (
                    "username",
                    "email",
                    "password1",
                    "password2",
                    "is_staff",
                    "is_active",
                    "is_superuser",
                    "groups",
                ),
            },
        ),
    )


admin.site.unregister(User)
admin.site.register(User, UserAdmin)


class CategoryAdmin(TreeAdmin):
    form = movenodeform_factory(Category)


admin.site.register(Category, CategoryAdmin)


class ArticleLikeInline(admin.TabularInline):
    model = ArticleLike
    extra = 0


class ArticleAdmin(admin.ModelAdmin):
    list_display = [
        "id",
        "title",
        "get_tags",
        "likes_count",
        "is_published",
    ]
    search_fields = ["id", "title"]

    def get_tags(self, article):
        tags = [str(tag) for tag in article.tags.all()]
        return ", ".join(tags)

    inlines = [
        ArticleLikeInline,
    ]


admin.site.register(Article, ArticleAdmin)

```
### Create `index.py` file.
```
# algolia_search/index.py

import algoliasearch_django as algoliasearch
from algoliasearch_django import AlgoliaIndex

from .models import Article


class ArticleIndex(AlgoliaIndex):
    """Index for Article model"""

    # ** fields to index if is_published is True
    name = "article"
    should_index = "is_published_indexing"
    fields = (
        "title",
        "description",
        "is_published",
        "tags_indexing",
        "category_indexing",
        "likes_count",
    )
    settings = {
        "searchableAttributes": [
            "title",
            "description",
            "tags_indexing",
            "category_indexing",
        ],
        "attributesForFaceting": [
            "title",
            "tags_indexing",
            "description",
            "category_indexing",
        ],
        "queryType": "prefixAll",
        # ** custom ranking rules with like_count
        "customRanking": [
            "desc(likes_count)",
        ],
        "advancedSyntax": True,
        "highlightPreTag": "<mark>",
        "highlightPostTag": "</mark>",
        "hitsPerPage": 15,
    }

    index_name = "articles"

algoliasearch.register(Article, ArticleIndex)

```
- Above we created `ArticleIndex` class for indexing.
```
 should_index = "is_published_indexing"
```
- here `should_index` attribute helps to index `Article` object which `is_published` is true. So, if it is false no objects are indexed.

```
fields = (
        "title",
        "description",
        "is_published",
        "category_indexing",
        "tags_indexing",
        "likes_count",
    )
```
- Simply, it is just defining out fields from `article` models.

```
settings = {
        "searchableAttributes": [
            "title",
            "description",
            "tags_indexing",
            "category_indexing",
        ],
        "attributesForFaceting": [
            "title",
            "tags_indexing",
            "description",
            "category_indexing",
        ],
        "queryType": "prefixAll",
        # ** custom ranking rules with like_count
        "customRanking": [
            "desc(likes_count)",
        ],
        "advancedSyntax": True,
        "highlightPreTag": "<mark>",
        "highlightPostTag": "</mark>",
        "hitsPerPage": 15,
    }
```
in setting options:
- **searchableAttributes**: The complete list of attributes used for searching.
- **attributesForFaceting**: The complete list of attributes that will be used for faceting.
   - to turn an attribute into a facet
   - to make any string attribute filterable.
- **queryType**: Controls if and how query words are interpreted as prefixes.
   - **prefixAll**: All query words are interpreted as prefixes. This option is not recommended, as it tends to yield counterintuitive results and has a negative impact on performance.
  - **prefixLast**: Only the last word is interpreted as a prefix (default behavior).

  - **prefixNone**: No query word is interpreted as a prefix. This option is not recommended, especially in an instant search setup, as the user will have to type the entire word(s) before getting any relevant results.

- **customRanking**: Specifies the custom ranking criterion.
  - Modifiers:#
    - asc: Sort by increasing value of the attribute.
    - desc: Sort by decreasing value of the attribute.

> Note: we have our custom rank according to like count of `articles`

- **advancedSyntax**: Enables the advanced query syntax.

   - This advanced syntax brings two additional features:

   - **Phrase query**: a specific sequence of terms that must be matched next to one another. A phrase query needs to be surrounded by double quotes ("). For example, the query "search engine" only returns a record if it contains “search engine” exactly in at least one attribute.

> Note: Typo tolerance is disabled inside the phrase (i.e. within the quotes).

  - **Prohibit operator**: excludes records that contain a specific term. To exclude a term, you need to prefix it with a minus (-). The engine only interprets the minus (-) as a prohibit operator when you place it at the start of a word. A minus (-) within double quotes (") is not treated as a prohibit operator.

- **highlightPreTag**: The HTML string to insert before the highlighted parts in all highlight and snippet results.
- **highlightPostTag**: The HTML string to insert after the highlighted parts in all highlight and snippet results.
- **hitsPerPage**: <HitsPerPage> is a widget that displays a menu of options to change the number of results per page.

Finally, register `Article`, `ArticleIndex`.


### Creating Custom Commands.

- create dir `algolia_search/management/commands` & copy from following [links](https://github.com/lyamaa/algolia_search_backends/tree/main/2.final/src/algolia_search/management/commands).
- After creating then run the following commands
```
$ python manage.py migrate
$ python manage.py makemigrations
$ python manage.py seed_superuser
$ python manage.py seed_users
$ python manage.py seed_articles
$ python manage.py seed_likes
```

### **ALgolia Dashboard**:
so we are going to create index and query suggestion:
   - **index**: indexing a list of article objects.
   - **query suggestion**: retrieves articles matching the query and displays a preview

- Login to [Algolia](https://www.algolia.com/users/sign_in) with your desired accounts.
- create new application

![algolia-11](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8a1dpogqdxdh2905jyzg.png)

- Select Algolia Package

![algolia-10](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i0xw82823noiyv0mwn3u.png)

-select a region, suitable for you

![algolia-9](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wjfawmds4taeyx2tkg4l.png)
- click on `Review Application Details`
- click on `check boxes `and `create application`
- create index `articles`

![algolia-8](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y02dv6yy26fg82f6czu9.png)
- Return to Dashboard and click on `API keys`

![algolia-7](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7kjhg7zg8db5rqi8uryx.png)

- Copy `keys` from `Application ID`, `Search-only API Key`, and `Admin API Key and paste it to `.env` file

![algolia-6](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xbgq9z4ilkx0io540drm.png)

- Run commands python to index your data from backends:
```
   $ python manage.py algolia_reindex
```
- Go to home `Dashboard` and click on articles.

![algolia-5](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cxi74774qffdpdje9iyv.png)
- List of datas are displayed as shown below
![algolia-11](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rekqs74qufo7b8z2ggbt.png)

###  Create `query suggestions`
- click on `query suggestions` tab.

![algolia-4](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gi0roctrv0w8vb6fklcz.png)

- click button to create `query suggestions`

![algolia-3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zp13ymrbmdz03uuiyonb.png)


![algolia-2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5mv1y7g3ptwqzmbo7alh.png)

- Accept and continue, wait for a while until it creates your `suggestion index`

- click to Edit Categories button and add the following categories attributes as shown in image.

![algolia-one](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/szrae59h748svutjntop.png)

- click to save button and wait for a while until it re-index.

## Frontend Integration
- [Algolia](https://www.algolia.com/doc/framework-integration/django/search/?client=python) suggest to use  [InstantSearch.js](https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/js/) library to build your search interface and perform search queries directly from the end-user browser without going through your server.
- Orginal code: [Autocomplete with React InstantSearch Hooks example](https://codesandbox.io/s/github/algolia/autocomplete/tree/next/examples/react-instantsearch-hooks)

- well, i had made some changes from original src code.

---
{% github https://github.com/lyamaa/algolia-search-frontend no-readme %}
---

### Final Words

Well, i found **algolia** to be simple and easy to configure than elastic search. ES is much more of writing code than algolia and bit more of complex.

Read comparision of **ES** and **algolia** : [Algolia VS Elasticsearch](https://www.algolia.com/compare-algolia-vs-elasticsearch/)

till then Bye bye 👋👋👋...


---
{% github https://github.com/lyamaa/algolia_search_backends no-readme %}
---