From 108eed65ddda983b371c9a9ce236ec34224eda17 Mon Sep 17 00:00:00 2001 From: sm4640 Date: Sun, 27 Jul 2025 23:50:02 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Feat:=20[#71]=20atlas=20search?= =?UTF-8?q?=EB=A5=BC=20=ED=99=9C=EC=9A=A9=ED=95=9C=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/urls.py | 1 + homes/__init__.py | 0 homes/admin.py | 3 ++ homes/apps.py | 6 +++ homes/filters.py | 0 homes/models.py | 0 homes/serializers.py | 1 + homes/services.py | 112 +++++++++++++++++++++++++++++++++++++++++++ homes/tests.py | 3 ++ homes/urls.py | 9 ++++ homes/views.py | 53 ++++++++++++++++++++ 11 files changed, 188 insertions(+) create mode 100644 homes/__init__.py create mode 100644 homes/admin.py create mode 100644 homes/apps.py create mode 100644 homes/filters.py create mode 100644 homes/models.py create mode 100644 homes/serializers.py create mode 100644 homes/services.py create mode 100644 homes/tests.py create mode 100644 homes/urls.py create mode 100644 homes/views.py diff --git a/config/urls.py b/config/urls.py index 2507916..4120ef7 100644 --- a/config/urls.py +++ b/config/urls.py @@ -12,6 +12,7 @@ urlpatterns = [ path('api/project/', include('projects.urls')), path('api/notification/', include('notifications.urls')), path('api/nocodetool/', include('nocodetools.urls')), + path('api/home/', include('homes.urls')), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # if settings.DEBUG: diff --git a/homes/__init__.py b/homes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/homes/admin.py b/homes/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/homes/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/homes/apps.py b/homes/apps.py new file mode 100644 index 0000000..10d5e34 --- /dev/null +++ b/homes/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class HomesConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'homes' diff --git a/homes/filters.py b/homes/filters.py new file mode 100644 index 0000000..e69de29 diff --git a/homes/models.py b/homes/models.py new file mode 100644 index 0000000..e69de29 diff --git a/homes/serializers.py b/homes/serializers.py new file mode 100644 index 0000000..ec274bc --- /dev/null +++ b/homes/serializers.py @@ -0,0 +1 @@ +# 포폴, 플젝 주간 랭킹, 광고 등 \ No newline at end of file diff --git a/homes/services.py b/homes/services.py new file mode 100644 index 0000000..1352d33 --- /dev/null +++ b/homes/services.py @@ -0,0 +1,112 @@ +from django.conf import settings +import certifi + +from pymongo import MongoClient + +from django.db.models import Case, When, IntegerField + +from projects.models import Project +from portfolios.models import Portfolio + +NOCODETOOL_MODEL_MAP = { + 'project': Project, + 'portfolio': Portfolio, + } + +class NocodetoolSearchService: + @staticmethod + def search(search_term, object_type, page, page_size): + client = MongoClient(settings.MONGODB_URI, tlsCAFile=certifi.where()) + db = client[settings.MONGODB_NAME] + coll = db["code"] + + filters = [] + if object_type in ("project", "portfolio"): + filters.append({"term": {"path": "object_type", "query": object_type}}) + + pipeline = [ + { + "$search": { + "index": "nocodetool_content_keyword_search_index", + "compound": { + "filter": filters, + "should": [ + { + "text": { + "query": search_term, + "path": "keyword", + "score": {"boost": {"value": 5}} + } + }, + { + "text": { + "query": search_term, + "path": "pages.elements.content", + "score": {"boost": {"value": 10}} + } + }, + { + "text": { + "query": search_term, + "path": "description", + "score": {"boost": {"value": 7}} + } + } + ], + "minimumShouldMatch": 1 + } + } + }, + { + "$project": { + "keyword": 1, + "description": 1, + "pages.elements.content": 1, + "object_type": 1, + "object_id": 1, + "score": {"$meta": "searchScore"} + } + }, + { + "$facet": { + "projects": [ + {"$match": {"object_type": "project"}}, + {"$sort": {"score": -1}}, + {"$skip": (page - 1) * page_size}, + {"$limit": page_size} + ], + "portfolios": [ + {"$match": {"object_type": "portfolio"}}, + {"$sort": {"score": -1}}, + {"$skip": (page - 1) * page_size}, + {"$limit": page_size} + ] + } + } + ] + + agg_result = list(coll.aggregate(pipeline)) + print(agg_result) + buckets = agg_result[0] if agg_result else {"projects": [], "portfolios": []} + return buckets + + def get_ids_from_buckets(buckets: list) -> list: + return [object["object_id"] for object in buckets] + + + +class NocodetoolToObjectService: + def get_objects_by_ids(object_type, objects_ids: list) -> list: + if not objects_ids: + return [] + + order = Case( + *[When(id=pk, then=pos) for pos, pk in enumerate(objects_ids)], + output_field=IntegerField() + ) + + object_model = NOCODETOOL_MODEL_MAP.get(object_type) + if not object_model: + return [] + + return list(object_model.objects.filter(id__in=objects_ids).order_by(order)) diff --git a/homes/tests.py b/homes/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/homes/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/homes/urls.py b/homes/urls.py new file mode 100644 index 0000000..626c39a --- /dev/null +++ b/homes/urls.py @@ -0,0 +1,9 @@ +from django.urls import include, path + +from .views import * + +app_name = 'homes' + +urlpatterns = [ + path('search/', SearchAPIView.as_view()), +] \ No newline at end of file diff --git a/homes/views.py b/homes/views.py new file mode 100644 index 0000000..5aa23e2 --- /dev/null +++ b/homes/views.py @@ -0,0 +1,53 @@ +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status, mixins, viewsets +from rest_framework.permissions import AllowAny, IsAuthenticated + +from django.db import transaction + +from .serializers import * +from .services import * + +from projects.models import Project +from projects.serializers import ProjectListViewSerializer + +from portfolios.models import Portfolio +from portfolios.serializers import PortfolioListViewSerializer + +from users.models import User + +class HomeAPIView(APIView): + def get(self, request): + pass + +class SearchAPIView(APIView): + def get(self, request): + search_team = request.query_params.get('q', '') + object_type = request.query_params.get('type', None) + try: + page = int(request.query_params.get("page", 1)) + except ValueError: + return Response({"is_page_int": False,"detail": "page and page_size must be integers"}, status=status.HTTP_400_BAD_REQUEST) + + if page < 1: + return Response({"is_page_gte_1": False, "detail": "page and page_size must be >= 1"}, status=status.HTTP_400_BAD_REQUEST) + + page_size = 8 + + buckets = NocodetoolSearchService.search(search_team, object_type, page, page_size) + project_list = NocodetoolSearchService.get_ids_from_buckets(buckets["projects"]) + portfolio_list = NocodetoolSearchService.get_ids_from_buckets(buckets["portfolios"]) + + projects_data = ProjectListViewSerializer( + NocodetoolToObjectService.get_objects_by_ids('project', project_list), + many=True).data + portfolios_data = PortfolioListViewSerializer( + NocodetoolToObjectService.get_objects_by_ids('portfolio', portfolio_list), + many=True).data + + return Response({ + "projects": projects_data, + "portfolios": portfolios_data, + "page": page, + "page_size": page_size + }, status=status.HTTP_200_OK)