├── .gitignore ├── .paket ├── Paket.Restore.targets ├── paket.exe └── paket.targets ├── Directory.build.props ├── FluentNest.Tests ├── .gitignore ├── Benchmarking.cs ├── DeleteTests.cs ├── EnumTests.cs ├── FilterTests.cs ├── FilterTests.txt ├── FluentNest.Tests.csproj ├── GroupByTests.cs ├── HistogramTests.cs ├── Model │ ├── Car.cs │ ├── CarType.cs │ └── User.cs ├── PropertyNamesTests.cs ├── StatisticsTests.cs ├── TestsBase.cs ├── app.config ├── packages.config └── paket.references ├── FluentNest.sln ├── FluentNest ├── .gitignore ├── AggType.cs ├── AggsContainer.cs ├── Filters.cs ├── FluentNest.csproj ├── GroupBys.cs ├── Names.cs ├── Ranges.cs ├── SortType.cs ├── Statistics.cs ├── StatisticsGetters.cs └── paket.references ├── LICENSE ├── README.md ├── appveyor.yml ├── build.cmd ├── global.json ├── paket.cmd ├── paket.dependencies ├── paket.lock ├── run_es.ps1 └── tests.cmd /.gitignore: -------------------------------------------------------------------------------- 1 | es.zip 2 | /elasticsearch-* 3 | paket-files/*.cached 4 | 5 | ## Ignore Visual Studio temporary files, build results, and 6 | ## files generated by popular Visual Studio add-ons. 7 | 8 | # User-specific files 9 | *.suo 10 | *.user 11 | *.userosscache 12 | *.sln.docstates 13 | 14 | # User-specific files (MonoDevelop/Xamarin Studio) 15 | *.userprefs 16 | 17 | # Build results 18 | [Dd]ebug/ 19 | [Dd]ebugPublic/ 20 | [Rr]elease/ 21 | [Rr]eleases/ 22 | x64/ 23 | x86/ 24 | build/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | 29 | # Visual Studo 2015 cache/options directory 30 | .vs/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | *_i.c 46 | *_p.c 47 | *_i.h 48 | *.ilk 49 | *.meta 50 | *.obj 51 | *.pch 52 | *.pdb 53 | *.pgc 54 | *.pgd 55 | *.rsp 56 | *.sbr 57 | *.tlb 58 | *.tli 59 | *.tlh 60 | *.tmp 61 | *.tmp_proj 62 | *.log 63 | *.vspscc 64 | *.vssscc 65 | .builds 66 | *.pidb 67 | *.svclog 68 | *.scc 69 | 70 | # Chutzpah Test files 71 | _Chutzpah* 72 | 73 | # Visual C++ cache files 74 | ipch/ 75 | *.aps 76 | *.ncb 77 | *.opensdf 78 | *.sdf 79 | *.cachefile 80 | 81 | # Visual Studio profiler 82 | *.psess 83 | *.vsp 84 | *.vspx 85 | 86 | # TFS 2012 Local Workspace 87 | $tf/ 88 | 89 | # Guidance Automation Toolkit 90 | *.gpState 91 | 92 | # ReSharper is a .NET coding add-in 93 | _ReSharper*/ 94 | *.[Rr]e[Ss]harper 95 | *.DotSettings.user 96 | 97 | # JustCode is a .NET coding addin-in 98 | .JustCode 99 | 100 | # TeamCity is a build add-in 101 | _TeamCity* 102 | 103 | # DotCover is a Code Coverage Tool 104 | *.dotCover 105 | 106 | # NCrunch 107 | _NCrunch_* 108 | .*crunch*.local.xml 109 | 110 | # MightyMoose 111 | *.mm.* 112 | AutoTest.Net/ 113 | 114 | # Web workbench (sass) 115 | .sass-cache/ 116 | 117 | # Installshield output folder 118 | [Ee]xpress/ 119 | 120 | # DocProject is a documentation generator add-in 121 | DocProject/buildhelp/ 122 | DocProject/Help/*.HxT 123 | DocProject/Help/*.HxC 124 | DocProject/Help/*.hhc 125 | DocProject/Help/*.hhk 126 | DocProject/Help/*.hhp 127 | DocProject/Help/Html2 128 | DocProject/Help/html 129 | 130 | # Click-Once directory 131 | publish/ 132 | 133 | # Publish Web Output 134 | *.[Pp]ublish.xml 135 | *.azurePubxml 136 | # TODO: Comment the next line if you want to checkin your web deploy settings 137 | # but database connection strings (with potential passwords) will be unencrypted 138 | *.pubxml 139 | *.publishproj 140 | 141 | # NuGet Packages 142 | *.nupkg 143 | # The packages folder can be ignored because of Package Restore 144 | **/packages/* 145 | # except build/, which is used as an MSBuild target. 146 | !**/packages/build/ 147 | # Uncomment if necessary however generally it will be regenerated when needed 148 | #!**/packages/repositories.config 149 | 150 | # Windows Azure Build Output 151 | csx/ 152 | *.build.csdef 153 | 154 | # Windows Store app package directory 155 | AppPackages/ 156 | 157 | # Others 158 | *.[Cc]ache 159 | ClientBin/ 160 | [Ss]tyle[Cc]op.* 161 | ~$* 162 | *~ 163 | *.dbmdl 164 | *.dbproj.schemaview 165 | *.pfx 166 | *.publishsettings 167 | node_modules/ 168 | bower_components/ 169 | 170 | # RIA/Silverlight projects 171 | Generated_Code/ 172 | 173 | # Backup & report files from converting an old project file 174 | # to a newer Visual Studio version. Backup files are not needed, 175 | # because we have git ;-) 176 | _UpgradeReport_Files/ 177 | Backup*/ 178 | UpgradeLog*.XML 179 | UpgradeLog*.htm 180 | 181 | # SQL Server files 182 | *.mdf 183 | *.ldf 184 | 185 | # Business Intelligence projects 186 | *.rdl.data 187 | *.bim.layout 188 | *.bim_*.settings 189 | 190 | # Microsoft Fakes 191 | FakesAssemblies/ 192 | 193 | # Node.js Tools for Visual Studio 194 | .ntvs_analysis.dat 195 | 196 | # Visual Studio 6 build log 197 | *.plg 198 | 199 | # Visual Studio 6 workspace options file 200 | *.opt 201 | 202 | *.v2.ncrunchsolution 203 | -------------------------------------------------------------------------------- /.paket/Paket.Restore.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 8 | 9 | true 10 | $(MSBuildThisFileDirectory) 11 | $(MSBuildThisFileDirectory)..\ 12 | $(PaketRootPath)paket-files\paket.restore.cached 13 | $(PaketRootPath)paket.lock 14 | /Library/Frameworks/Mono.framework/Commands/mono 15 | mono 16 | 17 | $(PaketRootPath)paket.exe 18 | $(PaketToolsPath)paket.exe 19 | "$(PaketExePath)" 20 | $(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" 21 | 22 | 23 | <_PaketExeExtension>$([System.IO.Path]::GetExtension("$(PaketExePath)")) 24 | dotnet "$(PaketExePath)" 25 | 26 | 27 | "$(PaketExePath)" 28 | 29 | $(PaketRootPath)paket.bootstrapper.exe 30 | $(PaketToolsPath)paket.bootstrapper.exe 31 | "$(PaketBootStrapperExePath)" 32 | $(MonoPath) --runtime=v4.0.30319 "$(PaketBootStrapperExePath)" 33 | 34 | 35 | 36 | 37 | true 38 | true 39 | 40 | 41 | 42 | 43 | 44 | 45 | true 46 | $(NoWarn);NU1603 47 | 48 | 49 | 50 | 51 | /usr/bin/shasum $(PaketRestoreCacheFile) | /usr/bin/awk '{ print $1 }' 52 | /usr/bin/shasum $(PaketLockFilePath) | /usr/bin/awk '{ print $1 }' 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | $([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)')) 66 | $([System.IO.File]::ReadAllText('$(PaketLockFilePath)')) 67 | true 68 | false 69 | true 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | $(MSBuildProjectDirectory)\obj\$(MSBuildProjectFile).paket.references.cached 79 | 80 | $(MSBuildProjectFullPath).paket.references 81 | 82 | $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references 83 | 84 | $(MSBuildProjectDirectory)\paket.references 85 | $(MSBuildProjectDirectory)\obj\$(MSBuildProjectFile).$(TargetFramework).paket.resolved 86 | true 87 | references-file-or-cache-not-found 88 | 89 | 90 | 91 | 92 | $([System.IO.File]::ReadAllText('$(PaketReferencesCachedFilePath)')) 93 | $([System.IO.File]::ReadAllText('$(PaketOriginalReferencesFilePath)')) 94 | references-file 95 | false 96 | 97 | 98 | 99 | 100 | false 101 | 102 | 103 | 104 | 105 | true 106 | target-framework '$(TargetFramework)' 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0]) 124 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1]) 125 | $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[4]) 126 | 127 | 128 | %(PaketReferencesFileLinesInfo.PackageVersion) 129 | All 130 | 131 | 132 | 133 | 134 | $(MSBuildProjectDirectory)/obj/$(MSBuildProjectFile).paket.clitools 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[0]) 144 | $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[1]) 145 | 146 | 147 | %(PaketCliToolFileLinesInfo.PackageVersion) 148 | 149 | 150 | 151 | 155 | 156 | 157 | 158 | 159 | 160 | false 161 | 162 | 163 | 164 | 165 | 166 | <_NuspecFilesNewLocation Include="$(BaseIntermediateOutputPath)$(Configuration)\*.nuspec"/> 167 | 168 | 169 | 170 | $(MSBuildProjectDirectory)/$(MSBuildProjectFile) 171 | true 172 | false 173 | true 174 | $(BaseIntermediateOutputPath)$(Configuration) 175 | $(BaseIntermediateOutputPath) 176 | 177 | 178 | 179 | <_NuspecFiles Include="$(AdjustedNuspecOutputPath)\*.nuspec"/> 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 232 | 233 | 274 | 275 | 276 | 277 | -------------------------------------------------------------------------------- /.paket/paket.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoonzis/fluentnest/92c6b01aac12c8ced260aa847bd4908b795fb56e/.paket/paket.exe -------------------------------------------------------------------------------- /.paket/paket.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | 7 | true 8 | $(MSBuildThisFileDirectory) 9 | $(MSBuildThisFileDirectory)..\ 10 | /Library/Frameworks/Mono.framework/Commands/mono 11 | mono 12 | 13 | 14 | 15 | $(PaketToolsPath)paket.exe 16 | $(PaketToolsPath)paket.bootstrapper.exe 17 | "$(PaketExePath)" 18 | $(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" 19 | "$(PaketBootStrapperExePath)" $(PaketBootStrapperCommandArgs) 20 | $(MonoPath) --runtime=v4.0.30319 $(PaketBootStrapperExePath) $(PaketBootStrapperCommandArgs) 21 | 22 | $(MSBuildProjectDirectory)\paket.references 23 | $(MSBuildStartupDirectory)\paket.references 24 | $(MSBuildProjectFullPath).paket.references 25 | $(PaketCommand) restore --references-files "$(PaketReferences)" 26 | $(PaketBootStrapperCommand) 27 | 28 | RestorePackages; $(BuildDependsOn); 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Directory.build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | FluentNest 4 | Jan Fajfr 5 | Fluent query language for ElasticSearch 6 | https://github.com/hoonzis/fluentnest 7 | Copyright © Jan Fajfr 2015 8 | ElasticSearch search NEST 9 | 1.0.0.0 10 | 1.0.0.0 11 | 12 | -------------------------------------------------------------------------------- /FluentNest.Tests/.gitignore: -------------------------------------------------------------------------------- 1 | *.v2.ncrunchproject 2 | -------------------------------------------------------------------------------- /FluentNest.Tests/Benchmarking.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using FluentNest.Tests.Model; 5 | using Nest; 6 | using NFluent; 7 | using Tests; 8 | using Xunit; 9 | 10 | namespace FluentNest.Tests 11 | { 12 | public class Benchmarking : TestsBase 13 | { 14 | private string AddSimpleTestData() 15 | { 16 | var indexName = "index_" + Guid.NewGuid(); 17 | 18 | Client.CreateIndex(indexName, x => x.Mappings(m => m 19 | .Map(t => t 20 | .Properties(prop => prop.Keyword(str => str.Name(s => s.Guid))) 21 | .Properties(prop => prop.Keyword(str => str.Name(s => s.Email))) 22 | ))); 23 | 24 | var cars = new List(); 25 | for (int i = 0; i < 10000; i++) 26 | { 27 | var car = new Car 28 | { 29 | Id = Guid.NewGuid(), 30 | Timestamp = new DateTime(2010, (i % 12) + 1, 1), 31 | Name = "name" + i % 3, 32 | Price = 10, 33 | Sold = i % 2 == 0, 34 | CarType = "Type" + i % 2, 35 | Emissions = i + 1, 36 | Guid = "test-" + i, 37 | Email = "Email@email" + i % 2 + ".com", 38 | Age = i + 1, 39 | Enabled = i % 2 == 0, 40 | Active = i % 2 == 0, 41 | EngineType = i % 2 == 0 ? EngineType.Diesel : EngineType.Standard 42 | }; 43 | cars.Add(car); 44 | } 45 | Client.Bulk(x => x.CreateMany(cars).Index(indexName)); 46 | Client.Flush(indexName); 47 | return indexName; 48 | } 49 | 50 | // This should confirm the theory that a two side range is faster then a bool with 2-one sided ranges 51 | [Fact] 52 | public void WithMergedRange() 53 | { 54 | var stopWatch = new Stopwatch(); 55 | var index = AddSimpleTestData(); 56 | var sc = new SearchDescriptor().Index(index).FilterOn(x => x.Emissions > 2 && x.Emissions < 6 && x.Price < 20); 57 | var json = Serialize(sc); 58 | Console.WriteLine(json); 59 | 60 | stopWatch.Start(); 61 | var allCars = Client.Search(sc); 62 | stopWatch.Stop(); 63 | 64 | Console.WriteLine("Query time:" + stopWatch.Elapsed); 65 | Check.That(allCars.Documents).HasSize(3); 66 | } 67 | 68 | [Fact] 69 | public void WithoutMergedRange() 70 | { 71 | var stopWatch = new Stopwatch(); 72 | var index = AddSimpleTestData(); 73 | var sc = new SearchDescriptor().Index(index).FilterOn(x => x.Emissions > 2 && x.Price < 20 && x.Emissions < 6); 74 | var json = Serialize(sc); 75 | Console.WriteLine(json); 76 | 77 | stopWatch.Start(); 78 | var allCars2 = Client.Search(sc); 79 | stopWatch.Stop(); 80 | 81 | Console.WriteLine("Query time:" + stopWatch.Elapsed); 82 | Check.That(allCars2.Documents).HasSize(3); 83 | } 84 | 85 | [Fact] 86 | public void WithMergedAndFilters() 87 | { 88 | var stopWatch = new Stopwatch(); 89 | var index = AddSimpleTestData(); 90 | var sc = 91 | new SearchDescriptor().Index(index).FilterOn( 92 | x => 93 | x.Emissions < 6 && x.Sold == true && x.Price > 4 && x.EngineType == EngineType.Diesel && 94 | x.Length < 4); 95 | 96 | var json = Serialize(sc); 97 | Console.WriteLine(json); 98 | 99 | stopWatch.Start(); 100 | var cars = Client.Search(sc); 101 | stopWatch.Stop(); 102 | 103 | Console.WriteLine("Query time:" + stopWatch.Elapsed); 104 | Check.That(cars.Documents).HasSize(3); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /FluentNest.Tests/DeleteTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentNest.Tests.Model; 3 | using Nest; 4 | using NFluent; 5 | using Tests; 6 | using Xunit; 7 | 8 | namespace FluentNest.Tests 9 | { 10 | public class DeleteTests : TestsBase 11 | { 12 | public string AddSimpleTestData() 13 | { 14 | var indexName = "index_" + Guid.NewGuid(); 15 | Client.CreateIndex(indexName, x => x.Mappings( 16 | m => m.Map(t => t 17 | .Properties(prop => prop.Keyword(str => str.Name(s => s.EngineType)))))); 18 | 19 | for (int i = 0; i < 10; i++) 20 | { 21 | var car = new Car 22 | { 23 | Id = Guid.NewGuid(), 24 | Timestamp = new DateTime(2010, i + 1, 1), 25 | Sold = i % 2 == 0, 26 | CarType = "Type" + i % 3, 27 | Length = i, 28 | EngineType = i % 2 == 0 ? EngineType.Diesel : EngineType.Standard, 29 | Weight = 5 30 | }; 31 | 32 | Client.Index(car, ind => ind.Index(indexName)); 33 | } 34 | Client.Flush(indexName); 35 | return indexName; 36 | } 37 | 38 | [Fact] 39 | public void DeleteByQuery() 40 | { 41 | var index = AddSimpleTestData(); 42 | var deleteResult = Client.DeleteByQuery(s => s.FilterOn(x => x.Sold).Index(index).Type(Types.AllTypes)); 43 | Check.That(deleteResult.IsValid).IsTrue(); 44 | Client.Refresh(index); 45 | var result = Client.Search(sc=>sc.Index(index).MatchAll()); 46 | Check.That(result.Hits).HasSize(5); 47 | Client.DeleteIndex(index); 48 | } 49 | 50 | [Fact] 51 | public void DeleteByQuery_FilterCreatedSeparately() 52 | { 53 | var index = AddSimpleTestData(); 54 | var filter = Filters.CreateFilter(x => x.EngineType == EngineType.Diesel); 55 | var deleteResult = Client.DeleteByQuery(s => s.FilterOn(filter).Index(index).Type(Types.AllTypes)); 56 | Check.That(deleteResult.IsValid).IsTrue(); 57 | Client.Refresh(index); 58 | var result = Client.Search(sc => sc.Index(index).MatchAll()); 59 | Check.That(result.Hits).HasSize(5); 60 | Client.DeleteIndex(index); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /FluentNest.Tests/EnumTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Elasticsearch.Net; 5 | using FluentNest.Tests.Model; 6 | using Nest; 7 | using Nest.JsonNetSerializer; 8 | using Newtonsoft.Json; 9 | using Newtonsoft.Json.Converters; 10 | using NFluent; 11 | using Tests; 12 | using Xunit; 13 | 14 | namespace FluentNest.Tests 15 | { 16 | public class EnumTests : TestsBase 17 | { 18 | private class StringEnumContractSerializer : ConnectionSettingsAwareSerializerBase 19 | { 20 | public StringEnumContractSerializer(IElasticsearchSerializer builtinSerializer, IConnectionSettingsValues connectionSettings) 21 | : base(builtinSerializer, connectionSettings) 22 | { 23 | } 24 | 25 | protected override IEnumerable CreateJsonConverters() 26 | { 27 | return base.CreateJsonConverters().Concat(new[] {new StringEnumConverter()}); 28 | } 29 | } 30 | 31 | public EnumTests() 32 | : base((builtIn, values) => new StringEnumContractSerializer(builtIn, values)) 33 | { 34 | 35 | } 36 | 37 | public string AddSimpleTestData() 38 | { 39 | var indexName = "index_" + Guid.NewGuid(); 40 | Client.CreateIndex(indexName, x => x.Mappings( 41 | m => m.Map(t => t 42 | .Properties(prop => prop 43 | .Keyword(str => str.Name(s => s.EngineType)) 44 | .Keyword(str => str.Name(s => s.NullableEngineType)))))); 45 | 46 | for (int i = 0; i < 10; i++) 47 | { 48 | var car = new Car 49 | { 50 | Id = Guid.NewGuid(), 51 | Timestamp = new DateTime(2010, i + 1, 1), 52 | Name = "Car" + i, 53 | Price = 10, 54 | Sold = i % 2 == 0, 55 | CarType = "Type" + i % 3, 56 | Length = i, 57 | EngineType = i % 2 == 0 ? EngineType.Diesel : EngineType.Standard, 58 | NullableEngineType = i % 2 == 0 ? EngineType.Diesel : EngineType.Standard, 59 | Weight = 5, 60 | ConditionalRanking = i % 2 == 0 ? null : (int?)i, 61 | Description = "Desc" + i, 62 | }; 63 | Client.Index(car, ind => ind.Index(indexName)); 64 | } 65 | Client.Flush(indexName); 66 | return indexName; 67 | } 68 | 69 | [Fact] 70 | public void Filtering_on_enum_property_should_work() 71 | { 72 | var index = AddSimpleTestData(); 73 | var result = Client.Search(s => s.Index(index).FilterOn(x => x.EngineType == EngineType.Diesel)); 74 | Check.That(result.Hits.Count()).IsEqualTo(5); 75 | Client.DeleteIndex(index); 76 | } 77 | 78 | [Fact] 79 | public void Filtering_on_nullable_enum_property_should_work() 80 | { 81 | var index = AddSimpleTestData(); 82 | var result = Client.Search(s => s.Index(index).FilterOn(x => x.NullableEngineType == EngineType.Diesel)); 83 | Check.That(result.Hits.Count()).IsEqualTo(5); 84 | Client.DeleteIndex(index); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /FluentNest.Tests/FilterTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using FluentNest.Tests.Model; 5 | using Nest; 6 | using NFluent; 7 | using Tests; 8 | using Xunit; 9 | 10 | namespace FluentNest.Tests 11 | { 12 | public class FilterTests : TestsBase 13 | { 14 | private const string MyFavoriteGuid = "test-test"; 15 | 16 | private string AddSimpleTestData() 17 | { 18 | var indexName = "index_" + Guid.NewGuid(); 19 | 20 | Client.CreateIndex(indexName, x => x.Mappings(m => m 21 | .Map(t => t 22 | .Properties(prop => prop.Keyword(str => str.Name(s => s.Guid))) 23 | .Properties(prop => prop.Keyword(str => str.Name(s => s.Email))) 24 | .Properties(prop => prop.Keyword(str => str.Name(s => s.PreviousOwners))) 25 | ))); 26 | 27 | var cars = new List(); 28 | for (int i = 0; i < 10; i++) 29 | { 30 | var car = new Car 31 | { 32 | Id = Guid.NewGuid(), 33 | Timestamp = new DateTime(2010,(i%12)+1,1), 34 | Name = "name"+i%3, 35 | Price = 10, 36 | Sold = i % 2 == 0, 37 | CarType = "Type" + i%2, 38 | Emissions = i+1, 39 | Guid = "test-" + i, 40 | Email = "Email@email" + i % 2 + ".com", 41 | Age = i + 1, 42 | Enabled = i % 2 == 0, 43 | Active = i % 2 == 0, 44 | Weight = i % 3 == 0 ? 10m : (decimal?)null, 45 | PreviousOwners = i % 2 == 0 ? null : i % 3 == 0 ? new string[0] : Enumerable.Range(0, i).Select(n => $"Owner n°{n}").ToArray() 46 | }; 47 | if (i == 1) 48 | { 49 | car.Guid = MyFavoriteGuid; 50 | } 51 | cars.Add(car); 52 | } 53 | Client.Bulk(x => x.CreateMany(cars).Index(indexName)); 54 | Client.Flush(indexName); 55 | return indexName; 56 | } 57 | 58 | [Fact] 59 | public void DateComparisonAndTerm() 60 | { 61 | var index = AddSimpleTestData(); 62 | 63 | var startDate = new DateTime(2010, 1, 1); 64 | var endDate = new DateTime(2010, 3, 1); 65 | var result = Client.Search(s => s.Index(index).FilterOn(x => x.Timestamp >= startDate && x.Timestamp <= endDate && x.CarType == "type0")); 66 | Client.DeleteIndex(index); 67 | 68 | Check.That(result.Documents).HasSize(2); 69 | } 70 | 71 | 72 | [Fact] 73 | public void NullableDateComparisonAndTerm() 74 | { 75 | var index = AddSimpleTestData(); 76 | 77 | DateTime? startDate = null; 78 | var endDate = new DateTime(2010, 3, 1); 79 | var result = Client.Search(s => s.Index(index).FilterOn(x => x.Timestamp >= startDate && x.Timestamp <= endDate && x.CarType == "type0")); 80 | Client.DeleteIndex(index); 81 | 82 | Check.That(result.Documents).HasSize(2); 83 | } 84 | 85 | [Fact] 86 | public void DateEqualityTest() 87 | { 88 | var index = AddSimpleTestData(); 89 | 90 | var startDate = new DateTime(2010, 1, 1); 91 | var result = Client.Search(s => s.Index(index).FilterOn(x => x.Timestamp == startDate)); 92 | Client.DeleteIndex(index); 93 | Check.That(result.Documents).HasSize(1); 94 | } 95 | 96 | [Fact] 97 | public void TestEqualityFilter() 98 | { 99 | var index = AddSimpleTestData(); 100 | 101 | var carType = "Type0".ToLower(); 102 | // Standard Nest way of getting the documents. Values are lowered by ES 103 | var result = Client.Search(s => s.Index(index).Query(x => x.Term(f => f.CarType, carType))); 104 | Check.That(result.Documents).HasSize(5); 105 | 106 | // Best way 107 | result = Client.Search(s => s.Index(index).FilterOn(x => x.CarType == carType)); 108 | Check.That(result.Documents).HasSize(5); 109 | Client.DeleteIndex(index); 110 | } 111 | 112 | [Fact] 113 | public void TestRangeFilters_And_And() 114 | { 115 | var index = AddSimpleTestData(); 116 | 117 | var startDate = new DateTime(2010, 1, 1); 118 | var endDate = new DateTime(2010, 5, 1); 119 | 120 | var result = Client.Search(s => s.Index(index).Query( 121 | q => q.Bool(b => b.Must(left => left.DateRange(f => f.Field(fd => fd.Timestamp).GreaterThan(startDate)), 122 | right => right.DateRange(f => f.Field(fd => fd.Timestamp).LessThan(endDate))) 123 | ) 124 | )); 125 | Check.That(result.Documents).HasSize(3); 126 | 127 | //Much better 128 | result = Client.Search(s => s.Index(index).FilterOn(f => f.Timestamp > startDate && f.Timestamp < endDate)); 129 | Check.That(result.Documents).HasSize(3); 130 | Client.DeleteIndex(index); 131 | } 132 | 133 | [Fact] 134 | public void FilterOnSpecialCharacter() 135 | { 136 | var index = AddSimpleTestData(); 137 | 138 | // these two searches should provide the same result 139 | var result = 140 | Client.Search( 141 | s => 142 | s.Index(index) 143 | .Query( 144 | q => 145 | q.Bool( 146 | b => b.Must(x => x.Term(term => term.Email, "Email@email1.com"))))); 147 | Check.That(result.Documents).HasSize(5); 148 | 149 | result = Client.Search(s => s.Index(index).FilterOn(f => f.Email == "Email@email1.com")); 150 | Check.That(result.Documents).HasSize(5); 151 | Client.DeleteIndex(index); 152 | } 153 | 154 | [Fact] 155 | public void TestConsecutiveFilters() 156 | { 157 | var index = AddSimpleTestData(); 158 | 159 | var filter = Filters 160 | .CreateFilter(x => x.Name == "name1" && x.Age >= 5) 161 | .AndFilteredOn(x => x.Email == "Email@email1.com"); 162 | 163 | var cars = Client.Search(s => s.Index(index).Query(_ => filter)); 164 | Client.DeleteIndex(index); 165 | Check.That(cars.Documents).HasSize(1); 166 | } 167 | 168 | [Fact] 169 | public void TestConsecutiveFiltersOnBoolean() 170 | { 171 | var index = AddSimpleTestData(); 172 | 173 | var filter = Filters 174 | .CreateFilter(x => x.Name == "name1" && x.Age >= 5) 175 | .AndFilteredOn(x => x.Active); 176 | 177 | var cars = Client.Search(s => s.Index(index).Query(_ => filter)); 178 | Client.DeleteIndex(index); 179 | Check.That(cars.Documents).HasSize(1); 180 | } 181 | 182 | [Fact] 183 | public void MultipleFiltersAndSomeAggregations() 184 | { 185 | var index = AddSimpleTestData(); 186 | 187 | var filter = Filters 188 | .CreateFilter(x => x.Name == "name1" && x.Age >= 5) 189 | .AndFilteredOn(x => x.Email == "Email@email1.com"); 190 | 191 | var result = Client.Search(sc => sc 192 | .Index(index) 193 | .FilterOn(filter) 194 | .Aggregations(agg => agg 195 | .SumBy(x=>x.Age) 196 | )); 197 | 198 | var sumValue = result.Aggregations.GetSum(x => x.Age); 199 | 200 | var aggsContainer = result.Aggregations.AsContainer(); 201 | var sum2 = aggsContainer.GetSum(x => x.Age); 202 | Client.DeleteIndex(index); 203 | Check.That(sumValue).Equals(8); 204 | Check.That(sum2).Equals(8); 205 | } 206 | 207 | [Fact] 208 | public void Or_Filter_Test() 209 | { 210 | var index = AddSimpleTestData(); 211 | var cars = Client.Search(s => s.Index(index).FilterOn(x=> x.Name == "name1" || x.Age >= 5)); 212 | Client.DeleteIndex(index); 213 | Check.That(cars.Documents).HasSize(7); 214 | } 215 | 216 | [Fact] 217 | public void Noq_equal_Filter_Test() 218 | { 219 | var index = AddSimpleTestData(); 220 | 221 | var filter = Filters 222 | .CreateFilter(x => x.Name != "name1" && x.Name != "name2"); 223 | 224 | var allCars = Client.Search(s => s.Index(index).Query(_ => filter)); 225 | Check.That(allCars.Documents).HasSize(4); 226 | Client.DeleteIndex(index); 227 | } 228 | 229 | [Fact] 230 | public void Bool_filter_test() 231 | { 232 | var index = AddSimpleTestData(); 233 | var allCars = Client.Search(s=>s.Index(index).FilterOn(f=>f.Active)); 234 | Check.That(allCars.Documents).HasSize(5); 235 | 236 | var filter = Filters.CreateFilter(x => x.Enabled); 237 | 238 | var allCars2 = Client.Search(s => s.Index(index).Query(_ => filter)); 239 | Check.That(allCars2.Documents).HasSize(5); 240 | Client.DeleteIndex(index); 241 | } 242 | 243 | [Fact] 244 | public void Null_filter_test() 245 | { 246 | var index = AddSimpleTestData(); 247 | var allCars = Client.Search(sd => sd.Index(index).Query(x => x.Bool(b => b.MustNot(n => n.Exists(c => c.Field(s => s.Weight)))))); 248 | Check.That(allCars.Documents).HasSize(6); 249 | 250 | var filter = Filters.CreateFilter(x => x.Weight == null); 251 | 252 | var weightlessCars = Client.Search(s => s.Index(index).Query(_ => filter)); 253 | Check.That(weightlessCars.Documents).HasSize(6); 254 | Client.DeleteIndex(index); 255 | } 256 | 257 | [Fact] 258 | public void NotNull_filter_test() 259 | { 260 | var index = AddSimpleTestData(); 261 | var allCars = Client.Search(sd => sd.Index(index).Query(x => x.Bool(b => b.Must(n => n.Exists(c => c.Field(s => s.Weight)))))); 262 | Check.That(allCars.Documents).HasSize(4); 263 | 264 | var filter = Filters.CreateFilter(x => x.Weight != null); 265 | 266 | var weightlessCars = Client.Search(s => s.Index(index).Query(_ => filter)); 267 | Check.That(weightlessCars.Documents).HasSize(4); 268 | Client.DeleteIndex(index); 269 | } 270 | 271 | [Fact] 272 | public void Decimal_Two_Side_Range_Test() 273 | { 274 | var index = AddSimpleTestData(); 275 | var allCars = Client.Search(s => s.Index(index).FilterOn(x=>x.Emissions > 2 && x.Emissions < 6)); 276 | Client.DeleteIndex(index); 277 | Check.That(allCars.Documents).HasSize(3); 278 | } 279 | 280 | [Fact] 281 | public void Guid_Filter_Test() 282 | { 283 | var index = AddSimpleTestData(); 284 | var result = Client.Search(sc => sc.Index(index).FilterOn(x => x.Guid == MyFavoriteGuid)); 285 | Check.That(result.Documents).HasSize(1); 286 | Client.DeleteIndex(index); 287 | } 288 | 289 | [Fact] 290 | public void Filter_ValueWithin_SingleItem() 291 | { 292 | var item = "Owner n°0"; 293 | var index = AddSimpleTestData(); 294 | var result = Client.Search(sc => sc.Index(index).FilterOn(Filters.ValueWithin(x => x.PreviousOwners, item)).TypedKeys(null)); 295 | Check.That(result.Documents).Not.HasSize(0); 296 | foreach (var previousOwners in result.Documents.Select(d => d.PreviousOwners)) 297 | { 298 | Check.That(previousOwners).Contains(item); 299 | } 300 | 301 | Client.DeleteIndex(index); 302 | } 303 | 304 | [Fact] 305 | public void Filter_ValueWithin_MultipleItems() 306 | { 307 | var items = new[] { "Owner n°0", "Onwer n°1" }; 308 | var index = AddSimpleTestData(); 309 | var result = Client.Search(sc => sc.Index(index).FilterOn(Filters.ValueWithin(x => x.PreviousOwners, items)).TypedKeys(null)); 310 | Check.That(result.Documents).Not.HasSize(0); 311 | foreach (var previousOwners in result.Documents.Select(d => d.PreviousOwners)) 312 | { 313 | Check.That(previousOwners.Join(items, a => a, b => b, (a, b) => 0)).Not.HasSize(0); 314 | } 315 | 316 | Client.DeleteIndex(index); 317 | } 318 | 319 | [Fact] 320 | public void Integer_Two_Side_Range_Test() 321 | { 322 | var sc = new SearchDescriptor().FilterOn(x => x.Age > 2 && x.Age < 6); 323 | CheckSD(sc, "Integer_Two_Side_Range_Test"); 324 | } 325 | 326 | [Fact] 327 | public void Three_Ands_Test() 328 | { 329 | var sc = new SearchDescriptor().FilterOn(x => x.Sold && x.Age < 6 && x.Emissions < 5); 330 | CheckSD(sc, "Three_Ands_Test"); 331 | } 332 | 333 | [Fact] 334 | public void Filter_ValueWithin_Test() 335 | { 336 | var list = new List {"name1", "name2"}; 337 | var sc = new SearchDescriptor().FilterOn(Filters.ValueWithin(x => x.Name, list)); 338 | CheckSD(sc, "Filter_ValueWithin_Test"); 339 | } 340 | 341 | [Fact] 342 | public void Filter_ValueWithin_AddedOnExistingFilter() 343 | { 344 | var filter = Filters.CreateFilter(x => x.Age > 8); 345 | var sc = new SearchDescriptor().FilterOn(filter.AndValueWithin(x=>x.Name, new List { "name1", "name2" } )); 346 | CheckSD(sc, "Filter_ValueWithin_AddedOnExistingFilter"); 347 | } 348 | 349 | [Fact] 350 | public void Custom_Field_Name_Test() 351 | { 352 | var sc = new SearchDescriptor().FilterOn(x => x.GetFieldNamed("sold") && x.GetFieldNamed("age") < 6 && x.GetFieldNamed("emissions") < 5); 353 | CheckSD(sc, "Three_Ands_Test"); 354 | } 355 | 356 | [Fact] 357 | public void Concatenated_custom_Field_Name_Test() 358 | { 359 | var old = "old"; 360 | 361 | DoTest(old); 362 | 363 | void DoTest(string o) 364 | { 365 | var sc = new SearchDescriptor().FilterOn(x => x.GetFieldNamed("s" + o) && x.GetFieldNamed("age") < 6 && x.GetFieldNamed("emissions") < 5); 366 | CheckSD(sc, "Three_Ands_Test"); 367 | } 368 | } 369 | } 370 | } 371 | 372 | -------------------------------------------------------------------------------- /FluentNest.Tests/FilterTests.txt: -------------------------------------------------------------------------------- 1 | ###Three_Ands_Test*** 2 | { 3 | "query": { 4 | "bool": { 5 | "must": [ 6 | { 7 | "term": { 8 | "sold": { 9 | "value": true 10 | } 11 | } 12 | }, 13 | { 14 | "range": { 15 | "age": { 16 | "lt": 6.0 17 | } 18 | } 19 | }, 20 | { 21 | "range": { 22 | "emissions": { 23 | "lt": 5.0 24 | } 25 | } 26 | } 27 | ] 28 | } 29 | } 30 | } 31 | ###Integer_Two_Side_Range_Test*** 32 | { 33 | "query": { 34 | "range": { 35 | "age": { 36 | "gt": 2.0, 37 | "lt": 6.0 38 | } 39 | } 40 | } 41 | } 42 | ###Filter_ValueWithin_Test*** 43 | { 44 | "query": { 45 | "bool": { 46 | "must": [ 47 | { 48 | "terms": { 49 | "name": [ 50 | "name1", 51 | "name2" 52 | ] 53 | } 54 | } 55 | ] 56 | } 57 | } 58 | } 59 | ###Filter_ValueWithin_AddedOnExistingFilter*** 60 | { 61 | "query": { 62 | "bool": { 63 | "must": [ 64 | { 65 | "range": { 66 | "age": { 67 | "gt": 8.0 68 | } 69 | } 70 | }, 71 | { 72 | "terms": { 73 | "name": [ 74 | "name1", 75 | "name2" 76 | ] 77 | } 78 | } 79 | ] 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /FluentNest.Tests/FluentNest.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net461 5 | false 6 | true 7 | 8 | 9 | 10 | Always 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /FluentNest.Tests/GroupByTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using FluentNest.Tests.Model; 5 | using NFluent; 6 | using Tests; 7 | using Xunit; 8 | using User = FluentNest.Tests.Model.User; 9 | 10 | namespace FluentNest.Tests 11 | { 12 | public class GroupByTests : TestsBase 13 | { 14 | public string CreateTestIndex() 15 | { 16 | var indexName = "index_" + Guid.NewGuid(); 17 | Client.CreateIndex(indexName, x => x.Mappings( 18 | m => m.Map(t => t 19 | .Properties(prop => prop.Keyword(str => str.Name(s => s.EngineType))) 20 | .Properties(prop => prop.Text(str => str.Name(s => s.CarType).Fielddata())) 21 | ))); 22 | 23 | return indexName; 24 | } 25 | 26 | public string AddSimpleTestData() 27 | { 28 | var indexName = CreateTestIndex(); 29 | 30 | for (int i = 0; i < 10; i++) 31 | { 32 | var car = new Car 33 | { 34 | Id = Guid.NewGuid(), 35 | Timestamp = new DateTime(2010, i + 1, 1), 36 | Name = "Car" + i, 37 | Price = 10, 38 | Sold = i % 2 == 0, 39 | CarType = "Type" + i % 3, 40 | Length = i, 41 | EngineType = i % 2 == 0 ? EngineType.Diesel : EngineType.Standard, 42 | Weight = 5, 43 | ConditionalRanking = i % 2 == 0 ? null : (int?)i, 44 | Description = "Desc" + i, 45 | }; 46 | Client.Index(car, ind => ind.Index(indexName)); 47 | } 48 | Client.Flush(indexName); 49 | return indexName; 50 | } 51 | 52 | [Fact] 53 | public void NestedGroupBy() 54 | { 55 | var index = AddSimpleTestData(); 56 | 57 | // The standard NEST way without FluentNest 58 | var result = Client.Search(s => s.Index(index) 59 | .Aggregations(fstAgg => fstAgg 60 | .Terms("firstLevel", f => f 61 | .Field(z => z.CarType) 62 | .Aggregations(sndLevel => sndLevel 63 | .Terms("secondLevel", f2 => f2.Field(f3 => f3.EngineType) 64 | .Aggregations(sums => sums 65 | .Sum("priceSum", son => son 66 | .Field(f4 => f4.Price)) 67 | ) 68 | ) 69 | ) 70 | ) 71 | ) 72 | ); 73 | 74 | var carTypesAgg = result.Aggregations.Terms("firstLevel"); 75 | 76 | foreach (var carType in carTypesAgg.Buckets) 77 | { 78 | var engineTypes = carType.Terms("secondLevel"); 79 | foreach (var engineType in engineTypes.Buckets) 80 | { 81 | var priceSum = (decimal)engineType.Sum("priceSum").Value; 82 | Check.That(priceSum).IsPositive(); 83 | } 84 | } 85 | 86 | // Now with FluentNest 87 | result =Client.Search(search => search.Index(index).Aggregations(agg => agg 88 | .SumBy(s => s.Price) 89 | .GroupBy(s => s.EngineType) 90 | .GroupBy(b => b.CarType) 91 | )); 92 | 93 | var aggsContainer = result.Aggregations.AsContainer(); 94 | var carTypes = aggsContainer.GetGroupBy(x => x.CarType).ToList(); 95 | Check.That(carTypes).HasSize(3); 96 | foreach (var carType in carTypes) 97 | { 98 | var container = carType.AsContainer(); 99 | var engineTypes = container.GetGroupBy(x => x.EngineType, k => new CarType 100 | { 101 | Type = k.Key, 102 | Price = k.GetSum(x => x.Price) 103 | }).ToList(); 104 | 105 | Check.That(engineTypes).HasSize(2); 106 | Check.That(engineTypes.First().Price).Equals(20m); 107 | } 108 | Client.DeleteIndex(index); 109 | } 110 | 111 | [Fact] 112 | public void GetDictionaryFromGroupBy() 113 | { 114 | var index = AddSimpleTestData(); 115 | 116 | var result = Client.Search(sc => sc.Index(index).Aggregations(agg => agg 117 | .SumBy(s => s.Price) 118 | .GroupBy(s => s.EngineType)) 119 | ); 120 | 121 | var aggsContainer = result.Aggregations.AsContainer(); 122 | 123 | var carTypesList = result.Aggregations.GetGroupBy(x => x.EngineType); 124 | var carTypesDictionary = aggsContainer.GetDictionary(x => x.EngineType); 125 | 126 | Check.That(carTypesDictionary).HasSize(2); 127 | Check.That(carTypesList).HasSize(2); 128 | Check.That(carTypesDictionary.Keys).ContainsExactly(EngineType.Diesel, EngineType.Standard); 129 | Client.DeleteIndex(index); 130 | } 131 | 132 | [Fact] 133 | public void GetDictionaryWithDecimalKeysFromGroupBy() 134 | { 135 | var index = AddSimpleTestData(); 136 | 137 | var result = 138 | Client.Search(search => search.Index(index).Aggregations(x => x.GroupBy(s => s.Price))); 139 | 140 | var aggsContainer = result.Aggregations.AsContainer(); 141 | var carTypes = aggsContainer.GetDictionary(x => x.Price); 142 | Check.That(carTypes).HasSize(1); 143 | Check.That(carTypes.Keys).ContainsExactly(10m); 144 | Client.DeleteIndex(index); 145 | } 146 | 147 | [Fact] 148 | public void GroupByStringKeys() 149 | { 150 | var index = AddSimpleTestData(); 151 | 152 | var result = Client.Search(search => search.Index(index).Aggregations(agg => agg 153 | .SumBy(s => s.Price) 154 | .GroupBy("engineType") 155 | )); 156 | 157 | var engineTypes = result.Aggregations.GetGroupBy("engineType"); 158 | Check.That(engineTypes).HasSize(2); 159 | Client.DeleteIndex(index); 160 | } 161 | 162 | [Fact] 163 | public void DynamicGroupByListOfKeys() 164 | { 165 | var index = AddSimpleTestData(); 166 | 167 | var result = Client.Search(search => search.Index(index).Aggregations(agg => agg 168 | .SumBy(s => s.Price) 169 | .GroupBy(new List { "engineType", "carType" }) 170 | )); 171 | 172 | var engineTypes = result.Aggregations.GetGroupBy("engineType").ToList(); 173 | Check.That(engineTypes).HasSize(2); 174 | 175 | foreach (var engineType in engineTypes) 176 | { 177 | var carTypes = engineType.GetGroupBy("carType"); 178 | Check.That(carTypes).HasSize(3); 179 | } 180 | 181 | Client.DeleteIndex(index); 182 | } 183 | 184 | [Fact] 185 | public void Distinct_Test() 186 | { 187 | var index = AddSimpleTestData(); 188 | 189 | var result = Client.Search(search => search.Index(index).Aggregations(agg => agg 190 | .DistinctBy(x => x.CarType) 191 | .DistinctBy(x => x.EngineType) 192 | )); 193 | 194 | var engineTypes = result.Aggregations.GetDistinct(x => x.EngineType).ToList(); 195 | 196 | var container = result.Aggregations.AsContainer(); 197 | var distinctCarTypes = container.GetDistinct(x => x.CarType).ToList(); 198 | 199 | Check.That(distinctCarTypes).IsNotNull(); 200 | Check.That(distinctCarTypes).HasSize(3); 201 | Check.That(distinctCarTypes).ContainsExactly("type0", "type1", "type2"); 202 | 203 | Check.That(engineTypes).IsNotNull(); 204 | Check.That(engineTypes).HasSize(2); 205 | Check.That(engineTypes).ContainsExactly(EngineType.Diesel, EngineType.Standard); 206 | Client.DeleteIndex(index); 207 | } 208 | 209 | [Fact] 210 | public void Simple_Filtered_Distinct_Test() 211 | { 212 | var index = AddSimpleTestData(); 213 | 214 | var result = Client.Search(search => search.Index(index) 215 | .FilterOn(f=> f.CarType == "type0") 216 | .Aggregations(agg => agg 217 | .DistinctBy(x => x.CarType) 218 | .DistinctBy(x => x.EngineType) 219 | ) 220 | ); 221 | 222 | var distinctCarTypes = result.Aggregations.GetDistinct(x => x.CarType).ToList(); 223 | var engineTypes = result.Aggregations.GetDistinct(x => x.EngineType).ToList(); 224 | 225 | Check.That(distinctCarTypes).IsNotNull(); 226 | Check.That(distinctCarTypes).HasSize(1); 227 | Check.That(distinctCarTypes).ContainsExactly("type0"); 228 | 229 | Check.That(engineTypes).IsNotNull(); 230 | Check.That(engineTypes).HasSize(2); 231 | Check.That(engineTypes).ContainsExactly(EngineType.Diesel, EngineType.Standard); 232 | Client.DeleteIndex(index); 233 | } 234 | 235 | [Fact] 236 | public void Distinct_Time_And_Term_Filter_Test() 237 | { 238 | var index = AddSimpleTestData(); 239 | 240 | var filter = Filters.CreateFilter(x => x.Timestamp > new DateTime(2010,2,1) && x.Timestamp < new DateTime(2010, 8, 1)) 241 | .AndFilteredOn(x => x.CarType == "type0"); 242 | 243 | var result = Client.Search(sc => sc.Index(index).FilterOn(filter).Aggregations(agg => agg 244 | .DistinctBy(x => x.CarType) 245 | .DistinctBy(x => x.EngineType) 246 | )); 247 | 248 | var distinctCarTypes = result.Aggregations.GetDistinct(x => x.CarType).ToList(); 249 | var engineTypes = result.Aggregations.GetDistinct(x => x.EngineType).ToList(); 250 | 251 | Check.That(distinctCarTypes).IsNotNull(); 252 | Check.That(distinctCarTypes).HasSize(1); 253 | Check.That(distinctCarTypes).ContainsExactly("type0"); 254 | 255 | Check.That(engineTypes).IsNotNull(); 256 | Check.That(engineTypes).HasSize(2); 257 | Check.That(engineTypes).ContainsExactly(EngineType.Diesel, EngineType.Standard); 258 | Client.DeleteIndex(index); 259 | } 260 | 261 | [Fact] 262 | public void Terms_Aggregation_Big_Size() 263 | { 264 | var index = CreateUsersIndex(200, 20); 265 | var result = Client.Search(sc => sc.Index(index).Aggregations(agg => agg.DistinctBy(x=>x.Nationality))); 266 | var nationalities = result.Aggregations.GetDistinct(x => x.Nationality).ToList(); 267 | 268 | Check.That(nationalities).IsNotNull(); 269 | Check.That(nationalities).HasSize(20); 270 | } 271 | [Fact] 272 | public void GroupBy_With_TopHits_Specifying_Properties_To_Fetch() 273 | { 274 | var index = AddSimpleTestData(); 275 | 276 | var result = Client.Search(search => search.Index(index).Aggregations(agg => agg 277 | //get name and weight for each retrieved document 278 | .TopHits(3, x => x.Name, x => x.Weight) 279 | .GroupBy(b => b.CarType) 280 | )); 281 | 282 | 283 | var carTypes = result.Aggregations.GetGroupBy(x => x.CarType).ToList(); 284 | Check.That(carTypes).HasSize(3); 285 | foreach (var carType in carTypes) 286 | { 287 | var hits = carType.GetTopHits().ToList(); 288 | Check.That(hits).HasSize(3); 289 | // we have asked only for name and weights 290 | Check.That(hits[0].Name).IsNotNull(); 291 | Check.That(hits[0].Weight).IsNotNull(); 292 | // description must be null 293 | Check.That(hits[0].Description).IsNull(); 294 | } 295 | Client.DeleteIndex(index); 296 | } 297 | 298 | [Fact] 299 | public void GroupBy_With_TopHits_NoProperties_GetsWholeSource() 300 | { 301 | var index = AddSimpleTestData(); 302 | 303 | var result = Client.Search(search => search.Index(index).Aggregations(x => x 304 | .TopHits(3) 305 | .GroupBy(b => b.CarType)) 306 | ); 307 | 308 | var carTypes = result.Aggregations.GetGroupBy(x => x.CarType).ToList(); 309 | Check.That(carTypes).HasSize(3); 310 | foreach (var carType in carTypes) 311 | { 312 | var hits = carType.GetTopHits().ToList(); 313 | Check.That(hits).HasSize(3); 314 | Check.That(hits[0].Name).IsNotNull(); 315 | Check.That(hits[0].Weight).IsNotNull(); 316 | Check.That(hits[0].Description).IsNotNull(); 317 | } 318 | Client.DeleteIndex(index); 319 | } 320 | 321 | [Fact] 322 | public void TopHits_In_Double_GroupBy() 323 | { 324 | var indexName = CreateUsersIndex(250, 2); 325 | 326 | var result = Client.Search(search => search.Index(indexName).Aggregations(agg => agg 327 | .TopHits(40, x => x.Name) 328 | .GroupBy(b => b.Active) 329 | .GroupBy(b => b.Nationality)) 330 | ); 331 | 332 | var userByNationality = result.Aggregations.GetGroupBy(x => x.Nationality).ToList(); 333 | Check.That(userByNationality).HasSize(2); 334 | foreach (var nationality in userByNationality) 335 | { 336 | var byActive = nationality.GetGroupBy(x => x.Active).ToList(); 337 | 338 | var activeUsers = byActive[0]; 339 | var inactiveUser = byActive[1]; 340 | 341 | 342 | var hits = activeUsers.GetTopHits().ToList(); 343 | Check.That(hits).HasSize(40); 344 | Check.That(hits[0].Name).IsNotNull(); 345 | 346 | hits = inactiveUser.GetTopHits().ToList(); 347 | 348 | Check.That(hits).HasSize(40); 349 | Check.That(hits[0].Name).IsNotNull(); 350 | } 351 | } 352 | 353 | private string CreateUsersIndex(int size, int nationalitiesCount) 354 | { 355 | var indexName = "index_" + Guid.NewGuid(); 356 | Client.CreateIndex(indexName, x => x.Mappings( 357 | m => m.Map(t => t 358 | .Properties(prop => prop.Text(str => str.Name(s => s.Nationality).Fielddata())) 359 | .Properties(prop => prop.Text(str => str.Name(s => s.Name).Fielddata())) 360 | .Properties(prop => prop.Text(str => str.Name(s => s.Email).Fielddata())) 361 | ))); 362 | var users = new List(); 363 | for (int i = 0; i < size; i++) 364 | { 365 | var user = new User 366 | { 367 | Id = Guid.NewGuid(), 368 | Name = "User" + i, 369 | Nationality = "Nationality" + i % nationalitiesCount, 370 | Active = i%3 == 0, 371 | Age = (i + 1) % 10, 372 | Email = "user@" + i 373 | }; 374 | 375 | users.Add(user); 376 | } 377 | Client.Bulk(x => x.CreateMany(users).Index(indexName)); 378 | Client.Flush(indexName); 379 | return indexName; 380 | } 381 | 382 | [Fact] 383 | public void TopHits_Sorted_SettingSize() 384 | { 385 | var index = CreateUsersIndex(100, 10); 386 | var result = Client.Search(search => search.Index(index).Aggregations(agg => agg 387 | // get 40 first users, sort by name. for each user retrieve name and email 388 | .SortedTopHits(40, x => x.Name, SortType.Ascending, x => x.Name, y => y.Email) 389 | .SortedTopHits(40, x => x.Name, SortType.Descending, x => x.Name, y => y.Email) 390 | .SumBy(x => x.Age) 391 | .GroupBy(b => b.Nationality)) 392 | ); 393 | 394 | var userByNationality = result.Aggregations.GetGroupBy(x => x.Nationality).ToList(); 395 | Check.That(userByNationality).HasSize(10); 396 | var firstNationality = userByNationality.Single(x => x.Key == "nationality0"); 397 | 398 | var ascendingHits = firstNationality.GetSortedTopHits(x => x.Name, SortType.Ascending).ToList(); 399 | Check.That(ascendingHits).HasSize(10); 400 | Check.That(ascendingHits[0].Name).IsNotNull(); 401 | 402 | Check.That(firstNationality.GetSum(x => x.Age)).Equals(10); 403 | Check.That(ascendingHits[0].Name).Equals("User0"); 404 | Check.That(ascendingHits[1].Name).Equals("User10"); 405 | Check.That(ascendingHits[2].Name).Equals("User20"); 406 | Check.That(ascendingHits[3].Name).Equals("User30"); 407 | 408 | var descendingHits = firstNationality.GetSortedTopHits(x => x.Name, SortType.Descending).ToList(); 409 | Check.That(descendingHits).HasSize(10); 410 | Check.That(descendingHits[0].Name).IsNotNull(); 411 | 412 | Check.That(descendingHits[0].Name).Equals("User90"); 413 | Check.That(descendingHits[1].Name).Equals("User80"); 414 | Check.That(descendingHits[2].Name).Equals("User70"); 415 | Check.That(descendingHits[3].Name).Equals("User60"); 416 | } 417 | 418 | [Fact] 419 | public void TopHits_Sorted_Supports_Named_Fields() 420 | { 421 | var index = CreateUsersIndex(100, 10); 422 | var result = Client.Search(search => search 423 | .Index(index) 424 | .Aggregations(agg => agg 425 | // get 40 first users, sort by name. for each user retrieve name and email 426 | .SortedTopHits(40, x => x.GetFieldNamed("name"), SortType.Ascending, x => x.GetFieldNamed("name"), y => y.Email) 427 | .SumBy(x => x.Age) 428 | .GroupBy(b => b.Nationality)) 429 | ); 430 | 431 | var userByNationality = result.Aggregations.GetGroupBy(x => x.Nationality).ToList(); 432 | Check.That(userByNationality).HasSize(10); 433 | var firstNotionality = userByNationality.Single(x => x.Key == "nationality0"); 434 | 435 | var ascendingHits = firstNotionality.GetSortedTopHits(x => x.GetFieldNamed("name"), SortType.Ascending).ToList(); 436 | Check.That(ascendingHits).HasSize(10); 437 | Check.That(ascendingHits[0].Name).IsNotNull(); 438 | Check.That(ascendingHits[0].Email).IsNotNull(); 439 | } 440 | 441 | [Fact] 442 | public void GettingNonExistingGroup_Test() 443 | { 444 | var index = CreateTestIndex(); 445 | 446 | var result = Client.Search(search => search.Index(index).Aggregations(agg => agg 447 | .GroupBy(b => b.Emissions) 448 | )); 449 | 450 | var exception = Record.Exception(() => result.Aggregations.GetGroupBy(x => x.CarType)); 451 | 452 | Assert.NotNull(exception); 453 | Assert.IsType(exception); 454 | Check.That(exception.Message).Contains("Available aggregations: GroupByEmissions"); 455 | } 456 | 457 | [Fact] 458 | public void NoAggregations_On_The_Result_Test() 459 | { 460 | var index = CreateTestIndex(); 461 | 462 | // no data - no aggregations on the result 463 | var result = Client.Search(search => search.Index(index).Aggregations(agg => agg)); 464 | 465 | var exception = Record.Exception(() => result.Aggregations.GetGroupBy(x => x.EngineType)); 466 | 467 | Assert.NotNull(exception); 468 | Assert.IsType(exception); 469 | Check.That(exception.Message).Contains("No aggregations"); 470 | } 471 | 472 | } 473 | } 474 | -------------------------------------------------------------------------------- /FluentNest.Tests/HistogramTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using FluentNest.Tests.Model; 4 | using Nest; 5 | using NFluent; 6 | using Tests; 7 | using Xunit; 8 | 9 | namespace FluentNest.Tests 10 | { 11 | public class HistogramTests : TestsBase 12 | { 13 | private string AddSimpleTestData() 14 | { 15 | var indexName = "index_" + Guid.NewGuid(); 16 | Client.CreateIndex(indexName, x => 17 | x.Mappings(m => m.Map(t => t 18 | .Properties(prop => prop.Keyword(str => str.Name(s => s.EngineType))) 19 | .Properties(prop => prop.Text(str => str.Name(s => s.CarType).Fielddata())) 20 | ))); 21 | for (int i = 0; i < 10; i++) 22 | { 23 | var car = new Car 24 | { 25 | Id = Guid.NewGuid(), 26 | Timestamp = new DateTime(2010,i+1,1), 27 | Name = "Car" + i, 28 | Price = 10, 29 | Sold = i % 2 == 0 ? true : false, 30 | CarType = "Type" + i%3, 31 | Length = i*2, 32 | Weight = i 33 | }; 34 | Client.Index(car, ind => ind.Index(indexName)); 35 | } 36 | Client.Flush(indexName); 37 | return indexName; 38 | } 39 | 40 | [Fact] 41 | public void MonthlyHistogramPerCarType() 42 | { 43 | var index = AddSimpleTestData(); 44 | 45 | var histogram = Client.Search(s => s.Index(index).Aggregations(a => a 46 | .DateHistogram(x => x.Timestamp, DateInterval.Month) 47 | .GroupBy(x => x.CarType)) 48 | ); 49 | 50 | var aggsContainer = histogram.Aggregations.AsContainer(); 51 | 52 | var carTypes = aggsContainer.GetDictionary(x => x.CarType, v => v.GetDateHistogram(f => f.Timestamp)); 53 | 54 | Check.That(carTypes).HasSize(3); 55 | 56 | // currently nest returns buckets in between the values 57 | // first type gets everything between first month and the 10th month -> that is 10 buckets 58 | var firstType = carTypes["type0"]; 59 | Check.That(firstType).HasSize(10); 60 | 61 | // second type and third type both get 7 buckets 62 | // second type from february to september 63 | var secondType = carTypes["type1"]; 64 | Check.That(secondType).HasSize(7); 65 | 66 | // third type from marz to october 67 | var thirdType = carTypes["type2"]; 68 | Check.That(thirdType).HasSize(7); 69 | 70 | Client.DeleteIndex(index); 71 | } 72 | 73 | [Fact] 74 | public void SumInMonthlyHistogram() 75 | { 76 | var index = AddSimpleTestData(); 77 | 78 | // First the standad NEST way 79 | var result = Client.Search(s => s.Index(index).Aggregations(a => a.DateHistogram("by_month", 80 | d => d.Field(x => x.Timestamp) 81 | .Interval(DateInterval.Month) 82 | .Aggregations( 83 | aggs => aggs.Sum("priceSum", dField => dField.Field(field => field.Price)))))); 84 | 85 | var histogram = result.Aggregations.DateHistogram("by_month"); 86 | Check.That(histogram.Buckets).HasSize(10); 87 | var firstMonth = histogram.Buckets.First(); 88 | var priceSum = firstMonth.Sum("priceSum"); 89 | Check.That(priceSum.Value.Value).Equals(10d); 90 | 91 | 92 | // Now with FluentNest 93 | result = Client.Search(sc => sc.Index(index).Aggregations(agg => agg 94 | .SumBy(x=>x.Price) 95 | .IntoDateHistogram(date => date.Timestamp, DateInterval.Month)) 96 | ); 97 | 98 | var histogram2 = result.Aggregations.GetDateHistogram(x => x.Timestamp); 99 | Check.That(histogram2).HasSize(10); 100 | Check.That(histogram2.All(x => x.GetSum(s => s.Price) == 10m)).IsTrue(); 101 | } 102 | 103 | [Fact] 104 | public void MonthlySumHistogramFilteredOnDates() 105 | { 106 | var index = AddSimpleTestData(); 107 | var start = new DateTime(2010, 1, 1); 108 | var end = new DateTime(2010, 4, 4); 109 | 110 | var result = Client.Search(sc => sc.Index(index) 111 | .FilterOn(f => f.Timestamp < end && f.Timestamp > start) 112 | .Aggregations(agg => agg 113 | .SumBy(x => x.Price) 114 | .IntoDateHistogram(date => date.Timestamp, DateInterval.Month) 115 | )); 116 | 117 | var histogram = result.Aggregations.GetDateHistogram(x => x.Timestamp); 118 | 119 | Check.That(histogram).HasSize(3); 120 | Client.DeleteIndex(index); 121 | } 122 | 123 | [Fact] 124 | public void StandardHistogramTest() 125 | { 126 | var index = AddSimpleTestData(); 127 | 128 | var result = Client.Search(sc => sc.Index(index) 129 | .Aggregations(x => x 130 | .SumBy(y => y.Price) 131 | .IntoHistogram(y => y.Length, 5) 132 | )); 133 | 134 | var histogram = result.Aggregations.GetHistogram(x => x.Length); 135 | Check.That(histogram).HasSize(4); 136 | Client.DeleteIndex(index); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /FluentNest.Tests/Model/Car.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FluentNest.Tests.Model 4 | { 5 | public enum EngineType 6 | { 7 | Diesel, 8 | Standard 9 | } 10 | 11 | public class Car 12 | { 13 | public Guid Id { get; set; } 14 | public String Name { get; set; } 15 | public decimal Price { get; set; } 16 | public double Length { get; set; } 17 | public bool Sold { get; set; } 18 | public DateTime Timestamp { get; set; } 19 | public String CarType { get; set; } 20 | public EngineType EngineType { get; set; } 21 | public EngineType? NullableEngineType { get; set; } 22 | public decimal? Weight { get; set; } 23 | public int? ConditionalRanking { get; set; } 24 | public decimal? PriceLimit { get; set; } 25 | public decimal Emissions { get; set; } 26 | public string Description { get; set; } 27 | public string Guid { get; set; } 28 | public bool Active { get; set; } 29 | public int Age { get; set; } 30 | public bool Enabled { get; set; } 31 | 32 | public string BIG_CASE_NAME { get; set; } 33 | 34 | // Of course cars don't have emails, but for my tests it's useful 35 | public string Email { get; set; } 36 | public DateTime? LastControlCheck { get; set; } 37 | public DateTime? LastAccident { get; set; } 38 | 39 | public string[] PreviousOwners { get; set; } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /FluentNest.Tests/Model/CarType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FluentNest.Tests.Model 4 | { 5 | public class CarType 6 | { 7 | public String Type { get; set; } 8 | public decimal Price { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /FluentNest.Tests/Model/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FluentNest.Tests.Model 4 | { 5 | public class User 6 | { 7 | public Guid Id { get; set; } 8 | public String Email { get; set; } 9 | 10 | public String Name { get; set; } 11 | 12 | public int Age { get; set; } 13 | 14 | public bool? Enabled { get; set; } 15 | 16 | public bool Active { get; set; } 17 | 18 | public DateTime CreationTime { get; set; } 19 | 20 | public String Nationality { get; set; } 21 | 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /FluentNest.Tests/PropertyNamesTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using FluentNest.Tests.Model; 4 | using Nest; 5 | using NFluent; 6 | using Tests; 7 | using Xunit; 8 | 9 | namespace FluentNest.Tests 10 | { 11 | public class PropertyNamesTests : TestsBase 12 | { 13 | public PropertyNamesTests() 14 | : base(null, connectionSettings => connectionSettings.DefaultFieldNameInferrer(p => p).DefaultTypeNameInferrer(p => p.Name)) 15 | { 16 | 17 | } 18 | 19 | private string AddSimpleTestData() 20 | { 21 | var indexName = "index_" + Guid.NewGuid(); 22 | Client.CreateIndex(indexName, x => x.Mappings(m => m 23 | .Map(t => t 24 | .Properties(prop => prop.Keyword(str => str.Name(s => s.Guid).Index())) 25 | .Properties(prop => prop.Keyword(str => str.Name(s => s.Email).Index())) 26 | ))); 27 | 28 | for (int i = 0; i < 10; i++) 29 | { 30 | var car = new Car 31 | { 32 | Id = Guid.NewGuid(), 33 | BIG_CASE_NAME = "big" + i % 3 34 | }; 35 | 36 | Client.Index(car, ind => ind.Index(indexName)); 37 | } 38 | Client.Flush(indexName); 39 | return indexName; 40 | } 41 | 42 | [Fact] 43 | public void TestCasing() 44 | { 45 | var indexName = AddSimpleTestData(); 46 | var filter = Filters.CreateFilter(f => f.BIG_CASE_NAME == "big1"); 47 | filter = filter.AndFilteredOn(f => f.BIG_CASE_NAME != "big2"); 48 | var sc = new SearchDescriptor().Index(indexName).FilterOn(filter); 49 | var query = Serialize(sc); 50 | Check.That(query).Contains("BIG_CASE_NAME"); 51 | var cars = Client.Search(sc).Hits.Select(h => h.Source); 52 | Check.That(cars).HasSize(3); 53 | Client.DeleteIndex(indexName); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /FluentNest.Tests/StatisticsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using FluentNest.Tests.Model; 4 | using Nest; 5 | using NFluent; 6 | using Tests; 7 | using Xunit; 8 | 9 | namespace FluentNest.Tests 10 | { 11 | public class StatisticsTests : TestsBase 12 | { 13 | public string CreateTestIndex() 14 | { 15 | var indexName = "index_" + Guid.NewGuid(); 16 | Client.CreateIndex(indexName, x => x.Mappings( 17 | m => m.Map(t => t 18 | .Properties(prop => prop.Keyword(str => str.Name(s => s.EngineType))) 19 | .Properties(prop => prop.Text(str => str.Name(s => s.CarType).Fielddata())) 20 | .Properties(prop => prop.Text(str => str.Name(s => s.Name).Fielddata())) 21 | ))); 22 | 23 | return indexName; 24 | } 25 | 26 | public string AddSimpleTestData() 27 | { 28 | var indexName = CreateTestIndex(); 29 | 30 | for (int i = 0; i < 10; i++) 31 | { 32 | var car = new Car 33 | { 34 | Id = Guid.NewGuid(), 35 | Timestamp = new DateTime(2010, i + 1, 1), 36 | Name = "Car" + i, 37 | Price = 10, 38 | Sold = i % 2 == 0 ? true : false, 39 | CarType = "Type" + i % 3, 40 | Length = i, 41 | EngineType = i % 2 == 0 ? EngineType.Diesel : EngineType.Standard, 42 | Weight = 5, 43 | ConditionalRanking = i % 2 == 0 ? null : (int?)i, 44 | Description = "Desc" + i, 45 | LastControlCheck = i % 2 == 0 ? new DateTime(2012, i + 1, 1) : (DateTime?)null, 46 | LastAccident = null 47 | }; 48 | 49 | Client.Index(car, ind => ind.Index(indexName)); 50 | } 51 | Client.Flush(indexName); 52 | return indexName; 53 | } 54 | 55 | [Fact] 56 | public void ConditionalStats_Without_FluentNest() 57 | { 58 | var index = AddSimpleTestData(); 59 | var result = Client.Search(search => search.Index(index) 60 | .Aggregations(agg => agg 61 | .Filter("filterOne", f => f.Filter(innerFilter => innerFilter.Term(fd => fd.EngineType, EngineType.Diesel)) 62 | .Aggregations(innerAgg => innerAgg.Sum("sumAgg", innerField => 63 | innerField.Field(field => field.Price))) 64 | ) 65 | .Filter("filterTwo", f => f.Filter(innerFilter => innerFilter.Term(fd => fd.CarType, "type1")) 66 | .Aggregations(innerAgg => innerAgg.Sum("sumAgg", innerField => 67 | innerField.Field(field => field.Price))) 68 | ) 69 | ) 70 | ); 71 | 72 | var sumAgg = result.Aggregations.Filter("filterOne"); 73 | Check.That(sumAgg).IsNotNull(); 74 | var sumValue = sumAgg.Sum("sumAgg"); 75 | Check.That(sumValue.Value).Equals(50d); 76 | 77 | var sumAgg2 = result.Aggregations.Filter("filterTwo"); 78 | Check.That(sumAgg2).IsNotNull(); 79 | var sumValue2 = sumAgg2.Sum("sumAgg"); 80 | Check.That(sumValue2.Value).Equals(30d); 81 | Client.DeleteIndex(index); 82 | } 83 | 84 | [Fact] 85 | public void CountAndCardinalityTest() 86 | { 87 | var index = AddSimpleTestData(); 88 | var result = Client.Search(sc => sc.Index(index).Aggregations(agg => agg 89 | .CountBy(x=>x.Price) 90 | .CardinalityBy(x => x.EngineType) 91 | )); 92 | var val = result.Aggregations.GetCount(x => x.Price); 93 | Check.That(val).Equals(10); 94 | var card = result.Aggregations.GetCardinality(x => x.EngineType); 95 | Check.That(card).Equals(2); 96 | Client.DeleteIndex(index); 97 | } 98 | 99 | [Fact] 100 | public void CardinalityFilterTest() 101 | { 102 | var index = AddSimpleTestData(); 103 | var result = Client.Search(sc => sc.Index(index).Aggregations(agg => agg 104 | .CardinalityBy(x => x.EngineType, x => x.EngineType == EngineType.Standard) 105 | )); 106 | 107 | var container = result.Aggregations.AsContainer(); 108 | var card = result.Aggregations.GetCardinality(x => x.EngineType, x => x.EngineType == EngineType.Standard); 109 | Check.That(card).Equals(1); 110 | Check.That(container.GetCardinality(x => x.EngineType, x => x.EngineType == EngineType.Standard)).Equals(1); 111 | } 112 | 113 | [Fact] 114 | public void TestConditionalSum() 115 | { 116 | var index = AddSimpleTestData(); 117 | 118 | var result = Client.Search(sc => sc.Index(index).Aggregations(agg => agg 119 | .SumBy(x => x.Price, x => x.Sold == true) 120 | )); 121 | 122 | var sum = result.Aggregations.GetSum(x => x.Price, x => x.Sold == true); 123 | Check.That(sum).Equals(50m); 124 | Client.DeleteIndex(index); 125 | } 126 | 127 | [Fact] 128 | public void MultipleAggregationsInSingleAggregation() 129 | { 130 | var index = AddSimpleTestData(); 131 | 132 | var result = Client.Search(s => s.Index(index).Aggregations(agg => agg 133 | .CountBy(x => x.Name, c => c.EngineType == EngineType.Diesel) 134 | .SumBy(x => x.Price) 135 | .AverageBy(x => x.Length) 136 | .CountBy(x => x.CarType) 137 | .CardinalityBy(x => x.EngineType)) 138 | ); 139 | 140 | var priceSum = result.Aggregations.GetSum(x => x.Price); 141 | var avgLength = result.Aggregations.GetAverage(x => x.Length); 142 | var count = result.Aggregations.GetCount(x => x.CarType); 143 | var typeOneCount = result.Aggregations.GetCount(x => x.Name, x => x.EngineType == EngineType.Diesel); 144 | var engineCardinality = result.Aggregations.GetCardinality(x => x.EngineType); 145 | 146 | Check.That(priceSum).Equals(100m); 147 | Check.That(avgLength).Equals(4.5d); 148 | Check.That(count).Equals(10); 149 | Check.That(typeOneCount).Equals(5); 150 | Check.That(engineCardinality).Equals(2); 151 | Client.DeleteIndex(index); 152 | } 153 | 154 | [Fact] 155 | public void MultipleAggregationsInSingleAggregation_ReversingOrder() 156 | { 157 | var index = AddSimpleTestData(); 158 | 159 | var result = Client.Search(s => s.Index(index).Aggregations(agg => agg 160 | .SumBy(x => x.Price) 161 | .AverageBy(x => x.Length) 162 | .CountBy(x => x.CarType) 163 | .CountBy(x => x.Name, c => c.EngineType == EngineType.Diesel) 164 | .SumBy(x => x.Price, c => c.CarType == "type1") 165 | )); 166 | 167 | var priceSum = result.Aggregations.GetSum(x => x.Price); 168 | var avgLength = result.Aggregations.GetAverage(x => x.Length); 169 | var count = result.Aggregations.GetCount(x => x.CarType); 170 | var typeOneCount = result.Aggregations.GetCount(x => x.Name, x => x.EngineType == EngineType.Diesel); 171 | var car1PriceSum = result.Aggregations.GetSum(x => x.Price, x => x.CarType == "type1"); 172 | 173 | var aggsContainer = result.Aggregations.AsContainer(); 174 | var priceSum2 = aggsContainer.GetSum(x => x.Price); 175 | var avgLength2 = aggsContainer.GetAverage(x => x.Length); 176 | var count2 = aggsContainer.GetCount(x => x.CarType); 177 | var typeOneCount2 = aggsContainer.GetCount(x => x.Name, x => x.EngineType == EngineType.Diesel); 178 | var car1PriceSum2 = aggsContainer.GetSum(x => x.Price, x => x.CarType == "type1"); 179 | 180 | 181 | Check.That(priceSum).Equals(100m); 182 | Check.That(avgLength).Equals(4.5d); 183 | Check.That(count).Equals(10); 184 | Check.That(typeOneCount).Equals(5); 185 | Check.That(car1PriceSum).Equals(30m); 186 | 187 | Check.That(priceSum2).Equals(100m); 188 | Check.That(avgLength2).Equals(4.5d); 189 | Check.That(count2).Equals(10); 190 | Check.That(typeOneCount2).Equals(5); 191 | Check.That(car1PriceSum2).Equals(30m); 192 | Client.DeleteIndex(index); 193 | } 194 | 195 | [Fact] 196 | public void SumOfNullableDecimal() 197 | { 198 | var index = AddSimpleTestData(); 199 | var result = Client.Search(sc => sc.Index(index).Aggregations(agg => agg.SumBy(x => x.Weight))); 200 | var sum = result.Aggregations.GetSum(x => x.Weight); 201 | 202 | var container = result.Aggregations.AsContainer(); 203 | 204 | var sum2 = container.GetSum(x => x.Weight); 205 | 206 | Check.That(sum).Equals(50m); 207 | Check.That(sum2).Equals(50m); 208 | Client.DeleteIndex(index); 209 | } 210 | 211 | [Fact] 212 | public void SumOfNamedField() 213 | { 214 | var index = AddSimpleTestData(); 215 | var sc = new SearchDescriptor().Index(index).Aggregations(agg => agg.SumBy(x => x.GetFieldNamed("weight"))); 216 | var result = Client.Search(sc); 217 | var sum = result.Aggs.GetSum(x => x.GetFieldNamed("weight")); 218 | 219 | var container = result.Aggs.AsContainer(); 220 | 221 | var sum2 = container.GetSum(x => x.GetFieldNamed("weight")); 222 | 223 | Check.That(sum).Equals(50m); 224 | Check.That(sum2).Equals(50m); 225 | Client.DeleteIndex(index); 226 | } 227 | 228 | [Fact] 229 | public void SumOfConcatenatedNamedField() 230 | { 231 | DoTest("eight"); 232 | 233 | void DoTest(string s) 234 | { 235 | var index = AddSimpleTestData(); 236 | var sc = new SearchDescriptor().Index(index) 237 | .Aggregations(agg => agg.SumBy(x => x.GetFieldNamed("w" + s))); 238 | var result = Client.Search(sc); 239 | var sum = result.Aggs.GetSum(x => x.GetFieldNamed("w" + s)); 240 | 241 | var container = result.Aggs.AsContainer(); 242 | 243 | var sum2 = container.GetSum(x => x.GetFieldNamed("w" + s)); 244 | 245 | Check.That(sum).Equals(50m); 246 | Check.That(sum2).Equals(50m); 247 | Client.DeleteIndex(index); 248 | } 249 | } 250 | 251 | [Fact] 252 | public void Condition_Equals_Not_Null_Test() 253 | { 254 | var index = AddSimpleTestData(); 255 | 256 | var result = Client.Search(sc => sc.Index(index).Aggregations(agg => agg 257 | .SumBy(x => x.Weight, x => x.ConditionalRanking.HasValue) 258 | )); 259 | 260 | var sum = result.Aggregations.GetSum(x => x.Weight, c => c.ConditionalRanking.HasValue); 261 | 262 | var container = result.Aggregations.AsContainer(); 263 | 264 | var sum2 = container.GetSum(x => x.Weight,c=>c.ConditionalRanking.HasValue); 265 | 266 | Check.That(sum).Equals(25m); 267 | Check.That(sum2).Equals(25m); 268 | Client.DeleteIndex(index); 269 | } 270 | 271 | [Fact] 272 | public void Two_Conditional_Sums_Similar_Condition_One_More_Restrained() 273 | { 274 | var index = AddSimpleTestData(); 275 | 276 | var result = Client.Search(sc => sc.Index(index).Aggregations(agg => agg 277 | .SumBy(x => x.Weight, x => x.ConditionalRanking.HasValue) 278 | .SumBy(x => x.Weight, x => x.ConditionalRanking.HasValue && x.CarType == "Type1") 279 | )); 280 | 281 | var sum = result.Aggregations.GetSum(x => x.Weight, c => c.ConditionalRanking.HasValue); 282 | var sum2 = result.Aggregations.GetSum(x => x.Weight, c => c.ConditionalRanking.HasValue && c.CarType == "Type1"); 283 | 284 | Check.That(sum).Equals(25m); 285 | Check.That(sum2).Equals(0m); 286 | Client.DeleteIndex(index); 287 | } 288 | 289 | [Fact] 290 | public void Percentiles_Test() 291 | { 292 | var index = AddSimpleTestData(); 293 | 294 | var result = Client.Search(sc => sc.Index(index).Aggregations(agg => agg 295 | .PercentilesBy(x=>x.Price) 296 | )); 297 | 298 | var percentiles = result.Aggregations.GetPercentile(x => x.Price); 299 | var container = result.Aggregations.AsContainer(); 300 | 301 | Check.That(percentiles).HasSize(7); 302 | Check.That(container.GetPercentile(x => x.Price)).HasSize(7); 303 | 304 | Check.That(percentiles.Single(x => x.Percentile == 50.0).Value).Equals(10d); 305 | Client.DeleteIndex(index); 306 | } 307 | 308 | [Fact] 309 | public void MinMaxTest_DoubleField() 310 | { 311 | var index = AddSimpleTestData(); 312 | 313 | var result = Client.Search(sc => sc.Index(index).Aggregations(agg => agg 314 | .MinBy(x => x.Length) 315 | .MaxBy(x=> x.Length)) 316 | ); 317 | 318 | var container = result.Aggregations.AsContainer(); 319 | var min = container.GetMin(x => x.Length); 320 | var max = container.GetMax(x => x.Length); 321 | 322 | Check.That(min).Equals(0d); 323 | Check.That(max).Equals(9d); 324 | Client.DeleteIndex(index); 325 | } 326 | 327 | [Fact] 328 | public void MinMaxTest_DateTimeField() 329 | { 330 | var index = AddSimpleTestData(); 331 | 332 | var result = Client.Search(sc => sc.Index(index).Aggregations(agg => agg 333 | .MinBy(x => x.Timestamp) 334 | .MaxBy(x => x.Timestamp)) 335 | ); 336 | 337 | var container = result.Aggregations.AsContainer(); 338 | var min = container.GetMin(x => x.Timestamp); 339 | var max = container.GetMax(x => x.Timestamp); 340 | 341 | Check.That(min).Equals(new DateTime(2010, 1, 1)); 342 | Check.That(max).Equals(new DateTime(2010, 10, 1)); 343 | Client.DeleteIndex(index); 344 | } 345 | 346 | [Fact] 347 | public void MinMaxTest_NullableDateTimeField() 348 | { 349 | var index = AddSimpleTestData(); 350 | 351 | var result = Client.Search(sc => sc.Index(index).Aggregations(agg => agg 352 | .MinBy(x => x.LastControlCheck) 353 | .MaxBy(x => x.LastControlCheck)) 354 | ); 355 | 356 | var container = result.Aggregations.AsContainer(); 357 | var min = container.GetMin(x => x.LastControlCheck); 358 | var max = container.GetMax(x => x.LastControlCheck); 359 | 360 | Check.That(min).Equals(new DateTime(2012, 1, 1)); 361 | Check.That(max).Equals(new DateTime(2012, 9, 1)); 362 | Client.DeleteIndex(index); 363 | } 364 | 365 | [Fact] 366 | public void MinMaxTest_NullableDateTimeField_AlwaysNull() 367 | { 368 | var index = AddSimpleTestData(); 369 | 370 | var result = Client.Search(sc => sc.Index(index).Aggregations(agg => agg 371 | .MinBy(x => x.LastAccident) 372 | .MaxBy(x => x.LastAccident)) 373 | ); 374 | 375 | var container = result.Aggregations.AsContainer(); 376 | var min = container.GetMin(x => x.LastAccident); 377 | var max = container.GetMax(x => x.LastAccident); 378 | 379 | Check.That(min).Equals(null); 380 | Check.That(max).Equals(null); 381 | Client.DeleteIndex(index); 382 | } 383 | 384 | [Fact] 385 | public void Agg_On_NUllable_Field_With_No_Result() 386 | { 387 | //all price limit values are null - the result should be null 388 | var index = AddSimpleTestData(); 389 | 390 | var result = 391 | Client.Search( 392 | search => search.Index(index).Take(10).Aggregations(agg => agg 393 | .MinBy(x => x.PriceLimit) 394 | .MaxBy(x=>x.PriceLimit) 395 | .PercentilesBy(x=> x.PriceLimit))); 396 | 397 | var container = result.Aggregations.AsContainer(); 398 | var min = container.GetMin(x => x.PriceLimit); 399 | Check.That(min).IsNull(); 400 | 401 | var max = container.GetMax(x => x.PriceLimit); 402 | Check.That(max).IsNull(); 403 | } 404 | 405 | [Fact] 406 | public void Min_Max_Stats_By_Test() 407 | { 408 | //all price limit values are null - the result should be null 409 | var index = AddSimpleTestData(); 410 | 411 | var result = 412 | Client.Search( 413 | search => search.Index(index) 414 | .Take(10).Aggregations(agg => agg 415 | .MinBy(x => x.Length) 416 | .MaxBy(x => x.Length) 417 | .StatsBy(x => x.Length))); 418 | 419 | var container = result.Aggregations.AsContainer(); 420 | var min = container.GetMin(x => x.Length); 421 | var max = container.GetMax(x => x.Length); 422 | var stats = container.GetStats(x => x.Length); 423 | Check.That(stats.Min).Equals(0d); 424 | Check.That(stats.Max).Equals(9d); 425 | Check.That(min).Equals(0d); 426 | Check.That(max).Equals(9d); 427 | Client.DeleteIndex(index); 428 | } 429 | 430 | [Fact] 431 | public void FirstByTests() 432 | { 433 | //very stupid test, getting the single value of engine type when engine type is diesel 434 | var index = AddSimpleTestData(); 435 | 436 | var result = Client.Search(sc => sc.Index(index).Aggregations(agg => agg 437 | .SumBy(x => x.Weight, x => x.ConditionalRanking.HasValue) 438 | .FirstBy(x => x.EngineType, c => c.EngineType == EngineType.Diesel) 439 | .FirstBy(x => x.CarType, c => c.Sold == true) 440 | .FirstBy(x => x.Length) 441 | )); 442 | 443 | var sum = result.Aggregations.GetSum(x => x.Weight, c => c.ConditionalRanking.HasValue); 444 | var engineType = result.Aggregations.GetFirstBy(x => x.EngineType, c => c.EngineType == EngineType.Diesel); 445 | 446 | //car type of first sold car 447 | var carType = result.Aggregations.GetFirstBy(x => x.CarType, c => c.Sold == true); 448 | 449 | var firstLength = result.Aggregations.GetFirstBy(x => x.Length); 450 | 451 | Check.That(sum).Equals(25m); 452 | Check.That(engineType).Equals(EngineType.Diesel); 453 | Check.That(carType).Equals("type0"); 454 | Check.That(firstLength).Equals(0d); 455 | 456 | var container = result.Aggregations.AsContainer(); 457 | var lengthFromContainer = container.GetFirstBy(x => x.Length); 458 | Check.That(lengthFromContainer).Equals(0d); 459 | Client.DeleteIndex(index); 460 | } 461 | 462 | [Fact] 463 | public void Non_Available_Conditional_Stat() 464 | { 465 | var index = CreateTestIndex(); 466 | 467 | var result = Client.Search(search => search.Index(index).Take(10).Aggregations(agg => agg)); 468 | var exception = Record.Exception(() => result.Aggregations.GetCount(x => x.CarType)); 469 | Assert.NotNull(exception); 470 | Assert.IsType(exception); 471 | Check.That(exception.Message).Contains("No aggregations"); 472 | } 473 | } 474 | } 475 | -------------------------------------------------------------------------------- /FluentNest.Tests/TestsBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Text; 7 | using Elasticsearch.Net; 8 | using Nest; 9 | using NFluent; 10 | 11 | namespace Tests 12 | { 13 | public class TestsBase 14 | { 15 | protected ElasticClient Client; 16 | 17 | private readonly Dictionary testResults; 18 | 19 | public TestsBase(ConnectionSettings.SourceSerializerFactory serializerFactory = null, Func additionalSettings = null) 20 | { 21 | var node = new Uri("http://localhost:9200"); 22 | var connectionPool = new SingleNodeConnectionPool(node); 23 | 24 | var settings = new ConnectionSettings(connectionPool, serializerFactory) 25 | .DefaultIndex("fluentnesttests") 26 | .ThrowExceptions(); 27 | if (additionalSettings != null) 28 | { 29 | settings = additionalSettings(settings); 30 | } 31 | 32 | Client = new ElasticClient(settings); 33 | 34 | testResults = LoadTestResults(this.GetType().Name); 35 | } 36 | 37 | public string Serialize(T entity) 38 | { 39 | using (var ms = new MemoryStream()) 40 | { 41 | Client.SourceSerializer.Serialize(entity, ms); 42 | return Encoding.UTF8.GetString(ms.ToArray()); 43 | } 44 | } 45 | 46 | private static readonly string AssemblyPath = Path.GetDirectoryName(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath); 47 | 48 | public Dictionary LoadTestResults(string className) 49 | { 50 | var fileName = Path.Combine(AssemblyPath, className + ".txt"); 51 | if (!File.Exists(fileName)) 52 | { 53 | return new Dictionary(); 54 | } 55 | 56 | var testLines = File.ReadAllText(className + ".txt").Split("###".ToCharArray()).Select(x=>x.Trim()); 57 | var values = testLines.Where(x=>x.Contains("***")).Select(x=>x.Trim()).Select(x => 58 | { 59 | var testContent = x.Split("***".ToCharArray()).Where(y => !string.IsNullOrWhiteSpace(y)).ToArray(); 60 | return new 61 | { 62 | Name = testContent[0].Trim(), 63 | Json = testContent[1].Trim() 64 | }; 65 | }); 66 | 67 | return values.ToDictionary(x => x.Name, y => y.Json); 68 | } 69 | 70 | public void CheckSD(SearchDescriptor sc, string testName) where T: class 71 | { 72 | var json = Serialize(sc); 73 | var escaped = string.Join("", json.Where(c => !char.IsWhiteSpace(c))); 74 | 75 | var expected = testResults[testName]; 76 | var escapedExpected = string.Join("", expected.Where(c => !char.IsWhiteSpace(c))); 77 | 78 | Check.That(escaped).Equals(escapedExpected); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /FluentNest.Tests/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /FluentNest.Tests/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /FluentNest.Tests/paket.references: -------------------------------------------------------------------------------- 1 | NEST 2 | NEST.JsonNetSerializer 3 | NFluent 4 | xunit 5 | xunit.runner.visualstudio -------------------------------------------------------------------------------- /FluentNest.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25123.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".paket", ".paket", "{89A31101-BA64-47F0-AE83-4694EF2198C9}" 7 | ProjectSection(SolutionItems) = preProject 8 | paket.dependencies = paket.dependencies 9 | EndProjectSection 10 | EndProject 11 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentNest", "FluentNest\FluentNest.csproj", "{4AA84915-4E63-48B5-B27B-579A56954B67}" 12 | EndProject 13 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentNest.Tests", "FluentNest.Tests\FluentNest.Tests.csproj", "{7F4479CC-E444-4A63-AD47-762C7390E2FD}" 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0B655168-DED9-4758-9DCD-EDA18014E233}" 16 | ProjectSection(SolutionItems) = preProject 17 | paket.cmd = paket.cmd 18 | paket.dependencies = paket.dependencies 19 | paket.lock = paket.lock 20 | README.md = README.md 21 | run_es.ps1 = run_es.ps1 22 | tests.cmd = tests.cmd 23 | EndProjectSection 24 | EndProject 25 | Global 26 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 27 | Debug|Any CPU = Debug|Any CPU 28 | Debug|x64 = Debug|x64 29 | Release|Any CPU = Release|Any CPU 30 | Release|x64 = Release|x64 31 | EndGlobalSection 32 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 33 | {4AA84915-4E63-48B5-B27B-579A56954B67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {4AA84915-4E63-48B5-B27B-579A56954B67}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {4AA84915-4E63-48B5-B27B-579A56954B67}.Debug|x64.ActiveCfg = Debug|Any CPU 36 | {4AA84915-4E63-48B5-B27B-579A56954B67}.Debug|x64.Build.0 = Debug|Any CPU 37 | {4AA84915-4E63-48B5-B27B-579A56954B67}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {4AA84915-4E63-48B5-B27B-579A56954B67}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {4AA84915-4E63-48B5-B27B-579A56954B67}.Release|x64.ActiveCfg = Release|Any CPU 40 | {4AA84915-4E63-48B5-B27B-579A56954B67}.Release|x64.Build.0 = Release|Any CPU 41 | {7F4479CC-E444-4A63-AD47-762C7390E2FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {7F4479CC-E444-4A63-AD47-762C7390E2FD}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {7F4479CC-E444-4A63-AD47-762C7390E2FD}.Debug|x64.ActiveCfg = Debug|Any CPU 44 | {7F4479CC-E444-4A63-AD47-762C7390E2FD}.Debug|x64.Build.0 = Debug|Any CPU 45 | {7F4479CC-E444-4A63-AD47-762C7390E2FD}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {7F4479CC-E444-4A63-AD47-762C7390E2FD}.Release|Any CPU.Build.0 = Release|Any CPU 47 | {7F4479CC-E444-4A63-AD47-762C7390E2FD}.Release|x64.ActiveCfg = Release|Any CPU 48 | {7F4479CC-E444-4A63-AD47-762C7390E2FD}.Release|x64.Build.0 = Release|Any CPU 49 | EndGlobalSection 50 | GlobalSection(SolutionProperties) = preSolution 51 | HideSolutionNode = FALSE 52 | EndGlobalSection 53 | EndGlobal 54 | -------------------------------------------------------------------------------- /FluentNest/.gitignore: -------------------------------------------------------------------------------- 1 | *.v2.ncrunchproject 2 | -------------------------------------------------------------------------------- /FluentNest/AggType.cs: -------------------------------------------------------------------------------- 1 | namespace FluentNest 2 | { 3 | public enum AggType 4 | { 5 | Min, 6 | Max, 7 | Average, 8 | Percentile, 9 | Sum, 10 | Count, 11 | Distinct, 12 | Cardinality, 13 | Stats, 14 | GroupBy, 15 | TopHits, 16 | First 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /FluentNest/AggsContainer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using Nest; 6 | 7 | namespace FluentNest 8 | { 9 | public class AggsContainer 10 | { 11 | private readonly AggregateDictionary aggs; 12 | 13 | public AggsContainer(AggregateDictionary aggs) 14 | { 15 | this.aggs = aggs; 16 | } 17 | 18 | public int GetCardinality(Expression> fieldGetter, Expression> filterRule = null) 19 | { 20 | return aggs.GetCardinality(fieldGetter, filterRule); 21 | } 22 | 23 | public TValue GetSum(Expression> fieldGetter, Expression> filterRule = null) 24 | { 25 | return aggs.GetSum(fieldGetter, filterRule); 26 | } 27 | 28 | public TValue GetAverage(Expression> fieldGetter) 29 | { 30 | return aggs.GetAverage(fieldGetter); 31 | } 32 | 33 | public TValue GetMin(Expression> fieldGetter) 34 | { 35 | return aggs.GetMin(fieldGetter); 36 | } 37 | 38 | public TValue GetMax(Expression> fieldGetter) 39 | { 40 | return aggs.GetMax(fieldGetter); 41 | } 42 | 43 | public int? GetCount(Expression> fieldGetter, Expression> filterRule = null) 44 | { 45 | return aggs.GetCount(fieldGetter, filterRule); 46 | } 47 | 48 | public IEnumerable GetDistinct(Expression> fieldGetter) 49 | { 50 | return aggs.GetDistinct(fieldGetter); 51 | } 52 | 53 | public IList GetPercentile(Expression> fieldGetter) 54 | { 55 | return aggs.GetPercentile(fieldGetter); 56 | } 57 | 58 | public StatsAggregate GetStats(Expression> fieldGetter) 59 | { 60 | return aggs.GetStats(fieldGetter); 61 | } 62 | 63 | public TValue GetFirstBy(Expression> fieldGetter, 64 | Expression> filterRule = null) 65 | { 66 | return aggs.GetFirstBy(fieldGetter, filterRule); 67 | } 68 | 69 | public IEnumerable> GetGroupBy(Expression> fieldGetter) 70 | { 71 | return aggs.GetGroupBy(fieldGetter); 72 | } 73 | 74 | public IEnumerable GetGroupBy(Expression> fieldGetter, Func, TItem> objectTransformer) 75 | { 76 | var buckets = aggs.GetGroupBy(fieldGetter); 77 | return buckets.Select(objectTransformer); 78 | } 79 | 80 | public IDictionary GetDictionary(Expression> keyGetter, Func, TValue> objectTransformer) 81 | { 82 | var aggName = keyGetter.GetAggName(AggType.GroupBy); 83 | var buckets = aggs.GetGroupBy(aggName); 84 | return buckets.ToDictionary(x => Filters.StringToAnything(x.Key), objectTransformer); 85 | } 86 | 87 | public IDictionary> GetDictionary(Expression> keyGetter) 88 | { 89 | var aggName = keyGetter.GetAggName(AggType.GroupBy); 90 | var buckets = aggs.GetGroupBy(aggName); 91 | return buckets.ToDictionary(x => Filters.StringToAnything(x.Key)); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /FluentNest/Filters.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | using System.Reflection; 7 | using Nest; 8 | 9 | namespace FluentNest 10 | { 11 | public static class Filters 12 | { 13 | public static string FirstCharacterToLower(this string str) 14 | { 15 | if (string.IsNullOrEmpty(str) || char.IsLower(str, 0)) 16 | return str; 17 | 18 | return char.ToLowerInvariant(str[0]) + str.Substring(1); 19 | } 20 | 21 | public static AggregationContainerDescriptor IntoDateHistogram(this AggregationContainerDescriptor innerAggregation, 22 | Expression> fieldGetter, DateInterval interval) where T : class 23 | { 24 | AggregationContainerDescriptor v = new AggregationContainerDescriptor(); 25 | var fieldName = GetName(fieldGetter); 26 | v.DateHistogram(fieldName, dr => 27 | { 28 | DateHistogramAggregationDescriptor dateAggDesc = new DateHistogramAggregationDescriptor(); 29 | dateAggDesc.Field(fieldGetter).Interval(interval); 30 | return dateAggDesc.Aggregations(x => innerAggregation); 31 | }); 32 | 33 | return v; 34 | } 35 | 36 | public static AggregationContainerDescriptor IntoHistogram(this AggregationContainerDescriptor innerAggregation, 37 | Expression> fieldGetter, int interval) where T : class 38 | { 39 | AggregationContainerDescriptor v = new AggregationContainerDescriptor(); 40 | var fieldName = GetName(fieldGetter); 41 | v.Histogram(fieldName, dr => 42 | { 43 | HistogramAggregationDescriptor dateAggDesc = new HistogramAggregationDescriptor(); 44 | dateAggDesc.Field(fieldGetter).Interval(interval); 45 | return dateAggDesc.Aggregations(x => innerAggregation); 46 | }); 47 | 48 | return v; 49 | } 50 | 51 | public static AggregationContainerDescriptor DateHistogram(this AggregationContainerDescriptor agg, 52 | Expression> fieldGetter, DateInterval dateInterval) where T : class 53 | { 54 | return agg.DateHistogram(GetName(fieldGetter), x => x.Field(fieldGetter).Interval(dateInterval)); 55 | } 56 | 57 | 58 | 59 | public static string GetName(this Expression> exp) 60 | { 61 | var fromGetFieldName = Names.GetNameFromGetFieldNamed(exp.Body); 62 | if (fromGetFieldName != null) 63 | { 64 | return fromGetFieldName; 65 | } 66 | 67 | if (exp.Body is MemberExpression memberBody) 68 | { 69 | return memberBody.Member.Name; 70 | } 71 | 72 | if (exp.Body is UnaryExpression unaryBody) 73 | { 74 | if (unaryBody.Operand is MemberExpression unaryExpression) 75 | { 76 | return unaryExpression.Member.Name; 77 | } 78 | } 79 | 80 | throw new NotImplementedException($"Left side expression too complicated - could not deduce name from {exp}"); 81 | } 82 | 83 | public static string GetAggName(this Expression> exp, AggType type) 84 | { 85 | var name = GetName(exp); 86 | return type + name; 87 | } 88 | 89 | public static IReadOnlyCollection GetDateHistogram(this KeyedBucket item, 90 | Expression> fieldGetter) 91 | { 92 | var histogramItem = item.DateHistogram(GetName(fieldGetter)); 93 | return histogramItem.Buckets; 94 | } 95 | 96 | public static IReadOnlyCollection GetDateHistogram(this AggregateDictionary aggs, 97 | Expression> fieldGetter) 98 | { 99 | var histogramItem = aggs.DateHistogram(GetName(fieldGetter)); 100 | return histogramItem.Buckets; 101 | } 102 | 103 | public static IReadOnlyCollection> GetHistogram(this AggregateDictionary aggs, 104 | Expression> fieldGetter) 105 | { 106 | var histogramItem = aggs.Histogram(GetName(fieldGetter)); 107 | return histogramItem.Buckets; 108 | } 109 | 110 | public static QueryContainer GenerateComparisonFilter(this Expression expression, ExpressionType type) 111 | where T : class 112 | { 113 | var binaryExpression = (BinaryExpression)expression; 114 | 115 | var value = GetValue(binaryExpression); 116 | 117 | if (value == null) 118 | { 119 | // if the value is null, no filters are added 120 | return new QueryContainer(); 121 | } 122 | 123 | var fieldName = GetFieldNameFromMemberOrGetFieldNamed(binaryExpression.Left); 124 | var filterDescriptor = new QueryContainerDescriptor(); 125 | 126 | switch (value) 127 | { 128 | case DateTime time: 129 | return filterDescriptor.DateRange(x => x.RangeOnDate(type, time).Field(fieldName)); 130 | case double _: 131 | case decimal _: 132 | return filterDescriptor.Range(x => x.RangeOnNumber(type, Convert.ToDouble(value)).Field(fieldName)); 133 | case int _: 134 | case long _: 135 | return filterDescriptor.Range(x => x.RangeOnNumber(type, Convert.ToInt64(value)).Field(fieldName)); 136 | } 137 | 138 | throw new InvalidOperationException("Comparison on non-supported type"); 139 | } 140 | 141 | private struct FieldOrExpression 142 | { 143 | public Field Field; 144 | public Expression> Expression; 145 | } 146 | 147 | public static QueryContainer GenerateEqualityFilter(this BinaryExpression binaryExpression) where T : class 148 | { 149 | var value = GetValue(binaryExpression); 150 | if (value == null) 151 | { 152 | return GenerateNonExistenceFilter(binaryExpression); 153 | } 154 | 155 | var queryContainerDescriptor = new QueryContainerDescriptor(); 156 | var fieldExpression = GetFieldExpression(binaryExpression.Left); 157 | return fieldExpression.Expression != null 158 | ? queryContainerDescriptor.Term(fieldExpression.Expression, value) 159 | : queryContainerDescriptor.Term(fieldExpression.Field, value); 160 | } 161 | 162 | private static QueryContainer GenerateNonExistenceFilter(BinaryExpression binaryExpression) where T : class 163 | { 164 | var queryContainerDescriptor = new QueryContainerDescriptor(); 165 | var fieldExpression = GetFieldExpression(binaryExpression.Left); 166 | return queryContainerDescriptor.Bool(b => b.MustNot(m => m.Exists(e => e.Field(fieldExpression.Expression ?? fieldExpression.Field)))); 167 | } 168 | 169 | public static QueryContainer GenerateNotEqualFilter(this BinaryExpression expression) where T : class 170 | { 171 | var equalityFilter = GenerateEqualityFilter(expression); 172 | var filterDescriptor = new QueryContainerDescriptor(); 173 | return filterDescriptor.Bool(x => x.MustNot(equalityFilter)); 174 | } 175 | 176 | public static QueryContainer GenerateBoolFilter(this Expression expression) where T : class 177 | { 178 | var filterDescriptor = new QueryContainerDescriptor(); 179 | var fieldName = expression.GenerateFilterName(); 180 | return filterDescriptor.Term(fieldName, true); 181 | } 182 | 183 | static FieldOrExpression GetFieldExpression(this Expression expression) 184 | { 185 | // We don't generalize between the member & our special method as reconstructing a "fake" expression allow 186 | // NEST to use it's custom casing rules but we want a specific name in the other case 187 | // (specifying the string in Field ctor) 188 | var memberExpression = expression as MemberExpression; 189 | if (memberExpression != null) 190 | { 191 | var memberName = GetFieldNameFromMemberOrGetFieldNamed(memberExpression); 192 | var param = Expression.Parameter(typeof(T)); 193 | var body = Expression.Convert(Expression.Property(param, memberName), typeof(object)); 194 | var exp = Expression.Lambda>(body, param); 195 | return new FieldOrExpression {Expression = exp, Field = null}; 196 | } 197 | 198 | if (expression is MethodCallExpression) 199 | { 200 | var memberName = GetFieldNameFromMemberOrGetFieldNamed(expression); 201 | if (memberName != null) 202 | { 203 | return new FieldOrExpression {Expression = null, Field = new Field(memberName)}; 204 | } 205 | } 206 | 207 | if (expression is UnaryExpression) 208 | { 209 | var unary = expression as UnaryExpression; 210 | return GetFieldExpression(unary.Operand); 211 | } 212 | 213 | throw new NotImplementedException(); 214 | } 215 | 216 | public static string GetFieldNameFromMemberOrGetFieldNamed(this Expression expression) 217 | { 218 | switch (expression) 219 | { 220 | case UnaryExpression unaryExpression when unaryExpression.NodeType == ExpressionType.Convert: 221 | return GetFieldNameFromMemberOrGetFieldNamed(unaryExpression.Operand); 222 | case MemberExpression memberExpression: 223 | return FirstCharacterToLower(memberExpression.Member.Name); 224 | } 225 | 226 | var name = Names.GetNameFromGetFieldNamed(expression); 227 | if (name != null) 228 | { 229 | return name; 230 | } 231 | 232 | throw new InvalidOperationException($"Can't get a field name for {expression}"); 233 | } 234 | 235 | public static bool IsComparisonType(this ExpressionType expType) 236 | { 237 | return expType == ExpressionType.LessThan || expType == ExpressionType.GreaterThan || expType == ExpressionType.LessThanOrEqual || expType == ExpressionType.GreaterThanOrEqual; 238 | } 239 | 240 | public static string GetFieldName(this Expression exp) 241 | { 242 | var binary = (BinaryExpression)exp; 243 | var fieldName = GetFieldNameFromMemberOrGetFieldNamed(binary.Left); 244 | return fieldName; 245 | } 246 | 247 | public static QueryContainer GenerateFilterDescription(this Expression expression) where T:class 248 | { 249 | var expType = expression.NodeType; 250 | 251 | if (expType == ExpressionType.AndAlso) 252 | { 253 | var binaryExpression = (BinaryExpression)expression; 254 | 255 | // handle special cases of two comparisons on the same field which should be compiled into a range request 256 | if (binaryExpression.Left.NodeType.IsComparisonType() && binaryExpression.Right.NodeType.IsComparisonType()) 257 | { 258 | if(binaryExpression.Left.GetFieldName() == binaryExpression.Right.GetFieldName()) 259 | { 260 | //we supose that on left hand and right hand we have a binary expressions 261 | var leftBinary = (BinaryExpression)binaryExpression.Left; 262 | var leftValue = GetValue(leftBinary); 263 | 264 | var fieldName = GetFieldNameFromMemberOrGetFieldNamed(leftBinary.Left); 265 | 266 | var rightBinary = (BinaryExpression)binaryExpression.Right; 267 | var rightValue = GetValue(rightBinary); 268 | return Ranges.GenerateRangeFilter(fieldName, leftValue, leftBinary.NodeType, rightValue, rightBinary.NodeType); 269 | } 270 | } 271 | 272 | var rightFilter = GenerateFilterDescription(binaryExpression.Right); 273 | var filterDescriptor = new QueryContainerDescriptor(); 274 | 275 | 276 | // Detecting a series of And filters 277 | var leftSide = binaryExpression.Left; 278 | var accumulatedExpressions = new List(); 279 | while (leftSide.NodeType == ExpressionType.AndAlso) 280 | { 281 | 282 | var asBinary = (BinaryExpression)leftSide; 283 | if (asBinary.Left.NodeType != ExpressionType.AndAlso) 284 | { 285 | accumulatedExpressions.Add(asBinary.Left); 286 | } 287 | 288 | if (asBinary.Right.NodeType != ExpressionType.AndAlso) 289 | { 290 | accumulatedExpressions.Add(asBinary.Right); 291 | } 292 | 293 | leftSide = asBinary.Left; 294 | } 295 | 296 | if (accumulatedExpressions.Count > 0) 297 | { 298 | var filters = accumulatedExpressions.Select(GenerateFilterDescription).ToList(); 299 | filters.Add(rightFilter); 300 | return filterDescriptor.Bool(s=> s.Must(filters.ToArray())); 301 | } 302 | 303 | var leftFilter = GenerateFilterDescription(binaryExpression.Left); 304 | return filterDescriptor.Bool(s => s.Must(leftFilter, rightFilter)); 305 | } 306 | 307 | if (expType == ExpressionType.Or || expType == ExpressionType.OrElse) 308 | { 309 | var binaryExpression = (BinaryExpression)expression; 310 | var leftFilter = GenerateFilterDescription(binaryExpression.Left); 311 | var rightFilter = GenerateFilterDescription(binaryExpression.Right); 312 | var filterDescriptor = new QueryContainerDescriptor(); 313 | return filterDescriptor.Bool(x => x.Should(leftFilter, rightFilter).MinimumShouldMatch(1)); 314 | } 315 | 316 | if (expType == ExpressionType.Equal) 317 | { 318 | return GenerateEqualityFilter(expression as BinaryExpression); 319 | } 320 | 321 | if(expType == ExpressionType.LessThan || expType == ExpressionType.GreaterThan || expType == ExpressionType.LessThanOrEqual || expType == ExpressionType.GreaterThanOrEqual) 322 | { 323 | return GenerateComparisonFilter(expression,expType); 324 | } 325 | 326 | if (expType == ExpressionType.MemberAccess) 327 | { 328 | var memberExpression = (MemberExpression)expression; 329 | //here we handle binary expressions in the from .field.hasValue 330 | if (memberExpression.Member.Name == "HasValue") 331 | { 332 | var parentFieldExpression = (memberExpression.Expression as MemberExpression); 333 | var parentFieldName = GetFieldNameFromMemberOrGetFieldNamed(parentFieldExpression); 334 | 335 | var filterDescriptor = new QueryContainerDescriptor(); 336 | return filterDescriptor.Exists(x => x.Field(parentFieldName)); 337 | } 338 | 339 | var isProperty = memberExpression.Member.MemberType == MemberTypes.Property; 340 | 341 | if (isProperty) 342 | { 343 | var propertyType = ((PropertyInfo) memberExpression.Member).PropertyType; 344 | if (propertyType == typeof (bool)) 345 | { 346 | return GenerateBoolFilter(memberExpression); 347 | } 348 | } 349 | } 350 | 351 | if (expType == ExpressionType.Call) 352 | { 353 | var callExpression = (MethodCallExpression)expression; 354 | if (callExpression.Method.ReturnType == typeof(bool) && Names.GetNameFromGetFieldNamed(expression) != null) 355 | { 356 | return GenerateBoolFilter(callExpression); 357 | } 358 | } 359 | 360 | if (expType == ExpressionType.Lambda) 361 | { 362 | var lambda = (LambdaExpression)expression; 363 | return GenerateFilterDescription(lambda.Body); 364 | } 365 | 366 | if (expType == ExpressionType.NotEqual) 367 | { 368 | return GenerateNotEqualFilter(expression as BinaryExpression); 369 | } 370 | 371 | throw new NotImplementedException(); 372 | } 373 | 374 | public static SearchDescriptor FilterOn(this SearchDescriptor searchDescriptor, Expression> filterRule) where T : class 375 | { 376 | var filterDescriptor = GenerateFilterDescription(filterRule.Body); 377 | return searchDescriptor.Query(_ => filterDescriptor); 378 | } 379 | 380 | public static SearchDescriptor FilterOn(this SearchDescriptor searchDescriptor, QueryContainer container) where T : class 381 | { 382 | return searchDescriptor.Query(q => container); 383 | } 384 | 385 | public static DeleteByQueryDescriptor FilterOn(this DeleteByQueryDescriptor deleteDescriptor, QueryContainer container) where T : class 386 | { 387 | return deleteDescriptor.Query(q => container); 388 | } 389 | 390 | public static DeleteByQueryDescriptor FilterOn(this DeleteByQueryDescriptor deleteDescriptor, Expression> filterRule) where T : class 391 | { 392 | 393 | var filterDescriptor = GenerateFilterDescription(filterRule.Body); 394 | return deleteDescriptor.Query(_ => filterDescriptor); 395 | } 396 | 397 | 398 | public static QueryContainer AndFilteredOn(this QueryContainer queryDescriptor, Expression> filterRule) where T : class 399 | { 400 | var filterDescriptor = new QueryContainerDescriptor(); 401 | var newPartOfQuery = GenerateFilterDescription(filterRule.Body); 402 | return filterDescriptor.Bool(x => x.Must(queryDescriptor, newPartOfQuery)); 403 | } 404 | 405 | public static QueryContainer AndValueWithin(this QueryContainer queryDescriptor, Expression> fieldGetter, IEnumerable list) where T : class 406 | { 407 | var filterDescriptor = new QueryContainerDescriptor(); 408 | var newFilter = new QueryContainerDescriptor(); 409 | var newPartOfQuery = newFilter.Terms(terms=>terms.Terms(list).Field(fieldGetter)); 410 | return filterDescriptor.Bool(x => x.Must(queryDescriptor, newPartOfQuery)); 411 | } 412 | 413 | public static QueryContainer AndValueWithin(this QueryContainer queryDescriptor, Expression> fieldGetter, string item) where T : class 414 | { 415 | var filterDescriptor = new QueryContainerDescriptor(); 416 | var newFilter = new QueryContainerDescriptor(); 417 | var newPartOfQuery = newFilter.Term(term => term.Value(item).Field(fieldGetter)); 418 | return filterDescriptor.Bool(x => x.Must(queryDescriptor, newPartOfQuery)); 419 | } 420 | 421 | public static QueryContainer CreateFilter(Expression> filterRule) where T : class 422 | { 423 | return GenerateFilterDescription(filterRule); 424 | } 425 | 426 | public static QueryContainer ValueWithin(Expression> propertyGetter, IEnumerable list) where T : class 427 | { 428 | return new QueryContainerDescriptor().AndValueWithin(propertyGetter, list); 429 | } 430 | 431 | public static QueryContainer ValueWithin(Expression> propertyGetter, string item) where T : class 432 | { 433 | return new QueryContainerDescriptor().AndValueWithin(propertyGetter, item); 434 | } 435 | 436 | private static object GetValue(BinaryExpression binaryExpression) 437 | { 438 | var leftHand = binaryExpression.Left; 439 | var valueExpression = binaryExpression.Right; 440 | 441 | if (leftHand is UnaryExpression) 442 | { 443 | // This is necessary in order to avoid the automatic cast of enums to the underlying integer representation 444 | // In some cases the lambda comes in the shape (Convert(EngineType), 0), where 0 represents the first case of the EngineType enum 445 | // In such cases, we don't want the value in the Terms to be 0, but rather we pass the enum value (e.g. EngineType.Diesel) 446 | // and we let the serializer to do it's job and spit out Term("fieldName","diesel") or Term("fieldName","0") depending whether it is converting enums as integers or strings 447 | // or anything else 448 | var unaryExpression = leftHand as UnaryExpression; 449 | var operandType = unaryExpression.Operand.Type; 450 | var underlyingNullableType = Nullable.GetUnderlyingType(operandType); 451 | var typeToConsider = underlyingNullableType != null ? underlyingNullableType : operandType; 452 | if (typeToConsider.IsEnum) 453 | { 454 | valueExpression = Expression.Convert(binaryExpression.Right, operandType); 455 | } 456 | } 457 | 458 | var objectMember = Expression.Convert(valueExpression, typeof(object)); 459 | var getterLambda = Expression.Lambda>(objectMember); 460 | var getter = getterLambda.Compile(); 461 | return getter(); 462 | 463 | } 464 | 465 | public static T Parse(string value) 466 | { 467 | return (T)Enum.Parse(typeof(T), value); 468 | } 469 | 470 | public static AggsContainer AsContainer (this AggregateDictionary aggs) 471 | { 472 | return new AggsContainer(aggs); 473 | } 474 | 475 | public static K StringToAnything(string value) 476 | { 477 | if ((typeof(K).IsEnum)) 478 | { 479 | return Parse(value); 480 | } 481 | else 482 | { 483 | TypeConverter typeConverter = TypeDescriptor.GetConverter(typeof(K)); 484 | return (K)typeConverter.ConvertFromString(value); 485 | } 486 | } 487 | } 488 | } -------------------------------------------------------------------------------- /FluentNest/FluentNest.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0;net461 5 | true 6 | true 7 | CS1591;CS0618 8 | 9 | 10 | -------------------------------------------------------------------------------- /FluentNest/GroupBys.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using Nest; 6 | 7 | namespace FluentNest 8 | { 9 | public static class GroupBys 10 | { 11 | /// 12 | /// Creates a nested aggregation. Wraps the aggregation on which it is called by a new Terms aggregation, using the provided fieldName. 13 | /// 14 | public static AggregationContainerDescriptor GroupBy(this AggregationContainerDescriptor innerAggregation, string fieldName) where T : class 15 | { 16 | var v = new AggregationContainerDescriptor(); 17 | v.Terms(fieldName, tr => 18 | { 19 | var trmAggDescriptor = new TermsAggregationDescriptor(); 20 | trmAggDescriptor.Field(fieldName); 21 | trmAggDescriptor.Size(int.MaxValue); 22 | return trmAggDescriptor.Aggregations(x => innerAggregation); 23 | }); 24 | 25 | return v; 26 | } 27 | 28 | /// 29 | /// Creates a nested aggregation. Wraps the aggregation on which it is called by a new Terms aggregation, using the provided fieldGetter function to terms on the field. 30 | /// 31 | public static AggregationContainerDescriptor GroupBy(this AggregationContainerDescriptor innerAggregation, Expression> fieldGetter) where T : class 32 | { 33 | var fieldName = fieldGetter.GetAggName(AggType.GroupBy); 34 | var v = new AggregationContainerDescriptor(); 35 | v.Terms(fieldName, tr => 36 | { 37 | var trmAggDescriptor = new TermsAggregationDescriptor(); 38 | trmAggDescriptor.Field(fieldGetter); 39 | trmAggDescriptor.Size(int.MaxValue); 40 | return trmAggDescriptor.Aggregations(x => innerAggregation); 41 | }); 42 | 43 | return v; 44 | } 45 | 46 | /// 47 | /// Groups on the list of provided fields, returns multiple nested groups 48 | /// 49 | public static AggregationContainerDescriptor GroupBy(this AggregationContainerDescriptor innerAggregation, IEnumerable keys) where T : class 50 | { 51 | var reversedAndLowered = keys.Select(x => x.FirstCharacterToLower()).Reverse().ToList(); 52 | var aggregations = reversedAndLowered.Aggregate(innerAggregation, (s, i) => s.GroupBy(i)); 53 | return aggregations; 54 | } 55 | 56 | /// 57 | /// Retrieves the terms aggregation just by it's name 58 | /// 59 | public static IReadOnlyCollection> GetGroupBy(this AggregateDictionary aggs, string aggName) 60 | { 61 | aggs.CheckForAggregationInResult(aggName); 62 | var itemsTerms = aggs.Terms(aggName); 63 | return itemsTerms.Buckets; 64 | } 65 | 66 | /// 67 | /// Retrieves the list of buckets if terms aggregation is present 68 | /// 69 | public static IEnumerable> GetGroupBy(this AggregateDictionary aggs, Expression> fieldGetter) 70 | { 71 | var aggName = fieldGetter.GetAggName(AggType.GroupBy); 72 | return aggs.GetGroupBy(aggName); 73 | } 74 | 75 | /// 76 | /// Checks if aggregation with given name is available on the result and throws if not 77 | /// 78 | public static void CheckForAggregationInResult(this AggregateDictionary aggs, string aggName) 79 | { 80 | if (aggs == null || aggs.Count == 0) 81 | { 82 | throw new InvalidOperationException("No aggregations available on the result"); 83 | } 84 | 85 | if (!aggs.ContainsKey(aggName)) 86 | { 87 | var availableAggregations = aggs.Select(x => x.Key).Aggregate((agg, x) => agg + "m" + x); 88 | throw new InvalidOperationException($"Aggregation {aggName} not in the result. Available aggregations: {availableAggregations}"); 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /FluentNest/Names.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using System.Reflection; 4 | 5 | namespace FluentNest 6 | { 7 | public static class Names 8 | { 9 | public static TField GetFieldNamed(this object target, string name) 10 | { 11 | throw new InvalidOperationException("This method should be used in extensions."); 12 | } 13 | 14 | private static MethodInfo getFieldNamedMethod = typeof(Names).GetMethod("GetFieldNamed"); 15 | 16 | public static string GetNameFromGetFieldNamed(Expression expression) 17 | { 18 | if (expression is UnaryExpression unaryExpression && unaryExpression.NodeType == ExpressionType.Convert) 19 | { 20 | return GetNameFromGetFieldNamed(unaryExpression.Operand); 21 | } 22 | 23 | if (!(expression is MethodCallExpression methodCall)) 24 | { 25 | return null; 26 | } 27 | 28 | if (methodCall.Method.GetGenericMethodDefinition() != getFieldNamedMethod) 29 | { 30 | return null; 31 | } 32 | 33 | if (!(methodCall.Arguments[1] is Expression nameParam) || nameParam.Type != typeof(string)) 34 | { 35 | return null; 36 | } 37 | 38 | return GetStringOf(nameParam); 39 | } 40 | 41 | private static string GetStringOf(Expression expr) 42 | { 43 | if (expr.Type != typeof(string)) 44 | { 45 | throw new InvalidOperationException("string only"); 46 | } 47 | 48 | if (expr is ConstantExpression constant) 49 | { 50 | return (string)constant.Value; 51 | } 52 | 53 | if (expr is MemberExpression memberExpr && memberExpr.Expression is ConstantExpression target) 54 | { 55 | if (memberExpr.Member.MemberType == MemberTypes.Field) 56 | { 57 | FieldInfo fi = (FieldInfo)memberExpr.Member; 58 | return (string)fi.GetValue(target.Value); 59 | } 60 | else 61 | { 62 | PropertyInfo pi = (PropertyInfo)memberExpr.Member; 63 | return (string)pi.GetValue(target.Value); 64 | } 65 | } 66 | 67 | if (expr is BinaryExpression methodCall && methodCall.NodeType == ExpressionType.Add) 68 | { 69 | var left = GetStringOf(methodCall.Left); 70 | var right = GetStringOf(methodCall.Right); 71 | return left + right; 72 | } 73 | 74 | var lambda = (Expression>)Expression.Lambda(expr); 75 | return lambda.Compile().Invoke(); 76 | } 77 | 78 | public static string GenerateFilterName(this Expression expression) 79 | { 80 | var fromGetFieldName = GetNameFromGetFieldNamed(expression); 81 | if (fromGetFieldName != null) 82 | { 83 | return fromGetFieldName; 84 | } 85 | 86 | var expType = expression.NodeType; 87 | 88 | if (expType == ExpressionType.AndAlso || expType == ExpressionType.Or || expType == ExpressionType.OrElse || expType == ExpressionType.Equal 89 | || expType == ExpressionType.LessThan || expType == ExpressionType.GreaterThan || expType == ExpressionType.LessThanOrEqual || expType == ExpressionType.GreaterThanOrEqual 90 | || expType == ExpressionType.NotEqual) 91 | { 92 | var binaryExpression = (BinaryExpression)expression; 93 | var leftFilterName = GenerateFilterName(binaryExpression.Left); 94 | var rightFilterName = GenerateFilterName(binaryExpression.Right); 95 | return leftFilterName + "_" + expType + "_" + rightFilterName; 96 | } 97 | 98 | if (expType == ExpressionType.MemberAccess) 99 | { 100 | var memberExpression = (MemberExpression)expression; 101 | //here we handle binary expressions in the from .field.hasValue 102 | if (memberExpression.Member.Name == "HasValue") 103 | { 104 | var parentFieldExpression = (memberExpression.Expression as MemberExpression); 105 | var parentFieldName = parentFieldExpression.GetFieldNameFromMemberOrGetFieldNamed(); 106 | return parentFieldName + ".hasValue"; 107 | } 108 | return memberExpression.GetFieldNameFromMemberOrGetFieldNamed(); 109 | } 110 | if (expType == ExpressionType.Lambda) 111 | { 112 | var lambda = (LambdaExpression)expression; 113 | return GenerateFilterName(lambda.Body); 114 | } 115 | if (expType == ExpressionType.Convert) 116 | { 117 | var unary = (UnaryExpression)expression; 118 | return GenerateFilterName(unary.Operand); 119 | } 120 | if (expType == ExpressionType.Constant) 121 | { 122 | var constExp = (ConstantExpression)expression; 123 | return constExp.Value.ToString(); 124 | } 125 | 126 | throw new NotImplementedException(); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /FluentNest/Ranges.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using Nest; 4 | 5 | namespace FluentNest 6 | { 7 | public static class Ranges 8 | { 9 | public static DateRangeQueryDescriptor RangeOnDate(this DateRangeQueryDescriptor filterDescriptor, ExpressionType type, DateTime value) where T : class 10 | { 11 | if (type == ExpressionType.LessThan) 12 | { 13 | return filterDescriptor.LessThan(value); 14 | } 15 | if (type == ExpressionType.GreaterThan) 16 | { 17 | return filterDescriptor.GreaterThan(value); 18 | } 19 | if (type == ExpressionType.LessThanOrEqual) 20 | { 21 | return filterDescriptor.LessThanOrEquals(value); 22 | } 23 | if (type == ExpressionType.GreaterThanOrEqual) 24 | { 25 | return filterDescriptor.GreaterThanOrEquals(value); 26 | } 27 | throw new NotImplementedException(); 28 | } 29 | 30 | public static NumericRangeQueryDescriptor RangeOnNumber(this NumericRangeQueryDescriptor filterDescriptor, ExpressionType type, double value) where T : class 31 | { 32 | if (type == ExpressionType.LessThan) 33 | { 34 | return filterDescriptor.LessThan(value); 35 | } 36 | if (type == ExpressionType.GreaterThan) 37 | { 38 | return filterDescriptor.GreaterThan(value); 39 | } 40 | if (type == ExpressionType.LessThanOrEqual) 41 | { 42 | return filterDescriptor.LessThanOrEquals(value); 43 | } 44 | if (type == ExpressionType.GreaterThanOrEqual) 45 | { 46 | return filterDescriptor.GreaterThanOrEquals(value); 47 | } 48 | throw new NotImplementedException(); 49 | } 50 | 51 | public static QueryContainer GenerateRangeFilter(string fieldName, object leftValue, ExpressionType leftType, object rightValue, ExpressionType rightType) 52 | where T : class 53 | { 54 | if (leftValue is DateTime) 55 | { 56 | var leftDate = (DateTime)leftValue; 57 | var rightDate = (DateTime)rightValue; 58 | var filterDescriptor = new QueryContainerDescriptor(); 59 | return filterDescriptor.DateRange(x => x.RangeOnDate(leftType, leftDate).RangeOnDate(rightType, rightDate).Field(fieldName)); 60 | } 61 | 62 | if (leftValue is decimal || leftValue is double || leftValue is long || leftValue is int) 63 | { 64 | var left = Convert.ToDouble(leftValue); 65 | var right = Convert.ToDouble(rightValue); 66 | var filterDescriptor = new QueryContainerDescriptor(); 67 | return filterDescriptor.Range(x => x.RangeOnNumber(leftType, left).RangeOnNumber(rightType, right).Field(fieldName)); 68 | } 69 | 70 | throw new NotImplementedException(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /FluentNest/SortType.cs: -------------------------------------------------------------------------------- 1 | namespace FluentNest 2 | { 3 | public enum SortType 4 | { 5 | Ascending, 6 | Descending 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /FluentNest/Statistics.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using Nest; 6 | 7 | namespace FluentNest 8 | { 9 | public static class Statistics 10 | { 11 | static Func, AggregationContainerDescriptor> GetAggregationFuncFromGetFieldNamed(Expression> fieldGetter, AggType aggType) where T : class 12 | { 13 | var name = Names.GetNameFromGetFieldNamed(fieldGetter.Body); 14 | 15 | if (name == null) 16 | { 17 | return null; 18 | } 19 | 20 | var namedField = new Field(name); 21 | var aggName = fieldGetter.GetAggName(aggType); 22 | switch (aggType) 23 | { 24 | case AggType.Sum: 25 | return x => x.Sum(aggName, field => field.Field(namedField)); 26 | case AggType.Count: 27 | return x => x.ValueCount(aggName, field => field.Field(namedField)); 28 | case AggType.Average: 29 | return x => x.Average(aggName, field => field.Field(namedField)); 30 | case AggType.Cardinality: 31 | return x => x.Cardinality(aggName, field => field.Field(namedField)); 32 | case AggType.Stats: 33 | return x => x.Stats(aggName, field => field.Field(namedField)); 34 | case AggType.Max: 35 | return x => x.Max(aggName, field => field.Field(namedField)); 36 | case AggType.Min: 37 | return x => x.Min(aggName, field => field.Field(namedField)); 38 | case AggType.First: 39 | return x => x.Terms(aggName, field => field.Field(namedField)); 40 | case AggType.Percentile: 41 | return x => x.Percentiles(aggName, field => field.Field(namedField)); 42 | } 43 | 44 | throw new NotImplementedException(); 45 | } 46 | 47 | private static Func, AggregationContainerDescriptor> GetAggregationFunc(Expression> fieldGetter, AggType aggType) where T : class 48 | { 49 | var fromGetFieldNamed = GetAggregationFuncFromGetFieldNamed(fieldGetter, aggType); 50 | if (fromGetFieldNamed != null) 51 | { 52 | return fromGetFieldNamed; 53 | } 54 | 55 | var aggName = fieldGetter.GetAggName(aggType); 56 | switch (aggType) 57 | { 58 | case AggType.Sum: 59 | return x => x.Sum(aggName, field => field.Field(fieldGetter)); 60 | case AggType.Count: 61 | return x => x.ValueCount(aggName, field => field.Field(fieldGetter)); 62 | case AggType.Average: 63 | return x => x.Average(aggName, field => field.Field(fieldGetter)); 64 | case AggType.Cardinality: 65 | return x => x.Cardinality(aggName, field => field.Field(fieldGetter)); 66 | case AggType.Stats: 67 | return x => x.Stats(aggName, field => field.Field(fieldGetter)); 68 | case AggType.Max: 69 | return x => x.Max(aggName, field => field.Field(fieldGetter)); 70 | case AggType.Min: 71 | return x => x.Min(aggName, field => field.Field(fieldGetter)); 72 | case AggType.First: 73 | return x => x.Terms(aggName, field => field.Field(fieldGetter)); 74 | case AggType.Percentile: 75 | return x => x.Percentiles(aggName, field => field.Field(fieldGetter)); 76 | } 77 | 78 | throw new NotImplementedException(); 79 | } 80 | 81 | private static AggregationContainerDescriptor GetStatsDescriptor(this AggregationContainerDescriptor agg, Expression> fieldGetter, AggType aggType, Expression> filterRule = null) where T : class 82 | { 83 | var aggFunc = GetAggregationFunc(fieldGetter, aggType); 84 | 85 | if (filterRule == null) 86 | { 87 | return aggFunc(agg); 88 | } 89 | 90 | var filterName = filterRule.GenerateFilterName(); 91 | agg.Filter(filterName, f => f.Filter(fd => filterRule.Body.GenerateFilterDescription()).Aggregations(aggFunc)); 92 | return agg; 93 | } 94 | 95 | public static AggregationContainerDescriptor SumBy(this AggregationContainerDescriptor agg, Expression> fieldGetter, Expression> filterRule = null) where T : class 96 | { 97 | return agg.GetStatsDescriptor(fieldGetter, AggType.Sum, filterRule); 98 | } 99 | 100 | public static AggregationContainerDescriptor CountBy(this AggregationContainerDescriptor agg, Expression> fieldGetter, Expression> filterRule = null) where T : class 101 | { 102 | return agg.GetStatsDescriptor(fieldGetter, AggType.Count, filterRule); 103 | } 104 | 105 | public static AggregationContainerDescriptor CardinalityBy(this AggregationContainerDescriptor agg, Expression> fieldGetter, Expression> filterRule = null) where T : class 106 | { 107 | return agg.GetStatsDescriptor(fieldGetter, AggType.Cardinality, filterRule); 108 | } 109 | 110 | public static AggregationContainerDescriptor AverageBy(this AggregationContainerDescriptor agg, Expression> fieldGetter, Expression> filterRule = null) where T : class 111 | { 112 | return agg.GetStatsDescriptor(fieldGetter, AggType.Average, filterRule); 113 | } 114 | 115 | public static AggregationContainerDescriptor PercentilesBy(this AggregationContainerDescriptor agg, Expression> fieldGetter, Expression> filterRule = null) where T : class 116 | { 117 | return agg.GetStatsDescriptor(fieldGetter, AggType.Percentile, filterRule); 118 | } 119 | 120 | public static AggregationContainerDescriptor MaxBy(this AggregationContainerDescriptor agg, Expression> fieldGetter, Expression> filterRule = null) where T : class 121 | { 122 | return agg.GetStatsDescriptor(fieldGetter, AggType.Max, filterRule); 123 | } 124 | 125 | public static AggregationContainerDescriptor MinBy(this AggregationContainerDescriptor agg, Expression> fieldGetter, Expression> filterRule = null) where T : class 126 | { 127 | return agg.GetStatsDescriptor(fieldGetter, AggType.Min, filterRule); 128 | } 129 | 130 | public static AggregationContainerDescriptor StatsBy(this AggregationContainerDescriptor agg, Expression> fieldGetter, Expression> filterRule = null) where T : class 131 | { 132 | return agg.GetStatsDescriptor(fieldGetter, AggType.Stats, filterRule); 133 | } 134 | 135 | public static AggregationContainerDescriptor FirstBy(this AggregationContainerDescriptor agg, Expression> fieldGetter, Expression> filterRule = null) where T : class 136 | { 137 | return agg.GetStatsDescriptor(fieldGetter, AggType.First, filterRule); 138 | } 139 | 140 | public static AggregationContainerDescriptor DistinctBy(this AggregationContainerDescriptor agg, Expression> fieldGetter) where T : class 141 | { 142 | var aggName = fieldGetter.GetAggName(AggType.Distinct); 143 | return agg.Terms(aggName, x => x.Field(fieldGetter).Size(int.MaxValue)); 144 | } 145 | 146 | public static AggregationContainerDescriptor TopHits(this AggregationContainerDescriptor agg, int size, params Expression>[] fieldGetter) where T : class 147 | { 148 | var aggName = AggType.TopHits.ToString(); 149 | return agg.TopHits(aggName, x => x.Size(size).Source(i=>i.Includes(f=>f.Fields(fieldGetter)))); 150 | } 151 | 152 | private class PromiseValue : IPromise where T : class 153 | { 154 | private readonly T value; 155 | 156 | public PromiseValue(T value) 157 | { 158 | this.value = value; 159 | } 160 | 161 | T IPromise.Value => value; 162 | } 163 | 164 | public static AggregationContainerDescriptor SortedTopHits(this AggregationContainerDescriptor agg, int size, Expression> fieldSort,SortType sorttype, params Expression>[] fieldGetter) where T : class 165 | { 166 | var aggName = sorttype + fieldSort.GetAggName(AggType.TopHits); 167 | var sortFieldDescriptor = new SortFieldDescriptor(); 168 | var fieldSortName = Names.GetNameFromGetFieldNamed(fieldSort.Body); 169 | sortFieldDescriptor = fieldSortName != null ? sortFieldDescriptor.Field(fieldSortName) : sortFieldDescriptor.Field(fieldSort); 170 | sortFieldDescriptor = sorttype == SortType.Ascending ? sortFieldDescriptor.Ascending() : sortFieldDescriptor.Descending(); 171 | 172 | var fieldNames = fieldGetter.Select(x => Names.GetNameFromGetFieldNamed(x.Body)).Where(x => x != null); 173 | var fieldGetters = fieldGetter.Where(x => Names.GetNameFromGetFieldNamed(x.Body) == null); 174 | 175 | var allFields = fieldNames.Select(x => new Field(x)).Concat(fieldGetters.Select(x => new Field(x))); 176 | 177 | return agg.TopHits( 178 | aggName, 179 | x => 180 | x 181 | .Size(size) 182 | .Source(i => i.Includes(f=>f.Fields(allFields))) 183 | .Sort(s => new PromiseValue>(new List {sortFieldDescriptor}))); 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /FluentNest/StatisticsGetters.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Security.Cryptography.X509Certificates; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using Nest; 9 | 10 | namespace FluentNest 11 | { 12 | public static class StatisticsGetters 13 | { 14 | /// 15 | /// Takes a value metric and forces a conversion to certain type 16 | /// 17 | private static TK ValueAsUndType(ValueAggregate agg) 18 | { 19 | if (agg == null) 20 | { 21 | return (TK)(object)null; 22 | } 23 | 24 | var type = typeof(TK); 25 | 26 | Type targetType; 27 | if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) 28 | { 29 | targetType = Nullable.GetUnderlyingType(type); 30 | } 31 | else 32 | { 33 | targetType = type; 34 | } 35 | 36 | if (agg.Value.HasValue && targetType != null) 37 | { 38 | // seems that by default ES stores the datetime value as unix timestamp in miliseconds 39 | if (targetType == typeof(DateTime)) 40 | { 41 | DateTime origin = new DateTime(1970, 1, 1, 0, 0, 0, 0); 42 | return (TK)(object)origin.AddMilliseconds(agg.Value.Value); 43 | } 44 | 45 | return (TK)Convert.ChangeType(agg.Value.Value, targetType); 46 | } 47 | 48 | return (TK)(object)null; 49 | } 50 | 51 | public static AggregateDictionary GetAggregationContainingResult(this AggregateDictionary aggs, 52 | Expression> filterRule = null) 53 | { 54 | if (filterRule == null) 55 | { 56 | return aggs; 57 | } 58 | 59 | var filterName = filterRule.GenerateFilterName(); 60 | aggs.CheckForAggregationInResult(filterName); 61 | return aggs.Filter(filterName); 62 | } 63 | 64 | public static int GetCardinality(this AggregateDictionary aggs, Expression> fieldGetter, Expression> filterRule = null) 65 | { 66 | var aggWithResult = GetAggregationContainingResult(aggs, filterRule); 67 | var aggName = fieldGetter.GetAggName(AggType.Cardinality); 68 | aggWithResult.CheckForAggregationInResult(aggName); 69 | var itemsTerms = aggWithResult.Cardinality(aggName); 70 | return (int)itemsTerms.Value.Value; 71 | } 72 | 73 | public static TK GetSum(this AggregateDictionary aggs, Expression> fieldGetter, Expression> filterRule = null) 74 | { 75 | var aggWithResult = GetAggregationContainingResult(aggs, filterRule); 76 | var aggName = fieldGetter.GetAggName(AggType.Sum); 77 | aggWithResult.CheckForAggregationInResult(aggName); 78 | var sumAgg = aggWithResult.Sum(aggName); 79 | return ValueAsUndType(sumAgg); 80 | } 81 | 82 | public static TK GetFirstBy(this AggregateDictionary aggs, Expression> fieldGetter, Expression> filterRule = null) 83 | { 84 | var aggWithResult = GetAggregationContainingResult(aggs, filterRule); 85 | var aggName = fieldGetter.GetAggName(AggType.First); 86 | aggWithResult.CheckForAggregationInResult(aggName); 87 | var termsAgg = aggWithResult.Terms(aggName); 88 | return Filters.StringToAnything(termsAgg.Buckets.First().Key); 89 | } 90 | 91 | public static TK GetAverage(this AggregateDictionary aggs, Expression> fieldGetter, Expression> filterRule = null) 92 | { 93 | var aggWithResult = GetAggregationContainingResult(aggs, filterRule); 94 | var aggName = fieldGetter.GetAggName(AggType.Average); 95 | aggWithResult.CheckForAggregationInResult(aggName); 96 | var avgAgg = aggWithResult.Average(aggName); 97 | return ValueAsUndType(avgAgg); 98 | } 99 | 100 | public static TK GetMin(this AggregateDictionary aggs, Expression> fieldGetter, Expression> filterRule = null) 101 | { 102 | var aggWithResult = GetAggregationContainingResult(aggs, filterRule); 103 | var aggName = fieldGetter.GetAggName(AggType.Min); 104 | aggWithResult.CheckForAggregationInResult(aggName); 105 | var minAgg = aggWithResult.Min(aggName); 106 | return ValueAsUndType(minAgg); 107 | } 108 | 109 | public static TK GetMax(this AggregateDictionary aggs, Expression> fieldGetter, Expression> filterRule = null) 110 | { 111 | var aggWithResult = GetAggregationContainingResult(aggs, filterRule); 112 | var aggName = fieldGetter.GetAggName(AggType.Max); 113 | aggWithResult.CheckForAggregationInResult(aggName); 114 | var maxAgg = aggWithResult.Max(aggName); 115 | return ValueAsUndType(maxAgg); 116 | } 117 | 118 | public static IList GetPercentile(this AggregateDictionary aggs, Expression> fieldGetter, Expression> filterRule = null) 119 | { 120 | var aggWithResult = GetAggregationContainingResult(aggs, filterRule); 121 | var aggName = fieldGetter.GetAggName(AggType.Percentile); 122 | aggWithResult.CheckForAggregationInResult(aggName); 123 | var itemsTerms = aggWithResult.Percentiles(aggName); 124 | return itemsTerms.Items; 125 | } 126 | 127 | public static StatsAggregate GetStats(this AggregateDictionary aggs, Expression> fieldGetter, Expression> filterRule = null) 128 | { 129 | var aggWithResult = GetAggregationContainingResult(aggs, filterRule); 130 | var aggName = fieldGetter.GetAggName(AggType.Stats); 131 | aggWithResult.CheckForAggregationInResult(aggName); 132 | var itemsTerms = aggWithResult.Stats(aggName); 133 | return itemsTerms; 134 | } 135 | 136 | public static int? GetCount(this AggregateDictionary aggs, Expression> fieldGetter, Expression> filterRule = null) 137 | { 138 | var aggWithResult = GetAggregationContainingResult(aggs, filterRule); 139 | var aggName = fieldGetter.GetAggName(AggType.Count); 140 | aggWithResult.CheckForAggregationInResult(aggName); 141 | var itemsTerms = aggWithResult.ValueCount(aggName); 142 | if (!itemsTerms.Value.HasValue) 143 | return null; 144 | return (int)itemsTerms.Value; 145 | } 146 | 147 | public static IEnumerable GetDistinct(this AggregateDictionary aggs, Expression> fieldGetter) 148 | { 149 | var aggName = fieldGetter.GetAggName(AggType.Distinct); 150 | var itemsTerms = aggs.Terms(aggName); 151 | var targetType = typeof(V); 152 | if (targetType.IsEnum) 153 | { 154 | return itemsTerms.Buckets.Select((x => Filters.Parse(x.Key))); 155 | } 156 | 157 | if (targetType == typeof(string)) 158 | { 159 | return itemsTerms.Buckets.Select(x => x.Key).Cast(); 160 | } 161 | 162 | if (targetType == typeof(long) || targetType == typeof(int)) 163 | { 164 | return itemsTerms.Buckets.Select(x => long.Parse(x.Key)).Cast(); 165 | } 166 | 167 | throw new NotImplementedException("You can get only distinct values of Strings, Enums, ints or long"); 168 | } 169 | 170 | public static IEnumerable GetTopHits(this AggregateDictionary aggs) where T:class 171 | { 172 | var topHits = aggs.TopHits(AggType.TopHits.ToString()); 173 | return topHits.Hits().Select(x => x.Source); 174 | } 175 | 176 | public static IEnumerable GetSortedTopHits(this AggregateDictionary aggs, Expression> sorter, SortType sortType) where T : class 177 | { 178 | var aggName = sortType + sorter.GetAggName(AggType.TopHits); 179 | var topHits = aggs.TopHits(aggName); 180 | return topHits.Hits().Select(x => x.Source); 181 | } 182 | } 183 | } -------------------------------------------------------------------------------- /FluentNest/paket.references: -------------------------------------------------------------------------------- 1 | NEST -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jan Fajfr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FluentNest 2 | 3 | [![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/fluentnest/community) 4 | 5 | Fluent way to query ElasticSearch from C# based on [NEST](https://github.com/elastic/elasticsearch-net). 6 | 7 | | Elastic Search & Nest Version | Build | Nuget | Branch | 8 | | ----------------------------- |:-----:| :----:|:------:| 9 | | 6.x | [![Build 6.x][build_6]][appveyor] | [![Nuget Package][nuget_6_shield]][nuget_6] | [master][github_6] 10 | | 5.x | [![Build 5.x][build_5]][appveyor] | [![Nuget Package][nuget_5_shield]][nuget_5] | [5.x][github_5] 11 | | 2.x | [![Build 2.x][build_2]][appveyor] | [![Nuget Package][nuget_2_shield]][nuget_2] | [2.x][github_2] 12 | | 1.7.x | [![Build 1.7.x][build_1]][appveyor] | [![Nuget Package][nuget_1_shield]][nuget_1] | [1.x][github_1] 13 | 14 | [appveyor]: https://ci.appveyor.com/project/hoonzis/fluentnest/history 15 | 16 | [build_6]: https://ci.appveyor.com/api/projects/status/wrorpoekyw416hn1/branch/master?svg=true 17 | [nuget_6]: https://www.nuget.org/packages/fluentnest 18 | [nuget_6_shield]: https://img.shields.io/nuget/v/fluentnest.svg 19 | [github_6]: https://github.com/hoonzis/fluentnest/ 20 | 21 | [build_5]: https://ci.appveyor.com/api/projects/status/wrorpoekyw416hn1/branch/5.x?svg=true 22 | [nuget_5]: https://www.nuget.org/packages/FluentNest/5.0.227 23 | [nuget_5_shield]: https://img.shields.io/badge/nuget-v5.0.227-blue.svg 24 | [github_5]: https://github.com/hoonzis/fluentnest/tree/5.x 25 | 26 | [build_2]: https://ci.appveyor.com/api/projects/status/wrorpoekyw416hn1/branch/2.x?svg=true 27 | [nuget_2]: https://www.nuget.org/packages/FluentNest/2.0.217 28 | [nuget_2_shield]: https://img.shields.io/badge/nuget-v2.0.217-blue.svg 29 | [github_2]: https://github.com/hoonzis/fluentnest/tree/2.x 30 | 31 | [build_1]: https://ci.appveyor.com/api/projects/status/wrorpoekyw416hn1/branch/1.0.0?svg=true 32 | [nuget_1]: https://www.nuget.org/packages/FluentNest/1.0.210 33 | [nuget_1_shield]: https://img.shields.io/badge/nuget-v1.0.210-blue.svg 34 | [github_1]: https://github.com/hoonzis/fluentnest/tree/1.0.0 35 | 36 | Complex queries in ElasticSearch JSON query language are barely readable. NEST takes some part of the pain away, but nested lambdas are still painful. *FluenNest* tries to simplify the query composition. Details are available in the [wiki](https://github.com/hoonzis/fluentnest/wiki/FluentNest-wiki). Motivation and few implementation details are described in [this blog post.](http://www.hoonzis.com/fluent-interface-for-elastic-search/) 37 | 38 | ## Statistics 39 | 40 | ```csharp 41 | var result = client.Search(sc => sc.Aggregations(agg => agg 42 | .SumBy(x => x.Price) 43 | .CardinalityBy(x => x.EngineType) 44 | ); 45 | 46 | var container = result.Aggregations.AsContainer(); 47 | var priceSum = container.GetSum(x => x.Price); 48 | var engines = container.GetCardinality(x => x.EngineType); 49 | ``` 50 | 51 | ## Conditional statistics 52 | 53 | Conditional sums can be quite complicated with NEST. One has to define a **Filter** aggregation with nested inner **Sum** or other aggregation. Here is quicker way with FluentNest: 54 | 55 | ```CSharp 56 | var result = client.Search(sc => sc.Aggregations(aggs => aggs 57 | .SumBy(x=>x.Price, c => c.EngineType == EngineType.Diesel) 58 | .SumBy(x=>x.Sales, c => c.CarType == "Car1")) 59 | ); 60 | ``` 61 | 62 | ## Filtering and expressions to queries compilation 63 | 64 | Filtering on multiple conditions might be complicated since you have to compose filters using **Bool** together with **Must** or **Should** methods, often resulting in huge lambdas. *FluentNest* can compile small expressions into NEST query language: 65 | 66 | ```csharp 67 | client.Search(s => s.FilterOn(f => f.Timestamp > startDate && f.Timestamp < endDate)); 68 | client.Search(s => s.FilterOn(f=> f.Ranking.HasValue || f.IsAllowed); 69 | client.Search(s => s.FilterOn(f=> f.Age > 10 || f.Age < 20 && f.Name == "Peter"); 70 | ``` 71 | 72 | ## Groups & Histograms 73 | 74 | Quite often you might want to calculate an aggregation per group or per histogram bucket: 75 | 76 | ```csharp 77 | var result = client.Search(sc => sc.Aggregations(agg => agg 78 | .SumBy(s => s.Price) 79 | .GroupBy(s => s.EngineType) 80 | ); 81 | ``` 82 | 83 | ```csharp 84 | var result = client.Search(s => s.Aggregations(agg => agg 85 | .SumBy(x => x.Price, x => x.EngineType == EngineType.Diesel) 86 | .IntoDateHistogram(date => date.Timestamp, DateInterval.Month) 87 | ); 88 | ``` 89 | 90 | Groups and histograms can be also nested: 91 | 92 | ```csharp 93 | var result = client.Search(sc => sc.Aggregations(agg => agg 94 | .SumBy(s => s.Price) 95 | .GroupBy(s => s.CarType) 96 | .IntoDateHistogram(date => date.Timestamp, DateInterval.Month)); 97 | ``` 98 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | skip_tags: true 2 | 3 | version: 6.0.{build} 4 | 5 | os: Visual Studio 2017 6 | 7 | # Done here so that the ES server has the time to initialize fully 8 | install: 9 | - ps: .\run_es.ps1 10 | 11 | configuration: release 12 | 13 | dotnet_csproj: 14 | patch: true 15 | file: 'Directory.build.props' 16 | package_version: '{version}' 17 | assembly_version: '{version}' 18 | 19 | before_build: 20 | - cmd: paket restore 21 | 22 | build_script: 23 | - cmd: build.cmd 24 | 25 | artifacts: 26 | - path: 'FluentNest\**\*.nupkg' 27 | 28 | for: 29 | - 30 | branches: 31 | only: 32 | - master 33 | 34 | deploy: 35 | provider: NuGet 36 | api_key: 37 | secure: Ax/Zt3hfzLf7jMzb9VqiCsTtGCr3c+sf6pozg3KrCm/d+qZnY2IWFmmbQYws5Frh 38 | skip_symbols: true 39 | artifact: /.*\.nupkg/ 40 | -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | setlocal 4 | 5 | call %~dp0\paket.cmd restore --silent 6 | 7 | dotnet pack -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "2.1.4" 4 | } 5 | } -------------------------------------------------------------------------------- /paket.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | "%~dp0\.paket\paket.exe" %1 -------------------------------------------------------------------------------- /paket.dependencies: -------------------------------------------------------------------------------- 1 | version 5.153.0 2 | 3 | source https://www.myget.org/F/elasticsearch-net/api/v3/index.json 4 | source https://www.nuget.org/api/v2/ 5 | 6 | nuget NEST ~> 6.4 7 | nuget NEST.JsonNetSerializer 8 | nuget Elasticsearch.Net ~> 6.4 9 | nuget NFluent 1.3.1.0 10 | nuget xunit 2.0.0 11 | nuget xunit.runner.visualstudio version_in_path: true 12 | nuget xunit.runner.console -------------------------------------------------------------------------------- /run_es.ps1: -------------------------------------------------------------------------------- 1 | Write-Host "Starting elasticsearch script" 2 | 3 | Invoke-WebRequest "https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.4.0.zip" -OutFile .\es.zip; 4 | 5 | $destFolder = "$pwd\elasticsearch-6.4.0"; 6 | 7 | $shell = new-object -com shell.application; 8 | 9 | if (Test-Path $destFolder ) 10 | { 11 | del $destFolder -Force -Recurse 12 | } 13 | 14 | Expand-Archive -Path es.zip -DestinationPath $pwd 15 | 16 | $elasticsearch = "$destFolder\bin\elasticsearch.bat" 17 | $arguments = "-d" 18 | Start-Process -NoNewWindow $elasticsearch $arguments 19 | -------------------------------------------------------------------------------- /tests.cmd: -------------------------------------------------------------------------------- 1 | call paket restore 2 | call "packages\xunit.runner.console\tools\net452\xunit.console.exe" FluentNest.Tests\bin\Debug\net45\FluentNest.Tests.dll --------------------------------------------------------------------------------