vector-inspector 0.2.4.3__tar.gz → 0.2.6__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.
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/PKG-INFO +5 -2
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/README.md +3 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/pyproject.toml +2 -2
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/core/connections/chroma_connection.py +28 -2
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/core/connections/qdrant_connection.py +61 -1
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/services/settings_service.py +1 -1
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/services/visualization_service.py +24 -17
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/ui/main_window.py +62 -6
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/ui/views/connection_view.py +43 -8
- vector_inspector-0.2.6/src/vector_inspector/ui/views/info_panel.py +287 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/ui/views/metadata_view.py +65 -25
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/ui/views/visualization_view.py +6 -2
- vector_inspector-0.2.6/src/vector_inspector/utils/__init__.py +1 -0
- vector_inspector-0.2.6/src/vector_inspector/utils/lazy_imports.py +49 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/__init__.py +0 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/__main__.py +0 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/core/__init__.py +0 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/core/connections/__init__.py +0 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/core/connections/base_connection.py +0 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/core/connections/template_connection.py +0 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/main.py +0 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/services/__init__.py +0 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/services/backup_restore_service.py +0 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/services/filter_service.py +0 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/services/import_export_service.py +0 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/ui/__init__.py +0 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/ui/components/__init__.py +0 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/ui/components/backup_restore_dialog.py +0 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/ui/components/filter_builder.py +0 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/ui/components/item_dialog.py +0 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/ui/components/loading_dialog.py +0 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/ui/views/__init__.py +0 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/ui/views/collection_browser.py +0 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/ui/views/search_view.py +0 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/tests/test_connections.py +0 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/tests/test_filter_service.py +0 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/tests/test_settings_service.py +0 -0
- {vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/tests/vector_inspector.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: vector-inspector
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
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
|
|
@@ -8,7 +8,7 @@ Project-URL: Homepage, https://vector-inspector.divinedevops.com
|
|
|
8
8
|
Project-URL: Source, https://github.com/anthonypdawson/vector-inspector
|
|
9
9
|
Project-URL: Issues, https://github.com/anthonypdawson/vector-inspector/issues
|
|
10
10
|
Project-URL: Documentation, https://github.com/anthonypdawson/vector-inspector#readme
|
|
11
|
-
Requires-Python:
|
|
11
|
+
Requires-Python: <3.13,>=3.10
|
|
12
12
|
Requires-Dist: chromadb>=0.4.22
|
|
13
13
|
Requires-Dist: qdrant-client>=1.7.0
|
|
14
14
|
Requires-Dist: pyside6>=6.6.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 and code is released frequently. Use with caution in critical or production environments.
|
|
31
|
+
|
|
29
32
|

|
|
30
33
|
[](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 and code is released frequently. Use with caution in critical or production environments.
|
|
5
|
+
|
|
3
6
|

|
|
4
7
|
[](https://pepy.tech/projects/vector-inspector)
|
|
5
8
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "vector-inspector"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.6"
|
|
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" },
|
|
@@ -20,7 +20,7 @@ dependencies = [
|
|
|
20
20
|
"pyarrow>=14.0.0",
|
|
21
21
|
"pinecone>=8.0.0",
|
|
22
22
|
]
|
|
23
|
-
requires-python = "
|
|
23
|
+
requires-python = ">=3.10,<3.13"
|
|
24
24
|
readme = "README.md"
|
|
25
25
|
|
|
26
26
|
[project.license]
|
|
@@ -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
|
-
|
|
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
|
|
@@ -10,7 +10,7 @@ class SettingsService:
|
|
|
10
10
|
|
|
11
11
|
def __init__(self):
|
|
12
12
|
"""Initialize settings service."""
|
|
13
|
-
self.settings_dir = Path.home() / ".vector-
|
|
13
|
+
self.settings_dir = Path.home() / ".vector-inspector"
|
|
14
14
|
self.settings_file = self.settings_dir / "settings.json"
|
|
15
15
|
self.settings: Dict[str, Any] = {}
|
|
16
16
|
self._load_settings()
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
"""Visualization service for dimensionality reduction."""
|
|
2
2
|
|
|
3
|
-
from typing import Optional, List, Tuple
|
|
4
|
-
import
|
|
5
|
-
from sklearn.decomposition import PCA
|
|
6
|
-
from sklearn.manifold import TSNE
|
|
7
|
-
import umap
|
|
3
|
+
from typing import Optional, List, Tuple, Any
|
|
4
|
+
import warnings
|
|
8
5
|
|
|
9
6
|
|
|
10
7
|
class VisualizationService:
|
|
@@ -16,7 +13,7 @@ class VisualizationService:
|
|
|
16
13
|
method: str = "pca",
|
|
17
14
|
n_components: int = 2,
|
|
18
15
|
**kwargs
|
|
19
|
-
) -> Optional[
|
|
16
|
+
) -> Optional[Any]:
|
|
20
17
|
"""
|
|
21
18
|
Reduce dimensionality of embeddings.
|
|
22
19
|
|
|
@@ -33,13 +30,19 @@ class VisualizationService:
|
|
|
33
30
|
return None
|
|
34
31
|
|
|
35
32
|
try:
|
|
33
|
+
# Lazy import numpy and models
|
|
34
|
+
from vector_inspector.utils.lazy_imports import get_numpy, get_sklearn_model
|
|
35
|
+
np = get_numpy()
|
|
36
|
+
|
|
36
37
|
X = np.array(embeddings)
|
|
37
38
|
|
|
38
|
-
if method == "pca":
|
|
39
|
+
if method.lower() == "pca":
|
|
40
|
+
PCA = get_sklearn_model('PCA')
|
|
39
41
|
reducer = PCA(n_components=n_components)
|
|
40
42
|
reduced = reducer.fit_transform(X)
|
|
41
43
|
|
|
42
|
-
elif method
|
|
44
|
+
elif method.lower() in ["t-sne", "tsne"]:
|
|
45
|
+
TSNE = get_sklearn_model('TSNE')
|
|
43
46
|
perplexity = kwargs.get("perplexity", min(30, len(embeddings) - 1))
|
|
44
47
|
reducer = TSNE(
|
|
45
48
|
n_components=n_components,
|
|
@@ -48,14 +51,18 @@ class VisualizationService:
|
|
|
48
51
|
)
|
|
49
52
|
reduced = reducer.fit_transform(X)
|
|
50
53
|
|
|
51
|
-
elif method == "umap":
|
|
54
|
+
elif method.lower() == "umap":
|
|
55
|
+
UMAP = get_sklearn_model('UMAP')
|
|
52
56
|
n_neighbors = kwargs.get("n_neighbors", min(15, len(embeddings) - 1))
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
# Suppress n_jobs warning when using random_state
|
|
58
|
+
with warnings.catch_warnings():
|
|
59
|
+
warnings.filterwarnings("ignore", message=".*n_jobs.*overridden.*")
|
|
60
|
+
reducer = UMAP(
|
|
61
|
+
n_components=n_components,
|
|
62
|
+
n_neighbors=n_neighbors,
|
|
63
|
+
random_state=42
|
|
64
|
+
)
|
|
65
|
+
reduced = reducer.fit_transform(X)
|
|
59
66
|
|
|
60
67
|
else:
|
|
61
68
|
print(f"Unknown method: {method}")
|
|
@@ -69,11 +76,11 @@ class VisualizationService:
|
|
|
69
76
|
|
|
70
77
|
@staticmethod
|
|
71
78
|
def prepare_plot_data(
|
|
72
|
-
reduced_embeddings:
|
|
79
|
+
reduced_embeddings: Any,
|
|
73
80
|
labels: Optional[List[str]] = None,
|
|
74
81
|
metadata: Optional[List[dict]] = None,
|
|
75
82
|
color_by: Optional[str] = None
|
|
76
|
-
) -> Tuple[
|
|
83
|
+
) -> Tuple[Any, List[str], List[str]]:
|
|
77
84
|
"""
|
|
78
85
|
Prepare data for plotting.
|
|
79
86
|
|
|
@@ -5,17 +5,18 @@ from PySide6.QtWidgets import (
|
|
|
5
5
|
QSplitter, QTabWidget, QStatusBar, QToolBar,
|
|
6
6
|
QMessageBox, QInputDialog, QFileDialog
|
|
7
7
|
)
|
|
8
|
-
from PySide6.QtCore import Qt, Signal
|
|
8
|
+
from PySide6.QtCore import Qt, Signal, QTimer
|
|
9
9
|
from PySide6.QtGui import QAction
|
|
10
10
|
|
|
11
11
|
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
|
+
from vector_inspector.ui.components.loading_dialog import LoadingDialog
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class MainWindow(QMainWindow):
|
|
@@ -27,6 +28,7 @@ class MainWindow(QMainWindow):
|
|
|
27
28
|
super().__init__()
|
|
28
29
|
self.connection: VectorDBConnection = ChromaDBConnection()
|
|
29
30
|
self.current_collection: str = ""
|
|
31
|
+
self.loading_dialog = LoadingDialog("Loading collection...", self)
|
|
30
32
|
|
|
31
33
|
self.setWindowTitle("Vector Inspector")
|
|
32
34
|
self.setGeometry(100, 100, 1400, 900)
|
|
@@ -63,13 +65,21 @@ class MainWindow(QMainWindow):
|
|
|
63
65
|
# Right panel - Tabbed views
|
|
64
66
|
self.tab_widget = QTabWidget()
|
|
65
67
|
|
|
68
|
+
self.info_panel = InfoPanel(self.connection)
|
|
66
69
|
self.metadata_view = MetadataView(self.connection)
|
|
67
70
|
self.search_view = SearchView(self.connection)
|
|
68
|
-
self.visualization_view =
|
|
71
|
+
self.visualization_view = None # Lazy loaded
|
|
69
72
|
|
|
73
|
+
self.tab_widget.addTab(self.info_panel, "Info")
|
|
70
74
|
self.tab_widget.addTab(self.metadata_view, "Data Browser")
|
|
71
75
|
self.tab_widget.addTab(self.search_view, "Search")
|
|
72
|
-
self.tab_widget.addTab(
|
|
76
|
+
self.tab_widget.addTab(QWidget(), "Visualization") # Placeholder
|
|
77
|
+
|
|
78
|
+
# Set Info tab as default
|
|
79
|
+
self.tab_widget.setCurrentIndex(0)
|
|
80
|
+
|
|
81
|
+
# Connect to tab change to lazy load visualization
|
|
82
|
+
self.tab_widget.currentChanged.connect(self._on_tab_changed)
|
|
73
83
|
|
|
74
84
|
# Add panels to splitter
|
|
75
85
|
main_splitter.addWidget(left_panel)
|
|
@@ -167,14 +177,34 @@ class MainWindow(QMainWindow):
|
|
|
167
177
|
self.connection_view.connection_created.connect(self._on_connection_created)
|
|
168
178
|
self.collection_browser.collection_selected.connect(self._on_collection_selected)
|
|
169
179
|
|
|
180
|
+
def _on_tab_changed(self, index: int):
|
|
181
|
+
"""Handle tab change - lazy load visualization tab."""
|
|
182
|
+
if index == 3 and self.visualization_view is None:
|
|
183
|
+
# Lazy load visualization view
|
|
184
|
+
from vector_inspector.ui.views.visualization_view import VisualizationView
|
|
185
|
+
self.visualization_view = VisualizationView(self.connection)
|
|
186
|
+
# Replace placeholder with actual view
|
|
187
|
+
self.tab_widget.removeTab(3)
|
|
188
|
+
self.tab_widget.insertTab(3, self.visualization_view, "Visualization")
|
|
189
|
+
self.tab_widget.setCurrentIndex(3)
|
|
190
|
+
# Set collection if one is already selected
|
|
191
|
+
if self.current_collection:
|
|
192
|
+
self.visualization_view.set_collection(self.current_collection)
|
|
193
|
+
|
|
170
194
|
def _on_connection_created(self, new_connection: VectorDBConnection):
|
|
171
195
|
"""Handle when a new connection instance is created."""
|
|
172
196
|
self.connection = new_connection
|
|
173
197
|
# Update all views with new connection
|
|
174
198
|
self.collection_browser.connection = new_connection
|
|
199
|
+
self.info_panel.connection = new_connection
|
|
175
200
|
self.metadata_view.connection = new_connection
|
|
176
201
|
self.search_view.connection = new_connection
|
|
177
|
-
|
|
202
|
+
# Only update visualization if it's been created
|
|
203
|
+
if self.visualization_view is not None:
|
|
204
|
+
self.visualization_view.connection = new_connection
|
|
205
|
+
# Refresh the collection browser to show new database's collections
|
|
206
|
+
if new_connection.is_connected:
|
|
207
|
+
self.collection_browser.refresh()
|
|
178
208
|
|
|
179
209
|
def _on_connect(self):
|
|
180
210
|
"""Handle connect action."""
|
|
@@ -187,6 +217,11 @@ class MainWindow(QMainWindow):
|
|
|
187
217
|
self.statusBar.showMessage("Disconnected")
|
|
188
218
|
self.connection_changed.emit(False)
|
|
189
219
|
self.collection_browser.clear()
|
|
220
|
+
# Clear info panel
|
|
221
|
+
self.info_panel.refresh_database_info()
|
|
222
|
+
else:
|
|
223
|
+
# Always clear collection browser on disconnect
|
|
224
|
+
self.collection_browser.clear()
|
|
190
225
|
|
|
191
226
|
def _on_connection_status_changed(self, connected: bool):
|
|
192
227
|
"""Handle connection status change."""
|
|
@@ -194,24 +229,45 @@ class MainWindow(QMainWindow):
|
|
|
194
229
|
self.statusBar.showMessage("Connected")
|
|
195
230
|
self.connection_changed.emit(True)
|
|
196
231
|
self._on_refresh_collections()
|
|
232
|
+
# Refresh info panel with new connection
|
|
233
|
+
self.info_panel.refresh_database_info()
|
|
197
234
|
else:
|
|
198
235
|
self.statusBar.showMessage("Connection failed")
|
|
199
236
|
self.connection_changed.emit(False)
|
|
237
|
+
# Clear info panel and collection browser
|
|
238
|
+
self.info_panel.refresh_database_info()
|
|
239
|
+
self.collection_browser.clear()
|
|
200
240
|
|
|
201
241
|
def _on_collection_selected(self, collection_name: str):
|
|
202
242
|
"""Handle collection selection."""
|
|
203
243
|
self.current_collection = collection_name
|
|
204
244
|
self.statusBar.showMessage(f"Collection: {collection_name}")
|
|
205
245
|
|
|
246
|
+
# Show loading dialog immediately
|
|
247
|
+
self.loading_dialog.show_loading(f"Loading collection '{collection_name}'...")
|
|
248
|
+
|
|
249
|
+
# Update views with new collection - use QTimer to allow loading dialog to appear first
|
|
250
|
+
QTimer.singleShot(10, lambda: self._update_views_for_collection(collection_name))
|
|
251
|
+
|
|
252
|
+
def _update_views_for_collection(self, collection_name: str):
|
|
253
|
+
"""Update all views with the selected collection."""
|
|
206
254
|
# Update all views with new collection
|
|
255
|
+
self.info_panel.set_collection(collection_name)
|
|
207
256
|
self.metadata_view.set_collection(collection_name)
|
|
208
257
|
self.search_view.set_collection(collection_name)
|
|
209
|
-
|
|
258
|
+
# Only update visualization if it's been created
|
|
259
|
+
if self.visualization_view is not None:
|
|
260
|
+
self.visualization_view.set_collection(collection_name)
|
|
261
|
+
|
|
262
|
+
# Hide loading dialog
|
|
263
|
+
self.loading_dialog.hide_loading()
|
|
210
264
|
|
|
211
265
|
def _on_refresh_collections(self):
|
|
212
266
|
"""Refresh collection list."""
|
|
213
267
|
if self.connection.is_connected:
|
|
214
268
|
self.collection_browser.refresh()
|
|
269
|
+
# Also refresh database info (collection count may have changed)
|
|
270
|
+
self.info_panel.refresh_database_info()
|
|
215
271
|
|
|
216
272
|
def _on_new_collection(self):
|
|
217
273
|
"""Create a new collection."""
|
{vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/ui/views/connection_view.py
RENAMED
|
@@ -5,7 +5,7 @@ from PySide6.QtWidgets import (
|
|
|
5
5
|
QPushButton, QDialog, QFormLayout, QLineEdit,
|
|
6
6
|
QRadioButton, QButtonGroup, QGroupBox, QFileDialog, QComboBox, QApplication, QCheckBox
|
|
7
7
|
)
|
|
8
|
-
from PySide6.QtCore import Signal
|
|
8
|
+
from PySide6.QtCore import Signal, QThread
|
|
9
9
|
|
|
10
10
|
from vector_inspector.core.connections.base_connection import VectorDBConnection
|
|
11
11
|
from vector_inspector.core.connections.chroma_connection import ChromaDBConnection
|
|
@@ -14,6 +14,28 @@ from vector_inspector.ui.components.loading_dialog import LoadingDialog
|
|
|
14
14
|
from vector_inspector.services.settings_service import SettingsService
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
class ConnectionThread(QThread):
|
|
18
|
+
"""Background thread for connecting to database."""
|
|
19
|
+
|
|
20
|
+
finished = Signal(bool, list) # success, collections
|
|
21
|
+
|
|
22
|
+
def __init__(self, connection):
|
|
23
|
+
super().__init__()
|
|
24
|
+
self.connection = connection
|
|
25
|
+
|
|
26
|
+
def run(self):
|
|
27
|
+
"""Connect to database and get collections."""
|
|
28
|
+
try:
|
|
29
|
+
success = self.connection.connect()
|
|
30
|
+
if success:
|
|
31
|
+
collections = self.connection.list_collections()
|
|
32
|
+
self.finished.emit(True, collections)
|
|
33
|
+
else:
|
|
34
|
+
self.finished.emit(False, [])
|
|
35
|
+
except Exception:
|
|
36
|
+
self.finished.emit(False, [])
|
|
37
|
+
|
|
38
|
+
|
|
17
39
|
class ConnectionDialog(QDialog):
|
|
18
40
|
"""Dialog for configuring database connection."""
|
|
19
41
|
|
|
@@ -175,6 +197,9 @@ class ConnectionDialog(QDialog):
|
|
|
175
197
|
|
|
176
198
|
def get_connection_config(self):
|
|
177
199
|
"""Get connection configuration from dialog."""
|
|
200
|
+
# Get current provider from combo box to ensure it's up to date
|
|
201
|
+
self.provider = self.provider_combo.currentData()
|
|
202
|
+
|
|
178
203
|
config = {"provider": self.provider}
|
|
179
204
|
|
|
180
205
|
if self.persistent_radio.isChecked():
|
|
@@ -294,6 +319,7 @@ class ConnectionView(QWidget):
|
|
|
294
319
|
self.connection = connection
|
|
295
320
|
self.loading_dialog = LoadingDialog("Connecting to database...", self)
|
|
296
321
|
self.settings_service = SettingsService()
|
|
322
|
+
self.connection_thread = None
|
|
297
323
|
self._setup_ui()
|
|
298
324
|
|
|
299
325
|
# Try to auto-connect if enabled in settings
|
|
@@ -339,7 +365,6 @@ class ConnectionView(QWidget):
|
|
|
339
365
|
def _connect_with_config(self, config: dict):
|
|
340
366
|
"""Connect to database with given configuration."""
|
|
341
367
|
self.loading_dialog.show_loading("Connecting to database...")
|
|
342
|
-
QApplication.processEvents()
|
|
343
368
|
|
|
344
369
|
provider = config.get("provider", "chromadb")
|
|
345
370
|
conn_type = config.get("type")
|
|
@@ -367,11 +392,25 @@ class ConnectionView(QWidget):
|
|
|
367
392
|
else: # ephemeral
|
|
368
393
|
self.connection = ChromaDBConnection()
|
|
369
394
|
|
|
395
|
+
# Store config for later use
|
|
396
|
+
self._pending_config = config
|
|
397
|
+
|
|
370
398
|
# Notify parent that connection instance changed
|
|
371
399
|
self.connection_created.emit(self.connection)
|
|
372
|
-
|
|
373
|
-
|
|
400
|
+
|
|
401
|
+
# Start background thread to connect
|
|
402
|
+
self.connection_thread = ConnectionThread(self.connection)
|
|
403
|
+
self.connection_thread.finished.connect(self._on_connection_finished)
|
|
404
|
+
self.connection_thread.start()
|
|
405
|
+
|
|
406
|
+
def _on_connection_finished(self, success: bool, collections: list):
|
|
407
|
+
"""Handle connection thread completion."""
|
|
408
|
+
self.loading_dialog.hide_loading()
|
|
409
|
+
|
|
374
410
|
if success:
|
|
411
|
+
config = self._pending_config
|
|
412
|
+
provider = config.get("provider", "chromadb")
|
|
413
|
+
|
|
375
414
|
# Show provider, path/host + collection count for clarity
|
|
376
415
|
details = []
|
|
377
416
|
details.append(f"provider: {provider}")
|
|
@@ -380,7 +419,6 @@ class ConnectionView(QWidget):
|
|
|
380
419
|
if hasattr(self.connection, 'host') and self.connection.host:
|
|
381
420
|
port = getattr(self.connection, 'port', None)
|
|
382
421
|
details.append(f"host: {self.connection.host}:{port}")
|
|
383
|
-
collections = self.connection.list_collections()
|
|
384
422
|
count_text = f"collections: {len(collections)}"
|
|
385
423
|
info = ", ".join(details)
|
|
386
424
|
self.status_label.setText(f"Status: Connected ({info}, {count_text})")
|
|
@@ -401,9 +439,6 @@ class ConnectionView(QWidget):
|
|
|
401
439
|
self.disconnect_button.setEnabled(False)
|
|
402
440
|
self.connection_changed.emit(False)
|
|
403
441
|
|
|
404
|
-
# Close loading dialog after everything is complete
|
|
405
|
-
self.loading_dialog.hide_loading()
|
|
406
|
-
|
|
407
442
|
def _disconnect(self):
|
|
408
443
|
"""Disconnect from database."""
|
|
409
444
|
self.connection.disconnect()
|
|
@@ -0,0 +1,287 @@
|
|
|
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
|
+
# Collection Information Section
|
|
60
|
+
self.collection_group = QGroupBox("Collection Information")
|
|
61
|
+
collection_layout = QVBoxLayout()
|
|
62
|
+
|
|
63
|
+
self.collection_name_label = self._create_info_row("Name:", "No collection selected")
|
|
64
|
+
self.vector_dim_label = self._create_info_row("Vector Dimension:", "N/A")
|
|
65
|
+
self.distance_metric_label = self._create_info_row("Distance Metric:", "N/A")
|
|
66
|
+
self.total_points_label = self._create_info_row("Total Points:", "0")
|
|
67
|
+
|
|
68
|
+
collection_layout.addWidget(self.collection_name_label)
|
|
69
|
+
collection_layout.addWidget(self.vector_dim_label)
|
|
70
|
+
collection_layout.addWidget(self.distance_metric_label)
|
|
71
|
+
collection_layout.addWidget(self.total_points_label)
|
|
72
|
+
|
|
73
|
+
# Payload Schema subsection
|
|
74
|
+
schema_label = QLabel("<b>Payload Schema:</b>")
|
|
75
|
+
collection_layout.addWidget(schema_label)
|
|
76
|
+
|
|
77
|
+
self.schema_label = QLabel("N/A")
|
|
78
|
+
self.schema_label.setWordWrap(True)
|
|
79
|
+
self.schema_label.setStyleSheet("color: gray; padding-left: 20px;")
|
|
80
|
+
collection_layout.addWidget(self.schema_label)
|
|
81
|
+
|
|
82
|
+
# Provider-specific details
|
|
83
|
+
provider_details_label = QLabel("<b>Provider-Specific Details:</b>")
|
|
84
|
+
collection_layout.addWidget(provider_details_label)
|
|
85
|
+
|
|
86
|
+
self.provider_details_label = QLabel("N/A")
|
|
87
|
+
self.provider_details_label.setWordWrap(True)
|
|
88
|
+
self.provider_details_label.setStyleSheet("color: gray; padding-left: 20px;")
|
|
89
|
+
collection_layout.addWidget(self.provider_details_label)
|
|
90
|
+
|
|
91
|
+
self.collection_group.setLayout(collection_layout)
|
|
92
|
+
container_layout.addWidget(self.collection_group)
|
|
93
|
+
|
|
94
|
+
# Add stretch to push content to top
|
|
95
|
+
container_layout.addStretch()
|
|
96
|
+
|
|
97
|
+
scroll.setWidget(container)
|
|
98
|
+
layout.addWidget(scroll)
|
|
99
|
+
|
|
100
|
+
# Initial state
|
|
101
|
+
self.refresh_database_info()
|
|
102
|
+
|
|
103
|
+
def _create_info_row(self, label: str, value: str) -> QWidget:
|
|
104
|
+
"""Create a row with label and value."""
|
|
105
|
+
row = QWidget()
|
|
106
|
+
row_layout = QHBoxLayout(row)
|
|
107
|
+
row_layout.setContentsMargins(0, 2, 0, 2)
|
|
108
|
+
|
|
109
|
+
label_widget = QLabel(f"<b>{label}</b>")
|
|
110
|
+
label_widget.setMinimumWidth(150)
|
|
111
|
+
row_layout.addWidget(label_widget)
|
|
112
|
+
|
|
113
|
+
value_widget = QLabel(value)
|
|
114
|
+
value_widget.setWordWrap(True)
|
|
115
|
+
value_widget.setStyleSheet("color: white;")
|
|
116
|
+
row_layout.addWidget(value_widget, stretch=1)
|
|
117
|
+
|
|
118
|
+
# Store value widget for later updates (use setProperty for type safety)
|
|
119
|
+
row.setProperty("value_label", value_widget)
|
|
120
|
+
|
|
121
|
+
return row
|
|
122
|
+
|
|
123
|
+
def refresh_database_info(self):
|
|
124
|
+
"""Refresh database connection information."""
|
|
125
|
+
if not self.connection or not self.connection.is_connected:
|
|
126
|
+
self._update_label(self.provider_label, "Not connected")
|
|
127
|
+
self._update_label(self.connection_type_label, "N/A")
|
|
128
|
+
self._update_label(self.endpoint_label, "N/A")
|
|
129
|
+
self._update_label(self.api_key_label, "N/A")
|
|
130
|
+
self._update_label(self.status_label, "Disconnected")
|
|
131
|
+
self._update_label(self.collections_count_label, "0")
|
|
132
|
+
# Also clear collection info
|
|
133
|
+
self._update_label(self.collection_name_label, "No collection selected")
|
|
134
|
+
self._update_label(self.vector_dim_label, "N/A")
|
|
135
|
+
self._update_label(self.distance_metric_label, "N/A")
|
|
136
|
+
self._update_label(self.total_points_label, "0")
|
|
137
|
+
self.schema_label.setText("N/A")
|
|
138
|
+
self.provider_details_label.setText("N/A")
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
# Get provider name
|
|
142
|
+
provider_name = self.connection.__class__.__name__.replace("Connection", "")
|
|
143
|
+
self._update_label(self.provider_label, provider_name)
|
|
144
|
+
|
|
145
|
+
# Get connection details
|
|
146
|
+
if isinstance(self.connection, ChromaDBConnection):
|
|
147
|
+
if self.connection.path:
|
|
148
|
+
self._update_label(self.connection_type_label, "Persistent (Local)")
|
|
149
|
+
self._update_label(self.endpoint_label, self.connection.path)
|
|
150
|
+
elif self.connection.host and self.connection.port:
|
|
151
|
+
self._update_label(self.connection_type_label, "HTTP (Remote)")
|
|
152
|
+
self._update_label(self.endpoint_label, f"{self.connection.host}:{self.connection.port}")
|
|
153
|
+
else:
|
|
154
|
+
self._update_label(self.connection_type_label, "Ephemeral (In-Memory)")
|
|
155
|
+
self._update_label(self.endpoint_label, "N/A")
|
|
156
|
+
self._update_label(self.api_key_label, "Not required")
|
|
157
|
+
|
|
158
|
+
elif isinstance(self.connection, QdrantConnection):
|
|
159
|
+
if self.connection.path:
|
|
160
|
+
self._update_label(self.connection_type_label, "Embedded (Local)")
|
|
161
|
+
self._update_label(self.endpoint_label, self.connection.path)
|
|
162
|
+
elif self.connection.url:
|
|
163
|
+
self._update_label(self.connection_type_label, "Remote (URL)")
|
|
164
|
+
self._update_label(self.endpoint_label, self.connection.url)
|
|
165
|
+
elif self.connection.host:
|
|
166
|
+
self._update_label(self.connection_type_label, "Remote (Host)")
|
|
167
|
+
self._update_label(self.endpoint_label, f"{self.connection.host}:{self.connection.port}")
|
|
168
|
+
else:
|
|
169
|
+
self._update_label(self.connection_type_label, "In-Memory")
|
|
170
|
+
self._update_label(self.endpoint_label, "N/A")
|
|
171
|
+
|
|
172
|
+
if self.connection.api_key:
|
|
173
|
+
self._update_label(self.api_key_label, "Present (hidden)")
|
|
174
|
+
else:
|
|
175
|
+
self._update_label(self.api_key_label, "Not configured")
|
|
176
|
+
else:
|
|
177
|
+
self._update_label(self.connection_type_label, "Unknown")
|
|
178
|
+
self._update_label(self.endpoint_label, "N/A")
|
|
179
|
+
self._update_label(self.api_key_label, "Unknown")
|
|
180
|
+
|
|
181
|
+
# Status
|
|
182
|
+
self._update_label(self.status_label, "Connected" if self.connection.is_connected else "Disconnected")
|
|
183
|
+
|
|
184
|
+
# Count collections
|
|
185
|
+
try:
|
|
186
|
+
collections = self.connection.list_collections()
|
|
187
|
+
self._update_label(self.collections_count_label, str(len(collections)))
|
|
188
|
+
except Exception as e:
|
|
189
|
+
self._update_label(self.collections_count_label, "Error")
|
|
190
|
+
|
|
191
|
+
def refresh_collection_info(self):
|
|
192
|
+
"""Refresh collection-specific information."""
|
|
193
|
+
if not self.current_collection or not self.connection or not self.connection.is_connected:
|
|
194
|
+
self._update_label(self.collection_name_label, "No collection selected")
|
|
195
|
+
self._update_label(self.vector_dim_label, "N/A")
|
|
196
|
+
self._update_label(self.distance_metric_label, "N/A")
|
|
197
|
+
self._update_label(self.total_points_label, "0")
|
|
198
|
+
self.schema_label.setText("N/A")
|
|
199
|
+
self.provider_details_label.setText("N/A")
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
# Get collection info
|
|
204
|
+
collection_info = self.connection.get_collection_info(self.current_collection)
|
|
205
|
+
|
|
206
|
+
if not collection_info:
|
|
207
|
+
self._update_label(self.collection_name_label, self.current_collection)
|
|
208
|
+
self._update_label(self.vector_dim_label, "Unable to retrieve")
|
|
209
|
+
self._update_label(self.distance_metric_label, "Unable to retrieve")
|
|
210
|
+
self._update_label(self.total_points_label, "Unable to retrieve")
|
|
211
|
+
self.schema_label.setText("Unable to retrieve collection info")
|
|
212
|
+
self.provider_details_label.setText("N/A")
|
|
213
|
+
return
|
|
214
|
+
|
|
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')}")
|
|
261
|
+
|
|
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;")
|
|
268
|
+
|
|
269
|
+
except Exception as e:
|
|
270
|
+
self._update_label(self.collection_name_label, self.current_collection)
|
|
271
|
+
self._update_label(self.vector_dim_label, "Error")
|
|
272
|
+
self._update_label(self.distance_metric_label, "Error")
|
|
273
|
+
self._update_label(self.total_points_label, "Error")
|
|
274
|
+
self.schema_label.setText(f"Error: {str(e)}")
|
|
275
|
+
self.schema_label.setStyleSheet("color: red; padding-left: 20px;")
|
|
276
|
+
self.provider_details_label.setText("N/A")
|
|
277
|
+
|
|
278
|
+
def set_collection(self, collection_name: str):
|
|
279
|
+
"""Set the current collection and refresh its information."""
|
|
280
|
+
self.current_collection = collection_name
|
|
281
|
+
self.refresh_collection_info()
|
|
282
|
+
|
|
283
|
+
def _update_label(self, row_widget: QWidget, value: str):
|
|
284
|
+
"""Update the value label in an info row."""
|
|
285
|
+
value_label = row_widget.property("value_label")
|
|
286
|
+
if value_label and isinstance(value_label, QLabel):
|
|
287
|
+
value_label.setText(value)
|
{vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/ui/views/metadata_view.py
RENAMED
|
@@ -7,7 +7,7 @@ from PySide6.QtWidgets import (
|
|
|
7
7
|
QLineEdit, QComboBox, QGroupBox, QHeaderView, QMessageBox, QDialog,
|
|
8
8
|
QFileDialog, QMenu
|
|
9
9
|
)
|
|
10
|
-
from PySide6.QtCore import Qt, QTimer
|
|
10
|
+
from PySide6.QtCore import Qt, QTimer, QThread, Signal
|
|
11
11
|
|
|
12
12
|
from vector_inspector.core.connections.base_connection import VectorDBConnection
|
|
13
13
|
from vector_inspector.ui.components.item_dialog import ItemDialog
|
|
@@ -19,6 +19,37 @@ from vector_inspector.services.settings_service import SettingsService
|
|
|
19
19
|
from PySide6.QtWidgets import QApplication
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
class DataLoadThread(QThread):
|
|
23
|
+
"""Background thread for loading collection data."""
|
|
24
|
+
|
|
25
|
+
finished = Signal(dict)
|
|
26
|
+
error = Signal(str)
|
|
27
|
+
|
|
28
|
+
def __init__(self, connection, collection, page_size, offset, server_filter):
|
|
29
|
+
super().__init__()
|
|
30
|
+
self.connection = connection
|
|
31
|
+
self.collection = collection
|
|
32
|
+
self.page_size = page_size
|
|
33
|
+
self.offset = offset
|
|
34
|
+
self.server_filter = server_filter
|
|
35
|
+
|
|
36
|
+
def run(self):
|
|
37
|
+
"""Load data from database."""
|
|
38
|
+
try:
|
|
39
|
+
data = self.connection.get_all_items(
|
|
40
|
+
self.collection,
|
|
41
|
+
limit=self.page_size,
|
|
42
|
+
offset=self.offset,
|
|
43
|
+
where=self.server_filter
|
|
44
|
+
)
|
|
45
|
+
if data:
|
|
46
|
+
self.finished.emit(data)
|
|
47
|
+
else:
|
|
48
|
+
self.error.emit("Failed to load data")
|
|
49
|
+
except Exception as e:
|
|
50
|
+
self.error.emit(str(e))
|
|
51
|
+
|
|
52
|
+
|
|
22
53
|
class MetadataView(QWidget):
|
|
23
54
|
"""View for browsing collection data and metadata."""
|
|
24
55
|
|
|
@@ -31,6 +62,7 @@ class MetadataView(QWidget):
|
|
|
31
62
|
self.current_page = 0
|
|
32
63
|
self.loading_dialog = LoadingDialog("Loading data...", self)
|
|
33
64
|
self.settings_service = SettingsService()
|
|
65
|
+
self.load_thread: Optional[DataLoadThread] = None
|
|
34
66
|
|
|
35
67
|
# Debounce timer for filter changes
|
|
36
68
|
self.filter_reload_timer = QTimer()
|
|
@@ -145,21 +177,11 @@ class MetadataView(QWidget):
|
|
|
145
177
|
self.current_collection = collection_name
|
|
146
178
|
self.current_page = 0
|
|
147
179
|
|
|
148
|
-
#
|
|
149
|
-
self.
|
|
150
|
-
|
|
180
|
+
# Update filter builder with supported operators
|
|
181
|
+
operators = self.connection.get_supported_filter_operators()
|
|
182
|
+
self.filter_builder.set_operators(operators)
|
|
151
183
|
|
|
152
|
-
|
|
153
|
-
# Update filter builder with supported operators
|
|
154
|
-
operators = self.connection.get_supported_filter_operators()
|
|
155
|
-
self.filter_builder.set_operators(operators)
|
|
156
|
-
|
|
157
|
-
self._load_data_internal()
|
|
158
|
-
|
|
159
|
-
# Ensure UI is fully updated before hiding loading dialog
|
|
160
|
-
QApplication.processEvents()
|
|
161
|
-
finally:
|
|
162
|
-
self.loading_dialog.hide_loading()
|
|
184
|
+
self._load_data_internal()
|
|
163
185
|
|
|
164
186
|
def _load_data(self):
|
|
165
187
|
"""Load data from current collection (with loading dialog)."""
|
|
@@ -182,35 +204,53 @@ class MetadataView(QWidget):
|
|
|
182
204
|
self.table.setRowCount(0)
|
|
183
205
|
return
|
|
184
206
|
|
|
207
|
+
# Cancel any existing load thread
|
|
208
|
+
if self.load_thread and self.load_thread.isRunning():
|
|
209
|
+
self.load_thread.quit()
|
|
210
|
+
self.load_thread.wait()
|
|
211
|
+
|
|
185
212
|
offset = self.current_page * self.page_size
|
|
186
213
|
|
|
187
214
|
# Get filters split into server-side and client-side
|
|
188
215
|
server_filter = None
|
|
189
|
-
client_filters = []
|
|
216
|
+
self.client_filters = []
|
|
190
217
|
if self.filter_group.isChecked() and self.filter_builder.has_filters():
|
|
191
|
-
server_filter, client_filters = self.filter_builder.get_filters_split()
|
|
218
|
+
server_filter, self.client_filters = self.filter_builder.get_filters_split()
|
|
192
219
|
|
|
193
|
-
|
|
220
|
+
# Start background thread to load data
|
|
221
|
+
self.load_thread = DataLoadThread(
|
|
222
|
+
self.connection,
|
|
194
223
|
self.current_collection,
|
|
195
|
-
|
|
196
|
-
offset
|
|
197
|
-
|
|
224
|
+
self.page_size,
|
|
225
|
+
offset,
|
|
226
|
+
server_filter
|
|
198
227
|
)
|
|
199
|
-
|
|
228
|
+
self.load_thread.finished.connect(self._on_data_loaded)
|
|
229
|
+
self.load_thread.error.connect(self._on_load_error)
|
|
230
|
+
self.load_thread.start()
|
|
231
|
+
|
|
232
|
+
def _on_data_loaded(self, data: Dict[str, Any]):
|
|
233
|
+
"""Handle data loaded from background thread."""
|
|
200
234
|
# Apply client-side filters if any
|
|
201
|
-
if client_filters and data:
|
|
202
|
-
data = apply_client_side_filters(data, client_filters)
|
|
235
|
+
if self.client_filters and data:
|
|
236
|
+
data = apply_client_side_filters(data, self.client_filters)
|
|
203
237
|
|
|
204
238
|
if not data:
|
|
205
|
-
self.status_label.setText("
|
|
239
|
+
self.status_label.setText("No data after filtering")
|
|
206
240
|
self.table.setRowCount(0)
|
|
207
241
|
return
|
|
242
|
+
|
|
208
243
|
self.current_data = data
|
|
209
244
|
self._populate_table(data)
|
|
210
245
|
self._update_pagination_controls()
|
|
211
246
|
|
|
212
247
|
# Update filter builder with available metadata fields
|
|
213
248
|
self._update_filter_fields(data)
|
|
249
|
+
|
|
250
|
+
def _on_load_error(self, error_msg: str):
|
|
251
|
+
"""Handle error from background thread."""
|
|
252
|
+
self.status_label.setText(f"Failed to load data: {error_msg}")
|
|
253
|
+
self.table.setRowCount(0)
|
|
214
254
|
|
|
215
255
|
def _update_filter_fields(self, data: Dict[str, Any]):
|
|
216
256
|
"""Update filter builder with available metadata field names."""
|
|
@@ -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:
|
|
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]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/services/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/services/filter_service.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/ui/components/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/ui/views/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{vector_inspector-0.2.4.3 → vector_inspector-0.2.6}/src/vector_inspector/ui/views/search_view.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|