Skip to content

Commit 6e96a0f

Browse files
authored
Multi mode search system (#232)
* multi search mode system A way to change the search from requiring all tags to and of the tags * better wording * Update start_win.bat Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> * Fix home_ui.py using PySide6 instead of PyQt5 * Refresh search on mode change * Search mode selections naming fix Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> * converted SearchMode from constants to enums
1 parent c75aff4 commit 6e96a0f

File tree

6 files changed

+273
-172
lines changed

6 files changed

+273
-172
lines changed

tagstudio/src/core/enums.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,10 @@ class Theme(str, enum.Enum):
1717
COLOR_PRESSED = "#65EEEEEE"
1818
COLOR_DISABLED = "#65F39CAA"
1919
COLOR_DISABLED_BG = "#65440D12"
20+
21+
22+
class SearchMode(int, enum.Enum):
23+
"""Operational modes for item searching."""
24+
25+
AND = 0
26+
OR = 1

tagstudio/src/core/library.py

Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from src.core.json_typing import JsonCollation, JsonEntry, JsonLibary, JsonTag
2121
from src.core.utils.str import strip_punctuation
2222
from src.core.utils.web import strip_web_protocol
23+
from src.core.enums import SearchMode
2324
from src.core.constants import (
2425
BACKUP_FOLDER_NAME,
2526
COLLAGE_FOLDER_NAME,
@@ -1290,7 +1291,12 @@ def get_entry_id_from_filepath(self, filename: Path):
12901291
return -1
12911292

12921293
def search_library(
1293-
self, query: str = None, entries=True, collations=True, tag_groups=True
1294+
self,
1295+
query: str = None,
1296+
entries=True,
1297+
collations=True,
1298+
tag_groups=True,
1299+
search_mode=SearchMode.AND,
12941300
) -> list[tuple[ItemType, int]]:
12951301
"""
12961302
Uses a search query to generate a filtered results list.
@@ -1300,7 +1306,7 @@ def search_library(
13001306
# self.filtered_entries.clear()
13011307
results: list[tuple[ItemType, int]] = []
13021308
collations_added = []
1303-
1309+
# print(f"Searching Library with query: {query} search_mode: {search_mode}")
13041310
if query:
13051311
# start_time = time.time()
13061312
query = query.strip().lower()
@@ -1320,6 +1326,7 @@ def search_library(
13201326

13211327
# Preprocess the Tag terms.
13221328
if query_words:
1329+
# print(query_words, self._tag_strings_to_id_map)
13231330
for i, term in enumerate(query_words):
13241331
for j, term in enumerate(query_words):
13251332
if (
@@ -1328,6 +1335,8 @@ def search_library(
13281335
in self._tag_strings_to_id_map
13291336
):
13301337
all_tag_terms.append(" ".join(query_words[i : j + 1]))
1338+
# print(all_tag_terms)
1339+
13311340
# This gets rid of any accidental term inclusions because they were words
13321341
# in another term. Ex. "3d" getting added in "3d art"
13331342
for i, term in enumerate(all_tag_terms):
@@ -1403,36 +1412,8 @@ def search_library(
14031412
# elif query in entry.filename.lower():
14041413
# self.filtered_entries.append(index)
14051414
elif entry_tags:
1406-
# For each verified, extracted Tag term.
1407-
failure_to_union_terms = False
1408-
for term in all_tag_terms:
1409-
# If the term from the previous loop was already verified:
1410-
if not failure_to_union_terms:
1411-
cluster: set = set()
1412-
# Add the immediate associated Tags to the set (ex. Name, Alias hits)
1413-
# Since this term could technically map to multiple IDs, iterate over it
1414-
# (You're 99.9999999% likely to just get 1 item)
1415-
for id in self._tag_strings_to_id_map[term]:
1416-
cluster.add(id)
1417-
cluster = cluster.union(
1418-
set(self.get_tag_cluster(id))
1419-
)
1420-
# print(f'Full Cluster: {cluster}')
1421-
# For each of the Tag IDs in the term's ID cluster:
1422-
for t in cluster:
1423-
# Assume that this ID from the cluster is not in the Entry.
1424-
# Wait to see if proven wrong.
1425-
failure_to_union_terms = True
1426-
# If the ID actually is in the Entry,
1427-
if t in entry_tags:
1428-
# There wasn't a failure to find one of the term's cluster IDs in the Entry.
1429-
# There is also no more need to keep checking the rest of the terms in the cluster.
1430-
failure_to_union_terms = False
1431-
# print(f'FOUND MATCH: {t}')
1432-
break
1433-
# print(f'\tFailure to Match: {t}')
1434-
# If there even were tag terms to search through AND they all match an entry
1435-
if all_tag_terms and not failure_to_union_terms:
1415+
# function to add entry to results
1416+
def add_entry(entry: Entry):
14361417
# self.filter_entries.append()
14371418
# self.filtered_file_list.append(file)
14381419
# results.append((SearchItemType.ENTRY, entry.id))
@@ -1457,6 +1438,54 @@ def search_library(
14571438
if not added:
14581439
results.append((ItemType.ENTRY, entry.id))
14591440

1441+
if search_mode == SearchMode.AND: # Include all terms
1442+
# For each verified, extracted Tag term.
1443+
failure_to_union_terms = False
1444+
for term in all_tag_terms:
1445+
# If the term from the previous loop was already verified:
1446+
if not failure_to_union_terms:
1447+
cluster: set = set()
1448+
# Add the immediate associated Tags to the set (ex. Name, Alias hits)
1449+
# Since this term could technically map to multiple IDs, iterate over it
1450+
# (You're 99.9999999% likely to just get 1 item)
1451+
for id in self._tag_strings_to_id_map[term]:
1452+
cluster.add(id)
1453+
cluster = cluster.union(
1454+
set(self.get_tag_cluster(id))
1455+
)
1456+
# print(f'Full Cluster: {cluster}')
1457+
# For each of the Tag IDs in the term's ID cluster:
1458+
for t in cluster:
1459+
# Assume that this ID from the cluster is not in the Entry.
1460+
# Wait to see if proven wrong.
1461+
failure_to_union_terms = True
1462+
# If the ID actually is in the Entry,
1463+
if t in entry_tags:
1464+
# There wasn't a failure to find one of the term's cluster IDs in the Entry.
1465+
# There is also no more need to keep checking the rest of the terms in the cluster.
1466+
failure_to_union_terms = False
1467+
# print(f"FOUND MATCH: {t}")
1468+
break
1469+
# print(f'\tFailure to Match: {t}')
1470+
# # failure_to_union_terms is used to determine if all terms in the query were found in the entry.
1471+
# # If there even were tag terms to search through AND they all match an entry
1472+
if all_tag_terms and not failure_to_union_terms:
1473+
add_entry(entry)
1474+
1475+
if search_mode == SearchMode.OR: # Include any terms
1476+
# For each verified, extracted Tag term.
1477+
for term in all_tag_terms:
1478+
# Add the immediate associated Tags to the set (ex. Name, Alias hits)
1479+
# Since this term could technically map to multiple IDs, iterate over it
1480+
# (You're 99.9999999% likely to just get 1 item)
1481+
for id in self._tag_strings_to_id_map[term]:
1482+
# If the ID actually is in the Entry,
1483+
if id in entry_tags:
1484+
# check if result already contains the entry
1485+
if (ItemType.ENTRY, entry.id) not in results:
1486+
add_entry(entry)
1487+
break
1488+
14601489
# sys.stdout.write(
14611490
# f'\r[INFO][FILTER]: {len(self.filtered_file_list)} matches found')
14621491
# sys.stdout.flush()

tagstudio/src/qt/main_window.py

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
from PySide6.QtWidgets import (QComboBox, QFrame, QGridLayout,
1919
QHBoxLayout, QVBoxLayout, QLayout, QLineEdit, QMainWindow,
2020
QPushButton, QScrollArea, QSizePolicy,
21-
QStatusBar, QWidget, QSplitter)
21+
QStatusBar, QWidget, QSplitter, QCheckBox,
22+
QSpacerItem)
2223
from src.qt.pagination import Pagination
2324

2425

@@ -52,6 +53,36 @@ def setupUi(self, MainWindow):
5253
self.gridLayout.setObjectName(u"gridLayout")
5354
self.horizontalLayout = QHBoxLayout()
5455
self.horizontalLayout.setObjectName(u"horizontalLayout")
56+
57+
# ComboBox goup for search type and thumbnail size
58+
self.horizontalLayout_3 = QHBoxLayout()
59+
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
60+
61+
# left side spacer
62+
spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
63+
self.horizontalLayout_3.addItem(spacerItem)
64+
65+
# Search type selector
66+
self.comboBox_2 = QComboBox(self.centralwidget)
67+
self.comboBox_2.setMinimumSize(QSize(165, 0))
68+
self.comboBox_2.setObjectName("comboBox_2")
69+
self.comboBox_2.addItem("")
70+
self.comboBox_2.addItem("")
71+
self.horizontalLayout_3.addWidget(self.comboBox_2)
72+
73+
# Thumbnail Size placeholder
74+
self.comboBox = QComboBox(self.centralwidget)
75+
self.comboBox.setObjectName(u"comboBox")
76+
sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
77+
sizePolicy.setHorizontalStretch(0)
78+
sizePolicy.setVerticalStretch(0)
79+
sizePolicy.setHeightForWidth(
80+
self.comboBox.sizePolicy().hasHeightForWidth())
81+
self.comboBox.setSizePolicy(sizePolicy)
82+
self.comboBox.setMinimumWidth(128)
83+
self.comboBox.setMaximumWidth(128)
84+
self.horizontalLayout_3.addWidget(self.comboBox)
85+
self.gridLayout.addLayout(self.horizontalLayout_3, 5, 0, 1, 1)
5586

5687
self.splitter = QSplitter()
5788
self.splitter.setObjectName(u"splitter")
@@ -138,18 +169,18 @@ def setupUi(self, MainWindow):
138169
self.horizontalLayout_2.addWidget(self.searchButton)
139170
self.gridLayout.addLayout(self.horizontalLayout_2, 3, 0, 1, 1)
140171

141-
self.comboBox = QComboBox(self.centralwidget)
142-
self.comboBox.setObjectName(u"comboBox")
143-
sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
144-
sizePolicy.setHorizontalStretch(0)
145-
sizePolicy.setVerticalStretch(0)
146-
sizePolicy.setHeightForWidth(
147-
self.comboBox.sizePolicy().hasHeightForWidth())
148-
self.comboBox.setSizePolicy(sizePolicy)
149-
self.comboBox.setMinimumWidth(128)
150-
self.comboBox.setMaximumWidth(128)
172+
# self.comboBox = QComboBox(self.centralwidget)
173+
# self.comboBox.setObjectName(u"comboBox")
174+
# sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
175+
# sizePolicy.setHorizontalStretch(0)
176+
# sizePolicy.setVerticalStretch(0)
177+
# sizePolicy.setHeightForWidth(
178+
# self.comboBox.sizePolicy().hasHeightForWidth())
179+
# self.comboBox.setSizePolicy(sizePolicy)
180+
# self.comboBox.setMinimumWidth(128)
181+
# self.comboBox.setMaximumWidth(128)
151182

152-
self.gridLayout.addWidget(self.comboBox, 4, 0, 1, 1, Qt.AlignRight)
183+
# self.gridLayout.addWidget(self.comboBox, 4, 0, 1, 1, Qt.AlignRight)
153184

154185
self.gridLayout_2.setContentsMargins(6, 6, 6, 6)
155186

@@ -181,15 +212,24 @@ def setupUi(self, MainWindow):
181212
def retranslateUi(self, MainWindow):
182213
MainWindow.setWindowTitle(QCoreApplication.translate(
183214
"MainWindow", u"MainWindow", None))
215+
# Navigation buttons
184216
self.backButton.setText(
185217
QCoreApplication.translate("MainWindow", u"<", None))
186218
self.forwardButton.setText(
187219
QCoreApplication.translate("MainWindow", u">", None))
220+
221+
# Search field
188222
self.searchField.setPlaceholderText(
189223
QCoreApplication.translate("MainWindow", u"Search Entries", None))
190224
self.searchButton.setText(
191225
QCoreApplication.translate("MainWindow", u"Search", None))
226+
227+
# Search type selector
228+
self.comboBox_2.setItemText(0, QCoreApplication.translate("MainWindow", "And (Includes All Tags)"))
229+
self.comboBox_2.setItemText(1, QCoreApplication.translate("MainWindow", "Or (Includes Any Tag)"))
192230
self.comboBox.setCurrentText("")
231+
232+
# Tumbnail size selector
193233
self.comboBox.setPlaceholderText(
194234
QCoreApplication.translate("MainWindow", u"Thumbnail Size", None))
195235
# retranslateUi

tagstudio/src/qt/ts_qt.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,11 @@
4343
QSplashScreen,
4444
QMenu,
4545
QMenuBar,
46+
QComboBox,
4647
)
4748
from humanfriendly import format_timespan
4849

49-
from src.core.enums import SettingItems
50+
from src.core.enums import SettingItems, SearchMode
5051
from src.core.library import ItemType
5152
from src.core.ts_core import TagStudioCore
5253
from src.core.constants import (
@@ -176,6 +177,8 @@ def __init__(self, core: TagStudioCore, args):
176177
self.nav_frames: list[NavigationState] = []
177178
self.cur_frame_idx: int = -1
178179

180+
self.search_mode = SearchMode.AND
181+
179182
# self.main_window = None
180183
# self.main_window = Ui_MainWindow()
181184

@@ -564,6 +567,12 @@ def init_library_window(self):
564567
search_field.returnPressed.connect(
565568
lambda: self.filter_items(self.main_window.searchField.text())
566569
)
570+
search_type_selector: QComboBox = self.main_window.comboBox_2
571+
search_type_selector.currentIndexChanged.connect(
572+
lambda: self.set_search_type(
573+
SearchMode(search_type_selector.currentIndex())
574+
)
575+
)
567576

568577
back_button: QPushButton = self.main_window.backButton
569578
back_button.clicked.connect(self.nav_back)
@@ -1334,7 +1343,7 @@ def filter_items(self, query: str = ""):
13341343

13351344
# self.filtered_items = self.lib.search_library(query)
13361345
# 73601 Entries at 500 size should be 246
1337-
all_items = self.lib.search_library(query)
1346+
all_items = self.lib.search_library(query, search_mode=self.search_mode)
13381347
frames: list[list[tuple[ItemType, int]]] = []
13391348
frame_count = math.ceil(len(all_items) / self.max_results)
13401349
for i in range(0, frame_count):
@@ -1375,6 +1384,10 @@ def filter_items(self, query: str = ""):
13751384

13761385
# self.update_thumbs()
13771386

1387+
def set_search_type(self, mode=SearchMode.AND):
1388+
self.search_mode = mode
1389+
self.filter_items(self.main_window.searchField.text())
1390+
13781391
def remove_recent_library(self, item_key: str):
13791392
self.settings.beginGroup(SettingItems.LIBS_LIST)
13801393
self.settings.remove(item_key)

0 commit comments

Comments
 (0)