From 978a6ade7639b3716c7591638a8f5b4a649b1d6c Mon Sep 17 00:00:00 2001 From: "Robert D. Schoening" Date: Mon, 20 Jul 2026 21:11:34 -0400 Subject: [PATCH] feat(gui): "Sort by relevance" checkbox (--rank) in Advanced Search Options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slice 3 (final surface) of ranking — the desktop equivalent of --rank. - _mixin_build: new rank_var + "Sort by relevance" checkbox in Advanced Search Options, with a tooltip noting it needs the index (Use Index). - _cli_runner._build_command_from_values: rank param → appends --rank. Both GUI dispatch paths (subprocess pipx + in-process frozen) use this shared builder, so both get it. - _mixin_search: pass rank from the checkbox on both search call sites. - _mixin_data: persist rank in settings + saved-search params (save/load/restore). - _mixin_suites: honor a saved search's rank when run in a suite. - tests: test_build_command_rank (flag emitted when on, absent when off). - docs: CHANGELOG + USER_GUIDE Advanced Options list. Ranking is now on all four surfaces: CLI, Python API, MCP, GUI. No version bump. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Robert D. Schoening --- CHANGELOG.md | 4 ++++ docs/USER_GUIDE.md | 2 +- peekdocs/gui/_cli_runner.py | 3 +++ peekdocs/gui/_mixin_build.py | 10 ++++++++++ peekdocs/gui/_mixin_data.py | 4 ++++ peekdocs/gui/_mixin_search.py | 2 ++ peekdocs/gui/_mixin_suites.py | 1 + tests/test_gui.py | 19 +++++++++++++++++++ 8 files changed, 44 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04052736..7a497dc0 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 204e45b7..0ead4f19 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 259e0b3e..c7d6c92b 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 831cd3d9..2c063a60 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 8eef0b48..4b2e4000 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 3b0668d0..f98b68b0 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 8976636b..eadfe422 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 41e90a6a..e0a75da5 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",