From b23e783f66fb935d7d8139e24f56c03b48179325 Mon Sep 17 00:00:00 2001 From: Maxim Date: Sun, 15 Dec 2024 03:00:06 +0300 Subject: [PATCH] add tool search api --- .../tool_repos/itool_repository.py | 13 +++++ .../services/tool_service/tool_service.py | 22 +++++++- .../src/infrastructure/api/tool_controller.py | 29 ++++++++++- backend/src/infrastructure/db/mongo.py | 18 +++++++ .../tool_repos/mongo_tool_repository.py | 52 +++++++++++++++++++ backend/src/main.py | 1 + 6 files changed, 132 insertions(+), 3 deletions(-) diff --git a/backend/src/core/repositories/tool_repos/itool_repository.py b/backend/src/core/repositories/tool_repos/itool_repository.py index 33a2bd9..437dd49 100644 --- a/backend/src/core/repositories/tool_repos/itool_repository.py +++ b/backend/src/core/repositories/tool_repos/itool_repository.py @@ -21,4 +21,17 @@ async def get_total_count(self) -> ToolPages: @abstractmethod async def exists(self, tool_name: str) -> bool: + pass + + @abstractmethod + async def search( + self, + query: str, + page: int, + page_size: int, + category: Optional[List[str]] = None, + type: Optional[List[str]] = None, + min_price: Optional[float] = None, + max_price: Optional[float] = None + ) -> List[ToolSummary]: pass \ No newline at end of file diff --git a/backend/src/core/services/tool_service/tool_service.py b/backend/src/core/services/tool_service/tool_service.py index fc12e72..d1478df 100644 --- a/backend/src/core/services/tool_service/tool_service.py +++ b/backend/src/core/services/tool_service/tool_service.py @@ -108,4 +108,24 @@ async def get_categories_with_types(self) -> List[CategoryWithTypes]: ) result.append(category_with_types) - return result \ No newline at end of file + return result + + async def search_tools( + self, + query: str, + page: int, + page_size: int, + category: Optional[List[str]] = None, + type: Optional[List[str]] = None, + min_price: Optional[float] = None, + max_price: Optional[float] = None + ) -> List[ToolSummary]: + return await self.tool_repo.search( + query=query, + page=page, + page_size=page_size, + category=category, + type=type, + min_price=min_price, + max_price=max_price + ) \ No newline at end of file diff --git a/backend/src/infrastructure/api/tool_controller.py b/backend/src/infrastructure/api/tool_controller.py index 87d4dac..abe528b 100644 --- a/backend/src/infrastructure/api/tool_controller.py +++ b/backend/src/infrastructure/api/tool_controller.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional from fastapi import APIRouter, Depends, Query from src.core.entities.category.category import CategoryName, CategoryCreated, CategoryWithTypes from src.core.entities.tool.tool import ToolCreated, ToolCreate, ToolSummary, ToolDetails, ToolPages @@ -7,6 +7,7 @@ from src.infrastructure.api.security.role_required import role_required from src.infrastructure.services_instances import get_tool_service from fastapi.security import OAuth2PasswordBearer +from pydantic import PositiveInt tool_router = APIRouter() category_router = APIRouter() @@ -14,6 +15,31 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login") +@tool_router.get( + path="/search", + status_code=200, + response_model=List[ToolSummary] +) +async def search_tools( + query: str = Query(..., min_length=3), + page: PositiveInt = Query(1), + page_size: PositiveInt = Query(12), + category: Optional[List[str]] = Query(None), + type: Optional[List[str]] = Query(None), + min_price: Optional[float] = Query(None), + max_price: Optional[float] = Query(None), + tool_service: ToolService = Depends(get_tool_service) +): + return await tool_service.search_tools( + query=query, + page=page, + page_size=page_size, + category=category, + type=type, + min_price=min_price, + max_price=max_price + ) + @tool_router.post( path="/", @@ -43,7 +69,6 @@ async def get_paginated_tools( - @tool_router.get( "/pages_count", status_code=200, diff --git a/backend/src/infrastructure/db/mongo.py b/backend/src/infrastructure/db/mongo.py index 56b6b19..22a3eaf 100644 --- a/backend/src/infrastructure/db/mongo.py +++ b/backend/src/infrastructure/db/mongo.py @@ -3,6 +3,7 @@ from src.configs.mongo_config import MongoConfig mongo_config = config.mongo +collections_config = config.collections class MongoDB: client: AsyncIOMotorClient | None = None @@ -23,6 +24,23 @@ async def close() -> None: else: raise ConnectionError("Client not connected") + @staticmethod + async def create_indexes() -> None: + await MongoDB.db[collections_config.tool_collection].create_index( + [ + ("name", "text"), + ("description", "text"), + ("category", "text"), + ("type", "text") + ], + weights={ + "name": 10, + "description": 5, + "category": 2, + "type": 2 + }, + default_language="russian" + ) @staticmethod def get_db_instance() -> AsyncIOMotorDatabase: diff --git a/backend/src/infrastructure/repo_implementations/tool_repos/mongo_tool_repository.py b/backend/src/infrastructure/repo_implementations/tool_repos/mongo_tool_repository.py index f982827..42b882e 100644 --- a/backend/src/infrastructure/repo_implementations/tool_repos/mongo_tool_repository.py +++ b/backend/src/infrastructure/repo_implementations/tool_repos/mongo_tool_repository.py @@ -10,6 +10,7 @@ class MongoToolRepository(IToolRepository): def __init__(self, db: AsyncIOMotorDatabase, tool_collection: str): self.tool_collection = db[tool_collection] + self.tool_collection_name = tool_collection async def create(self, tool: Tool) -> str: try: @@ -65,5 +66,56 @@ async def exists(self, tool_name: str) -> bool: try: count = await self.tool_collection.count_documents({"name": tool_name}) return count > 0 + except PyMongoError: + raise DatabaseError() + + async def search( + self, + query: str, + page: int, + page_size: int, + category: Optional[List[str]] = None, + type: Optional[List[str]] = None, + min_price: Optional[float] = None, + max_price: Optional[float] = None + ) -> List[ToolSummary]: + try: + skip = (page - 1) * page_size + + match_stage = {"$match": {"$text": {"$search": query}}} + + if category: + match_stage["$match"]["category"] = {"$in": category} + if type: + match_stage["$match"]["type"] = {"$in": type} + if min_price is not None: + match_stage["$match"]["dailyPrice"] = {"$gte": min_price} + if max_price is not None: + match_stage["$match"].setdefault("dailyPrice", {})["$lte"] = max_price + + + pipeline = [ + match_stage, + {"$addFields": {"score": {"$meta": "textScore"}}}, + {"$sort": {"score": -1}}, + {"$skip": skip}, + {"$limit": page_size}, + {"$project": { + "_id": 1, + "name": 1, + "dailyPrice": 1, + "images": 1, + "rating": 1, + "description": 1, + }} + ] + cursor = self.tool_collection.aggregate(pipeline) + + results = [] + + async for doc in cursor: + results.append(ToolSummary(**doc)) + + return results except PyMongoError: raise DatabaseError() \ No newline at end of file diff --git a/backend/src/main.py b/backend/src/main.py index 7a5363f..c663c30 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -16,6 +16,7 @@ @asynccontextmanager async def lifespan(app: FastAPI): await MongoDB.connect() + await MongoDB.create_indexes() yield await MongoDB.close()