vector-inspector 0.2.5__py3-none-any.whl → 0.2.7__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 (26) hide show
  1. vector_inspector/core/cache_manager.py +159 -0
  2. vector_inspector/core/connection_manager.py +277 -0
  3. vector_inspector/core/connections/chroma_connection.py +90 -5
  4. vector_inspector/core/connections/qdrant_connection.py +62 -8
  5. vector_inspector/core/embedding_utils.py +140 -0
  6. vector_inspector/services/backup_restore_service.py +3 -29
  7. vector_inspector/services/credential_service.py +130 -0
  8. vector_inspector/services/filter_service.py +1 -1
  9. vector_inspector/services/profile_service.py +409 -0
  10. vector_inspector/services/settings_service.py +20 -1
  11. vector_inspector/services/visualization_service.py +11 -7
  12. vector_inspector/ui/components/connection_manager_panel.py +320 -0
  13. vector_inspector/ui/components/profile_manager_panel.py +518 -0
  14. vector_inspector/ui/dialogs/__init__.py +5 -0
  15. vector_inspector/ui/dialogs/cross_db_migration.py +364 -0
  16. vector_inspector/ui/dialogs/embedding_config_dialog.py +176 -0
  17. vector_inspector/ui/main_window.py +429 -181
  18. vector_inspector/ui/views/connection_view.py +43 -8
  19. vector_inspector/ui/views/info_panel.py +226 -80
  20. vector_inspector/ui/views/metadata_view.py +136 -28
  21. vector_inspector/ui/views/search_view.py +43 -3
  22. {vector_inspector-0.2.5.dist-info → vector_inspector-0.2.7.dist-info}/METADATA +5 -3
  23. vector_inspector-0.2.7.dist-info/RECORD +45 -0
  24. vector_inspector-0.2.5.dist-info/RECORD +0 -35
  25. {vector_inspector-0.2.5.dist-info → vector_inspector-0.2.7.dist-info}/WHEEL +0 -0
  26. {vector_inspector-0.2.5.dist-info → vector_inspector-0.2.7.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,364 @@
1
+ """Cross-database operations for migrating data between vector databases."""
2
+
3
+ from typing import Optional, List, Dict, Any
4
+ from pathlib import Path
5
+ import tempfile
6
+ from PySide6.QtWidgets import (
7
+ QDialog, QVBoxLayout, QHBoxLayout, QLabel, QComboBox,
8
+ QPushButton, QProgressBar, QTextEdit, QGroupBox, QFormLayout,
9
+ QSpinBox, QCheckBox, QMessageBox
10
+ )
11
+ from PySide6.QtCore import QThread, Signal
12
+
13
+ from vector_inspector.core.connection_manager import ConnectionManager, ConnectionInstance
14
+ from vector_inspector.services.backup_restore_service import BackupRestoreService
15
+
16
+
17
+ class MigrationThread(QThread):
18
+ """Background thread for migrating data between databases using backup/restore."""
19
+
20
+ progress = Signal(int, str) # progress percentage, status message
21
+ finished = Signal(bool, str) # success, message
22
+
23
+ def __init__(
24
+ self,
25
+ source_conn: ConnectionInstance,
26
+ target_conn: ConnectionInstance,
27
+ source_collection: str,
28
+ target_collection: str,
29
+ include_embeddings: bool
30
+ ):
31
+ super().__init__()
32
+ self.source_conn = source_conn
33
+ self.target_conn = target_conn
34
+ self.source_collection = source_collection
35
+ self.target_collection = target_collection
36
+ self.include_embeddings = include_embeddings
37
+ self._cancelled = False
38
+ self.backup_service = BackupRestoreService()
39
+
40
+ def cancel(self):
41
+ """Cancel the migration."""
42
+ self._cancelled = True
43
+
44
+ def run(self):
45
+ """Run the migration using backup and restore."""
46
+ temp_backup_path = None
47
+ try:
48
+ if self._cancelled:
49
+ self.finished.emit(False, "Migration cancelled by user.")
50
+ return
51
+
52
+ # Ensure connections are active
53
+ if not self.source_conn.connection.is_connected:
54
+ self.finished.emit(False, "Source connection is not active.")
55
+ return
56
+
57
+ if not self.target_conn.connection.is_connected:
58
+ self.finished.emit(False, "Target connection is not active.")
59
+ return
60
+
61
+ # Create temporary directory for backup
62
+ temp_dir = tempfile.mkdtemp(prefix="vector_migration_")
63
+
64
+ # Step 1: Create backup of source collection
65
+ self.progress.emit(10, f"Creating backup of {self.source_collection}...")
66
+
67
+ temp_backup_path = self.backup_service.backup_collection(
68
+ self.source_conn.connection,
69
+ self.source_collection,
70
+ temp_dir,
71
+ include_embeddings=self.include_embeddings
72
+ )
73
+
74
+ if not temp_backup_path:
75
+ self.finished.emit(False, "Failed to create backup.")
76
+ return
77
+
78
+ if self._cancelled:
79
+ self.finished.emit(False, "Migration cancelled by user.")
80
+ return
81
+
82
+ # Step 2: Restore to target collection
83
+ self.progress.emit(50, f"Restoring to {self.target_collection}...")
84
+
85
+ # Verify target connection before restore
86
+ if not self.target_conn.connection.is_connected:
87
+ # Try to reconnect
88
+ if not self.target_conn.connection.connect():
89
+ self.finished.emit(False, "Target connection lost. Please try again.")
90
+ return
91
+
92
+ # Check if target collection exists
93
+ target_exists = self.target_collection in self.target_conn.collections
94
+
95
+ success = self.backup_service.restore_collection(
96
+ self.target_conn.connection,
97
+ temp_backup_path,
98
+ collection_name=self.target_collection,
99
+ overwrite=target_exists
100
+ )
101
+
102
+ if self._cancelled:
103
+ self.finished.emit(False, "Migration cancelled by user.")
104
+ return
105
+
106
+ if success:
107
+ self.progress.emit(100, f"Migration complete!")
108
+ self.finished.emit(True, f"Successfully migrated {self.source_collection} to {self.target_collection}")
109
+ else:
110
+ self.finished.emit(False, "Failed to restore to target collection.")
111
+
112
+ except Exception as e:
113
+ import traceback
114
+ error_details = traceback.format_exc()
115
+ print(f"Migration error details:\n{error_details}")
116
+ self.finished.emit(False, f"Migration error: {str(e)}")
117
+
118
+ finally:
119
+ # Clean up temporary backup file
120
+ if temp_backup_path:
121
+ try:
122
+ Path(temp_backup_path).unlink()
123
+ # Also remove temp directory if empty
124
+ temp_dir = Path(temp_backup_path).parent
125
+ if temp_dir.exists() and not list(temp_dir.iterdir()):
126
+ temp_dir.rmdir()
127
+ except Exception:
128
+ pass # Ignore cleanup errors
129
+
130
+
131
+ class CrossDatabaseMigrationDialog(QDialog):
132
+ """Dialog for migrating data between vector databases."""
133
+
134
+ def __init__(self, connection_manager: ConnectionManager, parent=None):
135
+ super().__init__(parent)
136
+ self.connection_manager = connection_manager
137
+ self.migration_thread: Optional[MigrationThread] = None
138
+
139
+ self.setWindowTitle("Cross-Database Migration")
140
+ self.setMinimumWidth(600)
141
+ self.setMinimumHeight(400)
142
+
143
+ self._setup_ui()
144
+ self._populate_connections()
145
+
146
+ def _setup_ui(self):
147
+ """Setup the UI."""
148
+ layout = QVBoxLayout(self)
149
+
150
+ # Source section
151
+ source_group = QGroupBox("Source")
152
+ source_layout = QFormLayout()
153
+
154
+ self.source_connection_combo = QComboBox()
155
+ self.source_connection_combo.currentIndexChanged.connect(self._on_source_connection_changed)
156
+ source_layout.addRow("Connection:", self.source_connection_combo)
157
+
158
+ self.source_collection_combo = QComboBox()
159
+ source_layout.addRow("Collection:", self.source_collection_combo)
160
+
161
+ source_group.setLayout(source_layout)
162
+ layout.addWidget(source_group)
163
+
164
+ # Target section
165
+ target_group = QGroupBox("Target")
166
+ target_layout = QFormLayout()
167
+
168
+ self.target_connection_combo = QComboBox()
169
+ self.target_connection_combo.currentIndexChanged.connect(self._on_target_connection_changed)
170
+ target_layout.addRow("Connection:", self.target_connection_combo)
171
+
172
+ self.target_collection_combo = QComboBox()
173
+ self.target_collection_combo.setEditable(True)
174
+ target_layout.addRow("Collection:", self.target_collection_combo)
175
+
176
+ self.create_new_check = QCheckBox("Create new collection if it doesn't exist")
177
+ self.create_new_check.setChecked(True)
178
+ target_layout.addRow("", self.create_new_check)
179
+
180
+ target_group.setLayout(target_layout)
181
+ layout.addWidget(target_group)
182
+
183
+ # Options
184
+ options_group = QGroupBox("Options")
185
+ options_layout = QFormLayout()
186
+
187
+ self.include_embeddings_check = QCheckBox("Include Embeddings")
188
+ self.include_embeddings_check.setChecked(True)
189
+ options_layout.addRow("", self.include_embeddings_check)
190
+
191
+ options_group.setLayout(options_layout)
192
+ layout.addWidget(options_group)
193
+
194
+ # Progress section
195
+ self.progress_bar = QProgressBar()
196
+ self.progress_bar.setRange(0, 100)
197
+ self.progress_bar.setValue(0)
198
+ layout.addWidget(self.progress_bar)
199
+
200
+ self.status_text = QTextEdit()
201
+ self.status_text.setReadOnly(True)
202
+ self.status_text.setMaximumHeight(100)
203
+ layout.addWidget(self.status_text)
204
+
205
+ # Buttons
206
+ button_layout = QHBoxLayout()
207
+
208
+ self.start_button = QPushButton("Start Migration")
209
+ self.start_button.clicked.connect(self._start_migration)
210
+ button_layout.addWidget(self.start_button)
211
+
212
+ self.cancel_button = QPushButton("Cancel")
213
+ self.cancel_button.clicked.connect(self._cancel_migration)
214
+ self.cancel_button.setEnabled(False)
215
+ button_layout.addWidget(self.cancel_button)
216
+
217
+ self.close_button = QPushButton("Close")
218
+ self.close_button.clicked.connect(self.close)
219
+ button_layout.addWidget(self.close_button)
220
+
221
+ layout.addLayout(button_layout)
222
+
223
+ def _populate_connections(self):
224
+ """Populate connection dropdowns."""
225
+ connections = self.connection_manager.get_all_connections()
226
+
227
+ self.source_connection_combo.clear()
228
+ self.target_connection_combo.clear()
229
+
230
+ for conn in connections:
231
+ self.source_connection_combo.addItem(conn.get_display_name(), conn.id)
232
+ self.target_connection_combo.addItem(conn.get_display_name(), conn.id)
233
+
234
+ # Populate collections for first connection
235
+ if connections:
236
+ self._on_source_connection_changed(0)
237
+ self._on_target_connection_changed(0)
238
+
239
+ def _on_source_connection_changed(self, index: int):
240
+ """Handle source connection change."""
241
+ connection_id = self.source_connection_combo.currentData()
242
+ if connection_id:
243
+ instance = self.connection_manager.get_connection(connection_id)
244
+ if instance:
245
+ self.source_collection_combo.clear()
246
+ self.source_collection_combo.addItems(instance.collections)
247
+
248
+ def _on_target_connection_changed(self, index: int):
249
+ """Handle target connection change."""
250
+ connection_id = self.target_connection_combo.currentData()
251
+ if connection_id:
252
+ instance = self.connection_manager.get_connection(connection_id)
253
+ if instance:
254
+ self.target_collection_combo.clear()
255
+ self.target_collection_combo.addItems(instance.collections)
256
+
257
+ def _start_migration(self):
258
+ """Start the migration."""
259
+ # Validate selection
260
+ source_conn_id = self.source_connection_combo.currentData()
261
+ target_conn_id = self.target_connection_combo.currentData()
262
+
263
+ if not source_conn_id or not target_conn_id:
264
+ QMessageBox.warning(self, "Invalid Selection", "Please select both source and target connections.")
265
+ return
266
+
267
+ if source_conn_id == target_conn_id:
268
+ source_coll = self.source_collection_combo.currentText()
269
+ target_coll = self.target_collection_combo.currentText()
270
+ if source_coll == target_coll:
271
+ QMessageBox.warning(
272
+ self,
273
+ "Invalid Selection",
274
+ "Source and target cannot be the same collection in the same connection."
275
+ )
276
+ return
277
+
278
+ source_conn = self.connection_manager.get_connection(source_conn_id)
279
+ target_conn = self.connection_manager.get_connection(target_conn_id)
280
+
281
+ if not source_conn or not target_conn:
282
+ QMessageBox.warning(self, "Error", "Failed to get connection instances.")
283
+ return
284
+
285
+ source_collection = self.source_collection_combo.currentText()
286
+ target_collection = self.target_collection_combo.currentText().strip()
287
+
288
+ if not source_collection or not target_collection:
289
+ QMessageBox.warning(self, "Invalid Selection", "Please select both source and target collections.")
290
+ return
291
+
292
+ # Check if target collection exists
293
+ target_exists = target_collection in target_conn.collections
294
+
295
+ # If target doesn't exist and we're not set to create, warn user
296
+ if not target_exists and not self.create_new_check.isChecked():
297
+ QMessageBox.warning(
298
+ self,
299
+ "Collection Does Not Exist",
300
+ f"Target collection '{target_collection}' does not exist.\n"
301
+ "Please check 'Create new collection' to allow automatic creation during migration."
302
+ )
303
+ return
304
+
305
+ # Confirm
306
+ action = "create and migrate" if not target_exists else "migrate"
307
+ reply = QMessageBox.question(
308
+ self,
309
+ "Confirm Migration",
310
+ f"Migrate data from:\n {source_conn.name}/{source_collection}\n"
311
+ f"to:\n {target_conn.name}/{target_collection}\n\n"
312
+ f"This will {action} all data. Continue?",
313
+ QMessageBox.Yes | QMessageBox.No
314
+ )
315
+
316
+ if reply != QMessageBox.Yes:
317
+ return
318
+
319
+ # Start migration thread
320
+ self.migration_thread = MigrationThread(
321
+ source_conn=source_conn,
322
+ target_conn=target_conn,
323
+ source_collection=source_collection,
324
+ target_collection=target_collection,
325
+ include_embeddings=self.include_embeddings_check.isChecked()
326
+ )
327
+
328
+ self.migration_thread.progress.connect(self._on_migration_progress)
329
+ self.migration_thread.finished.connect(self._on_migration_finished)
330
+
331
+ self.start_button.setEnabled(False)
332
+ self.cancel_button.setEnabled(True)
333
+ self.close_button.setEnabled(False)
334
+
335
+ self.status_text.clear()
336
+ self.progress_bar.setValue(0)
337
+
338
+ self.migration_thread.start()
339
+
340
+ def _cancel_migration(self):
341
+ """Cancel the migration."""
342
+ if self.migration_thread:
343
+ self.migration_thread.cancel()
344
+ self.status_text.append("Cancelling migration...")
345
+
346
+ def _on_migration_progress(self, progress: int, message: str):
347
+ """Handle migration progress update."""
348
+ self.progress_bar.setValue(progress)
349
+ self.status_text.append(message)
350
+
351
+ def _on_migration_finished(self, success: bool, message: str):
352
+ """Handle migration completion."""
353
+ self.status_text.append(message)
354
+
355
+ if success:
356
+ QMessageBox.information(self, "Success", message)
357
+ else:
358
+ QMessageBox.warning(self, "Failed", message)
359
+
360
+ self.start_button.setEnabled(True)
361
+ self.cancel_button.setEnabled(False)
362
+ self.close_button.setEnabled(True)
363
+ self.migration_thread = None
364
+
@@ -0,0 +1,176 @@
1
+ """Dialog for configuring embedding models for collections."""
2
+
3
+ from typing import Optional, Tuple
4
+ from PySide6.QtWidgets import (
5
+ QDialog, QVBoxLayout, QHBoxLayout, QLabel,
6
+ QComboBox, QPushButton, QGroupBox, QTextEdit,
7
+ QMessageBox
8
+ )
9
+ from PySide6.QtCore import Qt
10
+
11
+ from vector_inspector.core.embedding_utils import get_available_models_for_dimension, DIMENSION_TO_MODEL
12
+
13
+
14
+ class EmbeddingConfigDialog(QDialog):
15
+ """Dialog for selecting embedding model for a collection."""
16
+
17
+ def __init__(self, collection_name: str, vector_dimension: int,
18
+ current_model: Optional[str] = None,
19
+ current_type: Optional[str] = None,
20
+ parent=None):
21
+ super().__init__(parent)
22
+ self.collection_name = collection_name
23
+ self.vector_dimension = vector_dimension
24
+ self.current_model = current_model
25
+ self.current_type = current_type
26
+ self.selected_model = None
27
+ self.selected_type = None
28
+
29
+ self.setWindowTitle(f"Configure Embedding Model - {collection_name}")
30
+ self.setMinimumWidth(500)
31
+ self._setup_ui()
32
+
33
+ def _setup_ui(self):
34
+ """Setup dialog UI."""
35
+ layout = QVBoxLayout(self)
36
+
37
+ # Info section
38
+ info_group = QGroupBox("Collection Information")
39
+ info_layout = QVBoxLayout()
40
+
41
+ info_layout.addWidget(QLabel(f"<b>Collection:</b> {self.collection_name}"))
42
+ info_layout.addWidget(QLabel(f"<b>Vector Dimension:</b> {self.vector_dimension}"))
43
+
44
+ if self.current_model:
45
+ info_layout.addWidget(QLabel(f"<b>Current Model:</b> {self.current_model} ({self.current_type})"))
46
+ else:
47
+ warning = QLabel("⚠️ No embedding model configured - using automatic detection")
48
+ warning.setStyleSheet("color: orange;")
49
+ info_layout.addWidget(warning)
50
+
51
+ info_group.setLayout(info_layout)
52
+ layout.addWidget(info_group)
53
+
54
+ # Model selection section
55
+ model_group = QGroupBox("Embedding Model Selection")
56
+ model_layout = QVBoxLayout()
57
+
58
+ # Get available models for this dimension
59
+ available_models = get_available_models_for_dimension(self.vector_dimension)
60
+
61
+ if available_models:
62
+ model_layout.addWidget(QLabel(f"Available models for {self.vector_dimension}-dimensional vectors:"))
63
+
64
+ self.model_combo = QComboBox()
65
+ for model_name, model_type, description in available_models:
66
+ display_text = f"{model_name} ({model_type}) - {description}"
67
+ self.model_combo.addItem(display_text, (model_name, model_type))
68
+
69
+ # Set current selection if it matches
70
+ if self.current_model and self.current_type:
71
+ for i in range(self.model_combo.count()):
72
+ model_name, model_type = self.model_combo.itemData(i)
73
+ if model_name == self.current_model and model_type == self.current_type:
74
+ self.model_combo.setCurrentIndex(i)
75
+ break
76
+
77
+ model_layout.addWidget(self.model_combo)
78
+
79
+ # Description area
80
+ desc_label = QLabel("<b>About the selected model:</b>")
81
+ model_layout.addWidget(desc_label)
82
+
83
+ self.description_text = QTextEdit()
84
+ self.description_text.setReadOnly(True)
85
+ self.description_text.setMaximumHeight(100)
86
+ self.description_text.setStyleSheet("background-color: #f5f5f5; border: 1px solid #ccc; color: #000000;")
87
+ model_layout.addWidget(self.description_text)
88
+
89
+ # Update description when selection changes
90
+ self.model_combo.currentIndexChanged.connect(self._update_description)
91
+ self._update_description()
92
+
93
+ else:
94
+ # No known models for this dimension
95
+ warning = QLabel(f"⚠️ No pre-configured models available for {self.vector_dimension} dimensions.")
96
+ warning.setWordWrap(True)
97
+ model_layout.addWidget(warning)
98
+
99
+ # Show all available dimensions
100
+ dims_text = "Available dimensions: " + ", ".join(str(d) for d in sorted(DIMENSION_TO_MODEL.keys()))
101
+ model_layout.addWidget(QLabel(dims_text))
102
+
103
+ self.model_combo = None
104
+
105
+ model_group.setLayout(model_layout)
106
+ layout.addWidget(model_group)
107
+
108
+ # Buttons
109
+ button_layout = QHBoxLayout()
110
+ button_layout.addStretch()
111
+
112
+ self.save_btn = QPushButton("Save Configuration")
113
+ self.save_btn.clicked.connect(self.accept)
114
+ self.save_btn.setEnabled(available_models and len(available_models) > 0)
115
+
116
+ self.clear_btn = QPushButton("Clear Configuration")
117
+ self.clear_btn.clicked.connect(self._clear_config)
118
+ self.clear_btn.setEnabled(self.current_model is not None)
119
+
120
+ cancel_btn = QPushButton("Cancel")
121
+ cancel_btn.clicked.connect(self.reject)
122
+
123
+ button_layout.addWidget(self.save_btn)
124
+ button_layout.addWidget(self.clear_btn)
125
+ button_layout.addWidget(cancel_btn)
126
+
127
+ layout.addLayout(button_layout)
128
+
129
+ def _update_description(self):
130
+ """Update the description text based on selected model."""
131
+ if not self.model_combo:
132
+ return
133
+
134
+ model_name, model_type = self.model_combo.currentData()
135
+
136
+ descriptions = {
137
+ "sentence-transformer": (
138
+ "Sentence-Transformers are text-only embedding models optimized for semantic similarity. "
139
+ "They work well for text search, clustering, and classification tasks."
140
+ ),
141
+ "clip": (
142
+ "CLIP (Contrastive Language-Image Pre-training) is a multi-modal model that can embed both "
143
+ "text and images into the same vector space. This allows text queries to find semantically "
144
+ "similar images, and vice versa. Perfect for image search with text descriptions."
145
+ )
146
+ }
147
+
148
+ desc = descriptions.get(model_type, "Embedding model for vector similarity search.")
149
+ self.description_text.setPlainText(
150
+ f"Model: {model_name}\n"
151
+ f"Type: {model_type}\n"
152
+ f"Dimension: {self.vector_dimension}\n\n"
153
+ f"{desc}"
154
+ )
155
+
156
+ def _clear_config(self):
157
+ """Clear the embedding model configuration."""
158
+ reply = QMessageBox.question(
159
+ self,
160
+ "Clear Configuration",
161
+ "This will remove the custom embedding model configuration and use automatic detection. Continue?",
162
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
163
+ )
164
+
165
+ if reply == QMessageBox.StandardButton.Yes:
166
+ self.selected_model = None
167
+ self.selected_type = None
168
+ self.done(2) # Custom code for "clear"
169
+
170
+ def get_selection(self) -> Optional[Tuple[str, str]]:
171
+ """Get the selected model and type."""
172
+ if not self.model_combo:
173
+ return None
174
+
175
+ model_name, model_type = self.model_combo.currentData()
176
+ return (model_name, model_type)