astronolan commited on
Commit
28fcc08
·
1 Parent(s): 14148a2

Added moderation and tutorial

Browse files
Files changed (2) hide show
  1. src/components.py +49 -7
  2. src/services.py +114 -61
src/components.py CHANGED
@@ -488,9 +488,21 @@ def create_header():
488
  dbc.Col([
489
  html.Div([
490
  html.H1("galaxy semantic search", className="galaxy-title text-center mb-1"),
491
- html.P("powered by AION-Search", className="text-center mb-2",
492
- style={"color": "rgba(245, 245, 247, 0.5)", "font-weight": "300",
493
- "font-size": "0.8rem", "letter-spacing": "0.05em"}),
 
 
 
 
 
 
 
 
 
 
 
 
494
  html.Div(id="galaxy-count", className="galaxy-count text-center")
495
  ], className="text-center mb-3")
496
  ])
@@ -533,13 +545,29 @@ def create_search_container():
533
  return dbc.Row([
534
  dbc.Col([
535
  html.Div([
536
- # Info button in top right
537
  html.Div([
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
538
  dbc.Button([
539
  html.I(className="fas fa-info-circle")
540
  ], id="info-button", color="link", size="sm",
541
  className="info-button")
542
- ], style={"position": "absolute", "top": "8px", "right": "8px", "z-index": "1000"}),
 
543
 
544
  # Example search buttons
545
  html.Div([
@@ -886,9 +914,23 @@ def create_info_modal():
886
  html.P("Images are from DESI Legacy Surveys DR10 via the hips2fits service provided by the Strasbourg Astronomical Data Centre (CDS).",
887
  style={"color": "rgba(245, 245, 247, 0.6)", "margin-bottom": "0", "font-size": "0.75rem"})
888
  ]),
889
- dbc.ModalFooter(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
890
  dbc.Button("Close", id="close-info-modal", className="ms-auto")
891
- )
892
  ], id="info-modal", size="lg", is_open=False)
893
 
894
 
 
488
  dbc.Col([
489
  html.Div([
490
  html.H1("galaxy semantic search", className="galaxy-title text-center mb-1"),
491
+ html.P([
492
+ "powered by ",
493
+ html.A(
494
+ "AION-Search",
495
+ href="https://aion-search.github.io/",
496
+ target="_blank",
497
+ rel="noopener noreferrer",
498
+ style={
499
+ "color": "rgba(245, 245, 247, 0.5)",
500
+ "textDecoration": "underline"
501
+ }
502
+ )
503
+ ], className="text-center mb-2",
504
+ style={"color": "rgba(245, 245, 247, 0.5)", "font-weight": "300",
505
+ "font-size": "0.8rem", "letter-spacing": "0.05em"}),
506
  html.Div(id="galaxy-count", className="galaxy-count text-center")
507
  ], className="text-center mb-3")
508
  ])
 
545
  return dbc.Row([
546
  dbc.Col([
547
  html.Div([
548
+ # Info button and tutorial link in top right
549
  html.Div([
550
+ html.A(
551
+ "Tutorial",
552
+ href="https://blog.nolank.ca/aion-search/#best-practices",
553
+ target="_blank",
554
+ rel="noopener noreferrer",
555
+ style={
556
+ "textDecoration": "underline",
557
+ "marginRight": "0.1rem",
558
+ "color": "rgba(245, 245, 247, 0.6)",
559
+ "fontSize": "1.0rem",
560
+ "fontWeight": "600",
561
+ "letterSpacing": "0.03em",
562
+ "transition": "all 0.3s ease"
563
+ }
564
+ ),
565
  dbc.Button([
566
  html.I(className="fas fa-info-circle")
567
  ], id="info-button", color="link", size="sm",
568
  className="info-button")
569
+ ], style={"position": "absolute", "top": "8px", "right": "8px", "z-index": "1000",
570
+ "display": "flex", "alignItems": "center"}),
571
 
572
  # Example search buttons
573
  html.Div([
 
914
  html.P("Images are from DESI Legacy Surveys DR10 via the hips2fits service provided by the Strasbourg Astronomical Data Centre (CDS).",
915
  style={"color": "rgba(245, 245, 247, 0.6)", "margin-bottom": "0", "font-size": "0.75rem"})
916
  ]),
917
+ dbc.ModalFooter([
918
+ html.A(
919
+ dbc.Button([html.I(className="fas fa-file-alt me-2"), "Paper"], color="secondary"),
920
+ href="https://arxiv.org/abs/2512.11982",
921
+ target="_blank",
922
+ rel="noopener noreferrer",
923
+ className="me-2"
924
+ ),
925
+ html.A(
926
+ dbc.Button([html.I(className="fas fa-book me-2"), "Tutorial"], color="secondary"),
927
+ href="https://blog.nolank.ca/aion-search/#best-practices",
928
+ target="_blank",
929
+ rel="noopener noreferrer",
930
+ className="me-2"
931
+ ),
932
  dbc.Button("Close", id="close-info-modal", className="ms-auto")
933
+ ])
934
  ], id="info-modal", size="lg", is_open=False)
935
 
936
 
src/services.py CHANGED
@@ -185,6 +185,29 @@ class EmbeddingService:
185
  self.openai_client = OpenAI(api_key=OPENAI_API_KEY)
186
  return self.openai_client
187
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  def encode_text_query(self, query: str) -> np.ndarray:
189
  """Encode text query using OpenAI embeddings + CLIP text projector.
190
 
@@ -194,6 +217,10 @@ class EmbeddingService:
194
  Returns:
195
  CLIP embedding vector
196
  """
 
 
 
 
197
  client = self._get_openai_client()
198
 
199
  # Get OpenAI text embedding
@@ -220,6 +247,11 @@ class EmbeddingService:
220
  Returns:
221
  Combined normalized embedding vector
222
  """
 
 
 
 
 
223
  client = self._get_openai_client()
224
 
225
  # Get all embeddings at once for efficiency
@@ -426,14 +458,21 @@ class SearchService:
426
  Returns:
427
  DataFrame with search results
428
  """
429
- # Encode query
430
- query_embedding = self.embedding_service.encode_text_query(query)
431
-
432
- # Build filter
433
- filter_expr = self._build_rmag_filter(rmag_min, rmag_max)
434
-
435
- # Search Zilliz
436
- return self.zilliz_service.search(query_embedding, top_k, filter_expr)
 
 
 
 
 
 
 
437
 
438
  def search_vector(
439
  self,
@@ -455,14 +494,21 @@ class SearchService:
455
  Returns:
456
  DataFrame with search results
457
  """
458
- # Encode and combine vectors
459
- combined_embedding = self.embedding_service.encode_vector_queries(queries, operations)
460
-
461
- # Build filter
462
- filter_expr = self._build_rmag_filter(rmag_min, rmag_max)
463
-
464
- # Search Zilliz
465
- return self.zilliz_service.search(combined_embedding, top_k, filter_expr)
 
 
 
 
 
 
 
466
 
467
  def search_advanced(
468
  self,
@@ -488,51 +534,58 @@ class SearchService:
488
  Returns:
489
  DataFrame with search results
490
  """
491
- combined_embedding = None
492
-
493
- # Process text queries
494
- if text_queries and len(text_queries) > 0:
495
- for query, weight in zip(text_queries, text_weights):
496
- query_embedding = self.embedding_service.encode_text_query(query)
497
-
498
- # Apply weight
499
- weighted_embedding = query_embedding * weight
500
-
501
- if combined_embedding is None:
502
- combined_embedding = weighted_embedding
503
- else:
504
- combined_embedding += weighted_embedding
505
-
506
- # Process image queries
507
- if image_queries and len(image_queries) > 0:
508
- if self.image_service is None:
509
- raise RuntimeError("Image service not initialized")
510
-
511
- for img_query, weight in zip(image_queries, image_weights):
512
- # Encode image
513
- image_embedding = self.image_service.encode_image(
514
- ra=img_query['ra'],
515
- dec=img_query['dec'],
516
- fov=img_query.get('fov', 0.025),
517
- size=256
518
- )
519
-
520
- # Apply weight
521
- weighted_embedding = image_embedding * weight
522
-
523
- if combined_embedding is None:
524
- combined_embedding = weighted_embedding
525
- else:
526
- combined_embedding += weighted_embedding
527
 
528
- # Normalize the final combined embedding
529
- if combined_embedding is not None:
530
- norm = np.linalg.norm(combined_embedding)
531
- if norm > 0:
532
- combined_embedding = combined_embedding / norm
533
 
534
- # Build filter
535
- filter_expr = self._build_rmag_filter(rmag_min, rmag_max)
536
 
537
- # Search Zilliz
538
- return self.zilliz_service.search(combined_embedding, top_k, filter_expr)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  self.openai_client = OpenAI(api_key=OPENAI_API_KEY)
186
  return self.openai_client
187
 
188
+ def _moderate_content(self, text: str) -> bool:
189
+ """Check if text content is appropriate using OpenAI Moderation API.
190
+
191
+ Args:
192
+ text: Text to moderate
193
+
194
+ Returns:
195
+ True if content is safe, False if flagged
196
+ """
197
+ try:
198
+ client = self._get_openai_client()
199
+ response = client.moderations.create(input=text)
200
+
201
+ # If any category is flagged, reject the content
202
+ if response.results[0].flagged:
203
+ logger.warning(f"Content moderation flagged input")
204
+ return False
205
+ return True
206
+ except Exception as e:
207
+ logger.error(f"Moderation API error: {e}")
208
+ # On error, allow the content through (fail open)
209
+ return True
210
+
211
  def encode_text_query(self, query: str) -> np.ndarray:
212
  """Encode text query using OpenAI embeddings + CLIP text projector.
213
 
 
217
  Returns:
218
  CLIP embedding vector
219
  """
220
+ # Moderate content first
221
+ if not self._moderate_content(query):
222
+ raise ValueError("Content moderation filter triggered")
223
+
224
  client = self._get_openai_client()
225
 
226
  # Get OpenAI text embedding
 
247
  Returns:
248
  Combined normalized embedding vector
249
  """
250
+ # Moderate all queries first
251
+ for query in queries:
252
+ if not self._moderate_content(query):
253
+ raise ValueError("Content moderation filter triggered")
254
+
255
  client = self._get_openai_client()
256
 
257
  # Get all embeddings at once for efficiency
 
458
  Returns:
459
  DataFrame with search results
460
  """
461
+ try:
462
+ # Encode query
463
+ query_embedding = self.embedding_service.encode_text_query(query)
464
+
465
+ # Build filter
466
+ filter_expr = self._build_rmag_filter(rmag_min, rmag_max)
467
+
468
+ # Search Zilliz
469
+ return self.zilliz_service.search(query_embedding, top_k, filter_expr)
470
+ except ValueError as e:
471
+ # Content moderation triggered - return empty results silently
472
+ if "moderation" in str(e).lower():
473
+ logger.info("Search blocked by content moderation")
474
+ return pd.DataFrame()
475
+ raise
476
 
477
  def search_vector(
478
  self,
 
494
  Returns:
495
  DataFrame with search results
496
  """
497
+ try:
498
+ # Encode and combine vectors
499
+ combined_embedding = self.embedding_service.encode_vector_queries(queries, operations)
500
+
501
+ # Build filter
502
+ filter_expr = self._build_rmag_filter(rmag_min, rmag_max)
503
+
504
+ # Search Zilliz
505
+ return self.zilliz_service.search(combined_embedding, top_k, filter_expr)
506
+ except ValueError as e:
507
+ # Content moderation triggered - return empty results silently
508
+ if "moderation" in str(e).lower():
509
+ logger.info("Search blocked by content moderation")
510
+ return pd.DataFrame()
511
+ raise
512
 
513
  def search_advanced(
514
  self,
 
534
  Returns:
535
  DataFrame with search results
536
  """
537
+ try:
538
+ combined_embedding = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
539
 
540
+ # Process text queries
541
+ if text_queries and len(text_queries) > 0:
542
+ for query, weight in zip(text_queries, text_weights):
543
+ query_embedding = self.embedding_service.encode_text_query(query)
 
544
 
545
+ # Apply weight
546
+ weighted_embedding = query_embedding * weight
547
 
548
+ if combined_embedding is None:
549
+ combined_embedding = weighted_embedding
550
+ else:
551
+ combined_embedding += weighted_embedding
552
+
553
+ # Process image queries
554
+ if image_queries and len(image_queries) > 0:
555
+ if self.image_service is None:
556
+ raise RuntimeError("Image service not initialized")
557
+
558
+ for img_query, weight in zip(image_queries, image_weights):
559
+ # Encode image
560
+ image_embedding = self.image_service.encode_image(
561
+ ra=img_query['ra'],
562
+ dec=img_query['dec'],
563
+ fov=img_query.get('fov', 0.025),
564
+ size=256
565
+ )
566
+
567
+ # Apply weight
568
+ weighted_embedding = image_embedding * weight
569
+
570
+ if combined_embedding is None:
571
+ combined_embedding = weighted_embedding
572
+ else:
573
+ combined_embedding += weighted_embedding
574
+
575
+ # Normalize the final combined embedding
576
+ if combined_embedding is not None:
577
+ norm = np.linalg.norm(combined_embedding)
578
+ if norm > 0:
579
+ combined_embedding = combined_embedding / norm
580
+
581
+ # Build filter
582
+ filter_expr = self._build_rmag_filter(rmag_min, rmag_max)
583
+
584
+ # Search Zilliz
585
+ return self.zilliz_service.search(combined_embedding, top_k, filter_expr)
586
+ except ValueError as e:
587
+ # Content moderation triggered - return empty results silently
588
+ if "moderation" in str(e).lower():
589
+ logger.info("Search blocked by content moderation")
590
+ return pd.DataFrame()
591
+ raise