bat-6 commited on
Commit
6683f82
·
1 Parent(s): 8081e10

feat: implement modular hybrid similarity engine with semantic search and feature-based ranking components

Browse files
src/similarity_model/feature_similarity.py CHANGED
@@ -23,6 +23,8 @@ def safe_feature_list(features):
23
  Convert any feature input into clean List[str]
24
  """
25
  import numpy as np
 
 
26
 
27
  if features is None:
28
  return []
@@ -37,15 +39,32 @@ def safe_feature_list(features):
37
  features = list(features)
38
 
39
  if isinstance(features, str):
 
 
 
40
  try:
41
- import ast
42
- parsed = ast.literal_eval(features)
43
- if isinstance(parsed, list):
44
- features = parsed
45
- else:
46
- features = [features]
47
  except:
48
- features = [features]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
  if isinstance(features, list):
51
  cleaned = []
 
23
  Convert any feature input into clean List[str]
24
  """
25
  import numpy as np
26
+ import json
27
+ import ast
28
 
29
  if features is None:
30
  return []
 
39
  features = list(features)
40
 
41
  if isinstance(features, str):
42
+ features = features.strip()
43
+ parsed = None
44
+ # Try JSON parsing
45
  try:
46
+ parsed = json.loads(features)
47
+ if isinstance(parsed, str):
48
+ parsed = json.loads(parsed)
 
 
 
49
  except:
50
+ pass
51
+
52
+ # Try AST parsing as fallback
53
+ if not isinstance(parsed, list):
54
+ try:
55
+ parsed = ast.literal_eval(features)
56
+ if isinstance(parsed, str):
57
+ parsed = ast.literal_eval(parsed)
58
+ except:
59
+ pass
60
+
61
+ if isinstance(parsed, list):
62
+ features = parsed
63
+ else:
64
+ if features:
65
+ features = [features]
66
+ else:
67
+ features = []
68
 
69
  if isinstance(features, list):
70
  cleaned = []
src/similarity_model/hybrid_ranker.py CHANGED
@@ -90,7 +90,7 @@ def compute_hybrid_score(
90
 
91
 
92
  if feature_score == 0 or coverage == 0:
93
- return round(clamp(0.60 * semantic_score), 4)
94
 
95
 
96
 
@@ -126,14 +126,7 @@ def compute_originality(
126
  """
127
  hybrid_score = clamp(hybrid_score)
128
 
129
- if hybrid_score >= 0.80:
130
- originality = 20.0 - ((hybrid_score - 0.80) / 0.20) * 20.0
131
- elif hybrid_score >= 0.50:
132
- originality = 50.0 - ((hybrid_score - 0.50) / 0.30) * 30.0
133
- elif hybrid_score >= 0.30:
134
- originality = 80.0 - ((hybrid_score - 0.30) / 0.20) * 30.0
135
- else:
136
- originality = 100.0 - (hybrid_score / 0.30) * 20.0
137
 
138
  return round(max(0.0, min(100.0, originality)), 2)
139
 
 
90
 
91
 
92
  if feature_score == 0 or coverage == 0:
93
+ return round(clamp(0.85 * semantic_score), 4)
94
 
95
 
96
 
 
126
  """
127
  hybrid_score = clamp(hybrid_score)
128
 
129
+ originality = 100.0 * (1.0 - hybrid_score)
 
 
 
 
 
 
 
130
 
131
  return round(max(0.0, min(100.0, originality)), 2)
132
 
src/similarity_model/semantic_search.py CHANGED
@@ -106,9 +106,7 @@ def build_results(
106
  row = df.loc[idx]
107
 
108
  final_score = float(score)
109
-
110
 
111
-
112
  if query_words:
113
 
114
  title_words = tokenize(row[TITLE_COL])
 
106
  row = df.loc[idx]
107
 
108
  final_score = float(score)
 
109
 
 
110
  if query_words:
111
 
112
  title_words = tokenize(row[TITLE_COL])
src/similarity_model/similarity_engine.py CHANGED
@@ -230,7 +230,8 @@ def find_similar_projects(
230
  features: Optional[List[str]] = None,
231
  top_k: int = DEFAULT_TOP_K,
232
  search_pool: int = DEFAULT_SEARCH_POOL,
233
- auto_extract_features: bool = True
 
234
  ) -> pd.DataFrame:
235
  """
236
  Final Smart Pipeline
@@ -260,36 +261,37 @@ def find_similar_projects(
260
  "Preparing query..."
261
  )
262
 
263
- existing_project = df[
264
- df["project_title"].str.lower().str.strip()
265
- ==
266
- title.lower().strip()
267
- ]
268
-
269
- if len(existing_project) > 0:
270
-
271
- logger.info("Exact title match found — returning originality = 0")
272
-
273
- matched_row = existing_project.iloc[0]
274
- stored_features = safe_feature_list(matched_row["features"])
275
-
276
- return pd.DataFrame([{
277
- "project_id": int(matched_row.name),
278
- "project_title": matched_row["project_title"],
279
- "semantic_score": 1.0,
280
- "feature_score": 1.0,
281
- "coverage": 1.0,
282
- "hybrid_score": 1.0,
283
- "originality_score": 0.0,
284
- "confidence_score": 1.0,
285
- "duplicate_risk": "Very High",
286
- "shared_features_count": len(stored_features),
287
- "matched_features": [{"feature_a": f, "feature_b": f, "score": 1.0} for f in stored_features],
288
- "unique_query_features": [],
289
- "unique_candidate_features": [],
290
- "query_features_used": stored_features,
291
- "query_clean_text": title
292
- }])
 
293
 
294
  logger.info("Extracting new features")
295
 
@@ -324,12 +326,17 @@ def find_similar_projects(
324
 
325
  semantic_results = search_by_text(
326
  query_text=query_text,
327
- k=search_pool,
328
  min_score=DEFAULT_MIN_SEMANTIC_SCORE
329
  )
330
 
331
  if "message" in semantic_results.columns:
332
  return semantic_results
 
 
 
 
 
333
 
334
 
335
 
 
230
  features: Optional[List[str]] = None,
231
  top_k: int = DEFAULT_TOP_K,
232
  search_pool: int = DEFAULT_SEARCH_POOL,
233
+ auto_extract_features: bool = True,
234
+ exclude_self: bool = False
235
  ) -> pd.DataFrame:
236
  """
237
  Final Smart Pipeline
 
261
  "Preparing query..."
262
  )
263
 
264
+ if not exclude_self:
265
+ existing_project = df[
266
+ df["project_title"].str.lower().str.strip()
267
+ ==
268
+ title.lower().strip()
269
+ ]
270
+
271
+ if len(existing_project) > 0:
272
+
273
+ logger.info("Exact title match found — returning originality = 0")
274
+
275
+ matched_row = existing_project.iloc[0]
276
+ stored_features = safe_feature_list(matched_row["features"])
277
+
278
+ return pd.DataFrame([{
279
+ "project_id": int(matched_row.name),
280
+ "project_title": matched_row["project_title"],
281
+ "semantic_score": 1.0,
282
+ "feature_score": 1.0,
283
+ "coverage": 1.0,
284
+ "hybrid_score": 1.0,
285
+ "originality_score": 0.0,
286
+ "confidence_score": 1.0,
287
+ "duplicate_risk": "Very High",
288
+ "shared_features_count": len(stored_features),
289
+ "matched_features": [{"feature_a": f, "feature_b": f, "score": 1.0} for f in stored_features],
290
+ "unique_query_features": [],
291
+ "unique_candidate_features": [],
292
+ "query_features_used": stored_features,
293
+ "query_clean_text": title
294
+ }])
295
 
296
  logger.info("Extracting new features")
297
 
 
326
 
327
  semantic_results = search_by_text(
328
  query_text=query_text,
329
+ k=search_pool + (5 if exclude_self else 0),
330
  min_score=DEFAULT_MIN_SEMANTIC_SCORE
331
  )
332
 
333
  if "message" in semantic_results.columns:
334
  return semantic_results
335
+
336
+ if exclude_self and title.strip():
337
+ semantic_results = semantic_results[
338
+ semantic_results["project_title"].str.lower().str.strip() != title.lower().strip()
339
+ ]
340
 
341
 
342