Skip to content

Commit ffac5a3

Browse files
committed
use better search backend
1 parent d3d535e commit ffac5a3

File tree

2 files changed

+96
-9
lines changed

2 files changed

+96
-9
lines changed

server/services/api.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import cors from "cors";
33
import multer from "multer";
44
import { runModel, processDocuments, streamModel } from "./inference.js";
55
import { proxyMiddleware } from './middleware.js';
6-
import { search, research, researchV2 } from './utils.js';
6+
import { braveSearch as search, research, researchV2 } from './utils.js';
77

88
const api = Router();
99
const fieldSize = process.env.UPLOAD_FIELD_SIZE || 1024 * 1024 * 1024; // 1gb
@@ -20,8 +20,8 @@ api.get("/ping", (req, res) => {
2020
api.all("/proxy", proxyMiddleware);
2121

2222
api.get("/search", async (req, res) => {
23-
const { q, offset, time, vqd } = req.query;
24-
res.json(await search({keywords: q, offset, time, vqd}));
23+
// const { q, offset, time, vqd } = req.query;
24+
res.json(await search(req.query));
2525
});
2626

2727
api.get("/research", async (req, res) => {

server/services/utils.js

+93-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import ivm from "isolated-vm";
22
import DDG from "duck-duck-scrape";
3+
import { ProxyAgent } from "proxy-agent";
34
import { JSDOM } from "jsdom";
45
import { inspect } from "util";
56
import { parseDocument } from "./parsers.js";
@@ -10,7 +11,7 @@ const log = (value) => console.log(inspect(value, { depth: null, colors: true, c
1011
const modelId = "amazon.nova-pro-v1:0";
1112

1213
const DEFAULT_TOOLS = {
13-
search,
14+
search: ddgSearch,
1415
runJavascript,
1516
};
1617

@@ -373,12 +374,63 @@ Note: Please use runJavascript for all mathematical operations, including basic
373374
}
374375

375376
/**
376-
*
377-
* @param {*} param0
378-
* @returns
377+
* @param {Object} opts - Search options (q, count, offset, freshness, goggles)
378+
* @param {string} apiKey - Brave Search API key
379379
*/
380-
export async function search({ keywords, offset = 0, time, vqd }) {
381-
const response = await DDG.search(keywords, { offset, time, vqd });
380+
export async function braveSearch(opts, apiKey = process.env.BRAVE_SEARCH_API_KEY) {
381+
for (let key in opts) {
382+
if (opts[key] === undefined) {
383+
delete opts[key];
384+
}
385+
}
386+
const url = `https://api.search.brave.com/res/v1/web/search?${new URLSearchParams(opts)}`;
387+
const response = await fetch(url, {
388+
headers: {
389+
"Accept": "application/json",
390+
"Accept-Encoding": "gzip",
391+
"X-Subscription-Token": apiKey,
392+
},
393+
});
394+
395+
if (!response.ok) {
396+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
397+
}
398+
399+
return await response.json();
400+
}
401+
402+
/**
403+
*
404+
* @param {*} param0
405+
* @returns
406+
*/
407+
export async function ddgSearch({ keywords, offset = 0, time, vqd }, env = process.env) {
408+
const { PROXY_SOURCE } = env;
409+
const response = await retry(10, 500, async () => {
410+
let agent;
411+
if (PROXY_SOURCE) {
412+
const parseProxies = (proxies) => {
413+
try {
414+
const results = JSON.parse(proxies);
415+
if (Array.isArray(results?.data)) {
416+
return results.data.map((proxy) => `${proxy.protocols[0]}://${proxy.ip}:${proxy.port}`);
417+
}
418+
} catch (error) {
419+
return proxies.split("\n");
420+
}
421+
};
422+
const proxyResponse = await fetch(PROXY_SOURCE).then((res) => res.text());
423+
const proxies = parseProxies(proxyResponse);
424+
let proxy = proxies[Math.floor(Math.random() * proxies.length)];
425+
// prepend protocol if missing (use https:// by default)
426+
if (!/.+:\/\//.test(proxy)) {
427+
proxy = `https://${proxy}`;
428+
}
429+
console.log("using some proxy", proxy);
430+
agent = new ProxyAgent(proxy);
431+
}
432+
return await DDG.search(keywords, { offset, time, vqd }, { agent });
433+
});
382434
const appendBody = async (result) => ({ ...result, body: await extractTextFromUrl(result.url) });
383435
const results = await Promise.all((response.results || []).map(appendBody));
384436
return { vqd, results };
@@ -430,3 +482,38 @@ export async function runJavascript({ code, globalContext = {}, memoryLimit = 12
430482
const script = await isolate.compileScript(String(code));
431483
return await script.run(context);
432484
}
485+
486+
/**
487+
* Retries a function with exponential backoff
488+
* @param {number} maxAttempts - Maximum number of retry attempts
489+
* @param {number} initialDelay - Initial delay in milliseconds
490+
* @param {Function} fn - Async function to retry
491+
* @returns {Promise<any>} - Result of the function execution
492+
* @throws {Error} - Throws the last error encountered after all retries are exhausted
493+
*/
494+
export async function retry(maxAttempts, initialDelay, fn) {
495+
let lastError;
496+
497+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
498+
try {
499+
return await fn();
500+
} catch (error) {
501+
lastError = error;
502+
console.error(`Attempt ${attempt} failed:`, error);
503+
504+
if (attempt === maxAttempts) {
505+
break;
506+
}
507+
508+
// Calculate delay with exponential backoff: initialDelay * 2^(attempt-1)
509+
const delay = initialDelay * Math.pow(2, attempt - 1);
510+
511+
// Add some jitter to prevent thundering herd problem
512+
const jitter = Math.random() * 100;
513+
514+
await new Promise((resolve) => setTimeout(resolve, delay + jitter));
515+
}
516+
}
517+
518+
throw new Error(`Failed after ${maxAttempts} attempts. Last error: ${lastError.message}`);
519+
}

0 commit comments

Comments
 (0)