This is an automated email from the ASF dual-hosted git repository.

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new 3a9a29ff1e issue #7756: harden Hop GUI search against large result 
floods (#7757)
3a9a29ff1e is described below

commit 3a9a29ff1eb9ff1c2679a40d499e1cf2b29bba25
Author: Matt Casters <[email protected]>
AuthorDate: Mon Aug 3 08:58:33 2026 +0200

    issue #7756: harden Hop GUI search against large result floods (#7757)
    
    Keep Search Everywhere responsive when projects contain large CSV/JSON
    and other text files: skip oversized files, cap global and per-file hits,
    delay content search until a minimum query length, and run analysis off
    the UI thread. Expose all limits in Search settings and Configuration,
    and update the user manual for the current Search UX.
---
 .../images/hop-gui/search-options-dialog.png       | Bin 0 -> 136733 bytes
 docs/hop-user-manual/modules/ROOT/nav.adoc         |   1 +
 .../ROOT/pages/getting-started/hop-gui.adoc        |   2 -
 .../modules/ROOT/pages/hop-gui/index.adoc          |   3 +-
 .../pages/hop-gui/perspective-configuration.adoc   |   2 +-
 .../ROOT/pages/hop-gui/perspective-search.adoc     | 100 +++++++-
 .../modules/ROOT/pages/hop-gui/perspectives.adoc   |   8 +-
 .../file/types/text/BaseTextExplorerFileType.java  |   8 +
 .../hop/ui/hopgui/search/HopGuiSearchHelper.java   |  89 ++++++-
 .../ui/hopgui/search/HopGuiSearchResultsPanel.java | 155 +++++++++---
 .../ui/hopgui/search/SearchEverywhereDialog.java   | 180 +++++++++++---
 .../hop/ui/hopgui/search/SearchSettingsDialog.java | 237 ++++++++++++++++++
 .../search/TextFileContentSearchableAnalyser.java  |  31 ++-
 .../hopgui/search/config/SearchAnalysisResult.java |  48 ++++
 .../hop/ui/hopgui/search/config/SearchConfig.java  |  90 +++++++
 .../hopgui/search/config/SearchConfigPlugin.java   | 270 +++++++++++++++++++++
 .../search/config/SearchConfigSingleton.java       |  86 +++++++
 .../hop/ui/hopgui/search/config/SearchLimits.java  |  95 ++++++++
 .../config/messages/messages_en_US.properties      |  32 +++
 .../search/messages/messages_en_US.properties      |  28 +++
 .../ui/hopgui/search/HopGuiSearchHelperTest.java   |  92 +++++++
 .../TextFileContentSearchableAnalyserTest.java     |  18 ++
 .../ui/hopgui/search/config/SearchLimitsTest.java  |  71 ++++++
 23 files changed, 1561 insertions(+), 85 deletions(-)

diff --git 
a/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/search-options-dialog.png
 
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/search-options-dialog.png
new file mode 100644
index 0000000000..8c2dde001f
Binary files /dev/null and 
b/docs/hop-user-manual/modules/ROOT/assets/images/hop-gui/search-options-dialog.png
 differ
diff --git a/docs/hop-user-manual/modules/ROOT/nav.adoc 
b/docs/hop-user-manual/modules/ROOT/nav.adoc
index 2b5f548131..487d4b5ae7 100644
--- a/docs/hop-user-manual/modules/ROOT/nav.adoc
+++ b/docs/hop-user-manual/modules/ROOT/nav.adoc
@@ -31,6 +31,7 @@ under the License.
 ** xref:hop-gui/hop-gui-popup-dialog.adoc[Popup Dialog]
 ** xref:hop-gui/notes.adoc[Notes on pipelines and workflows]
 ** xref:hop-gui/perspectives.adoc[Perspectives]
+** xref:hop-gui/perspective-search.adoc[Search]
 ** xref:hop-gui/hop-gui-git.adoc[Working with git]
 ** xref:hop-gui/shortcuts.adoc[Keyboard Shortcuts]
 ** xref:hop-gui/hop-web.adoc[Hop Web]
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/getting-started/hop-gui.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/getting-started/hop-gui.adoc
index ff04dc77cf..19b882db70 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/getting-started/hop-gui.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/getting-started/hop-gui.adoc
@@ -71,8 +71,6 @@ image:getting-started/gs-hop-gui-overview-2.png[Hop Gui 
Overview]
 
 ** image:icons/folder.svg[width="24px"] 
xref:hop-gui/perspective-file-explorer.adoc[File explorer]
 
-** image:icons/search.svg[width="24px"] 
xref:hop-gui/perspective-search.adoc[Search]
-
 ** image:icons/plugin.svg[width="24px"] 
xref:hop-gui/perspective-plugin.adoc[Plugins explorer]
 
 ** image:icons/neo4j_logo.svg[width="24px"] 
xref:hop-gui/perspective-neo4j.adoc[Neo4j]
diff --git a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/index.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/index.adoc
index 14e444b002..bd93500113 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/index.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/index.adoc
@@ -23,7 +23,7 @@ under the License.
 
 Hop Gui is Hop's visual development environment. You'll spend a lot of time 
here in the various perspectives of Hop GUI.
 
-You'll spend most of your time building, previewing running and debugging 
workflows and pipelines, but there's also a lot of configuration that can be 
done from Hop Gui's metadata perspective, you can search for any metadata with 
the search perspective, manage a variety of files and even version control them 
from the file explorer perspective and much more.
+You'll spend most of your time building, previewing running and debugging 
workflows and pipelines, but there's also a lot of configuration that can be 
done from Hop Gui's metadata perspective, you can search across projects with 
xref:hop-gui/perspective-search.adoc[Search] (`CTRL-F`), manage a variety of 
files and even version control them from the file explorer perspective and much 
more.
 
 This page covers a number of topics that are very specific to working with Hop 
Gui.
 Check the chapters on xref:/pipeline/pipelines.adoc[pipelines] and 
xref:/workflow/workflows.adoc[workflows] to learn more about creating and 
running pipelines and workflows in Hop Gui.
@@ -34,6 +34,7 @@ Covered here are:
 * xref:hop-gui/hop-gui-popup-dialog.adoc[Popup Dialog]: the versatile popup 
dialog to work with metadata, workflows, pipelines and hops
 * xref:hop-gui/notes.adoc[Notes on pipelines and workflows]: canvas notes, 
including Markdown formatting, types, links, and images
 * xref:hop-gui/perspectives.adoc[Perspectives]: the various perspectives in 
the Hop Gui
+* xref:hop-gui/perspective-search.adoc[Search]: Search Everywhere (`CTRL-F`), 
limits, and results panel
 * xref:hop-gui/hop-gui-git.adoc[Working with git]: Manage files in the git 
version control system
 * xref:hop-gui/shortcuts.adoc[Keyboard Shortcuts]: a list of the keyboard 
shortcuts that are available in Hop Gui.
 * xref:hop-gui/hop-web.adoc[Hop Web]: a web version of Hop Gui that provides 
the exact same functionality and user experience as the desktop version
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-configuration.adoc
 
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-configuration.adoc
index eac252128e..263d5a1e4b 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-configuration.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-configuration.adoc
@@ -32,7 +32,7 @@ The configuration perspective groups configuration options in 
the following tabs
 
 * **General** options control the behavior of Apache Hop and Hop Gui.
 * **Look &amp; Feel** control how Hop Gui looks like on your desktop or in 
your browser.
-* **Plugin** options provide configuration options to control the behavior of 
your available Apache Hop plugins.
+* **Plugin** options provide configuration options to control the behavior of 
your available Apache Hop plugins (including Explorer and 
xref:hop-gui/perspective-search.adoc[Search] limits).
 * **System variables** allows you to set and configure Apache Hop global 
variables.
 
 == Detailed configuration tabs overview
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-search.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-search.adoc
index a62068a663..9f04ed8fc5 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-search.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-search.adoc
@@ -15,18 +15,104 @@ specific language governing permissions and limitations
 under the License.
 ////
 :imagesdir: ../assets/images
+:description: Search pipelines, workflows, metadata, variables, text files and 
menu actions from Hop Gui with Search Everywhere, optional safety limits, and a 
full results panel.
 
-= Search Perspective
+= Search
 
 Icon: image:icons/search.svg[width="24px"]
 
+Keyboard shortcut: `CTRL-F` (macOS: `Cmd-F`)
+
+Menu: *Edit → Find*
+
 == Description
-The Search perspective allows you to search through all metadata in your Hop 
projects.
-This not only includes workflows and actions, pipelines and transforms, but 
any other metadata type known to Hop.
 
-The search options are the location (all objects in Hop Gui or only the 
current project), and whether the search string is case sensitive (default: 
case insensitive) or the search string is a regular expression (default: false).
+Hop Gui search finds matches across your open files, project files (pipelines, 
workflows, and other searchable types), metadata, variables, and global menu 
actions.
+
+Search is available as a quick *Search Everywhere* popup and as a fuller 
results view in the bottom dock (*Show all results*).
+
+TIP: For large or batch searches from a terminal (including remote SSH 
sessions), use the xref:hop-tools/hop-search.adoc[hop-search] command-line 
tool. It is better suited to huge hit lists than the GUI.
+
+== Search Everywhere popup
+
+Open search with `CTRL-F` or *Edit → Find*. Type in the search field to filter 
results (search-as-you-type can be turned off in settings).
+
+* *Aa* — case-sensitive matching
+* `.*` — treat the query as a regular expression
+* image:icons/gear.svg[width="16px"] — open *Search settings* (limits and 
options; see below)
+
+Results are grouped by *Open files* and *Project files*, then by type 
(Pipeline, Workflow, metadata type, and so on). Use the arrow keys to move, 
*Enter* to open the selection, and *Escape* to close the popup.
+
+*Show all results* hands the same query to a search tab in the bottom dock for 
deeper browsing (details panel, open selected item).
+
+=== What is searched
+
+|===
+|Scope |Notes
+
+|Open files
+|Pipelines, workflows and text files open in Hop Gui tabs
+
+|Project files
+|Registered searchable file types under the project home (pipelines, 
workflows, SQL, XML, YAML, CSV, JSON, and similar), subject to size and options 
below
+
+|Metadata and variables
+|Connections, run configurations, variables, and other metadata known to Hop
+
+|Actions
+|Enabled main-menu commands whose labels match the query
+|===
+
+Object *names* are always considered. *Content* matching (including line-level 
hits in text files) only runs when the query is long enough (default: 3 
characters), so short prefixes while typing do not flood the UI with data-file 
matches.
+
+== Search settings
+
+Open the gear on the Search Everywhere bar (or on the results panel toolbar), 
or configure the same options under the 
xref:hop-gui/perspective-configuration.adoc[Configuration] perspective → 
*Plugin* options → *Search*.
+
+image::hop-gui/search-options-dialog.png[Search settings dialog,width="80%"]
+
+[%header, width=90%, cols="2,5,1"]
+|===
+|Option |Description |Default
+
+|Min. characters for content
+|Minimum query length before field and text-file *content* is searched. Names 
still match for shorter queries.
+|`3`
+
+|Maximum results
+|Hard cap on how many results Hop Gui builds for one search. Further matches 
are not collected so the UI stays responsive.
+|`500`
+
+|Max. matches per text file
+|Stop scanning a single text file after this many matching lines.
+|`20`
+
+|Max. text file size (MB)
+|Skip text files larger than this size for content search. Million-row 
CSV/JSON files should be searched with dedicated tools or 
xref:hop-tools/hop-search.adoc[hop-search], not loaded into Hop Gui.
+|`1`
+
+|Search-as-you-type delay (ms)
+|How long to wait after the last keystroke before running a live search.
+|`300`
+
+|Include project text files (CSV, JSON, …)
+|When enabled, project text files are content-searched (within size and match 
limits). Open text files remain eligible either way. Disable to focus on 
pipelines, workflows and metadata.
+|yes
+
+|Search as you type
+|When enabled, search runs automatically after the delay above. *Enter* always 
runs a search immediately.
+|yes
+|===
+
+Settings are stored in the Hop configuration (`hop-config.json`) under the 
`search` key and apply to both the popup and the results panel.
+
+== Full results panel
+
+From *Show all results*, or when Hop Gui opens a search tab for references, 
the bottom dock shows a toolbar (query, case/regex, refresh, settings), a 
grouped results tree, and a details side panel. Select a hit and use *Open* to 
jump to the matching object.
+
+Refresh reloads searchable objects from disk and open tabs after you change 
project files outside Hop Gui.
 
-The result table shows the type, name, filename, location, matching text and 
description.
-Use the 'Open' button at the bottom of the perspective to jump directly to the 
selected metadata object (e.g. open a transform in a pipeline).
+== Related
 
-TIP: The xref:hop-tools/hop-search.adoc[hop-search] tool provides the same 
functionality as the Search Perspective through the command line instead of in 
a GUI.
+* xref:hop-tools/hop-search.adoc[hop-search] — CLI search for projects and 
environments
+* xref:hop-gui/perspective-configuration.adoc[Configuration perspective] — 
other Hop Gui options
diff --git a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspectives.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspectives.adoc
index 6a05fb7e61..8669243dd9 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspectives.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspectives.adoc
@@ -17,7 +17,7 @@ under the License.
 
 = Hop Gui Perspectives
 :imagesdir: ../assets/images
-:description: Hop Gui provides a number of perspectives for a variety of 
tasks. You'll spend most of your time in the data orchestration perspective, 
building workflows and pipelines, but other perspectives for metadata 
management, file management, search etc offer tons of functionality.
+:description: Hop Gui provides a number of perspectives for a variety of 
tasks. You'll spend most of your time in the data orchestration perspective, 
building workflows and pipelines, but other perspectives for metadata 
management, file management and more offer tons of functionality.
 
 Hop comes with a number of perspectives built in.
 
@@ -36,8 +36,8 @@ Here are the available perspectives:
 
 * image:icons/folder.svg[width="24px"] 
xref:hop-gui/perspective-file-explorer.adoc[File explorer]
 
-* image:icons/search.svg[width="24px"] 
xref:hop-gui/perspective-search.adoc[Search]
-
 * image:icons/plugin.svg[width="24px"] 
xref:hop-gui/perspective-plugin.adoc[Plugins explorer]
 
-* image:icons/neo4j_logo.svg[width="24px"] 
xref:hop-gui/perspective-neo4j.adoc[Neo4j]
\ No newline at end of file
+* image:icons/neo4j_logo.svg[width="24px"] 
xref:hop-gui/perspective-neo4j.adoc[Neo4j]
+
+TIP: Project-wide search is not a left-toolbar perspective. Open it with 
`CTRL-F` (*Edit → Find*). See xref:hop-gui/perspective-search.adoc[Search].
\ No newline at end of file
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/text/BaseTextExplorerFileType.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/text/BaseTextExplorerFileType.java
index 02faf65e5a..04de85b905 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/text/BaseTextExplorerFileType.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/text/BaseTextExplorerFileType.java
@@ -36,6 +36,7 @@ import 
org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective;
 import 
org.apache.hop.ui.hopgui.perspective.explorer.file.types.base.BaseExplorerFileType;
 import org.apache.hop.ui.hopgui.search.HopGuiTextFileSearchable;
 import org.apache.hop.ui.hopgui.search.TextFileContent;
+import org.apache.hop.ui.hopgui.search.config.SearchLimits;
 
 public abstract class BaseTextExplorerFileType<T extends 
BaseTextExplorerFileTypeHandler>
     extends BaseExplorerFileType<T> {
@@ -73,6 +74,13 @@ public abstract class BaseTextExplorerFileType<T extends 
BaseTextExplorerFileTyp
       if (!fileObject.exists()) {
         throw new HopException("File '" + filename + "' doesn't exist");
       }
+      // Never load very large data files into GUI search (million-row 
CSV/JSON, …).
+      // Users should use dedicated tools for those. Limit is configurable 
under Search options.
+      long fileSize = fileObject.getContent().getSize();
+      long maxBytes = SearchLimits.fromConfig().getMaxTextFileSizeBytes();
+      if (maxBytes > 0 && fileSize > maxBytes) {
+        return null;
+      }
       String text;
       try (InputStream inputStream = HopVfs.getInputStream(fileObject)) {
         StringWriter writer = new StringWriter();
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/search/HopGuiSearchHelper.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/HopGuiSearchHelper.java
index d2d46baf52..b553a57d8f 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/search/HopGuiSearchHelper.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/search/HopGuiSearchHelper.java
@@ -47,6 +47,8 @@ import org.apache.hop.metadata.api.IHopMetadataProvider;
 import org.apache.hop.pipeline.PipelineMeta;
 import org.apache.hop.ui.hopgui.file.pipeline.HopPipelineFileType;
 import org.apache.hop.ui.hopgui.file.workflow.HopWorkflowFileType;
+import org.apache.hop.ui.hopgui.search.config.SearchAnalysisResult;
+import org.apache.hop.ui.hopgui.search.config.SearchLimits;
 import org.apache.hop.workflow.WorkflowMeta;
 
 /**
@@ -134,17 +136,60 @@ public final class HopGuiSearchHelper {
       Collection<ISearchable> searchables,
       ISearchQuery query,
       Map<Class<ISearchableAnalyser>, ISearchableAnalyser> analyserMap) {
+    return analyseLimited(searchables, query, analyserMap, null, 
null).getResults();
+  }
+
+  /**
+   * Like {@link #analyse} with GUI safety limits: global result cap, optional 
skip of project text
+   * files, and skip of text-file content when the query is shorter than the 
configured minimum.
+   *
+   * @param sourceByKey location indexes from {@link #enumerateAll}; used to 
keep open text files
+   *     when project text files are excluded; may be null
+   */
+  public static SearchAnalysisResult analyseLimited(
+      Collection<ISearchable> searchables,
+      ISearchQuery query,
+      Map<Class<ISearchableAnalyser>, ISearchableAnalyser> analyserMap,
+      SearchLimits limits,
+      Map<String, Integer> sourceByKey) {
+    SearchLimits effective = limits == null ? SearchLimits.defaults() : limits;
+    boolean contentAllowed = 
effective.allowsContentSearch(query.getSearchString());
+    boolean projectTextExcluded = !effective.isIncludeProjectTextFiles();
+    boolean truncated = false;
+
     List<ISearchResult> results = new ArrayList<>();
     for (ISearchable searchable : searchables) {
+      if (results.size() >= effective.getMaxResults()) {
+        truncated = true;
+        break;
+      }
       Object object = searchable.getSearchableObject();
-      if (object != null) {
-        ISearchableAnalyser searchableAnalyser = 
analyserMap.get(object.getClass());
-        if (searchableAnalyser != null) {
-          results.addAll(searchableAnalyser.search(searchable, query));
+      if (object == null) {
+        continue;
+      }
+      if (object instanceof TextFileContent) {
+        if (!contentAllowed) {
+          continue;
+        }
+        if (projectTextExcluded && !isOpenObject(searchable, sourceByKey)) {
+          continue;
         }
       }
+      ISearchableAnalyser searchableAnalyser = 
analyserMap.get(object.getClass());
+      if (searchableAnalyser == null) {
+        continue;
+      }
+      List<ISearchResult> found = searchableAnalyser.search(searchable, query);
+      for (ISearchResult result : found) {
+        if (results.size() >= effective.getMaxResults()) {
+          truncated = true;
+          break;
+        }
+        results.add(result);
+      }
     }
-    return results;
+    return new SearchAnalysisResult(
+        results, truncated, !contentAllowed, projectTextExcluded && 
contentAllowed);
   }
 
   /**
@@ -164,7 +209,26 @@ public final class HopGuiSearchHelper {
       ISearchQuery query,
       Map<Class<ISearchableAnalyser>, ISearchableAnalyser> analyserMap,
       boolean fuzzyNames) {
-    List<ISearchResult> results = new ArrayList<>(analyse(searchables, query, 
analyserMap));
+    return analyseRankedLimited(searchables, query, analyserMap, fuzzyNames, 
null, null)
+        .getResults();
+  }
+
+  /**
+   * Ranked analysis with GUI safety limits. Name matching always runs; 
text-file content is gated
+   * by {@link SearchLimits}.
+   */
+  public static SearchAnalysisResult analyseRankedLimited(
+      Collection<ISearchable> searchables,
+      ISearchQuery query,
+      Map<Class<ISearchableAnalyser>, ISearchableAnalyser> analyserMap,
+      boolean fuzzyNames,
+      SearchLimits limits,
+      Map<String, Integer> sourceByKey) {
+    SearchLimits effective = limits == null ? SearchLimits.defaults() : limits;
+    SearchAnalysisResult analysed =
+        analyseLimited(searchables, query, analyserMap, effective, 
sourceByKey);
+    List<ISearchResult> results = new ArrayList<>(analysed.getResults());
+    boolean truncated = analysed.isTruncated();
 
     SearchMatcher nameMatcher =
         new SearchMatcher(query.getSearchString(), query.isCaseSensitive(), 
query.isRegEx(), true);
@@ -175,6 +239,10 @@ public final class HopGuiSearchHelper {
         covered.add(searchableKey(result.getMatchingSearchable()));
       }
       for (ISearchable searchable : searchables) {
+        if (results.size() >= effective.getMaxResults()) {
+          truncated = true;
+          break;
+        }
         String name = searchable.getName();
         if (name != null
             && covered.add(searchableKey(searchable))
@@ -190,7 +258,11 @@ public final class HopGuiSearchHelper {
       relevance.put(result, relevance(result, nameMatcher));
     }
     results.sort((a, b) -> Double.compare(relevance.get(b), relevance.get(a)));
-    return results;
+    return new SearchAnalysisResult(
+        results,
+        truncated,
+        analysed.isContentSearchSkipped(),
+        analysed.isProjectTextFilesExcluded());
   }
 
   /**
@@ -281,6 +353,9 @@ public final class HopGuiSearchHelper {
     if (object instanceof PipelineMeta
         || object instanceof WorkflowMeta
         || object instanceof TextFileContent) {
+      if (sourceByKey == null) {
+        return false;
+      }
       Integer source = sourceByKey.get(searchableKey(searchable));
       return source != null && source == 0;
     }
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/search/HopGuiSearchResultsPanel.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/HopGuiSearchResultsPanel.java
index eb2bf4ddb4..f183081e3a 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/search/HopGuiSearchResultsPanel.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/HopGuiSearchResultsPanel.java
@@ -17,10 +17,12 @@
 
 package org.apache.hop.ui.hopgui.search;
 
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.search.ISearchResult;
 import org.apache.hop.core.search.ISearchable;
@@ -38,9 +40,12 @@ import org.apache.hop.ui.core.dialog.ErrorDialog;
 import org.apache.hop.ui.core.gui.GuiResource;
 import org.apache.hop.ui.core.gui.HopNamespace;
 import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.search.config.SearchAnalysisResult;
+import org.apache.hop.ui.hopgui.search.config.SearchLimits;
 import org.apache.hop.ui.hopgui.shared.AuditManagerGuiUtil;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.graphics.Image;
 import org.eclipse.swt.graphics.Point;
 import org.eclipse.swt.layout.FormAttachment;
 import org.eclipse.swt.layout.FormData;
@@ -50,6 +55,7 @@ import org.eclipse.swt.layout.GridLayout;
 import org.eclipse.swt.widgets.Button;
 import org.eclipse.swt.widgets.Combo;
 import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
 import org.eclipse.swt.widgets.Event;
 import org.eclipse.swt.widgets.Label;
 import org.eclipse.swt.widgets.Tree;
@@ -77,9 +83,6 @@ public class HopGuiSearchResultsPanel extends Composite {
   private static final String STATE_SASH_WEIGHTS_PROP = "weights";
   private static final int[] DEFAULT_SASH_WEIGHTS = {70, 30};
 
-  /** Debounce in ms between a keystroke and running the search. */
-  private static final int SEARCH_DELAY_MS = 300;
-
   private final HopGui hopGui;
   private final String eventListenerId;
   private List<ISearchablesLocation> searchablesLocations;
@@ -87,6 +90,8 @@ public class HopGuiSearchResultsPanel extends Composite {
   private Combo wSearchString;
   private Button wCaseSensitive;
   private Button wRegEx;
+  private Button wSettings;
+  private Label wlStatus;
   private SashForm sash;
   private Tree wTree;
   private Button wbOpen;
@@ -107,6 +112,14 @@ public class HopGuiSearchResultsPanel extends Composite {
   private Map<Class<ISearchableAnalyser>, ISearchableAnalyser> cachedAnalysers;
 
   private final Runnable searchRunnable = () -> search(new Event());
+  private final AtomicInteger searchGeneration = new AtomicInteger();
+  private final ExecutorService searchExecutor =
+      Executors.newSingleThreadExecutor(
+          r -> {
+            Thread t = new Thread(r, "hop-gui-search-panel");
+            t.setDaemon(true);
+            return t;
+          });
 
   /**
    * When true, programmatic changes to the search field (e.g. showing a 
reference label) must not
@@ -133,6 +146,8 @@ public class HopGuiSearchResultsPanel extends Composite {
     addDisposeListener(
         e -> {
           // The Dispose event fires before the child sash is released, so 
weights are still valid.
+          searchGeneration.incrementAndGet();
+          searchExecutor.shutdownNow();
           saveState();
           hopGui.getEventsHandler().removeEventListeners(eventListenerId);
         });
@@ -150,11 +165,11 @@ public class HopGuiSearchResultsPanel extends Composite {
     layout.marginBottom = margin;
     setLayout(layout);
 
-    // --- Top toolbar: search field, case/regex toggles, refresh ---
+    // --- Top toolbar: search field, case/regex toggles, refresh, settings ---
     //
     Composite toolbar = new Composite(this, SWT.NONE);
     PropsUi.setLook(toolbar);
-    toolbar.setLayout(new GridLayout(5, false));
+    toolbar.setLayout(new GridLayout(7, false));
     FormData fdToolbar = new FormData();
     fdToolbar.left = new FormAttachment(0, 0);
     fdToolbar.right = new FormAttachment(100, 0);
@@ -169,7 +184,7 @@ public class HopGuiSearchResultsPanel extends Composite {
     PropsUi.setLook(wSearchString);
     wSearchString.setFont(GuiResource.getInstance().getFontBold());
     wSearchString.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, 
false));
-    wSearchString.addListener(SWT.Modify, e -> scheduleSearch());
+    wSearchString.addListener(SWT.Modify, e -> onSearchModified());
     wSearchString.addListener(
         SWT.DefaultSelection,
         e -> {
@@ -200,6 +215,29 @@ public class HopGuiSearchResultsPanel extends Composite {
           search(e);
         });
 
+    wSettings = new Button(toolbar, SWT.PUSH);
+    PropsUi.setLook(wSettings);
+    Image gear = GuiResource.getInstance().getImage("ui/images/gear.svg", 16, 
16);
+    if (gear != null) {
+      wSettings.setImage(gear);
+    } else {
+      wSettings.setText("…");
+    }
+    wSettings.setToolTipText(
+        BaseMessages.getString(PKG, 
"HopGuiSearchResultsPanel.Settings.Tooltip"));
+    wSettings.addListener(
+        SWT.Selection,
+        e -> {
+          if (new SearchSettingsDialog(getShell(), hopGui).open()) {
+            invalidateCache();
+            search(new Event());
+          }
+        });
+
+    wlStatus = new Label(toolbar, SWT.LEFT);
+    PropsUi.setLook(wlStatus);
+    wlStatus.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
+
     // --- Bottom: Open button ---
     //
     wbOpen = new Button(this, SWT.PUSH);
@@ -316,12 +354,22 @@ public class HopGuiSearchResultsPanel extends Composite {
     }
   }
 
+  private void onSearchModified() {
+    if (isDisposed() || suppressAutoSearch) {
+      return;
+    }
+    if (!SearchLimits.fromConfig().isSearchAsYouType()) {
+      return;
+    }
+    scheduleSearch();
+  }
+
   private void scheduleSearch() {
     if (isDisposed() || suppressAutoSearch) {
       return;
     }
     getDisplay().timerExec(-1, searchRunnable);
-    getDisplay().timerExec(SEARCH_DELAY_MS, searchRunnable);
+    getDisplay().timerExec(SearchLimits.fromConfig().getDebounceMs(), 
searchRunnable);
   }
 
   /** Size the last column (Matched text) to take whatever width is left after 
the Name column. */
@@ -402,7 +450,7 @@ public class HopGuiSearchResultsPanel extends Composite {
   }
 
   /** Make sure the searchables across all locations are enumerated once (and 
cached). */
-  private boolean ensureLoaded() {
+  private synchronized boolean ensureLoaded() {
     if (cachedEnumeration != null) {
       return true;
     }
@@ -410,6 +458,9 @@ public class HopGuiSearchResultsPanel extends Composite {
       if (cachedAnalysers == null) {
         cachedAnalysers = HopGuiSearchHelper.loadSearchableAnalysers();
       }
+      if (searchablesLocations == null) {
+        searchablesLocations = hopGui.getSearchablesLocations();
+      }
       cachedEnumeration =
           HopGuiSearchHelper.enumerateAll(
               searchablesLocations,
@@ -434,31 +485,79 @@ public class HopGuiSearchResultsPanel extends Composite {
 
     String searchString = wSearchString.getText();
     if (Utils.isEmpty(searchString)) {
+      searchGeneration.incrementAndGet();
+      setStatus("");
       return;
     }
 
-    if (searchablesLocations == null) {
-      searchablesLocations = hopGui.getSearchablesLocations();
-    }
-    if (!ensureLoaded()) {
-      return;
-    }
+    final boolean caseSensitive = wCaseSensitive.getSelection();
+    final boolean regExp = wRegEx.getSelection();
+    final SearchLimits limits = SearchLimits.fromConfig();
+    final int generation = searchGeneration.incrementAndGet();
+    final Display display = getDisplay();
+    setStatus(BaseMessages.getString(PKG, 
"HopGuiSearchResultsPanel.Status.Searching"));
+
+    searchExecutor.execute(
+        () -> {
+          try {
+            if (!ensureLoaded()) {
+              display.asyncExec(
+                  () -> {
+                    if (generation == searchGeneration.get() && !isDisposed()) 
{
+                      setStatus("");
+                    }
+                  });
+              return;
+            }
+            SearchQuery query = new SearchQuery(searchString, caseSensitive, 
regExp);
+            SearchAnalysisResult analysis =
+                HopGuiSearchHelper.analyseRankedLimited(
+                    cachedEnumeration.getSearchables(),
+                    query,
+                    cachedAnalysers,
+                    true,
+                    limits,
+                    cachedEnumeration.getSourceByKey());
+            display.asyncExec(
+                () -> {
+                  if (generation != searchGeneration.get() || isDisposed()) {
+                    return;
+                  }
+                  populateTree(analysis.getResults(), 
cachedEnumeration.getSourceByKey());
+                  setStatusFromAnalysis(analysis, limits);
+                });
+          } catch (Exception e) {
+            hopGui.getLog().logError("Error while searching", e);
+            display.asyncExec(
+                () -> {
+                  if (generation == searchGeneration.get() && !isDisposed()) {
+                    setStatus("");
+                  }
+                });
+          }
+        });
+  }
 
-    SearchQuery query =
-        new SearchQuery(searchString, wCaseSensitive.getSelection(), 
wRegEx.getSelection());
+  private void setStatus(String text) {
+    if (wlStatus != null && !wlStatus.isDisposed()) {
+      wlStatus.setText(Const.NVL(text, ""));
+    }
+  }
 
-    // The searchables were enumerated once; here we only run the cheap 
in-memory matching.
-    // Errors are logged (not shown in a modal dialog) since this runs on 
every debounced keystroke.
-    List<ISearchResult> results;
-    try {
-      results =
-          HopGuiSearchHelper.analyseRanked(
-              cachedEnumeration.getSearchables(), query, cachedAnalysers, 
true);
-    } catch (Exception e) {
-      hopGui.getLog().logError("Error while searching", e);
-      results = new ArrayList<>();
+  private void setStatusFromAnalysis(SearchAnalysisResult analysis, 
SearchLimits limits) {
+    if (analysis.isTruncated()) {
+      setStatus(BaseMessages.getString(PKG, 
"HopGuiSearchResultsPanel.Status.Capped"));
+    } else if (analysis.isContentSearchSkipped()) {
+      setStatus(
+          BaseMessages.getString(
+              PKG,
+              "HopGuiSearchResultsPanel.Status.MinLength",
+              Integer.toString(limits.getMinContentQueryLength())));
+    } else if (analysis.isProjectTextFilesExcluded()) {
+      setStatus(BaseMessages.getString(PKG, 
"HopGuiSearchResultsPanel.Status.TextFilesExcluded"));
+    } else {
+      setStatus("");
     }
-    populateTree(results, cachedEnumeration.getSourceByKey());
   }
 
   /**
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/search/SearchEverywhereDialog.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/SearchEverywhereDialog.java
index efce59d17f..a6c6ccae40 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/search/SearchEverywhereDialog.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/SearchEverywhereDialog.java
@@ -22,6 +22,9 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.search.ISearchResult;
 import org.apache.hop.core.search.ISearchable;
@@ -35,6 +38,8 @@ import org.apache.hop.ui.core.gui.GuiMenuWidgets;
 import org.apache.hop.ui.core.gui.GuiResource;
 import org.apache.hop.ui.core.gui.WindowProperty;
 import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.search.config.SearchAnalysisResult;
+import org.apache.hop.ui.hopgui.search.config.SearchLimits;
 import org.apache.hop.ui.hopgui.shared.AuditManagerGuiUtil;
 import org.apache.hop.ui.hopgui.terminal.HopGuiBottomDock;
 import org.eclipse.swt.SWT;
@@ -70,9 +75,6 @@ public class SearchEverywhereDialog {
 
   private static final Class<?> PKG = SearchEverywhereDialog.class; // i18n
 
-  /** Debounce in ms between a keystroke and running the (potentially 
expensive) search. */
-  private static final int SEARCH_DELAY_MS = 250;
-
   /**
    * Maximum number of results shown per group in the popup; the full set 
lives in the perspective.
    */
@@ -101,6 +103,7 @@ public class SearchEverywhereDialog {
   private Combo wSearch;
   private Button wCaseSensitive;
   private Button wRegEx;
+  private Button wSettings;
   private Tree wTree;
   private TreeColumn nameColumn;
   private TreeColumn detailColumn;
@@ -109,6 +112,16 @@ public class SearchEverywhereDialog {
   private Color detailColor;
   private final Runnable searchRunnable = this::runSearch;
   private int fullResultCount;
+  private SearchAnalysisResult lastAnalysis = SearchAnalysisResult.empty();
+
+  private final AtomicInteger searchGeneration = new AtomicInteger();
+  private final ExecutorService searchExecutor =
+      Executors.newSingleThreadExecutor(
+          r -> {
+            Thread t = new Thread(r, "hop-gui-search");
+            t.setDaemon(true);
+            return t;
+          });
 
   public SearchEverywhereDialog(Shell parent, HopGui hopGui) {
     this.parent = parent;
@@ -131,10 +144,10 @@ public class SearchEverywhereDialog {
 
     int margin = PropsUi.getMargin();
 
-    // --- Search bar: text field + case / regex toggles ---
+    // --- Search bar: text field + case / regex toggles + settings ---
     Composite searchBar = new Composite(shell, SWT.NONE);
     PropsUi.setLook(searchBar);
-    GridLayout gridLayout = new GridLayout(3, false);
+    GridLayout gridLayout = new GridLayout(4, false);
     gridLayout.marginWidth = 0;
     gridLayout.horizontalSpacing = margin;
     searchBar.setLayout(gridLayout);
@@ -158,11 +171,25 @@ public class SearchEverywhereDialog {
     wCaseSensitive.setToolTipText(
         BaseMessages.getString(PKG, 
"SearchEverywhereDialog.CaseSensitive.Tooltip"));
     PropsUi.setLook(wCaseSensitive);
+    wCaseSensitive.addListener(SWT.Selection, e -> scheduleSearch());
 
     wRegEx = new Button(searchBar, SWT.CHECK);
     wRegEx.setText(BaseMessages.getString(PKG, 
"SearchEverywhereDialog.RegEx.Label"));
     wRegEx.setToolTipText(BaseMessages.getString(PKG, 
"SearchEverywhereDialog.RegEx.Tooltip"));
     PropsUi.setLook(wRegEx);
+    wRegEx.addListener(SWT.Selection, e -> scheduleSearch());
+
+    wSettings = new Button(searchBar, SWT.PUSH);
+    PropsUi.setLook(wSettings);
+    Image gear = GuiResource.getInstance().getImage("ui/images/gear.svg", 16, 
16);
+    if (gear != null) {
+      wSettings.setImage(gear);
+    } else {
+      wSettings.setText("…");
+    }
+    wSettings.setToolTipText(
+        BaseMessages.getString(PKG, 
"SearchEverywhereDialog.Settings.Tooltip"));
+    wSettings.addListener(SWT.Selection, e -> openSettings());
 
     // --- Footer: hint + "show all" handoff to the search perspective ---
     wShowAll = new Link(shell, SWT.NONE);
@@ -201,7 +228,7 @@ public class SearchEverywhereDialog {
     wTree.addListener(SWT.MouseDoubleClick, e -> openSelected());
 
     // All keyboard handling lives on the search field so focus never leaves 
it.
-    wSearch.addListener(SWT.Modify, e -> scheduleSearch());
+    wSearch.addListener(SWT.Modify, e -> onSearchModified());
     wSearch.addListener(SWT.KeyDown, this::handleSearchKey);
 
     // Close on Escape or via the window close button (both persist the window 
size).
@@ -234,6 +261,29 @@ public class SearchEverywhereDialog {
     }
   }
 
+  private void onSearchModified() {
+    SearchLimits limits = SearchLimits.fromConfig();
+    if (!limits.isSearchAsYouType()) {
+      return;
+    }
+    scheduleSearch();
+  }
+
+  private void openSettings() {
+    boolean saved = new SearchSettingsDialog(shell, hopGui).open();
+    if (saved) {
+      // Size / include-text-file settings may change what is loadable.
+      invalidateCache();
+      scheduleSearch();
+    }
+  }
+
+  private void invalidateCache() {
+    cachedSearchables = null;
+    cachedAnalysers = null;
+    sourceByKey.clear();
+  }
+
   /** Restore the previously remembered size/position, or center a sensible 
default. */
   private void restoreSize() {
     shell.setMinimumSize(MIN_WIDTH, MIN_HEIGHT);
@@ -291,9 +341,13 @@ public class SearchEverywhereDialog {
   }
 
   private void scheduleSearch() {
+    if (shell == null || shell.isDisposed()) {
+      return;
+    }
     Display display = shell.getDisplay();
     display.timerExec(-1, searchRunnable);
-    display.timerExec(SEARCH_DELAY_MS, searchRunnable);
+    int delay = SearchLimits.fromConfig().getDebounceMs();
+    display.timerExec(delay, searchRunnable);
   }
 
   private void handleSearchKey(Event event) {
@@ -309,7 +363,10 @@ public class SearchEverywhereDialog {
       case SWT.CR:
       case SWT.KEYPAD_CR:
         event.doit = false;
-        openSelected();
+        // Enter always runs a search immediately (even when 
search-as-you-type is off).
+        Display display = shell.getDisplay();
+        display.timerExec(-1, searchRunnable);
+        runSearch();
         break;
       default:
         // let the keystroke reach the text field
@@ -366,7 +423,7 @@ public class SearchEverywhereDialog {
    * de-duplicating objects that several locations report (e.g. metadata and 
variables are listed by
    * both the GUI and the project location). Subsequent keystrokes only 
re-match against this cache.
    */
-  private void ensureLoaded() {
+  private synchronized void ensureLoaded() {
     if (cachedSearchables != null) {
       return;
     }
@@ -391,39 +448,74 @@ public class SearchEverywhereDialog {
     if (shell == null || shell.isDisposed()) {
       return;
     }
-    wTree.removeAll();
-    fullResultCount = 0;
 
     String searchString = wSearch.getText();
     if (Utils.isEmpty(searchString)) {
+      searchGeneration.incrementAndGet();
+      wTree.removeAll();
+      fullResultCount = 0;
+      lastAnalysis = SearchAnalysisResult.empty();
       updateShowAll();
       return;
     }
 
-    ensureLoaded();
+    final boolean caseSensitive = wCaseSensitive.getSelection();
+    final boolean regExp = wRegEx.getSelection();
+    final SearchLimits limits = SearchLimits.fromConfig();
+    final int generation = searchGeneration.incrementAndGet();
+    final Display display = shell.getDisplay();
 
-    boolean caseSensitive = wCaseSensitive.getSelection();
-    boolean regExp = wRegEx.getSelection();
-    SearchQuery query = new SearchQuery(searchString, caseSensitive, regExp);
+    wTree.removeAll();
+    fullResultCount = 0;
+    updateShowAllSearching();
+
+    searchExecutor.execute(
+        () -> {
+          try {
+            ensureLoaded();
+            SearchQuery query = new SearchQuery(searchString, caseSensitive, 
regExp);
+            SearchAnalysisResult analysis =
+                HopGuiSearchHelper.analyseRankedLimited(
+                    cachedSearchables, query, cachedAnalysers, true, limits, 
sourceByKey);
+            display.asyncExec(
+                () -> {
+                  if (generation != searchGeneration.get() || shell == null || 
shell.isDisposed()) {
+                    return;
+                  }
+                  paintResults(analysis, searchString, regExp, caseSensitive, 
limits);
+                });
+          } catch (Exception e) {
+            hopGui.getLog().logError("Error while searching", e);
+            display.asyncExec(
+                () -> {
+                  if (generation != searchGeneration.get() || shell == null || 
shell.isDisposed()) {
+                    return;
+                  }
+                  lastAnalysis = SearchAnalysisResult.empty();
+                  updateShowAll();
+                });
+          }
+        });
+  }
 
-    // The searchables were enumerated once when the popup opened; here we 
only run the cheap
-    // in-memory analyser matching against that cached set. Errors are logged 
(not shown in a modal
-    // dialog) since this runs on every debounced keystroke.
-    List<ISearchResult> results;
-    try {
-      results = HopGuiSearchHelper.analyseRanked(cachedSearchables, query, 
cachedAnalysers, true);
-    } catch (Exception e) {
-      hopGui.getLog().logError("Error while searching", e);
-      results = new ArrayList<>();
-    }
+  private void paintResults(
+      SearchAnalysisResult analysis,
+      String searchString,
+      boolean regExp,
+      boolean caseSensitive,
+      SearchLimits limits) {
+    lastAnalysis = analysis;
+    wTree.removeAll();
+    fullResultCount = 0;
 
-    // Render the two-tier tree: section (Open/Project) -> type -> object -> 
the matches inside it.
     String searchTerm = regExp ? null : searchString;
     for (HopGuiSearchHelper.SearchSection section :
-        HopGuiSearchHelper.groupResults(results, sourceByKey)) {
+        HopGuiSearchHelper.groupResults(analysis.getResults(), sourceByKey)) {
       addSection(section, searchTerm);
     }
-    addCommandGroup(searchString, caseSensitive);
+    if (!analysis.isTruncated()) {
+      addCommandGroup(searchString, caseSensitive, limits);
+    }
 
     selectFirstLeaf();
     updateShowAll();
@@ -485,7 +577,7 @@ public class SearchEverywhereDialog {
         : HopGuiSearchHelper.sanitizePath(detail, hopGui.getVariables());
   }
 
-  private void addCommandGroup(String searchString, boolean caseSensitive) {
+  private void addCommandGroup(String searchString, boolean caseSensitive, 
SearchLimits limits) {
     GuiMenuWidgets menuWidgets = hopGui.getMainMenuWidgets();
     if (menuWidgets == null) {
       return;
@@ -557,11 +649,37 @@ public class SearchEverywhereDialog {
     }
   }
 
+  private void updateShowAllSearching() {
+    wShowAll.setText(BaseMessages.getString(PKG, 
"SearchEverywhereDialog.Searching.Label"));
+    wShowAll.requestLayout();
+  }
+
   private void updateShowAll() {
     if (fullResultCount > 0) {
+      String base =
+          BaseMessages.getString(
+              PKG, "SearchEverywhereDialog.ShowAll.Label", 
Integer.toString(fullResultCount));
+      if (lastAnalysis.isTruncated()) {
+        base = base + " — " + BaseMessages.getString(PKG, 
"SearchEverywhereDialog.Status.Capped");
+      } else if (lastAnalysis.isContentSearchSkipped()) {
+        base =
+            base
+                + " — "
+                + BaseMessages.getString(
+                    PKG,
+                    "SearchEverywhereDialog.Status.MinLength",
+                    
Integer.toString(SearchLimits.fromConfig().getMinContentQueryLength()));
+      }
+      wShowAll.setText(base);
+    } else if (lastAnalysis.isContentSearchSkipped()
+        && wSearch != null
+        && !wSearch.isDisposed()
+        && !Utils.isEmpty(wSearch.getText())) {
       wShowAll.setText(
           BaseMessages.getString(
-              PKG, "SearchEverywhereDialog.ShowAll.Label", 
Integer.toString(fullResultCount)));
+              PKG,
+              "SearchEverywhereDialog.Status.MinLengthOnly",
+              
Integer.toString(SearchLimits.fromConfig().getMinContentQueryLength())));
     } else {
       wShowAll.setText(BaseMessages.getString(PKG, 
"SearchEverywhereDialog.Hint.Label"));
     }
@@ -667,6 +785,8 @@ public class SearchEverywhereDialog {
   }
 
   private void dispose() {
+    searchGeneration.incrementAndGet();
+    searchExecutor.shutdownNow();
     if (shell != null && !shell.isDisposed()) {
       // Remember the size and position for next time.
       props.setScreen(new WindowProperty(shell));
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/search/SearchSettingsDialog.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/SearchSettingsDialog.java
new file mode 100644
index 0000000000..c373a38fc6
--- /dev/null
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/search/SearchSettingsDialog.java
@@ -0,0 +1,237 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui.search;
+
+import org.apache.hop.core.Const;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.dialog.BaseDialog;
+import org.apache.hop.ui.core.dialog.ErrorDialog;
+import org.apache.hop.ui.core.gui.GuiResource;
+import org.apache.hop.ui.core.gui.WindowProperty;
+import org.apache.hop.ui.core.widget.TextVar;
+import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.search.config.SearchConfig;
+import org.apache.hop.ui.hopgui.search.config.SearchConfigSingleton;
+import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+
+/** Compact settings dialog for Hop GUI search limits (also available in 
Configuration). */
+public class SearchSettingsDialog extends Dialog {
+
+  private static final Class<?> PKG = SearchEverywhereDialog.class; // shared 
search messages
+
+  private final Shell parent;
+  private final HopGui hopGui;
+  private final PropsUi props = PropsUi.getInstance();
+  private Shell shell;
+
+  private TextVar wMinLength;
+  private TextVar wMaxResults;
+  private TextVar wMaxPerFile;
+  private TextVar wMaxSizeMb;
+  private Button wIncludeTextFiles;
+  private Button wSearchAsYouType;
+  private TextVar wDebounceMs;
+
+  private boolean saved;
+
+  public SearchSettingsDialog(Shell parent, HopGui hopGui) {
+    super(parent, SWT.NONE);
+    this.parent = parent;
+    this.hopGui = hopGui;
+  }
+
+  /**
+   * Open the dialog.
+   *
+   * @return true if the user saved changes
+   */
+  public boolean open() {
+    shell = new Shell(parent, SWT.DIALOG_TRIM | SWT.RESIZE | 
SWT.APPLICATION_MODAL | SWT.SHEET);
+    shell.setText(BaseMessages.getString(PKG, 
"SearchSettingsDialog.Shell.Title"));
+    shell.setImage(GuiResource.getInstance().getImage("ui/images/gear.svg", 
16, 16));
+    PropsUi.setLook(shell);
+
+    FormLayout formLayout = new FormLayout();
+    formLayout.marginWidth = PropsUi.getFormMargin();
+    formLayout.marginHeight = PropsUi.getFormMargin();
+    shell.setLayout(formLayout);
+
+    int margin = PropsUi.getMargin();
+    int middle = props.getMiddlePct();
+
+    wMinLength =
+        addTextField(
+            "SearchSettingsDialog.MinContentQueryLength.Label",
+            "SearchSettingsDialog.MinContentQueryLength.Tooltip",
+            middle,
+            margin,
+            null);
+    wMaxResults =
+        addTextField(
+            "SearchSettingsDialog.MaxResults.Label",
+            "SearchSettingsDialog.MaxResults.Tooltip",
+            middle,
+            margin,
+            wMinLength);
+    wMaxPerFile =
+        addTextField(
+            "SearchSettingsDialog.MaxMatchesPerFile.Label",
+            "SearchSettingsDialog.MaxMatchesPerFile.Tooltip",
+            middle,
+            margin,
+            wMaxResults);
+    wMaxSizeMb =
+        addTextField(
+            "SearchSettingsDialog.MaxTextFileSizeMb.Label",
+            "SearchSettingsDialog.MaxTextFileSizeMb.Tooltip",
+            middle,
+            margin,
+            wMaxPerFile);
+    wDebounceMs =
+        addTextField(
+            "SearchSettingsDialog.DebounceMs.Label",
+            "SearchSettingsDialog.DebounceMs.Tooltip",
+            middle,
+            margin,
+            wMaxSizeMb);
+
+    wIncludeTextFiles = new Button(shell, SWT.CHECK);
+    PropsUi.setLook(wIncludeTextFiles);
+    wIncludeTextFiles.setText(
+        BaseMessages.getString(PKG, 
"SearchSettingsDialog.IncludeProjectTextFiles.Label"));
+    wIncludeTextFiles.setToolTipText(
+        BaseMessages.getString(PKG, 
"SearchSettingsDialog.IncludeProjectTextFiles.Tooltip"));
+    FormData fdInclude = new FormData();
+    fdInclude.left = new FormAttachment(0, 0);
+    fdInclude.right = new FormAttachment(100, 0);
+    fdInclude.top = new FormAttachment(wDebounceMs, margin * 2);
+    wIncludeTextFiles.setLayoutData(fdInclude);
+
+    wSearchAsYouType = new Button(shell, SWT.CHECK);
+    PropsUi.setLook(wSearchAsYouType);
+    wSearchAsYouType.setText(
+        BaseMessages.getString(PKG, 
"SearchSettingsDialog.SearchAsYouType.Label"));
+    wSearchAsYouType.setToolTipText(
+        BaseMessages.getString(PKG, 
"SearchSettingsDialog.SearchAsYouType.Tooltip"));
+    FormData fdAsYouType = new FormData();
+    fdAsYouType.left = new FormAttachment(0, 0);
+    fdAsYouType.right = new FormAttachment(100, 0);
+    fdAsYouType.top = new FormAttachment(wIncludeTextFiles, margin);
+    wSearchAsYouType.setLayoutData(fdAsYouType);
+
+    Button wOk = new Button(shell, SWT.PUSH);
+    wOk.setText(BaseMessages.getString(PKG, "System.Button.OK"));
+    wOk.addListener(SWT.Selection, e -> ok());
+
+    Button wCancel = new Button(shell, SWT.PUSH);
+    wCancel.setText(BaseMessages.getString(PKG, "System.Button.Cancel"));
+    wCancel.addListener(SWT.Selection, e -> cancel());
+
+    BaseTransformDialog.positionBottomButtons(
+        shell, new Button[] {wOk, wCancel}, margin, wSearchAsYouType);
+
+    loadValues();
+
+    BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel());
+    return saved;
+  }
+
+  private TextVar addTextField(
+      String labelKey, String tooltipKey, int middle, int margin, Control 
last) {
+    Label label = new Label(shell, SWT.RIGHT);
+    PropsUi.setLook(label);
+    label.setText(BaseMessages.getString(PKG, labelKey));
+    label.setToolTipText(BaseMessages.getString(PKG, tooltipKey));
+    FormData fdLabel = new FormData();
+    fdLabel.left = new FormAttachment(0, 0);
+    fdLabel.right = new FormAttachment(middle, -margin);
+    fdLabel.top = new FormAttachment(last, last == null ? 0 : margin);
+    label.setLayoutData(fdLabel);
+
+    TextVar field = new TextVar(hopGui.getVariables(), shell, SWT.SINGLE | 
SWT.LEFT | SWT.BORDER);
+    PropsUi.setLook(field);
+    field.setToolTipText(BaseMessages.getString(PKG, tooltipKey));
+    FormData fdField = new FormData();
+    fdField.left = new FormAttachment(middle, 0);
+    fdField.right = new FormAttachment(100, 0);
+    fdField.top = new FormAttachment(label, 0, SWT.CENTER);
+    field.setLayoutData(fdField);
+    return field;
+  }
+
+  private void loadValues() {
+    SearchConfig config = SearchConfigSingleton.getConfig();
+    wMinLength.setText(
+        Const.NVL(
+            config.getMinContentQueryLength(), 
SearchConfig.DEFAULT_MIN_CONTENT_QUERY_LENGTH));
+    wMaxResults.setText(Const.NVL(config.getMaxResults(), 
SearchConfig.DEFAULT_MAX_RESULTS));
+    wMaxPerFile.setText(
+        Const.NVL(config.getMaxMatchesPerFile(), 
SearchConfig.DEFAULT_MAX_MATCHES_PER_FILE));
+    wMaxSizeMb.setText(
+        Const.NVL(config.getMaxTextFileSizeMb(), 
SearchConfig.DEFAULT_MAX_TEXT_FILE_SIZE_MB));
+    wDebounceMs.setText(Const.NVL(config.getDebounceMs(), 
SearchConfig.DEFAULT_DEBOUNCE_MS));
+    wIncludeTextFiles.setSelection(
+        config.getIncludeProjectTextFiles() == null || 
config.getIncludeProjectTextFiles());
+    wSearchAsYouType.setSelection(
+        config.getSearchAsYouType() == null || config.getSearchAsYouType());
+  }
+
+  private void ok() {
+    SearchConfig config = SearchConfigSingleton.getConfig();
+    config.setMinContentQueryLength(wMinLength.getText());
+    config.setMaxResults(wMaxResults.getText());
+    config.setMaxMatchesPerFile(wMaxPerFile.getText());
+    config.setMaxTextFileSizeMb(wMaxSizeMb.getText());
+    config.setDebounceMs(wDebounceMs.getText());
+    config.setIncludeProjectTextFiles(wIncludeTextFiles.getSelection());
+    config.setSearchAsYouType(wSearchAsYouType.getSelection());
+    try {
+      SearchConfigSingleton.saveConfig();
+      saved = true;
+      dispose();
+    } catch (Exception e) {
+      new ErrorDialog(
+          shell,
+          BaseMessages.getString(PKG, "SearchSettingsDialog.Error.Title"),
+          BaseMessages.getString(PKG, 
"SearchSettingsDialog.Error.Save.Message"),
+          e);
+    }
+  }
+
+  private void cancel() {
+    saved = false;
+    dispose();
+  }
+
+  private void dispose() {
+    if (shell != null && !shell.isDisposed()) {
+      props.setScreen(new WindowProperty(shell));
+      shell.dispose();
+    }
+  }
+}
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/search/TextFileContentSearchableAnalyser.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/TextFileContentSearchableAnalyser.java
index d75f8efb52..11b5a8d320 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/search/TextFileContentSearchableAnalyser.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/TextFileContentSearchableAnalyser.java
@@ -25,6 +25,7 @@ import org.apache.hop.core.search.ISearchResult;
 import org.apache.hop.core.search.ISearchable;
 import org.apache.hop.core.search.ISearchableAnalyser;
 import org.apache.hop.core.search.SearchableAnalyserPlugin;
+import org.apache.hop.ui.hopgui.search.config.SearchLimits;
 
 @SearchableAnalyserPlugin(
     id = "TextFileContentSearchableAnalyser",
@@ -49,11 +50,31 @@ public class TextFileContentSearchableAnalyser extends 
BaseSearchableAnalyser<Te
       return results;
     }
 
-    String[] lines = content.split("\\R", -1);
-    for (int i = 0; i < lines.length; i++) {
-      String line = lines[i];
-      String lineNumber = String.valueOf(i + 1);
-      matchProperty(searchable, results, searchQuery, "line " + lineNumber, 
line, lineNumber);
+    int maxMatches = SearchLimits.fromConfig().getMaxMatchesPerFile();
+
+    // Stream line-by-line without allocating a full String[] for huge files.
+    int lineNumber = 0;
+    int start = 0;
+    final int length = content.length();
+    while (start <= length) {
+      int end = content.indexOf('\n', start);
+      if (end < 0) {
+        end = length;
+      }
+      String line = content.substring(start, end);
+      if (!line.isEmpty() && line.charAt(line.length() - 1) == '\r') {
+        line = line.substring(0, line.length() - 1);
+      }
+      lineNumber++;
+      matchProperty(
+          searchable, results, searchQuery, "line " + lineNumber, line, 
String.valueOf(lineNumber));
+      if (results.size() >= maxMatches) {
+        break;
+      }
+      if (end == length) {
+        break;
+      }
+      start = end + 1;
     }
 
     return results;
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/search/config/SearchAnalysisResult.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/config/SearchAnalysisResult.java
new file mode 100644
index 0000000000..e76b574283
--- /dev/null
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/config/SearchAnalysisResult.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui.search.config;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Getter;
+import org.apache.hop.core.search.ISearchResult;
+
+/** Outcome of a limited GUI search: results plus whether caps/filters 
applied. */
+@Getter
+public class SearchAnalysisResult {
+
+  private final List<ISearchResult> results;
+  private final boolean truncated;
+  private final boolean contentSearchSkipped;
+  private final boolean projectTextFilesExcluded;
+
+  public SearchAnalysisResult(
+      List<ISearchResult> results,
+      boolean truncated,
+      boolean contentSearchSkipped,
+      boolean projectTextFilesExcluded) {
+    this.results = results == null ? new ArrayList<>() : results;
+    this.truncated = truncated;
+    this.contentSearchSkipped = contentSearchSkipped;
+    this.projectTextFilesExcluded = projectTextFilesExcluded;
+  }
+
+  public static SearchAnalysisResult empty() {
+    return new SearchAnalysisResult(List.of(), false, false, false);
+  }
+}
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/search/config/SearchConfig.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/config/SearchConfig.java
new file mode 100644
index 0000000000..753d7aed6e
--- /dev/null
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/search/config/SearchConfig.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui.search.config;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Persistent Hop GUI search settings. Stored under {@link 
#HOP_CONFIG_SEARCH_KEY} in hop-config.
+ *
+ * <p>Large data files (million-row CSV/JSON, …) are intentionally excluded 
from GUI search via
+ * {@link #maxTextFileSizeMb}; use dedicated tools for those.
+ */
+@Getter
+@Setter
+public class SearchConfig {
+
+  public static final String HOP_CONFIG_SEARCH_KEY = "search";
+
+  public static final String DEFAULT_MIN_CONTENT_QUERY_LENGTH = "3";
+  public static final String DEFAULT_MAX_RESULTS = "500";
+  public static final String DEFAULT_MAX_MATCHES_PER_FILE = "20";
+  public static final String DEFAULT_MAX_TEXT_FILE_SIZE_MB = "1";
+  public static final String DEFAULT_DEBOUNCE_MS = "300";
+
+  /** Minimum characters before file/content matching runs (names still match 
below this). */
+  private String minContentQueryLength;
+
+  /** Hard cap on total search results built during analysis (GUI only). */
+  private String maxResults;
+
+  /** Cap on content matches reported per text file. */
+  private String maxMatchesPerFile;
+
+  /**
+   * Maximum size in megabytes of a text file that may be loaded for content 
search. Larger files
+   * are skipped entirely.
+   */
+  private String maxTextFileSizeMb;
+
+  /**
+   * When false, project text files (CSV, JSON, …) are not content-searched; 
open files still are.
+   */
+  private Boolean includeProjectTextFiles;
+
+  /** When true, search runs while typing (debounced); Enter always searches. 
*/
+  private Boolean searchAsYouType;
+
+  /** Debounce in milliseconds between keystrokes and running a live search. */
+  private String debounceMs;
+
+  public SearchConfig() {
+    this.minContentQueryLength = DEFAULT_MIN_CONTENT_QUERY_LENGTH;
+    this.maxResults = DEFAULT_MAX_RESULTS;
+    this.maxMatchesPerFile = DEFAULT_MAX_MATCHES_PER_FILE;
+    this.maxTextFileSizeMb = DEFAULT_MAX_TEXT_FILE_SIZE_MB;
+    this.includeProjectTextFiles = true;
+    this.searchAsYouType = true;
+    this.debounceMs = DEFAULT_DEBOUNCE_MS;
+  }
+
+  public SearchConfig(SearchConfig other) {
+    this();
+    if (other == null) {
+      return;
+    }
+    this.minContentQueryLength = other.minContentQueryLength;
+    this.maxResults = other.maxResults;
+    this.maxMatchesPerFile = other.maxMatchesPerFile;
+    this.maxTextFileSizeMb = other.maxTextFileSizeMb;
+    this.includeProjectTextFiles = other.includeProjectTextFiles;
+    this.searchAsYouType = other.searchAsYouType;
+    this.debounceMs = other.debounceMs;
+  }
+}
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/search/config/SearchConfigPlugin.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/config/SearchConfigPlugin.java
new file mode 100644
index 0000000000..6693c652d4
--- /dev/null
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/config/SearchConfigPlugin.java
@@ -0,0 +1,270 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui.search.config;
+
+import org.apache.hop.core.config.plugin.ConfigPlugin;
+import org.apache.hop.core.config.plugin.IConfigOptions;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.gui.plugin.GuiElementType;
+import org.apache.hop.core.gui.plugin.GuiPlugin;
+import org.apache.hop.core.gui.plugin.GuiWidgetElement;
+import org.apache.hop.core.logging.ILogChannel;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.metadata.api.IHasHopMetadataProvider;
+import org.apache.hop.ui.core.dialog.ErrorDialog;
+import org.apache.hop.ui.core.gui.GuiCompositeWidgets;
+import org.apache.hop.ui.core.gui.IGuiPluginCompositeWidgetsListener;
+import org.apache.hop.ui.core.widget.TextVar;
+import org.apache.hop.ui.hopgui.HopGui;
+import 
org.apache.hop.ui.hopgui.perspective.configuration.tabs.ConfigPluginOptionsTab;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Control;
+import picocli.CommandLine;
+
+@ConfigPlugin(
+    id = "SearchConfigPlugin",
+    description = "Configuration options for Hop GUI search",
+    category = ConfigPlugin.CATEGORY_CONFIG)
+@GuiPlugin(
+    description = "Search" // Tab label in options dialog
+    )
+public class SearchConfigPlugin implements IConfigOptions, 
IGuiPluginCompositeWidgetsListener {
+
+  private static final String WIDGET_ID_MIN_CONTENT_QUERY_LENGTH =
+      "20000-search-min-content-query-length";
+  private static final String WIDGET_ID_MAX_RESULTS = 
"20010-search-max-results";
+  private static final String WIDGET_ID_MAX_MATCHES_PER_FILE = 
"20020-search-max-matches-per-file";
+  private static final String WIDGET_ID_MAX_TEXT_FILE_SIZE_MB =
+      "20030-search-max-text-file-size-mb";
+  private static final String WIDGET_ID_INCLUDE_PROJECT_TEXT_FILES =
+      "20040-search-include-project-text-files";
+  private static final String WIDGET_ID_SEARCH_AS_YOU_TYPE = 
"20050-search-as-you-type";
+  private static final String WIDGET_ID_DEBOUNCE_MS = 
"20060-search-debounce-ms";
+
+  @GuiWidgetElement(
+      id = WIDGET_ID_MIN_CONTENT_QUERY_LENGTH,
+      parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
+      type = GuiElementType.TEXT,
+      variables = true,
+      label = "i18n::SearchConfig.MinContentQueryLength.Label",
+      toolTip = "i18n::SearchConfig.MinContentQueryLength.Tooltip")
+  @CommandLine.Option(
+      names = {"-smin", "--search-min-content-query-length"},
+      description = "Minimum characters before content search runs in Hop GUI")
+  private String minContentQueryLength;
+
+  @GuiWidgetElement(
+      id = WIDGET_ID_MAX_RESULTS,
+      parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
+      type = GuiElementType.TEXT,
+      variables = true,
+      label = "i18n::SearchConfig.MaxResults.Label",
+      toolTip = "i18n::SearchConfig.MaxResults.Tooltip")
+  @CommandLine.Option(
+      names = {"-smax", "--search-max-results"},
+      description = "Maximum number of search results built in Hop GUI")
+  private String maxResults;
+
+  @GuiWidgetElement(
+      id = WIDGET_ID_MAX_MATCHES_PER_FILE,
+      parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
+      type = GuiElementType.TEXT,
+      variables = true,
+      label = "i18n::SearchConfig.MaxMatchesPerFile.Label",
+      toolTip = "i18n::SearchConfig.MaxMatchesPerFile.Tooltip")
+  @CommandLine.Option(
+      names = {"-smpf", "--search-max-matches-per-file"},
+      description = "Maximum content matches per text file in Hop GUI search")
+  private String maxMatchesPerFile;
+
+  @GuiWidgetElement(
+      id = WIDGET_ID_MAX_TEXT_FILE_SIZE_MB,
+      parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
+      type = GuiElementType.TEXT,
+      variables = true,
+      label = "i18n::SearchConfig.MaxTextFileSizeMb.Label",
+      toolTip = "i18n::SearchConfig.MaxTextFileSizeMb.Tooltip")
+  @CommandLine.Option(
+      names = {"-sfs", "--search-max-text-file-size-mb"},
+      description = "Skip text files larger than this many MB in Hop GUI 
search")
+  private String maxTextFileSizeMb;
+
+  @GuiWidgetElement(
+      id = WIDGET_ID_INCLUDE_PROJECT_TEXT_FILES,
+      parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
+      type = GuiElementType.CHECKBOX,
+      label = "i18n::SearchConfig.IncludeProjectTextFiles.Label",
+      toolTip = "i18n::SearchConfig.IncludeProjectTextFiles.Tooltip")
+  @CommandLine.Option(
+      names = {"-sit", "--search-include-project-text-files"},
+      description = "Include project text files (CSV, JSON, …) in Hop GUI 
content search",
+      negatable = true)
+  private Boolean includeProjectTextFiles = true;
+
+  @GuiWidgetElement(
+      id = WIDGET_ID_SEARCH_AS_YOU_TYPE,
+      parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
+      type = GuiElementType.CHECKBOX,
+      label = "i18n::SearchConfig.SearchAsYouType.Label",
+      toolTip = "i18n::SearchConfig.SearchAsYouType.Tooltip")
+  @CommandLine.Option(
+      names = {"-sat", "--search-as-you-type"},
+      description = "Run Hop GUI search while typing (debounced)",
+      negatable = true)
+  private Boolean searchAsYouType = true;
+
+  @GuiWidgetElement(
+      id = WIDGET_ID_DEBOUNCE_MS,
+      parentId = ConfigPluginOptionsTab.GUI_WIDGETS_PARENT_ID,
+      type = GuiElementType.TEXT,
+      variables = true,
+      label = "i18n::SearchConfig.DebounceMs.Label",
+      toolTip = "i18n::SearchConfig.DebounceMs.Tooltip")
+  @CommandLine.Option(
+      names = {"-sdb", "--search-debounce-ms"},
+      description = "Debounce in ms between keystrokes and Hop GUI live 
search")
+  private String debounceMs;
+
+  public static SearchConfigPlugin getInstance() {
+    SearchConfigPlugin instance = new SearchConfigPlugin();
+    SearchConfig config = SearchConfigSingleton.getConfig();
+    instance.minContentQueryLength = config.getMinContentQueryLength();
+    instance.maxResults = config.getMaxResults();
+    instance.maxMatchesPerFile = config.getMaxMatchesPerFile();
+    instance.maxTextFileSizeMb = config.getMaxTextFileSizeMb();
+    instance.includeProjectTextFiles =
+        config.getIncludeProjectTextFiles() == null || 
config.getIncludeProjectTextFiles();
+    instance.searchAsYouType = config.getSearchAsYouType() == null || 
config.getSearchAsYouType();
+    instance.debounceMs = config.getDebounceMs();
+    return instance;
+  }
+
+  @Override
+  public boolean handleOption(
+      ILogChannel log, IHasHopMetadataProvider hasHopMetadataProvider, 
IVariables variables)
+      throws HopException {
+    SearchConfig config = SearchConfigSingleton.getConfig();
+    try {
+      boolean changed = false;
+
+      if (minContentQueryLength != null) {
+        config.setMinContentQueryLength(minContentQueryLength);
+        log.logBasic("Search: min content query length set to '" + 
minContentQueryLength + "'");
+        changed = true;
+      }
+      if (maxResults != null) {
+        config.setMaxResults(maxResults);
+        log.logBasic("Search: max results set to '" + maxResults + "'");
+        changed = true;
+      }
+      if (maxMatchesPerFile != null) {
+        config.setMaxMatchesPerFile(maxMatchesPerFile);
+        log.logBasic("Search: max matches per file set to '" + 
maxMatchesPerFile + "'");
+        changed = true;
+      }
+      if (maxTextFileSizeMb != null) {
+        config.setMaxTextFileSizeMb(maxTextFileSizeMb);
+        log.logBasic("Search: max text file size (MB) set to '" + 
maxTextFileSizeMb + "'");
+        changed = true;
+      }
+      if (includeProjectTextFiles != null) {
+        config.setIncludeProjectTextFiles(includeProjectTextFiles);
+        log.logBasic("Search: include project text files set to '" + 
includeProjectTextFiles + "'");
+        changed = true;
+      }
+      if (searchAsYouType != null) {
+        config.setSearchAsYouType(searchAsYouType);
+        log.logBasic("Search: search as you type set to '" + searchAsYouType + 
"'");
+        changed = true;
+      }
+      if (debounceMs != null) {
+        config.setDebounceMs(debounceMs);
+        log.logBasic("Search: debounce ms set to '" + debounceMs + "'");
+        changed = true;
+      }
+
+      if (changed) {
+        SearchConfigSingleton.saveConfig();
+      }
+      return changed;
+    } catch (Exception e) {
+      throw new HopException("Error handling search configuration options", e);
+    }
+  }
+
+  @Override
+  public void widgetsCreated(GuiCompositeWidgets compositeWidgets) {
+    // Do nothing
+  }
+
+  @Override
+  public void widgetsPopulated(GuiCompositeWidgets compositeWidgets) {
+    // Do nothing
+  }
+
+  @Override
+  public void widgetModified(
+      GuiCompositeWidgets compositeWidgets, Control changedWidget, String 
widgetId) {
+    persistContents(compositeWidgets);
+  }
+
+  @Override
+  public void persistContents(GuiCompositeWidgets compositeWidgets) {
+    SearchConfig config = SearchConfigSingleton.getConfig();
+    for (String widgetId : compositeWidgets.getWidgetsMap().keySet()) {
+      Control control = compositeWidgets.getWidgetsMap().get(widgetId);
+      switch (widgetId) {
+        case WIDGET_ID_MIN_CONTENT_QUERY_LENGTH:
+          minContentQueryLength = ((TextVar) control).getText();
+          config.setMinContentQueryLength(minContentQueryLength);
+          break;
+        case WIDGET_ID_MAX_RESULTS:
+          maxResults = ((TextVar) control).getText();
+          config.setMaxResults(maxResults);
+          break;
+        case WIDGET_ID_MAX_MATCHES_PER_FILE:
+          maxMatchesPerFile = ((TextVar) control).getText();
+          config.setMaxMatchesPerFile(maxMatchesPerFile);
+          break;
+        case WIDGET_ID_MAX_TEXT_FILE_SIZE_MB:
+          maxTextFileSizeMb = ((TextVar) control).getText();
+          config.setMaxTextFileSizeMb(maxTextFileSizeMb);
+          break;
+        case WIDGET_ID_INCLUDE_PROJECT_TEXT_FILES:
+          includeProjectTextFiles = ((Button) control).getSelection();
+          config.setIncludeProjectTextFiles(includeProjectTextFiles);
+          break;
+        case WIDGET_ID_SEARCH_AS_YOU_TYPE:
+          searchAsYouType = ((Button) control).getSelection();
+          config.setSearchAsYouType(searchAsYouType);
+          break;
+        case WIDGET_ID_DEBOUNCE_MS:
+          debounceMs = ((TextVar) control).getText();
+          config.setDebounceMs(debounceMs);
+          break;
+        default:
+          break;
+      }
+    }
+    try {
+      SearchConfigSingleton.saveConfig();
+    } catch (Exception e) {
+      new ErrorDialog(HopGui.getInstance().getShell(), "Error", "Error saving 
search options", e);
+    }
+  }
+}
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/search/config/SearchConfigSingleton.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/config/SearchConfigSingleton.java
new file mode 100644
index 0000000000..a6469bfe84
--- /dev/null
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/config/SearchConfigSingleton.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui.search.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.gson.Gson;
+import org.apache.hop.core.config.HopConfig;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.json.HopJson;
+import org.apache.hop.core.logging.LogChannel;
+
+public class SearchConfigSingleton {
+
+  private static SearchConfigSingleton configSingleton;
+
+  private SearchConfig searchConfig;
+
+  private SearchConfigSingleton() {
+    Object configObject =
+        
HopConfig.getInstance().getConfigMap().get(SearchConfig.HOP_CONFIG_SEARCH_KEY);
+    if (configObject == null) {
+      searchConfig = new SearchConfig();
+    } else {
+      try {
+        ObjectMapper mapper = HopJson.newMapper();
+        searchConfig = mapper.readValue(new Gson().toJson(configObject), 
SearchConfig.class);
+      } catch (Exception e) {
+        LogChannel.GENERAL.logError(
+            "Error reading search configuration, please check property '"
+                + SearchConfig.HOP_CONFIG_SEARCH_KEY
+                + "' in the Hop config json file",
+            e);
+        searchConfig = new SearchConfig();
+      }
+    }
+    
HopConfig.getInstance().getConfigMap().put(SearchConfig.HOP_CONFIG_SEARCH_KEY, 
searchConfig);
+  }
+
+  public static SearchConfig getConfig() {
+    if (configSingleton == null) {
+      configSingleton = new SearchConfigSingleton();
+    }
+    return configSingleton.searchConfig;
+  }
+
+  public static void saveConfig() throws HopException {
+    if (configSingleton == null) {
+      configSingleton = new SearchConfigSingleton();
+    }
+    HopConfig.getInstance()
+        .saveOption(SearchConfig.HOP_CONFIG_SEARCH_KEY, 
configSingleton.searchConfig);
+    HopConfig.getInstance().saveToFile();
+  }
+
+  /**
+   * Replace the in-memory config without writing hop-config. Intended for 
unit tests; also used
+   * carefully when applying settings that must take effect immediately.
+   */
+  public static void setConfigForTesting(SearchConfig config) {
+    if (configSingleton == null) {
+      // Avoid loading from disk when HopConfig is not initialized in unit 
tests.
+      configSingleton = new SearchConfigSingleton(config == null ? new 
SearchConfig() : config);
+      return;
+    }
+    configSingleton.searchConfig = config == null ? new SearchConfig() : 
config;
+  }
+
+  private SearchConfigSingleton(SearchConfig config) {
+    this.searchConfig = config;
+  }
+}
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/search/config/SearchLimits.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/search/config/SearchLimits.java
new file mode 100644
index 0000000000..88e605edb2
--- /dev/null
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/search/config/SearchLimits.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui.search.config;
+
+import lombok.Getter;
+import org.apache.hop.core.Const;
+
+/**
+ * Parsed, immutable snapshot of {@link SearchConfig} for one search run. 
Defaults match the product
+ * safety limits so the GUI never attempts million-row data-file content 
search.
+ */
+@Getter
+public final class SearchLimits {
+
+  public static final int DEFAULT_MIN_CONTENT_QUERY_LENGTH =
+      Integer.parseInt(SearchConfig.DEFAULT_MIN_CONTENT_QUERY_LENGTH);
+  public static final int DEFAULT_MAX_RESULTS = 
Integer.parseInt(SearchConfig.DEFAULT_MAX_RESULTS);
+  public static final int DEFAULT_MAX_MATCHES_PER_FILE =
+      Integer.parseInt(SearchConfig.DEFAULT_MAX_MATCHES_PER_FILE);
+  public static final int DEFAULT_MAX_TEXT_FILE_SIZE_MB =
+      Integer.parseInt(SearchConfig.DEFAULT_MAX_TEXT_FILE_SIZE_MB);
+  public static final int DEFAULT_DEBOUNCE_MS = 
Integer.parseInt(SearchConfig.DEFAULT_DEBOUNCE_MS);
+
+  private final int minContentQueryLength;
+  private final int maxResults;
+  private final int maxMatchesPerFile;
+  private final long maxTextFileSizeBytes;
+  private final boolean includeProjectTextFiles;
+  private final boolean searchAsYouType;
+  private final int debounceMs;
+
+  private SearchLimits(
+      int minContentQueryLength,
+      int maxResults,
+      int maxMatchesPerFile,
+      long maxTextFileSizeBytes,
+      boolean includeProjectTextFiles,
+      boolean searchAsYouType,
+      int debounceMs) {
+    this.minContentQueryLength = Math.max(1, minContentQueryLength);
+    this.maxResults = Math.max(1, maxResults);
+    this.maxMatchesPerFile = Math.max(1, maxMatchesPerFile);
+    this.maxTextFileSizeBytes = Math.max(0L, maxTextFileSizeBytes);
+    this.includeProjectTextFiles = includeProjectTextFiles;
+    this.searchAsYouType = searchAsYouType;
+    this.debounceMs = Math.max(0, debounceMs);
+  }
+
+  public static SearchLimits defaults() {
+    return fromConfig(new SearchConfig());
+  }
+
+  public static SearchLimits fromConfig() {
+    return fromConfig(SearchConfigSingleton.getConfig());
+  }
+
+  public static SearchLimits fromConfig(SearchConfig config) {
+    SearchConfig c = config == null ? new SearchConfig() : config;
+    int minLen = (int) Const.toLong(c.getMinContentQueryLength(), 
DEFAULT_MIN_CONTENT_QUERY_LENGTH);
+    int maxResults = (int) Const.toLong(c.getMaxResults(), 
DEFAULT_MAX_RESULTS);
+    int maxPerFile = (int) Const.toLong(c.getMaxMatchesPerFile(), 
DEFAULT_MAX_MATCHES_PER_FILE);
+    int maxMb = (int) Const.toLong(c.getMaxTextFileSizeMb(), 
DEFAULT_MAX_TEXT_FILE_SIZE_MB);
+    int debounce = (int) Const.toLong(c.getDebounceMs(), DEFAULT_DEBOUNCE_MS);
+    boolean includeText = c.getIncludeProjectTextFiles() == null || 
c.getIncludeProjectTextFiles();
+    boolean asYouType = c.getSearchAsYouType() == null || 
c.getSearchAsYouType();
+    return new SearchLimits(
+        minLen, maxResults, maxPerFile, maxMb * 1024L * 1024L, includeText, 
asYouType, debounce);
+  }
+
+  /**
+   * Whether content (including text-file line matching) is allowed for this 
query string. Name
+   * matching is always allowed.
+   */
+  public boolean allowsContentSearch(String query) {
+    if (query == null) {
+      return false;
+    }
+    return query.trim().length() >= minContentQueryLength;
+  }
+}
diff --git 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/search/config/messages/messages_en_US.properties
 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/search/config/messages/messages_en_US.properties
new file mode 100644
index 0000000000..6c06b3fb0e
--- /dev/null
+++ 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/search/config/messages/messages_en_US.properties
@@ -0,0 +1,32 @@
+#
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+SearchConfig.MinContentQueryLength.Label=Minimum characters for content search
+SearchConfig.MinContentQueryLength.Tooltip=Object names are always searched. 
File and field content is only searched when the query has at least this many 
characters (default 3). Prevents floods of hits while typing.
+SearchConfig.MaxResults.Label=Maximum search results
+SearchConfig.MaxResults.Tooltip=Hard limit on the number of results built for 
Hop GUI search (default 500). Further matches are not collected so the UI stays 
responsive.
+SearchConfig.MaxMatchesPerFile.Label=Maximum matches per text file
+SearchConfig.MaxMatchesPerFile.Tooltip=Stop scanning a single text file after 
this many matching lines (default 20).
+SearchConfig.MaxTextFileSizeMb.Label=Maximum text file size for search (MB)
+SearchConfig.MaxTextFileSizeMb.Tooltip=Text files larger than this are skipped 
for content search (default 1 MB). Use dedicated tools for million-row CSV/JSON 
files.
+SearchConfig.IncludeProjectTextFiles.Label=Include project text files in 
content search
+SearchConfig.IncludeProjectTextFiles.Tooltip=When enabled, CSV, JSON, SQL and 
other project text files are content-searched (subject to size and match 
limits). Open text files are always eligible. Disable to search only pipelines, 
workflows, metadata and open files.
+SearchConfig.SearchAsYouType.Label=Search as you type
+SearchConfig.SearchAsYouType.Tooltip=When enabled, search runs automatically 
after a short pause while typing. Press Enter to search immediately either way.
+SearchConfig.DebounceMs.Label=Search-as-you-type delay (ms)
+SearchConfig.DebounceMs.Tooltip=Milliseconds to wait after the last keystroke 
before running a live search (default 300).
diff --git 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/search/messages/messages_en_US.properties
 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/search/messages/messages_en_US.properties
index 170a653cd1..3ab1efed16 100644
--- 
a/ui/src/main/resources/org/apache/hop/ui/hopgui/search/messages/messages_en_US.properties
+++ 
b/ui/src/main/resources/org/apache/hop/ui/hopgui/search/messages/messages_en_US.properties
@@ -22,22 +22,46 @@ SearchEverywhereDialog.CaseSensitive.Label=Aa
 SearchEverywhereDialog.CaseSensitive.Tooltip=Case sensitive
 SearchEverywhereDialog.RegEx.Label=.*
 SearchEverywhereDialog.RegEx.Tooltip=Regular expression
+SearchEverywhereDialog.Settings.Tooltip=Search settings (limits, large file 
skip, search-as-you-type)
 SearchEverywhereDialog.Column.Name=Name
 SearchEverywhereDialog.Column.Location=Location
 SearchEverywhereDialog.Group.Open=Open files
 SearchEverywhereDialog.Group.Project=Project files
 SearchEverywhereDialog.Hint.Label=Type to search across all loaded locations
+SearchEverywhereDialog.Searching.Label=Searching\u2026
 SearchEverywhereDialog.ShowAll.Label=<a>Show all {0} results</a>
 SearchEverywhereDialog.ShowAll.TabTitle=Search results
+SearchEverywhereDialog.Status.Capped=results capped (see Search settings)
+SearchEverywhereDialog.Status.MinLength=type {0}+ characters to search file 
content
+SearchEverywhereDialog.Status.MinLengthOnly=Type at least {0} characters to 
search file content (names still match)
 SearchEverywhereDialog.Error.Title=Error
 SearchEverywhereDialog.Error.Message=Error while searching
 SearchEverywhereDialog.Error.Open.Message=Error opening {0}
 
+SearchSettingsDialog.Shell.Title=Search settings
+SearchSettingsDialog.MinContentQueryLength.Label=Min. characters for content
+SearchSettingsDialog.MinContentQueryLength.Tooltip=Names always match. Content 
(including text files) only when the query is this long.
+SearchSettingsDialog.MaxResults.Label=Maximum results
+SearchSettingsDialog.MaxResults.Tooltip=Hard cap on results collected for Hop 
GUI search.
+SearchSettingsDialog.MaxMatchesPerFile.Label=Max. matches per text file
+SearchSettingsDialog.MaxMatchesPerFile.Tooltip=Stop after this many matching 
lines in one text file.
+SearchSettingsDialog.MaxTextFileSizeMb.Label=Max. text file size (MB)
+SearchSettingsDialog.MaxTextFileSizeMb.Tooltip=Skip larger text files. Use 
dedicated tools for million-row CSV/JSON.
+SearchSettingsDialog.DebounceMs.Label=Search-as-you-type delay (ms)
+SearchSettingsDialog.DebounceMs.Tooltip=Wait this long after typing before 
running a live search.
+SearchSettingsDialog.IncludeProjectTextFiles.Label=Include project text files 
(CSV, JSON, \u2026)
+SearchSettingsDialog.IncludeProjectTextFiles.Tooltip=When off, only open text 
files are content-searched. Pipelines, workflows and metadata are always 
searched.
+SearchSettingsDialog.SearchAsYouType.Label=Search as you type
+SearchSettingsDialog.SearchAsYouType.Tooltip=When off, press Enter to search. 
Enter always searches immediately either way.
+SearchSettingsDialog.Error.Title=Error
+SearchSettingsDialog.Error.Save.Message=Error saving search settings
+
 HopGuiSearchResultsPanel.Label.Search=Search:
 HopGuiSearchResultsPanel.Option.CaseSensitive=Case sensitive?
 HopGuiSearchResultsPanel.Option.RegEx=Regular expression?
 HopGuiSearchResultsPanel.Refresh.Button.Label=Refresh
 HopGuiSearchResultsPanel.Refresh.Button.Tooltip=Reload the searchable objects 
(open files, project files, metadata and variables)
+HopGuiSearchResultsPanel.Settings.Tooltip=Search settings (limits, large file 
skip, search-as-you-type)
 HopGuiSearchResultsPanel.Open.Button.Label=Open
 HopGuiSearchResultsPanel.Column.Name=Name
 HopGuiSearchResultsPanel.Column.Match=Matched text
@@ -50,5 +74,9 @@ HopGuiSearchResultsPanel.Detail.Location=Location
 HopGuiSearchResultsPanel.Detail.Match=Matching Text
 HopGuiSearchResultsPanel.Detail.Value=Value
 HopGuiSearchResultsPanel.Detail.Description=Description
+HopGuiSearchResultsPanel.Status.Searching=Searching\u2026
+HopGuiSearchResultsPanel.Status.Capped=Results capped (see Search settings)
+HopGuiSearchResultsPanel.Status.MinLength=Type {0}+ characters to search file 
content
+HopGuiSearchResultsPanel.Status.TextFilesExcluded=Project text files excluded 
from content search
 HopGuiSearchResultsPanel.Error.Open.Title=Error
 HopGuiSearchResultsPanel.Error.Open.Message=Error opening {0}
diff --git 
a/ui/src/test/java/org/apache/hop/ui/hopgui/search/HopGuiSearchHelperTest.java 
b/ui/src/test/java/org/apache/hop/ui/hopgui/search/HopGuiSearchHelperTest.java
index c6328f9b95..39bc3be73a 100644
--- 
a/ui/src/test/java/org/apache/hop/ui/hopgui/search/HopGuiSearchHelperTest.java
+++ 
b/ui/src/test/java/org/apache/hop/ui/hopgui/search/HopGuiSearchHelperTest.java
@@ -29,12 +29,18 @@ import java.util.List;
 import java.util.Map;
 import org.apache.hop.core.search.ISearchResult;
 import org.apache.hop.core.search.ISearchable;
+import org.apache.hop.core.search.ISearchableAnalyser;
+import org.apache.hop.core.search.SearchQuery;
 import org.apache.hop.core.search.SearchResult;
 import org.apache.hop.pipeline.PipelineMeta;
 import org.apache.hop.ui.hopgui.file.pipeline.HopPipelineFileType;
 import org.apache.hop.ui.hopgui.search.HopGuiSearchHelper.SearchObjectGroup;
 import org.apache.hop.ui.hopgui.search.HopGuiSearchHelper.SearchSection;
 import org.apache.hop.ui.hopgui.search.HopGuiSearchHelper.SearchTypeGroup;
+import org.apache.hop.ui.hopgui.search.config.SearchAnalysisResult;
+import org.apache.hop.ui.hopgui.search.config.SearchConfig;
+import org.apache.hop.ui.hopgui.search.config.SearchConfigSingleton;
+import org.apache.hop.ui.hopgui.search.config.SearchLimits;
 import org.junit.jupiter.api.Test;
 
 /** Unit tests for the two-tier grouping that both search windows render. */
@@ -222,4 +228,90 @@ class HopGuiSearchHelperTest {
     assertTrue(HopGuiSearchHelper.isNameMatch(nameMatch(open)));
     assertFalse(HopGuiSearchHelper.isNameMatch(fieldMatch(open, "Table input", 
"select 1")));
   }
+
+  @Test
+  void analyseLimitedSkipsTextContentForShortQueryButKeepsNameMatches() {
+    StringBuilder csv = new StringBuilder();
+    for (int i = 0; i < 50; i++) {
+      csv.append("address line ").append(i).append('\n');
+    }
+    ISearchable text = textFile("addresses.csv", "/p/addresses.csv");
+    when(text.getSearchableObject())
+        .thenReturn(new TextFileContent("/p/addresses.csv", csv.toString()));
+    when(text.getName()).thenReturn("addresses.csv");
+
+    Map<Class<ISearchableAnalyser>, ISearchableAnalyser> analysers = 
textFileAnalysers();
+
+    SearchConfig config = new SearchConfig();
+    config.setMinContentQueryLength("3");
+    config.setMaxResults("500");
+    SearchLimits limits = SearchLimits.fromConfig(config);
+
+    SearchAnalysisResult shortQuery =
+        HopGuiSearchHelper.analyseRankedLimited(
+            List.of(text),
+            new SearchQuery("a", false, false),
+            analysers,
+            true,
+            limits,
+            source(text, 1));
+    assertTrue(shortQuery.isContentSearchSkipped());
+    // Name "addresses.csv" still matches "a"
+    assertFalse(shortQuery.getResults().isEmpty());
+    
assertTrue(shortQuery.getResults().stream().allMatch(HopGuiSearchHelper::isNameMatch));
+
+    SearchAnalysisResult longQuery =
+        HopGuiSearchHelper.analyseRankedLimited(
+            List.of(text),
+            new SearchQuery("address", false, false),
+            analysers,
+            true,
+            limits,
+            source(text, 1));
+    assertFalse(longQuery.isContentSearchSkipped());
+    assertTrue(longQuery.getResults().size() > 1);
+  }
+
+  @Test
+  void analyseLimitedRespectsGlobalMaxResults() {
+    StringBuilder csv = new StringBuilder();
+    for (int i = 0; i < 200; i++) {
+      csv.append("address ").append(i).append('\n');
+    }
+    ISearchable text = textFile("data.csv", "/p/data.csv");
+    when(text.getSearchableObject()).thenReturn(new 
TextFileContent("/p/data.csv", csv.toString()));
+
+    Map<Class<ISearchableAnalyser>, ISearchableAnalyser> analysers = 
textFileAnalysers();
+
+    SearchConfig config = new SearchConfig();
+    config.setMinContentQueryLength("1");
+    config.setMaxResults("15");
+    config.setMaxMatchesPerFile("100");
+    SearchLimits limits = SearchLimits.fromConfig(config);
+
+    // Temporarily raise per-file cap via config used by the analyser
+    SearchConfigSingleton.setConfigForTesting(config);
+    try {
+      SearchAnalysisResult analysis =
+          HopGuiSearchHelper.analyseRankedLimited(
+              List.of(text),
+              new SearchQuery("address", false, false),
+              analysers,
+              false,
+              limits,
+              source(text, 1));
+      assertTrue(analysis.isTruncated() || analysis.getResults().size() <= 15);
+      assertTrue(analysis.getResults().size() <= 15);
+    } finally {
+      SearchConfigSingleton.setConfigForTesting(new SearchConfig());
+    }
+  }
+
+  /** Analyser map keys are searchable object classes (not analyser classes). 
*/
+  @SuppressWarnings({"unchecked", "rawtypes"})
+  private static Map<Class<ISearchableAnalyser>, ISearchableAnalyser> 
textFileAnalysers() {
+    Map analysers = new HashMap();
+    analysers.put(TextFileContent.class, new 
TextFileContentSearchableAnalyser());
+    return analysers;
+  }
 }
diff --git 
a/ui/src/test/java/org/apache/hop/ui/hopgui/search/TextFileContentSearchableAnalyserTest.java
 
b/ui/src/test/java/org/apache/hop/ui/hopgui/search/TextFileContentSearchableAnalyserTest.java
index 484bdb8b9d..63b84b67bd 100644
--- 
a/ui/src/test/java/org/apache/hop/ui/hopgui/search/TextFileContentSearchableAnalyserTest.java
+++ 
b/ui/src/test/java/org/apache/hop/ui/hopgui/search/TextFileContentSearchableAnalyserTest.java
@@ -44,6 +44,24 @@ class TextFileContentSearchableAnalyserTest {
     assertTrue(results.get(0).getMatchingString().contains("foo"));
   }
 
+  @Test
+  void capsMatchesPerFileAtConfiguredLimit() {
+    StringBuilder content = new StringBuilder();
+    for (int i = 0; i < 100; i++) {
+      content.append("line with address ").append(i).append('\n');
+    }
+    TextFileContent file = new TextFileContent("/project/data.csv", 
content.toString());
+    ISearchable<TextFileContent> searchable =
+        new HopGuiTextFileSearchable("test", "CSV File", file);
+
+    TextFileContentSearchableAnalyser analyser = new 
TextFileContentSearchableAnalyser();
+    List<ISearchResult> results =
+        analyser.search(searchable, new SearchQuery("address", false, false));
+
+    // Default max matches per file is 20 — must not report all 100 synthetic 
lines.
+    assertEquals(20, results.size());
+  }
+
   @Test
   void emptyContentReturnsNoResults() {
     TextFileContent content = new TextFileContent("/project/empty.txt", "");
diff --git 
a/ui/src/test/java/org/apache/hop/ui/hopgui/search/config/SearchLimitsTest.java 
b/ui/src/test/java/org/apache/hop/ui/hopgui/search/config/SearchLimitsTest.java
new file mode 100644
index 0000000000..69a2c8f2fb
--- /dev/null
+++ 
b/ui/src/test/java/org/apache/hop/ui/hopgui/search/config/SearchLimitsTest.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui.search.config;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+class SearchLimitsTest {
+
+  @Test
+  void defaultsMatchProductSafetyLimits() {
+    SearchLimits limits = SearchLimits.defaults();
+    assertEquals(3, limits.getMinContentQueryLength());
+    assertEquals(500, limits.getMaxResults());
+    assertEquals(20, limits.getMaxMatchesPerFile());
+    assertEquals(1L * 1024 * 1024, limits.getMaxTextFileSizeBytes());
+    assertTrue(limits.isIncludeProjectTextFiles());
+    assertTrue(limits.isSearchAsYouType());
+    assertEquals(300, limits.getDebounceMs());
+  }
+
+  @Test
+  void contentSearchRequiresMinLength() {
+    SearchLimits limits = SearchLimits.defaults();
+    assertFalse(limits.allowsContentSearch("a"));
+    assertFalse(limits.allowsContentSearch("ad"));
+    assertTrue(limits.allowsContentSearch("add"));
+    assertTrue(limits.allowsContentSearch("address"));
+  }
+
+  @Test
+  void customConfigIsParsed() {
+    SearchConfig config = new SearchConfig();
+    config.setMinContentQueryLength("5");
+    config.setMaxResults("100");
+    config.setMaxMatchesPerFile("7");
+    config.setMaxTextFileSizeMb("2");
+    config.setIncludeProjectTextFiles(false);
+    config.setSearchAsYouType(false);
+    config.setDebounceMs("400");
+
+    SearchLimits limits = SearchLimits.fromConfig(config);
+    assertEquals(5, limits.getMinContentQueryLength());
+    assertEquals(100, limits.getMaxResults());
+    assertEquals(7, limits.getMaxMatchesPerFile());
+    assertEquals(2L * 1024 * 1024, limits.getMaxTextFileSizeBytes());
+    assertFalse(limits.isIncludeProjectTextFiles());
+    assertFalse(limits.isSearchAsYouType());
+    assertEquals(400, limits.getDebounceMs());
+    assertFalse(limits.allowsContentSearch("addr"));
+    assertTrue(limits.allowsContentSearch("addre"));
+  }
+}

Reply via email to