import express from 'express';
const router = express.Router();
// DuckDuckGo Instant Answer API - no key required
async function ddgInstantAnswer(query) {
const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1`;
const res = await fetch(url, {
headers: { 'User-Agent': 'IQAI-Dashboard/1.0' }
});
if (!res.ok) throw new Error('DDG search failed');
return res.json();
}
// DuckDuckGo HTML search scraper for more results
async function ddgWebSearch(query) {
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
const res = await fetch(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'text/html'
}
});
if (!res.ok) return [];
const html = await res.text();
// Parse result links and snippets from DDG HTML
const results = [];
const resultRegex = /]+class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
const snippetRegex = /]+class="result__snippet"[^>]*>([\s\S]*?)<\/a>/gi;
const links = [...html.matchAll(resultRegex)].slice(0, 8);
const snippets = [...html.matchAll(snippetRegex)].slice(0, 8);
for (let i = 0; i < Math.min(links.length, 5); i++) {
const title = links[i][2].replace(/<[^>]+>/g, '').trim();
const snippet = snippets[i] ? snippets[i][1].replace(/<[^>]+>/g, '').trim() : '';
const href = links[i][1];
// DDG redirects through their own URLs, extract the real URL
const realUrl = href.startsWith('//duckduckgo.com/l/?uddg=')
? decodeURIComponent(href.split('uddg=')[1]?.split('&')[0] || href)
: href;
if (title && !title.includes('DuckDuckGo')) {
results.push({ title, snippet, url: realUrl });
}
}
return results;
}
router.get('/', async (req, res) => {
const { q } = req.query;
if (!q) return res.status(400).json({ error: 'Query parameter q is required' });
try {
const [instant, webResults] = await Promise.allSettled([
ddgInstantAnswer(q),
ddgWebSearch(q)
]);
const answer = instant.status === 'fulfilled' ? instant.value : null;
const results = webResults.status === 'fulfilled' ? webResults.value : [];
// Format for injection into AI prompt
const formatted = formatSearchResults(q, answer, results);
res.json({
query: q,
answer,
results,
formatted
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
function formatSearchResults(query, instant, webResults) {
const lines = [`[WEB SEARCH RESULTS FOR: "${query}"]`, ''];
if (instant?.AbstractText) {
lines.push(`Summary: ${instant.AbstractText}`);
if (instant.AbstractSource) lines.push(`Source: ${instant.AbstractSource}`);
lines.push('');
}
if (instant?.Answer) {
lines.push(`Direct Answer: ${instant.Answer}`);
lines.push('');
}
if (webResults.length > 0) {
lines.push('Web Results:');
webResults.forEach((r, i) => {
lines.push(`${i + 1}. ${r.title}`);
if (r.snippet) lines.push(` ${r.snippet}`);
lines.push(` URL: ${r.url}`);
});
lines.push('');
}
if (instant?.RelatedTopics?.length > 0) {
const topics = instant.RelatedTopics
.filter(t => t.Text)
.slice(0, 3)
.map(t => `- ${t.Text}`);
if (topics.length > 0) {
lines.push('Related Topics:');
lines.push(...topics);
lines.push('');
}
}
lines.push('[END SEARCH RESULTS]');
return lines.join('\n');
}
export { router as searchRouter };