|
|
import gradio as gr |
|
|
import torch |
|
|
from sentence_transformers import SentenceTransformer |
|
|
from ddgs import DDGS |
|
|
import time |
|
|
|
|
|
|
|
|
model = SentenceTransformer( |
|
|
"RikkaBotan/stable-static-embedding-fast-retrieval-mrl-en", |
|
|
trust_remote_code=True, |
|
|
device="cuda" if torch.cuda.is_available() else "cpu" |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
def web_search(query, max_results=100): |
|
|
results = [] |
|
|
with DDGS() as ddgs: |
|
|
try: |
|
|
for i, r in enumerate(ddgs.text(query, max_results=max_results), start=1): |
|
|
try: |
|
|
results.append({ |
|
|
"title": r.get("title", ""), |
|
|
"body": r.get("body", ""), |
|
|
"href": r.get("href", "") |
|
|
}) |
|
|
except Exception as e: |
|
|
print(f"Skip doc {i}: {e}") |
|
|
except Exception as e: |
|
|
print(f"Skip web batch (max={max_results}): {e}") |
|
|
return results |
|
|
|
|
|
|
|
|
|
|
|
def semantic_web_search(query): |
|
|
if query.strip() == "": |
|
|
return "Please enter a search query." |
|
|
|
|
|
docs = web_search(query, max_results=100) |
|
|
texts = [d["title"] + " " + d["body"] for d in docs] |
|
|
|
|
|
with torch.no_grad(): |
|
|
embeddings = model.encode( |
|
|
[query] + texts[:256], |
|
|
convert_to_tensor=True, |
|
|
normalize_embeddings=True |
|
|
) |
|
|
|
|
|
query_emb = embeddings[0] |
|
|
doc_embs = embeddings[1:] |
|
|
scores = (query_emb @ doc_embs.T).cpu().numpy() |
|
|
|
|
|
ranked = sorted(zip(scores, docs), key=lambda x: x[0], reverse=True)[:30] |
|
|
|
|
|
md = "" |
|
|
for i, (score, d) in enumerate(ranked): |
|
|
md += f""" |
|
|
#### 💎 Rank {i+1} |
|
|
|
|
|
[{d['title']}]({d['href']}) |
|
|
|
|
|
**Score:** `{score:.4f}` |
|
|
|
|
|
{d['body']} |
|
|
|
|
|
--- |
|
|
""" |
|
|
return md |
|
|
|
|
|
|
|
|
|
|
|
def progressive_search(query, threshold=0.7, step=50, max_cap=999): |
|
|
if query.strip() == "": |
|
|
yield "Please enter a search query." |
|
|
return |
|
|
|
|
|
current_k = step |
|
|
|
|
|
while current_k <= max_cap: |
|
|
try: |
|
|
docs = web_search(query, max_results=current_k) |
|
|
except Exception as e: |
|
|
yield f"Skipped batch {current_k} due to error: {e}" |
|
|
current_k += step |
|
|
continue |
|
|
|
|
|
if len(docs) == 0: |
|
|
yield f"No documents fetched for {current_k} results" |
|
|
current_k += step |
|
|
continue |
|
|
|
|
|
texts = [d["title"] + " " + d["body"] for d in docs] |
|
|
|
|
|
with torch.no_grad(): |
|
|
embeddings = model.encode( |
|
|
[query] + texts[:256], |
|
|
convert_to_tensor=True, |
|
|
normalize_embeddings=True |
|
|
) |
|
|
|
|
|
query_emb = embeddings[0] |
|
|
doc_embs = embeddings[1:] |
|
|
scores = (query_emb @ doc_embs.T).cpu().numpy() |
|
|
best_score = float(scores.max()) |
|
|
|
|
|
md = f"### Searching…\n- Documents examined: `{len(docs)}`\n- Best score so far: `{best_score:.4f}`\n" |
|
|
yield md |
|
|
|
|
|
if best_score >= threshold: |
|
|
ranked = sorted(zip(scores, docs), key=lambda x: x[0], reverse=True)[:5] |
|
|
md = f"### Threshold reached!\n" |
|
|
for i, (score, d) in enumerate(ranked): |
|
|
md += f""" |
|
|
#### Rank {i+1} |
|
|
|
|
|
[{d['title']}]({d['href']}) |
|
|
|
|
|
**Score:** `{score:.4f}` |
|
|
|
|
|
{d['body']} |
|
|
|
|
|
--- |
|
|
""" |
|
|
yield md |
|
|
return |
|
|
|
|
|
current_k += step |
|
|
time.sleep(1) |
|
|
ranked = sorted(zip(scores, docs), key=lambda x: x[0], reverse=True)[:5] |
|
|
md = f"### Threshold not reached in max search range.\n" |
|
|
for i, (score, d) in enumerate(ranked): |
|
|
md += f""" |
|
|
#### Rank {i+1} |
|
|
|
|
|
[{d['title']}]({d['href']}) |
|
|
|
|
|
**Score:** `{score:.4f}` |
|
|
|
|
|
{d['body']} |
|
|
|
|
|
--- |
|
|
""" |
|
|
yield md |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pastel_css = """ |
|
|
body { |
|
|
background: linear-gradient(180deg, #f5f9ff 0%, #eaf3ff 40%, #dbeafe 100%); |
|
|
} |
|
|
|
|
|
/* gradient headings */ |
|
|
h1, h2, h3, h4 { |
|
|
background: linear-gradient(135deg, #0b1f5e 0%, #1e3a8a 15%, #3b82f6 30%, #93c5fd 100%); |
|
|
-webkit-background-clip: text; |
|
|
-webkit-text-fill-color: transparent; |
|
|
font-weight: 800; |
|
|
letter-spacing: 0.4px; |
|
|
padding: 4px; |
|
|
} |
|
|
|
|
|
/* optional: slightly softer subtitle tone */ |
|
|
h2, h3 { |
|
|
opacity: 0.9; |
|
|
} |
|
|
|
|
|
|
|
|
.gradio-container { |
|
|
font-family: 'Helvetica Neue', sans-serif; |
|
|
color: #1e3a8a; |
|
|
} |
|
|
|
|
|
/* model card */ |
|
|
.model-card { |
|
|
background: #ffffff; |
|
|
border-radius: 18px; |
|
|
padding: 22px; |
|
|
border: 1px solid #dbeafe; |
|
|
box-shadow: 0 12px 20px rgba(60,120,255,0.18); |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
|
|
|
/* result card */ |
|
|
.result-card { |
|
|
background: #ffffff; |
|
|
border-radius: 18px; |
|
|
padding: 22px; |
|
|
border: 1px solid #dbeafe; |
|
|
box-shadow: 0 12px 20px rgba(60,120,255,0.18); |
|
|
} |
|
|
|
|
|
.gr-markdown, .prose { |
|
|
border: none !important; |
|
|
box-shadow: none !important; |
|
|
padding: 0 !important; |
|
|
color: #1e3a8a !important; |
|
|
} |
|
|
|
|
|
.model-card, .result-card { |
|
|
background: #ffffff; |
|
|
color: #1e3a8a; |
|
|
} |
|
|
|
|
|
@media (prefers-color-scheme: dark) { |
|
|
body { |
|
|
background: linear-gradient(180deg, #0f172a 0%, #1e293b 40%, #334155 100%); |
|
|
} |
|
|
|
|
|
.gradio-container { |
|
|
color: #dbeafe; |
|
|
} |
|
|
|
|
|
.gr-markdown, .prose { |
|
|
color: #dbeafe !important; |
|
|
} |
|
|
|
|
|
.model-card, .result-card { |
|
|
background: #1a1a1a; |
|
|
color: #dbeafe; |
|
|
border: 1px solid #3b82f6; |
|
|
box-shadow: 0 12px 20px rgba(60,120,255,0.18); |
|
|
} |
|
|
|
|
|
.gr-markdown, .prose { |
|
|
color: #dbeafe !important; |
|
|
} |
|
|
} |
|
|
|
|
|
textarea, input { |
|
|
border-radius: 12px !important; |
|
|
border: 1px solid #c7ddff !important; |
|
|
background-color: #f5f9ff !important; |
|
|
color: #1e3a8a !important; |
|
|
} |
|
|
|
|
|
button { |
|
|
background: linear-gradient(135deg, #1e3a8a 0%, #3b82f6 40%, #93c5fd 100%) !important; |
|
|
color: #ffffff !important; |
|
|
border-radius: 14px !important; |
|
|
border: 1px solid #93c5fd !important; |
|
|
font-weight: 600; |
|
|
letter-spacing: 0.3px; |
|
|
|
|
|
box-shadow: |
|
|
0 6px 14px rgba(60,120,255,0.28), |
|
|
inset 0 1px 0 rgba(255,255,255,0.6); |
|
|
|
|
|
transition: all 0.25s ease; |
|
|
} |
|
|
|
|
|
button:hover { |
|
|
background: linear-gradient(135deg, #1b3380 0%, #2563eb 40%, #7fb8ff 100%) !important; |
|
|
box-shadow: |
|
|
0 8px 18px rgba(60,120,255,0.35), |
|
|
inset 0 1px 0 rgba(255,255,255,0.7); |
|
|
transform: translateY(-1px); |
|
|
} |
|
|
|
|
|
button:active { |
|
|
transform: translateY(1px); |
|
|
box-shadow: |
|
|
0 3px 8px rgba(60,120,255,0.2), |
|
|
inset 0 2px 4px rgba(0,0,0,0.08); |
|
|
} |
|
|
|
|
|
""" |
|
|
|
|
|
with gr.Blocks(css=pastel_css) as demo: |
|
|
|
|
|
gr.Markdown('# Semantic Web Search and Deep Web Search') |
|
|
gr.Markdown('## Fast Retrieval with Stable Static Embedding') |
|
|
|
|
|
with gr.Column(elem_classes="model-card"): |
|
|
gr.Markdown(""" |
|
|
## About this Model |
|
|
**RikkaBotan/stable-static-embedding-fast-retrieval-mrl-en** |
|
|
|
|
|
### Performance |
|
|
- **NanoBEIR NDCG@10 = 0.5124** |
|
|
- Higher than other static embedding models |
|
|
|
|
|
### Efficiency |
|
|
- 512 dimensions |
|
|
- ~2× faster retrieval |
|
|
- Separable Dynamic Tanh normalization |
|
|
""") |
|
|
|
|
|
with gr.Tabs(): |
|
|
|
|
|
|
|
|
with gr.Tab("Standard Search"): |
|
|
|
|
|
query1 = gr.Textbox( |
|
|
value="What is Stable Static Embedding?", |
|
|
label="Enter your search query" |
|
|
) |
|
|
|
|
|
btn1 = gr.Button("Search") |
|
|
|
|
|
with gr.Column(elem_classes="result-card"): |
|
|
out1 = gr.Markdown() |
|
|
|
|
|
btn1.click( |
|
|
semantic_web_search, |
|
|
inputs=query1, |
|
|
outputs=out1, |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
with gr.Tab("Deep Search"): |
|
|
|
|
|
query2 = gr.Textbox( |
|
|
value="What is Stable Static Embedding?", |
|
|
label="Enter your search query" |
|
|
) |
|
|
|
|
|
threshold = gr.Slider( |
|
|
0.3, 0.95, value=0.7, step=0.05, |
|
|
label="Score Threshold" |
|
|
) |
|
|
|
|
|
btn2 = gr.Button("Run Deep Search") |
|
|
|
|
|
with gr.Column(elem_classes="result-card"): |
|
|
out2 = gr.Markdown() |
|
|
|
|
|
btn2.click( |
|
|
progressive_search, |
|
|
inputs=[query2, threshold], |
|
|
outputs=out2, |
|
|
show_progress=True, |
|
|
) |
|
|
|
|
|
gr.Markdown("© 2026 Rikka Botan") |
|
|
|
|
|
demo.launch() |
|
|
|