-
-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* Change /api/search logic to Neynar search #525 * updated review changes * updated review changes
- Loading branch information
1 parent
4f46cd7
commit 9ecc8b3
Showing
2 changed files
with
44 additions
and
81 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,100 +1,63 @@ | ||
import { getTextMatchCondition } from '@/common/helpers/search'; | ||
import { Cast, initializeDataSourceWithRetry } from '@/lib/db'; | ||
import uniqBy from 'lodash.uniqby'; | ||
import { orderBy as orderByFn } from 'lodash'; | ||
import axios from 'axios'; | ||
import type { NextApiRequest, NextApiResponse } from 'next'; | ||
|
||
export const config = { | ||
maxDuration: 20, | ||
maxDuration: 20, // Max duration for the API route | ||
}; | ||
|
||
const timeoutThreshold = 19000; // 19 seconds to ensure it sends before the 20-second limit | ||
const timeoutThreshold = 19000; // 19 seconds timeout to ensure it completes within 20 seconds | ||
const TIMEOUT_ERROR_MESSAGE = 'Request timed out'; | ||
const powerbadgeFilter = 'AND casts.fid IN (SELECT fid FROM powerbadge)'; | ||
const NEYNAR_API_URL = 'https://api.neynar.com/v2/farcaster/cast/search'; | ||
const API_KEY = process.env.NEXT_PUBLIC_NEYNAR_API_KEY; | ||
|
||
export default async function handler(req: NextApiRequest, res: NextApiResponse) { | ||
let { limit, offset } = req.query; | ||
const { term, interval, orderBy, onlyPowerBadge, hideReplies, mentionFid, fromFid } = req.query; | ||
const { term, limit = 10, offset = 0, priorityMode = false } = req.query; | ||
|
||
// Validate the search term | ||
if (typeof term !== 'string' || term.length < 3) { | ||
return res.status(400).json({ error: 'Invalid search term' }); | ||
} | ||
if (!limit) { | ||
limit = '5'; | ||
} | ||
|
||
if (!offset) { | ||
offset = '0'; | ||
return res.status(400).json({ error: 'Invalid search term. Minimum 3 characters required.' }); | ||
} | ||
|
||
const start = process.hrtime(); | ||
const timeout = setTimeout(() => { | ||
res.status(503).json({ error: TIMEOUT_ERROR_MESSAGE, results: [], isTimeout: true }); | ||
}, timeoutThreshold); | ||
|
||
const AppDataSource = await initializeDataSourceWithRetry(); | ||
const dbConnectEnd = process.hrtime(start); | ||
|
||
const baseConditions = ` | ||
casts.deleted_at IS NULL | ||
${hideReplies === 'true' ? 'AND casts.parent_cast_hash IS NULL' : ''} | ||
${interval ? `AND timestamp >= NOW() - INTERVAL '${interval}'` : ''} | ||
${fromFid ? `AND casts.fid = ${fromFid}` : ''} | ||
`; | ||
|
||
const textMatchCondition = getTextMatchCondition(term); | ||
const query = ` | ||
(SELECT | ||
casts.hash, casts.fid, casts.text, casts.timestamp | ||
FROM casts | ||
WHERE | ||
${baseConditions} | ||
AND ${textMatchCondition} | ||
${onlyPowerBadge === 'true' ? powerbadgeFilter : ''} | ||
${orderBy ? `ORDER BY ${orderBy}` : 'ORDER BY timestamp DESC'} | ||
LIMIT $1 OFFSET $2 | ||
) | ||
${ | ||
mentionFid | ||
? ` | ||
UNION ALL | ||
(SELECT | ||
casts.hash, casts.fid, casts.text, casts.timestamp | ||
FROM casts | ||
WHERE | ||
${baseConditions} | ||
AND array_length(casts.mentions, 1) > 0 | ||
AND casts.mentions @> ARRAY[${mentionFid}] | ||
${onlyPowerBadge === 'true' ? powerbadgeFilter : ''} | ||
${orderBy ? `ORDER BY ${orderBy}` : 'ORDER BY timestamp DESC'} | ||
LIMIT $1 OFFSET $2 | ||
)` | ||
: '' | ||
} | ||
`; | ||
const vars = [limit, offset]; | ||
|
||
try { | ||
const queryStart = process.hrtime(); | ||
await AppDataSource.query(`SET statement_timeout TO '19s';`); | ||
|
||
const searchRepository = AppDataSource.getRepository(Cast); | ||
const queryPromise = await searchRepository.query(query, vars); | ||
const results = await queryPromise; | ||
const queryEnd = process.hrtime(queryStart); | ||
const totalEnd = process.hrtime(start); | ||
|
||
console.log(`DB Connection Time: ${dbConnectEnd[0] * 1000 + dbConnectEnd[1] / 1e6} ms`); | ||
console.log(`Query Execution Time: ${queryEnd[0] * 1000 + queryEnd[1] / 1e6} ms`); | ||
console.log(`Total Request Time: ${totalEnd[0] * 1000 + totalEnd[1] / 1e6} ms`); | ||
console.log('Search results:', results.length); | ||
// uniqBy hash | ||
const orderedResults = orderByFn(uniqBy(results, 'hash'), ['timestamp'], ['desc']); | ||
clearTimeout(timeout); // Clear the timeout if the request completes in time | ||
res.status(200).json({ results: orderedResults }); | ||
// Constructing the Neynar API URL with query parameters | ||
const apiUrl = `${NEYNAR_API_URL}?q=${encodeURIComponent( | ||
term | ||
)}&priority_mode=${priorityMode}&limit=${limit}&offset=${offset}`; | ||
|
||
console.log('Sending request to Neynar API...'); | ||
console.log('Request URL:', apiUrl); | ||
|
||
// Send GET request to Neynar API | ||
const response = await axios.get(apiUrl, { | ||
headers: { | ||
accept: 'application/json', | ||
api_key: API_KEY, | ||
}, | ||
timeout: timeoutThreshold, | ||
}); | ||
|
||
// console.log('Response from Neynar API:', response.data); | ||
|
||
// Parse and return the results | ||
const results = response.data?.result?.casts || []; | ||
clearTimeout(timeout); // Clear the timeout if the request completes successfully | ||
|
||
res.status(200).json({ results }); | ||
} catch (error) { | ||
clearTimeout(timeout); // Clear the timeout if the request completes in time | ||
console.log('error in search', error); | ||
res.status(500).json({ error: `Failed to fetch search results: ${error?.message || error}`, results: [] }); | ||
clearTimeout(timeout); // Clear the timeout in case of error | ||
if (axios.isAxiosError(error)) { | ||
console.error('Error during Neynar API request:', error.response?.data || error.message); | ||
} else { | ||
console.error('Error during Neynar API request:', error); | ||
} | ||
|
||
res.status(500).json({ | ||
error: 'Failed to fetch search results', | ||
results: [], | ||
}); | ||
} | ||
} |