2
votes

All, I am playing around with ElasticSearch 6.x with NEST and for simplicity, I created mappings with the following POCO settings based on the NEST 6.x documentation provided here https://www.elastic.co/guide/en/elasticsearch/client/net-api/current/parent-child-relationships.html. My data model is a simple one with

  • Customer

  • Order

  • Package

  • OrderItem

    Here is the C# POCO settings

    [ElasticsearchType(Name = "customer")]
    public class Customer 
    {
        [PropertyName("customerId")]
        public int CustomerId { get; set; }
    
        [PropertyName("firstName")]
        [Text]
        public string FirstName { get; set; }
    
        [PropertyName("lastName")]
        [Text]
        public string LastName { get; set; }
    
        [PropertyName("email")]
        [Keyword]
        public string Email { get; set; }
        [PropertyName("customer_join_field")]
        public JoinField CustomerJoinField { get; set; }
    }
    
        [ElasticsearchType(Name = "order")]
        public class Order : Customer
        {
            [PropertyName("orderId")]
            public int OrderId { get; set; }        
    
            [PropertyName("orderAmount")]
            public decimal Amount { get; set; }
    
            [Nested]
            [PropertyName("packages")]
            public List<Package> Packages { get; set; }
    
            [Nested]
            [PropertyName("orderItems")]
            public List<OrderItem> OrderItems { get; set; }
    
        }
    
        public class Package
        {
            public int PackageId { get; set; }
            public int Qty { get; set; }
            public int OrderId { get; set; }
            public string Weight { get; set; }
        }
    
        public class OrderItem
        {
            public int OrderItemId { get; set; }
            public int Quantity { get; set; }
            public decimal UnitPrice { get; set; }
        }
    

Building the ES client

    public static ElasticClient ESClient
    {
        get
        {

            var connectionPool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
            var settings = new ConnectionSettings(connectionPool)
                                          .DefaultMappingFor<Customer>(i => i
                                          .IndexName("fa")
                                          .TypeName("customer"))
                                          .DefaultMappingFor<Order>(i => i
                                          .IndexName("fa")
                                          .TypeName("customer"))                                              
                                          .EnableDebugMode()
                                          .PrettyJson()
                                          .RequestTimeout(TimeSpan.FromMinutes(2));

            return new ElasticClient(settings);
        }
    }

Configuring the Index(fa)

public static void ConfigureIndex()
{
    try
    {
        var createIndexResponse = ESClient.CreateIndex("fa", c => c
             .Index<Customer>()
             .Mappings(ms => ms
             .Map<Customer>(m => m
             .RoutingField(r => r.Required())
             .AutoMap<Customer>()
             .AutoMap<Order>()
             .Properties(props => props
                .Join(j => j
                    .Name(p => p.CustomerJoinField)
                    .Relations(r => r
                        .Join<Customer, Order>()
                        )
                    )
                 )
              )
          )
     );

    }

Adding the customer and order types( Under the same customer type, since an index can have only one type in ES 6.x)

public static void AddCustomerDocument(Customer cust)
    {
        try
        {
            var result = ESClient.Index<Customer>(cust,
                 c => c
                 .Id(cust.CustomerId)//to avaoid random Ids
                 .Routing(cust.CustomerId)
                );

            //var response = ESClient.Index(cust, i => i.Routing(Routing.From(cust)));
        }
        catch (Exception ex)
        {

            throw;
        }
    }   



public static void AddOrderDocument(Order order)
    {
        try
        {


            var result = ESClient.Index<Customer>(order,
                 c => c
                 .Id(order.OrderId)//to avaoid random Ids
                 .Routing(order.CustomerId)
                );

            //var response = ESClient.IndexDocument<Order>(order);
        }
        catch (Exception ex)
        {

            throw;
        }
    }

Generated mapping in ES is

{
    "fa": {
        "mappings": {
            "customer": {
                "_routing": {
                    "required": true
                },
                "properties": {
                    "customerId": {
                        "type": "integer"
                    },
                    "customer_join_field": {
                        "type": "join",
                        "eager_global_ordinals": true,
                        "relations": {
                            "customer": "order"
                        }
                    },
                    "email": {
                        "type": "keyword"
                    },
                    "firstName": {
                        "type": "text"
                    },
                    "lastName": {
                        "type": "text"
                    },
                    "orderAmount": {
                        "type": "double"
                    },
                    "orderId": {
                        "type": "integer"
                    },
                    "orderItems": {
                        "type": "nested",
                        "properties": {
                            "orderItemId": {
                                "type": "integer"
                            },
                            "quantity": {
                                "type": "integer"
                            },
                            "unitPrice": {
                                "type": "double"
                            }
                        }
                    },
                    "packages": {
                        "type": "nested",
                        "properties": {
                            "orderId": {
                                "type": "integer"
                            },
                            "packageId": {
                                "type": "integer"
                            },
                            "qty": {
                                "type": "integer"
                            },
                            "weight": {
                                "type": "text",
                                "fields": {
                                    "keyword": {
                                        "type": "keyword",
                                        "ignore_above": 256
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

I am querying this index to get a specific orderItem under a specific customer(CustomerId= 1) with the unitprice 12.23.

This is the Query DSL I have

{

    "query":{

        "parent_id":{
            "type":"order",
            "id":"1"
        },
            "nested":{
            "path":"orderItems",
            "score_mode":"avg",
            "query":{
                "bool":{
                    "must":[
                          {"match":{"orderItems.unitPrice" : "12.23"}}
                        ]
                }
            }
        }
    }
}

When I do this, I get the below eror message

{
    "error": {
        "root_cause": [
            {
                "type": "parsing_exception",
                "reason": "[parent_id] malformed query, expected [END_OBJECT] but found [FIELD_NAME]",
                "line": 9,
                "col": 4
            }
        ],
        "type": "parsing_exception",
        "reason": "[parent_id] malformed query, expected [END_OBJECT] but found [FIELD_NAME]",
        "line": 9,
        "col": 4
    },
    "status": 400
}

My question is

  1. Is it possible(or recommended) to do both parent child relationship and nested types in ES 6.x?

  2. If not, are my options limited to denormalizing the the models( have package, OrderItems be the children of Customer, rather than Order and add fields in Package and OrderItems? ) or do multiple queries against ES and move the logic of flattening the data format I want to application side?

This is the sample data I've created ( localhost:9200/fa/customer/_search)

{
    "took": 1,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 3,
        "max_score": 1,
        "hits": [
            {
                "_index": "fa",
                "_type": "customer",
                "_id": "1",
                "_score": 1,
                "_routing": "1",
                "_source": {
                    "customerId": 1,
                    "firstName": "Rennish",
                    "lastName": "Joseph",
                    "email": "[email protected]",
                    "customer_join_field": "customer"
                }
            },
            {
                "_index": "fa",
                "_type": "customer",
                "_id": "100",
                "_score": 1,
                "_routing": "1",
                "_source": {
                    "orderId": 100,
                    "orderAmount": 23.45,
                    "packages": [
                        {
                            "packageId": 1,
                            "qty": 2,
                            "orderId": 1,
                            "weight": "2.3"
                        },
                        {
                            "packageId": 2,
                            "qty": 1,
                            "orderId": 1,
                            "weight": "2.5"
                        }
                    ],
                    "orderItems": [
                        {
                            "orderItemId": 1,
                            "quantity": 2,
                            "unitPrice": 12.23
                        },
                        {
                            "orderItemId": 2,
                            "quantity": 1,
                            "unitPrice": 10.23
                        }
                    ],
                    "customerId": 1,
                    "customer_join_field": {
                        "name": "order",
                        "parent": "1"
                    }
                }
            },
            {
                "_index": "fa",
                "_type": "customer",
                "_id": "101",
                "_score": 1,
                "_routing": "1",
                "_source": {
                    "orderId": 101,
                    "orderAmount": 23.45,
                    "packages": [
                        {
                            "packageId": 1,
                            "qty": 2,
                            "orderId": 1,
                            "weight": "2.3"
                        },
                        {
                            "packageId": 2,
                            "qty": 1,
                            "orderId": 1,
                            "weight": "2.5"
                        }
                    ],
                    "orderItems": [
                        {
                            "orderItemId": 1,
                            "quantity": 2,
                            "unitPrice": 12.23
                        },
                        {
                            "orderItemId": 2,
                            "quantity": 1,
                            "unitPrice": 10.23
                        }
                    ],
                    "customerId": 1,
                    "customer_join_field": {
                        "name": "order",
                        "parent": "1"
                    }
                }
            }
        ]
    }
}
1

1 Answers

1
votes

We ended up creating separate indices for each of these entities ( Customer, Order, Package etc) and querying against all of these indices from our application and combined the results in the app. This was the recommendation from the consultant we've been working with from Elasticsearch. We did not do the parent child relationship in any of the index mappings. This might be helpful for someone in the future.