Replace old backend with new backend

Change-Id: I62640f31a05f9d90718950808662b8124155ef58
diff --git a/Changes b/Changes
index 618fb05..e4fa86b 100755
--- a/Changes
+++ b/Changes
@@ -1,3 +1,12 @@
+0.30 2018-10-24
+        - Rewrote backend:
+          - Removed dependency of Mojolicious::Plugin::Search.
+          - Removed abstract API.
+        - Improved backend error handling.
+        - Improved backend test suite.
+        - Removed MMap cache from default configuration
+          and rely on in-memory cache.
+
 0.29 2018-10-05
         - Deprecated Kalamar.api configuration key
           in favor of Kalamar.api_path.
diff --git a/lib/Kalamar.pm b/lib/Kalamar.pm
index 1fa6c52..83cea41 100644
--- a/lib/Kalamar.pm
+++ b/lib/Kalamar.pm
@@ -7,7 +7,7 @@
 use Mojo::Util qw/url_escape/;
 
 # Minor version - may be patched from package.json
-our $VERSION = '0.29';
+our $VERSION = '0.30';
 
 # Supported version of Backend API
 our $API_VERSION = '1.0';
@@ -142,7 +142,6 @@
 
   # Load plugins
   foreach (
-    'Search',                    # Abstract Search framework
     'TagHelpers::MailToChiffre', # Obfuscate email addresses
     'KalamarHelpers',            # Specific Helpers for Kalamar
     'KalamarErrors',             # Specific Errors for Kalamar
@@ -208,7 +207,7 @@
   });
 
   # Base query route
-  $r->get('/')->to('search2#query')->name('index');
+  $r->get('/')->to('search#query')->name('index');
 
   # Documentation routes
   $r->get('/doc')->to('documentation#page', page => 'korap')->name('doc_start');
@@ -221,10 +220,10 @@
 
   # Match route
   # Corpus route
-  my $corpus = $r->get('/corpus')->to('Search2#corpus_info')->name('corpus');
+  my $corpus = $r->get('/corpus')->to('search#corpus_info')->name('corpus');
   my $doc    = $r->route('/corpus/:corpus_id/:doc_id');
-  my $text   = $doc->get('/:text_id')->to('search2#text_info')->name('text');
-  my $match  = $doc->get('/:text_id/:match_id')->to('search2#match_info')->name('match');
+  my $text   = $doc->get('/:text_id')->to('search#text_info')->name('text');
+  my $match  = $doc->get('/:text_id/:match_id')->to('search#match_info')->name('match');
 
   # User Management
   my $user = $r->any('/user')->to(controller => 'User');
diff --git a/lib/Kalamar/API.pm b/lib/Kalamar/API.pm
deleted file mode 100644
index c8f9d25..0000000
--- a/lib/Kalamar/API.pm
+++ /dev/null
@@ -1,839 +0,0 @@
-package Kalamar::API;
-use Mojo::Base 'Mojolicious::Plugin';
-use Scalar::Util qw/blessed weaken/;
-use Mojo::JSON qw/true false/;
-use strict;
-use warnings;
-
-# KorAP Search engine for Mojolicious::Plugin::Search
-
-
-# TODO:
-#   This contains a lot of legacy code that is not
-#   necessary anymore. Quite a lot of endpoints are
-#   only proxies and should simply be piped through
-#   the backend (including auth code)
-
-# TODO: Add fixtures
-# TODO: Support search in corpus and virtualcollection
-# TODO: Support caching everywhere!
-# TODO: Correct use of stash info everywhere!
-# TODO: Alot is now underneath "meta"
-
-# TODO:
-#   Rewrite to use promises!
-
-
-# Register the plugin
-sub register {
-  my ($plugin, $mojo, $index_class, $param) = @_;
-  $param ||= {};
-
-  # Add attributes to the index class
-  $index_class->attr(api => $param->{api});
-  $index_class->attr([qw/cutoff
-			 query_language
-			 time_exceeded
-			 api_request
-       authorized
-			 _api_cache
-			 api_response
-			 benchmark
-			 query_jsonld
-			 collection
-			 collection_jsonld/]);
-  $index_class->attr(no_cache => 0);
-  $index_class->attr(status => 200);
-};
-
-
-# Search the index
-sub search {
-  my $self = shift;
-  my $index = shift;
-
-  # Get controller
-  my $c = $index->controller;
-
-  # If there is a callback, do async
-  my $cb = pop if ref $_[-1] && ref $_[-1] eq 'CODE';
-
-  # No query defined
-  unless ($index->query) {
-    return $cb->($index) if $cb;
-    return;
-  };
-
-  # Get query url
-  my $url = _query_url($index, @_);
-
-  # Cache based on URL
-  $index->_api_cache('total-' . $url->to_string);
-
-  my %param = @_;
-
-  # Set context based on parameter
-  # base/s:p
-  $url->query({ context => $param{'context'} // '40-t,40-t' });
-  # 'base/s:p'/'paragraph'
-
-  # Set path to search
-  $url->path('search');
-
-  # Check cache for total results
-  my $total_results;
-
-  # In case the user is not known, it is assumed, the user is not logged in
-  my $user = $c->stash('user') // 'not_logged_in';
-
-  if (!$index->no_cache &&
-        defined ($total_results = $c->chi->get($user . $index->_api_cache))) {
-
-    # Set total results from cache
-    $index->total_results($total_results);
-    $c->app->log->debug('Get total result from cache');
-
-    # Set cutoff unless already set
-    $url->query({cutoff => 'true'}) unless defined $index->cutoff;
-  };
-
-  # Set api request for debugging
-  $index->api_request($url->to_string);
-
-  # Create new user agent and set timeout to 2 minutes
-  #my $ua = $c->user->ua;
-  #$tx = $plugin->ua->start($tx);
-
-  #$ua->inactivity_timeout(120);
-
-  # Debugging
-  $c->app->log->debug('Search for ' . $index->api_request);
-
-  # Search non-blocking
-  if ($cb) {
-    $c->user->auth_request(
-      get => $url => sub {
-        my $tx = pop;
-        $self->_process_response('matches', $index, $tx);
-        weaken $index;
-        return $cb->($index);
-      });
-  }
-
-  # Search blocking
-  else {
-    my $tx = $c->user->auth_request(get => $url);
-    $self->_process_response('matches', $index, $tx);
-    return $index;
-  };
-};
-
-
-# Trace query serialization
-sub trace {
-  my $self = shift;
-  my $index = shift;
-
-  # Get controller
-  my $c = $index->controller;
-
-  # If there is a callback, do async
-  my $cb = pop if ref $_[-1] && ref $_[-1] eq 'CODE';
-
-  my %param = @_;
-
-  # No query defined
-  unless ($index->query(delete $param{query})) {
-    return $cb->($index) if $cb;
-    return;
-  };
-
-  # Get query url
-  my $url = _query_url($index, @_);
-
-  $url->path('search');
-
-  # Create new user agent and set timeout to 30 seconds
-  my $ua = $c->user->ua; # Mojo::UserAgent->new;
-  $ua->inactivity_timeout(30);
-
-  # Build transaction
-  my $tx = $ua->build_tx(TRACE => $url);
-
-  # non-blocking
-  if ($cb) {
-    weaken $index;
-
-    # Trace non-blocking
-    $ua->start(
-      $tx => sub {
-        $self->_process_response('trace', $index, pop);
-        return $cb->($index);
-      });
-  }
-  # Trace blocking
-  else {
-    my $tx = $ua->start($url);
-    return $self->_process_response('trace', $index, $tx);
-  };
-};
-
-
-# Get match info
-sub match {
-  my $self = shift;
-  my $index = shift;
-
-  # Get controller
-  my $c = $index->controller;
-
-  # If there is a callback, do async
-  my $cb = pop if ref $_[-1] && ref $_[-1] eq 'CODE';
-
-  my %param = @_;
-
-  my $url = Mojo::URL->new($index->api);
-
-  # Legacy: In old versions, doc_id contained text_id
-  # $param{doc_id} .= '.' . $param{text_id} if $param{text_id};
-
-  # Use hash slice to create path
-  $url->path(join('/', 'corpus', @param{qw/corpus_id doc_id text_id match_id/}, 'matchInfo'));
-
-  # Build match id
-  # $match = 'match-' . $corpus . '!' . $corpus . '_' . $doc . '-' . $match;
-
-  my %query;
-  $query{foundry} = $param{foundry};
-  $query{layer}   = $param{layer}   if defined $param{layer};
-  $query{spans}   = $param{spans} ? 'true' : 'false';
-
-  # Add query
-  $url->query(\%query);
-
-  $c->app->log->debug('Match info: ' . $url);
-
-  # Create new user agent and set timeout to 30 seconds
-  #  my $ua = $c->user->ua; # Mojo::UserAgent->new;
-  #  $ua->inactivity_timeout(30);
-
-  # non-blocking
-  if ($cb) {
-    # $c->u
-    $c->user->auth_request(get =>
-    # $ua->get(
-      $url => sub {
-        my $tx = pop;
-        $self->_process_response('match', $index, $tx);
-        weaken $index;
-        return $cb->($index);
-      });
-  }
-
-  # Match info blocking
-  else {
-    my $tx = $c->user->auth_request(get => $url);
-    # my $tx = $ua->get($url);
-    return $self->_process_response('match', $index, $tx);
-  };
-};
-
-
-# Get text info
-sub text {
-  my $self = shift;
-  my $index = shift;
-
-  # Get controller
-  my $c = $index->controller;
-
-  # If there is a callback, do async
-  my $cb = pop if ref $_[-1] && ref $_[-1] eq 'CODE';
-
-  my %param = @_;
-
-  my $url = Mojo::URL->new($index->api);
-
-  # Use hash slice to create path
-  $url->path(join('/', 'corpus', @param{qw/corpus_id doc_id text_id/}));
-
-  my %query;
-  $query{fields} = $param{fields};
-
-  # Add query
-  $url->query(\%query);
-
-  $c->app->log->debug('Text info: ' . $url);
-
-  # non-blocking
-  if ($cb) {
-    $c->user->auth_request(
-      get =>
-        $url => sub {
-          my $tx = pop;
-          $self->_process_response('text', $index, $tx);
-          weaken $index;
-          return $cb->($index);
-        });
-  }
-
-  # Text info blocking
-  else {
-    my $tx = $c->user->auth_request(get => $url);
-    return $self->_process_response('text', $index, $tx);
-  };
-};
-
-
-# Get corpus statistics
-sub statistics {
-  my $self = shift;
-  my $index = shift;
-
-  # Get controller
-  my $c = $index->controller;
-
-  # If there is a callback, do async
-  my $cb = pop if ref $_[-1] && ref $_[-1] eq 'CODE';
-
-  my %param = @_;
-
-  my $url = Mojo::URL->new($index->api);
-
-  # Use hash slice to create path
-  $url->path('statistics');
-
-  my %query;
-  $query{corpusQuery} = $param{cq};
-
-  # Add query
-  $url->query(\%query);
-
-  $c->stash('search._resource_cache' => $url->to_string);
-
-  $c->app->log->debug('Statistics info: ' . $url);
-
-  # non-blocking
-  if ($cb) {
-    $c->user->auth_request(
-      get =>
-        $url => sub {
-          my $tx = pop;
-          $self->_process_response('resource', $index, $tx);
-          weaken $index;
-          return $cb->($index);
-        });
-  }
-
-  # Statistics info blocking
-  else {
-    my $tx = $c->user->auth_request(get => $url);
-    return $self->_process_response('resource', $index, $tx);
-  };
-};
-
-
-
-# Get resource information
-sub resource {
-  my $self = shift;
-  my $index = shift;
-
-  # Get controller
-  my $c = $index->controller;
-
-  my $user = $c->stash('user') // 'not_logged_in';
-
-  # If there is a callback, do async
-  my $cb = pop if ref $_[-1] && ref $_[-1] eq 'CODE';
-
-  my %param = @_;
-
-  # Rename info endpoints regarding resource
-  my $type = $param{type} // 'collection';
-  $type = 'virtualcollection' if $type eq 'collection';
-
-  # Create resource URL
-  my $url = Mojo::URL->new($index->api)->path($type);
-
-  # Debugging
-  $c->app->log->debug('Get resource info on '. $url);
-
-  # Check for cached information
-  if (my $json = $c->chi->get($user . $url->to_string)) {
-
-    # TODO: That's unfortunate, as it prohibits caching of multiple resources
-    $c->app->log->debug('Get resource info from cache');
-    $c->stash('search.resource' => $json);
-    return $cb->($index) if $cb;
-    return $json;
-  };
-
-  $c->stash('search._resource_cache' => $url->to_string);
-
-  # Create new user agent and set timeout to 30 seconds
-  #my $ua = $c->ua; # Mojo::UserAgent->new;
-  #$ua->inactivity_timeout(30);
-
-  # Get resource information async
-  if ($cb) {
-    $c->user->auth_request(get =>
-      $url => sub {
-        $self->_process_response('resource', $index, pop);
-        weaken $index;
-        return $cb->($index);
-      })
-  }
-
-  # Get resource information blocking
-  else {
-    my $tx = $c->user->auth_request(get => $url);
-    $self->_process_response('resource', $index, $tx);
-  };
-};
-
-
-# Process response - especially error messages etc.
-sub _process_response {
-  my ($self, $type, $index, $tx) = @_;
-  my $c = $index->controller;
-
-  my $json;
-  my $res = $tx->res;
-
-  # Json failure
-  unless ($json = $res->json) {
-    $c->notify(error => 'JSON response is invalid');
-    $index->status(0);
-    return;
-  };
-
-  # Set api response as jsonld
-  $index->api_response($json);
-
-  # An error has occurded
-  if (my $e = $tx->error) {
-
-    # Send error
-    $self->_notify_on_error($c, 1, $tx->res);
-
-    # $c->notify(
-    # error =>
-    # ($e->{code} ? $e->{code} . ': ' : '') .
-    # $e->{message} . ' for ' . $type . ' (remote)'
-    # );
-    $index->status($e->{code} // 0);
-    return;
-  };
-
-
-  # Response was fine
-  if ($res->is_success) {
-
-    # expected response for matches
-    if ($type eq 'matches') {
-      $self->_process_response_matches($index, $json);
-    }
-    elsif ($type eq 'trace') {
-      $self->_process_response_trace($index, $json);
-    }
-    elsif ($type eq 'match') {
-      $self->_process_response_match($index, $json);
-    }
-    elsif ($type eq 'resource') {
-      $self->_process_response_resource($index, $json);
-    }
-    elsif ($type eq 'text') {
-      $self->_process_response_text($index, $json);
-    };
-
-    return 1 if ref $json ne 'HASH';
-
-    $self->_notify_on_warnings($c, $json);
-    $self->_notify_on_error($c, 0, $json);
-  }
-
-  # Request failed
-  else {
-    $index->status(0);
-    $self->_notify_on_error($c, 1, $tx->res);
-  };
-  return 1;
-};
-
-
-# Handle match results
-sub _process_response_matches {
-  my ($self, $index, $json) = @_;
-
-  # Process meta
-  my $meta = $json->{meta};
-
-  # Reformat benchmark counter
-  my $benchmark = $meta->{benchmark};
-  if ($benchmark && $benchmark =~ s/\s+(m)?s$//) {
-    $benchmark = sprintf("%.2f", $benchmark) . ($1 ? $1 : '') . 's';
-  };
-
-  # Set benchmark
-  $index->benchmark($benchmark);
-
-  # Set time exceeded
-  if ($meta->{timeExceeded} && $meta->{timeExceeded} eq Mojo::JSON::true) {
-    $index->time_exceeded(1);
-  };
-
-  # Set result values
-  $index->items_per_page($meta->{itemsPerPage});
-
-  # Set authorization
-  $index->authorized($meta->{authorized}) if $meta->{authorized};
-
-  # Bouncing query
-  #  if ($json->{query}) {
-  #    $index->query_jsonld($json->{query});
-  #  };
-
-  # Legacy
-  # elsif ($json->{request}->{query}) {
-  #   $index->query_jsonld($json->{request}->{query});
-  # };
-
-  # Bouncing collection query
-  if ($json->{collection}) {
-    $index->collection_jsonld($json->{collection});
-  }
-
-  # Legacy
-  # elsif ($json->{request}->{collection}) {
-  #  $index->collection_jsonld($json->{request}->{collection});
-  # };
-
-  $index->results(_map_matches($json->{matches}));
-
-  # Total results not set by stash
-  if ($index->total_results == -1) {
-
-    if ($meta->{totalResults} && $meta->{totalResults} > -1) {
-      my $c = $index->controller;
-
-      # TODO: Cache on auth_keys!
-      my $user = $c->stash('user') // 'not_logged_in';
-
-      $c->app->log->debug('Cache total result');
-      $c->chi->set($user . $index->_api_cache => $meta->{totalResults}, '120min');
-      $index->total_results($meta->{totalResults});
-    };
-  };
-};
-
-
-# Process query serialization response
-sub _process_response_match {
-  my ($self, $index, $json) = @_;
-  $index->results(_map_match($json));
-};
-
-
-# Process query serialization response
-sub _process_response_text {
-  my ($self, $index, $json) = @_;
-  $index->results($json);
-};
-
-
-# Process trace response
-sub _process_response_trace {
-  my ($self, $index, $json) = @_;
-  $index->query_jsonld($json);
-};
-
-
-# Process resource response
-sub _process_response_resource {
-  my ($self, $index, $json) = @_;
-  my $c = $index->controller;
-
-  my $user = $c->stash('user') // 'not_logged_in';
-
-  # TODO: That's unfortunate, as it prohibits multiple resources
-  $c->stash('search.resource' => $json);
-  $c->app->log->debug('Cache resource info');
-  $c->chi->set($user . $c->stash('search._resource_cache') => $json, '24 hours');
-};
-
-
-# Parse error messages and forward them to the user
-sub _notify_on_error {
-  my ($self, $c, $failure, $res) = @_;
-  my $json = $res;
-
-  my $log = $c->app->log;
-
-  # Check if the response is already json
-  if (blessed $res) {
-    $json = $res->json if blessed $res ne 'Mojo::JSON';
-  };
-
-  # Check json response error message
-  if ($json) {
-
-    # Legacy, but still in use by Kustvakt
-    if ($json->{error}) {
-
-      # Temp
-      $json->{error} =~ s/;\s+null$//;
-      $c->notify(error => $json->{error});
-      return;
-    }
-
-    # New error messages
-    elsif ($json->{errstr}) {
-      # Temp
-      $json->{errstr} =~ s/;\s+null$//;
-      $c->notify(error => $json->{errstr});
-      return;
-    }
-
-    elsif ($json->{errors}) {
-      my $errors = $json->{errors};
-      # TODO: Check for ref!
-      foreach (@$errors) {
-        $c->notify(
-          error =>
-            ($_->[0] ? $_->[0] . ': ' : '') .
-            ($_->[1] || 'Unknown')
-          );
-      };
-    }
-
-    # policy service error messages
-    elsif ($json->{status}) {
-      $c->notify(error => 'Middleware error ' . $json->{status});
-      return;
-    };
-  };
-
-  # Doesn't matter what - there is a failure!
-  if ($failure) {
-    $c->notify(error => (
-      ($res->{code}    ? $res->{code} . ': ' : '') .
-      ($res->{message} ? $res->{message}     : 'Unknown error') .
-      ' (remote)'
-    ));
-  };
-};
-
-
-sub _notify_on_warnings {
-  my ($self, $c, $json) = @_;
-
-  # Add warnings (Legacy)
-  if ($json->{warning}) {
-    $json->{warning} =~ s/;\s+null$//;
-    $c->notify(warn => $json->{warning});
-  }
-
-  # Add warnings
-  elsif ($json->{warnings}) {
-
-    my $warnings = $json->{warnings};
-    # TODO: Check for ref!
-    foreach (@$warnings) {
-      $c->notify(
-        warn =>
-          ($_->[0] ? $_->[0] . ': ' : '') .
-          $_->[1]
-        );
-    };
-  };
-};
-
-
-# Cleanup array of matches
-sub _map_matches {
-  return () unless $_[0];
-  map { _map_match($_) } @{ shift() };
-};
-
-
-# Cleanup single match
-sub _map_match {
-  my $x = shift or return;
-
-  # legacy match id
-  if ($x->{matchID}) {
-    $x->{matchID} =~ s/^match\-(?:[^!]+!|[^_]+_)[^\.]+?\.[^-]+?-// or
-      $x->{matchID} =~ s!^match\-(?:[^\/]+\/){2}[^-]+?-!!;
-  };
-
-  (
-    $x->{corpusID},
-    $x->{docID},
-    $x->{textID}
-  ) = ($x->{textSigle} =~ /^([^_]+?)_+([^\.]+?)\.(.+?)$/);
-
-  # $x->{docID} =~ s/^[^_]+_//;
-  # Legacy: In old versions the text_id was part of the doc_id
-  #  unless ($x->{textID}) {
-  #    ($x->{docID}, $x->{textID}) = split '\.', $x->{docID};
-  #  };
-
-  $x;
-};
-
-# Build query url
-sub _query_url {
-  my ($index, %param) = @_;
-
-  # Set cutoff from param
-  $index->cutoff(delete $param{cutoff});
-
-  # Set collection from param
-  $index->collection(delete $param{collection});
-
-  # Set query language
-  $index->query_language(delete $param{query_language} // 'poliqarp');
-
-  # Should results be cached? Defaults to "yes"
-  $index->no_cache(1) if $param{no_cache};
-
-  # Init the query with stuff coming from the index
-  my %query;
-  $query{q}      = $index->query;
-  $query{ql}     = $index->query_language;
-  $query{page}   = $index->start_page if $index->start_page;
-  $query{count}  = $index->items_per_page if $index->items_per_page;
-  $query{cq}     = $index->collection if $index->collection;
-  $query{cutoff} = 'true' if $index->cutoff;
-
-  # Create query url
-  my $url = Mojo::URL->new($index->api);
-  $url->query(\%query);
-  return $url;
-};
-
-
-1;
-
-
-__END__
-
-=pod
-
-=encoding utf8
-
-=head1 NAME
-
-Kalamar::API
-
-=head1 DESCRIPTION
-
-L<Kalamar::API> is a search engine class for L<Mojolicious::Plugin::Search>
-that uses the KorAP Web API.
-
-B<The Web API as well as L<Mojolicious::Plugin::Search> are not stable yet,
-so this class is expected to change in the near future. Do not rely on its API!>
-
-
-=head1 METHODS
-
-L<Kalamar::API> inherits all methods from L<Mojolicious::Plugin> and
-implements the following new ones.
-
-
-=head2 register
-
-See L<Mojolicious::Plugin::Search> for registering search engines.
-In addition to the mentioned query parameters, the following parameters are supported:
-
-
-=over 2
-
-=item B<query_language>
-
-One of the supported query languages, like C<poliqarp> or C<annis>.
-
-
-=item B<cutoff>
-
-Cut off results following the current page (i.e. don't count the number of results).
-
-
-=item B<no_cache>
-
-Do not cache search results. Defaults to C<0>.
-
-
-=back
-
-In addition to the mentioned index attributes, the following attributes are supported:
-
-=over 2
-
-=item B<api>
-
-The API address.
-
-
-=item B<time_exceeded>
-
-Report on time outs, that may mean, not all results were retrieved.
-
-
-=item B<api_request>
-
-Report the whole API request.
-
-
-=item B<api_response>
-
-Report the whole API response (a KoralQuery object).
-
-
-=item B<benchmarks>
-
-Report on processing time for benchmarking.
-
-
-=item B<query_jsonld>
-
-The KoralQuery realization of the C<query> object.
-
-=back
-
-=head2 search
-
-Search the index.
-
-=head2 trace
-
-Trace query serializations.
-
-=head2 match
-
-Get match information.
-
-=head2 resource
-
-Get resource information.
-
-
-=head1 COPYRIGHT AND LICENSE
-
-Copyright (C) 2015-2018, L<IDS Mannheim|http://www.ids-mannheim.de/>
-Author: L<Nils Diewald|http://nils-diewald.de/>
-
-Kalamar is developed as part of the L<KorAP|http://korap.ids-mannheim.de/>
-Corpus Analysis Platform at the
-L<Institute for the German Language (IDS)|http://ids-mannheim.de/>,
-member of the
-L<Leibniz-Gemeinschaft|http://www.leibniz-gemeinschaft.de/en/about-us/leibniz-competition/projekte-2011/2011-funding-line-2/>
-and supported by the L<KobRA|http://www.kobra.tu-dortmund.de> project,
-funded by the
-L<Federal Ministry of Education and Research (BMBF)|http://www.bmbf.de/en/>.
-
-Kalamar is free software published under the
-L<BSD-2 License|https://raw.githubusercontent.com/KorAP/Kalamar/master/LICENSE>.
-
-=cut
diff --git a/lib/Kalamar/Controller/Search.pm b/lib/Kalamar/Controller/Search.pm
index 968402d..2359274 100644
--- a/lib/Kalamar/Controller/Search.pm
+++ b/lib/Kalamar/Controller/Search.pm
@@ -1,230 +1,464 @@
 package Kalamar::Controller::Search;
 use Mojo::Base 'Mojolicious::Controller';
-use Mojo::Util qw/deprecated/;
+use Mojo::Collection 'c';
+use Mojo::ByteStream 'b';
+use POSIX 'ceil';
+
+has no_cache => 0;
+
+has items_per_page => 25;
+
+# TODO:
+#   Support server timing API
+
+# TODO:
+#   Add match_info template for HTML
+#
+# TODO:
+#   Support search in corpus and virtualcollection
+#
+# TODO:
+#   set caches with timing like '120min'
 
 
-# Query the KorAP backends and render a template
+
+# Query endpoint
 sub query {
   my $c = shift;
+
+  # Validate user input
   my $v = $c->validation;
 
-  $v->optional('q');
-  $v->optional('ql');
-  $v->optional('collection');
-  $v->optional('action');
-  $v->optional('snippet');
-  $v->optional('cutoff');
-  $v->optional('count');
-  $v->optional('p');
+  $v->optional('q', 'trim');
+  $v->optional('ql')->in(qw/poliqarp cosmas2 annis cql fcsql/);
+  $v->optional('collection', 'trim'); # Legacy
+  $v->optional('cq', 'trim');
+  # $v->optional('action'); # action 'inspect' is no longer valid
+  # $v->optional('snippet');
+  $v->optional('cutoff')->in(qw/true false/);
+  $v->optional('count')->num(1, undef);
+  $v->optional('p', 'trim')->num(1, undef); # Start page
+  $v->optional('o', 'trim')->num(1, undef); # Offset
+  $v->optional('context');
 
-  #my $tx = $ua->build_tx(TRACE => $url . 'search?cq=corpusAuthor+%3D+%22Baum%22');
-  #{"@context":"http://korap.ids-mannheim.de/ns/koral/0.3/context.jsonld","errors":[[301,"You did not specify a query!"]],"collection":{"@type":"koral:doc","key":"corpusAuthor","value":"Baum","match":"match:eq"}}
-
+  # Get query
   my $query = $v->param('q');
 
+  # TODO:
+  #   Check for validation errors!
+
   # No query
   unless ($query) {
     return $c->render($c->loc('Template_intro', 'intro'));
   };
 
-  # Base parameters for remote access
-  my %param = (
-    query_language => scalar $v->param('ql'),
-    query => $query,
-    collection => scalar $v->param('collection')
-  );
+  my %query = ();
+  $query{q}       = $query;
+  $query{ql}      = $v->param('ql') // 'poliqarp';
+  $query{count}   = $v->param('count') // $c->items_per_page;
+  $query{cq}      = $v->param('cq') // $v->param('collection');
+  $query{cutoff}  = $v->param('cutoff');
+  # Before: 'base/s:p'/'paragraph'
+  $query{context} = $v->param('context') // '40-t,40-t';
 
-  # May be not relevant
-  my $inspect = (scalar $v->param('action') // '') eq 'inspect' ? 1 : 0;
+  # Start page
+  my $page = $v->param('p') // 1;
 
-  # Just check the serialization non-blocking
-  if ($inspect) {
-    $c->search->trace(
-      %param => sub {
-        return $c->render(template => 'query_info');
-      }
-    );
-    return;
+  $c->stash(query => $query);
+  $c->stash(ql => $query{ql});
+
+  my $items_per_page = $c->items_per_page;
+
+  # Set count
+  if ($query{count} && $query{count} <= $c->items_per_page ) {
+    $items_per_page = delete $query{count};
+    $query{count} = $items_per_page;
   };
 
-  # Choose the snippet based on the parameter
-  my $template = scalar $v->param('snippet') ? 'snippet' : 'search';
-
-  # Search non-blocking
-  my $tx = $c->render_later->tx;
+  $c->stash(items_per_page => $items_per_page);
 
   # TODO:
-  #   This should be simplified to use Promises only
-  Mojo::IOLoop->delay(
+  #   if ($v->param('action') eq 'inspect') use trace!
+
+  # Set offset
+  # From Mojolicious::Plugin::Search::Index
+  $query{offset} = $v->param('o') || ((($page // 1) - 1) * ($items_per_page || 1));
+
+  # Create remote request URL
+  my $url = Mojo::URL->new($c->korap->api);
+  $url->path('search');
+  # $url->query(%query);
+  $url->query(map { $_ => $query{$_}} sort keys %query);
+
+  # In case the user is not known, it is assumed, the user is not logged in
+  my $total_cache_str;
+
+  # Check if total results information is cached
+  my $total_results = -1;
+  unless ($c->no_cache) {
+
+    # Create cache string
+    my $user = $c->user->handle;
+    my $cache_url = $url->clone;
+    $cache_url->query->remove('context')->remove('count')->remove('cutoff')->remove('offset');
+    $total_cache_str = "total-$user-" . $cache_url->to_string;
+
+    $c->app->log->debug('Check for total results: ' . $total_cache_str);
+
+    # Get total results value
+    $total_results = $c->chi->get($total_cache_str);
+
+    # Set stash if cache exists
+    if (defined $total_results) {
+      $c->stash(total_results => $total_results);
+
+      $c->app->log->debug('Get total result from cache: ' . $total_results);
+
+      # Set cutoff unless already set
+      $url->query({cutoff => 'true'});
+    };
+  };
+
+  # Wait for rendering
+  $c->render_later;
+
+  # Fetch resource
+  $c->cached_koral_p('get', $url)->then(
+
+    # Process response
     sub {
-      my $delay = shift;
+      my $json = shift;
 
-      # Search with a callback (async)
-      $c->search(
-        cutoff     => scalar $v->param('cutoff'),
-        count      => scalar $v->param('count'),
-        start_page => scalar $v->param('p'),
-        cb         => $delay->begin,
-        %param
-      ) if $query;
+      #######################
+      # Cache total results #
+      #######################
+      # The stash is set in case the total results value is from the cache,
+      # so in that case, it does not need to be cached again
+      my $total_results = $c->stash('total_results');
 
-      # Search resource (async)
-      # $c->search->resource(
-      #   type => 'collection',
-      #   $delay->begin
-      # );
-    },
+      unless (defined $total_results) {
 
-    # Collected search
-    sub {
+        # There are results to remember
+        if ($json->{meta}->{totalResults} >= 0) {
 
-      # Render to the template
-      return $c->render(template => $template);
+          # Remove cutoff requirement again
+          # $url->query([cutoff => 'true']);
+
+          $total_results = $json->{meta}->{totalResults};
+          $c->stash(total_results => $total_results);
+
+          $c->app->log->debug('Set for total results: ' . $total_cache_str);
+
+          # Set cache
+          $c->chi->set($total_cache_str => $total_results);
+        }
+
+        # Undefined total results
+        else {
+          $c->stash(total_results => -1);
+        };
+      };
+
+
+      $c->stash(total_pages => 0);
+
+      # Set total pages
+      # From Mojolicious::Plugin::Search::Index
+      if ($total_results > 0) {
+        $c->stash(
+          total_pages => ceil($total_results / ($c->stash('items_per_page') || 1))
+        );
+      };
+
+      # Process meta
+      my $meta = $json->{meta};
+
+      # TODO:
+      #   Set benchmark in case of development mode only.
+      #   Use server timing API
+      #
+      # Reformat benchmark counter
+      # my $benchmark = $meta->{benchmark};
+      # if ($benchmark && $benchmark =~ s/\s+(m)?s$//) {
+      #   $benchmark = sprintf("%.2f", $benchmark) . ($1 ? $1 : '') . 's';
+      # };
+      #
+      # # Set benchmark
+      # $self->stash(benchmark => $benchmark);
+
+      # Set time exceeded
+      if ($meta->{timeExceeded} &&
+            $meta->{timeExceeded} eq Mojo::JSON::true) {
+        $c->stash(time_exceeded => 1);
+      };
+
+      # Set result values
+      $c->stash(items_per_page => $meta->{itemsPerPage});
+
+      # Bouncing collection query
+      if ($json->{corpus} || $json->{collection}) {
+        $c->stash(corpus_jsonld => ($json->{corpus} || $json->{collection}));
+      };
+
+      # TODO:
+      #   scalar $v->param('snippet') ? 'snippet' : 'search';
+
+      # Render result
+      return $c->render(
+        q => $c->stash('query'),
+        ql => $c->stash('ql'),
+        start_page => $page,
+        start_index => $json->{meta}->{startIndex},
+        results => _map_matches($json->{matches}),
+        template => 'search'
+      );
     }
-  )->catch(sub { $c->helpers->reply->exception(pop) and undef $tx })->wait;
+
+      # Deal with errors
+  )->catch(
+    sub {
+      my $err_msg = shift;
+
+      # Only raised in case of connection errors
+      if ($err_msg) {
+        $c->stash('err_msg' => 'backendNotAvailable');
+        $c->notify(error => { src => 'Backend' } => $err_msg)
+      };
+
+      # $c->_notify_on_errors(shift);
+      return $c->render(
+        q => $c->stash('query'),
+        ql => $c->stash('ql'),
+        template => 'failure'
+      );
+    }
+  )
+
+  # Start IOLoop
+  ->wait;
+
+  return 1;
 };
 
 
-# Get meta data of a text
+# Corpus info endpoint
+# This replaces the collections endpoint
+sub corpus_info {
+  my $c = shift;
+
+  # Input validation
+  my $v = $c->validation;
+  $v->optional('cq');
+
+  my $url = Mojo::URL->new($c->korap->api);
+
+  # Use hash slice to create path
+  $url->path('statistics');
+
+  # Add query
+  $url->query(corpusQuery => $v->param('cq'));
+
+  $c->app->log->debug("Statistics info: $url");
+
+  # Async
+  $c->render_later;
+
+  # Request koral, maybe cached
+  $c->cached_koral_p('get', $url)
+
+  # Process response
+  ->then(
+    sub {
+      my $json = shift;
+      return $c->render(
+        json => $c->notifications(json => $json),
+        status => 200
+      );
+    }
+  )
+
+  # Deal with errors
+  ->catch(
+    sub {
+      return $c->render(
+        json => $c->notifications('json')
+      )
+    }
+  )
+
+  # Start IOLoop
+  ->wait;
+
+  return 1;
+};
+
+
+# Text info endpoint
 sub text_info {
   my $c = shift;
 
-  my %query = (fields => '*');
-  if ($c->param('fields')) {
-    $query{fields} = $c->param('fields')
-  };
+  # Input validation
+  my $v = $c->validation;
+  $v->optional('fields');
 
+  my %query = (fields => '@all');
+  $query{fields} = $v->param('fields') if $v->param('fields');
+
+  my $url = Mojo::URL->new($c->korap->api);
+
+  # Use hash slice to create path
+  $url->path(
+    join('/', (
+      'corpus',
+      $c->stash('corpus_id'),
+      $c->stash('doc_id'),
+      $c->stash('text_id')
+    ))
+  );
+  $url->query(%query);
+
+  # Async
   $c->render_later;
 
-  # Use the API for fetching matching information non-blocking
-  $c->search->text(
-    corpus_id => $c->stash('corpus_id'),
-    doc_id    => $c->stash('doc_id'),
-    text_id   => $c->stash('text_id'),
-    %query,
+  # Request koral, maybe cached
+  $c->cached_koral_p('get', $url)
 
-    # Callback for async search
+  # Process response
+  ->then(
     sub {
-      my $index = shift;
-      return $c->respond_to(
-
-        # Render json if requested
-        json => sub {
-          # Add notifications to the matching json
-          # TODO: There should be a special notification engine doing that!
-          my $notes = $c->notifications(json => $index->results->[0]);
-          $c->render(
-            json => $notes,
-            status => $index->status
-          );
-        }
+      my $json = shift;
+      return $c->render(
+        json => $c->notifications(json => $json),
+        status => 200
       );
     }
-  );
+  )
+
+  # Deal with errors
+  ->catch(
+    sub {
+      return $c->render(
+        json => $c->notifications('json')
+      )
+    }
+  )
+
+  # Start IOLoop
+  ->wait;
+
+  return 1;
 };
 
 
-
-# Get information about a match
-# Note: This is called 'match_info' as 'match' is reserved
+# Match info endpoint
 sub match_info {
   my $c = shift;
 
+  # Validate user input
+  my $v = $c->validation;
+  $v->optional('foundry');
+  $v->optional('layer');
+  $v->optional('spans')->in(qw/true false/);
+
   # Old API foundry/layer usage
   my $foundry = '*';
   my %query = (foundry => '*');
-  if ($c->param('foundry')) {
-    $query{foundry} = scalar $c->param('foundry');
-    if ($c->param('layer')) {
-      $query{layer} = scalar $c->param('layer');
-    };
-    if ($c->param('spans')) {
-      $query{spans} = 'true';
-    };
+  if ($v->param('foundry')) {
+    $query{foundry} = $v->param('foundry');
+    $query{layer} = $v->param('layer') if $v->param('layer');
+    $query{spans} = $v->param('spans') if $v->param('spans');
   };
 
-  # Async
+  # Create new request API
+  my $url = Mojo::URL->new($c->korap->api);
+
+  # Use stash information to create url path
+  $url->path(
+    join('/', (
+      'corpus',
+      $c->stash('corpus_id'),
+      $c->stash('doc_id'),
+      $c->stash('text_id'),
+      $c->stash('match_id'),
+      'matchInfo'
+    ))
+  );
+
+  # Set query parameters in order
+  $url->query(map { $_ => $query{$_}} sort keys %query);
+
   $c->render_later;
 
-  # Use the API for fetching matching information non-blocking
-  $c->search->match(
-    corpus_id => $c->stash('corpus_id'),
-    doc_id    => $c->stash('doc_id'),
-    text_id   => $c->stash('text_id'),
-    match_id  => $c->stash('match_id'),
-    %query,
-
-    # Callback for async search
+  $c->cached_koral_p('get', $url)->then(
     sub {
-      my $index = shift;
-      return $c->respond_to(
+      my $json = shift;
 
-        # Render json if requested
-        json => sub {
-          # Add notifications to the matching json
-          # TODO: There should be a special notification engine doing that!
-          my $notes = $c->notifications(json => $index->results->[0]);
-          $c->render(
-            json => $notes,
-            status => $index->status
-          );
-        },
+      # Process results
+      $json = _map_match($json);
+      $c->stash(results => $json);
 
-        # Render html if requested
-        html => sub {
-          return $c->render(
-            layout   => 'default',
-            template => 'match_info'
-          )
-        }
+      return $c->render(
+        json => $c->notifications(json => $json),
+        status => 200
       );
+
+      return $json;
     }
-  );
-};
+  )
 
-
-# Get information about collections
-sub corpus_info {
-  my $c = shift;
-  my $v = $c->validation;
-
-  $v->optional('cq');
-
-  # Async
-  $c->render_later;
-
-  $c->search->statistics(
-    cq => $v->param('cq'),
+  # Deal with errors
+  ->catch(
     sub {
-      my $notes = $c->notifications(json => $c->stash('search.resource'));
-      return $c->render(json => $notes);
+      return $c->render(
+        json => $c->notifications('json')
+      )
     }
-  );
+  )
+
+  # Start IOLoop
+  ->wait;
+
+  return 1;
 };
 
 
-sub collections {
-  my $c = shift;
-  # Async
-  $c->render_later;
-
-  deprecated 'collections() is deprecated in favour of corpus_info';
-
-  # Get resource (for all)
-  $c->search->resource(
-    type => 'collection',
-    sub {
-      my $notes = $c->notifications(json => $c->stash('search.resource'));
-      return $c->render(json => $notes);
-    }
-  );
+# Cleanup array of matches
+sub _map_matches {
+  return c() unless $_[0];
+  c(map { _map_match($_) } @{ shift() });
 };
 
+
+# Cleanup single match
+sub _map_match {
+  my $match = shift or return;
+
+  # Legacy match id
+  if ($match->{matchID}) {
+    $match->{matchID} =~ s/^match\-(?:[^!]+!|[^_]+_)[^\.]+?\.[^-]+?-// or
+      $match->{matchID} =~ s!^match\-(?:[^\/]+\/){2}[^-]+?-!!;
+  };
+
+  return unless $match->{textSigle};
+
+  # Set IDs based on the sigle
+  (
+    $match->{corpusID},
+    $match->{docID},
+    $match->{textID}
+  ) = ($match->{textSigle} =~ /^([^_]+?)_+([^\.]+?)\.(.+?)$/);
+
+  return $match;
+};
+
+
 1;
 
 
 __END__
 
+__END__
+
 =pod
 
 =encoding utf8
@@ -306,12 +540,42 @@
 
 B<This parameter is directly forwarded to the API and may not be supported in the future.>
 
+=item B<o>
+
+If set, the matches will offset to the given match in the result set.
+Will default to 0.
+
+B<This parameter is directly forwarded to the API and may not be supported in the future.>
+
+=item B<context>
+
+The context of the snippets to retrieve. Defaults to C<40-t,40-t>.
+
+B<This parameter is directly forwarded to the API and may not be supported in the future.>
+
+=item B<cq>
+
+The corpus query to limit the search to.
+
 =back
 
 
+=head2 corpus
+
+  /corpus?cq=corpusSigle+%3D+%22GOE%22
+
+Returns statistics information for a virtual corpus.
+
+=head2 text
+
+  /corpus/:corpus_id/:doc_id/:text_id
+
+Returns meta data information for a specific text.
+
+
 =head2 match
 
-  /:corpus_id/:doc_id/:text_id/:match_id?foundry=*
+  /corpus/:corpus_id/:doc_id/:text_id/:match_id?foundry=*
 
 Returns information to a match either as a C<JSON> or an C<HTML> document.
 The path defines the concrete match, by corpus identifier, document identifier,
@@ -348,33 +612,6 @@
 
 =back
 
-In addition to the given parameters, the following path values are expected.
-
-=over 2
-
-=item B<corpus_id>
-
-The corpus sigle as defined by DeReKo.
-
-
-=item B<doc_id>
-
-The document sigle as defined by DeReKo.
-
-
-=item B<text_id>
-
-The text sigle as defined by DeReKo.
-
-
-=item B<match_id>
-
-The ID of the match, normally generated by the search backend.
-This contains the span of the match in the text and possibly further
-information (like highlights).
-
-=back
-
 
 =head1 COPYRIGHT AND LICENSE
 
diff --git a/lib/Kalamar/Controller/Search2.pm b/lib/Kalamar/Controller/Search2.pm
deleted file mode 100644
index f071fbf..0000000
--- a/lib/Kalamar/Controller/Search2.pm
+++ /dev/null
@@ -1,634 +0,0 @@
-package Kalamar::Controller::Search2;
-use Mojo::Base 'Mojolicious::Controller';
-use Data::Dumper;
-use Mojo::Collection 'c';
-use Mojo::ByteStream 'b';
-use POSIX 'ceil';
-
-has no_cache => 0;
-
-has items_per_page => 25;
-
-# TODO:
-#   Support server timing API
-
-# TODO:
-#   Add match_info template for HTML
-#
-# TODO:
-#   Support search in corpus and virtualcollection
-#
-# TODO:
-#   set caches with timing like '120min'
-
-
-
-# Query endpoint
-sub query {
-  my $c = shift;
-
-  # Validate user input
-  my $v = $c->validation;
-
-  $v->optional('q', 'trim');
-  $v->optional('ql')->in(qw/poliqarp cosmas2 annis cql fcsql/);
-  $v->optional('collection', 'trim'); # Legacy
-  $v->optional('cq', 'trim');
-  # $v->optional('action'); # action 'inspect' is no longer valid
-  # $v->optional('snippet');
-  $v->optional('cutoff')->in(qw/true false/);
-  $v->optional('count')->num(1, undef);
-  $v->optional('p', 'trim')->num(1, undef); # Start page
-  $v->optional('o', 'trim')->num(1, undef); # Offset
-  $v->optional('context');
-
-  # Get query
-  my $query = $v->param('q');
-
-  # TODO:
-  #   Check for validation errors!
-
-  # No query
-  unless ($query) {
-    return $c->render($c->loc('Template_intro', 'intro'));
-  };
-
-  my %query = ();
-  $query{q}       = $query;
-  $query{ql}      = $v->param('ql') // 'poliqarp';
-  $query{count}   = $v->param('count') // $c->items_per_page;
-  $query{cq}      = $v->param('cq') // $v->param('collection');
-  $query{cutoff}  = $v->param('cutoff');
-  # Before: 'base/s:p'/'paragraph'
-  $query{context} = $v->param('context') // '40-t,40-t';
-
-  # Start page
-  my $page = $v->param('p') // 1;
-
-  $c->stash(query => $query);
-  $c->stash(ql => $query{ql});
-
-  my $items_per_page = $c->items_per_page;
-
-  # Set count
-  if ($query{count} && $query{count} <= $c->items_per_page ) {
-    $items_per_page = delete $query{count};
-    $query{count} = $items_per_page;
-  };
-
-  $c->stash(items_per_page => $items_per_page);
-
-  # TODO:
-  #   if ($v->param('action') eq 'inspect') use trace!
-
-  # Set offset
-  # From Mojolicious::Plugin::Search::Index
-  $query{offset} = $v->param('o') || ((($page // 1) - 1) * ($items_per_page || 1));
-
-  # Create remote request URL
-  my $url = Mojo::URL->new($c->korap->api);
-  $url->path('search');
-  # $url->query(%query);
-  $url->query(map { $_ => $query{$_}} sort keys %query);
-
-  # In case the user is not known, it is assumed, the user is not logged in
-  my $total_cache_str;
-
-  # Check if total results information is cached
-  my $total_results = -1;
-  unless ($c->no_cache) {
-
-    # Create cache string
-    my $user = $c->user->handle;
-    my $cache_url = $url->clone;
-    $cache_url->query->remove('context')->remove('count')->remove('cutoff')->remove('offset');
-    $total_cache_str = "total-$user-" . $cache_url->to_string;
-
-    $c->app->log->debug('Check for total results: ' . $total_cache_str);
-
-    # Get total results value
-    $total_results = $c->chi->get($total_cache_str);
-
-    # Set stash if cache exists
-    if (defined $total_results) {
-      $c->stash(total_results => $total_results);
-
-      $c->app->log->debug('Get total result from cache: ' . $total_results);
-
-      # Set cutoff unless already set
-      $url->query({cutoff => 'true'});
-    };
-  };
-
-  # Wait for rendering
-  $c->render_later;
-
-  # Fetch resource
-  $c->cached_koral_p('get', $url)->then(
-
-    # Process response
-    sub {
-      my $json = shift;
-
-      #######################
-      # Cache total results #
-      #######################
-      # The stash is set in case the total results value is from the cache,
-      # so in that case, it does not need to be cached again
-      my $total_results = $c->stash('total_results');
-
-      unless (defined $total_results) {
-
-        # There are results to remember
-        if ($json->{meta}->{totalResults} >= 0) {
-
-          # Remove cutoff requirement again
-          # $url->query([cutoff => 'true']);
-
-          $total_results = $json->{meta}->{totalResults};
-          $c->stash(total_results => $total_results);
-
-          $c->app->log->debug('Set for total results: ' . $total_cache_str);
-
-          # Set cache
-          $c->chi->set($total_cache_str => $total_results);
-        }
-
-        # Undefined total results
-        else {
-          $c->stash(total_results => -1);
-        };
-      };
-
-
-      $c->stash(total_pages => 0);
-
-      # Set total pages
-      # From Mojolicious::Plugin::Search::Index
-      if ($total_results > 0) {
-        $c->stash(
-          total_pages => ceil($total_results / ($c->stash('items_per_page') || 1))
-        );
-      };
-
-      # Process meta
-      my $meta = $json->{meta};
-
-      # TODO:
-      #   Set benchmark in case of development mode only.
-      #   Use server timing API
-      #
-      # Reformat benchmark counter
-      # my $benchmark = $meta->{benchmark};
-      # if ($benchmark && $benchmark =~ s/\s+(m)?s$//) {
-      #   $benchmark = sprintf("%.2f", $benchmark) . ($1 ? $1 : '') . 's';
-      # };
-      #
-      # # Set benchmark
-      # $self->stash(benchmark => $benchmark);
-
-      # Set time exceeded
-      if ($meta->{timeExceeded} &&
-            $meta->{timeExceeded} eq Mojo::JSON::true) {
-        $c->stash(time_exceeded => 1);
-      };
-
-      # Set result values
-      $c->stash(items_per_page => $meta->{itemsPerPage});
-
-      # Bouncing collection query
-      if ($json->{corpus} || $json->{collection}) {
-        $c->stash(corpus_jsonld => ($json->{corpus} || $json->{collection}));
-      };
-
-      # TODO:
-      #   scalar $v->param('snippet') ? 'snippet' : 'search2';
-
-      # Render result
-      return $c->render(
-        q => $c->stash('query'),
-        ql => $c->stash('ql'),
-        start_page => $page,
-        start_index => $json->{meta}->{startIndex},
-        results => _map_matches($json->{matches}),
-        template => 'search2'
-      );
-    }
-
-      # Deal with errors
-  )->catch(
-    sub {
-      my $err_msg = shift;
-
-      # Only raised in case of connection errors
-      if ($err_msg) {
-        $c->stash('err_msg' => 'backendNotAvailable');
-        $c->notify(error => { src => 'Backend' } => $err_msg)
-      };
-
-      # $c->_notify_on_errors(shift);
-      return $c->render(
-        q => $c->stash('query'),
-        ql => $c->stash('ql'),
-        template => 'failure'
-      );
-    }
-  )
-
-  # Start IOLoop
-  ->wait;
-
-  return 1;
-};
-
-
-# Corpus info endpoint
-# This replaces the collections endpoint
-sub corpus_info {
-  my $c = shift;
-
-  # Input validation
-  my $v = $c->validation;
-  $v->optional('cq');
-
-  my $url = Mojo::URL->new($c->korap->api);
-
-  # Use hash slice to create path
-  $url->path('statistics');
-
-  # Add query
-  $url->query(corpusQuery => $v->param('cq'));
-
-  $c->app->log->debug("Statistics info: $url");
-
-  # Async
-  $c->render_later;
-
-  # Request koral, maybe cached
-  $c->cached_koral_p('get', $url)
-
-  # Process response
-  ->then(
-    sub {
-      my $json = shift;
-      return $c->render(
-        json => $c->notifications(json => $json),
-        status => 200
-      );
-    }
-  )
-
-  # Deal with errors
-  ->catch(
-    sub {
-      return $c->render(
-        json => $c->notifications('json')
-      )
-    }
-  )
-
-  # Start IOLoop
-  ->wait;
-
-  return 1;
-};
-
-
-# Text info endpoint
-sub text_info {
-  my $c = shift;
-
-  # Input validation
-  my $v = $c->validation;
-  $v->optional('fields');
-
-  my %query = (fields => '@all');
-  $query{fields} = $v->param('fields') if $v->param('fields');
-
-  my $url = Mojo::URL->new($c->korap->api);
-
-  # Use hash slice to create path
-  $url->path(
-    join('/', (
-      'corpus',
-      $c->stash('corpus_id'),
-      $c->stash('doc_id'),
-      $c->stash('text_id')
-    ))
-  );
-  $url->query(%query);
-
-  # Async
-  $c->render_later;
-
-  # Request koral, maybe cached
-  $c->cached_koral_p('get', $url)
-
-  # Process response
-  ->then(
-    sub {
-      my $json = shift;
-      return $c->render(
-        json => $c->notifications(json => $json),
-        status => 200
-      );
-    }
-  )
-
-  # Deal with errors
-  ->catch(
-    sub {
-      return $c->render(
-        json => $c->notifications('json')
-      )
-    }
-  )
-
-  # Start IOLoop
-  ->wait;
-
-  return 1;
-};
-
-
-# Match info endpoint
-sub match_info {
-  my $c = shift;
-
-  # Validate user input
-  my $v = $c->validation;
-  $v->optional('foundry');
-  $v->optional('layer');
-  $v->optional('spans')->in(qw/true false/);
-
-  # Old API foundry/layer usage
-  my $foundry = '*';
-  my %query = (foundry => '*');
-  if ($v->param('foundry')) {
-    $query{foundry} = $v->param('foundry');
-    $query{layer} = $v->param('layer') if $v->param('layer');
-    $query{spans} = $v->param('spans') if $v->param('spans');
-  };
-
-  # Create new request API
-  my $url = Mojo::URL->new($c->korap->api);
-
-  # Use stash information to create url path
-  $url->path(
-    join('/', (
-      'corpus',
-      $c->stash('corpus_id'),
-      $c->stash('doc_id'),
-      $c->stash('text_id'),
-      $c->stash('match_id'),
-      'matchInfo'
-    ))
-  );
-
-  # Set query parameters in order
-  $url->query(map { $_ => $query{$_}} sort keys %query);
-
-  $c->render_later;
-
-  $c->cached_koral_p('get', $url)->then(
-    sub {
-      my $json = shift;
-
-      # Process results
-      $json = _map_match($json);
-      $c->stash(results => $json);
-
-      return $c->render(
-        json => $c->notifications(json => $json),
-        status => 200
-      );
-
-      return $json;
-    }
-  )
-
-  # Deal with errors
-  ->catch(
-    sub {
-      return $c->render(
-        json => $c->notifications('json')
-      )
-    }
-  )
-
-  # Start IOLoop
-  ->wait;
-
-  return 1;
-};
-
-
-# Cleanup array of matches
-sub _map_matches {
-  return c() unless $_[0];
-  c(map { _map_match($_) } @{ shift() });
-};
-
-
-# Cleanup single match
-sub _map_match {
-  my $match = shift or return;
-
-  # Legacy match id
-  if ($match->{matchID}) {
-    $match->{matchID} =~ s/^match\-(?:[^!]+!|[^_]+_)[^\.]+?\.[^-]+?-// or
-      $match->{matchID} =~ s!^match\-(?:[^\/]+\/){2}[^-]+?-!!;
-  };
-
-  return unless $match->{textSigle};
-
-  # Set IDs based on the sigle
-  (
-    $match->{corpusID},
-    $match->{docID},
-    $match->{textID}
-  ) = ($match->{textSigle} =~ /^([^_]+?)_+([^\.]+?)\.(.+?)$/);
-
-  return $match;
-};
-
-
-1;
-
-
-__END__
-
-__END__
-
-=pod
-
-=encoding utf8
-
-=head1 NAME
-
-Kalamar::Controller::Search
-
-
-=head1 DESCRIPTION
-
-L<Kalamar::Controller::Search> is the controller class for
-search related endpoints in Kalamar. Actions are released when routes
-match.
-
-
-=head1 METHODS
-
-L<Kalamar::Controller::Search> inherits all methods from
-L<Mojolicious::Controller> and implements the following new ones.
-
-=head2 query
-
-  GET /?q=Baum&ql=poliqarp
-
-Action for all queries to the system. Returns C<HTML> only for the moment.
-
-The following parameters are supported.
-
-
-=over 2
-
-=item B<q>
-
-The query string. This may any query written in a supported query language.
-
-
-=item B<ql>
-
-The query language. This may be any query language supported by the system,
-written as the API expects the string.
-
-
-=item B<action>
-
-May be C<inspect>. In that case, the serialized request is mirrored instead of
-processed.
-
-B<This switch is experimental and may change without warnings!>
-
-
-=item B<snippet>
-
-If set, the query is returned in the snippet view template.
-
-B<This parameter is experimental and may change without warnings!>
-
-
-=item B<cutoff>
-
-If set, the query will be cut off after the matches.
-
-B<This parameter is directly forwarded to the API and may not be supported in the future.>
-
-
-=item B<count>
-
-If set, the query will be only return the given number of matches,
-in case the API supports it. Will fallback to the default number of matches defined
-by the API or the backend.
-
-B<This parameter is directly forwarded to the API and may not be supported in the future.>
-
-
-=item B<p>
-
-If set, the query will page to the given number of pages in the result set.
-Will default to 1.
-
-B<This parameter is directly forwarded to the API and may not be supported in the future.>
-
-=item B<o>
-
-If set, the matches will offset to the given match in the result set.
-Will default to 0.
-
-B<This parameter is directly forwarded to the API and may not be supported in the future.>
-
-=item B<context>
-
-The context of the snippets to retrieve. Defaults to C<40-t,40-t>.
-
-B<This parameter is directly forwarded to the API and may not be supported in the future.>
-
-=item B<cq>
-
-The corpus query to limit the search to.
-
-=back
-
-
-=head2 corpus
-
-  /corpus?cq=corpusSigle+%3D+%22GOE%22
-
-Returns statistics information for a virtual corpus.
-
-=head2 text
-
-  /corpus/:corpus_id/:doc_id/:text_id
-
-Returns meta data information for a specific text.
-
-
-=head2 match
-
-  /corpus/:corpus_id/:doc_id/:text_id/:match_id?foundry=*
-
-Returns information to a match either as a C<JSON> or an C<HTML> document.
-The path defines the concrete match, by corpus identifier, document identifier,
-text identifier (all information as given by DeReKo), and match identifier
-(essentially the position of the match in the document, including highlight information).
-
-The following parameters are supported.
-
-
-=over 2
-
-=item B<foundry>
-
-Expects a foundry definition for retrieved information.
-If not given, returns all annotations for the match.
-If given, returns only given layer information for the defined foundry.
-
-B<This parameter is experimental and may change without warnings!>
-
-
-=item B<layer>
-
-Expects a layer definition for retrieved information.
-If not given, returns all annotations for the foundry.
-If given, returns only given layer information for the defined foundry.
-
-B<This parameter is experimental and may change without warnings!>
-
-
-=item B<spans>
-
-Boolean value - either C<true> or C<false> - indicating, whether span information
-(i.e. for tree structures) should be retrieved.
-
-=back
-
-
-=head1 COPYRIGHT AND LICENSE
-
-Copyright (C) 2015-2018, L<IDS Mannheim|http://www.ids-mannheim.de/>
-Author: L<Nils Diewald|http://nils-diewald.de/>
-
-Kalamar is developed as part of the L<KorAP|http://korap.ids-mannheim.de/>
-Corpus Analysis Platform at the
-L<Institute for the German Language (IDS)|http://ids-mannheim.de/>,
-member of the
-L<Leibniz-Gemeinschaft|http://www.leibniz-gemeinschaft.de/en/about-us/leibniz-competition/projekte-2011/2011-funding-line-2/>
-and supported by the L<KobRA|http://www.kobra.tu-dortmund.de> project,
-funded by the
-L<Federal Ministry of Education and Research (BMBF)|http://www.bmbf.de/en/>.
-
-Kalamar is free software published under the
-L<BSD-2 License|https://raw.githubusercontent.com/KorAP/Kalamar/master/LICENSE>.
-
-=cut
diff --git a/lib/Kalamar/Plugin/KalamarHelpers.pm b/lib/Kalamar/Plugin/KalamarHelpers.pm
index 6e0b228..295a04f 100644
--- a/lib/Kalamar/Plugin/KalamarHelpers.pm
+++ b/lib/Kalamar/Plugin/KalamarHelpers.pm
@@ -243,7 +243,7 @@
   # Establish 'search_results' taghelper
   # This is based on Mojolicious::Plugin::Search
   $mojo->helper(
-    search_results2 => sub {
+    search_results => sub {
       my $c = shift;
 
       # This is a tag helper for templates
diff --git a/lib/Kalamar/Plugin/KalamarUser.pm b/lib/Kalamar/Plugin/KalamarUser.pm
index 19bb2a7..4cf7642 100644
--- a/lib/Kalamar/Plugin/KalamarUser.pm
+++ b/lib/Kalamar/Plugin/KalamarUser.pm
@@ -401,10 +401,3 @@
 
 __END__
 
-# Failure
-entity {
-  "errors":[
-    [204,"authentication token is expired","eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0MSIsImlzcyI6Imh0dHA6IiwiZXhwIjoxNDUyOTY2NzAxOTYxfQ.W_rJjJ8i82Srw7MiSPRGeIBLE-rMPmSPK9BA7Dt_7Yc"]
-  ]
-}
-
diff --git a/package.json b/package.json
index 8ee1b7f..27c14f7 100755
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
   "name": "Kalamar",
   "description": "Mojolicious-based Frontend for KorAP",
   "license": "BSD-2-Clause",
-  "version": "0.29.2",
+  "version": "0.30.0",
   "pluginVersion": "0.1",
   "repository" : {
     "type": "git",
diff --git a/templates/failure.html.ep b/templates/failure.html.ep
index d9673b6..509333a 100644
--- a/templates/failure.html.ep
+++ b/templates/failure.html.ep
@@ -2,7 +2,7 @@
 
 <div id="resultinfo"><p class="found"></p></div>
 
-%= include 'query2'
+%= include 'query'
 
 <p class="no-results"><%= loc('notIssued') %></p>
 % if (stash('err_msg')) {
diff --git a/templates/query.html.ep b/templates/query.html.ep
index b3e52fd..fa9980c 100644
--- a/templates/query.html.ep
+++ b/templates/query.html.ep
@@ -1,8 +1,8 @@
 % use Mojo::JSON 'encode_json';
 
-% if (search->api_response) {
+% if (stash('api_response')) {
 %=  javascript begin
-%   my $kq_hash = search->api_response;
+%   my $kq_hash = stash('api_response');
 %   $kq_hash->{matches} = ["..."];
   KorAP.koralQuery = <%= b(encode_json($kq_hash))->decode %>;
 %   end
diff --git a/templates/query2.html.ep b/templates/query2.html.ep
deleted file mode 100644
index fa9980c..0000000
--- a/templates/query2.html.ep
+++ /dev/null
@@ -1,9 +0,0 @@
-% use Mojo::JSON 'encode_json';
-
-% if (stash('api_response')) {
-%=  javascript begin
-%   my $kq_hash = stash('api_response');
-%   $kq_hash->{matches} = ["..."];
-  KorAP.koralQuery = <%= b(encode_json($kq_hash))->decode %>;
-%   end
-% };
diff --git a/templates/search.html.ep b/templates/search.html.ep
index 21589ee..be95990 100644
--- a/templates/search.html.ep
+++ b/templates/search.html.ep
@@ -1,17 +1,17 @@
-% layout 'main', title => loc('searchtitle', q => search->query, ql => search->query_language), schematype => 'SearchResultsPage';
+% layout 'main', title => loc('searchtitle', q => stash('q'), ql => stash('ql')), schematype => 'SearchResultsPage';
 
-<div id="resultinfo" <% if (search->results->size) { %> class="found"<%} %>>
-  <div id="pagination"><%= pagination(search->start_page, search->total_pages, url_with->query(['p' => '{page}'])) =%></div>
-% my $found = search->total_results;
+<div id="resultinfo" <% if (stash('results')->size) { %> class="found"<%} %>>
+  <div id="pagination"><%= pagination(stash('start_page'), stash('total_pages'), url_with->query(['p' => '{page}'])) =%></div>
+% my $found = stash('total_results') // 0;
   <p class="found">\
 % if ($found != -1) {
 %   my $found_text = loc('numf', number => $found);
-%   if (search->time_exceeded) {
+%   if (stash('time_exceeded')) {
 %     $found_text = '> ' . $found_text;
 %   };
 <span id="total-results"><%= $found_text %></span> <%= loc('matchCount', found => $found) %>\
-%# <% if (search->benchmark) { %> (~ <%= search->benchmark %>)<% } %>
-% } elsif (search->start_index == 0 && search->results->size == 0) {
+%# <% if (stash('benchmark')) { %> (~ <%= stash('benchmark') %>)<% } %>
+% } elsif (stash('start_index') == 0 && stash('results')->size == 0) {
 <span id="total-results">0</span> <%= loc('matchCount', found => $found) %>\
 % };
 </p>
@@ -20,13 +20,13 @@
 %= include 'query'
 
 <div id="search">
-% if (search->total_results != 0 && search->results->size) {
+% if (stash('results')->size && stash('total_results') != 0) {
   <ol class="align-left">
 %=  search_results begin
 %=    include 'match', match => $_
 %   end
   </ol>
-% } elsif (search->results->size == 0) {
-<p id="no-results"><%= loc 'noMatches', q => search->query, ql => search->query_language %></p>
+% } elsif (stash('results')->size == 0) {
+<p class="no-results"><%= loc 'noMatches', q => stash('q'), ql => stash('ql') %></p>
 % }
 </div>
diff --git a/templates/search2.html.ep b/templates/search2.html.ep
deleted file mode 100644
index 907cc03..0000000
--- a/templates/search2.html.ep
+++ /dev/null
@@ -1,32 +0,0 @@
-% layout 'main', title => loc('searchtitle', q => stash('q'), ql => stash('ql')), schematype => 'SearchResultsPage';
-
-<div id="resultinfo" <% if (stash('results')->size) { %> class="found"<%} %>>
-  <div id="pagination"><%= pagination(stash('start_page'), stash('total_pages'), url_with->query(['p' => '{page}'])) =%></div>
-% my $found = stash('total_results') // 0;
-  <p class="found">\
-% if ($found != -1) {
-%   my $found_text = loc('numf', number => $found);
-%   if (stash('time_exceeded')) {
-%     $found_text = '> ' . $found_text;
-%   };
-<span id="total-results"><%= $found_text %></span> <%= loc('matchCount', found => $found) %>\
-%# <% if (stash('benchmark')) { %> (~ <%= stash('benchmark') %>)<% } %>
-% } elsif (stash('start_index') == 0 && stash('results')->size == 0) {
-<span id="total-results">0</span> <%= loc('matchCount', found => $found) %>\
-% };
-</p>
-</div>
-
-%= include 'query2'
-
-<div id="search">
-% if (stash('results')->size && stash('total_results') != 0) {
-  <ol class="align-left">
-%=  search_results2 begin
-%=    include 'match', match => $_
-%   end
-  </ol>
-% } elsif (stash('results')->size == 0) {
-<p class="no-results"><%= loc 'noMatches', q => stash('q'), ql => stash('ql') %></p>
-% }
-</div>