const express = require('express'); const db = require('../db'); const router = express.Router(); // GET /api/search?q=term router.get('/', (req, res) => { const q = req.query.q || ''; const term = q.trim(); if (!term) { return res.json({ messages: [], pdfs: [] }); } try { const messageResults = []; const pdfResults = []; const fts5 = db._fts5Available; // Sanitize term for FTS5: quote each whitespace-separated term const safeTerm = term.split(/\s+/).filter(Boolean).map(t => `"${t.replace(/"/g, '""')}"`).join(' '); const fts5Term = fts5 ? safeTerm : null; if (fts5 && fts5Term) { // FTS5 path: use MATCH with snippet highlighting and BM25 ranking const messageRows = db.prepare(` SELECT m.id, m.conversation_id, m.content, snippet(messages_fts, 1, '', '', '…', 16) as snippet, rank FROM messages_fts JOIN messages m ON m.id = messages_fts.rowid WHERE messages_fts MATCH ? ORDER BY rank LIMIT 25 `).all(fts5Term); const pdfRows = db.prepare(` SELECT p.id, p.original_name, p.content_markdown, snippet(pdfs_fts, 1, '', '', '…', 16) as snippet, rank FROM pdfs_fts JOIN pdfs p ON p.id = pdfs_fts.rowid WHERE pdfs_fts MATCH ? ORDER BY rank LIMIT 25 `).all(fts5Term); for (const row of messageRows) { messageResults.push({ type: 'message', id: row.id, title: row.content?.slice(0, 60) || 'Mensaje', snippet: row.snippet || '', rank: row.rank || 0, conversation_id: row.conversation_id, }); } for (const row of pdfRows) { pdfResults.push({ type: 'pdf', id: row.id, title: row.original_name || 'PDF', snippet: row.snippet || '', rank: row.rank || 0, }); } } else { // LIKE fallback (no ranking, no snippets) const likeTerm = `%${term}%`; const messageRows = db.prepare( `SELECT id, conversation_id, content FROM messages WHERE content LIKE ? LIMIT 25` ).all(likeTerm); const pdfRows = db.prepare( `SELECT id, original_name, content_markdown FROM pdfs WHERE content_markdown LIKE ? LIMIT 25` ).all(likeTerm); for (const row of messageRows) { messageResults.push({ type: 'message', id: row.id, title: row.content?.slice(0, 60) || 'Mensaje', snippet: row.content?.slice(0, 120) || '', rank: 0, conversation_id: row.conversation_id, }); } for (const row of pdfRows) { pdfResults.push({ type: 'pdf', id: row.id, title: row.original_name || 'PDF', snippet: row.content_markdown?.slice(0, 120) || '', rank: 0, }); } } // Return grouped results — keep message BM25 and PDF BM25 ranks separate (different indexes) messageResults.sort((a, b) => (a.rank || 0) - (b.rank || 0)); pdfResults.sort((a, b) => (a.rank || 0) - (b.rank || 0)); res.json({ messages: messageResults.slice(0, 25), pdfs: pdfResults.slice(0, 25) }); } catch (err) { console.error('[search] error:', err.message); res.status(500).json({ error: err.message }); } }); module.exports = router;