feat(search): pg_trgm-Suchindex + Backend-Endpunkt statt Client-Filter
Command Palette lud bisher beim Öffnen ALLE Objekte und filterte nur clientseitig per .includes() - unscharf und würde bei mehr Objekten langsam werden. Jetzt: GIN-Trigram-Index auf objekt.name/code (Migration 0023), neuer GET /suche?q=-Endpunkt (Ähnlichkeits-Ranking per pg_trgm similarity()), Frontend debounced (250ms) statt vollständiger Objektliste. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
This commit is contained in:
@@ -3,7 +3,12 @@ import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { apiRequest } from "../api/client";
|
||||
import { useAuth } from "../auth/AuthContext";
|
||||
import type { Objekt } from "../api/types";
|
||||
|
||||
interface SuchTreffer {
|
||||
id: number;
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface Eintrag {
|
||||
id: string;
|
||||
@@ -22,7 +27,7 @@ export function CommandPalette() {
|
||||
const { istVerantwortlich, istAdmin } = useAuth();
|
||||
const [offen, setOffen] = useState(false);
|
||||
const [suche, setSuche] = useState("");
|
||||
const [objekte, setObjekte] = useState<Objekt[]>([]);
|
||||
const [objektTreffer, setObjektTreffer] = useState<SuchTreffer[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
@@ -44,13 +49,34 @@ export function CommandPalette() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (offen && objekte.length === 0) {
|
||||
apiRequest<Objekt[]>("/objekte")
|
||||
.then(setObjekte)
|
||||
.catch(() => undefined);
|
||||
if (!offen) {
|
||||
setSuche("");
|
||||
setObjektTreffer([]);
|
||||
}
|
||||
if (!offen) setSuche("");
|
||||
}, [offen, objekte.length]);
|
||||
}, [offen]);
|
||||
|
||||
// Debounced Backend-Suche statt kompletter Objektliste + Client-Filter
|
||||
// (SEARCH-001) - vorher wurden ALLE Objekte beim Öffnen geladen und nur
|
||||
// clientseitig gefiltert, was bei vielen Objekten langsam/unscharf würde.
|
||||
useEffect(() => {
|
||||
const trimmed = suche.trim();
|
||||
if (trimmed.length < 2) {
|
||||
setObjektTreffer([]);
|
||||
return;
|
||||
}
|
||||
let abgebrochen = false;
|
||||
const timer = setTimeout(() => {
|
||||
apiRequest<SuchTreffer[]>(`/suche?q=${encodeURIComponent(trimmed)}`)
|
||||
.then((treffer) => {
|
||||
if (!abgebrochen) setObjektTreffer(treffer);
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}, 250);
|
||||
return () => {
|
||||
abgebrochen = true;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [suche]);
|
||||
|
||||
const aktionen = useMemo<Eintrag[]>(() => {
|
||||
const liste: Eintrag[] = [
|
||||
@@ -83,19 +109,16 @@ export function CommandPalette() {
|
||||
return liste;
|
||||
}, [navigate, istVerantwortlich, istAdmin]);
|
||||
|
||||
const objektTreffer = useMemo<Eintrag[]>(() => {
|
||||
if (suche.trim().length === 0) return [];
|
||||
const s = suche.toLowerCase();
|
||||
return objekte
|
||||
.filter((o) => o.name.toLowerCase().includes(s) || o.code.toLowerCase().includes(s))
|
||||
.slice(0, 8)
|
||||
.map((o) => ({
|
||||
const objektEintraege = useMemo<Eintrag[]>(
|
||||
() =>
|
||||
objektTreffer.map((o) => ({
|
||||
id: `objekt-${o.id}`,
|
||||
label: o.name,
|
||||
hinweis: o.code,
|
||||
gehe: () => navigate(`/akte/objekt/${o.id}`),
|
||||
}));
|
||||
}, [suche, objekte, navigate]);
|
||||
})),
|
||||
[objektTreffer, navigate]
|
||||
);
|
||||
|
||||
const gefilterteAktionen = useMemo(() => {
|
||||
if (suche.trim().length === 0) return aktionen;
|
||||
@@ -105,7 +128,7 @@ export function CommandPalette() {
|
||||
|
||||
if (!offen) return null;
|
||||
|
||||
const treffer = [...objektTreffer, ...gefilterteAktionen];
|
||||
const treffer = [...objektEintraege, ...gefilterteAktionen];
|
||||
|
||||
function auswaehlen(eintrag: Eintrag) {
|
||||
eintrag.gehe();
|
||||
|
||||
Reference in New Issue
Block a user