Wer nach einem Material sucht (z.B. "Kompresse"), will die Objekte finden,
die es aktiv führen - nicht nur Objekte, deren eigener Name/Code passt.
GIN-Trigram-Index auf material.name (Migration 0025), /suche joint jetzt
zusätzlich über objektposition (nur status=aktiv) auf material.name.
Direkter Objekt-Treffer hat Vorrang vor Material-Treffer für dasselbe
Objekt. Command Palette zeigt bei Material-Treffern einen Hinweis
("führt „Kompresse“") statt nur dem Code.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
78 lines
3.1 KiB
Python
78 lines
3.1 KiB
Python
from fastapi import APIRouter, Depends
|
|
from sqlalchemy import func, or_, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.api.deps import get_current_user
|
|
from app.db.session import get_db
|
|
from app.models.objekt import Objekt
|
|
from app.models.objektposition import Objektposition, ObjektpositionStatus
|
|
from app.models.stammdaten import Material
|
|
from app.schemas.suche import SuchTreffer
|
|
|
|
router = APIRouter()
|
|
|
|
_TREFFER_LIMIT = 8
|
|
|
|
|
|
@router.get("/suche", response_model=list[SuchTreffer])
|
|
async def suche(
|
|
q: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
_=Depends(get_current_user),
|
|
) -> list[SuchTreffer]:
|
|
"""Globale Suche (Command Palette) über Objektname/-code UND Material-
|
|
Namen (SEARCH-002: wer "Kompressen" sucht, will Objekte finden, die
|
|
welche führen) - per pg_trgm-Index statt vollständig geladener
|
|
Objektliste + Client-Filter (SEARCH-001). Kurze Eingaben (<2 Zeichen)
|
|
liefern bewusst nichts - sonst zu viele Zufallstreffer über den
|
|
Ähnlichkeits-Schwellenwert von pg_trgm."""
|
|
q = q.strip()
|
|
if len(q) < 2:
|
|
return []
|
|
|
|
muster = f"%{q}%"
|
|
|
|
aehnlichkeit_objekt = func.greatest(func.similarity(Objekt.name, q), func.similarity(Objekt.code, q))
|
|
direkt_stmt = (
|
|
select(Objekt.id, Objekt.code, Objekt.name, aehnlichkeit_objekt.label("score"))
|
|
.where(or_(Objekt.name.ilike(muster), Objekt.code.ilike(muster), aehnlichkeit_objekt > 0.2))
|
|
.order_by(aehnlichkeit_objekt.desc())
|
|
.limit(_TREFFER_LIMIT)
|
|
)
|
|
direkt_ergebnis = (await db.execute(direkt_stmt)).all()
|
|
|
|
aehnlichkeit_material = func.similarity(Material.name, q)
|
|
material_stmt = (
|
|
select(Objekt.id, Objekt.code, Objekt.name, Material.name.label("material_name"), aehnlichkeit_material.label("score"))
|
|
.join(Objektposition, Objektposition.objekt_id == Objekt.id)
|
|
.join(Material, Material.id == Objektposition.material_id)
|
|
.where(
|
|
Objektposition.status == ObjektpositionStatus.aktiv,
|
|
or_(Material.name.ilike(muster), aehnlichkeit_material > 0.2),
|
|
)
|
|
.order_by(aehnlichkeit_material.desc())
|
|
.limit(_TREFFER_LIMIT)
|
|
)
|
|
material_ergebnis = (await db.execute(material_stmt)).all()
|
|
|
|
# Zusammenführen: direkter Objekt-Treffer schlägt Material-Treffer für
|
|
# dasselbe Objekt (kein "über Material XY" anzeigen, wenn der Objektname
|
|
# selbst schon passt), sonst höherer Score gewinnt bei Mehrfachtreffer.
|
|
kandidaten: dict[int, SuchTreffer] = {}
|
|
scores: dict[int, float] = {}
|
|
for row in direkt_ergebnis:
|
|
kandidaten[row.id] = SuchTreffer(id=row.id, code=row.code, name=row.name)
|
|
scores[row.id] = float(row.score or 0)
|
|
for row in material_ergebnis:
|
|
score = float(row.score or 0)
|
|
if row.id in kandidaten:
|
|
continue
|
|
if row.id not in scores or score > scores[row.id]:
|
|
kandidaten[row.id] = SuchTreffer(
|
|
id=row.id, code=row.code, name=row.name, ueber_material=row.material_name
|
|
)
|
|
scores[row.id] = score
|
|
|
|
treffer = sorted(kandidaten.values(), key=lambda t: scores[t.id], reverse=True)
|
|
return treffer[:_TREFFER_LIMIT]
|