You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
86 lines
2.4 KiB
86 lines
2.4 KiB
import { dbGlobal } from "drizzle-pkg/lib/db";
|
|
import { items, itemTags, tags } from "drizzle-pkg/lib/schema/collection";
|
|
import { eq, inArray, sql } from "drizzle-orm";
|
|
|
|
export async function fullTextSearch(
|
|
query: string,
|
|
userId: number,
|
|
limit = 20
|
|
) {
|
|
// Sanitize input
|
|
const sanitized = query.replace(/['"*^~]/g, " ").trim();
|
|
if (!sanitized) return [];
|
|
|
|
// Build FTS5 query with prefix matching
|
|
const terms = sanitized.split(/\s+/).filter(Boolean);
|
|
const ftsQuery = terms.map((t) => `"${t}"*`).join(" AND ");
|
|
|
|
const rows = await dbGlobal.all<{
|
|
id: number;
|
|
title: string;
|
|
description: string | null;
|
|
type: string;
|
|
highlight: string;
|
|
}>(
|
|
sql`
|
|
SELECT
|
|
i.id,
|
|
i.title,
|
|
i.description,
|
|
i.type,
|
|
i.rating,
|
|
i.starred,
|
|
i.created_at,
|
|
i.cover_url,
|
|
i.source_host,
|
|
i.url,
|
|
snippet(items_fts, 1, '<mark>', '</mark>', '...', 32) AS highlight
|
|
FROM items_fts
|
|
JOIN items i ON i.id = items_fts.rowid
|
|
WHERE items_fts MATCH ${ftsQuery}
|
|
AND i.user_id = ${userId}
|
|
AND i.is_archived = 0
|
|
ORDER BY rank
|
|
LIMIT ${limit}
|
|
`
|
|
);
|
|
|
|
// Load tags for results
|
|
const itemIds = rows.map((r) => r.id);
|
|
let tagMap: Record<number, { id: number; name: string; color: string }[]> = {};
|
|
if (itemIds.length > 0) {
|
|
const tagRows = await dbGlobal
|
|
.select({ itemId: itemTags.itemId, id: tags.id, name: tags.name, color: tags.color })
|
|
.from(itemTags)
|
|
.innerJoin(tags, eq(itemTags.tagId, tags.id))
|
|
.where(inArray(itemTags.itemId, itemIds));
|
|
for (const t of tagRows) {
|
|
if (!tagMap[t.itemId]) tagMap[t.itemId] = [];
|
|
tagMap[t.itemId].push({ id: t.id, name: t.name, color: t.color });
|
|
}
|
|
}
|
|
|
|
return rows.map((r) => ({ ...r, tags: tagMap[r.id] || [] }));
|
|
}
|
|
|
|
export async function suggestSearch(query: string, userId: number, limit = 5) {
|
|
const sanitized = query.replace(/['"*^~]/g, " ").trim();
|
|
if (!sanitized) return [];
|
|
|
|
const terms = sanitized.split(/\s+/).filter(Boolean);
|
|
const ftsQuery = terms.map((t) => `"${t}"*`).join(" AND ");
|
|
|
|
const rows = await dbGlobal.all<{ suggestion: string }>(
|
|
sql`
|
|
SELECT DISTINCT i.title AS suggestion
|
|
FROM items_fts
|
|
JOIN items i ON i.id = items_fts.rowid
|
|
WHERE items_fts MATCH ${ftsQuery}
|
|
AND i.user_id = ${userId}
|
|
ORDER BY rank
|
|
LIMIT ${limit}
|
|
`
|
|
);
|
|
|
|
return rows.map((r) => r.suggestion);
|
|
}
|
|
|