diff --git a/src/application/controller/papers.controller.ts b/src/application/controller/papers.controller.ts index 74efcdf..3151334 100644 --- a/src/application/controller/papers.controller.ts +++ b/src/application/controller/papers.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, HttpCode, Param, ParseUUIDPipe, Req, UseInterceptors } from "@nestjs/common"; +import { CacheInterceptor, Controller, Get, HttpCode, Inject, Param, ParseUUIDPipe, Req, UseInterceptors } from "@nestjs/common"; import { SearchService } from "../../core/services/common/search.service"; import { PageInterceptor } from "../../core/interceptors/page.interceptor"; import { ApiExtraModels, ApiGatewayTimeoutResponse, ApiOperation, ApiResponse, ApiTags, getSchemaPath } from "@nestjs/swagger"; @@ -13,6 +13,7 @@ import { EsHitDto, EsResponseDto, PageDto, PaperDto } from "../../core/domain"; path: 'papers', }) @ApiExtraModels(RequestDto, EsHitDto, EsResponseDto) +// @UseInterceptors(CacheInterceptor) export class PapersController { constructor(private searchService: SearchService) {} diff --git a/src/core/interceptors/page.interceptor.ts b/src/core/interceptors/page.interceptor.ts index 76e1934..01eda50 100644 --- a/src/core/interceptors/page.interceptor.ts +++ b/src/core/interceptors/page.interceptor.ts @@ -1,6 +1,6 @@ import { HttpService } from "@nestjs/axios"; -import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common"; -import { Observable, map, take } from "rxjs"; +import { CACHE_MANAGER, CallHandler, ExecutionContext, Inject, Injectable, NestInterceptor } from "@nestjs/common"; +import { Observable, map, take, switchMap } from "rxjs"; import { PageDto } from "../domain/dtos"; import { EsQueryDto } from "../domain/dtos/elastic/es-query.dto"; import { RequestDto } from "../domain/dtos/request.dto"; @@ -9,62 +9,7 @@ import { EsTime } from "../domain/enums/es-time.enum"; import { Order } from "../domain/enums/page-order.enum"; import { PageMeta } from "../domain/interfaces"; import { EsPit } from "../domain/interfaces/elastic/es-pit.interface"; - -/** - * Previous search data storage - */ -class PrevSearch { - /** - * Constructs an uninitialized object - */ - constructor() { - this.pit = undefined; - this.tiebreaker = undefined; - this.prevPage = -1; - } - - /** - * PIT object of the previous search - */ - private pit: EsPit; - set _pit(pit: EsPit) { - this.pit = pit; - } - get _pit(): EsPit { - return this.pit; - } - - /** - * Tiebreaker and sort parameters - */ - private tiebreaker: unknown[]; - set _tiebreaker(tiebreaker: unknown[]) { - this.tiebreaker = tiebreaker; - } - get _tiebreaker(): unknown[] { - return this.tiebreaker; - } - - /** - * Number of the previous page - */ - private prevPage: number; - set _prevPage(page: number) { - this.prevPage = page; - } - get _prevPage(): number { - return this.prevPage; - } - - /** - * Checks if there was the search before current one - * @returns true/false, showing whether or not there was another search before - */ - public isSet(): boolean { - if (this.pit && this.tiebreaker && this.prevPage !== -1) return true; - return false; - } -} +import { Cache } from 'cache-manager' /** * Pagination-implementing interceptor @@ -73,95 +18,13 @@ class PrevSearch { export class PageInterceptor implements NestInterceptor { /** * Injects needed dependencies and instantiates the storage object - * @param httpService - * @param searchService + * @param httpService + * @param searchService */ - constructor(private readonly httpService: HttpService) { - this.prevSearch = new PrevSearch; - } - - /** - * Override of intercept() method, specified in NestInterceptor interface - * @param context - * @param next - * @returns Page with content and metadata - */ - async intercept(context: ExecutionContext, next: CallHandler): Promise> { - let request: RequestDto = context.switchToHttp().getRequest(); - const query: SearchQueryDto = request.query; - let reverse: boolean = false; - - request.es_query = new EsQueryDto(); - - request.es_query.query = { - query_string: { - query: query.query, - default_field: 'content', - } - }; - request.es_query.sort = [ - { _score: { order: !query?.order ? Order.DESC : query.order } }, - { _shard_doc: 'desc' } - ]; - - if (this.prevSearch.isSet()) { - request.es_query.pit = this.prevSearch._pit; - request.es_query.search_after = this.prevSearch._tiebreaker; - - let limit = !query?.limit ? 10 : query.limit; - request.es_query.size = limit * Math.abs(query.page - this.prevSearch._prevPage); - - if (query.page < this.prevSearch._prevPage) { - request.es_query.sort = [{ _score: { order: 'asc' } }]; - request.es_query.size += limit - 1; - reverse = true; - } else if (query.page == this.prevSearch._prevPage) { - // Caching should be HERE - request.es_query.sort = [{ _score: { order: 'asc' } }]; - reverse = true; - } - } else { - this.prevSearch._pit = request.es_query.pit = await this.getPIT(1); - - let limit = !query?.limit ? 10 : query.limit; - request.es_query.size = limit * query.page; - } - - return next.handle().pipe( - map((res) => { - // Setting the page meta-data - let meta: PageMeta = { - total: res.hits.total.value, - pagenum: !query?.page ? 1 : +query.page, - order: query?.order?.toUpperCase() === Order.ASC ? Order.ASC : Order.DESC, - pagesize: !query?.limit ? 10 : query.limit, - hasNext: undefined, - hasPrev: undefined, - }; - meta.hasNext = meta.pagenum * meta.pagesize < meta.total ? true : false; - meta.hasPrev = meta.pagenum != 1 ? true : false; - - // Saving the search info - this.prevSearch._pit.id = res.pit_id; - this.prevSearch._tiebreaker = res.hits.hits[res.hits.hits.length - 1]?.sort; - this.prevSearch._prevPage = query.page; - - // Check if the performed search is a backwards search - let data = res.hits.hits.slice(-meta.pagesize); - if (reverse) { - this.prevSearch._tiebreaker = data[0]?.sort; - data.reverse(); - reverse = false; - } - - // Omitting the redundant info and leaving only the document - data = data.map((el) => el._source); - - // Return the page - return new PageDto(data, meta); - }) - ); - } + constructor( + private readonly httpService: HttpService, + @Inject(CACHE_MANAGER) private cacheManager: Cache + ) {} /** * Elastichsearch server port-number @@ -174,9 +37,84 @@ export class PageInterceptor implements NestInterceptor { private readonly ES_IP = process.env.ES_CONTAINER_NAME; /** - * Info about previously completed search + * Override of intercept() method, specified in NestInterceptor interface + * @param context + * @param next + * @returns Page with content and metadata */ - private prevSearch: PrevSearch; + async intercept(context: ExecutionContext, next: CallHandler): Promise> { + const request: RequestDto = context.switchToHttp().getRequest(); + const query: SearchQueryDto = request.query; + let reverse: boolean = false; + + request.es_query = new EsQueryDto(); + request.es_query.query = { + query_string: { + query: query.query, + default_field: 'content', + } + }; + request.es_query.sort = [ + { _score: { order: !query?.order ? Order.DESC : query.order } }, + { _shard_doc: 'desc' } + ]; + + const limit = !query?.limit ? 10 : query.limit; + + if (await this.cacheManager.get('prevPage')) { + if (query.page == (await this.cacheManager.get('_pagenum'))) return await this.cacheManager.get('prevPage'); + + request.es_query.pit = await this.cacheManager.get('_pit'); + request.es_query.search_after = await this.cacheManager.get('_sa'); + request.es_query.size = limit * Math.abs(query.page - (await this.cacheManager.get('_pagenum'))); + + if (query.page < (await this.cacheManager.get('_pagenum'))) { + request.es_query.sort = [{ _score: { order: Order.ASC } }]; + request.es_query.size += limit - 1; + reverse = true; + } + } else { + request.es_query.pit = await this.getPIT(1); + request.es_query.size = limit * query.page; + } + + return next.handle().pipe( + switchMap(async (res) => { + // Setting the page meta-data + let meta: PageMeta = { + total: res.hits.total.value, + pagenum: !query?.page ? 1 : +query.page, + order: query?.order?.toUpperCase() === Order.ASC ? Order.ASC : Order.DESC, + pagesize: !query?.limit ? 10 : query.limit, + hasNext: undefined, + hasPrev: undefined, + }; + meta.hasNext = meta.pagenum * meta.pagesize < meta.total ? true : false; + meta.hasPrev = meta.pagenum != 1 ? true : false; + + // Saving the search info + await this.cacheManager.set('_pit', { id: res.pit_id, keep_alive: `1${EsTime.min}` }) + await this.cacheManager.set('_sa', res.hits.hits[res.hits.hits.length - 1]?.sort); + await this.cacheManager.set('_pagenum', query.page); + + // Check if the performed search is a backwards search + let data = res.hits.hits.slice(-meta.pagesize); + if (reverse) { + this.cacheManager.set('_sa', data[0]?.sort); + data.reverse(); + reverse = false; + } + + // Omitting the redundant info and leaving only the document + data = data.map((el) => el._source); + + // Cache and return the page + const page: PageDto = new PageDto(data, meta); + await this.cacheManager.set('prevPage', page); + return page; + }) + ); + } /** * Acquires a PIT ID from Elasticsearch, needed for a request diff --git a/src/infrastructure/modules/search.module.ts b/src/infrastructure/modules/search.module.ts index 526c7d7..8fd2f60 100644 --- a/src/infrastructure/modules/search.module.ts +++ b/src/infrastructure/modules/search.module.ts @@ -1,5 +1,5 @@ import { HttpModule } from "@nestjs/axios"; -import { Module } from "@nestjs/common"; +import { CacheModule, Module } from "@nestjs/common"; import { PapersController } from "../../application"; import { SearchService } from "../../core/services/common/search.service"; @@ -9,6 +9,7 @@ import { SearchService } from "../../core/services/common/search.service"; @Module({ imports: [ HttpModule, + CacheModule.register(), ], exports: [SearchService], providers: [SearchService], diff --git a/src/test/e2e/papers.controller.e2e.spec.ts b/src/test/e2e/papers.controller.e2e.spec.ts index 922bdc0..2934683 100644 --- a/src/test/e2e/papers.controller.e2e.spec.ts +++ b/src/test/e2e/papers.controller.e2e.spec.ts @@ -75,7 +75,7 @@ describe('E2E Testing of /papers', () => { let httpGetSpy = jest.spyOn(httpService, 'get').mockReturnValueOnce(axiosRes); const test = await request(app.getHttpServer()) - .get('/papers/2d3dc418-7778-abab-b33f-3d63aa25db41') // ??? Fetch a random object from DB + .get('/papers/2d3dc418-7778-abab-b33f-3d63aa25db41') .expect(200); // Expect HttpService.get() method to be touched diff --git a/src/test/page.interceptor.spec.ts b/src/test/page.interceptor.spec.ts index e3fda46..5a60f02 100644 --- a/src/test/page.interceptor.spec.ts +++ b/src/test/page.interceptor.spec.ts @@ -1,4 +1,5 @@ import { HttpService } from "@nestjs/axios"; +import { CacheModule, CACHE_MANAGER } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { Test } from "@nestjs/testing"; import { Observable, of } from "rxjs"; @@ -44,7 +45,8 @@ describe('Unit tests for PageInterceptor', () => { isGlobal: true, cache: true, expandVariables: true, - }) + }), + CacheModule.register() ], }).compile(); @@ -159,7 +161,7 @@ describe('Unit tests for PageInterceptor', () => { }); }); - it('Should reverse the search results', () => { + it('Should reverse the search results', async () => { execCtxMock.getRequest.mockReturnValueOnce({ query: { page: 1, @@ -168,10 +170,8 @@ describe('Unit tests for PageInterceptor', () => { } }); - pageInter['prevSearch']._prevPage = 3; - pageInter['prevSearch'].isSet = jest.fn().mockImplementationOnce(() => { - return true; - }) + await pageInter['cacheManager'].set('_pagenum', 3); + await pageInter['cacheManager'].set('prevPage', { set: "yes" }); callHandlerMock.handle.mockReturnValueOnce( of({ @@ -187,8 +187,8 @@ describe('Unit tests for PageInterceptor', () => { ); pageInter.intercept(execCtxMock, callHandlerMock).then((res) => { - res.subscribe((page) => { - expect(pageInter['prevSearch']._tiebreaker).toEqual(['1', 'less relevant']); + res.subscribe(async (page) => { + expect(await pageInter['cacheManager'].get('_sa')).toEqual(['1', 'less relevant']); expect(page.data).toEqual(['3', '2', '1']); }); }); @@ -315,4 +315,8 @@ describe('Unit tests for PageInterceptor', () => { expect(pageInter.deletePIT('')).resolves.toBe(true); }); }); + + afterEach(() => { + pageInter['cacheManager'].reset(); + }) }); \ No newline at end of file diff --git a/src/test/papers.controller.spec.ts b/src/test/papers.controller.spec.ts index 5a1ff79..36c4eb4 100644 --- a/src/test/papers.controller.spec.ts +++ b/src/test/papers.controller.spec.ts @@ -1,5 +1,5 @@ import { HttpModule } from "@nestjs/axios"; -import { NotFoundException } from "@nestjs/common"; +import { CacheModule, NotFoundException } from "@nestjs/common"; import { Test } from "@nestjs/testing"; import { PapersController } from "src/application"; import { SearchService } from "src/core/services/common/search.service"; @@ -21,7 +21,10 @@ describe('Unit tests for PapersController', () => { } } ], - imports: [HttpModule] + imports: [ + HttpModule, + CacheModule.register() + ] }).compile(); papersController = moduleRef.get(PapersController);