diff --git a/CHANGELOG.md b/CHANGELOG.md index 0405273..7a497dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] ### Added +- **GUI: a "Sort by relevance" checkbox in Advanced Search Options** — the + desktop equivalent of `--rank`. Orders Standard Search matches by relevance; + needs the index (a tooltip says so). Persists with your other saved settings. + Ranking is now available in all four surfaces: CLI, Python API, MCP, and GUI. - **MCP `search_documents` gains a `rank` option — relevance-order the matches the assistant sees.** When results are capped, ranking makes the returned window the *most relevant* matches rather than just the first ones. Opt-in diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 204e45b..0ead4f1 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -382,7 +382,7 @@ The Search Wizard has its own **Change Folder** button and operates independentl **Advanced Search Options:** -Click the **▶ Advanced Search Options** header to expand the inline panel in the left pane (the chevron flips to **▼**). The panel carries every search-tuning option: AND mode, recursive search, fuzzy matching, wildcards, OCR, regex, whole-word matching, expression mode, inverse search, **Use Index**, exclude terms, file type filtering, proximity, context lines, CPU cores, max matches, range filters, specific files, save as, append to, output directory, **Redirect cloud-synced output paths to `~/peekdocs_reports`** (the sticky cloud-output guard — silently redirects report writes when the output folder is inside OneDrive / Google Drive / iCloud Drive / Dropbox; see [Cloud-synced folders](#cloud-synced-folders)), additional output formats (CSV, JSON, PDF, HTML), timestamp filenames, **Delete on Close**, **Clear history on close**, Restrict permissions, and Notify on Search Complete. Every terminal flag is available in the GUI. You don't need any of them for a basic search. Hover over any option to see a description of what it does. At the bottom of the panel are two button rows: top row holds **Save Defaults** (saves your current search terms, folder, and all options as defaults to `~/.peekdocsrc` — the next time you open the GUI, everything will be pre-filled), **Restore Saved Defaults** (reloads saved defaults from `~/.peekdocsrc` into the GUI), and **Inspect .peekdocsrc** (shows the current saved settings, read-only). Bottom row holds the destructive pair, both red: **Reset All Fields** (clears fields and restores the GUI to its default state — current session only; saved defaults in `~/.peekdocsrc` are unchanged unless you also click Save Defaults after resetting), and **Restore Factory Settings** (deletes `~/.peekdocsrc` entirely and returns everything to first-launch defaults — destructive, this cannot be undone). To collapse the panel, click the **▼ Advanced Search Options** header at the top of the panel (the chevron flips back to **▶**). Your settings persist for the next search either way. +Click the **▶ Advanced Search Options** header to expand the inline panel in the left pane (the chevron flips to **▼**). The panel carries every search-tuning option: AND mode, recursive search, fuzzy matching, wildcards, OCR, regex, whole-word matching, expression mode, inverse search, **Use Index**, **Sort by relevance** (orders matches most-relevant-first — needs Use Index), exclude terms, file type filtering, proximity, context lines, CPU cores, max matches, range filters, specific files, save as, append to, output directory, **Redirect cloud-synced output paths to `~/peekdocs_reports`** (the sticky cloud-output guard — silently redirects report writes when the output folder is inside OneDrive / Google Drive / iCloud Drive / Dropbox; see [Cloud-synced folders](#cloud-synced-folders)), additional output formats (CSV, JSON, PDF, HTML), timestamp filenames, **Delete on Close**, **Clear history on close**, Restrict permissions, and Notify on Search Complete. Every terminal flag is available in the GUI. You don't need any of them for a basic search. Hover over any option to see a description of what it does. At the bottom of the panel are two button rows: top row holds **Save Defaults** (saves your current search terms, folder, and all options as defaults to `~/.peekdocsrc` — the next time you open the GUI, everything will be pre-filled), **Restore Saved Defaults** (reloads saved defaults from `~/.peekdocsrc` into the GUI), and **Inspect .peekdocsrc** (shows the current saved settings, read-only). Bottom row holds the destructive pair, both red: **Reset All Fields** (clears fields and restores the GUI to its default state — current session only; saved defaults in `~/.peekdocsrc` are unchanged unless you also click Save Defaults after resetting), and **Restore Factory Settings** (deletes `~/.peekdocsrc` entirely and returns everything to first-launch defaults — destructive, this cannot be undone). To collapse the panel, click the **▼ Advanced Search Options** header at the top of the panel (the chevron flips back to **▶**). Your settings persist for the next search either way. > **Which checkboxes auto-save vs require Save Defaults.** Most Advanced Search Options checkboxes only persist across sessions if you click **Save Defaults** at the bottom of the panel. Three checkboxes are treated as session defaults and auto-save on every toggle: **Recursive**, **Whole Word**, and **Use Index**. The rest (**AND/OR mode**, **Fuzzy**, **Wildcard**, **Regex**, **OCR**, **Expression**, **Inverse**) are per-search toggles — they apply to the next search but reset to OFF the next time you launch peekdocs unless you click **Save Defaults** after flipping them. **OCR** specifically is non-persisting by design: Tesseract can take many seconds per scanned PDF, so silently leaving OCR on across sessions would surprise users with much slower runs they didn't ask for. If you regularly search scanned documents, click **Save Defaults** with OCR checked once, and it'll be on at every launch. diff --git a/peekdocs/gui/_cli_runner.py b/peekdocs/gui/_cli_runner.py index 259e0b3..c7d6c92 100644 --- a/peekdocs/gui/_cli_runner.py +++ b/peekdocs/gui/_cli_runner.py @@ -232,6 +232,7 @@ def _build_command_from_values( inverse=False, expression=False, whole_word=False, + rank=False, max_matches="", max_file_size_mb="", timestamp_suffix="", @@ -281,6 +282,8 @@ def _build_command_from_values( cmd.append("-x") if whole_word: cmd.append("-W") + if rank: + cmd.append("--rank") if inverse: cmd.append("--inverse") diff --git a/peekdocs/gui/_mixin_build.py b/peekdocs/gui/_mixin_build.py index 831cd3d..2c063a6 100644 --- a/peekdocs/gui/_mixin_build.py +++ b/peekdocs/gui/_mixin_build.py @@ -334,6 +334,7 @@ def _build_search_row(self): self.and_mode_var = ctk.StringVar(value="off") self.whole_word_var = ctk.StringVar(value="on") self.index_search_var = ctk.StringVar(value="off") + self.rank_var = ctk.StringVar(value="off") import tkinter as _tk_step2 from peekdocs.i18n import t as _t @@ -746,6 +747,15 @@ def _build_advanced_panel(self): self._adv_cb_whole_word.grid(row=2, column=2, padx=(0, 15), pady=(5, 0), sticky="w") Tooltip(self._adv_cb_whole_word, "Matches complete words only. 'bob' matches 'bob' but not 'bobcat'.") + self._adv_cb_rank = ctk.CTkCheckBox( + cb_frame, text="Sort by relevance", variable=self.rank_var, + onvalue="on", offvalue="off", + command=lambda: self._save_ui_preference("rank", self.rank_var.get() == "on"), + ) + self._adv_cb_rank.grid(row=3, column=0, padx=(0, 15), pady=(5, 0), sticky="w") + Tooltip(self._adv_cb_rank, "Order matches by relevance (most relevant first) instead of file order. " + "Needs the search index (turn on Use Index); otherwise results stay in file order.") + # Use Index used to sit at row 1 / col 4 of this grid. It now # lives below the Output Dir entry — see the cb_index_search # creation after that row. diff --git a/peekdocs/gui/_mixin_data.py b/peekdocs/gui/_mixin_data.py index 8eef0b4..4b2e400 100644 --- a/peekdocs/gui/_mixin_data.py +++ b/peekdocs/gui/_mixin_data.py @@ -64,6 +64,7 @@ def _save_current_settings(self): settings["inverse"] = (self.inverse_var.get() == "on") settings["expression"] = (self.expression_var.get() == "on") settings["whole_word"] = (self.whole_word_var.get() == "on") + settings["rank"] = (self.rank_var.get() == "on") settings["timestamp"] = (self.timestamp_var.get() == "on") # Integer settings cores_val = self.cores_entry.get().strip() @@ -194,6 +195,7 @@ def _load_saved_settings(self): self.inverse_var.set("on" if config.get("inverse") else "off") self.expression_var.set("on" if config.get("expression") else "off") self.whole_word_var.set("on" if config.get("whole_word", _whole_word_default) else "off") + self.rank_var.set("on" if config.get("rank", False) else "off") self.timestamp_var.set("on" if config.get("timestamp", False) else "off") self.delete_reports_var.set("on" if config.get("delete_reports_on_close", False) else "off") self.clear_history_var.set("on" if config.get("clear_history_on_close", False) else "off") @@ -1054,6 +1056,7 @@ def _collect_gui_params(self): "inverse": self.inverse_var.get() == "on", "expression": self.expression_var.get() == "on", "whole_word": self.whole_word_var.get() == "on", + "rank": self.rank_var.get() == "on", "output_docx": self.output_docx_var.get() == "on", "output_csv": self.output_csv_var.get() == "on", "output_json": self.output_json_var.get() == "on", @@ -1101,6 +1104,7 @@ def _apply_params_to_gui(self, params): self.inverse_var.set("on" if params.get("inverse") else "off") self.expression_var.set("on" if params.get("expression") else "off") self.whole_word_var.set("on" if params.get("whole_word") else "off") + self.rank_var.set("on" if params.get("rank") else "off") if params.get("expression"): self.search_entry.configure(placeholder_text='e.g. (budget OR revenue) AND NOT draft') else: diff --git a/peekdocs/gui/_mixin_search.py b/peekdocs/gui/_mixin_search.py index 3b0668d..f98b68b 100644 --- a/peekdocs/gui/_mixin_search.py +++ b/peekdocs/gui/_mixin_search.py @@ -157,6 +157,7 @@ def start_search(self): inverse=self.inverse_var.get() == "on", expression=self.expression_var.get() == "on", whole_word=self.whole_word_var.get() == "on", + rank=self.rank_var.get() == "on", max_matches=self.max_matches_entry.get(), max_file_size_mb=self.max_file_size_entry.get(), timestamp_suffix=self._last_ts_suffix, @@ -394,6 +395,7 @@ def _multi_folder_thread(self, folders, search_text): inverse=self.inverse_var.get() == "on", expression=self.expression_var.get() == "on", whole_word=self.whole_word_var.get() == "on", + rank=self.rank_var.get() == "on", max_matches=self.max_matches_entry.get(), max_file_size_mb=self.max_file_size_entry.get(), range_filters=self.range_entry.get(), diff --git a/peekdocs/gui/_mixin_suites.py b/peekdocs/gui/_mixin_suites.py index 8976636..eadfe42 100644 --- a/peekdocs/gui/_mixin_suites.py +++ b/peekdocs/gui/_mixin_suites.py @@ -787,6 +787,7 @@ def _run(): inverse=params.get("inverse", False), expression=params.get("expression", False), whole_word=params.get("whole_word", False), + rank=params.get("rank", False), max_matches=str(params.get("max_matches", "")) if params.get("max_matches") else "", max_file_size_mb=str(params.get("max_file_size_mb", "")) if params.get("max_file_size_mb") else "", range_filters=params.get("range_filters", ""), diff --git a/tests/test_gui.py b/tests/test_gui.py index 41e90a6..e0a75da 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -21,6 +21,25 @@ def test_build_command_basic(tmp_path): assert "-r" not in cmd +def test_build_command_rank(tmp_path): + on = _build_command_from_values( + search_text="budget", folder=str(tmp_path), + and_mode=False, recursive=False, fuzzy=False, + wildcard=False, ocr=False, regex=False, + exclude="", file_types="", proximity="", + context_before="", context_after="", index_search=True, rank=True, + ) + assert "--rank" in on + off = _build_command_from_values( + search_text="budget", folder=str(tmp_path), + and_mode=False, recursive=False, fuzzy=False, + wildcard=False, ocr=False, regex=False, + exclude="", file_types="", proximity="", + context_before="", context_after="", index_search=True, rank=False, + ) + assert "--rank" not in off + + def test_build_command_all_flags(tmp_path): cmd = _build_command_from_values( search_text="hello world",