source: Klonkt/src/routes/search.js@ 32cc601

main
Last change on this file since 32cc601 was 7bc636b, checked in by Robin <robin@…>, 4 months ago

Initial commit — PrutFolio v1 source (pulled from Hetzner /srv/prutfolio)

  • Property mode set to 100644
File size: 2.3 KB
Line 
1/**
2 * GET /search?q=...
3 *
4 * Queries the posts_fts virtual table (FTS5) for the current site.
5 * Search is restricted to published posts of the resolved site.
6 *
7 * FTS5 quirks handled:
8 * - Empty / whitespace-only query: render the form with no results.
9 * - User input is wrapped in double quotes so FTS5 treats it as a phrase
10 * (avoids syntax errors from special chars like "OR", parentheses, etc.).
11 * - Snippet() builds the highlighted excerpt; we keep markup minimal so
12 * the EJS view can wrap the matches in <mark>.
13 */
14
15import express from 'express';
16import db from '../config/database.js';
17import { renderPage } from '../middleware/render.js';
18
19const router = express.Router();
20
21// Wrap user input as a single FTS5 phrase. Strip embedded double-quotes so
22// the wrapping stays balanced. FTS5 phrase queries are forgiving and avoid
23// the operator-soup pitfalls of bare user input.
24function asPhrase(q) {
25 return '"' + q.replace(/"/g, '') + '"';
26}
27
28router.get('/', (req, res) => {
29 const site = res.locals.site;
30 const rawQ = (req.query.q || '').toString().trim();
31
32 if (!site) return res.status(404).send('No site');
33
34 // Empty query — render the page with the form and no results.
35 if (!rawQ) {
36 return renderPage(req, res, 'pages/search', {
37 pageTitle: 'Search',
38 bodyClass: 'on-special',
39 query: '',
40 results: [],
41 total: 0,
42 });
43 }
44
45 let results = [];
46 let total = 0;
47 let queryError = null;
48
49 try {
50 // FTS5 join → posts table, filter by site + published
51 results = db.prepare(`
52 SELECT
53 p.slug,
54 p.title,
55 p.excerpt,
56 p.published_at,
57 u.username AS author_username,
58 snippet(posts_fts, 0, '<mark>', '</mark>', '…', 18) AS snippet,
59 bm25(posts_fts) AS score
60 FROM posts_fts
61 JOIN posts p ON p.id = posts_fts.post_id
62 JOIN users u ON u.id = p.author_id
63 WHERE posts_fts MATCH ?
64 AND p.site_id = ?
65 AND p.status = 'published'
66 ORDER BY score ASC
67 LIMIT 50
68 `).all(asPhrase(rawQ), site.id);
69
70 total = results.length;
71 } catch (err) {
72 queryError = err.message;
73 }
74
75 renderPage(req, res, 'pages/search', {
76 pageTitle: `Search: ${rawQ}`,
77 bodyClass: 'on-special',
78 query: rawQ,
79 results,
80 total,
81 queryError,
82 });
83});
84
85export default router;
Note: See TracBrowser for help on using the repository browser.