Zwei Luecken, die im Issue zusammen stehen. **Im Aufgabendashboard fehlte ein Suchfeld.** Es gab Filter nach Status und Projekt, aber keine Moeglichkeit, nach einem Wort zu suchen. Das Suchfeld deckt Titel, Beschreibung, Labels und Projektname ab. Gefiltert wird im Browser - die Aufgaben liegen dort ohnehin alle vor, es braucht also keine zusaetzliche Abfrage und es gibt keine Verzoegerung beim Tippen. Wer sucht, sucht dabei auch in Erledigtem: sonst faende man die Aufgabe von letzter Woche nicht wieder, obwohl man genau sie meint. Der Schalter "Erledigte einblenden" gilt nur ohne Suchbegriff. **Die Volltextsuche kannte nur Notizen.** Task ist jetzt ebenfalls dabei, nach demselben Muster: deutsche Textsuche, ts_headline fuer den Ausschnitt, Hervorhebung als [[…]] statt als HTML. Deutsche Stammformen greifen damit auch hier - "Rechnung" findet "Rechnungen". Dazu ein Ausdrucksindex Task_fulltext_idx wie Note_fulltext_idx. Der Ausdruck muss Zeichen fuer Zeichen dem in der Abfrage entsprechen, sonst greift er nicht; nachgeprueft mit EXPLAIN, der Plan zeigt einen Bitmap Index Scan darauf. "prisma migrate diff" laesst ihn in Ruhe - Gegenprobe kommt leer heraus. Das Antwortfeld heisst weiterhin "hits" und meint die Notizen. Umbenennen haette nur Aufrufer gebrochen, ohne etwas zu verbessern; Aufgaben kommen als eigenes Feld dazu. Beide Stellen, die Treffer anzeigen - Projektbrett und Schnellsuche -, zeigen sie jetzt. Geprueft: 261 Unit-Tests, 36 E2E-Tests. Die Volltextsuche zusaetzlich gegen die laufende Anwendung: Treffer in Titel und Beschreibung, Erledigtes inbegriffen, Stammformen greifen, fremde Konten finden nichts. Refs #4
This commit is contained in:
@@ -12,16 +12,30 @@ interface Hit {
|
||||
snippet: string;
|
||||
}
|
||||
|
||||
interface TaskHit {
|
||||
projectId: string;
|
||||
taskId: string;
|
||||
title: string;
|
||||
status: string;
|
||||
/** Ausschnitt aus der Beschreibung; leer, wenn nur der Titel passte. */
|
||||
snippet: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Volltextsuche ueber Notizen.
|
||||
* Volltextsuche ueber Notizen und Aufgaben.
|
||||
*
|
||||
* Die Suche im Board arbeitet auf den bereits geladenen Projekten und deckt
|
||||
* Name, Kurzbeschreibung und Tags ab. Notizen liegen dort nicht vor – die
|
||||
* mitzuladen wuerde die Seite unnoetig aufblaehen. Deshalb dieser Endpunkt:
|
||||
* Postgres sucht mit seiner deutschen Textsuche direkt in der Datenbank.
|
||||
* Name, Kurzbeschreibung und Tags ab. Notizen und Aufgaben liegen dort nicht
|
||||
* vor – die mitzuladen wuerde die Seite unnoetig aufblaehen. Deshalb dieser
|
||||
* Endpunkt: Postgres sucht mit seiner deutschen Textsuche direkt in der
|
||||
* Datenbank.
|
||||
*
|
||||
* ts_headline liefert gleich den passenden Ausschnitt mit; die Hervorhebung
|
||||
* kommt als [[…]] zurueck, damit die Oberflaeche kein HTML einbetten muss.
|
||||
*
|
||||
* Das Feld heisst weiterhin "hits" und meint die Notizen – umbenennen haette
|
||||
* nur Aufrufer gebrochen, ohne etwas zu verbessern. Aufgaben kommen als
|
||||
* eigenes Feld dazu.
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
const auth = await requireUserApi();
|
||||
@@ -29,7 +43,7 @@ export async function GET(request: Request) {
|
||||
|
||||
const query = (new URL(request.url).searchParams.get("q") ?? "").trim();
|
||||
if (query.length < 2) {
|
||||
return NextResponse.json({ query, hits: [], projectIds: [] });
|
||||
return NextResponse.json({ query, hits: [], tasks: [], projectIds: [] });
|
||||
}
|
||||
|
||||
// websearch_to_tsquery versteht Eingaben wie "docker -compose" oder
|
||||
@@ -57,11 +71,48 @@ export async function GET(request: Request) {
|
||||
LIMIT 50
|
||||
`;
|
||||
|
||||
/**
|
||||
* Dieselbe Rechnung fuer Aufgaben.
|
||||
*
|
||||
* Der Ausdruck in to_tsvector muss Zeichen fuer Zeichen dem in der
|
||||
* Migration entsprechen (Task_fulltext_idx), sonst greift der Index nicht
|
||||
* und Postgres rechnet ihn je Zeile neu.
|
||||
*
|
||||
* Erledigtes bleibt drin: wer sucht, sucht oft gerade das, was er letzte
|
||||
* Woche abgehakt hat.
|
||||
*/
|
||||
const tasks = await prisma.$queryRaw<TaskHit[]>`
|
||||
SELECT
|
||||
t."projectId" AS "projectId",
|
||||
t."id" AS "taskId",
|
||||
t."title" AS "title",
|
||||
t."status"::text AS "status",
|
||||
ts_headline(
|
||||
'german',
|
||||
coalesce(t."description", ''),
|
||||
websearch_to_tsquery('german', ${query}),
|
||||
'StartSel=[[, StopSel=]], MaxWords=18, MinWords=6, ShortWord=2, MaxFragments=1'
|
||||
) AS "snippet"
|
||||
FROM "Task" t
|
||||
JOIN "Project" p ON p."id" = t."projectId"
|
||||
WHERE p."ownerId" = ${auth.user.id}
|
||||
AND to_tsvector('german', coalesce(t."title", '') || ' ' || coalesce(t."description", ''))
|
||||
@@ websearch_to_tsquery('german', ${query})
|
||||
ORDER BY ts_rank(
|
||||
to_tsvector('german', coalesce(t."title", '') || ' ' || coalesce(t."description", '')),
|
||||
websearch_to_tsquery('german', ${query})
|
||||
) DESC
|
||||
LIMIT 50
|
||||
`;
|
||||
|
||||
return NextResponse.json({
|
||||
query,
|
||||
hits,
|
||||
tasks,
|
||||
// Die Oberflaeche braucht vor allem die betroffenen Projekte, um sie im
|
||||
// Board einzublenden.
|
||||
projectIds: Array.from(new Set(hits.map((hit) => hit.projectId))),
|
||||
// Board einzublenden – jetzt aus beiden Quellen.
|
||||
projectIds: Array.from(
|
||||
new Set([...hits.map((hit) => hit.projectId), ...tasks.map((hit) => hit.projectId)]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user