From b4654c0eafc72925ee9f390a397946116d6d02e2 Mon Sep 17 00:00:00 2001
From: Eike Cochu <eike@cochu.com>
Date: Thu, 4 Feb 2016 00:03:49 +0100
Subject: [PATCH] updated indexing

indexing now includes topics, had to move indexing to after topic modeling and store processed text in db
added pagination directive to ui
---
 .../main/java/de/vipra/cmd/file/Filebase.java |  6 +-
 .../java/de/vipra/cmd/file/JGibbFilebase.java | 12 ++--
 .../de/vipra/cmd/model/ProcessedArticle.java  | 53 -----------------
 .../de/vipra/cmd/option/ClearCommand.java     |  6 +-
 .../de/vipra/cmd/option/ImportCommand.java    | 58 +++++++++++--------
 .../vipra/rest/resource/SearchResource.java   |  2 +-
 vipra-ui/css/app.css                          |  4 +-
 vipra-ui/css/app.less                         | 17 ++++--
 vipra-ui/html/articles/index.html             |  4 +-
 vipra-ui/html/articles/show.html              |  4 ++
 vipra-ui/html/directives/pagination.html      | 15 +++++
 vipra-ui/html/index.html                      |  4 +-
 vipra-ui/html/topics/show.html                | 39 +++++++++++++
 vipra-ui/js/app.js                            |  6 +-
 vipra-ui/js/controllers.js                    | 45 ++++++++++++--
 vipra-ui/js/directives.js                     | 18 +++++-
 .../java/de/vipra/util/an/QueryIgnore.java    |  2 +
 .../java/de/vipra/util/model/ArticleFull.java | 28 +++++++++
 .../main/java/de/vipra/util/model/Topic.java  |  5 ++
 .../java/de/vipra/util/model/TopicRef.java    |  6 --
 .../vipra/util/service/DatabaseService.java   |  4 +-
 21 files changed, 222 insertions(+), 116 deletions(-)
 delete mode 100644 vipra-cmd/src/main/java/de/vipra/cmd/model/ProcessedArticle.java
 create mode 100644 vipra-ui/html/directives/pagination.html

diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/file/Filebase.java b/vipra-cmd/src/main/java/de/vipra/cmd/file/Filebase.java
index 0bc63735..9730d405 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/file/Filebase.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/file/Filebase.java
@@ -5,10 +5,10 @@ import java.io.File;
 import java.io.IOException;
 
 import de.vipra.cmd.ex.FilebaseException;
-import de.vipra.cmd.model.ProcessedArticle;
 import de.vipra.util.Config;
 import de.vipra.util.Constants;
 import de.vipra.util.ex.ConfigException;
+import de.vipra.util.model.ArticleFull;
 
 public abstract class Filebase implements Closeable {
 
@@ -45,7 +45,7 @@ public abstract class Filebase implements Closeable {
 		return vocab;
 	}
 
-	public void remove(ProcessedArticle article) throws FilebaseException {
+	public void remove(ArticleFull article) throws FilebaseException {
 		remove(article.getId().toString());
 	}
 
@@ -56,7 +56,7 @@ public abstract class Filebase implements Closeable {
 		vocab.close();
 	}
 
-	public abstract void add(ProcessedArticle article) throws FilebaseException;
+	public abstract void add(ArticleFull article) throws FilebaseException;
 
 	public abstract void remove(String id) throws FilebaseException;
 
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/file/JGibbFilebase.java b/vipra-cmd/src/main/java/de/vipra/cmd/file/JGibbFilebase.java
index d5ee6273..1fb52be4 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/file/JGibbFilebase.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/file/JGibbFilebase.java
@@ -7,15 +7,15 @@ import java.util.ArrayList;
 import java.util.List;
 
 import de.vipra.cmd.ex.FilebaseException;
-import de.vipra.cmd.model.ProcessedArticle;
 import de.vipra.util.ex.NotImplementedException;
+import de.vipra.util.model.ArticleFull;
 
 public class JGibbFilebase extends Filebase {
 
 	private final File modelFile;
 	private final FilebaseIndex index;
 	private final FilebaseVocabulary vocab;
-	private final List<ProcessedArticle> articles;
+	private final List<ArticleFull> articles;
 
 	private final int bufferMaxSize = 100;
 
@@ -28,8 +28,8 @@ public class JGibbFilebase extends Filebase {
 	}
 
 	@Override
-	public void add(ProcessedArticle article) throws FilebaseException {
-		String[] words = article.getProcessedText().getText().split("\\s+");
+	public void add(ArticleFull article) throws FilebaseException {
+		String[] words = article.getProcessedText().split("\\s+");
 		vocab.addVocabulary(words);
 		index.add(article.getId().toString());
 		articles.add(article);
@@ -56,12 +56,12 @@ public class JGibbFilebase extends Filebase {
 
 			// write articles
 			raf.seek(raf.length());
-			for (ProcessedArticle a : articles) {
+			for (ArticleFull a : articles) {
 				if (linesep)
 					raf.writeBytes(System.lineSeparator());
 				else
 					linesep = true;
-				raf.writeBytes(a.getProcessedText().getText());
+				raf.writeBytes(a.getProcessedText());
 			}
 
 			raf.close();
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/model/ProcessedArticle.java b/vipra-cmd/src/main/java/de/vipra/cmd/model/ProcessedArticle.java
deleted file mode 100644
index e11dd916..00000000
--- a/vipra-cmd/src/main/java/de/vipra/cmd/model/ProcessedArticle.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package de.vipra.cmd.model;
-
-import java.util.Date;
-
-import org.json.simple.JSONObject;
-import org.mongodb.morphia.annotations.Entity;
-import org.mongodb.morphia.annotations.Transient;
-
-import de.vipra.cmd.text.ProcessedText;
-import de.vipra.util.an.ElasticIndex;
-
-@SuppressWarnings("serial")
-@Entity(value = "articles", noClassnameStored = true)
-public class ProcessedArticle extends de.vipra.util.model.ArticleFull {
-
-	@Transient
-	private ProcessedText processedText;
-
-	public ProcessedText getProcessedText() {
-		return processedText;
-	}
-
-	public void setProcessedText(ProcessedText processedText) {
-		this.processedText = processedText;
-	}
-
-	@ElasticIndex("title")
-	public String serializeTitle() {
-		return super.getTitle();
-	}
-
-	@ElasticIndex("date")
-	public Date serializeDate() {
-		return super.getDate();
-	}
-
-	@ElasticIndex("text")
-	public String serializeText() {
-		return processedText.getText();
-	}
-
-	public void fromJSON(JSONObject obj) {
-		if (obj.containsKey("title"))
-			setTitle(obj.get("title").toString());
-		if (obj.containsKey("text"))
-			setText(obj.get("text").toString());
-		if (obj.containsKey("url"))
-			setUrl(obj.get("url").toString());
-		if (obj.containsKey("date"))
-			setDate(obj.get("date").toString());
-	}
-
-}
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/ClearCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/ClearCommand.java
index 1ee15d55..0c03aa63 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/option/ClearCommand.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/ClearCommand.java
@@ -9,10 +9,10 @@ import org.apache.logging.log4j.Logger;
 import org.bson.types.ObjectId;
 import org.elasticsearch.client.Client;
 
-import de.vipra.cmd.model.ProcessedArticle;
 import de.vipra.util.Config;
 import de.vipra.util.ConsoleUtils;
 import de.vipra.util.ESClient;
+import de.vipra.util.model.Article;
 import de.vipra.util.model.Import;
 import de.vipra.util.model.TopicFull;
 import de.vipra.util.model.Word;
@@ -25,7 +25,7 @@ public class ClearCommand implements Command {
 
 	private boolean defaults;
 	private Config config;
-	private DatabaseService<ProcessedArticle, ObjectId> dbArticles;
+	private DatabaseService<Article, ObjectId> dbArticles;
 	private DatabaseService<TopicFull, ObjectId> dbTopics;
 	private DatabaseService<Word, String> dbWords;
 	private DatabaseService<Import, ObjectId> dbImports;
@@ -37,7 +37,7 @@ public class ClearCommand implements Command {
 
 	private void clear() throws Exception {
 		config = Config.getConfig();
-		dbArticles = DatabaseService.getDatabaseService(config, ProcessedArticle.class);
+		dbArticles = DatabaseService.getDatabaseService(config, Article.class);
 		dbTopics = DatabaseService.getDatabaseService(config, TopicFull.class);
 		dbWords = DatabaseService.getDatabaseService(config, Word.class);
 		dbImports = DatabaseService.getDatabaseService(config, Import.class);
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/ImportCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/ImportCommand.java
index f6e960bf..4d879875 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/option/ImportCommand.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/ImportCommand.java
@@ -21,7 +21,6 @@ import org.json.simple.parser.JSONParser;
 import de.vipra.cmd.file.Filebase;
 import de.vipra.cmd.file.FilebaseIndex;
 import de.vipra.cmd.lda.LDAAnalyzer;
-import de.vipra.cmd.model.ProcessedArticle;
 import de.vipra.cmd.text.ProcessedText;
 import de.vipra.cmd.text.Processor;
 import de.vipra.util.Config;
@@ -35,6 +34,7 @@ import de.vipra.util.Timer;
 import de.vipra.util.WordMap;
 import de.vipra.util.ex.DatabaseException;
 import de.vipra.util.model.Article;
+import de.vipra.util.model.ArticleFull;
 import de.vipra.util.model.ArticleStats;
 import de.vipra.util.model.Import;
 import de.vipra.util.model.Topic;
@@ -51,7 +51,7 @@ public class ImportCommand implements Command {
 	private ArrayList<File> files = new ArrayList<>();
 	private JSONParser parser = new JSONParser();
 	private Config config;
-	private DatabaseService<ProcessedArticle, ObjectId> dbArticles;
+	private DatabaseService<ArticleFull, ObjectId> dbArticles;
 	private DatabaseService<TopicFull, ObjectId> dbTopics;
 	private DatabaseService<Word, String> dbWords;
 	private DatabaseService<Import, ObjectId> dbImports;
@@ -60,7 +60,7 @@ public class ImportCommand implements Command {
 	private WordMap wordMap;
 	private LDAAnalyzer analyzer;
 	private Client elasticClient;
-	private ElasticSerializer<ProcessedArticle> elasticSerializer;
+	private ElasticSerializer<ArticleFull> elasticSerializer;
 
 	/**
 	 * Import command to import articles into the database, do topic modeling
@@ -111,15 +111,14 @@ public class ImportCommand implements Command {
 	 */
 	private Article importArticle(JSONObject obj) throws Exception {
 		out.info("importing \"" + obj.get("title") + "\"");
-		ProcessedArticle article = new ProcessedArticle();
-		article.fromJSON(obj);
+		ArticleFull article = articleFromJSON(obj);
 
 		// preprocess text and generate text statistics
 		ProcessedText processedText = preprocessor.process(article.getText());
 		ArticleStats articleStats = ArticleStats.generateFromText(processedText.getText(), wordMap);
 
 		// add article to mongodb
-		article.setProcessedText(processedText);
+		article.setProcessedText(processedText.getText());
 		article.setStats(articleStats);
 		article = dbArticles.createSingle(article);
 
@@ -132,10 +131,6 @@ public class ImportCommand implements Command {
 		// add article to filebase
 		filebase.add(article);
 
-		// index article
-		Map<String, Object> source = elasticSerializer.serialize(article);
-		elasticClient.prepareIndex("articles", "article", article.getId().toString()).setSource(source).get();
-
 		// return article reference
 		return new Article(article.getId());
 	}
@@ -169,12 +164,23 @@ public class ImportCommand implements Command {
 		return articles;
 	}
 
+	private ArticleFull articleFromJSON(JSONObject obj) {
+		ArticleFull article = new ArticleFull();
+		if (obj.containsKey("title"))
+			article.setTitle(obj.get("title").toString());
+		if (obj.containsKey("text"))
+			article.setText(obj.get("text").toString());
+		if (obj.containsKey("url"))
+			article.setUrl(obj.get("url").toString());
+		if (obj.containsKey("date"))
+			article.setDate(obj.get("date").toString());
+		return article;
+	}
+
 	@Override
 	public void run() throws Exception {
 		config = Config.getConfig();
-		@SuppressWarnings("unused")
-		Config asd = Config.getConfig();
-		dbArticles = DatabaseService.getDatabaseService(config, ProcessedArticle.class);
+		dbArticles = DatabaseService.getDatabaseService(config, ArticleFull.class);
 		dbTopics = DatabaseService.getDatabaseService(config, TopicFull.class);
 		dbWords = DatabaseService.getDatabaseService(config, Word.class);
 		dbImports = DatabaseService.getDatabaseService(config, Import.class);
@@ -183,7 +189,7 @@ public class ImportCommand implements Command {
 		wordMap = new WordMap(dbWords);
 		analyzer = LDAAnalyzer.getAnalyzer(config, wordMap);
 		elasticClient = ESClient.getClient(config);
-		elasticSerializer = new ElasticSerializer<>(ProcessedArticle.class);
+		elasticSerializer = new ElasticSerializer<>(ArticleFull.class);
 
 		out.info("using data directory: " + config.getDataDirectory().getAbsolutePath());
 		out.info("using preprocessor: " + preprocessor.getName());
@@ -222,7 +228,7 @@ public class ImportCommand implements Command {
 		out.info("saving topic definitions");
 		int batchSize = 100;
 		ConvertStream<TopicFull> topicDefs = analyzer.getTopicDefinitions();
-		Map<String, String> topicIndexMap = new HashMap<>();
+		Map<String, TopicFull> topicIndexMap = new HashMap<>();
 		dbTopics.drop();
 		List<TopicFull> newTopicDefs = new ArrayList<>(batchSize);
 		List<Topic> newTopicRefs = new ArrayList<>();
@@ -232,26 +238,28 @@ public class ImportCommand implements Command {
 			if (newTopicDefs.size() == batchSize || !it.hasNext()) {
 				dbTopics.createMultiple(newTopicDefs);
 				for (TopicFull newTopicDef : newTopicDefs) {
-					topicIndexMap.put(Integer.toString(newTopicDef.getIndex()), newTopicDef.getId().toString());
+					topicIndexMap.put(Integer.toString(newTopicDef.getIndex()), newTopicDef);
 					newTopicRefs.add(new Topic(newTopicDef.getId()));
 				}
+				newTopicDefs.clear();
 			}
 		}
 		importOp.setTopics(newTopicRefs);
 		timer.lap("saving topics");
 
 		/*
-		 * save topic refs
+		 * save topic refs and index article
 		 */
 		out.info("saving document topics");
 		ConvertStream<List<TopicRef>> topicStream = analyzer.getTopics();
 		FilebaseIndex index = filebase.getIndex();
 		Iterator<String> indexIter = index.iterator();
 		Iterator<List<TopicRef>> topicRefsListIter = topicStream.iterator();
+		elasticClient.admin().indices().prepareDelete("_all").get();
 		while (indexIter.hasNext() && topicRefsListIter.hasNext()) {
 			// get article from database
 			String id = indexIter.next();
-			ProcessedArticle article = dbArticles.getSingle(MongoUtils.objectId(id));
+			ArticleFull article = dbArticles.getSingle(MongoUtils.objectId(id));
 			if (article == null) {
 				log.error("no article found in db for id " + id);
 				continue;
@@ -267,10 +275,10 @@ public class ImportCommand implements Command {
 					topicRefsIter.remove();
 					continue;
 				}
-				String topicObjectId = topicIndexMap.get(topicRef.getTopicIndex());
-				if (topicObjectId != null)
-					topicRef.setTopicId(topicObjectId);
-				else
+				TopicFull topicFull = topicIndexMap.get(topicRef.getTopicIndex());
+				if (topicFull != null) {
+					topicRef.setTopic(new Topic(topicFull.getId(), topicFull.getName()));
+				} else
 					log.error("no object id for topic index " + topicRef.getTopicIndex());
 			}
 
@@ -281,10 +289,14 @@ public class ImportCommand implements Command {
 			} catch (DatabaseException e) {
 				log.error("could not update article: " + article.getTitle() + " (" + article.getId() + ")");
 			}
+
+			// index article
+			Map<String, Object> source = elasticSerializer.serialize(article);
+			elasticClient.prepareIndex("articles", "article", article.getId().toString()).setSource(source).get();
 		}
 		List<Word> importedWords = wordMap.getNewWords();
 		importOp.setWords(importedWords);
-		timer.lap("saving topic refs");
+		timer.lap("saving topic refs and indexing");
 
 		/*
 		 * save words
diff --git a/vipra-rest/src/main/java/de/vipra/rest/resource/SearchResource.java b/vipra-rest/src/main/java/de/vipra/rest/resource/SearchResource.java
index 5df5c56b..faf5f0e7 100644
--- a/vipra-rest/src/main/java/de/vipra/rest/resource/SearchResource.java
+++ b/vipra-rest/src/main/java/de/vipra/rest/resource/SearchResource.java
@@ -33,7 +33,7 @@ import de.vipra.util.StringUtils;
 import de.vipra.util.ex.ConfigException;
 import de.vipra.util.model.ArticleFull;
 
-@Path("/search")
+@Path("search")
 public class SearchResource {
 
 	@Context
diff --git a/vipra-ui/css/app.css b/vipra-ui/css/app.css
index c138c2d1..26bd0d5b 100644
--- a/vipra-ui/css/app.css
+++ b/vipra-ui/css/app.css
@@ -1,2 +1,2 @@
-html{position:relative;min-height:100%}body{padding-bottom:20px;margin-bottom:60px}.heading{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default;font-size:120px;text-align:center;background:transparent url(/img/logo.svg) no-repeat 50% 50%;background-size:contain;height:200px;line-height:200px;margin:35px 0}.search-results{padding:15px}.search-results .search-result{margin-bottom:20px}.search-results .search-result a{font-size:1.5rem}.ellipsize{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.navbar-default .collapse:not(.in) .navbar-nav>.active>a,.navbar-default .collapse:not(.in) .navbar-nav>.active>a:focus,.navbar-default .collapse:not(.in) .navbar-nav>.active>a:hover{border-bottom:3px solid;padding-bottom:12px}.navbar-default .navbar-header{padding:0 10px}.navbar-default .navbar-brand{background:transparent url(/img/logo.svg) no-repeat 50% 50%;background-size:contain}.navbar-default .navbar-brand.spin,.navbar-default .navbar-brand:hover:not(.spin){-webkit-animation:a 4s linear infinite;animation:a 4s linear infinite}.row-spaced{margin-top:15px;margin-bottom:15px}.footer{position:absolute;bottom:0;width:100%;height:50px;border-top-width:1px;border-top-style:solid}.noselect{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default}@-webkit-keyframes a{to{-webkit-transform:rotateY(1turn)}}@keyframes a{to{-webkit-transform:rotateY(1turn);transform:rotateY(1turn)}}
-/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImFwcC5sZXNzIiwiYXBwLmNzcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxLQUNFLGtCQUFBLEFBQ0EsZUFBQSxDQ0NELEFERUQsS0FDRSxvQkFBQSxBQUVBLGtCQUFBLENDQUQsQURHRCxTQXlFRSwyQkFBQSxBQUNBLHlCQUFBLEFBQ0Esc0JBQUEsQUFDQSxxQkFBQSxBQUNBLGlCQUFBLEFBQ0EsZUFBQSxBQTVFQSxnQkFBQSxBQUNBLGtCQUFBLEFBQ0EsNERBQUEsQUFDQSx3QkFBQSxBQUNBLGFBQUEsQUFDQSxrQkFBQSxBQUNBLGFBQUEsQ0NJRCxBRERELGdCQUNFLFlBQUEsQ0NHRCxBREpELCtCQUlJLGtCQUFBLENDR0gsQURQRCxpQ0FPTSxnQkFBQSxDQ0dMLEFERUQsV0FDRSxtQkFBQSxBQUNBLGdCQUFBLEFBQ0Esc0JBQUEsQ0NBRCxBRE1LLHVMQUdFLHdCQUFBLEFBQ0EsbUJBQUEsQ0NKUCxBREhELCtCQWFJLGNBQUEsQ0NQSCxBRE5ELDhCQWlCSSw0REFBQSxBQUNBLHVCQUFBLENDUkgsQURTRyxrRkFFRSx1Q0FBQSxBQUVBLDhCQUFBLENDUEwsQURZRCxZQUNFLGdCQUFBLEFBQ0Esa0JBQUEsQ0NWRCxBRGFELFFBQ0Usa0JBQUEsQUFDQSxTQUFBLEFBQ0EsV0FBQSxBQUVBLFlBQUEsQUFDQSxxQkFBQSxBQUNBLHNCQUFBLENDWEQsQURjRCxVQUNFLDJCQUFBLEFBQ0EseUJBQUEsQUFDQSxzQkFBQSxBQUNBLHFCQUFBLEFBQ0EsaUJBQUEsQUFDQSxjQUFBLENDWkQsQURnQkQscUJBQTBCLEdBQU8sZ0NBQUEsQ0NQOUIsQ0FDRixBRE9ELGFBQWtCLEdBQU8saUNBQUEsQUFBb0Msd0JBQUEsQ0NGMUQsQ0FDRiIsImZpbGUiOiJhcHAuY3NzIiwic291cmNlc0NvbnRlbnQiOlsiaHRtbCB7XG4gIHBvc2l0aW9uOiByZWxhdGl2ZTtcbiAgbWluLWhlaWdodDogMTAwJTtcbn1cblxuYm9keSB7XG4gIHBhZGRpbmctYm90dG9tOiAyMHB4O1xuICAvKiBNYXJnaW4gYm90dG9tIGJ5IGZvb3RlciBoZWlnaHQgKi9cbiAgbWFyZ2luLWJvdHRvbTogNjBweDtcbn1cblxuLmhlYWRpbmcge1xuICAubm9zZWxlY3Q7XG4gIGZvbnQtc2l6ZTogMTIwcHg7XG4gIHRleHQtYWxpZ246IGNlbnRlcjtcbiAgYmFja2dyb3VuZDogdHJhbnNwYXJlbnQgdXJsKC9pbWcvbG9nby5zdmcpIG5vLXJlcGVhdCA1MCUgNTAlO1xuICBiYWNrZ3JvdW5kLXNpemU6IGNvbnRhaW47XG4gIGhlaWdodDogMjAwcHg7XG4gIGxpbmUtaGVpZ2h0OiAyMDBweDtcbiAgbWFyZ2luOiAzNXB4IDA7XG59XG5cbi5zZWFyY2gtcmVzdWx0cyB7XG4gIHBhZGRpbmc6IDE1cHg7XG5cbiAgLnNlYXJjaC1yZXN1bHQge1xuICAgIG1hcmdpbi1ib3R0b206IDIwcHg7XG5cbiAgICBhIHtcbiAgICAgIGZvbnQtc2l6ZTogMS41cmVtO1xuICAgIH1cbiAgfVxufVxuXG4uZWxsaXBzaXplIHtcbiAgd2hpdGUtc3BhY2U6IG5vd3JhcDtcbiAgb3ZlcmZsb3c6IGhpZGRlbjtcbiAgdGV4dC1vdmVyZmxvdzogZWxsaXBzaXM7XG59XG5cbi5uYXZiYXItZGVmYXVsdCB7XG4gIC5jb2xsYXBzZTpub3QoLmluKSB7XG4gICAgLm5hdmJhci1uYXYgPiAuYWN0aXZlIHtcbiAgICAgICY+IGEsXG4gICAgICAmPiBhOmhvdmVyLFxuICAgICAgJj4gYTpmb2N1cyB7XG4gICAgICAgIGJvcmRlci1ib3R0b206IDNweCBzb2xpZDtcbiAgICAgICAgcGFkZGluZy1ib3R0b206IDEycHg7XG4gICAgICB9XG4gICAgfVxuICB9XG5cbiAgLm5hdmJhci1oZWFkZXIge1xuICAgIHBhZGRpbmc6IDAgMTBweDtcbiAgfVxuXG4gIC5uYXZiYXItYnJhbmQge1xuICAgIGJhY2tncm91bmQ6IHRyYW5zcGFyZW50IHVybCgvaW1nL2xvZ28uc3ZnKSBuby1yZXBlYXQgNTAlIDUwJTtcbiAgICBiYWNrZ3JvdW5kLXNpemU6IGNvbnRhaW47XG4gICAgJi5zcGluLFxuICAgICY6aG92ZXI6bm90KC5zcGluKSB7XG4gICAgICAtd2Via2l0LWFuaW1hdGlvbjpzcGluIDRzIGxpbmVhciBpbmZpbml0ZTtcbiAgICAgIC1tb3otYW5pbWF0aW9uOnNwaW4gNHMgbGluZWFyIGluZmluaXRlO1xuICAgICAgYW5pbWF0aW9uOnNwaW4gNHMgbGluZWFyIGluZmluaXRlO1xuICAgIH1cbiAgfVxufVxuXG4ucm93LXNwYWNlZCB7XG4gIG1hcmdpbi10b3A6IDE1cHg7XG4gIG1hcmdpbi1ib3R0b206IDE1cHg7XG59XG5cbi5mb290ZXIge1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIGJvdHRvbTogMDtcbiAgd2lkdGg6IDEwMCU7XG4gIC8qIFNldCB0aGUgZml4ZWQgaGVpZ2h0IG9mIHRoZSBmb290ZXIgaGVyZSAqL1xuICBoZWlnaHQ6IDUwcHg7XG4gIGJvcmRlci10b3Atd2lkdGg6IDFweDtcbiAgYm9yZGVyLXRvcC1zdHlsZTogc29saWQ7XG59XG5cbi5ub3NlbGVjdCB7XG4gIC13ZWJraXQtdG91Y2gtY2FsbG91dDogbm9uZTtcbiAgLXdlYmtpdC11c2VyLXNlbGVjdDogbm9uZTtcbiAgLW1vei11c2VyLXNlbGVjdDogbm9uZTtcbiAgLW1zLXVzZXItc2VsZWN0OiBub25lO1xuICB1c2VyLXNlbGVjdDogbm9uZTtcbiAgY3Vyc29yOiBkZWZhdWx0O1xufVxuXG5ALW1vei1rZXlmcmFtZXMgc3BpbiB7IDEwMCUgeyAtbW96LXRyYW5zZm9ybTogcm90YXRlWSgzNjBkZWcpOyB9IH1cbkAtd2Via2l0LWtleWZyYW1lcyBzcGluIHsgMTAwJSB7IC13ZWJraXQtdHJhbnNmb3JtOiByb3RhdGVZKDM2MGRlZyk7IH0gfVxuQGtleWZyYW1lcyBzcGluIHsgMTAwJSB7IC13ZWJraXQtdHJhbnNmb3JtOiByb3RhdGVZKDM2MGRlZyk7IHRyYW5zZm9ybTpyb3RhdGVZKDM2MGRlZyk7IH0gfSIsImh0bWwge1xuICBwb3NpdGlvbjogcmVsYXRpdmU7XG4gIG1pbi1oZWlnaHQ6IDEwMCU7XG59XG5ib2R5IHtcbiAgcGFkZGluZy1ib3R0b206IDIwcHg7XG4gIC8qIE1hcmdpbiBib3R0b20gYnkgZm9vdGVyIGhlaWdodCAqL1xuICBtYXJnaW4tYm90dG9tOiA2MHB4O1xufVxuLmhlYWRpbmcge1xuICAtd2Via2l0LXRvdWNoLWNhbGxvdXQ6IG5vbmU7XG4gIC13ZWJraXQtdXNlci1zZWxlY3Q6IG5vbmU7XG4gIC1tb3otdXNlci1zZWxlY3Q6IG5vbmU7XG4gIC1tcy11c2VyLXNlbGVjdDogbm9uZTtcbiAgdXNlci1zZWxlY3Q6IG5vbmU7XG4gIGN1cnNvcjogZGVmYXVsdDtcbiAgZm9udC1zaXplOiAxMjBweDtcbiAgdGV4dC1hbGlnbjogY2VudGVyO1xuICBiYWNrZ3JvdW5kOiB0cmFuc3BhcmVudCB1cmwoL2ltZy9sb2dvLnN2Zykgbm8tcmVwZWF0IDUwJSA1MCU7XG4gIGJhY2tncm91bmQtc2l6ZTogY29udGFpbjtcbiAgaGVpZ2h0OiAyMDBweDtcbiAgbGluZS1oZWlnaHQ6IDIwMHB4O1xuICBtYXJnaW46IDM1cHggMDtcbn1cbi5zZWFyY2gtcmVzdWx0cyB7XG4gIHBhZGRpbmc6IDE1cHg7XG59XG4uc2VhcmNoLXJlc3VsdHMgLnNlYXJjaC1yZXN1bHQge1xuICBtYXJnaW4tYm90dG9tOiAyMHB4O1xufVxuLnNlYXJjaC1yZXN1bHRzIC5zZWFyY2gtcmVzdWx0IGEge1xuICBmb250LXNpemU6IDEuNXJlbTtcbn1cbi5lbGxpcHNpemUge1xuICB3aGl0ZS1zcGFjZTogbm93cmFwO1xuICBvdmVyZmxvdzogaGlkZGVuO1xuICB0ZXh0LW92ZXJmbG93OiBlbGxpcHNpcztcbn1cbi5uYXZiYXItZGVmYXVsdCAuY29sbGFwc2U6bm90KC5pbikgLm5hdmJhci1uYXYgPiAuYWN0aXZlID4gYSxcbi5uYXZiYXItZGVmYXVsdCAuY29sbGFwc2U6bm90KC5pbikgLm5hdmJhci1uYXYgPiAuYWN0aXZlID4gYTpob3Zlcixcbi5uYXZiYXItZGVmYXVsdCAuY29sbGFwc2U6bm90KC5pbikgLm5hdmJhci1uYXYgPiAuYWN0aXZlID4gYTpmb2N1cyB7XG4gIGJvcmRlci1ib3R0b206IDNweCBzb2xpZDtcbiAgcGFkZGluZy1ib3R0b206IDEycHg7XG59XG4ubmF2YmFyLWRlZmF1bHQgLm5hdmJhci1oZWFkZXIge1xuICBwYWRkaW5nOiAwIDEwcHg7XG59XG4ubmF2YmFyLWRlZmF1bHQgLm5hdmJhci1icmFuZCB7XG4gIGJhY2tncm91bmQ6IHRyYW5zcGFyZW50IHVybCgvaW1nL2xvZ28uc3ZnKSBuby1yZXBlYXQgNTAlIDUwJTtcbiAgYmFja2dyb3VuZC1zaXplOiBjb250YWluO1xufVxuLm5hdmJhci1kZWZhdWx0IC5uYXZiYXItYnJhbmQuc3Bpbixcbi5uYXZiYXItZGVmYXVsdCAubmF2YmFyLWJyYW5kOmhvdmVyOm5vdCguc3Bpbikge1xuICAtd2Via2l0LWFuaW1hdGlvbjogc3BpbiA0cyBsaW5lYXIgaW5maW5pdGU7XG4gIC1tb3otYW5pbWF0aW9uOiBzcGluIDRzIGxpbmVhciBpbmZpbml0ZTtcbiAgYW5pbWF0aW9uOiBzcGluIDRzIGxpbmVhciBpbmZpbml0ZTtcbn1cbi5yb3ctc3BhY2VkIHtcbiAgbWFyZ2luLXRvcDogMTVweDtcbiAgbWFyZ2luLWJvdHRvbTogMTVweDtcbn1cbi5mb290ZXIge1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIGJvdHRvbTogMDtcbiAgd2lkdGg6IDEwMCU7XG4gIC8qIFNldCB0aGUgZml4ZWQgaGVpZ2h0IG9mIHRoZSBmb290ZXIgaGVyZSAqL1xuICBoZWlnaHQ6IDUwcHg7XG4gIGJvcmRlci10b3Atd2lkdGg6IDFweDtcbiAgYm9yZGVyLXRvcC1zdHlsZTogc29saWQ7XG59XG4ubm9zZWxlY3Qge1xuICAtd2Via2l0LXRvdWNoLWNhbGxvdXQ6IG5vbmU7XG4gIC13ZWJraXQtdXNlci1zZWxlY3Q6IG5vbmU7XG4gIC1tb3otdXNlci1zZWxlY3Q6IG5vbmU7XG4gIC1tcy11c2VyLXNlbGVjdDogbm9uZTtcbiAgdXNlci1zZWxlY3Q6IG5vbmU7XG4gIGN1cnNvcjogZGVmYXVsdDtcbn1cbkAtbW96LWtleWZyYW1lcyBzcGluIHtcbiAgMTAwJSB7XG4gICAgLW1vei10cmFuc2Zvcm06IHJvdGF0ZVkoMzYwZGVnKTtcbiAgfVxufVxuQC13ZWJraXQta2V5ZnJhbWVzIHNwaW4ge1xuICAxMDAlIHtcbiAgICAtd2Via2l0LXRyYW5zZm9ybTogcm90YXRlWSgzNjBkZWcpO1xuICB9XG59XG5Aa2V5ZnJhbWVzIHNwaW4ge1xuICAxMDAlIHtcbiAgICAtd2Via2l0LXRyYW5zZm9ybTogcm90YXRlWSgzNjBkZWcpO1xuICAgIHRyYW5zZm9ybTogcm90YXRlWSgzNjBkZWcpO1xuICB9XG59XG4iXSwic291cmNlUm9vdCI6Ii9zb3VyY2UvIn0= */
+html{position:relative;min-height:100%}body{padding-bottom:20px;margin-bottom:60px}.heading{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default;background:transparent url(/img/logo.svg) no-repeat 50% 50%;background-size:contain;height:125px;margin:25px 0}.search-results{padding:15px}.search-results .search-result{margin-bottom:20px}.search-results .search-result a{font-size:1.5rem}.ellipsize{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.navbar-default .collapse:not(.in) .navbar-nav>.active>a,.navbar-default .collapse:not(.in) .navbar-nav>.active>a:focus,.navbar-default .collapse:not(.in) .navbar-nav>.active>a:hover{border-bottom:3px solid;padding-bottom:12px}.navbar-default .navbar-header{padding:0 10px}.navbar-default .navbar-brand{background:transparent url(/img/logo.svg) no-repeat 50% 50%;background-size:contain}.navbar-default .navbar-brand.spin,.navbar-default .navbar-brand:hover:not(.spin){-webkit-animation:a 4s linear infinite;animation:a 4s linear infinite}.row-spaced{margin-top:15px;margin-bottom:15px}.footer{position:absolute;bottom:0;width:100%;height:50px;border-top-width:1px;border-top-style:solid}.noselect{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default}@-webkit-keyframes a{to{-webkit-transform:rotateY(1turn)}}@keyframes a{to{-webkit-transform:rotateY(1turn);transform:rotateY(1turn)}}
+/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImFwcC5sZXNzIiwiYXBwLmNzcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxLQUNFLGtCQUFBLEFBQ0EsZUFBQSxDQ0NELEFERUQsS0FDRSxvQkFBQSxBQUVBLGtCQUFBLENDQUQsQURHRCxTQXNFRSwyQkFBQSxBQUNBLHlCQUFBLEFBQ0Esc0JBQUEsQUFDQSxxQkFBQSxBQUNBLGlCQUFBLEFBQ0EsZUFBQSxBQXpFQSw0REFBQSxBQUNBLHdCQUFBLEFBQ0EsYUFBQSxBQUNBLGFBQUEsQ0NJRCxBRERELGdCQUNFLFlBQUEsQ0NHRCxBREpELCtCQUlJLGtCQUFBLENDR0gsQURQRCxpQ0FPTSxnQkFBQSxDQ0dMLEFERUQsV0FDRSxtQkFBQSxBQUNBLGdCQUFBLEFBQ0Esc0JBQUEsQ0NBRCxBRE1LLHVMQUdFLHdCQUFBLEFBQ0EsbUJBQUEsQ0NKUCxBREhELCtCQWFJLGNBQUEsQ0NQSCxBRE5ELDhCQWlCSSw0REFBQSxBQUNBLHVCQUFBLENDUkgsQURTRyxrRkFFRSx1Q0FBQSxBQUVBLDhCQUFBLENDUEwsQURZRCxZQUNFLGdCQUFBLEFBQ0Esa0JBQUEsQ0NWRCxBRGFELFFBQ0Usa0JBQUEsQUFDQSxTQUFBLEFBQ0EsV0FBQSxBQUVBLFlBQUEsQUFDQSxxQkFBQSxBQUNBLHNCQUFBLENDWEQsQURjRCxVQUNFLDJCQUFBLEFBQ0EseUJBQUEsQUFDQSxzQkFBQSxBQUNBLHFCQUFBLEFBQ0EsaUJBQUEsQUFDQSxjQUFBLENDWkQsQURnQkQscUJBQTBCLEdBQU8sZ0NBQUEsQ0NQOUIsQ0FDRixBRE9ELGFBQWtCLEdBQU8saUNBQUEsQUFBb0Msd0JBQUEsQ0NGMUQsQ0FDRiIsImZpbGUiOiJhcHAuY3NzIiwic291cmNlc0NvbnRlbnQiOlsiaHRtbCB7XG4gIHBvc2l0aW9uOiByZWxhdGl2ZTtcbiAgbWluLWhlaWdodDogMTAwJTtcbn1cblxuYm9keSB7XG4gIHBhZGRpbmctYm90dG9tOiAyMHB4O1xuICAvKiBNYXJnaW4gYm90dG9tIGJ5IGZvb3RlciBoZWlnaHQgKi9cbiAgbWFyZ2luLWJvdHRvbTogNjBweDtcbn1cblxuLmhlYWRpbmcge1xuICAubm9zZWxlY3Q7XG4gIGJhY2tncm91bmQ6IHRyYW5zcGFyZW50IHVybCgvaW1nL2xvZ28uc3ZnKSBuby1yZXBlYXQgNTAlIDUwJTtcbiAgYmFja2dyb3VuZC1zaXplOiBjb250YWluO1xuICBoZWlnaHQ6IDEyNXB4O1xuICBtYXJnaW46IDI1cHggMDtcbn1cblxuLnNlYXJjaC1yZXN1bHRzIHtcbiAgcGFkZGluZzogMTVweDtcblxuICAuc2VhcmNoLXJlc3VsdCB7XG4gICAgbWFyZ2luLWJvdHRvbTogMjBweDtcblxuICAgIGEge1xuICAgICAgZm9udC1zaXplOiAxLjVyZW07XG4gICAgfVxuICB9XG59XG5cbi5lbGxpcHNpemUge1xuICB3aGl0ZS1zcGFjZTogbm93cmFwO1xuICBvdmVyZmxvdzogaGlkZGVuO1xuICB0ZXh0LW92ZXJmbG93OiBlbGxpcHNpcztcbn1cblxuLm5hdmJhci1kZWZhdWx0IHtcbiAgLmNvbGxhcHNlOm5vdCguaW4pIHtcbiAgICAubmF2YmFyLW5hdiA+IC5hY3RpdmUge1xuICAgICAgJj4gYSxcbiAgICAgICY+IGE6aG92ZXIsXG4gICAgICAmPiBhOmZvY3VzIHtcbiAgICAgICAgYm9yZGVyLWJvdHRvbTogM3B4IHNvbGlkO1xuICAgICAgICBwYWRkaW5nLWJvdHRvbTogMTJweDtcbiAgICAgIH1cbiAgICB9XG4gIH1cblxuICAubmF2YmFyLWhlYWRlciB7XG4gICAgcGFkZGluZzogMCAxMHB4O1xuICB9XG5cbiAgLm5hdmJhci1icmFuZCB7XG4gICAgYmFja2dyb3VuZDogdHJhbnNwYXJlbnQgdXJsKC9pbWcvbG9nby5zdmcpIG5vLXJlcGVhdCA1MCUgNTAlO1xuICAgIGJhY2tncm91bmQtc2l6ZTogY29udGFpbjtcbiAgICAmLnNwaW4sXG4gICAgJjpob3Zlcjpub3QoLnNwaW4pIHtcbiAgICAgIC13ZWJraXQtYW5pbWF0aW9uOnNwaW4gNHMgbGluZWFyIGluZmluaXRlO1xuICAgICAgLW1vei1hbmltYXRpb246c3BpbiA0cyBsaW5lYXIgaW5maW5pdGU7XG4gICAgICBhbmltYXRpb246c3BpbiA0cyBsaW5lYXIgaW5maW5pdGU7XG4gICAgfVxuICB9XG59XG5cbi5yb3ctc3BhY2VkIHtcbiAgbWFyZ2luLXRvcDogMTVweDtcbiAgbWFyZ2luLWJvdHRvbTogMTVweDtcbn1cblxuLmZvb3RlciB7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgYm90dG9tOiAwO1xuICB3aWR0aDogMTAwJTtcbiAgLyogU2V0IHRoZSBmaXhlZCBoZWlnaHQgb2YgdGhlIGZvb3RlciBoZXJlICovXG4gIGhlaWdodDogNTBweDtcbiAgYm9yZGVyLXRvcC13aWR0aDogMXB4O1xuICBib3JkZXItdG9wLXN0eWxlOiBzb2xpZDtcbn1cblxuLm5vc2VsZWN0IHtcbiAgLXdlYmtpdC10b3VjaC1jYWxsb3V0OiBub25lO1xuICAtd2Via2l0LXVzZXItc2VsZWN0OiBub25lO1xuICAtbW96LXVzZXItc2VsZWN0OiBub25lO1xuICAtbXMtdXNlci1zZWxlY3Q6IG5vbmU7XG4gIHVzZXItc2VsZWN0OiBub25lO1xuICBjdXJzb3I6IGRlZmF1bHQ7XG59XG5cbkAtbW96LWtleWZyYW1lcyBzcGluIHsgMTAwJSB7IC1tb3otdHJhbnNmb3JtOiByb3RhdGVZKDM2MGRlZyk7IH0gfVxuQC13ZWJraXQta2V5ZnJhbWVzIHNwaW4geyAxMDAlIHsgLXdlYmtpdC10cmFuc2Zvcm06IHJvdGF0ZVkoMzYwZGVnKTsgfSB9XG5Aa2V5ZnJhbWVzIHNwaW4geyAxMDAlIHsgLXdlYmtpdC10cmFuc2Zvcm06IHJvdGF0ZVkoMzYwZGVnKTsgdHJhbnNmb3JtOnJvdGF0ZVkoMzYwZGVnKTsgfSB9IiwiaHRtbCB7XG4gIHBvc2l0aW9uOiByZWxhdGl2ZTtcbiAgbWluLWhlaWdodDogMTAwJTtcbn1cbmJvZHkge1xuICBwYWRkaW5nLWJvdHRvbTogMjBweDtcbiAgLyogTWFyZ2luIGJvdHRvbSBieSBmb290ZXIgaGVpZ2h0ICovXG4gIG1hcmdpbi1ib3R0b206IDYwcHg7XG59XG4uaGVhZGluZyB7XG4gIC13ZWJraXQtdG91Y2gtY2FsbG91dDogbm9uZTtcbiAgLXdlYmtpdC11c2VyLXNlbGVjdDogbm9uZTtcbiAgLW1vei11c2VyLXNlbGVjdDogbm9uZTtcbiAgLW1zLXVzZXItc2VsZWN0OiBub25lO1xuICB1c2VyLXNlbGVjdDogbm9uZTtcbiAgY3Vyc29yOiBkZWZhdWx0O1xuICBiYWNrZ3JvdW5kOiB0cmFuc3BhcmVudCB1cmwoL2ltZy9sb2dvLnN2Zykgbm8tcmVwZWF0IDUwJSA1MCU7XG4gIGJhY2tncm91bmQtc2l6ZTogY29udGFpbjtcbiAgaGVpZ2h0OiAxMjVweDtcbiAgbWFyZ2luOiAyNXB4IDA7XG59XG4uc2VhcmNoLXJlc3VsdHMge1xuICBwYWRkaW5nOiAxNXB4O1xufVxuLnNlYXJjaC1yZXN1bHRzIC5zZWFyY2gtcmVzdWx0IHtcbiAgbWFyZ2luLWJvdHRvbTogMjBweDtcbn1cbi5zZWFyY2gtcmVzdWx0cyAuc2VhcmNoLXJlc3VsdCBhIHtcbiAgZm9udC1zaXplOiAxLjVyZW07XG59XG4uZWxsaXBzaXplIHtcbiAgd2hpdGUtc3BhY2U6IG5vd3JhcDtcbiAgb3ZlcmZsb3c6IGhpZGRlbjtcbiAgdGV4dC1vdmVyZmxvdzogZWxsaXBzaXM7XG59XG4ubmF2YmFyLWRlZmF1bHQgLmNvbGxhcHNlOm5vdCguaW4pIC5uYXZiYXItbmF2ID4gLmFjdGl2ZSA+IGEsXG4ubmF2YmFyLWRlZmF1bHQgLmNvbGxhcHNlOm5vdCguaW4pIC5uYXZiYXItbmF2ID4gLmFjdGl2ZSA+IGE6aG92ZXIsXG4ubmF2YmFyLWRlZmF1bHQgLmNvbGxhcHNlOm5vdCguaW4pIC5uYXZiYXItbmF2ID4gLmFjdGl2ZSA+IGE6Zm9jdXMge1xuICBib3JkZXItYm90dG9tOiAzcHggc29saWQ7XG4gIHBhZGRpbmctYm90dG9tOiAxMnB4O1xufVxuLm5hdmJhci1kZWZhdWx0IC5uYXZiYXItaGVhZGVyIHtcbiAgcGFkZGluZzogMCAxMHB4O1xufVxuLm5hdmJhci1kZWZhdWx0IC5uYXZiYXItYnJhbmQge1xuICBiYWNrZ3JvdW5kOiB0cmFuc3BhcmVudCB1cmwoL2ltZy9sb2dvLnN2Zykgbm8tcmVwZWF0IDUwJSA1MCU7XG4gIGJhY2tncm91bmQtc2l6ZTogY29udGFpbjtcbn1cbi5uYXZiYXItZGVmYXVsdCAubmF2YmFyLWJyYW5kLnNwaW4sXG4ubmF2YmFyLWRlZmF1bHQgLm5hdmJhci1icmFuZDpob3Zlcjpub3QoLnNwaW4pIHtcbiAgLXdlYmtpdC1hbmltYXRpb246IHNwaW4gNHMgbGluZWFyIGluZmluaXRlO1xuICAtbW96LWFuaW1hdGlvbjogc3BpbiA0cyBsaW5lYXIgaW5maW5pdGU7XG4gIGFuaW1hdGlvbjogc3BpbiA0cyBsaW5lYXIgaW5maW5pdGU7XG59XG4ucm93LXNwYWNlZCB7XG4gIG1hcmdpbi10b3A6IDE1cHg7XG4gIG1hcmdpbi1ib3R0b206IDE1cHg7XG59XG4uZm9vdGVyIHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICBib3R0b206IDA7XG4gIHdpZHRoOiAxMDAlO1xuICAvKiBTZXQgdGhlIGZpeGVkIGhlaWdodCBvZiB0aGUgZm9vdGVyIGhlcmUgKi9cbiAgaGVpZ2h0OiA1MHB4O1xuICBib3JkZXItdG9wLXdpZHRoOiAxcHg7XG4gIGJvcmRlci10b3Atc3R5bGU6IHNvbGlkO1xufVxuLm5vc2VsZWN0IHtcbiAgLXdlYmtpdC10b3VjaC1jYWxsb3V0OiBub25lO1xuICAtd2Via2l0LXVzZXItc2VsZWN0OiBub25lO1xuICAtbW96LXVzZXItc2VsZWN0OiBub25lO1xuICAtbXMtdXNlci1zZWxlY3Q6IG5vbmU7XG4gIHVzZXItc2VsZWN0OiBub25lO1xuICBjdXJzb3I6IGRlZmF1bHQ7XG59XG5ALW1vei1rZXlmcmFtZXMgc3BpbiB7XG4gIDEwMCUge1xuICAgIC1tb3otdHJhbnNmb3JtOiByb3RhdGVZKDM2MGRlZyk7XG4gIH1cbn1cbkAtd2Via2l0LWtleWZyYW1lcyBzcGluIHtcbiAgMTAwJSB7XG4gICAgLXdlYmtpdC10cmFuc2Zvcm06IHJvdGF0ZVkoMzYwZGVnKTtcbiAgfVxufVxuQGtleWZyYW1lcyBzcGluIHtcbiAgMTAwJSB7XG4gICAgLXdlYmtpdC10cmFuc2Zvcm06IHJvdGF0ZVkoMzYwZGVnKTtcbiAgICB0cmFuc2Zvcm06IHJvdGF0ZVkoMzYwZGVnKTtcbiAgfVxufVxuIl0sInNvdXJjZVJvb3QiOiIvc291cmNlLyJ9 */
diff --git a/vipra-ui/css/app.less b/vipra-ui/css/app.less
index df90b128..e6440674 100644
--- a/vipra-ui/css/app.less
+++ b/vipra-ui/css/app.less
@@ -11,13 +11,10 @@ body {
 
 .heading {
   .noselect;
-  font-size: 120px;
-  text-align: center;
   background: transparent url(/img/logo.svg) no-repeat 50% 50%;
   background-size: contain;
-  height: 200px;
-  line-height: 200px;
-  margin: 35px 0;
+  height: 125px;
+  margin: 25px 0;
 }
 
 .search-results {
@@ -81,6 +78,16 @@ body {
   border-top-style: solid;
 }
 
+.loading {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background rgba(0,0,0,0.2);
+
+}
+
 .noselect {
   -webkit-touch-callout: none;
   -webkit-user-select: none;
diff --git a/vipra-ui/html/articles/index.html b/vipra-ui/html/articles/index.html
index 1ac91c5c..9ce005c3 100644
--- a/vipra-ui/html/articles/index.html
+++ b/vipra-ui/html/articles/index.html
@@ -6,4 +6,6 @@
   <li ng-repeat="article in articles">
     <a ui-sref="articles.show({id: article.id})">{{article.title}}</a>
   </li>
-</ul>
\ No newline at end of file
+</ul>
+
+<pagination total="articlesMeta.total" page="page" limit="limit"/>
\ No newline at end of file
diff --git a/vipra-ui/html/articles/show.html b/vipra-ui/html/articles/show.html
index 3800387d..e8ed5e34 100644
--- a/vipra-ui/html/articles/show.html
+++ b/vipra-ui/html/articles/show.html
@@ -4,6 +4,10 @@
 
 <table class="table table-bordered table-condensed">
   <tbody>
+    <tr>
+      <th>ID</th>
+      <td ng-bind="article.id"></td>
+    </tr>
     <tr>
       <th>URL</th>
       <td><a ng-href="{{article.url}}" ng-bind="article.url"></a></td>
diff --git a/vipra-ui/html/directives/pagination.html b/vipra-ui/html/directives/pagination.html
new file mode 100644
index 00000000..3947b26b
--- /dev/null
+++ b/vipra-ui/html/directives/pagination.html
@@ -0,0 +1,15 @@
+<nav ng-show="total > limit">
+  <ul class="pagination">
+    <li ng-class="{disabled:page==1}">
+      <a ui-sref="{page:page==2?null:page-1}" ng-show="page>1">&laquo;</a>
+      <span ng-hide="page>1">&laquo;</span>
+    </li>
+    <li ng-class="{active:p==page}" ng-repeat="p in pages">
+      <a ui-sref="{page:p===1?null:p}" ng-bind="p"></a>
+    </li>
+    <li ng-class="{disabled:page>=maxPage}">
+      <a ui-sref="{page:page+1}" ng-show="page<maxPage">&raquo;</a>
+      <span ng-hide="page<maxPage">&raquo;</span>
+    </li>
+  </ul>
+</nav>
\ No newline at end of file
diff --git a/vipra-ui/html/index.html b/vipra-ui/html/index.html
index b8d0f892..bb5516f3 100644
--- a/vipra-ui/html/index.html
+++ b/vipra-ui/html/index.html
@@ -2,9 +2,7 @@
 
   <div class="row">
     <div class="col-md-12">
-      <div class="heading">
-        'v&#618;pr&#601;
-      </div>
+      <div class="heading"></div>
     </div>
   </div>
 
diff --git a/vipra-ui/html/topics/show.html b/vipra-ui/html/topics/show.html
index e69de29b..08b26339 100644
--- a/vipra-ui/html/topics/show.html
+++ b/vipra-ui/html/topics/show.html
@@ -0,0 +1,39 @@
+<h1 ng-bind="topic.name"></h1>
+
+<table class="table table-bordered table-condensed">
+  <tbody>
+    <tr>
+      <th>ID</th>
+      <td ng-bind="topic.id"></td>
+    </tr>
+    <tr>
+      <th>Index</th>
+      <td ng-bind="topic.index"></td>
+    </tr>
+    <tr>
+      <th>Created</th>
+      <td ng-bind="(topic.created | formatDateTime)"></td>
+    </tr>
+    <tr>
+      <th>Last modified</th>
+      <td ng-bind="(topic.modified | formatDateTime)"></td>
+    </tr>
+  </tbody>
+</table>
+
+<h4>Words</h4>
+
+<table class="table table-bordered table-condensed">
+  <thead>
+    <tr>
+      <th>Word</th>
+      <th>Likeliness</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr ng-repeat="word in topic.words">
+      <td><a ui-sref="words.show({id:word.word})" ng-bind="word.word"></a></td>
+      <td ng-bind="word.likeliness"></td>
+    </tr>
+  </tbody>
+</table>
\ No newline at end of file
diff --git a/vipra-ui/js/app.js b/vipra-ui/js/app.js
index a48c4a7e..fcb3999b 100644
--- a/vipra-ui/js/app.js
+++ b/vipra-ui/js/app.js
@@ -34,7 +34,7 @@
     });
 
     $stateProvider.state('articles.index', {
-      url: '',
+      url: '?page',
       templateUrl: tplBase + '/articles/index.html',
       controller: 'ArticlesIndexController'
     });
@@ -54,7 +54,7 @@
     });
 
     $stateProvider.state('topics.index', {
-      url: '',
+      url: '?page',
       templateUrl: tplBase + '/topics/index.html',
       controller: 'TopicsIndexController'
     });
@@ -74,7 +74,7 @@
     });
 
     $stateProvider.state('words.index', {
-      url: '',
+      url: '?page',
       templateUrl: tplBase + '/words/index.html',
       controller: 'WordsIndexController'
     });
diff --git a/vipra-ui/js/controllers.js b/vipra-ui/js/controllers.js
index 14b33503..2572c6ae 100644
--- a/vipra-ui/js/controllers.js
+++ b/vipra-ui/js/controllers.js
@@ -6,7 +6,9 @@
   ]);
 
   var latestItemsCount = 3,
-      searchItemsCount = 10;
+      searchItemsCount = 10,
+      pageSize = 100,
+      paginationPadding = 4;
 
   app.controller('IndexController', ['$scope', '$location', 'ArticleFactory', 'TopicFactory', 'WordFactory', 'SearchFactory',
     function($scope, $location, ArticleFactory, TopicFactory, WordFactory, SearchFactory) {
@@ -40,10 +42,16 @@
    * ARTICLES
    */
 
-  app.controller('ArticlesIndexController', ['$scope', 'ArticleFactory',
-    function($scope, ArticleFactory) {
+  app.controller('ArticlesIndexController', ['$scope', '$stateParams', 'ArticleFactory',
+    function($scope, $stateParams, ArticleFactory) {
+
+    $scope.page = Math.max($stateParams.page || 1, 1);
+    $scope.limit = pageSize;
 
-    ArticleFactory.query(function(response) {
+    ArticleFactory.query({
+      skip: ($scope.page-1)*pageSize,
+      limit: pageSize
+    }, function(response) {
       $scope.articles = response.data;
       $scope.articlesMeta = response.meta;
       $scope.queryTime = response.$queryTime;
@@ -114,4 +122,33 @@
 
   }]);
 
+  /*
+   * DIRECTIVES
+   */
+
+   app.controller('PaginationController', ['$scope',
+    function($scope) {
+
+      $scope.calculatePages = function() {
+        var pages = [],
+            max   = Math.ceil($scope.total/$scope.limit*1.0),
+            start = Math.max($scope.page - paginationPadding, 1),
+            end   = Math.min(Math.max($scope.page + paginationPadding, start + paginationPadding * 2), max);
+        for(var i = start; i <= end; i++) {
+          pages.push(i);
+        }
+        $scope.pages = pages;
+        $scope.maxPage = max;
+      };
+
+      $scope.$watchGroup(['total', 'page', 'limit'], function(newVal, oldVal) {
+        if(!angular.equals(newVal, oldVal)) {
+          $scope.calculatePages();
+        }
+      });
+
+      $scope.calculatePages();
+
+   }]);
+
 })();
\ No newline at end of file
diff --git a/vipra-ui/js/directives.js b/vipra-ui/js/directives.js
index e421c434..b0493828 100644
--- a/vipra-ui/js/directives.js
+++ b/vipra-ui/js/directives.js
@@ -1,6 +1,8 @@
 (function() {
 
-  var app = angular.module('vipra.directives', []);
+  var app = angular.module('vipra.directives', [
+    'ui.router'
+  ]);
 
   app.directive('topicLink', function() {
     return {
@@ -22,4 +24,18 @@
     };
   });
 
+  app.directive('pagination', function() {
+    return {
+      restrict: 'E',
+      replace: true,
+      scope: {
+        total: '=',
+        page: '=',
+        limit: '='
+      },
+      controller: 'PaginationController',
+      templateUrl: 'html/directives/pagination.html'
+    };
+  });
+
 })();
\ No newline at end of file
diff --git a/vipra-util/src/main/java/de/vipra/util/an/QueryIgnore.java b/vipra-util/src/main/java/de/vipra/util/an/QueryIgnore.java
index 422439e7..8e2c7df1 100644
--- a/vipra-util/src/main/java/de/vipra/util/an/QueryIgnore.java
+++ b/vipra-util/src/main/java/de/vipra/util/an/QueryIgnore.java
@@ -13,4 +13,6 @@ public @interface QueryIgnore {
 
 	public boolean multi() default false;
 
+	public boolean all() default false;
+
 }
diff --git a/vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java b/vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java
index a55d0f8b..d5dd234f 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java
@@ -5,6 +5,7 @@ import java.io.IOException;
 import java.io.Serializable;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
@@ -26,6 +27,7 @@ import de.vipra.util.FileUtils;
 import de.vipra.util.MongoUtils;
 import de.vipra.util.NestedMap;
 import de.vipra.util.StringUtils;
+import de.vipra.util.an.ElasticIndex;
 import de.vipra.util.an.QueryIgnore;
 
 @SuppressWarnings("serial")
@@ -38,13 +40,19 @@ public class ArticleFull extends FileModel<ObjectId> implements Serializable {
 	@Id
 	private ObjectId id;
 
+	@ElasticIndex("title")
 	private String title;
 
 	@QueryIgnore(multi = true)
 	private String text;
 
+	@ElasticIndex("text")
+	@QueryIgnore(multi = true)
+	private String processedText;
+
 	private String url;
 
+	@ElasticIndex("date")
 	private Date date;
 
 	@Embedded
@@ -95,6 +103,14 @@ public class ArticleFull extends FileModel<ObjectId> implements Serializable {
 		this.text = text;
 	}
 
+	public String getProcessedText() {
+		return processedText;
+	}
+
+	public void setProcessedText(String processedText) {
+		this.processedText = processedText;
+	}
+
 	public String getUrl() {
 		return url;
 	}
@@ -128,6 +144,18 @@ public class ArticleFull extends FileModel<ObjectId> implements Serializable {
 		this.topics = topics;
 	}
 
+	@ElasticIndex("topics")
+	public String[] serializeTopics() {
+		List<TopicRef> refs = getTopics();
+		if (refs == null)
+			return new String[0];
+		List<String> topics = new ArrayList<>(refs.size());
+		for (TopicRef ref : refs) {
+			topics.add(ref.getTopic().getName());
+		}
+		return topics.toArray(new String[topics.size()]);
+	}
+
 	public ArticleStats getStats() {
 		return stats;
 	}
diff --git a/vipra-util/src/main/java/de/vipra/util/model/Topic.java b/vipra-util/src/main/java/de/vipra/util/model/Topic.java
index 5f097b90..760510f2 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/Topic.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/Topic.java
@@ -25,6 +25,11 @@ public class Topic implements Model<ObjectId>, Serializable {
 		this.id = id;
 	}
 
+	public Topic(ObjectId id, String name) {
+		this.id = id;
+		this.name = name;
+	}
+
 	@Override
 	public ObjectId getId() {
 		return id;
diff --git a/vipra-util/src/main/java/de/vipra/util/model/TopicRef.java b/vipra-util/src/main/java/de/vipra/util/model/TopicRef.java
index 35d8755e..920abdce 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/TopicRef.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/TopicRef.java
@@ -6,8 +6,6 @@ import org.mongodb.morphia.annotations.Embedded;
 import org.mongodb.morphia.annotations.Reference;
 import org.mongodb.morphia.annotations.Transient;
 
-import de.vipra.util.MongoUtils;
-
 @SuppressWarnings("serial")
 @Embedded
 public class TopicRef implements Comparable<TopicRef>, Serializable {
@@ -26,10 +24,6 @@ public class TopicRef implements Comparable<TopicRef>, Serializable {
 		this.topicIndex = index;
 	}
 
-	public void setTopicId(String id) {
-		this.topic = new Topic(MongoUtils.objectId(id));
-	}
-
 	public int getCount() {
 		return count;
 	}
diff --git a/vipra-util/src/main/java/de/vipra/util/service/DatabaseService.java b/vipra-util/src/main/java/de/vipra/util/service/DatabaseService.java
index d68e1bcc..da1e780a 100644
--- a/vipra-util/src/main/java/de/vipra/util/service/DatabaseService.java
+++ b/vipra-util/src/main/java/de/vipra/util/service/DatabaseService.java
@@ -35,9 +35,9 @@ public class DatabaseService<Type extends Model<IdType>, IdType> implements Serv
 		for (Field field : fields) {
 			QueryIgnore qi = field.getDeclaredAnnotation(QueryIgnore.class);
 			if (qi != null) {
-				if (qi.single())
+				if (qi.single() || qi.all())
 					ignoreSingle.add(field.getName());
-				if (qi.multi())
+				if (qi.multi() || qi.all())
 					ignoreMulti.add(field.getName());
 			}
 		}
-- 
GitLab