Skip to content

Commit 0b3b49a

Browse files
authored
Create Search-Ranking-GUI-Pro-DDGS.py
1 parent c89487f commit 0b3b49a

1 file changed

Lines changed: 224 additions & 0 deletions

File tree

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import threading
2+
import webbrowser
3+
import tkinter as tk
4+
from tkinter import messagebox, simpledialog
5+
from urllib.parse import urlparse
6+
from dataclasses import dataclass
7+
from typing import List
8+
9+
import ttkbootstrap as tb
10+
from ttkbootstrap.constants import *
11+
from ttkbootstrap.widgets.scrolled import ScrolledText
12+
13+
from sklearn.feature_extraction.text import TfidfVectorizer
14+
from sklearn.metrics.pairwise import cosine_similarity
15+
from rank_bm25 import BM25Okapi
16+
from ddgs import DDGS
17+
18+
# ---------------- CONFIG ---------------- #
19+
RESULTS_PER_PAGE = 6 # Can be changed via UI
20+
21+
# ---------------- GLOBAL STATE ---------------- #
22+
all_ranked_results: List["SearchResult"] = []
23+
current_page = 1
24+
recent_queries: List[str] = []
25+
26+
# ---------------- DATA STRUCTURE ---------------- #
27+
@dataclass
28+
class SearchResult:
29+
title: str
30+
url: str
31+
display_url: str
32+
snippet: str
33+
34+
# ---------------- URL HELPERS ---------------- #
35+
def short_display_url(url: str) -> str:
36+
parsed = urlparse(url)
37+
path = parsed.path.strip("/").split("/")[:5]
38+
if path and path[0]:
39+
return f"{parsed.netloc} › " + " › ".join(path)
40+
return parsed.netloc
41+
42+
# ---------------- SEARCH LOGIC ---------------- #
43+
def fetch_search_results(query: str) -> List[SearchResult]:
44+
results = []
45+
try:
46+
with DDGS() as ddgs:
47+
for entry in ddgs.text(query, max_results=25):
48+
title = entry.get("title") or "Untitled"
49+
url = entry.get("href") or ""
50+
snippet = entry.get("body") or "No description available."
51+
results.append(SearchResult(
52+
title=title,
53+
url=url,
54+
display_url=short_display_url(url),
55+
snippet=snippet
56+
))
57+
except Exception as e:
58+
messagebox.showerror("Search Error", f"An error occurred: {str(e)}")
59+
return []
60+
return results
61+
62+
def rank_results(query: str, results: List[SearchResult]) -> List[tuple]:
63+
if not results:
64+
return []
65+
66+
docs = [f"{r.title} {r.snippet}" for r in results]
67+
vectorizer = TfidfVectorizer(stop_words="english")
68+
tfidf_matrix = vectorizer.fit_transform(docs + [query])
69+
70+
if tfidf_matrix.shape[0] <= 1:
71+
return [(res, 0) for res in results]
72+
73+
tfidf_scores = cosine_similarity(tfidf_matrix[-1], tfidf_matrix[:-1]).flatten()
74+
bm25 = BM25Okapi([d.lower().split() for d in docs])
75+
bm25_scores = bm25.get_scores(query.lower().split())
76+
77+
ranked = []
78+
for i, res in enumerate(results):
79+
score = (tfidf_scores[i] + bm25_scores[i]) / 2
80+
ranked.append((res, score))
81+
82+
return sorted(ranked, key=lambda x: x[1], reverse=True)
83+
84+
# ---------------- UI HELPERS ---------------- #
85+
def open_url(url: str):
86+
if url:
87+
webbrowser.open_new_tab(url)
88+
89+
def highlight_query_words(text_widget, query):
90+
for word in query.split():
91+
start = "1.0"
92+
while True:
93+
pos = text_widget.search(word, start, stopindex="end", nocase=True)
94+
if not pos:
95+
break
96+
end_pos = f"{pos}+{len(word)}c"
97+
text_widget.tag_add("highlight", pos, end_pos)
98+
start = end_pos
99+
text_widget.tag_config("highlight", background="#fff59d") # yellow
100+
101+
# ---------------- DISPLAY ---------------- #
102+
def display_page():
103+
text.configure(state="normal")
104+
text.delete("1.0", "end")
105+
106+
start = (current_page - 1) * RESULTS_PER_PAGE
107+
end = start + RESULTS_PER_PAGE
108+
page_results = all_ranked_results[start:end]
109+
110+
if not page_results:
111+
text.insert("end", "No results found.\n")
112+
text.configure(state="disabled")
113+
update_pagination()
114+
return
115+
116+
for idx, (res, _) in enumerate(page_results):
117+
# Title
118+
title_tag = f"title_{idx}"
119+
text.tag_config(title_tag, foreground="#1a0dab", font=("Segoe UI", 14, "bold"), spacing1=5)
120+
text.insert("end", res.title + "\n", title_tag)
121+
text.tag_bind(title_tag, "<Double-Button-1>", lambda e, url=res.url: open_url(url))
122+
text.tag_bind(title_tag, "<Enter>", lambda e: text.config(cursor="hand2"))
123+
text.tag_bind(title_tag, "<Leave>", lambda e: text.config(cursor=""))
124+
125+
# URL
126+
url_tag = f"url_{idx}"
127+
text.tag_config(url_tag, foreground="#188038", font=("Segoe UI", 11, "italic"))
128+
text.insert("end", res.display_url + "\n", url_tag)
129+
130+
# Snippet
131+
snippet_tag = f"snippet_{idx}"
132+
text.tag_config(snippet_tag, foreground="#4d5156", font=("Segoe UI", 12), spacing3=10)
133+
text.insert("end", res.snippet + "\n\n", snippet_tag)
134+
135+
# Highlight query words
136+
highlight_query_words(text, query_entry.get())
137+
138+
text.configure(state="disabled")
139+
text.yview_moveto(0)
140+
update_pagination()
141+
142+
# ---------------- PAGINATION ---------------- #
143+
def next_page():
144+
global current_page
145+
total_pages = max(1, (len(all_ranked_results) - 1) // RESULTS_PER_PAGE + 1)
146+
if current_page < total_pages:
147+
current_page += 1
148+
display_page()
149+
150+
def prev_page():
151+
global current_page
152+
if current_page > 1:
153+
current_page -= 1
154+
display_page()
155+
156+
def update_pagination():
157+
total_pages = max(1, (len(all_ranked_results) - 1) // RESULTS_PER_PAGE + 1)
158+
page_label.config(text=f"Page {current_page} of {total_pages}")
159+
prev_btn.config(state=DISABLED if current_page == 1 else NORMAL)
160+
next_btn.config(state=DISABLED if current_page >= total_pages else NORMAL)
161+
162+
# ---------------- SEARCH ---------------- #
163+
def perform_search():
164+
query = query_entry.get().strip()
165+
if not query:
166+
messagebox.showwarning("Input Required", "Enter a search query.")
167+
return
168+
if query not in recent_queries:
169+
recent_queries.append(query)
170+
threading.Thread(target=search_thread, args=(query,), daemon=True).start()
171+
172+
def search_thread(query):
173+
global all_ranked_results, current_page
174+
current_page = 1
175+
update_text(lambda: text.insert("end", "Searching...\n"))
176+
177+
results = fetch_search_results(query)
178+
all_ranked_results = rank_results(query, results)
179+
display_page()
180+
181+
def update_text(callback):
182+
text.configure(state="normal")
183+
callback()
184+
text.configure(state="disabled")
185+
186+
# ---------------- UI SETUP ---------------- #
187+
app = tb.Window(title="Search Ranking App", themename="flatly", size=(980,700), resizable=(True,True))
188+
189+
top = tb.Frame(app, padding=15)
190+
top.pack(fill=X)
191+
192+
tb.Label(top, text="Search", font=("Segoe UI", 16, "bold")).pack(anchor=W)
193+
194+
query_entry = tb.Entry(top, font=("Segoe UI",12))
195+
query_entry.pack(fill=X, pady=8)
196+
query_entry.bind("<Return>", lambda e: perform_search())
197+
198+
tb.Button(top, text="Search", bootstyle="primary", command=perform_search).pack(anchor=E)
199+
200+
# Results
201+
result_frame = tb.Frame(app, padding=(15,5))
202+
result_frame.pack(fill=BOTH, expand=True)
203+
204+
result_box = ScrolledText(result_frame, autohide=True)
205+
result_box.pack(fill=BOTH, expand=True)
206+
207+
text = result_box.text
208+
text.configure(state="disabled", wrap="word")
209+
210+
# Pagination
211+
nav = tb.Frame(app, padding=10)
212+
nav.pack(fill=X)
213+
214+
prev_btn = tb.Button(nav, text="← Prev", bootstyle="secondary", command=prev_page)
215+
prev_btn.pack(side=LEFT)
216+
217+
page_label = tb.Label(nav, text="Page 1", font=("Segoe UI",10))
218+
page_label.pack(side=LEFT, padx=10)
219+
220+
next_btn = tb.Button(nav, text="Next →", bootstyle="secondary", command=next_page)
221+
next_btn.pack(side=LEFT)
222+
223+
# ---------------- RUN ---------------- #
224+
app.mainloop()

0 commit comments

Comments
 (0)