इलास्टिक खोज आधारभूत

Elasticsearch json rest api को साथ एक खोज इन्जिन हो, लुसेन प्रयोग गरी जाभामा लेखिएको। यस इन्जिनका सबै फाइदाहरूको विवरण यहाँ उपलब्ध छ आधिकारिक वेबसाइट। निम्नमा हामी ES को रूपमा Elasticsearch लाई सन्दर्भ गर्नेछौं।

समान इन्जिनहरू कागजात डेटाबेसमा जटिल खोजहरूको लागि प्रयोग गरिन्छ। उदाहरणका लागि, भाषाको मोर्फोलोजीलाई ध्यानमा राखेर खोज्नुहोस् वा भौगोलिक निर्देशांकहरूद्वारा खोज्नुहोस्।

यस लेखमा म अनुक्रमणिका ब्लग पोष्टहरूको उदाहरण प्रयोग गरेर ES को आधारभूत कुराहरूको बारेमा कुरा गर्नेछु। म तपाईंलाई कागजातहरू कसरी फिल्टर गर्ने, क्रमबद्ध गर्ने र खोज्ने भनेर देखाउनेछु।

अपरेटिङ सिस्टममा निर्भर नहुनको लागि, म CURL प्रयोग गरेर ES मा सबै अनुरोधहरू गर्नेछु। गुगल क्रोम भनिने प्लगइन पनि छ भावना.

पाठमा कागजात र अन्य स्रोतहरूको लिङ्कहरू छन्। अन्तमा कागजातमा द्रुत पहुँचको लागि लिङ्कहरू छन्। अपरिचित सर्तहरूको परिभाषाहरू फेला पार्न सकिन्छ शब्दावलीहरू.

ES स्थापना गर्दै

यो गर्नको लागि, हामीलाई पहिले Java चाहिन्छ। विकासकर्ताहरू सिफारिस गर्नुहोस् Java 8 अपडेट 20 वा Java 7 अपडेट 55 भन्दा नयाँ संस्करणहरू स्थापना गर्नुहोस्।

ES वितरण मा उपलब्ध छ विकासकर्ता वेबसाइट। संग्रह अनप्याक गरेपछि तपाईंले चलाउन आवश्यक छ bin/elasticsearch। पनि उपलब्ध छ apt र yum को लागि प्याकेजहरू। त्यहाँ छ डकरको लागि आधिकारिक छवि. स्थापना बारे थप.

स्थापना र सुरुवात पछि, कार्यक्षमता जाँच गरौं:

# для удобства запомним адрес в переменную
#export ES_URL=$(docker-machine ip dev):9200
export ES_URL=localhost:9200

curl -X GET $ES_URL

हामीले यो जस्तै केहि प्राप्त गर्नेछौं:

{
  "name" : "Heimdall",
  "cluster_name" : "elasticsearch",
  "version" : {
    "number" : "2.2.1",
    "build_hash" : "d045fc29d1932bce18b2e65ab8b297fbf6cd41a1",
    "build_timestamp" : "2016-03-09T09:38:54Z",
    "build_snapshot" : false,
    "lucene_version" : "5.4.1"
  },
  "tagline" : "You Know, for Search"
}

अनुक्रमणिका

ES मा एउटा पोस्ट थपौं:

# Добавим документ c id 1 типа post в индекс blog.
# ?pretty указывает, что вывод должен быть человеко-читаемым.

curl -XPUT "$ES_URL/blog/post/1?pretty" -d'
{
  "title": "Веселые котята",
  "content": "<p>Смешная история про котят<p>",
  "tags": [
    "котята",
    "смешная история"
  ],
  "published_at": "2014-09-12T20:44:42+00:00"
}'

सर्भर प्रतिक्रिया:

{
  "_index" : "blog",
  "_type" : "post",
  "_id" : "1",
  "_version" : 1,
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "created" : false
}

ES स्वचालित रूपमा सिर्जना गरियो अनुक्रमणिका ब्लग र प्रकार पोस्ट। हामी एक सशर्त समानता कोर्न सक्छौं: एक सूचकांक एक डाटाबेस हो, र एक प्रकार यो डेटाबेस मा एक तालिका हो। प्रत्येक प्रकारको आफ्नै योजना छ - म्यापिङ, केवल एक सम्बन्ध तालिका जस्तै। कागजात अनुक्रमित हुँदा म्यापिङ स्वचालित रूपमा उत्पन्न हुन्छ:

# Получим mapping всех типов индекса blog
curl -XGET "$ES_URL/blog/_mapping?pretty"

सर्भर प्रतिक्रियामा, मैले टिप्पणीहरूमा अनुक्रमित कागजातको क्षेत्रहरूको मानहरू थपे:

{
  "blog" : {
    "mappings" : {
      "post" : {
        "properties" : {
          /* "content": "<p>Смешная история про котят<p>", */ 
          "content" : {
            "type" : "string"
          },
          /* "published_at": "2014-09-12T20:44:42+00:00" */
          "published_at" : {
            "type" : "date",
            "format" : "strict_date_optional_time||epoch_millis"
          },
          /* "tags": ["котята", "смешная история"] */
          "tags" : {
            "type" : "string"
          },
          /*  "title": "Веселые котята" */
          "title" : {
            "type" : "string"
          }
        }
      }
    }
  }
}

यो ध्यान दिन लायक छ कि ES ले एकल मान र मानहरूको एरे बीच फरक गर्दैन। उदाहरणका लागि, शीर्षक फिल्डले केवल शीर्षक समावेश गर्दछ, र ट्याग फिल्डले स्ट्रिङहरूको एर्रे समावेश गर्दछ, यद्यपि तिनीहरू म्यापिङमा उस्तै रूपमा प्रतिनिधित्व हुन्छन्।
हामी म्यापिङको बारेमा पछि कुरा गर्नेछौं।

अनुरोधहरू

यसको आईडी द्वारा कागजात पुन: प्राप्त गर्दै:

# извлечем документ с id 1 типа post из индекса blog
curl -XGET "$ES_URL/blog/post/1?pretty"
{
  "_index" : "blog",
  "_type" : "post",
  "_id" : "1",
  "_version" : 1,
  "found" : true,
  "_source" : {
    "title" : "Веселые котята",
    "content" : "<p>Смешная история про котят<p>",
    "tags" : [ "котята", "смешная история" ],
    "published_at" : "2014-09-12T20:44:42+00:00"
  }
}

प्रतिक्रियामा नयाँ कुञ्जीहरू देखा पर्यो: _version и _source। सामान्यतया, सबै कुञ्जीहरू बाट सुरु हुन्छ _ आधिकारिक रूपमा वर्गीकृत छन्।

कुञ्जी _version कागजात संस्करण देखाउँछ। आशावादी लकिङ मेकानिज्मले काम गर्नको लागि यो आवश्यक छ। उदाहरणका लागि, हामी संस्करण 1 भएको कागजात परिवर्तन गर्न चाहन्छौं। हामीले परिवर्तन गरिएको कागजात पेश गर्छौं र यो संस्करण 1 भएको कागजातको सम्पादन हो भनी संकेत गर्छौं। यदि अरू कसैले पनि संस्करण 1 को साथ कागजात सम्पादन गर्यो र हाम्रो अगाडि परिवर्तनहरू पेश गर्यो भने, तब ES ले हाम्रा परिवर्तनहरू स्वीकार गर्दैन, किनभने यसले कागजातलाई संस्करण २ सँग भण्डार गर्छ।

कुञ्जी _source हामीले अनुक्रमित गरेको कागजात समावेश गर्दछ। ES ले खोज कार्यका लागि यो मान प्रयोग गर्दैन किनभने अनुक्रमणिकाहरू खोजी गर्न प्रयोग गरिन्छ। ठाउँ बचत गर्न, ES ले संकुचित स्रोत कागजात भण्डारण गर्दछ। यदि हामीलाई केवल आईडी चाहिन्छ, र सम्पूर्ण स्रोत कागजात होइन, तब हामी स्रोत भण्डारण असक्षम गर्न सक्छौं।

यदि हामीलाई थप जानकारी आवश्यक छैन भने, हामी _source को सामग्री मात्र प्राप्त गर्न सक्छौं:

curl -XGET "$ES_URL/blog/post/1/_source?pretty"
{
  "title" : "Веселые котята",
  "content" : "<p>Смешная история про котят<p>",
  "tags" : [ "котята", "смешная история" ],
  "published_at" : "2014-09-12T20:44:42+00:00"
}

तपाईले केहि क्षेत्रहरू मात्र चयन गर्न सक्नुहुन्छ:

# извлечем только поле title
curl -XGET "$ES_URL/blog/post/1?_source=title&pretty"
{
  "_index" : "blog",
  "_type" : "post",
  "_id" : "1",
  "_version" : 1,
  "found" : true,
  "_source" : {
    "title" : "Веселые котята"
  }
}

केही थप पोष्टहरू अनुक्रमणिका गरौं र थप जटिल प्रश्नहरू चलाउनुहोस्।

curl -XPUT "$ES_URL/blog/post/2" -d'
{
  "title": "Веселые щенки",
  "content": "<p>Смешная история про щенков<p>",
  "tags": [
    "щенки",
    "смешная история"
  ],
  "published_at": "2014-08-12T20:44:42+00:00"
}'
curl -XPUT "$ES_URL/blog/post/3" -d'
{
  "title": "Как у меня появился котенок",
  "content": "<p>Душераздирающая история про бедного котенка с улицы<p>",
  "tags": [
    "котята"
  ],
  "published_at": "2014-07-21T20:44:42+00:00"
}'

क्रमबद्ध गर्दै

# найдем последний пост по дате публикации и извлечем поля title и published_at
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
  "size": 1,
  "_source": ["title", "published_at"],
  "sort": [{"published_at": "desc"}]
}'
{
  "took" : 8,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 3,
    "max_score" : null,
    "hits" : [ {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "1",
      "_score" : null,
      "_source" : {
        "title" : "Веселые котята",
        "published_at" : "2014-09-12T20:44:42+00:00"
      },
      "sort" : [ 1410554682000 ]
    } ]
  }
}

हामीले अन्तिम पोस्ट रोज्यौं। size जारी गर्न कागजातहरूको संख्या सीमित गर्दछ। total अनुरोधसँग मेल खाने कागजातहरूको कुल संख्या देखाउँछ। sort आउटपुटमा पूर्णांकहरूको एर्रे हुन्छ जसद्वारा क्रमबद्ध गरिन्छ। ती। मितिलाई पूर्णांकमा रूपान्तरण गरियो। क्रमबद्ध बारे थप जानकारी पाउन सकिन्छ कागजात.

फिल्टर र प्रश्नहरू

संस्करण २ देखि ES ले फिल्टर र क्वेरीहरू बीचको भिन्नता देखाउँदैन, बरु सन्दर्भको अवधारणा प्रस्तुत गरिएको छ.
क्वेरी सन्दर्भ फिल्टर सन्दर्भ भन्दा फरक छ कि क्वेरीले _score उत्पन्न गर्दछ र क्यास गरिएको छैन। म तपाईंलाई पछि देखाउनेछु _score के हो।

मिति अनुसार फिल्टर गर्नुहोस्

हामी अनुरोध प्रयोग गर्दछौं सीमा फिल्टरको सन्दर्भमा:

# получим посты, опубликованные 1ого сентября или позже
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
  "filter": {
    "range": {
      "published_at": { "gte": "2014-09-01" }
    }
  }
}'

ट्यागहरूद्वारा फिल्टर गर्नुहोस्

हामी प्रयोग गर्छौ अवधि प्रश्न दिइएको शब्द समावेश कागजात आईडी खोज्न:

# найдем все документы, в поле tags которых есть элемент 'котята'
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
  "_source": [
    "title",
    "tags"
  ],
  "filter": {
    "term": {
      "tags": "котята"
    }
  }
}'
{
  "took" : 9,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 2,
    "max_score" : 1.0,
    "hits" : [ {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "1",
      "_score" : 1.0,
      "_source" : {
        "title" : "Веселые котята",
        "tags" : [ "котята", "смешная история" ]
      }
    }, {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "3",
      "_score" : 1.0,
      "_source" : {
        "title" : "Как у меня появился котенок",
        "tags" : [ "котята" ]
      }
    } ]
  }
}

पूर्ण पाठ खोज

हाम्रा तीनवटा कागजातहरूले सामग्री क्षेत्रमा निम्न समावेश गर्दछ:

  • <p>Смешная история про котят<p>
  • <p>Смешная история про щенков<p>
  • <p>Душераздирающая история про бедного котенка с улицы<p>

हामी प्रयोग गर्छौ मिलान क्वेरी दिइएको शब्द समावेश कागजात आईडी खोज्न:

# source: false означает, что не нужно извлекать _source найденных документов
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
  "_source": false,
  "query": {
    "match": {
      "content": "история"
    }
  }
}'
{
  "took" : 13,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 3,
    "max_score" : 0.11506981,
    "hits" : [ {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "2",
      "_score" : 0.11506981
    }, {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "1",
      "_score" : 0.11506981
    }, {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "3",
      "_score" : 0.095891505
    } ]
  }
}

यद्यपि, यदि हामीले सामग्री फिल्डमा "कथाहरू" खोज्छौं भने, हामीले केहि पनि भेट्टाउने छैनौं, किनभने अनुक्रमणिकाले मौलिक शब्दहरू मात्र समावेश गर्दछ, तिनीहरूको काण्डहरू होइन। उच्च-गुणस्तर खोजी गर्नको लागि, तपाईंले विश्लेषक कन्फिगर गर्न आवश्यक छ।

क्षेत्र _score शोहरू प्रासंगिकता। यदि अनुरोध फिल्टर सन्दर्भमा कार्यान्वयन गरिएको छ भने, _score मान सधैं 1 बराबर हुनेछ, जसको अर्थ फिल्टरसँग पूर्ण मिलान हुन्छ।

विश्लेषकहरू

विश्लेषकहरू स्रोत पाठलाई टोकनहरूको सेटमा रूपान्तरण गर्न आवश्यक छ।
विश्लेषकहरू एक हुन्छन् टोकनइजर र धेरै वैकल्पिक टोकनफिल्टरहरू। Tokenizer धेरै द्वारा अघि हुन सक्छ चारफिल्टरहरू। टोकनाइजरहरूले स्रोत स्ट्रिङलाई टोकनहरूमा विभाजन गर्दछ, जस्तै स्पेस र विराम चिन्हहरू। TokenFilter ले टोकनहरू परिवर्तन गर्न, मेटाउन वा नयाँ थप्न सक्छ, उदाहरणका लागि, शब्दको स्टेम मात्र छोड्नुहोस्, पूर्वसर्गहरू हटाउनुहोस्, समानार्थी शब्दहरू थप्नुहोस्। CharFilter - सम्पूर्ण स्रोत स्ट्रिङ परिवर्तन गर्दछ, उदाहरणका लागि, html ट्यागहरू काट्छ।

ES सँग धेरै छन् मानक विश्लेषकहरू। उदाहरणका लागि, एक विश्लेषक रूसी.

सदुपयोग गरौं API र हेरौं कसरी मानक र रूसी विश्लेषकहरूले "बिरालोका बच्चाहरूको बारेमा हास्यास्पद कथाहरू" लाई रूपान्तरण गर्छन्:

# используем анализатор standard       
# обязательно нужно перекодировать не ASCII символы
curl -XGET "$ES_URL/_analyze?pretty&analyzer=standard&text=%D0%92%D0%B5%D1%81%D0%B5%D0%BB%D1%8B%D0%B5%20%D0%B8%D1%81%D1%82%D0%BE%D1%80%D0%B8%D0%B8%20%D0%BF%D1%80%D0%BE%20%D0%BA%D0%BE%D1%82%D1%8F%D1%82"
{
  "tokens" : [ {
    "token" : "веселые",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "<ALPHANUM>",
    "position" : 0
  }, {
    "token" : "истории",
    "start_offset" : 8,
    "end_offset" : 15,
    "type" : "<ALPHANUM>",
    "position" : 1
  }, {
    "token" : "про",
    "start_offset" : 16,
    "end_offset" : 19,
    "type" : "<ALPHANUM>",
    "position" : 2
  }, {
    "token" : "котят",
    "start_offset" : 20,
    "end_offset" : 25,
    "type" : "<ALPHANUM>",
    "position" : 3
  } ]
}
# используем анализатор russian
curl -XGET "$ES_URL/_analyze?pretty&analyzer=russian&text=%D0%92%D0%B5%D1%81%D0%B5%D0%BB%D1%8B%D0%B5%20%D0%B8%D1%81%D1%82%D0%BE%D1%80%D0%B8%D0%B8%20%D0%BF%D1%80%D0%BE%20%D0%BA%D0%BE%D1%82%D1%8F%D1%82"
{
  "tokens" : [ {
    "token" : "весел",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "<ALPHANUM>",
    "position" : 0
  }, {
    "token" : "истор",
    "start_offset" : 8,
    "end_offset" : 15,
    "type" : "<ALPHANUM>",
    "position" : 1
  }, {
    "token" : "кот",
    "start_offset" : 20,
    "end_offset" : 25,
    "type" : "<ALPHANUM>",
    "position" : 3
  } ]
}

मानक विश्लेषकले स्ट्रिङलाई स्पेसद्वारा विभाजित गर्यो र सबै कुरालाई सानो अक्षरमा रूपान्तरण गर्यो, रूसी विश्लेषकले महत्त्वपूर्ण शब्दहरू हटायो, यसलाई सानो अक्षरमा रूपान्तरण गर्यो र शब्दहरूको स्टेम छोड्यो।

रूसी विश्लेषकले कुन टोकनाइजर, टोकनफिल्टर, चारफिल्टरहरू प्रयोग गर्छ हेरौं:

{
  "filter": {
    "russian_stop": {
      "type":       "stop",
      "stopwords":  "_russian_"
    },
    "russian_keywords": {
      "type":       "keyword_marker",
      "keywords":   []
    },
    "russian_stemmer": {
      "type":       "stemmer",
      "language":   "russian"
    }
  },
  "analyzer": {
    "russian": {
      "tokenizer":  "standard",
      /* TokenFilters */
      "filter": [
        "lowercase",
        "russian_stop",
        "russian_keywords",
        "russian_stemmer"
      ]
      /* CharFilters отсутствуют */
    }
  }
}

रूसीमा आधारित हाम्रो विश्लेषकको वर्णन गरौं, जसले html ट्यागहरू काट्नेछ। यसलाई पूर्वनिर्धारित कल गरौं, किनभने यस नामको एक विश्लेषक पूर्वनिर्धारित रूपमा प्रयोग गरिनेछ।

{
  "filter": {
    "ru_stop": {
      "type":       "stop",
      "stopwords":  "_russian_"
    },
    "ru_stemmer": {
      "type":       "stemmer",
      "language":   "russian"
    }
  },
  "analyzer": {
    "default": {
      /* добавляем удаление html тегов */
      "char_filter": ["html_strip"],
      "tokenizer":  "standard",
      "filter": [
        "lowercase",
        "ru_stop",
        "ru_stemmer"
      ]
    }
  }
}

पहिले, सबै HTML ट्यागहरू स्रोत स्ट्रिङबाट हटाइनेछ, त्यसपछि टोकनाइजर मानकले यसलाई टोकनहरूमा विभाजन गर्नेछ, परिणामस्वरूप टोकनहरू सानो अक्षरमा सारिनेछ, तुच्छ शब्दहरू हटाइनेछ, र बाँकी टोकनहरू शब्दको स्टेम रहनेछन्।

अनुक्रमणिका सिर्जना गर्दै

माथि हामीले पूर्वनिर्धारित विश्लेषक वर्णन गरेका छौं। यो सबै स्ट्रिङ क्षेत्रहरूमा लागू हुनेछ। हाम्रो पोष्टले ट्यागहरूको एर्रे समावेश गर्दछ, त्यसैले ट्यागहरू पनि विश्लेषकद्वारा प्रशोधन गरिनेछ। किनभने हामी ट्यागमा ठ्याक्कै मिल्ने गरी पोष्टहरू खोजिरहेका छौं, त्यसपछि हामीले ट्याग फिल्डको लागि विश्लेषण असक्षम गर्न आवश्यक छ।

एक विश्लेषक र म्यापिङ संग अनुक्रमणिका blog2 सिर्जना गरौं, जसमा ट्याग फिल्डको विश्लेषण असक्षम गरिएको छ:

curl -XPOST "$ES_URL/blog2" -d'
{
  "settings": {
    "analysis": {
      "filter": {
        "ru_stop": {
          "type": "stop",
          "stopwords": "_russian_"
        },
        "ru_stemmer": {
          "type": "stemmer",
          "language": "russian"
        }
      },
      "analyzer": {
        "default": {
          "char_filter": [
            "html_strip"
          ],
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "ru_stop",
            "ru_stemmer"
          ]
        }
      }
    }
  },
  "mappings": {
    "post": {
      "properties": {
        "content": {
          "type": "string"
        },
        "published_at": {
          "type": "date"
        },
        "tags": {
          "type": "string",
          "index": "not_analyzed"
        },
        "title": {
          "type": "string"
        }
      }
    }
  }
}'

यो अनुक्रमणिका (blog3) मा उही 2 पोष्टहरू थपौं। म यो प्रक्रिया छोड्नेछु किनभने ... यो ब्लग अनुक्रमणिकामा कागजातहरू थप्नु जस्तै हो।

अभिव्यक्ति समर्थनको साथ पूर्ण पाठ खोज

अर्को प्रकारको अनुरोधलाई हेरौं:

# найдем документы, в которых встречается слово 'истории'
# query -> simple_query_string -> query содержит поисковый запрос
# поле title имеет приоритет 3
# поле tags имеет приоритет 2
# поле content имеет приоритет 1
# приоритет используется при ранжировании результатов
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
  "query": {
    "simple_query_string": {
      "query": "истории",
      "fields": [
        "title^3",
        "tags^2",
        "content"
      ]
    }
  }
}'

किनभने हामी रूसी स्टेमिङको साथ एक विश्लेषक प्रयोग गर्दैछौं, त्यसपछि यो अनुरोधले सबै कागजातहरू फिर्ता गर्नेछ, यद्यपि तिनीहरूमा 'इतिहास' शब्द मात्र समावेश छ।

अनुरोधमा विशेष वर्णहरू हुन सक्छन्, उदाहरणका लागि:

""fried eggs" +(eggplant | potato) -frittata"

सिन्ट्याक्स अनुरोध गर्नुहोस्:

+ signifies AND operation
| signifies OR operation
- negates a single token
" wraps a number of tokens to signify a phrase for searching
* at the end of a term signifies a prefix query
( and ) signify precedence
~N after a word signifies edit distance (fuzziness)
~N after a phrase signifies slop amount
# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
  "query": {
    "simple_query_string": {
      "query": "-щенки",
      "fields": [
        "title^3",
        "tags^2",
        "content"
      ]
    }
  }
}'

# получим 2 поста про котиков

सन्दर्भ

PS

यदि तपाईं यस्ता लेख-पाठहरूमा रुचि राख्नुहुन्छ, नयाँ लेखहरूको लागि विचारहरू छन् वा सहयोगको लागि प्रस्तावहरू छन् भने, म व्यक्तिगत सन्देशमा वा m.kuzmin+habr@darkleaf.ru मेल मार्फत सन्देश प्राप्त गर्न पाउँदा खुसी हुनेछु।

स्रोत: www.habr.com