@@ -399,6 +399,233 @@ def test_indexmissing_enables_ismissing_query(self, client, redis_url, worker_id
399399 index .delete (drop = True )
400400
401401
402+ class TestIndexEmptyIntegration :
403+ """Integration tests for INDEXEMPTY functionality."""
404+
405+ def test_text_field_index_empty_creates_successfully (
406+ self , client , redis_url , worker_id
407+ ):
408+ """Test that INDEXEMPTY on text field allows index creation."""
409+ skip_if_search_version_below_for_indexmissing (client )
410+ schema_dict = {
411+ "index" : {
412+ "name" : f"test_text_empty_{ worker_id } " ,
413+ "prefix" : f"textempty_{ worker_id } :" ,
414+ "storage_type" : "hash" ,
415+ },
416+ "fields" : [
417+ {
418+ "name" : "description" ,
419+ "type" : "text" ,
420+ "attrs" : {"index_empty" : True },
421+ }
422+ ],
423+ }
424+
425+ schema = IndexSchema .from_dict (schema_dict )
426+ index = SearchIndex (schema = schema , redis_url = redis_url )
427+ index .create (overwrite = True )
428+
429+ # Verify index was created
430+ info = client .execute_command ("FT.INFO" , f"test_text_empty_{ worker_id } " )
431+ assert info is not None
432+
433+ # Create documents with empty and non-empty values
434+ client .hset (f"textempty_{ worker_id } :1" , "description" , "has content" )
435+ client .hset (f"textempty_{ worker_id } :2" , "description" , "" )
436+ client .hset (f"textempty_{ worker_id } :3" , "description" , "more content" )
437+
438+ # Search should work, empty string doc should be indexed
439+ result = client .execute_command (
440+ "FT.SEARCH" ,
441+ f"test_text_empty_{ worker_id } " ,
442+ "*" ,
443+ )
444+ # All 3 docs should be found
445+ assert result [0 ] == 3
446+
447+ # Cleanup
448+ client .delete (
449+ f"textempty_{ worker_id } :1" ,
450+ f"textempty_{ worker_id } :2" ,
451+ f"textempty_{ worker_id } :3" ,
452+ )
453+ index .delete (drop = True )
454+
455+ def test_tag_field_index_empty_creates_successfully (
456+ self , client , redis_url , worker_id
457+ ):
458+ """Test that INDEXEMPTY on tag field allows index creation."""
459+ skip_if_search_version_below_for_indexmissing (client )
460+ schema_dict = {
461+ "index" : {
462+ "name" : f"test_tag_empty_{ worker_id } " ,
463+ "prefix" : f"tagempty_{ worker_id } :" ,
464+ "storage_type" : "hash" ,
465+ },
466+ "fields" : [
467+ {
468+ "name" : "category" ,
469+ "type" : "tag" ,
470+ "attrs" : {"index_empty" : True },
471+ }
472+ ],
473+ }
474+
475+ schema = IndexSchema .from_dict (schema_dict )
476+ index = SearchIndex (schema = schema , redis_url = redis_url )
477+ index .create (overwrite = True )
478+
479+ # Verify index was created
480+ info = client .execute_command ("FT.INFO" , f"test_tag_empty_{ worker_id } " )
481+ assert info is not None
482+
483+ # Create documents with empty and non-empty values
484+ client .hset (f"tagempty_{ worker_id } :1" , "category" , "electronics" )
485+ client .hset (f"tagempty_{ worker_id } :2" , "category" , "" )
486+ client .hset (f"tagempty_{ worker_id } :3" , "category" , "books" )
487+
488+ # Search should work
489+ result = client .execute_command (
490+ "FT.SEARCH" ,
491+ f"test_tag_empty_{ worker_id } " ,
492+ "*" ,
493+ )
494+ # All 3 docs should be found
495+ assert result [0 ] == 3
496+
497+ # Cleanup
498+ client .delete (
499+ f"tagempty_{ worker_id } :1" ,
500+ f"tagempty_{ worker_id } :2" ,
501+ f"tagempty_{ worker_id } :3" ,
502+ )
503+ index .delete (drop = True )
504+
505+
506+ class TestUnfModifierIntegration :
507+ """Integration tests for UNF (un-normalized form) modifier."""
508+
509+ def test_text_field_unf_requires_sortable (self , client , redis_url , worker_id ):
510+ """Test that UNF on text field works only when sortable is also True."""
511+ skip_if_search_version_below_for_indexmissing (client )
512+ schema_dict = {
513+ "index" : {
514+ "name" : f"test_text_unf_{ worker_id } " ,
515+ "prefix" : f"textunf_{ worker_id } :" ,
516+ "storage_type" : "hash" ,
517+ },
518+ "fields" : [
519+ {
520+ "name" : "title" ,
521+ "type" : "text" ,
522+ "attrs" : {"sortable" : True , "unf" : True },
523+ }
524+ ],
525+ }
526+
527+ schema = IndexSchema .from_dict (schema_dict )
528+ index = SearchIndex (schema = schema , redis_url = redis_url )
529+
530+ # Should create successfully
531+ index .create (overwrite = True )
532+
533+ info = client .execute_command ("FT.INFO" , f"test_text_unf_{ worker_id } " )
534+ assert info is not None
535+
536+ index .delete (drop = True )
537+
538+ def test_numeric_field_unf_with_sortable (self , client , redis_url , worker_id ):
539+ """Test that UNF on numeric field works when sortable is True."""
540+ skip_if_search_version_below_for_indexmissing (client )
541+ schema_dict = {
542+ "index" : {
543+ "name" : f"test_num_unf_{ worker_id } " ,
544+ "prefix" : f"numunf_{ worker_id } :" ,
545+ "storage_type" : "hash" ,
546+ },
547+ "fields" : [
548+ {
549+ "name" : "price" ,
550+ "type" : "numeric" ,
551+ "attrs" : {"sortable" : True , "unf" : True },
552+ }
553+ ],
554+ }
555+
556+ schema = IndexSchema .from_dict (schema_dict )
557+ index = SearchIndex (schema = schema , redis_url = redis_url )
558+
559+ # Should create successfully
560+ index .create (overwrite = True )
561+
562+ info = client .execute_command ("FT.INFO" , f"test_num_unf_{ worker_id } " )
563+ assert info is not None
564+
565+ index .delete (drop = True )
566+
567+
568+ class TestNoIndexModifierIntegration :
569+ """Integration tests for NOINDEX modifier."""
570+
571+ def test_noindex_with_sortable_allows_sorting_not_searching (
572+ self , client , redis_url , worker_id
573+ ):
574+ """Test that NOINDEX field can be sorted but not searched."""
575+ schema_dict = {
576+ "index" : {
577+ "name" : f"test_noindex_{ worker_id } " ,
578+ "prefix" : f"noindex_{ worker_id } :" ,
579+ "storage_type" : "hash" ,
580+ },
581+ "fields" : [
582+ {
583+ "name" : "searchable" ,
584+ "type" : "text" ,
585+ },
586+ {
587+ "name" : "sort_only" ,
588+ "type" : "numeric" ,
589+ "attrs" : {"sortable" : True , "no_index" : True },
590+ },
591+ ],
592+ }
593+
594+ schema = IndexSchema .from_dict (schema_dict )
595+ index = SearchIndex (schema = schema , redis_url = redis_url )
596+ index .create (overwrite = True )
597+
598+ # Add test documents
599+ client .hset (
600+ f"noindex_{ worker_id } :1" , mapping = {"searchable" : "hello" , "sort_only" : 10 }
601+ )
602+ client .hset (
603+ f"noindex_{ worker_id } :2" , mapping = {"searchable" : "world" , "sort_only" : 5 }
604+ )
605+ client .hset (
606+ f"noindex_{ worker_id } :3" , mapping = {"searchable" : "test" , "sort_only" : 15 }
607+ )
608+
609+ # Sorting by no_index field should work
610+ result = client .execute_command (
611+ "FT.SEARCH" ,
612+ f"test_noindex_{ worker_id } " ,
613+ "*" ,
614+ "SORTBY" ,
615+ "sort_only" ,
616+ "ASC" ,
617+ )
618+ assert result [0 ] == 3
619+
620+ # Cleanup
621+ client .delete (
622+ f"noindex_{ worker_id } :1" ,
623+ f"noindex_{ worker_id } :2" ,
624+ f"noindex_{ worker_id } :3" ,
625+ )
626+ index .delete (drop = True )
627+
628+
402629class TestFieldTypeModifierSupport :
403630 """Test that field types only support their documented modifiers."""
404631
0 commit comments