vector-inspector 0.2.0__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 (32) hide show
  1. vector_inspector/__init__.py +3 -0
  2. vector_inspector/__main__.py +4 -0
  3. vector_inspector/core/__init__.py +1 -0
  4. vector_inspector/core/connections/__init__.py +7 -0
  5. vector_inspector/core/connections/base_connection.py +233 -0
  6. vector_inspector/core/connections/chroma_connection.py +384 -0
  7. vector_inspector/core/connections/qdrant_connection.py +723 -0
  8. vector_inspector/core/connections/template_connection.py +346 -0
  9. vector_inspector/main.py +21 -0
  10. vector_inspector/services/__init__.py +1 -0
  11. vector_inspector/services/backup_restore_service.py +286 -0
  12. vector_inspector/services/filter_service.py +72 -0
  13. vector_inspector/services/import_export_service.py +287 -0
  14. vector_inspector/services/settings_service.py +60 -0
  15. vector_inspector/services/visualization_service.py +116 -0
  16. vector_inspector/ui/__init__.py +1 -0
  17. vector_inspector/ui/components/__init__.py +1 -0
  18. vector_inspector/ui/components/backup_restore_dialog.py +350 -0
  19. vector_inspector/ui/components/filter_builder.py +370 -0
  20. vector_inspector/ui/components/item_dialog.py +118 -0
  21. vector_inspector/ui/components/loading_dialog.py +30 -0
  22. vector_inspector/ui/main_window.py +288 -0
  23. vector_inspector/ui/views/__init__.py +1 -0
  24. vector_inspector/ui/views/collection_browser.py +112 -0
  25. vector_inspector/ui/views/connection_view.py +423 -0
  26. vector_inspector/ui/views/metadata_view.py +555 -0
  27. vector_inspector/ui/views/search_view.py +268 -0
  28. vector_inspector/ui/views/visualization_view.py +245 -0
  29. vector_inspector-0.2.0.dist-info/METADATA +382 -0
  30. vector_inspector-0.2.0.dist-info/RECORD +32 -0
  31. vector_inspector-0.2.0.dist-info/WHEEL +4 -0
  32. vector_inspector-0.2.0.dist-info/entry_points.txt +5 -0
@@ -0,0 +1,268 @@
1
+ """Search interface for similarity queries."""
2
+
3
+ from typing import Optional, Dict, Any
4
+ from PySide6.QtWidgets import (
5
+ QWidget, QVBoxLayout, QHBoxLayout, QTextEdit,
6
+ QPushButton, QLabel, QSpinBox, QTableWidget,
7
+ QTableWidgetItem, QGroupBox, QSplitter, QCheckBox, QApplication
8
+ )
9
+ from PySide6.QtCore import Qt
10
+
11
+ from vector_inspector.core.connections.base_connection import VectorDBConnection
12
+ from vector_inspector.ui.components.filter_builder import FilterBuilder
13
+ from vector_inspector.ui.components.loading_dialog import LoadingDialog
14
+ from vector_inspector.services.filter_service import apply_client_side_filters
15
+
16
+
17
+ class SearchView(QWidget):
18
+ """View for performing similarity searches."""
19
+
20
+ def __init__(self, connection: VectorDBConnection, parent=None):
21
+ super().__init__(parent)
22
+ self.connection = connection
23
+ self.current_collection: str = ""
24
+ self.search_results: Optional[Dict[str, Any]] = None
25
+ self.loading_dialog = LoadingDialog("Searching...", self)
26
+
27
+ self._setup_ui()
28
+
29
+ def _setup_ui(self):
30
+ """Setup widget UI."""
31
+ layout = QVBoxLayout(self)
32
+
33
+ # Create splitter for query and results
34
+ splitter = QSplitter(Qt.Vertical)
35
+
36
+ # Query section
37
+ query_widget = QWidget()
38
+ query_layout = QVBoxLayout(query_widget)
39
+
40
+ query_group = QGroupBox("Search Query")
41
+ query_group_layout = QVBoxLayout()
42
+
43
+ # Query input
44
+ query_group_layout.addWidget(QLabel("Enter search text:"))
45
+ self.query_input = QTextEdit()
46
+ self.query_input.setMaximumHeight(100)
47
+ self.query_input.setPlaceholderText("Enter text to search for similar vectors...")
48
+ query_group_layout.addWidget(self.query_input)
49
+
50
+ # Search controls
51
+ controls_layout = QHBoxLayout()
52
+
53
+ controls_layout.addWidget(QLabel("Results:"))
54
+ self.n_results_spin = QSpinBox()
55
+ self.n_results_spin.setMinimum(1)
56
+ self.n_results_spin.setMaximum(100)
57
+ self.n_results_spin.setValue(10)
58
+ controls_layout.addWidget(self.n_results_spin)
59
+
60
+ controls_layout.addStretch()
61
+
62
+ self.search_button = QPushButton("Search")
63
+ self.search_button.clicked.connect(self._perform_search)
64
+ self.search_button.setDefault(True)
65
+ controls_layout.addWidget(self.search_button)
66
+
67
+ query_group_layout.addLayout(controls_layout)
68
+ query_group.setLayout(query_group_layout)
69
+ query_layout.addWidget(query_group)
70
+
71
+ # Advanced filters section
72
+ filter_group = QGroupBox("Advanced Metadata Filters")
73
+ filter_group.setCheckable(True)
74
+ filter_group.setChecked(False)
75
+ filter_group_layout = QVBoxLayout()
76
+
77
+ # Filter builder
78
+ self.filter_builder = FilterBuilder()
79
+ filter_group_layout.addWidget(self.filter_builder)
80
+
81
+ filter_group.setLayout(filter_group_layout)
82
+ query_layout.addWidget(filter_group)
83
+ self.filter_group = filter_group
84
+
85
+ splitter.addWidget(query_widget)
86
+
87
+ # Results section
88
+ results_widget = QWidget()
89
+ results_layout = QVBoxLayout(results_widget)
90
+ results_layout.setContentsMargins(0, 0, 0, 0)
91
+
92
+ results_group = QGroupBox("Search Results")
93
+ results_group_layout = QVBoxLayout()
94
+
95
+ self.results_table = QTableWidget()
96
+ self.results_table.setSelectionBehavior(QTableWidget.SelectRows)
97
+ self.results_table.setAlternatingRowColors(True)
98
+ results_group_layout.addWidget(self.results_table)
99
+
100
+ self.results_status = QLabel("No search performed")
101
+ self.results_status.setStyleSheet("color: gray;")
102
+ results_group_layout.addWidget(self.results_status)
103
+
104
+ results_group.setLayout(results_group_layout)
105
+ results_layout.addWidget(results_group)
106
+
107
+ splitter.addWidget(results_widget)
108
+
109
+ # Set splitter proportions
110
+ splitter.setStretchFactor(0, 1)
111
+ splitter.setStretchFactor(1, 2)
112
+
113
+ layout.addWidget(splitter)
114
+
115
+ def set_collection(self, collection_name: str):
116
+ """Set the current collection to search."""
117
+ self.current_collection = collection_name
118
+ self.search_results = None
119
+
120
+ # Clear search form inputs
121
+ self.query_input.clear()
122
+ self.results_table.setRowCount(0)
123
+ self.results_status.setText(f"Collection: {collection_name}")
124
+
125
+ # Reset filters
126
+ self.filter_builder._clear_all()
127
+ self.filter_group.setChecked(False)
128
+
129
+ # Update filter builder with supported operators
130
+ operators = self.connection.get_supported_filter_operators()
131
+ self.filter_builder.set_operators(operators)
132
+
133
+ # Load metadata fields immediately (even if tab is not visible)
134
+ self._load_metadata_fields()
135
+
136
+ def _load_metadata_fields(self):
137
+ """Load metadata field names from collection for filter builder."""
138
+ if not self.current_collection:
139
+ return
140
+
141
+ try:
142
+ # Get a small sample to extract field names
143
+ sample_data = self.connection.get_all_items(
144
+ self.current_collection,
145
+ limit=1
146
+ )
147
+
148
+ if sample_data and sample_data.get("metadatas"):
149
+ metadatas = sample_data["metadatas"]
150
+ if metadatas and len(metadatas) > 0 and metadatas[0]:
151
+ field_names = sorted(metadatas[0].keys())
152
+ self.filter_builder.set_available_fields(field_names)
153
+ except Exception as e:
154
+ # Silently ignore errors - fields can still be typed manually
155
+ print(f"Note: Could not auto-populate filter fields: {e}")
156
+
157
+ def _perform_search(self):
158
+ """Perform similarity search."""
159
+ if not self.current_collection:
160
+ self.results_status.setText("No collection selected")
161
+ return
162
+
163
+ query_text = self.query_input.toPlainText().strip()
164
+ if not query_text:
165
+ self.results_status.setText("Please enter search text")
166
+ return
167
+
168
+ n_results = self.n_results_spin.value()
169
+
170
+ # Get filters split into server-side and client-side
171
+ server_filter = None
172
+ client_filters = []
173
+ if self.filter_group.isChecked() and self.filter_builder.has_filters():
174
+ server_filter, client_filters = self.filter_builder.get_filters_split()
175
+ if server_filter or client_filters:
176
+ filter_summary = self.filter_builder.get_filter_summary()
177
+ self.results_status.setText(f"Searching with filters: {filter_summary}")
178
+
179
+ # Show loading indicator
180
+ self.loading_dialog.show_loading("Searching for similar vectors...")
181
+ QApplication.processEvents()
182
+
183
+ try:
184
+ # Perform query
185
+ results = self.connection.query_collection(
186
+ self.current_collection,
187
+ query_texts=[query_text],
188
+ n_results=n_results,
189
+ where=server_filter
190
+ )
191
+ finally:
192
+ self.loading_dialog.hide_loading()
193
+
194
+ if not results:
195
+ self.results_status.setText("Search failed")
196
+ self.results_table.setRowCount(0)
197
+ return
198
+
199
+ # Apply client-side filters if any
200
+ if client_filters and results:
201
+ # Restructure results for filtering
202
+ filter_data = {
203
+ "ids": results.get("ids", [[]])[0],
204
+ "documents": results.get("documents", [[]])[0],
205
+ "metadatas": results.get("metadatas", [[]])[0],
206
+ }
207
+ filtered = apply_client_side_filters(filter_data, client_filters)
208
+
209
+ # Restructure back to query results format
210
+ results = {
211
+ "ids": [filtered["ids"]],
212
+ "documents": [filtered["documents"]],
213
+ "metadatas": [filtered["metadatas"]],
214
+ "distances": [[results.get("distances", [[]])[0][i]
215
+ for i, orig_id in enumerate(results.get("ids", [[]])[0])
216
+ if orig_id in filtered["ids"]]]
217
+ }
218
+
219
+ self.search_results = results
220
+ self._display_results(results)
221
+
222
+ def _display_results(self, results: Dict[str, Any]):
223
+ """Display search results in table."""
224
+ ids = results.get("ids", [[]])[0]
225
+ documents = results.get("documents", [[]])[0]
226
+ metadatas = results.get("metadatas", [[]])[0]
227
+ distances = results.get("distances", [[]])[0]
228
+
229
+ if not ids:
230
+ self.results_table.setRowCount(0)
231
+ self.results_status.setText("No results found")
232
+ return
233
+
234
+ # Determine columns
235
+ columns = ["Rank", "Distance", "ID", "Document"]
236
+ if metadatas and metadatas[0]:
237
+ metadata_keys = list(metadatas[0].keys())
238
+ columns.extend(metadata_keys)
239
+
240
+ self.results_table.setColumnCount(len(columns))
241
+ self.results_table.setHorizontalHeaderLabels(columns)
242
+ self.results_table.setRowCount(len(ids))
243
+
244
+ # Populate rows
245
+ for row, (id_val, doc, meta, dist) in enumerate(zip(ids, documents, metadatas, distances)):
246
+ # Rank
247
+ self.results_table.setItem(row, 0, QTableWidgetItem(str(row + 1)))
248
+
249
+ # Distance/similarity score
250
+ self.results_table.setItem(row, 1, QTableWidgetItem(f"{dist:.4f}"))
251
+
252
+ # ID
253
+ self.results_table.setItem(row, 2, QTableWidgetItem(str(id_val)))
254
+
255
+ # Document
256
+ doc_text = str(doc) if doc else ""
257
+ if len(doc_text) > 150:
258
+ doc_text = doc_text[:150] + "..."
259
+ self.results_table.setItem(row, 3, QTableWidgetItem(doc_text))
260
+
261
+ # Metadata columns
262
+ if meta:
263
+ for col_idx, key in enumerate(metadata_keys, start=4):
264
+ value = meta.get(key, "")
265
+ self.results_table.setItem(row, col_idx, QTableWidgetItem(str(value)))
266
+
267
+ self.results_table.resizeColumnsToContents()
268
+ self.results_status.setText(f"Found {len(ids)} results")
@@ -0,0 +1,245 @@
1
+ """Vector visualization view with dimensionality reduction."""
2
+
3
+ from typing import Optional, Dict, Any
4
+ import traceback
5
+ from PySide6.QtWidgets import (
6
+ QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
7
+ QLabel, QComboBox, QSpinBox, QGroupBox, QMessageBox
8
+ )
9
+ from PySide6.QtCore import Qt, QThread, Signal
10
+ from PySide6.QtWebEngineWidgets import QWebEngineView
11
+ import plotly.graph_objects as go
12
+ import numpy as np
13
+
14
+ from vector_inspector.core.connections.base_connection import VectorDBConnection
15
+ from vector_inspector.services.visualization_service import VisualizationService
16
+
17
+
18
+ class VisualizationThread(QThread):
19
+ """Background thread for dimensionality reduction."""
20
+
21
+ finished = Signal(np.ndarray)
22
+ error = Signal(str)
23
+
24
+ def __init__(self, embeddings, method, n_components):
25
+ super().__init__()
26
+ self.embeddings = embeddings
27
+ self.method = method
28
+ self.n_components = n_components
29
+
30
+ def run(self):
31
+ """Run dimensionality reduction."""
32
+ try:
33
+ result = VisualizationService.reduce_dimensions(
34
+ self.embeddings,
35
+ method=self.method,
36
+ n_components=self.n_components
37
+ )
38
+ if result is not None:
39
+ self.finished.emit(result)
40
+ else:
41
+ self.error.emit("Dimensionality reduction failed")
42
+ except Exception as e:
43
+ traceback.print_exc()
44
+ self.error.emit(str(e))
45
+
46
+
47
+ class VisualizationView(QWidget):
48
+ """View for visualizing vectors in 2D/3D."""
49
+
50
+ def __init__(self, connection: VectorDBConnection, parent=None):
51
+ super().__init__(parent)
52
+ self.connection = connection
53
+ self.current_collection: str = ""
54
+ self.current_data: Optional[Dict[str, Any]] = None
55
+ self.reduced_data: Optional[np.ndarray] = None
56
+ self.visualization_thread: Optional[VisualizationThread] = None
57
+
58
+ self._setup_ui()
59
+
60
+ def _setup_ui(self):
61
+ """Setup widget UI."""
62
+ layout = QVBoxLayout(self)
63
+
64
+ # Controls
65
+ controls_group = QGroupBox("Visualization Settings")
66
+ controls_layout = QHBoxLayout()
67
+
68
+ # Method selection
69
+ controls_layout.addWidget(QLabel("Method:"))
70
+ self.method_combo = QComboBox()
71
+ self.method_combo.addItems(["PCA", "t-SNE", "UMAP"])
72
+ controls_layout.addWidget(self.method_combo)
73
+
74
+ # Dimensions
75
+ controls_layout.addWidget(QLabel("Dimensions:"))
76
+ self.dimensions_combo = QComboBox()
77
+ self.dimensions_combo.addItems(["2D", "3D"])
78
+ controls_layout.addWidget(self.dimensions_combo)
79
+
80
+ # Sample size
81
+ controls_layout.addWidget(QLabel("Sample size:"))
82
+ self.sample_spin = QSpinBox()
83
+ self.sample_spin.setMinimum(10)
84
+ self.sample_spin.setMaximum(10000)
85
+ self.sample_spin.setValue(500)
86
+ self.sample_spin.setSingleStep(100)
87
+ controls_layout.addWidget(self.sample_spin)
88
+
89
+ controls_layout.addStretch()
90
+
91
+ # Generate button
92
+ self.generate_button = QPushButton("Generate Visualization")
93
+ self.generate_button.clicked.connect(self._generate_visualization)
94
+ controls_layout.addWidget(self.generate_button)
95
+
96
+ controls_group.setLayout(controls_layout)
97
+ layout.addWidget(controls_group)
98
+
99
+ # Embedded web view for Plotly
100
+ self.web_view = QWebEngineView()
101
+ layout.addWidget(self.web_view, stretch=10)
102
+
103
+ # Status
104
+ self.status_label = QLabel("No collection selected")
105
+ self.status_label.setStyleSheet("color: gray;")
106
+ self.status_label.setMaximumHeight(30)
107
+ layout.addWidget(self.status_label)
108
+
109
+ def set_collection(self, collection_name: str):
110
+ """Set the current collection to visualize."""
111
+ self.current_collection = collection_name
112
+ self.current_data = None
113
+ self.reduced_data = None
114
+ self.status_label.setText(f"Collection: {collection_name}")
115
+
116
+ def _generate_visualization(self):
117
+ """Generate visualization of vectors."""
118
+ if not self.current_collection:
119
+ QMessageBox.warning(self, "No Collection", "Please select a collection first.")
120
+ return
121
+
122
+ # Load data with embeddings
123
+ sample_size = self.sample_spin.value()
124
+ data = self.connection.get_all_items(
125
+ self.current_collection,
126
+ limit=sample_size
127
+ )
128
+
129
+ if data is None or not data or "embeddings" not in data or data["embeddings"] is None or len(data["embeddings"]) == 0:
130
+ QMessageBox.warning(
131
+ self,
132
+ "No Data",
133
+ "No embeddings found in collection. Make sure the collection contains vector embeddings."
134
+ )
135
+ return
136
+
137
+ self.current_data = data
138
+ self.status_label.setText("Reducing dimensions...")
139
+ self.generate_button.setEnabled(False)
140
+
141
+ # Get parameters
142
+ method = self.method_combo.currentText().lower()
143
+ if method == "t-sne":
144
+ method = "tsne"
145
+ n_components = 2 if self.dimensions_combo.currentText() == "2D" else 3
146
+
147
+ # Run dimensionality reduction in background thread
148
+ self.visualization_thread = VisualizationThread(
149
+ data["embeddings"],
150
+ method,
151
+ n_components
152
+ )
153
+ self.visualization_thread.finished.connect(self._on_reduction_finished)
154
+ self.visualization_thread.error.connect(self._on_reduction_error)
155
+ self.visualization_thread.start()
156
+
157
+ def _on_reduction_finished(self, reduced_data: np.ndarray):
158
+ """Handle dimensionality reduction completion."""
159
+ self.reduced_data = reduced_data
160
+ self._create_plot()
161
+ self.generate_button.setEnabled(True)
162
+ self.status_label.setText("Visualization complete")
163
+
164
+ def _on_reduction_error(self, error_msg: str):
165
+ """Handle dimensionality reduction error."""
166
+ print(f"Error: Visualization failed: {error_msg}")
167
+ QMessageBox.warning(self, "Error", f"Visualization failed: {error_msg}")
168
+ self.generate_button.setEnabled(True)
169
+ self.status_label.setText("Visualization failed")
170
+
171
+ def _create_plot(self):
172
+ """Create plotly visualization."""
173
+ if self.reduced_data is None or self.current_data is None:
174
+ return
175
+
176
+ ids = self.current_data.get("ids", [])
177
+ documents = self.current_data.get("documents", [])
178
+ metadatas = self.current_data.get("metadatas", [])
179
+
180
+ # Prepare hover text
181
+ hover_texts = []
182
+ for i, (id_val, doc) in enumerate(zip(ids, documents)):
183
+ doc_preview = str(doc)[:100] if doc else "No document"
184
+ hover_texts.append(f"ID: {id_val}<br>Doc: {doc_preview}")
185
+
186
+ # Create plot
187
+ if self.reduced_data.shape[1] == 2:
188
+ # 2D plot
189
+ fig = go.Figure(data=[
190
+ go.Scatter(
191
+ x=self.reduced_data[:, 0],
192
+ y=self.reduced_data[:, 1],
193
+ mode='markers',
194
+ marker=dict(
195
+ size=8,
196
+ color=list(range(len(ids))),
197
+ colorscale='Viridis',
198
+ showscale=True
199
+ ),
200
+ text=hover_texts,
201
+ hoverinfo='text'
202
+ )
203
+ ])
204
+
205
+ fig.update_layout(
206
+ title=f"Vector Visualization - {self.method_combo.currentText()}",
207
+ xaxis_title="Component 1",
208
+ yaxis_title="Component 2",
209
+ hovermode='closest',
210
+ height=800,
211
+ width=1200
212
+ )
213
+ else:
214
+ # 3D plot
215
+ fig = go.Figure(data=[
216
+ go.Scatter3d(
217
+ x=self.reduced_data[:, 0],
218
+ y=self.reduced_data[:, 1],
219
+ z=self.reduced_data[:, 2],
220
+ mode='markers',
221
+ marker=dict(
222
+ size=5,
223
+ color=list(range(len(ids))),
224
+ colorscale='Viridis',
225
+ showscale=True
226
+ ),
227
+ text=hover_texts,
228
+ hoverinfo='text'
229
+ )
230
+ ])
231
+
232
+ fig.update_layout(
233
+ title=f"Vector Visualization - {self.method_combo.currentText()}",
234
+ scene=dict(
235
+ xaxis_title="Component 1",
236
+ yaxis_title="Component 2",
237
+ zaxis_title="Component 3"
238
+ ),
239
+ height=800,
240
+ width=1200
241
+ )
242
+
243
+ # Display in embedded web view
244
+ html = fig.to_html(include_plotlyjs='cdn')
245
+ self.web_view.setHtml(html)