Skip to content

[feature](nereids) Support SEARCH in joins and per-field analyzers - #67932

Draft
eldenmoon wants to merge 2 commits into
apache:masterfrom
eldenmoon:branch-search-join-master-pr
Draft

eldenmoon wants to merge 2 commits into
apache:masterfrom
eldenmoon:branch-search-join-master-pr

Conversation

@eldenmoon

Copy link
Copy Markdown
Member

What problem does this PR solve?

SEARCH in WHERE is rejected when its input contains an OLAP join. Residual predicates such as (MATCH AND EXISTS (...)) OR joined_column = ... can also reach execution without an inverted-index evaluation path. Filtering the scan by MATCH alone would incorrectly remove rows selected by the other OR branch.

This PR binds SEARCH field dependencies before pruning and predicate movement, and extends the existing scan virtual-column rule to materialize MATCH/SEARCH booleans used by projections, residual filters, and join conditions. The original SQL boolean expression and join multiplicity are preserved. This includes WHERE predicates moved into INNER JOIN conditions by the optimizer.

It also supports per-field analyzer selection, for example search('name@exact:"John Smith" AND title@text:software'), including selectors in the fields option. Selected index properties use the existing FE/BE interface. Quoted literal @ field names remain supported.

Each SEARCH expression still references one table instance; separate SEARCH expressions can be combined across tables using SQL AND/OR. SEARCH across an outer join's null-generating side remains conservatively gated. Explicit SEARCH projections/ON clauses, tuple IN subqueries, analyzer-IN, and multiple analyzers for the same field within one SEARCH are outside this change.

No BE code, storage format, Thrift, or new plan-node changes are included. Typed VARIANT TopN uses existing lazy materialization; sorting an untyped VARIANT value still requires an explicit cast.

Release note

Support SEARCH predicates in OLAP join queries and per-field analyzer selection, and evaluate residual MATCH/SEARCH expressions through indexed scan virtual columns.

Check List (For Author)

  • Test
    • Regression test
    • Unit Test
    • Manual test (details below)
    • No need to test or manual test.

Validation on the original development baseline 16ab0566e9796d0498e6d2b3221e2a59d5e94ef7 with these changes:

  • ASAN BE/FE build and FE Checkstyle passed.
  • A 250-test FE matrix passed; a subsequent focused 15-test run passed after adding INNER JOIN-condition handling (the runs overlap).
  • Seven regression suites passed in normal comparison mode: test_crm_search_join_document, test_crm_search_analyzers, test_crm_search_variant_topn, test_search_usage_restrictions, test_search_null_semantics, test_search_variant_subcolumn_analyzer, and test_match_projection_virtual_column.
  • Generated expectations cover the CRM query examples, duplicate/NULL/unmatched join rows, MOW updates, analyzer differences, and unsupported syntax. Non-equivalent query variants have separate expected results.
  • Typed TopN with and without lazy materialization returned the same 100 ordered rows and payloads; EXPLAIN confirmed MaterializeNode. These are correctness checks, not performance measurements.
  • Disabling the virtual-column rule with MATCH fallback disabled reproduced match_any not support execute_match for the OR/EXISTS control query; enabling it returned the expected five rows.

For this PR, only the two feature/test commits were cherry-picked onto master df36e174b99557a004bc3ad57faa019da4a7d91f. Range comparison shows unchanged test changes and only surrounding Analyzer context differences. Tests have not been rerun on this rebased master head; the draft records that validation boundary explicitly. No C++ files changed, so clang-format is not applicable.

  • Behavior changed:

    • No.
    • Yes. SEARCH is accepted in supported OLAP JOIN WHERE contexts; per-field analyzer selectors are recognized.
  • Does this need documentation?

    • No.
    • Yes. Follow-up documentation should describe analyzer selectors and supported SEARCH/JOIN placements.

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label

@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@eldenmoon

Copy link
Copy Markdown
Member Author

run buildall

1 similar comment
@eldenmoon

Copy link
Copy Markdown
Member Author

run buildall

@eldenmoon

Copy link
Copy Markdown
Member Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Static review at exact head 3606a1a2af1b2a3632f0c6d57e89e44959f55205. I found six distinct issues: three can return wrong results or query the wrong index (selector collision, outer-join SEARCH(NULL), and analyzer-selected EXACT), one newly rejects a valid passthrough alias, one rejects equivalent analyzer spellings, and one evaluates the same MATCH virtual expression twice.

Checkpoint conclusions: the change is focused on the necessary analyzer, rewrite, expression, and translation stages, and the changed tests broadly cover direct/VARIANT OLAP joins, MOW behavior, TERM/PHRASE analyzers, and typed VARIANT TopN. They do not cover the six inline triggers. No new shared-state concurrency, lifecycle/static initialization, configuration, persistence/EditLog, transaction/write, or storage-format mechanism is introduced. Existing field-binding/index-properties transport remains positionally aligned for ordinary and VARIANT slots, but it cannot preserve explicit-selector intent for EXACT as implemented. Join types, ExprIds/nullability, predicate movement, top-down reentry, direct/residual paths, options fields, generated outputs, ordering, and negative-test form were checked; the duplicate virtual MATCH is the only additional performance issue. Existing diagnostics are otherwise sufficient.

This was a static-only review as required: I did not run builds or tests. The author reports validation on an older development baseline, and current exact-head CI is separate from that claim. There was no additional user focus and no existing inline review thread to duplicate.

while (analyzerSeparator > 0 && bindingName.charAt(analyzerSeparator - 1) == '\\') {
analyzerSeparator = bindingName.lastIndexOf('@', analyzerSeparator - 1);
}
if (analyzerSeparator >= 0 && findSlotByName(bindingName, scan) == null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Preserve analyzer-selector syntax before schema lookup. For an unquoted name@english the parser leaves @ unescaped, but this branch treats it as a selector only when a slot named name@english does not exist. If a table has both indexed name and a literal name@english column, the query silently binds the literal column and its index instead of applying the english analyzer to name, so it can return different rows. Please carry quoted/unquoted provenance through parsing and split every unquoted selector independently of slot collisions, with a collision regression.

}
String fieldKey = normalizedFieldName.toLowerCase(Locale.ROOT);
if (fieldAnalyzers.containsKey(fieldKey)
&& !Objects.equals(fieldAnalyzers.get(fieldKey), binding.getAnalyzerName())) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] Compare analyzer identities with the same normalization used for index lookup. isAnalyzerMatched accepts analyzer names case-insensitively, so both name@CRM_DOC_TEXT and name@crm_doc_text resolve to the same index, but this Objects.equals check then rejects them as two analyzers for one field. Normalize with Locale.ROOT (or compare case-insensitively) and cover mixed-case spellings in one DSL.

originalFieldName, search.getDslString()));
}
checkInvertedIndexExists(scan.getTable(), slot.getName(), search.getDslString(), false);
checkInvertedIndexExists(tableForSlot(slot, scan), slot.getName(), search.getDslString(), false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Check the physical original column rather than the visible alias. For a passthrough such as (SELECT content AS body FROM t) s, findSlotByName returns body and that slot retains originalTable=t/originalColumn=content, but this call asks t for a column named body and falsely reports that no index exists. The VARIANT parent path has the same alias issue. Please use the slot's original column (plus subpath where applicable) for index validation while retaining the alias only for DSL binding, and add renamed-output regressions.

for (Expression child : children) {
if (!(child instanceof SlotReference || child instanceof ElementAt)) {
if (!(child instanceof SlotReference || child instanceof ElementAt
|| child instanceof NullLiteral)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Do not let inference-only NULL children persist into executable SEARCH plans. For a LEFT JOIN b ON FALSE with search('content:john') IS NULL on b, join elimination produces NULL AS content and filter-through-project substitutes it here, yielding Search(NULL) directly over a's scan. The materializer skips it, the final Filter-to-scan check admits it, and BE's no-iterator path produces empty data and null bitmaps, so SEARCH is false/non-null and the preserved rows are wrongly rejected. Please keep symbolic NULL replacement non-persistent or reject non-slot/subcolumn children before translation, and cover false outer-join padding.

return null;
}
List<NamedExpression> projects = new ArrayList<>(project.getProjects());
projects.add(result.second);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] Record or reuse the materialization before leaving this project. If the same MATCH appears in this child projection and a preserved-side outer-join ON condition, pushDownJoin first reaches this path and appends one virtual slot; top-down traversal then reaches the rebuilt project, where the direct Project-to-scan rule allocates a second alias because it does not consult the scan's existing virtual columns. Both ExprIds stay referenced and the segment iterator evaluates/materializes the predicate twice. Please centralize the reuse check and assert this plan has one virtual MATCH column.

Column column = slot.getOriginalColumn().orElse(null);
if (column != null) {
invertedIndex = olapTbl.getInvertedIndex(column, slot.getSubPath());
invertedIndex = olapTbl.getInvertedIndex(column, slot.getSubPath(), analyzer);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Ensure this selected analyzer also constrains BE reader choice for EXACT. FE resolves the requested index here and sends its properties, but FieldReaderResolver derives analyzer_key only when the query type is not EQUAL_QUERY; SEARCH maps EXACT to EQUAL_QUERY. Two custom standard/keyword analyzers are both FULLTEXT readers, so the empty-key selector can pick the lower index ID instead of the requested keyword analyzer and return different rows. Please honor an explicit analyzer for every clause type (then apply type preference within that analyzer) and add a two-analyzer EXACT regression.

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 74.71% (127/170) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 16789 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 3606a1a2af1b2a3632f0c6d57e89e44959f55205, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17572	3026	3030	3026
q2	2089	251	226	226
q3	10243	858	515	515
q4	4672	260	211	211
q5	7658	544	395	395
q6	140	113	94	94
q7	517	495	385	385
q8	9240	951	960	951
q9	3468	2390	2349	2349
q10	6512	879	723	723
q11	394	209	177	177
q12	615	258	197	197
q13	18123	1524	1160	1160
q14	155	150	143	143
q15	q16	430	398	375	375
q17	1339	860	882	860
q18	3114	2243	2240	2240
q19	1266	921	758	758
q20	372	280	202	202
q21	5600	1573	1811	1573
q22	314	266	229	229
Total cold run time: 93833 ms
Total hot run time: 16789 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	3416	3328	3283	3283
q2	531	396	370	370
q3	2177	2417	2219	2219
q4	1175	1164	898	898
q5	2179	2111	2111	2111
q6	171	117	87	87
q7	1054	921	845	845
q8	1583	1377	1389	1377
q9	3131	3075	3090	3075
q10	1875	1795	1636	1636
q11	357	271	244	244
q12	454	428	339	339
q13	1474	1535	1164	1164
q14	181	178	152	152
q15	q16	395	393	357	357
q17	3631	3333	3233	3233
q18	4818	4406	4706	4406
q19	860	806	920	806
q20	1008	962	821	821
q21	3821	3137	3325	3137
q22	402	353	331	331
Total cold run time: 34693 ms
Total hot run time: 30891 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 81601 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 3606a1a2af1b2a3632f0c6d57e89e44959f55205, data reload: false

query5	4267	417	336	336
query6	382	142	129	129
query7	4925	439	228	228
query8	296	131	124	124
query9	8705	2917	2873	2873
query10	403	219	184	184
query11	5395	1039	927	927
query12	120	71	74	71
query13	1208	446	326	326
query14	5975	2203	2089	2089
query14_1	1997	1960	1959	1959
query15	177	125	111	111
query16	921	379	368	368
query17	849	452	349	349
query18	2332	330	245	245
query19	166	141	112	112
query20	86	71	68	68
query21	202	102	88	88
query22	5510	5275	5266	5266
query23	6800	6257	6038	6038
query23_1	6165	6049	6140	6049
query24	7281	1099	775	775
query24_1	770	787	772	772
query25	449	287	224	224
query26	1231	230	127	127
query27	2782	408	258	258
query28	4668	1502	1480	1480
query29	887	411	325	325
query30	245	151	131	131
query31	798	397	326	326
query32	126	71	73	71
query33	437	203	174	174
query34	975	833	479	479
query35	386	388	336	336
query36	572	565	563	563
query37	125	83	68	68
query38	993	836	797	797
query39	487	487	465	465
query39_1	458	468	460	460
query40	197	87	73	73
query41	54	52	52	52
query42	73	72	72	72
query43	236	244	209	209
query44	997	529	530	529
query45	110	104	99	99
query46	797	838	554	554
query47	777	778	732	732
query48	290	283	227	227
query49	538	243	182	182
query50	751	256	186	186
query51	8202	8047	8099	8047
query52	69	72	64	64
query53	196	190	146	146
query54	203	168	147	147
query55	73	61	52	52
query56	210	188	150	150
query57	829	659	658	658
query58	197	181	159	159
query59	1203	1220	1102	1102
query60	232	183	171	171
query61	113	126	136	126
query62	341	204	171	171
query63	171	144	147	144
query64	2772	671	617	617
query65	1620	1650	1546	1546
query66	1809	267	191	191
query67	9816	9792	9644	9644
query68	2998	1154	754	754
query69	343	207	203	203
query70	657	620	615	615
query71	247	176	153	153
query72	2249	1671	1461	1461
query73	676	586	330	330
query74	2009	1224	1126	1126
query75	1180	1096	963	963
query76	2368	698	545	545
query77	252	258	200	200
query78	3972	3775	3184	3184
query79	1232	794	566	566
query80	1331	317	264	264
query81	462	152	138	138
query82	583	122	92	92
query83	282	213	188	188
query84	267	108	89	89
query85	1062	326	275	275
query86	352	179	162	162
query87	1037	976	895	895
query88	2769	2092	2104	2092
query89	282	197	175	175
query90	1781	120	127	120
query91	126	119	96	96
query92	74	69	71	69
query93	1113	1124	687	687
query94	552	252	211	211
query95	521	324	227	227
query96	760	531	295	295
query97	1073	1034	1001	1001
query98	140	131	134	131
query99	419	337	305	305
Total cold run time: 176041 ms
Total hot run time: 81601 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 14.85 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 3606a1a2af1b2a3632f0c6d57e89e44959f55205, data reload: false

query1	0.01	0.00	0.01
query2	0.08	0.04	0.04
query3	0.25	0.12	0.11
query4	1.60	0.09	0.10
query5	0.17	0.16	0.15
query6	1.29	0.71	0.69
query7	0.03	0.01	0.00
query8	0.05	0.03	0.02
query9	0.30	0.21	0.21
query10	0.37	0.35	0.33
query11	0.17	0.12	0.11
query12	0.17	0.12	0.12
query13	0.31	0.31	0.30
query14	0.45	0.46	0.45
query15	0.36	0.35	0.36
query16	0.24	0.20	0.23
query17	0.68	0.70	0.68
query18	0.18	0.17	0.17
query19	1.17	1.20	1.16
query20	0.02	0.00	0.00
query21	15.43	0.15	0.11
query22	5.06	0.04	0.04
query23	16.17	0.25	0.11
query24	3.06	0.30	0.25
query25	0.10	0.06	0.06
query26	0.74	0.15	0.12
query27	0.05	0.03	0.02
query28	3.69	0.56	0.29
query29	12.44	3.18	2.54
query30	0.25	0.13	0.14
query31	2.75	0.36	0.17
query32	3.53	0.31	0.23
query33	1.41	1.59	1.55
query34	15.41	2.19	1.81
query35	1.75	1.75	1.78
query36	0.45	0.32	0.28
query37	0.07	0.04	0.04
query38	0.04	0.02	0.02
query39	0.03	0.03	0.02
query40	0.11	0.07	0.08
query41	0.07	0.03	0.02
query42	0.03	0.02	0.02
query43	0.03	0.02	0.02
Total cold run time: 90.57 s
Total hot run time: 14.85 s

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants