Vertiefung · Such-Algorithmus

3 Stufen, keine Vektor-DB

Gewichtungslogik, Stoppwort-Filter, Snippet-Extraktion und BFS-Pfadfindung im Detail.

01
Stufe 1: Quick Search — Gewichtete Metadaten

Tag-Matches ×3, Titel-Matches ×2, Substring ×1/×0.5. Relevanz-Boost: critical +30%, high +20%. Stoppwörter (de+en) werden vor der Suche entfernt.

# Quick Search Scoring
score = 0.0
tag_matches = query_words & tags # × 3.0
title_matches = query_words & title_words # × 2.0
partial_tag_match # × 1.0 (substring in tag)
partial_title_match # × 0.5 (substring in title)

# Relevanz-Boost
boost = {"critical": 1.5, "high": 1.0, "medium": 0.5, "low": 0.0}
score *= (1 + boost[relevance] * 0.2)
02
Stufe 2: Deep Search — Volltext mit Diminishing Returns

Wird nur aktiviert wenn Quick Search weniger als --limit Treffer liefert. Zählt Wort-Vorkommen im Content, deckelt bei 5 (diminishing returns). Phrase-Bonus wenn zwei Query-Wörter nebeneinander stehen.

# Deep Search — für jeden Node der in Quick nicht gefunden wurde
for qw in query_words:
  count = content_lower.count(qw)
  score += min(count, 5) * 0.5 # max 2.5 pro Wort

# Phrase-Bonus: "database performance" nebeneinander
for pair in word_pairs:
  if pair in content_lower:
    score += 2.0 # starker Bonus!
03
Stufe 3: Relations — Der Graph antwortet mit

Mit --with-relations importiert cm_search die get_node_relations Funktion aus cm_relate. Zu jedem Treffer werden ein- und ausgehende Kanten angezeigt. Maximal 5 Relationen pro Treffer.

# In cm_search.py — Stufe 3
if args.with_relations:
  from cm_relate import get_node_relations
  rels = get_node_relations(ws, result_id)
  # rels = {"outgoing": [...], "incoming": [...]}
  for rel in rels["outgoing"][:5]:
    print(f" {icon} ──{label}──→ [{rel.to}] {title}")
04
BFS-Pfadfindung — Kürzester Weg im Graph

find_path() in cm_relate baut eine Adjazenzliste aus der Kanten-Liste und führt Standard-BFS durch. Besonderheit: Traversiert auch reverse Richtung (markiert mit ~). Max depth: 6 Hops.

# BFS in cm_relate.py
adj = {} # Adjazenzliste bauen
for edge in edges:
  adj[edge["from"]].append((edge["to"], rel))
  adj[edge["to"]].append((edge["from"], f"~{rel}")) # ← reverse!

queue = deque([(from_id, [(from_id, None)])])
visited = {from_id}
while queue:
  current, path = queue.popleft()
  if current == to_id: return path # Gefunden!
  for neighbor, rel in adj[current]:
    if neighbor not in visited:
      queue.append((neighbor, path + [(neighbor, rel)]))