Spaces:
Paused
Paused
Download app.py from Saksham4540/Database: direct link, hf CLI and curl.
- Browser
- Download file 13.1 kB
-
https://huggingface.co/spaces/Saksham4540/Database/resolve/main/app.py
- Command line
-
hf download hf://spaces/Saksham4540/Database/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Saksham4540/Database/resolve/main/app.py
13.1 kB
| """Gradio UI + FastAPI backend for ICMR + HITEK Search API. | |
| Runs on Hugging Face Spaces (free Gradio tier). | |
| Uses remote HF split indexes β no local database download needed. | |
| """ | |
| import asyncio | |
| import glob | |
| import json | |
| import os | |
| import threading | |
| from concurrent.futures import ThreadPoolExecutor | |
| from typing import Any | |
| import duckdb | |
| import gradio as gr | |
| from fastapi import FastAPI, HTTPException, Query, Response | |
| from pydantic import BaseModel | |
| # ββ Config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| BASE = os.path.dirname(os.path.abspath(__file__)) | |
| HF_INDEX_BASE = os.environ.get( | |
| "ICMR_HF_INDEX_BASE", | |
| "https://huggingface.co/datasets/Kzr0xx/icrm-hitek-full-db-mixed/resolve/main", | |
| ).rstrip("/") | |
| INDEX_SOURCE = os.environ.get("ICMR_INDEX_SOURCE", "remote").lower() | |
| PARALLELISM = int(os.environ.get("ICMR_PARALLEL", "2")) | |
| THREADS_PER_CONN = int(os.environ.get("ICMR_THREADS_PER_CONN", "2")) | |
| DUPLICATE_CAP = 2 | |
| SEARCH_FIELDS = [ | |
| "name", "fathersName", "phoneNumber", "aadharNumber", "otherNumber", | |
| "address", "district", "pincode", "state", "town", "source", | |
| ] | |
| NUMBER_FIELDS = ["phoneNumber", "aadharNumber", "otherNumber"] | |
| IDX_PHONE = "idx_phone" | |
| IDX_AADHAR = "idx_aadhar" | |
| REMOTE_INDEXES = { | |
| "phone": [f"{HF_INDEX_BASE}/idx_phone.{i}.parquet" for i in range(7)], | |
| "aadhar": [f"{HF_INDEX_BASE}/idx_aadhar.{i}.parquet" for i in range(7)], | |
| } | |
| CREDITS = { | |
| "developer": "@kzr0x", | |
| "team": "TEAM DARK KNIGHT APIs", | |
| "channel": "@API_WALLAH", | |
| } | |
| # ββ DuckDB Connection Pool ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _conns: list[duckdb.DuckDBPyConnection] = [] | |
| _conns_lock = threading.Lock() | |
| _thread_local = threading.local() | |
| pool = ThreadPoolExecutor(max_workers=PARALLELISM, thread_name_prefix="duck") | |
| def _idx_ready(kind: str) -> bool: | |
| return kind in REMOTE_INDEXES | |
| def _new_conn() -> duckdb.DuckDBPyConnection: | |
| con = duckdb.connect() | |
| con.execute("INSTALL parquet; LOAD parquet;") | |
| con.execute("INSTALL httpfs; LOAD httpfs;") | |
| # Create sorted index views from remote HF parts | |
| for kind, urls in REMOTE_INDEXES.items(): | |
| view = f"people_{kind}" | |
| lst = ", ".join(f"'{u}'" for u in urls) | |
| con.execute(f"CREATE OR REPLACE VIEW {view} AS SELECT * FROM read_parquet([{lst}])") | |
| con.execute(f"SET threads = {THREADS_PER_CONN}") | |
| return con | |
| def _thread_id() -> int: | |
| tid = getattr(_thread_local, "id", None) | |
| if tid is None: | |
| with _conns_lock: | |
| tid = len(_conns) | |
| _thread_local.id = tid | |
| return tid | |
| def _get_conn() -> duckdb.DuckDBPyConnection: | |
| ident = _thread_id() | |
| with _conns_lock: | |
| while len(_conns) <= ident: | |
| _conns.append(_new_conn()) | |
| return _conns[ident] | |
| # ββ Dedup & Connected Records βββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _person_key(row: dict) -> tuple: | |
| ph = (row.get("phoneNumber") or "").strip() | |
| ad = (row.get("aadharNumber") or "").strip() | |
| if ph or ad: | |
| return (ph, ad) | |
| return (row.get("name") or "").strip(), (row.get("fathersName") or "").strip() | |
| def _connected_numbers(row: dict) -> list[dict]: | |
| connected, seen = [], set() | |
| for field in NUMBER_FIELDS: | |
| raw = row.get(field) | |
| if raw is None: | |
| continue | |
| value = str(raw).strip() | |
| if not value or value in seen: | |
| continue | |
| seen.add(value) | |
| connected.append({"field": field, "value": value}) | |
| return connected | |
| def _cap_duplicates(rows: list[dict]) -> list[dict]: | |
| seen: dict[tuple, int] = {} | |
| out = [] | |
| for r in rows: | |
| k = _person_key(r) | |
| n = seen.get(k, 0) | |
| if n < DUPLICATE_CAP: | |
| seen[k] = n + 1 | |
| record = dict(r) | |
| record["connected_numbers"] = _connected_numbers(record) | |
| out.append(record) | |
| return out | |
| # ββ Search Logic ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _run_field_search(field: str, value: str, mode: str, limit: int) -> dict: | |
| if field not in SEARCH_FIELDS: | |
| raise ValueError(f"Unknown field: {field}") | |
| v = value.replace("'", "''") | |
| if mode == "exact": | |
| if field == "phoneNumber" and _idx_ready("phone"): | |
| view = "people_phone" | |
| elif field == "aadharNumber" and _idx_ready("aadhar"): | |
| view = "people_aadhar" | |
| elif field == "otherNumber": | |
| # otherNumber not sorted β skip to avoid slow scan | |
| return {"field": field, "value": value, "mode": mode, "count": 0, "results": []} | |
| else: | |
| return {"field": field, "value": value, "mode": mode, "count": 0, "results": []} | |
| sql = f"SELECT * FROM {view} WHERE {field} = '{v}' LIMIT {limit * DUPLICATE_CAP + 20}" | |
| elif mode == "contains": | |
| if field == "name": | |
| # Name search not available in remote-only mode | |
| return {"field": field, "value": value, "mode": mode, "count": 0, "results": []} | |
| v2 = v.replace("%", r"\%").replace("_", r"\_") | |
| sql = f"SELECT * FROM people_phone WHERE {field} ILIKE '%{v2}%' ESCAPE '\\' LIMIT {limit * DUPLICATE_CAP + 20}" | |
| else: | |
| raise ValueError(f"Unknown mode: {mode}") | |
| con = _get_conn() | |
| rows = con.execute(sql).fetchall() | |
| cols = [d[0] for d in con.description] | |
| results = _cap_duplicates([dict(zip(cols, r)) for r in rows])[:limit] | |
| return {"field": field, "value": value, "mode": mode, "count": len(results), "results": results} | |
| def _unified_search(q: str, limit: int = 10) -> dict: | |
| q = q.strip() | |
| is_num = q.isdigit() and len(q) >= 8 | |
| if is_num: | |
| all_rows = [] | |
| searched = [] | |
| # Phone index first (fast) | |
| if _idx_ready("phone"): | |
| r = _run_field_search("phoneNumber", q, "exact", limit) | |
| all_rows.extend(r["results"]) | |
| searched.append("phoneNumber") | |
| # Aadhar index second | |
| if not all_rows and _idx_ready("aadhar"): | |
| r = _run_field_search("aadharNumber", q, "exact", limit) | |
| all_rows.extend(r["results"]) | |
| searched.append("aadharNumber") | |
| all_rows = _cap_duplicates(all_rows)[:limit] | |
| return { | |
| "query": q, "searched_fields": searched, | |
| "count": len(all_rows), "results": all_rows, | |
| } | |
| else: | |
| return {"query": q, "searched_fields": [], "count": 0, "results": []} | |
| # ββ FastAPI (for API access) ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| fastapi_app = FastAPI(title="ICMR + HITEK Search API") | |
| class BatchRequest(BaseModel): | |
| queries: list[dict[str, Any]] | |
| limit: int = 10 | |
| def root(): | |
| return {"app": "ICMR + HITEK Search API", "records": 2_504_793_870, | |
| "indexes": {"phone": _idx_ready("phone"), "aadhar": _idx_ready("aadhar")}, | |
| "index_source": INDEX_SOURCE, "columns": SEARCH_FIELDS, | |
| "credits": CREDITS, "docs": "/docs"} | |
| def health(): | |
| return {"status": "ok", "raw_database_required": False, | |
| "indexes": {"phone": _idx_ready("phone"), "aadhar": _idx_ready("aadhar")}, | |
| "index_source": INDEX_SOURCE} | |
| async def search( | |
| q: str | None = Query(None), | |
| mobile: str | None = Query(None), | |
| field: str | None = Query(None), | |
| mode: str = Query("exact"), | |
| limit: int = Query(10, ge=1, le=1000), | |
| pretty: bool = Query(True), | |
| ): | |
| q_val = (q or mobile or "").strip() | |
| if not q_val: | |
| raise HTTPException(422, "Provide q or mobile") | |
| loop = asyncio.get_running_loop() | |
| if field: | |
| data = await loop.run_in_executor(pool, _run_field_search, field, q_val, mode, limit) | |
| else: | |
| data = await loop.run_in_executor(pool, _unified_search, q_val, limit) | |
| result = {"success": bool(data["count"]), **data, "number": q_val, | |
| "total": data["count"], "credits": CREDITS} | |
| content = json.dumps(result, indent=2 if pretty else None, ensure_ascii=False) | |
| return Response(content=content, media_type="application/json") | |
| async def search_parallel(req: BatchRequest): | |
| if not req.queries: | |
| raise HTTPException(400, "queries must not be empty") | |
| if len(req.queries) > 50: | |
| raise HTTPException(400, "max 50 queries per batch") | |
| loop = asyncio.get_running_loop() | |
| tasks = [ | |
| loop.run_in_executor(pool, _run_field_search, | |
| item.get("field", "phoneNumber"), | |
| item.get("value", ""), | |
| item.get("mode", "exact"), | |
| int(item.get("limit", req.limit))) | |
| for item in req.queries | |
| ] | |
| results = await asyncio.gather(*tasks) | |
| return Response(content=json.dumps({"searches": len(req.queries), "results": list(results)}, | |
| indent=2, ensure_ascii=False), | |
| media_type="application/json") | |
| # ββ Gradio UI βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def format_result(row: dict) -> str: | |
| """Format a single result record as readable text.""" | |
| lines = [] | |
| for field in SEARCH_FIELDS: | |
| val = row.get(field, "") | |
| if val: | |
| lines.append(f"**{field}:** {val}") | |
| # Connected numbers | |
| cn = row.get("connected_numbers", []) | |
| if cn: | |
| nums = ", ".join(f"{c['field']}={c['value']}" for c in cn) | |
| lines.append(f"**connected:** {nums}") | |
| return "\n\n".join(lines) | |
| def search_ui(query: str, limit: int) -> str: | |
| """Main Gradio search function.""" | |
| if not query or not query.strip(): | |
| return "β οΈ Kuch toh search karo β phone, aadhar, ya name daalo." | |
| q = query.strip() | |
| try: | |
| data = _unified_search(q, int(limit)) | |
| except Exception as e: | |
| return f"β Error: {str(e)}" | |
| count = data["count"] | |
| results = data["results"] | |
| searched = ", ".join(data.get("searched_fields", [])) | |
| if not results: | |
| return f"π **Query:** `{q}`\n**Searched:** {searched}\n\nβ **No data found** for this number." | |
| header = f"π **Query:** `{q}` | **Found:** {count} results | **Searched:** {searched}\n\n---\n\n" | |
| parts = [] | |
| for i, row in enumerate(results, 1): | |
| parts.append(f"### Result {i}\n{format_result(row)}") | |
| return header + "\n\n---\n\n".join(parts) | |
| def build_ui(): | |
| with gr.Blocks( | |
| title="ICMR Search API", | |
| theme=gr.themes.Soft(), | |
| css=""" | |
| .main-title { text-align: center; margin-bottom: 0; } | |
| .subtitle { text-align: center; color: #666; margin-top: 0; } | |
| """ | |
| ) as demo: | |
| gr.Markdown("# π ICMR + HITEK Search API", elem_classes="main-title") | |
| gr.Markdown("Search **2.5 billion records** β phone, Aadhaar, name, address & more", elem_classes="subtitle") | |
| with gr.Row(): | |
| with gr.Column(scale=3): | |
| query_input = gr.Textbox( | |
| label="Search Query", | |
| placeholder="Phone number, Aadhaar, ya name daalo...", | |
| lines=1, | |
| ) | |
| with gr.Column(scale=1): | |
| limit_slider = gr.Slider( | |
| minimum=1, maximum=50, value=10, step=1, | |
| label="Max Results", | |
| ) | |
| search_btn = gr.Button("π Search", variant="primary", size="lg") | |
| output = gr.Markdown(label="Results") | |
| search_btn.click( | |
| fn=search_ui, | |
| inputs=[query_input, limit_slider], | |
| outputs=output, | |
| ) | |
| query_input.submit( | |
| fn=search_ui, | |
| inputs=[query_input, limit_slider], | |
| outputs=output, | |
| ) | |
| gr.Markdown("---") | |
| with gr.Accordion("π‘ API Info", open=False): | |
| gr.Markdown(""" | |
| **Endpoints** (via FastAPI): | |
| - `GET /search?q=<number>` β Phone/Aadhaar search | |
| - `GET /search?mobile=<number>` β Phone search (alias) | |
| - `GET /health` β Health check | |
| - `GET /docs` β Swagger UI | |
| **Source:** [HF Dataset](https://huggingface.co/datasets/Kzr0xx/icrm-hitek-full-db-mixed) | |
| **Developer:** @kzr0x | **Team:** TEAM DARK KNIGHT APIs | |
| """) | |
| return demo | |
| # ββ Mount Gradio on FastAPI βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| demo = build_ui() | |
| app = gr.mount_gradio_app(fastapi_app, demo, path="/") | |