Skip to content

feat: add TEXT NGRAM indexes, Grep index routing, and max-count - #55136

Open
junjiejiangjjj wants to merge 1 commit into
milvus-io:masterfrom
junjiejiangjjj:grep-ngram
Open

junjiejiangjjj wants to merge 1 commit into
milvus-io:masterfrom
junjiejiangjjj:grep-ngram

Conversation

@junjiejiangjjj

@junjiejiangjjj junjiejiangjjj commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

This PR adds TEXT NGRAM index support, NGRAM-backed Grep routing for sealed VARCHAR and TEXT fields, and the user-facing -m/--max-count Grep option.

Explicit NGRAM indexes are accepted on TEXT fields, including when scalar autoindex is enabled, while preserving the explicit NGRAM index type. The change includes parameter validation, the existing index persistence/build/load path, native TEXT/LOB tests, and reload/restart verification. TEXT AUTOINDEX and other user-created scalar index types remain unsupported.

Grep routing is implemented in this PR and is enabled automatically for eligible sealed VARCHAR and TEXT fields with a whole-field NGRAM index, including existing VARCHAR NGRAM fields. Rust extracts necessary literal conditions from regexes, transfers the condition tree through Protobuf and the C API, and C++ uses INgramReader::ConditionTreeCandidates to obtain candidate offsets. The exact Rust matcher verifies original field values and produces match details. The required invariant is that every actual match remains an index candidate.

Routing works as follows:

  • Eligible conditions normally use the NGRAM index directly.
  • Conditions rooted at OR with at least 32 literal leaves and a local execution limit at most 128 first use Probe: read and exactly match a bounded input prefix, return if the limit is satisfied, or choose continued scanning versus index lookup for the unread suffix.
  • Probe uses an internal fixed match-ratio threshold of 0.05. It is not exposed as a runtime configuration.
  • Growing segments, missing eligible indexes, and conditions unsuitable for pruning use raw scanning.

The included performance results compare Probe with direct Index routing and document workload-dependent benefits and regressions. They do not establish a universal Probe speedup or an optimal general-purpose threshold.

The new -m/--max-count option caps matching lines independently for each queried field value. It is distinct from the Query record limit and is carried through the Go parser, native boundary, and Rust matcher. The last occurrence wins; zero returns no matches after pattern validation and compilation.

Design documents:

issue: #53020

@sre-ci-robot

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
To complete the pull request process, please assign weiliu1031 after the PR has been reviewed.
You can assign the PR to them by writing /assign @weiliu1031 in a comment when ready.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@mergify mergify Bot added dco-passed DCO check passed. kind/feature Issues related to feature request from users labels Nov 4, 2026
@sre-ci-robot

Copy link
Copy Markdown
Contributor

[ci-v2-notice]
Notice: New ci-v2 system is enabled for this PR.

To rerun ci-v2 checks, comment with:

  • /ci-rerun-code-check // for ci-v2/code-check
  • /ci-rerun-code-check-macos // for Code Checker MacOS (GitHub Actions)
  • /ci-rerun-build // for ci-v2/build
  • /ci-rerun-build-all // for ci-v2/build-all (multi-arch builds)
  • /ci-rerun-buildenv // for ci-v2/build-env (build milvus-env builder images; update .env after the new tag is ready)
  • /ci-rerun-ut-integration // for ci-v2/ut-integration, will rerun ci-v2/build
  • /ci-rerun-ut-go // for ci-v2/ut-go, will rerun ci-v2/build
  • /ci-rerun-ut-cpp // for ci-v2/ut-cpp
  • /ci-rerun-ut // for all ci-v2/ut-integration, ci-v2/ut-go, ci-v2/ut-cpp, will rerun ci-v2/build
  • /ci-rerun-e2e-default // for ci-v2/e2e-default
  • /ci-rerun-e2e-pulsar-wp // for ci-v2/e2e-pulsar-wp (GCP branch-equivalent Pulsar + Woodpecker Python E2E)
  • /ci-rerun-e2e-amd // for ci-v2/e2e-amd (e2e pool dispatcher)
  • /ci-rerun-e2e-dist-wp // for ci-v2/e2e-dist-wp (Tencent distributed woodpecker-service boundary)
  • /ci-rerun-build-ut-cov // for ci-v2/build-ut-cov (build + unit tests in one pipeline)
  • /ci-rerun-build-ut-cov-tcus // force TC-US build-ut-cov; respects Portal limit, never falls back to AWS
  • /ci-rerun-gosdk // for ci-v2/go-sdk (Go SDK E2E tests, ARM)
  • /ci-rerun-gosdk-std // for ci-v2/go-sdk-std (Go SDK E2E, standalone)
  • /ci-rerun-gosdk-dist-wp // for ci-v2/go-sdk-dist-wp (Go SDK E2E, distributed + Woodpecker service)

If you have any questions or requests, please contact @zhikunyao.

@liliu-z liliu-z left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TEXT admission part looks fine to me. My main concern is scope. The description says Grep index routing is not implemented here, but the diff already implements it: Rust literal-condition extraction, the cgo proto, INgramReader::ConditionTreeCandidates, and Index/Probe routing in SegmentGrep.cpp. It also adds a new user-facing -m/--max-count option and a new function.grep.probeMatchRatio config.

Routing is on by default, with no switch, for every grep on a sealed segment whose field has a whole-field NGRAM index. That includes existing VARCHAR NGRAM fields, not just TEXT. A gap in the necessary-condition extraction would silently drop matching rows, so reviewers need to know this is in the PR. Could we split it into (1) TEXT NGRAM admission, (2) grep NGRAM routing, and (3) -m? At minimum, please update the title and description.

Also, ruff format --check fails on tests/python_client/testcases/indexes/test_ngram.py.

if strings.HasPrefix(value, "--") {
// Value-taking flags consume the next token even if it resembles an option.
name, attached, hasValue := strings.Cut(value, "=")
if name == "--max-count" {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-m/--max-count is new user-facing grep syntax and has nothing to do with NGRAM. The SDK passes the args string through as-is, so this becomes a public contract once it ships. Could it go in its own PR with its own description?

Comment thread pkg/util/paramtable/function_param.go Outdated
}
p.GrepBatchSize.Init(base.mgr)

p.GrepProbeMatchRatio = ParamItem{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have numbers showing Probe helps on the target workload? The design doc says the 5% default isn't validated, and the perf content was removed. If I read condition.rs right, -i -e error already expands to an OR of 32 five-byte literals (MAX_SEQUENCES). So most case-insensitive queries with limit <= 128 would take this path, not just rare wide-OR patterns. Without data, I'd ship Index-only routing first and add Probe and this config later. Once a config is exported, it's hard to remove.

persisted index format, or error-code projection was changed. No performance
speedup is claimed by this correctness run.

Evidence: `/tmp/grep-max-count-e2e-q5nn27kt/verification.json`, `e2e-report.json`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: these /tmp/... paths only exist on your machine and won't mean anything to readers. The same goes for §8 "Documentation Corrections" in the new doc. I'd drop both and keep the docs about the design itself.

@sre-ci-robot

Copy link
Copy Markdown
Contributor

❌ CI Loop Results 287f3aa

Stage Result Duration Tests
✅ Build SUCCESS 10.9min -
❌ Code-Check FAILURE 2.3min -
❌ UT-Integration FAILURE 92.9min -
✅ UT-GO SUCCESS 36.8min -
✅ UT-CPP-Cov SUCCESS 42.1min 22969 total, 22969 passed, 0 failed

Total: 113min | Pipeline | Artifacts

Overall Coverage: 78.7%
Diff Coverage: CPP 96.4% (1294 hit, 48 miss, 1342 measurable lines, 768 unmeasured) | Go 96.5% (55 hit, 2 miss, 57 measurable lines, 390 unmeasured)
Diff Coverage HTML: view changed lines
Go Patch Warning: WARNING: Go patch coverage is partial; 390 changed lines were unmeasured.
Total Patch Coverage: 96.4% (1349/1399 measurable lines, 1158 unmeasured)

Failed Test Logs:

@sre-ci-robot

Copy link
Copy Markdown
Contributor

❌ CI Loop Results 7e4118f

Stage Result Duration Tests
✅ Build SUCCESS 8.4min -
❌ Code-Check FAILURE 3.7min -
✅ UT-Integration SUCCESS 42.5min -
✅ UT-GO SUCCESS 24.3min -
✅ UT-CPP-Cov SUCCESS 40.9min 22969 total, 22969 passed, 0 failed

Total: 58min | Pipeline | Artifacts

Overall Coverage: 78.7%
Diff Coverage: CPP 96.4% (1294 hit, 48 miss, 1342 measurable lines, 768 unmeasured) | Go 96.5% (55 hit, 2 miss, 57 measurable lines, 390 unmeasured)
Diff Coverage HTML: view changed lines
Go Patch Warning: WARNING: Go patch coverage is partial; 390 changed lines were unmeasured.
Total Patch Coverage: 96.4% (1349/1399 measurable lines, 1158 unmeasured)

Failed Test Logs:

@sre-ci-robot

Copy link
Copy Markdown
Contributor

✅ CI Loop Results 4cb8d15

Stage Result Duration Tests
✅ Build SUCCESS 16.7min -
✅ Code-Check SUCCESS 7.8min -
✅ UT-Integration SUCCESS 58.5min -
✅ UT-GO SUCCESS 41.2min -
✅ UT-CPP-Cov SUCCESS 81.5min 22969 total, 22969 passed, 0 failed

Total: 132min | Pipeline | Artifacts

Overall Coverage: 78.7%
Diff Coverage: CPP 96.4% (1291 hit, 48 miss, 1339 measurable lines, 769 unmeasured) | Go 96.0% (48 hit, 2 miss, 50 measurable lines, 369 unmeasured)
Diff Coverage HTML: view changed lines
Go Patch Warning: WARNING: Go patch coverage is partial; 369 changed lines were unmeasured.
Total Patch Coverage: 96.4% (1339/1389 measurable lines, 1138 unmeasured)

@mergify mergify Bot added the ci-passed label Nov 5, 2026
@mergify mergify Bot removed the ci-passed label Nov 6, 2026
@junjiejiangjjj junjiejiangjjj changed the title feat: support TEXT NGRAM indexes for future Grep acceleration feat: add TEXT NGRAM indexes, Grep index routing, and max-count Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

@liliu-z Updated the title and description to cover TEXT NGRAM support, Grep index routing, and -m/--max-count. We’ll keep these changes in one PR.

@sre-ci-robot

Copy link
Copy Markdown
Contributor

❌ CI Loop Results 02c0070

Stage Result Duration Tests
❌ Build FAILURE 5.2min -
❌ Code-Check SKIPPED - -
❌ UT-Integration SKIPPED - -
❌ UT-GO SKIPPED - -
❌ UT-CPP-Cov SKIPPED - -

Total: 10min | Pipeline | Artifacts

Failed Test Logs:

@sre-ci-robot

Copy link
Copy Markdown
Contributor

❌ CI Loop Results f5b9668

Stage Result Duration Tests
✅ Build SUCCESS 16.9min -
✅ Code-Check SUCCESS 12.7min -
❌ UT-Integration UNKNOWN - -
✅ UT-GO SUCCESS 40.6min -
❌ UT-CPP-Cov FAILURE 48.9min -

Total: 77min | Pipeline | Artifacts
Diff Coverage: Go 96.0% (48 hit, 2 miss, 50 measurable lines, 369 unmeasured)
Go Patch Warning: WARNING: Go patch coverage is partial; 369 changed lines were unmeasured.
Total Patch Coverage: 96.0% (48/50 measurable lines, 369 unmeasured)

Failed Test Logs:

Allow explicit NGRAM indexes on TEXT fields and preserve the index type
when scalar autoindex is enabled. Keep TEXT AUTOINDEX and other
user-created scalar index types unsupported.

Add admission, parameter validation, native TEXT/LOB build-load tests,
and a standalone verification script. Verify metadata persistence,
DataNode builds, and raw TEXT retrieval across reload and restart.
Shutdown issues remain unresolved.

This establishes the index prerequisite for Grep acceleration. Follow-up
work will extract necessary literal conditions from Rust regexes, use
NGRAM to filter candidate offsets before reading TEXT/LOB values, and
retain the Grep matcher for exact refinement. Grep index routing is not
implemented in this change.

Signed-off-by: junjie.jiang <junjie.jiang@zilliz.com>

@liliu-z liliu-z left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for updating the title/description and making the ratio an internal constant. I re-checked the extraction (condition.rs) and the C++ planning (PlanNgramRegexCondition) for the direction of every fallback: AND only drops children, OR turns into True as soon as any branch does, budget overflows weaken to True, and the HIR is built with the same case/Unicode settings as the matcher. I couldn't find a way for a real match to fall outside the candidate set, so no correctness blocker from me. What's left is mostly Probe and the default-on performance, see inline.

}

bool
ShouldProbe(const GrepIndex& index, int64_t limit) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding the numbers. To me they argue for leaving Probe out for now. In §7.3 Probe is slower than No-Probe on all three TEXT corpora (Go 10.09 vs 9.19 ms, Python 11.17 vs 9.99, Linux 45.55 vs 34.26) and roughly even on VARCHAR, and the c=16 runs in §7.4 show no throughput gain. The clear win only shows up on the 60%-hit severity fixture.

The trigger is also somewhat accidental. 32 leaves is the same number as MAX_SEQUENCES, so for -i it comes down to word length: -i error, -i panic and -i failed take Probe, while -i warn, -i timeout and -i exception go straight to Index (I checked with condition.rs against regex-syntax 0.8.11). I'd ship direct Index routing first and bring Probe back once there are route counters, as §7.1 already suggests. §6.2 and §7.1/7.3/7.4 could go with it.

segment->HasFieldData(field) &&
(!snapshot || view_column != nullptr);

auto index = PrepareGrepIndex(segment, &op_ctx, meta, plan, size, rows);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Routing is on for every eligible sealed segment, including existing VARCHAR NGRAM fields, and §7.2 shows the 10K short-text set at c=32 dropping from 1391 to 709 QPS (p50 20.9 → 37.8 ms). The cause hasn't been profiled yet. Could we find out where that time goes before this becomes the default? Until then it's hard to say whether short-text collections should take this path at all.

// queryScanNativeSegment borrows the exact native handle retained by retrieval.
// The caller owns the legacy segment pin or QueryView lease through scanning,
// identity retrieval and final materialization; this helper must not release it.
func queryScanNativeSegment(segment Segment) (segcore.CSegment, SegmentType, error) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I read it right, this also makes grep work on QueryView-served segments (sealedReadState and SN growing views). Before, they failed with QueryScan requires a local segcore segment. That's a separate fix and it isn't in the description. Could you mention it there and add a small test? The s.native vs parent.csegment choice is easy to break later.


The four-corpus threshold aggregate favors 0.25 over 0.05 by about 3.35% at p50 and 3.46% at p95, but individual workloads regress, including HDFS `termi`, limit 32, with about 23.6% higher cross-round geometric-mean p95. This does not establish a better general default. No-Probe improves the source-code aggregates while losing on HDFS and on several high-hit early-return subsets. The evidence supports workload-dependent routing, not a universal Probe speedup or a TEXT-specific limit cutoff.

Entry points: [corpus benchmark](../../../tests/scripts/grep_probe_corpus.py), [threshold report](../../../tests/scripts/grep_probe_corpus_report.py), and [No-Probe comparison](../../../tests/scripts/grep_probe_compare_no_probe.py). Local raw evidence is retained under `/tmp/grep-probe-multi-corpus-20261105` and `/tmp/grep-no-probe-20261105`; these are local artifacts, not repository-published datasets.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the /tmp/... paths are still here and in §7.4. The linked tests/scripts/grep_probe_*.py and grep_severity_probe_ab.py aren't in this PR either, so those links are broken. I'd either add the scripts or drop the references.

@sre-ci-robot

Copy link
Copy Markdown
Contributor

❌ CI Loop Results 95bbfd3

Stage Result Duration Tests
✅ Build SUCCESS 21.3min -
✅ Code-Check SUCCESS 12.9min -
❌ UT-Integration UNKNOWN - -
✅ UT-GO SUCCESS 39.9min -
❌ UT-CPP-Cov FAILURE 48.0min -

Total: 81min | Pipeline | Artifacts
Diff Coverage: Go 79.7% (63 hit, 16 miss, 79 measurable lines, 375 unmeasured)
Go Patch Warning: WARNING: Go patch coverage is partial; 375 changed lines were unmeasured.
Total Patch Coverage: 79.7% (63/79 measurable lines, 375 unmeasured)

Failed Test Logs:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/compilation area/test dco-passed DCO check passed. kind/feature Issues related to feature request from users sig/testing size/XXL Denotes a PR that changes 1000+ lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants