Skip to content

Commit

Permalink
Change /api/search logic to Neynar search #525 (#544)
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
gabrieltemtsen authored Nov 30, 2024
1 parent 4f46cd7 commit 9ecc8b3
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 81 deletions.
2 changes: 1 addition & 1 deletion next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
123 changes: 43 additions & 80 deletions pages/api/search.ts
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: [],
});
}
}

0 comments on commit 9ecc8b3

Please sign in to comment.