Elasticsearch.Nest 教程系列 4-3 映射:Fluent mapping | 通过 lambda 表达式流畅映射

  • 本系列博文是“伪”官方文档翻译(更加本土化),并非完全将官方文档进行翻译,而是在查阅、测试原始文档并转换为自己真知灼见后的“准”翻译。有不同见解 / 说明不周的地方,还请海涵、不吝拍砖 :)

  • 官方文档见此:https://www.elastic.co/guide/en/elasticsearch/client/net-api/current/introduction.html

  • 本系列对应的版本环境:ElasticSearch@7.3.1,NEST@7.3.1,IDE 和开发平台默认为 VS2019,.NET CORE 2.1


Fluent mapping 允许你可以最大程度地控制 POCOs 到 ES 字段的映射。你可以认为这种方式类似于 Automapper 中通过 CreateMap<T1, T2>().ForMember 来显式指定属性之间的映射。Fluent mapping 同样需要你显式指定每一个 POCO 属性跟 ES 字段的映射关系。

下面使用 Company 和 Employee 这 2 个 类来进行演示,两者之间的关系如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Company
{
public string Name { get; set; }
public List<Employee> Employees { get; set; }
}

public class Employee
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Salary { get; set; }
public DateTime Birthday { get; set; }
public bool IsManager { get; set; }
public List<Employee> Employees { get; set; }
public TimeSpan Hours { get; set; }
}

手动映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var createIndexResponse = _client.Indices.Create("myindex", c => c
.Map<Company>(m => m
.Properties(ps => ps
.Text(s => s //映射为 text 数据类型
.Name(n => n.Name)
)
.Object<Employee>(o => o //属性映射为 Object
.Name(n => n.Employees)
.Properties(eps => eps
.Text(s => s
.Name(e => e.FirstName)
)
.Text(s => s
.Name(e => e.LastName)
)
.Number(n => n
.Name(e => e.Salary)
.Type(NumberType.Integer)
)
)
)
)
)
);
  • Company.Name 属性被映射为 text 数据类型

  • Company.Employees 属性被映射为 object 类型,另外该属性对应的仅对 Employee 类中的 FirstName,LastName 以及 Salary 这 3 个属性进行了映射。

最终 json 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"myindex" : {
"mappings" : {
"properties" : {
"employees" : {
"properties" : {
"firstName" : {
"type" : "text"
},
"lastName" : {
"type" : "text"
},
"salary" : {
"type" : "integer"
}
}
},
"name" : {
"type" : "text"
}
}
}
}
}

手动映射的方式自定义程度强大,但如何 POCO 类很复杂的话,一个一个手动指定就让人崩溃了。

在日常开发中,我们一般只是需要对部分属性进行显式指定,跟 automapper 一样,NEST 同样提供了部分显式映射的功能:使用 .AutoMap() 将 CLR 属性类型先自动映射一波,之后通过显式指定(覆盖的方式)进行自定义调整。

  • 默认情况下使用 .AutoMap()将 List属性作为 object 数据类型,这里我们通过 Nested 方法进行显式覆盖指定,示例如下:

1
2
3
4
5
6
7
8
9
10
11
var createIndexResponse = _client.Indices.Create("myindex", c => c
.Map<Company>(m => m
.AutoMap() //1
.Properties(ps => ps
.Nested<Employee>(n => n //2
.Name(nn => nn.Employees)
)
)
//.AutoMap() 在这里执行跟放 "1" 处执行,等效。
)
);
  • .AutoMap() 是一个幂函数,调用的顺序不影响最终的结果,即上面的代码,如果把 AutoMap 方法放到 Properties 方法之后,效果是一样的。

映射结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"myindex" : {
"mappings" : {
"properties" : {
"employees" : {
"type" : "nested"
},
"name" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
}
}

Fluent mapping 的优先级别 Auto mapping 的优先级要高,同时比 Attribute Mapping 的优先级也要高,基于此,你也可以把 Fluent mapping 和 Attribute mapping 结合使用,如下:

假设有如下 POCO 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
[ElasticsearchType(RelationName = "company")]
public class CompanyWithAttributes
{
[Keyword(NullValue = "null", Similarity = "BM25")]
public string Name { get; set; }

[Text(Name = "office_hours")]
public TimeSpan? HeadOfficeHours { get; set; }

[Object(Store = false)]
public List<EmployeeWithAttributes> Employees { get; set; }
}

[ElasticsearchType(RelationName = "employee")]
public class EmployeeWithAttributes
{
[Text(Name = "first_name")]
public string FirstName { get; set; }

[Text(Name = "last_name")]
public string LastName { get; set; }

[Number(DocValues = false, IgnoreMalformed = true, Coerce = true)]
public int Salary { get; set; }

[Date(Format = "MMddyyyy")]
public DateTime Birthday { get; set; }

[Boolean(NullValue = false, Store = true)]
public bool IsManager { get; set; }

[Nested]
[PropertyName("empl")]
public List<Employee> Employees { get; set; }
}

结合 Auto mapping 和 Attributes mapping

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var createIndexResponse = _client.Indices.Create("myindex", c => c
.Map<CompanyWithAttributes>(m => m
.AutoMap() // 1.自动映射 company
.Properties(ps => ps // 2.覆盖利用特性指定的值
.Nested<EmployeeWithAttributes>(n => n
.Name(nn => nn.Employees)
.AutoMap() // 3.自动映射嵌套的 employee
.Properties(pps => pps // 4.覆盖利用特性指定的值
.Text(s => s
.Name(e => e.FirstName)
.Fields(fs => fs
.Keyword(ss => ss
.Name("firstNameRaw")
)
.TokenCount(t => t
.Name("length")
.Analyzer("standard")
)
)
)
.Number(nu => nu
.Name(e => e.Salary)
.Type(NumberType.Double)
.IgnoreMalformed(false)
)
.Date(d => d
.Name(e => e.Birthday)
.Format("MM-dd-yy")
)
)
)
)
)
);

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
{
"myindex" : {
"mappings" : {
"properties" : {
"employees" : {
"type" : "nested",
"properties" : {
"birthday" : {
"type" : "date",
"format" : "MM-dd-yy"
},
"empl" : {
"type" : "nested",
"properties" : {
"birthday" : {
"type" : "date"
},
"employees" : {
"type" : "object"
},
"firstName" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"hours" : {
"type" : "long"
},
"isManager" : {
"type" : "boolean"
},
"lastName" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"salary" : {
"type" : "integer"
}
}
},
"first_name" : {
"type" : "text",
"fields" : {
"firstNameRaw" : {
"type" : "keyword"
},
"length" : {
"type" : "token_count",
"analyzer" : "standard"
}
}
},
"isManager" : {
"type" : "boolean",
"store" : true,
"null_value" : false
},
"last_name" : {
"type" : "text"
},
"salary" : {
"type" : "double",
"ignore_malformed" : false
}
}
},
"name" : {
"type" : "keyword",
"similarity" : "BM25",
"null_value" : "null"
},
"office_hours" : {
"type" : "text"
}
}
}
}
}

  • 通过在 .Nested 内部调用 .AutoMap() 方法,可以自动映射 Employee 的嵌套属性,并再次通过手动映射来覆盖自动推断的映射。