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


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

构造函数

通过使用 Field 的构造函数:

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:当你使用如下构造函数时:

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

var fieldString = "name^2.1"; //boost为 2.1
var field = new Field(fieldString);

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

隐式转换

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

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 实例:

//使用 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() 方法,你可以修改默认行为,如下:

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

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

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

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

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

使用表达式

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

支持多级嵌套属性:

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

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

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

支持 Linq:

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));

字典:

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

    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(),可以描述应映射的子字段及其映射方式:

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 方法

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

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

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() 方法将后缀“追加”到表达式中。在要将相同的后缀应用于字段列表的情况下,这很有用:

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后缀”:

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()

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 特性:

public class BuiltIn
{
    [Text(Name = "naam")]
    public string Name { get; set; }
}

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

使用 DataMember 特性:

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


    ` csharp
    public class DataMember
    {
    [DataMember(Name = “nameFromDataMember”)]
    public string Name { get; set; }
    }

Expect(“nameFromDataMember”).WhenSerializing(Nest.Infer.Field(p => p.Name));


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

<p class="code-caption" data-lang="csharp" data-line_number="backend" data-trim_indent="backend" data-label_position="outer" data-labels_left="Code" data-labels_right=":" data-labels_copy="Copy Code"><span class="code-caption-label"></span></p>
``` csharp 
public class SerializerSpecific
{
    [PropertyName("nameInJson"), JsonProperty("nameInJson")]
    public string Name { get; set; }
}

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

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

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 可以设定不同的推断规则。

假设有如下演示类:

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

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

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

对于如下表达式代码:

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));

有:

  • fieldNameOnA == “c.name”
  • fieldNameOnB == “c.name”

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

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,默认情况下将使用骆驼命名属性。

假设有以下测试用例:

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 中进行注册

private class CustomPropertyMappingProvider : PropertyMappingProvider
{
    public override IPropertyMapping CreatePropertyMapping(MemberInfo memberInfo)
    {
        return memberInfo.Name == nameof(Precedence.AskSerializer)
            ? new PropertyMapping { Name = "ask" }
            : base.CreatePropertyMapping(memberInfo);
    }
}

在 ConnectionSettings 中注册

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));

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

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

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));