From fbbf2fb6142d879bad296c104c1e4da42e93dc3a Mon Sep 17 00:00:00 2001
From: Etienne LOUPIAS <eloupias@grandlyon.com>
Date: Wed, 2 Feb 2022 15:20:46 +0000
Subject: [PATCH] feat(search): search-improvement

---
 .gitlab-ci.yml                                |  6 +-
 .../structure-search-response.interface.ts    |  1 +
 .../services/structure.service.spec.ts        |  1 +
 .../structures-search.service.spec.ts         | 77 +++++++++++++++++++
 .../services/structures-search.service.ts     | 28 +++++--
 .../structures-for-search.mock.service.ts     | 62 +++++++++++++++
 6 files changed, 167 insertions(+), 8 deletions(-)
 create mode 100644 src/structures/services/structures-search.service.spec.ts
 create mode 100644 test/mock/services/structures-for-search.mock.service.ts

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4ef6f7f43..a10231ea2 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 79c5e31df..8faccec6f 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 82128e410..87160583d 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 000000000..ed226632d
--- /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 bd6f53bee..9e17f5a67 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 000000000..766f721fd
--- /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,
+      },
+    ];
+  }
+}
-- 
GitLab