diff --git a/USER_GUIDE.md b/USER_GUIDE.md
index 634497ba96..eaf1c6417e 100644
--- a/USER_GUIDE.md
+++ b/USER_GUIDE.md
@@ -408,7 +408,7 @@ be audited whenever the matching help text changes.
| Setting | Current default | Source of truth |
|---|---|---|
-| Query/diff result limit | `20` (`--limit`, alias `--top` for query commands), max `10000` | CLI help and query runners |
+| Query/diff result limit | `20` (`--limit`, aliases `--top` for query commands and `--max-results` for `search`), max `10000` | CLI help and query runners |
| Search snippet lines | `8` (`--snippet-lines`, max `20`) | CLI help and search runner |
| Max line width | `512` (`--max-line-width`, `0` disables) | `LineWidthFormatter.DefaultMaxLineWidth` |
| Index max file size | `4MiB` unless `CDIDX_MAX_FILE_BYTES` is set | index runner help |
@@ -1272,6 +1272,10 @@ Default `cdidx search` is literal-safe unless you explicitly opt into raw FTS5:
| Raw FTS5 mode | With `--fts`, the query is passed to FTS5 as raw syntax. Supported operators include `content:term` column filters, `NEAR(foo bar, 5)`, `foo OR bar`, `foo NOT bar`, parenthesized groups, prefix tokens such as `foo*`, and quoted phrases such as `"foo bar"`. |
| No `--fts` | Operator-like characters are treated as literal query content except for cdidx's documented literal-safe prefix shorthand. |
+For punctuation-heavy code phrases such as `catch { }`, normal search may emit a rerun hint. Use `--exact-substring` when braces, operators, punctuation, and case need byte-for-byte matching.
+
+For whitespace-containing literal queries passed as one argument, such as `cdidx search "not supported"`, normal search still uses FTS token matching but ranks chunks containing the exact phrase ahead of token-only matches.
+
Search case behavior depends on the selected mode:
| Mode or text | Case and token behavior |
@@ -1314,11 +1318,11 @@ same source location.
| `--issue-title
` / `--issue-label ` | `search --format issue-drafts` | Set the title for an ad hoc search draft and add label hints. `--issue-label` can be repeated or comma-separated. |
| `--check` | `status` | Verify that `.cdidx/codeindex.db` exactly matches the current indexable workspace by comparing DB file paths/checksums against a fresh filesystem scan. Matching indexes exit `0`; stale indexes exit `5`. |
| `--dry-run` | `index` | Scan files and report what would change without writing to the database |
-| `--limit ` | Query result commands except `suggestions` | Max results (default: 20, max: 10000; `map` uses it per section) |
+| `--limit ` / `--max-results ` | Query result commands except `suggestions`; `--max-results` is `search` only | Max results (default: 20, max: 10000; `map` uses it per section) |
| `--lang ` | Query commands | Filter by language (case-insensitive; `--lang Python` is treated as `--lang python`). Common aliases such as `c#`, `cs`, `kt`, and `kts` are also accepted. Unknown values emit an `Available: ` hint on zero-result responses in human-readable output. |
| `--visibility ` | `definition`, `symbols`, `unused`, `hotspots` | Include only symbols with the requested visibility values: `public`, `protected`, `internal`, `private`. `public` matches stored exported aliases such as `pub`, `open`, and `export`; `private` also matches `fileprivate`. |
| `--exclude-visibility ` | `definition`, `symbols`, `unused`, `hotspots` | Exclude symbols with the requested visibility values. Accepts the same comma-separated values and alias expansion as `--visibility`. |
-| `--path ` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect`, `validate` | Restrict results to glob-style path patterns. `*` and `?` are wildcards. Repeatable; multiple values are OR'd together |
+| `--path ` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect`, `validate` | Restrict results to glob-style path patterns. `*` and `?` are wildcards. Repeatable; multiple values are OR'd together. Quote shell globs such as `--path 'src/**'` so the shell passes one literal pattern. |
| `--query ` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `inspect`, `impact` | Pass a query literal explicitly, useful when the query starts with `-`. Query commands except `find` also accept `-- ` as a one-token query escape while continuing to parse later options. |
| `--recipe ` | `search` | Run a reusable audit recipe such as `risky-code`. Use `recipe/query` form, such as `risky-code/raw-diagnostic-echo`, to run one child query directly. Recipe runs default to `--audit-scope source`, applying the recipe's production-code path and exclusion metadata before normal search filters and snippet controls; `--limit` / `--top` is per child query. Text, `--json` / `--format json`, `--format compact`, and `--format issue-drafts` are supported, and issue drafts include a replay command. |
| `--include-query ` / `--exclude-query ` | `search --recipe ` | Include or exclude child recipe queries by name. Repeatable and comma-separated; names are listed by `cdidx search --list-recipes`. |
@@ -1372,6 +1376,7 @@ same source location.
| `--reverse` | `deps` | Reverse lookup: show files that depend ON the matched path |
| `--strict-not-found` | Query commands | Return exit code `2` when a valid query produces zero rows. Without this flag, zero-result queries exit `0` and keep their normal empty/zero-result output. |
| `--top ` | Query commands | Alias for `--limit` |
+| `--max-results ` | `search` | Alias for `--limit` |
| `--color ` | All commands | Control ANSI color output. Accepts `auto` (default), `always`, or `never`. Precedence: `--color` flag > `CLICOLOR_FORCE` > `NO_COLOR` > `CLICOLOR=0` > terminal capability auto-detect. Auto mode treats redirected stdout and StringWriter-style test capture as non-ANSI; on Windows it also accepts ConPTY/Windows Terminal virtual-terminal support and terminal hints such as `WT_SESSION`, `WT_PROFILE_ID`, `TERM_PROGRAM`, or non-`dumb` `TERM`. Use `--color=always` to keep colored kind labels through a pager such as `cdidx symbols Foo \| less -R`; use `--color=never` (or `NO_COLOR=1`) to suppress ANSI even on a TTY. |
| `--palette ` | All commands | Choose the ANSI palette used when color output is enabled. Accepts `basic` (8-color SGR 30–37, the default fallback for minimal SSH/CI terminals), `256` (256-color `\x1b[38;5;Nm`), or `truecolor` (24-bit RGB `\x1b[38;2;R;G;Bm`). Precedence: `--palette` flag > `CDIDX_COLOR_PALETTE` env var > `COLORTERM` / `TERM` auto-detect. The basic palette avoids `\x1b[90m` (bright-black / dim), which is unreadable on many minimal terminals. |
| `--metrics ` | All commands (and MCP tool calls) | Append one JSONL metrics record per CLI command / MCP tool call to ``. The `CDIDX_METRICS=` environment variable provides the same destination as a fallback when the flag is not passed. Best-effort: any IO failure (missing directory, read-only mount, etc.) is swallowed silently and never breaks the underlying command. |
@@ -2828,7 +2833,7 @@ render できます。
| 設定 | 現在の既定値 | Source of truth |
|---|---|---|
-| Query/diff result limit | `20`(`--limit`、query command では alias `--top`)、最大 `10000` | CLI help と query runners |
+| Query/diff result limit | `20`(`--limit`、query command では alias `--top`、`search` では `--max-results`)、最大 `10000` | CLI help と query runners |
| Search snippet lines | `8`(`--snippet-lines`、最大 `20`) | CLI help と search runner |
| Max line width | `512`(`--max-line-width`、`0` で無効) | `LineWidthFormatter.DefaultMaxLineWidth` |
| Index max file size | `CDIDX_MAX_FILE_BYTES` 未設定時は `4MiB` | index runner help |
@@ -3712,6 +3717,10 @@ cdidx report --output report.tgz --json
| raw FTS5 mode | `--fts` 付きでは query を raw FTS5 構文としてそのまま渡します。利用できる演算子には `content:term` の列 filter、`NEAR(foo bar, 5)`、`foo OR bar`、`foo NOT bar`、括弧 grouping、`foo*` のような prefix token、`"foo bar"` のような quoted phrase があります。 |
| `--fts` なし | cdidx が明示している literal-safe prefix shorthand を除き、operator に見える文字も literal な query 内容として扱います。 |
+`catch { }` のように記号の多いコード片では、通常検索が再実行ヒントを出す場合があります。brace、operator、punctuation、大文字小文字まで byte-for-byte に一致させたい場合は `--exact-substring` を使います。
+
+`cdidx search "not supported"` のように空白を含む literal query を 1 引数で渡した場合、通常検索は引き続き FTS token matching を使いますが、exact phrase を含む chunk を token-only match より前に並べます。
+
検索の大小文字の扱いは mode ごとに異なります:
| mode / text | 大小文字と token の扱い |
@@ -3753,10 +3762,10 @@ raw match density を正確に測る、といった理由で全 raw chunk hit
| `--issue-title ` / `--issue-label ` | `search --format issue-drafts` | ad hoc search draft の title を指定し、label hint を追加します。`--issue-label` は繰り返し指定またはカンマ区切りに対応します。 |
| `--check` | `status` | DB のファイル path/checksum と現在の index 対象 workspace を比較し、`.cdidx/codeindex.db` が完全一致するか確認。完全一致なら終了コード `0`、stale なら `5` |
| `--dry-run` | `index` | DB に書き込まず、どの変更が発生するかだけを走査して報告 |
-| `--limit ` | `suggestions` 以外のクエリ結果コマンド | 最大結果数(デフォルト: 20、最大: 10000。`map` では各セクションごとの件数) |
+| `--limit ` / `--max-results ` | `suggestions` 以外のクエリ結果コマンド。`--max-results` は `search` のみ | 最大結果数(デフォルト: 20、最大: 10000。`map` では各セクションごとの件数) |
| `--visibility ` | `definition`, `symbols`, `unused`, `hotspots` | `public`, `protected`, `internal`, `private` の可視性でシンボルを絞り込む。`public` は `pub`、`open`、`export` などの保存済み exported alias にも一致し、`private` は `fileprivate` にも一致 |
| `--exclude-visibility ` | `definition`, `symbols`, `unused`, `hotspots` | 指定した可視性のシンボルを除外する。値と alias 展開は `--visibility` と同じ |
-| `--path ` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect`, `validate` | glob 形式のパスパターンで結果を絞る。`*` と `?` がワイルドカード。繰り返し指定可(複数値は OR で結合) |
+| `--path ` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `map`, `inspect`, `validate` | glob 形式のパスパターンで結果を絞る。`*` と `?` がワイルドカード。繰り返し指定可(複数値は OR で結合)。`--path 'src/**'` のように shell glob を引用し、shell が 1 つの literal pattern として渡すようにする。 |
| `--query ` | `search`, `definition`, `references`, `callers`, `callees`, `symbols`, `files`, `find`, `inspect`, `impact` | クエリを明示的なリテラルとして渡す。クエリが `-` で始まる場合に有用。`find` 以外のクエリ系コマンドでは `-- ` も1トークンのクエリエスケープとして受け付け、その後のオプション解析を続ける。 |
| `--recipe ` | `search` | `risky-code` などの再利用可能な audit recipe を実行する。`risky-code/raw-diagnostic-echo` のような `recipe/query` 形式で child query を1つだけ直接実行できる。Recipe 実行は既定で `--audit-scope source` になり、recipe の本番コード向け path / exclusion metadata を適用したうえで、通常の search filter と snippet control を選択された各 query に適用する。`--limit` / `--top` は child query ごとの上限になる。text、`--json` / `--format json`、`--format compact`、`--format issue-drafts` に対応し、issue draft には再実行コマンドを含める。 |
| `--include-query ` / `--exclude-query ` | `search --recipe ` | recipe 内の child query を名前で含める、または除外する。繰り返し指定とカンマ区切りに対応し、名前は `cdidx search --list-recipes` で確認できる。 |
@@ -3808,6 +3817,7 @@ raw match density を正確に測る、といった理由で全 raw chunk hit
| `--reverse` | `deps` | 逆引き: 指定パスに依存しているファイルを表示 |
| `--workspace-db ` | `deps` | file dependency query に別の CodeIndex DB を追加する。最大 7 個の distinct な追加 DB(`--db` を含め合計 8 個)まで繰り返し指定でき、JSON edge には同じ相対パスを区別できるよう `source_db` / `target_db` が含まれる。 |
| `--top ` | クエリ系 | `--limit` のエイリアス |
+| `--max-results ` | `search` | `--limit` のエイリアス |
| `--color ` | 全コマンド | ANSI カラー出力の制御。`auto`(既定)、`always`、`never` を受け付ける。優先順位: `--color` フラグ > `CLICOLOR_FORCE` > `NO_COLOR` > `CLICOLOR=0` > 端末能力の自動判定。auto では redirected stdout と StringWriter 風のテスト capture を非 ANSI とみなし、Windows では ConPTY / Windows Terminal の virtual-terminal 対応と `WT_SESSION`、`WT_PROFILE_ID`、`TERM_PROGRAM`、非 `dumb` の `TERM` などの端末ヒントも見る。`cdidx symbols Foo \| less -R` のような pager pipe でも色を維持したい場合は `--color=always`、TTY 上でも ANSI を抑止したい場合は `--color=never`(または `NO_COLOR=1`)を指定する。 |
| `--palette ` | 全コマンド | カラー出力が有効なときに用いる ANSI パレットを選択する。`basic`(標準8色 SGR 30–37、最小 SSH/CI 端末向けの既定フォールバック)、`256`(256色 `\x1b[38;5;Nm`)、`truecolor`(24ビット RGB `\x1b[38;2;R;G;Bm`)を受け付ける。優先順位: `--palette` フラグ > `CDIDX_COLOR_PALETTE` 環境変数 > `COLORTERM` / `TERM` 自動判定。`basic` パレットは最小端末で読みにくい `\x1b[90m`(暗灰 / dim)を避ける。 |
| `--metrics ` | 全コマンド(および MCP ツール呼び出し) | CLI コマンド / MCP ツール呼び出し 1 回ごとに JSONL レコードを 1 行ずつ `` に追記する。フラグ未指定時のフォールバックとして `CDIDX_METRICS=` 環境変数でも同じ出力先を指定できる。Best-effort のため、ディレクトリが無い・read-only マウント等の IO 失敗は黙って握り潰し、本体コマンドを壊さない。 |
diff --git a/changelog.d/unreleased/3386.fixed.md b/changelog.d/unreleased/3386.fixed.md
new file mode 100644
index 0000000000..01fdfd200e
--- /dev/null
+++ b/changelog.d/unreleased/3386.fixed.md
@@ -0,0 +1,18 @@
+---
+category: fixed
+issues:
+ - 3386
+affected:
+ - src/CodeIndex/Cli/SearchQueryAdvisor.cs
+ - tests/CodeIndex.Tests/QueryCommandRunnerSearchHintTests.cs
+ - tests/CodeIndex.Tests/McpServerTests.cs
+ - USER_GUIDE.md
+---
+
+## English
+
+- **Punctuation-heavy search hints now include a concrete exact-substring rerun path (#3386)** — CLI and MCP search recovery hints now tell users to rerun punctuation-heavy code phrases with `--exact-substring` or `exactSubstring=true`.
+
+## 日本語
+
+- **記号の多い検索ヒントが exact-substring の再実行方法を具体的に案内するようになりました (#3386)** — CLI と MCP の search recovery hint は、記号の多いコード片を `--exact-substring` または `exactSubstring=true` で再実行するよう案内します。
diff --git a/changelog.d/unreleased/3389.fixed.md b/changelog.d/unreleased/3389.fixed.md
new file mode 100644
index 0000000000..2d83bbc017
--- /dev/null
+++ b/changelog.d/unreleased/3389.fixed.md
@@ -0,0 +1,17 @@
+---
+category: fixed
+issues:
+ - 3389
+affected:
+ - src/CodeIndex/Database/DbSearchReader.cs
+ - tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs
+ - USER_GUIDE.md
+---
+
+## English
+
+- **Multiword literal searches rank exact phrase chunks first (#3389)** — normal `search` now boosts chunks containing the exact whitespace phrase, such as `not supported`, ahead of token-only matches.
+
+## 日本語
+
+- **複数語の literal search が exact phrase を含む chunk を先に並べるようになりました (#3389)** — 通常の `search` は `not supported` のような空白を含む exact phrase を持つ chunk を、token-only match より前に並べます。
diff --git a/changelog.d/unreleased/3445.fixed.md b/changelog.d/unreleased/3445.fixed.md
new file mode 100644
index 0000000000..c6d5676d8a
--- /dev/null
+++ b/changelog.d/unreleased/3445.fixed.md
@@ -0,0 +1,17 @@
+---
+category: fixed
+issues:
+ - 3445
+affected:
+ - src/CodeIndex/Cli/QueryCommandRunner.cs
+ - tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs
+ - USER_GUIDE.md
+---
+
+## English
+
+- **Search explains expanded `--path` globs (#3445)** — when a shell-expanded path glob leaves extra path-like positional arguments, `cdidx search` now suggests quoting `--path` so one literal glob reaches the CLI.
+
+## 日本語
+
+- **`search` が展開済み `--path` glob の対処を案内するようになりました (#3445)** — shell 展開された path glob が余剰の path 風 positional argument として残った場合、`cdidx search` は `--path` を引用して 1 つの literal glob として渡すよう案内します。
diff --git a/changelog.d/unreleased/3521.fixed.md b/changelog.d/unreleased/3521.fixed.md
new file mode 100644
index 0000000000..3c2e2062e0
--- /dev/null
+++ b/changelog.d/unreleased/3521.fixed.md
@@ -0,0 +1,19 @@
+---
+category: fixed
+issues:
+ - 3521
+affected:
+ - src/CodeIndex/Cli/QueryCommandRunner.cs
+ - src/CodeIndex/Cli/CliFlagSchema.cs
+ - src/CodeIndex/Cli/ConsoleUi.cs
+ - tests/CodeIndex.Tests/QueryCommandRunnerSearchTests.cs
+ - USER_GUIDE.md
+---
+
+## English
+
+- **Search accepts `--max-results` and guides dash-prefixed queries (#3521)** — `cdidx search` now treats `--max-results` as a `--limit` alias and tells users how to pass a literal query such as `--profile` through `--query` or `--`.
+
+## 日本語
+
+- **`search` が `--max-results` と dash 始まりの query を案内するようになりました (#3521)** — `cdidx search` は `--max-results` を `--limit` のエイリアスとして扱い、`--profile` のような literal query を `--query` または `--` で渡す方法を案内します。
diff --git a/src/CodeIndex/Cli/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs
index d3172c3a37..02f5bb5ca3 100644
--- a/src/CodeIndex/Cli/CliFlagSchema.cs
+++ b/src/CodeIndex/Cli/CliFlagSchema.cs
@@ -244,6 +244,7 @@ private static IReadOnlyList BuildAll()
new() { Name = "--slow-query-ms", ValuePlaceholder = "", Description = "Log profiled SQL statements at or above this millisecond threshold", Commands = Set(ProfileCommands) },
new() { Name = "--trace", ValuePlaceholder = "", Description = "Emit one structured JSON query trace line to stderr or a daily log file", Commands = Set(TraceCommands) },
new() { Name = "--limit", ValuePlaceholder = "", Description = "Max results", Commands = Set(LimitCapableCommands.Concat(new[] { "suggestions" }).ToArray()) },
+ new() { Name = "--max-results", ValuePlaceholder = "", Description = "Search alias for --limit", Commands = Set("search") },
new() { Name = "--top", ValuePlaceholder = "", Description = "Max results", Commands = Set(LimitCapableCommands) },
new() { Name = "--offset", ValuePlaceholder = "", Description = "Suggestions: skip this many filtered rows before output", Commands = Set("suggestions") },
new() { Name = "--lang", ValuePlaceholder = "", Description = "Filter by language", Commands = Set(LangCapableCommands) },
diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs
index b201f99386..ab6b88f794 100644
--- a/src/CodeIndex/Cli/ConsoleUi.cs
+++ b/src/CodeIndex/Cli/ConsoleUi.cs
@@ -87,7 +87,7 @@ private static readonly (string Command, string Usage)[] CommandUsageLines =
("index-commits", "cdidx index --commits [commit-ref ...] [--db ] [--verbose] [--dry-run] [--json] [--memory-trace] [--duration-format ] [--max-file-bytes ] [--include-symbol-kind [,]] [--exclude-symbol-kind [,]]"),
("index-changed-between", "cdidx index --changed-between [--db ] [--verbose] [--dry-run] [--json] [--memory-trace] [--duration-format ] [--max-file-bytes ] [--include-symbol-kind [,]] [--exclude-symbol-kind [,]]"),
("index-files", "cdidx index --files [path ...] [--db ] [--verbose] [--dry-run] [--json] [--memory-trace] [--duration-format ] [--max-file-bytes ] [--include-symbol-kind [,]] [--exclude-symbol-kind [,]]"),
- ("search", "cdidx search |--query |-- |--recipe |--list-recipes|--named-query = [--named-query = ...] [--include-query ] [--exclude-query ] [--cursor ] [--audit-scope ] [--db ] [--json[=ndjson|array]] [--pretty] [--format ] [--open-issues ] [--repo ] [--issue-title ] [--issue-label ] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--exclude-comments] [--exclude-strings] [--exclude-fixtures] [--snippet-lines ] [--snippet-focus ] [--max-line-width ] [--fts] [--exact|--exact-substring] [--prefix] [--count] [--group-by ] [--since ] [--no-dedup] [--no-visibility-rank] [--require-before ] [--require-after ] [--reject-before ] [--reject-after ] [--guard-window ]"),
+ ("search", "cdidx search |--query |-- |--recipe |--list-recipes|--named-query = [--named-query = ...] [--include-query ] [--exclude-query ] [--cursor ] [--audit-scope ] [--db ] [--json[=ndjson|array]] [--pretty] [--format ] [--open-issues ] [--repo ] [--issue-title ] [--issue-label ] [--verbose] [--limit |--top |--max-results ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--exclude-comments] [--exclude-strings] [--exclude-fixtures] [--snippet-lines ] [--snippet-focus ] [--max-line-width ] [--fts] [--exact|--exact-substring] [--prefix] [--count] [--group-by ] [--since ] [--no-dedup] [--no-visibility-rank] [--require-before ] [--require-after ] [--reject-before ] [--reject-after ] [--guard-window ]"),
("definition", "cdidx definition |--query |-- [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--visibility ] [--exclude-visibility ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--exact|--exact-name] [--count] [--since ]"),
("goto", "cdidx goto |--query |-- [--db ] [--json] [--limit |--top ] [--lang ] [--kind ] [--path ] [--exclude-path ] [--exclude-tests] [--exact|--exact-name] [--all]"),
("references", "cdidx references |--query |-- [--db ] [--json] [--format ] [--verbose] [--limit |--top ] [--lang ] [--kind ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--snippet-lines ] [--max-line-width ] [--exact|--exact-name] [--count]"),
@@ -1028,7 +1028,8 @@ private static void PrintFlagReference(Action WriteHelpLine)
WriteHelpLine(" --quiet, -q, --silent Query commands: suppress informational stderr output, including zero-result hints and summaries; errors still print. Overrides --verbose stderr text.");
WriteHelpLine(" --profile Read commands: append SQL timing, row-count, and EXPLAIN QUERY PLAN JSON after the normal result");
WriteHelpLine(" --slow-query-ms Read commands: log profiled SQL statements that take at least ms (use 0 to log every statement)");
- Console.WriteLine(" --limit , --top Max results to return (default: 20)");
+ Console.WriteLine(" --limit , --top , --max-results ");
+ Console.WriteLine(" Max results to return (default: 20)");
Console.WriteLine(" --lang Filter by language (aliases: bat, cmd, cshtml, razor, ts, tsx, cts, mts)");
Console.WriteLine(" --path Restrict matches to glob-style path patterns (* and ?)");
WriteHelpLine($" --query Pass a query literal, useful when the query starts with '-' (`search`/`find` max {QueryLimits.MaxQueryLength} chars)");
diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs
index d0ce9782a3..579366165d 100644
--- a/src/CodeIndex/Cli/QueryCommandRunner.cs
+++ b/src/CodeIndex/Cli/QueryCommandRunner.cs
@@ -139,6 +139,7 @@ public static class QueryCommandRunner
"--db",
"--data-dir",
"--limit",
+ "--max-results",
"--top",
"--lang",
"--kind",
@@ -793,7 +794,7 @@ public static int RunSearch(string[] cmdArgs, JsonSerializerOptions jsonOptions)
WriteUsageError(
"search requires a query argument",
GetUsageLineOrThrow("search"),
- "Add the text you want to search for after the command, for example: `cdidx search authenticate`.");
+ BuildMissingSearchQueryHint(cmdArgs));
return CommandExitCodes.UsageError;
}
if (options.Query.Length > QueryLimits.MaxQueryLength)
@@ -7830,10 +7831,12 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue)
}
break;
case "--limit":
+ case "--max-results":
case "--top":
- if (!TryReadRawOptionValue(args, ref i, "--limit", inlineValue, out var limitValue, out var missingLimitError))
+ var limitOptionName = normalizedArg == "--top" ? "--limit" : normalizedArg;
+ if (!TryReadRawOptionValue(args, ref i, limitOptionName, inlineValue, out var limitValue, out var missingLimitError))
AddParseError(missingLimitError!);
- else if (TryParsePositiveInt(limitValue!, "--limit", out var parsedLimit, out var limitError))
+ else if (TryParsePositiveInt(limitValue!, limitOptionName, out var parsedLimit, out var limitError))
{
WarnIfDuplicateSingleValueOption("--limit", limitValue!);
limit = parsedLimit;
@@ -9870,11 +9873,26 @@ private static bool TryWriteUnexpectedExtraPositionals(string commandName, Query
CommandErrorWriter.Write(
$"unexpected extra positional {ConsoleUi.Counted(options.ExtraNames.Count, "argument")} for {commandName}: {string.Join(", ", options.ExtraNames.Select(name => $"`{name}`"))}.",
- "quote multi-word queries as a single argument, or remove the extra positional values.",
+ BuildUnexpectedExtraPositionalsHint(commandName, options),
GetUsageLineOrThrow(commandName));
return true;
}
+ private static string BuildUnexpectedExtraPositionalsHint(string commandName, QueryCommandOptions options)
+ {
+ if (string.Equals(commandName, "search", StringComparison.Ordinal)
+ && options.PathPatterns.Count > 0
+ && options.ExtraNames.Any(IsPathLikeArgument))
+ {
+ return "quote --path globs so the shell passes one literal pattern, e.g. `--path 'src/CodeIndex/**'`; remove the expanded path arguments and rerun.";
+ }
+
+ return "quote multi-word queries as a single argument, or remove the extra positional values.";
+ }
+
+ private static bool IsPathLikeArgument(string value) =>
+ value.Contains('/') || value.Contains('\\');
+
private static bool TryWriteUnexpectedPositionals(string commandName, QueryCommandOptions options)
{
var unexpected = new List();
@@ -9891,6 +9909,69 @@ private static bool TryWriteUnexpectedPositionals(string commandName, QueryComma
return true;
}
+ private static string BuildMissingSearchQueryHint(string[] cmdArgs)
+ {
+ var candidate = FindOptionLookingSearchLiteralCandidate(cmdArgs);
+ if (candidate != null)
+ {
+ var display = ConsoleUi.FormatBoundedValue(candidate);
+ return $"Add the text you want to search for after the command. If you meant to search for `{display}`, pass it as `--query \"{display}\"` or after `--`, for example: `cdidx search -- \"{display}\"`.";
+ }
+
+ return "Add the text you want to search for after the command, for example: `cdidx search authenticate`. If the query itself starts with `--`, pass it as `--query \"--profile\"` or after `--`, for example: `cdidx search -- \"--profile\"`.";
+ }
+
+ private static string? FindOptionLookingSearchLiteralCandidate(string[] cmdArgs)
+ {
+ for (var i = 0; i < cmdArgs.Length; i++)
+ {
+ var arg = cmdArgs[i];
+ if (arg == "--")
+ return i + 1 < cmdArgs.Length && cmdArgs[i + 1].StartsWith("-", StringComparison.Ordinal)
+ ? cmdArgs[i + 1]
+ : null;
+
+ var inlineValue = TrySplitInlineOptionValue(arg, out var inlineOptionName)
+ ? arg[(inlineOptionName!.Length + 1)..]
+ : null;
+ var normalizedArg = inlineOptionName ?? arg;
+ if (ValueTakingOptions.Contains(normalizedArg))
+ {
+ if (inlineValue == null)
+ i++;
+ continue;
+ }
+
+ if (!arg.StartsWith("--", StringComparison.Ordinal))
+ continue;
+ if (SearchMissingQueryControlFlags.Contains(normalizedArg))
+ continue;
+
+ return arg;
+ }
+
+ return null;
+ }
+
+ private static readonly HashSet SearchMissingQueryControlFlags =
+ [
+ "--exact",
+ "--exact-name",
+ "--exact-substring",
+ "--prefix",
+ "--fts",
+ "--json",
+ "--pretty",
+ "--count",
+ "--no-dedup",
+ "--no-visibility-rank",
+ "--exclude-tests",
+ "--strict-not-found",
+ "--verbose",
+ "--quiet",
+ "--silent",
+ ];
+
private static string GetUsageLineOrThrow(string commandName) =>
ConsoleUi.GetUsageLine(commandName)
?? throw new InvalidOperationException($"Missing usage line for command '{commandName}'.");
@@ -11535,6 +11616,7 @@ private static void WriteSqlGraphContractWarningIfNeeded(bool json, SqlGraphCont
new Dictionary(StringComparer.Ordinal)
{
["--limit"] = 10_000,
+ ["--max-results"] = 10_000,
["--snippet-lines"] = SearchSnippetFormatter.MaxSnippetLines,
["--max-line-width"] = LineWidthFormatter.MaxAllowedLineWidth,
["--slow-query-ms"] = 3_600_000,
diff --git a/src/CodeIndex/Cli/SearchQueryAdvisor.cs b/src/CodeIndex/Cli/SearchQueryAdvisor.cs
index 99fe2df2c5..e480bcdf26 100644
--- a/src/CodeIndex/Cli/SearchQueryAdvisor.cs
+++ b/src/CodeIndex/Cli/SearchQueryAdvisor.cs
@@ -3,8 +3,10 @@ namespace CodeIndex.Cli;
internal static class SearchQueryAdvisor
{
internal const string ExactSubstringHintReason = "punctuation_heavy_query";
- internal const string CliExactSubstringSuggestedAction = "This looks like a literal code phrase; try --exact-substring for punctuation-sensitive matching.";
- internal const string McpExactSubstringSuggestedAction = "This looks like a literal code phrase; try exactSubstring for punctuation-sensitive matching.";
+ internal const string CliExactSubstringSuggestedAction =
+ "This looks like a literal code phrase; rerun with `--exact-substring`, for example: `cdidx search --exact-substring --query \"...\"`, for punctuation-sensitive matching.";
+ internal const string McpExactSubstringSuggestedAction =
+ "This looks like a literal code phrase; rerun the search with exactSubstring=true for punctuation-sensitive matching.";
internal static SearchQueryHint? BuildExactSubstringHint(string? query, bool rawQuery, bool exact, bool prefix)
=> ShouldSuggestExactSubstring(query, rawQuery, exact, prefix)
diff --git a/src/CodeIndex/Database/DbSearchReader.cs b/src/CodeIndex/Database/DbSearchReader.cs
index 76f555b9f1..df6548201f 100644
--- a/src/CodeIndex/Database/DbSearchReader.cs
+++ b/src/CodeIndex/Database/DbSearchReader.cs
@@ -123,7 +123,7 @@ public List Search(string query, int limit = 20, string? lang = nu
var coverageTokens = exact ? new List() : GetSearchCoverageTokens(normalizedQuery, rawQuery);
var hasGuardFilters = guardFilters is { Count: > 0 };
var searchMatchLineContext = SearchMatchLineContext.Create(query, lang, exact);
- var exactSubstringBoost = !exact && !rawQuery && IsPunctuationHeavyLiteralQuery(query);
+ var exactLiteralBoost = !exact && !rawQuery && ShouldBoostExactLiteralSearch(query);
var guardedRequestedLimit = Math.Max(0, guardRequestedLimit ?? limit);
var guardedCandidateLimit = hasGuardFilters ? GetGuardedSearchCandidateLimit(guardedRequestedLimit, cursor) : 0;
using var cmd = _conn.CreateCommand();
@@ -166,7 +166,7 @@ FROM fts_chunks
if (since != null && _fileColumns.Contains("modified"))
sql += " AND f.modified >= @since";
AppendPathFilters(ref sql, pathPatterns, excludePathPatterns, excludeTests);
- sql += $" ORDER BY {GetSearchOrderSql(coverageTokens.Count, exactSubstringBoost)}";
+ sql += $" ORDER BY {GetSearchOrderSql(coverageTokens.Count, exactLiteralBoost)}";
if (hasGuardFilters)
sql += " LIMIT @candidateFetchLimit";
else
@@ -460,7 +460,7 @@ FROM fts_chunks
sql += " AND f.modified >= @since";
AppendPathFilters(ref sql, pathPatterns, excludePathPatterns, excludeTests);
- sql += $" ORDER BY {GetSearchOrderSql(coverageTokens.Count, exactSubstringBoost: false)}";
+ sql += $" ORDER BY {GetSearchOrderSql(coverageTokens.Count, exactLiteralBoost: false)}";
cmd.CommandText = sql;
if (exact)
@@ -1365,13 +1365,22 @@ private static bool OverlapsOrTouches((int Start, int End) interval, int start,
}
}
- private static string GetSearchOrderSql(int coverageTokenCount, bool exactSubstringBoost)
+ private static string GetSearchOrderSql(int coverageTokenCount, bool exactLiteralBoost)
{
var coverageOrder = GetSearchCoverageOrderSql(coverageTokenCount);
- var exactSubstringOrder = exactSubstringBoost
+ var exactLiteralOrder = exactLiteralBoost
? $"CASE WHEN instr({GetExactSearchTextSql("c.content", "f.lang")}, {GetExactSearchTextSql("@rankingQuery", "f.lang")}) > 0 THEN 0 ELSE 1 END, "
: string.Empty;
- return $"{PathBucketOrder}, {exactSubstringOrder}{ExactSymbolMatchOrder}, {PrefixSymbolMatchOrder}, {SearchVisibilityOrder}, {PathTextMatchOrder}, {ChunkTextMatchOrder}, {ChunkStructuredFieldOrder}, {ChunkSymbolKindOrder}, {ChunkSymbolDepthOrder}, {coverageOrder}rank, f.modified DESC, f.path, c.id ASC";
+ return $"{PathBucketOrder}, {exactLiteralOrder}{ExactSymbolMatchOrder}, {PrefixSymbolMatchOrder}, {SearchVisibilityOrder}, {PathTextMatchOrder}, {ChunkTextMatchOrder}, {ChunkStructuredFieldOrder}, {ChunkSymbolKindOrder}, {ChunkSymbolDepthOrder}, {coverageOrder}rank, f.modified DESC, f.path, c.id ASC";
+ }
+
+ private static bool ShouldBoostExactLiteralSearch(string query)
+ {
+ var trimmed = query.Trim();
+ if (trimmed.Length == 0 || !trimmed.Any(char.IsLetterOrDigit))
+ return false;
+
+ return IsPunctuationHeavyLiteralQuery(trimmed) || IsMultiwordLiteralPhrase(trimmed);
}
private static bool IsPunctuationHeavyLiteralQuery(string query)
@@ -1385,6 +1394,12 @@ private static bool IsPunctuationHeavyLiteralQuery(string query)
return punctuationCount >= 2 || tokens.Any(IsStandaloneCodeOperatorToken);
}
+ private static bool IsMultiwordLiteralPhrase(string query)
+ {
+ var tokens = query.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
+ return tokens.Length > 1 && tokens.All(token => token.Any(char.IsLetterOrDigit));
+ }
+
private static bool IsStandaloneCodeOperatorToken(string token)
=> token.Length > 0
&& token.All(ch => !char.IsLetterOrDigit(ch) && !char.IsWhiteSpace(ch) && ch != '_')
diff --git a/tests/CodeIndex.Tests/ConsoleUiTests.cs b/tests/CodeIndex.Tests/ConsoleUiTests.cs
index ae0ad16c21..2d9a2e141b 100644
--- a/tests/CodeIndex.Tests/ConsoleUiTests.cs
+++ b/tests/CodeIndex.Tests/ConsoleUiTests.cs
@@ -115,7 +115,7 @@ public void PrintUsage_WithoutBanner_HidesAsciiArtAndEasterEggFlags()
Assert.Contains("cdidx references |--query |-- ", output);
Assert.Contains("cdidx callers |--query |-- ", output);
Assert.Contains("cdidx callees |--query |-- ", output);
- Assert.Contains("cdidx search |--query |-- |--recipe |--list-recipes|--named-query = [--named-query = ...] [--include-query ] [--exclude-query ] [--cursor ] [--audit-scope ] [--db ] [--json[=ndjson|array]] [--pretty] [--format ] [--open-issues ] [--repo ] [--issue-title ] [--issue-label ] [--verbose] [--limit |--top ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--exclude-comments] [--exclude-strings] [--exclude-fixtures] [--snippet-lines ] [--snippet-focus ] [--max-line-width ] [--fts] [--exact|--exact-substring] [--prefix] [--count] [--group-by ] [--since ] [--no-dedup] [--no-visibility-rank] [--require-before ] [--require-after ] [--reject-before ] [--reject-after ] [--guard-window ]", output);
+ Assert.Contains("cdidx search |--query |-- |--recipe |--list-recipes|--named-query = [--named-query = ...] [--include-query