Skip to content

[Feature Request] Add a substring / grep (LIKE '%...%') benchmark case, backed by the Pizza&Chili corpus #827

Description

@xiaofan-luan

Summary

I'd like to propose a substring / "grep" search benchmark casetext LIKE '%pattern%' over a text column — plus adding the Pizza&Chili corpus as a new data source to back it.

Today VDBBench has no lexical coverage at all: every case ultimately drives search_embedding, and there is no substring, wildcard, or full-text case type. #687 asks for match_phrase + kNN hybrid, which is adjacent but a different problem — phrase matching runs on a tokenized inverted index, while arbitrary substring matching cannot use one (the pattern may start or end mid-token). Both are worth having; this issue is about the substring half.

Motivation

Substring filtering over text is a first-class production workload, not a niche one:

  • Log / observability search — find every line containing a request ID, an error fragment, a stack frame.
  • Code search — identifier and snippet lookup.
  • RAG metadata filtering — narrow a corpus by a substring on a text attribute before or during vector retrieval.

Multiple systems in VDBBench's comparison set already ship dedicated index structures for exactly this, so the case is meaningfully cross-DB rather than vendor-specific:

System Mechanism
Milvus NGRAM index; FM-index (suffix-array based) landing now
Elasticsearch / OpenSearch wildcard field type
PostgreSQL / pgvector pg_trgm GIN index
ClickHouse ngrambf_v1 / tokenbf_v1 skip indexes

There is currently no shared, reproducible benchmark for any of it. The practical consequence is that every vendor publishes numbers on a self-chosen corpus with self-chosen patterns, and none of them are falsifiable or comparable.

Why this cannot be a single number

This is the part that shapes the whole design. Substring-index performance is not one value — it moves by orders of magnitude along axes that a naive benchmark holds fixed:

  1. Pattern length. Suffix-array / FM-index backward search costs scale with pattern length. n-gram indexes go the other way: patterns shorter than n degenerate to a full scan. The crossover between the two families lives entirely in this axis, so a benchmark with one fixed pattern length cannot show it.
  2. Selectivity (occurrence count). Counting matches is cheap; locating them costs time proportional to the number of hits. High-frequency patterns are the worst case for a compressed index and the best case for a bitmap. A single hand-picked pattern hides this cliff completely.
  3. Build time and index size. This is the adoption gate — an index that is 3× the raw text is a different product than one that is 0.3×. It belongs in the report, not a footnote.
  4. Corpus repetitiveness. Determines how much a compressed index can exploit; separates run-length-compressed indexes (r-index) from plain FM-index by a large factor.

So the deliverable is a matrix — selectivity × pattern length — not a headline number.

Proposed data source: Pizza&Chili corpus

pizzachili.dcc.uchile.cl has been the standard corpus for compressed full-text indexes since ~2005 (Ferragina @ Pisa + Navarro @ Chile — hence the name). Using it means results are directly comparable to twenty years of published numbers, and reviewers in this area recognize the setup immediately.

Concretely I'd propose these, matching the axes above. δ is substring complexity, the standard repetitiveness measure — lower = more repetitive:

File Size Alphabet δ Why include it
english (200MB prefix) 200 MB of 2.1 GB 239 Natural-language baseline
sources (200MB prefix) 200 MB of 201 MiB 230 Code search; C/Java from Linux 2.6.11.6 + GCC 4.0.0
einstein.en.txt 446 MiB 139 42,884 Extreme repetition — all revisions of one Wikipedia article. 7z's to 320 KB. Separates run-length-compressed indexes from plain ones
HDFS_1 100 MiB 73 1,173,611 Hadoop DFS logs
BGL 100 MiB 78 1,301,165 Blue Gene/L supercomputer logs
Thunderbird 100 MiB 92 1,048,593 Thunderbird supercomputer logs
Android 100 MiB 176 881,571 Android system logs
EDGAR 100 MiB 71 1,116,985 SEC EDGAR financial system logs
Windows 100 MiB 91 61,711 Windows system logs — notably more regular than the other five, useful as a contrast

The six log files are the highest-value group: real system logs are the actual target workload, and they're already normalized to 100 MiB each, which conveniently sits at about one segment/shard worth of text for most engines — per-segment index build and size can be measured without any truncation.

Pizza&Chili also ships three execution traces (Horspool 546 MiB, NQueens 289 MiB, Quicksort 364 MiB) with δ in the tens of thousands. They produce very flattering numbers precisely because they're so repetitive, so I'd either leave them out or label them clearly as a corner case rather than a representative workload.

Query set design

This is where most of the real work is, and where a shared benchmark adds the most value over everyone rolling their own:

  • Selectivity buckets: ~1e-6, 1e-5, 1e-4, 1e-3, 1e-2, plus a high-frequency bucket.
  • Pattern length buckets: 2, 4, 8, 16, 32 characters.
  • N patterns per cell, sampled deterministically (fixed seed) from the corpus itself so every pattern is guaranteed to have the intended hit count.
  • Ship the generator script plus the resulting pattern file — the patterns are kilobytes, so no large artifact needs hosting for this part.

Report each cell separately. Averaging across selectivity is what makes published substring-search numbers uninformative.

Ground truth is exact — and that changes the design

Unlike ANN, substring-match ground truth is exact and cheap to compute: it's just the set of matching row IDs from a linear scan. Two useful consequences:

  1. Correctness becomes a pass/fail assertion, not a tuning dial. Recall must be exactly 1.0. Anything else is a bug in the index, not a speed/accuracy trade-off. That's a stronger guarantee than any ANN case can offer and it should be checked, since a wrong-but-fast substring index is easy to ship accidentally.
  2. No large GT artifact is needed. A linear scan regenerates it locally in seconds, so GT can be produced at prepare-time rather than downloaded — sidestepping the artifact-size problem that dominates [Feature Request] Add a Large-TopK benchmark case: LAION-100M with top-1M ground truth #826.

Metrics to report

  • Index build time and index size (absolute, and as a ratio to raw text)
  • p50 / p95 / p99 latency per (selectivity, length) cell
  • QPS under concurrency
  • Resident memory
  • Correctness assertion (exact match against GT)

Two query shapes

  1. Scalar-only grepSELECT id WHERE text LIKE '%pat%' LIMIT n, no vector involved. This isolates the index and is the number that's comparable to the full-text-index literature.
  2. Filtered ANN — vector search carrying a substring predicate. This is the shape that actually shows up in RAG, and it stresses a different path (pre-filter vs. post-filter, bitmap handoff to the vector index) that shape 1 never touches.

Shape 2 fits the existing runner almost as-is. Shape 1 does not, which brings us to:

What's missing in VDBBench today

Mapping the gap concretely, since this touches more than one extension point:

  • FilterOp has no substring opbackend/filter.py:6-9 has only NumGE, StrEqual, NonFilter. Needs a StrContains plus a Filter subclass, and each client extends supported_filter_types / prepare_filter. The rejection path for non-supporting clients already exists (FilterNotSupportedError in assembler.py), so unsupported DBs degrade cleanly.
  • There is no non-vector query pathVectorDB.search_embedding is the sole query entry point, and SerialSearchRunner unconditionally passes an embedding. Shape 1 needs a new abstract method (something like scalar_query(expr, limit)) and a runner path that doesn't require a query vector.
  • Metrics are selected by CaseLabel, not by the case class — task_runner.py:241-258 dispatches on it. A new metric shape means a new CaseLabel, a new _run_*_case branch, and new Metric fields.
  • Datasets assume a vector columnBaseDataset is built around emb + neighbors_id. A text dataset needs an id + text variant, registered in both the Dataset enum and DatasetWithSizeType/DatasetWithSizeMap.
  • Hosting: converting the selected files to parquet and mirroring them under the existing assets.zilliz.com/benchmark/ root would need no new download machinery — AwsS3Reader / AliyunOSSReader already handle it.
  • Worth noting there's a documented recipe for adding a client (README "Adding New Clients") but none for adding a dataset or a case. If this lands, that gap is probably worth closing at the same time.

Open questions for maintainers

  1. Is a non-vector (scalar-only) case type something the project wants to host at all, or should this be scoped to filtered-ANN only to avoid introducing a second query path?
  2. Licensing — Pizza&Chili is derived from Gutenberg, Linux/GCC sources, Wikipedia, and public log datasets. I'd want to confirm redistribution terms before mirroring to assets.zilliz.com; alternatively the case could download from the upstream mirror directly. Preference?
  3. Full matrix, or a trimmed default? The full selectivity × length grid across 9 corpora is a long run — a --quick subset (say the six log files at three selectivity levels) is probably what most people should run by default.

Happy to contribute the corpus conversion, the pattern generator, and the Milvus-side client implementation if there's interest in the direction.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions