Spaces:
Runtime error
Runtime error
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 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
features = parsed
|
| 45 |
-
else:
|
| 46 |
-
features = [features]
|
| 47 |
except:
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 94 |
|
| 95 |
|
| 96 |
|
|
@@ -126,14 +126,7 @@ def compute_originality(
|
|
| 126 |
"""
|
| 127 |
hybrid_score = clamp(hybrid_score)
|
| 128 |
|
| 129 |
-
|
| 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 |
-
|
| 264 |
-
df[
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 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 |
|