From b4c9e5e93aa8c1130815c884a53eb480128db88d Mon Sep 17 00:00:00 2001 From: Nick A Date: Fri, 4 Apr 2025 17:08:02 -0400 Subject: [PATCH 1/4] basic search feature down --- backend/api/search.py | 18 ++++++++++++++++++ backend/main.py | 4 ++-- backend/services/__init__.py | 3 ++- backend/services/search.py | 37 ++++++++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 backend/api/search.py create mode 100644 backend/services/search.py diff --git a/backend/api/search.py b/backend/api/search.py new file mode 100644 index 0000000..22a3cda --- /dev/null +++ b/backend/api/search.py @@ -0,0 +1,18 @@ +from fastapi import APIRouter, Depends + +from ..services import SearchService +from ..models.resource_model import Resource +from ..models.service_model import Service + + +api = APIRouter(prefix="/api/search") + +openapi_tags = { + "name": "Search", + "description": "Search through all resources and services for a string.", +} + +@api.post("", tags=["Search"]) +def search(query: str, search_svc: SearchService = Depends()) -> list[Resource | Service]: + return search_svc.search(query) + \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index 12ecaad..9f1973f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -3,7 +3,7 @@ from fastapi.middleware.gzip import GZipMiddleware -from .api import user, health, service, resource, tag +from .api import user, health, service, resource, tag, search description = """ Welcome to the **COMPASS** RESTful Application Programming Interface. @@ -24,7 +24,7 @@ app.add_middleware(GZipMiddleware) -feature_apis = [user, health, service, resource, tag] +feature_apis = [user, health, service, resource, tag, search] for feature_api in feature_apis: app.include_router(feature_api.api) diff --git a/backend/services/__init__.py b/backend/services/__init__.py index 4067973..aac7718 100644 --- a/backend/services/__init__.py +++ b/backend/services/__init__.py @@ -1,4 +1,5 @@ from .user import UserService from .resource import ResourceService from .tag import TagService -from .service import ServiceService \ No newline at end of file +from .service import ServiceService +from .search import SearchService \ No newline at end of file diff --git a/backend/services/search.py b/backend/services/search.py new file mode 100644 index 0000000..0acc3c2 --- /dev/null +++ b/backend/services/search.py @@ -0,0 +1,37 @@ +from fastapi import Depends +from ..database import db_session +from sqlalchemy.orm import Session +from sqlalchemy import or_, select +from ..models.resource_model import Resource +from ..models.service_model import Service +from ..entities.resource_entity import ResourceEntity +from backend.entities.service_entity import ServiceEntity +from ..models.user_model import User, UserTypeEnum + + +class SearchService: + def __init__(self, session: Session = Depends(db_session)): + self._session = session + + def search(self, query_str: str) -> list[Resource | Service]: + results = [] + models = [ + ResourceEntity, + ServiceEntity, + ] + + for model in models: + columns = [column for column in model.__table__.columns] + + filters = [ + column.ilike(f"%{query_str}%") + for column in columns + if column.type.__class__.__name__ in ["String", "Text"] + ] + + if filters: + query = self._session.query(model).filter(or_(*filters)) + results.extend(query.all()) + + return results + From fedec869bb07d3b8adbd9af7e3c0b7ed9df08290 Mon Sep 17 00:00:00 2001 From: Nick A Date: Sun, 27 Apr 2025 22:56:30 -0400 Subject: [PATCH 2/4] finalized search backend --- backend/api/search.py | 9 +++-- backend/services/search.py | 71 ++++++++++++++++++++++++++++---------- 2 files changed, 57 insertions(+), 23 deletions(-) diff --git a/backend/api/search.py b/backend/api/search.py index 22a3cda..0ba74b9 100644 --- a/backend/api/search.py +++ b/backend/api/search.py @@ -1,9 +1,8 @@ from fastapi import APIRouter, Depends -from ..services import SearchService -from ..models.resource_model import Resource -from ..models.service_model import Service +from backend.services.search import SearchResult +from ..services import SearchService api = APIRouter(prefix="/api/search") @@ -12,7 +11,7 @@ "description": "Search through all resources and services for a string.", } -@api.post("", tags=["Search"]) -def search(query: str, search_svc: SearchService = Depends()) -> list[Resource | Service]: +@api.get("", tags=["Search"]) +def search(query: str, search_svc: SearchService = Depends()) -> list[SearchResult]: return search_svc.search(query) \ No newline at end of file diff --git a/backend/services/search.py b/backend/services/search.py index 0acc3c2..c3e7fc8 100644 --- a/backend/services/search.py +++ b/backend/services/search.py @@ -1,37 +1,72 @@ +from typing import Literal from fastapi import Depends +from pydantic import BaseModel + +from backend.entities.user_entity import UserEntity +from backend.models.user_model import User + from ..database import db_session from sqlalchemy.orm import Session -from sqlalchemy import or_, select +from sqlalchemy import ( + ARRAY, + BinaryExpression, + String, + Text, + literal_column, + or_, + func, + select, + exists, +) from ..models.resource_model import Resource from ..models.service_model import Service from ..entities.resource_entity import ResourceEntity from backend.entities.service_entity import ServiceEntity -from ..models.user_model import User, UserTypeEnum + + +class SearchResult(BaseModel): + type: Literal["resource", "service", "user"] + data: Resource | Service | User class SearchService: def __init__(self, session: Session = Depends(db_session)): self._session = session - def search(self, query_str: str) -> list[Resource | Service]: - results = [] - models = [ + def search(self, query_str: str) -> list[SearchResult]: + """Searches through all tables for a string.""" + results: list[SearchResult] = [] + entities = ( ResourceEntity, ServiceEntity, - ] + UserEntity, + ) - for model in models: - columns = [column for column in model.__table__.columns] - - filters = [ - column.ilike(f"%{query_str}%") - for column in columns - if column.type.__class__.__name__ in ["String", "Text"] - ] + for entity in entities: + columns = entity.__table__.columns - if filters: - query = self._session.query(model).filter(or_(*filters)) - results.extend(query.all()) + filters: list[BinaryExpression[bool]] = [] + for column in columns: + if isinstance(column.type, String) or isinstance(column.type, Text): + filters.append(column.cast(String).ilike(f"%{query_str}%")) + elif isinstance(column.type, ARRAY): + # Custom filter that checks the query string against each element in the array + filters.append( + exists( + select(1) + .select_from( + func.unnest(column.cast(ARRAY(String))).alias("element") + ) + .where(literal_column("element").ilike(f"%{query_str}%")) + ) + ) + if filters: + query = self._session.query(entity).filter(or_(*filters)) + results.extend( + [ + SearchResult(type=entity.__tablename__, data=result.to_model()) + for result in query.all() + ] + ) return results - From 547fce197a77ddd1daa77235fd6e29d16684b4eb Mon Sep 17 00:00:00 2001 From: Nick A Date: Sun, 27 Apr 2025 23:03:40 -0400 Subject: [PATCH 3/4] removed users from search --- backend/services/search.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/services/search.py b/backend/services/search.py index c3e7fc8..d69d85a 100644 --- a/backend/services/search.py +++ b/backend/services/search.py @@ -25,8 +25,8 @@ class SearchResult(BaseModel): - type: Literal["resource", "service", "user"] - data: Resource | Service | User + type: Literal["resource", "service"] + data: Resource | Service class SearchService: @@ -39,7 +39,6 @@ def search(self, query_str: str) -> list[SearchResult]: entities = ( ResourceEntity, ServiceEntity, - UserEntity, ) for entity in entities: From b7710fa0073063228fb16edff0c8e3f515137cf4 Mon Sep 17 00:00:00 2001 From: Nick A Date: Sun, 27 Apr 2025 23:04:09 -0400 Subject: [PATCH 4/4] updated imports --- backend/services/search.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/backend/services/search.py b/backend/services/search.py index d69d85a..6000389 100644 --- a/backend/services/search.py +++ b/backend/services/search.py @@ -2,9 +2,6 @@ from fastapi import Depends from pydantic import BaseModel -from backend.entities.user_entity import UserEntity -from backend.models.user_model import User - from ..database import db_session from sqlalchemy.orm import Session from sqlalchemy import (