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 +21 -0
- api/package.json +1 -0
- api/src/app.module.ts +2 -1
- api/src/matching/matching.controller.spec.ts +20 -0
- api/src/matching/matching.controller.ts +21 -0
- api/src/matching/matching.module.ts +9 -0
- api/src/matching/matching.service.spec.ts +18 -0
- api/src/matching/matching.service.ts +33 -0
- api/src/matching/mock-data.ts +29 -0
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 |
+
];
|