Improve error handling
Change-Id: I1f54cf9cd4770d6602f70036cf0e27c9ede8c893
diff --git a/Makefile.PL b/Makefile.PL
index 4293fbf..19d8bc5 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -15,7 +15,7 @@
LICENSE => 'freebsd',
PREREQ_PM => {
'Mojolicious' => '8.02',
- 'Mojolicious::Plugin::TagHelpers::Pagination' => 0.06,
+ 'Mojolicious::Plugin::TagHelpers::Pagination' => 0.07,
'Mojolicious::Plugin::TagHelpers::MailToChiffre' => 0.09,
'Mojolicious::Plugin::ClosedRedirect' => 0.14,
'Mojolicious::Plugin::Notifications' => 1.01,
diff --git a/dev/js/src/init.js b/dev/js/src/init.js
index 4f8c41f..1666588 100644
--- a/dev/js/src/init.js
+++ b/dev/js/src/init.js
@@ -73,7 +73,11 @@
if (KorAP.Notifications !== undefined) {
var n = KorAP.Notifications;
for (var i = 0; i < n.length; i++) {
- alertifyClass.log(n[i][1], n[i][0], 10000);
+ var msg = n[i][1];
+ if (n[i][2]) {
+ msg += '<code class="src">'+n[i][2]+'</code>';
+ };
+ alertifyClass.log(msg, n[i][0], 10000);
};
};
diff --git a/dev/scss/main/resultinfo.scss b/dev/scss/main/resultinfo.scss
index 0308df6..77fbfd4 100644
--- a/dev/scss/main/resultinfo.scss
+++ b/dev/scss/main/resultinfo.scss
@@ -91,7 +91,7 @@
font-weight: bold;
}
-#no-results {
+.no-results {
margin: 0 auto;
text-align: center;
code {
diff --git a/kalamar.dict b/kalamar.dict
index 623f7be..9961603 100644
--- a/kalamar.dict
+++ b/kalamar.dict
@@ -47,6 +47,8 @@
matchCount => 'Treffer',
noMatches => 'Es wurden keine Treffer für <%== loc("searchjob") %> gefunden.',
notFound => '404 - Seite nicht gefunden',
+ notIssued => 'Die Suche konnte nicht durchgeführt werden.',
+ backendNotAvailable => 'Das Backend ist nicht verfügbar unter <code><%= app->korap->api =></code>!',
jsFile => 'kalamar-<%= $Kalamar::VERSION %>-de.js',
underConstruction => 'In Vorbereitung!',
korap => {
@@ -132,6 +134,8 @@
matchCount => '<%= quant($found, "match", "matches") %>',
noMatches => 'There were no matches found for <%== loc("searchjob") %>.',
notFound => '404 - Page not found',
+ notIssued => 'Unable to perform the search.',
+ backendNotAvailable => 'The backend is not available at <code><%= app->korap->api %></code>!',
glimpse => {
-short => 'Glimpse',
desc => 'Just show the first matches in arbitrary order'
diff --git a/lib/Kalamar.pm b/lib/Kalamar.pm
index 340d1fc..f5151b4 100644
--- a/lib/Kalamar.pm
+++ b/lib/Kalamar.pm
@@ -116,7 +116,8 @@
# Client notifications
$self->plugin(Notifications => {
'Kalamar::Plugin::Notifications' => 1,
- JSON => 1
+ JSON => 1,
+ 'HTML' => 1
});
# Localization framework
@@ -208,10 +209,9 @@
# Base query route
$r->get('/')->to('search2#query')->name('index');
- $r->get('/q2')->to('search2#query');
- # Collection route
- $r->get('/corpus')->to('Search#corpus_info')->name('corpus');
+ # Corpus route
+ $r->get('/corpus')->to('Search2#corpus_info')->name('corpus');
# Documentation routes
$r->get('/doc')->to('documentation#page', page => 'korap')->name('doc_start');
@@ -225,11 +225,8 @@
# Match route
my $corpus = $r->route('/corpus/:corpus_id');
my $doc = $corpus->get('/:doc_id');
- 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');
-
- $r->get('/corpus2')->to('Search2#corpus_info')->name('corpus');
- $r->route('/corpus2/:corpus_id/:doc_id/:text_id/:match_id')->to('search2#match_info')->name('match');
+ 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');
# User Management
my $user = $r->any('/user')->to(controller => 'User');
diff --git a/lib/Kalamar/Controller/Search2.pm b/lib/Kalamar/Controller/Search2.pm
index 397d7c5..7922a2a 100644
--- a/lib/Kalamar/Controller/Search2.pm
+++ b/lib/Kalamar/Controller/Search2.pm
@@ -23,9 +23,6 @@
# Validate user input
my $v = $c->validation;
- # In case the user is not known, it is assumed, the user is not logged in
- my $user = $c->stash('user') // 'not_logged_in';
-
$v->optional('q', 'trim');
$v->optional('ql')->in(qw/poliqarp cosmas2 annis cql fcsql/);
$v->optional('collection', 'trim'); # Legacy
@@ -50,28 +47,33 @@
};
my %query = ();
- $query{q} = $v->param('q');
+ $query{q} = $query;
$query{ql} = $v->param('ql') // 'poliqarp';
- $query{p} = $v->param('p') // 1; # Start page
$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);
# Set offset
# From Mojolicious::Plugin::Search::Index
- $query{o} = $v->param('o') || ((($query{p} // 1) - 1) * ($items_per_page || 1));
+ $query{offset} = $v->param('o') || ((($page // 1) - 1) * ($items_per_page || 1));
# already set by stash - or use plugin param
@@ -95,27 +97,35 @@
# $url->query(%query);
$url->query(map { $_ => $query{$_}} sort keys %query);
- # Check if total results is cached
+ # 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-' . $user . '-' . $url->to_string);
+ $total_results = $c->chi->get($total_cache_str);
# Set stash if cache exists
- $c->stash(total_results => $total_results) if $total_results;
- $c->app->log->debug('Get total result from cache');
+ if (defined $total_results) {
+ $c->stash(total_results => $total_results);
- # Set cutoff unless already set
- $url->query({cutoff => 'true'});
+ $c->app->log->debug('Get total result from cache: ' . $total_results);
+
+ # Set cutoff unless already set
+ $url->query({cutoff => 'true'});
+ };
};
- # Choose the snippet based on the parameter
- # TODO:
- # scalar $v->param('snippet') ? 'snippet' : 'search2';
- $c->stash(template => 'search2');
-
-
# Wait for rendering
$c->render_later;
@@ -132,26 +142,30 @@
# 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');
- if (!$total_results) {
+
+ unless (defined $total_results) {
# There are results to remember
if ($json->{meta}->{totalResults} >= 0) {
# Remove cutoff requirement again
- $url->query([cutoff => 'true']);
+ # $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-' . $user . '-' . $url->to_string => $total_results
- );
+ $c->chi->set($total_cache_str => $total_results);
+ }
+
+ # Undefined total results
+ else {
+ $c->stash(total_results => -1);
};
- }
- else {
- $c->stash(total_results => -1);
- }
+ };
+
$c->stash(total_pages => 0);
@@ -163,28 +177,76 @@
);
};
- # Process match results
- $c->_process_query_response($json);
+ # 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 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->{corpus} || $json->{collection}) {
+ $c->stash(corpus_jsonld => ($json->{corpus} || $json->{collection}));
+ };
+
+ # TODO:
+ # scalar $v->param('snippet') ? 'snippet' : 'search2';
# Render result
return $c->render(
- q => $query,
- ql => $query{ql},
- start_page => $query{p},
+ 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(
- results => c(),
- q => $query,
- ql => $query{ql},
- start_page => 1
+ q => $c->stash('query'),
+ ql => $c->stash('ql'),
+ template => 'failure'
);
}
)
@@ -353,63 +415,6 @@
};
-# Process response and set stash values
-sub _process_query_response {
- my ($self, $json) = @_;
-
- # 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) {
- $self->stash(time_exceeded => 1);
- };
-
- # Set result values
- $self->stash(items_per_page => $meta->{itemsPerPage});
-
- ## Bouncing query
- ## if ($json->{query}) {
- ## $index->query_jsonld($json->{query});
- ## };
-
- ## Legacy
- ## elsif ($json->{request}->{query}) {
- ## $index->query_jsonld($json->{request}->{query});
- ## };
-
-
- if ($meta->{totalResults}) {
- $self->stash(total_results => $meta->{totalResults});
- };
-
- # Bouncing collection query
- if ($json->{corpus} || $json->{collection}) {
- $self->stash(corpus_jsonld => ($json->{corpus} || $json->{collection}));
- };
-
- # Set results to stash
- $self->stash(
- results => _map_matches($json->{matches})
- );
-
- return;
-};
-
-
1;
diff --git a/lib/Kalamar/Plugin/KalamarErrors.pm b/lib/Kalamar/Plugin/KalamarErrors.pm
index fd9ead3..2c897fc 100644
--- a/lib/Kalamar/Plugin/KalamarErrors.pm
+++ b/lib/Kalamar/Plugin/KalamarErrors.pm
@@ -59,11 +59,11 @@
}
);
- # Catch connection errors
+ # Catch errors and warnings
+ # This won't be called for connection errors!
$mojo->helper(
catch_errors_and_warnings => sub {
my ($c, $tx) = @_;
-
my $err = $tx->error;
if ($err && $err->{code} != 500) {
@@ -79,17 +79,19 @@
if (!$json && !$err) {
$c->notify(error => 'JSON response is invalid');
- return Mojo::Promise->new->reject;
+ return; # Mojo::Promise->new->reject;
};
# There is json
if ($json) {
+ $c->stash(api_response => $json);
+
# There are errors
if ($c->notify_on_errors($json)) {
# Return on errors - ignore warnings
- return Mojo::Promise->new->reject;
+ return;# Mojo::Promise->new->reject;
};
# Notify on warnings
@@ -99,7 +101,7 @@
if ($json->{status}) {
$c->notify(error => 'Middleware error ' . $json->{'status'});
- return Mojo::Promise->new->reject;
+ return;# Mojo::Promise->new->reject;
};
}
@@ -108,7 +110,7 @@
# Send rejection promise
$c->notify(error => $err->{code} . ': ' . $err->{message});
- return Mojo::Promise->new->reject;
+ return; #Mojo::Promise->new->reject;
};
return $json;
diff --git a/lib/Kalamar/Plugin/KalamarHelpers.pm b/lib/Kalamar/Plugin/KalamarHelpers.pm
index b0006c3..6e0b228 100644
--- a/lib/Kalamar/Plugin/KalamarHelpers.pm
+++ b/lib/Kalamar/Plugin/KalamarHelpers.pm
@@ -277,23 +277,14 @@
);
- # Get a cached request from the backend
+ # Get a cached request from the backend as a promise
$mojo->helper(
cached_koral_p => sub {
my ($c, $method, $url) = @_;
# In case the user is not known, it is assumed,
# the user is not logged in
- my $user = $c->stash('user');
- unless ($user) {
- $user = $c->session('user');
- if ($user) {
- $c->stash(user => $user);
- }
- else {
- $user = 'not_logged_in';
- }
- };
+ my $user = $c->user->handle;
# Set api request for debugging
my $cache_str = "$method-$user-" . $url->to_string;
@@ -302,10 +293,10 @@
if ($c->no_cache) {
return $c->user->auth_request_p($method => $url)->then(
sub {
- my $json = shift;
+ my $tx = shift;
# Catch errors and warnings
- $c->stash(api_response => $json);
- return $c->catch_errors_and_warnings($json);
+ return ($c->catch_errors_and_warnings($tx) ||
+ Mojo::Promise->new->reject);
}
);
};
@@ -319,7 +310,7 @@
if ($koral) {
# Mark response as cache
- $koral->{'X-cached'} = Mojo::JSON->true;
+ $c->res->headers->add('X-Kalamar-Cache' => 'true');
# The promise is already satisfied by the cache
return Mojo::Promise->new->resolve($koral)->then(
@@ -335,17 +326,15 @@
# Resolve request
return $c->user->auth_request_p($method => $url)->then(
sub {
- my $json = shift;
- # Catch errors and warnings
- $c->stash(api_response => $json);
- return $c->catch_errors_and_warnings($json);
+ my $tx = shift;
+ return ($c->catch_errors_and_warnings($tx) ||
+ Mojo::Promise->new->reject);
}
)->then(
# Cache on success
sub {
my $json = shift;
$c->chi->set($cache_str => $json);
- $c->stash(api_response => $json);
return $json;
}
);
diff --git a/lib/Kalamar/Plugin/KalamarUser.pm b/lib/Kalamar/Plugin/KalamarUser.pm
index 41251a1..19bb2a7 100644
--- a/lib/Kalamar/Plugin/KalamarUser.pm
+++ b/lib/Kalamar/Plugin/KalamarUser.pm
@@ -86,6 +86,29 @@
}
);
+ # Get user handle
+ $mojo->helper(
+ 'user.handle' => sub {
+ my $c = shift;
+
+ # Get from stash
+ my $user = $c->stash('user');
+ return $user if $user;
+
+ # Get from session
+ $user = $c->session('user');
+
+ # Set in stash
+ if ($user) {
+ $c->stash(user => $user);
+ return $user;
+ };
+
+ return 'not_logged_in';
+ }
+ );
+
+
# Request with authorization header
$mojo->helper(
'user.auth_request' => sub {
diff --git a/lib/Kalamar/Plugin/Notifications.pm b/lib/Kalamar/Plugin/Notifications.pm
index 9fea51b..8b6df94 100644
--- a/lib/Kalamar/Plugin/Notifications.pm
+++ b/lib/Kalamar/Plugin/Notifications.pm
@@ -21,6 +21,9 @@
foreach (@$notify_array) {
$js .= 'KorAP.Notifications.push([';
$js .= quote($_->[0]) . ',' . quote($_->[-1]);
+ if (ref $_->[1] && ref $_->[1] eq 'HASH') {
+ $js .= ',' . quote($_->[1]->{src}) if $_->[1]->{src};
+ };
$js .= "]);\n";
$noscript .= qq{<div class="notify notify-} . $_->[0] . '">' .
diff --git a/t/corpus_info.t b/t/corpus_info.t
index 1d8290b..502ffd2 100644
--- a/t/corpus_info.t
+++ b/t/corpus_info.t
@@ -25,37 +25,37 @@
$fake_backend->pattern->defaults->{app}->log($t->app->log);
# Query passed
-$t->get_ok('/corpus2')
+$t->get_ok('/corpus')
->status_is(200)
->json_is('/documents', 11)
->json_is('/tokens', 665842)
->json_is('/sentences', 25074)
->json_is('/paragraphs', 772)
- ->json_is('/X-cached', undef)
+ ->header_isnt('X-Kalamar-Cache', 'true')
;
-$t->get_ok('/corpus2?cq=docSigle+%3D+\"GOE/AGA\"')
+$t->get_ok('/corpus?cq=docSigle+%3D+\"GOE/AGA\"')
->status_is(200)
->json_is('/documents', 5)
->json_is('/tokens', 108557)
->json_is('/sentences', 3835)
->json_is('/paragraphs', 124)
- ->json_is('/X-cached', undef)
+ ->header_isnt('X-Kalamar-Cache', 'true')
;
-$t->get_ok('/corpus2?cq=4')
+$t->get_ok('/corpus?cq=4')
->status_is(400)
->json_is('/notifications/0/1', "302: Could not parse query >>> (4) <<<.")
;
# Query passed
-$t->get_ok('/corpus2')
+$t->get_ok('/corpus')
->status_is(200)
->json_is('/documents', 11)
->json_is('/tokens', 665842)
->json_is('/sentences', 25074)
->json_is('/paragraphs', 772)
- ->json_is('/X-cached', 1)
+ ->header_is('X-Kalamar-Cache', 'true')
;
diff --git a/t/fixtures.t b/t/fixtures.t
index ba795f7..ea0047c 100644
--- a/t/fixtures.t
+++ b/t/fixtures.t
@@ -24,16 +24,18 @@
->content_like(qr!Oooops!)
;
-$t->get_ok('/search?q=[orth=das&ql=poliqarp')
+$t->get_ok('/search?q=[orth=das&ql=poliqarp&offset=0&count=25')
->status_is(400)
+ ->text_is('#error', '')
->json_is('/errors/0/0',302)
->json_is('/errors/0/1','Parantheses/brackets unbalanced.')
->json_is('/errors/1/0',302)
->json_is('/errors/1/1','Could not parse query >>> [orth=das <<<.')
;
-$t->get_ok('/search?q=baum&ql=poliqarp')
+$t->get_ok('/search?q=baum&ql=poliqarp&offset=0&count=25')
->status_is(200)
+ ->text_is('#error', '')
->json_is('/meta/count', 25)
->json_is('/meta/serialQuery', "tokens:s:Baum")
->json_is('/matches/0/docSigle', "GOE/AGI")
diff --git a/t/fixtures/fake_backend.pl b/t/fixtures/fake_backend.pl
index 89ad3c3..9f06697 100644
--- a/t/fixtures/fake_backend.pl
+++ b/t/fixtures/fake_backend.pl
@@ -36,6 +36,8 @@
my $c = shift;
my $q_name = shift;
my $file = $fixture_path->child("response_$q_name.json");
+ $c->app->log->debug("Load response from $file");
+
unless (-f $file) {
return {
status => 500,
@@ -44,6 +46,7 @@
}
}
};
+
my $response = $file->slurp;
return decode_json($response);
};
@@ -64,6 +67,8 @@
$v->optional('ql');
$v->optional('count');
$v->optional('context');
+ $v->optional('offset');
+ $v->optional('cutoff')->in(qw/true false/);
$c->app->log->debug('Receive request');
@@ -78,11 +83,16 @@
};
if (!$v->param('q')) {
- return $c->render(%{$c->load_response('no_query')});
+ return $c->render(%{$c->load_response('query_no_query')});
};
+ my @slug_base = ($v->param('q'));
+ push @slug_base, 'o' . $v->param('offset') if defined $v->param('offset');
+ push @slug_base, 'c' . $v->param('count') if defined $v->param('count');
+ push @slug_base, 'co' . $v->param('cutoff') if defined $v->param('cutoff');
+
# Get response based on query parameter
- my $response = $c->load_response(slugify($v->param('q')));
+ my $response = $c->load_response('query_' . slugify(join('_', @slug_base)));
# Check authentification
if (my $auth = $c->req->headers->header('Authorization')) {
diff --git a/t/fixtures/response_query_baum_o0_c25_cotrue.json b/t/fixtures/response_query_baum_o0_c25_cotrue.json
new file mode 100644
index 0000000..6e0921e
--- /dev/null
+++ b/t/fixtures/response_query_baum_o0_c25_cotrue.json
@@ -0,0 +1,77 @@
+{
+ "status" : 200,
+ "json" : {
+ "@context" : "http://korap.ids-mannheim.de/ns/KoralQuery/v0.3/context.jsonld",
+ "meta" : {
+ "count" : 25,
+ "startIndex" : 0,
+ "cutOff": true,
+ "authorized" : null,
+ "timeout" : 120000,
+ "context" : {
+ "left" : ["token",40],
+ "right" : ["token",40]
+ },
+ "fields" : ["pubDate","subTitle","author","pubPlace","title","textSigle","UID","ID","layerInfos","corpusSigle","docSigle","corpusID","textClass"],
+ "version" : "0.55.7",
+ "benchmark" : "0.120577834 s",
+ "totalResults" : -1,
+ "serialQuery" : "tokens:s:Baum",
+ "itemsPerPage" : 25
+ },
+ "query" : {
+ "@type" : "koral:token",
+ "wrap" : {
+ "@type" : "koral:term",
+ "layer" : "orth",
+ "key" : "Baum",
+ "match" : "match:eq",
+ "foundry" : "opennlp",
+ "rewrites" : [
+ {
+ "@type" : "koral:rewrite",
+ "src" : "Kustvakt",
+ "operation" : "operation:injection",
+ "scope" : "foundry"
+ }
+ ]
+ }
+ },
+ "matches" : [
+ {
+ "field" : "tokens",
+ "pubPlace" : "München",
+ "textSigle" : "GOE/AGI/00000",
+ "docSigle" : "GOE/AGI",
+ "corpusSigle" : "GOE",
+ "title" : "Italienische Reise",
+ "subTitle" : "Auch ich in Arkadien!",
+ "author" : "Goethe, Johann Wolfgang von",
+ "layerInfos" : "base/s=spans corenlp/c=spans corenlp/p=tokens corenlp/s=spans dereko/s=spans malt/d=rels mdp/d=rels opennlp/p=tokens opennlp/s=spans tt/l=tokens tt/p=tokens tt/s=spans",
+ "startMore" : true,
+ "endMore" : true,
+ "snippet" : "<span class=\"context-left\"><span class=\"more\"></span>sie etwas bedeuten zu wollen und machte mit der Oberlippe eine fatale Miene. ich sprach sehr viel mit ihr durch, sie war überall zu Hause und merkte gut auf die Gegenstände. so fragte sie mich einmal, was das für ein </span><span class=\"match\"><mark>Baum</mark></span><span class=\"context-right\"> sei. es war ein schöner großer Ahorn, der erste, der mir auf der ganzen Reise zu Gesichte kam. den hatte sie doch gleich bemerkt und freute sich, da mehrere nach und nach erschienen, daß sie auch diesen Baum unterscheiden könne<span class=\"more\"></span></span>",
+ "matchID" : "match-GOE/AGI/00000-p2030-2031",
+ "UID" : 0,
+ "pubDate" : "1982"
+ },
+ {
+ "field" : "tokens",
+ "pubPlace" : "München",
+ "textSigle" : "GOE/AGI/00001",
+ "docSigle" : "GOE/AGI",
+ "corpusSigle" : "GOE",
+ "title" : "Italienische Reise",
+ "subTitle" : "Auch ich in Arkadien!",
+ "author" : "Goethe, Johann Wolfgang von",
+ "layerInfos" : "base/s=spans corenlp/c=spans corenlp/p=tokens corenlp/s=spans dereko/s=spans malt/d=rels mdp/d=rels opennlp/p=tokens opennlp/s=spans tt/l=tokens tt/p=tokens tt/s=spans",
+ "startMore" : true,
+ "endMore" : true,
+ "snippet" : "<span class=\"context-left\"><span class=\"more\"></span>für ein Baum sei. es war ein schöner großer Ahorn, der erste, der mir auf der ganzen Reise zu Gesichte kam. den hatte sie doch gleich bemerkt und freute sich, da mehrere nach und nach erschienen, daß sie auch diesen </span><span class=\"match\"><mark>Baum</mark></span><span class=\"context-right\"> unterscheiden könne. sie gehe, sagte sie, nach Bozen auf die Messe, wo ich doch wahrscheinlich auch hinzöge. wenn sie mich dort anträfe, müsse ich ihr einen Jahrmarkt kaufen, welches ich ihr denn auch versprach. dort wollte sie auch ihre neue<span class=\"more\"></span></span>",
+ "matchID" : "match-GOE/AGI/00000-p2068-2069",
+ "UID" : 0,
+ "pubDate" : "1982"
+ }
+ ]
+ }
+}
diff --git a/t/fixtures/response_query_der_o0_c2.json b/t/fixtures/response_query_der_o0_c2.json
new file mode 100644
index 0000000..f712139
--- /dev/null
+++ b/t/fixtures/response_query_der_o0_c2.json
@@ -0,0 +1,4 @@
+{
+ "status" : 200,
+ "json" : {"@context":"http://korap.ids-mannheim.de/ns/KoralQuery/v0.3/context.jsonld","meta":{"count":2,"startIndex":0,"timeout":120000,"context":{"left":["token",6],"right":["token",6]},"fields":["textSigle","author","docSigle","availability","title","pubDate","UID","corpusID","textClass","subTitle","layerInfos","ID","pubPlace","corpusSigle"],"version":"0.58.0","benchmark":"22.347006999999998 ms","totalResults":14581,"serialQuery":"tokens:s:der","itemsPerPage":2},"query":{"@type":"koral:token","wrap":{"@type":"koral:term","match":"match:eq","layer":"orth","key":"der","foundry":"opennlp","rewrites":[{"@type":"koral:rewrite","src":"Kustvakt","operation":"operation:injection","scope":"foundry"}]}},"matches":[{"field":"tokens","pubPlace":"München","textSigle":"GOE/AGI/00000","docSigle":"GOE/AGI","corpusSigle":"GOE","title":"Italienische Reise","subTitle":"Auch ich in Arkadien!","author":"Goethe, Johann Wolfgang von","availability":"ACA-NC","layerInfos":"corenlp/c=spans corenlp/p=tokens corenlp/s=spans dereko/s=spans malt/d=rels marmot/m=tokens marmot/p=tokens opennlp/p=tokens opennlp/s=spans tt/l=tokens tt/p=tokens","startMore":true,"endMore":true,"license":"ACA-NC","snippet":"<span class=\"context-left\"><span class=\"more\"></span>das Stift Waldsassen entgegen - köstliche Besitztümer </span><span class=\"match\"><mark>der</mark></span><span class=\"context-right\"> geistlichen Herren, die früher als andere<span class=\"more\"></span></span>","matchID":"match-GOE/AGI/00000-p161-162","pubDate":"1982","UID":0},{"field":"tokens","pubPlace":"München","textSigle":"GOE/AGI/00000","docSigle":"GOE/AGI","corpusSigle":"GOE","title":"Italienische Reise","subTitle":"Auch ich in Arkadien!","author":"Goethe, Johann Wolfgang von","availability":"ACA-NC","layerInfos":"corenlp/c=spans corenlp/p=tokens corenlp/s=spans dereko/s=spans malt/d=rels marmot/m=tokens marmot/p=tokens opennlp/p=tokens opennlp/s=spans tt/l=tokens tt/p=tokens","startMore":true,"endMore":true,"license":"ACA-NC","snippet":"<span class=\"context-left\"><span class=\"more\"></span>Kloster im Lande weit umher Besitzungen. </span><span class=\"match\"><mark>der</mark></span><span class=\"context-right\"> Boden ist aufgelöster Tonschiefer. der Quarz<span class=\"more\"></span></span>","matchID":"match-GOE/AGI/00000-p200-201","pubDate":"1982","UID":0}]}
+}
diff --git a/t/fixtures/response_query_der_o0_c2_cotrue.json b/t/fixtures/response_query_der_o0_c2_cotrue.json
new file mode 100644
index 0000000..a683805
--- /dev/null
+++ b/t/fixtures/response_query_der_o0_c2_cotrue.json
@@ -0,0 +1,4 @@
+{
+ "status" : 200,
+ "json" : {"@context":"http://korap.ids-mannheim.de/ns/KoralQuery/v0.3/context.jsonld","meta":{"cutOff":true,"count":2,"startIndex":0,"timeout":120000,"context":{"left":["token",6],"right":["token",6]},"fields":["textSigle","author","docSigle","availability","title","pubDate","UID","corpusID","textClass","subTitle","layerInfos","ID","pubPlace","corpusSigle"],"version":"0.58.0","benchmark":"29.377798 ms","totalResults":-1,"serialQuery":"tokens:s:der","itemsPerPage":2},"query":{"@type":"koral:token","wrap":{"@type":"koral:term","match":"match:eq","layer":"orth","key":"der","foundry":"opennlp","rewrites":[{"@type":"koral:rewrite","src":"Kustvakt","operation":"operation:injection","scope":"foundry"}]}},"matches":[{"field":"tokens","pubPlace":"München","textSigle":"GOE/AGI/00000","docSigle":"GOE/AGI","corpusSigle":"GOE","title":"Italienische Reise","subTitle":"Auch ich in Arkadien!","author":"Goethe, Johann Wolfgang von","availability":"ACA-NC","layerInfos":"corenlp/c=spans corenlp/p=tokens corenlp/s=spans dereko/s=spans malt/d=rels marmot/m=tokens marmot/p=tokens opennlp/p=tokens opennlp/s=spans tt/l=tokens tt/p=tokens","startMore":true,"endMore":true,"license":"ACA-NC","snippet":"<span class=\"context-left\"><span class=\"more\"></span>das Stift Waldsassen entgegen - köstliche Besitztümer </span><span class=\"match\"><mark>der</mark></span><span class=\"context-right\"> geistlichen Herren, die früher als andere<span class=\"more\"></span></span>","matchID":"match-GOE/AGI/00000-p161-162","pubDate":"1982","UID":0},{"field":"tokens","pubPlace":"München","textSigle":"GOE/AGI/00000","docSigle":"GOE/AGI","corpusSigle":"GOE","title":"Italienische Reise","subTitle":"Auch ich in Arkadien!","author":"Goethe, Johann Wolfgang von","availability":"ACA-NC","layerInfos":"corenlp/c=spans corenlp/p=tokens corenlp/s=spans dereko/s=spans malt/d=rels marmot/m=tokens marmot/p=tokens opennlp/p=tokens opennlp/s=spans tt/l=tokens tt/p=tokens","startMore":true,"endMore":true,"license":"ACA-NC","snippet":"<span class=\"context-left\"><span class=\"more\"></span>Kloster im Lande weit umher Besitzungen. </span><span class=\"match\"><mark>der</mark></span><span class=\"context-right\"> Boden ist aufgelöster Tonschiefer. der Quarz<span class=\"more\"></span></span>","matchID":"match-GOE/AGI/00000-p200-201","pubDate":"1982","UID":0}]}
+}
diff --git a/t/fixtures/response_query_der_o2_c2.json b/t/fixtures/response_query_der_o2_c2.json
new file mode 100644
index 0000000..ae77282
--- /dev/null
+++ b/t/fixtures/response_query_der_o2_c2.json
@@ -0,0 +1,4 @@
+{
+ "status" : 200,
+ "json" : {"@context":"http://korap.ids-mannheim.de/ns/KoralQuery/v0.3/context.jsonld","meta":{"count":2,"startIndex":2,"timeout":120000,"context":{"left":["token",6],"right":["token",6]},"fields":["textSigle","author","docSigle","availability","title","pubDate","UID","corpusID","textClass","subTitle","layerInfos","ID","pubPlace","corpusSigle"],"version":"0.58.0","benchmark":"31.207413 ms","totalResults":14581,"serialQuery":"tokens:s:der","itemsPerPage":2},"query":{"@type":"koral:token","wrap":{"@type":"koral:term","match":"match:eq","layer":"orth","key":"der","foundry":"opennlp","rewrites":[{"@type":"koral:rewrite","src":"Kustvakt","operation":"operation:injection","scope":"foundry"}]}},"matches":[{"field":"tokens","pubPlace":"München","textSigle":"GOE/AGI/00000","docSigle":"GOE/AGI","corpusSigle":"GOE","title":"Italienische Reise","subTitle":"Auch ich in Arkadien!","author":"Goethe, Johann Wolfgang von","availability":"ACA-NC","layerInfos":"corenlp/c=spans corenlp/p=tokens corenlp/s=spans dereko/s=spans malt/d=rels marmot/m=tokens marmot/p=tokens opennlp/p=tokens opennlp/s=spans tt/l=tokens tt/p=tokens","startMore":true,"endMore":true,"license":"ACA-NC","snippet":"<span class=\"context-left\"><span class=\"more\"></span>Besitzungen. der Boden ist aufgelöster Tonschiefer. </span><span class=\"match\"><mark>der</mark></span><span class=\"context-right\"> Quarz, der sich in dieser Gebirgsart<span class=\"more\"></span></span>","matchID":"match-GOE/AGI/00000-p205-206","pubDate":"1982","UID":0},{"field":"tokens","pubPlace":"München","textSigle":"GOE/AGI/00000","docSigle":"GOE/AGI","corpusSigle":"GOE","title":"Italienische Reise","subTitle":"Auch ich in Arkadien!","author":"Goethe, Johann Wolfgang von","availability":"ACA-NC","layerInfos":"corenlp/c=spans corenlp/p=tokens corenlp/s=spans dereko/s=spans malt/d=rels marmot/m=tokens marmot/p=tokens opennlp/p=tokens opennlp/s=spans tt/l=tokens tt/p=tokens","startMore":true,"endMore":true,"license":"ACA-NC","snippet":"<span class=\"context-left\"><span class=\"more\"></span>Boden ist aufgelöster Tonschiefer. der Quarz, </span><span class=\"match\"><mark>der</mark></span><span class=\"context-right\"> sich in dieser Gebirgsart befindet und<span class=\"more\"></span></span>","matchID":"match-GOE/AGI/00000-p207-208","pubDate":"1982","UID":0}]}
+}
diff --git a/t/fixtures/response_query_der_o2_c2_cotrue.json b/t/fixtures/response_query_der_o2_c2_cotrue.json
new file mode 100644
index 0000000..c0ef128
--- /dev/null
+++ b/t/fixtures/response_query_der_o2_c2_cotrue.json
@@ -0,0 +1,4 @@
+{
+ "status" : 200,
+ "json" : {"@context":"http://korap.ids-mannheim.de/ns/KoralQuery/v0.3/context.jsonld","meta":{"cutOff":true,"count":2,"startIndex":2,"timeout":120000,"context":{"left":["token",6],"right":["token",6]},"fields":["textSigle","author","docSigle","availability","title","pubDate","UID","corpusID","textClass","subTitle","layerInfos","ID","pubPlace","corpusSigle"],"version":"0.58.0","benchmark":"21.959009 ms","totalResults":-1,"serialQuery":"tokens:s:der","itemsPerPage":2},"query":{"@type":"koral:token","wrap":{"@type":"koral:term","match":"match:eq","layer":"orth","key":"der","foundry":"opennlp","rewrites":[{"@type":"koral:rewrite","src":"Kustvakt","operation":"operation:injection","scope":"foundry"}]}},"matches":[{"field":"tokens","pubPlace":"München","textSigle":"GOE/AGI/00000","docSigle":"GOE/AGI","corpusSigle":"GOE","title":"Italienische Reise","subTitle":"Auch ich in Arkadien!","author":"Goethe, Johann Wolfgang von","availability":"ACA-NC","layerInfos":"corenlp/c=spans corenlp/p=tokens corenlp/s=spans dereko/s=spans malt/d=rels marmot/m=tokens marmot/p=tokens opennlp/p=tokens opennlp/s=spans tt/l=tokens tt/p=tokens","startMore":true,"endMore":true,"license":"ACA-NC","snippet":"<span class=\"context-left\"><span class=\"more\"></span>Besitzungen. der Boden ist aufgelöster Tonschiefer. </span><span class=\"match\"><mark>der</mark></span><span class=\"context-right\"> Quarz, der sich in dieser Gebirgsart<span class=\"more\"></span></span>","matchID":"match-GOE/AGI/00000-p205-206","pubDate":"1982","UID":0},{"field":"tokens","pubPlace":"München","textSigle":"GOE/AGI/00000","docSigle":"GOE/AGI","corpusSigle":"GOE","title":"Italienische Reise","subTitle":"Auch ich in Arkadien!","author":"Goethe, Johann Wolfgang von","availability":"ACA-NC","layerInfos":"corenlp/c=spans corenlp/p=tokens corenlp/s=spans dereko/s=spans malt/d=rels marmot/m=tokens marmot/p=tokens opennlp/p=tokens opennlp/s=spans tt/l=tokens tt/p=tokens","startMore":true,"endMore":true,"license":"ACA-NC","snippet":"<span class=\"context-left\"><span class=\"more\"></span>Boden ist aufgelöster Tonschiefer. der Quarz, </span><span class=\"match\"><mark>der</mark></span><span class=\"context-right\"> sich in dieser Gebirgsart befindet und<span class=\"more\"></span></span>","matchID":"match-GOE/AGI/00000-p207-208","pubDate":"1982","UID":0}]}
+}
diff --git a/t/match_info.t b/t/match_info.t
index c07e7aa..bd9afe9 100644
--- a/t/match_info.t
+++ b/t/match_info.t
@@ -25,14 +25,14 @@
$fake_backend->pattern->defaults->{app}->log($t->app->log);
# Query passed
-$t->get_ok('/corpus2/WPD15/232/39681/p2133-2134?spans=false&foundry=*')
+$t->get_ok('/corpus/WPD15/232/39681/p2133-2134?spans=false&foundry=*')
->status_is(200)
->json_is('/textSigle', 'WPD15/232/39681')
->json_like('/snippet', qr!<span class=\"context-left\">!)
- ->json_is('/X-cached', undef)
+ ->header_isnt('X-Kalamar-Cache', 'true')
;
-$t->get_ok('/corpus2/GOE/AGF/02286/p75682-75683')
+$t->get_ok('/corpus/GOE/AGF/02286/p75682-75683')
->status_is(200)
->json_is('/textSigle', 'GOE/AGF/02286')
->json_is('/title','Materialien zur Geschichte der Farbenlehre')
@@ -40,7 +40,7 @@
# TODO:
# It's surprising, that it doesn't return a 404!
-$t->get_ok('/corpus2/notfound/X/X/p0-1')
+$t->get_ok('/corpus/notfound/X/X/p0-1')
->status_is(200)
->json_is('/textSigle', 'NOTFOUND/X/X')
->json_is('/corpusID', undef)
@@ -48,7 +48,7 @@
# TODO:
# Should probably return a 500!
-$t->get_ok('/corpus2/fail/x/x/p0-0')
+$t->get_ok('/corpus/fail/x/x/p0-0')
->status_is(200)
->json_is('/notifications/0/0', 'error')
->json_is('/notifications/0/1', 'Unable to load query response from /home/ndiewald/Repositories/korap-git/Kalamar/t/fixtures/response_matchinfo_fail_x_x_p0-0.json')
@@ -56,7 +56,7 @@
# TODO:
# Should probably return a 4xx!
-$t->get_ok('/corpus2/GOE/AGF/02286/p-2-0')
+$t->get_ok('/corpus/GOE/AGF/02286/p-2-0')
->status_is(200)
->json_is('/notifications/0/0', 'error')
->json_is('/notifications/0/1', '730: Invalid match identifier')
@@ -64,19 +64,19 @@
# TODO:
# It's surprising, that it doesn't return a 404!
-$t->get_ok('/corpus2/notfound2/X/X/p0-1')
+$t->get_ok('/corpus/notfound2/X/X/p0-1')
->status_is(404)
->json_is('/notifications/0/0', 'error')
->json_is('/notifications/0/1', '404: Not Found')
;
-$t->get_ok('/corpus2/brokenerr/X/X/p0-1')
+$t->get_ok('/corpus/brokenerr/X/X/p0-1')
->status_is(409)
->json_is('/notifications/0/0', 'error')
->json_is('/notifications/0/1', 'Message structure failed')
;
-$t->get_ok('/corpus2/brokenwarn/X/X/p0-1')
+$t->get_ok('/corpus/brokenwarn/X/X/p0-1')
->status_is(200)
->json_is('/notifications/0/0', 'warning')
->json_is('/notifications/0/1', '1: Warning 1')
@@ -84,18 +84,18 @@
->json_is('/notifications/1/1', 'Message structure failed')
;
-$t->get_ok('/corpus2/brokenerr2/X/X/p0-1')
+$t->get_ok('/corpus/brokenerr2/X/X/p0-1')
->status_is(417)
->json_is('/notifications/0/0', 'error')
->json_is('/notifications/0/1', 'Message structure failed')
;
# Get from cache
-$t->get_ok('/corpus2/WPD15/232/39681/p2133-2134?spans=false&foundry=*')
+$t->get_ok('/corpus/WPD15/232/39681/p2133-2134?spans=false&foundry=*')
->status_is(200)
->json_is('/textSigle', 'WPD15/232/39681')
->json_like('/snippet', qr!<span class=\"context-left\">!)
- ->json_is('/X-cached', 1)
+ ->header_is('X-Kalamar-Cache', 'true')
;
diff --git a/t/query.t b/t/query.t
index 4bc97dc..97f4997 100644
--- a/t/query.t
+++ b/t/query.t
@@ -25,9 +25,10 @@
$fake_backend->pattern->defaults->{app}->log($t->app->log);
# Query passed
-$t->get_ok('/q2?q=baum')
+$t->get_ok('/?q=baum')
->status_is(200)
->text_is('#error','')
+
->text_is('title', 'KorAP: Find »baum« with Poliqarp')
->element_exists('meta[name="DC.title"][content="KorAP: Find »baum« with Poliqarp"]')
->element_exists('body[itemscope][itemtype="http://schema.org/SearchResultsPage"]')
@@ -42,6 +43,9 @@
->content_like(qr/\"authorized\":null/)
->content_like(qr/\"pubDate\",\"subTitle\",\"author\"/)
+ # No cutOff
+ ->content_unlike(qr!\"cutOff":true!)
+
->element_exists('li[data-text-sigle=GOE/AGI/00000]')
->element_exists('li:nth-of-type(1) div.flop')
->element_exists('li[data-text-sigle=GOE/AGI/00001]')
@@ -65,13 +69,87 @@
->text_like('li:nth-of-type(1) p.ref', qr!by Goethe, Johann Wolfgang!)
->text_is('li:nth-of-type(1) p.ref time[datetime=1982]', 1982)
->text_is('li:nth-of-type(1) p.ref span.sigle', '[GOE/AGI/00000]')
+ ->header_isnt('X-Kalamar-Cache', 'true')
;
-$t->get_ok('/q2?q=[orth=das')
+$t->get_ok('/?q=[orth=das')
->status_is(400)
->text_is('div.notify-error:nth-of-type(1)', '302: Parantheses/brackets unbalanced.')
->text_like('div.notify-error:nth-of-type(2)', qr!302: Could not parse query .+? \[orth=das.+?!)
;
+# Query with partial cache (for total results)
+$t->get_ok('/?q=baum')
+ ->status_is(200)
+ ->text_is('#error','')
+ ->text_is('title', 'KorAP: Find »baum« with Poliqarp')
+ ->element_exists('meta[name="DC.title"][content="KorAP: Find »baum« with Poliqarp"]')
+ ->element_exists('body[itemscope][itemtype="http://schema.org/SearchResultsPage"]')
+ ->header_isnt('X-Kalamar-Cache', 'true')
+ ->content_like(qr!\"cutOff":true!)
+ ->text_is('#total-results', 51)
+ ;
+
+# Query with full cache
+$t->get_ok('/?q=baum')
+ ->status_is(200)
+ ->text_is('#error','')
+ ->text_is('title', 'KorAP: Find »baum« with Poliqarp')
+ ->element_exists('meta[name="DC.title"][content="KorAP: Find »baum« with Poliqarp"]')
+ ->element_exists('body[itemscope][itemtype="http://schema.org/SearchResultsPage"]')
+ ->header_is('X-Kalamar-Cache', 'true')
+ ->content_like(qr!\"cutOff":true!)
+ ->text_is('#total-results', 51)
+ ;
+
+
+# Query with page information
+$t->get_ok('/?q=der&p=1&count=2')
+ ->status_is(200)
+ ->text_is('#error','')
+ ->text_is('title', 'KorAP: Find »der« with Poliqarp')
+
+ # Total results
+ ->text_is('#total-results', '14,581')
+
+ # Total pages
+ ->element_count_is('#pagination a', 7)
+ ->text_is('#pagination a:nth-of-type(6) span', 7291)
+ ->content_like(qr!\"count":2!)
+ ->content_like(qr!\"startIndex":0!)
+ ->content_like(qr!\"itemsPerPage":2!)
+
+ # No caching
+ ->header_isnt('X-Kalamar-Cache', 'true')
+
+ # Not searched for "der" before
+ ->content_unlike(qr!\"cutOff":true!)
+ ;
+
+# Query with page information - next page
+$t->get_ok('/?q=der&p=2&count=2')
+ ->status_is(200)
+ ->text_is('#error','')
+ ->text_is('title', 'KorAP: Find »der« with Poliqarp')
+
+ # Total results
+ ->text_is('#total-results', '14,581')
+
+ # Total pages
+ ->element_count_is('#pagination a', 7)
+ ->text_is('#pagination a:nth-of-type(6) span', 7291)
+ ->content_like(qr!\"count":2!)
+ ->content_like(qr!\"itemsPerPage":2!)
+ ->content_like(qr!\"startIndex":2!)
+
+ # No caching
+ ->header_isnt('X-Kalamar-Cache', 'true')
+ ->content_like(qr!\"cutOff":true!)
+ ;
+
+
+
+
done_testing;
+__END__
diff --git a/t/remote.t b/t/remote.t
index 328b4ef..a04ca5c 100644
--- a/t/remote.t
+++ b/t/remote.t
@@ -23,13 +23,9 @@
$fake_backend->pattern->defaults->{app}->log($t->app->log);
+
if (0) {
-$t->get_ok('/')
- ->status_is(200)
- ->text_is('title', 'KorAP - Corpus Analysis Platform')
- ->text_like('h1 span', qr/KorAP - Corpus Analysis Platform/i)
- ;
# Check paging
$t->get_ok('/?q=Baum')
@@ -75,13 +71,12 @@
};
-
-
# Check for query error
$t->get_ok('/?q=[orth=das&ql=poliqarp')
->element_exists('.notify-error')
->text_is('.notify-error', '302: Parantheses/brackets unbalanced.')
->content_like(qr!KorAP\.koralQuery =!)
+ ->text_is('.no-results:nth-of-type(1)', 'Unable to perform the search.')
;
done_testing;
diff --git a/templates/exception.html.ep b/templates/exception.html.ep
index 7d8af3b..af0aaf6 100644
--- a/templates/exception.html.ep
+++ b/templates/exception.html.ep
@@ -1,6 +1,6 @@
% my $msg = $exception->message // '500: Internal Server Error';
% layout 'main', title => 'KorAP: ' . $msg;
-<p id="no-results"><%= $msg %></p>
+<p class="no-results"><%= $msg %></p>
% notify('error' => $msg);
diff --git a/templates/failure.html.ep b/templates/failure.html.ep
new file mode 100644
index 0000000..d9673b6
--- /dev/null
+++ b/templates/failure.html.ep
@@ -0,0 +1,10 @@
+% layout 'main', title => loc('searchtitle', q => stash('q'), ql => stash('ql')), schematype => 'SearchResultsPage';
+
+<div id="resultinfo"><p class="found"></p></div>
+
+%= include 'query2'
+
+<p class="no-results"><%= loc('notIssued') %></p>
+% if (stash('err_msg')) {
+<p class="no-results"><%= loc(stash('err_msg'),stash('err_msg')) %></p>
+% }
diff --git a/templates/not_found.html.ep b/templates/not_found.html.ep
index 953ab43..8410580 100644
--- a/templates/not_found.html.ep
+++ b/templates/not_found.html.ep
@@ -1,6 +1,6 @@
% my $msg = stash('msg') // loc('notFound');
% layout 'main', title => 'KorAP: ' . loc('notFound');
-<p id="no-results"><%= $msg %></p>
+<p class="no-results"><%= $msg %></p>
% notify('warn' => $msg);
diff --git a/templates/search2.html.ep b/templates/search2.html.ep
index 5a72ca5..907cc03 100644
--- a/templates/search2.html.ep
+++ b/templates/search2.html.ep
@@ -1,4 +1,5 @@
% 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;
@@ -26,6 +27,6 @@
% end
</ol>
% } elsif (stash('results')->size == 0) {
-<p id="no-results"><%= loc 'noMatches', q => stash('q'), ql => stash('ql') %></p>
+<p class="no-results"><%= loc 'noMatches', q => stash('q'), ql => stash('ql') %></p>
% }
</div>