DEV Community

Cover image for Elastic Search + Django
Lyamaa
Lyamaa

Posted on • Edited on

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

Top comments (1)

Collapse
 
sirdarknight profile image
SirDarknight

You should talk a little bit about what you're exactly doing and hoping to accomplish with this instead of just showing the codes.