diff --git a/app/frontend/ui_graph_service.py b/app/frontend/ui_graph_service.py index 87319f0..1141fb6 100644 --- a/app/frontend/ui_graph_service.py +++ b/app/frontend/ui_graph_service.py @@ -200,14 +200,20 @@ class GraphExplorerService: 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 - if note_title: - shoulds.append(models.FieldCondition(key="target_id", match=models.MatchValue(value=note_title))) + # WICHTIG: Wir müssen auch nach "Titel#Abschnitt" Format suchen! + note_titles_to_search = [] + if note_title: + note_titles_to_search.append(note_title) else: # Fallback: Lade Titel der Notes, wenn note_title nicht übergeben wurde for nid in note_ids: note = self._fetch_note_cached(nid) if note and note.get("title"): - shoulds.append(models.FieldCondition(key="target_id", match=models.MatchValue(value=note.get("title")))) + note_titles_to_search.append(note.get("title")) + + # Für jeden Titel: Suche nach exaktem Match + for title in note_titles_to_search: + shoulds.append(models.FieldCondition(key="target_id", match=models.MatchValue(value=title))) if shoulds: in_filter = models.Filter( @@ -217,7 +223,35 @@ class GraphExplorerService: # Limit erhöht, um alle eingehenden Kanten zu finden res_in, _ = self.client.scroll(self.edges_col, scroll_filter=in_filter, limit=2000, with_payload=True) results.extend(res_in) + + # Case D: ZUSÄTZLICHE Suche für "Titel#Abschnitt" Format + # Da Qdrant keine Wildcard-Suche hat, müssen wir breiter suchen und clientseitig filtern + # Wir suchen nach allen Kanten, die mit einem unserer Titel beginnen + if note_titles_to_search: + # Erweiterte Suche: Lade alle relevanten Kanten und filtere clientseitig + # Dies ist notwendig, weil "Titel#Abschnitt" nicht exakt mit "Titel" übereinstimmt + # OPTIMIERUNG: Nur wenn wir Titel haben, und mit begrenztem Limit + extended_filter = models.Filter( + must=[models.FieldCondition(key="kind", match=models.MatchExcept(**{"except": SYSTEM_EDGES}))] + ) + # Lade Kanten für die clientseitige Filterung (Limit basierend auf Anzahl der Titel) + # Für jeden Titel könnten mehrere "Titel#Abschnitt" Varianten existieren + res_extended, _ = self.client.scroll(self.edges_col, scroll_filter=extended_filter, limit=3000, with_payload=True) + # Clientseitige Filterung: Finde Kanten, deren target_id mit einem unserer Titel beginnt + # Erstelle Set der bereits gefundenen Edge-IDs für schnelle Deduplizierung + existing_edge_ids = {r.id for r in results} + + 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) + for title in note_titles_to_search: + if tgt_id.startswith(title + "#") or tgt_id == title: + results.append(edge) + existing_edge_ids.add(edge.id) + break # Nur einmal hinzufügen, auch wenn mehrere Titel passen + return results def _find_connected_edges_batch(self, note_ids): @@ -296,10 +330,10 @@ class GraphExplorerService: """Löst eine ID (Chunk, Note oder Titel) zu einer Note Payload auf.""" if not ref_str: return None - # Fall A: Chunk ID (enthält #) + # Fall A: Chunk ID oder Titel#Abschnitt (enthält #) if "#" in ref_str: try: - # Versuch 1: Chunk ID direkt + # Versuch 1: Chunk ID direkt (Format: note_id#c01) res = self.client.retrieve(self.chunks_col, ids=[ref_str], with_payload=True) if res and res[0].payload: note_id = res[0].payload.get("note_id") @@ -309,10 +343,25 @@ class GraphExplorerService: except Exception: pass - # Versuch 2: NoteID#Section (Hash abtrennen) + # Versuch 2: NoteID#Section (Hash abtrennen und als Note-ID versuchen) 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 # Fall B: Note ID direkt note = self._fetch_note_cached(ref_str)