From 3dce7fc3444ca6dc29e42102dc0d99606fd57fcc Mon Sep 17 00:00:00 2001 From: Mojtaba-z Date: Tue, 6 May 2025 16:22:35 +0330 Subject: [PATCH] initail elasticsearch --- Rasaddam_Backend/settings.py | 24 +++++--- apps/authentication/api/v1/api.py | 5 ++ apps/authentication/api/v1/search_view.py | 53 ++++++++++++++++++ .../api/v1/serializers/serializer.py | 11 ++++ apps/authentication/api/v1/urls.py | 6 +- apps/authentication/documents.py | 24 ++++++++ apps/authentication/views.py | 2 - apps/search/__init__.py | 0 apps/search/admin.py | 3 + apps/search/apps.py | 6 ++ apps/search/certs/http.p12 | Bin 0 -> 10029 bytes apps/search/certs/http_ca.crt | 31 ++++++++++ apps/search/certs/transport.p12 | Bin 0 -> 5838 bytes apps/search/models.py | 3 + apps/search/tests.py | 3 + apps/search/views.py | 4 ++ apps/tag/models.py | 6 ++ requirements.txt | 6 +- 18 files changed, 176 insertions(+), 11 deletions(-) create mode 100644 apps/authentication/api/v1/search_view.py create mode 100644 apps/authentication/documents.py create mode 100644 apps/search/__init__.py create mode 100644 apps/search/admin.py create mode 100644 apps/search/apps.py create mode 100644 apps/search/certs/http.p12 create mode 100644 apps/search/certs/http_ca.crt create mode 100644 apps/search/certs/transport.p12 create mode 100644 apps/search/models.py create mode 100644 apps/search/tests.py create mode 100644 apps/search/views.py diff --git a/Rasaddam_Backend/settings.py b/Rasaddam_Backend/settings.py index 969fd7b..9aba70b 100644 --- a/Rasaddam_Backend/settings.py +++ b/Rasaddam_Backend/settings.py @@ -14,11 +14,9 @@ from pathlib import Path from datetime import timedelta from django.conf import settings - # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent - # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ @@ -28,8 +26,7 @@ SECRET_KEY = 'django-insecure-@0apn-lk85pfw=z00x2ib$w9#rwz8%2v4i_n^^9jz-m9b+y55* # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'https://rasadyar.net/'] - +ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'https://rasadyar.net/', 'https://localhost:9200'] # Application definition @@ -40,6 +37,8 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django_elasticsearch_dsl', + 'django_elasticsearch_dsl_drf', 'rest_framework', "corsheaders", 'rest_framework_simplejwt', @@ -52,6 +51,7 @@ INSTALLED_APPS = [ 'apps.pos_machine.apps.PosMachineConfig', 'apps.tag.apps.TagConfig', 'apps.warehouse.apps.WarehouseConfig', + 'apps.search.apps.SearchConfig', 'rest_captcha', 'captcha', ] @@ -85,7 +85,6 @@ TEMPLATES = [ WSGI_APPLICATION = 'Rasaddam_Backend.wsgi.application' - # Database # https://docs.djangoproject.com/en/5.2/ref/settings/#databases @@ -111,6 +110,8 @@ REST_FRAMEWORK = { 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.BasicAuthentication', ), + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", + "PAGE_SIZE": 25 } SIMPLE_JWT = { @@ -183,6 +184,17 @@ REST_CAPTCHA = { 'NOISE_FUNCTION': 'apps.captcha_app.api.v1.serializers.noise_default' } +ELASTICSEARCH_DSL = { + # elastic HSA256 finger print f7d94c1da0668ba7874e5e09c3b1b91284fcdda97c361e0165401dc9375531b0 # noqa + # liara elastic password uYkiQ860vLW8DIbWpNjqtz2B # noqa + # local system password =z66+LCIebq4NQRR_+=R # noqa + "default": { + "hosts": "https://127.0.0.1:9200", + "http_auth": ("elastic", "uYkiQ860vLW8DIbWpNjqtz2B"), + } +} + + # Password validation # https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators @@ -201,7 +213,6 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] - # Internationalization # https://docs.djangoproject.com/en/5.2/topics/i18n/ @@ -245,7 +256,6 @@ CORS_ALLOWED_ORIGINS = ( 'https://rasadyar.net' ) - SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') SECURE_SSL_REDIRECT = False SESSION_COOKIE_SECURE = False diff --git a/apps/authentication/api/v1/api.py b/apps/authentication/api/v1/api.py index 0ae2dda..2838e49 100644 --- a/apps/authentication/api/v1/api.py +++ b/apps/authentication/api/v1/api.py @@ -4,6 +4,7 @@ from rest_framework_simplejwt.views import TokenObtainPairView from rest_framework.viewsets import ModelViewSet from rest_framework.decorators import action from apps.authentication.models import User +from rest_framework.views import APIView from django.db import transaction @@ -28,3 +29,7 @@ class Authentication(ModelViewSet): @transaction.atomic def login(self, request): pass + + +class UserViewSet(ModelViewSet): + pass \ No newline at end of file diff --git a/apps/authentication/api/v1/search_view.py b/apps/authentication/api/v1/search_view.py new file mode 100644 index 0000000..84db93c --- /dev/null +++ b/apps/authentication/api/v1/search_view.py @@ -0,0 +1,53 @@ +from apps.authentication.api.v1.serializers.serializer import UserSerializer +from rest_framework.pagination import LimitOffsetPagination +from rest_framework.viewsets import ModelViewSet, ViewSet +from apps.authentication.documents import UserDocument +from rest_framework.response import Response +from django.http.response import HttpResponse +from apps.authentication.models import User +from rest_framework.views import APIView +from elasticsearch_dsl.query import Q +import abc + + +class PaginatedElasticSearchApiView(APIView, LimitOffsetPagination): + """Base ApiView Class for elasticsearch views with pagination + Other ApiView classes should inherit from this class""" + serializer_class = None + document_class = None + + @abc.abstractmethod + def generate_q_expression(self, query): + """This method should be overridden + and return a Q() expression.""" + + def get(self, request, query): + try: + q = self.generate_q_expression(query) + search = self.document_class.search().query(q) + response = search.execute() + + print(f"Found {response.hits.total.value} hit(s) for query: '{query}'") + + results = self.paginate_queryset(response, request, view=self) # noqa + serializer = self.serializer_class(results, many=True) + return self.get_paginated_response(serializer.data) + except Exception as e: + return HttpResponse(e, status=500) + + +class SearchUsersApiView(PaginatedElasticSearchApiView): # noqa + """Search in Users""" + + serializer_class = UserSerializer + document_class = UserDocument + + def generate_q_expression(self, query): + return Q( + 'multi_match', + query=query, + fields=[ + 'username', + 'mobile' + ], fuzziness='auto' + ) diff --git a/apps/authentication/api/v1/serializers/serializer.py b/apps/authentication/api/v1/serializers/serializer.py index e69de29..3df7158 100644 --- a/apps/authentication/api/v1/serializers/serializer.py +++ b/apps/authentication/api/v1/serializers/serializer.py @@ -0,0 +1,11 @@ +from rest_framework import serializers +from apps.authentication.models import User + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = [ + 'username', + 'mobile' + ] diff --git a/apps/authentication/api/v1/urls.py b/apps/authentication/api/v1/urls.py index 15d42ea..dd2ce67 100644 --- a/apps/authentication/api/v1/urls.py +++ b/apps/authentication/api/v1/urls.py @@ -5,10 +5,14 @@ from rest_framework_simplejwt.views import ( TokenRefreshView, TokenVerifyView ) -from .api import CustomizedTokenObtainPairView +from .api import ( + CustomizedTokenObtainPairView +) +from .search_view import SearchUsersApiView urlpatterns = [ path('login/', CustomizedTokenObtainPairView.as_view(), name='token_obtain_pair'), + path('search_user/', SearchUsersApiView.as_view(), name='search_user'), path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), path('token/verify/', TokenVerifyView.as_view(), name='token_verify'), ] diff --git a/apps/authentication/documents.py b/apps/authentication/documents.py new file mode 100644 index 0000000..accb310 --- /dev/null +++ b/apps/authentication/documents.py @@ -0,0 +1,24 @@ +from .models import User, Province +from django_elasticsearch_dsl import Document, fields +from django_elasticsearch_dsl.registries import registry + + +@registry.register_document +class UserDocument(Document): + """ElasticSearch Document for indexing users""" + + class Index: + name = 'users' + settings = { + 'number_of_shards': 1, + 'number_of_replicas': 0 # number of copies from data in document + } + + class Django: + model = User + fields = [ + "id", + "username", + "mobile", + "nationality" + ] diff --git a/apps/authentication/views.py b/apps/authentication/views.py index 91ea44a..60f00ef 100644 --- a/apps/authentication/views.py +++ b/apps/authentication/views.py @@ -1,3 +1 @@ -from django.shortcuts import render - # Create your views here. diff --git a/apps/search/__init__.py b/apps/search/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/search/admin.py b/apps/search/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/apps/search/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/search/apps.py b/apps/search/apps.py new file mode 100644 index 0000000..9bcfcd2 --- /dev/null +++ b/apps/search/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class SearchConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.search' diff --git a/apps/search/certs/http.p12 b/apps/search/certs/http.p12 new file mode 100644 index 0000000000000000000000000000000000000000..1b883267be8e399be5bddc03da40b495224625ff GIT binary patch literal 10029 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