Spaces:
Running
Running
Commit
Β·
fa7eb7f
1
Parent(s):
0d23870
Enhance search functionality and UI components
Browse files- Updated example queries for better clarity and relevance
- Added a "Copy link" button for search results and galaxy details
- Implemented URL state management for shareable search links
- src/callbacks.py +549 -28
- src/components.py +22 -6
- src/config.py +7 -0
- src/hf_logging.py +83 -0
- src/url_state.py +206 -0
- src/utils.py +137 -39
src/callbacks.py
CHANGED
|
@@ -4,9 +4,10 @@ import json
|
|
| 4 |
import time
|
| 5 |
import logging
|
| 6 |
import traceback
|
|
|
|
| 7 |
import pandas as pd
|
| 8 |
import dash
|
| 9 |
-
from dash import Input, Output, State, callback_context, html
|
| 10 |
import dash_bootstrap_components as dbc
|
| 11 |
|
| 12 |
import src.config as config
|
|
@@ -184,12 +185,12 @@ def register_callbacks(app, search_service: SearchService):
|
|
| 184 |
button_id = ctx.triggered[0]["prop_id"].split(".")[0]
|
| 185 |
|
| 186 |
example_queries = {
|
| 187 |
-
"example-1": "
|
| 188 |
"example-2": "A peculiar interacting galaxy system featuring plenty of tidal tails and a disturbed morphology",
|
| 189 |
"example-3": "galaxy with stream",
|
| 190 |
"example-5": "A violent merger in progress with visible tidal features",
|
| 191 |
"example-6": "Low surface brightness",
|
| 192 |
-
"example-7": "face-on ring
|
| 193 |
"example-8": "a bursty, star forming galaxy"
|
| 194 |
}
|
| 195 |
|
|
@@ -205,7 +206,8 @@ def register_callbacks(app, search_service: SearchService):
|
|
| 205 |
Output("search-results", "children"),
|
| 206 |
Output("search-data", "data"),
|
| 207 |
Output("download-button", "disabled"),
|
| 208 |
-
Output("download-button-advanced", "disabled")
|
|
|
|
| 209 |
[Input("search-button", "n_clicks"),
|
| 210 |
Input("search-input", "n_submit"),
|
| 211 |
Input("search-button-advanced", "n_clicks")],
|
|
@@ -246,7 +248,10 @@ def register_callbacks(app, search_service: SearchService):
|
|
| 246 |
# Otherwise perform basic search
|
| 247 |
query = query_basic
|
| 248 |
if not query or not query.strip():
|
| 249 |
-
return "", dbc.Alert("Please enter a search query", color="warning"), None, True, True
|
|
|
|
|
|
|
|
|
|
| 250 |
|
| 251 |
try:
|
| 252 |
# Extract min and max from slider range
|
|
@@ -264,13 +269,13 @@ def register_callbacks(app, search_service: SearchService):
|
|
| 264 |
rmag_min=rmag_min,
|
| 265 |
rmag_max=rmag_max
|
| 266 |
)
|
| 267 |
-
log_query_to_csv(query_xml)
|
| 268 |
|
| 269 |
# Build results grid - only load first 60 images
|
| 270 |
grid_items = build_galaxy_grid(df.head(DEFAULT_DISPLAY_COUNT))
|
| 271 |
|
| 272 |
# Prepare data for store
|
| 273 |
-
search_data = prepare_search_data(df, query)
|
| 274 |
|
| 275 |
# Create load more button
|
| 276 |
load_more_button = create_load_more_button(len(df), DEFAULT_DISPLAY_COUNT) if len(df) > DEFAULT_DISPLAY_COUNT else None
|
|
@@ -282,8 +287,19 @@ def register_callbacks(app, search_service: SearchService):
|
|
| 282 |
|
| 283 |
# Build complete results container
|
| 284 |
results_container = html.Div([
|
| 285 |
-
html.
|
| 286 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
html.P(f"'{query}'{filter_desc}",
|
| 288 |
className="text-center mb-3",
|
| 289 |
style={"color": "rgba(245, 245, 247, 0.6)", "font-size": "0.9rem"}),
|
|
@@ -291,20 +307,51 @@ def register_callbacks(app, search_service: SearchService):
|
|
| 291 |
load_more_button
|
| 292 |
])
|
| 293 |
|
| 294 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
|
| 296 |
except Exception as e:
|
| 297 |
error_msg = dbc.Alert(f"Search failed: {str(e)}", color="danger")
|
| 298 |
logger.error(f"Search error: {e}")
|
| 299 |
logger.error(f"Full traceback:\n{traceback.format_exc()}")
|
| 300 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
|
| 302 |
@app.callback(
|
| 303 |
[Output("galaxy-modal", "is_open"),
|
| 304 |
Output("modal-title", "children"),
|
| 305 |
Output("modal-image", "children"),
|
| 306 |
Output("modal-description", "children"),
|
| 307 |
-
Output("current-galaxy-data", "data")
|
|
|
|
| 308 |
[Input({"type": "galaxy-image", "index": dash.dependencies.ALL}, "n_clicks"),
|
| 309 |
Input("close-modal", "n_clicks")],
|
| 310 |
[State("galaxy-modal", "is_open"),
|
|
@@ -316,17 +363,17 @@ def register_callbacks(app, search_service: SearchService):
|
|
| 316 |
ctx = callback_context
|
| 317 |
|
| 318 |
if not ctx.triggered:
|
| 319 |
-
return False, "", "", "", None
|
| 320 |
|
| 321 |
if ctx.triggered[0]["prop_id"] == "close-modal.n_clicks":
|
| 322 |
-
return False, "", "", "", None
|
| 323 |
|
| 324 |
if search_data:
|
| 325 |
triggered_prop = ctx.triggered[0]["prop_id"]
|
| 326 |
triggered_value = ctx.triggered[0]["value"]
|
| 327 |
|
| 328 |
if triggered_value is None or triggered_value == 0:
|
| 329 |
-
return False, "", "", "", None
|
| 330 |
|
| 331 |
if "galaxy-image" in triggered_prop:
|
| 332 |
try:
|
|
@@ -345,17 +392,101 @@ def register_callbacks(app, search_service: SearchService):
|
|
| 345 |
"r_mag": galaxy_info["r_mag"]
|
| 346 |
}
|
| 347 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
return (
|
| 349 |
True,
|
| 350 |
f"Galaxy at RA={galaxy_info['ra']:.6f}, Dec={galaxy_info['dec']:.6f}",
|
| 351 |
image_element,
|
| 352 |
description_element,
|
| 353 |
-
galaxy_data
|
|
|
|
| 354 |
)
|
| 355 |
except:
|
| 356 |
pass
|
| 357 |
|
| 358 |
-
return False, "", "", "", None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
|
| 360 |
@app.callback(
|
| 361 |
Output("info-modal", "is_open"),
|
|
@@ -401,8 +532,19 @@ def register_callbacks(app, search_service: SearchService):
|
|
| 401 |
load_more_button = create_load_more_button(total_count, next_count) if next_count < total_count else None
|
| 402 |
|
| 403 |
results_container = html.Div([
|
| 404 |
-
html.
|
| 405 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
html.P(f"'{search_data['query']}'",
|
| 407 |
className="text-center mb-3",
|
| 408 |
style={"color": "rgba(245, 245, 247, 0.6)", "font-size": "0.9rem"}),
|
|
@@ -492,6 +634,9 @@ def register_callbacks(app, search_service: SearchService):
|
|
| 492 |
additional_ra_values, additional_dec_values,
|
| 493 |
additional_operations):
|
| 494 |
"""Perform advanced search combining main query with additional queries."""
|
|
|
|
|
|
|
|
|
|
| 495 |
def operation_to_weight(op_str):
|
| 496 |
"""Convert operation string to float weight."""
|
| 497 |
if op_str == "+":
|
|
@@ -557,7 +702,7 @@ def register_callbacks(app, search_service: SearchService):
|
|
| 557 |
|
| 558 |
# Validate that we have at least one query
|
| 559 |
if not text_queries and not image_queries:
|
| 560 |
-
return "", dbc.Alert("Please enter at least one text or image query", color="warning"), None, True, True
|
| 561 |
|
| 562 |
try:
|
| 563 |
# Extract min and max from slider range
|
|
@@ -585,7 +730,7 @@ def register_callbacks(app, search_service: SearchService):
|
|
| 585 |
rmag_min=rmag_min,
|
| 586 |
rmag_max=rmag_max
|
| 587 |
)
|
| 588 |
-
log_query_to_csv(query_xml)
|
| 589 |
|
| 590 |
# Build results grid
|
| 591 |
grid_items = build_galaxy_grid(df.head(DEFAULT_DISPLAY_COUNT))
|
|
@@ -637,7 +782,7 @@ def register_callbacks(app, search_service: SearchService):
|
|
| 637 |
filter_desc = f" + r-mag: [{rmag_min:.1f}, {rmag_max:.1f}]"
|
| 638 |
|
| 639 |
# Prepare data for store
|
| 640 |
-
search_data = prepare_search_data(df, query_description, is_vector_search=True)
|
| 641 |
search_data["text_queries"] = text_queries
|
| 642 |
search_data["text_weights"] = text_weights
|
| 643 |
search_data["image_queries"] = image_queries
|
|
@@ -648,8 +793,19 @@ def register_callbacks(app, search_service: SearchService):
|
|
| 648 |
|
| 649 |
# Build results container
|
| 650 |
results_container = html.Div([
|
| 651 |
-
html.
|
| 652 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 653 |
html.P(
|
| 654 |
query_display_parts + ([f"{filter_desc}"] if filter_desc else []),
|
| 655 |
className="text-center mb-3",
|
|
@@ -659,17 +815,378 @@ def register_callbacks(app, search_service: SearchService):
|
|
| 659 |
load_more_button
|
| 660 |
])
|
| 661 |
|
| 662 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 663 |
|
| 664 |
except Exception as e:
|
| 665 |
error_msg = dbc.Alert(f"Advanced search failed: {str(e)}", color="danger")
|
| 666 |
logger.error(f"Advanced search error: {e}")
|
| 667 |
logger.error(f"Full traceback:\n{traceback.format_exc()}")
|
| 668 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 669 |
|
| 670 |
|
| 671 |
# Helper functions for callbacks
|
| 672 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 673 |
def build_galaxy_grid(df: pd.DataFrame) -> list:
|
| 674 |
"""Build galaxy grid items from DataFrame.
|
| 675 |
|
|
@@ -731,18 +1248,19 @@ def build_galaxy_card(galaxy_info: dict, index: int):
|
|
| 731 |
], width=6, md=4, lg=2, className="mb-2 px-1")
|
| 732 |
|
| 733 |
|
| 734 |
-
def prepare_search_data(df: pd.DataFrame, query: str, is_vector_search: bool = False) -> dict:
|
| 735 |
"""Prepare search data for storage.
|
| 736 |
|
| 737 |
Args:
|
| 738 |
df: DataFrame with search results
|
| 739 |
query: Search query string
|
| 740 |
is_vector_search: Whether this is a vector search
|
|
|
|
| 741 |
|
| 742 |
Returns:
|
| 743 |
Dictionary with search data
|
| 744 |
"""
|
| 745 |
-
|
| 746 |
ZILLIZ_PRIMARY_KEY: df[ZILLIZ_PRIMARY_KEY].tolist(),
|
| 747 |
"ra": df['ra'].tolist(),
|
| 748 |
"dec": df['dec'].tolist(),
|
|
@@ -753,6 +1271,9 @@ def prepare_search_data(df: pd.DataFrame, query: str, is_vector_search: bool = F
|
|
| 753 |
"query": query,
|
| 754 |
"is_vector_search": is_vector_search
|
| 755 |
}
|
|
|
|
|
|
|
|
|
|
| 756 |
|
| 757 |
|
| 758 |
def extract_galaxy_info(search_data: dict, index: int) -> dict:
|
|
|
|
| 4 |
import time
|
| 5 |
import logging
|
| 6 |
import traceback
|
| 7 |
+
import uuid
|
| 8 |
import pandas as pd
|
| 9 |
import dash
|
| 10 |
+
from dash import Input, Output, State, callback_context, html, dcc
|
| 11 |
import dash_bootstrap_components as dbc
|
| 12 |
|
| 13 |
import src.config as config
|
|
|
|
| 185 |
button_id = ctx.triggered[0]["prop_id"].split(".")[0]
|
| 186 |
|
| 187 |
example_queries = {
|
| 188 |
+
"example-1": "Two edge-on galaxies",
|
| 189 |
"example-2": "A peculiar interacting galaxy system featuring plenty of tidal tails and a disturbed morphology",
|
| 190 |
"example-3": "galaxy with stream",
|
| 191 |
"example-5": "A violent merger in progress with visible tidal features",
|
| 192 |
"example-6": "Low surface brightness",
|
| 193 |
+
"example-7": "A face-on spiral with ring-like circular structure around a core",
|
| 194 |
"example-8": "a bursty, star forming galaxy"
|
| 195 |
}
|
| 196 |
|
|
|
|
| 206 |
Output("search-results", "children"),
|
| 207 |
Output("search-data", "data"),
|
| 208 |
Output("download-button", "disabled"),
|
| 209 |
+
Output("download-button-advanced", "disabled"),
|
| 210 |
+
Output("current-search-params", "data")],
|
| 211 |
[Input("search-button", "n_clicks"),
|
| 212 |
Input("search-input", "n_submit"),
|
| 213 |
Input("search-button-advanced", "n_clicks")],
|
|
|
|
| 248 |
# Otherwise perform basic search
|
| 249 |
query = query_basic
|
| 250 |
if not query or not query.strip():
|
| 251 |
+
return "", dbc.Alert("Please enter a search query", color="warning"), None, True, True, None
|
| 252 |
+
|
| 253 |
+
# Generate unique request_id for this search
|
| 254 |
+
request_id = uuid.uuid4().hex
|
| 255 |
|
| 256 |
try:
|
| 257 |
# Extract min and max from slider range
|
|
|
|
| 269 |
rmag_min=rmag_min,
|
| 270 |
rmag_max=rmag_max
|
| 271 |
)
|
| 272 |
+
log_query_to_csv(query_xml, request_id=request_id)
|
| 273 |
|
| 274 |
# Build results grid - only load first 60 images
|
| 275 |
grid_items = build_galaxy_grid(df.head(DEFAULT_DISPLAY_COUNT))
|
| 276 |
|
| 277 |
# Prepare data for store
|
| 278 |
+
search_data = prepare_search_data(df, query, request_id=request_id)
|
| 279 |
|
| 280 |
# Create load more button
|
| 281 |
load_more_button = create_load_more_button(len(df), DEFAULT_DISPLAY_COUNT) if len(df) > DEFAULT_DISPLAY_COUNT else None
|
|
|
|
| 287 |
|
| 288 |
# Build complete results container
|
| 289 |
results_container = html.Div([
|
| 290 |
+
html.Div([
|
| 291 |
+
html.P(f"Top {len(df)} matching galaxies (showing {min(DEFAULT_DISPLAY_COUNT, len(df))})",
|
| 292 |
+
className="results-header mb-2 text-center d-inline-block me-2"),
|
| 293 |
+
dbc.Button(
|
| 294 |
+
[html.I(className="fas fa-link me-1"), "Copy link"],
|
| 295 |
+
id="copy-results-link",
|
| 296 |
+
color="link",
|
| 297 |
+
size="sm",
|
| 298 |
+
n_clicks=0,
|
| 299 |
+
className="info-button"
|
| 300 |
+
),
|
| 301 |
+
html.Span(id="copy-results-feedback", style={"marginLeft": "8px", "color": "#28a745", "fontSize": "0.8rem"})
|
| 302 |
+
], className="text-center"),
|
| 303 |
html.P(f"'{query}'{filter_desc}",
|
| 304 |
className="text-center mb-3",
|
| 305 |
style={"color": "rgba(245, 245, 247, 0.6)", "font-size": "0.9rem"}),
|
|
|
|
| 307 |
load_more_button
|
| 308 |
])
|
| 309 |
|
| 310 |
+
# Store search params for URL generation
|
| 311 |
+
search_params = {
|
| 312 |
+
"text_queries": [query],
|
| 313 |
+
"text_weights": [1.0],
|
| 314 |
+
"image_queries": [],
|
| 315 |
+
"image_weights": [],
|
| 316 |
+
"rmag_min": rmag_min,
|
| 317 |
+
"rmag_max": rmag_max
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
return "", results_container, search_data, False, False, search_params
|
| 321 |
|
| 322 |
except Exception as e:
|
| 323 |
error_msg = dbc.Alert(f"Search failed: {str(e)}", color="danger")
|
| 324 |
logger.error(f"Search error: {e}")
|
| 325 |
logger.error(f"Full traceback:\n{traceback.format_exc()}")
|
| 326 |
+
|
| 327 |
+
# Log error
|
| 328 |
+
from src.utils import build_query_xml, log_query_to_csv
|
| 329 |
+
try:
|
| 330 |
+
query_xml = build_query_xml(
|
| 331 |
+
text_queries=[query],
|
| 332 |
+
text_weights=[1.0],
|
| 333 |
+
rmag_min=rmag_range[0] if rmag_range else None,
|
| 334 |
+
rmag_max=rmag_range[1] if rmag_range else None
|
| 335 |
+
)
|
| 336 |
+
log_query_to_csv(
|
| 337 |
+
query_xml,
|
| 338 |
+
request_id=request_id,
|
| 339 |
+
error_occurred=True,
|
| 340 |
+
error_message=str(e),
|
| 341 |
+
error_type=type(e).__name__
|
| 342 |
+
)
|
| 343 |
+
except:
|
| 344 |
+
pass
|
| 345 |
+
|
| 346 |
+
return "", error_msg, None, True, True, None
|
| 347 |
|
| 348 |
@app.callback(
|
| 349 |
[Output("galaxy-modal", "is_open"),
|
| 350 |
Output("modal-title", "children"),
|
| 351 |
Output("modal-image", "children"),
|
| 352 |
Output("modal-description", "children"),
|
| 353 |
+
Output("current-galaxy-data", "data"),
|
| 354 |
+
Output("copy-galaxy-feedback", "children", allow_duplicate=True)],
|
| 355 |
[Input({"type": "galaxy-image", "index": dash.dependencies.ALL}, "n_clicks"),
|
| 356 |
Input("close-modal", "n_clicks")],
|
| 357 |
[State("galaxy-modal", "is_open"),
|
|
|
|
| 363 |
ctx = callback_context
|
| 364 |
|
| 365 |
if not ctx.triggered:
|
| 366 |
+
return False, "", "", "", None, ""
|
| 367 |
|
| 368 |
if ctx.triggered[0]["prop_id"] == "close-modal.n_clicks":
|
| 369 |
+
return False, "", "", "", None, ""
|
| 370 |
|
| 371 |
if search_data:
|
| 372 |
triggered_prop = ctx.triggered[0]["prop_id"]
|
| 373 |
triggered_value = ctx.triggered[0]["value"]
|
| 374 |
|
| 375 |
if triggered_value is None or triggered_value == 0:
|
| 376 |
+
return False, "", "", "", None, ""
|
| 377 |
|
| 378 |
if "galaxy-image" in triggered_prop:
|
| 379 |
try:
|
|
|
|
| 392 |
"r_mag": galaxy_info["r_mag"]
|
| 393 |
}
|
| 394 |
|
| 395 |
+
# Log click event
|
| 396 |
+
from src.utils import log_click_event
|
| 397 |
+
request_id = search_data.get("request_id")
|
| 398 |
+
log_click_event(
|
| 399 |
+
request_id=request_id,
|
| 400 |
+
rank=clicked_idx, # 0-indexed rank
|
| 401 |
+
primary_key=galaxy_info[ZILLIZ_PRIMARY_KEY],
|
| 402 |
+
ra=galaxy_info["ra"],
|
| 403 |
+
dec=galaxy_info["dec"],
|
| 404 |
+
r_mag=galaxy_info["r_mag"],
|
| 405 |
+
distance=galaxy_info["distance"]
|
| 406 |
+
)
|
| 407 |
+
|
| 408 |
return (
|
| 409 |
True,
|
| 410 |
f"Galaxy at RA={galaxy_info['ra']:.6f}, Dec={galaxy_info['dec']:.6f}",
|
| 411 |
image_element,
|
| 412 |
description_element,
|
| 413 |
+
galaxy_data,
|
| 414 |
+
"" # Clear copy feedback when opening new galaxy
|
| 415 |
)
|
| 416 |
except:
|
| 417 |
pass
|
| 418 |
|
| 419 |
+
return False, "", "", "", None, ""
|
| 420 |
+
|
| 421 |
+
@app.callback(
|
| 422 |
+
[Output("galaxy-modal", "is_open", allow_duplicate=True),
|
| 423 |
+
Output("modal-title", "children", allow_duplicate=True),
|
| 424 |
+
Output("modal-image", "children", allow_duplicate=True),
|
| 425 |
+
Output("modal-description", "children", allow_duplicate=True),
|
| 426 |
+
Output("current-galaxy-data", "data", allow_duplicate=True),
|
| 427 |
+
Output("pending-expand-galaxy", "data", allow_duplicate=True),
|
| 428 |
+
Output("copy-galaxy-feedback", "children", allow_duplicate=True)],
|
| 429 |
+
[Input("search-data", "data")],
|
| 430 |
+
[State("pending-expand-galaxy", "data")],
|
| 431 |
+
prevent_initial_call=True
|
| 432 |
+
)
|
| 433 |
+
def expand_galaxy_from_url(search_data, pending_galaxy_id):
|
| 434 |
+
"""Expand galaxy modal if there's a pending galaxy from URL state."""
|
| 435 |
+
if not pending_galaxy_id or not search_data:
|
| 436 |
+
return dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update
|
| 437 |
+
|
| 438 |
+
# Find the galaxy by ID in search results
|
| 439 |
+
primary_keys = search_data.get(ZILLIZ_PRIMARY_KEY, [])
|
| 440 |
+
|
| 441 |
+
# Handle potential bytes encoding issue - try to match with and without b'' wrapper
|
| 442 |
+
target_id = pending_galaxy_id
|
| 443 |
+
# Clean up the ID if it has bytes notation
|
| 444 |
+
if isinstance(target_id, str) and target_id.startswith("b'") and target_id.endswith("'"):
|
| 445 |
+
target_id = target_id[2:-1]
|
| 446 |
+
|
| 447 |
+
try:
|
| 448 |
+
# Try to find exact match first
|
| 449 |
+
if target_id in primary_keys:
|
| 450 |
+
idx = primary_keys.index(target_id)
|
| 451 |
+
else:
|
| 452 |
+
# Try cleaning primary keys too
|
| 453 |
+
cleaned_keys = []
|
| 454 |
+
for pk in primary_keys:
|
| 455 |
+
if isinstance(pk, str) and pk.startswith("b'") and pk.endswith("'"):
|
| 456 |
+
cleaned_keys.append(pk[2:-1])
|
| 457 |
+
else:
|
| 458 |
+
cleaned_keys.append(pk)
|
| 459 |
+
|
| 460 |
+
if target_id in cleaned_keys:
|
| 461 |
+
idx = cleaned_keys.index(target_id)
|
| 462 |
+
else:
|
| 463 |
+
# Galaxy not found in results
|
| 464 |
+
return dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, None, dash.no_update
|
| 465 |
+
|
| 466 |
+
# Extract galaxy info and build modal content
|
| 467 |
+
galaxy_info = extract_galaxy_info(search_data, idx)
|
| 468 |
+
image_element, description_element = build_modal_content(galaxy_info)
|
| 469 |
+
|
| 470 |
+
galaxy_data = {
|
| 471 |
+
ZILLIZ_PRIMARY_KEY: galaxy_info[ZILLIZ_PRIMARY_KEY],
|
| 472 |
+
"ra": galaxy_info["ra"],
|
| 473 |
+
"dec": galaxy_info["dec"],
|
| 474 |
+
"distance": galaxy_info["distance"],
|
| 475 |
+
"r_mag": galaxy_info["r_mag"]
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
return (
|
| 479 |
+
True, # Open modal
|
| 480 |
+
f"Galaxy at RA={galaxy_info['ra']:.6f}, Dec={galaxy_info['dec']:.6f}",
|
| 481 |
+
image_element,
|
| 482 |
+
description_element,
|
| 483 |
+
galaxy_data,
|
| 484 |
+
None, # Clear pending galaxy
|
| 485 |
+
"" # Clear copy feedback when opening galaxy from URL
|
| 486 |
+
)
|
| 487 |
+
except (ValueError, IndexError):
|
| 488 |
+
# Galaxy not found, clear the pending state
|
| 489 |
+
return dash.no_update, dash.no_update, dash.no_update, dash.no_update, dash.no_update, None, dash.no_update
|
| 490 |
|
| 491 |
@app.callback(
|
| 492 |
Output("info-modal", "is_open"),
|
|
|
|
| 532 |
load_more_button = create_load_more_button(total_count, next_count) if next_count < total_count else None
|
| 533 |
|
| 534 |
results_container = html.Div([
|
| 535 |
+
html.Div([
|
| 536 |
+
html.P(f"Top {total_count} matching galaxies (showing {next_count})",
|
| 537 |
+
className="results-header mb-2 text-center d-inline-block me-2"),
|
| 538 |
+
dbc.Button(
|
| 539 |
+
[html.I(className="fas fa-link me-1"), "Copy link"],
|
| 540 |
+
id="copy-results-link",
|
| 541 |
+
color="link",
|
| 542 |
+
size="sm",
|
| 543 |
+
n_clicks=0,
|
| 544 |
+
className="info-button"
|
| 545 |
+
),
|
| 546 |
+
html.Span(id="copy-results-feedback", style={"marginLeft": "8px", "color": "#28a745", "fontSize": "0.8rem"})
|
| 547 |
+
], className="text-center"),
|
| 548 |
html.P(f"'{search_data['query']}'",
|
| 549 |
className="text-center mb-3",
|
| 550 |
style={"color": "rgba(245, 245, 247, 0.6)", "font-size": "0.9rem"}),
|
|
|
|
| 634 |
additional_ra_values, additional_dec_values,
|
| 635 |
additional_operations):
|
| 636 |
"""Perform advanced search combining main query with additional queries."""
|
| 637 |
+
# Generate unique request_id for this search
|
| 638 |
+
request_id = uuid.uuid4().hex
|
| 639 |
+
|
| 640 |
def operation_to_weight(op_str):
|
| 641 |
"""Convert operation string to float weight."""
|
| 642 |
if op_str == "+":
|
|
|
|
| 702 |
|
| 703 |
# Validate that we have at least one query
|
| 704 |
if not text_queries and not image_queries:
|
| 705 |
+
return "", dbc.Alert("Please enter at least one text or image query", color="warning"), None, True, True, None
|
| 706 |
|
| 707 |
try:
|
| 708 |
# Extract min and max from slider range
|
|
|
|
| 730 |
rmag_min=rmag_min,
|
| 731 |
rmag_max=rmag_max
|
| 732 |
)
|
| 733 |
+
log_query_to_csv(query_xml, request_id=request_id)
|
| 734 |
|
| 735 |
# Build results grid
|
| 736 |
grid_items = build_galaxy_grid(df.head(DEFAULT_DISPLAY_COUNT))
|
|
|
|
| 782 |
filter_desc = f" + r-mag: [{rmag_min:.1f}, {rmag_max:.1f}]"
|
| 783 |
|
| 784 |
# Prepare data for store
|
| 785 |
+
search_data = prepare_search_data(df, query_description, is_vector_search=True, request_id=request_id)
|
| 786 |
search_data["text_queries"] = text_queries
|
| 787 |
search_data["text_weights"] = text_weights
|
| 788 |
search_data["image_queries"] = image_queries
|
|
|
|
| 793 |
|
| 794 |
# Build results container
|
| 795 |
results_container = html.Div([
|
| 796 |
+
html.Div([
|
| 797 |
+
html.P(f"Top {len(df)} matching galaxies (showing {min(DEFAULT_DISPLAY_COUNT, len(df))})",
|
| 798 |
+
className="results-header mb-2 text-center d-inline-block me-2"),
|
| 799 |
+
dbc.Button(
|
| 800 |
+
[html.I(className="fas fa-link me-1"), "Copy link"],
|
| 801 |
+
id="copy-results-link",
|
| 802 |
+
color="link",
|
| 803 |
+
size="sm",
|
| 804 |
+
n_clicks=0,
|
| 805 |
+
className="info-button"
|
| 806 |
+
),
|
| 807 |
+
html.Span(id="copy-results-feedback", style={"marginLeft": "8px", "color": "#28a745", "fontSize": "0.8rem"})
|
| 808 |
+
], className="text-center"),
|
| 809 |
html.P(
|
| 810 |
query_display_parts + ([f"{filter_desc}"] if filter_desc else []),
|
| 811 |
className="text-center mb-3",
|
|
|
|
| 815 |
load_more_button
|
| 816 |
])
|
| 817 |
|
| 818 |
+
# Store search params for URL generation (convert image_queries dict to tuple format)
|
| 819 |
+
search_params = {
|
| 820 |
+
"text_queries": text_queries,
|
| 821 |
+
"text_weights": text_weights,
|
| 822 |
+
"image_queries": [(img['ra'], img['dec'], img.get('fov', 0.025)) for img in image_queries],
|
| 823 |
+
"image_weights": image_weights,
|
| 824 |
+
"rmag_min": rmag_min,
|
| 825 |
+
"rmag_max": rmag_max
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
+
return "", results_container, search_data, False, False, search_params
|
| 829 |
|
| 830 |
except Exception as e:
|
| 831 |
error_msg = dbc.Alert(f"Advanced search failed: {str(e)}", color="danger")
|
| 832 |
logger.error(f"Advanced search error: {e}")
|
| 833 |
logger.error(f"Full traceback:\n{traceback.format_exc()}")
|
| 834 |
+
|
| 835 |
+
# Log error
|
| 836 |
+
from src.utils import build_query_xml, log_query_to_csv
|
| 837 |
+
try:
|
| 838 |
+
query_xml = build_query_xml(
|
| 839 |
+
text_queries=text_queries if text_queries else None,
|
| 840 |
+
text_weights=text_weights if text_weights else None,
|
| 841 |
+
image_queries=image_queries if image_queries else None,
|
| 842 |
+
image_weights=image_weights if image_weights else None,
|
| 843 |
+
rmag_min=rmag_range[0] if rmag_range else None,
|
| 844 |
+
rmag_max=rmag_range[1] if rmag_range else None
|
| 845 |
+
)
|
| 846 |
+
log_query_to_csv(
|
| 847 |
+
query_xml,
|
| 848 |
+
request_id=request_id,
|
| 849 |
+
error_occurred=True,
|
| 850 |
+
error_message=str(e),
|
| 851 |
+
error_type=type(e).__name__
|
| 852 |
+
)
|
| 853 |
+
except:
|
| 854 |
+
pass
|
| 855 |
+
|
| 856 |
+
return "", error_msg, None, True, True, None
|
| 857 |
+
|
| 858 |
+
# ===== URL State Management Callbacks =====
|
| 859 |
+
|
| 860 |
+
@app.callback(
|
| 861 |
+
[Output("search-input-advanced", "value", allow_duplicate=True),
|
| 862 |
+
Output("main-vector-operation", "value", allow_duplicate=True),
|
| 863 |
+
Output("main-query-type", "value", allow_duplicate=True),
|
| 864 |
+
Output("main-vector-ra", "value", allow_duplicate=True),
|
| 865 |
+
Output("main-vector-dec", "value", allow_duplicate=True),
|
| 866 |
+
Output("vector-inputs", "children", allow_duplicate=True),
|
| 867 |
+
Output("rmag-slider", "value", allow_duplicate=True),
|
| 868 |
+
Output("advanced-search-interface", "style", allow_duplicate=True),
|
| 869 |
+
Output("basic-search-bar", "style", allow_duplicate=True),
|
| 870 |
+
Output("vector-arrow", "className", allow_duplicate=True),
|
| 871 |
+
Output("url-search-trigger", "data", allow_duplicate=True),
|
| 872 |
+
Output("pending-expand-galaxy", "data")],
|
| 873 |
+
Input("url", "search"),
|
| 874 |
+
prevent_initial_call=True
|
| 875 |
+
)
|
| 876 |
+
def restore_state_from_url(search_string):
|
| 877 |
+
"""Parse URL and populate UI, then trigger search if state is present."""
|
| 878 |
+
from src.url_state import parse_url_search_param
|
| 879 |
+
|
| 880 |
+
# Parse URL state
|
| 881 |
+
state = parse_url_search_param(search_string)
|
| 882 |
+
|
| 883 |
+
# If no state, return defaults (don't trigger search)
|
| 884 |
+
if not state.get('text_queries') and not state.get('image_queries'):
|
| 885 |
+
return (
|
| 886 |
+
dash.no_update, # search-input-advanced
|
| 887 |
+
dash.no_update, # main-vector-operation
|
| 888 |
+
dash.no_update, # main-query-type
|
| 889 |
+
dash.no_update, # main-vector-ra
|
| 890 |
+
dash.no_update, # main-vector-dec
|
| 891 |
+
dash.no_update, # vector-inputs
|
| 892 |
+
dash.no_update, # rmag-slider
|
| 893 |
+
dash.no_update, # advanced-search-interface style
|
| 894 |
+
dash.no_update, # basic-search-bar style
|
| 895 |
+
dash.no_update, # vector-arrow
|
| 896 |
+
dash.no_update, # url-search-trigger
|
| 897 |
+
None # pending-expand-galaxy
|
| 898 |
+
)
|
| 899 |
+
|
| 900 |
+
# Always restore to advanced mode
|
| 901 |
+
text_queries = state.get('text_queries', [])
|
| 902 |
+
text_weights = state.get('text_weights', [])
|
| 903 |
+
image_queries = state.get('image_queries', [])
|
| 904 |
+
image_weights = state.get('image_weights', [])
|
| 905 |
+
rmag_min = state.get('rmag_min', 13.0)
|
| 906 |
+
rmag_max = state.get('rmag_max', 20.0)
|
| 907 |
+
expand_galaxy = state.get('expand_galaxy')
|
| 908 |
+
|
| 909 |
+
# Determine main query (first query - text or image)
|
| 910 |
+
main_text = ""
|
| 911 |
+
main_operation = "+"
|
| 912 |
+
main_query_type = "text"
|
| 913 |
+
main_ra = None
|
| 914 |
+
main_dec = None
|
| 915 |
+
vector_children = []
|
| 916 |
+
|
| 917 |
+
all_queries = []
|
| 918 |
+
# Combine text and image queries with their types
|
| 919 |
+
for i, (query, weight) in enumerate(zip(text_queries, text_weights)):
|
| 920 |
+
all_queries.append(('text', query, None, None, weight))
|
| 921 |
+
for i, (img_query, weight) in enumerate(zip(image_queries, image_weights)):
|
| 922 |
+
all_queries.append(('image', None, img_query[0], img_query[1], weight))
|
| 923 |
+
|
| 924 |
+
if all_queries:
|
| 925 |
+
# First query becomes main query
|
| 926 |
+
first_type, first_text, first_ra, first_dec, first_weight = all_queries[0]
|
| 927 |
+
main_query_type = first_type
|
| 928 |
+
main_operation = weight_to_operation_str(first_weight)
|
| 929 |
+
|
| 930 |
+
if first_type == 'text':
|
| 931 |
+
main_text = first_text
|
| 932 |
+
else:
|
| 933 |
+
main_ra = first_ra
|
| 934 |
+
main_dec = first_dec
|
| 935 |
+
|
| 936 |
+
# Remaining queries become additional vectors
|
| 937 |
+
for idx, (qtype, qtext, qra, qdec, qweight) in enumerate(all_queries[1:]):
|
| 938 |
+
from src.components import create_vector_input_row
|
| 939 |
+
operation_str = weight_to_operation_str(qweight)
|
| 940 |
+
if qtype == 'text':
|
| 941 |
+
row = create_vector_input_row(
|
| 942 |
+
idx,
|
| 943 |
+
query_type='text',
|
| 944 |
+
text_value=qtext,
|
| 945 |
+
operation=operation_str
|
| 946 |
+
)
|
| 947 |
+
vector_children.append(row)
|
| 948 |
+
else:
|
| 949 |
+
row = create_vector_input_row(
|
| 950 |
+
idx,
|
| 951 |
+
query_type='image',
|
| 952 |
+
ra=qra,
|
| 953 |
+
dec=qdec,
|
| 954 |
+
operation=operation_str
|
| 955 |
+
)
|
| 956 |
+
vector_children.append(row)
|
| 957 |
+
|
| 958 |
+
return (
|
| 959 |
+
main_text, # search-input-advanced
|
| 960 |
+
main_operation, # main-vector-operation
|
| 961 |
+
main_query_type, # main-query-type
|
| 962 |
+
main_ra, # main-vector-ra
|
| 963 |
+
main_dec, # main-vector-dec
|
| 964 |
+
vector_children, # vector-inputs
|
| 965 |
+
[rmag_min, rmag_max], # rmag-slider
|
| 966 |
+
{"display": "block"}, # advanced-search-interface (show)
|
| 967 |
+
{"display": "none"}, # basic-search-bar (hide)
|
| 968 |
+
"fas fa-chevron-up me-2", # vector-arrow (up arrow for advanced mode)
|
| 969 |
+
1, # url-search-trigger (trigger search)
|
| 970 |
+
expand_galaxy # pending-expand-galaxy
|
| 971 |
+
)
|
| 972 |
+
|
| 973 |
+
@app.callback(
|
| 974 |
+
Output("search-button-advanced", "n_clicks", allow_duplicate=True),
|
| 975 |
+
Input("url-search-trigger", "data"),
|
| 976 |
+
State("search-button-advanced", "n_clicks"),
|
| 977 |
+
prevent_initial_call=True
|
| 978 |
+
)
|
| 979 |
+
def trigger_search_from_url(trigger, current_clicks):
|
| 980 |
+
"""Click search button after URL state is restored."""
|
| 981 |
+
if trigger:
|
| 982 |
+
return (current_clicks or 0) + 1
|
| 983 |
+
return dash.no_update
|
| 984 |
+
|
| 985 |
+
@app.callback(
|
| 986 |
+
Output("url", "search", allow_duplicate=True),
|
| 987 |
+
Input("current-search-params", "data"),
|
| 988 |
+
State("url", "search"),
|
| 989 |
+
prevent_initial_call=True
|
| 990 |
+
)
|
| 991 |
+
def update_url_after_search(search_params, current_url):
|
| 992 |
+
"""Update browser URL without reload after search completes."""
|
| 993 |
+
from src.url_state import encode_search_state
|
| 994 |
+
from src.config import URL_STATE_PARAM
|
| 995 |
+
|
| 996 |
+
if not search_params:
|
| 997 |
+
return dash.no_update
|
| 998 |
+
|
| 999 |
+
# Extract params
|
| 1000 |
+
text_queries = search_params.get('text_queries', [])
|
| 1001 |
+
text_weights = search_params.get('text_weights', [])
|
| 1002 |
+
image_queries = search_params.get('image_queries', [])
|
| 1003 |
+
image_weights = search_params.get('image_weights', [])
|
| 1004 |
+
rmag_min = search_params.get('rmag_min', 13.0)
|
| 1005 |
+
rmag_max = search_params.get('rmag_max', 20.0)
|
| 1006 |
+
|
| 1007 |
+
# Encode state
|
| 1008 |
+
encoded = encode_search_state(
|
| 1009 |
+
text_queries=text_queries,
|
| 1010 |
+
text_weights=text_weights,
|
| 1011 |
+
image_queries=image_queries,
|
| 1012 |
+
image_weights=image_weights,
|
| 1013 |
+
rmag_min=rmag_min,
|
| 1014 |
+
rmag_max=rmag_max
|
| 1015 |
+
)
|
| 1016 |
+
|
| 1017 |
+
# Build new URL search string
|
| 1018 |
+
new_url = f"?{URL_STATE_PARAM}={encoded}"
|
| 1019 |
+
|
| 1020 |
+
# Only update if different from current
|
| 1021 |
+
if new_url == current_url:
|
| 1022 |
+
raise dash.exceptions.PreventUpdate
|
| 1023 |
+
|
| 1024 |
+
return new_url
|
| 1025 |
+
|
| 1026 |
+
# Clientside callback for copying results link to clipboard
|
| 1027 |
+
app.clientside_callback(
|
| 1028 |
+
"""
|
| 1029 |
+
function(n_clicks, search_params) {
|
| 1030 |
+
if (!n_clicks || !search_params) {
|
| 1031 |
+
return "";
|
| 1032 |
+
}
|
| 1033 |
+
|
| 1034 |
+
// Build the URL from search params
|
| 1035 |
+
var state = {};
|
| 1036 |
+
if (search_params.text_queries && search_params.text_queries.length > 0) {
|
| 1037 |
+
state.tq = search_params.text_queries;
|
| 1038 |
+
state.tw = search_params.text_weights;
|
| 1039 |
+
}
|
| 1040 |
+
if (search_params.image_queries && search_params.image_queries.length > 0) {
|
| 1041 |
+
state.iq = search_params.image_queries;
|
| 1042 |
+
state.iw = search_params.image_weights;
|
| 1043 |
+
}
|
| 1044 |
+
if (search_params.rmag_min !== 13.0) {
|
| 1045 |
+
state.rmin = search_params.rmag_min;
|
| 1046 |
+
}
|
| 1047 |
+
if (search_params.rmag_max !== 20.0) {
|
| 1048 |
+
state.rmax = search_params.rmag_max;
|
| 1049 |
+
}
|
| 1050 |
+
|
| 1051 |
+
var jsonStr = JSON.stringify(state);
|
| 1052 |
+
var encoded = btoa(jsonStr).replace(/=/g, '');
|
| 1053 |
+
var url = window.location.origin + "?s=" + encoded;
|
| 1054 |
+
|
| 1055 |
+
// Copy to clipboard using fallback method
|
| 1056 |
+
var textArea = document.createElement("textarea");
|
| 1057 |
+
textArea.value = url;
|
| 1058 |
+
textArea.style.position = "fixed";
|
| 1059 |
+
textArea.style.left = "-999999px";
|
| 1060 |
+
textArea.style.top = "-999999px";
|
| 1061 |
+
document.body.appendChild(textArea);
|
| 1062 |
+
textArea.focus();
|
| 1063 |
+
textArea.select();
|
| 1064 |
+
|
| 1065 |
+
try {
|
| 1066 |
+
document.execCommand('copy');
|
| 1067 |
+
document.body.removeChild(textArea);
|
| 1068 |
+
// Clear feedback after 2 seconds
|
| 1069 |
+
setTimeout(function() {
|
| 1070 |
+
var el = document.getElementById('copy-results-feedback');
|
| 1071 |
+
if (el) el.textContent = '';
|
| 1072 |
+
}, 2000);
|
| 1073 |
+
return "Copied!";
|
| 1074 |
+
} catch (err) {
|
| 1075 |
+
document.body.removeChild(textArea);
|
| 1076 |
+
// Try modern API as fallback
|
| 1077 |
+
if (navigator.clipboard) {
|
| 1078 |
+
navigator.clipboard.writeText(url);
|
| 1079 |
+
setTimeout(function() {
|
| 1080 |
+
var el = document.getElementById('copy-results-feedback');
|
| 1081 |
+
if (el) el.textContent = '';
|
| 1082 |
+
}, 2000);
|
| 1083 |
+
return "Copied!";
|
| 1084 |
+
}
|
| 1085 |
+
return "Failed to copy";
|
| 1086 |
+
}
|
| 1087 |
+
}
|
| 1088 |
+
""",
|
| 1089 |
+
Output("copy-results-feedback", "children"),
|
| 1090 |
+
Input("copy-results-link", "n_clicks"),
|
| 1091 |
+
State("current-search-params", "data"),
|
| 1092 |
+
prevent_initial_call=True
|
| 1093 |
+
)
|
| 1094 |
+
|
| 1095 |
+
# Clientside callback for copying galaxy link to clipboard
|
| 1096 |
+
app.clientside_callback(
|
| 1097 |
+
"""
|
| 1098 |
+
function(n_clicks, search_params, galaxy_data) {
|
| 1099 |
+
if (!n_clicks || !search_params || !galaxy_data) {
|
| 1100 |
+
return "";
|
| 1101 |
+
}
|
| 1102 |
+
|
| 1103 |
+
// Build the URL from search params with galaxy expansion
|
| 1104 |
+
var state = {};
|
| 1105 |
+
if (search_params.text_queries && search_params.text_queries.length > 0) {
|
| 1106 |
+
state.tq = search_params.text_queries;
|
| 1107 |
+
state.tw = search_params.text_weights;
|
| 1108 |
+
}
|
| 1109 |
+
if (search_params.image_queries && search_params.image_queries.length > 0) {
|
| 1110 |
+
state.iq = search_params.image_queries;
|
| 1111 |
+
state.iw = search_params.image_weights;
|
| 1112 |
+
}
|
| 1113 |
+
if (search_params.rmag_min !== 13.0) {
|
| 1114 |
+
state.rmin = search_params.rmag_min;
|
| 1115 |
+
}
|
| 1116 |
+
if (search_params.rmag_max !== 20.0) {
|
| 1117 |
+
state.rmax = search_params.rmag_max;
|
| 1118 |
+
}
|
| 1119 |
+
// Add galaxy expansion
|
| 1120 |
+
if (galaxy_data.object_id) {
|
| 1121 |
+
var galaxyId = galaxy_data.object_id;
|
| 1122 |
+
// Clean up bytes notation if present (e.g., "b'1500m885-7090'" -> "1500m885-7090")
|
| 1123 |
+
if (typeof galaxyId === 'string' && galaxyId.startsWith("b'") && galaxyId.endsWith("'")) {
|
| 1124 |
+
galaxyId = galaxyId.slice(2, -1);
|
| 1125 |
+
}
|
| 1126 |
+
state.exp = galaxyId;
|
| 1127 |
+
}
|
| 1128 |
+
|
| 1129 |
+
var jsonStr = JSON.stringify(state);
|
| 1130 |
+
var encoded = btoa(jsonStr).replace(/=/g, '');
|
| 1131 |
+
var url = window.location.origin + "?s=" + encoded;
|
| 1132 |
+
|
| 1133 |
+
// Copy to clipboard using fallback method
|
| 1134 |
+
var textArea = document.createElement("textarea");
|
| 1135 |
+
textArea.value = url;
|
| 1136 |
+
textArea.style.position = "fixed";
|
| 1137 |
+
textArea.style.left = "-999999px";
|
| 1138 |
+
textArea.style.top = "-999999px";
|
| 1139 |
+
document.body.appendChild(textArea);
|
| 1140 |
+
textArea.focus();
|
| 1141 |
+
textArea.select();
|
| 1142 |
+
|
| 1143 |
+
try {
|
| 1144 |
+
document.execCommand('copy');
|
| 1145 |
+
document.body.removeChild(textArea);
|
| 1146 |
+
// Clear feedback after 2 seconds
|
| 1147 |
+
setTimeout(function() {
|
| 1148 |
+
var el = document.getElementById('copy-galaxy-feedback');
|
| 1149 |
+
if (el) el.textContent = '';
|
| 1150 |
+
}, 2000);
|
| 1151 |
+
return "Copied!";
|
| 1152 |
+
} catch (err) {
|
| 1153 |
+
document.body.removeChild(textArea);
|
| 1154 |
+
// Try modern API as fallback
|
| 1155 |
+
if (navigator.clipboard) {
|
| 1156 |
+
navigator.clipboard.writeText(url);
|
| 1157 |
+
setTimeout(function() {
|
| 1158 |
+
var el = document.getElementById('copy-galaxy-feedback');
|
| 1159 |
+
if (el) el.textContent = '';
|
| 1160 |
+
}, 2000);
|
| 1161 |
+
return "Copied!";
|
| 1162 |
+
}
|
| 1163 |
+
return "Failed to copy";
|
| 1164 |
+
}
|
| 1165 |
+
}
|
| 1166 |
+
""",
|
| 1167 |
+
Output("copy-galaxy-feedback", "children"),
|
| 1168 |
+
Input("copy-galaxy-link", "n_clicks"),
|
| 1169 |
+
[State("current-search-params", "data"),
|
| 1170 |
+
State("current-galaxy-data", "data")],
|
| 1171 |
+
prevent_initial_call=True
|
| 1172 |
+
)
|
| 1173 |
|
| 1174 |
|
| 1175 |
# Helper functions for callbacks
|
| 1176 |
|
| 1177 |
+
def weight_to_operation_str(weight):
|
| 1178 |
+
"""Convert weight float to operation string for UI."""
|
| 1179 |
+
if weight == 1.0:
|
| 1180 |
+
return "+"
|
| 1181 |
+
elif weight == -1.0:
|
| 1182 |
+
return "-"
|
| 1183 |
+
elif weight > 0:
|
| 1184 |
+
return f"+{int(weight)}"
|
| 1185 |
+
else:
|
| 1186 |
+
return str(int(weight))
|
| 1187 |
+
|
| 1188 |
+
|
| 1189 |
+
|
| 1190 |
def build_galaxy_grid(df: pd.DataFrame) -> list:
|
| 1191 |
"""Build galaxy grid items from DataFrame.
|
| 1192 |
|
|
|
|
| 1248 |
], width=6, md=4, lg=2, className="mb-2 px-1")
|
| 1249 |
|
| 1250 |
|
| 1251 |
+
def prepare_search_data(df: pd.DataFrame, query: str, is_vector_search: bool = False, request_id: str = None) -> dict:
|
| 1252 |
"""Prepare search data for storage.
|
| 1253 |
|
| 1254 |
Args:
|
| 1255 |
df: DataFrame with search results
|
| 1256 |
query: Search query string
|
| 1257 |
is_vector_search: Whether this is a vector search
|
| 1258 |
+
request_id: Unique ID for this request
|
| 1259 |
|
| 1260 |
Returns:
|
| 1261 |
Dictionary with search data
|
| 1262 |
"""
|
| 1263 |
+
data = {
|
| 1264 |
ZILLIZ_PRIMARY_KEY: df[ZILLIZ_PRIMARY_KEY].tolist(),
|
| 1265 |
"ra": df['ra'].tolist(),
|
| 1266 |
"dec": df['dec'].tolist(),
|
|
|
|
| 1271 |
"query": query,
|
| 1272 |
"is_vector_search": is_vector_search
|
| 1273 |
}
|
| 1274 |
+
if request_id:
|
| 1275 |
+
data["request_id"] = request_id
|
| 1276 |
+
return data
|
| 1277 |
|
| 1278 |
|
| 1279 |
def extract_galaxy_info(search_data: dict, index: int) -> dict:
|
src/components.py
CHANGED
|
@@ -542,8 +542,8 @@ def create_search_container():
|
|
| 542 |
style={"color": "rgba(245, 245, 247, 0.5)", "font-weight": "300",
|
| 543 |
"font-size": "0.75rem", "letter-spacing": "0.02em"}),
|
| 544 |
html.Div([
|
| 545 |
-
dbc.Button([html.I(className="fas fa-
|
| 546 |
-
|
| 547 |
dbc.Button([html.I(className="fas fa-water me-2"), "Tidal"],
|
| 548 |
id="example-2", className="example-button me-2 mb-2", size="sm", color="light"),
|
| 549 |
dbc.Button([html.I(className="fas fa-stream me-2"), "Stream"],
|
|
@@ -552,7 +552,7 @@ def create_search_container():
|
|
| 552 |
id="example-5", className="example-button me-2 mb-2", size="sm", color="light"),
|
| 553 |
dbc.Button([html.I(className="fas fa-moon me-2"), "Low surface brightness"],
|
| 554 |
id="example-6", className="example-button me-2 mb-2", size="sm", color="light"),
|
| 555 |
-
dbc.Button([html.I(className="fas fa-ring me-2"), "Ring
|
| 556 |
id="example-7", className="example-button me-2 mb-2", size="sm", color="light"),
|
| 557 |
dbc.Button([html.I(className="fas fa-star me-2"), "A bursty, star forming galaxy"],
|
| 558 |
id="example-8", className="example-button mb-2", size="sm", color="light")
|
|
@@ -699,7 +699,8 @@ def create_search_container():
|
|
| 699 |
], className="mb-3")
|
| 700 |
|
| 701 |
|
| 702 |
-
def create_vector_input_row(index: int, query_type: str = "text", ra: float = None, dec: float = None, fov: float = 0.025
|
|
|
|
| 703 |
"""Create a single vector input row with operation selector, query type toggle, and conditional inputs.
|
| 704 |
|
| 705 |
Args:
|
|
@@ -708,6 +709,8 @@ def create_vector_input_row(index: int, query_type: str = "text", ra: float = No
|
|
| 708 |
ra: Initial RA value for image queries (default: None)
|
| 709 |
dec: Initial Dec value for image queries (default: None)
|
| 710 |
fov: Initial FoV value for image queries (default: 0.025)
|
|
|
|
|
|
|
| 711 |
|
| 712 |
Returns:
|
| 713 |
Dash Bootstrap Row component with text/image mode toggle
|
|
@@ -731,7 +734,7 @@ def create_vector_input_row(index: int, query_type: str = "text", ra: float = No
|
|
| 731 |
{"label": "-5", "value": "-5"},
|
| 732 |
{"label": "-10", "value": "-10"}
|
| 733 |
],
|
| 734 |
-
value=
|
| 735 |
style={"width": "70px"},
|
| 736 |
className="d-inline-block vector-operation-select"
|
| 737 |
)
|
|
@@ -756,7 +759,8 @@ def create_vector_input_row(index: int, query_type: str = "text", ra: float = No
|
|
| 756 |
dbc.Input(
|
| 757 |
id={"type": "vector-text", "index": index},
|
| 758 |
placeholder="Enter text query...",
|
| 759 |
-
type="text"
|
|
|
|
| 760 |
)
|
| 761 |
], id={"type": "text-input-container", "index": index}, style=text_display),
|
| 762 |
# Image inputs (shown when type is "image")
|
|
@@ -810,9 +814,13 @@ def create_results_container():
|
|
| 810 |
def create_stores():
|
| 811 |
"""Create Dash Store components for data persistence."""
|
| 812 |
return [
|
|
|
|
| 813 |
dcc.Store(id="search-data"),
|
| 814 |
dcc.Store(id="current-galaxy-data"),
|
| 815 |
dcc.Store(id="vector-inputs-count", data=1),
|
|
|
|
|
|
|
|
|
|
| 816 |
dcc.Download(id="download-csv")
|
| 817 |
]
|
| 818 |
|
|
@@ -832,6 +840,14 @@ def create_galaxy_modal():
|
|
| 832 |
color="primary",
|
| 833 |
className="me-2"
|
| 834 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 835 |
dbc.Button("Close", id="close-modal", className="ms-auto")
|
| 836 |
])
|
| 837 |
], id="galaxy-modal", size="lg", is_open=False)
|
|
|
|
| 542 |
style={"color": "rgba(245, 245, 247, 0.5)", "font-weight": "300",
|
| 543 |
"font-size": "0.75rem", "letter-spacing": "0.02em"}),
|
| 544 |
html.Div([
|
| 545 |
+
dbc.Button([html.I(className="fas fa-clone me-2"), "Two edge-on galaxies"],
|
| 546 |
+
id="example-1", className="example-button me-2 mb-2", size="sm", color="light"),
|
| 547 |
dbc.Button([html.I(className="fas fa-water me-2"), "Tidal"],
|
| 548 |
id="example-2", className="example-button me-2 mb-2", size="sm", color="light"),
|
| 549 |
dbc.Button([html.I(className="fas fa-stream me-2"), "Stream"],
|
|
|
|
| 552 |
id="example-5", className="example-button me-2 mb-2", size="sm", color="light"),
|
| 553 |
dbc.Button([html.I(className="fas fa-moon me-2"), "Low surface brightness"],
|
| 554 |
id="example-6", className="example-button me-2 mb-2", size="sm", color="light"),
|
| 555 |
+
dbc.Button([html.I(className="fas fa-ring me-2"), "Ring-like structure"],
|
| 556 |
id="example-7", className="example-button me-2 mb-2", size="sm", color="light"),
|
| 557 |
dbc.Button([html.I(className="fas fa-star me-2"), "A bursty, star forming galaxy"],
|
| 558 |
id="example-8", className="example-button mb-2", size="sm", color="light")
|
|
|
|
| 699 |
], className="mb-3")
|
| 700 |
|
| 701 |
|
| 702 |
+
def create_vector_input_row(index: int, query_type: str = "text", ra: float = None, dec: float = None, fov: float = 0.025,
|
| 703 |
+
text_value: str = None, operation: str = "+"):
|
| 704 |
"""Create a single vector input row with operation selector, query type toggle, and conditional inputs.
|
| 705 |
|
| 706 |
Args:
|
|
|
|
| 709 |
ra: Initial RA value for image queries (default: None)
|
| 710 |
dec: Initial Dec value for image queries (default: None)
|
| 711 |
fov: Initial FoV value for image queries (default: 0.025)
|
| 712 |
+
text_value: Initial text value for text queries (default: None)
|
| 713 |
+
operation: Initial operation value (default: "+")
|
| 714 |
|
| 715 |
Returns:
|
| 716 |
Dash Bootstrap Row component with text/image mode toggle
|
|
|
|
| 734 |
{"label": "-5", "value": "-5"},
|
| 735 |
{"label": "-10", "value": "-10"}
|
| 736 |
],
|
| 737 |
+
value=operation,
|
| 738 |
style={"width": "70px"},
|
| 739 |
className="d-inline-block vector-operation-select"
|
| 740 |
)
|
|
|
|
| 759 |
dbc.Input(
|
| 760 |
id={"type": "vector-text", "index": index},
|
| 761 |
placeholder="Enter text query...",
|
| 762 |
+
type="text",
|
| 763 |
+
value=text_value
|
| 764 |
)
|
| 765 |
], id={"type": "text-input-container", "index": index}, style=text_display),
|
| 766 |
# Image inputs (shown when type is "image")
|
|
|
|
| 814 |
def create_stores():
|
| 815 |
"""Create Dash Store components for data persistence."""
|
| 816 |
return [
|
| 817 |
+
dcc.Location(id='url', refresh=False),
|
| 818 |
dcc.Store(id="search-data"),
|
| 819 |
dcc.Store(id="current-galaxy-data"),
|
| 820 |
dcc.Store(id="vector-inputs-count", data=1),
|
| 821 |
+
dcc.Store(id="current-search-params"), # Stores current search params for URL generation
|
| 822 |
+
dcc.Store(id="pending-expand-galaxy"), # Stores galaxy to expand after URL-based search
|
| 823 |
+
dcc.Store(id="url-search-trigger", data=0), # Triggers search after URL state restore
|
| 824 |
dcc.Download(id="download-csv")
|
| 825 |
]
|
| 826 |
|
|
|
|
| 840 |
color="primary",
|
| 841 |
className="me-2"
|
| 842 |
),
|
| 843 |
+
dbc.Button(
|
| 844 |
+
[html.I(className="fas fa-link me-2"), "Copy link"],
|
| 845 |
+
id="copy-galaxy-link",
|
| 846 |
+
color="secondary",
|
| 847 |
+
className="me-2",
|
| 848 |
+
n_clicks=0
|
| 849 |
+
),
|
| 850 |
+
html.Span(id="copy-galaxy-feedback", style={"color": "#28a745", "fontSize": "0.8rem"}),
|
| 851 |
dbc.Button("Close", id="close-modal", className="ms-auto")
|
| 852 |
])
|
| 853 |
], id="galaxy-modal", size="lg", is_open=False)
|
src/config.py
CHANGED
|
@@ -56,9 +56,16 @@ IMAGE_WIDTH = "100%"
|
|
| 56 |
CUTOUT_FOV = 0.025
|
| 57 |
CUTOUT_SIZE = 256
|
| 58 |
|
|
|
|
|
|
|
|
|
|
| 59 |
# Logging Configuration
|
| 60 |
VCU_COST_PER_MILLION = 4.0 # $4 per 1 million vCU
|
| 61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
# Feature Flags (for future features)
|
| 63 |
FEATURE_IMAGE_SEARCH = False
|
| 64 |
FEATURE_AUTH = False
|
|
|
|
| 56 |
CUTOUT_FOV = 0.025
|
| 57 |
CUTOUT_SIZE = 256
|
| 58 |
|
| 59 |
+
# URL State Configuration
|
| 60 |
+
URL_STATE_PARAM = "s" # Query parameter name for encoded state
|
| 61 |
+
|
| 62 |
# Logging Configuration
|
| 63 |
VCU_COST_PER_MILLION = 4.0 # $4 per 1 million vCU
|
| 64 |
|
| 65 |
+
# Hugging Face Dataset Logging
|
| 66 |
+
HF_LOG_REPO_ID = os.getenv("HF_LOG_REPO_ID") # e.g. "nolank/aionsearch-query-logs"
|
| 67 |
+
HF_LOG_EVERY_MINUTES = int(os.getenv("HF_LOG_EVERY_MINUTES", "10"))
|
| 68 |
+
|
| 69 |
# Feature Flags (for future features)
|
| 70 |
FEATURE_IMAGE_SEARCH = False
|
| 71 |
FEATURE_AUTH = False
|
src/hf_logging.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/hf_logging.py
|
| 2 |
+
"""Hugging Face dataset logging with CommitScheduler."""
|
| 3 |
+
|
| 4 |
+
import json
|
| 5 |
+
import uuid
|
| 6 |
+
import logging
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
|
| 10 |
+
from huggingface_hub import CommitScheduler
|
| 11 |
+
|
| 12 |
+
from src.config import HF_LOG_REPO_ID, HF_LOG_EVERY_MINUTES
|
| 13 |
+
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
# Generate a unique session ID for this app instance
|
| 17 |
+
SESSION_ID = uuid.uuid4().hex
|
| 18 |
+
|
| 19 |
+
# Initialize scheduler & local file if logging is configured
|
| 20 |
+
if HF_LOG_REPO_ID:
|
| 21 |
+
feedback_file = Path("query_logs") / f"queries_{uuid.uuid4().hex}.jsonl"
|
| 22 |
+
feedback_folder = feedback_file.parent
|
| 23 |
+
feedback_folder.mkdir(parents=True, exist_ok=True)
|
| 24 |
+
|
| 25 |
+
scheduler = CommitScheduler(
|
| 26 |
+
repo_id=HF_LOG_REPO_ID,
|
| 27 |
+
repo_type="dataset",
|
| 28 |
+
folder_path=feedback_folder,
|
| 29 |
+
path_in_repo="data", # files go in data/ on the dataset
|
| 30 |
+
every=HF_LOG_EVERY_MINUTES, # in minutes
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
# Print initialization info
|
| 34 |
+
print("\n" + "="*80)
|
| 35 |
+
print("HF DATASET LOGGING INITIALIZED")
|
| 36 |
+
print("="*80)
|
| 37 |
+
print(f"Session ID: {SESSION_ID}")
|
| 38 |
+
print(f"Repository: {HF_LOG_REPO_ID}")
|
| 39 |
+
print(f"Local directory: {feedback_folder.absolute()}")
|
| 40 |
+
print(f"Local file: {feedback_file.name}")
|
| 41 |
+
print(f"Commit frequency: every {HF_LOG_EVERY_MINUTES} minutes")
|
| 42 |
+
print("="*80 + "\n")
|
| 43 |
+
else:
|
| 44 |
+
scheduler = None
|
| 45 |
+
feedback_file = None
|
| 46 |
+
print("HF dataset logging disabled (HF_LOG_REPO_ID not set)")
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def log_query_event(payload: dict) -> None:
|
| 50 |
+
"""Append one JSON log line that CommitScheduler will push to the Hub.
|
| 51 |
+
|
| 52 |
+
payload must be JSON-serializable. session_id and timestamp are added if missing.
|
| 53 |
+
No-op if HF_LOG_REPO_ID is not configured.
|
| 54 |
+
|
| 55 |
+
Expected payload fields:
|
| 56 |
+
- log_type: Type of event (aql_query, zilliz_query_stats, click_event)
|
| 57 |
+
- request_id: Unique ID for the request/search (optional, but recommended)
|
| 58 |
+
- session_id: Session ID (auto-added if missing)
|
| 59 |
+
- timestamp: ISO timestamp (auto-added if missing)
|
| 60 |
+
- error_occurred: Boolean indicating if an error occurred (optional)
|
| 61 |
+
- error_message: Error message if error_occurred is True (optional)
|
| 62 |
+
- error_type: Type of error (optional)
|
| 63 |
+
... plus other event-specific fields
|
| 64 |
+
"""
|
| 65 |
+
if scheduler is None or feedback_file is None:
|
| 66 |
+
return
|
| 67 |
+
|
| 68 |
+
# Auto-add session_id if not present
|
| 69 |
+
if "session_id" not in payload:
|
| 70 |
+
payload["session_id"] = SESSION_ID
|
| 71 |
+
|
| 72 |
+
# Auto-add timestamp if not present
|
| 73 |
+
if "timestamp" not in payload:
|
| 74 |
+
payload["timestamp"] = datetime.utcnow().isoformat()
|
| 75 |
+
|
| 76 |
+
try:
|
| 77 |
+
with scheduler.lock:
|
| 78 |
+
with feedback_file.open("a") as f:
|
| 79 |
+
f.write(json.dumps(payload))
|
| 80 |
+
f.write("\n")
|
| 81 |
+
print(f"β Logged to HF dataset: {payload.get('log_type', 'unknown')}")
|
| 82 |
+
except Exception as e:
|
| 83 |
+
logger.error(f"Failed to write query log locally: {e}")
|
src/url_state.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
URL state encoding/decoding for shareable search URLs.
|
| 3 |
+
|
| 4 |
+
Core principle: All searches are represented uniformly - basic search is just
|
| 5 |
+
advanced search with a single text query at weight +1.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import base64
|
| 9 |
+
import json
|
| 10 |
+
from typing import Dict, List, Optional, Tuple, Any
|
| 11 |
+
|
| 12 |
+
try:
|
| 13 |
+
from flask import request
|
| 14 |
+
except ImportError:
|
| 15 |
+
request = None
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def encode_search_state(
|
| 19 |
+
text_queries: List[str],
|
| 20 |
+
text_weights: List[float],
|
| 21 |
+
image_queries: List[Tuple[float, float, float]], # (ra, dec, fov)
|
| 22 |
+
image_weights: List[float],
|
| 23 |
+
rmag_min: float = 13.0,
|
| 24 |
+
rmag_max: float = 20.0,
|
| 25 |
+
expand_galaxy: Optional[str] = None
|
| 26 |
+
) -> str:
|
| 27 |
+
"""
|
| 28 |
+
Encode search state into a compact base64 string.
|
| 29 |
+
|
| 30 |
+
Args:
|
| 31 |
+
text_queries: List of text query strings
|
| 32 |
+
text_weights: List of weights for text queries
|
| 33 |
+
image_queries: List of (ra, dec, fov) tuples
|
| 34 |
+
image_weights: List of weights for image queries
|
| 35 |
+
rmag_min: Minimum r-band magnitude (default 13.0)
|
| 36 |
+
rmag_max: Maximum r-band magnitude (default 20.0)
|
| 37 |
+
expand_galaxy: Optional object_id to expand in modal after search
|
| 38 |
+
|
| 39 |
+
Returns:
|
| 40 |
+
URL-safe base64 encoded string
|
| 41 |
+
"""
|
| 42 |
+
state = {}
|
| 43 |
+
|
| 44 |
+
# Only include non-empty query arrays
|
| 45 |
+
if text_queries:
|
| 46 |
+
state["tq"] = text_queries
|
| 47 |
+
state["tw"] = text_weights
|
| 48 |
+
|
| 49 |
+
if image_queries:
|
| 50 |
+
state["iq"] = image_queries
|
| 51 |
+
state["iw"] = image_weights
|
| 52 |
+
|
| 53 |
+
# Only include magnitude limits if they differ from defaults
|
| 54 |
+
if rmag_min != 13.0:
|
| 55 |
+
state["rmin"] = rmag_min
|
| 56 |
+
if rmag_max != 20.0:
|
| 57 |
+
state["rmax"] = rmag_max
|
| 58 |
+
|
| 59 |
+
# Include expand galaxy if specified
|
| 60 |
+
if expand_galaxy:
|
| 61 |
+
state["exp"] = expand_galaxy
|
| 62 |
+
|
| 63 |
+
# Encode to JSON then base64 (URL-safe)
|
| 64 |
+
json_str = json.dumps(state, separators=(',', ':')) # Compact JSON
|
| 65 |
+
encoded = base64.urlsafe_b64encode(json_str.encode('utf-8')).decode('utf-8')
|
| 66 |
+
|
| 67 |
+
# Remove padding (= characters) to make URL shorter - we'll add back on decode
|
| 68 |
+
return encoded.rstrip('=')
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def decode_search_state(encoded: str) -> Dict[str, Any]:
|
| 72 |
+
"""
|
| 73 |
+
Decode a base64-encoded search state string.
|
| 74 |
+
|
| 75 |
+
Args:
|
| 76 |
+
encoded: URL-safe base64 encoded string
|
| 77 |
+
|
| 78 |
+
Returns:
|
| 79 |
+
Dictionary with full keys:
|
| 80 |
+
{
|
| 81 |
+
'text_queries': [...],
|
| 82 |
+
'text_weights': [...],
|
| 83 |
+
'image_queries': [...],
|
| 84 |
+
'image_weights': [...],
|
| 85 |
+
'rmag_min': float,
|
| 86 |
+
'rmag_max': float,
|
| 87 |
+
'expand_galaxy': str or None
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
Returns empty/default state if decoding fails.
|
| 91 |
+
"""
|
| 92 |
+
# Default/empty state
|
| 93 |
+
default_state = {
|
| 94 |
+
'text_queries': [],
|
| 95 |
+
'text_weights': [],
|
| 96 |
+
'image_queries': [],
|
| 97 |
+
'image_weights': [],
|
| 98 |
+
'rmag_min': 13.0,
|
| 99 |
+
'rmag_max': 20.0,
|
| 100 |
+
'expand_galaxy': None
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
if not encoded:
|
| 104 |
+
return default_state
|
| 105 |
+
|
| 106 |
+
try:
|
| 107 |
+
# Add back padding if needed
|
| 108 |
+
padding = 4 - (len(encoded) % 4)
|
| 109 |
+
if padding != 4:
|
| 110 |
+
encoded += '=' * padding
|
| 111 |
+
|
| 112 |
+
# Decode from base64 then JSON
|
| 113 |
+
json_str = base64.urlsafe_b64decode(encoded.encode('utf-8')).decode('utf-8')
|
| 114 |
+
compact_state = json.loads(json_str)
|
| 115 |
+
|
| 116 |
+
# Expand compact keys to full keys
|
| 117 |
+
state = {
|
| 118 |
+
'text_queries': compact_state.get('tq', []),
|
| 119 |
+
'text_weights': compact_state.get('tw', []),
|
| 120 |
+
'image_queries': compact_state.get('iq', []),
|
| 121 |
+
'image_weights': compact_state.get('iw', []),
|
| 122 |
+
'rmag_min': compact_state.get('rmin', 13.0),
|
| 123 |
+
'rmag_max': compact_state.get('rmax', 20.0),
|
| 124 |
+
'expand_galaxy': compact_state.get('exp', None)
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
return state
|
| 128 |
+
|
| 129 |
+
except (ValueError, KeyError, json.JSONDecodeError, Exception):
|
| 130 |
+
# Silently fail and return empty state for any decode errors
|
| 131 |
+
return default_state
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def build_share_url(
|
| 135 |
+
text_queries: List[str],
|
| 136 |
+
text_weights: List[float],
|
| 137 |
+
image_queries: List[Tuple[float, float, float]],
|
| 138 |
+
image_weights: List[float],
|
| 139 |
+
rmag_min: float = 13.0,
|
| 140 |
+
rmag_max: float = 20.0,
|
| 141 |
+
expand_galaxy: Optional[str] = None
|
| 142 |
+
) -> str:
|
| 143 |
+
"""
|
| 144 |
+
Build a complete shareable URL for the current search state.
|
| 145 |
+
|
| 146 |
+
Args:
|
| 147 |
+
Same as encode_search_state()
|
| 148 |
+
|
| 149 |
+
Returns:
|
| 150 |
+
Complete URL like: https://app.com?s=<encoded_state>
|
| 151 |
+
"""
|
| 152 |
+
from src.config import URL_STATE_PARAM
|
| 153 |
+
|
| 154 |
+
# Get base URL from Flask request context
|
| 155 |
+
try:
|
| 156 |
+
if request and hasattr(request, 'host_url'):
|
| 157 |
+
base_url = request.host_url.rstrip('/')
|
| 158 |
+
else:
|
| 159 |
+
base_url = "http://localhost:8050"
|
| 160 |
+
except (RuntimeError, AttributeError):
|
| 161 |
+
# If not in request context (e.g., during testing), use a placeholder
|
| 162 |
+
base_url = "http://localhost:8050"
|
| 163 |
+
|
| 164 |
+
# Encode the state
|
| 165 |
+
encoded = encode_search_state(
|
| 166 |
+
text_queries=text_queries,
|
| 167 |
+
text_weights=text_weights,
|
| 168 |
+
image_queries=image_queries,
|
| 169 |
+
image_weights=image_weights,
|
| 170 |
+
rmag_min=rmag_min,
|
| 171 |
+
rmag_max=rmag_max,
|
| 172 |
+
expand_galaxy=expand_galaxy
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
return f"{base_url}?{URL_STATE_PARAM}={encoded}"
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def parse_url_search_param(search_string: str) -> Dict[str, Any]:
|
| 179 |
+
"""
|
| 180 |
+
Parse the search parameter from a URL query string.
|
| 181 |
+
|
| 182 |
+
Args:
|
| 183 |
+
search_string: The URL search/query string (e.g., "?s=eyJxdWVy...")
|
| 184 |
+
|
| 185 |
+
Returns:
|
| 186 |
+
Decoded state dictionary (same format as decode_search_state)
|
| 187 |
+
"""
|
| 188 |
+
from src.config import URL_STATE_PARAM
|
| 189 |
+
|
| 190 |
+
if not search_string:
|
| 191 |
+
return decode_search_state("")
|
| 192 |
+
|
| 193 |
+
# Remove leading '?' if present
|
| 194 |
+
search_string = search_string.lstrip('?')
|
| 195 |
+
|
| 196 |
+
# Parse query parameters
|
| 197 |
+
params = {}
|
| 198 |
+
for param in search_string.split('&'):
|
| 199 |
+
if '=' in param:
|
| 200 |
+
key, value = param.split('=', 1)
|
| 201 |
+
params[key] = value
|
| 202 |
+
|
| 203 |
+
# Get the state parameter
|
| 204 |
+
encoded = params.get(URL_STATE_PARAM, '')
|
| 205 |
+
|
| 206 |
+
return decode_search_state(encoded)
|
src/utils.py
CHANGED
|
@@ -2,10 +2,12 @@
|
|
| 2 |
|
| 3 |
import json
|
| 4 |
import logging
|
|
|
|
| 5 |
from pathlib import Path
|
| 6 |
from datetime import datetime
|
| 7 |
-
from typing import Dict, Any
|
| 8 |
from src.config import CUTOUT_FOV, CUTOUT_SIZE, VCU_COST_PER_MILLION
|
|
|
|
| 9 |
|
| 10 |
logger = logging.getLogger(__name__)
|
| 11 |
|
|
@@ -34,9 +36,13 @@ def log_zilliz_query(
|
|
| 34 |
query_info: Dict[str, Any],
|
| 35 |
result_count: int,
|
| 36 |
query_time: float,
|
| 37 |
-
cost_vcu: int = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
) -> None:
|
| 39 |
-
"""Print Zilliz query info to terminal
|
| 40 |
|
| 41 |
Args:
|
| 42 |
query_type: Type of query (e.g., "vector_search", "text_search")
|
|
@@ -44,6 +50,10 @@ def log_zilliz_query(
|
|
| 44 |
result_count: Number of results returned
|
| 45 |
query_time: Query execution time in seconds
|
| 46 |
cost_vcu: Cost in vCU units
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
"""
|
| 48 |
timestamp = datetime.now().isoformat()
|
| 49 |
|
|
@@ -72,6 +82,29 @@ def log_zilliz_query(
|
|
| 72 |
f"{cost_vcu} vCU (${cost_usd:.6f})"
|
| 73 |
)
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
def format_galaxy_count(count: int) -> str:
|
| 77 |
"""Format galaxy count with thousands separator.
|
|
@@ -104,69 +137,77 @@ def build_query_xml(
|
|
| 104 |
rmag_max: Maximum r_mag filter value
|
| 105 |
|
| 106 |
Returns:
|
| 107 |
-
XML string representation of the query
|
| 108 |
"""
|
| 109 |
xml_parts = ['<query>']
|
| 110 |
|
| 111 |
# Add text queries
|
| 112 |
if text_queries and len(text_queries) > 0:
|
| 113 |
-
xml_parts.append('
|
| 114 |
for query, weight in zip(text_queries, text_weights):
|
| 115 |
-
xml_parts.append('
|
| 116 |
-
xml_parts.append(f'
|
| 117 |
-
xml_parts.append(f'
|
| 118 |
-
xml_parts.append('
|
| 119 |
-
xml_parts.append('
|
| 120 |
|
| 121 |
# Add image queries
|
| 122 |
if image_queries and len(image_queries) > 0:
|
| 123 |
-
xml_parts.append('
|
| 124 |
for img_query, weight in zip(image_queries, image_weights):
|
| 125 |
-
xml_parts.append('
|
| 126 |
-
xml_parts.append(f'
|
| 127 |
-
xml_parts.append(f'
|
| 128 |
-
xml_parts.append(f'
|
| 129 |
-
xml_parts.append(f'
|
| 130 |
-
xml_parts.append('
|
| 131 |
-
xml_parts.append('
|
| 132 |
|
| 133 |
# Add filters
|
| 134 |
if rmag_min is not None or rmag_max is not None:
|
| 135 |
-
xml_parts.append('
|
| 136 |
if rmag_min is not None and rmag_max is not None:
|
| 137 |
-
xml_parts.append('
|
| 138 |
-
xml_parts.append('
|
| 139 |
-
xml_parts.append('
|
| 140 |
-
xml_parts.append(f'
|
| 141 |
-
xml_parts.append(f'
|
| 142 |
-
xml_parts.append('
|
| 143 |
elif rmag_min is not None:
|
| 144 |
-
xml_parts.append('
|
| 145 |
-
xml_parts.append('
|
| 146 |
-
xml_parts.append('
|
| 147 |
-
xml_parts.append(f'
|
| 148 |
-
xml_parts.append('
|
| 149 |
elif rmag_max is not None:
|
| 150 |
-
xml_parts.append('
|
| 151 |
-
xml_parts.append('
|
| 152 |
-
xml_parts.append('
|
| 153 |
-
xml_parts.append(f'
|
| 154 |
-
xml_parts.append('
|
| 155 |
-
xml_parts.append('
|
| 156 |
|
| 157 |
xml_parts.append('</query>')
|
| 158 |
-
return '
|
| 159 |
|
| 160 |
|
| 161 |
def log_query_to_csv(
|
| 162 |
query_xml: str,
|
| 163 |
-
csv_path: str = "logs/query_log.csv"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
) -> None:
|
| 165 |
-
"""Print query XML to terminal
|
| 166 |
|
| 167 |
Args:
|
| 168 |
query_xml: XML string representation of the query
|
| 169 |
csv_path: Deprecated parameter (kept for backward compatibility)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
"""
|
| 171 |
timestamp = datetime.now().isoformat()
|
| 172 |
|
|
@@ -178,3 +219,60 @@ def log_query_to_csv(
|
|
| 178 |
print("="*80 + "\n")
|
| 179 |
|
| 180 |
logger.info(f"Query printed to terminal")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
import json
|
| 4 |
import logging
|
| 5 |
+
import uuid
|
| 6 |
from pathlib import Path
|
| 7 |
from datetime import datetime
|
| 8 |
+
from typing import Dict, Any, Optional
|
| 9 |
from src.config import CUTOUT_FOV, CUTOUT_SIZE, VCU_COST_PER_MILLION
|
| 10 |
+
from src.hf_logging import log_query_event, SESSION_ID
|
| 11 |
|
| 12 |
logger = logging.getLogger(__name__)
|
| 13 |
|
|
|
|
| 36 |
query_info: Dict[str, Any],
|
| 37 |
result_count: int,
|
| 38 |
query_time: float,
|
| 39 |
+
cost_vcu: int = 0,
|
| 40 |
+
request_id: Optional[str] = None,
|
| 41 |
+
error_occurred: bool = False,
|
| 42 |
+
error_message: Optional[str] = None,
|
| 43 |
+
error_type: Optional[str] = None
|
| 44 |
) -> None:
|
| 45 |
+
"""Print Zilliz query info to terminal and log to HF dataset.
|
| 46 |
|
| 47 |
Args:
|
| 48 |
query_type: Type of query (e.g., "vector_search", "text_search")
|
|
|
|
| 50 |
result_count: Number of results returned
|
| 51 |
query_time: Query execution time in seconds
|
| 52 |
cost_vcu: Cost in vCU units
|
| 53 |
+
request_id: Unique ID for this request
|
| 54 |
+
error_occurred: Whether an error occurred
|
| 55 |
+
error_message: Error message if error_occurred is True
|
| 56 |
+
error_type: Type of error if error_occurred is True
|
| 57 |
"""
|
| 58 |
timestamp = datetime.now().isoformat()
|
| 59 |
|
|
|
|
| 82 |
f"{cost_vcu} vCU (${cost_usd:.6f})"
|
| 83 |
)
|
| 84 |
|
| 85 |
+
# Log Zilliz stats to HF dataset
|
| 86 |
+
try:
|
| 87 |
+
payload = {
|
| 88 |
+
"log_type": "zilliz_query_stats",
|
| 89 |
+
"timestamp": timestamp,
|
| 90 |
+
"query_type": query_type,
|
| 91 |
+
"query_info": query_info,
|
| 92 |
+
"result_count": result_count,
|
| 93 |
+
"query_time_seconds": query_time,
|
| 94 |
+
"cost_vcu": cost_vcu,
|
| 95 |
+
"cost_usd": cost_usd,
|
| 96 |
+
"error_occurred": error_occurred,
|
| 97 |
+
}
|
| 98 |
+
if request_id:
|
| 99 |
+
payload["request_id"] = request_id
|
| 100 |
+
if error_occurred:
|
| 101 |
+
payload["error_message"] = error_message
|
| 102 |
+
payload["error_type"] = error_type
|
| 103 |
+
|
| 104 |
+
log_query_event(payload)
|
| 105 |
+
except Exception as e:
|
| 106 |
+
logger.error(f"Failed to send Zilliz stats to HF dataset: {e}")
|
| 107 |
+
|
| 108 |
|
| 109 |
def format_galaxy_count(count: int) -> str:
|
| 110 |
"""Format galaxy count with thousands separator.
|
|
|
|
| 137 |
rmag_max: Maximum r_mag filter value
|
| 138 |
|
| 139 |
Returns:
|
| 140 |
+
XML string representation of the query (single line)
|
| 141 |
"""
|
| 142 |
xml_parts = ['<query>']
|
| 143 |
|
| 144 |
# Add text queries
|
| 145 |
if text_queries and len(text_queries) > 0:
|
| 146 |
+
xml_parts.append('<text>')
|
| 147 |
for query, weight in zip(text_queries, text_weights):
|
| 148 |
+
xml_parts.append('<term>')
|
| 149 |
+
xml_parts.append(f'<weight>{weight}</weight>')
|
| 150 |
+
xml_parts.append(f'<content>{query}</content>')
|
| 151 |
+
xml_parts.append('</term>')
|
| 152 |
+
xml_parts.append('</text>')
|
| 153 |
|
| 154 |
# Add image queries
|
| 155 |
if image_queries and len(image_queries) > 0:
|
| 156 |
+
xml_parts.append('<image>')
|
| 157 |
for img_query, weight in zip(image_queries, image_weights):
|
| 158 |
+
xml_parts.append('<reference>')
|
| 159 |
+
xml_parts.append(f'<ra>{img_query["ra"]}</ra>')
|
| 160 |
+
xml_parts.append(f'<dec>{img_query["dec"]}</dec>')
|
| 161 |
+
xml_parts.append(f'<fov>{img_query["fov"]}</fov>')
|
| 162 |
+
xml_parts.append(f'<weight>{weight}</weight>')
|
| 163 |
+
xml_parts.append('</reference>')
|
| 164 |
+
xml_parts.append('</image>')
|
| 165 |
|
| 166 |
# Add filters
|
| 167 |
if rmag_min is not None or rmag_max is not None:
|
| 168 |
+
xml_parts.append('<filters>')
|
| 169 |
if rmag_min is not None and rmag_max is not None:
|
| 170 |
+
xml_parts.append('<filter>')
|
| 171 |
+
xml_parts.append('<column>r_mag</column>')
|
| 172 |
+
xml_parts.append('<operator>between</operator>')
|
| 173 |
+
xml_parts.append(f'<value_min>{rmag_min}</value_min>')
|
| 174 |
+
xml_parts.append(f'<value_max>{rmag_max}</value_max>')
|
| 175 |
+
xml_parts.append('</filter>')
|
| 176 |
elif rmag_min is not None:
|
| 177 |
+
xml_parts.append('<filter>')
|
| 178 |
+
xml_parts.append('<column>r_mag</column>')
|
| 179 |
+
xml_parts.append('<operator>gte</operator>')
|
| 180 |
+
xml_parts.append(f'<value>{rmag_min}</value>')
|
| 181 |
+
xml_parts.append('</filter>')
|
| 182 |
elif rmag_max is not None:
|
| 183 |
+
xml_parts.append('<filter>')
|
| 184 |
+
xml_parts.append('<column>r_mag</column>')
|
| 185 |
+
xml_parts.append('<operator>lte</operator>')
|
| 186 |
+
xml_parts.append(f'<value>{rmag_max}</value>')
|
| 187 |
+
xml_parts.append('</filter>')
|
| 188 |
+
xml_parts.append('</filters>')
|
| 189 |
|
| 190 |
xml_parts.append('</query>')
|
| 191 |
+
return ''.join(xml_parts)
|
| 192 |
|
| 193 |
|
| 194 |
def log_query_to_csv(
|
| 195 |
query_xml: str,
|
| 196 |
+
csv_path: str = "logs/query_log.csv",
|
| 197 |
+
request_id: Optional[str] = None,
|
| 198 |
+
error_occurred: bool = False,
|
| 199 |
+
error_message: Optional[str] = None,
|
| 200 |
+
error_type: Optional[str] = None
|
| 201 |
) -> None:
|
| 202 |
+
"""Print query XML to terminal and log to HF dataset.
|
| 203 |
|
| 204 |
Args:
|
| 205 |
query_xml: XML string representation of the query
|
| 206 |
csv_path: Deprecated parameter (kept for backward compatibility)
|
| 207 |
+
request_id: Unique ID for this request
|
| 208 |
+
error_occurred: Whether an error occurred during search
|
| 209 |
+
error_message: Error message if error_occurred is True
|
| 210 |
+
error_type: Type of error if error_occurred is True
|
| 211 |
"""
|
| 212 |
timestamp = datetime.now().isoformat()
|
| 213 |
|
|
|
|
| 219 |
print("="*80 + "\n")
|
| 220 |
|
| 221 |
logger.info(f"Query printed to terminal")
|
| 222 |
+
|
| 223 |
+
# Log to HF dataset
|
| 224 |
+
try:
|
| 225 |
+
payload = {
|
| 226 |
+
"log_type": "aql_query",
|
| 227 |
+
"timestamp": timestamp,
|
| 228 |
+
"query_xml": query_xml,
|
| 229 |
+
"error_occurred": error_occurred,
|
| 230 |
+
}
|
| 231 |
+
if request_id:
|
| 232 |
+
payload["request_id"] = request_id
|
| 233 |
+
if error_occurred:
|
| 234 |
+
payload["error_message"] = error_message
|
| 235 |
+
payload["error_type"] = error_type
|
| 236 |
+
|
| 237 |
+
log_query_event(payload)
|
| 238 |
+
except Exception as e:
|
| 239 |
+
logger.error(f"Failed to send query log to HF dataset: {e}")
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def log_click_event(
|
| 243 |
+
request_id: Optional[str],
|
| 244 |
+
rank: int,
|
| 245 |
+
primary_key: str,
|
| 246 |
+
ra: float,
|
| 247 |
+
dec: float,
|
| 248 |
+
r_mag: float,
|
| 249 |
+
distance: float
|
| 250 |
+
) -> None:
|
| 251 |
+
"""Log a galaxy tile click event to HF dataset.
|
| 252 |
+
|
| 253 |
+
Args:
|
| 254 |
+
request_id: Unique ID for the search request that produced this galaxy
|
| 255 |
+
rank: Position in search results (0-indexed)
|
| 256 |
+
primary_key: Primary key of the clicked galaxy
|
| 257 |
+
ra: Right ascension
|
| 258 |
+
dec: Declination
|
| 259 |
+
r_mag: r-band magnitude
|
| 260 |
+
distance: Cosine similarity score
|
| 261 |
+
"""
|
| 262 |
+
try:
|
| 263 |
+
payload = {
|
| 264 |
+
"log_type": "click_event",
|
| 265 |
+
"rank": rank,
|
| 266 |
+
"primary_key": primary_key,
|
| 267 |
+
"ra": ra,
|
| 268 |
+
"dec": dec,
|
| 269 |
+
"r_mag": r_mag,
|
| 270 |
+
"distance": distance,
|
| 271 |
+
}
|
| 272 |
+
if request_id:
|
| 273 |
+
payload["request_id"] = request_id
|
| 274 |
+
|
| 275 |
+
log_query_event(payload)
|
| 276 |
+
logger.info(f"Logged click event: rank={rank}, primary_key={primary_key}")
|
| 277 |
+
except Exception as e:
|
| 278 |
+
logger.error(f"Failed to log click event: {e}")
|