Skip to content

Commit 96453ca

Browse files
committed
Support 2nd level field collapsing as per elastic/elasticsearch#31808
1 parent ae973dc commit 96453ca

File tree

5 files changed

+124
-9
lines changed

5 files changed

+124
-9
lines changed

src/Nest/Search/Search/Collapsing/FieldCollapse.cs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,38 +27,52 @@ public interface IFieldCollapse
2727
[JsonProperty("inner_hits")]
2828
IInnerHits InnerHits { get; set; }
2929

30+
/// <summary>
31+
/// The expansion of the group is done by sending an additional query for each inner_hit request for each collapsed hit returned
32+
/// in the response. This can significantly slow things down if you have too many groups and/or inner_hit requests.
33+
/// The max_concurrent_group_searches request parameter can be used to control the maximum number of
34+
/// concurrent searches allowed in this phase. The default is based on the number of data nodes and the
35+
/// default search thread pool size.
36+
/// </summary>
3037
[JsonProperty("max_concurrent_group_searches")]
3138
int? MaxConcurrentGroupSearches { get; set; }
3239
}
3340

34-
/// <inheritdoc/>
41+
/// <inheritdoc cref="IFieldCollapse"/>
3542
public class FieldCollapse : IFieldCollapse
3643
{
37-
/// <inheritdoc/>
44+
/// <inheritdoc cref="IFieldCollapse.Field"/>
3845
public Field Field { get; set; }
3946

47+
/// <inheritdoc cref="IFieldCollapse.InnerHits"/>
4048
public IInnerHits InnerHits { get; set; }
4149

50+
/// <inheritdoc cref="IFieldCollapse.MaxConcurrentGroupSearches"/>
4251
public int? MaxConcurrentGroupSearches { get; set; }
52+
4353
}
4454

45-
/// <inheritdoc/>
55+
/// <inheritdoc cref="IFieldCollapse"/>
4656
public class FieldCollapseDescriptor<T> : DescriptorBase<FieldCollapseDescriptor<T>, IFieldCollapse>, IFieldCollapse
4757
where T : class
4858
{
49-
/// <inheritdoc/>
5059
Field IFieldCollapse.Field { get; set; }
5160
IInnerHits IFieldCollapse.InnerHits { get; set; }
5261
int? IFieldCollapse.MaxConcurrentGroupSearches { get; set; }
5362

63+
/// <inheritdoc cref="IFieldCollapse.MaxConcurrentGroupSearches"/>
5464
public FieldCollapseDescriptor<T> MaxConcurrentGroupSearches(int? maxConcurrentGroupSearches) =>
5565
Assign(a => a.MaxConcurrentGroupSearches = maxConcurrentGroupSearches);
5666

67+
/// <inheritdoc cref="IFieldCollapse.Field"/>
5768
public FieldCollapseDescriptor<T> Field(Field field) => Assign(a => a.Field = field);
5869

70+
/// <inheritdoc cref="IFieldCollapse.Field"/>
5971
public FieldCollapseDescriptor<T> Field(Expression<Func<T, object>> objectPath) => Assign(a => a.Field = objectPath);
6072

73+
/// <inheritdoc cref="IFieldCollapse.InnherHits"/>
6174
public FieldCollapseDescriptor<T> InnerHits(Func<InnerHitsDescriptor<T>, IInnerHits> selector = null) =>
6275
Assign(a => a.InnerHits = selector.InvokeOrDefault(new InnerHitsDescriptor<T>()));
76+
6377
}
6478
}

src/Nest/Search/Search/InnerHits/InnerHits.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ public interface IInnerHits
4141

4242
[JsonProperty("ignore_unmapped")]
4343
bool? IgnoreUnmapped { get; set; }
44+
45+
/// <summary>
46+
/// Provides a second level of collapsing, NOTE: Elasticsearch only supports collapsing up to two levels.
47+
/// </summary>
48+
[JsonProperty("collapse")]
49+
IFieldCollapse Collapse { get; set; }
4450
}
4551

4652
public class InnerHits : IInnerHits
@@ -66,6 +72,9 @@ public class InnerHits : IInnerHits
6672
public Fields DocValueFields { get; set; }
6773

6874
public bool? IgnoreUnmapped { get; set; }
75+
76+
/// <inheritdoc cref="IInnerHits.Collapse"/>
77+
public IFieldCollapse Collapse { get; set; }
6978
}
7079

7180
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
@@ -82,6 +91,7 @@ public class InnerHitsDescriptor<T> : DescriptorBase<InnerHitsDescriptor<T>, IIn
8291
IScriptFields IInnerHits.ScriptFields { get; set; }
8392
Fields IInnerHits.DocValueFields { get; set; }
8493
bool? IInnerHits.IgnoreUnmapped { get; set; }
94+
IFieldCollapse IInnerHits.Collapse { get; set; }
8595

8696
public InnerHitsDescriptor<T> From(int? from) => Assign(a => a.From = from);
8797

@@ -115,5 +125,9 @@ public InnerHitsDescriptor<T> DocValueFields(Func<FieldsDescriptor<T>, IPromise<
115125
public InnerHitsDescriptor<T> DocValueFields(Fields fields) => Assign(a => a.DocValueFields = fields);
116126

117127
public InnerHitsDescriptor<T> IgnoreUnmapped(bool? ignoreUnmapped = true) => Assign(a => a.IgnoreUnmapped = ignoreUnmapped);
128+
129+
/// <inheritdoc cref="IInnerHits.Collapse"/>
130+
public InnerHitsDescriptor<T> Collapse(Func<FieldCollapseDescriptor<T>, IFieldCollapse> collapseSelector) =>
131+
Assign(a => a.Collapse = collapseSelector?.Invoke(new FieldCollapseDescriptor<T>()));
118132
}
119133
}

src/Nest/Search/Search/SearchRequest.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,7 @@ public SearchDescriptor<T> Highlight(Func<HighlightDescriptor<T>, IHighlight> hi
399399
/// For instance the query below retrieves the best tweet for each user and sorts them by number of likes.
400400
/// <para>
401401
/// NOTE: The collapsing is applied to the top hits only and does not affect aggregations.
402+
/// You can only collapse to a depth of 2.
402403
/// </para>
403404
/// </summary>
404405
public SearchDescriptor<T> Collapse(Func<FieldCollapseDescriptor<T>, IFieldCollapse> collapseSelector) =>

src/Tests/Tests.Configuration/tests.default.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# tracked by git).
66

77
# mode either u (unit test), i (integration test) or m (mixed mode)
8-
mode: u
8+
mode: m
99
# the elasticsearch version that should be started
1010
# Can be a snapshot version of sonatype or "latest" to get the latest snapshot of sonatype
1111
elasticsearch_version: 6.4.1

src/Tests/Tests/Search/Search/Collapsing/FieldCollapseUsageTests.cs

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
using System;
2+
using Elastic.Xunit.XunitPlumbing;
23
using FluentAssertions;
34
using Nest;
45
using Tests.Core.Extensions;
56
using Tests.Core.ManagedElasticsearch.Clusters;
67
using Tests.Core.ManagedElasticsearch.NodeSeeders;
78
using Tests.Domain;
89
using Tests.Framework.Integration;
9-
using Tests.Framework.ManagedElasticsearch.Clusters;
10-
using Tests.Framework.ManagedElasticsearch.NodeSeeders;
1110
using static Nest.Infer;
1211

1312
namespace Tests.Search.Search.Collapsing
1413
{
15-
/**
16-
*/
1714
public class FieldCollapseUsageTests : SearchUsageTestBase
1815
{
1916
protected override string UrlPath => $"/{DefaultSeeder.ProjectsAliasFilter}/doc/_search";
@@ -77,4 +74,93 @@ protected override void ExpectResponse(ISearchResponse<Project> response)
7774
}
7875
}
7976
}
77+
78+
[SkipVersion("<6.4.0", "2nd level collapsing is a new feature in 6.4.0")]
79+
public class FieldCollapseSecondLevelUsageTests : SearchUsageTestBase
80+
{
81+
protected override string UrlPath => $"/{DefaultSeeder.ProjectsAliasFilter}/doc/_search";
82+
83+
public FieldCollapseSecondLevelUsageTests(ReadOnlyCluster cluster, EndpointUsage usage) : base(cluster, usage) { }
84+
85+
protected override object ExpectJson => new
86+
{
87+
_source = new { excludes = new [] { "*" } },
88+
collapse = new {
89+
field = "state",
90+
inner_hits = new {
91+
_source = new {
92+
excludes = new [] { "*" }
93+
},
94+
collapse = new {
95+
field = "name"
96+
},
97+
from = 1,
98+
name = "stateofbeing",
99+
size = 5
100+
},
101+
max_concurrent_group_searches = 1000
102+
}
103+
};
104+
105+
protected override Func<SearchDescriptor<Project>, ISearchRequest> Fluent => s => s
106+
.Source(source=>source.ExcludeAll())
107+
.Index(DefaultSeeder.ProjectsAliasFilter)
108+
.Collapse(c => c
109+
.Field(f => f.State)
110+
.MaxConcurrentGroupSearches(1000)
111+
.InnerHits(i => i
112+
.Source(source=>source.ExcludeAll())
113+
.Name(nameof(StateOfBeing).ToLowerInvariant())
114+
.Size(5)
115+
.From(1)
116+
.Collapse(c2 => c2
117+
.Field(p=>p.Name)
118+
)
119+
)
120+
);
121+
122+
protected override SearchRequest<Project> Initializer => new SearchRequest<Project>(DefaultSeeder.ProjectsAliasFilter)
123+
{
124+
Source = SourceFilter.ExcludeAll,
125+
Collapse = new FieldCollapse
126+
{
127+
Field = Field<Project>(p => p.State),
128+
MaxConcurrentGroupSearches = 1000,
129+
InnerHits = new InnerHits
130+
{
131+
Source = SourceFilter.ExcludeAll,
132+
Name = nameof(StateOfBeing).ToLowerInvariant(),
133+
Size = 5,
134+
From = 1,
135+
Collapse = new FieldCollapse
136+
{
137+
Field = Field<Project>(p=>p.Name)
138+
}
139+
}
140+
}
141+
};
142+
143+
protected override void ExpectResponse(ISearchResponse<Project> response)
144+
{
145+
var numberOfStates = Enum.GetValues(typeof(StateOfBeing)).Length;
146+
response.HitsMetadata.Total.Should().BeGreaterThan(numberOfStates);
147+
response.Hits.Count.Should().Be(numberOfStates);
148+
foreach (var hit in response.Hits)
149+
{
150+
var name = nameof(StateOfBeing).ToLowerInvariant();
151+
hit.InnerHits.Should().NotBeNull().And.ContainKey(name);
152+
var innerHits = hit.InnerHits[name];
153+
innerHits.Hits.Total.Should().BeGreaterThan(0);
154+
var i = 0;
155+
foreach (var innerHit in innerHits.Hits.Hits)
156+
{
157+
i++;
158+
innerHit.Fields.Should().NotBeEmpty()
159+
.And.ContainKey("name");
160+
}
161+
162+
i.Should().NotBe(0, "we expect to inspect 2nd level collapsed fields");
163+
}
164+
}
165+
}
80166
}

0 commit comments

Comments
 (0)