1212#include " core/compact_object.h"
1313#include " core/qlist.h"
1414#include " core/score_map.h"
15+ #include " core/search/block_list.h"
16+ #include " core/search/search.h"
1517#include " core/small_string.h"
1618#include " core/sorted_map.h"
1719#include " core/string_map.h"
1820#include " core/string_set.h"
1921#include " redis/redis_aux.h"
22+ #include " util/fibers/fibers.h"
2023
2124extern " C" {
2225#include " redis/zmalloc.h"
@@ -25,6 +28,7 @@ extern "C" {
2528ABSL_DECLARE_FLAG (bool , experimental_flat_json);
2629
2730using namespace dfly ;
31+ using namespace std ::chrono_literals;
2832
2933class PageUsageStatsTest : public ::testing::Test {
3034 protected:
@@ -213,3 +217,197 @@ TEST_F(PageUsageStatsTest, JSONCons) {
213217 EXPECT_EQ (json_obj->at (" count" ).as_integer <uint8_t >(), 1 );
214218 EXPECT_EQ (json_obj->at (" checked" ).as_bool (), false );
215219}
220+
221+ TEST_F (PageUsageStatsTest, QuotaChecks) {
222+ {
223+ PageUsage p{CollectPageStats::NO, 0 };
224+ EXPECT_FALSE (p.QuotaDepleted ());
225+ }
226+ {
227+ PageUsage p{CollectPageStats::NO, 0 , 4 };
228+ util::ThisFiber::SleepFor (5us);
229+ EXPECT_TRUE (p.QuotaDepleted ());
230+ }
231+ }
232+
233+ TEST_F (PageUsageStatsTest, BlockList) {
234+ search::BlockList<search::SortedVector<search::DocId>> bl{&m_, 20 };
235+ PageUsage p{CollectPageStats::NO, 0.1 };
236+ p.SetForceReallocate (true );
237+
238+ // empty list
239+ auto result = bl.Defragment (&p);
240+ EXPECT_FALSE (result.quota_depleted );
241+ EXPECT_EQ (result.objects_moved , 0 );
242+
243+ // single item will move twice, once for the blocklist and once for the sorted vector
244+ bl.Insert (1 );
245+ result = bl.Defragment (&p);
246+ EXPECT_FALSE (result.quota_depleted );
247+ EXPECT_EQ (result.objects_moved , 2 );
248+
249+ // quota depleted without defragmentation
250+ PageUsage p_zero{CollectPageStats::NO, 0.1 , 0 };
251+ p_zero.SetForceReallocate (true );
252+ result = bl.Defragment (&p_zero);
253+ EXPECT_TRUE (result.quota_depleted );
254+ EXPECT_EQ (result.objects_moved , 0 );
255+ }
256+
257+ TEST_F (PageUsageStatsTest, BlockListDefragmentResumes) {
258+ search::BlockList<search::SortedVector<search::DocId>> bl{&m_, 20 };
259+ PageUsage p{CollectPageStats::NO, 0.1 };
260+ p.SetForceReallocate (true );
261+
262+ for (size_t i = 0 ; i < 1000 ; ++i) {
263+ bl.Insert (i);
264+ }
265+
266+ PageUsage p_small_quota{CollectPageStats::NO, 0.1 , 10 };
267+ p_small_quota.SetForceReallocate (true );
268+ util::ThisFiber::SleepFor (10us);
269+ auto result = bl.Defragment (&p_small_quota);
270+ EXPECT_TRUE (result.quota_depleted );
271+ EXPECT_GE (result.objects_moved , 0 );
272+
273+ result = bl.Defragment (&p);
274+ EXPECT_FALSE (result.quota_depleted );
275+ EXPECT_GT (result.objects_moved , 0 );
276+ }
277+
278+ TEST_F (PageUsageStatsTest, BlockListWithPairs) {
279+ search::BlockList<search::SortedVector<std::pair<search::DocId, double >>> bl{&m_, 20 };
280+ PageUsage p{CollectPageStats::NO, 0.1 };
281+ p.SetForceReallocate (true );
282+
283+ for (size_t i = 0 ; i < 100 ; ++i) {
284+ bl.Insert ({i, i * 1.1 });
285+ }
286+
287+ const auto result = bl.Defragment (&p);
288+ EXPECT_FALSE (result.quota_depleted );
289+ EXPECT_GT (result.objects_moved , 0 );
290+ }
291+
292+ TEST_F (PageUsageStatsTest, BlockListWithNonDefragmentableContainer) {
293+ search::BlockList<search::CompressedSortedSet> bl{&m_, 20 };
294+ PageUsage p{CollectPageStats::NO, 0.1 };
295+ p.SetForceReallocate (true );
296+
297+ // empty list
298+ auto result = bl.Defragment (&p);
299+ EXPECT_FALSE (result.quota_depleted );
300+ EXPECT_EQ (result.objects_moved , 0 );
301+
302+ // will reallocate once for the blocklist, the inner sorted set will be skipped
303+ bl.Insert (1 );
304+ result = bl.Defragment (&p);
305+ EXPECT_FALSE (result.quota_depleted );
306+ EXPECT_EQ (result.objects_moved , 1 );
307+ }
308+
309+ class MockDocument final : public search::DocumentAccessor {
310+ public:
311+ MockDocument () {
312+ words.reserve (1000 );
313+ for (size_t i = 0 ; i < 1000 ; ++i) {
314+ words.push_back (absl::StrFormat (" word-%d" , i));
315+ }
316+ }
317+
318+ std::optional<StringList> GetStrings (std::string_view active_field) const override {
319+ return {{words[absl::GetCurrentTimeNanos () % words.size ()]}};
320+ }
321+ std::optional<VectorInfo> GetVector (std::string_view active_field) const override {
322+ return std::nullopt ;
323+ }
324+ std::optional<NumsList> GetNumbers (std::string_view active_field) const override {
325+ return {{1 , 2 , 3 , 4 }};
326+ }
327+ std::optional<StringList> GetTags (std::string_view active_field) const override {
328+ return {{words[absl::GetCurrentTimeNanos () % words.size ()]}};
329+ }
330+
331+ std::vector<std::string> words;
332+ };
333+
334+ TEST_F (PageUsageStatsTest, DefragmentTagIndex) {
335+ search::Schema schema;
336+ schema.fields [" field_name" ] =
337+ search::SchemaField{search::SchemaField::TAG, 0 , " fn" , search::SchemaField::TagParams{}};
338+ search::FieldIndices index{schema, {}, &m_, nullptr };
339+
340+ PageUsage p{CollectPageStats::NO, 0.1 };
341+ p.SetForceReallocate (true );
342+
343+ // Empty index
344+ search::DefragmentResult result = index.Defragment (&p);
345+ EXPECT_FALSE (result.quota_depleted );
346+ EXPECT_EQ (result.objects_moved , 0 );
347+
348+ const MockDocument md;
349+ index.Add (1 , md);
350+
351+ result = index.Defragment (&p);
352+ EXPECT_FALSE (result.quota_depleted );
353+ // single doc with single term returned by `GetTags` should result in two reallocations.
354+ EXPECT_EQ (result.objects_moved , 2 );
355+
356+ PageUsage p_zero{CollectPageStats::NO, 0.1 , 0 };
357+ p_zero.SetForceReallocate (true );
358+ result = index.Defragment (&p_zero);
359+ EXPECT_TRUE (result.quota_depleted );
360+ EXPECT_EQ (result.objects_moved , 0 );
361+ }
362+
363+ TEST_F (PageUsageStatsTest, TagIndexDefragResumeWithChanges) {
364+ search::Schema schema;
365+ schema.fields [" field_name" ] =
366+ search::SchemaField{search::SchemaField::TAG, 0 , " fn" , search::SchemaField::TagParams{}};
367+ search::FieldIndices index{schema, {}, &m_, nullptr };
368+
369+ PageUsage p{CollectPageStats::NO, 0.1 };
370+ p.SetForceReallocate (true );
371+
372+ const MockDocument md;
373+ for (size_t i = 0 ; i < 100 ; ++i) {
374+ index.Add (i, md);
375+ }
376+
377+ PageUsage p_small_quota{CollectPageStats::NO, 0.1 , 10 };
378+ p_small_quota.SetForceReallocate (true );
379+ search::DefragmentResult result = index.Defragment (&p_small_quota);
380+ EXPECT_TRUE (result.quota_depleted );
381+ EXPECT_GE (result.objects_moved , 0 );
382+
383+ index.Remove (99 , md);
384+
385+ for (size_t i = 200 ; i < 300 ; ++i) {
386+ index.Add (i, md);
387+ }
388+
389+ result = index.Defragment (&p);
390+ EXPECT_FALSE (result.quota_depleted );
391+ EXPECT_GT (result.objects_moved , 0 );
392+ }
393+
394+ TEST_F (PageUsageStatsTest, DefragmentIndexWithNonDefragmentableFields) {
395+ search::Schema schema;
396+ schema.fields [" text" ] =
397+ search::SchemaField{search::SchemaField::TEXT, 0 , " fn" , search::SchemaField::TextParams{}};
398+ schema.fields [" num" ] = search::SchemaField{search::SchemaField::NUMERIC, 0 , " fn" ,
399+ search::SchemaField::NumericParams{}};
400+ search::IndicesOptions options{{}};
401+ search::FieldIndices index{schema, options, &m_, nullptr };
402+
403+ PageUsage p{CollectPageStats::NO, 0.1 };
404+ p.SetForceReallocate (true );
405+
406+ const MockDocument md;
407+ index.Add (1 , md);
408+
409+ // Unsupported index types will skip defragmenting themselves
410+ const search::DefragmentResult result = index.Defragment (&p);
411+ EXPECT_FALSE (result.quota_depleted );
412+ EXPECT_EQ (result.objects_moved , 0 );
413+ }
0 commit comments