| """ |
| NetraLink Entity Resolution — deployment app |
| Loads: |
| 1. The Siamese-BERT sentence-transformers encoder (this repo) |
| 2. xgboost_matcher.joblib + feature_config.json (this repo) |
| Reproduces the exact feature pipeline from the training notebook |
| (pair_features / FEATURE_NAMES) so predictions match training. |
| """ |
|
|
| import json |
| import re |
|
|
| import joblib |
| import numpy as np |
| from huggingface_hub import hf_hub_download |
| from rapidfuzz import fuzz |
| from sentence_transformers import SentenceTransformer, util as st_util |
|
|
| import gradio as gr |
|
|
| REPO_ID = "ankoor123/Netralink-entity-resolution" |
|
|
| |
| |
| |
|
|
| print("Loading Siamese-BERT encoder...") |
| siamese_model = SentenceTransformer(REPO_ID) |
|
|
| print("Loading XGBoost matcher + feature config...") |
| xgb_path = hf_hub_download(REPO_ID, "xgboost_matcher.joblib") |
| xgb_model = joblib.load(xgb_path) |
|
|
| config_path = hf_hub_download(REPO_ID, "feature_config.json") |
| with open(config_path, "r", encoding="utf-8") as f: |
| feature_config = json.load(f) |
| FEATURE_NAMES = feature_config["feature_names"] |
|
|
| |
| |
| |
|
|
|
|
| def normalize_text(text): |
| text = str(text).lower() |
| text = re.sub(r"\s+", " ", text) |
| return text.strip() |
|
|
|
|
| def get_norm(m): |
| if m.get("norm"): |
| return str(m["norm"]) |
| return normalize_text(m["text"]) |
|
|
|
|
| def mention_input_text(m): |
| context = m.get("context", "") or "" |
| if len(context) > 200: |
| context = context[:200] |
| return f"[{m.get('type', '')}] {m['text']} :: {context}" |
|
|
|
|
| def pair_features(m1, m2): |
| emb1 = siamese_model.encode(mention_input_text(m1), convert_to_tensor=True) |
| emb2 = siamese_model.encode(mention_input_text(m2), convert_to_tensor=True) |
| cos_sim = float(st_util.cos_sim(emb1, emb2)[0][0]) |
|
|
| norm1, norm2 = get_norm(m1), get_norm(m2) |
|
|
| ratio = fuzz.ratio(norm1, norm2) / 100.0 |
| token_sort_ratio = fuzz.token_sort_ratio(norm1, norm2) / 100.0 |
|
|
| soundex1 = m1.get("soundex", "") |
| soundex2 = m2.get("soundex", "") |
| soundex_match = 1.0 if soundex1 and soundex1 == soundex2 else 0.0 |
|
|
| len_diff = abs(len(norm1) - len(norm2)) |
|
|
| values = { |
| "cosine_sim": cos_sim, |
| "char_ratio": ratio, |
| "token_sort_ratio": token_sort_ratio, |
| "soundex_match": soundex_match, |
| "len_diff": len_diff, |
| } |
| |
| return [values[name] for name in FEATURE_NAMES], values |
|
|
|
|
| |
| |
| |
|
|
|
|
| def predict(text_a, type_a, context_a, text_b, type_b, context_b): |
| if not text_a.strip() or not text_b.strip(): |
| return "Enter text for both mentions.", {} |
|
|
| m1 = {"text": text_a.strip(), "type": type_a.strip(), "context": context_a.strip() or text_a.strip()} |
| m2 = {"text": text_b.strip(), "type": type_b.strip(), "context": context_b.strip() or text_b.strip()} |
|
|
| feat_vector, feat_dict = pair_features(m1, m2) |
| prob = float(xgb_model.predict_proba(np.array([feat_vector], dtype=np.float32))[0, 1]) |
| label = "MATCH" if prob >= 0.5 else "NO MATCH" |
|
|
| result = f"{label} (match probability: {prob:.3f})" |
| return result, feat_dict |
|
|
|
|
| |
| |
| |
|
|
| with gr.Blocks(title="NetraLink Entity Resolution") as demo: |
| gr.Markdown( |
| "# NetraLink Entity Resolution\n" |
| "Siamese-BERT (IndicBERTv2) similarity + XGBoost matcher.\n" |
| "Enter two entity mentions to check whether they refer to the same entity." |
| ) |
| with gr.Row(): |
| with gr.Column(): |
| gr.Markdown("**Mention A**") |
| text_a = gr.Textbox(label="Text") |
| type_a = gr.Textbox(label="Entity type (e.g. P, ACC, PH)", value="") |
| context_a = gr.Textbox(label="Context (optional, defaults to Text)", lines=2) |
| with gr.Column(): |
| gr.Markdown("**Mention B**") |
| text_b = gr.Textbox(label="Text") |
| type_b = gr.Textbox(label="Entity type (e.g. P, ACC, PH)", value="") |
| context_b = gr.Textbox(label="Context (optional, defaults to Text)", lines=2) |
|
|
| btn = gr.Button("Resolve", variant="primary") |
| output_label = gr.Textbox(label="Result") |
| output_features = gr.JSON(label="Feature breakdown") |
|
|
| btn.click( |
| predict, |
| inputs=[text_a, type_a, context_a, text_b, type_b, context_b], |
| outputs=[output_label, output_features], |
| ) |
|
|
| if __name__ == "__main__": |
| demo.launch() |
|
|