Skip to content

Commit b8c03eb

Browse files
committed
Fix #606 and #607, also improved API around a great deal by exposing a .FieldSelections propery on SearchResult wich does the hard part of typing the propery accessor for you
1 parent a0b9121 commit b8c03eb

File tree

12 files changed

+121
-65
lines changed

12 files changed

+121
-65
lines changed

Diff for: src/Nest/Domain/FieldSelection.cs

+19-8
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,31 @@ public interface IFieldSelection<out T>
1717
/// As of elasticsearch fields are always returned as an array. except for internal metadata values such as routing.
1818
/// </summary>
1919
/// <typeparam name="K">The type to return the value as, remember that if your field is a string K should be string[]</typeparam>
20-
K FieldValue<K>(string path);
20+
K FieldValues<K>(string path);
2121

22-
K[] FieldValue<TBindTo, K>(Expression<Func<TBindTo, object>> objectPath)
22+
K[] FieldValues<TBindTo, K>(Expression<Func<TBindTo, object>> objectPath)
2323
where TBindTo : class;
2424

25+
IDictionary<string, object> FieldValuesDictionary { get; set; }
2526
}
2627

2728
public class FieldSelection<T> : IFieldSelection<T>
2829
{
2930
private ElasticInferrer Infer { get; set; }
30-
public FieldSelection(IConnectionSettingsValues settings, IDictionary<string, object> values = null)
31+
public FieldSelection(IConnectionSettingsValues settings, IDictionary<string, object> valuesDictionary = null)
3132
{
3233
this.Infer = new ElasticInferrer(settings);
33-
this.FieldValues = values;
34+
((IFieldSelection<T>)this).FieldValuesDictionary = valuesDictionary;
3435
}
3536

3637
[JsonConverter(typeof(DictionaryKeysAreNotPropertyNamesJsonConverter))]
37-
public IDictionary<string, object> FieldValues { get; internal set; }
38+
IDictionary<string, object> IFieldSelection<T>.FieldValuesDictionary { get; set; }
3839

3940
/// <summary>
4041
/// As of elasticsearch fields are always returned as an array. except for internal metadata values such as routing.
4142
/// </summary>
4243
/// <typeparam name="K">The type to return the value as, remember that if your field is a string K should be string[]</typeparam>
43-
public K FieldValue<K>(string path)
44+
public K FieldValues<K>(string path)
4445
{
4546
return this.FieldArray<K>(path);
4647
}
@@ -49,21 +50,31 @@ public K FieldValue<K>(string path)
4950
/// As of elasticsearch fields are always returned as an array.
5051
/// except for internal metadata values such as routing.
5152
/// </summary>
52-
public K[] FieldValue<TBindTo, K>(Expression<Func<TBindTo, object>> objectPath)
53+
public K[] FieldValues<TBindTo, K>(Expression<Func<TBindTo, object>> objectPath)
5354
where TBindTo : class
5455
{
5556
var path = this.Infer.PropertyPath(objectPath);
5657
return this.FieldArray<K[]>(path);
5758
}
5859

60+
/// <summary>
61+
/// As of elasticsearch fields are always returned as an array.
62+
/// except for internal metadata values such as routing.
63+
/// </summary>
64+
public K[] FieldValues<K>(Expression<Func<T, K>> objectPath)
65+
{
66+
var path = this.Infer.PropertyPath(objectPath);
67+
return this.FieldArray<K[]>(path);
68+
}
69+
5970
/// <summary>
6071
/// As of elasticsearch fields are always returned as an array. except for internal metadata values such as routing.
6172
/// </summary>
6273
/// <typeparam name="K">The type to return the value as, remember that if your field is a string K should be string[]</typeparam>
6374
private K FieldArray<K>(string path)
6475
{
6576
object o;
66-
if (FieldValues.TryGetValue(path, out o))
77+
if (((IFieldSelection<T>)this).FieldValuesDictionary.TryGetValue(path, out o))
6778
{
6879
var t = typeof(K);
6980
if (o is JArray && t.GetInterfaces().Contains(typeof(IEnumerable)))

Diff for: src/Nest/Domain/Responses/BaseResponse.cs

+14-3
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,28 @@ public BaseResponse()
2222
public virtual bool IsValid { get; internal set; }
2323
public IElasticsearchResponse ConnectionStatus { get; internal set; }
2424
public ElasticInferrer _infer;
25+
26+
protected IConnectionSettingsValues Settings
27+
{
28+
get
29+
{
30+
if (this.ConnectionStatus == null)
31+
return null;
32+
33+
var settings = this.ConnectionStatus.Settings as IConnectionSettingsValues;
34+
return settings;
35+
}
36+
}
37+
2538

2639
public ElasticInferrer Infer
2740
{
2841
get
2942
{
3043
if (this._infer != null)
3144
return this._infer;
32-
if (this.ConnectionStatus == null)
33-
return null;
3445

35-
var settings = this.ConnectionStatus.Settings as IConnectionSettingsValues;
46+
var settings = this.Settings;
3647
if (settings == null)
3748
return null;
3849
this._infer = new ElasticInferrer(settings);

Diff for: src/Nest/Domain/Responses/GetResponse.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,13 @@ public K[] FieldValue<TBindTo, K>(Expression<Func<TBindTo, object>> objectPath)
7474
where TBindTo : class
7575
{
7676
if (this.Fields == null) return default(K[]);
77-
return this.Fields.FieldValue<TBindTo,K>(objectPath);
77+
return this.Fields.FieldValues<TBindTo,K>(objectPath);
7878
}
7979

8080
public K FieldValue<K>(string path)
8181
{
8282
if (this.Fields == null) return default(K);
83-
return this.Fields.FieldValue<K>(path);
83+
return this.Fields.FieldValues<K>(path);
8484
}
8585

8686
}

Diff for: src/Nest/Domain/Responses/SearchResponse.cs

+38-33
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections.Generic;
2+
using Nest.Domain;
23
using Nest.Resolvers.Converters;
34
using Newtonsoft.Json;
45
using System.Linq.Expressions;
@@ -20,8 +21,21 @@ public interface ISearchResponse<T> : IResponse where T : class
2021
string ScrollId { get; }
2122
long Total { get; }
2223
double MaxScore { get; }
24+
/// <summary>
25+
/// Returns a view on the documents inside the hits that are returned.
26+
/// <para>NOTE: if you use Fields() on the search descriptor .Documents will be empty use
27+
/// .Fields instead or try the 'source filtering' feature introduced in Elasticsearch 1.0
28+
/// using .Source() on the search descriptor to get Documents of type T with only certain parts selected
29+
/// </para>
30+
/// </summary>
2331
IEnumerable<T> Documents { get; }
2432
IEnumerable<IHit<T>> Hits { get; }
33+
34+
/// <summary>
35+
/// Will return the field selections inside the hits when the search descriptor specified .Fields.
36+
/// Otherwise this will always be an empty collection.
37+
/// </summary>
38+
IEnumerable<FieldSelection<T>> FieldSelections { get; }
2539
HighlightDocumentDictionary Highlights { get; }
2640
F Facet<F>(Expression<Func<T, object>> expression) where F : class, IFacet;
2741
F Facet<F>(string fieldName) where F : class, IFacet;
@@ -54,7 +68,7 @@ public SearchResponse()
5468
public IDictionary<string, IAggregation> Aggregations { get; internal set; }
5569

5670
private AggregationsHelper _agg = null;
57-
71+
[JsonIgnore]
5872
public AggregationsHelper Aggs
5973
{
6074
get { return _agg ?? (_agg = new AggregationsHelper(this.Aggregations)); }
@@ -72,55 +86,46 @@ public AggregationsHelper Aggs
7286
[JsonProperty(PropertyName = "_scroll_id")]
7387
public string ScrollId { get; internal set; }
7488

75-
public long Total
76-
{
77-
get
78-
{
79-
if (this.HitsMetaData == null)
80-
{
81-
return 0;
82-
}
83-
return this.HitsMetaData.Total;
84-
}
85-
}
89+
[JsonIgnore]
90+
public long Total { get { return this.HitsMetaData == null ? 0 : this.HitsMetaData.Total; } }
8691

87-
public double MaxScore
88-
{
89-
get
90-
{
91-
if (this.HitsMetaData == null)
92-
{
93-
return 0;
94-
}
95-
return this.HitsMetaData.MaxScore;
96-
}
97-
}
92+
[JsonIgnore]
93+
public double MaxScore { get { return this.HitsMetaData == null ? 0 : this.HitsMetaData.MaxScore; } }
9894

9995
private IList<T> _documents;
96+
/// <inheritdoc />
97+
[JsonIgnore]
10098
public IEnumerable<T> Documents
10199
{
102100
get
103101
{
104-
if (this.HitsMetaData != null && this._documents == null)
105-
this._documents = this.HitsMetaData.Hits.Select(h => h.Source).ToList();
106-
return this._documents ?? Enumerable.Empty<T>();
102+
return this._documents ?? (this._documents = this.Hits
103+
.Select(h => h.Source)
104+
.Where(d => d != null)
105+
.ToList());
107106
}
108107
}
109108

110109
[JsonIgnore]
111110
public IEnumerable<IHit<T>> Hits
112111
{
113-
get
114-
{
115-
if (this.HitsMetaData != null)
116-
{
117-
return this.HitsMetaData.Hits;
118-
}
112+
get { return this.HitsMetaData != null ? (IEnumerable<IHit<T>>) this.HitsMetaData.Hits : new List<Hit<T>>(); }
113+
}
119114

120-
return new List<Hit<T>>();
115+
/// <inheritdoc />
116+
[JsonIgnore]
117+
public IEnumerable<FieldSelection<T>> FieldSelections
118+
{
119+
get
120+
{
121+
return this.Hits
122+
.Select(h => h.Fields)
123+
.Where(f=>f != null)
124+
.Select(f => new FieldSelection<T>(this.Settings, f.FieldValuesDictionary));
121125
}
122126
}
123127

128+
124129
public F Facet<F>(Expression<Func<T, object>> expression) where F : class, IFacet
125130
{
126131
var fieldName = this.Infer.PropertyPath(expression);

Diff for: src/Nest/Resolvers/Converters/MultiGetHitConverter.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ private static void CreateMultiHit<T>(MultiHitTuple tuple, JsonSerializer serial
4747
var source = tuple.Hit["fields"];
4848
if (source != null)
4949
{
50-
f.FieldValues = serializer.Deserialize<Dictionary<string, object>>( source.CreateReader());
50+
((IFieldSelection<T>)f).FieldValuesDictionary = serializer.Deserialize<Dictionary<string, object>>( source.CreateReader());
5151
hit.FieldSelection = f;
5252
}
5353

Diff for: src/Tests/Nest.Tests.Integration/Core/Get/GetFullTests.cs

+3-4
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,9 @@ public void GetUsingDescriptorWithTypeAndFields()
5454

5555
result.Source.Should().BeNull();
5656
result.Fields.Should().NotBeNull();
57-
result.Fields.FieldValues.Should().NotBeNull().And.HaveCount(4);
58-
result.Fields.FieldValue<ElasticsearchProject, string>(p => p.Name).Should().BeEquivalentTo(new [] {"pyelasticsearch"});
59-
result.Fields.FieldValue<ElasticsearchProject, int>(p => p.Id).Should().BeEquivalentTo( new []{1});
60-
result.Fields.FieldValue<ElasticsearchProject, string>(p => p.DoubleValue).Should().NotBeEquivalentTo(new [] {default(double) });
57+
result.Fields.FieldValues<ElasticsearchProject, string>(p => p.Name).Should().BeEquivalentTo(new [] {"pyelasticsearch"});
58+
result.Fields.FieldValues<ElasticsearchProject, int>(p => p.Id).Should().BeEquivalentTo( new []{1});
59+
result.Fields.FieldValues<ElasticsearchProject, string>(p => p.DoubleValue).Should().NotBeEquivalentTo(new [] {default(double) });
6160

6261
}
6362

Diff for: src/Tests/Nest.Tests.Integration/Core/Get/GetMultiTests.cs

+6-6
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ public void GetMultiWithMetaData()
9292

9393
var fieldSelection = personHit.FieldSelection;
9494
fieldSelection.Should().NotBeNull();
95-
fieldSelection.FieldValue<Person, int>(p=>p.Id).Should().BeEquivalentTo(new []{authorId});
96-
fieldSelection.FieldValue<Person, string>(p => p.FirstName)
95+
fieldSelection.FieldValues<Person, int>(p=>p.Id).Should().BeEquivalentTo(new []{authorId});
96+
fieldSelection.FieldValues<Person, string>(p => p.FirstName)
9797
.Should().NotBeEmpty();
9898

9999
}
@@ -122,15 +122,15 @@ public void GetMultiWithMetaDataUsingCleanApi()
122122
//personHit.FieldSelection would work too
123123
var personFieldSelection = result.GetFieldSelection<Person>(authorId);
124124
personFieldSelection.Should().NotBeNull();
125-
personFieldSelection.FieldValue<Person, int>(p => p.Id).Should().BeEquivalentTo(new []{authorId});
126-
personFieldSelection.FieldValue<Person, string>(p => p.FirstName)
125+
personFieldSelection.FieldValues<Person, int>(p => p.Id).Should().BeEquivalentTo(new []{authorId});
126+
personFieldSelection.FieldValues<Person, string>(p => p.FirstName)
127127
.Should().NotBeEmpty();
128128

129129
var projectFieldSelection = result.GetFieldSelection<ElasticsearchProject>(projectId);
130130
projectFieldSelection.Should().NotBeNull();
131-
projectFieldSelection.FieldValue<ElasticsearchProject, int>(p => p.Id)
131+
projectFieldSelection.FieldValues<ElasticsearchProject, int>(p => p.Id)
132132
.Should().BeEquivalentTo(new []{projectId});
133-
projectFieldSelection.FieldValue<ElasticsearchProject, string>(p => p.Followers.First().FirstName)
133+
projectFieldSelection.FieldValues<ElasticsearchProject, string>(p => p.Followers.First().FirstName)
134134
.Should().NotBeEmpty();
135135

136136
}

Diff for: src/Tests/Nest.Tests.Integration/Core/Get/GetTests.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@ public void GetWithFieldsDeep()
4040
).Fields;
4141

4242
Assert.NotNull(fieldSelection);
43-
var name = fieldSelection.FieldValue<ElasticsearchProject, string>(f => f.Name);
43+
var name = fieldSelection.FieldValues<ElasticsearchProject, string>(f => f.Name);
4444
Assert.IsNotEmpty(name);
45-
var list = fieldSelection.FieldValue<ElasticsearchProject, string>(f=>f.Followers.First().FirstName);
45+
var list = fieldSelection.FieldValues<ElasticsearchProject, string>(f=>f.Followers.First().FirstName);
4646
Assert.NotNull(list);
4747
Assert.IsNotEmpty(list);
4848

Diff for: src/Tests/Nest.Tests.Integration/Search/FieldTests/FieldsTest.cs

+25-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
namespace Nest.Tests.Integration.Search.FieldTests
1+
using System.Data.Common;
2+
using FluentAssertions;
3+
4+
namespace Nest.Tests.Integration.Search.FieldTests
25
{
36
using System.Collections.Generic;
47
using System.Linq;
@@ -30,8 +33,27 @@ public void Search_WithFieldsRemoved_ReturnsDocuments_ResultingArrayOfDocsShould
3033

3134
Assert.True(queryResults.IsValid);
3235

33-
foreach (var d in queryResults.Documents)
34-
Assert.IsNotNull(d);
36+
queryResults.Documents.Should().BeEmpty();
37+
38+
foreach (var doc in queryResults.FieldSelections)
39+
{
40+
// "content" is a string
41+
var content = doc.FieldValues(p => p.Content).First();
42+
content.Should().NotBeEmpty();
43+
44+
// intValues is a List<int>
45+
// the string overload needs to be typed as int[]
46+
// because elasticsearch will return special fields such as _routing, _parent
47+
// as string not string[] return [] would make this unreachable
48+
var intValues = doc.FieldValues<int[]>("intValues");
49+
intValues.Should().NotBeEmpty().And.OnlyContain(i => i != 0);
50+
51+
//functionally equivalent, we need to flatten the expression with First()
52+
//so that the returned type is int[] and not List<int>[];
53+
intValues = doc.FieldValues(p => p.IntValues.First());
54+
intValues.Should().NotBeEmpty().And.OnlyContain(i => i != 0);
55+
}
56+
3557
}
3658
}
3759
}

Diff for: src/Tests/Nest.Tests.Integration/Search/ScriptFields/ScriptFieldsTest.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public void SimpleExplain()
3030
);
3131
Assert.True(queryResults.IsValid);
3232
Assert.True(queryResults.Hits.Any());
33-
Assert.True(queryResults.Hits.All(h=>h.Fields.FieldValue<int[]>("locscriptfield").HasAny()));
33+
Assert.True(queryResults.Hits.All(h=>h.Fields.FieldValues<int[]>("locscriptfield").HasAny()));
3434
}
3535
}
3636
}

Diff for: src/Tests/Nest.Tests.MockData/DataSources/IntListSource.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public class IntListSource : DatasourceBase<List<int>>
1414
IntSource intSource = new IntSource();
1515
public override List<int> Next(IGenerationSession session)
1616
{
17-
var count = Math.Abs(intSource.Next(session)) % 3;
17+
var count = (Math.Abs(intSource.Next(session)) % 3) +1;
1818

1919
var values = new List<int>();
2020
for (var i = 0; i < count; i++ )

Diff for: src/Tests/Nest.Tests.Unit/Search/Fields/FieldsTests.cs

+9-1
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,20 @@ public void FieldsSelectionIsCovariantAsWell()
4141
results.Total.Should().Be(1605);
4242

4343
results.Hits.Should().NotBeNull().And.HaveCount(10);
44+
45+
//ugly way to get a hold of the fields
4446
var classAHits = results.Hits.OfType<Hit<ClassA>>();
4547
classAHits.Should().NotBeNull().And.HaveCount(3);
4648

4749
var classAHit = classAHits.First();
4850
classAHit.Fields.Should().NotBeNull();
49-
var lang = classAHit.Fields.FieldValue<ClassA, string>(p => p.Lang).FirstOrDefault();
51+
var lang = classAHit.Fields.FieldValues<ClassA, string>(p => p.Lang).FirstOrDefault();
52+
lang.Should().NotBeNullOrEmpty();
53+
54+
//prettier way to get a hold of the fields
55+
results.FieldSelections.Should().NotBeEmpty();
56+
var firstHit = results.FieldSelections.First();
57+
lang = firstHit.FieldValues(p => p.Lang).FirstOrDefault();
5058
lang.Should().NotBeNullOrEmpty();
5159

5260

0 commit comments

Comments
 (0)