├── .gitignore ├── LICENSE ├── README.md ├── blog ├── __init__.py ├── admin.py ├── apps.py ├── documents.py ├── management │ └── commands │ │ └── populate_db.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── serializers.py ├── tests.py ├── urls.py └── views.py ├── core ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py ├── manage.py ├── requirements.txt └── search ├── __init__.py ├── admin.py ├── apps.py ├── migrations └── __init__.py ├── models.py ├── tests.py ├── urls.py └── views.py /.gitignore: -------------------------------------------------------------------------------- 1 | db.sqlite3 2 | env 3 | venv 4 | __pycache__ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 TestDriven Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django DRF Elasticsearch 2 | 3 | ## Want to learn how to build this? 4 | 5 | Check out the [post](https://testdriven.io/blog/django-drf-elasticsearch/). 6 | 7 | ## Want to use this project? 8 | 9 | 1. Fork/Clone 10 | 11 | 2. [Install Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/8.11/docker.html) if you haven't already and make sure it is running on port `9200`. Make sure to update the `ELASTICSEARCH_DSL` config in *core/settings.py*. 12 | 13 | 3. Create and activate a virtual environment: 14 | 15 | ```sh 16 | $ python3.12 -m venv venv && source venv/bin/activate 17 | ``` 18 | 19 | 4. Install the requirements: 20 | 21 | ```sh 22 | (venv)$ pip install -r requirements.txt 23 | ``` 24 | 25 | 5. Apply the migrations: 26 | 27 | ```sh 28 | (venv)$ python manage.py migrate 29 | ``` 30 | 31 | 6. Populate the database with some test data by running the following command: 32 | 33 | ```sh 34 | (venv)$ python manage.py populate_db 35 | ``` 36 | 37 | 7. Create and populate the Elasticsearch index and mapping: 38 | 39 | ```sh 40 | (venv)$ python manage.py search_index --rebuild 41 | ``` 42 | 43 | 8. Run the server 44 | 45 | ```sh 46 | (venv)$ python manage.py runserver 47 | ``` 48 | 49 | 9. Test Elasticsearch with the following queries: 50 | 51 | - [http://127.0.0.1:8000/search/user/mike/](http://127.0.0.1:8000/search/user/mike/) - should find the user 'mike13' 52 | - [http://127.0.0.1:8000/search/user/jess_/](http://127.0.0.1:8000/search/user/jess_/) - should find the user 'jess_' 53 | - [http://127.0.0.1:8000/search/category/seo/](http://127.0.0.1:8000/search/category/seo/) - should find the category 'SEO optimization' 54 | - [http://127.0.0.1:8000/search/category/progreming/](http://127.0.0.1:8000/search/category/progreming/) - should find the category 'Programming' (:warning: notice the typo) 55 | - [http://127.0.0.1:8000/search/article/linux/](http://127.0.0.1:8000/search/article/linux/) - should find the article 'Installing the latest version of Ubuntu' 56 | - [http://127.0.0.1:8000/search/article/java/](http://127.0.0.1:8000/search/article/java/) - should find the article 'Which programming language is the best?' 57 | -------------------------------------------------------------------------------- /blog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/django-drf-elasticsearch/a651737df0ca3c42a5cca39a1034164fec028757/blog/__init__.py -------------------------------------------------------------------------------- /blog/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from blog.models import Category, Article 4 | 5 | 6 | admin.site.register(Category) 7 | admin.site.register(Article) 8 | -------------------------------------------------------------------------------- /blog/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BlogConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "blog" 7 | -------------------------------------------------------------------------------- /blog/documents.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django_elasticsearch_dsl import Document, fields 3 | from django_elasticsearch_dsl.registries import registry 4 | 5 | from blog.models import Category, Article 6 | 7 | 8 | @registry.register_document 9 | class UserDocument(Document): 10 | class Index: 11 | name = "users" 12 | settings = { 13 | "number_of_shards": 1, 14 | "number_of_replicas": 0, 15 | } 16 | 17 | class Django: 18 | model = User 19 | fields = [ 20 | "id", 21 | "first_name", 22 | "last_name", 23 | "username", 24 | ] 25 | 26 | 27 | @registry.register_document 28 | class CategoryDocument(Document): 29 | id = fields.IntegerField() 30 | 31 | class Index: 32 | name = "categories" 33 | settings = { 34 | "number_of_shards": 1, 35 | "number_of_replicas": 0, 36 | } 37 | 38 | class Django: 39 | model = Category 40 | fields = [ 41 | "name", 42 | "description", 43 | ] 44 | 45 | 46 | @registry.register_document 47 | class ArticleDocument(Document): 48 | author = fields.ObjectField(properties={ 49 | "id": fields.IntegerField(), 50 | "first_name": fields.TextField(), 51 | "last_name": fields.TextField(), 52 | "username": fields.TextField(), 53 | }) 54 | categories = fields.ObjectField(properties={ 55 | "id": fields.IntegerField(), 56 | "name": fields.TextField(), 57 | "description": fields.TextField(), 58 | }) 59 | type = fields.TextField(attr="type_to_string") 60 | 61 | class Index: 62 | name = "articles" 63 | settings = { 64 | "number_of_shards": 1, 65 | "number_of_replicas": 0, 66 | } 67 | 68 | class Django: 69 | model = Article 70 | fields = [ 71 | "title", 72 | "content", 73 | "created_datetime", 74 | "updated_datetime", 75 | ] 76 | -------------------------------------------------------------------------------- /blog/management/commands/populate_db.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.core.management.base import BaseCommand 3 | 4 | from blog.models import Category, Article 5 | 6 | 7 | class Command(BaseCommand): 8 | help = "Populates the database with some testing data." 9 | 10 | def handle(self, *args, **options): 11 | self.stdout.write(self.style.SUCCESS("Started database population process...")) 12 | 13 | if User.objects.filter(username="mike13").exists(): 14 | self.stdout.write(self.style.SUCCESS("Database has already been populated. Cancelling the operation.")) 15 | return 16 | 17 | # Create users 18 | mike = User.objects.create_user(username="mike13", password="really_strong_password123") 19 | mike.first_name = "Mike" 20 | mike.last_name = "Smith" 21 | mike.save() 22 | 23 | jess = User.objects.create_user(username="jess_", password="really_strong_password123") 24 | jess.first_name = "Jess" 25 | jess.last_name = "Brown" 26 | jess.save() 27 | 28 | johnny = User.objects.create_user(username="johnny", password="really_strong_password123") 29 | johnny.first_name = "Johnny" 30 | johnny.last_name = "Davis" 31 | johnny.save() 32 | 33 | # Create categories 34 | system_administration = Category.objects.create(name="System administration") 35 | seo_optimization = Category.objects.create(name="SEO optimization") 36 | programming = Category.objects.create(name="Programming") 37 | 38 | # Create articles 39 | website_article = Article.objects.create( 40 | title="How to code and deploy a website?", 41 | author=mike, 42 | type="TU", 43 | content="There are numerous ways of how you can deploy a website...", 44 | ) 45 | website_article.save() 46 | website_article.categories.add(programming, system_administration, seo_optimization) 47 | 48 | google_article = Article.objects.create( 49 | title="How to improve your Google rating?", 50 | author=jess, 51 | type="TU", 52 | content="Firstly, add the correct SEO tags...", 53 | ) 54 | google_article.save() 55 | google_article.categories.add(seo_optimization) 56 | 57 | programming_article = Article.objects.create( 58 | title="Which programming language is the best?", 59 | author=jess, 60 | type="RS", 61 | content="The best programming languages are:\n1) Python\n2) Java\n3) C/C++...", 62 | ) 63 | programming_article.save() 64 | programming_article.categories.add(programming) 65 | 66 | ubuntu_article = Article.objects.create( 67 | title="Installing the latest version of Ubuntu", 68 | author=johnny, 69 | type="TU", 70 | content="In this tutorial, we'll take a look at how to setup the latest version of Ubuntu. Ubuntu " 71 | "(/ʊˈbʊntuː/ is a Linux distribution based on Debian and composed mostly of free and open-source" 72 | " software. Ubuntu is officially released in three editions: Desktop, Server, and Core for " 73 | "Internet of things devices and robots.", 74 | ) 75 | ubuntu_article.save() 76 | ubuntu_article.categories.add(system_administration) 77 | 78 | django_article = Article.objects.create( 79 | title="Django REST Framework and Elasticsearch", 80 | author=johnny, 81 | type="TU", 82 | content="In this tutorial, we'll look at how to integrate Django REST Framework with Elasticsearch. " 83 | "We'll use Django to model our data and DRF to serialize and serve it. Finally, we'll index the data " 84 | "with Elasticsearch and make it searchable.", 85 | ) 86 | django_article.save() 87 | django_article.categories.add(system_administration) 88 | 89 | self.stdout.write(self.style.SUCCESS("Successfully populated the database.")) 90 | -------------------------------------------------------------------------------- /blog/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.7 on 2023-11-25 16:22 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name="Category", 19 | fields=[ 20 | ( 21 | "id", 22 | models.BigAutoField( 23 | auto_created=True, 24 | primary_key=True, 25 | serialize=False, 26 | verbose_name="ID", 27 | ), 28 | ), 29 | ("name", models.CharField(max_length=32)), 30 | ("description", models.TextField(blank=True, null=True)), 31 | ], 32 | options={ 33 | "verbose_name_plural": "categories", 34 | }, 35 | ), 36 | migrations.CreateModel( 37 | name="Article", 38 | fields=[ 39 | ( 40 | "id", 41 | models.BigAutoField( 42 | auto_created=True, 43 | primary_key=True, 44 | serialize=False, 45 | verbose_name="ID", 46 | ), 47 | ), 48 | ("title", models.CharField(max_length=256)), 49 | ( 50 | "type", 51 | models.CharField( 52 | choices=[ 53 | ("UN", "Unspecified"), 54 | ("TU", "Tutorial"), 55 | ("RS", "Research"), 56 | ("RW", "Review"), 57 | ], 58 | default="UN", 59 | max_length=2, 60 | ), 61 | ), 62 | ("content", models.TextField()), 63 | ("created_datetime", models.DateTimeField(auto_now_add=True)), 64 | ("updated_datetime", models.DateTimeField(auto_now=True)), 65 | ( 66 | "author", 67 | models.ForeignKey( 68 | on_delete=django.db.models.deletion.CASCADE, 69 | to=settings.AUTH_USER_MODEL, 70 | ), 71 | ), 72 | ( 73 | "categories", 74 | models.ManyToManyField( 75 | blank=True, related_name="categories", to="blog.category" 76 | ), 77 | ), 78 | ], 79 | ), 80 | ] 81 | -------------------------------------------------------------------------------- /blog/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/django-drf-elasticsearch/a651737df0ca3c42a5cca39a1034164fec028757/blog/migrations/__init__.py -------------------------------------------------------------------------------- /blog/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.db import models 3 | 4 | 5 | class Category(models.Model): 6 | name = models.CharField(max_length=32) 7 | description = models.TextField(null=True, blank=True) 8 | 9 | class Meta: 10 | verbose_name_plural = "categories" 11 | 12 | def __str__(self): 13 | return f"{self.name}" 14 | 15 | 16 | ARTICLE_TYPES = [ 17 | ("UN", "Unspecified"), 18 | ("TU", "Tutorial"), 19 | ("RS", "Research"), 20 | ("RW", "Review"), 21 | ] 22 | 23 | 24 | class Article(models.Model): 25 | title = models.CharField(max_length=256) 26 | author = models.ForeignKey(to=User, on_delete=models.CASCADE) 27 | type = models.CharField(max_length=2, choices=ARTICLE_TYPES, default="UN") 28 | categories = models.ManyToManyField(to=Category, blank=True, related_name="categories") 29 | content = models.TextField() 30 | created_datetime = models.DateTimeField(auto_now_add=True) 31 | updated_datetime = models.DateTimeField(auto_now=True) 32 | 33 | def type_to_string(self): 34 | if self.type == "UN": 35 | return "Unspecified" 36 | elif self.type == "TU": 37 | return "Tutorial" 38 | elif self.type == "RS": 39 | return "Research" 40 | elif self.type == "RW": 41 | return "Review" 42 | 43 | def __str__(self): 44 | return f"{self.author}: {self.title} ({self.created_datetime.date()})" 45 | -------------------------------------------------------------------------------- /blog/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from rest_framework import serializers 3 | 4 | from blog.models import Article, Category 5 | 6 | 7 | class UserSerializer(serializers.ModelSerializer): 8 | class Meta: 9 | model = User 10 | fields = ("id", "username", "first_name", "last_name") 11 | 12 | 13 | class CategorySerializer(serializers.ModelSerializer): 14 | class Meta: 15 | model = Category 16 | fields = "__all__" 17 | 18 | 19 | class ArticleSerializer(serializers.ModelSerializer): 20 | author = UserSerializer() 21 | categories = CategorySerializer(many=True) 22 | 23 | class Meta: 24 | model = Article 25 | fields = "__all__" 26 | -------------------------------------------------------------------------------- /blog/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /blog/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from rest_framework import routers 3 | 4 | from blog.views import UserViewSet, CategoryViewSet, ArticleViewSet 5 | 6 | router = routers.DefaultRouter() 7 | router.register(r"user", UserViewSet) 8 | router.register(r"category", CategoryViewSet) 9 | router.register(r"article", ArticleViewSet) 10 | 11 | urlpatterns = [ 12 | path("", include(router.urls)), 13 | ] 14 | -------------------------------------------------------------------------------- /blog/views.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from rest_framework import viewsets 3 | 4 | from blog.models import Category, Article 5 | from blog.serializers import CategorySerializer, ArticleSerializer, UserSerializer 6 | 7 | 8 | class UserViewSet(viewsets.ModelViewSet): 9 | serializer_class = UserSerializer 10 | queryset = User.objects.all() 11 | 12 | 13 | class CategoryViewSet(viewsets.ModelViewSet): 14 | serializer_class = CategorySerializer 15 | queryset = Category.objects.all() 16 | 17 | 18 | class ArticleViewSet(viewsets.ModelViewSet): 19 | serializer_class = ArticleSerializer 20 | queryset = Article.objects.all() 21 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/django-drf-elasticsearch/a651737df0ca3c42a5cca39a1034164fec028757/core/__init__.py -------------------------------------------------------------------------------- /core/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for core project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /core/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for core project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.2.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.2/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = "django-insecure-i1+iuh7*lled$8_igu^0fgw4@z5g+!+p0m4g5=c96ie$r@o-5w" 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | "django.contrib.admin", 35 | "django.contrib.auth", 36 | "django.contrib.contenttypes", 37 | "django.contrib.sessions", 38 | "django.contrib.messages", 39 | "django.contrib.staticfiles", 40 | "django_elasticsearch_dsl", 41 | "blog.apps.BlogConfig", 42 | "search.apps.SearchConfig", 43 | "rest_framework", 44 | ] 45 | 46 | MIDDLEWARE = [ 47 | "django.middleware.security.SecurityMiddleware", 48 | "django.contrib.sessions.middleware.SessionMiddleware", 49 | "django.middleware.common.CommonMiddleware", 50 | "django.middleware.csrf.CsrfViewMiddleware", 51 | "django.contrib.auth.middleware.AuthenticationMiddleware", 52 | "django.contrib.messages.middleware.MessageMiddleware", 53 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 54 | ] 55 | 56 | ROOT_URLCONF = "core.urls" 57 | 58 | TEMPLATES = [ 59 | { 60 | "BACKEND": "django.template.backends.django.DjangoTemplates", 61 | "DIRS": [], 62 | "APP_DIRS": True, 63 | "OPTIONS": { 64 | "context_processors": [ 65 | "django.template.context_processors.debug", 66 | "django.template.context_processors.request", 67 | "django.contrib.auth.context_processors.auth", 68 | "django.contrib.messages.context_processors.messages", 69 | ], 70 | }, 71 | }, 72 | ] 73 | 74 | WSGI_APPLICATION = "core.wsgi.application" 75 | 76 | 77 | # Database 78 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases 79 | 80 | DATABASES = { 81 | "default": { 82 | "ENGINE": "django.db.backends.sqlite3", 83 | "NAME": BASE_DIR / "db.sqlite3", 84 | } 85 | } 86 | 87 | 88 | # Password validation 89 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators 90 | 91 | AUTH_PASSWORD_VALIDATORS = [ 92 | { 93 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 94 | }, 95 | { 96 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 97 | }, 98 | { 99 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 100 | }, 101 | { 102 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 103 | }, 104 | ] 105 | 106 | 107 | # Internationalization 108 | # https://docs.djangoproject.com/en/4.2/topics/i18n/ 109 | 110 | LANGUAGE_CODE = "en-us" 111 | 112 | TIME_ZONE = "UTC" 113 | 114 | USE_I18N = True 115 | 116 | USE_TZ = True 117 | 118 | 119 | # Static files (CSS, JavaScript, Images) 120 | # https://docs.djangoproject.com/en/4.2/howto/static-files/ 121 | 122 | STATIC_URL = "static/" 123 | 124 | # Default primary key field type 125 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field 126 | 127 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 128 | 129 | REST_FRAMEWORK = { 130 | "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", 131 | "PAGE_SIZE": 25 132 | } 133 | 134 | # Elasticsearch 135 | # https://django-elasticsearch-dsl.readthedocs.io/en/latest/settings.html 136 | 137 | ELASTICSEARCH_DSL = { 138 | "default": { 139 | "hosts": "https://localhost:9200", 140 | "http_auth": ("elastic", "YOUR_PASSWORD"), 141 | "ca_certs": "PATH_TO_http_ca.crt", 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /core/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include 3 | 4 | urlpatterns = [ 5 | path("blog/", include("blog.urls")), 6 | path("search/", include("search.urls")), 7 | path("admin/", admin.site.urls), 8 | ] 9 | -------------------------------------------------------------------------------- /core/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for core project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==4.2.7 2 | django-elasticsearch-dsl==8.0 3 | djangorestframework==3.14.0 4 | elasticsearch==8.11.0 5 | elasticsearch-dsl==8.11.0 6 | -------------------------------------------------------------------------------- /search/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/django-drf-elasticsearch/a651737df0ca3c42a5cca39a1034164fec028757/search/__init__.py -------------------------------------------------------------------------------- /search/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /search/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SearchConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "search" 7 | -------------------------------------------------------------------------------- /search/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/testdrivenio/django-drf-elasticsearch/a651737df0ca3c42a5cca39a1034164fec028757/search/migrations/__init__.py -------------------------------------------------------------------------------- /search/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /search/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /search/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from search.views import SearchArticles, SearchCategories, SearchUsers 4 | 5 | urlpatterns = [ 6 | path("user//", SearchUsers.as_view()), 7 | path("category//", SearchCategories.as_view()), 8 | path("article//", SearchArticles.as_view()), 9 | ] 10 | -------------------------------------------------------------------------------- /search/views.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | from django.http import HttpResponse 4 | from elasticsearch_dsl import Q 5 | from rest_framework.pagination import LimitOffsetPagination 6 | from rest_framework.views import APIView 7 | 8 | from blog.documents import ArticleDocument, UserDocument, CategoryDocument 9 | from blog.serializers import ArticleSerializer, UserSerializer, CategorySerializer 10 | 11 | 12 | class PaginatedElasticSearchAPIView(APIView, LimitOffsetPagination): 13 | serializer_class = None 14 | document_class = None 15 | 16 | @abc.abstractmethod 17 | def generate_q_expression(self, query): 18 | """This method should be overridden 19 | and return a Q() expression.""" 20 | 21 | def get(self, request, query): 22 | try: 23 | q = self.generate_q_expression(query) 24 | search = self.document_class.search().query(q) 25 | response = search.execute() 26 | 27 | print(f"Found {response.hits.total.value} hit(s) for query: '{query}'") 28 | 29 | results = self.paginate_queryset(response, request, view=self) 30 | serializer = self.serializer_class(results, many=True) 31 | return self.get_paginated_response(serializer.data) 32 | except Exception as e: 33 | return HttpResponse(e, status=500) 34 | 35 | 36 | # views 37 | 38 | 39 | class SearchUsers(PaginatedElasticSearchAPIView): 40 | serializer_class = UserSerializer 41 | document_class = UserDocument 42 | 43 | def generate_q_expression(self, query): 44 | return Q("bool", 45 | should=[ 46 | Q("match", username=query), 47 | Q("match", first_name=query), 48 | Q("match", last_name=query), 49 | ], minimum_should_match=1) 50 | 51 | 52 | class SearchCategories(PaginatedElasticSearchAPIView): 53 | serializer_class = CategorySerializer 54 | document_class = CategoryDocument 55 | 56 | def generate_q_expression(self, query): 57 | return Q( 58 | "multi_match", query=query, 59 | fields=[ 60 | "name", 61 | "description", 62 | ], fuzziness="auto") 63 | 64 | 65 | class SearchArticles(PaginatedElasticSearchAPIView): 66 | serializer_class = ArticleSerializer 67 | document_class = ArticleDocument 68 | 69 | def generate_q_expression(self, query): 70 | return Q( 71 | "multi_match", query=query, 72 | fields=[ 73 | "title", 74 | "author", 75 | "type", 76 | "content" 77 | ], fuzziness="auto") 78 | --------------------------------------------------------------------------------