Skip to content

Commit 8dedec2

Browse files
committed
MDS settings and living without a searcher
1 parent 2f5b8ee commit 8dedec2

7 files changed

Lines changed: 225 additions & 26 deletions

File tree

activity_browser/app/pages/settings/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
from .startup import StartupSettingsChapter
55
from .appearance import AppearanceSettingsChapter
66
from .project_manager import ProjectManagerSettingsChapter
7+
from .metadatastore import MetadataStoreSettingsChapter
78

89
__all__ = [
910
"SettingsPage",
1011
"BaseSettingsChapter",
1112
"StartupSettingsChapter",
1213
"AppearanceSettingsChapter",
1314
"ProjectManagerSettingsChapter",
15+
"MetadataStoreSettingsChapter",
1416
]
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# -*- coding: utf-8 -*-
2+
from loguru import logger
3+
from qtpy import QtWidgets
4+
5+
from activity_browser.app import settings
6+
from activity_browser.app.pages.settings.base import BaseSettingsChapter
7+
8+
9+
class MetadataStoreSettingsChapter(BaseSettingsChapter):
10+
"""Chapter for metadatastore-related settings."""
11+
12+
def __init__(self, parent=None):
13+
super().__init__(parent)
14+
15+
# Caching enabled checkbox
16+
self.caching_checkbox = QtWidgets.QCheckBox("Enable caching")
17+
self.caching_checkbox.setToolTip(
18+
"Enable caching for faster data access. "
19+
"Disable if you experience memory issues or want to force fresh data loading."
20+
)
21+
22+
# Searcher enabled checkbox
23+
self.searcher_checkbox = QtWidgets.QCheckBox("Enable searcher")
24+
self.searcher_checkbox.setToolTip(
25+
"Enable the full-text search functionality for activities and metadata. "
26+
"Disable if you experience performance issues with large databases."
27+
)
28+
29+
self.build_layout()
30+
self.connect_signals()
31+
self.reset()
32+
33+
def connect_signals(self):
34+
"""Connect signals and slots."""
35+
# Emit changed signal when settings change
36+
self.caching_checkbox.stateChanged.connect(lambda: self.changed.emit())
37+
self.searcher_checkbox.stateChanged.connect(lambda: self.changed.emit())
38+
39+
def build_layout(self):
40+
"""Build the chapter layout."""
41+
layout = QtWidgets.QVBoxLayout()
42+
43+
# Metadata store group
44+
metadatastore_group = QtWidgets.QGroupBox("Metadata Store Options")
45+
metadatastore_layout = QtWidgets.QVBoxLayout()
46+
47+
metadatastore_layout.addWidget(self.caching_checkbox)
48+
metadatastore_layout.addWidget(self.searcher_checkbox)
49+
50+
# Add description label
51+
description = QtWidgets.QLabel(
52+
"These settings control the behavior of the metadata store, "
53+
"which manages activity and exchange metadata for improved performance."
54+
)
55+
description.setWordWrap(True)
56+
description.setStyleSheet("color: gray; font-size: 10pt;")
57+
metadatastore_layout.addWidget(description)
58+
59+
metadatastore_group.setLayout(metadatastore_layout)
60+
61+
layout.addWidget(metadatastore_group)
62+
layout.addStretch()
63+
64+
self.setLayout(layout)
65+
66+
# --- Settings management methods --- #
67+
def reset(self):
68+
"""(Re)set to initial values."""
69+
try:
70+
self.caching_checkbox.setChecked(
71+
settings["metadatastore"]["caching_enabled"]
72+
)
73+
self.searcher_checkbox.setChecked(
74+
settings["metadatastore"]["searcher_enabled"]
75+
)
76+
except (KeyError, TypeError):
77+
# Use defaults if settings don't exist yet
78+
self.caching_checkbox.setChecked(True)
79+
self.searcher_checkbox.setChecked(True)
80+
81+
def has_changes(self):
82+
"""Check if there are unsaved changes."""
83+
try:
84+
current_state = {
85+
'caching_enabled': self.caching_checkbox.isChecked(),
86+
'searcher_enabled': self.searcher_checkbox.isChecked(),
87+
}
88+
initial_state = {
89+
'caching_enabled': settings["metadatastore"]["caching_enabled"],
90+
'searcher_enabled': settings["metadatastore"]["searcher_enabled"],
91+
}
92+
return current_state != initial_state
93+
except (KeyError, TypeError):
94+
# If settings don't exist, check against defaults
95+
return (self.caching_checkbox.isChecked() != True or
96+
self.searcher_checkbox.isChecked() != True)
97+
98+
def set_settings(self):
99+
"""Save metadatastore settings."""
100+
if "metadatastore" not in settings.global_config:
101+
settings.global_config["metadatastore"] = {}
102+
103+
settings.global_config["metadatastore"]["caching_enabled"] = self.caching_checkbox.isChecked()
104+
settings.global_config["metadatastore"]["searcher_enabled"] = self.searcher_checkbox.isChecked()
105+
106+
logger.info(
107+
f"Metadatastore settings saved: "
108+
f"caching={self.caching_checkbox.isChecked()}, "
109+
f"searcher={self.searcher_checkbox.isChecked()}"
110+
)
111+

activity_browser/app/pages/settings/settings_page.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from .startup import StartupSettingsChapter
1212
from .appearance import AppearanceSettingsChapter
1313
from .project_manager import ProjectManagerSettingsChapter
14+
from .metadatastore import MetadataStoreSettingsChapter
1415

1516

1617
class SettingsPage(QtWidgets.QWidget):
@@ -37,12 +38,14 @@ def __init__(self, parent=None):
3738
self.startup_chapter = StartupSettingsChapter(self)
3839
self.appearance_chapter = AppearanceSettingsChapter(self)
3940
self.project_manager_chapter = ProjectManagerSettingsChapter(self)
40-
41+
self.metadatastore_chapter = MetadataStoreSettingsChapter(self)
42+
4143
# Add chapters to the stack
4244
self.chapters = [
4345
("Startup", self.startup_chapter),
4446
("Appearance", self.appearance_chapter),
4547
("Projects", self.project_manager_chapter),
48+
("Metadata Store", self.metadatastore_chapter),
4649
]
4750

4851
for name, widget in self.chapters:

activity_browser/bwutils/metadata/loader.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
from qtpy.QtCore import QObject, QThread, Signal, SignalInstance
1010

11+
from activity_browser.bwutils.settings import Settings
12+
1113
from .metadata import MetaDataStore
1214
from .fields import secondary_types, primary, secondary, search_engine_whitelist, all_fields
1315

@@ -42,7 +44,7 @@ def load_project(self):
4244
self.secondary_status = "loading"
4345

4446
# check for valid cache and load from it if available
45-
if self._has_cache():
47+
if self._has_cache() and Settings()["metadatastore"]["caching_enabled"]:
4648
self.cache_load_project()
4749
return
4850

@@ -266,6 +268,10 @@ def run(self):
266268
logger.debug("Skipping searcher initialization due to AB_NO_SEARCHER environment variable")
267269
return
268270

271+
if Settings()["metadatastore"]["searcher_enabled"] is False:
272+
logger.debug("Skipping searcher initialization due to settings")
273+
return
274+
269275
if self.mds.searcher is not None:
270276
old_searcher = self.mds.searcher
271277
self.mds.searcher = None

activity_browser/bwutils/metadata/metadata.py

Lines changed: 86 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import pandas as pd
77

8+
from activity_browser.bwutils.settings import Settings
89
from .fields import all_fields, all_types
910

1011

@@ -101,22 +102,22 @@ def flush_mutations(self) -> tuple[set[tuple[str, str]], set[tuple[str, str]], s
101102
self._updated.clear()
102103
self._deleted.clear()
103104

104-
cache_path = filesystem.get_project_ab_path() / "metadatastore_cache.pkl"
105-
self._dataframe.to_pickle(cache_path)
105+
if Settings()["metadatastore"]["caching_enabled"]:
106+
cache_path = filesystem.get_project_ab_path() / "metadatastore_cache.pkl"
107+
self._dataframe.to_pickle(cache_path)
106108

107109
return added, updated, deleted
108110

109111
def match(self, **kwargs: dict[str, str]) -> pd.DataFrame:
110112
"""Return a slice of the dataframe matching the criteria.
111113
"""
112-
with self._df_lock:
113-
df = self._dataframe.query(
114-
" and ".join(
115-
[
116-
f"`{key}`.astype('str') == {str(value)!r}" if not pd.isna(value) else f"`{key}`.isnull()"
117-
for key, value in kwargs.items()
118-
])
119-
)
114+
df = self._dataframe.query(
115+
" and ".join(
116+
[
117+
f"`{key}`.astype('str') == {str(value)!r}" if not pd.isna(value) else f"`{key}`.isnull()"
118+
for key, value in kwargs.items()
119+
])
120+
)
120121

121122
return df
122123

@@ -142,23 +143,85 @@ def get_database_metadata(self, db_name: str, columns: list = None) -> pd.DataFr
142143
df = self._dataframe.loc[[db_name], columns]
143144
return df.reindex(columns, axis="columns")
144145

146+
def _pandas_search(self, query: str, database: str = None, columns: list = None) -> pd.DataFrame:
147+
"""Fallback pandas-based search when searcher is not initialized.
148+
149+
Args:
150+
query: Search query string, may contain key:value parameters
151+
database: Optional database name to restrict search
152+
columns: Optional list of columns to return
153+
154+
Returns:
155+
DataFrame with matching results
156+
"""
157+
params, clean_query = get_query_parameters(query)
158+
columns = columns if columns is not None else all_fields
159+
160+
# Start with the full dataframe or database subset
161+
if database and database in self.databases:
162+
df = self._dataframe.loc[[database]]
163+
else:
164+
df = self._dataframe
165+
166+
if not clean_query.strip():
167+
# If no search query, just filter by parameters
168+
if params:
169+
extra_query = " & ".join(
170+
[
171+
f"`{key}`.astype('str').str.contains('{value}', case=False)"
172+
for key, value in params.items()
173+
if key in df.columns
174+
]
175+
)
176+
if extra_query:
177+
df = df.query(extra_query)
178+
return df[columns]
179+
180+
# Search across text fields: name, product, synonyms, categories, unit, location
181+
search_fields = ['name', 'product', 'synonyms', 'categories', 'unit', 'location', 'CAS number']
182+
mask = pd.Series([False] * len(df), index=df.index)
183+
184+
for field in search_fields:
185+
if field in df.columns:
186+
# Case-insensitive search
187+
mask |= df[field].astype(str).str.contains(clean_query, case=False, na=False)
188+
189+
df = df[mask]
190+
191+
# Apply additional parameter filters if any
192+
if params:
193+
extra_query = " & ".join(
194+
[
195+
f"`{key}`.astype('str').str.contains('{value}', case=False)"
196+
for key, value in params.items()
197+
if key in df.columns
198+
]
199+
)
200+
if extra_query:
201+
df = df.query(extra_query)
202+
203+
return df[columns] if columns else df
204+
145205
def search(self, query: str, columns: list = None) -> pd.DataFrame:
146-
if not self.searcher:
147-
logger.warning(f"Attempted to search metadata before searcher was initialized.")
148-
return pd.DataFrame(columns=columns or all_fields)
206+
if self.searcher:
207+
# Advanced searcher is initialized, so use that
208+
params, query = get_query_parameters(query)
209+
result = self.searcher.search(query)
210+
return self._meta_from_result(params, result, columns)
149211

150-
params, query = get_query_parameters(query)
151-
result = self.searcher.search(query)
152-
return self._meta_from_result(params, result, columns)
212+
# Fallback to simple pandas search
213+
logger.debug("Using simple pandas search as searcher is not initialized.")
214+
return self._pandas_search(query, columns=columns)
153215

154216
def search_database(self, query: str, database: str, columns: list = None) -> pd.DataFrame:
155-
if not self.searcher:
156-
logger.warning(f"Attempted to search metadata before searcher was initialized.")
157-
return pd.DataFrame(columns=columns or all_fields)
158-
159-
params, query = get_query_parameters(query)
160-
result = self.searcher.fuzzy_search(query, database=database)
161-
return self._meta_from_result(params, result, columns)
217+
if self.searcher:
218+
params, query = get_query_parameters(query)
219+
result = self.searcher.fuzzy_search(query, database=database)
220+
return self._meta_from_result(params, result, columns)
221+
222+
# Fallback to simple pandas search
223+
logger.debug(f"Using simple pandas search for database '{database}' as searcher is not initialized.")
224+
return self._pandas_search(query, database=database, columns=columns)
162225

163226
def _meta_from_result(self, params: dict, result: list[int], columns: list = None) -> pd.DataFrame:
164227
df = self._dataframe.loc[self.dataframe["id"].isin(result), columns or all_fields]

activity_browser/bwutils/settings.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
"appearance": {
1818
"theme": "default",
1919
"pane_tab_position": "bottom",
20+
},
21+
"metadatastore": {
22+
"caching_enabled": True,
23+
"searcher_enabled": True,
2024
}
2125
}
2226

@@ -52,7 +56,11 @@ def __getitem__(self, key):
5256
return self.virtual_config[key]
5357
if key in self.project_config:
5458
return self.project_config[key]
55-
return self.global_config[key]
59+
if key in self.global_config:
60+
return self.global_config[key]
61+
if key in defaults:
62+
return defaults[key]
63+
raise KeyError(f"Setting '{key}' not found in any configuration level.")
5664

5765
def __setitem__(self, key, value):
5866
if isinstance(key, tuple):

activity_browser/ui/widgets/text_edit.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,9 @@ def __init__(self, parent=None):
174174
self.database_name = ""
175175

176176
def _sanitize_input(self):
177+
if not self.mds.searcher:
178+
return
179+
177180
self._debounce_timer.stop()
178181
text = self.toPlainText()
179182
clean_text = self.mds.searcher.ONE_SPACE_PATTERN.sub(" ", text)
@@ -198,6 +201,9 @@ def _sanitize_input(self):
198201
self._set_debounce()
199202

200203
def _set_autocomplete_items(self):
204+
if not self.mds.searcher:
205+
return
206+
201207
text = self.toPlainText()
202208
if text.startswith("="):
203209
self.model.setStringList([])

0 commit comments

Comments
 (0)