Repository navigation
feat(global-index): support row filters in full-text search - #485
Open
SteNicholas wants to merge 1 commit into
Open
SteNicholas wants to merge 1 commit into
SteNicholas wants to merge 1 commit into
Conversation
Add FullTextSearchBuilder::WithFilter(), aligned with Java withFilter. Rows that fail the filter are excluded before ranking, so the result is the top k among matching rows. Matching rows reach the existing index as include row ids, so BM25 statistics stay those of the full corpus. The builder splits the filter into partition and data predicates, as Java does: a conjunct over both partition and data fields stays in the data filter and also restricts the partitions it implies. The scan attaches the scalar index files that intersect each split, which bumps the split encoding to version 2, and the read resolves the data filter into matched rows with those files. The scan and the split reject a scalar index file whose row range is negative or reversed, or whose row count overflows int64. GlobalIndexEvaluatorImpl now reports the fields that decided a result, so that a superset can be told from an exact answer. Unlike in Java, a field with a conjunct that no index could evaluate does not count. The rows of a superset are excluded with a warning by default. With the new global-index.filter.refine-from-data option, FilteredRowIdReader verifies them against the planned snapshot instead. It reads the data with the schema that the search resolved, so a column renamed or replaced since then cannot point the filter at another field. Once the planned snapshot has expired, a search of a tag reads the tag instead, which must still refer to that snapshot. StaticFromTagStartingScanner now reads tags from the branch of the scan, as in Java, instead of always from the main branch, so a refined search of a branch tag verifies rows of that branch. The new scalar-index.search-mode option, which falls back on global-index.search-mode, supports only fast so far; a row filter in another mode fails with NotImplemented.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose
Linked issue: close #405
A full-text search in Paimon C++ could not carry a row filter, so a caller had to filter after
ranking and got fewer than
limitrows. This addsFullTextSearchBuilder::WithFilter(), alignedwith Java
FullTextSearchBuilder#withFilter(apache/paimon#9855): only the matching rows areranked, so the result is the top k among them, and they reach the existing index as include row
ids, so BM25 statistics stay those of the full corpus.
PartitionPredicate#splitPartitionPredicatesAndDataPredicatesdoes,conjuncts over partition fields alone are ANDed into the partition filter, and the rest become the
data filter. A conjunct over both partition and data fields stays in the data filter and also
restricts the partitions it implies, projected as Java
PredicateProjectionConverterdoes, so(p = 1 AND a > 0) OR (p = 2 AND a > 0)also searches onlyp = 1 OR p = 2. Repeated calls areANDed.
DataEvolutionFullTextScan: also keeps the index files that index a filter field, andattaches to each split the scalar pre-filter files that intersect its shard: files that are not
full-text indexes, or full-text indexes with extra fields. Unlike in Java, the latter rule holds
whichever column the index serves, as
lucene-ftsanswers every scalar predicate with all of itsrows. A scalar index file whose row range is negative, reversed or overflows int64 fails the plan.
IndexFullTextSearchSplit: version 2 of the encoding appends the scalar index files, asversion 2 of Java
IndexFullTextSearchSplitdoes; version 1 splits are still read.DataEvolutionFullTextRead#matchedRows: resolves the data filter before ranking with thescalar indexes of the splits, read per field as one union (
GlobalIndexScanImpl:: CreateForScalarFiltersandScanWithCoverage, as JavaDataEvolutionGlobalIndexScanner).fastscalar-index.search-mode. Ifno index can evaluate the filter at all, nothing is searched and a warning is logged.
so the search never returns a non-matching row. With the new
global-index.filter.refine-from-dataoption (defaultfalse, Java description), they areverified against the data instead.
searchRowRanges ∧ liveRows ∧ matchedRows.GlobalIndexEvaluatorImpl: reports the fields that decided a result(
EvaluateWithContributingFields, as JavaGlobalIndexEvaluator.Evaluation), so that a supersetcan be told from an exact answer: not every filter field contributed, or there is a
Contains,EndsWithorLikeleaf. Unlike in Java, a field with a conjunct that no index could evaluatedoes not count even if another of its conjuncts was evaluated, as the result is then only a
superset for that field.
FilteredRowIdReader: as in Java, reads only the filter columns and_ROW_IDof thecandidates, pinned to the planned snapshot and with the schema that the search resolved, so a
column renamed or replaced since then cannot point the filter at another field. Unlike in Java,
once the planned snapshot has expired, a search of
scan.tag-namereads the tag instead, whichmust still refer to the planned snapshot.
StaticFromTagStartingScanner: reads tags from the branch of the scan, as in Java, instead ofalways from the main branch.
scalar-index.search-modefalls back onglobal-index.search-mode, as JavaCoreOptions#scalarIndexSearchModedoes. Onlyfastis supported so far; a row filter infullor
detailfails withNotImplemented, as does a filter with a full-text match.Not part of this change:
fullanddetailscalar search modes ([Feature] Support full-text search modes for unindexed row ranges #406), which decide the rows that no scalar indexcovers by reading their filter columns.
Tests
UT:
src/paimon/core/table/source/data_evolution_full_text_read_test.cpp, with fake scalar indexers:TestFilterRanksOnlyMatchedRows,TestFilterExcludesRowsThatIndexesCannotDecide(no scalarfile, a declining or multi-field one, a partly indexed conjunction,
Contains/EndsWith/Like,rows that no scalar file covers),
TestFilterUnionsIndexesOfDifferentTypes, and filter validation inTestBuilderValidatesSettings(unknown field,_ROW_ID, wrong type, full-text match, a scalarsearch mode other than
fast, null filter).src/paimon/core/table/source/data_evolution_full_text_scan_test.cpp:TestScalarPreFilterFilesand
TestIntersectingFiles, including invalid row ranges.src/paimon/core/table/source/full_text_search_split_test.cpp:TestSerializeRoundTrip(theversion 2 bytes are pinned and version 1 bytes still read),
TestSerializeScalarIndexFiles, andinvalid scalar index files in
TestDeserializeRejectsInvalidBytes,TestDeserializeRejectsCorruptEntriesandTestCreateValidatesArguments.src/paimon/core/table/source/filtered_row_id_reader_test.cpp:TestExactWhenEveryFilterFieldContributedandTestCandidateOnlyLeavesAreNotExact.src/paimon/core/global_index/global_index_evaluator_impl_test.cpp:ReportsContributingFields,AndWithEmptyResultShortCircuitsandCollectFieldIds.src/paimon/core/core_options_test.cpp:TestParseIndexSearchModefor the full-text and scalarfamilies, and
global-index.filter.refine-from-datainTestDefaultValueandTestFromMap.IT (
test/inte/global_index_test.cpp), aligned with JavaNativeFullTextRowFilterTestandFullTextSearchBuilderTest:TestFullTextTableFullTextSearchWithFilterandTestLuceneTableFullTextSearchWithFilter:equality, range,
IN,NotEqual,ORacross fields, no and all matching rows, equality on aBTree of the text column, accumulated filters, the top row among matching rows with
limit1,a column without an index or with only a full-text index, a partly indexed conjunction and
Contains/EndsWith/Likewith refine on and off, scalar index files attached only for filteredcolumns, serialized splits, and refine with
scan.snapshot-id,scan.tag-nameandscan.timestamp-millis.TestFullTextTableFullTextSearchWithPartitionAndDataFilterandTestLuceneTableFullTextSearchWithPartitionAndDataFilter: partition and data predicates combined,with
SetPartitionFilter()and refine, and the partitions that a mixed conjunct implies.TestFullTextSearchSkipsRowsDeletedByDeletionVectorsandTestLuceneSearchSkipsRowsDeletedByDeletionVectors: a filter with deletion vectors, a scalar indexfile per shard, and refine across shards.
TestFilteredRowIdReader: deletion vectors, a pinned older snapshot, a tag moved to anothersnapshot, the tag fallback once the snapshot has expired (also with an empty plan), a branch tag,
and a column renamed or replaced after planning.
API and Format
New public API:
FullTextSearchBuilder::WithFilter(), andOptions::SCALAR_INDEX_SEARCH_MODEandOptions::GLOBAL_INDEX_FILTER_REFINE_FROM_DATA. The serialized form of full-text search splits,which is specific to Paimon C++, moves to version 2 and still reads version 1. No storage format or
protocol change.
Documentation
Yes.
docs/source/user_guide/global_index.rstadds a "Row Filters" section covering the predicatesplit, implied partitions, the
fastmode, excluded candidates andglobal-index.filter.refine-from-data, and the docs ofFullTextSearchBuilder,FullTextScan,FullTextReadand the new options describe the filter.Generative AI tooling
Generated-by: Claude Code 2.1.311 (Claude Opus 5.5)
🤖 Generated with Claude Code