Spaces:
Sleeping
Sleeping
| """ | |
| Smart Dataset Search Engine | |
| ---------------------------- | |
| Search datasets by MEANING, not by title. | |
| Pipeline: | |
| 1. User types a natural-language query. | |
| 2. We pull a candidate pool of datasets from HuggingFace Hub | |
| (keyword pre-filter, so we don't have to embed the whole Hub). | |
| 3. We embed the query + each candidate's description/card text. | |
| 4. We rank candidates by a blend of: | |
| - semantic similarity (cosine sim of embeddings) | |
| - a "quality" signal (downloads, likes, recency, metadata completeness) | |
| 5. We show the top results with a quality badge and a link. | |
| Kaggle support is stubbed in (see search_kaggle) so it can be enabled later | |
| by adding KAGGLE_USERNAME / KAGGLE_KEY as Space secrets. | |
| """ | |
| import os | |
| import math | |
| import datetime | |
| from dataclasses import dataclass, field | |
| from typing import Optional | |
| import numpy as np | |
| import gradio as gr | |
| from huggingface_hub import HfApi | |
| from sentence_transformers import SentenceTransformer | |
| # --------------------------------------------------------------------------- | |
| # Config | |
| # --------------------------------------------------------------------------- | |
| EMBED_MODEL_NAME = "all-MiniLM-L6-v2" # small, fast, good enough for free CPU Spaces | |
| HF_CANDIDATE_POOL = 40 # how many HF datasets we pull per query before re-ranking | |
| TOP_K_DEFAULT = 10 | |
| # Weight given to semantic similarity vs. quality score when ranking. | |
| # similarity is the main driver; quality nudges ties and surfaces well-documented datasets. | |
| SIMILARITY_WEIGHT = 0.75 | |
| QUALITY_WEIGHT = 0.25 | |
| # --------------------------------------------------------------------------- | |
| # Data model | |
| # --------------------------------------------------------------------------- | |
| class DatasetResult: | |
| source: str # "HuggingFace" | "Kaggle" | |
| dataset_id: str | |
| title: str | |
| description: str | |
| url: str | |
| downloads: Optional[int] = None | |
| likes: Optional[int] = None | |
| last_modified: Optional[datetime.datetime] = None | |
| tags: list = field(default_factory=list) | |
| similarity: float = 0.0 | |
| quality: float = 0.0 | |
| final_score: float = 0.0 | |
| # --------------------------------------------------------------------------- | |
| # Embedding model (loaded once, lazily, at first use) | |
| # --------------------------------------------------------------------------- | |
| _model = None | |
| def get_model() -> SentenceTransformer: | |
| global _model | |
| if _model is None: | |
| _model = SentenceTransformer(EMBED_MODEL_NAME) | |
| return _model | |
| def embed_texts(texts: list[str]) -> np.ndarray: | |
| model = get_model() | |
| embeddings = model.encode(texts, convert_to_numpy=True, normalize_embeddings=True) | |
| return embeddings | |
| def cosine_sim_matrix(query_vec: np.ndarray, doc_vecs: np.ndarray) -> np.ndarray: | |
| # Vectors are already normalized, so dot product == cosine similarity. | |
| return doc_vecs @ query_vec | |
| # --------------------------------------------------------------------------- | |
| # HuggingFace search | |
| # --------------------------------------------------------------------------- | |
| _hf_api = HfApi() | |
| def _extract_hf_description(card_data, tags: Optional[list]) -> str: | |
| """ | |
| Pull the best available free-text description out of a HF DatasetInfo. | |
| card_data is a DatasetCardData object (or None) - it doesn't always carry | |
| free text, so we fall back to tags as a weak signal if nothing else exists. | |
| """ | |
| text_parts = [] | |
| if card_data is not None: | |
| # DatasetCardData can carry a 'description' or similar free text in some cards. | |
| desc = getattr(card_data, "description", None) | |
| if desc: | |
| text_parts.append(str(desc)) | |
| pretty_name = getattr(card_data, "pretty_name", None) | |
| if pretty_name: | |
| text_parts.append(str(pretty_name)) | |
| if not text_parts and tags: | |
| text_parts.append("Tags: " + ", ".join(tags[:15])) | |
| return " | ".join(text_parts) if text_parts else "" | |
| def search_huggingface(query: str, pool_size: int = HF_CANDIDATE_POOL) -> list[DatasetResult]: | |
| """ | |
| Pull a candidate pool of datasets from the HF Hub using its own keyword | |
| search, then return them as DatasetResult objects for re-ranking. | |
| """ | |
| try: | |
| raw_results = list( | |
| _hf_api.list_datasets( | |
| search=query, | |
| limit=pool_size, | |
| full=True, # pulls cardData so we get descriptions, not just IDs | |
| ) | |
| ) | |
| except Exception as e: | |
| # Network issues, rate limits, etc. Surface gracefully rather than crashing the UI. | |
| print(f"[HF search error] {e}") | |
| return [] | |
| results = [] | |
| for d in raw_results: | |
| if getattr(d, "private", False) or getattr(d, "disabled", False): | |
| continue | |
| description = _extract_hf_description(getattr(d, "card_data", None), d.tags) | |
| title = d.id.split("/")[-1].replace("-", " ").replace("_", " ") | |
| results.append( | |
| DatasetResult( | |
| source="HuggingFace", | |
| dataset_id=d.id, | |
| title=title, | |
| description=description or "(no description available)", | |
| url=f"https://huggingface.co/datasets/{d.id}", | |
| downloads=getattr(d, "downloads", None) or 0, | |
| likes=getattr(d, "likes", None) or 0, | |
| last_modified=getattr(d, "last_modified", None), | |
| tags=d.tags or [], | |
| ) | |
| ) | |
| return results | |
| # --------------------------------------------------------------------------- | |
| # Kaggle search (stub - activate once KAGGLE_USERNAME / KAGGLE_KEY are set) | |
| # --------------------------------------------------------------------------- | |
| def kaggle_configured() -> bool: | |
| return bool(os.environ.get("KAGGLE_USERNAME")) and bool(os.environ.get("KAGGLE_KEY")) | |
| def search_kaggle(query: str, pool_size: int = 20) -> list[DatasetResult]: | |
| """ | |
| Placeholder for Kaggle dataset search. | |
| To enable: | |
| 1. Get kaggle.json from kaggle.com -> Settings -> API -> Create New Token | |
| 2. In the HF Space settings, add secrets: | |
| KAGGLE_USERNAME = <username from kaggle.json> | |
| KAGGLE_KEY = <key from kaggle.json> | |
| 3. Add "kaggle" to requirements.txt | |
| 4. Uncomment the implementation below. | |
| """ | |
| if not kaggle_configured(): | |
| return [] | |
| try: | |
| # Local import so the app doesn't hard-require the kaggle package | |
| # until credentials are actually present. | |
| from kaggle.api.kaggle_api_extended import KaggleApi | |
| api = KaggleApi() | |
| api.authenticate() | |
| datasets = api.dataset_list(search=query)[:pool_size] | |
| results = [] | |
| for ds in datasets: | |
| results.append( | |
| DatasetResult( | |
| source="Kaggle", | |
| dataset_id=ds.ref, | |
| title=getattr(ds, "title", ds.ref), | |
| description=getattr(ds, "subtitle", "") or "(no description available)", | |
| url=f"https://www.kaggle.com/datasets/{ds.ref}", | |
| downloads=getattr(ds, "downloadCount", 0), | |
| likes=getattr(ds, "voteCount", 0), | |
| last_modified=None, | |
| tags=[t.name for t in getattr(ds, "tags", [])] if getattr(ds, "tags", None) else [], | |
| ) | |
| ) | |
| return results | |
| except Exception as e: | |
| print(f"[Kaggle search error] {e}") | |
| return [] | |
| # --------------------------------------------------------------------------- | |
| # Quality scoring | |
| # --------------------------------------------------------------------------- | |
| def _log_scale(value: float, cap: float) -> float: | |
| """Squash an unbounded count (downloads/likes) into 0..1 using a log curve.""" | |
| if value <= 0: | |
| return 0.0 | |
| return min(1.0, math.log1p(value) / math.log1p(cap)) | |
| def compute_quality_score(d: DatasetResult) -> float: | |
| """ | |
| A transparent, explainable proxy for dataset quality - NOT a measure of | |
| actual data cleanliness (that would need downloading and profiling the | |
| data itself). This blends: | |
| - popularity (downloads, likes) - log-scaled so a handful of huge | |
| datasets don't dominate the whole ranking | |
| - recency - newer datasets score slightly higher, with a long decay | |
| - metadata completeness - has a real description? has tags? | |
| """ | |
| downloads_score = _log_scale(d.downloads or 0, cap=1_000_000) | |
| likes_score = _log_scale(d.likes or 0, cap=1_000) | |
| # Recency: full credit if updated within ~90 days, decaying over ~3 years. | |
| recency_score = 0.5 # neutral default if we don't know | |
| if d.last_modified is not None: | |
| try: | |
| now = datetime.datetime.now(datetime.timezone.utc) | |
| last_mod = d.last_modified | |
| if last_mod.tzinfo is None: | |
| last_mod = last_mod.replace(tzinfo=datetime.timezone.utc) | |
| age_days = (now - last_mod).days | |
| recency_score = max(0.0, 1.0 - (age_days / (365 * 3))) | |
| except Exception: | |
| recency_score = 0.5 | |
| has_real_description = 1.0 if (d.description and "no description" not in d.description) else 0.0 | |
| has_tags = 1.0 if d.tags else 0.0 | |
| completeness_score = (has_real_description * 0.7) + (has_tags * 0.3) | |
| quality = ( | |
| 0.40 * downloads_score | |
| + 0.20 * likes_score | |
| + 0.15 * recency_score | |
| + 0.25 * completeness_score | |
| ) | |
| return round(quality, 4) | |
| # --------------------------------------------------------------------------- | |
| # Core search + ranking pipeline | |
| # --------------------------------------------------------------------------- | |
| def run_search(query: str, top_k: int, use_hf: bool, use_kaggle: bool) -> list[DatasetResult]: | |
| query = (query or "").strip() | |
| if not query: | |
| return [] | |
| candidates: list[DatasetResult] = [] | |
| if use_hf: | |
| candidates.extend(search_huggingface(query)) | |
| if use_kaggle: | |
| candidates.extend(search_kaggle(query)) | |
| if not candidates: | |
| return [] | |
| # Embed query + all candidate descriptions together (one batch call). | |
| texts = [query] + [f"{c.title}. {c.description}" for c in candidates] | |
| embeddings = embed_texts(texts) | |
| query_vec, doc_vecs = embeddings[0], embeddings[1:] | |
| similarities = cosine_sim_matrix(query_vec, doc_vecs) | |
| for c, sim in zip(candidates, similarities): | |
| c.similarity = float(sim) | |
| c.quality = compute_quality_score(c) | |
| # similarity is roughly in [-1, 1]; clamp negatives to 0 since they're irrelevant matches | |
| sim_clamped = max(0.0, c.similarity) | |
| c.final_score = (SIMILARITY_WEIGHT * sim_clamped) + (QUALITY_WEIGHT * c.quality) | |
| candidates.sort(key=lambda c: c.final_score, reverse=True) | |
| return candidates[:top_k] | |
| # --------------------------------------------------------------------------- | |
| # UI rendering | |
| # --------------------------------------------------------------------------- | |
| def quality_badge(score: float) -> str: | |
| if score >= 0.66: | |
| return "π’ High" | |
| if score >= 0.33: | |
| return "π‘ Medium" | |
| return "π΄ Low" | |
| def format_results_markdown(results: list[DatasetResult]) -> str: | |
| if not results: | |
| return ( | |
| "### No results yet\n\n" | |
| "Try a query like *\"sensor data from industrial machines with timestamps\"* " | |
| "or *\"Sindhi text for sentiment analysis\"*." | |
| ) | |
| lines = [f"### Found {len(results)} result(s)\n"] | |
| for i, r in enumerate(results, 1): | |
| downloads = f"{r.downloads:,}" if r.downloads is not None else "β" | |
| likes = f"{r.likes:,}" if r.likes is not None else "β" | |
| desc = r.description | |
| if len(desc) > 280: | |
| desc = desc[:280].rsplit(" ", 1)[0] + "β¦" | |
| lines.append( | |
| f"**{i}. [{r.title}]({r.url})** \n" | |
| f"`{r.source}` Β· Quality: {quality_badge(r.quality)} " | |
| f"(match {r.similarity:.2f} Β· quality {r.quality:.2f}) Β· " | |
| f"β¬ {downloads} Β· β€ {likes} \n" | |
| f"{desc}\n" | |
| ) | |
| return "\n".join(lines) | |
| def search_handler(query: str, top_k: int, use_hf: bool, use_kaggle: bool): | |
| if not use_hf and not use_kaggle: | |
| return "### Pick at least one source (HuggingFace or Kaggle) above." | |
| results = run_search(query, int(top_k), use_hf, use_kaggle) | |
| return format_results_markdown(results) | |
| # --------------------------------------------------------------------------- | |
| # Gradio UI | |
| # --------------------------------------------------------------------------- | |
| EXAMPLE_QUERIES = [ | |
| "sensor data from industrial machines with timestamps", | |
| "Sindhi language text for sentiment analysis", | |
| "medical images for cancer detection", | |
| "satellite imagery for crop monitoring", | |
| "conversational dialogue data for chatbots", | |
| ] | |
| with gr.Blocks(title="Smart Dataset Search Engine", theme=gr.themes.Soft()) as demo: | |
| gr.Markdown( | |
| """ | |
| # π Smart Dataset Search Engine | |
| Search datasets by **meaning**, not by title. | |
| Type what you need in plain English β the engine semantically matches your | |
| query against dataset descriptions (not just keywords in the name) and | |
| ranks results by relevance + a quality signal (popularity, recency, metadata | |
| completeness). | |
| """ | |
| ) | |
| with gr.Row(): | |
| query_box = gr.Textbox( | |
| label="Describe the dataset you need", | |
| placeholder='e.g. "sensor data from industrial machines with timestamps"', | |
| scale=4, | |
| ) | |
| top_k_slider = gr.Slider( | |
| minimum=3, maximum=20, value=TOP_K_DEFAULT, step=1, label="Results", scale=1 | |
| ) | |
| with gr.Row(): | |
| use_hf_checkbox = gr.Checkbox(value=True, label="Search HuggingFace") | |
| use_kaggle_checkbox = gr.Checkbox( | |
| value=kaggle_configured(), | |
| label="Search Kaggle" + ("" if kaggle_configured() else " (add KAGGLE_USERNAME / KAGGLE_KEY secrets to enable)"), | |
| interactive=kaggle_configured(), | |
| ) | |
| search_btn = gr.Button("Search", variant="primary") | |
| gr.Examples(examples=EXAMPLE_QUERIES, inputs=query_box) | |
| results_md = gr.Markdown(format_results_markdown([])) | |
| search_btn.click( | |
| fn=search_handler, | |
| inputs=[query_box, top_k_slider, use_hf_checkbox, use_kaggle_checkbox], | |
| outputs=results_md, | |
| ) | |
| query_box.submit( | |
| fn=search_handler, | |
| inputs=[query_box, top_k_slider, use_hf_checkbox, use_kaggle_checkbox], | |
| outputs=results_md, | |
| ) | |
| gr.Markdown( | |
| """ | |
| --- | |
| **How ranking works:** results are scored as `0.75 Γ semantic similarity + 0.25 Γ quality`. | |
| Quality blends log-scaled downloads/likes, recency, and metadata completeness β it's a proxy | |
| for *discoverability and documentation*, not a guarantee of clean data. Always inspect a | |
| dataset's card before using it in production. | |
| """ | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |