Elastic Search + Django

lyamaa

Lyamaa

Posted on May 21, 2021

Elastic Search + Django

Requirements:


Project Setup:

$ mkdir dj_elastic && cd dj_elastic
$ python3 -m venv env
$ source env/bin/activate
$ poetry init
$ poetry add django djangorestframework django-autoslug black isort
$ poetry add django-haystack drf-haystack
$ poetry add elasticsearch==^7.x.x
$ django-admin.py startproject main
$ python manage.py startapp searches
$ python manage.py startapp commons
Enter fullscreen mode Exit fullscreen mode

project directory should look like:

── dj_elastic
├── main
│ ├── **init**.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
└── commons
└── searches
Enter fullscreen mode Exit fullscreen mode

Main app /url.py

from django.contrib import admin
from django.urls import path
from django.urls.conf import include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/v1/", include("searches.urls")),
]

Enter fullscreen mode Exit fullscreen mode

main/settings.py

INSTALLED_APPS = [
    "searches",
    "commons",
    "haystack",
    "rest_framework",
]
TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [BASE_DIR / "templates"],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]
HAYSTACK_CONNECTIONS = {
    "default": {
        'ENGINE': 'haystack.backends.elasticsearch7_backend.Elasticsearch7SearchEngine',
        "URL": "http://127.0.0.1:9200/",
        "INDEX_NAME": "haystack",
    },
}

HAYSTACK_SIGNAL_PROCESSOR = "haystack.signals.RealtimeSignalProcessor"

Enter fullscreen mode Exit fullscreen mode

👏🏻 Great, finished with basic setups....
Next, lets create models. Navigate to commons/models.py

# commons/models.py

from django.db import models
from autoslug import AutoSlugField
from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _


def slugify(value):
    return value.replace(" ", "-").lower()


class ConfigChoiceCategory(models.Model):

    name = models.CharField(
        _("Config Choice Category Name"),
        help_text=_("Required and Unique"),
        max_length=255,
        unique=True,
    )
    slug = AutoSlugField(
        verbose_name=_("Config Choice Category Slug"),
        populate_from="name",
        slugify=slugify,
    )
    entered_by = models.ForeignKey(User, blank=True, on_delete=models.CASCADE)
    is_active = models.BooleanField(default=True)

    class Meta:
        verbose_name = _("Config Choice Category")
        verbose_name_plural = _(" Config Choice Categories")

    def __str__(self):
        return self.name


class ConfigChoice(models.Model):
    name = models.CharField(
        _("Config Choice Name"),
        help_text=_("Required and Unique"),
        max_length=255,
        unique=True,
    )
    description = models.TextField()
    slug = AutoSlugField(
        verbose_name=_("Config Choice Slug"),
        populate_from="name",
        slugify=slugify,
    )
    config_choice_category = models.ForeignKey(
        ConfigChoiceCategory, on_delete=models.CASCADE
    )
    entered_by = models.ForeignKey(User, on_delete=models.CASCADE)

    class Meta:
        verbose_name = _("Config Choice")
        verbose_name_plural = _("Config Choices")

    def __str__(self) -> str:
        return self.name


class Address(models.Model):
    street_1 = models.CharField(max_length=200)
    street_2 = models.CharField(max_length=200, null=True, blank=True)
    city = models.CharField(max_length=100)
    state = models.CharField(max_length=100)
    zip_code = models.CharField(max_length=100)
    country = models.CharField(max_length=50)
    latitude = models.FloatField()
    longitude = models.FloatField()

    def __str__(self):
        return f"{self.street_1}, {self.city}, {self.state}, {self.country}"``

Enter fullscreen mode Exit fullscreen mode

Here, we:

  • Created a models ConfigChoiceCategory and ConfigChoice, where configchoice has relation with ConfigChoiceCategory.
  • And we have Address Model too

Register models to admin.py

from django.contrib import admin

# Register your models here.

from .models import (
    Address,
    ConfigChoice,
    ConfigChoiceCategory,
)


admin.site.register(ConfigChoiceCategory)
admin.site.register(ConfigChoice)
admin.site.register(Address)


Enter fullscreen mode Exit fullscreen mode

So, let's navigate to searches app and create models for hotels.


#searches/models.py

from commons.models import Address, ConfigChoice
from django.db import models
from django.utils.translation import gettext_lazy as _

from autoslug import AutoSlugField


def slugify(value):
    return value.replace(" ", "-").lower()


class CoreModel(models.Model):

    created = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True


class HotelType(models.Model):
    name = models.CharField(_("Hotel Types Name"), max_length=255)

    class Meta:
        verbose_name = _("Hotel Type")
        verbose_name_plural = _("Hotel Types")

    def __str__(self) -> str:
        return self.name


class HotelSpecifications(models.Model):
    hotel_type = models.ForeignKey(HotelType, on_delete=models.RESTRICT)
    name = models.CharField(_("Hotel Spec Name"), max_length=255)

    class Meta:
        verbose_name = _("Hotel Specification")
        verbose_name_plural = _("Hotel Specifications")

    def __str__(self) -> str:
        return f"{self.name}"


class Hotel(CoreModel):
    name = models.CharField(_("Hotel Name"), max_length=50)
    description = models.TextField(_("Hotel Descriptions"), default="")
    hotel_type = models.ForeignKey(HotelType, on_delete=models.CASCADE)
    slug = AutoSlugField(
        verbose_name=_("Hotel Slug"),
        populate_from="name",
        slugify=slugify,
    )
    is_active = models.BooleanField(default=True)
    config_choice = models.ForeignKey(ConfigChoice, on_delete=models.RESTRICT)

    class Meta:
        verbose_name = _("Hotel")
        verbose_name_plural = _("Hotels")

    def get_absolute_url(self):
        return f"/{self.slug}/"

    def __str__(self) -> str:
        return self.name


class HotelSpecificationValue(models.Model):
    hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE)
    specification = models.ForeignKey(HotelSpecifications, on_delete=models.RESTRICT)
    value = models.CharField(
        _("Value"),
        max_length=255,
        help_text=_("Hotel specification value (maximum of 255 words"),
    )

    class Meta:
        verbose_name = _("Hotel Specification Value")
        verbose_name_plural = _("Hotel Specification Values")

    def __str__(self):
        return self.value


class HotelImage(CoreModel):
    hotel = models.ForeignKey(
        Hotel, on_delete=models.CASCADE, related_name="hotel_image"
    )
    image_urls = models.URLField(
        _("Hotel Image URLs"),
        help_text=_("Images Urls"),
    )
    caption = models.CharField(
        verbose_name=_("Alternative text"),
        help_text=_("Please add alturnative text"),
        max_length=255,
        null=True,
        blank=True,
    )
    is_feature = models.BooleanField(default=False)

    class Meta:
        verbose_name = _("Hotel Image")
        verbose_name_plural = _("Hotel Images")


class HotelAddress(models.Model):
    hotel = models.ForeignKey(
        Hotel, on_delete=models.CASCADE, related_name="hotel_address"
    )
    address = models.ForeignKey(Address, on_delete=models.CASCADE)

    def __str__(self):
        return f"{self.hotel.name} {self.address.city}"

Enter fullscreen mode Exit fullscreen mode

Registering models to admin.py

from django.contrib import admin

from .models import (
    Hotel,
    HotelImage,
    HotelSpecifications,
    HotelSpecificationValue,
    HotelType,
    HotelAddress,
)


class HotelSpecificationInline(admin.TabularInline):
    model = HotelSpecifications


@admin.register(HotelType)
class HotelTypeAdmin(admin.ModelAdmin):
    inlines = [
        HotelSpecificationInline,
    ]


class HotelImageInline(admin.TabularInline):
    model = HotelImage


class HotelSpecificationValueInline(admin.TabularInline):
    model = HotelSpecificationValue


@admin.register(Hotel)
class HotelAdmin(admin.ModelAdmin):
    inlines = [HotelSpecificationValueInline, HotelImageInline]


admin.site.register(HotelAddress)

Enter fullscreen mode Exit fullscreen mode

create a file search_indexes.py inside searches app.

#searches/search_indexes.py

from django.utils import timezone
from haystack import indexes

from .models import Hotel, HotelAddress, HotelImage, HotelSpecificationValue


class HotelIndex(indexes.SearchIndex, indexes.Indexable):
    text = indexes.CharField(document=True, use_template=True)
    name = indexes.CharField(model_attr="name")
    hotel_type = indexes.CharField(model_attr="hotel_type")
    config_choice = indexes.CharField(model_attr="config_choice")
    autocomplete = indexes.EdgeNgramField()

    @staticmethod
    def prepare_autocomplete(obj):
        return " ".join((obj.name, obj.hotel_type.name, obj.config_choice.name))

    def get_model(self):
        return Hotel


class HotelSpecIndex(indexes.SearchIndex, indexes.Indexable):
    text = indexes.CharField(document=True, use_template=True)
    value = indexes.CharField(model_attr="value")

    def get_model(self):
        return HotelSpecificationValue


class HotelImageIndex(indexes.SearchIndex, indexes.Indexable):
    text = indexes.CharField(document=True, use_template=True)
    image_urls = indexes.CharField(model_attr="image_urls")
    caption = indexes.CharField(model_attr="caption")

    def get_model(self):
        return HotelImage


class HotelAddressIndex(indexes.SearchIndex, indexes.Indexable):
    text = indexes.CharField(document=True, use_template=True)
    address = indexes.CharField(model_attr="address")

    def get_model(self):
        return HotelAddress
Enter fullscreen mode Exit fullscreen mode
  • Creates a unique SearchIndex for each type of Model you wish to index, though you can reuse the same SearchIndex between different models if you take care in doing so and your field names are very standardized.
  • To build a SearchIndex, all that’s necessary is to subclass both indexes.SearchIndex & indexes.Indexable, define the fields you want to store data with and define a get_model method.

Serialization and views:

#searches/serializers.py

from drf_haystack.serializers import HaystackSerializer

from .search_indexes import (
    HotelIndex,
    HotelSpecIndex,
    HotelImageIndex,
    HotelAddressIndex,
)


class AggregateSerializer(HaystackSerializer):
    class Meta:
        index_classes = [HotelIndex, HotelSpecIndex, HotelImageIndex, HotelAddressIndex]
        fields = [
            "name",
            "hotel",
            "config_choice",
            "value",
            "image_urls",
            "caption",
            "address",
            "autocomplete",
        ]

Enter fullscreen mode Exit fullscreen mode
# searches/serializers.py

from .serializers import AggregateSerializer
from rest_framework.mixins import ListModelMixin
from drf_haystack.generics import HaystackGenericAPIView


class AggregateSearchViewSet(ListModelMixin, HaystackGenericAPIView):

    serializer_class = AggregateSerializer

    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)

Enter fullscreen mode Exit fullscreen mode

so you can create a each class of serializers for each models Like this.

# searches/urls.py

from django.urls import path

from .views import AggregateSearchViewSet


urlpatterns = [
path("hotels/search/", AggregateSearchViewSet.as_view())
]

Enter fullscreen mode Exit fullscreen mode

Create a templates directory inside searches app.
Templates folder will look like this:

templates
    ├── search
        ├── indexes
            ├── searches
                ├── hotel_text.txt
                ├── hoteladdress_text.txt
                ├── hotelimage_text.txt
                ├── hotelspecificationvalue_text.txt

Enter fullscreen mode Exit fullscreen mode

Finally migrate your apps, createsuperuser and add some hotels data using django admin panels.

Simply run

./manage.py rebuild_index.

You’ll get some totals of how many models were processed and placed in the index.

Query time!

Now that we have a view wired up, we can start using it. By default, the HaystackGenericAPIView class is set up to use the HaystackFilter. This is the most basic filter included and can do basic search by querying any of the field included in the fields attribute on the Serializer.

http://127.0.0.1:8000/api/v1/hotels/search/

[
    {
        "image_urls": "https://images.moviesanywhere.com/8ccb2868a61ac0612d780eb3b18e5220/6fda2dc9-a774-4ba6-9e80-679accfcc8ed.jpg?h=375&resize=fit&w=250",
        "caption": "img"
    },
    {
        "name": "Transylvania Hotal",
        "config_choice": "Active",
        "autocomplete": "Transylvania Hotal 3 Star Active"
    },
    {
        "value": "12 AD"
    },
    {
        "value": "Monsters Hotel"
    },
    {
        "address": "US"
    },
    {
        "value": "12 AD"
    },
    {
        "value": "gogogog"
    },
    {
        "image_urls": "https://images.moviesanywhere.com/8ccb2868a61ac0612d780eb3b18e5220/6fda2dc9-a774-4ba6-9e80-679accfcc8ed.jpg?h=375&resize=fit&w=250",
        "caption": "img"
    },
    {
        "value": "lONG LONG TIME AGO"
    },
    {
        "name": "demo",
        "config_choice": "Active",
        "autocomplete": "demo 3 Star Active"
    },
    {
        "value": "lONG LONG TIME AGO"
    }
]


http://127.0.0.1:8000/api/v1/hotels/search/?name="demo"

"results": [
        {
            "name": "demo",
            "config_choice": "Active",
            "autocomplete": "demo 3 Star Active"
        }
    ]
Enter fullscreen mode Exit fullscreen mode

GitHub logo lyamaa / elastic_search_dj_example

Examples on Django + haystack + elastic search

💖 💪 🙅 🚩
lyamaa
Lyamaa

Posted on May 21, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Elastic Search + Django
django Elastic Search + Django

May 21, 2021