Search code examples
c#.netelasticsearchnest

Elasticsearch null pointer when indexing parent document


I am attempting to set up a parent-child relationship in Elasticsearch, and I am consistently getting a null pointer exception from the server when trying to index the parent.

I am using Elasticsearch and NEST both at version 6.2.

I've basically been following this guide from the documentation: https://www.elastic.co/guide/en/elasticsearch/client/net-api/current/parent-child-relationships.html

Objects: (ESDocument is the base class, ESVendor is the parent, and ESLocation is the child)

    [ElasticsearchType(Name = "Doc", IdProperty = "Id")]
    public abstract class ESDocument
    {
        public int Id { get; set; }
        public JoinField MyJoinField { get; set; }
    }

    [ElasticsearchType(Name = "Vendor", IdProperty = "ConsigneeID")]
        public class ESVendor: ESDocument
        {
            public int VendorID { get; set; }
            public int VendorTypeID { get; set; }
            public int ClientID { get; set; }
            public int CompanyID { get; private set; }
            public int LocationID { get; set; }
            public bool Active { get; set; }
            public TimeSpan? OpenTime { get; set; }
            public TimeSpan? CloseTime { get; set; }
            public bool AppointmentRequired { get; set; }
            public decimal? Latitude { get; set; }
            public decimal? Longitude { get; set; }
            public bool PositionRequested { get; set; }
            public bool PositionFailed { get; set; }
            public int? NoteID { get; set; }
            public int? OldID { get; set; }


            public ESVendor(Vendor vendor)
            {
                if (vendor == null)
                {
                    return;
                }
                Id = vendor.VendorID;
                CompanyID = vendor.CompanyID;
                Active = vendor.Active;
                VendorID = vendor.VendorID;
                Latitude = vendor.Latitude;
                Longitude = vendor.Longitude;
                VendorTypeID = vendor.VendorTypeID;
                ClientID = vendor.ClientID;
                LocationID = vendor.LocationID;
                Latitude = vendor.Latitude;
                OpenTime = vendor.OpenTime;
                CloseTime = vendor.CloseTime;
                AppointmentRequired = vendor.AppointmentRequired;
                PositionRequested = vendor.PositionRequested;
                PositionFailed = vendor.PositionFailed;
                NoteID = vendor.NoteID;
                OldID = vendor.OldID;

            }

            public ESVendor()
            {
            }
        }

    [ElasticsearchType(Name = "Location", IdProperty = "LocationID")]
        public class ESLocation: ESDocument
        {
            public int LocationID { get; set; }
            public int CompanyID { get; private set; }
            public string Name { get; set; }
            public string Address { get; set; }
            public string Address2 { get; set; }
            public string CityStateZipCountry { get; set; }
            public string Note { get; set; }
            public string City { get; set; }
            public string State { get; set; }
            public string Zip { get; set; }
            public string Country { get; set; }

            public ESLocation(Location location)
            {
                if (location == null)
                {
                    return;
                }
                Id = location.LocationID;
                CompanyID = location.CompanyID;
                Address = location.Address;
                Address2 = location.Address2;
                LocationID = location.LocationID;
                Name = location.Name;
                Note = location.Note;
                City = location.City;
                State = location.State;
                Zip = location.Zip;
                Country = location.Country;
                CityStateZipCountry = location.Country + " " + location.State + " " + location.Zip + " " + location.Country;
            }

            public ESLocation()
            {
            }
        }

Mapping + Indexing:

    StaticConnectionPool connectionPool = new StaticConnectionPool(_nodes);
ConnectionSettings connectionSettings = new ConnectionSettings(connectionPool, sourceSerializer: SourceSerializer)
    .DisableDirectStreaming()
    .DefaultMappingFor<ESDocument>(m => m.IndexName("vendors").TypeName("doc"))
    .DefaultMappingFor<ESVendor>(m => m.IndexName("vendors").TypeName("doc").RelationName("Vendor_Location"))
    .DefaultMappingFor<ESLocation>(m => m.IndexName("vendors").TypeName("doc"));
ElasticClient esClient = new ElasticClient(connectionSettings);


IExistsResponse existsResponse = await esClient.IndexExistsAsync(new IndexExistsRequest(Indices.Parse("vendors")));
if (!existsResponse.Exists)
{
    esClient.CreateIndex("vendors", c => c
        .Index<ESVendor>()
        .Mappings(ms => ms
            .Map<ESVendor>(m => m
                .RoutingField(r => r.Required())
                .AutoMap<ESVendor>()
                .AutoMap<ESLocation>()
                .Properties(props => props
                    .Join(j => j
                        .Name(p => p.MyJoinField)
                        .Relations(r => r
                            .Join<ESVendor, ESLocation>()
                        )
                    )
                )
            )
        ));

}


using (Entities dbContext = new Entities())
{
    foreach (Vendor vendor in dbContext.Vendors)
    {
        if (!(await esClient.DocumentExistsAsync(new DocumentExistsRequest("vendors", typeof (Vendor), vendor.VendorID))).Exists)
        {
            ESVendor parent = new ESVendor(vendor)
            {
                MyJoinField = JoinField.Root<ESVendor>()
            };
            var result = esClient.IndexDocument<ESDocument>(parent);


            ESLocation child = new ESLocation(vendor.Location)
            {
                MyJoinField = JoinField.Link<ESLocation, ESVendor>(parent)
            };
            result = esClient.IndexDocument<ESDocument>(child);
        }
    }
}

It is consistently returning the below error every time it attempts to index the parent document.

    Invalid NEST response built from a unsuccessful low level call on POST: /vendors/doc
# Audit trail of this API call:
 - [1] BadResponse: Node: http://192.168.50.240:9200/ Took: 00:00:00.2060485
# OriginalException: Elasticsearch.Net.ElasticsearchClientException: The remote server returned an error: (500) Internal Server Error.. Call: Status code 500 from: POST /vendors/doc. ServerError: Type: null_pointer_exception Reason: "id must not be null" ---> System.Net.WebException: The remote server returned an error: (500) Internal Server Error.
   at System.Net.HttpWebRequest.GetResponse()
   at Elasticsearch.Net.HttpConnection.Request[TResponse](RequestData requestData)
   --- End of inner exception stack trace ---
# Request:
{"vendorID":1,"vendorTypeID":1,"clientID":349,"companyID":1,"locationID":4994,"active":true,"openTime":null,"closeTime":null,"appointmentRequired":false,"latitude":null,"longitude":null,"positionRequested":false,"positionFailed":false,"noteID":null,"oldID":1626,"id":1,"myJoinField":"Vendor_Location"}
# Response:
{"error":{"root_cause":[{"type":"null_pointer_exception","reason":"id must not be null"}],"type":"null_pointer_exception","reason":"id must not be null"},"status":500}

I've been fighting this for days now when it should have been a 30 minute task, I'm at my wits end.

Thank you in advance, your assistance is very much appreciated!


Solution

  • The problem is the attribute applied to EsVendor

    [ElasticsearchType(Name = "Vendor", IdProperty = "ConsigneeID")]
    public class ESVendor: ESDocument
    {
         // ...
    }
    

    The IdProperty tells NEST to infer the id for EsVendor types from the ConsigneeID property, but looking at the model provided, there is no property with that name. It looks like you don't need to specify this property in the attribute, and instead can rely on IdProperty in the ElasticsearchTypeAttribute applied to ESDocument, which will infer the Id from the Id property.

    So, removing IdProperty from the attribute applied to EsVendor

    [ElasticsearchType(Name = "Vendor")]
    public class ESVendor : ESDocument
    {
        // ...
    }
    

    will now send an index request similar to (other properties skipped for brevity) for an EsVendor

    PUT http://localhost:9200/vendors/doc/1?routing=1 
    {
      "id": 1,
      "vendorID": 1,
      "myJoinField": "Vendor_Location"
    }
    

    We might also want to lean on the compiler too, for string values that reference properties, and use nameof(Id)

    [ElasticsearchType(Name = "Doc", IdProperty = nameof(Id))]
    public abstract class ESDocument
    {
        public int Id { get; set; }
        public JoinField MyJoinField { get; set; }
    }
    

    We can in fact remove IdProperty altogether if we want, as using the Id property to infer an ID for the document is default NEST behaviour. Going further, because the types have a DefaultMappingFor<T> on ConnectionSettings, we can remove the ElasticsearchType attributes altogether, since the default mappings on connection settings will take precedence.