-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtfidf_search.py
More file actions
130 lines (111 loc) · 4.15 KB
/
Copy pathtfidf_search.py
File metadata and controls
130 lines (111 loc) · 4.15 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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
"""
TF-IDF semantic search over failure classes (shared by CLI and Freshness Watch).
Pure stdlib. Default data paths use bundled `src/data` (git checkout or wheel).
"""
from __future__ import annotations
import json
import math
import re
from collections import Counter
from pathlib import Path
from typing import Any
from src.taxonomy_paths import bundled_data_dir
STOPWORDS = {
"a", "an", "the", "and", "or", "but", "in", "on", "at", "to", "for",
"of", "with", "by", "from", "is", "are", "was", "were", "be", "been",
"have", "has", "had", "do", "does", "did", "will", "would", "could",
"should", "may", "might", "this", "that", "these", "those", "it", "its",
"as", "not", "no", "can", "via", "into", "which", "when", "than",
"without", "using", "used", "such", "more", "other", "also", "any",
"all", "each", "some", "if", "then", "even", "both", "their", "they",
"them", "what", "how", "where", "who", "while", "after",
"before", "about", "through",
}
def tokenize(text: str) -> list[str]:
text = text.lower()
tokens = re.findall(r"[a-z][a-z0-9\-]*[a-z0-9]|[a-z]", text)
return [t for t in tokens if t not in STOPWORDS and len(t) > 1]
def _query_vec(
query: str, vocab_index: dict[str, int], idf: list[float]
) -> dict[int, float]:
tokens = tokenize(query)
counts = Counter(tokens)
if not counts:
return {}
max_c = max(counts.values())
vec: dict[int, float] = {}
norm_sq = 0.0
for term, count in counts.items():
if term in vocab_index:
idx = vocab_index[term]
weight = (count / max_c) * idf[idx]
vec[idx] = weight
norm_sq += weight * weight
if norm_sq > 0:
norm = math.sqrt(norm_sq)
vec = {k: v / norm for k, v in vec.items()}
return vec
def _cosine(q_vec: dict[int, float], doc_vec: dict[int, float]) -> float:
if len(q_vec) > len(doc_vec):
q_vec, doc_vec = doc_vec, q_vec
return sum(q_vec[k] * doc_vec.get(k, 0.0) for k in q_vec)
def load_search_index(data_dir: Path | None = None) -> dict[str, Any]:
root = data_dir or bundled_data_dir()
path = root / "search_index.json"
if not path.exists():
raise FileNotFoundError(
f"search_index.json not found at {path}. Run: python scripts/generate_embeddings.py"
)
return json.loads(path.read_text(encoding="utf-8"))
def search_classes(
query: str,
top_k: int = 8,
data_dir: Path | None = None,
group_filter: str | None = None,
severity_filter: str | None = None,
) -> list[dict[str, Any]]:
"""
Return top_k failure classes by TF-IDF cosine similarity to `query`.
Empty list if the query matches no indexed vocabulary terms.
"""
root = data_dir or bundled_data_dir()
idx = load_search_index(root)
failures_path = root / "failures.json"
failures_data = json.loads(failures_path.read_text(encoding="utf-8"))
failures_by_id = {f["id"]: f for f in failures_data["failures"]}
vocab = idx["vocab"]
idf = idx["idf"]
vectors = idx["vectors"]
vocab_index = {term: i for i, term in enumerate(vocab)}
q = _query_vec(query, vocab_index, idf)
if not q:
return []
scores: list[tuple[float, str, dict]] = []
for fid, doc_vec in vectors.items():
doc_vec_int = {int(k): v for k, v in doc_vec.items()}
failure = failures_by_id.get(fid)
if not failure:
continue
if group_filter and failure.get("group", "").upper() != group_filter.upper():
continue
if (
severity_filter
and failure.get("severity", "STANDARD").upper() != severity_filter.upper()
):
continue
sim = _cosine(q, doc_vec_int)
scores.append((sim, fid, failure))
scores.sort(key=lambda x: -x[0])
out: list[dict[str, Any]] = []
for sim, fid, f in scores[:top_k]:
out.append(
{
"id": fid,
"name": f["name"],
"group": f["group"],
"severity": f.get("severity", "STANDARD"),
"score": round(sim, 4),
"mechanism": f.get("mechanism", ""),
}
)
return out