kdallash commited on
Commit
fa7bfcd
ยท
verified ยท
1 Parent(s): 230d833

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +152 -94
app.py CHANGED
@@ -3,63 +3,74 @@ import pandas as pd
3
  import numpy as np
4
  import faiss
5
  import pickle
6
-
7
  from sentence_transformers import SentenceTransformer
8
 
9
- # Load data & models ONCE
 
 
10
 
11
- # Load dataset
12
  df = pd.read_csv("data/hadith.csv")
13
-
14
- # Load embeddings
15
  hadith_embeddings = np.load("data/hadith_embeddings.npy")
16
  print(f"Loaded hadith embeddings: {hadith_embeddings.shape}")
17
 
18
- # Load BM25
19
  with open("data/bm25.pkl", "rb") as f:
20
  bm25 = pickle.load(f)
21
 
22
- # Load anchor FAISS index
23
  anchor_index = faiss.read_index("data/faiss_anchor.index")
24
  print(f"Anchor index dimension: {anchor_index.d}")
25
 
26
- # Load anchor mapping
27
  with open("data/anchor_dict.pkl", "rb") as f:
28
  anchor_dict = pickle.load(f)
29
 
30
  with open("data/unique_anchor_texts.pkl", "rb") as f:
31
  unique_anchor_texts = pickle.load(f)
32
 
33
- # Load embedding model
34
- model = SentenceTransformer(
35
- "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
36
- )
37
  model.max_seq_length = 512
38
 
39
- # Test embedding dimension
40
  test_emb = model.encode("test", normalize_embeddings=True)
41
  print(f"Model embedding dimension: {test_emb.shape}")
42
-
43
- # Verify dimensions match
44
  if test_emb.shape[0] != anchor_index.d:
45
  raise ValueError(
46
  f"Dimension mismatch! Model outputs {test_emb.shape[0]}D but "
47
- f"anchor_index expects {anchor_index.d}D. "
48
- f"Rebuild your anchor_index with the same model."
49
  )
50
 
51
- # Import retrieval logic
52
  from retrieval import hybrid_search_fixed
53
  from utils import preprocess_query
 
54
  # ===============================
55
- # Search Function
56
  # ===============================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
 
 
 
58
  def search_hadith(query, top_k):
59
- if not query.strip():
60
  return "<p class='empty'>Please enter a search query.</p>"
61
 
62
- results_df, _ = hybrid_search_fixed(
 
63
  query=query,
64
  df=df,
65
  bm25=bm25,
@@ -72,131 +83,179 @@ def search_hadith(query, top_k):
72
  top_k=int(top_k)
73
  )
74
 
75
- html = "<div class='results'>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
- for i, row in results_df.iterrows():
78
- html += f"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  <div class="card">
80
  <div class="card-header">
81
- <span class="index">#{i+1}</span>
82
- <span class="topic">{row['main_subj']}</span>
 
 
 
 
 
 
 
83
  </div>
84
 
85
- <div class="text">
86
- {row['matn_text']}
 
 
 
87
  </div>
88
 
89
  <div class="footer">
90
- <a href="{row['url']}" target="_blank">
91
- ๐Ÿ“– View Hadith Source
92
- </a>
93
  </div>
94
  </div>
95
  """
 
96
 
97
- html += "</div>"
98
- return html
99
 
100
  # ===============================
101
- # Custom CSS
102
  # ===============================
103
-
104
  custom_css = """
105
- body {
106
- background-color: #f7f9fc;
107
- }
108
 
109
- .results {
110
- direction: rtl;
111
- font-family: "Tahoma", "Arial", sans-serif;
112
- }
113
 
 
114
  .card {
115
- background: white;
116
- border-radius: 12px;
117
- padding: 18px 20px;
118
- margin-bottom: 16px;
119
- box-shadow: 0 4px 14px rgba(0,0,0,0.06);
 
120
  }
121
 
 
122
  .card-header {
123
- display: flex;
124
- align-items: center;
125
- gap: 10px;
126
- margin-bottom: 12px;
 
127
  }
128
 
 
129
  .index {
130
- background: #2563eb;
131
- color: white;
132
- padding: 4px 10px;
133
- border-radius: 20px;
134
- font-size: 14px;
 
 
 
135
  }
136
 
137
- .topic {
138
- font-weight: bold;
139
- color: #1f2937;
140
- font-size: 16px;
141
  }
142
 
143
- .text {
144
- line-height: 1.9;
145
- font-size: 16px;
146
- color: #111827;
147
- margin-bottom: 14px;
148
  }
149
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  .footer a {
151
- text-decoration: none;
152
- color: #2563eb;
153
- font-weight: bold;
154
  }
 
155
 
156
- .footer a:hover {
157
- text-decoration: underline;
158
- }
159
 
160
- .empty {
161
- direction: ltr;
162
- font-size: 16px;
163
- color: #6b7280;
164
- text-align: center;
165
  }
166
  """
167
 
168
  # ===============================
169
  # Gradio Interface
170
  # ===============================
171
-
172
  interface = gr.Interface(
173
  fn=search_hadith,
174
  inputs=[
175
  gr.Textbox(
176
- label="ุฃุฏุฎู„ ู…ูˆุถูˆุน ุงู„ุจุญุซ ุฃูˆ ุงู„ุณุคุงู„",
177
- placeholder="ู…ุซุงู„: ุฃู‡ู…ูŠุฉ ุงู„ู†ูŠุฉ ูˆุฃุซุฑู‡ุง ููŠ ู‚ุจูˆู„ ุงู„ุฃุนู…ุงู„",
178
-
179
  lines=2
180
  ),
181
- gr.Slider(
182
- minimum=1,
183
- maximum=20,
184
- value=5,
185
- step=1,
186
- label="๐Ÿ“Œ Number of Results"
187
- )
188
  ],
189
  outputs=gr.HTML(),
190
  title="๐Ÿ“š Intelligent Hadith Search Engine",
191
  description="""
192
  An AI-powered semantic search engine for Hadith retrieval.
193
- The system combines lexical, semantic, and topic-aware retrieval
194
- to return results based on meaning โ€” not just keyword matching.
195
  """,
196
- examples=[
197
  ["ุฃู‡ู…ูŠุฉ ุงู„ู†ูŠุฉ ูˆุฃุซุฑู‡ุง ููŠ ู‚ุจูˆู„ ุงู„ุฃุนู…ุงู„", 5],
198
- ["ูุถู„ ุงู„ุตู„ุงุฉ", 5],
199
- ["ุญู‚ูˆู‚ ุงู„ุฌุงุฑ", 5]],
 
200
  css=custom_css,
201
  allow_flagging="never"
202
  )
@@ -204,6 +263,5 @@ interface = gr.Interface(
204
  # ===============================
205
  # Launch
206
  # ===============================
207
-
208
  if __name__ == "__main__":
209
- interface.launch()
 
3
  import numpy as np
4
  import faiss
5
  import pickle
6
+ import html
7
  from sentence_transformers import SentenceTransformer
8
 
9
+ # ===============================
10
+ # Load data & models (ONCE)
11
+ # ===============================
12
 
 
13
  df = pd.read_csv("data/hadith.csv")
 
 
14
  hadith_embeddings = np.load("data/hadith_embeddings.npy")
15
  print(f"Loaded hadith embeddings: {hadith_embeddings.shape}")
16
 
 
17
  with open("data/bm25.pkl", "rb") as f:
18
  bm25 = pickle.load(f)
19
 
 
20
  anchor_index = faiss.read_index("data/faiss_anchor.index")
21
  print(f"Anchor index dimension: {anchor_index.d}")
22
 
 
23
  with open("data/anchor_dict.pkl", "rb") as f:
24
  anchor_dict = pickle.load(f)
25
 
26
  with open("data/unique_anchor_texts.pkl", "rb") as f:
27
  unique_anchor_texts = pickle.load(f)
28
 
29
+ model = SentenceTransformer("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
 
 
 
30
  model.max_seq_length = 512
31
 
32
+ # Quick dimension check
33
  test_emb = model.encode("test", normalize_embeddings=True)
34
  print(f"Model embedding dimension: {test_emb.shape}")
 
 
35
  if test_emb.shape[0] != anchor_index.d:
36
  raise ValueError(
37
  f"Dimension mismatch! Model outputs {test_emb.shape[0]}D but "
38
+ f"anchor_index expects {anchor_index.d}D. Rebuild your anchor_index with the same model."
 
39
  )
40
 
41
+ # Import retrieval
42
  from retrieval import hybrid_search_fixed
43
  from utils import preprocess_query
44
+
45
  # ===============================
46
+ # Helper: safe getter & short preview
47
  # ===============================
48
+ def safe_get(row, col):
49
+ """Get column value safely, return empty string if missing or NaN."""
50
+ try:
51
+ v = row.get(col, "")
52
+ if pd.isna(v):
53
+ return ""
54
+ return str(v)
55
+ except Exception:
56
+ return ""
57
+
58
+ def short_preview(text, length=300):
59
+ text = text.strip()
60
+ if len(text) <= length:
61
+ return text
62
+ # find a safe break (space) near length
63
+ return text[:length].rsplit(' ', 1)[0] + "..."
64
 
65
+ # ===============================
66
+ # Search Function
67
+ # ===============================
68
  def search_hadith(query, top_k):
69
+ if not query or not str(query).strip():
70
  return "<p class='empty'>Please enter a search query.</p>"
71
 
72
+ # Get results + debug (debug contains full-length score arrays)
73
+ results_df, debug = hybrid_search_fixed(
74
  query=query,
75
  df=df,
76
  bm25=bm25,
 
83
  top_k=int(top_k)
84
  )
85
 
86
+ # If debug is provided and contains final_scores, use it for per-hadith score display
87
+ final_scores = debug.get("final_scores") if isinstance(debug, dict) else None
88
+
89
+ # Build HTML results: professional cards with title and metadata
90
+ html_parts = []
91
+ html_parts.append("<div class='results'>")
92
+
93
+ for rank, (_, row) in enumerate(results_df.iterrows(), start=1):
94
+ # original hadith index in df (row.name)
95
+ hadith_idx = row.name if hasattr(row, "name") else ""
96
+
97
+ # columns: prefer 'hadith_title' else fallback to first 50 chars of matn_text
98
+ hadith_title = safe_get(row, "hadith_title")
99
+ if not hadith_title:
100
+ # fallback: try short generated title from main_subj or matn_text
101
+ hadith_title = safe_get(row, "main_subj") or short_preview(safe_get(row, "matn_text"), 60)
102
+
103
+ # matn_text (main hadith body) - preserve Arabic text, escape HTML
104
+ matn_text = safe_get(row, "matn_text")
105
+ matn_text_escaped = html.escape(matn_text).replace("\n", "<br>")
106
 
107
+ topic = html.escape(safe_get(row, "main_subj"))
108
+ source_url = safe_get(row, "url")
109
+
110
+ # score (if available) - normalize to 0..1 and show percentage
111
+ score_display = ""
112
+ if isinstance(final_scores, (list, np.ndarray)):
113
+ try:
114
+ s = float(final_scores[int(hadith_idx)])
115
+ score_display = f"{s:.3f}"
116
+ except Exception:
117
+ score_display = ""
118
+
119
+ # short preview and full details toggle
120
+ preview = short_preview(matn_text, length=360)
121
+ preview_escaped = html.escape(preview).replace("\n", "<br>")
122
+
123
+ card_html = f"""
124
  <div class="card">
125
  <div class="card-header">
126
+ <div class="left">
127
+ <div class="index">#{rank}</div>
128
+ <div class="title">{html.escape(hadith_title)}</div>
129
+ <div class="topic">Topic: {topic}</div>
130
+ </div>
131
+ <div class="right-meta">
132
+ <div class="meta">ID: <span class="meta-val">{hadith_idx}</span></div>
133
+ {f'<div class="meta">Score: <span class="meta-val">{score_display}</span></div>' if score_display else ''}
134
+ </div>
135
  </div>
136
 
137
+ <div class="text-rtl">
138
+ <details>
139
+ <summary class="summary">{preview_escaped}</summary>
140
+ <div class="full-text">{matn_text_escaped}</div>
141
+ </details>
142
  </div>
143
 
144
  <div class="footer">
145
+ {"<a href='" + html.escape(source_url) + "' target='_blank' rel='noopener noreferrer'>๐Ÿ“– View source</a>" if source_url else ""}
 
 
146
  </div>
147
  </div>
148
  """
149
+ html_parts.append(card_html)
150
 
151
+ html_parts.append("</div>")
152
+ return "\n".join(html_parts)
153
 
154
  # ===============================
155
+ # Custom CSS (polished, professional)
156
  # ===============================
 
157
  custom_css = """
158
+ /* Page */
159
+ body { background-color: #f5f7fb; font-family: Inter, "Segoe UI", Tahoma, Arial, sans-serif; }
 
160
 
161
+ /* Container (results) keeps RTL for hadith text blocks but LTR for metadata */
162
+ .results { margin-top: 10px; }
 
 
163
 
164
+ /* Card */
165
  .card {
166
+ background: #ffffff;
167
+ border-radius: 12px;
168
+ padding: 18px;
169
+ margin-bottom: 14px;
170
+ box-shadow: 0 6px 20px rgba(18, 38, 63, 0.06);
171
+ border: 1px solid rgba(20, 43, 74, 0.03);
172
  }
173
 
174
+ /* Header layout */
175
  .card-header {
176
+ display: flex;
177
+ justify-content: space-between;
178
+ align-items: flex-start;
179
+ gap: 12px;
180
+ margin-bottom: 10px;
181
  }
182
 
183
+ .card-header .left { display:flex; flex-direction: column; gap:6px; }
184
  .index {
185
+ display:inline-block;
186
+ background: linear-gradient(90deg,#2563eb,#06b6d4);
187
+ color: white;
188
+ padding: 6px 10px;
189
+ border-radius: 20px;
190
+ font-weight: 600;
191
+ font-size: 13px;
192
+ width: fit-content;
193
  }
194
 
195
+ .title {
196
+ font-size: 18px;
197
+ font-weight: 700;
198
+ color: #0f172a;
199
  }
200
 
201
+ .topic {
202
+ font-size: 13px;
203
+ color: #475569;
204
+ margin-top: 4px;
 
205
  }
206
 
207
+ /* Right meta */
208
+ .right-meta { text-align: right; min-width: 120px; }
209
+ .meta { font-size: 12px; color: #6b7280; }
210
+ .meta-val { color: #0f172a; font-weight: 600; margin-left: 6px; }
211
+
212
+ /* Hadith text: keep RTL and readable */
213
+ .text-rtl { direction: rtl; margin-top: 6px; margin-bottom: 10px; }
214
+ .text-rtl .summary { cursor: pointer; font-size: 16px; color: #111827; line-height: 1.9; }
215
+ .text-rtl .full-text { margin-top: 8px; font-size: 16px; color: #111827; line-height: 1.9; }
216
+
217
+ /* Footer link */
218
+ .footer { margin-top: 12px; }
219
  .footer a {
220
+ text-decoration: none;
221
+ color: #2563eb; font-weight: 600;
 
222
  }
223
+ .footer a:hover { text-decoration: underline; }
224
 
225
+ /* Empty state */
226
+ .empty { font-size: 15px; color: #6b7280; text-align:center; padding: 12px; }
 
227
 
228
+ /* Responsive */
229
+ @media (max-width: 700px) {
230
+ .card-header { flex-direction: column; align-items: flex-start; }
231
+ .right-meta { text-align: left; }
 
232
  }
233
  """
234
 
235
  # ===============================
236
  # Gradio Interface
237
  # ===============================
 
238
  interface = gr.Interface(
239
  fn=search_hadith,
240
  inputs=[
241
  gr.Textbox(
242
+ label="๐Ÿ” Search Query",
243
+ placeholder="e.g. Importance of intention in Islam / ุฃู‡ู…ูŠุฉ ุงู„ู†ูŠุฉ",
 
244
  lines=2
245
  ),
246
+ gr.Slider(minimum=1, maximum=20, value=5, step=1, label="๐Ÿ“Œ Number of Results")
 
 
 
 
 
 
247
  ],
248
  outputs=gr.HTML(),
249
  title="๐Ÿ“š Intelligent Hadith Search Engine",
250
  description="""
251
  An AI-powered semantic search engine for Hadith retrieval.
252
+ Combines lexical (BM25), semantic embeddings, and topic-aware anchors.
 
253
  """,
254
+ examples=[
255
  ["ุฃู‡ู…ูŠุฉ ุงู„ู†ูŠุฉ ูˆุฃุซุฑู‡ุง ููŠ ู‚ุจูˆู„ ุงู„ุฃุนู…ุงู„", 5],
256
+ ["ูุถู„ ุงู„ุตุฏู‚ุฉ", 5],
257
+ ["ุญู‚ูˆู‚ ุงู„ูˆุงู„ุฏูŠู†", 5]
258
+ ],
259
  css=custom_css,
260
  allow_flagging="never"
261
  )
 
263
  # ===============================
264
  # Launch
265
  # ===============================
 
266
  if __name__ == "__main__":
267
+ interface.launch()