Repository navigation
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: 3b38a72 The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Duplicate code analysis5 new clones introduced by this PR.
If one of these duplicates a helper that already exists, import it instead jscpd compared this branch against |
E2E Test Results❌ Error reading test results: ENOENT: no such file or directory, scandir 'all-test-results' |
64ef09d to
7fae5c3
Compare
🔴 Tier 4 — CriticalTouches authentication, tenancy data models, the public API or shipped database config — or substantially changes the query rendering engine, background tasks, the OTel pipeline, image build, or release CI. Why this tier:
Review process: Deep review from a domain expert. Synchronous walkthrough may be required. Stats
|
| WHERE ${partsFilter} | ||
| AND startsWith(token, ${keyPrefix(k)}) | ||
| AND v != '' | ||
| LIMIT ${{ UNSAFE_RAW_SQL: String(TEXT_INDEX_TOKENS_PER_KEY) }} |
There was a problem hiding this comment.
🟠 major — The 20,000-token LIMIT counts each part's copy of a value, so "Load more" returns far fewer than 10,000 values and the "most frequent" ranking only covers some parts
mergeTreeTextIndex() returns one row per (part, token), so the per-key LIMIT 20000 (and the per-column one at line 1958) counts the same value once for every part it appears in. "Load more" (DBSearchPageFilters/hooks.ts:259, mapKeyIndexMaxParts: null, SINGLE_KEY_VALUES_LIMIT = 10000) reads every part. With 40 parts that each hold the same 2,000 values, the read stops at about 500 distinct values. Before this change, groupUniqArray(10000) returned all 2,000. The topKWeighted weights also come only from the parts read before the LIMIT, so the "most frequent" order can be skewed toward whichever parts were read first. The comment's invariant ("Keep it above Load more's 10,000 values") assumes the tokens are distinct. Fix: scale the LIMIT by the number of parts read, or drop it when maxParts is null (the Load more path). Add an integration test that inserts the same values into several parts.
Nothing blocks merge automatically, but a maintainer will expect this fixed if it is a real defect in code this PR changes. If it is about surrounding code, reply and say so instead of patching. Do not widen the PR. How to respond
| const TEXT_INDEX_KEYS_PER_QUERY = 10; | ||
| // Smallest dictionary block size that gets one read per key; see | ||
| // getMapTextIndexKeyValues. | ||
| const PER_KEY_TEXT_INDEX_MIN_DICTIONARY_BLOCK_SIZE = 2048; |
There was a problem hiding this comment.
🔵 minor — The per-key threshold is 2048, but the changeset and PR say 4096 or more
PER_KEY_TEXT_INDEX_MIN_DICTIONARY_BLOCK_SIZE = 2048, but .changeset/topk-text-index-values.md (published to users) says "large dictionary blocks (4096 or more". An index with dictionary_block_size = 2048 gets the per-key path even though the docs say it shouldn't. Make the constant and the changeset agree. If 2048 is intended, add a 2048 case to the dictionary-block-size test.
Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond
PR Review
4 finding(s): 🔴 0 critical · 🟠 1 major · 🔵 3 minor 1 posted as inline comment(s) on the changed lines. 3 listed below. Findings outside the changed lines
2 minor
Severity is the reviewer's own estimate and is used for ordering, not filtering. No finding blocks merge automatically; a maintainer decides. |
| ) | ||
| ENGINE = MergeTree() | ||
| ORDER BY (Timestamp) | ||
| SETTINGS text_index_dictionary_block_size = 8192 |
There was a problem hiding this comment.
🔵 minor — The integration tests no longer run the small-block (default 512) query path
Every text-index table in metadata.int.test.ts and kvItemsRewrite.int.test.ts now sets text_index_dictionary_block_size = 8192. So the rewritten groupUniqArray/OR'd startsWith query never runs against a real ClickHouse server. It now has an inlined limit and no SELECT * FROM (...) wrapper, and it is what every table without the ALTER gets. Only SQL string matching in metadata.test.ts checks it. Add an integration case on a table with the default block size so the shape most users get is executed for real.
Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond
Deep Review✅ No critical issues found. The change is a well-scoped, well-documented performance refactor. No P0/P1 defects surfaced: SQL inlining stays within the existing 🟡 P2 -- recommended
🔵 P3 nitpicks (5)
Reviewers (4): security, data-migrations, maintainability, testing (plus orchestrator direct analysis of the dispatch path and query-shape selection). Testing gaps:
No finding blocks merge automatically. A maintainer will expect P0/P1 findings in code this PR changes to be fixed; P2/P3 are your call -- fix or reply. Never fix findings about surrounding code here; reply instead. Do not widen the PR. How to respond |
Summary
Map key=value text index value lookups now run in parallel batches of 10 keys. Indexes with large dictionary blocks read each key separately and return its most frequent values with
topKWeighted, so a high-cardinality key stops early and delays only its own batch. Indexes with small blocks keepmain's query shape within each batch. Plain-column text indexes return the most frequent values withtopKWeighted, in batches of 10 columns.Stacked on #3635, which limits these reads to a configurable set of parts (20 median-sized by default).
The problem
getMapTextIndexKeyValues(Map key=value indexes, e.g.idx_log_attr_items) andgetTextIndexKeyValues(text indexes on plain columns, e.g.TraceId):groupUniqArray(limit)over a sorted dictionary), not the common ones.max_rows_to_read(3M by default) withread_overflow_mode = 'break'. The dictionary is read in sorted token order, so once a high-cardinality key used up the budget, every key that sorts after it came back with no values and no error. On a realLogAttributeslookup, only 22 of 100 keys got values.The fix
topKWeighted(limit)(value, cardinality).cardinalityis the token's row count in the part, so the values are the most frequent, most frequent first.TEXT_INDEX_KEYS_PER_QUERY). They stream in as each query finishes.UNION ALLbranch reading at most 20,000 tokens (TEXT_INDEX_TOKENS_PER_KEY), so a high-cardinality key's read stops early. Nomax_rows_to_readcap ('0', after the team's settings): the per-keyLIMITbounds each read.main's query shape, in queries of at most 10 keys: one read with OR'dstartsWithfilters,groupUniqArray(limit), and themax_rows_to_readcap withread_overflow_mode = 'break'. It keepsmain's known limits: the alphabetically first values, and keys that sort after a high-cardinality key can come back empty. Batches of 10 limit that to at most 9 other keys. The cap is marked with ashortcut:comment.dictionary_block_sizein the index definition, then the table'stext_index_dictionary_block_size, then the server's MergeTree setting, then 512.max_rows_to_readcap either.Why the block size decides
Every read of
mergeTreeTextIndex()opens each part's index and loads its sparse index, which has one entry per dictionary block. SeparateUNION ALLbranches don't share that work.On a local benchmark (1M rows, 3M tokens, 10 parts), a batch of 10 keys:
startsWith)TextIndexReadSparseIndexBlocksThe sparse index was 17.7 KB per part with 512-token blocks and 1.3 KB with 8192. On ClickHouse Cloud, an earlier version with per-key branches on default-block indexes measured about 0.18 CPU-s and 10–30 MiB of cache reads per part per branch (26-part
otel_logs: 804 ms for one key, 772 ms for a key that doesn't exist). With 8192-token blocks that fixed cost is small enough for per-key branches to pay off.Cost and known limits
main. Each opens every part's index once (small blocks) or once per key (large blocks).text_index_dictionary_block_sizekeep their old block size. This only picks the query shape; results are the same either way.Seed schemas: 8,192-token blocks as a table setting
The collector's migration now runs
ALTER TABLE … MODIFY SETTING text_index_dictionary_block_size = 8192onotel_logs,otel_tracesandhyperdx_sessionsafter the seed, so the per-key path applies to them.ALTERis limited to 26.6+, and 26.2–26.5 keep 512-token blocks.topKWeighteduses about 1.6× the CPU per token ofgroupUniqArray. Memory is the same.getEngineSettingmoved fromDBRowTable.tsxtocommon-utils/core/utils.tsso both use one parser forengine_fullsettings.How to reproduce
Starting point:
yarn devagainst ClickHouse 26.3+, with the OTelotel_logsschema (key=value text indexesidx_log_attr_items,idx_res_attr_items), and a Map key with one rare value that sorts first and one common value, e.g.map('freq.key', if(number = 0, 'a-rare', 'z-common'))fromnumbers(4).On
main:freq.keylistsa-rarefirst, andsystem.query_logshows one values query for all keys.On this branch:
text_index_dictionary_block_size = 8192.freq.keylistsz-commonfirst.system.query_log, there is one values query per 10 keys, with oneUNION ALLbranch per key. RunALTER TABLE otel_logs MODIFY SETTING text_index_dictionary_block_size = 512and reload: the query shape follows the table setting, so each values query now hasmain's shape for 10 keys.References