Files

150 lines
5.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
graph-search.py — 图感知搜索
流程:
1. qmd vsearch 获取语义匹配结果(Top 10)
2. 对每个结果,扩展相邻节点:
- 默认:读取各页面 relations 字段(实时,慢但零状态)
- --use-graph:读取预构建的 knowledge-graph.json(快,需先 build
3. 去重后返回(直接关联 + 关系扩展)
用法:
python tools/scripts/graph-search.py "查询词" [--count 15] [--depth 1] [--use-graph]
依赖:
- qmd 已安装(通过 node
- --use-graph 需先运行 knowledge-graph.py build
"""
import argparse
import json
import re
import subprocess
import sys
from pathlib import Path
try:
import yaml
except ImportError:
yaml = None
QMD = r'node "C:\Users\hhhh2024\AppData\Roaming\npm\node_modules\@tobilu\qmd\dist\cli\qmd.js"'
WIKI = Path(r"D:\Applications\app\kepano-obsidian-main\wiki")
GRAPH_DB = Path(r"D:\Applications\app\kepano-obsidian-main\tools\data\knowledge-graph.json")
def qmd_search(query: str, count: int = 10) -> list[str]:
"""调用 qmd vsearch 获取匹配的页面名列表"""
cmd = f'{QMD} vsearch "{query}" -c wiki -n {count}'
result = subprocess.run(cmd, capture_output=True, text=True, shell=True, timeout=30)
lines = [line.strip() for line in result.stdout.splitlines() if line.strip()]
return lines
def get_relations(page_name: str) -> list[dict]:
"""读取页面的 relations 字段"""
fp = WIKI / f"{page_name}.md"
if not fp.exists():
return []
content = fp.read_text(encoding="utf-8")
m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
if not m:
return []
if not yaml:
return []
try:
front = yaml.safe_load(m.group(1)) or {}
except:
return []
return front.get("relations", [])
def expand_nodes(nodes: list[str], depth: int = 1) -> list[str]:
"""从起始节点出发,沿 relations 扩展相邻节点(实时读取页面 frontmatter"""
expanded = list(nodes)
frontier = list(nodes)
for _ in range(depth):
next_frontier = []
for node in frontier:
rels = get_relations(node)
for r in rels:
target = r.get("target", "").strip("[]")
if target and target not in expanded:
expanded.append(target)
next_frontier.append(target)
frontier = next_frontier
if not frontier:
break
return expanded
def expand_nodes_via_graph(nodes: list[str], depth: int = 1, min_conf: int = 3) -> list[str]:
"""通过预构建的 knowledge-graph.json 扩展(更快,无 IO"""
if not GRAPH_DB.exists():
return list(nodes)
graph = json.loads(GRAPH_DB.read_text(encoding="utf-8"))
adj: dict[str, list] = {}
for e in graph["edges"]:
adj.setdefault(e["source"], []).append((e["target"], e.get("confidence", 3)))
adj.setdefault(e["target"], []).append((e["source"], e.get("confidence", 3)))
expanded = list(nodes)
frontier = list(nodes)
for _ in range(depth):
next_frontier = []
for node in frontier:
for target, conf in adj.get(node, []):
if conf >= min_conf and target not in expanded:
expanded.append(target)
next_frontier.append(target)
frontier = next_frontier
if not frontier:
break
return expanded
def main():
parser = argparse.ArgumentParser(description="Graph-aware search for wiki pages")
parser.add_argument("query", help="Search query")
parser.add_argument("--count", type=int, default=15, help="Max results")
parser.add_argument("--depth", type=int, default=1, help="Graph expansion depth")
parser.add_argument("--use-graph", action="store_true",
help="Use prebuilt knowledge-graph.json instead of live frontmatter")
args = parser.parse_args()
print(f"=== Searching: {args.query} ===")
results = qmd_search(args.query, count=max(10, args.count))
if not results:
print(" No results from qmd")
return
print(f"\nDirect matches ({len(results)}):")
for r in results:
print(f" [[{r}]]")
if args.use_graph and not GRAPH_DB.exists():
print(f"\n (note: {GRAPH_DB.name} not found, run `knowledge-graph.py build`; "
f"falling back to live frontmatter)")
if args.use_graph and GRAPH_DB.exists():
expanded = expand_nodes_via_graph(results, depth=args.depth)
source = "knowledge-graph.json"
else:
expanded = expand_nodes(results, depth=args.depth)
source = "live frontmatter"
new = [e for e in expanded if e not in results]
if new:
print(f"\nGraph-expanded (via {source}, depth={args.depth}):")
for n in new[:args.count]:
print(f" [[{n}]]")
if len(new) > args.count:
print(f" ... and {len(new) - args.count} more")
print(f"\nTotal unique: {len(expanded)}")
if __name__ == "__main__":
main()