feat(api): unified FTS search
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>
This commit is contained in:
37
lib/api/routes/search.js
Normal file
37
lib/api/routes/search.js
Normal file
@@ -0,0 +1,37 @@
|
||||
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
|
||||
}));
|
||||
})
|
||||
);
|
||||
Reference in New Issue
Block a user