Elasticsearch.Nest 教程系列 4-5 映射:Parent/Child relationships | 子父级关系映射


如果把 官方示例 放在你本地进行测试,会发现没有问题,但当你连接测试 ES 服务器的时候,就会报各种错误:

  • 这是因为官方使用了 InMemoryConnection ,所有请求并不会真实发送到测试 ES 服务器,规避掉了不少现实问题。以下示例基于官方文档进行了修正,使用本地搭建测试 ES 服务器。

父子关系的数据结构,在日常开发过程中,使用还是比较平凡的:

  • 在 ES 6.x 之前,你可以在一个索引中包含多种 type(类型)。通过给定类型的特殊 _parent 字段映射,可以创建 1对N 关系 的父-子文档。
    • 通过这种方式在索引子项时,你需要传递一个 _parent id 作为路由键(需要确保父项、其子项及所有祖先都保存在同一个分片上)。
  • 但从 ES 6.x 开始,type 不再支持多类型,固定 _type=_doc。(另外在 ES5 的时候,ES 的 index 相当于 DB,_type 相当于 DB 中的表名,但从 ES7 开始,ES 的 index 相当于 DB中的表,_type 固定为 _doc)

嵌套对象和父子结构文档的差异对比和使用建议:

Nested Object Parent/Child
优点 文档存储再一起,读取性能高 父子文档可以独立更新
缺点 更新嵌套的子文档时,需要更新整个文档 需要额外的内存维护关系。读取性能相对差
适用场景 子文档偶尔更新,以查询为主 子文档更新频繁

既然索引不再允许将不同 _type 类型存储在同一索引中,那么如何创建父联接?

通过 JoinField 属性

唯一的要求是作为子父级关系的类必须要具有数据类型为 JoinField 的属性。通过该属性,在请求的时候,可以生成关联类型。

示例:

public abstract class MyDocument
{
    public int Id { get; set; }
    public JoinField ParentChildRelation { get; set; }
}

public class MyParent : MyDocument
{
    [Text]
    public string ParentProperty { get; set; }
}
public class MyChild : MyDocument
{
    [Text]
    public string ChildProperty { get; set; }
}
  • MyParent 和 MyChild 均继承自 MyDocument,且注意 MyDocument.ParentChildRelation 的数据类型为 JoinField (该类型是 Nest 库提供的数据类型) 。
    • 这里,parentChildRelation 即为 Join 类型的名称。

将 MyChild 和 MyParent 设置相同的索引和默认文档类型,以确保它们在同一个索引中:

  • 需要将 MyDocument,MyChild 和 MyParent 的索引名称都指定为 “index”确保它们在同一个索引中。
  • DefaultMappingFor 提供了 RelationName ,在 MyParent 上 指定子父级的关联名称为 “parent”。

public ElasticSearchClient(ElasticSearchSettings esSettings)
{
    _esSettings = esSettings;
    var settings = new ConnectionSettings(new Uri(_esSettings.ServerUri))
    .DefaultMappingFor<MyDocument>(m => m.IndexName("index"))
    .DefaultMappingFor<MyChild>(m => m.IndexName("index"))
    .DefaultMappingFor<MyParent>(m => m.IndexName("index").RelationName("parent")) //你也可以通过 RelationName 来设定关系名,最终会体现在创建索引的请求上
    .EnableDebugMode();

    _client = new ElasticClient(settings);
}

通过设置 ConnectionStrings,可以将 MyParent 和 MyChild 映射为索引的一部分,并需要确保子父级文档在同一个分片上:

var createIndexResponse = client.Indices.Create("index", c => c
    .Index<MyDocument>()
    .Map<MyDocument>(m => m
        .RoutingField(r => r.Required())  //1.加上 RoutingField,以保证父子文档被分配到同一个分片上
        .AutoMap<MyParent>()            //2.自动映射所有 MyParent 属性
        .AutoMap<MyChild>()             //3.自动映射所有 MyChild 属性
        .Properties(props => props
            .Join(j => j                        //4.AutoMap 不会映射 JoinField,需要手动指定
                .Name(p => p.MyJoinField)  // 标记A
                .Relations(r => r
                    .Join<MyParent, MyChild>()  //因为 ConnectionSettings 指定了 MyParent 的 RelationName 为 parent,所以这里在映射父/子关系的时候,父类型的名称为 “parent”,而 子类顶的名称会自动反射为类的完整名称(即包含了命名空间)。
                )
            )
        )
    )
);
  • 因为 NEST 的 AutoMap 方法不会自动配置 JoinField 映射,所以你需要手动装配。
  • 以上装配方式将 MyChild 设置为 MyParent 的子集。.Join() 方法有许多重载,你可以按需进行选择。

以上命令相当于执行了下面的 ES 命令:

PUT /index
{
    "mappings": {
        "properties": {
            "childProperty": {
                "type": "text"
            },
            "id": {
                "type": "integer"
            },
            "myJoinField": {
                "relations": {
                    "parent": "mychild" 
                },
                "type": "join"
            },
            "parentProperty": {
                "type": "text"
            }
        },
        "_routing": {
            "required": true
        }
    }
}

  • 在 ConnectionString 上设置映射的时候,把 MyParent 的关系名称设置为了“parent”。稍后在执行强类型的 has_child 和 has_parent 查询时,这也很方便。

添加父、子文档到 ES 中

添加父文档到 ES 中

通过 MyParent 的关系名称(”parent”)标记文档,以下三种方式都是等效的:

var parentDocument = new MyParent
{
    Id = 1,
    ParentProperty = "a parent prop",
    MyJoinField = JoinField.Root<MyParent>() // 指明是 MyParent 类的跟文档(会自动对应到"parent")
};

parentDocument = new MyParent
{
    Id = 1,
    ParentProperty = "a parent prop",
    MyJoinField = typeof(MyParent) // 指明是 MyParent 类的跟文档(会自动对应到"parent")
};

parentDocument = new MyParent
{
    Id = 1,
    ParentProperty = "a parent prop",
    MyJoinField = "parent"   //可以直接使用在 ConnectionSettings 配置的字段。
};

/* 以下官方示例有问题,虽然作者说包含了 JointField 属性的类不需要再显式指定 routing,但实际测试下来,并非如此---这是一个bug,从 issues 上看已经延续了3年之久,至今未修复。

至于示例文档为什么能跑通且不报错,是因为示例文档并非真实请求---使用了 InMemoryConnection。

// 因为开启了 routing,所以在请求的时候,请求 URL 上需要指定 routing,而以下索引方式并不会添加 ?routing
var indexParent = client.IndexDocument(parentDocument);

*/

//使用 Index 方法来进行索引文档
var indexParent = _client.Index(parentDocument, desc =>
{
    desc.Routing(parentDocument.Id); //显式指定 Routing 的值
    return desc;
});

最终生成的请求命令 为:

PUT /index/_doc/1?routing=1
{
    "parentProperty": "a parent prop",
    "id": 1,
    "parentChildRelation": "parent"
}

添加子文档到 ES 中

在添加子文档的时候必须指定它的父文档id:

  • 使用 route 参数保证父子文档被分配到相同的分片上。

依葫芦画瓢,将子文档连接到它的父文档上。这里直接使用上面的 parentDocument 对象 来创建连接:

/* 同样这里不能使用官方通过 IndexDocument 来索引文档的例子
var indexChild = client.IndexDocument(new MyChild
{
    MyJoinField = JoinField.Link<MyChild, MyParent>(parentDocument)
});
*/

//使用 Index 来代替 IndexDocument 以可以显式指定 routing
var child = new MyChild
{
    Id = 10002,
    ChildProperty = "a child prop",
    ParentChildRelation = JoinField.Link<MyChild, MyParent>(parentDocument)
    //使用该重载也是可以的 ParentChildRelation = JoinField.Link<MyChild>(1)
};
var indexChild = _client.Index(child, desc =>
{
    desc.Routing(parentDocument.Id);  //显式指定 routing,同 parentid
    return desc;
});

生成的请求命令为:

PUT /index/_doc/10002?routing=1
{
    "childProperty": "a child prop",
    "id": 10002,
    "parentChildRelation": {
        "name": "mychild",
        "parent": "1"
    }
}

最终在 index 索引中,生成的父子文档内容如下:

{
    "hits" : [
      {
        "_index" : "index",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : 1.0,
        "_routing" : "1",
        "_source" : {
          "parentProperty" : "a parent prop",
          "id" : 1,
          "parentChildRelation" : "parent"
        }
      },
      {
        "_index" : "index",
        "_type" : "_doc",
        "_id" : "10002",
        "_score" : 1.0,
        "_routing" : "1",
        "_source" : {
          "childProperty" : "a child prop",
          "id" : 10002,
          "parentChildRelation" : {
            "name" : "mychild",
            "parent" : "1"
          }
        }
      }
    ]
}

ElasticClient.Inferrer

  • 在索引的时候,需要指定 routing
  • 在过去,你需要在请求中使用 parent= 的方式(在 ES 5.x),或者 routing= 的方式(在 ES 6.x)来路由父子文档,在 Nest 7 中, Nest 提供了一个帮助类来推断正确的路由值,它会根据 JoinField 属性来推断父类。

var infer = _client.Infer;
var parentInfo = new MyParent
{
    Id = 2,
    ParentProperty = "a parent prop 2",
    ParentChildRelation = JoinField.Root<MyParent>()
};
var parentRoute = infer.Routing(parentInfo); //结果为父文档id:2

var child = new MyChild
{
    Id = 20002,
    ChildProperty = "a child prop of P2",
    ParentChildRelation = JoinField.Link<MyChild, MyParent>(parentInfo)
};
var childRoute = infer.Routing(child);   //结果为 父文档id:2

通过 Routing 方法来改写上面索引父子文档的例子:

//索引父文档
var parentInfo = new MyParent
{
    Id = 2,
    ParentProperty = "a parent prop 2",
    ParentChildRelation = JoinField.Root<MyParent>()
};
var parentResponse = _client.Index(parentInfo, i => i.Routing(Routing.From(parentInfo)));

//索引子文档
var child = new MyChild
{
    Id = 20002,
    ChildProperty = "a child prop of P2",
    ParentChildRelation = JoinField.Link<MyChild, MyParent>(parentInfo)
};
var childResponse = _client.Index(child, i => i.Routing(Routing.From(child)));

最终添加的文档内容为:

{
    "hits" : [
          {
            "_index" : "index",
            "_type" : "_doc",
            "_id" : "2",
            "_score" : 1.0,
            "_routing" : "2",
            "_source" : {
              "id" : 2,
              "parentProperty" : "a parent prop 2",
              "parentChildRelation" : "parent"
            }
          },
          {
            "_index" : "index",
            "_type" : "_doc",
            "_id" : "20002",
            "_score" : 1.0,
            "_routing" : "2",
            "_source" : {
              "id" : 20002,
              "childProperty" : "a child prop of P2",
              "parentChildRelation" : {
                "name" : "mychild",
                "parent" : "2"
              }
            }
          }
    ]
}