niedziela, 29 września 2013

[FullTextSearch] ElasticSearch: river MongoDB

Najpopularniejszym sposobem dodawania danych do indeksów ElasticSearch jest wysyłanie odpowiednich REST-owych wiadomości. Ale to nie jedyny sposób. Możemy czasem pobierać dane także bezpośrednio z zewnętrznych źródeł, na przykład z baz danych. Mechanizmem, który umożliwia synchronizację pomiędzy źródłem danych, a indeksem ElasticSearch jest river. Rivery są dostępne dla różnych baz NoSql, przykładowo dla MongoDB musimy zainstalować ten river. Aby uruchomić synchronizację pomiędzy instancją bazy, a indeksem ElasticSearch musimy wykonać poniższe kroki. Rozpoczynamy od instalacji pluginu do ElasticSearch. Wchodzimy do katalogu bin w ElasticSearch i wpisujemy polecenie

plugin.bat -i com.github.richardwilly98.elasticsearch/elasticsearch-river-mongodb/1.7.0

Warto pamiętać, że przy instalacji rivera muszą się zgadzać wersje ES i MongoDB. Kompatybilność możemy sprawdzić tutaj.

Teraz musimy przygotować bazę do pracy w trybie replikacji. Plugin river synchronizuje dane dzięki plikowi oplog.rs, udostępnianemu przez MongoDB tylko w trybie replikacji. Plik ten zawiera log z wszystkich zmian, jakie były wykonywane na danych. Pierwotnie jego celem było synchronizowanie danych pomiędzy wieloma replikami, ale wykorzystuje go także river. Do celów ElasticSearch można włączyć replikację składającą się z jednej repliki.

1. Uruchamiamy jeden proces, na którym chodzi baza poleceniem mongod --dbpath rsdata --replSet rs0
2. W drugim oknie wchodzimy do narzędzia konfiguracji poleceniem mongo
3. Wpisujemy polecenie rs.initiate() i czekamy, aż nasz node ustawi się jako PRIMARY (naciśnięcie enter po kilkunastu sekundach powinno upewnić nas o zmianie stanu)

Teraz uruchamiamy ElasticSearch pozostawiając MongoDB działające w innym oknie, tryb konfiguracji możemy opuścić.Wysyłamy PUT z konfiguracją mapowania kolekcji w bazie na indeks w ElasticSearch.

PUT http://localhost:9200/_river/mongolink/_meta HTTP/1.1
User-Agent: Fiddler
content-type: application/json
Host: localhost:9200
Content-Length: 214

{
  "type" : "mongodb",
  "mongodb" : {
   "servers" : [
    { "host" : "localhost", "port" : 27017 }
   ],
   "db" : "esbook",
   "collection" : "products"
  },
  "index" : {
   "name" : "esbook"
  }
}

Na indeksie _river stworzyliśmy typ mongolink, który będzie się mapował na kolekcję products z bazy esbook . Teraz możemy dodać dane, na przykład z konsoli mongo.

rs0:PRIMARY> use esbook
switched to db esbook
rs0:PRIMARY> db.products.insert({ "name" : "book", "value" : 200 });

Teraz możemy obejrzeć zmapowane zasoby pod adresem http://localhost:9200/esbook/_search?pretty

sobota, 28 września 2013

[FullTextSearch] ElasticSearch: Faceting

Terminem Faceting określa się technikę agregowania danych gromadzonych w indeksach ElasticSearch. Wysyłając odpowiednie zapytania możemy pobrać pewne informacje statystyczne na temat dokumentów, jakie przechowujemy. Pierwszy rodzaj statystyki, jaką możemy sobie stworzyć to grupowanie po tokenach utworzonych na danym polu, a następnie zliczenie ilości. Wysyłamy zapytanie...

POST http://localhost:9200/books/book/_search HTTP/1.1
User-Agent: Fiddler
content-type: application/json
Host: localhost:9200
Content-Length: 135

{
 "query" : { "match_all" : {} },
 "facets" : {
  "author_facet" : {
   "terms" : {
    "field" : "author"
   }
  }
 }
}

...i w odpowiedzi otrzymujemy:

{
   "took":4,
   "timed_out":false,
   "_shards":{
      "total":5,
      "successful":5,
      "failed":0
   },
   "hits":{
      "total":5,
      "max_score":1.0,
      "hits":[
       (...)
      ]
   },
   "facets":{
      "author_facet":{
         "_type":"terms",
         "missing":0,
         "total":10,
         "other":0,
         "terms":[
            {
               "term":"michio",
               "count":3
            },
            {
               "term":"kaku",
               "count":3
            },
            {
               "term":"roger",
               "count":2
            },
            {
               "term":"penrose",
               "count":2
            }
         ]
      }
   }
}

Dane możemy także agregować definiując zakresy, na przykład

POST http://localhost:9200/books/book/_search HTTP/1.1
User-Agent: Fiddler
content-type: application/json
Host: localhost:9200
Content-Length: 246

{
 "query" : { "match_all" : {} },
 "facets" : {
  "ranges_facet_result" : {
   "range" : {
    "field" : "year",
    "ranges" : [
     { "to" : 1995 },
     { "from" : 1995, "to" : 2000 },
     { "from" : 2000 }
    ]
   }
  }
 }
}

Zwrócone nam zostanie więcej informacji niż poprzednio, ponieważ agregujemy po polu typu numerycznego

{
   "took":145,
   "timed_out":false,
   "_shards":{
      "total":5,
      "successful":5,
      "failed":0
   },
   "hits":{
      "total":5,
      "max_score":1.0,
      "hits":[

      ]
   },
   "facets":{
      "ranges_facet_result":{
         "_type":"range",
         "ranges":[
            {
               "to":1995.0,
               "count":2,
               "min":1989.0,
               "max":1993.0,
               "total_count":2,
               "total":3982.0,
               "mean":1991.0
            },
            {
               "from":1995.0,
               "to":2000.0,
               "count":1,
               "min":1995.0,
               "max":1995.0,
               "total_count":1,
               "total":1995.0,
               "mean":1995.0
            },
            {
               "from":2000.0,
               "count":2,
               "min":2004.0,
               "max":2006.0,
               "total_count":2,
               "total":4010.0,
               "mean":2005.0
            }
         ]
      }
   }
}

Powyższe zapytanie możemy także wykonać przy równomiernym rozłożeniu danych (tak jak w histogramie).

POST http://localhost:9200/books/book/_search HTTP/1.1
User-Agent: Fiddler
content-type: application/json
Host: localhost:9200
Content-Length: 156

{
 "query" : { "match_all" : {} },
 "facets" : {
  "total_histogram" : {
   "histogram" : {
    "field" : "year",
    "interval" : 5
   }
  }
 }
}

Najwięcej danych do analizy dostarcza nam jednak zapytanie typu statistical.

POST http://localhost:9200/books/book/_search HTTP/1.1
User-Agent: Fiddler
content-type: application/json
Host: localhost:9200
Content-Length: 139

{
 "query" : { "match_all" : {} },
 "facets" : {
  "statistical_test" : {
   "statistical" : {
    "field" : "price"
   }
  }
 }
}

ElasticSearch zwraca między innymi zakres, średnią, wariancję, odchylenie standardowe i sumę kwadratów.

{
   "took":55,
   "timed_out":false,
   "_shards":{
      "total":5,
      "successful":5,
      "failed":0
   },
   "hits":{
      "total":5,
      "max_score":1.0,
      "hits":[(...)]
   },
   "facets":{
      "statistical_test":{
         "_type":"statistical",
         "count":5,
         "total":114.67999999999999,
         "min":12.06,
         "max":42.0,
         "mean":22.936,
         "sum_of_squares":3157.3112,
         "variance":105.40214400000013,
         "std_deviation":10.266554631423345
      }
   }
}

[FullTextSearch] ElasticSearch: Highlighting

Wyniki wyszukiwania bardzo często są wyświetlane bezpośrednio na interfejsie użytkownika, np. poprzez wstawienie ich bezpośrednio do pewnych kontenerów HTML-owych. W związku z tym bardzo przydatna okazuje się możliwość zaznaczania wyszukanej frazy. W ElasticSearch możemy skorzystać z takiej funkcjonalności w bardzo prosty sposób.

POST http://localhost:9200/books/book/_search HTTP/1.1
User-Agent: Fiddler
content-type: application/json
Host: localhost:9200
Content-Length: 276

{
 "query" : {
  "prefix" : {
   "author" : "ro"
  }
 },
 "highlight" : {
  "require_field_match" : true,
  "fields" : {
   "title" : { "pre_tags" : [ "<b>" ], "post_tags" : [ "</b>" ] },
   "author" : { "pre_tags" : [ "<b>" ], "post_tags" : [ "</b>" ] }
  }
 }
}

W odpowiedzi otrzymamy:

HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Content-Length: 660

{"took":8,"timed_out":false,"_shards":{"total":5,"successful":5,"failed":0},"hits":{"total":2,"max_score":1.0,"hits":[{"_index":"books","_type":"book","_id":"5","_score":1.0, "_source" : {
 "title": "The Road to Reality: A Complete Guide to the Laws of the Universe",
 "author": "Roger Penrose",
 "year": 2004,
 "price": 42.00
},"highlight":{"author":["<b>Roger</b> Penrose"]}},{"_index":"books","_type":"book","_id":"4","_score":1.0, "_source" : {
 "title": "The Emperor's New Mind: Concerning Computers, Minds and The Laws of Physics",
 "author": "Roger Penrose",
 "year": 1989,
 "price": 22.74
},"highlight":{"author":["<b>Roger</b> Penrose"]}}]}}

Nieustawienie pola require_field_match spowoduje, że znaleziona fraza będzie zaznaczana we wszystkich skonfigurowanych polach, a nie tylko na tym polu, do którego wysyłamy zapytanie.

piątek, 27 września 2013

[FullTextSearch] ElasticSearch: Filtrowanie, Sortowanie, Paginacja

Filtrowanie

Filtrowanie zawęża zbiór dokumentów, na których odbywa się przeszukiwanie pełnotekstowe. Podejście takie znacznie poprawia wydajność zapytań. Filtr nie wpływa także na wynik wyszukiwania (pole _score) w zwracanej kolekcji wyników. Przykład poprawnego filtrowanego zapytania, gdzie kolekcja jest najpierw filtrowana, a później odpytana po indeksie:

POST http://localhost:9200/books/book/_search HTTP/1.1
User-Agent: Fiddler
Content-Type: application/json
Host: localhost:9200
Content-Length: 160

{
 "query": {
  "filtered" : {
   "query" : {
    "field" : { "author" : "Kaku" }
   },
    "filter" : {
    "term" : { "year" : 1993 }
   }
  }
 }
}

Filtrować można także zawężając przeszukiwany zbiór

POST http://localhost:9200/books/book/_search HTTP/1.1
User-Agent: Fiddler
Content-Type: application/json
Host: localhost:9200
Content-Length: 331

{
   "query":{
      "filtered":{
         "query":{
            "term":{
               "title":"worlds"
            }
         },
         "filter":{
            "range":{
               "year":{
                  "from":2000,
                  "to":2013
               }
            }
         }
      }
   }
}

Sortowanie

Ponieważ ElasticSearch może działać jako zwykła dokumentowa baza danych, musi także wspierać sortowanie zwracanych elementów. Wykorzystujemy do tego pole sort.

POST http://localhost:9200/books/book/_search HTTP/1.1
User-Agent: Fiddler
Content-Type: application/json
Host: localhost:9200
Content-Length: 156

{
"query" : {
 "match" : {
  "author" : {
   "query" : "penrose kaku",
   "operator" : "or"
  }
 }
},
  "sort" : [
    { "price" : "asc" }
  ]
}

Paginacja

Stronnicowanie jest dziś powszechnie stosowaną techniką umożliwiającą przesyłanie danych małymi porcjami. Również ElasticSearch wspiera stronnicowanie za pomocą pól from oraz size. 

POST http://localhost:9200/books/book/_search HTTP/1.1
User-Agent: Fiddler
Content-Type: application/json
Host: localhost:9200
Content-Length: 184

{
"query" : {
 "match" : {
  "author" : {
   "query" : "penrose kaku",
   "operator" : "or"
  }
 }
},
  "sort" : [
    { "price" : "asc" }
  ],
 "from" : 2,
 "size" : 2
}

[FullTextSearch] ElasticSearch: Query DSL

Zapytania do ElasticSearch wysyłamy REST-owo. Do prostego pobierania dokumentów wystarczą nam sparametryzowane zapytania typu GET. Aby jednak poznać pełnię możliwości ElasticSearch musimy poznać język zapytań DSL, w którym parametry zapytania ustawiamy poprzez załączenie z żądaniem typu POST odpowiedniego obiektu w postaci JSON. ElasticSearch udostępnia wiele rodzajów zapytań. Poniżej ciekawsze z nich.

Term Query

Tego typu zapytania można wykonywać na dwa sposoby. Przy użyciu czasownika GET

http://localhost:9200/books/book/_search?q=title:parallel&pretty=true

lub za pomocą czasownika POST

POST http://localhost:9200/books/book/_search?pretty=true HTTP/1.1
User-Agent: Fiddler
content-type: application/json
Host: localhost:9200
Content-Length: 59

{
 "query" : {
  "term" : { "title" : "parallel" }
 }
}

W obu przypadkach otrzymujemy ten sam rezultat

HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Content-Length: 809

{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 2,
    "max_score" : 0.095891505,
    "hits" : [ {
      "_index" : "books",
      "_type" : "book",
      "_id" : "1",
      "_score" : 0.095891505, "_source" : {
 "title": "Parallel Worlds: A Journey Through Creation, Higher Dimensions, and the Future of the Cosmos",
 "author": "Michio Kaku",
 "year": 2006,
 "price": 12.06
}
    }, {
      "_index" : "books",
      "_type" : "book",
      "_id" : "3",
      "_score" : 0.095891505, "_source" : {
 "title": "Hyperspace: A Scientific Odyssey Through Parallel Universes, Time Warps, and the Tenth Dimension",
 "author": "Michio Kaku",
 "year": 1995,
 "price": 16.36
}
    } ]
  }
}

Term Query to takie zapytanie, które zwraca wyniki tylko wtedy, gdy podamy dokładnie słowo, które znajduje się w danym polu. Nie zadziałają tutaj zapytania przedrostowe typu "paral*". Wyniki za każdym razem dostępne będą w tablicy dostępnej pod hits.hits. Pełny dokument zwracany jest w polu _source. Pobieranie tak zagnieżdżonych informacji wydaje się niewygodne, a czasami także niepotrzebnie przesyłany jest cały duży dokument, dlatego możemy podać, które pola nas interesują.

POST http://localhost:9200/books/book/_search?pretty=true HTTP/1.1
User-Agent: Fiddler
content-type: application/json
Host: localhost:9200
Content-Length: 92

{
 "fields" : ["title","author"],
 "query" : {
  "term" : { "title" : "parallel" }
 }
}

W rezultacie otrzymamy:

HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Content-Length: 797

{
  "took" : 9,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "failed" : 0
  },
  "hits" : {
    "total" : 2,
    "max_score" : 0.095891505,
    "hits" : [ {
      "_index" : "books",
      "_type" : "book",
      "_id" : "1",
      "_score" : 0.095891505,
      "fields" : {
        "title" : "Parallel Worlds: A Journey Through Creation, Higher Dimensions, and the Future of the Cosmos",
        "author" : "Michio Kaku"
      }
    }, {
      "_index" : "books",
      "_type" : "book",
      "_id" : "3",
      "_score" : 0.095891505,
      "fields" : {
        "title" : "Hyperspace: A Scientific Odyssey Through Parallel Universes, Time Warps, and the Tenth Dimension",
        "author" : "Michio Kaku"
      }
    } ]
  }
}

Odpytywać można także tylko indeks (bez podawania typu book) oraz wiele indeksów lub wiele typów. Na przykład:

http://localhost:9200/books,books5/book/_search?q=author:kaku&pretty=true

Terms Query

Podajemy kilka wartości, które nie będą analizowane (muszą to być dokładne wartości), oraz opcjonalnie ile z nich musi pasować, aby zwrócony został dany dokument.

POST http://localhost:9200/books/book/_search?pretty=true HTTP/1.1
User-Agent: Fiddler
content-type: application/json
Host: localhost:9200
Content-Length: 111

{
 "query" : {
  "terms" : { "title" : ["parallel","hyperspace","worlds"],
  "minimum_match" : 2
  }
 }
}

Match Query

W tym zapytaniu podane wyrażenie zostaje przetworzone przez analizator. A zatem tym razem możemy podawać niepełne wyrazy, na przykład:

POST http://localhost:9200/books/book/_search?pretty=true HTTP/1.1
User-Agent: Fiddler
content-type: application/json
Host: localhost:9200
Content-Length: 65

{
 "query" : {
  "match" : { "title" :"para worlds"
  }
 }
}

Zapytanie takie zwróci wyniki. Domyślnie słowa są łączone operatorem OR, można to zmienić w następujący sposób.

POST http://localhost:9200/books/book/_search?pretty=true HTTP/1.1
User-Agent: Fiddler
content-type: application/json
Host: localhost:9200
Content-Length: 122

{
 "query" : {
  "match" : {
   "title" : {
    "query" : "parallel worlds",
    "operator" : "and"
   }
  }
 }
}

Match Phrase

Jeżeli chcemy wyszukać frazy składającej się z większej ilości słów, korzystamy z match_phrase, gdzie parametr slop definiuje, ile nieznanych słów może się mieścić pomiędzy podanymi wyrazami.

POST http://localhost:9200/books/book/_search?pretty=true HTTP/1.1
User-Agent: Fiddler
content-type: application/json
Host: localhost:9200
Content-Length: 121

{
 "query" : {
  "match_phrase" : {
   "title" : {
    "query" : "parallel worlds",
    "slop" : 1
   }
  }
 }
}

Multi Match

Efekt zapytania typu "Match" możemy uzyskać także na wielu polach dokumentu.

POST http://localhost:9200/books/book/_search?pretty=true HTTP/1.1
User-Agent: Fiddler
content-type: application/json
Host: localhost:9200
Content-Length: 131

{
 "query" : { 
  "multi_match" : {
   "query" : "univers michio penrose",
   "fields" : [ "title", "author" ]
  }
 }
}

Identifiers Query

Kiedy dokładnie znamy identyfikatory dokumentów, które chcemy pobrać, korzystamy z tego typu zapytania.

POST http://localhost:9200/books/book/_search?pretty=true HTTP/1.1
User-Agent: Fiddler
content-type: application/json
Host: localhost:9200
Content-Length: 73

{
 "query" : {
  "ids" : {
   "values" : [ "1", "4", "5" ]
  }
 }
}

Prefix Query

Wyszukiwanie po przedrostkach.

POST http://localhost:9200/books/book/_search?pretty=true HTTP/1.1
User-Agent: Fiddler
content-type: application/json
Host: localhost:9200
Content-Length: 63

{
 "query" : {
  "prefix" : {
   "title" : "uni"
  }
 }
}

Fuzzy Like This Query

Zapytanie, które dopuszcza literówki w podanej frazie.

POST http://localhost:9200/books/book/_search?pretty=true HTTP/1.1
User-Agent: Fiddler
content-type: application/json
Host: localhost:9200
Content-Length: 115

{
 "query" : {
  "fuzzy_like_this" : {
   "fields" : ["title", "author"],
   "like_text" : "penrse"
  }
 }
}

Range Query

Podajemy zakresy, przydatne dla wartości numerycznych.

POST http://localhost:9200/books/book/_search?pretty=true HTTP/1.1
User-Agent: Fiddler
content-type: application/json
Host: localhost:9200
Content-Length: 100

{
 "query" : {
  "range" : {
   "year" : {
    "from" : 1990,
    "to" : 2000
   }
  }
 }
}

czwartek, 26 września 2013

[FullTextSearch] ElasticSearch: Mapping

Po dodaniu dokumentów do indeksu ElasticSearch stworzy za nas domyślny mapping na podstawie dodawanych danych. Mappingi założone na danym indeksie możemy obejrzeć wysyłając zapytanie REST-owe.

GET http://localhost:9200/books/_mapping?pretty HTTP/1.1

Zwrócony nam zostanie mapping dla wszystkich typów założonych na danym indeksie.

HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=UTF-8
Content-Length: 325

{
  "books" : {
    "book" : {
      "properties" : {
        "author" : {
          "type" : "string"
        },
        "price" : {
          "type" : "double"
        },
        "title" : {
          "type" : "string"
        },
        "year" : {
          "type" : "long"
        }
      }
    }
  }
}

Jeżeli kolejne dokumenty naruszą wcześniej zdefiniowany mapping (np. zmieniając typ jednego z pól w JSONie"), ElasticSearch zwróci nam błąd.

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=UTF-8
Content-Length: 129

{"error":"MapperParsingException[failed to parse [year]]; nested: NumberFormatException[For input string: \"a\"]; ","status":400}

Mappingi możemy także definiować ręcznie. Przed wysłaniem jakichkolwiek dokumentów wysyłamy żadanie typu POST pod adres zawierający nazwę nowego indeksu.

POST http://localhost:9200/books2/ HTTP/1.1
User-Agent: Fiddler
Host: localhost:9200
Content-Type: application/json
Content-Length: 384

{
  "mappings": {
    "book" : {
      "_source": {
        "enabled": false 
      },
      "properties" : {
        "author" : {
          "type" : "string"
        },
        "price" : {
          "type" : "double"
        },
        "title" : {
          "type" : "string"
        },
        "year" : {
          "type" : "long"
        }
      }
    }
  }
}

Ustawienie_source.enabled na false spowoduje, że zapytania zwracać nam będą jedynie identyfikatory dokumentów bez ich zawartości. Inny przykład pokazujący, co można ustawić dla poszczególnych properties.

{
  "mappings": {
    "book" : {
      "properties" : {
        "author" : {
          "type" : "string",
          "index" : "not_analyzed"
        },
        "price" : {
          "type" : "double",
          "store" : "yes"
        },
        "title" : {
          "type" : "string"
        },
        "year" : {
          "type" : "long"
        }
      }
    }
  }
}

Pole index ustawione na analyzed sprawi że wartości zostaną przeanalizowane i zaindeksowane przez ElastcSearch. Dla pól typu string ustawienie wartości not_analyzed spowoduje, że pole zostanie zaindeksowane, ale nie będzie przetwarzane przez analyzer, tak więc search zwróci wyniki tylko w przypadku, gdy podamy dokładnie któryś z wyrazów zawartych w tym polu. Nie będzie zatem możliwe na przykład przeszukiwanie po przedrostkach typu "Mi*". Pole store odpowiada temu, czy oryginalna wartość pola powinna być wpisywana do indeksu. Dodatkowo dla dat możemy ustawiać format zgodnie z tą listą. Przy bardziej zaawansowanych problemach możemy także dla danego indeksu wybrać specjalny analizator.

Maping dynamiczny można także w pewien sposób konfigurować. Jeżeli chcemy wymusić, aby pola wysyłane jako string były rzutowane do long-ów wtedy gdy to możliwe, przy tworzeniu indeksu wysyłamy następujący obiekt.

POST http://localhost:9200/books5 HTTP/1.1
User-Agent: Fiddler
Host: localhost:9200
Content-Type: application/json
Content-Length: 83

{
  "mappings": {
    "book" : {
      "numeric_detection" : true
    }
  }
}

niedziela, 22 września 2013

[FullTextSearch] ElasticSearch: CRUD

Używając ElasticSearch jako zwykłej dokumentowej bazy danych możemy w prosty sposób wykonywać CRUD-owe operacje. Jak już wspomniano w poprzednim poście, wszystkie operacje wykonuje się przy użyciu RESTa i odpowiednich verb-ów HTTP.

Dodawanie

PUT http://localhost:9200/books/book/1 HTTP/1.1
User-Agent: Fiddler
Content-Type: application/json
Host: localhost:9200
Content-Length: 175

{
	"title": "Parallel Worlds: A Journey Through Creation, Higher Dimensions, and the Future of the Cosmos",
	"author": "Michio Kaku",
	"year": "2006",
	"price": "12.06"
}

Dodaliśmy zasób do kolekcji books. Zasób jest typu book. Na końcu dodajemy unikalny identyfikator. Jeżeli chcemy, by serwer ElasticSearch wygenerował za nas Id wysyłamy żądanie typu POST pod adres /books/book/.

Pobieranie

Aby pobrać zasób po identyfikatorze wysyłamy żądanie typu GET.
 
GET http://localhost:9200/books/book/1 HTTP/1.1
User-Agent: Fiddler
Content-Type: application/json
Host: localhost:9200

W odpowiedzi otrzymujemy JSON w następującej postaci:

HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Content-Length: 259

{"_index":"books","_type":"book","_id":"1","_version":1,"exists":true, "_source" : {
	"title": "Parallel Worlds: A Journey Through Creation, Higher Dimensions, and the Future of the Cosmos",
	"author": "Michio Kaku",
	"year": "2006",
	"price": "12.06"
}}

Modyfikacja

Do modyfikacji wykorzystujemy POST, gdzie w body podajemy "ścieżkę" do property, które chcemy zmodyfikować.

POST http://localhost:9200/books/book/1/_update HTTP/1.1
User-Agent: Fiddler
Content-Type: application/json
Host: localhost:9200
Content-Length: 57

{
  "script": "ctx._source.author = \"Kaku, Michio\""
}

Usuwanie

Do usunięcia wykorzystuje się czasownik DELETE.
 
DELETE http://localhost:9200/books/book/1/ HTTP/1.1
User-Agent: Fiddler
Content-Type: application/json
Host: localhost:9200

Ponowne wysłanie GET'a pod powyższy adres zwraca kod HTTP 404.

czwartek, 12 września 2013

[FullTextSearch] ElasticSearch: Wprowadzenie



ElasticSearch to open sourcowy projekt serwera wspierającego wyszukiwanie pełnotekstowe. Zapoczątkowany został w 2010 roku w oparciu o popularne rozwiązanie Apache Lucene. Obecnie jest jednym z najpopularniejszych engine'ów do Full Text Search obok Solr i Lucene. Komunikacja z ES odbywa się poprzez REST. Niektórzy wykorzystują go także jako dokumentową bazę danych, zapewniającą podobnie jak MongoDB dobrą skalowalność i możliwość rozproszenia danych przez mechanizm shardingu.

Podstawowe pojęcia związane z ElasticSearch:

Indeks
W indeksach trzymane są dane, może on być interpretowany jako odpowiednik SQL-owej tabeli, z tą różnicą, że indeks jest zoptymalizowany pod kątem szybkiego przeszukiwania i pobierania danych.

Dokument
Dokument to pojedyncza encja danych, odpowiednik rekordu z SQL. Dokumenty składają się z pól odpowiadających kolumnom. Każde pole jest typowane (numer, string, data itd). Należy pamiętać, że dokumenty nie muszą zachowywać takiej samej struktury w obrębie indeksu.

Typ dokumentu
Pozwala rozróżniać rodzaje dokumentów trzymane w obrębie jednego indeksu.

Node'y i klastry
Umożliwiają rozproszenie serwera ElasticSearch na wiele fizycznych maszyn. Grupa takich maszyn jest nazywana klastrem, natomiast każdy z nich jest nazywany nodem.

Shard
Porcja danych. Pojedynczy shard jest osobnym indeksem Lucene i może być umieszczony na osobnym serwerze, ElasticSearch otrzymując zapytanie wie, do którego sharda je przekierować. Przykładowo nazwiska można pogrupować alfabetycznie tak, jak kilkutomową encyklopedię. Jeżeli zapytanie zaczyna się od A to trafia do pierwszego sharda, jeżeli od np. J to do drugiego.

Replica
Replika to kopia danych z głównego sharda, która musi być z nim synchronizowana. Kiedy główny shard padnie, jego replika będzie promowana do bycia głównym shardem i do niej zostaną przekierowane zapytania. Rozwiązanie takie zapewnia niezawodność danych.

Aby skorzystać z ElasticSearch wystarczy pobrać plik .zip ze strony http://www.elasticsearch.org/ , a następnie rozpakować go w dowolnym miejscu na dysku. Serwer uruchamiany z command line poleceniem elasticsearch.bat wywołanym z poziomu folderu bin. Budowane indeksy są przechowywane w folderze data. ElasticSearch domyślnie działa na porcie 9200.

Do poprawnego działania ElasticSearch potrzebuje zainstalowany Java Development Kit, oraz ustawioną ścieżkę do JAVA_HOME. Instrukcja na stronie Oracle.

sobota, 7 września 2013

[C#|Visual Studio] ServiceStack: Caching i Walidacja

Cache'ować dane można w ServiceStacku na różne sposoby. W rozwiązaniach linuksowych wykorzystuje się Redis - bazę "InMemory". Można korzystać także z MemoryCacheClient, którego konfigurujemy w pliku Global.asax.

container.Register<ICacheClient>(new MemoryCacheClient());

Cache można ustawiać i resetować w RESTowych endpointach. Ważne jest, aby każda cache'owana wartość miała swój unikalmy klucz, który możemy utworzyć za pomocą klasy UrnId.

public object Post(Entry entry)
{
    var cacheKey = UrnId.Create<Entry>(entry.Date.ToShortDateString());
    RequestContext.RemoveFromCache(base.Cache, cacheKey);
    CounterService.Add(entry.Count);
    return new EntryResponse() {Total = CounterService.Print()};
}

public object Get(Entry entry)
{
    var cacheKey = UrnId.Create<Entry>(entry.Date.ToShortDateString());
    return RequestContext.ToOptimizedResultUsingCache(base.Cache, cacheKey, new TimeSpan(0,0,0,5), 
        () => (object)CounterService.Print());
}

Wyliczenie cache'owanej wartości odbywa się poprzez lambda expression.

ServiceStack w genialny sposób wspiera tworzenie reguł walidacyjnych za pomocą FluentValidation. Reguły tworzymy w konstruktorach odpowiednich klas. Walidację należy najpierw zarejestrować.

public class MyEntryAppHost : AppHostBase
{
    public override void Configure(Funq.Container container)
    {
        Plugins.Add(new ValidationFeature());
        container.RegisterValidators(typeof(EntryService).Assembly);
    }
}

Przykładowa klasa walidacyjna:

public class EntryValidator : AbstractValidator<Entry>
{
    public EntryValidator()
    {
        RuleFor(e => e.Count).GreaterThanOrEqualTo(0).WithMessage("Koniecznie nieujemna");
        RuleFor(e => e.Date).LessThanOrEqualTo(DateTime.Now);
    }
}

piątek, 6 września 2013

[C#|Visual Studio] ServiceStack: IoC i ORM

Domyślnym kontenerem IoC w ServiceStacku jest Funq. Jest to prosty kontener zoptymalizowany pod kątem szybkości i lekkości. Domyślnie rejestrując w nim zależności rejestrujemy je jako Singletony. ServiceStack automatycznie wykona wstrzyknięcie zależności do publicznego property w klasach dziedziczących po Service. Przykład konfiguracji Funq (w pliku Global.asax i metodzie Configure).

public override void Configure(Funq.Container container)
{
    container.RegisterAutoWired<CounterService>();
    container.RegisterAutoWiredAs<CounterService, ICounterService>().ReusedWithin(ReuseScope.None);
    container.Register<ICounterService>(new CounterService());

    var cs = container.Resolve<ICounterService>();

    Console.WriteLine("Registered type is {0}", cs.GetType());
}

Zależności możemy rejestrować w trybie AutoWired, gdzie przeważnie wykonuje się mapowanie klasa na interfejs lub klasa w samą siebie. Do ręcznego ustawiania zależności wykorzystuje się z kolei metodę Register, natomiast do pobierania metodę Resolve. Jeżeli chcemy, by za każdym razem kontener zwracał nowy obiekt, musimy dopisać instrukcję ReusedWithin z parametrem None.

Mechanizmem do mapowania obiektowo - relacyjnego jest w ServiceStacku OrmLite. Jest to mapper prosty w użyciu i lekki, a zarazem bardzo wydajny, Kolejną jego zaletą jest multiplatformowość - działa z wieloma systemami, co widać przy próbie instalacji NuGetem.


Do tworzenia tabel wykorzystuje się obiekty POCO, które mogą być tymi samymi obiektami co DTO. Mamy do dyspozycji także znane z EntityFramework DataAnnotations. Przykład obiektu POCO:


[Route("/entry")]
[Authenticate]
[RequiredRole("User")]
public class Entry : IReturn<EntryResponse>
{
    [AutoIncrement]
    public int Id { get; set; }
    public DateTime Date { get; set; }
    public int Count { get; set; }
}

OrmLite typy złożone serializuje i zapisuje jako BLOBy. Można natomiast zakładać constrainty poprzez użycie odpowiednich atrybutów. Do tworzenia połączeń służy typ IDbConnectionFactory, który można skonfigurować w kontenerze IoC.

public override void Configure(Funq.Container container)
{
    IDbConnectionFactory factory =
        new OrmLiteConnectionFactory(HttpContext.Current.Server.MapPath("~/App_Data/data.txt"),
                                     SqliteDialect.Provider);

    container.Register<IDbConnectionFactory>(factory);

    container.RegisterAutoWiredAs<CounterService, ICounterService>();
}

Operacje na bazie przeprowadza się w bardzo wygodny sposób

public class CounterService : ICounterService
{
    public IDbConnectionFactory DbConnectionFactory { get; set; }
    private int _counter;

    public void Add(int i)
    {
        _counter += i;
        using (var dbConn = DbConnectionFactory.CreateDbConnection())
        {
            dbConn.Open();
            dbConn.CreateTable<Entry>();
            dbConn.Insert<Entry>(new Entry()
                                     {
                                         Count = i,
                                         Date = DateTime.Now
                                     });
        }
    }

    public int Print()
    {
        using (var dbConn = DbConnectionFactory.CreateDbConnection())
        {
            //WHERE - lambda expression lub SQL jako string
            dbConn.Open();
            return dbConn.Select<Entry>().Sum(x => x.Count);
        }
    }
}