Skip to content
Merged
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
46 changes: 46 additions & 0 deletions api/developer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Vercel Serverless Function — serves a single developer's full data from Cosmos DB
*
* Endpoint: /api/developer?id=<login>
*/
import { CosmosClient } from '@azure/cosmos';

const COSMOS_ENDPOINT = process.env.COSMOS_ENDPOINT;
const COSMOS_KEY = process.env.COSMOS_KEY;
const DATABASE = 'devglobe';
const CONTAINER = 'developers';

export default async function handler(req, res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET');
res.setHeader('Cache-Control', 's-maxage=3600, stale-while-revalidate=600');
res.setHeader('Content-Type', 'application/json');

const { id } = req.query;
if (!id) {
return res.status(400).json({ error: 'Query parameter "id" is required' });
}

if (!COSMOS_ENDPOINT || !COSMOS_KEY) {
return res.status(500).json({ error: 'Cosmos DB credentials not configured' });
}

try {
const client = new CosmosClient({ endpoint: COSMOS_ENDPOINT, key: COSMOS_KEY });
const container = client.database(DATABASE).container(CONTAINER);

const { resources } = await container.items.query({
query: 'SELECT c.id, c.login, c.name, c.avatarUrl, c.bio, c.location, c.lat, c.lng, c.followers, c.totalStars, c.totalForks, c.totalCommits, c.topLanguage, c.languages, c.publicRepos, c.topRepos, c.soReputation, c.soAnswers, c.soAcceptRate, c.soBadges, c.soUserId FROM c WHERE c.id = @id',
parameters: [{ name: '@id', value: id }]
}).fetchAll();

if (resources.length === 0) {
return res.status(404).json({ error: 'Developer not found' });
}

res.status(200).json(resources[0]);
} catch (err) {
console.error('Cosmos DB error:', err.message);
res.status(500).json({ error: 'Failed to fetch developer data' });
}
}
4 changes: 3 additions & 1 deletion api/developers.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ export default async function handler(req, res) {
const client = new CosmosClient({ endpoint: COSMOS_ENDPOINT, key: COSMOS_KEY });
const container = client.database(DATABASE).container(CONTAINER);

// Slim projection — only fields needed for globe rendering, leaderboard, and scoring
// Detail panel fetches full doc on demand via /api/developer?id=...
const { resources } = await container.items
.query('SELECT * FROM c')
.query('SELECT c.id, c.login, c.name, c.avatarUrl, c.location, c.lat, c.lng, c.followers, c.totalStars, c.totalForks, c.totalCommits, c.topLanguage, c.soReputation, c.soAnswers, c.soAcceptRate, c.soBadges FROM c')
.fetchAll();

res.status(200).json(resources);
Expand Down
86 changes: 47 additions & 39 deletions api/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,60 +67,68 @@ export default async function handler(req, res) {
results = resources;

} else if (mode === 'text') {
const searchTerm = q.toLowerCase();
const { resources } = await container.items.query({
query: `
SELECT TOP ${limit}
c.id, c.login, c.name, c.avatarUrl, c.location, c.lat, c.lng,
c.topLanguage, c.score, c.totalStars, c.followers, c.soReputation
FROM c
WHERE FullTextContains(c.login, @q)
OR FullTextContains(c.name, @q)
OR FullTextContains(c.location, @q)
OR FullTextContains(c.bio, @q)
OR FullTextContains(c.topLanguage, @q)
ORDER BY RANK FullTextScore(c.login, [@q]) +
FullTextScore(c.name, [@q]) +
FullTextScore(c.location, [@q]) +
FullTextScore(c.bio, [@q]) +
FullTextScore(c.topLanguage, [@q])
WHERE CONTAINS(LOWER(c.login), @q)
OR CONTAINS(LOWER(c.name), @q)
OR CONTAINS(LOWER(c.location), @q)
OR CONTAINS(LOWER(c.bio), @q)
OR CONTAINS(LOWER(c.topLanguage), @q)
ORDER BY c.score DESC
`,
parameters: [{ name: '@q', value: q }]
parameters: [{ name: '@q', value: searchTerm }]
}).fetchAll();
results = resources;

} else {
// Hybrid: RRF fusion of vector + full-text
// Hybrid: client-side RRF fusion of vector + text results
if (!OPENAI_ENDPOINT || !OPENAI_KEY) {
return res.status(500).json({ error: 'OpenAI not configured for hybrid search' });
}
const searchTerm = q.toLowerCase();
const embedding = await getEmbedding(q);
const { resources } = await container.items.query({
query: `
SELECT TOP ${limit}
c.id, c.login, c.name, c.avatarUrl, c.location, c.lat, c.lng,
c.topLanguage, c.score, c.totalStars, c.followers, c.soReputation
FROM c
WHERE FullTextContains(c.login, @q)
OR FullTextContains(c.name, @q)
OR FullTextContains(c.location, @q)
OR FullTextContains(c.bio, @q)
OR FullTextContains(c.topLanguage, @q)
OR VectorDistance(c.embedding, @embedding) > 0.7
ORDER BY RANK RRF(
FullTextScore(c.login, [@q]) +
FullTextScore(c.name, [@q]) +
FullTextScore(c.location, [@q]) +
FullTextScore(c.bio, [@q]) +
FullTextScore(c.topLanguage, [@q]),
VectorDistance(c.embedding, @embedding)
)
`,
parameters: [
{ name: '@q', value: q },
{ name: '@embedding', value: embedding }
]
}).fetchAll();
results = resources;

// Run vector and text searches in parallel
const [vectorRes, textRes] = await Promise.all([
container.items.query({
query: `
SELECT TOP ${limit}
c.id, c.login, c.name, c.avatarUrl, c.location, c.lat, c.lng,
c.topLanguage, c.score, c.totalStars, c.followers, c.soReputation
FROM c
ORDER BY VectorDistance(c.embedding, @embedding)
`,
parameters: [{ name: '@embedding', value: embedding }]
}).fetchAll(),
container.items.query({
query: `
SELECT TOP ${limit}
c.id, c.login, c.name, c.avatarUrl, c.location, c.lat, c.lng,
c.topLanguage, c.score, c.totalStars, c.followers, c.soReputation
FROM c
WHERE CONTAINS(LOWER(c.login), @q)
OR CONTAINS(LOWER(c.name), @q)
OR CONTAINS(LOWER(c.location), @q)
OR CONTAINS(LOWER(c.bio), @q)
OR CONTAINS(LOWER(c.topLanguage), @q)
ORDER BY c.score DESC
`,
parameters: [{ name: '@q', value: searchTerm }]
}).fetchAll()
]);

// RRF fusion
const k = 60;
const rrf = new Map();
const allMap = new Map();
vectorRes.resources.forEach((r, i) => { rrf.set(r.login, (rrf.get(r.login) || 0) + 1 / (k + i + 1)); allMap.set(r.login, r); });
textRes.resources.forEach((r, i) => { rrf.set(r.login, (rrf.get(r.login) || 0) + 1 / (k + i + 1)); allMap.set(r.login, r); });
results = [...rrf.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit).map(([login]) => allMap.get(login));
}

res.json({ query: q, mode, count: results.length, results });
Expand Down
5,274 changes: 5,274 additions & 0 deletions dist/assets/index-BNP1Oj68.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions dist/assets/index-uNUnB7ja.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions dist/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DevGlobe — Visualizing the World's Top Open-Source Contributors</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script type="module" crossorigin src="/assets/index-BNP1Oj68.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-uNUnB7ja.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
Expand Down
Loading
Loading