Single GET /api/search?q=&space_id=&kinds=&limit=&offset= unions FTS hits across pages / refs / source_docs / messages with a `kind` discriminator and ts_rank ordering. Each branch's to_tsvector matches the GIN index expression on its source table so indexes are used. Messages have no space_id and are excluded when a space filter is set. Hybrid vector / RRF lands in Plan 3. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
38 lines
1.1 KiB
JavaScript
38 lines
1.1 KiB
JavaScript
import { Router } from 'express';
|
|
import { z } from 'zod';
|
|
import * as repo from '../../db/repos/search.js';
|
|
import { parsePagination } from '../pagination.js';
|
|
import { validate } from '../validate.js';
|
|
import { asyncWrap } from '../errors.js';
|
|
|
|
const KINDS = ['page','ref','source_doc','message'];
|
|
|
|
const querySchema = z.object({
|
|
q: z.string().min(1),
|
|
space_id: z.string().uuid().optional(),
|
|
kinds: z.string().optional(),
|
|
limit: z.string().optional(),
|
|
offset: z.string().optional()
|
|
});
|
|
|
|
export const router = Router();
|
|
|
|
// Hybrid vector + reciprocal-rank-fusion search lands in Plan 3 (see
|
|
// docs/superpowers/specs/2026-05-31-void-v2-design.md §search).
|
|
router.get('/',
|
|
validate({ query: querySchema }),
|
|
asyncWrap(async (req, res) => {
|
|
const { limit, offset } = parsePagination(req);
|
|
const rawKinds = req.validatedQuery.kinds;
|
|
const kinds = rawKinds
|
|
? rawKinds.split(',').map(s => s.trim()).filter(k => KINDS.includes(k))
|
|
: null;
|
|
res.json(await repo.fts({
|
|
q: req.validatedQuery.q,
|
|
space_id: req.validatedQuery.space_id ?? null,
|
|
kinds,
|
|
limit, offset
|
|
}));
|
|
})
|
|
);
|