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.
- vector_inspector/__init__.py +3 -0
- vector_inspector/__main__.py +4 -0
- vector_inspector/core/__init__.py +1 -0
- vector_inspector/core/connections/__init__.py +7 -0
- vector_inspector/core/connections/base_connection.py +233 -0
- vector_inspector/core/connections/chroma_connection.py +384 -0
- vector_inspector/core/connections/qdrant_connection.py +723 -0
- vector_inspector/core/connections/template_connection.py +346 -0
- vector_inspector/main.py +21 -0
- vector_inspector/services/__init__.py +1 -0
- vector_inspector/services/backup_restore_service.py +286 -0
- vector_inspector/services/filter_service.py +72 -0
- vector_inspector/services/import_export_service.py +287 -0
- vector_inspector/services/settings_service.py +60 -0
- vector_inspector/services/visualization_service.py +116 -0
- vector_inspector/ui/__init__.py +1 -0
- vector_inspector/ui/components/__init__.py +1 -0
- vector_inspector/ui/components/backup_restore_dialog.py +350 -0
- vector_inspector/ui/components/filter_builder.py +370 -0
- vector_inspector/ui/components/item_dialog.py +118 -0
- vector_inspector/ui/components/loading_dialog.py +30 -0
- vector_inspector/ui/main_window.py +288 -0
- vector_inspector/ui/views/__init__.py +1 -0
- vector_inspector/ui/views/collection_browser.py +112 -0
- vector_inspector/ui/views/connection_view.py +423 -0
- vector_inspector/ui/views/metadata_view.py +555 -0
- vector_inspector/ui/views/search_view.py +268 -0
- vector_inspector/ui/views/visualization_view.py +245 -0
- vector_inspector-0.2.0.dist-info/METADATA +382 -0
- vector_inspector-0.2.0.dist-info/RECORD +32 -0
- vector_inspector-0.2.0.dist-info/WHEEL +4 -0
- 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)
|