Requirements:
- Django
- Elastic Search Install (required version 7)
- Drf Haystack
- Poetry Install or you can use pip or pipenv
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
project directory should look like:
── dj_elastic
├── main
│ ├── **init**.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
└── commons
└── searches
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")),
]
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"
👏🏻 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}"``
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)
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}"
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)
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
- 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",
]
# 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)
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())
]
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
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"
}
]
Top comments (1)
You should talk a little bit about what you're exactly doing and hoping to accomplish with this instead of just showing the codes.