From fedec869bb07d3b8adbd9af7e3c0b7ed9df08290 Mon Sep 17 00:00:00 2001 From: Nick A Date: Sun, 27 Apr 2025 22:56:30 -0400 Subject: [PATCH] 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 @@ openapi_tags = { "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 + + 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(model).filter(or_(*filters)) - results.extend(query.all()) - + 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 -