From 98f21323fba2d04bd28bc382125c03f93af6c113 Mon Sep 17 00:00:00 2001 From: Lars Date: Sun, 28 Dec 2025 11:35:18 +0100 Subject: [PATCH] bug fix --- app/frontend/ui_graph_service.py | 84 +++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 29 deletions(-) diff --git a/app/frontend/ui_graph_service.py b/app/frontend/ui_graph_service.py index 5287b2a..ac36fe5 100644 --- a/app/frontend/ui_graph_service.py +++ b/app/frontend/ui_graph_service.py @@ -199,8 +199,9 @@ class GraphExplorerService: # Case B: Edge zeigt direkt auf unsere Note ID shoulds.append(models.FieldCondition(key="target_id", match=models.MatchAny(any=note_ids))) - # Case C: Edge zeigt auf unseren Titel (Wikilinks) - auch wenn note_title None ist, versuchen wir es mit den Titeln der Notes - # WICHTIG: Wir müssen auch nach "Titel#Abschnitt" Format suchen! + # Case C: Edge zeigt auf unseren Titel (Wikilinks) + # WICHTIG: target_id ist der vollständige Wikilink-Text ohne [[]], z.B. "Meine Prinzipien 2025#P3 – Disziplin" + # Das kann sein: "Titel" oder "Titel#Abschnitt" oder "Titel#Abschnitt (Details)" note_titles_to_search = [] if note_title: note_titles_to_search.append(note_title) @@ -211,13 +212,13 @@ class GraphExplorerService: if note and note.get("title"): note_titles_to_search.append(note.get("title")) - # Für jeden Titel: Suche nach exaktem Match UND nach "Titel#*" Varianten - # Da Qdrant keine Wildcard-Suche hat, müssen wir explizit nach bekannten Varianten suchen - # ABER: Wir wissen nicht, welche Abschnitte existieren, also müssen wir alle möglichen target_ids prüfen + # Für jeden Titel: Suche nach exaktem Match + # WICHTIG: target_id kann "Titel" oder "Titel#Abschnitt" oder "Titel#Abschnitt (Details)" sein + # Wir suchen nach exaktem Match für "Titel" for title in note_titles_to_search: - # Exakte Übereinstimmung + # Exakte Übereinstimmung (für target_id = "Titel") shoulds.append(models.FieldCondition(key="target_id", match=models.MatchValue(value=title))) - # WICHTIG: "Titel#Abschnitt" Varianten werden in Case D gefunden (clientseitige Filterung) + # WICHTIG: "Titel#*" Varianten werden in Case D gefunden (clientseitige Filterung) if shoulds: in_filter = models.Filter( @@ -229,7 +230,7 @@ class GraphExplorerService: results.extend(res_in) # Case D: ZUSÄTZLICHE Suche für "Titel#Abschnitt" Format (nur für INCOMING edges) - # PROBLEM: Wikilinks wie [[Titel#Abschnitt]] werden als target_id="Titel#Abschnitt" gespeichert + # PROBLEM: target_id ist der vollständige Wikilink-Text, z.B. "Meine Prinzipien 2025#P3 – Disziplin" # Da Qdrant keine Wildcard-Suche hat, müssen wir breiter suchen und clientseitig filtern # WICHTIG: Diese Suche ist nur für eingehende Kanten relevant # Für ausgehende Kanten werden alle über note_id gefunden, unabhängig vom target_id Format @@ -254,12 +255,12 @@ class GraphExplorerService: for edge in res_extended: tgt_id = edge.payload.get("target_id", "") if tgt_id and edge.id not in existing_edge_ids: - # Prüfe, ob target_id mit einem unserer Titel beginnt (für "Titel#Abschnitt" Format) - # ODER exakt dem Titel entspricht + # Prüfe, ob target_id mit einem unserer Titel beginnt + # target_id kann sein: "Titel", "Titel#Abschnitt", "Titel#Abschnitt (Details)" for title in note_titles_to_search: - # Exakte Übereinstimmung oder beginnt mit "Titel#" - # WICHTIG: startswith prüft auch exakte Übereinstimmung (title == tgt_id) - if tgt_id.startswith(title + "#") or tgt_id == title: + # Exakte Übereinstimmung ODER beginnt mit "Titel#" + # WICHTIG: startswith mit "#" findet alle Varianten wie "Titel#P3 – Disziplin" + if tgt_id == title or tgt_id.startswith(title + "#"): results.append(edge) existing_edge_ids.add(edge.id) break # Nur einmal hinzufügen, auch wenn mehrere Titel passen @@ -339,10 +340,18 @@ class GraphExplorerService: return None def _resolve_note_from_ref(self, ref_str): - """Löst eine ID (Chunk, Note oder Titel) zu einer Note Payload auf.""" + """ + Löst eine Referenz zu einer Note Payload auf. + + ref_str kann sein: + - Note-ID: "20250101-meine-note" + - Chunk-ID: "20250101-meine-note#c01" + - Titel: "Meine Prinzipien 2025" + - Wikilink-Text: "Meine Prinzipien 2025#P3 – Disziplin (Selbstführung & Familie)" + """ if not ref_str: return None - # Fall A: Chunk ID oder Titel#Abschnitt (enthält #) + # Fall A: Enthält # (kann Chunk-ID oder Wikilink mit Abschnitt sein) if "#" in ref_str: try: # Versuch 1: Chunk ID direkt (Format: note_id#c01) @@ -356,24 +365,41 @@ class GraphExplorerService: pass # Versuch 2: NoteID#Section (Hash abtrennen und als Note-ID versuchen) + # z.B. "20250101-meine-note#Abschnitt" -> "20250101-meine-note" possible_note_id = ref_str.split("#")[0] note = self._fetch_note_cached(possible_note_id) if note: return note - # Versuch 3: Titel#Abschnitt (Hash abtrennen und als Titel suchen) - # Dies ist wichtig für Wikilinks im Format [[Titel#Abschnitt]] - possible_title = ref_str.split("#")[0] - try: - res, _ = self.client.scroll( - collection_name=self.notes_col, - scroll_filter=models.Filter(must=[models.FieldCondition(key="title", match=models.MatchValue(value=possible_title))]), - limit=1, with_payload=True - ) - if res and res[0].payload: - self._note_cache[res[0].payload['note_id']] = res[0].payload - return res[0].payload - except Exception: - pass + # Versuch 3: Wikilink-Text mit Abschnitt (z.B. "Meine Prinzipien 2025#P3 – Disziplin") + # WICHTIG: target_id ist der vollständige Wikilink-Text, wir müssen den Titel-Teil extrahieren + # Der Teil vor dem ersten "#" ist der Titel + possible_title = ref_str.split("#")[0].strip() + if possible_title: + try: + # Suche nach exaktem Titel-Match + res, _ = self.client.scroll( + collection_name=self.notes_col, + scroll_filter=models.Filter(must=[models.FieldCondition(key="title", match=models.MatchValue(value=possible_title))]), + limit=1, with_payload=True + ) + if res and res[0].payload: + self._note_cache[res[0].payload['note_id']] = res[0].payload + return res[0].payload + except Exception: + pass + + # Fallback: Text-Suche für Fuzzy-Matching + try: + res, _ = self.client.scroll( + collection_name=self.notes_col, + scroll_filter=models.Filter(must=[models.FieldCondition(key="title", match=models.MatchText(text=possible_title))]), + limit=1, with_payload=True + ) + if res and res[0].payload: + self._note_cache[res[0].payload['note_id']] = res[0].payload + return res[0].payload + except Exception: + pass # Fall B: Note ID direkt note = self._fetch_note_cached(ref_str)