Roger Surf commited on
Commit
aaf84ca
·
1 Parent(s): b7e75fd

feat: add matching service with cosine similarity - API working

Browse files
api/package-lock.json CHANGED
@@ -11,6 +11,7 @@
11
  "dependencies": {
12
  "@nestjs/common": "^11.0.1",
13
  "@nestjs/core": "^11.0.1",
 
14
  "@nestjs/platform-express": "^11.0.1",
15
  "hbs": "^4.2.0",
16
  "mathjs": "^15.1.0",
@@ -2206,6 +2207,26 @@
2206
  }
2207
  }
2208
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2209
  "node_modules/@nestjs/platform-express": {
2210
  "version": "11.1.9",
2211
  "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.9.tgz",
 
11
  "dependencies": {
12
  "@nestjs/common": "^11.0.1",
13
  "@nestjs/core": "^11.0.1",
14
+ "@nestjs/mapped-types": "*",
15
  "@nestjs/platform-express": "^11.0.1",
16
  "hbs": "^4.2.0",
17
  "mathjs": "^15.1.0",
 
2207
  }
2208
  }
2209
  },
2210
+ "node_modules/@nestjs/mapped-types": {
2211
+ "version": "2.1.0",
2212
+ "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz",
2213
+ "integrity": "sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==",
2214
+ "license": "MIT",
2215
+ "peerDependencies": {
2216
+ "@nestjs/common": "^10.0.0 || ^11.0.0",
2217
+ "class-transformer": "^0.4.0 || ^0.5.0",
2218
+ "class-validator": "^0.13.0 || ^0.14.0",
2219
+ "reflect-metadata": "^0.1.12 || ^0.2.0"
2220
+ },
2221
+ "peerDependenciesMeta": {
2222
+ "class-transformer": {
2223
+ "optional": true
2224
+ },
2225
+ "class-validator": {
2226
+ "optional": true
2227
+ }
2228
+ }
2229
+ },
2230
  "node_modules/@nestjs/platform-express": {
2231
  "version": "11.1.9",
2232
  "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.9.tgz",
api/package.json CHANGED
@@ -22,6 +22,7 @@
22
  "dependencies": {
23
  "@nestjs/common": "^11.0.1",
24
  "@nestjs/core": "^11.0.1",
 
25
  "@nestjs/platform-express": "^11.0.1",
26
  "hbs": "^4.2.0",
27
  "mathjs": "^15.1.0",
 
22
  "dependencies": {
23
  "@nestjs/common": "^11.0.1",
24
  "@nestjs/core": "^11.0.1",
25
+ "@nestjs/mapped-types": "*",
26
  "@nestjs/platform-express": "^11.0.1",
27
  "hbs": "^4.2.0",
28
  "mathjs": "^15.1.0",
api/src/app.module.ts CHANGED
@@ -1,9 +1,10 @@
1
  import { Module } from '@nestjs/common';
2
  import { AppController } from './app.controller';
3
  import { AppService } from './app.service';
 
4
 
5
  @Module({
6
- imports: [],
7
  controllers: [AppController],
8
  providers: [AppService],
9
  })
 
1
  import { Module } from '@nestjs/common';
2
  import { AppController } from './app.controller';
3
  import { AppService } from './app.service';
4
+ import { MatchingModule } from './matching/matching.module';
5
 
6
  @Module({
7
+ imports: [MatchingModule],
8
  controllers: [AppController],
9
  providers: [AppService],
10
  })
api/src/matching/matching.controller.spec.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Test, TestingModule } from '@nestjs/testing';
2
+ import { MatchingController } from './matching.controller';
3
+ import { MatchingService } from './matching.service';
4
+
5
+ describe('MatchingController', () => {
6
+ let controller: MatchingController;
7
+
8
+ beforeEach(async () => {
9
+ const module: TestingModule = await Test.createTestingModule({
10
+ controllers: [MatchingController],
11
+ providers: [MatchingService],
12
+ }).compile();
13
+
14
+ controller = module.get<MatchingController>(MatchingController);
15
+ });
16
+
17
+ it('should be defined', () => {
18
+ expect(controller).toBeDefined();
19
+ });
20
+ });
api/src/matching/matching.controller.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Controller, Get, Query, Param } from '@nestjs/common';
2
+ import { MatchingService } from './matching.service';
3
+
4
+ @Controller('api/matching')
5
+ export class MatchingController {
6
+ constructor(private readonly matchingService: MatchingService) {}
7
+
8
+ @Get('candidate/:id/matches')
9
+ getMatches(
10
+ @Param('id') candidateId: string,
11
+ @Query('topK') topK?: number
12
+ ) {
13
+ const id = parseInt(candidateId);
14
+ const k = topK ? parseInt(topK.toString()) : 10;
15
+
16
+ return {
17
+ candidate: this.matchingService.getCandidateData(id),
18
+ matches: this.matchingService.findTopMatches(k),
19
+ };
20
+ }
21
+ }
api/src/matching/matching.module.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { Module } from '@nestjs/common';
2
+ import { MatchingService } from './matching.service';
3
+ import { MatchingController } from './matching.controller';
4
+
5
+ @Module({
6
+ controllers: [MatchingController],
7
+ providers: [MatchingService],
8
+ })
9
+ export class MatchingModule {}
api/src/matching/matching.service.spec.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Test, TestingModule } from '@nestjs/testing';
2
+ import { MatchingService } from './matching.service';
3
+
4
+ describe('MatchingService', () => {
5
+ let service: MatchingService;
6
+
7
+ beforeEach(async () => {
8
+ const module: TestingModule = await Test.createTestingModule({
9
+ providers: [MatchingService],
10
+ }).compile();
11
+
12
+ service = module.get<MatchingService>(MatchingService);
13
+ });
14
+
15
+ it('should be defined', () => {
16
+ expect(service).toBeDefined();
17
+ });
18
+ });
api/src/matching/matching.service.ts ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Injectable } from '@nestjs/common';
2
+ import * as math from 'mathjs';
3
+ import { mockCandidate, mockCompanies } from './mock-data';
4
+
5
+ @Injectable()
6
+ export class MatchingService {
7
+
8
+ cosineSimilarity(vecA: number[], vecB: number[]): number {
9
+ const dotProduct = math.dot(vecA, vecB) as number;
10
+ const normA = math.norm(vecA) as number;
11
+ const normB = math.norm(vecB) as number;
12
+ return dotProduct / (normA * normB);
13
+ }
14
+
15
+ findTopMatches(candidateId: number, topK: number = 10) {
16
+ const candidate = mockCandidate;
17
+
18
+ const matches = mockCompanies.map((company) => ({
19
+ companyId: company.id,
20
+ companyName: company.name,
21
+ jobTitle: company.title,
22
+ score: this.cosineSimilarity(candidate.embedding, company.embedding),
23
+ }));
24
+
25
+ return matches
26
+ .sort((a, b) => b.score - a.score)
27
+ .slice(0, topK);
28
+ }
29
+
30
+ getCandidateData(candidateId: number) {
31
+ return mockCandidate;
32
+ }
33
+ }
api/src/matching/mock-data.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const mockCandidate = {
2
+ id: 0,
3
+ name: 'Candidate #0',
4
+ skills: ['Python', 'Machine Learning', 'Data Science', 'SQL', 'AWS'],
5
+ experience: '5 years',
6
+ education: 'Master in Computer Science',
7
+ embedding: [0.1, 0.2, 0.3, 0.4, 0.5] // Simplificado (real seria 384 dims)
8
+ };
9
+
10
+ export const mockCompanies = [
11
+ {
12
+ id: 42,
13
+ name: 'Anblicks',
14
+ title: 'Data Scientist',
15
+ embedding: [0.12, 0.19, 0.31, 0.38, 0.52]
16
+ },
17
+ {
18
+ id: 15,
19
+ name: 'iO Associates',
20
+ title: 'ML Engineer',
21
+ embedding: [0.11, 0.21, 0.29, 0.41, 0.48]
22
+ },
23
+ {
24
+ id: 89,
25
+ name: 'DATAECONOMY',
26
+ title: 'Senior Data Analyst',
27
+ embedding: [0.09, 0.18, 0.33, 0.39, 0.51]
28
+ }
29
+ ];