diff --git a/fastapi_book_server/app/services/author.py b/fastapi_book_server/app/services/author.py index 7ddf4a9..42a1881 100644 --- a/fastapi_book_server/app/services/author.py +++ b/fastapi_book_server/app/services/author.py @@ -48,7 +48,7 @@ class AuthorTGRMSearchService(TRGMSearchService): GET_OBJECT_IDS_QUERY = GET_OBJECT_IDS_QUERY -GET_OBJECT_ID_QUERY = """ +GET_OBJECTS_ID_QUERY = """ WITH filtered_authors AS ( SELECT id FROM authors WHERE EXISTS ( @@ -64,7 +64,7 @@ SELECT id FROM filtered_authors; class GetRandomAuthorService(GetRandomService): MODEL_CLASS = Author - GET_OBJECT_ID_QUERY = GET_OBJECT_ID_QUERY + GET_OBJECTS_ID_QUERY = GET_OBJECTS_ID_QUERY class AuthorMeiliSearchService(MeiliSearchService): diff --git a/fastapi_book_server/app/services/book.py b/fastapi_book_server/app/services/book.py index 4b0775c..ca32c04 100644 --- a/fastapi_book_server/app/services/book.py +++ b/fastapi_book_server/app/services/book.py @@ -100,7 +100,7 @@ SELECT id FROM filtered_books; class GetRandomBookService(GetRandomService): MODEL_CLASS = BookDB - GET_OBJECT_ID_QUERY = GET_OBJECT_IDS_QUERY + GET_OBJECTS_ID_QUERY = GET_OBJECTS_ID_QUERY class BookMeiliSearchService(MeiliSearchService): diff --git a/fastapi_book_server/app/services/common.py b/fastapi_book_server/app/services/common.py index 4df9bcf..2f6dc03 100644 --- a/fastapi_book_server/app/services/common.py +++ b/fastapi_book_server/app/services/common.py @@ -26,7 +26,7 @@ class BaseSearchService(Generic[MODEL, QUERY], abc.ABC): SELECT_RELATED: Optional[Union[list[str], str]] = None PREFETCH_RELATED: Optional[Union[list[str], str]] = None CUSTOM_CACHE_PREFIX: Optional[str] = None - CACHE_TTL = 60 * 60 + CACHE_TTL = 6 * 60 * 60 @classmethod def get_params(cls) -> AbstractParams: @@ -237,6 +237,8 @@ class MeiliSearchService(Generic[MODEL], BaseSearchService[MODEL, SearchQuery]): class GetRandomService(Generic[MODEL]): MODEL_CLASS: Optional[MODEL] = None GET_OBJECTS_ID_QUERY: Optional[str] = None + CUSTOM_CACHE_PREFIX: Optional[str] = None + CACHE_TTL = 6 * 60 * 60 @classmethod @property @@ -249,6 +251,21 @@ class GetRandomService(Generic[MODEL]): def database(cls) -> Database: return cls.model.Meta.database + @classmethod + @property + def cache_prefix(cls) -> str: + return cls.CUSTOM_CACHE_PREFIX or cls.model.Meta.tablename + + @staticmethod + def _get_query_hash(query: frozenset[str]): + return hash(query) + + @classmethod + def get_cache_key(cls, query: frozenset[str]) -> str: + model_class_name = cls.cache_prefix + query_hash = cls._get_query_hash(query) + return f"random_{model_class_name}_{query_hash}" + @classmethod @property def objects_id_query(cls) -> str: @@ -258,15 +275,59 @@ class GetRandomService(Generic[MODEL]): return cls.GET_OBJECTS_ID_QUERY @classmethod - async def get_objects(cls, allowed_langs: frozenset[str]) -> list[int]: + async def _get_objects_from_db(cls, allowed_langs: frozenset[str]) -> list[int]: objects = await cls.database.fetch_all( cls.objects_id_query, {"langs": allowed_langs} ) return [obj["id"] for obj in objects] @classmethod - async def get_random_id(cls, allowed_langs: frozenset[str]) -> int: - object_ids = await cls.get_objects(allowed_langs) + async def _get_objects_from_cache( + cls, allowed_langs: frozenset[str], redis: aioredis.Redis + ) -> Optional[list[int]]: + try: + key = cls.get_cache_key(allowed_langs) + data = await redis.get(key) + + if data is None: + return None + + return orjson.loads(data) + except aioredis.RedisError as e: + print(e) + return None + + @classmethod + async def _cache_object_ids( + cls, object_ids: list[int], allowed_langs: frozenset[str], redis: aioredis.Redis + ) -> bool: + try: + key = cls.get_cache_key(allowed_langs) + await redis.set(key, orjson.dumps(object_ids), ex=cls.CACHE_TTL) + return True + except aioredis.RedisError as e: + print(e) + return False + + @classmethod + async def get_objects( + cls, allowed_langs: frozenset[str], redis: aioredis.Redis + ) -> list[int]: + cached_object_ids = await cls._get_objects_from_cache(allowed_langs, redis) + + if cached_object_ids is not None: + return cached_object_ids + + object_ids = await cls._get_objects_from_db(allowed_langs) + await cls._cache_object_ids(object_ids, allowed_langs, redis) + + return object_ids + + @classmethod + async def get_random_id( + cls, allowed_langs: frozenset[str], redis: aioredis.Redis + ) -> int: + object_ids = await cls.get_objects(allowed_langs, redis) return choice(object_ids) diff --git a/fastapi_book_server/app/services/sequence.py b/fastapi_book_server/app/services/sequence.py index f5b70c6..cc7be33 100644 --- a/fastapi_book_server/app/services/sequence.py +++ b/fastapi_book_server/app/services/sequence.py @@ -36,7 +36,7 @@ class SequenceTGRMSearchService(TRGMSearchService): GET_OBJECT_IDS_QUERY = GET_OBJECT_IDS_QUERY -GET_OBJECT_ID_QUERY = """ +GET_OBJECTS_ID_QUERY = """ WITH filtered_sequences AS ( SELECT id FROM sequences WHERE EXISTS ( @@ -55,7 +55,7 @@ ORDER BY RANDOM() LIMIT 1; class GetRandomSequenceService(GetRandomService): MODEL_CLASS = Sequence - GET_OBJECT_ID_QUERY = GET_OBJECT_ID_QUERY + GET_OBJECTS_ID_QUERY = GET_OBJECTS_ID_QUERY class SequenceMeiliSearchService(MeiliSearchService): diff --git a/fastapi_book_server/app/views/author.py b/fastapi_book_server/app/views/author.py index dbdb8d8..31e22a1 100644 --- a/fastapi_book_server/app/views/author.py +++ b/fastapi_book_server/app/views/author.py @@ -54,8 +54,12 @@ async def create_author(data: CreateAuthor): @author_router.get("/random", response_model=Author) -async def get_random_author(allowed_langs: frozenset[str] = Depends(get_allowed_langs)): - author_id = await GetRandomAuthorService.get_random_id(allowed_langs) +async def get_random_author( + request: Request, allowed_langs: frozenset[str] = Depends(get_allowed_langs) +): + author_id = await GetRandomAuthorService.get_random_id( + allowed_langs, request.app.state.redis + ) return ( await AuthorDB.objects.select_related(SELECT_RELATED_FIELDS) diff --git a/fastapi_book_server/app/views/book.py b/fastapi_book_server/app/views/book.py index 3a6cafe..0cceb5d 100644 --- a/fastapi_book_server/app/views/book.py +++ b/fastapi_book_server/app/views/book.py @@ -56,8 +56,12 @@ async def create_book(data: Union[CreateBook, CreateRemoteBook]): @book_router.get("/random", response_model=BookDetail) -async def get_random_book(allowed_langs: frozenset[str] = Depends(get_allowed_langs)): - book_id = await GetRandomBookService.get_random_id(allowed_langs) +async def get_random_book( + request: Request, allowed_langs: frozenset[str] = Depends(get_allowed_langs) +): + book_id = await GetRandomBookService.get_random_id( + allowed_langs, request.app.state.redis + ) book = ( await BookDB.objects.select_related(SELECT_RELATED_FIELDS + ["sequences"]) diff --git a/fastapi_book_server/app/views/sequence.py b/fastapi_book_server/app/views/sequence.py index c1fe152..6754ad6 100644 --- a/fastapi_book_server/app/views/sequence.py +++ b/fastapi_book_server/app/views/sequence.py @@ -28,9 +28,12 @@ async def get_sequences(): @sequence_router.get("/random", response_model=Sequence) async def get_random_sequence( + request: Request, allowed_langs: frozenset[str] = Depends(get_allowed_langs), ): - sequence_id = await GetRandomSequenceService.get_random_id(allowed_langs) + sequence_id = await GetRandomSequenceService.get_random_id( + allowed_langs, request.app.state.redis + ) return await SequenceDB.objects.get(id=sequence_id)