diff --git a/.gitignore b/.gitignore
index b1560f30bd683b1965da24debdc6572a10556488..c1b2f25afd8086eb8bdcfae699eb60df23b123fa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@
 .vagrant/
 vm/webapps/
 vm/data/
+vm/test/
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/Main.java b/vipra-cmd/src/main/java/de/vipra/cmd/Main.java
index 5aa791b0622b3c23c716b7aa598f51d446ef02a3..f1ae2a5e2c6832060e2fefb75f808064b62ae224 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/Main.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/Main.java
@@ -1,6 +1,7 @@
 package de.vipra.cmd;
 
 import static de.vipra.cmd.CmdOptions.OPT_CLEAR;
+import static de.vipra.cmd.CmdOptions.OPT_CONFIG;
 import static de.vipra.cmd.CmdOptions.OPT_DEBUG;
 import static de.vipra.cmd.CmdOptions.OPT_DEFAULTS;
 import static de.vipra.cmd.CmdOptions.OPT_HELP;
@@ -9,7 +10,6 @@ import static de.vipra.cmd.CmdOptions.OPT_SHELL;
 import static de.vipra.cmd.CmdOptions.OPT_SILENT;
 import static de.vipra.cmd.CmdOptions.OPT_STATS;
 import static de.vipra.cmd.CmdOptions.OPT_TEST;
-import static de.vipra.cmd.CmdOptions.OPT_CONFIG;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/ex/AnalyzerException.java b/vipra-cmd/src/main/java/de/vipra/cmd/ex/AnalyzerException.java
new file mode 100644
index 0000000000000000000000000000000000000000..70b6207b65a7e148c98cb7344b7535bc4bb529df
--- /dev/null
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/ex/AnalyzerException.java
@@ -0,0 +1,15 @@
+package de.vipra.cmd.ex;
+
+public class AnalyzerException extends Exception {
+
+	private static final long serialVersionUID = 1L;
+
+	public AnalyzerException(String msg) {
+		super(msg);
+	}
+
+	public AnalyzerException(Exception e) {
+		super(e);
+	}
+
+}
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/ex/LDAAnalyzerException.java b/vipra-cmd/src/main/java/de/vipra/cmd/ex/LDAAnalyzerException.java
deleted file mode 100644
index bf55ee835d9827c5c78d77a099b0f0aedae22078..0000000000000000000000000000000000000000
--- a/vipra-cmd/src/main/java/de/vipra/cmd/ex/LDAAnalyzerException.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package de.vipra.cmd.ex;
-
-public class LDAAnalyzerException extends Exception {
-
-	private static final long serialVersionUID = 1L;
-
-	public LDAAnalyzerException(String msg) {
-		super(msg);
-	}
-
-	public LDAAnalyzerException(Exception e) {
-		super(e);
-	}
-
-}
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/ex/PreprocessorException.java b/vipra-cmd/src/main/java/de/vipra/cmd/ex/PreprocessorException.java
deleted file mode 100644
index 1b4f6ade0e0a299e38c65220ab07ec95201ba605..0000000000000000000000000000000000000000
--- a/vipra-cmd/src/main/java/de/vipra/cmd/ex/PreprocessorException.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package de.vipra.cmd.ex;
-
-public class PreprocessorException extends Exception {
-
-	private static final long serialVersionUID = 1L;
-
-	public PreprocessorException(String msg) {
-		super(msg);
-	}
-
-	public PreprocessorException(Exception e) {
-		super(e);
-	}
-
-}
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/ex/ProcessorException.java b/vipra-cmd/src/main/java/de/vipra/cmd/ex/ProcessorException.java
new file mode 100644
index 0000000000000000000000000000000000000000..4889e6fcac0f66993e4126b836a4991034c5d6d1
--- /dev/null
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/ex/ProcessorException.java
@@ -0,0 +1,15 @@
+package de.vipra.cmd.ex;
+
+public class ProcessorException extends Exception {
+
+	private static final long serialVersionUID = 1L;
+
+	public ProcessorException(String msg) {
+		super(msg);
+	}
+
+	public ProcessorException(Exception e) {
+		super(e);
+	}
+
+}
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/file/DynNMFFilebase.java b/vipra-cmd/src/main/java/de/vipra/cmd/file/DynNMFFilebase.java
new file mode 100644
index 0000000000000000000000000000000000000000..321d542b606261816af1d6375dcaac625437aab0
--- /dev/null
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/file/DynNMFFilebase.java
@@ -0,0 +1,63 @@
+package de.vipra.cmd.file;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import de.vipra.cmd.ex.FilebaseException;
+import de.vipra.util.Constants.WindowResolution;
+import de.vipra.util.FileUtils;
+import de.vipra.util.model.ArticleFull;
+
+public class DynNMFFilebase extends Filebase {
+
+	private final File modelDir;
+	private final WindowResolution windowResolution;
+	private final Map<String, File> dirMap;
+
+	public DynNMFFilebase(File dataDir, WindowResolution windowResolution) throws FilebaseException {
+		super(dataDir, "dynlda");
+		this.modelDir = super.getModelDir();
+		this.windowResolution = windowResolution;
+		this.dirMap = new HashMap<>();
+	}
+
+	@Override
+	public void write(List<ArticleFull> articles) throws IOException {
+		if (!articles.isEmpty()) {
+			for (ArticleFull article : articles) {
+				File windowDir = getWindowDir(article.getDate());
+				File articleFile = new File(windowDir, article.getId().toString());
+				FileUtils.writeStringToFile(articleFile, article.getTitle() + "\n" + article.getProcessedText());
+			}
+		}
+	}
+
+	private File getWindowDir(Date date) {
+		Calendar c = new GregorianCalendar();
+		c.setTime(date);
+		String dirName = "" + c.get(Calendar.YEAR);
+		switch (windowResolution) {
+			case MONTHLY:
+				break;
+			case QUARTERLY:
+				break;
+			case YEARLY:
+			default:
+				break;
+		}
+		File dir = dirMap.get(dirName);
+		if (dir == null) {
+			dir = new File(modelDir, dirName);
+			dir.mkdirs();
+			dirMap.put(dirName, dir);
+		}
+		return dir;
+	}
+
+}
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 9730d4059970d6f35e2a2ee82e34164457b3042c..58c0f596a17ec749b88aa4768561d1fc1869e406 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
@@ -3,6 +3,8 @@ package de.vipra.cmd.file;
 import java.io.Closeable;
 import java.io.File;
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
 
 import de.vipra.cmd.ex.FilebaseException;
 import de.vipra.util.Config;
@@ -12,12 +14,16 @@ import de.vipra.util.model.ArticleFull;
 
 public abstract class Filebase implements Closeable {
 
+	private final String modelName;
 	private final File modelDir;
-	private final File modelFile;
 	private final FilebaseIndex index;
 	private final FilebaseVocabulary vocab;
+	private final List<ArticleFull> articles;
+
+	private final int bufferMaxSize = 100;
 
 	public Filebase(File dataDir, String modelName) throws FilebaseException {
+		this.modelName = modelName;
 		this.modelDir = new File(dataDir, modelName);
 		if (!modelDir.exists()) {
 			if (!modelDir.mkdirs()) {
@@ -25,16 +31,20 @@ public abstract class Filebase implements Closeable {
 			}
 		}
 		try {
-			this.modelFile = new File(modelDir, modelName);
 			this.index = new FilebaseIndex(new File(modelDir, Constants.INDEX_FILE));
 			this.vocab = new FilebaseVocabulary(new File(modelDir, Constants.VOCAB_FILE));
 		} catch (IOException e) {
 			throw new FilebaseException("could not read index: " + e.getMessage());
 		}
+		this.articles = new ArrayList<>(bufferMaxSize);
+	}
+
+	public File getModelDir() {
+		return modelDir;
 	}
 
 	public File getModelFile() {
-		return modelFile;
+		return new File(modelDir, modelName);
 	}
 
 	public FilebaseIndex getIndex() {
@@ -45,30 +55,43 @@ public abstract class Filebase implements Closeable {
 		return vocab;
 	}
 
-	public void remove(ArticleFull article) throws FilebaseException {
-		remove(article.getId().toString());
+	public List<ArticleFull> getArticles() {
+		return articles;
 	}
 
 	@Override
 	public void close() throws IOException {
-		write();
+		write(articles);
 		index.close();
 		vocab.close();
 	}
 
-	public abstract void add(ArticleFull article) throws FilebaseException;
+	public void add(ArticleFull article) throws FilebaseException {
+		String[] words = article.getProcessedText().split("\\s+");
+		vocab.addVocabulary(words);
+		index.add(article.getId().toString());
+		articles.add(article);
 
-	public abstract void remove(String id) throws FilebaseException;
+		if (articles.size() >= bufferMaxSize) {
+			try {
+				write(articles);
+			} catch (IOException e) {
+				throw new FilebaseException(e);
+			}
+		}
+	}
 
-	public abstract void write() throws IOException;
+	public abstract void write(List<ArticleFull> articles) throws IOException;
 
 	public static Filebase getFilebase(Config config) throws FilebaseException, ConfigException {
 		File dataDir = config.getDataDirectory();
 		switch (config.analyzer) {
+			case DYNNMF:
+				return new DynNMFFilebase(dataDir, config.windowResolution);
 			case JGIBB:
-			case DEFAULT:
-			default:
 				return new JGibbFilebase(dataDir);
+			default:
+				return null;
 		}
 	}
 
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 1fb52be44e2265bd33d3730eee3f7098e9ecbb4e..0883a698b4212a3b37a8161dd9817416fc85ef4f 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
@@ -3,53 +3,22 @@ package de.vipra.cmd.file;
 import java.io.File;
 import java.io.IOException;
 import java.io.RandomAccessFile;
-import java.util.ArrayList;
 import java.util.List;
 
 import de.vipra.cmd.ex.FilebaseException;
-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<ArticleFull> articles;
-
-	private final int bufferMaxSize = 100;
 
 	public JGibbFilebase(File dataDir) throws FilebaseException {
 		super(dataDir, "jgibb");
 		this.modelFile = getModelFile();
-		this.index = getIndex();
-		this.vocab = getVocab();
-		this.articles = new ArrayList<>();
-	}
-
-	@Override
-	public void add(ArticleFull article) throws FilebaseException {
-		String[] words = article.getProcessedText().split("\\s+");
-		vocab.addVocabulary(words);
-		index.add(article.getId().toString());
-		articles.add(article);
-
-		if (articles.size() >= bufferMaxSize) {
-			try {
-				write();
-			} catch (IOException e) {
-				throw new FilebaseException(e);
-			}
-		}
-	}
-
-	@Override
-	public void remove(String id) {
-		throw new NotImplementedException();
 	}
 
 	@Override
-	public void write() throws IOException {
+	public void write(List<ArticleFull> articles) throws IOException {
 		if (!articles.isEmpty()) {
 			boolean linesep = modelFile.exists();
 			RandomAccessFile raf = new RandomAccessFile(modelFile, "rw");
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/lda/LDAAnalyzer.java b/vipra-cmd/src/main/java/de/vipra/cmd/lda/Analyzer.java
similarity index 69%
rename from vipra-cmd/src/main/java/de/vipra/cmd/lda/LDAAnalyzer.java
rename to vipra-cmd/src/main/java/de/vipra/cmd/lda/Analyzer.java
index d69375d39b31768cfb3fa1f06e56d43e519ebb1b..0e1c87c7ffe1245794732209401bdd6e0c731f81 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/lda/LDAAnalyzer.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/lda/Analyzer.java
@@ -2,18 +2,18 @@ package de.vipra.cmd.lda;
 
 import java.util.List;
 
-import de.vipra.cmd.ex.LDAAnalyzerException;
+import de.vipra.cmd.ex.AnalyzerException;
 import de.vipra.util.Config;
 import de.vipra.util.ConvertStream;
 import de.vipra.util.WordMap;
 import de.vipra.util.model.TopicFull;
 import de.vipra.util.model.TopicRef;
 
-public abstract class LDAAnalyzer {
+public abstract class Analyzer {
 
 	private final String name;
 
-	protected LDAAnalyzer(String name) {
+	protected Analyzer(String name) {
 		this.name = name;
 	}
 
@@ -21,9 +21,9 @@ public abstract class LDAAnalyzer {
 		return name;
 	}
 
-	public abstract void init(Config config, WordMap wordMap) throws LDAAnalyzerException;
+	public abstract void init(Config config, WordMap wordMap) throws AnalyzerException;
 
-	public abstract void analyze() throws LDAAnalyzerException;
+	public abstract void analyze() throws AnalyzerException;
 
 	/**
 	 * Returns a converting stream of topics, read from the topic definition
@@ -31,9 +31,9 @@ public abstract class LDAAnalyzer {
 	 * assigned to that topic with a certain likeliness.
 	 * 
 	 * @return topic definition stream
-	 * @throws LDAAnalyzerException
+	 * @throws AnalyzerException
 	 */
-	public abstract ConvertStream<TopicFull> getTopicDefinitions() throws LDAAnalyzerException;
+	public abstract ConvertStream<TopicFull> getTopicDefinitions() throws AnalyzerException;
 
 	/**
 	 * Returns a converting stream of lists of topic references. Normally, topic
@@ -42,18 +42,21 @@ public abstract class LDAAnalyzer {
 	 * 
 	 * @return stream of lists of topic references per document (ordered by
 	 *         index)
-	 * @throws LDAAnalyzerException
+	 * @throws AnalyzerException
 	 */
-	public abstract ConvertStream<List<TopicRef>> getTopics() throws LDAAnalyzerException;
+	public abstract ConvertStream<List<TopicRef>> getTopics() throws AnalyzerException;
 
-	public static LDAAnalyzer getAnalyzer(Config config, WordMap wordMap) throws LDAAnalyzerException {
-		LDAAnalyzer analyzer = null;
+	public static Analyzer getAnalyzer(Config config, WordMap wordMap) throws AnalyzerException {
+		Analyzer analyzer = null;
 		switch (config.analyzer) {
+			case DYNNMF:
+				analyzer = new DynNMFAnalyzer();
+				break;
 			case JGIBB:
-			case DEFAULT:
-			default:
-				analyzer = new JGibbLDAAnalyzer();
+				analyzer = new JGibbAnalyzer();
 				break;
+			default:
+				return null;
 		}
 		analyzer.init(config, wordMap);
 		return analyzer;
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/lda/DynNMFAnalyzer.java b/vipra-cmd/src/main/java/de/vipra/cmd/lda/DynNMFAnalyzer.java
new file mode 100644
index 0000000000000000000000000000000000000000..8112c8667f6f72bc11332b8ef2424bd34f04c659
--- /dev/null
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/lda/DynNMFAnalyzer.java
@@ -0,0 +1,42 @@
+package de.vipra.cmd.lda;
+
+import java.util.List;
+
+import de.vipra.cmd.ex.AnalyzerException;
+import de.vipra.util.Config;
+import de.vipra.util.ConvertStream;
+import de.vipra.util.WordMap;
+import de.vipra.util.model.TopicFull;
+import de.vipra.util.model.TopicRef;
+
+public class DynNMFAnalyzer extends Analyzer {
+
+	protected DynNMFAnalyzer() {
+		super("Dynamic NMF Analyzer");
+	}
+
+	@Override
+	public void init(Config config, WordMap wordMap) throws AnalyzerException {
+		// TODO Auto-generated method stub
+
+	}
+
+	@Override
+	public void analyze() throws AnalyzerException {
+		// TODO Auto-generated method stub
+
+	}
+
+	@Override
+	public ConvertStream<TopicFull> getTopicDefinitions() throws AnalyzerException {
+		// TODO Auto-generated method stub
+		return null;
+	}
+
+	@Override
+	public ConvertStream<List<TopicRef>> getTopics() throws AnalyzerException {
+		// TODO Auto-generated method stub
+		return null;
+	}
+
+}
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/lda/JGibbLDAAnalyzer.java b/vipra-cmd/src/main/java/de/vipra/cmd/lda/JGibbAnalyzer.java
similarity index 86%
rename from vipra-cmd/src/main/java/de/vipra/cmd/lda/JGibbLDAAnalyzer.java
rename to vipra-cmd/src/main/java/de/vipra/cmd/lda/JGibbAnalyzer.java
index 65a012c8f48861a05dd795db154ee25d73fc6c7f..d1c486c15eb3dfe9d76e99c3150a1137b7a32a79 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/lda/JGibbLDAAnalyzer.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/lda/JGibbAnalyzer.java
@@ -12,7 +12,7 @@ import java.util.Map.Entry;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
-import de.vipra.cmd.ex.LDAAnalyzerException;
+import de.vipra.cmd.ex.AnalyzerException;
 import de.vipra.util.Config;
 import de.vipra.util.Constants;
 import de.vipra.util.ConvertStream;
@@ -29,9 +29,9 @@ import jgibblda.Inferencer;
 import jgibblda.LDACmdOption;
 import jgibblda.Model;
 
-public class JGibbLDAAnalyzer extends LDAAnalyzer {
+public class JGibbAnalyzer extends Analyzer {
 
-	public static final Logger log = LogManager.getLogger(JGibbLDAAnalyzer.class);
+	public static final Logger log = LogManager.getLogger(JGibbAnalyzer.class);
 	public static final String NAME = "jgibb";
 
 	private File dataDir;
@@ -40,18 +40,18 @@ public class JGibbLDAAnalyzer extends LDAAnalyzer {
 	private LDACmdOption options;
 	private WordMap wordMap;
 
-	protected JGibbLDAAnalyzer() {
+	protected JGibbAnalyzer() {
 		super("JGibb Analyzer");
 	}
 
 	@Override
-	public void init(Config config, WordMap wordMap) throws LDAAnalyzerException {
+	public void init(Config config, WordMap wordMap) throws AnalyzerException {
 		options = new LDACmdOption();
 
 		try {
 			dataDir = config.getDataDirectory();
 		} catch (ConfigException e) {
-			throw new LDAAnalyzerException(e);
+			throw new AnalyzerException(e);
 		}
 
 		modelDir = new File(dataDir, NAME);
@@ -83,15 +83,15 @@ public class JGibbLDAAnalyzer extends LDAAnalyzer {
 	}
 
 	@Override
-	public void analyze() throws LDAAnalyzerException {
+	public void analyze() throws AnalyzerException {
 		if (!modelFile.exists()) {
-			throw new LDAAnalyzerException("model file does not exist: " + modelFile.getAbsolutePath());
+			throw new AnalyzerException("model file does not exist: " + modelFile.getAbsolutePath());
 		}
 		estimate();
 	}
 
 	@Override
-	public ConvertStream<TopicFull> getTopicDefinitions() throws LDAAnalyzerException {
+	public ConvertStream<TopicFull> getTopicDefinitions() throws AnalyzerException {
 		File twords = new File(modelDir, NAME + ".twords");
 		try {
 			return new ConvertStream<TopicFull>(twords) {
@@ -139,12 +139,12 @@ public class JGibbLDAAnalyzer extends LDAAnalyzer {
 				}
 			};
 		} catch (FileNotFoundException e) {
-			throw new LDAAnalyzerException(e);
+			throw new AnalyzerException(e);
 		}
 	}
 
 	@Override
-	public ConvertStream<List<TopicRef>> getTopics() throws LDAAnalyzerException {
+	public ConvertStream<List<TopicRef>> getTopics() throws AnalyzerException {
 		File tassign = new File(modelDir, NAME + ".tassign");
 		try {
 			return new ConvertStream<List<TopicRef>>(tassign) {
@@ -177,7 +177,7 @@ public class JGibbLDAAnalyzer extends LDAAnalyzer {
 				}
 			};
 		} catch (FileNotFoundException e) {
-			throw new LDAAnalyzerException(e);
+			throw new AnalyzerException(e);
 		}
 	}
 
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 4d87987548557d5fc715bf4632b9ef68200e28f2..6eeed9e277c5f9f67e1c2cb5ea10ada951aadf6f 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
@@ -20,7 +20,7 @@ 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.lda.Analyzer;
 import de.vipra.cmd.text.ProcessedText;
 import de.vipra.cmd.text.Processor;
 import de.vipra.util.Config;
@@ -58,7 +58,7 @@ public class ImportCommand implements Command {
 	private Filebase filebase;
 	private Processor preprocessor;
 	private WordMap wordMap;
-	private LDAAnalyzer analyzer;
+	private Analyzer analyzer;
 	private Client elasticClient;
 	private ElasticSerializer<ArticleFull> elasticSerializer;
 
@@ -187,7 +187,7 @@ public class ImportCommand implements Command {
 		filebase = Filebase.getFilebase(config);
 		preprocessor = Processor.getProcessor(config);
 		wordMap = new WordMap(dbWords);
-		analyzer = LDAAnalyzer.getAnalyzer(config, wordMap);
+		analyzer = Analyzer.getAnalyzer(config, wordMap);
 		elasticClient = ESClient.getClient(config);
 		elasticSerializer = new ElasticSerializer<>(ArticleFull.class);
 
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/text/CoreNLPProcessor.java b/vipra-cmd/src/main/java/de/vipra/cmd/text/CoreNLPProcessor.java
index b2b4605beea50cbb13d183ff17a2d21603920ec7..620c9189c615474b86806761253ce7503e52f203 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/text/CoreNLPProcessor.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/text/CoreNLPProcessor.java
@@ -3,7 +3,7 @@ package de.vipra.cmd.text;
 import java.util.List;
 import java.util.Properties;
 
-import de.vipra.cmd.ex.PreprocessorException;
+import de.vipra.cmd.ex.ProcessorException;
 import edu.stanford.nlp.ling.CoreAnnotations.SentencesAnnotation;
 import edu.stanford.nlp.ling.CoreAnnotations.TokensAnnotation;
 import edu.stanford.nlp.ling.CoreLabel;
@@ -28,7 +28,7 @@ public class CoreNLPProcessor extends Processor {
 	}
 
 	@Override
-	public ProcessedText process(String input) throws PreprocessorException {
+	public ProcessedText process(String input) throws ProcessorException {
 		Annotation doc = new Annotation(input.toLowerCase());
 		nlp.annotate(doc);
 		StringBuilder sb = new StringBuilder();
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/text/Processor.java b/vipra-cmd/src/main/java/de/vipra/cmd/text/Processor.java
index 51498e5be87b107f348bcee178519187f44a6132..6ea52b604972b155160ccdbfd00bbb64d6225d3a 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/text/Processor.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/text/Processor.java
@@ -2,7 +2,7 @@ package de.vipra.cmd.text;
 
 import java.util.List;
 
-import de.vipra.cmd.ex.PreprocessorException;
+import de.vipra.cmd.ex.ProcessorException;
 import de.vipra.util.Config;
 import de.vipra.util.Constants;
 
@@ -18,7 +18,7 @@ public abstract class Processor {
 		return name;
 	}
 
-	public abstract ProcessedText process(String input) throws PreprocessorException;
+	public abstract ProcessedText process(String input) throws ProcessorException;
 
 	public static Processor getProcessor(Config config) {
 		List<String> stopWords = Constants.STOPWORDS;
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 faf5f0e78b3a154d089ecc8933e54034c46fe2f3..49f27b9d0f090c9fe9bc7d7703b68e46cbb750d7 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
@@ -29,7 +29,6 @@ import de.vipra.rest.model.Wrapper;
 import de.vipra.util.Config;
 import de.vipra.util.ESClient;
 import de.vipra.util.MongoUtils;
-import de.vipra.util.StringUtils;
 import de.vipra.util.ex.ConfigException;
 import de.vipra.util.model.ArticleFull;
 
@@ -50,8 +49,7 @@ public class SearchResource {
 	@GET
 	@Produces(MediaType.APPLICATION_JSON)
 	public Response doSearch(@QueryParam("skip") Integer skip, @QueryParam("limit") Integer limit,
-			@QueryParam("excerpt") Integer excerpt, @QueryParam("fields") String fields,
-			@QueryParam("query") String query) {
+			@QueryParam("fields") String fields, @QueryParam("query") String query) {
 		Wrapper<List<ArticleFull>> res = new Wrapper<>();
 
 		if (skip == null || skip < 0)
@@ -60,9 +58,6 @@ public class SearchResource {
 		if (limit == null || limit < 0)
 			limit = 20;
 
-		if (excerpt == null || excerpt < 0)
-			excerpt = 250;
-
 		if (query == null || query.isEmpty() || limit == 0)
 			return res.noContent();
 
@@ -88,10 +83,10 @@ public class SearchResource {
 			article.setId(MongoUtils.objectId(hit.getId()));
 			if (allowedFields == null || allowedFields.contains("title"))
 				article.setTitle(source.get("title").toString());
-			if (allowedFields == null || allowedFields.contains("text"))
-				article.setText(StringUtils.ellipsize(source.get("text").toString(), excerpt));
 			if (allowedFields == null || allowedFields.contains("date"))
 				article.setDate(source.get("date").toString());
+			if (allowedFields == null || allowedFields.contains("excerpt"))
+				article.setText(source.get("excerpt").toString());
 			article.addMeta("score", hit.getScore());
 			articles.add(article);
 		}
diff --git a/vipra-ui/css/app.css b/vipra-ui/css/app.css
index 26bd0d5bdfb0ccce4dc5ccb25721f3907329829a..02ae7baff2ce3d300f2abc19665ab11bccdc583e 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;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 */
+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,.navbar-default .navbar-brand.spin,.navbar-default .navbar-brand:hover:not(.spin){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}.navbar-default .navbar-brand.spin.spinner-small,.navbar-default .navbar-brand:hover:not(.spin).spinner-small{padding:20px}.row-spaced{margin-top:15px;margin-bottom:15px}.footer{width:100%;height:50px;border-top-width:1px;border-top-style:solid}.footer,.loading:before{position:absolute;bottom:0}.loading:before{top:0;left:0;right:0;background:rgba(0,0,0,.2);content:" ";z-index:1}.spinner{background:transparent url(/img/logo.svg) no-repeat 50% 50%;background-size:contain;-webkit-animation:a 4s linear infinite;animation:a 4s linear infinite}.spinner.spinner-small{padding:20px}.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,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImFwcC5sZXNzIiwiYXBwLmNzcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxLQUNFLGtCQUFBLEFBQ0EsZUFBQSxDQ0NELEFERUQsS0FDRSxvQkFBQSxBQUVBLGtCQUFBLENDQUQsQURHRCxTQTJGRSwyQkFBQSxBQUNBLHlCQUFBLEFBQ0Esc0JBQUEsQUFDQSxxQkFBQSxBQUNBLGlCQUFBLEFBQ0EsZUFBQSxBQTlGQSw0REFBQSxBQUNBLHdCQUFBLEFBQ0EsYUFBQSxBQUNBLGFBQUEsQ0NJRCxBRERELGdCQUNFLFlBQUEsQ0NHRCxBREpELCtCQUlJLGtCQUFBLENDR0gsQURQRCxpQ0FPTSxnQkFBQSxDQ0dMLEFERUQsV0FDRSxtQkFBQSxBQUNBLGdCQUFBLEFBQ0Esc0JBQUEsQ0NBRCxBRE1LLHVMQUdFLHdCQUFBLEFBQ0EsbUJBQUEsQ0NKUCxBREhELCtCQWFJLGNBQUEsQ0NQSCxBRGFHLGdIQUZBLDREQUFBLEFBQ0EsdUJBQUEsQ0FDQSxBQ0RILGtGRHFDQyx1Q0FBQSxBQUVBLDhCQUFBLENDdkNELEFEeUNDLDhHQUNFLFlBQUEsQ0N0Q0gsQURJRCxZQUNFLGdCQUFBLEFBQ0Esa0JBQUEsQ0NGRCxBREtELFFBR0UsV0FBQSxBQUVBLFlBQUEsQUFDQSxxQkFBQSxBQUNBLHNCQUFBLENDSEQsQURNRCx3QkFURSxrQkFBQSxBQUNBLFFBQUEsQ0FRRixBQ0lDLGdCREZDLE1BQUEsQUFDQSxPQUFBLEFBQ0EsUUFBQSxBQUVBLDBCQUFBLEFBQ0EsWUFBQSxBQUNBLFNBQUEsQ0NKRCxBRE9ELFNBQ0UsNERBQUEsQUFDQSx3QkFBQSxBQUNBLHVDQUFBLEFBRUEsOEJBQUEsQ0NMRCxBRE9DLHVCQUNFLFlBQUEsQ0NMSCxBRFNELFVBQ0UsMkJBQUEsQUFDQSx5QkFBQSxBQUNBLHNCQUFBLEFBQ0EscUJBQUEsQUFDQSxpQkFBQSxBQUNBLGNBQUEsQ0NQRCxBRFdELHFCQUEwQixHQUFPLGdDQUFBLENDRjlCLENBQ0YsQURFRCxhQUFrQixHQUFPLGlDQUFBLEFBQW9DLHdCQUFBLENDRzFELENBQ0YiLCJmaWxlIjoiYXBwLmNzcyIsInNvdXJjZXNDb250ZW50IjpbImh0bWwge1xuICBwb3NpdGlvbjogcmVsYXRpdmU7XG4gIG1pbi1oZWlnaHQ6IDEwMCU7XG59XG5cbmJvZHkge1xuICBwYWRkaW5nLWJvdHRvbTogMjBweDtcbiAgLyogTWFyZ2luIGJvdHRvbSBieSBmb290ZXIgaGVpZ2h0ICovXG4gIG1hcmdpbi1ib3R0b206IDYwcHg7XG59XG5cbi5oZWFkaW5nIHtcbiAgLm5vc2VsZWN0O1xuICBiYWNrZ3JvdW5kOiB0cmFuc3BhcmVudCB1cmwoL2ltZy9sb2dvLnN2Zykgbm8tcmVwZWF0IDUwJSA1MCU7XG4gIGJhY2tncm91bmQtc2l6ZTogY29udGFpbjtcbiAgaGVpZ2h0OiAxMjVweDtcbiAgbWFyZ2luOiAyNXB4IDA7XG59XG5cbi5zZWFyY2gtcmVzdWx0cyB7XG4gIHBhZGRpbmc6IDE1cHg7XG5cbiAgLnNlYXJjaC1yZXN1bHQge1xuICAgIG1hcmdpbi1ib3R0b206IDIwcHg7XG5cbiAgICBhIHtcbiAgICAgIGZvbnQtc2l6ZTogMS41cmVtO1xuICAgIH1cbiAgfVxufVxuXG4uZWxsaXBzaXplIHtcbiAgd2hpdGUtc3BhY2U6IG5vd3JhcDtcbiAgb3ZlcmZsb3c6IGhpZGRlbjtcbiAgdGV4dC1vdmVyZmxvdzogZWxsaXBzaXM7XG59XG5cbi5uYXZiYXItZGVmYXVsdCB7XG4gIC5jb2xsYXBzZTpub3QoLmluKSB7XG4gICAgLm5hdmJhci1uYXYgPiAuYWN0aXZlIHtcbiAgICAgICY+IGEsXG4gICAgICAmPiBhOmhvdmVyLFxuICAgICAgJj4gYTpmb2N1cyB7XG4gICAgICAgIGJvcmRlci1ib3R0b206IDNweCBzb2xpZDtcbiAgICAgICAgcGFkZGluZy1ib3R0b206IDEycHg7XG4gICAgICB9XG4gICAgfVxuICB9XG5cbiAgLm5hdmJhci1oZWFkZXIge1xuICAgIHBhZGRpbmc6IDAgMTBweDtcbiAgfVxuXG4gIC5uYXZiYXItYnJhbmQge1xuICAgIGJhY2tncm91bmQ6IHRyYW5zcGFyZW50IHVybCgvaW1nL2xvZ28uc3ZnKSBuby1yZXBlYXQgNTAlIDUwJTtcbiAgICBiYWNrZ3JvdW5kLXNpemU6IGNvbnRhaW47XG4gICAgJi5zcGluLFxuICAgICY6aG92ZXI6bm90KC5zcGluKSB7XG4gICAgICAuc3Bpbm5lcjtcbiAgICB9XG4gIH1cbn1cblxuLnJvdy1zcGFjZWQge1xuICBtYXJnaW4tdG9wOiAxNXB4O1xuICBtYXJnaW4tYm90dG9tOiAxNXB4O1xufVxuXG4uZm9vdGVyIHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICBib3R0b206IDA7XG4gIHdpZHRoOiAxMDAlO1xuICAvKiBTZXQgdGhlIGZpeGVkIGhlaWdodCBvZiB0aGUgZm9vdGVyIGhlcmUgKi9cbiAgaGVpZ2h0OiA1MHB4O1xuICBib3JkZXItdG9wLXdpZHRoOiAxcHg7XG4gIGJvcmRlci10b3Atc3R5bGU6IHNvbGlkO1xufVxuXG4ubG9hZGluZzpiZWZvcmUge1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIHRvcDogMDtcbiAgbGVmdDogMDtcbiAgcmlnaHQ6IDA7XG4gIGJvdHRvbTogMDtcbiAgYmFja2dyb3VuZDogcmdiYSgwLDAsMCwwLjIpO1xuICBjb250ZW50OiBcIiBcIjtcbiAgei1pbmRleDogOTk5OTtcbn1cblxuLnNwaW5uZXIge1xuICBiYWNrZ3JvdW5kOiB0cmFuc3BhcmVudCB1cmwoL2ltZy9sb2dvLnN2Zykgbm8tcmVwZWF0IDUwJSA1MCU7XG4gIGJhY2tncm91bmQtc2l6ZTogY29udGFpbjtcbiAgLXdlYmtpdC1hbmltYXRpb246c3BpbiA0cyBsaW5lYXIgaW5maW5pdGU7XG4gIC1tb3otYW5pbWF0aW9uOnNwaW4gNHMgbGluZWFyIGluZmluaXRlO1xuICBhbmltYXRpb246c3BpbiA0cyBsaW5lYXIgaW5maW5pdGU7XG5cbiAgJi5zcGlubmVyLXNtYWxsIHtcbiAgICBwYWRkaW5nOiAyMHB4O1xuICB9XG59XG5cbi5ub3NlbGVjdCB7XG4gIC13ZWJraXQtdG91Y2gtY2FsbG91dDogbm9uZTtcbiAgLXdlYmtpdC11c2VyLXNlbGVjdDogbm9uZTtcbiAgLW1vei11c2VyLXNlbGVjdDogbm9uZTtcbiAgLW1zLXVzZXItc2VsZWN0OiBub25lO1xuICB1c2VyLXNlbGVjdDogbm9uZTtcbiAgY3Vyc29yOiBkZWZhdWx0O1xufVxuXG5ALW1vei1rZXlmcmFtZXMgc3BpbiB7IDEwMCUgeyAtbW96LXRyYW5zZm9ybTogcm90YXRlWSgzNjBkZWcpOyB9IH1cbkAtd2Via2l0LWtleWZyYW1lcyBzcGluIHsgMTAwJSB7IC13ZWJraXQtdHJhbnNmb3JtOiByb3RhdGVZKDM2MGRlZyk7IH0gfVxuQGtleWZyYW1lcyBzcGluIHsgMTAwJSB7IC13ZWJraXQtdHJhbnNmb3JtOiByb3RhdGVZKDM2MGRlZyk7IHRyYW5zZm9ybTpyb3RhdGVZKDM2MGRlZyk7IH0gfSIsImh0bWwge1xuICBwb3NpdGlvbjogcmVsYXRpdmU7XG4gIG1pbi1oZWlnaHQ6IDEwMCU7XG59XG5ib2R5IHtcbiAgcGFkZGluZy1ib3R0b206IDIwcHg7XG4gIC8qIE1hcmdpbiBib3R0b20gYnkgZm9vdGVyIGhlaWdodCAqL1xuICBtYXJnaW4tYm90dG9tOiA2MHB4O1xufVxuLmhlYWRpbmcge1xuICAtd2Via2l0LXRvdWNoLWNhbGxvdXQ6IG5vbmU7XG4gIC13ZWJraXQtdXNlci1zZWxlY3Q6IG5vbmU7XG4gIC1tb3otdXNlci1zZWxlY3Q6IG5vbmU7XG4gIC1tcy11c2VyLXNlbGVjdDogbm9uZTtcbiAgdXNlci1zZWxlY3Q6IG5vbmU7XG4gIGN1cnNvcjogZGVmYXVsdDtcbiAgYmFja2dyb3VuZDogdHJhbnNwYXJlbnQgdXJsKC9pbWcvbG9nby5zdmcpIG5vLXJlcGVhdCA1MCUgNTAlO1xuICBiYWNrZ3JvdW5kLXNpemU6IGNvbnRhaW47XG4gIGhlaWdodDogMTI1cHg7XG4gIG1hcmdpbjogMjVweCAwO1xufVxuLnNlYXJjaC1yZXN1bHRzIHtcbiAgcGFkZGluZzogMTVweDtcbn1cbi5zZWFyY2gtcmVzdWx0cyAuc2VhcmNoLXJlc3VsdCB7XG4gIG1hcmdpbi1ib3R0b206IDIwcHg7XG59XG4uc2VhcmNoLXJlc3VsdHMgLnNlYXJjaC1yZXN1bHQgYSB7XG4gIGZvbnQtc2l6ZTogMS41cmVtO1xufVxuLmVsbGlwc2l6ZSB7XG4gIHdoaXRlLXNwYWNlOiBub3dyYXA7XG4gIG92ZXJmbG93OiBoaWRkZW47XG4gIHRleHQtb3ZlcmZsb3c6IGVsbGlwc2lzO1xufVxuLm5hdmJhci1kZWZhdWx0IC5jb2xsYXBzZTpub3QoLmluKSAubmF2YmFyLW5hdiA+IC5hY3RpdmUgPiBhLFxuLm5hdmJhci1kZWZhdWx0IC5jb2xsYXBzZTpub3QoLmluKSAubmF2YmFyLW5hdiA+IC5hY3RpdmUgPiBhOmhvdmVyLFxuLm5hdmJhci1kZWZhdWx0IC5jb2xsYXBzZTpub3QoLmluKSAubmF2YmFyLW5hdiA+IC5hY3RpdmUgPiBhOmZvY3VzIHtcbiAgYm9yZGVyLWJvdHRvbTogM3B4IHNvbGlkO1xuICBwYWRkaW5nLWJvdHRvbTogMTJweDtcbn1cbi5uYXZiYXItZGVmYXVsdCAubmF2YmFyLWhlYWRlciB7XG4gIHBhZGRpbmc6IDAgMTBweDtcbn1cbi5uYXZiYXItZGVmYXVsdCAubmF2YmFyLWJyYW5kIHtcbiAgYmFja2dyb3VuZDogdHJhbnNwYXJlbnQgdXJsKC9pbWcvbG9nby5zdmcpIG5vLXJlcGVhdCA1MCUgNTAlO1xuICBiYWNrZ3JvdW5kLXNpemU6IGNvbnRhaW47XG59XG4ubmF2YmFyLWRlZmF1bHQgLm5hdmJhci1icmFuZC5zcGluLFxuLm5hdmJhci1kZWZhdWx0IC5uYXZiYXItYnJhbmQ6aG92ZXI6bm90KC5zcGluKSB7XG4gIGJhY2tncm91bmQ6IHRyYW5zcGFyZW50IHVybCgvaW1nL2xvZ28uc3ZnKSBuby1yZXBlYXQgNTAlIDUwJTtcbiAgYmFja2dyb3VuZC1zaXplOiBjb250YWluO1xuICAtd2Via2l0LWFuaW1hdGlvbjogc3BpbiA0cyBsaW5lYXIgaW5maW5pdGU7XG4gIC1tb3otYW5pbWF0aW9uOiBzcGluIDRzIGxpbmVhciBpbmZpbml0ZTtcbiAgYW5pbWF0aW9uOiBzcGluIDRzIGxpbmVhciBpbmZpbml0ZTtcbn1cbi5uYXZiYXItZGVmYXVsdCAubmF2YmFyLWJyYW5kLnNwaW4uc3Bpbm5lci1zbWFsbCxcbi5uYXZiYXItZGVmYXVsdCAubmF2YmFyLWJyYW5kOmhvdmVyOm5vdCguc3Bpbikuc3Bpbm5lci1zbWFsbCB7XG4gIHBhZGRpbmc6IDIwcHg7XG59XG4ucm93LXNwYWNlZCB7XG4gIG1hcmdpbi10b3A6IDE1cHg7XG4gIG1hcmdpbi1ib3R0b206IDE1cHg7XG59XG4uZm9vdGVyIHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICBib3R0b206IDA7XG4gIHdpZHRoOiAxMDAlO1xuICAvKiBTZXQgdGhlIGZpeGVkIGhlaWdodCBvZiB0aGUgZm9vdGVyIGhlcmUgKi9cbiAgaGVpZ2h0OiA1MHB4O1xuICBib3JkZXItdG9wLXdpZHRoOiAxcHg7XG4gIGJvcmRlci10b3Atc3R5bGU6IHNvbGlkO1xufVxuLmxvYWRpbmc6YmVmb3JlIHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICB0b3A6IDA7XG4gIGxlZnQ6IDA7XG4gIHJpZ2h0OiAwO1xuICBib3R0b206IDA7XG4gIGJhY2tncm91bmQ6IHJnYmEoMCwgMCwgMCwgMC4yKTtcbiAgY29udGVudDogXCIgXCI7XG4gIHotaW5kZXg6IDk5OTk7XG59XG4uc3Bpbm5lciB7XG4gIGJhY2tncm91bmQ6IHRyYW5zcGFyZW50IHVybCgvaW1nL2xvZ28uc3ZnKSBuby1yZXBlYXQgNTAlIDUwJTtcbiAgYmFja2dyb3VuZC1zaXplOiBjb250YWluO1xuICAtd2Via2l0LWFuaW1hdGlvbjogc3BpbiA0cyBsaW5lYXIgaW5maW5pdGU7XG4gIC1tb3otYW5pbWF0aW9uOiBzcGluIDRzIGxpbmVhciBpbmZpbml0ZTtcbiAgYW5pbWF0aW9uOiBzcGluIDRzIGxpbmVhciBpbmZpbml0ZTtcbn1cbi5zcGlubmVyLnNwaW5uZXItc21hbGwge1xuICBwYWRkaW5nOiAyMHB4O1xufVxuLm5vc2VsZWN0IHtcbiAgLXdlYmtpdC10b3VjaC1jYWxsb3V0OiBub25lO1xuICAtd2Via2l0LXVzZXItc2VsZWN0OiBub25lO1xuICAtbW96LXVzZXItc2VsZWN0OiBub25lO1xuICAtbXMtdXNlci1zZWxlY3Q6IG5vbmU7XG4gIHVzZXItc2VsZWN0OiBub25lO1xuICBjdXJzb3I6IGRlZmF1bHQ7XG59XG5ALW1vei1rZXlmcmFtZXMgc3BpbiB7XG4gIDEwMCUge1xuICAgIC1tb3otdHJhbnNmb3JtOiByb3RhdGVZKDM2MGRlZyk7XG4gIH1cbn1cbkAtd2Via2l0LWtleWZyYW1lcyBzcGluIHtcbiAgMTAwJSB7XG4gICAgLXdlYmtpdC10cmFuc2Zvcm06IHJvdGF0ZVkoMzYwZGVnKTtcbiAgfVxufVxuQGtleWZyYW1lcyBzcGluIHtcbiAgMTAwJSB7XG4gICAgLXdlYmtpdC10cmFuc2Zvcm06IHJvdGF0ZVkoMzYwZGVnKTtcbiAgICB0cmFuc2Zvcm06IHJvdGF0ZVkoMzYwZGVnKTtcbiAgfVxufVxuIl0sInNvdXJjZVJvb3QiOiIvc291cmNlLyJ9 */
diff --git a/vipra-ui/css/app.less b/vipra-ui/css/app.less
index e64406743dd5e10f5c96c395a9ff54e1e64ef5f8..537ff232c056c30fa94ca042ca28010f6b8d7442 100644
--- a/vipra-ui/css/app.less
+++ b/vipra-ui/css/app.less
@@ -56,9 +56,7 @@ body {
     background-size: contain;
     &.spin,
     &:hover:not(.spin) {
-      -webkit-animation:spin 4s linear infinite;
-      -moz-animation:spin 4s linear infinite;
-      animation:spin 4s linear infinite;
+      .spinner;
     }
   }
 }
@@ -78,14 +76,27 @@ body {
   border-top-style: solid;
 }
 
-.loading {
+.loading:before {
   position: absolute;
   top: 0;
   left: 0;
   right: 0;
   bottom: 0;
-  background rgba(0,0,0,0.2);
+  background: rgba(0,0,0,0.2);
+  content: " ";
+  z-index: 9999;
+}
 
+.spinner {
+  background: transparent url(/img/logo.svg) no-repeat 50% 50%;
+  background-size: contain;
+  -webkit-animation:spin 4s linear infinite;
+  -moz-animation:spin 4s linear infinite;
+  animation:spin 4s linear infinite;
+
+  &.spinner-small {
+    padding: 20px;
+  }
 }
 
 .noselect {
diff --git a/vipra-ui/html/articles/index.html b/vipra-ui/html/articles/index.html
index 9ce005c39cc7e6dbc211f9004debe94299888663..5560bfaff51ace0ddeb7c818e2d572d2fa7270fd 100644
--- a/vipra-ui/html/articles/index.html
+++ b/vipra-ui/html/articles/index.html
@@ -4,7 +4,7 @@
 
 <ul class="list-unstyled">
   <li ng-repeat="article in articles">
-    <a ui-sref="articles.show({id: article.id})">{{article.title}}</a>
+    <a ui-sref="articles.show({id: article.id})" ng-bind="::article.title"></a>
   </li>
 </ul>
 
diff --git a/vipra-ui/html/articles/show.html b/vipra-ui/html/articles/show.html
index e8ed5e3454fc2558ff2994dff6e2d86c3dc0d453..13c354fed285907d6d71498d6aa9e97483a918bf 100644
--- a/vipra-ui/html/articles/show.html
+++ b/vipra-ui/html/articles/show.html
@@ -1,16 +1,18 @@
-<h1 ng-bind="article.title"></h1>
-
-<p ng-bind="(article.date | formatDate)"></p>
+<h1 ng-bind="::article.title"></h1>
 
 <table class="table table-bordered table-condensed">
   <tbody>
     <tr>
       <th>ID</th>
-      <td ng-bind="article.id"></td>
+      <td ng-bind="::article.id"></td>
+    </tr>
+    <tr>
+      <th>Date</th>
+      <td ng-bind="::article.date"></td>
     </tr>
     <tr>
       <th>URL</th>
-      <td><a ng-href="{{article.url}}" ng-bind="article.url"></a></td>
+      <td><a ng-href="{{::article.url}}" ng-bind="::article.url"></a></td>
     </tr>
     <tr>
       <th>Topics</th>
@@ -22,17 +24,17 @@
     </tr>
     <tr>
       <th>Created</th>
-      <td ng-bind="(article.created | formatDateTime)"></td>
+      <td ng-bind="::article.created"></td>
     </tr>
     <tr>
       <th>Last modified</th>
-      <td ng-bind="(article.modified | formatDateTime)"></td>
+      <td ng-bind="::article.modified"></td>
     </tr>
     <tr>
       <th>Word count</th>
-      <td ng-bind="article.stats.wordCount"></td>
+      <td ng-bind="::article.stats.wordCount"></td>
     </tr>
   </tbody>
 </table>
 
-<p ng-bind="article.text"></p>
\ No newline at end of file
+<p ng-bind="::article.text"></p>
\ No newline at end of file
diff --git a/vipra-ui/html/index.html b/vipra-ui/html/index.html
index bb5516f33c6cb90de3ded38df9b6cf93b4024fa1..6d4c4c0f97d41be2318f1ff9482b93b3da106a2a 100644
--- a/vipra-ui/html/index.html
+++ b/vipra-ui/html/index.html
@@ -1,12 +1,12 @@
 <div class="container">
 
-  <div class="row">
+  <div class="row" slide-on="search">
     <div class="col-md-12">
       <div class="heading"></div>
     </div>
   </div>
 
-  <div class="row">
+  <div class="row" slide-on="search">
     <div class="col-md-6 text-center">
       <h4>Latest articles</h4>
       <ul class="list-unstyled">
@@ -39,8 +39,14 @@
     </div>
   </div>
 
-  <div class="row row-spaced" ng-show="searchResults.length > 0">
-    <div class="col-md-12">
+  <div class="row row-spaced">
+    <div class="text-center" ng-show="searching">
+      Searching...
+    </div>
+    <div class="col-md-12" ng-show="!searching && search && searchResults.length == 0">
+      <h4>No Results <query-time/></h4>
+    </div>
+    <div class="col-md-12" ng-show="searchResults.length > 0">
       <h4>Results <query-time/></h4>
       <ul class="list-unstyled search-results">
         <li class="search-result" ng-repeat="article in searchResults">
diff --git a/vipra-ui/html/topics/show.html b/vipra-ui/html/topics/show.html
index 08b2633990c5eeb8210fae60e03a8f03eb0c8b12..45b6595814b7f04d7ce8788b4e79e849f4e983c8 100644
--- a/vipra-ui/html/topics/show.html
+++ b/vipra-ui/html/topics/show.html
@@ -1,22 +1,22 @@
-<h1 ng-bind="topic.name"></h1>
+<h1 ng-bind="::topic.name"></h1>
 
 <table class="table table-bordered table-condensed">
   <tbody>
     <tr>
       <th>ID</th>
-      <td ng-bind="topic.id"></td>
+      <td ng-bind="::topic.id"></td>
     </tr>
     <tr>
       <th>Index</th>
-      <td ng-bind="topic.index"></td>
+      <td ng-bind="::topic.index"></td>
     </tr>
     <tr>
       <th>Created</th>
-      <td ng-bind="(topic.created | formatDateTime)"></td>
+      <td ng-bind="::topic.created"></td>
     </tr>
     <tr>
       <th>Last modified</th>
-      <td ng-bind="(topic.modified | formatDateTime)"></td>
+      <td ng-bind="::topic.modified"></td>
     </tr>
   </tbody>
 </table>
@@ -31,7 +31,7 @@
     </tr>
   </thead>
   <tbody>
-    <tr ng-repeat="word in topic.words">
+    <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>
diff --git a/vipra-ui/index.html b/vipra-ui/index.html
index e9759f89731f6d1f6e3120c7602bccc964c8aa89..b6c4dc9a970d37a971ee8700bd30ddbd0e817878 100644
--- a/vipra-ui/index.html
+++ b/vipra-ui/index.html
@@ -42,6 +42,7 @@
     <script src="js/directives.js"></script>
     <script src="js/factories.js"></script>
     <script src="js/filters.js"></script>
+    <script src="js/helpers.js"></script>
     <script src="js/services.js"></script>
   </head>
   <body>
diff --git a/vipra-ui/js/app.js b/vipra-ui/js/app.js
index fcb3999b45884125492c3b177f98dc6257c8d059..3215a9d97d3cb8a5c0851e4785e5a1a269f9d572 100644
--- a/vipra-ui/js/app.js
+++ b/vipra-ui/js/app.js
@@ -21,8 +21,7 @@
     $stateProvider.state('index', {
       url: '/',
       templateUrl: tplBase + '/index.html',
-      controller: 'IndexController',
-      reloadOnSearch: false
+      controller: 'IndexController'
     });
 
     // states: articles
diff --git a/vipra-ui/js/controllers.js b/vipra-ui/js/controllers.js
index 2572c6aebc5ce955f691f158b40b3455ef516a05..7a5b23e5704bd782bcdf6f252d550586a42c0777 100644
--- a/vipra-ui/js/controllers.js
+++ b/vipra-ui/js/controllers.js
@@ -27,7 +27,9 @@
 
     $scope.$watch('search', function() {
       if($scope.search) {
+        $scope.searching = true;
         SearchFactory.query({limit:searchItemsCount, query:$scope.search}, function(response) {
+          $scope.searching = false;
           $scope.searchResults = response.data;
           $scope.queryTime = response.$queryTime;
         });
@@ -43,7 +45,7 @@
    */
 
   app.controller('ArticlesIndexController', ['$scope', '$stateParams', 'ArticleFactory',
-    function($scope, $stateParams, ArticleFactory) {
+    function($scope, $stateParams, ArticleFactory, testService) {
 
     $scope.page = Math.max($stateParams.page || 1, 1);
     $scope.limit = pageSize;
@@ -60,10 +62,13 @@
   }]);
 
   app.controller('ArticlesShowController', ['$scope', '$stateParams', 'ArticleFactory',
-    function($scope, $stateParams, ArticleFactory) {
+    function($scope, $stateParams, ArticleFactory, testService) {
 
     ArticleFactory.get({id: $stateParams.id}, function(response) {
       $scope.article = response.data;
+      $scope.article.date = formatDate($scope.article.date);
+      $scope.article.created = formatDateTime($scope.article.created);
+      $scope.article.modified = formatDateTime($scope.article.modified);
       $scope.articleMeta = response.meta;
       $scope.queryTime = response.$queryTime;
     });
@@ -90,6 +95,8 @@
 
     TopicFactory.get({id: $stateParams.id}, function(response) {
       $scope.topic = response.data;
+      $scope.topic.created = formatDateTime($scope.topic.created);
+      $scope.topic.modified = formatDateTime($scope.topic.modified);
       $scope.topicMeta = response.meta;
       $scope.queryTime = response.$queryTime;
     });
diff --git a/vipra-ui/js/directives.js b/vipra-ui/js/directives.js
index b049382822e1aa7c35bbe4b4aa1cbeeebb09f763..9b8dda0a49c1b243818c811695f34c81eb201db9 100644
--- a/vipra-ui/js/directives.js
+++ b/vipra-ui/js/directives.js
@@ -4,6 +4,8 @@
     'ui.router'
   ]);
 
+  var slideDuration = 250;
+
   app.directive('topicLink', function() {
     return {
       scope: {
@@ -38,4 +40,23 @@
     };
   });
 
+  app.directive('slideOn', function() {
+    return {
+      scope: {
+        slideOn: '='
+      },
+      link: function($scope, $elem) {
+        $scope.$watch('slideOn', function(newVal, oldVal) {
+          if(newVal != oldVal) {
+            if(newVal) {
+              $elem.slideUp(slideDuration);
+            } else {
+              $elem.slideDown(slideDuration);
+            }
+          }
+        });
+      }
+    };
+  });
+
 })();
\ No newline at end of file
diff --git a/vipra-ui/js/filters.js b/vipra-ui/js/filters.js
index a4233098d28b5e1984301ff35c61ee44861256c6..192f846992f3eb6ee8d32326a2ed87f8d698265f 100644
--- a/vipra-ui/js/filters.js
+++ b/vipra-ui/js/filters.js
@@ -3,24 +3,15 @@
   var app = angular.module('vipra.filters', []);
 
   app.filter('toPercent', function() {
-    return function(input) {
-      if(typeof input !== 'number')
-        input = parseInt(input, 10);
-      return Math.round(input * 100);
-    };
+    return toPercent;
   });
 
   app.filter('formatDate', function() {
-    return function(input) {
-      return new Date(input).toLocaleDateString();
-    };
+    return formatDate;
   });
 
   app.filter('formatDateTime', function() {
-    return function(input) {
-      var date = new Date(input);
-      return date.toLocaleDateString() + " " + date.toLocaleTimeString();
-    };
+    return formatDateTime;
   });
 
 })();
\ No newline at end of file
diff --git a/vipra-ui/js/helpers.js b/vipra-ui/js/helpers.js
new file mode 100644
index 0000000000000000000000000000000000000000..14980a3641014cddae33cb15c451db261023ba1a
--- /dev/null
+++ b/vipra-ui/js/helpers.js
@@ -0,0 +1,18 @@
+(function() {
+
+  window.formatDate = function(date) {
+    return new Date(date).toLocaleDateString();
+  };
+
+  window.formatDateTime = function(date) {
+    date = new Date(date);
+    return date.toLocaleDateString() + " " + date.toLocaleTimeString();
+  };
+
+  window.toPercent = function(input) {
+    if(typeof input !== 'number')
+      input = parseInt(input, 10);
+    return Math.round(input * 100);
+  };
+
+})();
\ No newline at end of file
diff --git a/vipra-util/src/main/java/de/vipra/util/Config.java b/vipra-util/src/main/java/de/vipra/util/Config.java
index 77417165150a5700226a6f94569c38a7c279c6ad..95f77ab8436ef29ba9db0ad66432ee91a7291b54 100644
--- a/vipra-util/src/main/java/de/vipra/util/Config.java
+++ b/vipra-util/src/main/java/de/vipra/util/Config.java
@@ -7,9 +7,9 @@ import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
-import java.util.Map.Entry;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Properties;
 import java.util.Set;
 
@@ -18,6 +18,7 @@ import org.slf4j.LoggerFactory;
 
 import de.vipra.util.Constants.Analyzer;
 import de.vipra.util.Constants.Processor;
+import de.vipra.util.Constants.WindowResolution;
 import de.vipra.util.an.ConfigKey;
 import de.vipra.util.ex.ConfigException;
 import de.vipra.util.model.Model;
@@ -44,6 +45,9 @@ public class Config {
 	@ConfigKey("tm.analyzer")
 	public Analyzer analyzer = Constants.Analyzer.DEFAULT;
 
+	@ConfigKey("tm.windowresolution")
+	public WindowResolution windowResolution = Constants.WindowResolution.DEFAULT;
+
 	@ConfigKey("tm.saveallwords")
 	public boolean saveAllWords = Constants.SAVE_ALL_WORDS;
 
diff --git a/vipra-util/src/main/java/de/vipra/util/Constants.java b/vipra-util/src/main/java/de/vipra/util/Constants.java
index ca915f4c37676455f629bfe405439fe8b56b3165..a9b232ed9f163cd4446a6d31076400820da81472 100644
--- a/vipra-util/src/main/java/de/vipra/util/Constants.java
+++ b/vipra-util/src/main/java/de/vipra/util/Constants.java
@@ -181,6 +181,16 @@ public class Constants {
 	 */
 	public static final String REGEX_SINGLECHAR = "\\b\\w\\b";
 
+	/*
+	 * INDEX
+	 */
+
+	/**
+	 * The length of the text excerpt used for indexing and displaying text in
+	 * search results.
+	 */
+	public static final int EXCERPT_LENGTH = 250;
+
 	/*
 	 * OTHER
 	 */
@@ -225,6 +235,7 @@ public class Constants {
 	 */
 	public static enum Analyzer {
 		JGIBB("jgibb"),
+		DYNNMF("dynnmf"),
 		DEFAULT(JGIBB);
 
 		public final String name;
@@ -249,4 +260,35 @@ public class Constants {
 		}
 	}
 
+	/**
+	 * Describes the window size, when using dynamic topic modeling
+	 */
+	public static enum WindowResolution {
+		MONTHLY("monthly"),
+		QUARTERLY("quarterly"),
+		YEARLY("yearly"),
+		DEFAULT(MONTHLY);
+
+		public final String name;
+
+		private WindowResolution(String name) {
+			this.name = name;
+		}
+
+		private WindowResolution(WindowResolution def) {
+			this.name = def.name;
+		}
+
+		public static WindowResolution fromString(String text) {
+			if (text != null) {
+				for (WindowResolution b : WindowResolution.values()) {
+					if (text.equalsIgnoreCase(b.name)) {
+						return b;
+					}
+				}
+			}
+			return DEFAULT;
+		}
+	}
+
 }
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 d5dd234f9ba9d9d3f361b6f80897fa75caf977b3..d2effd84b4e28315107e7e23a7546aacea6c60ed 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
@@ -103,6 +103,11 @@ public class ArticleFull extends FileModel<ObjectId> implements Serializable {
 		this.text = text;
 	}
 
+	@ElasticIndex("excerpt")
+	public String serializeText() {
+		return StringUtils.ellipsize(text, Constants.EXCERPT_LENGTH);
+	}
+
 	public String getProcessedText() {
 		return processedText;
 	}