@@ -2426,6 +2426,203 @@ class InvertedIndexReaderTest : public testing::Test {
24262426 }
24272427 }
24282428
2429+ // Candidate-pushdown cache policy: only a query that actually joined the
2430+ // candidate bitmap into its evaluation (multi-term phrase) produces a
2431+ // partial result that must stay out of the query cache. A query that never
2432+ // consumes the candidate (MATCH_ANY here) still computes the full-segment
2433+ // bitmap, and a cold miss must keep filling the cache even while
2434+ // candidate_rows is published on the context.
2435+ void test_candidate_pushdown_cache_policy () {
2436+ std::string_view rowset_id = " test_candidate_cache_policy" ;
2437+ int seg_id = 0 ;
2438+
2439+ std::vector<Slice> values = {
2440+ Slice (" the quick brown fox jumps over the lazy dog" ),
2441+ Slice (" apache doris is a fast analytical database" ),
2442+ Slice (" inverted index provides fast text search capabilities" )};
2443+
2444+ TabletIndex idx_meta;
2445+ auto index_meta_pb = std::make_unique<TabletIndexPB>();
2446+ index_meta_pb->set_index_type (IndexType::INVERTED );
2447+ index_meta_pb->set_index_id (1 );
2448+ index_meta_pb->set_index_name (" test_candidate_cache_policy" );
2449+ index_meta_pb->clear_col_unique_id ();
2450+ index_meta_pb->add_col_unique_id (1 );
2451+ index_meta_pb->mutable_properties ()->insert ({" parser" , " english" });
2452+ index_meta_pb->mutable_properties ()->insert ({" lower_case" , " true" });
2453+ index_meta_pb->mutable_properties ()->insert ({" support_phrase" , " true" });
2454+ idx_meta.init_from_pb (*index_meta_pb.get ());
2455+
2456+ std::string index_path_prefix;
2457+ prepare_string_index (rowset_id, seg_id, values, &idx_meta, &index_path_prefix);
2458+
2459+ OlapReaderStatistics stats;
2460+ RuntimeState runtime_state;
2461+ TQueryOptions query_options;
2462+ query_options.enable_inverted_index_query_cache = true ;
2463+ query_options.enable_inverted_index_searcher_cache = false ;
2464+ query_options.inverted_index_max_expansions = 50 ;
2465+ runtime_state.set_query_options (query_options);
2466+
2467+ auto reader = std::make_shared<IndexFileReader>(
2468+ io::global_local_filesystem (), index_path_prefix, InvertedIndexStorageFormatPB::V2 );
2469+ EXPECT_TRUE (reader->init ().ok ());
2470+ auto fulltext_reader = FullTextIndexReader::create_shared (&idx_meta, reader);
2471+ EXPECT_NE (fulltext_reader, nullptr );
2472+
2473+ io::IOContext io_ctx;
2474+ IndexQueryContextPtr context = std::make_shared<IndexQueryContext>();
2475+ context->io_ctx = &io_ctx;
2476+ context->stats = &stats;
2477+ context->runtime_state = &runtime_state;
2478+
2479+ roaring::Roaring candidate;
2480+ candidate.add (0 );
2481+ candidate.add (1 );
2482+ context->candidate_rows = &candidate;
2483+
2484+ // MATCH_ANY never consumes the candidate: full-segment result, cacheable.
2485+ {
2486+ Field qp = Field::create_field<TYPE_STRING >(std::string (" quick database" ));
2487+
2488+ std::shared_ptr<roaring::Roaring> first = std::make_shared<roaring::Roaring>();
2489+ auto status = fulltext_reader->query (context, " 1" , qp,
2490+ InvertedIndexQueryType::MATCH_ANY_QUERY , first);
2491+ EXPECT_TRUE (status.ok ()) << status;
2492+ EXPECT_GT (first->cardinality (), 0 );
2493+
2494+ std::shared_ptr<roaring::Roaring> second = std::make_shared<roaring::Roaring>();
2495+ status = fulltext_reader->query (context, " 1" , qp,
2496+ InvertedIndexQueryType::MATCH_ANY_QUERY , second);
2497+ EXPECT_TRUE (status.ok ()) << status;
2498+ EXPECT_EQ (stats.inverted_index_query_cache_hit , 1 )
2499+ << " the full-segment result of a non-consuming query must be cached "
2500+ " even while candidate_rows is published" ;
2501+ EXPECT_EQ (*first, *second);
2502+ }
2503+
2504+ // A multi-term phrase joins the candidate into its leapfrog: its result
2505+ // is partial and must never be inserted into the cache.
2506+ {
2507+ Field qp = Field::create_field<TYPE_STRING >(std::string (" quick brown" ));
2508+
2509+ std::shared_ptr<roaring::Roaring> first = std::make_shared<roaring::Roaring>();
2510+ auto status = fulltext_reader->query (context, " 1" , qp,
2511+ InvertedIndexQueryType::MATCH_PHRASE_QUERY , first);
2512+ EXPECT_TRUE (status.ok ()) << status;
2513+ EXPECT_EQ (first->cardinality (), 1 );
2514+ EXPECT_TRUE (first->contains (0 ));
2515+
2516+ std::shared_ptr<roaring::Roaring> second = std::make_shared<roaring::Roaring>();
2517+ status = fulltext_reader->query (context, " 1" , qp,
2518+ InvertedIndexQueryType::MATCH_PHRASE_QUERY , second);
2519+ EXPECT_TRUE (status.ok ()) << status;
2520+ EXPECT_EQ (stats.inverted_index_query_cache_hit , 1 )
2521+ << " a candidate-restricted phrase result must not be served from or "
2522+ " inserted into the query cache" ;
2523+ EXPECT_EQ (*first, *second);
2524+ }
2525+
2526+ context->candidate_rows = nullptr ;
2527+ }
2528+
2529+ // The consumed flag must be re-armed per search: a range query on the
2530+ // untokenized reader never passes through match_index_search, so a stale
2531+ // flag left by an earlier candidate-consuming phrase search must not
2532+ // block its (full-segment) result from entering the cache.
2533+ void test_candidate_consumed_flag_reset_between_readers () {
2534+ std::vector<Slice> fulltext_values = {Slice (" the quick brown fox" )};
2535+ TabletIndex fulltext_meta;
2536+ auto fulltext_meta_pb = std::make_unique<TabletIndexPB>();
2537+ fulltext_meta_pb->set_index_type (IndexType::INVERTED );
2538+ fulltext_meta_pb->set_index_id (1 );
2539+ fulltext_meta_pb->set_index_name (" test_consumed_reset_ft" );
2540+ fulltext_meta_pb->clear_col_unique_id ();
2541+ fulltext_meta_pb->add_col_unique_id (1 );
2542+ fulltext_meta_pb->mutable_properties ()->insert ({" parser" , " english" });
2543+ fulltext_meta_pb->mutable_properties ()->insert ({" support_phrase" , " true" });
2544+ fulltext_meta.init_from_pb (*fulltext_meta_pb.get ());
2545+ std::string fulltext_prefix;
2546+ prepare_string_index (" test_consumed_reset_ft" , 0 , fulltext_values, &fulltext_meta,
2547+ &fulltext_prefix);
2548+
2549+ std::vector<Slice> plain_values = {Slice (" alpha" ), Slice (" beta" )};
2550+ TabletIndex plain_meta;
2551+ auto plain_meta_pb = std::make_unique<TabletIndexPB>();
2552+ plain_meta_pb->set_index_type (IndexType::INVERTED );
2553+ plain_meta_pb->set_index_id (2 );
2554+ plain_meta_pb->set_index_name (" test_consumed_reset_plain" );
2555+ plain_meta_pb->clear_col_unique_id ();
2556+ plain_meta_pb->add_col_unique_id (1 );
2557+ plain_meta.init_from_pb (*plain_meta_pb.get ());
2558+ std::string plain_prefix;
2559+ prepare_string_index (" test_consumed_reset_plain" , 0 , plain_values, &plain_meta,
2560+ &plain_prefix);
2561+
2562+ OlapReaderStatistics stats;
2563+ RuntimeState runtime_state;
2564+ TQueryOptions query_options;
2565+ query_options.enable_inverted_index_query_cache = true ;
2566+ query_options.enable_inverted_index_searcher_cache = false ;
2567+ query_options.inverted_index_max_expansions = 50 ;
2568+ runtime_state.set_query_options (query_options);
2569+
2570+ io::IOContext io_ctx;
2571+ IndexQueryContextPtr context = std::make_shared<IndexQueryContext>();
2572+ context->io_ctx = &io_ctx;
2573+ context->stats = &stats;
2574+ context->runtime_state = &runtime_state;
2575+
2576+ roaring::Roaring candidate;
2577+ candidate.add (0 );
2578+ context->candidate_rows = &candidate;
2579+
2580+ // 1) A consuming phrase search on the fulltext reader sets the flag.
2581+ {
2582+ auto reader = std::make_shared<IndexFileReader>(io::global_local_filesystem (),
2583+ fulltext_prefix,
2584+ InvertedIndexStorageFormatPB::V2 );
2585+ EXPECT_TRUE (reader->init ().ok ());
2586+ auto fulltext_reader = FullTextIndexReader::create_shared (&fulltext_meta, reader);
2587+ std::shared_ptr<roaring::Roaring> bitmap = std::make_shared<roaring::Roaring>();
2588+ Field qp = Field::create_field<TYPE_STRING >(std::string (" quick brown" ));
2589+ EXPECT_TRUE (fulltext_reader
2590+ ->query (context, " 1" , qp,
2591+ InvertedIndexQueryType::MATCH_PHRASE_QUERY , bitmap)
2592+ .ok ());
2593+ }
2594+
2595+ // 2) A range query on the untokenized reader takes the switch branch
2596+ // that bypasses match_index_search; its full-segment result must
2597+ // still be cached (second run hits).
2598+ {
2599+ auto reader = std::make_shared<IndexFileReader>(
2600+ io::global_local_filesystem (), plain_prefix, InvertedIndexStorageFormatPB::V2 );
2601+ EXPECT_TRUE (reader->init ().ok ());
2602+ auto plain_reader = StringTypeInvertedIndexReader::create_shared (&plain_meta, reader);
2603+ Field qp = Field::create_field<TYPE_STRING >(std::string (" alpha" ));
2604+
2605+ std::shared_ptr<roaring::Roaring> first = std::make_shared<roaring::Roaring>();
2606+ EXPECT_TRUE (plain_reader
2607+ ->query (context, " 1" , qp,
2608+ InvertedIndexQueryType::GREATER_EQUAL_QUERY , first)
2609+ .ok ());
2610+ EXPECT_EQ (first->cardinality (), 2 );
2611+
2612+ std::shared_ptr<roaring::Roaring> second = std::make_shared<roaring::Roaring>();
2613+ EXPECT_TRUE (plain_reader
2614+ ->query (context, " 1" , qp,
2615+ InvertedIndexQueryType::GREATER_EQUAL_QUERY , second)
2616+ .ok ());
2617+ EXPECT_EQ (stats.inverted_index_query_cache_hit , 1 )
2618+ << " a stale consumed flag from the earlier phrase search must not "
2619+ " block caching of the range query's full-segment result" ;
2620+ EXPECT_EQ (*first, *second);
2621+ }
2622+
2623+ context->candidate_rows = nullptr ;
2624+ }
2625+
24292626 // Test fulltext index with comprehensive query types
24302627 void test_fulltext_comprehensive_queries () {
24312628 std::string_view rowset_id = " test_fulltext_comprehensive" ;
@@ -4320,6 +4517,14 @@ TEST_F(InvertedIndexReaderTest, UnsupportedDataTypes) {
43204517 test_unsupported_data_types ();
43214518}
43224519
4520+ TEST_F (InvertedIndexReaderTest, CandidatePushdownCachePolicy) {
4521+ test_candidate_pushdown_cache_policy ();
4522+ }
4523+
4524+ TEST_F (InvertedIndexReaderTest, CandidateConsumedFlagResetBetweenReaders) {
4525+ test_candidate_consumed_flag_reset_between_readers ();
4526+ }
4527+
43234528// Test InvertedIndexResultBitmap operator|= with NULL handling
43244529TEST_F (InvertedIndexReaderTest, ResultBitmapOrOperatorNullHandling) {
43254530 // Test SQL three-valued logic for OR:
0 commit comments