diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4ef6f7f43d6ef023e19dadd3355c43ab62b0daf1..a10231ea23ba3c07571272679828cc8226abd1ce 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -37,11 +37,15 @@ deploy_dev: test: stage: test image: ${CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX}/node:14.15.4 + services: + - name: ${CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX}/elasticsearch:7.16.2 + alias: elasticsearch + command: ['bin/elasticsearch', '-Expack.security.enabled=false', '-Ediscovery.type=single-node'] before_script: - export GHOST_HOST_AND_PORT=http://localhost:2368 - export GHOST_ADMIN_API_KEY=60142bc9e33940000156bccc:6217742e2671e322612e89cac9bab61fcd01822709fe5d8f5e6a5b3e54d5e6bb - export SALT=$TEST_SALT - - export ELASTICSEARCH_NODE=http://localhost:9200 + - export ELASTICSEARCH_NODE=http://elasticsearch:9200 script: - npm i - npm run test:cov diff --git a/src/structures/interfaces/structure-search-response.interface.ts b/src/structures/interfaces/structure-search-response.interface.ts index 79c5e31df2bfc3265023440dd17fc69c7ffae8c6..8faccec6fc3956131e90eabdfc296eaa4e9b8f98 100644 --- a/src/structures/interfaces/structure-search-response.interface.ts +++ b/src/structures/interfaces/structure-search-response.interface.ts @@ -3,6 +3,7 @@ import { StructureSearchBody } from './structure-search-body.interface'; export interface StructureSearchResult { hits: { total: number; + max_score: number; hits: Array<{ _score: number; _source: StructureSearchBody; diff --git a/src/structures/services/structure.service.spec.ts b/src/structures/services/structure.service.spec.ts index 82128e4105f934f8062832a0b90b5cc21b25ab4e..87160583d02726edf20689229bbbad5c37ed9232 100644 --- a/src/structures/services/structure.service.spec.ts +++ b/src/structures/services/structure.service.spec.ts @@ -44,6 +44,7 @@ describe('StructuresService', () => { }).compile(); service = module.get<StructuresService>(StructuresService); + service['structuresSearchService']['index'] = 'structures-unit-test'; }); it('should be defined', () => { diff --git a/src/structures/services/structures-search.service.spec.ts b/src/structures/services/structures-search.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..ed226632d31a01e424d865f37166a39a6a61679d --- /dev/null +++ b/src/structures/services/structures-search.service.spec.ts @@ -0,0 +1,77 @@ +import { Logger } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { StructuresForSearchServiceMock } from '../../../test/mock/services/structures-for-search.mock.service'; +import { MailerModule } from '../../mailer/mailer.module'; +import { SearchModule } from '../../search/search.module'; +import { StructuresSearchService } from './structures-search.service'; +import { StructuresService } from './structures.service'; +describe('StructuresSearchService', () => { + let service: StructuresSearchService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [MailerModule, SearchModule, ConfigModule], + providers: [ + StructuresSearchService, + { + provide: StructuresService, + useClass: StructuresForSearchServiceMock, + }, + ], + }).compile(); + + service = module.get<StructuresSearchService>(StructuresSearchService); + service['index'] = 'structures-unit-test'; + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should create index', async () => { + await service.dropIndex(); + const res = await service.createStructureIndex(); + expect(res).toBeTruthy(); + }); + + it('should index structures', async () => { + const structuresForSearchService = new StructuresForSearchServiceMock(); + const structures = structuresForSearchService.findAll(); + + const res = await Promise.all( + structures.map((structure: any) => { + service.indexStructure(structure); + }) + ); + expect(res).toBeTruthy(); + + // wait for the new structures to be indexed before search + await service.refreshIndexStructure(); + // but we still need to wait the refresh to be done + await new Promise((r) => setTimeout(r, 1000)); + }); + + it('should find maisons de la métropole', async () => { + const res = await service.search('maison de la'); + //Logger.log(JSON.stringify(res)); + expect(res[0].structureName).toContain('Maison de la Métropole'); + expect(res[1].structureName).toContain('Maison de la Métropole'); + }); + + it('should find metropole', async () => { + const res = await service.search('metropole'); + expect(res[0].structureName).toContain('Métropole'); + }); + + it('should find text in description', async () => { + const res = await service.search('liseuse'); + expect(res.length).toBe(1); + expect(res[0].structureName).toContain("Médiathèque d'Ecully"); + }); + + it('should drop index', async () => { + const res = await service.dropIndex(); + expect(res).toBeTruthy(); + }); +}); diff --git a/src/structures/services/structures-search.service.ts b/src/structures/services/structures-search.service.ts index bd6f53beef23ba2148b6325e9ac713ecb5e75af1..9e17f5a672e5d5e937f3ad967999974ac492e353 100644 --- a/src/structures/services/structures-search.service.ts +++ b/src/structures/services/structures-search.service.ts @@ -29,6 +29,20 @@ export class StructuresSearchService { public async createStructureIndex(): Promise<any> { return this.elasticsearchService.indices.create({ index: this.index, + body: { + settings: { + analysis: { + analyzer: { + default: { + type: 'french', + }, + default_search: { + type: 'french', + }, + }, + }, + }, + }, }); } @@ -54,6 +68,12 @@ export class StructuresSearchService { return structure; } + public async refreshIndexStructure(): Promise<any> { + return this.elasticsearchService.indices.refresh({ + index: this.index, + }); + } + public async search(searchString: string): Promise<StructureSearchBody[]> { searchString = searchString ? searchString + '*' : '*'; const { body } = await this.elasticsearchService.search<StructureSearchResult>({ @@ -71,14 +91,8 @@ export class StructuresSearchService { }, }, }); - const maxScore = Math.max.apply( - Math, - body.hits.hits.map(function (hit) { - return hit._score; - }) - ); const sortedHits = body.hits.hits.filter(function (elem) { - return elem._score >= maxScore / 1.5; + return elem._score >= body.hits.max_score / 1.5; }); return sortedHits.map((item) => item._source); } diff --git a/test/mock/services/structures-for-search.mock.service.ts b/test/mock/services/structures-for-search.mock.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..766f721fd8bb675ea1b141944dd31533aed588e0 --- /dev/null +++ b/test/mock/services/structures-for-search.mock.service.ts @@ -0,0 +1,62 @@ +export class StructuresForSearchServiceMock { + findAll() { + return [ + { + _id: '607ef197225ffd001391edb9', + structureName: "Médiathèque d'Ecully", + structureType: 'mediatheque', + address: { + numero: '1', + street: 'Avenue Edouard Aynard', + commune: 'Écully', + }, + description: + 'Nous sommes une équipe de 6 personnes accompagnant les usagers dans leur démarche de découverte des outils numériques, mettant à disposition sous forme de prêt des liseuses et du livre numérique et organisant des ateliers individuels de prise en main des outils numériques et tentant de répondre aux questions des usages sur des sujets divers.', + }, + { + _id: '60368194cda3ba42b8e621dd', + structureName: 'Maison des associations (Grézieu-la-Varenne)', + structureType: 'autre', + address: { + numero: null, + street: " Place de l'Abbe Launay", + commune: 'Grézieu-la-Varenne', + }, + description: null, + }, + { + _id: '60b4b0836a9d4500313b8661', + structureName: 'Mairie (La Tour de Salvagny)', + structureType: 'mairie', + address: { + numero: null, + street: 'Place de la Mairie', + commune: 'La Tour-de-Salvagny', + }, + description: null, + }, + { + _id: '604b84e914d486001790ee57', + structureName: 'Maison de la Métropole (Ecully)', + structureType: 'mdm', + address: { + numero: '10', + street: 'Chemin Jean-Marie Vianney', + commune: 'Écully', + }, + description: null, + }, + { + _id: '61977124eb90f20031137c35', + structureName: 'Maison de la Métropole (Oullins)', + structureType: 'mdm', + address: { + numero: '17', + street: 'Rue Tupin', + commune: 'Oullins', + }, + description: null, + }, + ]; + } +}