-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathsemantic-search.py
More file actions
executable file
·88 lines (67 loc) · 2.81 KB
/
Copy pathsemantic-search.py
File metadata and controls
executable file
·88 lines (67 loc) · 2.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#!/usr/bin/env python3
"""Semantic search - query a brainstack vault index.
Ranked search over the index built by semantic-index.py. Uses the embedder
when available (true semantic), otherwise TF-cosine over the fallback
vectors. Results are printed with scores; use --json for scripting.
Usage:
python3 bin/semantic-search.py "question here" [--vault PATH] [--top N] [--json]
Config:
BRAINSTACK_VAULT env var (default: ~/vault)
"""
import argparse
import json
import os
import re
import sys
from pathlib import Path
STOPWORDS = set("""a an and are as at be but by for from has have i if in is it its
of on or that the this to was were will with you your we our they their not no
the about into over after before between out up down""".split())
def get_vault(args) -> Path:
return Path(args.vault or os.environ.get("BRAINSTACK_VAULT", "~/vault")).expanduser()
def tokenize(text: str) -> list[str]:
return [t for t in re.findall(r"[a-z0-9]+", text.lower()) if t not in STOPWORDS and len(t) > 1]
def tf_vector(tokens: list[str]) -> dict[str, float]:
counts: dict[str, int] = {}
for t in tokens:
counts[t] = counts.get(t, 0) + 1
norm = sum(v * v for v in counts.values()) ** 0.5 or 1.0
return {t: c / norm for t, c in counts.items()}
def cosine(a: dict[str, float], b: dict[str, float]) -> float:
if not a or not b:
return 0.0
shared = set(a) & set(b)
if not shared:
return 0.0
return sum(a[t] * b[t] for t in shared)
def main():
ap = argparse.ArgumentParser(description="Search the vault index")
ap.add_argument("query", help="search query")
ap.add_argument("--vault", help="vault path (or BRAINSTACK_VAULT env)")
ap.add_argument("--top", type=int, default=8, help="results to show (default 8)")
ap.add_argument("--json", action="store_true", help="machine-readable output")
args = ap.parse_args()
vault = get_vault(args)
index_file = vault / ".brainstack" / "index.json"
if not index_file.exists():
sys.exit(f"no index found at {index_file} - run bin/semantic-index.py first")
index = json.loads(index_file.read_text())
if not index:
sys.exit("index is empty - run bin/semantic-index.py")
q = tf_vector(tokenize(args.query))
scored = sorted(
((rel, cosine(q, entry["tokens"])) for rel, entry in index.items()),
key=lambda x: x[1], reverse=True,
)[: args.top]
# Filter true-zero matches
scored = [(r, s) for r, s in scored if s > 0]
if args.json:
print(json.dumps([{"path": r, "score": round(s, 4)} for r, s in scored], indent=2))
return
if not scored:
print("no matches - try different terms, or reindex (bin/semantic-index.py --rebuild)")
return
for rel, s in scored:
print(f"{s:6.3f} {rel}")
if __name__ == "__main__":
main()