Func<T,T>應用之Elasticsearch查詢語句構造器的開發

丶Pz發表於2016-12-21

前言

  之前專案中做Elasticsearch相關開發的時候,雖然藉助了第三方的元件PlainElastic.Net,但是由於當時不熟悉用法,而選擇了自己拼接查詢語句。例如:

            string queryGroup = "{\"query\": {\"match\": { \"roomid\": \"FRIEND_12686_10035\" }}}";
            //關鍵字查詢
            string queryKeyWord = "{ \"query\": {\"match_phrase\": {\"content\": {\"query\": \"" + keyword + "\",\"slop\": 0} } }}";
            //是否圖片 查詢
            string queryImg = "{ \"term\": {\"isimg\": true }}";
            //是否包含檔案查詢
            string queryFile = "{ \"term\": {\"isfile\": true }}";
            //大於小於某個時間段查詢
            string queryTimeRange = "{\"range\": {\"addtime\": { \"gt\": \"" + st + "\",\"lt\": \"" + et + "\" }} }";
            //大於某個時間
            string queryTimeRangeGt = "{\"range\": {\"addtime\": { \"gt\": \"" + st + "\"}} }";
            //小於某個時間
            string queryTimeRangeLt = "{\"range\": {\"addtime\": { \"lt\": \"" + et + "\" }} }";

  後來慢慢看了下該元件的原始碼,想自己簡單實現一下,看看到底是什麼原理。

分析

  先來一個簡單的小例子:PlainElastic中的demo示例:

      string query = new QueryBuilder<Tweet>()        // This will generate: 
          .Query(q => q                         // { "query": { "term": { "User": "somebody" } } }
            .Term(t => t
              .Field(tweet=> tweet.User).Value("somebody")
            )
          ).Build();

  可以看到,構造查詢語句的時候很靈活,直接用表示式的形式,最後通過Build方法,生成相應的查詢語句,於是乎,照著葫蘆畫瓢,開始吧。其實,不管如何寫語句,最終都是對字串的拼接,生成最終的查詢語句。那我們就從最簡單的term查詢開始。比如一條查詢語句就是 {"query":{"term":{"name":"zhangsan"}}},這條語句的的意思,就是查詢 name 為zhangsan的資料。(需要讀者懂elasticsearch查詢語法)

  先不考慮封裝,直接新建一個類,就叫 TermFilter,內部實現了 Term 的語言構造。由於需要鏈式呼叫,所以裡面的方法一般都返回  this 。

        private Dictionary<string, object> _terms;
public TermFilter() { _terms = new Dictionary<string, object>(); } public TermFilter KeyValue(string key, object value) { _terms.Add(key, value); return this; }

  如上述程式碼所示,當我們呼叫KeyValue方法時,傳入key和value,新增到內部的Dictionary中。然後重寫 ToString 方法,構造Term語句

       private void Build()
        {
            StringBuilder str = new StringBuilder();
            int i = 0;
            foreach (KeyValuePair<string, object> kv in _terms)
            {
                str.Append("{\"term\":{\"" + kv.Key + "\":" + kv.Value + "}}");
                if (i >= 0 && i < _terms.Count - 1)
                {
                    str.Append(",");
                }
                i++;
            }
            _condition = str.ToString();
        }
        public override string ToString()
        {
            Build();

            return base.ToString();
        }

  遍歷Dictionary,構造term語句,Term構造完之後,我們需要在外層加一個Query,由於Query是通用的,所以也需要提取出。於是乎,又多了一個類,叫做Filter,這個是查詢的入口,裡面有兩個方法,一個Bool方法,一個Query方法:

     public Filter() { }
        //
        public Filter Bool(Func<BoolFilter, BoolFilter> boolFunc)
        {
            string boolFuncResult = boolFunc(new BoolFilter()).ToString();
            _condition = "{\"query\":{\"filtered\":{\"filter\":{" + boolFuncResult + "}}}";
            return this;
        }
        public Filter Query(Func<BoolFilter, BoolFilter> boolFunc)
        {
            string boolFuncResult = boolFunc(new BoolFilter()).ToString();
            _condition = "{\"query\":" + boolFuncResult;
            return this;
        }

  直接看Query方法,裡面的引數為 Func<BoolFilter,BoolFilter> boolFunc,好吧,這裡的Boss終於出場了,就是核心類,BoolFilter,它內部實現了,Must,Shoud,MustNot,And,Or,等方法。當然還有Term。我們直接看Term方法。

 public BoolFilter Term(Func<TermFilter, TermFilter> termFunc)
        {
            PrapareCondition();
            _condition += termFunc(new TermFilter());
            return this;
        }

  同理,因為鏈式呼叫,還是返回this,上述程式碼中由於termFunc 返回的是一個TermFilter物件,然後toString之後,就相當於追加 相應的Term語句。ToString方法最終也是返回 _condition欄位的值。好吧,我猜你越來越暈了,沒關係,我們在看最後一個類,就可以實戰了。

      public QueryCreator Filter(Func<Filter, Filter> filter)
        {
            _condition += filter(new Filter());
            return this;
        }

  好了,到這裡,程式碼基本結束了。重新梳理一遍:

  首先,最外層程式碼呼叫Filter方法,Filter實現了Query方法,Query內部傳入了BoolFilter引數,在呼叫Term方法,最終由TermFilter實現語句的構造,所以,外部最終程式碼呼叫起來是這樣的。

var result = creator.Filter(f =>  //Filter內部呼叫Query方法,
                                            f.Query(q => //Query呼叫BoolFilter的Term方法
                                                        q.Term(t => //BoolFilter又呼叫TermFilter的KeyValue方法
                                                                    t.KeyValue("name", "zhangsan"))))
                                                                    .BuildBeautiful();//最後構造出我們想要的結果

  

如上圖,from和size是預設的。下面我們來個稍微複雜點的。比如在一個使用者表,想要查詢 使用者型別為 3 的且地區為 北京 的 並且滿足 年齡是 20  歲或者 工作經驗為1年 的使用者。並且根據 姓名倒敘排序,分頁取第3頁的20條資料。

首先分析一下,這裡我們要使用 and 查詢,and 裡面還包括  or 查詢。 構造語句如下:

        var result = creator.Filter(f => 
                                            f.Bool(b => //bool查詢
                                            b.Must(m => //must,必須符合條件
                                            m.And(a => //and查詢
                                            a.Term(t => //構造查詢條件
                                            t.KeyValue("type", 3).KeyValue("area", "北京")).
                                            Or(o => o.Term(t1 => t1.KeyValue("age", 20).KeyValue("experience", 1))))))).//or查詢,構造查詢條件
                                            Page(3).//頁碼
                                            Size(20).//每頁大小
                                            OrderByDesc("name").//姓名倒敘排序
                                            BuildBeautiful();//根據之前的條件建立查詢語句

  哈哈,是不是有點繞啊,其實要想用這個,還是得會點ES查詢語法的,就好比SQL語句一樣,你不理解,是查不出東東的。用這種方式我們就能夠避免手動寫ES查詢語句了,只要經過程式碼簡單配製就好嘍,不過我還是乖乖用第三方元件吧,自己寫的太渣了。。。

看看構造成的語句:

 

好了就到這裡吧。當做自己研究的總結了。

相關文章