vector-inspector 0.2.6__py3-none-any.whl → 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. vector_inspector/config/__init__.py +4 -0
  2. vector_inspector/config/known_embedding_models.json +432 -0
  3. vector_inspector/core/cache_manager.py +159 -0
  4. vector_inspector/core/connection_manager.py +277 -0
  5. vector_inspector/core/connections/__init__.py +2 -1
  6. vector_inspector/core/connections/base_connection.py +42 -1
  7. vector_inspector/core/connections/chroma_connection.py +137 -16
  8. vector_inspector/core/connections/pinecone_connection.py +768 -0
  9. vector_inspector/core/connections/qdrant_connection.py +62 -8
  10. vector_inspector/core/embedding_providers/__init__.py +14 -0
  11. vector_inspector/core/embedding_providers/base_provider.py +128 -0
  12. vector_inspector/core/embedding_providers/clip_provider.py +260 -0
  13. vector_inspector/core/embedding_providers/provider_factory.py +176 -0
  14. vector_inspector/core/embedding_providers/sentence_transformer_provider.py +203 -0
  15. vector_inspector/core/embedding_utils.py +167 -0
  16. vector_inspector/core/model_registry.py +205 -0
  17. vector_inspector/services/backup_restore_service.py +19 -29
  18. vector_inspector/services/credential_service.py +130 -0
  19. vector_inspector/services/filter_service.py +1 -1
  20. vector_inspector/services/profile_service.py +409 -0
  21. vector_inspector/services/settings_service.py +136 -1
  22. vector_inspector/ui/components/connection_manager_panel.py +327 -0
  23. vector_inspector/ui/components/profile_manager_panel.py +565 -0
  24. vector_inspector/ui/dialogs/__init__.py +6 -0
  25. vector_inspector/ui/dialogs/cross_db_migration.py +383 -0
  26. vector_inspector/ui/dialogs/embedding_config_dialog.py +315 -0
  27. vector_inspector/ui/dialogs/provider_type_dialog.py +189 -0
  28. vector_inspector/ui/main_window.py +456 -190
  29. vector_inspector/ui/views/connection_view.py +55 -10
  30. vector_inspector/ui/views/info_panel.py +272 -55
  31. vector_inspector/ui/views/metadata_view.py +71 -3
  32. vector_inspector/ui/views/search_view.py +44 -4
  33. vector_inspector/ui/views/visualization_view.py +19 -5
  34. {vector_inspector-0.2.6.dist-info → vector_inspector-0.3.1.dist-info}/METADATA +3 -1
  35. vector_inspector-0.3.1.dist-info/RECORD +55 -0
  36. vector_inspector-0.2.6.dist-info/RECORD +0 -35
  37. {vector_inspector-0.2.6.dist-info → vector_inspector-0.3.1.dist-info}/WHEEL +0 -0
  38. {vector_inspector-0.2.6.dist-info → vector_inspector-0.3.1.dist-info}/entry_points.txt +0 -0
@@ -10,6 +10,7 @@ from PySide6.QtCore import Signal, QThread
10
10
  from vector_inspector.core.connections.base_connection import VectorDBConnection
11
11
  from vector_inspector.core.connections.chroma_connection import ChromaDBConnection
12
12
  from vector_inspector.core.connections.qdrant_connection import QdrantConnection
13
+ from vector_inspector.core.connections.pinecone_connection import PineconeConnection
13
14
  from vector_inspector.ui.components.loading_dialog import LoadingDialog
14
15
  from vector_inspector.services.settings_service import SettingsService
15
16
 
@@ -66,6 +67,7 @@ class ConnectionDialog(QDialog):
66
67
  self.provider_combo = QComboBox()
67
68
  self.provider_combo.addItem("ChromaDB", "chromadb")
68
69
  self.provider_combo.addItem("Qdrant", "qdrant")
70
+ self.provider_combo.addItem("Pinecone", "pinecone")
69
71
  self.provider_combo.currentIndexChanged.connect(self._on_provider_changed)
70
72
  provider_layout.addWidget(self.provider_combo)
71
73
  provider_group.setLayout(provider_layout)
@@ -179,19 +181,42 @@ class ConnectionDialog(QDialog):
179
181
  if self.port_input.text() == "6333":
180
182
  self.port_input.setText("8000")
181
183
 
182
- # Show/hide API key field
183
- is_http = self.http_radio.isChecked()
184
- self.api_key_input.setEnabled(is_http and self.provider == "qdrant")
184
+ # For Pinecone, hide persistent/HTTP options and only show API key
185
+ if self.provider == "pinecone":
186
+ self.persistent_radio.setEnabled(False)
187
+ self.http_radio.setEnabled(True)
188
+ self.http_radio.setChecked(True)
189
+ self.ephemeral_radio.setEnabled(False)
190
+ self.path_input.setEnabled(False)
191
+ self.host_input.setEnabled(False)
192
+ self.port_input.setEnabled(False)
193
+ self.api_key_input.setEnabled(True)
194
+ else:
195
+ self.persistent_radio.setEnabled(True)
196
+ self.http_radio.setEnabled(True)
197
+ self.ephemeral_radio.setEnabled(True)
198
+ # Show/hide API key field
199
+ is_http = self.http_radio.isChecked()
200
+ self.api_key_input.setEnabled(is_http and self.provider == "qdrant")
201
+ # Update path/host/port based on connection type
202
+ self._on_type_changed()
185
203
 
186
204
  def _on_type_changed(self):
187
205
  """Handle connection type change."""
188
206
  is_persistent = self.persistent_radio.isChecked()
189
207
  is_http = self.http_radio.isChecked()
190
208
 
191
- self.path_input.setEnabled(is_persistent)
192
- self.host_input.setEnabled(is_http)
193
- self.port_input.setEnabled(is_http)
194
- self.api_key_input.setEnabled(is_http and self.provider == "qdrant")
209
+ # Pinecone always uses API key, no path/host/port
210
+ if self.provider == "pinecone":
211
+ self.path_input.setEnabled(False)
212
+ self.host_input.setEnabled(False)
213
+ self.port_input.setEnabled(False)
214
+ self.api_key_input.setEnabled(True)
215
+ else:
216
+ self.path_input.setEnabled(is_persistent)
217
+ self.host_input.setEnabled(is_http)
218
+ self.port_input.setEnabled(is_http)
219
+ self.api_key_input.setEnabled(is_http and self.provider == "qdrant")
195
220
 
196
221
  self._update_absolute_preview()
197
222
 
@@ -202,7 +227,13 @@ class ConnectionDialog(QDialog):
202
227
 
203
228
  config = {"provider": self.provider}
204
229
 
205
- if self.persistent_radio.isChecked():
230
+ # Pinecone only needs API key
231
+ if self.provider == "pinecone":
232
+ config.update({
233
+ "type": "cloud",
234
+ "api_key": self.api_key_input.text()
235
+ })
236
+ elif self.persistent_radio.isChecked():
206
237
  config.update({"type": "persistent", "path": self.path_input.text()})
207
238
  elif self.http_radio.isChecked():
208
239
  config.update({
@@ -286,7 +317,13 @@ class ConnectionDialog(QDialog):
286
317
 
287
318
  # Set connection type
288
319
  conn_type = last_config.get("type", "persistent")
289
- if conn_type == "persistent":
320
+ if conn_type == "cloud":
321
+ # Pinecone cloud connection
322
+ self.http_radio.setChecked(True)
323
+ api_key = last_config.get("api_key")
324
+ if api_key:
325
+ self.api_key_input.setText(api_key)
326
+ elif conn_type == "persistent":
290
327
  self.persistent_radio.setChecked(True)
291
328
  path = last_config.get("path", "")
292
329
  if path:
@@ -370,7 +407,15 @@ class ConnectionView(QWidget):
370
407
  conn_type = config.get("type")
371
408
 
372
409
  # Create appropriate connection instance based on provider
373
- if provider == "qdrant":
410
+ if provider == "pinecone":
411
+ api_key = config.get("api_key")
412
+ if not api_key:
413
+ self.loading_dialog.hide_loading()
414
+ from PySide6.QtWidgets import QMessageBox
415
+ QMessageBox.warning(self, "Missing API Key", "Pinecone requires an API key to connect.")
416
+ return
417
+ self.connection = PineconeConnection(api_key=api_key)
418
+ elif provider == "qdrant":
374
419
  if conn_type == "persistent":
375
420
  self.connection = QdrantConnection(path=config.get("path"))
376
421
  elif conn_type == "http":
@@ -3,13 +3,17 @@
3
3
  from typing import Optional, Dict, Any
4
4
  from PySide6.QtWidgets import (
5
5
  QWidget, QVBoxLayout, QHBoxLayout, QLabel,
6
- QGroupBox, QScrollArea, QFrame
6
+ QGroupBox, QScrollArea, QFrame, QPushButton
7
7
  )
8
8
  from PySide6.QtCore import Qt, QObject
9
+ from PySide6.QtWidgets import QDialog
10
+ from PySide6.QtWidgets import QApplication
9
11
 
10
12
  from vector_inspector.core.connections.base_connection import VectorDBConnection
11
13
  from vector_inspector.core.connections.chroma_connection import ChromaDBConnection
12
14
  from vector_inspector.core.connections.qdrant_connection import QdrantConnection
15
+ from vector_inspector.core.connections.pinecone_connection import PineconeConnection
16
+ from vector_inspector.core.cache_manager import get_cache_manager
13
17
 
14
18
 
15
19
  class InfoPanel(QWidget):
@@ -18,7 +22,10 @@ class InfoPanel(QWidget):
18
22
  def __init__(self, connection: VectorDBConnection, parent=None):
19
23
  super().__init__(parent)
20
24
  self.connection = connection
25
+ self.connection_id: str = "" # Will be set when collection is set
21
26
  self.current_collection: str = ""
27
+ self.current_database: str = ""
28
+ self.cache_manager = get_cache_manager()
22
29
  self._setup_ui()
23
30
 
24
31
  def _setup_ui(self):
@@ -65,10 +72,31 @@ class InfoPanel(QWidget):
65
72
  self.distance_metric_label = self._create_info_row("Distance Metric:", "N/A")
66
73
  self.total_points_label = self._create_info_row("Total Points:", "0")
67
74
 
75
+ # Embedding model row with configure button
76
+ embedding_row = QWidget()
77
+ embedding_layout = QHBoxLayout(embedding_row)
78
+ embedding_layout.setContentsMargins(0, 2, 0, 2)
79
+
80
+ embedding_label = QLabel("<b>Embedding Model:</b>")
81
+ embedding_label.setMinimumWidth(150)
82
+ self.embedding_model_label = QLabel("Auto-detect")
83
+ self.embedding_model_label.setStyleSheet("color: gray;")
84
+ self.embedding_model_label.setWordWrap(True)
85
+
86
+ self.configure_embedding_btn = QPushButton("Configure...")
87
+ self.configure_embedding_btn.setMaximumWidth(100)
88
+ self.configure_embedding_btn.clicked.connect(self._configure_embedding_model)
89
+ self.configure_embedding_btn.setEnabled(False)
90
+
91
+ embedding_layout.addWidget(embedding_label)
92
+ embedding_layout.addWidget(self.embedding_model_label, 1)
93
+ embedding_layout.addWidget(self.configure_embedding_btn)
94
+
68
95
  collection_layout.addWidget(self.collection_name_label)
69
96
  collection_layout.addWidget(self.vector_dim_label)
70
97
  collection_layout.addWidget(self.distance_metric_label)
71
98
  collection_layout.addWidget(self.total_points_label)
99
+ collection_layout.addWidget(embedding_row)
72
100
 
73
101
  # Payload Schema subsection
74
102
  schema_label = QLabel("<b>Payload Schema:</b>")
@@ -173,6 +201,15 @@ class InfoPanel(QWidget):
173
201
  self._update_label(self.api_key_label, "Present (hidden)")
174
202
  else:
175
203
  self._update_label(self.api_key_label, "Not configured")
204
+
205
+ elif isinstance(self.connection, PineconeConnection):
206
+ self._update_label(self.connection_type_label, "Cloud")
207
+ self._update_label(self.endpoint_label, "Pinecone Cloud")
208
+
209
+ if self.connection.api_key:
210
+ self._update_label(self.api_key_label, "Present (hidden)")
211
+ else:
212
+ self._update_label(self.api_key_label, "Not configured")
176
213
  else:
177
214
  self._update_label(self.connection_type_label, "Unknown")
178
215
  self._update_label(self.endpoint_label, "N/A")
@@ -200,7 +237,7 @@ class InfoPanel(QWidget):
200
237
  return
201
238
 
202
239
  try:
203
- # Get collection info
240
+ # Get collection info from database
204
241
  collection_info = self.connection.get_collection_info(self.current_collection)
205
242
 
206
243
  if not collection_info:
@@ -212,59 +249,18 @@ class InfoPanel(QWidget):
212
249
  self.provider_details_label.setText("N/A")
213
250
  return
214
251
 
215
- # Update basic info
216
- self._update_label(self.collection_name_label, self.current_collection)
217
-
218
- # Vector dimension
219
- vector_dim = collection_info.get("vector_dimension", "Unknown")
220
- self._update_label(self.vector_dim_label, str(vector_dim))
221
-
222
- # Distance metric
223
- distance = collection_info.get("distance_metric", "Unknown")
224
- self._update_label(self.distance_metric_label, distance)
225
-
226
- # Total points
227
- count = collection_info.get("count", 0)
228
- self._update_label(self.total_points_label, f"{count:,}")
229
-
230
- # Metadata schema
231
- metadata_fields = collection_info.get("metadata_fields", [])
232
- if metadata_fields:
233
- schema_text = "\n".join([f"• {field}" for field in sorted(metadata_fields)])
234
- self.schema_label.setText(schema_text)
235
- self.schema_label.setStyleSheet("color: white; padding-left: 20px; font-family: monospace;")
236
- else:
237
- self.schema_label.setText("No metadata fields found")
238
- self.schema_label.setStyleSheet("color: gray; padding-left: 20px;")
239
-
240
- # Provider-specific details
241
- details_list = []
242
-
243
- if isinstance(self.connection, ChromaDBConnection):
244
- details_list.append("• Provider: ChromaDB")
245
- details_list.append("• Supports: Documents, Metadata, Embeddings")
246
- details_list.append("• Default embedding: all-MiniLM-L6-v2")
247
-
248
- elif isinstance(self.connection, QdrantConnection):
249
- details_list.append("• Provider: Qdrant")
250
- details_list.append("• Supports: Points, Payload, Vectors")
251
- # Get additional Qdrant-specific info if available
252
- if "config" in collection_info:
253
- config = collection_info["config"]
254
- if "hnsw_config" in config:
255
- hnsw = config["hnsw_config"]
256
- details_list.append(f"• HNSW M: {hnsw.get('m', 'N/A')}")
257
- details_list.append(f"• HNSW ef_construct: {hnsw.get('ef_construct', 'N/A')}")
258
- if "optimizer_config" in config:
259
- opt = config["optimizer_config"]
260
- details_list.append(f"• Indexing threshold: {opt.get('indexing_threshold', 'N/A')}")
252
+ # Display the info
253
+ self._display_collection_info(collection_info)
261
254
 
262
- if details_list:
263
- self.provider_details_label.setText("\n".join(details_list))
264
- self.provider_details_label.setStyleSheet("color: white; padding-left: 20px; font-family: monospace;")
265
- else:
266
- self.provider_details_label.setText("No additional details available")
267
- self.provider_details_label.setStyleSheet("color: gray; padding-left: 20px;")
255
+ # Save to cache
256
+ if self.current_database and self.current_collection:
257
+ print(f"[InfoPanel] Saving collection info to cache: db='{self.current_database}', coll='{self.current_collection}'")
258
+ self.cache_manager.update(
259
+ self.current_database,
260
+ self.current_collection,
261
+ user_inputs={'collection_info': collection_info}
262
+ )
263
+ print(f"[InfoPanel] ✓ Saved collection info to cache.")
268
264
 
269
265
  except Exception as e:
270
266
  self._update_label(self.collection_name_label, self.current_collection)
@@ -275,9 +271,101 @@ class InfoPanel(QWidget):
275
271
  self.schema_label.setStyleSheet("color: red; padding-left: 20px;")
276
272
  self.provider_details_label.setText("N/A")
277
273
 
278
- def set_collection(self, collection_name: str):
274
+ def _display_collection_info(self, collection_info: Dict[str, Any]):
275
+ """Display collection information (from cache or fresh query)."""
276
+ # Update basic info
277
+ self._update_label(self.collection_name_label, self.current_collection)
278
+
279
+ # Vector dimension
280
+ vector_dim = collection_info.get("vector_dimension", "Unknown")
281
+ self._update_label(self.vector_dim_label, str(vector_dim))
282
+
283
+ # Enable configure button if we have a valid dimension
284
+ self.configure_embedding_btn.setEnabled(
285
+ vector_dim != "Unknown" and isinstance(vector_dim, int)
286
+ )
287
+
288
+ # Update embedding model display
289
+ self._update_embedding_model_display(collection_info)
290
+
291
+ # Distance metric
292
+ distance = collection_info.get("distance_metric", "Unknown")
293
+ self._update_label(self.distance_metric_label, distance)
294
+
295
+ # Total points
296
+ count = collection_info.get("count", 0)
297
+ self._update_label(self.total_points_label, f"{count:,}")
298
+
299
+ # Metadata schema
300
+ metadata_fields = collection_info.get("metadata_fields", [])
301
+ if metadata_fields:
302
+ schema_text = "\n".join([f"• {field}" for field in sorted(metadata_fields)])
303
+ self.schema_label.setText(schema_text)
304
+ self.schema_label.setStyleSheet("color: white; padding-left: 20px; font-family: monospace;")
305
+ else:
306
+ self.schema_label.setText("No metadata fields found")
307
+ self.schema_label.setStyleSheet("color: gray; padding-left: 20px;")
308
+
309
+ # Provider-specific details
310
+ details_list = []
311
+
312
+ if isinstance(self.connection, ChromaDBConnection):
313
+ details_list.append("• Provider: ChromaDB")
314
+ details_list.append("• Supports: Documents, Metadata, Embeddings")
315
+ details_list.append("• Default embedding: all-MiniLM-L6-v2")
316
+
317
+ elif isinstance(self.connection, QdrantConnection):
318
+ details_list.append("• Provider: Qdrant")
319
+ details_list.append("• Supports: Points, Payload, Vectors")
320
+ # Get additional Qdrant-specific info if available
321
+ if "config" in collection_info:
322
+ config = collection_info["config"]
323
+ if "hnsw_config" in config:
324
+ hnsw = config["hnsw_config"]
325
+ details_list.append(f"• HNSW M: {hnsw.get('m', 'N/A')}")
326
+ details_list.append(f"• HNSW ef_construct: {hnsw.get('ef_construct', 'N/A')}")
327
+ if "optimizer_config" in config:
328
+ opt = config["optimizer_config"]
329
+ details_list.append(f"• Indexing threshold: {opt.get('indexing_threshold', 'N/A')}")
330
+
331
+ elif isinstance(self.connection, PineconeConnection):
332
+ details_list.append("• Provider: Pinecone")
333
+ details_list.append("• Supports: Vectors, Metadata")
334
+ details_list.append("• Cloud-hosted vector database")
335
+ # Add Pinecone-specific info if available
336
+ if "host" in collection_info:
337
+ details_list.append(f"• Host: {collection_info['host']}")
338
+ if "status" in collection_info:
339
+ details_list.append(f"• Status: {collection_info['status']}")
340
+ if "spec" in collection_info:
341
+ details_list.append(f"• Spec: {collection_info['spec']}")
342
+
343
+ if details_list:
344
+ self.provider_details_label.setText("\n".join(details_list))
345
+ self.provider_details_label.setStyleSheet("color: white; padding-left: 20px; font-family: monospace;")
346
+ else:
347
+ self.provider_details_label.setText("No additional details available")
348
+ self.provider_details_label.setStyleSheet("color: gray; padding-left: 20px;")
349
+
350
+ def set_collection(self, collection_name: str, database_name: str = ""):
279
351
  """Set the current collection and refresh its information."""
280
352
  self.current_collection = collection_name
353
+ # Always update database_name if provided
354
+ if database_name:
355
+ self.current_database = database_name
356
+ self.connection_id = database_name # database_name is the connection ID
357
+
358
+ print(f"[InfoPanel] Setting collection: db='{self.current_database}', coll='{collection_name}'")
359
+
360
+ # Check cache first for collection info
361
+ cached = self.cache_manager.get(self.current_database, self.current_collection)
362
+ if cached and hasattr(cached, 'user_inputs') and cached.user_inputs.get('collection_info'):
363
+ print(f"[InfoPanel] ✓ Cache HIT! Loading collection info from cache.")
364
+ collection_info = cached.user_inputs['collection_info']
365
+ self._display_collection_info(collection_info)
366
+ return
367
+
368
+ print(f"[InfoPanel] ✗ Cache MISS. Loading collection info from database...")
281
369
  self.refresh_collection_info()
282
370
 
283
371
  def _update_label(self, row_widget: QWidget, value: str):
@@ -285,3 +373,132 @@ class InfoPanel(QWidget):
285
373
  value_label = row_widget.property("value_label")
286
374
  if value_label and isinstance(value_label, QLabel):
287
375
  value_label.setText(value)
376
+
377
+ def _update_embedding_model_display(self, collection_info: Dict[str, Any]):
378
+ """Update the embedding model label based on current configuration."""
379
+ from ...services.settings_service import SettingsService
380
+
381
+ # Check if stored in collection metadata
382
+ if 'embedding_model' in collection_info:
383
+ model_name = collection_info['embedding_model']
384
+ model_type = collection_info.get('embedding_model_type', 'stored')
385
+ self.embedding_model_label.setText(f"{model_name} ({model_type})")
386
+ self.embedding_model_label.setStyleSheet("color: lightgreen;")
387
+ return
388
+
389
+ # Try to get from connection using the helper method
390
+ if self.connection and self.current_collection:
391
+ detected_model = self.connection.get_embedding_model(self.current_collection, self.connection_id)
392
+ if detected_model:
393
+ self.embedding_model_label.setText(f"{detected_model} (detected)")
394
+ self.embedding_model_label.setStyleSheet("color: lightgreen;")
395
+ return
396
+
397
+ # Check user settings
398
+ settings = SettingsService()
399
+ model_info = settings.get_embedding_model(self.connection_id, self.current_collection)
400
+
401
+ if model_info:
402
+ model_name = model_info['model']
403
+ model_type = model_info.get('type', 'unknown')
404
+ self.embedding_model_label.setText(f"{model_name} ({model_type})")
405
+ self.embedding_model_label.setStyleSheet("color: lightblue;")
406
+ return
407
+
408
+ # No configuration - using auto-detect
409
+ self.embedding_model_label.setText("Auto-detect (dimension-based)")
410
+ self.embedding_model_label.setStyleSheet("color: orange;")
411
+
412
+ def _configure_embedding_model(self):
413
+ """Open dialog to configure embedding model for current collection."""
414
+ if not self.current_collection:
415
+ return
416
+
417
+ # Show loading immediately; preparing can touch DB/registry
418
+ from ..components.loading_dialog import LoadingDialog
419
+ loading = LoadingDialog("Preparing model configuration...", self)
420
+ loading.show_loading("Preparing model configuration...")
421
+ QApplication.processEvents()
422
+
423
+ from ..dialogs import ProviderTypeDialog, EmbeddingConfigDialog
424
+ from ...services.settings_service import SettingsService
425
+
426
+ # Get current collection info
427
+ try:
428
+ collection_info = self.connection.get_collection_info(self.current_collection)
429
+ finally:
430
+ # Hide loading before presenting dialogs
431
+ loading.hide_loading()
432
+ if not collection_info:
433
+ return
434
+
435
+ vector_dim = collection_info.get("vector_dimension")
436
+ if not vector_dim or vector_dim == "Unknown":
437
+ return
438
+
439
+ # Get current configuration if any
440
+ settings = SettingsService()
441
+
442
+ current_model = None
443
+ current_type = None
444
+
445
+ # Check metadata first
446
+ if 'embedding_model' in collection_info:
447
+ current_model = collection_info['embedding_model']
448
+ current_type = collection_info.get('embedding_model_type', 'stored')
449
+ # Then check settings
450
+ else:
451
+ model_info = settings.get_embedding_model(self.connection_id, self.current_collection)
452
+ if model_info:
453
+ current_model = model_info.get('model')
454
+ current_type = model_info.get('type')
455
+
456
+ # Step 1: Provider Type Selection
457
+ type_dialog = ProviderTypeDialog(
458
+ self.current_collection,
459
+ vector_dim,
460
+ self
461
+ )
462
+
463
+ type_result = type_dialog.exec()
464
+ if type_result != QDialog.DialogCode.Accepted:
465
+ return # User cancelled
466
+
467
+ provider_type = type_dialog.get_selected_type()
468
+ if not provider_type:
469
+ return
470
+
471
+ # Step 2: Model Selection (filtered by provider type)
472
+ model_dialog = EmbeddingConfigDialog(
473
+ self.current_collection,
474
+ vector_dim,
475
+ provider_type,
476
+ current_model,
477
+ current_type,
478
+ self
479
+ )
480
+
481
+ # Optionally show brief loading while populating models
482
+ # (dialog itself handles content; only show if provider lists are large)
483
+ result = model_dialog.exec()
484
+
485
+ if result == QDialog.DialogCode.Accepted:
486
+ # Save the configuration using the new SettingsService method
487
+ selection = model_dialog.get_selection()
488
+ if selection:
489
+ model_name, model_type = selection
490
+ settings.save_embedding_model(self.connection_id, self.current_collection, model_name, model_type)
491
+
492
+ # Refresh display
493
+ self._update_embedding_model_display(collection_info)
494
+
495
+ print(f"✓ Configured embedding model for '{self.current_collection}': {model_name} ({model_type})")
496
+
497
+ elif result == 2: # Clear configuration
498
+ # Remove from settings using the new SettingsService method
499
+ settings.remove_embedding_model(self.connection_id, self.current_collection)
500
+
501
+ # Refresh display
502
+ self._update_embedding_model_display(collection_info)
503
+
504
+ print(f"✓ Cleared embedding model configuration for '{self.current_collection}'")
@@ -16,6 +16,7 @@ from vector_inspector.ui.components.filter_builder import FilterBuilder
16
16
  from vector_inspector.services.import_export_service import ImportExportService
17
17
  from vector_inspector.services.filter_service import apply_client_side_filters
18
18
  from vector_inspector.services.settings_service import SettingsService
19
+ from vector_inspector.core.cache_manager import get_cache_manager, CacheEntry
19
20
  from PySide6.QtWidgets import QApplication
20
21
 
21
22
 
@@ -57,12 +58,14 @@ class MetadataView(QWidget):
57
58
  super().__init__(parent)
58
59
  self.connection = connection
59
60
  self.current_collection: str = ""
61
+ self.current_database: str = ""
60
62
  self.current_data: Optional[Dict[str, Any]] = None
61
63
  self.page_size = 50
62
64
  self.current_page = 0
63
65
  self.loading_dialog = LoadingDialog("Loading data...", self)
64
66
  self.settings_service = SettingsService()
65
67
  self.load_thread: Optional[DataLoadThread] = None
68
+ self.cache_manager = get_cache_manager()
66
69
 
67
70
  # Debounce timer for filter changes
68
71
  self.filter_reload_timer = QTimer()
@@ -107,8 +110,9 @@ class MetadataView(QWidget):
107
110
  controls_layout.addStretch()
108
111
 
109
112
  # Refresh button
110
- self.refresh_button = QPushButton("Refresh")
111
- self.refresh_button.clicked.connect(self._load_data)
113
+ self.refresh_button = QPushButton("🔄 Refresh")
114
+ self.refresh_button.clicked.connect(self._refresh_data)
115
+ self.refresh_button.setToolTip("Refresh data and clear cache")
112
116
  controls_layout.addWidget(self.refresh_button)
113
117
 
114
118
  # Add/Delete buttons
@@ -172,9 +176,40 @@ class MetadataView(QWidget):
172
176
  self.status_label.setStyleSheet("color: gray;")
173
177
  layout.addWidget(self.status_label)
174
178
 
175
- def set_collection(self, collection_name: str):
179
+ def set_collection(self, collection_name: str, database_name: str = ""):
176
180
  """Set the current collection to display."""
177
181
  self.current_collection = collection_name
182
+ # Always update database_name if provided (even if empty string on first call)
183
+ if database_name: # Only update if non-empty
184
+ self.current_database = database_name
185
+
186
+ # Debug: Check cache status
187
+ print(f"[MetadataView] Setting collection: db='{self.current_database}', coll='{collection_name}'")
188
+ print(f"[MetadataView] Cache enabled: {self.cache_manager.is_enabled()}")
189
+
190
+ # Check cache first
191
+ cached = self.cache_manager.get(self.current_database, self.current_collection)
192
+ if cached and cached.data:
193
+ print(f"[MetadataView] ✓ Cache HIT! Loading from cache.")
194
+ # Restore from cache
195
+ self.current_page = 0
196
+ self.current_data = cached.data
197
+ self._populate_table(cached.data)
198
+ self._update_pagination_controls()
199
+ self._update_filter_fields(cached.data)
200
+
201
+ # Restore UI state
202
+ if cached.scroll_position:
203
+ self.table.verticalScrollBar().setValue(cached.scroll_position)
204
+ if cached.search_query:
205
+ # Restore filter state if applicable
206
+ pass
207
+
208
+ self.status_label.setText(f"✓ Loaded from cache - {len(cached.data.get('ids', []))} items")
209
+ return
210
+
211
+ print(f"[MetadataView] ✗ Cache MISS. Loading from database...")
212
+ # Not in cache, load from database
178
213
  self.current_page = 0
179
214
 
180
215
  # Update filter builder with supported operators
@@ -246,6 +281,19 @@ class MetadataView(QWidget):
246
281
 
247
282
  # Update filter builder with available metadata fields
248
283
  self._update_filter_fields(data)
284
+
285
+ # Save to cache
286
+ if self.current_database and self.current_collection:
287
+ print(f"[MetadataView] Saving to cache: db='{self.current_database}', coll='{self.current_collection}'")
288
+ cache_entry = CacheEntry(
289
+ data=data,
290
+ scroll_position=self.table.verticalScrollBar().value(),
291
+ search_query=self.filter_builder.to_dict() if hasattr(self.filter_builder, 'to_dict') else ""
292
+ )
293
+ self.cache_manager.set(self.current_database, self.current_collection, cache_entry)
294
+ print(f"[MetadataView] ✓ Saved to cache. Total entries: {len(self.cache_manager._cache)}")
295
+ else:
296
+ print(f"[MetadataView] ✗ NOT saving to cache - db='{self.current_database}', coll='{self.current_collection}'")
249
297
 
250
298
  def _on_load_error(self, error_msg: str):
251
299
  """Handle error from background thread."""
@@ -365,6 +413,9 @@ class MetadataView(QWidget):
365
413
  )
366
414
 
367
415
  if success:
416
+ # Invalidate cache after adding item
417
+ if self.current_database and self.current_collection:
418
+ self.cache_manager.invalidate(self.current_database, self.current_collection)
368
419
  QMessageBox.information(self, "Success", "Item added successfully.")
369
420
  self._load_data()
370
421
  else:
@@ -399,6 +450,9 @@ class MetadataView(QWidget):
399
450
  if reply == QMessageBox.Yes:
400
451
  success = self.connection.delete_items(self.current_collection, ids=ids_to_delete)
401
452
  if success:
453
+ # Invalidate cache after deletion
454
+ if self.current_database and self.current_collection:
455
+ self.cache_manager.invalidate(self.current_database, self.current_collection)
402
456
  QMessageBox.information(self, "Success", "Items deleted successfully.")
403
457
  self._load_data()
404
458
  else:
@@ -422,6 +476,13 @@ class MetadataView(QWidget):
422
476
  self.current_page = 0
423
477
  self._load_data()
424
478
 
479
+ def _refresh_data(self):
480
+ """Refresh data and invalidate cache."""
481
+ if self.current_database and self.current_collection:
482
+ self.cache_manager.invalidate(self.current_database, self.current_collection)
483
+ self.current_page = 0
484
+ self._load_data()
485
+
425
486
  def _on_row_double_clicked(self, index):
426
487
  """Handle double-click on a row to edit item."""
427
488
  if not self.current_collection or not self.current_data:
@@ -462,6 +523,9 @@ class MetadataView(QWidget):
462
523
  )
463
524
 
464
525
  if success:
526
+ # Invalidate cache after updating item
527
+ if self.current_database and self.current_collection:
528
+ self.cache_manager.invalidate(self.current_database, self.current_collection)
465
529
  QMessageBox.information(self, "Success", "Item updated successfully.")
466
530
  self._load_data()
467
531
  else:
@@ -653,6 +717,10 @@ class MetadataView(QWidget):
653
717
  self.loading_dialog.hide_loading()
654
718
 
655
719
  if success:
720
+ # Invalidate cache after import
721
+ if self.current_database and self.current_collection:
722
+ self.cache_manager.invalidate(self.current_database, self.current_collection)
723
+
656
724
  # Save the directory for next time
657
725
  from pathlib import Path
658
726
  self.settings_service.set("last_import_export_dir", str(Path(file_path).parent))