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