github-actions[bot] commited on
Commit
05b9d1b
·
1 Parent(s): 01fb9df

chore: sync app/ and src/ from GitHub

Browse files
Files changed (5) hide show
  1. app/app.py +42 -58
  2. src/bm25.py +14 -45
  3. src/rag_pipeline.py +1 -9
  4. src/semantic.py +24 -17
  5. src/utils.py +51 -1
app/app.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import csv, sys
2
  from datetime import datetime
3
  from pathlib import Path
@@ -8,13 +9,12 @@ import markdown
8
  ROOT_FOLDER = Path(__file__).resolve().parent.parent
9
 
10
  sys.path.append(str(ROOT_FOLDER))
11
- import pickle
12
  import os
13
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
14
- from src.retrieval_helpers import enrich_search_results,enrich_bm25_search_results
15
- from src.semantic import load_vector_store
16
  from src.rag_pipeline import run_rag
17
- from src.bm25 import load
18
  from src.hybrid import HybridRetriever
19
 
20
  from dotenv import load_dotenv
@@ -40,7 +40,6 @@ TOP_K = 5
40
 
41
  HF_TOKEN = os.getenv('HF_TOKEN')
42
 
43
- from datasets import load_dataset
44
  from huggingface_hub import snapshot_download, login
45
 
46
  # ─── Custom CSS ───────────────────────────────────────────────────────────────
@@ -49,46 +48,26 @@ with open('./app/styles.css', "r") as f:
49
 
50
  st.markdown(f"<style>{css}</style>", unsafe_allow_html=True)
51
 
52
- @st.cache_resource
53
- def load_hf_dataset():
54
- return load_dataset(
55
- "McAuley-Lab/Amazon-Reviews-2023",
56
- "raw_meta_Grocery_and_Gourmet_Food",
57
- trust_remote_code=True,
58
- token=HF_TOKEN
59
- )
60
-
61
  VECTOR_STORE_DIR = ROOT / "data" / "processed"
62
 
63
  @st.cache_resource
64
  def load_vector_store_cached():
65
-
66
  login(token=HF_TOKEN, add_to_git_credential=False)
67
  VECTOR_STORE_DIR.mkdir(parents=True, exist_ok=True)
68
 
69
- # Skip download if already cached on disk
70
- if not any(VECTOR_STORE_DIR.iterdir()):
71
- snapshot_path = Path(snapshot_download(
72
- repo_id="rishadaz/amazon_retriever-storage",
73
- repo_type="dataset",
74
- local_dir=str(VECTOR_STORE_DIR),
75
- token=HF_TOKEN,
76
- ))
77
- else:
78
- snapshot_path = VECTOR_STORE_DIR
79
-
80
- mini_index_path = snapshot_path / "tokenisation" / "bm25_index_mini.pkl"
81
- embeddings_dir = snapshot_path / "embeddings"
82
-
83
- if not mini_index_path.exists():
84
- raise FileNotFoundError(f"BM25 index not found at {mini_index_path}")
85
- if not embeddings_dir.exists():
86
- raise FileNotFoundError(f"Embeddings dir not found at {embeddings_dir}")
87
 
88
- with open(mini_index_path, "rb") as f:
89
- bm25_retriever = pickle.load(f)
90
 
91
- vector_store = load_vector_store(embeddings_dir)
 
92
 
93
  return vector_store, bm25_retriever
94
 
@@ -97,14 +76,12 @@ def load_vector_store_cached():
97
  # read the mini versions of the files we have provided in the repo
98
 
99
  data_source = os.getenv('DATA_SOURCE')
100
-
101
  # note: remote has the full generated corpus and
102
  # embeddings which can take a long time to download and
103
  # the app might become heavy too and slow down
104
  # processing. For development pls use the smaller "local" corpus
105
 
106
- HF_DATASET = load_hf_dataset()
107
-
108
  if data_source == 'local':
109
  MINI_INDEX_PATH = ROOT / "data" / "processed" / "tokenisation" / "bm25_index_mini.pkl"
110
 
@@ -124,7 +101,7 @@ def bm25_search(query: str, top_k: int = 3) -> list[dict]:
124
  Returns top_k review-level results (may include multiple reviews per ASIN).
125
  """
126
 
127
- results = enrich_bm25_search_results(retriever, query, top_k, HF_DATASET['full'])
128
  return results
129
 
130
 
@@ -136,7 +113,7 @@ def semantic_search(query: str, top_k: int = 3) -> list[dict]:
136
  Returns top_k review-level results (scores are cosine similarities, 0–1).
137
  """
138
 
139
- results = enrich_search_results(vector_store, query, top_k, HF_DATASET['full'])
140
  return results
141
 
142
  hybrid_retriever = HybridRetriever(
@@ -148,7 +125,7 @@ hybrid_retriever = HybridRetriever(
148
  )
149
 
150
  def llm_retriever(query: str, top_k: int = 5):
151
- answer, docs = run_rag(hybrid_retriever, query=query, hf_dataset=HF_DATASET['full'])
152
  return answer, docs
153
 
154
 
@@ -177,18 +154,25 @@ def log_feedback(query: str, mode: str, asin: str, title: str, vote: str) -> Non
177
  "vote": vote,
178
  })
179
 
180
- def render_product(ind, item):
181
- reviews = item.get("reviews",{})
182
- title = item["title"]
 
 
 
 
 
 
183
  avg_rating = item["average_rating"]
184
  n_reviews = len(reviews)
185
  # total_reviews = item.get('total_reviews', n_reviews)
186
  rating_number = item.get('rating_number', 0)
187
  asin = item['parent_asin']
188
  review_word = "review" if n_reviews == 1 else "reviews"
189
- large_images = item.get('images', {}).get('large', [])
190
- image_html = f'<img src="{large_images[0]}" style="width:100%;max-width:200px;border-radius:8px;margin-bottom:8px;" />' if large_images else ''
191
  raw_price = item.get('price')
 
192
  try:
193
  price_val = float(str(raw_price).replace('$', '').replace(',', '').strip())
194
  price_html = f'<span style="color:#2ecc71;font-weight:600">${price_val:.2f}</span>'
@@ -197,7 +181,11 @@ def render_product(ind, item):
197
 
198
 
199
  # ── Product card header ───────────────────────────────────────────
200
- score_badge = f'<span class="score-badge">similarity score: {float(item["score"]):.2f}</span>' if 'score' in item else "<span/>"
 
 
 
 
201
 
202
  st.markdown(
203
  f"""
@@ -207,7 +195,7 @@ def render_product(ind, item):
207
  <span class="stars">{stars(avg_rating)}</span>
208
  &nbsp;<small style="color:#888">{avg_rating:.1f}/5 avg ({rating_number:,} ratings)</small>
209
  &nbsp;&nbsp;
210
- {score_badge}
211
  {"&nbsp;&nbsp;" + price_html if price_html else ""}
212
  </div>
213
  """,
@@ -248,13 +236,13 @@ def render_product(ind, item):
248
 
249
 
250
 
251
- def render_results(results: list[dict], mode: str, query: str) -> None:
252
  if not results:
253
  st.info("No results returned.")
254
  return
255
 
256
  for ind, item in enumerate(results):
257
- render_product(ind,item)
258
 
259
  # ─── App layout ───────────────────────────────────────────────────────────────
260
  st.markdown(
@@ -319,7 +307,7 @@ with tab_search:
319
  if mode == "BM25"
320
  else st.session_state.semantic_results
321
  )
322
- render_results(results, mode=mode.lower(), query=st.session_state.last_query)
323
 
324
  # ─── LLM Tab ──────────────────────────────────────────────────────────────────
325
  with tab_llm:
@@ -343,12 +331,8 @@ with tab_llm:
343
  st.markdown("#### 📦 Retrieved Products")
344
  docs = st.session_state.get("llm_docs", [])
345
  if docs:
346
- # Build scrollable card list in one HTML block
347
- cards_html = "<div class='doc-sidebar'>"
348
- for i, doc in enumerate(docs, 1):
349
- render_product(i,doc)
350
- cards_html += "</div>"
351
- st.markdown(cards_html, unsafe_allow_html=True)
352
  else:
353
  st.markdown("<p style='color:#aaa;'>No documents retrieved.</p>", unsafe_allow_html=True)
354
 
 
1
+ import json
2
  import csv, sys
3
  from datetime import datetime
4
  from pathlib import Path
 
9
  ROOT_FOLDER = Path(__file__).resolve().parent.parent
10
 
11
  sys.path.append(str(ROOT_FOLDER))
12
+ import sys
13
  import os
14
  sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
15
+ from src.semantic import load_vector_store, enrich_search_results
 
16
  from src.rag_pipeline import run_rag
17
+ from src.bm25 import load, search
18
  from src.hybrid import HybridRetriever
19
 
20
  from dotenv import load_dotenv
 
40
 
41
  HF_TOKEN = os.getenv('HF_TOKEN')
42
 
 
43
  from huggingface_hub import snapshot_download, login
44
 
45
  # ─── Custom CSS ───────────────────────────────────────────────────────────────
 
48
 
49
  st.markdown(f"<style>{css}</style>", unsafe_allow_html=True)
50
 
 
 
 
 
 
 
 
 
 
51
  VECTOR_STORE_DIR = ROOT / "data" / "processed"
52
 
53
  @st.cache_resource
54
  def load_vector_store_cached():
 
55
  login(token=HF_TOKEN, add_to_git_credential=False)
56
  VECTOR_STORE_DIR.mkdir(parents=True, exist_ok=True)
57
 
58
+ snapshot_path = snapshot_download(
59
+ repo_id="rishadaz/amazon_retriever-storage",
60
+ repo_type="dataset",
61
+ local_dir=str(VECTOR_STORE_DIR),
62
+ split='full',
63
+ token=HF_TOKEN,
64
+ )
 
 
 
 
 
 
 
 
 
 
 
65
 
66
+ mini_index_path = Path(snapshot_path) / "tokenisation" / "bm25_index_mini.pkl"
67
+ embeddings_dir = Path(snapshot_path) / "embeddings"
68
 
69
+ vector_store = load_vector_store(embeddings_dir)
70
+ bm25_retriever = load(mini_index_path)
71
 
72
  return vector_store, bm25_retriever
73
 
 
76
  # read the mini versions of the files we have provided in the repo
77
 
78
  data_source = os.getenv('DATA_SOURCE')
79
+ print(f"Running with data source {data_source}")
80
  # note: remote has the full generated corpus and
81
  # embeddings which can take a long time to download and
82
  # the app might become heavy too and slow down
83
  # processing. For development pls use the smaller "local" corpus
84
 
 
 
85
  if data_source == 'local':
86
  MINI_INDEX_PATH = ROOT / "data" / "processed" / "tokenisation" / "bm25_index_mini.pkl"
87
 
 
101
  Returns top_k review-level results (may include multiple reviews per ASIN).
102
  """
103
 
104
+ results = search(retriever, query, top_k)
105
  return results
106
 
107
 
 
113
  Returns top_k review-level results (scores are cosine similarities, 0–1).
114
  """
115
 
116
+ results = enrich_search_results(vector_store, query, top_k)
117
  return results
118
 
119
  hybrid_retriever = HybridRetriever(
 
125
  )
126
 
127
  def llm_retriever(query: str, top_k: int = 5):
128
+ answer, docs = run_rag(hybrid_retriever, query=query)
129
  return answer, docs
130
 
131
 
 
154
  "vote": vote,
155
  })
156
 
157
+ def render_product(ind, item, mode):
158
+ item = dict(item)
159
+ if "reviews" in item.keys():
160
+ reviews = item.get("reviews",{})
161
+ elif "top_reviews" in item.keys():
162
+ reviews = item.get("top_reviews",{})
163
+ else:
164
+ reviews = []
165
+ title = item.get("title","")
166
  avg_rating = item["average_rating"]
167
  n_reviews = len(reviews)
168
  # total_reviews = item.get('total_reviews', n_reviews)
169
  rating_number = item.get('rating_number', 0)
170
  asin = item['parent_asin']
171
  review_word = "review" if n_reviews == 1 else "reviews"
172
+ large_image = item.get('image', "")
173
+ image_html = f'<img src="{large_image}" style="width:100%;max-width:200px;border-radius:8px;margin-bottom:8px;" />' if large_image else f'<image src="" />'
174
  raw_price = item.get('price')
175
+ score = item.get('score',None) if 'score' in item else item.get('hybrid_score',None)
176
  try:
177
  price_val = float(str(raw_price).replace('$', '').replace(',', '').strip())
178
  price_html = f'<span style="color:#2ecc71;font-weight:600">${price_val:.2f}</span>'
 
181
 
182
 
183
  # ── Product card header ───────────────────────────────────────────
184
+ score_badge = f'<span class="score-badge">{mode} score: {float(score):.2f}</span>' if score else "<span/>"
185
+ if 'retrieval_source' in item:
186
+ source_badge = f'<span class="score-badge">Source: {item['retrieval_source']}</span>'
187
+ else:
188
+ source_badge = '<span />'
189
 
190
  st.markdown(
191
  f"""
 
195
  <span class="stars">{stars(avg_rating)}</span>
196
  &nbsp;<small style="color:#888">{avg_rating:.1f}/5 avg ({rating_number:,} ratings)</small>
197
  &nbsp;&nbsp;
198
+ {score_badge} {source_badge}
199
  {"&nbsp;&nbsp;" + price_html if price_html else ""}
200
  </div>
201
  """,
 
236
 
237
 
238
 
239
+ def render_results(results: list[dict], mode: str) -> None:
240
  if not results:
241
  st.info("No results returned.")
242
  return
243
 
244
  for ind, item in enumerate(results):
245
+ render_product(ind,item, mode)
246
 
247
  # ─── App layout ───────────────────────────────────────────────────────────────
248
  st.markdown(
 
307
  if mode == "BM25"
308
  else st.session_state.semantic_results
309
  )
310
+ render_results(results, mode=mode.lower())
311
 
312
  # ─── LLM Tab ──────────────────────────────────────────────────────────────────
313
  with tab_llm:
 
331
  st.markdown("#### 📦 Retrieved Products")
332
  docs = st.session_state.get("llm_docs", [])
333
  if docs:
334
+ docs = [json.loads(json.dumps(obj.metadata, default=str)) for obj in docs]
335
+ render_results(docs, mode='hybrid')
 
 
 
 
336
  else:
337
  st.markdown("<p style='color:#aaa;'>No documents retrieved.</p>", unsafe_allow_html=True)
338
 
src/bm25.py CHANGED
@@ -26,7 +26,7 @@ from langchain_core.documents import Document
26
  ROOT_FOLDER = Path(__file__).resolve().parent.parent
27
 
28
  sys.path.append(str(ROOT_FOLDER))
29
- from src.utils import simple_tokenize
30
  from src.eda_helpers import get_best_reviews
31
 
32
 
@@ -162,22 +162,6 @@ Details:
162
  Top Reviews (showing {n_reviews}):
163
  {review_lines}"""
164
 
165
-
166
- def _extract_image_url(images: Any) -> str:
167
- """
168
- Extract the best available image URL from the images field.
169
- The field is a dict with keys: thumb, large, hi_res, variant — each a list.
170
- Prefers 'large', falls back to 'thumb', then 'hi_res'. Returns "" if none found.
171
- """
172
- if not images or not isinstance(images, dict):
173
- return ""
174
- for key in ("large", "thumb", "hi_res"):
175
- urls = images.get(key)
176
- if isinstance(urls, list) and urls and urls[0]:
177
- return urls[0]
178
- return ""
179
-
180
-
181
  def build_document(product: dict, top_reviews: list[dict]) -> Document | None:
182
  """
183
  Build one LangChain Document for a single product row from the metadata Dataset.
@@ -201,7 +185,7 @@ def build_document(product: dict, top_reviews: list[dict]) -> Document | None:
201
  "details": details_dict,
202
  "average_rating": product.get("average_rating"),
203
  "rating_number": product.get("rating_number"),
204
- "image_url": _extract_image_url(product.get("images")),
205
  "top_reviews": top_reviews,
206
  }
207
 
@@ -389,33 +373,18 @@ def search(
389
  # Get raw BM25 scores for ALL documents
390
  scores = retriever.vectorizer.get_scores(tokenized_query) # np.ndarray, len = n_docs
391
 
392
- # Get top-k doc indices by score
393
- top_indices = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)[:top_k]
394
-
395
- results = []
396
- for idx in top_indices:
397
- doc = retriever.docs[idx] # retriever.docs holds the original Document list
398
- m = doc.metadata
399
- top_reviews = m.get("top_reviews", [])
400
-
401
- rated = [r["rating"] for r in top_reviews if r.get("rating") is not None]
402
- avg_rating = round(sum(rated) / len(rated), 1) if rated else 0.0
403
-
404
- if top_reviews and top_reviews[0].get("text"):
405
- snippet = top_reviews[0]["text"][:300]
406
- else:
407
- snippet = m.get("description", "")[:300]
408
-
409
- results.append({
410
- "asin": m.get("parent_asin", ""),
411
- "title": m.get("title", ""),
412
- "text": snippet,
413
- "rating": avg_rating,
414
- "score": float(scores[idx]),
415
- "top_reviews": top_reviews,
416
- })
417
-
418
- return results
419
 
420
 
421
  # ── notebook entry point ──────────────────────────────────────────────────────
 
26
  ROOT_FOLDER = Path(__file__).resolve().parent.parent
27
 
28
  sys.path.append(str(ROOT_FOLDER))
29
+ from src.utils import simple_tokenize, extract_image
30
  from src.eda_helpers import get_best_reviews
31
 
32
 
 
162
  Top Reviews (showing {n_reviews}):
163
  {review_lines}"""
164
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  def build_document(product: dict, top_reviews: list[dict]) -> Document | None:
166
  """
167
  Build one LangChain Document for a single product row from the metadata Dataset.
 
185
  "details": details_dict,
186
  "average_rating": product.get("average_rating"),
187
  "rating_number": product.get("rating_number"),
188
+ "image": extract_image(product),
189
  "top_reviews": top_reviews,
190
  }
191
 
 
373
  # Get raw BM25 scores for ALL documents
374
  scores = retriever.vectorizer.get_scores(tokenized_query) # np.ndarray, len = n_docs
375
 
376
+ # Get indices of top-k scores
377
+ top_indices = sorted(
378
+ range(len(scores)),
379
+ key=scores.__getitem__,
380
+ reverse=True
381
+ )[:top_k]
382
+
383
+ # Collect metadata for top results, including score
384
+ return [
385
+ {**retriever.docs[i].metadata, "score": scores[i]}
386
+ for i in top_indices
387
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
 
389
 
390
  # ── notebook entry point ──────────────────────────────────────────────────────
src/rag_pipeline.py CHANGED
@@ -21,8 +21,6 @@ from langchain_core.output_parsers import StrOutputParser
21
  from langchain_core.prompts import ChatPromptTemplate
22
  from langchain_core.runnables import RunnableLambda, RunnablePassthrough
23
  from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
24
- from src.retrieval_helpers import _format_docs
25
-
26
  # ---------------------------------------------------------------------------
27
  # Logging
28
  # ---------------------------------------------------------------------------
@@ -203,7 +201,6 @@ def run_rag(
203
  max_new_tokens: int = DEFAULT_MAX_NEW_TOKENS,
204
  provider: str = "auto",
205
  verbose: bool = False,
206
- hf_dataset = None
207
  ) -> str:
208
  """
209
  Execute a full RAG pipeline and return the model's answer.
@@ -295,10 +292,5 @@ def run_rag(
295
  logger.info("Invoking RAG chain for query: %r", query)
296
  answer: str = rag_chain.invoke(query)
297
  logger.debug("RAG answer: %s", answer)
298
-
299
- if hf_dataset:
300
- docs = _format_docs(retrieved_docs, hf_dataset)
301
- else:
302
- docs = retrieved_docs
303
 
304
- return answer, docs
 
21
  from langchain_core.prompts import ChatPromptTemplate
22
  from langchain_core.runnables import RunnableLambda, RunnablePassthrough
23
  from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
 
 
24
  # ---------------------------------------------------------------------------
25
  # Logging
26
  # ---------------------------------------------------------------------------
 
201
  max_new_tokens: int = DEFAULT_MAX_NEW_TOKENS,
202
  provider: str = "auto",
203
  verbose: bool = False,
 
204
  ) -> str:
205
  """
206
  Execute a full RAG pipeline and return the model's answer.
 
292
  logger.info("Invoking RAG chain for query: %r", query)
293
  answer: str = rag_chain.invoke(query)
294
  logger.debug("RAG answer: %s", answer)
 
 
 
 
 
295
 
296
+ return answer, retrieved_docs
src/semantic.py CHANGED
@@ -17,6 +17,7 @@ Typical usage
17
 
18
  import logging
19
  from typing import Any
 
20
  import torch
21
  import json, os, sys
22
  from pathlib import Path
@@ -31,6 +32,7 @@ ROOT_FOLDER = Path(__file__).resolve().parent.parent
31
 
32
  sys.path.append(str(ROOT_FOLDER))
33
  from src.eda_helpers import get_best_reviews
 
34
 
35
  logger = logging.getLogger(__name__)
36
 
@@ -141,6 +143,8 @@ def create_document(product, reviews: Dataset) -> Document | None:
141
  "rating_number": product.get("rating_number"),
142
  # --- categorical (filterable) ---
143
  "main_category": product.get("main_category", ""),
 
 
144
  "categories": product.get("categories") or [],
145
  # --- free-form (display only; coerce to str for FAISS compatibility) ---
146
  "details": str(product.get("details") or ""),
@@ -257,28 +261,31 @@ def build_and_save_vector_store(
257
  # Search
258
  # ---------------------------------------------------------------------------
259
 
260
- def semantic_search(
261
- query: str,
262
- vector_store: FAISS,
263
- k: int = DEFAULT_TOP_K,
264
- filter = None,
265
- ) -> list[Document]:
266
  """
267
- Run a semantic similarity search against a pre-built *vector_store*.
268
-
269
  Args:
270
- query: Natural-language search query.
271
- vector_store: A FAISS store built with :func:`build_vector_store`.
272
- k: Number of results to return.
273
- filter: Optional metadata filter dict, e.g.
274
- ``{"main_category": "Electronics"}``.
275
-
276
  Returns:
277
- Ordered list of the *k* most relevant Documents.
278
  """
279
  results = vector_store.similarity_search_with_score(query, k=k, filter=filter)
280
- logger.info("'%s' -> %d results", query, len(results))
281
- return results
 
 
 
 
 
 
 
 
 
282
 
283
  # ---------------------------------------------------------------------------
284
  # Read existing vector store
 
17
 
18
  import logging
19
  from typing import Any
20
+
21
  import torch
22
  import json, os, sys
23
  from pathlib import Path
 
32
 
33
  sys.path.append(str(ROOT_FOLDER))
34
  from src.eda_helpers import get_best_reviews
35
+ from src.utils import decode_ratings, extract_image
36
 
37
  logger = logging.getLogger(__name__)
38
 
 
143
  "rating_number": product.get("rating_number"),
144
  # --- categorical (filterable) ---
145
  "main_category": product.get("main_category", ""),
146
+ "title": product.get("title", ""),
147
+ "image": extract_image(product),
148
  "categories": product.get("categories") or [],
149
  # --- free-form (display only; coerce to str for FAISS compatibility) ---
150
  "details": str(product.get("details") or ""),
 
261
  # Search
262
  # ---------------------------------------------------------------------------
263
 
264
+ def enrich_search_results(vector_store, query: str, k: int, filter=None):
 
 
 
 
 
265
  """
266
+ Perform similarity search and enrich results with HuggingFace dataset metadata.
267
+
268
  Args:
269
+ vector_store: LangChain vector store instance
270
+ query: Search query string
271
+ k: Number of results to return
272
+ filter: Filter dict for similarity search
273
+
 
274
  Returns:
275
+ List of enriched metadata objects as dicts
276
  """
277
  results = vector_store.similarity_search_with_score(query, k=k, filter=filter)
278
+
279
+ enriched_results = []
280
+
281
+ for doc, score in results:
282
+ metadata_object = {**doc.metadata} # start with all doc metadata
283
+ metadata_object['score'] = float(score)
284
+ metadata_object['reviews'] = decode_ratings(doc.page_content) or []
285
+
286
+ enriched_results.append(metadata_object)
287
+
288
+ return [json.loads(json.dumps(obj, default=str)) for obj in enriched_results]
289
 
290
  # ---------------------------------------------------------------------------
291
  # Read existing vector store
src/utils.py CHANGED
@@ -1,4 +1,5 @@
1
  import re
 
2
  import nltk
3
  from nltk.corpus import stopwords
4
 
@@ -17,4 +18,53 @@ def simple_tokenize(text):
17
  text = re.sub(r"[^a-z0-9\s]", "", text)
18
  tokens = text.split()
19
  tokens = [t for t in tokens if t not in STOPWORDS]
20
- return tokens
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import re
2
+ import json
3
  import nltk
4
  from nltk.corpus import stopwords
5
 
 
18
  text = re.sub(r"[^a-z0-9\s]", "", text)
19
  tokens = text.split()
20
  tokens = [t for t in tokens if t not in STOPWORDS]
21
+ return tokens
22
+
23
+ def extract_image(row):
24
+ """
25
+ Return the first large image URL from the HF dataset row, or None.
26
+
27
+ Expected structure (adjust key names to match your dataset):
28
+ row["images"] = {"large": ["https://...", ...], ...}
29
+ or a JSON-encoded string of the same shape.
30
+ """
31
+ images = row.get("images")
32
+ if images is None:
33
+ return None
34
+
35
+ # Some datasets store this column as a JSON string
36
+ if isinstance(images, str):
37
+ try:
38
+ images = json.loads(images)
39
+ except json.JSONDecodeError:
40
+ return None
41
+
42
+ if not isinstance(images, dict):
43
+ return None
44
+
45
+ large = images.get("large")
46
+ if large and isinstance(large, list) and len(large) > 0:
47
+ return large[0]
48
+
49
+ return None
50
+
51
+ def decode_ratings(page_content):
52
+ block_pattern = r'\[\d\.0★\].*'
53
+ matches = re.findall(block_pattern, page_content)
54
+ if matches:
55
+ pattern = r'\[(\d\.0)★\]\s*(.*?)\s*—\s*(.*)'
56
+ parsed = []
57
+
58
+ for r in matches[:3]:
59
+ match = re.match(pattern, r)
60
+ if match:
61
+ rating, title, text = match.groups()
62
+ parsed.append({
63
+ 'rating': float(rating),
64
+ 'title': title.strip(),
65
+ 'text': text.strip()
66
+ })
67
+
68
+ return(parsed)
69
+ else:
70
+ return {}