Elasticsearch.Nest 教程系列 9-4 转换:Field inference | 字段推断

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

  • 官方文档见此: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


不少 Elasticsearch API 接口在使用的时候会希望知道字段在原始文档中的路径(以字符串的形式),NEST 提供了 Field 类来允许你获得这些字段路径字符串。

构造函数

通过使用 Field 的构造函数:

1
2
3
4
5
6
7
8
var fieldString = new Field("name");

//使用重载方法
var fieldProperty = new Field(typeof(Project).GetProperty(nameof(Project.Name)));

//使用 lambda 表达式的方式
Expression<Func<Project, object>> expression = p => p.Name;
var fieldExpression = new Field(expression);

你也可以在实例化的时候设定 boost 值。

eg:当你使用如下构造函数时:

1
public Field(string name, double? boost = null, string format = null)
  • 你可以设定一个提升值(boost),之后 Nest 会从设定的 Field 名称中获得对应的 boost 值。

1
2
3
4
5
var fieldString = "name^2.1"; //boost为 2.1
var field = new Field(fieldString);

//等效于
var field = new Field("name", 2.1);

隐式转换

跟构造函数一样,你也可以直接将 string,PropertyInfo,lambda 表达式隐式转换为 Field 类,如下:

1
2
3
4
5
6
Field fieldString = "name";

Field fieldProperty = typeof(Project).GetProperty(nameof(Project.Name));

Expression<Func<Project, object>> expression = p => p.Name;
Field fieldExpression = expression;

使用 Nest.Infer 方法

通过 Nest.Infer 可以简化从表达式创建 Field 实例:

1
2
3
4
5
//使用 Nest.Infer 
var field = Nest.Infer.Field<Project>(p => p.Name);

//指定 boost
var field = Nest.Infer.Field<Project>(p => p.Name, 2.1);

Field 名称的自动大小写转换

默认情况下,NEST 为了跟 JavaScript 和 JSON 统一,对 Field 使用 camelCase。

通过使用 ConnectionSettings 的 DefaultFieldNameInferrer() 方法,你可以修改默认行为,如下:

1
2
var settings = new ConnectionSettings(new Uri(_esSettings.ServerUri))
.DefaultFieldNameInferrer(dfni=>dfni.ToUpper())
  • 将命名推断变更为了全部大写。

但是,当使用下面的构造函数的时候,总是逐字匹配的(不会转换,传入什么样就是什么样):

1
public Field(string name, double? boost = null, string format = null)

如果您希望NEST完全不更改字段名称的大小写,只需将Func<string,string>传递给 DefaultFieldNameInferrer 即可,该函数仅返回输入字符串:

1
2
var settings = new ConnectionSettings(new Uri(_esSettings.ServerUri))
.DefaultFieldNameInferrer(p => p)

使用表达式

需要注意的是:表达式只支持成员变量。

支持多级嵌套属性:

1
Expect("leadDeveloper.firstName").WhenSerializing(Nest.Infer.Field<Project>(p => p.LeadDeveloper.FirstName));

处理集合索引器时,将忽略索引器访问权限,你可以遍历集合的属性:

1
Expect("curatedTags").WhenSerializing(Nest.Infer.Field<Project>(p => p.CuratedTags[0]));

支持 Linq:

1
2
3
Expect("curatedTags").WhenSerializing(Nest.Infer.Field<Project>(p => p.CuratedTags.First()));
Expect("curatedTags.added").WhenSerializing(Nest.Infer.Field<Project>(p => p.CuratedTags[0].Added));
Expect("curatedTags.name").WhenSerializing(Nest.Infer.Field<Project>(p => p.CuratedTags.First().Name));

字典:

  • 使用字典的时候,会自动级联转换如下:

1
2
3
4
5
6
Expect("metadata.hardcoded").WhenSerializing(Nest.Infer.Field<Project>(p => p.Metadata["hardcoded"]));
Expect("metadata.hardcoded.created").WhenSerializing(Nest.Infer.Field<Project>(p => p.Metadata["hardcoded"].Created));
//对于 key,也会自动激烈使用
var variable = "var";
Expect("metadata.var").WhenSerializing(Nest.Infer.Field<Project>(p => p.Metadata[variable]));
Expect("metadata.var.created").WhenSerializing(Nest.Infer.Field<Project>(p => p.Metadata[variable].Created));

如果使用的是 Elasticearch 的多字段,这些“虚拟”子字段并不总是映射回你的 POCO 对象。通过在表达式上调用 .Suffix(),可以描述应映射的子字段及其映射方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Expect("leadDeveloper.firstName.raw").WhenSerializing(
Nest.Infer.Field<Project>(p => p.LeadDeveloper.FirstName.Suffix("raw")));

Expect("curatedTags.raw").WhenSerializing(
Nest.Infer.Field<Project>(p => p.CuratedTags[0].Suffix("raw")));

Expect("curatedTags.raw").WhenSerializing(
Nest.Infer.Field<Project>(p => p.CuratedTags.First().Suffix("raw")));

Expect("curatedTags.added.raw").WhenSerializing(
Nest.Infer.Field<Project>(p => p.CuratedTags[0].Added.Suffix("raw")));

Expect("metadata.hardcoded.raw").WhenSerializing(
Nest.Infer.Field<Project>(p => p.Metadata["hardcoded"].Suffix("raw")));

Expect("metadata.hardcoded.created.raw").WhenSerializing(
Nest.Infer.Field<Project>(p => p.Metadata["hardcoded"].Created.Suffix("raw")));

你也可以直接链式使用 Suffix 方法

1
2
Expect("curatedTags.name.raw.evendeeper").WhenSerializing(
Nest.Infer.Field<Project>(p => p.CuratedTags.First().Name.Suffix("raw").Suffix("evendeeper")));

传递给 Suffix 方法的值也将会被正确估算:

1
2
3
4
5
6
7
var variable = "var";
var suffix = "unanalyzed";
Expect("metadata.var.unanalyzed").WhenSerializing(
Nest.Infer.Field<Project>(p => p.Metadata[variable].Suffix(suffix)));

Expect("metadata.var.created.unanalyzed").WhenSerializing(
Nest.Infer.Field<Project>(p => p.Metadata[variable].Created.Suffix(suffix)));

也可以使用 .AppendSuffix() 方法将后缀“追加”到表达式中。在要将相同的后缀应用于字段列表的情况下,这很有用:

1
2
3
4
5
6
7
8
var expressions = new List<Expression<Func<Project, object>>>
{
p => p.Name,
p => p.Description,
p => p.CuratedTags.First().Name,
p => p.LeadDeveloper.FirstName,
p => p.Metadata["hardcoded"]
};

在每个后面增加“raw后缀”:

1
2
3
4
5
6
7
8
var fieldExpressions =
expressions.Select<Expression<Func<Project, object>>, Field>(e => e.AppendSuffix("raw")).ToList();

Expect("name.raw").WhenSerializing(fieldExpressions[0]);
Expect("description.raw").WhenSerializing(fieldExpressions[1]);
Expect("curatedTags.name.raw").WhenSerializing(fieldExpressions[2]);
Expect("leadDeveloper.firstName.raw").WhenSerializing(fieldExpressions[3]);
Expect("metadata.hardcoded.raw").WhenSerializing(fieldExpressions[4]);

你也可以链式调用 .AppendSuffix()

1
2
3
4
5
6
7
8
var multiSuffixFieldExpressions =
expressions.Select<Expression<Func<Project, object>>, Field>(e => e.AppendSuffix("raw").AppendSuffix("evendeeper")).ToList();

Expect("name.raw.evendeeper").WhenSerializing(multiSuffixFieldExpressions[0]);
Expect("description.raw.evendeeper").WhenSerializing(multiSuffixFieldExpressions[1]);
Expect("curatedTags.name.raw.evendeeper").WhenSerializing(multiSuffixFieldExpressions[2]);
Expect("leadDeveloper.firstName.raw.evendeeper").WhenSerializing(multiSuffixFieldExpressions[3]);
Expect("metadata.hardcoded.raw.evendeeper").WhenSerializing(multiSuffixFieldExpressions[4]);

使用特性

使用 NEST 的属性特性可以让你指定一个新的名称来代替属性名。

使用 Text 特性:

1
2
3
4
5
6
7
public class BuiltIn
{
[Text(Name = "naam")]
public string Name { get; set; }
}

Expect("naam").WhenSerializing(Nest.Infer.Field<BuiltIn>(p => p.Name));

使用 DataMember 特性:

  • 如果一个属性使用了 DataMember 特性,那么你可以直接在该特性上进行指定。

1
2
3
4
5
6
7
public class DataMember
{
[DataMember(Name = "nameFromDataMember")]
public string Name { get; set; }
}

Expect("nameFromDataMember").WhenSerializing(Nest.Infer.Field<DataMember>(p => p.Name));

使用各序列化器的特性:

  • NEST 支持使用特定的 JSON 库提供的序列化特性:如将 JsonNetSerializer 作为默认序列化,使用 JsonPropertyAttribute 来设定字段名

1
2
3
4
5
6
7
public class SerializerSpecific
{
[PropertyName("nameInJson"), JsonProperty("nameInJson")]
public string Name { get; set; }
}

Expect("nameInJson").WhenSerializing(Nest.Infer.Field<SerializerSpecific>(p => p.Name));

如果在一个属性上同时指定了 Nest 提供的属性特性和序列化器提供的属性特性,Nest 特性优先级较高,如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class Both
{
[Text(Name = "naam")]
[PropertyName("nameInJson"), DataMember(Name = "nameInJson")]
public string Name { get; set; }
}

Expect("naam").WhenSerializing(Nest.Infer.Field<Both>(p => p.Name));
Expect(new
{
naam = "Martijn Laarman"
}).WhenSerializing(new Both { Name = "Martijn Laarman" });

字段推断缓存

每个 ConnectionSettings 实例都会缓存字段名称的解析:

  • 即不同的 ConnectionSettings 可以设定不同的推断规则。

假设有如下演示类:

1
2
3
4
5
6
7
8
class A { public C C { get; set; } }

class B { public C C { get; set; } }

class C
{
public string Name { get; set; }
}

对于如下表达式代码:

1
2
var fieldNameOnA = _client.Infer.Field(Nest.Infer.Field<A>(p => p.C.Name));
var fieldNameOnB = _client.Infer.Field(Nest.Infer.Field<B>(p => p.C.Name));

有:

现在创建一个新的 ConnectionSettings,重新映射 A.C 为 “d”:

1
2
3
4
5
6
7
8
9
10
11
12
var newConnectionSettings = new TestConnectionSettings()
.DefaultMappingFor<A>(m => m
.PropertyName(p => p.C, "d")
);

var newClient = new ElasticClient(newConnectionSettings);

fieldNameOnA = newClient.Infer.Field(Nest.Infer.Field<A>(p => p.C.Name));
fieldNameOnB = newClient.Infer.Field(Nest.Infer.Field<B>(p => p.C.Name));

fieldNameOnA.Should().Be("d.name"); // 使用了默认映射规则
fieldNameOnB.Should().Be("c.name");

推断字段名称的优先顺序

如上述,设定字段名称的方式如下:

  • 在 ConnectionSettings上使用 .PropertyName 命名属性。

  • 使用 Nest 的 PropertyNameAttribute。

  • 使用序列化器提供的特性。

  • 使用 DataMemberAttribute。

  • 在 ConnectionSettings 上使用 DefaultFieldNameInferrer,默认情况下将使用骆驼命名属性。

假设有以下测试用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private class Precedence
{
[Text(Name = "renamedIgnoresNest")] [PropertyName("renamedIgnoresJsonProperty"),JsonProperty("renamedIgnoresJsonProperty")]
public string RenamedOnConnectionSettings { get; set; } //1

[Text(Name = "nestAtt")]
[PropertyName("nestProp"),JsonProperty("jsonProp")]
public string NestAttribute { get; set; } //2

[PropertyName("nestProp"),JsonProperty("jsonProp")]
public string NestProperty { get; set; } //3

[DataMember(Name ="jsonProp")]
public string JsonProperty { get; set; } //4

[DataMember(Name = "data")]
public string DataMember { get; set; } //5

}

优先级如下:

  1. 即使此属性应用了各种属性,如果在 ConnectionSettings 上进行了设定,则 ConnectionSettings 上设置的优先级最高。

  2. 同时具有TextAttribute,PropertyNameAttribute 和 JsonPropertyAttribute:TextAttribute 优先级高。

  3. 同时具有 PropertyNameAttribute 和 JsonPropertyAttribute:PropertyNameAttribute 优先级高。

  4. JsonPropertyAttribute 优先级高。

  5. DataMemberAttribute 优先级高。

自定义一个 IPropertyMappingProvider:将所有 AskSerializer 命名为 ask,并在 ConnectionSettings 中进行注册

1
2
3
4
5
6
7
8
9
private class CustomPropertyMappingProvider : PropertyMappingProvider
{
public override IPropertyMapping CreatePropertyMapping(MemberInfo memberInfo)
{
return memberInfo.Name == nameof(Precedence.AskSerializer)
? new PropertyMapping { Name = "ask" }
: base.CreatePropertyMapping(memberInfo);
}
}

在 ConnectionSettings 中注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var usingSettings = WithConnectionSettings(s => s

.DefaultMappingFor<Precedence>(m => m
.PropertyName(p => p.RenamedOnConnectionSettings, "renamed")
)
.DefaultFieldNameInferrer(p => p.ToUpperInvariant()) //如果没有其他规则适用或没有为给定字段指定字段,则为字段的默认推断
).WithPropertyMappingProvider(new CustomPropertyMappingProvider()); //使用自定义映射器

usingSettings.Expect("renamed").ForField(Nest.Infer.Field<Precedence>(p => p.RenamedOnConnectionSettings));
usingSettings.Expect("nestAtt").ForField(Nest.Infer.Field<Precedence>(p => p.NestAttribute));
usingSettings.Expect("nestProp").ForField(Nest.Infer.Field<Precedence>(p => p.NestProperty));
usingSettings.Expect("jsonProp").ForField(Nest.Infer.Field<Precedence>(p => p.JsonProperty));
usingSettings.Expect("ask").ForField(Nest.Infer.Field<Precedence>(p => p.AskSerializer));
usingSettings.Expect("data").ForField(Nest.Infer.Field<Precedence>(p => p.DataMember));
usingSettings.Expect("DEFAULTFIELDNAMEINFERRER").ForField(Nest.Infer.Field<Precedence>(p => p.DefaultFieldNameInferrer));

索引文档时也适用相同的命名规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
usingSettings.Expect(new []
{
"ask",
"DEFAULTFIELDNAMEINFERRER",
"jsonProp",
"nestProp",
"nestAtt",
"renamed",
"data"
}).AsPropertiesOf(new Precedence
{
RenamedOnConnectionSettings = "renamed on connection settings",
NestAttribute = "using a nest attribute",
NestProperty = "using a nest property",
JsonProperty = "the default serializer resolves json property attributes",
AskSerializer = "serializer fiddled with this one",
DefaultFieldNameInferrer = "shouting much?",
DataMember = "using a DataMember attribute"
});

覆盖继承的字段推断

从基本类型继承的属性可以忽略,并在 ConnectionSetting 上使用 DefaultMappingFor

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
public class Parent
{
public int Id { get; set; }
public string Description { get; set; }
public string IgnoreMe { get; set; }
}

public class Child : Parent { }

var usingSettings = WithConnectionSettings(s => s
.DefaultMappingFor<Child>(m => m
.PropertyName(p => p.Description, "desc") //重命名 Description 为 desc
.Ignore(p => p.IgnoreMe) //在 Child 类中忽略基类中的 IgnoreMe 属性
)
);
usingSettings.Expect(new []
{
"id",
"desc",
}).AsPropertiesOf(new Child
{
Id = 1,
Description = "this property will be renamed for Child",
IgnoreMe = "this property will be ignored (won't be serialized) for Child",
});

public class SourceModel
{
[PropertyName("gexo")]
public GeoModel Geo { get; set; }
}

public class GeoModel
{
[DataMember(Name = "country_iso_code")]
public string CountryIsoCode { get; set; }
}

var usingSettings = WithConnectionSettings(s => s)
.WithSourceSerializer(JsonNetSerializer.Default);

usingSettings.Expect("gexo").ForField(Field<SourceModel>(p=>p.Geo));
usingSettings.Expect("country_iso_code").ForField(Field<GeoModel>(p=>p.CountryIsoCode));
usingSettings.Expect(new []
{
"country_iso_code",
}).AsPropertiesOf(new GeoModel { CountryIsoCode = "nl" });
usingSettings.Expect(new []
{
"gexo",
}).AsPropertiesOf(new SourceModel { Geo = new GeoModel { CountryIsoCode = "nl" } });

usingSettings.Expect("gexo.country_iso_code").ForField(Field<SourceModel>(p=>p.Geo.CountryIsoCode));