Skip to content

Commit 18ea39e

Browse files
authored
feat(file-index): support precise file index predicate evaluation (#490)
1 parent 70c392e commit 18ea39e

55 files changed

Lines changed: 3725 additions & 1076 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/source/user_guide/file_index.rst‎

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,20 @@ File Index
2121
Full-text predicates and root TopK
2222
----------------------------------
2323

24+
Enable ``ReadContextBuilder::EnablePreciseFileIndexEvaluate(true)`` to precisely
25+
filter rows at the file-index-query stage, before reading all columns requested by
26+
the query from each data file. It evaluates the complete predicate to compute
27+
exact matching row positions within that file.
28+
29+
For a root full-text or vector TopK request, predicate filtering, row-range
30+
restrictions and deletion vectors are applied before the TopK limit. Filtering after
31+
TopK truncation could discard selected rows. The final row positions are then passed
32+
to the data-file reader to restrict the read of all requested columns.
33+
34+
The option defaults to ``false`` and requires ``file-index.read.enabled=true``.
35+
Full-text predicates and root TopK requests require this option; configuring
36+
them while it is disabled is rejected by ``ReadContextBuilder::Finish()``.
37+
2438
``PredicateBuilder::FullTextMatch`` creates a ``LeafPredicate`` for a full-text
2539
condition without TopK truncation. It holds a shared ``FullTextMatch`` request,
2640
whose query uses the same JSON DSL as ``FullTextSearch``. Do not modify a request
@@ -45,7 +59,9 @@ positions in that schema. For example, the following constructs
4559
std::shared_ptr<paimon::Predicate> filter,
4660
paimon::PredicateBuilder::Or({text, price}));
4761
paimon::ReadContextBuilder builder(table_path);
48-
builder.SetReadFieldNames({"body", "price"}).SetPredicate(filter);
62+
builder.EnablePreciseFileIndexEvaluate(true)
63+
.SetReadFieldNames({"body", "price"})
64+
.SetPredicate(filter);
4965
5066
``SetVectorSearch`` and ``SetFullTextSearch`` configure a separate root TopK
5167
request. They are mutually exclusive and require a positive limit. The intended
@@ -54,7 +70,9 @@ field does not need to be projected. A full-text match inside a predicate never
5470
has a TopK limit.
5571

5672
Score output is requested through an explicit ``_<field>_INDEX_SCORE``
57-
projection. For example, request the score of a full-text
73+
projection. Precise evaluation rejects the unnamed ``_INDEX_SCORE`` column,
74+
whether selected by field name, field ID or an explicit read schema.
75+
For example, request the score of a full-text
5876
predicate by projecting ``_body_INDEX_SCORE``:
5977

6078
.. code-block:: cpp
@@ -65,7 +83,9 @@ predicate by projecting ``_body_INDEX_SCORE``:
6583
std::shared_ptr<paimon::Predicate> text,
6684
paimon::PredicateBuilder::FullTextMatch(0, request));
6785
paimon::ReadContextBuilder builder(table_path);
68-
builder.SetReadFieldNames({"body", "_body_INDEX_SCORE"}).SetPredicate(text);
86+
builder.EnablePreciseFileIndexEvaluate(true)
87+
.SetReadFieldNames({"body", "_body_INDEX_SCORE"})
88+
.SetPredicate(text);
6989
7090
Score fields have type ``float32`` and preserve the source field's exact name,
7191
including case. Select them with ``SetReadFieldNames`` or ``SetReadSchema``;

‎include/paimon/file_index/file_index_reader.h‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
#include <cstdint>
2222
#include <memory>
23+
#include <optional>
2324
#include <vector>
2425

2526
#include "paimon/file_index/file_index_result.h"
@@ -31,6 +32,7 @@
3132
#include "paimon/visibility.h"
3233

3334
namespace paimon {
35+
class LeafPredicate;
3436
struct FullTextMatch;
3537
/// Evaluates filter predicates against a file-level index to determine file eligibility.
3638
///
@@ -42,8 +44,21 @@ struct FullTextMatch;
4244
/// of the indexed data; otherwise, index filtering results may be incorrect.
4345
class PAIMON_EXPORT FileIndexReader : public FunctionVisitor<std::shared_ptr<FileIndexResult>> {
4446
public:
47+
enum class SearchType { FULL_TEXT_MATCH, FULL_TEXT_SEARCH, VECTOR_SEARCH };
48+
4549
~FileIndexReader() override = default;
4650

51+
/// Whether this reader can execute the requested search without probing it with a query.
52+
virtual bool SupportsSearch(SearchType search_type) const;
53+
54+
/// Estimates the fraction of rows in the data file expected to match the predicate.
55+
/// Returns a value in [0, 1], or std::nullopt if no estimate is available.
56+
/// Values outside [0, 1] are also treated as unknown.
57+
/// The estimate only guides predicate execution order; it is not used to filter rows.
58+
/// TODO(lisizhuo.lsz): Add selectivity estimates for scalar indexes such as bitmap indexes.
59+
virtual std::optional<double> EstimateSelectivity(
60+
const std::shared_ptr<LeafPredicate>& predicate) const;
61+
4762
Result<std::shared_ptr<FileIndexResult>> VisitIsNotNull() override;
4863

4964
Result<std::shared_ptr<FileIndexResult>> VisitIsNull() override;
@@ -77,13 +92,16 @@ class PAIMON_EXPORT FileIndexReader : public FunctionVisitor<std::shared_ptr<Fil
7792
virtual Result<std::shared_ptr<ScoredFileIndexResult>> VisitVectorSearch(
7893
const std::shared_ptr<VectorSearch>& vector_search);
7994

80-
/// Execute full-text search and return matching file-local physical row positions.
81-
virtual Result<std::shared_ptr<FileIndexResult>> VisitFullTextSearch(
95+
/// Execute full-text TopK search and return scored file-local physical row positions.
96+
/// Scores are required for merging results across files, regardless of score projection.
97+
virtual Result<std::shared_ptr<ScoredFileIndexResult>> VisitFullTextSearch(
8298
const std::shared_ptr<FullTextSearch>& full_text_search);
8399

84100
/// Return every matching file-local physical row position, without TopK truncation.
85-
Result<std::shared_ptr<FileIndexResult>> VisitFullTextMatch(
86-
const std::shared_ptr<FullTextMatch>& full_text_match) override;
101+
/// When needs_scores is true, return a ScoredFileIndexResult, including for empty results.
102+
/// When needs_scores is false, return a BitmapIndexResult, not a ScoredFileIndexResult.
103+
virtual Result<std::shared_ptr<FileIndexResult>> VisitFullTextMatch(
104+
const std::shared_ptr<FullTextMatch>& full_text_match, bool needs_scores = false);
87105
};
88106

89107
} // namespace paimon

‎include/paimon/file_index/file_indexer.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ class PAIMON_EXPORT FileIndexer {
4747
return false;
4848
}
4949

50+
/// Create a reader for an empty index entry without serialized bytes.
51+
/// Scalar indexers use the default reader. Search indexers must preserve their capabilities.
52+
virtual std::shared_ptr<FileIndexReader> CreateEmptyReader() const;
53+
5054
/// Extra columns required while building this index, in addition to the indexed column.
5155
virtual Result<std::optional<std::vector<std::string>>> GetExtraFieldNames() const {
5256
return std::optional<std::vector<std::string>>(std::nullopt);

‎include/paimon/file_index/scored_file_index_result.h‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
namespace paimon {
3131

32-
/// File-local vector search result. Scores correspond to row positions in ascending order.
32+
/// File-local search result. Scores correspond to row positions in ascending order.
3333
class PAIMON_EXPORT ScoredFileIndexResult : public FileIndexResult {
3434
public:
3535
static Result<std::shared_ptr<ScoredFileIndexResult>> Create(RoaringBitmap32&& row_positions,

‎include/paimon/predicate/function_visitor.h‎

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,16 @@
1818

1919
#pragma once
2020

21-
#include <memory>
2221
#include <string>
2322
#include <vector>
2423

2524
#include "paimon/predicate/leaf_predicate.h"
2625
#include "paimon/predicate/literal.h"
2726
#include "paimon/result.h"
28-
#include "paimon/status.h"
2927
#include "paimon/visibility.h"
3028

3129
namespace paimon {
32-
struct FullTextMatch;
33-
34-
/// A visitor interface for evaluating filter predicates on indexed columns.
30+
/// A visitor interface for evaluating scalar filter predicates on indexed columns.
3531
/// @tparam T The result type produced by each visit method (e.g., a file index result or global
3632
/// index result).
3733
template <typename T>
@@ -80,11 +76,5 @@ class PAIMON_EXPORT FunctionVisitor {
8076

8177
/// Evaluates whether string values like the given string.
8278
virtual Result<T> VisitLike(const Literal& literal) = 0;
83-
84-
/// Evaluates a full-text condition without TopK truncation.
85-
/// Visitors without full-text support return NotImplemented.
86-
virtual Result<T> VisitFullTextMatch(const std::shared_ptr<FullTextMatch>& full_text_match) {
87-
return Status::NotImplemented("This visitor does not support complete full-text matching");
88-
}
8979
};
9080
} // namespace paimon

‎include/paimon/predicate/predicate_utils.h‎

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class PAIMON_EXPORT PredicateUtils {
6464
/// Whether the predicate contains a full-text matching condition.
6565
static bool ContainsFullTextMatch(const std::shared_ptr<Predicate>& predicate);
6666

67+
/// Dispatches scalar leaf predicates. Search predicates use dedicated index reader methods.
6768
template <typename T>
6869
static Result<T> VisitPredicate(const std::shared_ptr<LeafPredicate>& predicate,
6970
const std::shared_ptr<FunctionVisitor<T>>& visitor) {
@@ -117,21 +118,13 @@ class PAIMON_EXPORT PredicateUtils {
117118
assert(predicate->Literals().size() == 1);
118119
return visitor->VisitLike(predicate->Literals()[0]);
119120
}
120-
case Function::Type::FULL_TEXT_MATCH: {
121-
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<FullTextMatch> full_text_match,
122-
GetFullTextMatch(predicate));
123-
return visitor->VisitFullTextMatch(full_text_match);
124-
}
125121
default:
126122
return Status::Invalid("invalid " + predicate->GetFunction().ToString() +
127123
" function in leaf predicate");
128124
}
129125
}
130126

131127
private:
132-
static Result<std::shared_ptr<FullTextMatch>> GetFullTextMatch(
133-
const std::shared_ptr<LeafPredicate>& predicate);
134-
135128
static Result<std::optional<std::shared_ptr<Predicate>>> ReconstructPredicateWithPickedFields(
136129
const std::shared_ptr<Predicate>& predicate,
137130
const std::map<std::string, int32_t>& picked_field_name_to_idx);

‎include/paimon/read_context.h‎

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -52,23 +52,26 @@ struct VectorSearch;
5252
/// @see ReadContextBuilder
5353
class PAIMON_EXPORT ReadContext {
5454
public:
55-
ReadContext(
56-
const std::string& path, const std::string& branch,
57-
const std::vector<std::string>& read_field_names,
58-
const std::vector<int32_t>& read_field_ids, const std::shared_ptr<Predicate>& predicate,
59-
const std::shared_ptr<VectorSearch>& vector_search,
60-
const std::shared_ptr<FullTextSearch>& full_text_search, bool enable_predicate_filter,
61-
bool enable_prefetch, bool enable_late_materializing, uint32_t prefetch_batch_count,
62-
uint32_t prefetch_max_parallel_num, bool enable_multi_thread_row_to_batch,
63-
uint32_t row_to_batch_thread_number, const std::optional<std::string>& table_schema,
64-
const std::shared_ptr<MemoryPool>& memory_pool, const std::shared_ptr<Executor>& executor,
65-
const std::shared_ptr<FileSystem>& specific_file_system,
66-
const std::map<std::string, std::string>& fs_scheme_to_identifier_map,
67-
const std::shared_ptr<RealtimeContext>& realtime_context,
68-
const std::map<std::string, std::string>& options, bool read_ahead_cache_enabled,
69-
const CacheConfig& cache_config, const std::shared_ptr<Cache>& cache,
70-
const std::shared_ptr<FormatTable>& format_table, WarmupLevel warmup_level,
71-
const std::shared_ptr<Catalog>& catalog, const std::optional<Identifier>& identifier);
55+
ReadContext(const std::string& path, const std::string& branch,
56+
const std::vector<std::string>& read_field_names,
57+
const std::vector<int32_t>& read_field_ids,
58+
const std::shared_ptr<Predicate>& predicate,
59+
const std::shared_ptr<VectorSearch>& vector_search,
60+
const std::shared_ptr<FullTextSearch>& full_text_search,
61+
bool enable_precise_file_index_evaluate, bool enable_predicate_filter,
62+
bool enable_prefetch, bool enable_late_materializing, uint32_t prefetch_batch_count,
63+
uint32_t prefetch_max_parallel_num, bool enable_multi_thread_row_to_batch,
64+
uint32_t row_to_batch_thread_number, const std::optional<std::string>& table_schema,
65+
const std::shared_ptr<MemoryPool>& memory_pool,
66+
const std::shared_ptr<Executor>& executor,
67+
const std::shared_ptr<FileSystem>& specific_file_system,
68+
const std::map<std::string, std::string>& fs_scheme_to_identifier_map,
69+
const std::shared_ptr<RealtimeContext>& realtime_context,
70+
const std::map<std::string, std::string>& options, bool read_ahead_cache_enabled,
71+
const CacheConfig& cache_config, const std::shared_ptr<Cache>& cache,
72+
const std::shared_ptr<FormatTable>& format_table, WarmupLevel warmup_level,
73+
const std::shared_ptr<Catalog>& catalog,
74+
const std::optional<Identifier>& identifier);
7275

7376
~ReadContext();
7477

@@ -113,6 +116,10 @@ class PAIMON_EXPORT ReadContext {
113116
return vector_search_ != nullptr || full_text_search_ != nullptr;
114117
}
115118

119+
bool EnablePreciseFileIndexEvaluate() const {
120+
return enable_precise_file_index_evaluate_;
121+
}
122+
116123
bool EnablePredicateFilter() const {
117124
return enable_predicate_filter_;
118125
}
@@ -208,6 +215,7 @@ class PAIMON_EXPORT ReadContext {
208215
std::shared_ptr<Predicate> predicate_;
209216
std::shared_ptr<VectorSearch> vector_search_;
210217
std::shared_ptr<FullTextSearch> full_text_search_;
218+
bool enable_precise_file_index_evaluate_;
211219
bool enable_predicate_filter_;
212220
bool enable_prefetch_;
213221
bool enable_late_materializing_;
@@ -352,39 +360,52 @@ class PAIMON_EXPORT ReadContextBuilder {
352360
///
353361
/// All referenced fields, including full-text match fields, must be projected in the read
354362
/// schema.
355-
/// TODO(lisizhuo.lsz): The behavior here will be aligned with Java Paimon in a follow-up
356-
/// change: fields referenced in predicates will no longer be required to appear in the
357-
/// projection. Full-text matches can be combined with scalar predicates using
358-
/// PredicateBuilder::And/Or. Full-text predicate execution is not implemented yet; reading such
359-
/// a predicate returns NotImplemented.
363+
/// TODO(lisizhuo.lsz): Align with Java Paimon so fields referenced by predicates no longer need
364+
/// to appear in the projection.
365+
/// Full-text matches can be combined with scalar predicates using PredicateBuilder::And/Or.
366+
/// Predicates on virtual index score columns (_INDEX_SCORE or _<field>_INDEX_SCORE) are not
367+
/// supported.
360368
///
361369
/// @param predicate Shared pointer to the predicate for data filtering.
362370
/// @return Reference to this builder for method chaining.
371+
/// @note Predicates containing FullTextMatch require EnablePreciseFileIndexEvaluate(true).
363372
ReadContextBuilder& SetPredicate(const std::shared_ptr<Predicate>& predicate);
364373

365374
/// Configure the root vector TopK request, logically applied after the predicate filter.
366375
/// The search field need not be projected. SetVectorSearch is mutually exclusive with
367376
/// SetFullTextSearch and must have a positive limit.
368377
/// Scores are required internally for TopK. Project `_<field>_INDEX_SCORE` to include them
369-
/// in the result. Per-field score output is not implemented yet; reading such a projection
370-
/// returns NotImplemented.
371-
/// TODO(lisizhuo.lsz): Execute combined read predicates and merge TopK across files.
378+
/// in the result.
379+
/// TODO(lisizhuo.lsz): Merge TopK across files.
372380
/// @param vector_search Vector search request, or nullptr to clear it.
373381
/// @return Reference to this builder for method chaining.
382+
/// @note Requires EnablePreciseFileIndexEvaluate(true).
374383
ReadContextBuilder& SetVectorSearch(const std::shared_ptr<VectorSearch>& vector_search);
375384

376385
/// Configure the root full-text TopK request, logically applied after the predicate filter.
377386
/// The search field need not be projected. Use PredicateBuilder::FullTextMatch for a
378387
/// full-text filter without TopK truncation. SetFullTextSearch is mutually exclusive with
379388
/// SetVectorSearch and must have a positive limit.
380389
/// Scores are required internally for TopK. Project `_<field>_INDEX_SCORE` to include them
381-
/// in the result. Per-field score output is not implemented yet; reading such a projection
382-
/// returns NotImplemented.
383-
/// TODO(lisizhuo.lsz): Execute combined read predicates and merge TopK across files.
390+
/// in the result.
391+
/// TODO(lisizhuo.lsz): Merge TopK across files.
384392
/// @param full_text_search Full-text search request, or nullptr to clear it.
385393
/// @return Reference to this builder for method chaining.
394+
/// @note Requires EnablePreciseFileIndexEvaluate(true).
386395
ReadContextBuilder& SetFullTextSearch(const std::shared_ptr<FullTextSearch>& full_text_search);
387396

397+
/// Compute exact file-local result row positions during file-index evaluation, using the
398+
/// configured Predicate and optional VectorSearch or FullTextSearch request.
399+
/// Requires file-index.read.enabled=true. Reads available file indexes and rechecks scalar
400+
/// index candidates against the data. If a scalar predicate's field has no index, reads the
401+
/// data to compute exact matching rows. Only scalar predicates may be evaluated without an
402+
/// index. Score output must use named projections such as _<field>_INDEX_SCORE;
403+
/// projecting _INDEX_SCORE by name, field ID or read schema is not supported in this mode.
404+
/// The default is false.
405+
/// @param enabled Whether to compute exact result row positions during file-index evaluation.
406+
/// @return Reference to this builder for method chaining.
407+
ReadContextBuilder& EnablePreciseFileIndexEvaluate(bool enabled);
408+
388409
/// Whether to perform precise filtering according to predicates for data read from format
389410
/// reader.
390411
/// @param enabled Whether to enable precise filtering (default: false)

‎src/paimon/CMakeLists.txt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ set(PAIMON_COMMON_SRCS
6969
common/factories/io_hook.cpp
7070
common/factories/factory_creator.cpp
7171
common/file_index/bitmap/bitmap_index_result.cpp
72+
common/file_index/file_indexer.cpp
7273
common/file_index/file_indexer_factory.cpp
7374
common/file_index/file_index_format.cpp
7475
common/file_index/file_index_format_utils.cpp
@@ -412,6 +413,7 @@ set(PAIMON_CORE_SRCS
412413
core/operation/orphan_files_cleaner.cpp
413414
core/operation/orphan_files_cleaner_impl.cpp
414415
core/operation/raw_file_split_read.cpp
416+
core/operation/precise_file_index_read.cpp
415417
core/operation/read_context.cpp
416418
core/operation/scan_context.cpp
417419
core/manifest/snapshot_live_manifest_entries.cpp
@@ -992,6 +994,7 @@ if(PAIMON_BUILD_TESTS)
992994
core/operation/commit/realtime_deduplicate_dv_materializer_test.cpp
993995
core/operation/commit/sequence_snapshot_properties_test.cpp
994996
core/operation/key_value_file_store_write_test.cpp
997+
core/operation/precise_file_index_read_test.cpp
995998
core/operation/internal_read_context_test.cpp
996999
core/operation/abstract_split_read_test.cpp
9971000
core/operation/append_only_file_store_write_test.cpp

0 commit comments

Comments
 (0)