Covariant search results

edit

NEST directly supports returning covariant result sets. Meaning a result can be typed to an interface or base class but the actual instance type of the result can be that of the subclass directly

Let’s look at an example; Imagine we want to search over multiple types that all implement ISearchResult

public interface ISearchResult
{
    string Name { get; set; }
}

public abstract class BaseX : ISearchResult
{
    public string Name { get; set; }
}

We have three implementations of ISearchResult namely A, B and C

public class A : BaseX
{
    public int PropertyOnA { get; set; }
}

public class B : BaseX
{
    public int PropertyOnB { get; set; }
}

public class C : BaseX
{
    public int PropertyOnC { get; set; }
}

Using types

edit

The most straightforward way to search over multiple types is to type the response to the parent interface or base class and pass the actual types we want to search over using .Type()

var result = this._client.Search<ISearchResult>(s => s
    .Type(Types.Type(typeof(A), typeof(B), typeof(C)))
    .Size(100)
);

NEST will translate this to a search over /index/a,b,c/_search; hits that have "_type" : "a" will be serialized to A and so forth

Here we assume our response is valid and that we received the 100 documents we are expecting. Remember result.Documents is an IReadOnlyCollection<ISearchResult>

result.ShouldBeValid();
result.Documents.Count.Should().Be(100);

To prove the returned result set is covariant we filter the documents based on their actual type and assert the returned subsets are the expected sizes

var aDocuments = result.Documents.OfType<A>();
var bDocuments = result.Documents.OfType<B>();
var cDocuments = result.Documents.OfType<C>();

aDocuments.Count().Should().Be(25);
bDocuments.Count().Should().Be(25);
cDocuments.Count().Should().Be(50);

and assume that properties that only exist on the subclass itself are properly filled

aDocuments.Should().OnlyContain(a => a.PropertyOnA > 0);
bDocuments.Should().OnlyContain(a => a.PropertyOnB > 0);
cDocuments.Should().OnlyContain(a => a.PropertyOnC > 0);

Using ConcreteTypeSelector

edit

A more low level approach is to inspect the hit yourself and determine the CLR type to deserialize to

var result = this._client.Search<ISearchResult>(s => s
    .ConcreteTypeSelector((d, h) => h.Type == "a" ? typeof(A) : h.Type == "b" ? typeof(B) : typeof(C))
    .Size(100)
);

here for each hit we’ll call the delegate passed to ConcreteTypeSelector where

  • d is a representation of the _source exposed as a dynamic type
  • a typed h which represents the encapsulating hit of the source i.e. Hit<dynamic>

Here we assume our response is valid and that we received the 100 documents we are expecting. Remember result.Documents is an IReadOnlyCollection<ISearchResult>

result.ShouldBeValid();
result.Documents.Count.Should().Be(100);

To prove the returned result set is covariant we filter the documents based on their actual type and assert the returned subsets are the expected sizes

var aDocuments = result.Documents.OfType<A>();
var bDocuments = result.Documents.OfType<B>();
var cDocuments = result.Documents.OfType<C>();

aDocuments.Count().Should().Be(25);
bDocuments.Count().Should().Be(25);
cDocuments.Count().Should().Be(50);

and assume that properties that only exist on the subclass itself are properly filled

aDocuments.Should().OnlyContain(a => a.PropertyOnA > 0);
bDocuments.Should().OnlyContain(a => a.PropertyOnB > 0);
cDocuments.Should().OnlyContain(a => a.PropertyOnC > 0);

Using CovariantTypes

edit

The Scroll API is a continuation of the previous Search example so Types() are lost. You can hint at the types using .CovariantTypes()

var result = this._client.Scroll<ISearchResult>(TimeSpan.FromMinutes(60), "scrollId", s => s
    .CovariantTypes(Types.Type(typeof(A), typeof(B), typeof(C)))
);

NEST will translate this to a search over /index/a,b,c/_search; hits that have "_type" : "a" will be serialized to A and so forth

Here we assume our response is valid and that we received the 100 documents we are expecting. Remember result.Documents is an IReadOnlyCollection<ISearchResult>

result.ShouldBeValid();
result.Documents.Count.Should().Be(100);

To prove the returned result set is covariant we filter the documents based on their actual type and assert the returned subsets are the expected sizes

var aDocuments = result.Documents.OfType<A>();
var bDocuments = result.Documents.OfType<B>();
var cDocuments = result.Documents.OfType<C>();

aDocuments.Count().Should().Be(25);
bDocuments.Count().Should().Be(25);
cDocuments.Count().Should().Be(50);

and assume that properties that only exist on the subclass itself are properly filled

aDocuments.Should().OnlyContain(a => a.PropertyOnA > 0);
bDocuments.Should().OnlyContain(a => a.PropertyOnB > 0);
cDocuments.Should().OnlyContain(a => a.PropertyOnC > 0);

The more low level concrete type selector can also be specified on scroll

var result = this._client.Scroll<ISearchResult>(TimeSpan.FromMinutes(1), "scrollid", s => s
    .ConcreteTypeSelector((d, h) => h.Type == "a" ? typeof(A) : h.Type == "b" ? typeof(B) : typeof(C))
);

As before, within the delegate passed to .ConcreteTypeSelector

  • d is the _source typed as dynamic
  • h is the encapsulating typed hit

Here we assume our response is valid and that we received the 100 documents we are expecting. Remember result.Documents is an IReadOnlyCollection<ISearchResult>

result.ShouldBeValid();
result.Documents.Count.Should().Be(100);

To prove the returned result set is covariant we filter the documents based on their actual type and assert the returned subsets are the expected sizes

var aDocuments = result.Documents.OfType<A>();
var bDocuments = result.Documents.OfType<B>();
var cDocuments = result.Documents.OfType<C>();

aDocuments.Count().Should().Be(25);
bDocuments.Count().Should().Be(25);
cDocuments.Count().Should().Be(50);

and assume that properties that only exist on the subclass itself are properly filled

aDocuments.Should().OnlyContain(a => a.PropertyOnA > 0);
bDocuments.Should().OnlyContain(a => a.PropertyOnB > 0);
cDocuments.Should().OnlyContain(a => a.PropertyOnC > 0);

Covariance also works over subclasses not just interfaces

var result = this._client.Search<BaseX>(s => s
    .Type(Types.Type(typeof(A), typeof(B), typeof(C)))
    .Size(100)
);
result.ShouldBeValid();
result.Documents.Count.Should().Be(100);

var aDocuments = result.Documents.OfType<A>();
var bDocuments = result.Documents.OfType<B>();
var cDocuments = result.Documents.OfType<C>();

aDocuments.Count().Should().Be(25);
bDocuments.Count().Should().Be(25);
cDocuments.Count().Should().Be(50);

aDocuments.Should().OnlyContain(a => a.PropertyOnA > 0);
bDocuments.Should().OnlyContain(a => a.PropertyOnB > 0);
cDocuments.Should().OnlyContain(a => a.PropertyOnC > 0);