Skip to content

Commit edc6a4b

Browse files
authored
Create FutureSearch-Engine.py
1 parent b83454b commit edc6a4b

1 file changed

Lines changed: 238 additions & 0 deletions

File tree

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import threading
2+
import webbrowser
3+
import tkinter as tk
4+
from urllib.parse import urlparse
5+
from dataclasses import dataclass
6+
from typing import List
7+
8+
import ttkbootstrap as tb
9+
from ttkbootstrap.constants import *
10+
11+
from sklearn.feature_extraction.text import TfidfVectorizer
12+
from sklearn.metrics.pairwise import cosine_similarity
13+
from rank_bm25 import BM25Okapi
14+
from ddgs import DDGS
15+
16+
# ---------------- CONFIG ---------------- #
17+
RESULTS_PER_PAGE = 5
18+
19+
# ---------------- GLOBAL STATE ---------------- #
20+
all_ranked_results: List["SearchResult"] = []
21+
current_page = 1
22+
23+
# ---------------- DATA STRUCTURE ---------------- #
24+
@dataclass
25+
class SearchResult:
26+
title: str
27+
url: str
28+
display_url: str
29+
snippet: str
30+
31+
# ---------------- HELPERS ---------------- #
32+
def short_display_url(url: str) -> str:
33+
parsed = urlparse(url)
34+
path = parsed.path.strip("/").split("/")[:5]
35+
if path and path[0]:
36+
return f"{parsed.netloc} › " + " › ".join(path)
37+
return parsed.netloc
38+
39+
def open_url(url: str):
40+
webbrowser.open_new_tab(url)
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+
tb.Messagebox.show_error("Search Error", str(e))
59+
return results
60+
61+
def rank_results(query: str, results: List[SearchResult]) -> List[tuple]:
62+
if not results:
63+
return []
64+
docs = [f"{r.title} {r.snippet}" for r in results]
65+
vectorizer = TfidfVectorizer(stop_words="english")
66+
tfidf_matrix = vectorizer.fit_transform(docs + [query])
67+
tfidf_scores = cosine_similarity(tfidf_matrix[-1], tfidf_matrix[:-1]).flatten()
68+
bm25 = BM25Okapi([d.lower().split() for d in docs])
69+
bm25_scores = bm25.get_scores(query.lower().split())
70+
ranked = []
71+
for i, res in enumerate(results):
72+
score = (tfidf_scores[i] + bm25_scores[i]) / 2
73+
ranked.append((res, score))
74+
return sorted(ranked, key=lambda x: x[1], reverse=True)
75+
76+
# ---------------- DISPLAY ---------------- #
77+
def display_page():
78+
for widget in results_frame.winfo_children():
79+
widget.destroy()
80+
81+
start = (current_page - 1) * RESULTS_PER_PAGE
82+
end = start + RESULTS_PER_PAGE
83+
page_results = all_ranked_results[start:end]
84+
85+
if not page_results:
86+
tb.Label(
87+
results_frame,
88+
text="No results found.",
89+
font=("Segoe UI", 12),
90+
foreground="#333333"
91+
).pack(pady=10)
92+
return
93+
94+
for res, score in page_results:
95+
# Frame without extra background, inherits parent
96+
card = tb.Frame(results_frame, padding=15)
97+
card.pack(fill=X, pady=8, padx=5)
98+
99+
# Title (clickable with hover effect)
100+
title_label = tb.Label(
101+
card,
102+
text=res.title,
103+
font=("Segoe UI", 14, "bold"),
104+
cursor="hand2",
105+
foreground="#1a0dab"
106+
)
107+
title_label.pack(anchor=W)
108+
title_label.bind("<Button-1>", lambda e, url=res.url: open_url(url))
109+
title_label.bind("<Enter>", lambda e: title_label.configure(foreground="#0b3d91"))
110+
title_label.bind("<Leave>", lambda e: title_label.configure(foreground="#1a0dab"))
111+
112+
# URL / Domain
113+
tb.Label(
114+
card,
115+
text=res.display_url,
116+
font=("Segoe UI", 10, "italic"),
117+
foreground="#006400"
118+
).pack(anchor=W, pady=(2,5))
119+
120+
# Snippet with ellipsis
121+
snippet_text = res.snippet
122+
if len(snippet_text) > 250:
123+
snippet_text = snippet_text[:247] + "..."
124+
tb.Label(
125+
card,
126+
text=snippet_text,
127+
font=("Segoe UI", 12),
128+
wraplength=900,
129+
foreground="#333333"
130+
).pack(anchor=W)
131+
132+
# ---------------- PAGINATION ---------------- #
133+
def next_page():
134+
global current_page
135+
if current_page * RESULTS_PER_PAGE < len(all_ranked_results):
136+
current_page += 1
137+
display_page()
138+
update_pagination()
139+
140+
def prev_page():
141+
global current_page
142+
if current_page > 1:
143+
current_page -= 1
144+
display_page()
145+
update_pagination()
146+
147+
def update_pagination():
148+
total = max(1, (len(all_ranked_results) - 1) // RESULTS_PER_PAGE + 1)
149+
page_label.config(text=f"Page {current_page} of {total}")
150+
prev_btn.config(state=DISABLED if current_page == 1 else NORMAL)
151+
next_btn.config(state=DISABLED if current_page == total else NORMAL)
152+
153+
# ---------------- SEARCH ---------------- #
154+
def perform_search():
155+
query = query_entry.get().strip()
156+
if not query:
157+
tb.Messagebox.show_warning("Input Required", "Enter a search query.")
158+
return
159+
160+
# Disable search button and show "Searching..." text
161+
search_button.config(state=DISABLED)
162+
for widget in results_frame.winfo_children():
163+
widget.destroy()
164+
tb.Label(results_frame, text="Searching...", font=("Segoe UI", 12)).pack(pady=8, padx=5)
165+
166+
# Start the search in a separate thread
167+
threading.Thread(target=search_thread, args=(query,), daemon=True).start()
168+
169+
170+
def search_thread(query):
171+
global all_ranked_results, current_page
172+
173+
try:
174+
# Fetch and rank results
175+
results = fetch_search_results(query)
176+
all_ranked_results = rank_results(query, results)
177+
current_page = 1
178+
179+
# Schedule the UI update on the main thread
180+
app.after(0, lambda: (
181+
display_page(),
182+
update_pagination(),
183+
search_button.config(state=NORMAL) # Re-enable search button
184+
))
185+
186+
except Exception as e:
187+
# Show error on the main thread
188+
app.after(0, lambda: (
189+
tb.Messagebox.show_error("Search Error", str(e)),
190+
search_button.config(state=NORMAL)
191+
))
192+
193+
# ---------------- UI ---------------- #
194+
app = tb.Window(title="FutureSearch Engine", themename="flatly", size=(980, 720))
195+
196+
# Top search frame
197+
top_frame = tb.Frame(app, padding=15)
198+
top_frame.pack(fill=X)
199+
200+
tb.Label(top_frame, text="🌟 FutureSearch Engine", font=("Segoe UI", 18, "bold")).pack(anchor=W)
201+
query_entry = tb.Entry(top_frame, font=("Segoe UI", 12))
202+
query_entry.pack(fill=X, pady=8)
203+
query_entry.bind("<Return>", lambda e: perform_search())
204+
# Assign the search button to a variable
205+
search_button = tb.Button(top_frame, text="Search", bootstyle="success", command=perform_search)
206+
search_button.pack(anchor=E)
207+
208+
209+
# Scrollable results
210+
results_container = tb.Frame(app)
211+
results_container.pack(fill=BOTH, expand=True)
212+
213+
canvas = tk.Canvas(results_container, highlightthickness=0)
214+
scrollbar = tb.Scrollbar(results_container, orient="vertical", command=canvas.yview)
215+
results_frame = tb.Frame(canvas)
216+
217+
results_frame.bind(
218+
"<Configure>",
219+
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
220+
)
221+
222+
canvas.create_window((0, 0), window=results_frame, anchor="nw")
223+
canvas.configure(yscrollcommand=scrollbar.set)
224+
225+
canvas.pack(side="left", fill="both", expand=True)
226+
scrollbar.pack(side="right", fill="y")
227+
228+
# Pagination
229+
nav_frame = tb.Frame(app, padding=10)
230+
nav_frame.pack(fill=X)
231+
prev_btn = tb.Button(nav_frame, text="← Prev", bootstyle="secondary", command=prev_page)
232+
prev_btn.pack(side=LEFT)
233+
page_label = tb.Label(nav_frame, text="Page 1", font=("Segoe UI", 10))
234+
page_label.pack(side=LEFT, padx=10)
235+
next_btn = tb.Button(nav_frame, text="Next →", bootstyle="secondary", command=next_page)
236+
next_btn.pack(side=LEFT)
237+
238+
app.mainloop()

0 commit comments

Comments
 (0)