vector-inspector 0.2.4__tar.gz → 0.2.5__tar.gz

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-0.2.4 → vector_inspector-0.2.5}/PKG-INFO +7 -4
  2. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/README.md +3 -0
  3. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/pyproject.toml +4 -4
  4. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/core/connections/chroma_connection.py +28 -2
  5. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/core/connections/qdrant_connection.py +61 -1
  6. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/services/visualization_service.py +15 -12
  7. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/ui/main_window.py +48 -5
  8. vector_inspector-0.2.5/src/vector_inspector/ui/views/info_panel.py +311 -0
  9. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/ui/views/visualization_view.py +6 -2
  10. vector_inspector-0.2.5/src/vector_inspector/utils/__init__.py +1 -0
  11. vector_inspector-0.2.5/src/vector_inspector/utils/lazy_imports.py +49 -0
  12. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/__init__.py +0 -0
  13. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/__main__.py +0 -0
  14. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/core/__init__.py +0 -0
  15. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/core/connections/__init__.py +0 -0
  16. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/core/connections/base_connection.py +0 -0
  17. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/core/connections/template_connection.py +0 -0
  18. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/main.py +0 -0
  19. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/services/__init__.py +0 -0
  20. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/services/backup_restore_service.py +0 -0
  21. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/services/filter_service.py +0 -0
  22. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/services/import_export_service.py +0 -0
  23. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/services/settings_service.py +0 -0
  24. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/ui/__init__.py +0 -0
  25. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/ui/components/__init__.py +0 -0
  26. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/ui/components/backup_restore_dialog.py +0 -0
  27. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/ui/components/filter_builder.py +0 -0
  28. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/ui/components/item_dialog.py +0 -0
  29. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/ui/components/loading_dialog.py +0 -0
  30. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/ui/views/__init__.py +0 -0
  31. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/ui/views/collection_browser.py +0 -0
  32. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/ui/views/connection_view.py +0 -0
  33. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/ui/views/metadata_view.py +0 -0
  34. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/src/vector_inspector/ui/views/search_view.py +0 -0
  35. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/tests/test_connections.py +0 -0
  36. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/tests/test_filter_service.py +0 -0
  37. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/tests/test_settings_service.py +0 -0
  38. {vector_inspector-0.2.4 → vector_inspector-0.2.5}/tests/vector_inspector.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vector-inspector
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary: A comprehensive desktop application for visualizing, querying, and managing vector database data
5
5
  Author-Email: Anthony Dawson <anthonypdawson+github@gmail.com>
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://vector-inspector.divinedevops.com
8
- Project-URL: Source, https://github.com/anthony-dawson/vector-inspector
9
- Project-URL: Issues, https://github.com/anthony-dawson/vector-inspector/issues
10
- Project-URL: Documentation, https://github.com/anthony-dawson/vector-inspector#readme
8
+ Project-URL: Source, https://github.com/anthonypdawson/vector-inspector
9
+ Project-URL: Issues, https://github.com/anthonypdawson/vector-inspector/issues
10
+ Project-URL: Documentation, https://github.com/anthonypdawson/vector-inspector#readme
11
11
  Requires-Python: ==3.12.*
12
12
  Requires-Dist: chromadb>=0.4.22
13
13
  Requires-Dist: qdrant-client>=1.7.0
@@ -26,6 +26,9 @@ Description-Content-Type: text/markdown
26
26
 
27
27
 
28
28
  # Vector Inspector
29
+
30
+ > **Disclaimer:** This tool is currently under active development and is **not production ready**. Not all features have been thoroughly tested. Use with caution in critical or production environments.
31
+
29
32
  ![PyPI](https://img.shields.io/pypi/v/vector-inspector)
30
33
  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/vector-inspector?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/vector-inspector)
31
34
 
@@ -1,5 +1,8 @@
1
1
 
2
2
  # Vector Inspector
3
+
4
+ > **Disclaimer:** This tool is currently under active development and is **not production ready**. Not all features have been thoroughly tested. Use with caution in critical or production environments.
5
+
3
6
  ![PyPI](https://img.shields.io/pypi/v/vector-inspector)
4
7
  [![PyPI Downloads](https://static.pepy.tech/personalized-badge/vector-inspector?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/vector-inspector)
5
8
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "vector-inspector"
3
- version = "0.2.4"
3
+ version = "0.2.5"
4
4
  description = "A comprehensive desktop application for visualizing, querying, and managing vector database data"
5
5
  authors = [
6
6
  { name = "Anthony Dawson", email = "anthonypdawson+github@gmail.com" },
@@ -28,9 +28,9 @@ text = "MIT"
28
28
 
29
29
  [project.urls]
30
30
  Homepage = "https://vector-inspector.divinedevops.com"
31
- Source = "https://github.com/anthony-dawson/vector-inspector"
32
- Issues = "https://github.com/anthony-dawson/vector-inspector/issues"
33
- Documentation = "https://github.com/anthony-dawson/vector-inspector#readme"
31
+ Source = "https://github.com/anthonypdawson/vector-inspector"
32
+ Issues = "https://github.com/anthonypdawson/vector-inspector/issues"
33
+ Documentation = "https://github.com/anthonypdawson/vector-inspector#readme"
34
34
 
35
35
  [project.scripts]
36
36
  vector-inspector = "vector_inspector.main:main"
@@ -125,16 +125,42 @@ class ChromaDBConnection(VectorDBConnection):
125
125
 
126
126
  try:
127
127
  count = collection.count()
128
- # Get a sample to determine metadata fields
129
- sample = collection.get(limit=1, include=["metadatas"])
128
+ # Get a sample to determine metadata fields and vector dimensions
129
+ sample = collection.get(limit=1, include=["metadatas", "embeddings"])
130
130
  metadata_fields = []
131
+ vector_dimension = "Unknown"
132
+
131
133
  if sample and sample["metadatas"]:
132
134
  metadata_fields = list(sample["metadatas"][0].keys()) if sample["metadatas"][0] else []
133
135
 
136
+ # Determine vector dimensions from embeddings
137
+ embeddings = sample.get("embeddings") if sample else None
138
+ if embeddings is not None and len(embeddings) > 0 and embeddings[0] is not None:
139
+ vector_dimension = len(embeddings[0])
140
+
141
+ # ChromaDB uses cosine distance by default (or can be configured)
142
+ # Try to get metadata from collection if available
143
+ distance_metric = "Cosine (default)"
144
+ try:
145
+ # ChromaDB collections may have metadata about distance function
146
+ col_metadata = collection.metadata
147
+ if col_metadata and "hnsw:space" in col_metadata:
148
+ space = col_metadata["hnsw:space"]
149
+ if space == "l2":
150
+ distance_metric = "Euclidean (L2)"
151
+ elif space == "ip":
152
+ distance_metric = "Inner Product"
153
+ elif space == "cosine":
154
+ distance_metric = "Cosine"
155
+ except:
156
+ pass # Use default if unable to determine
157
+
134
158
  return {
135
159
  "name": name,
136
160
  "count": count,
137
161
  "metadata_fields": metadata_fields,
162
+ "vector_dimension": vector_dimension,
163
+ "distance_metric": distance_metric,
138
164
  }
139
165
  except Exception as e:
140
166
  print(f"Failed to get collection info: {e}")
@@ -191,11 +191,71 @@ class QdrantConnection(VectorDBConnection):
191
191
  # Extract metadata fields, excluding 'document' if present
192
192
  metadata_fields = [k for k in point.payload.keys() if k != 'document']
193
193
 
194
- return {
194
+ # Extract vector configuration
195
+ vector_dimension = "Unknown"
196
+ distance_metric = "Unknown"
197
+ config_details = {}
198
+
199
+ if collection_info.config:
200
+ # Get vector parameters
201
+ if hasattr(collection_info.config, 'params'):
202
+ params = collection_info.config.params
203
+ if hasattr(params, 'vectors'):
204
+ vectors = params.vectors
205
+ # Handle both dict and object access
206
+ if isinstance(vectors, dict):
207
+ # Named vectors
208
+ first_vector = next(iter(vectors.values()), None)
209
+ if first_vector:
210
+ vector_dimension = getattr(first_vector, 'size', 'Unknown')
211
+ distance = getattr(first_vector, 'distance', None)
212
+ else:
213
+ # Single vector config
214
+ vector_dimension = getattr(vectors, 'size', 'Unknown')
215
+ distance = getattr(vectors, 'distance', None)
216
+
217
+ # Map distance enum to readable name
218
+ if distance:
219
+ distance_str = str(distance)
220
+ if 'COSINE' in distance_str.upper():
221
+ distance_metric = "Cosine"
222
+ elif 'EUCLID' in distance_str.upper():
223
+ distance_metric = "Euclidean"
224
+ elif 'DOT' in distance_str.upper():
225
+ distance_metric = "Dot Product"
226
+ elif 'MANHATTAN' in distance_str.upper():
227
+ distance_metric = "Manhattan"
228
+ else:
229
+ distance_metric = distance_str
230
+
231
+ # Get HNSW config if available
232
+ if hasattr(collection_info.config, 'hnsw_config'):
233
+ hnsw = collection_info.config.hnsw_config
234
+ config_details['hnsw_config'] = {
235
+ 'm': getattr(hnsw, 'm', None),
236
+ 'ef_construct': getattr(hnsw, 'ef_construct', None),
237
+ }
238
+
239
+ # Get optimizer config if available
240
+ if hasattr(collection_info.config, 'optimizer_config'):
241
+ opt = collection_info.config.optimizer_config
242
+ config_details['optimizer_config'] = {
243
+ 'indexing_threshold': getattr(opt, 'indexing_threshold', None),
244
+ }
245
+
246
+ result = {
195
247
  "name": name,
196
248
  "count": collection_info.points_count,
197
249
  "metadata_fields": metadata_fields,
250
+ "vector_dimension": vector_dimension,
251
+ "distance_metric": distance_metric,
198
252
  }
253
+
254
+ if config_details:
255
+ result['config'] = config_details
256
+
257
+ return result
258
+
199
259
  except Exception as e:
200
260
  print(f"Failed to get collection info: {e}")
201
261
  return None
@@ -1,10 +1,6 @@
1
1
  """Visualization service for dimensionality reduction."""
2
2
 
3
- from typing import Optional, List, Tuple
4
- import numpy as np
5
- from sklearn.decomposition import PCA
6
- from sklearn.manifold import TSNE
7
- import umap
3
+ from typing import Optional, List, Tuple, Any
8
4
 
9
5
 
10
6
  class VisualizationService:
@@ -16,7 +12,7 @@ class VisualizationService:
16
12
  method: str = "pca",
17
13
  n_components: int = 2,
18
14
  **kwargs
19
- ) -> Optional[np.ndarray]:
15
+ ) -> Optional[Any]:
20
16
  """
21
17
  Reduce dimensionality of embeddings.
22
18
 
@@ -33,13 +29,19 @@ class VisualizationService:
33
29
  return None
34
30
 
35
31
  try:
32
+ # Lazy import numpy and models
33
+ from vector_inspector.utils.lazy_imports import get_numpy, get_sklearn_model
34
+ np = get_numpy()
35
+
36
36
  X = np.array(embeddings)
37
37
 
38
- if method == "pca":
38
+ if method.lower() == "pca":
39
+ PCA = get_sklearn_model('PCA')
39
40
  reducer = PCA(n_components=n_components)
40
41
  reduced = reducer.fit_transform(X)
41
42
 
42
- elif method == "tsne":
43
+ elif method.lower() == "t-sne":
44
+ TSNE = get_sklearn_model('TSNE')
43
45
  perplexity = kwargs.get("perplexity", min(30, len(embeddings) - 1))
44
46
  reducer = TSNE(
45
47
  n_components=n_components,
@@ -48,9 +50,10 @@ class VisualizationService:
48
50
  )
49
51
  reduced = reducer.fit_transform(X)
50
52
 
51
- elif method == "umap":
53
+ elif method.lower() == "umap":
54
+ UMAP = get_sklearn_model('UMAP')
52
55
  n_neighbors = kwargs.get("n_neighbors", min(15, len(embeddings) - 1))
53
- reducer = umap.UMAP(
56
+ reducer = UMAP(
54
57
  n_components=n_components,
55
58
  n_neighbors=n_neighbors,
56
59
  random_state=42
@@ -69,11 +72,11 @@ class VisualizationService:
69
72
 
70
73
  @staticmethod
71
74
  def prepare_plot_data(
72
- reduced_embeddings: np.ndarray,
75
+ reduced_embeddings: Any,
73
76
  labels: Optional[List[str]] = None,
74
77
  metadata: Optional[List[dict]] = None,
75
78
  color_by: Optional[str] = None
76
- ) -> Tuple[np.ndarray, List[str], List[str]]:
79
+ ) -> Tuple[Any, List[str], List[str]]:
77
80
  """
78
81
  Prepare data for plotting.
79
82
 
@@ -12,9 +12,9 @@ from vector_inspector.core.connections.base_connection import VectorDBConnection
12
12
  from vector_inspector.core.connections.chroma_connection import ChromaDBConnection
13
13
  from vector_inspector.ui.views.connection_view import ConnectionView
14
14
  from vector_inspector.ui.views.collection_browser import CollectionBrowser
15
+ from vector_inspector.ui.views.info_panel import InfoPanel
15
16
  from vector_inspector.ui.views.metadata_view import MetadataView
16
17
  from vector_inspector.ui.views.search_view import SearchView
17
- from vector_inspector.ui.views.visualization_view import VisualizationView
18
18
  from vector_inspector.ui.components.backup_restore_dialog import BackupRestoreDialog
19
19
 
20
20
 
@@ -63,13 +63,21 @@ class MainWindow(QMainWindow):
63
63
  # Right panel - Tabbed views
64
64
  self.tab_widget = QTabWidget()
65
65
 
66
+ self.info_panel = InfoPanel(self.connection)
66
67
  self.metadata_view = MetadataView(self.connection)
67
68
  self.search_view = SearchView(self.connection)
68
- self.visualization_view = VisualizationView(self.connection)
69
+ self.visualization_view = None # Lazy loaded
69
70
 
71
+ self.tab_widget.addTab(self.info_panel, "Info")
70
72
  self.tab_widget.addTab(self.metadata_view, "Data Browser")
71
73
  self.tab_widget.addTab(self.search_view, "Search")
72
- self.tab_widget.addTab(self.visualization_view, "Visualization")
74
+ self.tab_widget.addTab(QWidget(), "Visualization") # Placeholder
75
+
76
+ # Set Info tab as default
77
+ self.tab_widget.setCurrentIndex(0)
78
+
79
+ # Connect to tab change to lazy load visualization
80
+ self.tab_widget.currentChanged.connect(self._on_tab_changed)
73
81
 
74
82
  # Add panels to splitter
75
83
  main_splitter.addWidget(left_panel)
@@ -167,14 +175,34 @@ class MainWindow(QMainWindow):
167
175
  self.connection_view.connection_created.connect(self._on_connection_created)
168
176
  self.collection_browser.collection_selected.connect(self._on_collection_selected)
169
177
 
178
+ def _on_tab_changed(self, index: int):
179
+ """Handle tab change - lazy load visualization tab."""
180
+ if index == 3 and self.visualization_view is None:
181
+ # Lazy load visualization view
182
+ from vector_inspector.ui.views.visualization_view import VisualizationView
183
+ self.visualization_view = VisualizationView(self.connection)
184
+ # Replace placeholder with actual view
185
+ self.tab_widget.removeTab(3)
186
+ self.tab_widget.insertTab(3, self.visualization_view, "Visualization")
187
+ self.tab_widget.setCurrentIndex(3)
188
+ # Set collection if one is already selected
189
+ if self.current_collection:
190
+ self.visualization_view.set_collection(self.current_collection)
191
+
170
192
  def _on_connection_created(self, new_connection: VectorDBConnection):
171
193
  """Handle when a new connection instance is created."""
172
194
  self.connection = new_connection
173
195
  # Update all views with new connection
174
196
  self.collection_browser.connection = new_connection
197
+ self.info_panel.connection = new_connection
175
198
  self.metadata_view.connection = new_connection
176
199
  self.search_view.connection = new_connection
177
- self.visualization_view.connection = new_connection
200
+ # Only update visualization if it's been created
201
+ if self.visualization_view is not None:
202
+ self.visualization_view.connection = new_connection
203
+ # Refresh the collection browser to show new database's collections
204
+ if new_connection.is_connected:
205
+ self.collection_browser.refresh()
178
206
 
179
207
  def _on_connect(self):
180
208
  """Handle connect action."""
@@ -187,6 +215,11 @@ class MainWindow(QMainWindow):
187
215
  self.statusBar.showMessage("Disconnected")
188
216
  self.connection_changed.emit(False)
189
217
  self.collection_browser.clear()
218
+ # Clear info panel
219
+ self.info_panel.refresh_database_info()
220
+ else:
221
+ # Always clear collection browser on disconnect
222
+ self.collection_browser.clear()
190
223
 
191
224
  def _on_connection_status_changed(self, connected: bool):
192
225
  """Handle connection status change."""
@@ -194,9 +227,14 @@ class MainWindow(QMainWindow):
194
227
  self.statusBar.showMessage("Connected")
195
228
  self.connection_changed.emit(True)
196
229
  self._on_refresh_collections()
230
+ # Refresh info panel with new connection
231
+ self.info_panel.refresh_database_info()
197
232
  else:
198
233
  self.statusBar.showMessage("Connection failed")
199
234
  self.connection_changed.emit(False)
235
+ # Clear info panel and collection browser
236
+ self.info_panel.refresh_database_info()
237
+ self.collection_browser.clear()
200
238
 
201
239
  def _on_collection_selected(self, collection_name: str):
202
240
  """Handle collection selection."""
@@ -204,14 +242,19 @@ class MainWindow(QMainWindow):
204
242
  self.statusBar.showMessage(f"Collection: {collection_name}")
205
243
 
206
244
  # Update all views with new collection
245
+ self.info_panel.set_collection(collection_name)
207
246
  self.metadata_view.set_collection(collection_name)
208
247
  self.search_view.set_collection(collection_name)
209
- self.visualization_view.set_collection(collection_name)
248
+ # Only update visualization if it's been created
249
+ if self.visualization_view is not None:
250
+ self.visualization_view.set_collection(collection_name)
210
251
 
211
252
  def _on_refresh_collections(self):
212
253
  """Refresh collection list."""
213
254
  if self.connection.is_connected:
214
255
  self.collection_browser.refresh()
256
+ # Also refresh database info (collection count may have changed)
257
+ self.info_panel.refresh_database_info()
215
258
 
216
259
  def _on_new_collection(self):
217
260
  """Create a new collection."""
@@ -0,0 +1,311 @@
1
+ """Information panel for displaying database and collection metadata."""
2
+
3
+ from typing import Optional, Dict, Any
4
+ from PySide6.QtWidgets import (
5
+ QWidget, QVBoxLayout, QHBoxLayout, QLabel,
6
+ QGroupBox, QScrollArea, QFrame
7
+ )
8
+ from PySide6.QtCore import Qt, QObject
9
+
10
+ from vector_inspector.core.connections.base_connection import VectorDBConnection
11
+ from vector_inspector.core.connections.chroma_connection import ChromaDBConnection
12
+ from vector_inspector.core.connections.qdrant_connection import QdrantConnection
13
+
14
+
15
+ class InfoPanel(QWidget):
16
+ """Panel for displaying database and collection information."""
17
+
18
+ def __init__(self, connection: VectorDBConnection, parent=None):
19
+ super().__init__(parent)
20
+ self.connection = connection
21
+ self.current_collection: str = ""
22
+ self._setup_ui()
23
+
24
+ def _setup_ui(self):
25
+ """Setup widget UI."""
26
+ layout = QVBoxLayout(self)
27
+
28
+ # Create scroll area for content
29
+ scroll = QScrollArea()
30
+ scroll.setWidgetResizable(True)
31
+ scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
32
+
33
+ # Container for all info sections
34
+ container = QWidget()
35
+ container_layout = QVBoxLayout(container)
36
+ container_layout.setSpacing(10)
37
+
38
+ # Database Information Section
39
+ self.db_group = QGroupBox("Database Information")
40
+ db_layout = QVBoxLayout()
41
+
42
+ self.provider_label = self._create_info_row("Provider:", "Not connected")
43
+ self.connection_type_label = self._create_info_row("Connection Type:", "N/A")
44
+ self.endpoint_label = self._create_info_row("Endpoint:", "N/A")
45
+ self.api_key_label = self._create_info_row("API Key:", "N/A")
46
+ self.status_label = self._create_info_row("Status:", "Disconnected")
47
+ self.collections_count_label = self._create_info_row("Total Collections:", "0")
48
+
49
+ db_layout.addWidget(self.provider_label)
50
+ db_layout.addWidget(self.connection_type_label)
51
+ db_layout.addWidget(self.endpoint_label)
52
+ db_layout.addWidget(self.api_key_label)
53
+ db_layout.addWidget(self.status_label)
54
+ db_layout.addWidget(self.collections_count_label)
55
+
56
+ self.db_group.setLayout(db_layout)
57
+ container_layout.addWidget(self.db_group)
58
+
59
+ # Collections List Section
60
+ self.collections_group = QGroupBox("Available Collections")
61
+ collections_layout = QVBoxLayout()
62
+
63
+ self.collections_list_label = QLabel("No collections")
64
+ self.collections_list_label.setWordWrap(True)
65
+ self.collections_list_label.setStyleSheet("color: gray; padding: 10px;")
66
+ collections_layout.addWidget(self.collections_list_label)
67
+
68
+ self.collections_group.setLayout(collections_layout)
69
+ container_layout.addWidget(self.collections_group)
70
+
71
+ # Collection Information Section
72
+ self.collection_group = QGroupBox("Collection Information")
73
+ collection_layout = QVBoxLayout()
74
+
75
+ self.collection_name_label = self._create_info_row("Name:", "No collection selected")
76
+ self.vector_dim_label = self._create_info_row("Vector Dimension:", "N/A")
77
+ self.distance_metric_label = self._create_info_row("Distance Metric:", "N/A")
78
+ self.total_points_label = self._create_info_row("Total Points:", "0")
79
+
80
+ collection_layout.addWidget(self.collection_name_label)
81
+ collection_layout.addWidget(self.vector_dim_label)
82
+ collection_layout.addWidget(self.distance_metric_label)
83
+ collection_layout.addWidget(self.total_points_label)
84
+
85
+ # Payload Schema subsection
86
+ schema_label = QLabel("<b>Payload Schema:</b>")
87
+ collection_layout.addWidget(schema_label)
88
+
89
+ self.schema_label = QLabel("N/A")
90
+ self.schema_label.setWordWrap(True)
91
+ self.schema_label.setStyleSheet("color: gray; padding-left: 20px;")
92
+ collection_layout.addWidget(self.schema_label)
93
+
94
+ # Provider-specific details
95
+ provider_details_label = QLabel("<b>Provider-Specific Details:</b>")
96
+ collection_layout.addWidget(provider_details_label)
97
+
98
+ self.provider_details_label = QLabel("N/A")
99
+ self.provider_details_label.setWordWrap(True)
100
+ self.provider_details_label.setStyleSheet("color: gray; padding-left: 20px;")
101
+ collection_layout.addWidget(self.provider_details_label)
102
+
103
+ self.collection_group.setLayout(collection_layout)
104
+ container_layout.addWidget(self.collection_group)
105
+
106
+ # Add stretch to push content to top
107
+ container_layout.addStretch()
108
+
109
+ scroll.setWidget(container)
110
+ layout.addWidget(scroll)
111
+
112
+ # Initial state
113
+ self.refresh_database_info()
114
+
115
+ def _create_info_row(self, label: str, value: str) -> QWidget:
116
+ """Create a row with label and value."""
117
+ row = QWidget()
118
+ row_layout = QHBoxLayout(row)
119
+ row_layout.setContentsMargins(0, 2, 0, 2)
120
+
121
+ label_widget = QLabel(f"<b>{label}</b>")
122
+ label_widget.setMinimumWidth(150)
123
+ row_layout.addWidget(label_widget)
124
+
125
+ value_widget = QLabel(value)
126
+ value_widget.setWordWrap(True)
127
+ value_widget.setStyleSheet("color: white;")
128
+ row_layout.addWidget(value_widget, stretch=1)
129
+
130
+ # Store value widget for later updates (use setProperty for type safety)
131
+ row.setProperty("value_label", value_widget)
132
+
133
+ return row
134
+
135
+ def refresh_database_info(self):
136
+ """Refresh database connection information."""
137
+ if not self.connection or not self.connection.is_connected:
138
+ self._update_label(self.provider_label, "Not connected")
139
+ self._update_label(self.connection_type_label, "N/A")
140
+ self._update_label(self.endpoint_label, "N/A")
141
+ self._update_label(self.api_key_label, "N/A")
142
+ self._update_label(self.status_label, "Disconnected")
143
+ self._update_label(self.collections_count_label, "0")
144
+ self.collections_list_label.setText("No collections")
145
+ self.collections_list_label.setStyleSheet("color: gray; padding: 10px;")
146
+ # Also clear collection info
147
+ self._update_label(self.collection_name_label, "No collection selected")
148
+ self._update_label(self.vector_dim_label, "N/A")
149
+ self._update_label(self.distance_metric_label, "N/A")
150
+ self._update_label(self.total_points_label, "0")
151
+ self.schema_label.setText("N/A")
152
+ self.provider_details_label.setText("N/A")
153
+ return
154
+
155
+ # Get provider name
156
+ provider_name = self.connection.__class__.__name__.replace("Connection", "")
157
+ self._update_label(self.provider_label, provider_name)
158
+
159
+ # Get connection details
160
+ if isinstance(self.connection, ChromaDBConnection):
161
+ if self.connection.path:
162
+ self._update_label(self.connection_type_label, "Persistent (Local)")
163
+ self._update_label(self.endpoint_label, self.connection.path)
164
+ elif self.connection.host and self.connection.port:
165
+ self._update_label(self.connection_type_label, "HTTP (Remote)")
166
+ self._update_label(self.endpoint_label, f"{self.connection.host}:{self.connection.port}")
167
+ else:
168
+ self._update_label(self.connection_type_label, "Ephemeral (In-Memory)")
169
+ self._update_label(self.endpoint_label, "N/A")
170
+ self._update_label(self.api_key_label, "Not required")
171
+
172
+ elif isinstance(self.connection, QdrantConnection):
173
+ if self.connection.path:
174
+ self._update_label(self.connection_type_label, "Embedded (Local)")
175
+ self._update_label(self.endpoint_label, self.connection.path)
176
+ elif self.connection.url:
177
+ self._update_label(self.connection_type_label, "Remote (URL)")
178
+ self._update_label(self.endpoint_label, self.connection.url)
179
+ elif self.connection.host:
180
+ self._update_label(self.connection_type_label, "Remote (Host)")
181
+ self._update_label(self.endpoint_label, f"{self.connection.host}:{self.connection.port}")
182
+ else:
183
+ self._update_label(self.connection_type_label, "In-Memory")
184
+ self._update_label(self.endpoint_label, "N/A")
185
+
186
+ if self.connection.api_key:
187
+ self._update_label(self.api_key_label, "Present (hidden)")
188
+ else:
189
+ self._update_label(self.api_key_label, "Not configured")
190
+ else:
191
+ self._update_label(self.connection_type_label, "Unknown")
192
+ self._update_label(self.endpoint_label, "N/A")
193
+ self._update_label(self.api_key_label, "Unknown")
194
+
195
+ # Status
196
+ self._update_label(self.status_label, "Connected" if self.connection.is_connected else "Disconnected")
197
+
198
+ # List collections
199
+ try:
200
+ collections = self.connection.list_collections()
201
+ self._update_label(self.collections_count_label, str(len(collections)))
202
+
203
+ if collections:
204
+ collections_text = "\n".join([f"• {name}" for name in sorted(collections)])
205
+ self.collections_list_label.setText(collections_text)
206
+ self.collections_list_label.setStyleSheet("color: white; padding: 10px; font-family: monospace;")
207
+ else:
208
+ self.collections_list_label.setText("No collections found")
209
+ self.collections_list_label.setStyleSheet("color: gray; padding: 10px;")
210
+ except Exception as e:
211
+ self._update_label(self.collections_count_label, "Error")
212
+ self.collections_list_label.setText(f"Error loading collections: {str(e)}")
213
+ self.collections_list_label.setStyleSheet("color: red; padding: 10px;")
214
+
215
+ def refresh_collection_info(self):
216
+ """Refresh collection-specific information."""
217
+ if not self.current_collection or not self.connection or not self.connection.is_connected:
218
+ self._update_label(self.collection_name_label, "No collection selected")
219
+ self._update_label(self.vector_dim_label, "N/A")
220
+ self._update_label(self.distance_metric_label, "N/A")
221
+ self._update_label(self.total_points_label, "0")
222
+ self.schema_label.setText("N/A")
223
+ self.provider_details_label.setText("N/A")
224
+ return
225
+
226
+ try:
227
+ # Get collection info
228
+ collection_info = self.connection.get_collection_info(self.current_collection)
229
+
230
+ if not collection_info:
231
+ self._update_label(self.collection_name_label, self.current_collection)
232
+ self._update_label(self.vector_dim_label, "Unable to retrieve")
233
+ self._update_label(self.distance_metric_label, "Unable to retrieve")
234
+ self._update_label(self.total_points_label, "Unable to retrieve")
235
+ self.schema_label.setText("Unable to retrieve collection info")
236
+ self.provider_details_label.setText("N/A")
237
+ return
238
+
239
+ # Update basic info
240
+ self._update_label(self.collection_name_label, self.current_collection)
241
+
242
+ # Vector dimension
243
+ vector_dim = collection_info.get("vector_dimension", "Unknown")
244
+ self._update_label(self.vector_dim_label, str(vector_dim))
245
+
246
+ # Distance metric
247
+ distance = collection_info.get("distance_metric", "Unknown")
248
+ self._update_label(self.distance_metric_label, distance)
249
+
250
+ # Total points
251
+ count = collection_info.get("count", 0)
252
+ self._update_label(self.total_points_label, f"{count:,}")
253
+
254
+ # Metadata schema
255
+ metadata_fields = collection_info.get("metadata_fields", [])
256
+ if metadata_fields:
257
+ schema_text = "\n".join([f"• {field}" for field in sorted(metadata_fields)])
258
+ self.schema_label.setText(schema_text)
259
+ self.schema_label.setStyleSheet("color: white; padding-left: 20px; font-family: monospace;")
260
+ else:
261
+ self.schema_label.setText("No metadata fields found")
262
+ self.schema_label.setStyleSheet("color: gray; padding-left: 20px;")
263
+
264
+ # Provider-specific details
265
+ details_list = []
266
+
267
+ if isinstance(self.connection, ChromaDBConnection):
268
+ details_list.append("• Provider: ChromaDB")
269
+ details_list.append("• Supports: Documents, Metadata, Embeddings")
270
+ details_list.append("• Default embedding: all-MiniLM-L6-v2")
271
+
272
+ elif isinstance(self.connection, QdrantConnection):
273
+ details_list.append("• Provider: Qdrant")
274
+ details_list.append("• Supports: Points, Payload, Vectors")
275
+ # Get additional Qdrant-specific info if available
276
+ if "config" in collection_info:
277
+ config = collection_info["config"]
278
+ if "hnsw_config" in config:
279
+ hnsw = config["hnsw_config"]
280
+ details_list.append(f"• HNSW M: {hnsw.get('m', 'N/A')}")
281
+ details_list.append(f"• HNSW ef_construct: {hnsw.get('ef_construct', 'N/A')}")
282
+ if "optimizer_config" in config:
283
+ opt = config["optimizer_config"]
284
+ details_list.append(f"• Indexing threshold: {opt.get('indexing_threshold', 'N/A')}")
285
+
286
+ if details_list:
287
+ self.provider_details_label.setText("\n".join(details_list))
288
+ self.provider_details_label.setStyleSheet("color: white; padding-left: 20px; font-family: monospace;")
289
+ else:
290
+ self.provider_details_label.setText("No additional details available")
291
+ self.provider_details_label.setStyleSheet("color: gray; padding-left: 20px;")
292
+
293
+ except Exception as e:
294
+ self._update_label(self.collection_name_label, self.current_collection)
295
+ self._update_label(self.vector_dim_label, "Error")
296
+ self._update_label(self.distance_metric_label, "Error")
297
+ self._update_label(self.total_points_label, "Error")
298
+ self.schema_label.setText(f"Error: {str(e)}")
299
+ self.schema_label.setStyleSheet("color: red; padding-left: 20px;")
300
+ self.provider_details_label.setText("N/A")
301
+
302
+ def set_collection(self, collection_name: str):
303
+ """Set the current collection and refresh its information."""
304
+ self.current_collection = collection_name
305
+ self.refresh_collection_info()
306
+
307
+ def _update_label(self, row_widget: QWidget, value: str):
308
+ """Update the value label in an info row."""
309
+ value_label = row_widget.property("value_label")
310
+ if value_label and isinstance(value_label, QLabel):
311
+ value_label.setText(value)
@@ -1,5 +1,6 @@
1
1
  """Vector visualization view with dimensionality reduction."""
2
2
 
3
+ from __future__ import annotations
3
4
  from typing import Optional, Dict, Any
4
5
  import traceback
5
6
  from PySide6.QtWidgets import (
@@ -8,7 +9,6 @@ from PySide6.QtWidgets import (
8
9
  )
9
10
  from PySide6.QtCore import Qt, QThread, Signal
10
11
  from PySide6.QtWebEngineWidgets import QWebEngineView
11
- import plotly.graph_objects as go
12
12
  import numpy as np
13
13
 
14
14
  from vector_inspector.core.connections.base_connection import VectorDBConnection
@@ -154,7 +154,7 @@ class VisualizationView(QWidget):
154
154
  self.visualization_thread.error.connect(self._on_reduction_error)
155
155
  self.visualization_thread.start()
156
156
 
157
- def _on_reduction_finished(self, reduced_data: np.ndarray):
157
+ def _on_reduction_finished(self, reduced_data: Any):
158
158
  """Handle dimensionality reduction completion."""
159
159
  self.reduced_data = reduced_data
160
160
  self._create_plot()
@@ -172,6 +172,10 @@ class VisualizationView(QWidget):
172
172
  """Create plotly visualization."""
173
173
  if self.reduced_data is None or self.current_data is None:
174
174
  return
175
+
176
+ # Lazy import plotly
177
+ from vector_inspector.utils.lazy_imports import get_plotly
178
+ go = get_plotly()
175
179
 
176
180
  ids = self.current_data.get("ids", [])
177
181
  documents = self.current_data.get("documents", [])
@@ -0,0 +1 @@
1
+ """Utilities for Vector Inspector."""
@@ -0,0 +1,49 @@
1
+ """Lazy import utilities for performance optimization."""
2
+
3
+ from typing import Any
4
+
5
+ _plotly_cache = None
6
+ _sklearn_cache = {}
7
+ _numpy_cache = None
8
+
9
+
10
+ def get_plotly():
11
+ """Lazy import plotly graph_objects."""
12
+ global _plotly_cache
13
+ if _plotly_cache is None:
14
+ import plotly.graph_objects as go
15
+ _plotly_cache = go
16
+ return _plotly_cache
17
+
18
+
19
+ def get_numpy():
20
+ """Lazy import numpy."""
21
+ global _numpy_cache
22
+ if _numpy_cache is None:
23
+ import numpy as np
24
+ _numpy_cache = np
25
+ return _numpy_cache
26
+
27
+
28
+ def get_sklearn_model(model_name: str) -> Any:
29
+ """
30
+ Lazy import sklearn models.
31
+
32
+ Args:
33
+ model_name: Name of the model ('PCA', 'TSNE', 'UMAP')
34
+
35
+ Returns:
36
+ The model class
37
+ """
38
+ global _sklearn_cache
39
+ if model_name not in _sklearn_cache:
40
+ if model_name == 'PCA':
41
+ from sklearn.decomposition import PCA
42
+ _sklearn_cache['PCA'] = PCA
43
+ elif model_name == 'TSNE':
44
+ from sklearn.manifold import TSNE
45
+ _sklearn_cache['TSNE'] = TSNE
46
+ elif model_name == 'UMAP':
47
+ import umap
48
+ _sklearn_cache['UMAP'] = umap.UMAP
49
+ return _sklearn_cache[model_name]