@@ -598,8 +598,173 @@ public async Task SearchDocuments_SearchIsMatchScoring_ShouldReturnResults()
598598 await indexClient . DeleteIndexAsync ( indexName ) ;
599599 }
600600
601+ // Collection field tests (issue #6)
602+
603+ [ Fact ]
604+ public async Task CollectionField_UploadAndGet_ReturnsArrayValues ( )
605+ {
606+ const string indexName = "test-collection-roundtrip" ;
607+ var indexClient = factory . CreateSearchIndexClient ( ) ;
608+ var searchClient = factory . CreateSearchClient ( indexName ) ;
609+
610+ await CreateTaggedProductIndexAsync ( indexClient , indexName ) ;
611+ await UploadTaggedProductsAsync ( searchClient ) ;
612+
613+ var doc = await searchClient . GetDocumentAsync < TaggedProduct > ( "1" ) ;
614+
615+ Assert . NotNull ( doc . Value ) ;
616+ Assert . Equal ( "1" , doc . Value . Id ) ;
617+ Assert . Equal ( new [ ] { "red" , "cotton" , "shirt" } , doc . Value . Tags ) ;
618+ Assert . Equal ( new [ ] { 8 , 10 , 12 } , doc . Value . Sizes ) ;
619+
620+ await indexClient . DeleteIndexAsync ( indexName ) ;
621+ }
622+
623+ [ Fact ]
624+ public async Task CollectionField_FilterAnyEqual_ReturnsMatchingDocuments ( )
625+ {
626+ const string indexName = "test-collection-any-eq" ;
627+ var indexClient = factory . CreateSearchIndexClient ( ) ;
628+ var searchClient = factory . CreateSearchClient ( indexName ) ;
629+
630+ await CreateTaggedProductIndexAsync ( indexClient , indexName ) ;
631+ await UploadTaggedProductsAsync ( searchClient ) ;
632+
633+ var options = new SearchOptions { Filter = "Tags/any(t: t eq 'cotton')" , Size = 50 } ;
634+ var results = await searchClient . SearchAsync < TaggedProduct > ( "*" , options ) ;
635+ var ids = ( await results . Value . GetResultsAsync ( ) . ToListAsync ( ) )
636+ . Select ( r => r . Document . Id )
637+ . OrderBy ( id => id )
638+ . ToList ( ) ;
639+
640+ // Doc 1 (Red Shirt) and doc 4 (Green Socks) both have 'cotton' in their tags.
641+ Assert . Equal ( [ "1" , "4" ] , ids ) ;
642+ }
643+
644+ [ Fact ]
645+ public async Task CollectionField_FilterAnySearchIn_ReturnsMatchingDocuments ( )
646+ {
647+ const string indexName = "test-collection-any-searchin" ;
648+ var indexClient = factory . CreateSearchIndexClient ( ) ;
649+ var searchClient = factory . CreateSearchClient ( indexName ) ;
650+
651+ await CreateTaggedProductIndexAsync ( indexClient , indexName ) ;
652+ await UploadTaggedProductsAsync ( searchClient ) ;
653+
654+ var options = new SearchOptions { Filter = "Tags/any(t: search.in(t, 'wool,denim'))" , Size = 50 } ;
655+ var results = await searchClient . SearchAsync < TaggedProduct > ( "*" , options ) ;
656+ var ids = ( await results . Value . GetResultsAsync ( ) . ToListAsync ( ) )
657+ . Select ( r => r . Document . Id )
658+ . OrderBy ( id => id )
659+ . ToList ( ) ;
660+
661+ Assert . Equal ( [ "2" , "3" ] , ids ) ;
662+ }
663+
664+ [ Fact ]
665+ public async Task CollectionField_FilterAllNotEqual_ExcludesDocsContainingValue ( )
666+ {
667+ const string indexName = "test-collection-all-ne" ;
668+ var indexClient = factory . CreateSearchIndexClient ( ) ;
669+ var searchClient = factory . CreateSearchClient ( indexName ) ;
670+
671+ await CreateTaggedProductIndexAsync ( indexClient , indexName ) ;
672+ await UploadTaggedProductsAsync ( searchClient ) ;
673+
674+ // all(t: t ne 'cotton') — only docs whose tags do NOT contain 'cotton' should match.
675+ // Doc 1 and doc 4 contain 'cotton', so only doc 2 (jeans) and doc 3 (hat) should match.
676+ var options = new SearchOptions { Filter = "Tags/all(t: t ne 'cotton')" , Size = 50 } ;
677+ var results = await searchClient . SearchAsync < TaggedProduct > ( "*" , options ) ;
678+ var ids = ( await results . Value . GetResultsAsync ( ) . ToListAsync ( ) )
679+ . Select ( r => r . Document . Id )
680+ . OrderBy ( id => id )
681+ . ToList ( ) ;
682+
683+ Assert . Equal ( [ "2" , "3" ] , ids ) ;
684+ }
685+
686+ [ Fact ]
687+ public async Task CollectionField_FilterAnyNumericRange_MatchesDocsWithAnyValueInRange ( )
688+ {
689+ const string indexName = "test-collection-any-numeric" ;
690+ var indexClient = factory . CreateSearchIndexClient ( ) ;
691+ var searchClient = factory . CreateSearchClient ( indexName ) ;
692+
693+ await CreateTaggedProductIndexAsync ( indexClient , indexName ) ;
694+ await UploadTaggedProductsAsync ( searchClient ) ;
695+
696+ // Doc 1 has Sizes [8,10,12] — 12 is >= 12. Doc 2 has Sizes [30,32,34] — all >= 12.
697+ var options = new SearchOptions { Filter = "Sizes/any(s: s ge 12)" , Size = 50 } ;
698+ var results = await searchClient . SearchAsync < TaggedProduct > ( "*" , options ) ;
699+ var ids = ( await results . Value . GetResultsAsync ( ) . ToListAsync ( ) )
700+ . Select ( r => r . Document . Id )
701+ . OrderBy ( id => id )
702+ . ToList ( ) ;
703+
704+ Assert . Equal ( [ "1" , "2" ] , ids ) ;
705+ }
706+
707+ [ Fact ]
708+ public async Task CollectionField_FullTextSearchOnSearchableCollection_MatchesAcrossElements ( )
709+ {
710+ const string indexName = "test-collection-fulltext" ;
711+ var indexClient = factory . CreateSearchIndexClient ( ) ;
712+ var searchClient = factory . CreateSearchClient ( indexName ) ;
713+
714+ await CreateTaggedProductIndexAsync ( indexClient , indexName ) ;
715+ await UploadTaggedProductsAsync ( searchClient ) ;
716+
717+ // Tags is searchable; a free-text search for 'denim' should match doc 2 even though
718+ // 'denim' is just one entry in its tags array.
719+ var options = new SearchOptions { SearchFields = { "Tags" } , Size = 50 } ;
720+ var results = await searchClient . SearchAsync < TaggedProduct > ( "denim" , options ) ;
721+ var items = await results . Value . GetResultsAsync ( ) . ToListAsync ( ) ;
722+
723+ Assert . Single ( items ) ;
724+ Assert . Equal ( "2" , items [ 0 ] . Document . Id ) ;
725+ }
726+
601727 // Helper Methods
602728
729+ private static async Task < SearchIndex > CreateTaggedProductIndexAsync ( SearchIndexClient indexClient , string indexName )
730+ {
731+ try
732+ {
733+ await indexClient . DeleteIndexAsync ( indexName ) ;
734+ }
735+ catch ( Azure . RequestFailedException ex ) when ( ex . Status == 404 )
736+ {
737+ // expected
738+ }
739+
740+ var index = new SearchIndex ( indexName )
741+ {
742+ Fields =
743+ [
744+ new SearchField ( nameof ( TaggedProduct . Id ) , SearchFieldDataType . String ) { IsKey = true , IsStored = true , IsFilterable = true } ,
745+ new SearchField ( nameof ( TaggedProduct . Name ) , SearchFieldDataType . String ) { IsSearchable = true , IsStored = true } ,
746+ new SearchField ( nameof ( TaggedProduct . Tags ) , SearchFieldDataType . Collection ( SearchFieldDataType . String ) ) { IsSearchable = true , IsFilterable = true , IsStored = true } ,
747+ new SearchField ( nameof ( TaggedProduct . Sizes ) , SearchFieldDataType . Collection ( SearchFieldDataType . Int32 ) ) { IsFilterable = true , IsStored = true }
748+ ]
749+ } ;
750+
751+ await indexClient . CreateIndexAsync ( index ) ;
752+ return index ;
753+ }
754+
755+ private static async Task UploadTaggedProductsAsync ( SearchClient searchClient )
756+ {
757+ var documents = new List < TaggedProduct >
758+ {
759+ new ( ) { Id = "1" , Name = "Red Shirt" , Tags = [ "red" , "cotton" , "shirt" ] , Sizes = [ 8 , 10 , 12 ] } ,
760+ new ( ) { Id = "2" , Name = "Blue Jeans" , Tags = [ "blue" , "denim" , "pants" ] , Sizes = [ 30 , 32 , 34 ] } ,
761+ new ( ) { Id = "3" , Name = "Wool Hat" , Tags = [ "wool" , "warm" ] , Sizes = [ ] } ,
762+ new ( ) { Id = "4" , Name = "Green Socks" , Tags = [ "green" , "cotton" ] , Sizes = [ 9 , 10 , 11 ] } ,
763+ } ;
764+ var batch = IndexDocumentsBatch . Upload ( documents ) ;
765+ await searchClient . IndexDocumentsAsync ( batch ) ;
766+ }
767+
603768 private static async Task < SearchIndex > CreateIndexAsync ( SearchIndexClient indexClient , string indexName )
604769 {
605770 // Clean up any existing index
0 commit comments