使用Elasticsearch和C#理解和实现CRUD APP的初学者教程——第2部分

目录

介绍

创建一个演示应用

通过NEST与Elastic连接

第1组(索引、更新和删除)

第2组(标准查询)

第4组(范围查询)

第5组(聚合)

求和

平均

计数

最小/最大

结论


介绍

第一部分中,我们学习了如何设置、配置和运行一堆Elastic语句。现在是时候将其转换为C#完全可操作的CRUD应用程序了。让我们完成它。

创建一个演示应用

第一步,创建一个新的Windows窗体解决方案。可以从这里下载,它看起来像这样:

红色突出显示的参考文献是最重要的,您可以通过NuGet获得它们。顾名思义,NESTElasticsearch DLLElasticsearch.NET抽象。

在我撰写本文时,官方文档显然已经过时了。无论哪种方式,您都可以通过http://nest.azurewebsites.net/进行访问。

所有蓝色区域是我决定组织该项目的方式。相当标准:BLL代表业务规则,DAL代表数据访问层,DTO包含实体,而View拥有我们的Windows窗体。

通过NESTElastic连接

按照我展示的抽象,我们的数据访问层非常简单:

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";
}

如您所见,所有名称都引用了我们在本文第一部分中创建的存储。

1组(索引、更新和删除)

我们将把以前学过的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()方法。

2组(标准查询)

正如我之前提到的,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<IHit<DTO.Customer=>, DTO.Customer=> 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查询组合过滤器。此处可用的子句为mustmust notshould尽管乍一看可能看起来很奇怪,但与其他数据库大体相同:

  • 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范围,在其上应用GreatLower运算符。第二个将包含比用户为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专业人员一起发生。

随着时间的流逝,您可能会意识到,它确实为您设计了许多解决方案。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值