diff --git a/src/Nest/DSL/Filter/FilterContainer.cs b/src/Nest/DSL/Filter/FilterContainer.cs index b6d6e19d7c0..0abbd30a8e2 100644 --- a/src/Nest/DSL/Filter/FilterContainer.cs +++ b/src/Nest/DSL/Filter/FilterContainer.cs @@ -56,6 +56,8 @@ public bool IsConditionless ITypeFilter IFilterContainer.Type { get; set; } + IIndicesFilter IFilterContainer.Indices { get; set; } + IMatchAllFilter IFilterContainer.MatchAll { get; set; } IHasChildFilter IFilterContainer.HasChild { get; set; } diff --git a/src/Nest/DSL/Filter/FilterDescriptor.cs b/src/Nest/DSL/Filter/FilterDescriptor.cs index eaa281d8572..32e15cef96a 100644 --- a/src/Nest/DSL/Filter/FilterDescriptor.cs +++ b/src/Nest/DSL/Filter/FilterDescriptor.cs @@ -606,6 +606,34 @@ public FilterContainer Type(string type) filter.Value = type; return this.New(filter, f => f.Type = filter); } + + /// + /// The indices filter can be used when executed across multiple indices, allowing to have a + /// filter that executes only when executed on an index that matches a specific list of indices, + /// and another filter that executes when it is executed on an index that does not match the listed indices. + /// + public FilterContainer Indices(Action> filterSelector) where K : class + { + var filter = new IndicesFilterDescriptor(); + if (filterSelector != null) + filterSelector(filter); + + return this.New(filter, f => f.Indices = filter); + } + + /// + /// The indices filter can be used when executed across multiple indices, allowing to have a + /// filter that executes only when executed on an index that matches a specific list of indices, + /// and another filter that executes when it is executed on an index that does not match the listed indices. + /// + public FilterContainer Indices(Action> filterSelector) + { + var filter = new IndicesFilterDescriptor(); + if (filterSelector != null) + filterSelector(filter); + + return this.New(filter, f => f.Indices = filter); + } /// /// Filters documents matching the provided document / mapping type. diff --git a/src/Nest/DSL/Filter/IFilterContainer.cs b/src/Nest/DSL/Filter/IFilterContainer.cs index 88d013d6dcb..f1fb3f17410 100644 --- a/src/Nest/DSL/Filter/IFilterContainer.cs +++ b/src/Nest/DSL/Filter/IFilterContainer.cs @@ -69,6 +69,9 @@ public interface IFilterContainer [JsonProperty(PropertyName = "limit")] ILimitFilter Limit { get; set; } + [JsonProperty(PropertyName = "indices")] + IIndicesFilter Indices { get; set; } + [JsonProperty(PropertyName = "type")] ITypeFilter Type { get; set; } diff --git a/src/Nest/DSL/Filter/IndicesFilterDescriptor.cs b/src/Nest/DSL/Filter/IndicesFilterDescriptor.cs new file mode 100644 index 00000000000..a83b01634ff --- /dev/null +++ b/src/Nest/DSL/Filter/IndicesFilterDescriptor.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Nest.Resolvers.Converters; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + + +namespace Nest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + [JsonConverter(typeof(ReadAsTypeConverter>))] + public interface IIndicesFilter : IFilter + { + [JsonProperty("indices")] + IEnumerable Indices { get; set; } + + [JsonProperty("filter")] + [JsonConverter(typeof(CompositeJsonConverter>, CustomJsonConverter>))] + IFilterContainer Filter { get; set; } + + [JsonProperty("no_match_filter")] + [JsonConverter(typeof(NoMatchFilterConverter))] + IFilterContainer NoMatchFilter { get; set; } + + } + + public class NoMatchFilterConverter : CompositeJsonConverter>, CustomJsonConverter> + { + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.String) + { + var en = serializer.Deserialize(reader); + return new NoMatchFilterContainer {Shortcut = en}; + } + + return base.ReadJson(reader, objectType, existingValue, serializer); + } + } + + public class NoMatchFilterContainer : FilterContainer, ICustomJson + { + public NoMatchShortcut? Shortcut { get; set; } + + object ICustomJson.GetCustomJson() + { + if (this.Shortcut.HasValue) return this.Shortcut; + var f = ((IFilterContainer)this); + if (f.RawFilter.IsNullOrEmpty()) return f; + return new RawJson(f.RawFilter); + } + } + + public class IndicesFilter : PlainFilter, IIndicesFilter + { + protected internal override void WrapInContainer(IFilterContainer container) + { + container.Indices = this; + } + + bool IFilter.IsConditionless { get { return false; } } + public NestedScore? Score { get; set; } + public IFilterContainer Filter { get; set; } + public IFilterContainer NoMatchFilter { get; set; } + public IEnumerable Indices { get; set; } + } + + public class IndicesFilterDescriptor : FilterBase, IIndicesFilter where T : class + { + IFilterContainer IIndicesFilter.Filter { get; set; } + + IFilterContainer IIndicesFilter.NoMatchFilter { get; set; } + + IEnumerable IIndicesFilter.Indices { get; set; } + + bool IFilter.IsConditionless + { + get + { + return ((IIndicesFilter)this).NoMatchFilter == null && ((IIndicesFilter)this).Filter == null; + } + } + + public IndicesFilterDescriptor Filter(Func, FilterContainer> filterSelector) + { + var qd = new FilterDescriptor(); + var q = filterSelector(qd); + if (q.IsConditionless) + return this; + + + ((IIndicesFilter)this).Filter = q; + return this; + } + + public IndicesFilterDescriptor Filter(Func, FilterContainer> filterSelector) where K : class + { + var qd = new FilterDescriptor(); + var q = filterSelector(qd); + if (q.IsConditionless) + return this; + + ((IIndicesFilter)this).Filter = q; + return this; + } + + public IndicesFilterDescriptor NoMatchFilter(NoMatchShortcut shortcut) + { + ((IIndicesFilter)this).NoMatchFilter = new NoMatchFilterContainer { Shortcut = shortcut }; + return this; + } + + public IndicesFilterDescriptor NoMatchFilter(Func, FilterContainer> filterSelector) + { + var qd = new FilterDescriptor(); + var q = filterSelector(qd); + if (q.IsConditionless) + return this; + + ((IIndicesFilter)this).NoMatchFilter = q; + return this; + } + public IndicesFilterDescriptor NoMatchFilter(Func, IFilterContainer> filterSelector) where K : class + { + var qd = new FilterDescriptor(); + var q = filterSelector(qd); + if (q.IsConditionless) + return this; + + ((IIndicesFilter)this).NoMatchFilter = q; + return this; + } + public IndicesFilterDescriptor Indices(IEnumerable indices) + { + ((IIndicesFilter)this).Indices = indices; + return this; + } + } +} diff --git a/src/Nest/DSL/Query/IndicesQueryDescriptor.cs b/src/Nest/DSL/Query/IndicesQueryDescriptor.cs index be073858c0f..2cdac70f4bc 100644 --- a/src/Nest/DSL/Query/IndicesQueryDescriptor.cs +++ b/src/Nest/DSL/Query/IndicesQueryDescriptor.cs @@ -12,6 +12,9 @@ namespace Nest [JsonConverter(typeof(ReadAsTypeConverter>))] public interface IIndicesQuery : IQuery { + [JsonProperty("indices")] + IEnumerable Indices { get; set; } + [JsonProperty("score_mode"), JsonConverter(typeof (StringEnumConverter))] NestedScore? Score { get; set; } @@ -20,10 +23,35 @@ public interface IIndicesQuery : IQuery IQueryContainer Query { get; set; } [JsonProperty("no_match_query")] + [JsonConverter(typeof(NoMatchQueryConverter))] IQueryContainer NoMatchQuery { get; set; } + } - [JsonProperty("indices")] - IEnumerable Indices { get; set; } + public class NoMatchQueryConverter : CompositeJsonConverter>, CustomJsonConverter> + { + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.String) + { + var en = serializer.Deserialize(reader); + return new NoMatchQueryContainer {Shortcut = en}; + } + + return base.ReadJson(reader, objectType, existingValue, serializer); + } + } + + public class NoMatchQueryContainer : QueryContainer, ICustomJson + { + public NoMatchShortcut? Shortcut { get; set; } + + object ICustomJson.GetCustomJson() + { + if (this.Shortcut.HasValue) return this.Shortcut; + var f = ((IQueryContainer)this); + if (f.RawQuery.IsNullOrEmpty()) return f; + return new RawJson(f.RawQuery); + } } public class IndicesQuery : PlainQuery, IIndicesQuery @@ -81,6 +109,12 @@ public IndicesQueryDescriptor Query(Func, QueryContaine return this; } + public IndicesQueryDescriptor NoMatchQuery(NoMatchShortcut shortcut) + { + ((IIndicesQuery)this).NoMatchQuery = new NoMatchQueryContainer { Shortcut = shortcut }; + return this; + } + public IndicesQueryDescriptor NoMatchQuery(Func, QueryContainer> querySelector) { var qd = new QueryDescriptor(); diff --git a/src/Nest/Domain/DSL/NoMatchShortcut.cs b/src/Nest/Domain/DSL/NoMatchShortcut.cs new file mode 100644 index 00000000000..7524e37f78f --- /dev/null +++ b/src/Nest/Domain/DSL/NoMatchShortcut.cs @@ -0,0 +1,13 @@ +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace Nest +{ + [JsonConverter(typeof (StringEnumConverter))] + public enum NoMatchShortcut + { + [EnumMember(Value = "none")] None, + [EnumMember(Value = "all")] All + } +} \ No newline at end of file diff --git a/src/Nest/Nest.csproj b/src/Nest/Nest.csproj index 0e4ebc7c7f1..bc124e255c8 100644 --- a/src/Nest/Nest.csproj +++ b/src/Nest/Nest.csproj @@ -253,6 +253,8 @@ + + @@ -1096,4 +1098,4 @@ --> - + \ No newline at end of file diff --git a/src/Tests/Nest.Tests.Unit/Nest.Tests.Unit.csproj b/src/Tests/Nest.Tests.Unit/Nest.Tests.Unit.csproj index d1522f3a1ec..9dc40f90d12 100644 --- a/src/Tests/Nest.Tests.Unit/Nest.Tests.Unit.csproj +++ b/src/Tests/Nest.Tests.Unit/Nest.Tests.Unit.csproj @@ -402,6 +402,7 @@ + @@ -418,6 +419,7 @@ + @@ -1318,4 +1320,4 @@ --> - + \ No newline at end of file diff --git a/src/Tests/Nest.Tests.Unit/Reproduce/Reproduce1187Tests.cs b/src/Tests/Nest.Tests.Unit/Reproduce/Reproduce1187Tests.cs new file mode 100644 index 00000000000..da0295a9b5c --- /dev/null +++ b/src/Tests/Nest.Tests.Unit/Reproduce/Reproduce1187Tests.cs @@ -0,0 +1,148 @@ +using System.IO; +using FluentAssertions; +using Nest.Tests.MockData.Domain; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Nest.Tests.Unit.Reproduce +{ + [TestFixture] + public class Reproduce1187Tests : BaseJsonTests + { + [Test] + public void IsClientSideSearchQueryDeserialisable() + { + var newDescriptor = Deserialize>(original); + var actual = Serialize(newDescriptor);//Should is empty + + var descriptorJobject = JObject.Parse(actual); + var expressionList = new object[][] + { + new[] {"size"},//Works + new[] {"from"},//Works + new[] {"query", "filtered", "query", "query_string", "fields"},//Works + new[] {"query", "filtered", "filter", "bool", "must"},//Works +//Can't find contents of should + new object[] {"query", "filtered", "filter", "bool", "should", 0, "indices", "filter"}, + new object[] {"query", "filtered", "filter", "bool", "should", 0, "indices", "no_match_filter"}, + new[] {"aggs", "Databases", "terms", "field"},//Works + new[] {"aggs", "Year", "terms", "field"}//Works + }; + foreach (var e in expressionList) + VerifyJsonPath(descriptorJobject, e); + + //If we do a search without the below line + //it seems to contect /_all/object/_search instead of /_search + newDescriptor.AllTypes().AllIndices(); + } + + private static void VerifyJsonPath(JToken descriptorJobject, IEnumerable strings) + { + foreach (var item in strings) + { + Assert.IsNotNull(descriptorJobject[item], item.ToString()); + descriptorJobject = descriptorJobject[item]; + if (item.ToString() == "no_match_filter") + descriptorJobject.ToString().Should().Be("none"); + } + } + + public string Serialize(T obj) where T : class + { + var json = Encoding.UTF8.GetString(_client.Serializer.Serialize(obj)); + return json; + } + + public T Deserialize(string json) where T : class + { + return _client.Serializer.Deserialize(new MemoryStream(Encoding.UTF8.GetBytes(json))); + } + + const string original = + @"{ + ""size"":10, + ""from"":0, + ""query"":{ + ""filtered"":{ + ""query"":{ + ""query_string"":{ + ""query"":""*"", + ""default_operator"":""AND"", + ""fields"":[ + ""type1._all"", + ""type2.title"", + ""type3._all"", + ""type4._all"" + ] + } + }, + ""filter"":{ + ""bool"":{ + ""must"":[ + + ], + ""should"":[ + { + ""indices"":{ + ""index"":""index1"", + ""filter"":{ + ""terms"":{ + ""_type"":[ + ""type1"", + ""type2"", + ""type3"" + ] + } + }, + ""no_match_filter"":""none"" + } + }, + { + ""indices"":{ + ""index"":""index2"", + ""filter"":{ + ""terms"":{ + ""_type"":[ + ""type2"", + ""type4"" + ] + } + }, + ""no_match_filter"":""none"" + } + } + ] + } + } + } + }, + ""aggs"":{ + ""Databases"":{ + ""terms"":{ + ""field"":""_type"", + ""size"":5 + } + }, + ""Index"":{ + ""terms"":{ + ""field"":""_index"", + ""size"":5 + } + }, + ""Year"":{ + ""terms"":{ + ""field"":""publicationYear"", + ""size"":5 + } + } + } +}"; + + } +} diff --git a/src/Tests/Nest.Tests.Unit/Search/Filter/Singles/IndicesFilterJson.cs b/src/Tests/Nest.Tests.Unit/Search/Filter/Singles/IndicesFilterJson.cs new file mode 100644 index 00000000000..8d3e5aee31c --- /dev/null +++ b/src/Tests/Nest.Tests.Unit/Search/Filter/Singles/IndicesFilterJson.cs @@ -0,0 +1,88 @@ +using System; +using NUnit.Framework; +using Nest.Tests.MockData.Domain; +using NUnit.Framework.Constraints; + +namespace Nest.Tests.Unit.Search.Filter.Singles +{ + [TestFixture] + public class IndicesFilterJson + { + [Test] + public void IndicesFilter() + { + var s = new SearchDescriptor() + .From(0) + .Size(10) + .Filter(filter=>filter + .Indices(i=>i + .Indices(new [] { "index1", "index2"}) + .Filter(f=>f.Term(p=>p.Name, "NEST")) + .NoMatchFilter(f=>f.Term(p=>p.Name, "Elasticsearch.NET")) + ) + ); + + var json = TestElasticClient.Serialize(s); + var expected = @"{ + from: 0, + size: 10, + filter: { + indices: { + indices: [ + ""index1"", + ""index2"" + ], + filter: { + term: { + name: ""NEST"" + } + }, + no_match_filter: { + term: { + name: ""Elasticsearch.NET"" + } + } + } + } +}"; + Assert.True(json.JsonEquals(expected), json); + } + + [Test] + public void IndicesFilterWithShortcut() + { + var s = new SearchDescriptor() + .From(0) + .Size(10) + .Filter(filter=>filter + .Indices(i=>i + .Indices(new [] { "index1", "index2"}) + .Filter(f=>f.Term(p=>p.Name, "NEST")) + .NoMatchFilter(NoMatchShortcut.None) + ) + ); + + var json = TestElasticClient.Serialize(s); + var expected = @"{ + from: 0, + size: 10, + filter: { + indices: { + indices: [ + ""index1"", + ""index2"" + ], + filter: { + term: { + name: ""NEST"" + } + }, + no_match_filter: ""none"" + } + } +}"; + Assert.True(json.JsonEquals(expected), json); + } + + } +} diff --git a/src/Tests/Nest.Tests.Unit/Search/Query/Singles/IndicesQueryJson.cs b/src/Tests/Nest.Tests.Unit/Search/Query/Singles/IndicesQueryJson.cs index 94aa5344e73..918d910b8c8 100644 --- a/src/Tests/Nest.Tests.Unit/Search/Query/Singles/IndicesQueryJson.cs +++ b/src/Tests/Nest.Tests.Unit/Search/Query/Singles/IndicesQueryJson.cs @@ -67,6 +67,7 @@ public void IndicesOtherTypeQuery() }}"; Assert.True(json.JsonEquals(expected), json); } + [Test] public void IndicesQuery() { @@ -97,5 +98,35 @@ public void IndicesQuery() }}"; Assert.True(json.JsonEquals(expected), json); } + + [Test] + public void IndicesQueryShortcut() + { + var s = new SearchDescriptor() + .From(0) + .Size(10) + .Query(q => q + .Indices(fz => fz + .Indices(new[] { "elasticsearchprojects", "people", "randomindex" }) + .Query(qq => qq.Term(f => f.Name, "elasticsearch.pm")) + .NoMatchQuery(NoMatchShortcut.All) + ) + ); + var json = TestElasticClient.Serialize(s); + var expected = @"{ from: 0, size: 10, query : + { + indices: { + query: { term : { name : { value : ""elasticsearch.pm"" } } }, + no_match_query: ""all"", + indices: [ + ""elasticsearchprojects"", + ""people"", + ""randomindex"" + ] + } + }}"; + Assert.True(json.JsonEquals(expected), json); + } + } }