Anpassung gui
This commit is contained in:
parent
ac9956bf00
commit
e180018c99
|
|
@ -163,31 +163,33 @@ class GraphExplorerService:
|
||||||
return previews
|
return previews
|
||||||
|
|
||||||
def _find_connected_edges(self, note_ids, note_title=None):
|
def _find_connected_edges(self, note_ids, note_title=None):
|
||||||
"""Findet eingehende und ausgehende Kanten."""
|
"""
|
||||||
|
Findet eingehende und ausgehende Kanten.
|
||||||
|
|
||||||
|
WICHTIG: target_id enthält nur den Titel (ohne #Abschnitt).
|
||||||
|
target_section ist ein separates Feld für Abschnitt-Informationen.
|
||||||
|
"""
|
||||||
results = []
|
results = []
|
||||||
|
if not note_ids:
|
||||||
|
return results
|
||||||
|
|
||||||
# 1. OUTGOING EDGES (Der "Owner"-Fix)
|
# 1. OUTGOING EDGES (Der "Owner"-Fix)
|
||||||
# Wir suchen Kanten, die im Feld 'note_id' (Owner) eine unserer Notizen haben.
|
# Wir suchen Kanten, die im Feld 'note_id' (Owner) eine unserer Notizen haben.
|
||||||
# Das findet ALLE ausgehenden Kanten, egal ob sie an einem Chunk oder der Note hängen.
|
# Das findet ALLE ausgehenden Kanten, egal ob sie an einem Chunk oder der Note hängen.
|
||||||
if note_ids:
|
out_filter = models.Filter(must=[
|
||||||
out_filter = models.Filter(must=[
|
models.FieldCondition(key="note_id", match=models.MatchAny(any=note_ids)),
|
||||||
models.FieldCondition(key="note_id", match=models.MatchAny(any=note_ids)),
|
models.FieldCondition(key="kind", match=models.MatchExcept(**{"except": SYSTEM_EDGES}))
|
||||||
models.FieldCondition(key="kind", match=models.MatchExcept(**{"except": SYSTEM_EDGES}))
|
])
|
||||||
])
|
res_out, _ = self.client.scroll(self.edges_col, scroll_filter=out_filter, limit=2000, with_payload=True)
|
||||||
# Limit hoch, um alles zu finden
|
results.extend(res_out)
|
||||||
res_out, _ = self.client.scroll(self.edges_col, scroll_filter=out_filter, limit=500, with_payload=True)
|
|
||||||
results.extend(res_out)
|
|
||||||
|
|
||||||
# 2. INCOMING EDGES (Ziel = Chunk ID oder Titel oder Note ID)
|
# 2. INCOMING EDGES (Ziel = Chunk ID, Note ID oder Titel)
|
||||||
# Hier müssen wir Chunks auflösen, um Treffer auf Chunks zu finden.
|
# WICHTIG: target_id enthält nur den Titel, target_section ist separat
|
||||||
|
|
||||||
# Chunk IDs der aktuellen Notes holen
|
# Chunk IDs der aktuellen Notes holen
|
||||||
chunk_ids = []
|
c_filter = models.Filter(must=[models.FieldCondition(key="note_id", match=models.MatchAny(any=note_ids))])
|
||||||
if note_ids:
|
chunks, _ = self.client.scroll(self.chunks_col, scroll_filter=c_filter, limit=1000, with_payload=False)
|
||||||
c_filter = models.Filter(must=[models.FieldCondition(key="note_id", match=models.MatchAny(any=note_ids))])
|
chunk_ids = [c.id for c in chunks]
|
||||||
chunks, _ = self.client.scroll(self.chunks_col, scroll_filter=c_filter, limit=300)
|
|
||||||
chunk_ids = [c.id for c in chunks]
|
|
||||||
|
|
||||||
shoulds = []
|
shoulds = []
|
||||||
# Case A: Edge zeigt auf einen unserer Chunks
|
# Case A: Edge zeigt auf einen unserer Chunks
|
||||||
|
|
@ -195,42 +197,92 @@ class GraphExplorerService:
|
||||||
shoulds.append(models.FieldCondition(key="target_id", match=models.MatchAny(any=chunk_ids)))
|
shoulds.append(models.FieldCondition(key="target_id", match=models.MatchAny(any=chunk_ids)))
|
||||||
|
|
||||||
# Case B: Edge zeigt direkt auf unsere Note ID
|
# Case B: Edge zeigt direkt auf unsere Note ID
|
||||||
if note_ids:
|
shoulds.append(models.FieldCondition(key="target_id", match=models.MatchAny(any=note_ids)))
|
||||||
shoulds.append(models.FieldCondition(key="target_id", match=models.MatchAny(any=note_ids)))
|
|
||||||
|
# Case C: Edge zeigt auf unseren Titel
|
||||||
# Case C: Edge zeigt auf unseren Titel (Wikilinks)
|
# WICHTIG: target_id enthält nur den Titel (z.B. "Meine Prinzipien 2025")
|
||||||
if note_title:
|
# target_section enthält die Abschnitt-Information (z.B. "P3 – Disziplin"), wenn gesetzt
|
||||||
shoulds.append(models.FieldCondition(key="target_id", match=models.MatchValue(value=note_title)))
|
|
||||||
|
# Sammle alle relevanten Titel (inkl. Aliase)
|
||||||
|
titles_to_search = []
|
||||||
|
if note_title:
|
||||||
|
titles_to_search.append(note_title)
|
||||||
|
|
||||||
|
# Lade auch Titel aus den Notes selbst (falls note_title nicht übergeben wurde)
|
||||||
|
for nid in note_ids:
|
||||||
|
note = self._fetch_note_cached(nid)
|
||||||
|
if note:
|
||||||
|
note_title_from_db = note.get("title")
|
||||||
|
if note_title_from_db and note_title_from_db not in titles_to_search:
|
||||||
|
titles_to_search.append(note_title_from_db)
|
||||||
|
# Aliase hinzufügen
|
||||||
|
aliases = note.get("aliases", [])
|
||||||
|
if isinstance(aliases, str):
|
||||||
|
aliases = [aliases]
|
||||||
|
for alias in aliases:
|
||||||
|
if alias and alias not in titles_to_search:
|
||||||
|
titles_to_search.append(alias)
|
||||||
|
|
||||||
|
# Für jeden Titel: Suche nach exaktem Match
|
||||||
|
# target_id enthält nur den Titel, daher reicht MatchValue
|
||||||
|
for title in titles_to_search:
|
||||||
|
shoulds.append(models.FieldCondition(key="target_id", match=models.MatchValue(value=title)))
|
||||||
|
|
||||||
if shoulds:
|
if shoulds:
|
||||||
in_filter = models.Filter(
|
in_filter = models.Filter(
|
||||||
must=[models.FieldCondition(key="kind", match=models.MatchExcept(**{"except": SYSTEM_EDGES}))],
|
must=[models.FieldCondition(key="kind", match=models.MatchExcept(**{"except": SYSTEM_EDGES}))],
|
||||||
should=shoulds
|
should=shoulds
|
||||||
)
|
)
|
||||||
res_in, _ = self.client.scroll(self.edges_col, scroll_filter=in_filter, limit=500, with_payload=True)
|
res_in, _ = self.client.scroll(self.edges_col, scroll_filter=in_filter, limit=2000, with_payload=True)
|
||||||
results.extend(res_in)
|
results.extend(res_in)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def _find_connected_edges_batch(self, note_ids):
|
def _find_connected_edges_batch(self, note_ids):
|
||||||
# Wrapper für Level 2 Suche
|
"""
|
||||||
return self._find_connected_edges(note_ids)
|
Wrapper für Level 2 Suche.
|
||||||
|
Lädt Titel der ersten Note für Titel-basierte Suche.
|
||||||
|
"""
|
||||||
|
if not note_ids:
|
||||||
|
return []
|
||||||
|
first_note = self._fetch_note_cached(note_ids[0])
|
||||||
|
note_title = first_note.get("title") if first_note else None
|
||||||
|
return self._find_connected_edges(note_ids, note_title=note_title)
|
||||||
|
|
||||||
def _process_edge(self, record, nodes_dict, unique_edges, current_depth):
|
def _process_edge(self, record, nodes_dict, unique_edges, current_depth):
|
||||||
"""Verarbeitet eine rohe Edge, löst IDs auf und fügt sie den Dictionaries hinzu."""
|
"""
|
||||||
|
Verarbeitet eine rohe Edge, löst IDs auf und fügt sie den Dictionaries hinzu.
|
||||||
|
|
||||||
|
WICHTIG: Beide Richtungen werden unterstützt:
|
||||||
|
- Ausgehende Kanten: source_id gehört zu unserer Note (via note_id Owner)
|
||||||
|
- Eingehende Kanten: target_id zeigt auf unsere Note (via target_id Match)
|
||||||
|
"""
|
||||||
|
if not record or not record.payload:
|
||||||
|
return None, None
|
||||||
|
|
||||||
payload = record.payload
|
payload = record.payload
|
||||||
src_ref = payload.get("source_id")
|
src_ref = payload.get("source_id")
|
||||||
tgt_ref = payload.get("target_id")
|
tgt_ref = payload.get("target_id")
|
||||||
kind = payload.get("kind")
|
kind = payload.get("kind")
|
||||||
provenance = payload.get("provenance", "explicit")
|
provenance = payload.get("provenance", "explicit")
|
||||||
|
|
||||||
|
# Prüfe, ob beide Referenzen vorhanden sind
|
||||||
|
if not src_ref or not tgt_ref:
|
||||||
|
return None, None
|
||||||
|
|
||||||
# IDs zu Notes auflösen
|
# IDs zu Notes auflösen
|
||||||
|
# WICHTIG: source_id kann Chunk-ID (note_id#c01), Note-ID oder Titel sein
|
||||||
|
# WICHTIG: target_id kann Chunk-ID, Note-ID oder Titel sein (ohne #Abschnitt)
|
||||||
src_note = self._resolve_note_from_ref(src_ref)
|
src_note = self._resolve_note_from_ref(src_ref)
|
||||||
tgt_note = self._resolve_note_from_ref(tgt_ref)
|
tgt_note = self._resolve_note_from_ref(tgt_ref)
|
||||||
|
|
||||||
if src_note and tgt_note:
|
if src_note and tgt_note:
|
||||||
src_id = src_note['note_id']
|
src_id = src_note.get('note_id')
|
||||||
tgt_id = tgt_note['note_id']
|
tgt_id = tgt_note.get('note_id')
|
||||||
|
|
||||||
|
# Prüfe, ob beide IDs vorhanden sind
|
||||||
|
if not src_id or not tgt_id:
|
||||||
|
return None, None
|
||||||
|
|
||||||
if src_id != tgt_id:
|
if src_id != tgt_id:
|
||||||
# Nodes hinzufügen
|
# Nodes hinzufügen
|
||||||
|
|
@ -245,7 +297,7 @@ class GraphExplorerService:
|
||||||
# Bevorzuge explizite Kanten vor Smart Kanten
|
# Bevorzuge explizite Kanten vor Smart Kanten
|
||||||
is_current_explicit = (provenance in ["explicit", "rule"])
|
is_current_explicit = (provenance in ["explicit", "rule"])
|
||||||
if existing:
|
if existing:
|
||||||
is_existing_explicit = (existing['provenance'] in ["explicit", "rule"])
|
is_existing_explicit = (existing.get('provenance', '') in ["explicit", "rule"])
|
||||||
if is_existing_explicit and not is_current_explicit:
|
if is_existing_explicit and not is_current_explicit:
|
||||||
should_update = False
|
should_update = False
|
||||||
|
|
||||||
|
|
@ -267,33 +319,104 @@ class GraphExplorerService:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _resolve_note_from_ref(self, ref_str):
|
def _resolve_note_from_ref(self, ref_str):
|
||||||
"""Löst eine ID (Chunk, Note oder Titel) zu einer Note Payload auf."""
|
"""
|
||||||
if not ref_str: return None
|
Löst eine Referenz zu einer Note Payload auf.
|
||||||
|
|
||||||
# Fall A: Chunk ID (enthält #)
|
WICHTIG: Wenn ref_str ein Titel#Abschnitt Format hat, wird nur der Titel-Teil verwendet.
|
||||||
|
Unterstützt:
|
||||||
|
- Note-ID: "20250101-meine-note"
|
||||||
|
- Chunk-ID: "20250101-meine-note#c01"
|
||||||
|
- Titel: "Meine Prinzipien 2025"
|
||||||
|
- Titel#Abschnitt: "Meine Prinzipien 2025#P3 – Disziplin" (trennt Abschnitt ab, sucht nur nach Titel)
|
||||||
|
"""
|
||||||
|
if not ref_str:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Fall A: Enthält # (kann Chunk-ID oder Titel#Abschnitt sein)
|
||||||
if "#" in ref_str:
|
if "#" in ref_str:
|
||||||
try:
|
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)
|
res = self.client.retrieve(self.chunks_col, ids=[ref_str], with_payload=True)
|
||||||
if res: return self._fetch_note_cached(res[0].payload.get("note_id"))
|
if res and res[0].payload:
|
||||||
except: pass
|
note_id = res[0].payload.get("note_id")
|
||||||
|
if note_id:
|
||||||
|
return self._fetch_note_cached(note_id)
|
||||||
|
except:
|
||||||
|
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]
|
# z.B. "20250101-meine-note#Abschnitt" -> "20250101-meine-note"
|
||||||
if self._fetch_note_cached(possible_note_id): return self._fetch_note_cached(possible_note_id)
|
possible_note_id = ref_str.split("#")[0].strip()
|
||||||
|
note = self._fetch_note_cached(possible_note_id)
|
||||||
|
if note:
|
||||||
|
return note
|
||||||
|
|
||||||
|
# Versuch 3: Titel#Abschnitt (Hash abtrennen und als Titel suchen)
|
||||||
|
# z.B. "Meine Prinzipien 2025#P3 – Disziplin" -> "Meine Prinzipien 2025"
|
||||||
|
# WICHTIG: target_id enthält nur den Titel, daher suchen wir nur nach dem Titel-Teil
|
||||||
|
possible_title = ref_str.split("#")[0].strip()
|
||||||
|
if possible_title:
|
||||||
|
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:
|
||||||
|
payload = res[0].payload
|
||||||
|
self._note_cache[payload['note_id']] = payload
|
||||||
|
return payload
|
||||||
|
|
||||||
|
# Fallback: Text-Suche für Fuzzy-Matching
|
||||||
|
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=10, with_payload=True
|
||||||
|
)
|
||||||
|
if res:
|
||||||
|
# Nimm das erste Ergebnis, das exakt oder beginnend mit possible_title übereinstimmt
|
||||||
|
for r in res:
|
||||||
|
if r.payload:
|
||||||
|
note_title = r.payload.get("title", "")
|
||||||
|
if note_title == possible_title or note_title.startswith(possible_title):
|
||||||
|
payload = r.payload
|
||||||
|
self._note_cache[payload['note_id']] = payload
|
||||||
|
return payload
|
||||||
|
|
||||||
# Fall B: Note ID direkt
|
# Fall B: Note ID direkt
|
||||||
if self._fetch_note_cached(ref_str): return self._fetch_note_cached(ref_str)
|
note = self._fetch_note_cached(ref_str)
|
||||||
|
if note:
|
||||||
|
return note
|
||||||
|
|
||||||
# Fall C: Titel
|
# Fall C: Titel (exakte Übereinstimmung)
|
||||||
res, _ = self.client.scroll(
|
res, _ = self.client.scroll(
|
||||||
collection_name=self.notes_col,
|
collection_name=self.notes_col,
|
||||||
scroll_filter=models.Filter(must=[models.FieldCondition(key="title", match=models.MatchValue(value=ref_str))]),
|
scroll_filter=models.Filter(must=[
|
||||||
|
models.FieldCondition(key="title", match=models.MatchValue(value=ref_str))
|
||||||
|
]),
|
||||||
limit=1, with_payload=True
|
limit=1, with_payload=True
|
||||||
)
|
)
|
||||||
if res:
|
if res and res[0].payload:
|
||||||
self._note_cache[res[0].payload['note_id']] = res[0].payload
|
payload = res[0].payload
|
||||||
return res[0].payload
|
self._note_cache[payload['note_id']] = payload
|
||||||
|
return payload
|
||||||
|
|
||||||
|
# Fall D: Titel (Text-Suche für Fuzzy-Matching)
|
||||||
|
res, _ = self.client.scroll(
|
||||||
|
collection_name=self.notes_col,
|
||||||
|
scroll_filter=models.Filter(must=[
|
||||||
|
models.FieldCondition(key="title", match=models.MatchText(text=ref_str))
|
||||||
|
]),
|
||||||
|
limit=1, with_payload=True
|
||||||
|
)
|
||||||
|
if res and res[0].payload:
|
||||||
|
payload = res[0].payload
|
||||||
|
self._note_cache[payload['note_id']] = payload
|
||||||
|
return payload
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _add_node_to_dict(self, node_dict, note_payload, level=1):
|
def _add_node_to_dict(self, node_dict, note_payload, level=1):
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user