Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Job Similarity Visualization Feature with New API Routes and Page #167

Closed
wants to merge 38 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions apps/registry/app/api/job-similarity/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { createClient } from '@supabase/supabase-js';
import { NextResponse } from 'next/server';

const supabaseUrl = 'https://itxuhvvwryeuzuyihpkp.supabase.co';

// This ensures the route is always dynamic
export const dynamic = 'force-dynamic';

export async function GET(request) {
// During build time or when SUPABASE_KEY is not available
if (!process.env.SUPABASE_KEY) {
return NextResponse.json(
{ message: 'API not available during build' },
{ status: 503 }
);
}

try {
const supabase = createClient(supabaseUrl, process.env.SUPABASE_KEY);
const { searchParams } = new URL(request.url);
const limit = parseInt(searchParams.get('limit')) || 750;

console.time('getJobSimilarityData');
const { data, error } = await supabase
.from('jobs')
.select('uuid, embedding_v5, gpt_content')
.not('embedding_v5', 'is', null)
.limit(limit)
.order('created_at', { ascending: false });

if (error) {
console.error('Error fetching job similarity data:', error);
return NextResponse.json(
{ message: 'Error fetching job similarity data' },
{ status: 500 }
);
}

console.timeEnd('getJobSimilarityData');

// Parse embeddings and job titles
const parsedData = data
.map((item) => {
let jobTitle = 'Unknown Position';
let gptContent = null;
try {
gptContent = JSON.parse(item.gpt_content);
jobTitle = gptContent?.title || 'Unknown Position';
} catch (e) {
console.warn('Failed to parse gpt_content for job:', item.uuid);
}

return {
uuid: item.uuid,
title: jobTitle,
company: gptContent?.company,
countryCode: gptContent?.countryCode,
embedding:
typeof item.embedding_v5 === 'string'
? JSON.parse(item.embedding_v5)
: Array.isArray(item.embedding_v5)
? item.embedding_v5
: null,
};
})
.filter((item) => item.embedding !== null);

return NextResponse.json(parsedData, {
headers: {
'Content-Type': 'application/json',
'Cache-Control':
'public, max-age=86400, s-maxage=86400, stale-while-revalidate=86400',
},
});
} catch (error) {
console.error('Error in job similarity endpoint:', error);
return NextResponse.json(
{ message: 'Internal server error' },
{ status: 500 }
);
}
}
2 changes: 1 addition & 1 deletion apps/registry/app/api/resumes/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export async function GET(request) {
try {
const supabase = createClient(supabaseUrl, process.env.SUPABASE_KEY);
const { searchParams } = new URL(request.url);
const limit = parseInt(searchParams.get('limit')) || 3000;
const limit = parseInt(searchParams.get('limit')) || 2000;
const page = parseInt(searchParams.get('page')) || 1;
const search = searchParams.get('search') || '';

Expand Down
79 changes: 79 additions & 0 deletions apps/registry/app/api/similarity/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { createClient } from '@supabase/supabase-js';
import { NextResponse } from 'next/server';

const supabaseUrl = 'https://itxuhvvwryeuzuyihpkp.supabase.co';

// This ensures the route is always dynamic
export const dynamic = 'force-dynamic';

export async function GET(request) {
// During build time or when SUPABASE_KEY is not available
if (!process.env.SUPABASE_KEY) {
return NextResponse.json(
{ message: 'API not available during build' },
{ status: 503 }
);
}

try {
const supabase = createClient(supabaseUrl, process.env.SUPABASE_KEY);
const { searchParams } = new URL(request.url);
const limit = parseInt(searchParams.get('limit')) || 1000;

console.time('getResumeSimilarityData');
const { data, error } = await supabase
.from('resumes')
.select('username, embedding, resume')
.not('embedding', 'is', null)
.limit(limit)
.order('created_at', { ascending: false });

if (error) {
console.error('Error fetching resume similarity data:', error);
return NextResponse.json(
{ message: 'Error fetching resume similarity data' },
{ status: 500 }
);
}

console.timeEnd('getResumeSimilarityData');

// Parse embeddings from strings to numerical arrays and extract position
const parsedData = data
.map((item) => {
let resumeData;
try {
resumeData = JSON.parse(item.resume);
} catch (e) {
console.warn('Failed to parse resume for user:', item.username);
resumeData = {};
}

return {
username: item.username,
embedding:
typeof item.embedding === 'string'
? JSON.parse(item.embedding)
: Array.isArray(item.embedding)
? item.embedding
: null,
position: resumeData?.basics?.label || 'Unknown Position',
};
})
.filter((item) => item.embedding !== null);

return NextResponse.json(parsedData, {
headers: {
'Content-Type': 'application/json',
'Cache-Control':
'public, max-age=86400, s-maxage=86400, stale-while-revalidate=86400',
},
});
} catch (error) {
console.error('Error in similarity endpoint:', error);
return NextResponse.json(
{ message: 'Internal server error' },
{ status: 500 }
);
}
}
10 changes: 10 additions & 0 deletions apps/registry/app/components/Menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ export default function Menu({ session }) {
>
Jobs
</Link>
<Link
href="/job-similarity"
className={`text-xl font-bold ${
isActive('/job-similarity')
? 'text-secondary-900 underline'
: 'text-black'
} hover:text-secondary-900 transition-colors duration-200`}
>
Similarity
</Link>
<a
href="https://github.com/jsonresume/jsonresume.org"
className="text-xl font-bold text-black hover:text-secondary-900 transition-colors duration-200"
Expand Down
Loading
Loading