目录
介绍
创建一个演示应用
通过NEST与Elastic连接
第1组(索引、更新和删除)
第2组(标准查询)
第4组(范围查询)
第5组(聚合)
求和
平均
计数
最小/最大
结论
介绍在第一部分中,我们学习了如何设置、配置和运行一堆Elastic语句。现在是时候将其转换为C#完全可操作的CRUD应用程序了。让我们完成它。
创建一个演示应用第一步,创建一个新的Windows窗体解决方案。可以从这里下载,它看起来像这样:
用红色突出显示的参考文献是最重要的,您可以通过NuGet获得它们。顾名思义,NEST和Elasticsearch DLL是Elasticsearch的.NET抽象。
在我撰写本文时,官方文档显然已经过时了。无论哪种方式,您都可以通过http://nest.azurewebsites.net/进行访问。
所有蓝色区域是我决定组织该项目的方式。相当标准:BLL代表业务规则,DAL代表数据访问层,DTO包含实体,而View拥有我们的Windows窗体。
通过NEST与Elastic连接按照我展示的抽象,我们的数据访问层非常简单:
namespace Elastic_CRUD.DAL
{
/// Elastic client
public class EsClient
{
/// URI
private const string ES_URI = "http://localhost:9200";
/// Elastic settings
private ConnectionSettings _settings;
/// Current instantiated client
public ElasticClient Current { get; set; }
/// Constructor
public EsClient()
{
var node = new Uri(ES_URI);
_settings = new ConnectionSettings(node);
_settings.SetDefaultIndex(DTO.Constants.DEFAULT_INDEX);
_settings.MapDefaultTypeNames(m => m.Add(typeof(DTO.Customer),
DTO.Constants.DEFAULT_INDEX_TYPE));
Current = new ElasticClient(_settings);
Current.Map(m => m.MapFromAttributes());
}
}
}
名为“ Current”的属性是Elastic REST客户端的抽象。所有CRUD命令都将通过它完成。这里的另一个重要部分是“Settings”,我将所有配置键分组为一个简单的类:
/// System constant values
public static class Constants
{
/// Elastic index name
public const string DEFAULT_INDEX = "crud_sample";
/// Elastic type of a given index
public const string DEFAULT_INDEX_TYPE = "Customer_Info";
/// Basic date format
public const string BASIC_DATE = "yyyyMMdd";
}
如您所见,所有名称都引用了我们在本文第一部分中创建的存储。
我们将把以前学过的Elastic语句复制到此WinForm应用程序中。为了对其进行组织,最后为每组功能提供了一个选项卡,因此其中有五个:
在第一个选项卡,你可以看到,将负责添加、更新和删除客户。鉴于此,客户实体是非常重要的部分,必须使用NEST装饰对其进行正确映射,如下所示:
/// Customer entity
[ElasticType(Name = "Customer_Info")]
public class Customer
{
/// _id field
[ElasticProperty(Name="_id", NumericType = NumberType.Long)]
public int Id { get; set; }
/// name field
[ElasticProperty(Name = "name", Index = FieldIndexOption.NotAnalyzed)]
public string Name { get; set; }
/// age field
[ElasticProperty(Name = "age", NumericType = NumberType.Integer)]
public int Age { get; set; }
/// birthday field
[ElasticProperty(Name = "birthday", Type = FieldType.Date, DateFormat = "basic_date")]
public string Birthday { get; set; }
/// haschildren field
[ElasticProperty(Name = "hasChildren")]
public bool HasChildren { get; set; }
/// enrollmentFee field
[ElasticProperty(Name = "enrollmentFee", NumericType = NumberType.Double)]
public double EnrollmentFee { get; set; }
/// opnion field
[ElasticProperty(Name = "opinion", Index = FieldIndexOption.NotAnalyzed)]
public string Opinion { get; set; }
}
既然我们已经有了REST连接并且我们的客户实体已完全映射,那么该写一些逻辑了。添加或更新记录应使用几乎相同的逻辑。Elastic是足够聪明的,可以通过检查给定ID的存在来决定是新记录还是更新。
/// Inserting or Updating a doc
public bool Index(DTO.Customer customer)
{
var response = _EsClientDAL.Current.Index
(customer, c => c.Type(DTO.Constants.DEFAULT_INDEX_TYPE));
if (response.Created == false && response.ServerError != null)
throw new Exception(response.ServerError.Error);
else
return true;
}
API中负责该方法的方法称为“Index()”,因为将文档保存到Lucene存储中时,正确的术语是“索引”。
请注意,我们使用常量索引类型(“Customer_Info”)来告知NEST客户将在何处添加/更新。粗略地说,这种索引类型是我们在Elastic世界中的表。
NEST用法中会出现的另一件事是lambda表示法,几乎所有NEST API的方法都可以通过它来工作。如今,使用lambda远远不是什么新闻,但它不像常规C#标记那样简单。
删除是最简单的方法:
/// Deleting a row
public bool Delete(string id)
{
return _EsClientDAL.Current
.Delete(new Nest.DeleteRequest(DTO.Constants.DEFAULT_INDEX,
DTO.Constants.DEFAULT_INDEX_TYPE,
id.Trim())).Found;
}
与“Index()”方法非常相似,但是这里只需要告知客户ID。并且,当然要调用“Delete()”方法。
正如我之前提到的,Elastic在查询方面确实非常足智多谋,因此这里不可能涵盖所有高级内容。但是,在完成以下示例之后,您将能够了解其基本知识,因此稍后开始编写自己的用户案例。
在第二个选项卡拥有三个查询:
1、按ID搜索:它基本上使用有效的ID,并且仅考虑以下因素:
/// Querying by ID
public List QueryById(string id)
{
QueryContainer queryById = new TermQuery() { Field = "_id", Value = id.Trim() };
var hits = _EsClientDAL.Current
.Search(s => s.Query(q => q.MatchAll() && queryById))
.Hits;
List typedList = hits.Select(hit => ConvertHitToCustumer(hit)).ToList();
return typedList;
}
/// Anonymous method to translate from a Hit to our customer DTO
private DTO.Customer ConvertHitToCustumer(IHit hit)
{
Func func = (x) =>
{
hit.Source.Id = Convert.ToInt32(hit.Id);
return hit.Source;
};
return func.Invoke(hit);
}
让我们慢慢来。
首先,有必要创建一个NEST QueryContainer对象,告知我们要用作搜索条件的字段。在这种情况下,是客户编号。
该查询对象将被Search()方法用作参数,以获取Hits(从Elastic返回的结果集)。
最后一步是通过ConvertHitToCustomer方法将Hits转换为我们已知的Customer实体。
我本可以用一种方法完成所有操作,但是我决定将其拆分。原因是为了证明有几种选择来组织代码,而不是将它们全部组合在一个难以阅读的Lambda语句中。
2、使用所有字段进行查询,并使用“AND”运算符将它们组合起来:
/// Querying by all fields with 'AND' operator
public List QueryByAllFieldsUsingAnd(DTO.Customer costumer)
{
IQueryContainer query = CreateSimpleQueryUsingAnd(costumer);
var hits = _EsClientDAL.Current
.Search(s => s.Query(query))
.Hits;
List typedList = hits.Select(hit => ConvertHitToCustumer(hit)).ToList();
return typedList;
}
/// Create a query using all fields with 'AND' operator
private IQueryContainer CreateSimpleQueryUsingAnd(DTO.Customer customer)
{
QueryContainer queryContainer = null;
queryContainer &= new TermQuery() { Field = "_id", Value = customer.Id };
queryContainer &= new TermQuery() { Field = "name", Value = customer.Name };
queryContainer &= new TermQuery() { Field = "age", Value = customer.Age };
queryContainer &= new TermQuery()
{ Field = "birthday", Value = customer.Birthday };
queryContainer &= new TermQuery()
{ Field = "hasChildren", Value= customer.HasChildren };
queryContainer &= new TermQuery()
{ Field = "enrollmentFee", Value=customer.EnrollmentFee };
return queryContainer;
}
和ID搜索背后的想法相同,但是现在我们的查询对象是由CreateSimpleQueryUsingAnd方法创建的。它接收一个客户实体,并将其转换为NEST QueryContainer对象。请注意,我们正在使用“&=”NEST自定义运算符(表示“AND”)连接所有字段。
3、它遵循前面的示例,但是将字段与OR “|=”运算符组合在一起。
/// Querying by all fields with 'OR' operator
public List QueryByAllFieldsUsingOr(DTO.Customer costumer)
{
IQueryContainer query = CreateSimpleQueryUsingOr(costumer);
var hits = _EsClientDAL.Current
.Search(s => s.Query(query))
.Hits;
List typedList = hits.Select(hit => ConvertHitToCustumer(hit)).ToList();
return typedList;
}
/// Create a query using all fields with 'AND' operator
private IQueryContainer CreateSimpleQueryUsingOr(DTO.Customer customer)
{
QueryContainer queryContainer = null;
queryContainer |= new TermQuery() { Field = "_id", Value = customer.Id };
queryContainer |= new TermQuery() { Field = "name", Value = customer.Name };
queryContainer |= new TermQuery() { Field = "age", Value = customer.Age };
queryContainer |= new TermQuery()
{ Field = "birthday", Value = customer.Birthday };
queryContainer |= new TermQuery()
{ Field = "hasChildren", Value = customer.HasChildren };
queryContainer |= new TermQuery()
{ Field = "enrollmentFee", Value = customer.EnrollmentFee };
return queryContainer;
}
第3组(合并查询)
第三个标签显示了如何使用bool查询组合过滤器。此处可用的子句为“must”,“must not”和“should”。尽管乍一看可能看起来很奇怪,但与其他数据库大体相同:
- must:子句(查询)必须出现在匹配的文档中。
- must_not:子句(查询)不得出现在匹配的文档中。
- should:子句(查询)应出现在匹配的文档中。在没有“must”子句的布尔查询中,一个或多个should子句必须与文档匹配。should可以使用minimum_should_match参数设置要匹配的最小子句数。
将其翻译成我们的C#应用程序,我们将获得:
/// Querying combining fields
public List QueryUsingCombinations(DTO.CombinedFilter filter)
{
//Build Elastic "Should" filtering object for "Ages":
FilterContainer[] agesFiltering = new FilterContainer[filter.Ages.Count];
for (int i = 0; i < filter.Ages.Count; i++)
{
FilterDescriptor clause = new FilterDescriptor();
agesFiltering[i] = clause.Term("age", int.Parse(filter.Ages[i]));
}
//Build Elastic "Must Not" filtering object for "Names":
FilterContainer[] nameFiltering = new FilterContainer[filter.Names.Count];
for (int i = 0; i < filter.Names.Count; i++)
{
FilterDescriptor clause = new FilterDescriptor();
nameFiltering[i] = clause.Term("name", filter.Names[i]);
}
//Run the combined query:
var hits = _EsClientDAL.Current.Search(s => s
.Query(q => q
.Filtered(fq => fq
.Query(qq => qq.MatchAll())
.Filter(ff => ff
.Bool(b => b
.Must(m1 => m1.Term
("hasChildren", filter.HasChildren))
.MustNot(nameFiltering)
.Should(agesFiltering)
)
)
)
)
).Hits;
//Translate the hits and return the list
List typedList = hits.Select(hit ==> ConvertHitToCustumer(hit)).ToList();
return typedList;
}
在这里,您可以看到第一个循环为给定的“年龄”创建了“should”过滤器集合,而下一个循环又为所提供的“名称”构建了“必须禁止子句”列表。
“must”子句将仅应用于“hasChildren”字段,因此此处无需收集。
将所有过滤器对象填满后,只需将所有参数作为参数传递给lambda Search()方法即可。
第4组(范围查询)在第四个选项卡中,我们将讨论范围查询(与SQL中的'between', 'greater than', 'less than' 等运算符非常相似)。
为了重现这一点,我们将结合两个范围查询,突出显示如下:
我们的BLL有一种方法可以组成此查询并运行它:
/// Querying using ranges
public List QueryUsingRanges(DTO.RangeFilter filter)
{
FilterContainer[] ranges = new FilterContainer[2];
//Build Elastic range filtering object for "Enrollment Fee":
FilterDescriptor clause1 = new FilterDescriptor();
ranges[0] = clause1.Range(r => r.OnField(f =>
f.EnrollmentFee).Greater(filter.EnrollmentFeeStart)
.Lower(filter.EnrollmentFeeEnd));
//Build Elastic range filtering object for "Birthday":
FilterDescriptor clause2 = new FilterDescriptor();
ranges[1] = clause2.Range(r => r.OnField(f => f.Birthday)
.Greater(filter.Birthday.ToString
(DTO.Constants.BASIC_DATE)));
//Run the combined query:
var hits = _EsClientDAL.Current
.Search(s => s
.Query(q => q
.Filtered(fq => fq
.Query(qq => qq.MatchAll())
.Filter(ff => ff
.Bool(b => b
.Must(ranges)
)
)
)
)
).Hits;
//Translate the hits and return the list
List typedList = hits.Select(hit => ConvertHitToCustumer(hit)).ToList();
return typedList;
}
详细介绍该方法,它将创建一个包含两个项目的FilterContainer对象:
第一个保留“EnrollmentFee”范围,在其上应用“Great”和“Lower”运算符。第二个将包含比用户为”Birthday”字段提供的值大的值。
请注意,自存储概念以来,我们需要坚持使用的日期格式(请参阅第一篇文章)。设置完毕后,只需将其作为参数发送到Search()即可。
第5组(聚合)最后,第五个标签显示了我认为最酷的功能,即聚合。
正如我在上一篇文章中所指出的那样,此功能对于量化数据特别有用,因此很有意义。
第一个combobox保留所有可用字段,第二个保留聚合选项。为了简单起见,我在这里显示最受欢迎的聚合:
求和private void ExecuteSumAggregation
(DTO.Aggregations filter, Dictionary list, string agg_nickname)
{
var response = _EsClientDAL.Current
.Search(s => s
.Aggregations(a => a
.Sum(agg_nickname, st => st
.Field(filter.Field)
)
)
);
list.Add(filter.Field + " Sum", response.Aggs.Sum(agg_nickname).Value.Value);
}
平均
private void ExecuteAvgAggregation
(DTO.Aggregations filter, Dictionary list, string agg_nickname)
{
var response = _EsClientDAL.Current
.Search(s => s
.Aggregations(a => a
.Average(agg_nickname, st => st
.Field(filter.Field)
)
)
);
list.Add(filter.Field + " Average", response.Aggs.Average(agg_nickname).Value.Value);
计数
private void ExecuteCountAggregation
(DTO.Aggregations filter, Dictionary list, string agg_nickname)
{
var response = _EsClientDAL.Current
.Search(s => s
.Aggregations(a => a
.Terms(agg_nickname, st => st
.Field(filter.Field)
.Size(int.MaxValue)
.ExecutionHint
(TermsAggregationExecutionHint.GlobalOrdinals)
)
)
);
foreach (var item in response.Aggs.Terms(agg_nickname).Items)
{
list.Add(item.Key, item.DocCount);
}
}
最小/最大
private void ExecuteMaxAggregation
(DTO.Aggregations filter, Dictionary list, string agg_nickname)
{
var response = _EsClientDAL.Current
.Search(s => s
.Aggregations(a => a
.Max(agg_nickname,
st => st //Replace ‘.Max’ for ‘.Min’ to get
//the min value
.Field(filter.Field)
)
)
);
list.Add(filter.Field + " Max", response.Aggs.Sum(agg_nickname).Value.Value);
}
结论
由于大多数开发人员习惯使用关系数据库,因此使用非关系存储可能会充满挑战,甚至很奇怪。至少,对我而言,这是事实。
我已经在多个项目中使用大多数已知的关系数据库,并且它的概念、标准确实扎根于我的脑海。
因此,与这种新兴的存储技术保持联系正在改变我的看法,我可能会认为,这种看法正在全球范围内与其他IT专业人员一起发生。
随着时间的流逝,您可能会意识到,它确实为您设计了许多解决方案。