diff --git a/ma-impl.sublime-workspace b/ma-impl.sublime-workspace
index 0a9c9a9b0229e6cdc8a4c459bc0da93f5ffd99b5..1178f37887c3d459c6e40efa5159186a480d8a98 100644
--- a/ma-impl.sublime-workspace
+++ b/ma-impl.sublime-workspace
@@ -275,15 +275,6 @@
 	},
 	"buffers":
 	[
-		{
-			"contents": "stemming to lemmatization (corenlp benutzen)\ntop n words for topic name\nbuch: natural language processing with java\ndynamic lda\n\nVISUALISIERUNG!",
-			"settings":
-			{
-				"buffer_size": 144,
-				"line_ending": "Unix",
-				"name": "stemming to lemmatization (corenlp benutzen)"
-			}
-		}
 	],
 	"build_system": "",
 	"build_system_choices":
@@ -463,6 +454,7 @@
 	"expanded_folders":
 	[
 		"/home/eike/repos/master/ma-impl",
+		"/home/eike/repos/master/ma-impl/vipra-cmd",
 		"/home/eike/repos/master/ma-impl/vipra-ui",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/adapters",
@@ -471,15 +463,21 @@
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/routes/topics",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/articles",
-		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/components"
+		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/components",
+		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/topics",
+		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/topics/show"
 	],
 	"file_history":
 	[
+		"/home/eike/repos/master/ma-impl/vipra-ui/app/components/topic-link.js",
+		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/articles/show.hbs",
+		"/home/eike/.local/share/vipra/jgibb/jgibb.twords",
+		"/home/eike/.local/share/vipra/jgibb/jgibb.tassign",
+		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/topics/index.hbs",
+		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/topics/show/index.hbs",
 		"/home/eike/Downloads/FRITZ.Box 7490 113.06.30_17.01.16_2147.export",
 		"/home/eike/repos/master/ma-impl/vm/data/test-1.json",
 		"/home/eike/repos/master/ma-impl/vm/data/test-2.json",
-		"/home/eike/.local/share/vipra/jgibb/jgibb.twords",
-		"/home/eike/.local/share/vipra/jgibb/jgibb.tassign",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/routes/topics/index.js",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/articles/index.hbs",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/adapters/application.js",
@@ -494,18 +492,14 @@
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/models/topic.js",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/routes/topics/show/edit.js",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/components/topics-list.hbs",
-		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/topics/index.hbs",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/router.js",
-		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/topics/show/index.hbs",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/routes/topics/show.js",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/topics/show/edit.hbs",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/topics/show.hbs",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/helpers/topic-share.js",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/styles/app.css",
-		"/home/eike/repos/master/ma-impl/vipra-ui/app/components/topic-link.js",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/components/topic-link.hbs",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/topics/edit.hbs",
-		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/articles/show.hbs",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/routes/articles/show.js",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/templates/articles/edit.hbs",
 		"/home/eike/repos/master/ma-impl/vipra-ui/app/routes/topics/edit.js",
@@ -926,38 +920,8 @@
 	"groups":
 	[
 		{
-			"selected": 0,
 			"sheets":
 			[
-				{
-					"buffer": 0,
-					"semi_transient": false,
-					"settings":
-					{
-						"buffer_size": 144,
-						"regions":
-						{
-						},
-						"selection":
-						[
-							[
-								99,
-								99
-							]
-						],
-						"settings":
-						{
-							"auto_name": "stemming to lemmatization (corenlp benutzen)",
-							"default_dir": "/home/eike/repos/master/ma-impl",
-							"syntax": "Packages/Text/Plain text.tmLanguage"
-						},
-						"translation.x": 0.0,
-						"translation.y": 0.0,
-						"zoom_level": 1.0
-					},
-					"stack_index": 0,
-					"type": "text"
-				}
 			]
 		}
 	],
diff --git a/vipra-cmd/pom.xml b/vipra-cmd/pom.xml
index 5aed56ed1592d2fc7f23fb6262bfb56bdbc0963e..eb06c6280c715624504631972ef393625d0854d6 100644
--- a/vipra-cmd/pom.xml
+++ b/vipra-cmd/pom.xml
@@ -86,6 +86,11 @@
 			<artifactId>log4j-slf4j-impl</artifactId>
 			<version>${log4jVersion}</version>
 		</dependency>
+		<dependency>
+			<groupId>uk.org.lidalia</groupId>
+			<artifactId>sysout-over-slf4j</artifactId>
+			<version>1.0.2</version>
+		</dependency>
 
 		<!-- MongoDB Database Adapter -->
 		<dependency>
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 4c488805545eb165f9bb8e73868370a9b7e6e88e..4a08e5fc1fcec0670305dc15f258b436e2ebfc87 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/Main.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/Main.java
@@ -30,6 +30,7 @@ import de.vipra.util.ConsoleUtils;
 import de.vipra.util.ConsoleUtils.Choice;
 import de.vipra.util.StringUtils;
 import de.vipra.util.Timer;
+import uk.org.lidalia.sysoutslf4j.context.SysOutOverSLF4J;
 
 public class Main {
 
@@ -38,6 +39,7 @@ public class Main {
 
 	static {
 		MorphiaLoggerFactory.registerLogger(SLF4JLoggerImplFactory.class);
+		SysOutOverSLF4J.sendSystemOutAndErrToSLF4J();
 	}
 
 	public static void main(String[] args) {
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/lda/JGibbLDAAnalyzer.java b/vipra-cmd/src/main/java/de/vipra/cmd/lda/JGibbLDAAnalyzer.java
index 684f0f65e536fbc0ee5662b5a4d50eb67fb5d710..df84fbe297193d17a2660f7c8fadbb6069aa540f 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/lda/JGibbLDAAnalyzer.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/lda/JGibbLDAAnalyzer.java
@@ -17,9 +17,11 @@ import de.vipra.util.Config;
 import de.vipra.util.ConvertStream;
 import de.vipra.util.StringUtils;
 import de.vipra.util.ex.ConfigException;
-import de.vipra.util.model.Topic;
+import de.vipra.util.model.TopicFull;
 import de.vipra.util.model.TopicRef;
 import de.vipra.util.model.TopicWord;
+import de.vipra.util.model.Word;
+import de.vipra.util.model.WordMap;
 import jgibblda.Estimator;
 import jgibblda.Inferencer;
 import jgibblda.LDACmdOption;
@@ -33,13 +35,14 @@ public class JGibbLDAAnalyzer extends LDAAnalyzer {
 	private File modelDir;
 	private File modelFile;
 	private LDACmdOption options;
+	private WordMap wordMap;
 
 	protected JGibbLDAAnalyzer() {
 		super("JGibb Analyzer");
 	}
 
 	@Override
-	public void init(Config config) throws LDAAnalyzerException {
+	public void init(Config config, WordMap wordMap) throws LDAAnalyzerException {
 		options = new LDACmdOption();
 
 		try {
@@ -57,6 +60,8 @@ public class JGibbLDAAnalyzer extends LDAAnalyzer {
 		options.dfile = modelFile.getName();
 
 		options.modelName = "jgibb";
+
+		this.wordMap = wordMap;
 	}
 
 	private void estimate() {
@@ -81,28 +86,40 @@ public class JGibbLDAAnalyzer extends LDAAnalyzer {
 	}
 
 	@Override
-	public ConvertStream<Topic> getTopicDefinitions() throws LDAAnalyzerException {
+	public ConvertStream<TopicFull> getTopicDefinitions() throws LDAAnalyzerException {
 		File twords = new File(modelDir, "jgibb.twords");
 		try {
-			return new ConvertStream<Topic>(twords) {
+			return new ConvertStream<TopicFull>(twords) {
 				@Override
-				public Topic convert(String line) {
-					Topic topicDef = new Topic();
+				public TopicFull convert(String line) {
+					TopicFull topicDef = new TopicFull();
 					List<TopicWord> topicWords = new ArrayList<>();
+
+					// get index of topic
+					// the index will be used as a temporary id until the topic
+					// is created in the database
 					Integer index = StringUtils.getFirstNumber(line);
 					if (index == null) {
 						log.error("could not extract topic index from line: " + line);
 					} else {
 						topicDef.setIndex(index);
 					}
+
+					// get all lines that follow until the next topic is
+					// discovered (line does not start with a tab)
 					String nextLine;
 					while ((nextLine = nextLine()) != null) {
 						if (nextLine.startsWith("\t")) {
 							String[] parts = nextLine.trim().split("\\s+");
 							try {
-								topicWords.add(new TopicWord(parts[0], Double.parseDouble(parts[1])));
+								Word word = wordMap.get(parts[0]);
+								double likeliness = Double.parseDouble(parts[1]);
+								TopicWord topicWord = new TopicWord(word, likeliness);
+								topicWords.add(topicWord);
 							} catch (NumberFormatException e) {
 								log.error("could not parse number in line: " + nextLine);
+							} catch (ArrayIndexOutOfBoundsException e) {
+								log.error("ill formatted topic definition in line: " + nextLine);
 							}
 						} else {
 							buffer(nextLine);
@@ -110,8 +127,8 @@ public class JGibbLDAAnalyzer extends LDAAnalyzer {
 						}
 					}
 					Collections.sort(topicWords);
-					topicDef.setWords(topicWords);
-					topicDef.setName(topicDef.getNameFromWords());
+					topicDef.setTopicWords(topicWords);
+					topicDef.setName(TopicFull.getNameFromWords(topicWords));
 					return topicDef;
 				}
 			};
@@ -131,6 +148,9 @@ public class JGibbLDAAnalyzer extends LDAAnalyzer {
 					Map<String, Integer> countMap = new HashMap<>();
 					String[] wordList = line.split("\\s+");
 					for (String word : wordList) {
+						// the file uses a simple integer as topic ids. Use this
+						// id as a temporary id until the topic is created in
+						// the database
 						String topic = word.split(":")[1];
 						Integer count = countMap.get(topic);
 						countMap.put(topic, count == null ? 1 : count + 1);
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/lda/LDAAnalyzer.java b/vipra-cmd/src/main/java/de/vipra/cmd/lda/LDAAnalyzer.java
index dc3e11319d6591d1172d194055219ef05f090a7a..6a34f1ceec90eda810685d7e38b067906723bba4 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/lda/LDAAnalyzer.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/lda/LDAAnalyzer.java
@@ -7,8 +7,9 @@ import de.vipra.util.Config;
 import de.vipra.util.Config.Key;
 import de.vipra.util.Constants;
 import de.vipra.util.ConvertStream;
-import de.vipra.util.model.Topic;
+import de.vipra.util.model.TopicFull;
 import de.vipra.util.model.TopicRef;
+import de.vipra.util.model.WordMap;
 
 public abstract class LDAAnalyzer {
 
@@ -22,15 +23,32 @@ public abstract class LDAAnalyzer {
 		return name;
 	}
 
-	public abstract void init(Config config) throws LDAAnalyzerException;
+	public abstract void init(Config config, WordMap wordMap) throws LDAAnalyzerException;
 
 	public abstract void analyze() throws LDAAnalyzerException;
 
-	public abstract ConvertStream<Topic> getTopicDefinitions() throws LDAAnalyzerException;
+	/**
+	 * Returns a converting stream of topics, read from the topic definition
+	 * file. Usually, a topic definition consists of a list of words, that are
+	 * assigned to that topic with a certain likeliness.
+	 * 
+	 * @return topic definition stream
+	 * @throws LDAAnalyzerException
+	 */
+	public abstract ConvertStream<TopicFull> getTopicDefinitions() throws LDAAnalyzerException;
 
+	/**
+	 * Returns a converting stream of lists of topic references. Normally, topic
+	 * modeling outputs topics for each word of each document. These references
+	 * are returned by this function.
+	 * 
+	 * @return stream of lists of topic references per document (ordered by
+	 *         index)
+	 * @throws LDAAnalyzerException
+	 */
 	public abstract ConvertStream<List<TopicRef>> getTopics() throws LDAAnalyzerException;
 
-	public static LDAAnalyzer getAnalyzer(Config config) throws LDAAnalyzerException {
+	public static LDAAnalyzer getAnalyzer(Config config, WordMap wordMap) throws LDAAnalyzerException {
 		LDAAnalyzer analyzer = null;
 		switch (Constants.Analyzer.fromString(config.getString(Key.ANALYZER))) {
 			case JGIBB:
@@ -39,7 +57,7 @@ public abstract class LDAAnalyzer {
 				analyzer = new JGibbLDAAnalyzer();
 				break;
 		}
-		analyzer.init(config);
+		analyzer.init(config, wordMap);
 		return analyzer;
 	}
 
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
index 897bcc0ada11c858abfd05c44dcf07c4b9f84025..aea42f749f606fc774e903e6d683296c770d91cb 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/model/ProcessedArticle.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/model/ProcessedArticle.java
@@ -6,6 +6,7 @@ import org.mongodb.morphia.annotations.Transient;
 
 import de.vipra.cmd.text.ProcessedText;
 
+@SuppressWarnings("serial")
 @Entity(value = "articles", noClassnameStored = true)
 public class ProcessedArticle extends de.vipra.util.model.Article {
 
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 178ec40448a8b5c801f1034fbcb62a2c3e246995..d271eabc64c75a79c23e36812ad7e02b68ee4072 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
@@ -13,7 +13,8 @@ import de.vipra.cmd.model.ProcessedArticle;
 import de.vipra.util.Config;
 import de.vipra.util.ConsoleUtils;
 import de.vipra.util.ex.ConfigException;
-import de.vipra.util.model.Topic;
+import de.vipra.util.model.TopicFull;
+import de.vipra.util.model.Word;
 import de.vipra.util.service.DatabaseService;
 
 public class ClearCommand implements Command {
@@ -24,7 +25,8 @@ public class ClearCommand implements Command {
 	private boolean defaults;
 	private Config config;
 	private DatabaseService<ProcessedArticle> dbArticles;
-	private DatabaseService<Topic> dbTopics;
+	private DatabaseService<TopicFull> dbTopics;
+	private DatabaseService<Word> dbWords;
 
 	public ClearCommand(boolean defaults) {
 		this.defaults = defaults;
@@ -34,7 +36,8 @@ public class ClearCommand implements Command {
 		try {
 			config = Config.getConfig();
 			dbArticles = DatabaseService.getDatabaseService(config, ProcessedArticle.class);
-			dbTopics = DatabaseService.getDatabaseService(config, Topic.class);
+			dbTopics = DatabaseService.getDatabaseService(config, TopicFull.class);
+			dbWords = DatabaseService.getDatabaseService(config, Word.class);
 		} catch (Exception e) {
 			throw new ClearException(e);
 		}
@@ -42,6 +45,7 @@ public class ClearCommand implements Command {
 		out.info("clearing database");
 		dbArticles.drop();
 		dbTopics.drop();
+		dbWords.drop();
 
 		out.info("clearing filebase");
 		File dataDir = config.getDataDirectory();
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 2f85032520412e00a193a14b93a3913f2d73de07..1a54d02dc32420a22cad46339522e56c90855786 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
@@ -33,8 +33,10 @@ import de.vipra.util.StringUtils;
 import de.vipra.util.Timer;
 import de.vipra.util.ex.DatabaseException;
 import de.vipra.util.model.ArticleStats;
-import de.vipra.util.model.Topic;
+import de.vipra.util.model.TopicFull;
 import de.vipra.util.model.TopicRef;
+import de.vipra.util.model.Word;
+import de.vipra.util.model.WordMap;
 import de.vipra.util.service.DatabaseService;
 
 public class ImportCommand implements Command {
@@ -46,9 +48,11 @@ public class ImportCommand implements Command {
 	private JSONParser parser = new JSONParser();
 	private Config config;
 	private DatabaseService<ProcessedArticle> dbArticles;
-	private DatabaseService<Topic> dbTopics;
+	private DatabaseService<TopicFull> dbTopics;
+	private DatabaseService<Word> dbWords;
 	private Filebase filebase;
 	private Processor preprocessor;
+	private WordMap wordMap;
 	private LDAAnalyzer analyzer;
 
 	/**
@@ -106,7 +110,7 @@ public class ImportCommand implements Command {
 		try {
 			// preprocess text and generate text statistics
 			ProcessedText processedText = preprocessor.preprocess(article.getText());
-			ArticleStats articleStats = ArticleStats.generateFromText(processedText.getText());
+			ArticleStats articleStats = ArticleStats.generateFromText(processedText.getText(), wordMap);
 
 			// add article to mongodb
 			article.setProcessedText(processedText);
@@ -166,14 +170,15 @@ public class ImportCommand implements Command {
 	 * @throws DatabaseException
 	 */
 	private Map<String, String> saveTopicDefinitions() throws LDAAnalyzerException, DatabaseException {
-		ConvertStream<Topic> topics = analyzer.getTopicDefinitions();
+		ConvertStream<TopicFull> topics = analyzer.getTopicDefinitions();
 		Map<String, String> topicIndexMap = new HashMap<>();
 
 		// recreate topics in database
+		// create one topic at a time for less memory usage
 		dbTopics.drop();
-		for (Topic topic : topics) {
-			Topic newTopic = dbTopics.createSingle(topic);
-			topicIndexMap.put(Integer.toString(newTopic.getIndex()), newTopic.getId().toString());
+		for (TopicFull topic : topics) {
+			dbTopics.createSingle(topic);
+			topicIndexMap.put(Integer.toString(topic.getIndex()), topic.getId().toString());
 		}
 
 		return topicIndexMap;
@@ -223,10 +228,12 @@ public class ImportCommand implements Command {
 		try {
 			config = Config.getConfig();
 			dbArticles = DatabaseService.getDatabaseService(config, ProcessedArticle.class);
-			dbTopics = DatabaseService.getDatabaseService(config, Topic.class);
+			dbTopics = DatabaseService.getDatabaseService(config, TopicFull.class);
+			dbWords = DatabaseService.getDatabaseService(config, Word.class);
 			filebase = Filebase.getFilebase(config);
 			preprocessor = Processor.getPreprocessor(config);
-			analyzer = LDAAnalyzer.getAnalyzer(config);
+			wordMap = new WordMap(dbWords);
+			analyzer = LDAAnalyzer.getAnalyzer(config, wordMap);
 
 			out.info("using data directory: " + config.getDataDirectory().getAbsolutePath());
 			out.info("using preprocessor: " + preprocessor.getName());
@@ -238,26 +245,32 @@ public class ImportCommand implements Command {
 			// import files into database and filebase
 			out.info("file import");
 			long imported = importFiles(files);
-			long durImport = timer.lap();
+			timer.lap("import");
 
 			// write filebase
 			out.info("writing file index");
 			filebase.close();
-			timer.lap();
+			timer.lap("filebase write");
 
 			// do topic modeling
 			out.info("topic modeling");
 			analyzer.analyze();
-			long durAnalyze = timer.lap();
+			timer.lap("topic modeling");
 
 			// save topic model
-			out.info("saving topic models");
+			out.info("saving topic definitions");
 			Map<String, String> topicIndexMap = saveTopicDefinitions();
+			timer.lap("saving topics");
+
+			// save topic refs
+			out.info("saving document topics");
 			saveTopicsPerDocument(topicIndexMap);
+			timer.lap("saving topic refs");
 
-			out.info("imported " + imported + " " + (imported == 1 ? "article" : "articles"));
-			out.info("import: " + StringUtils.timeString(durImport) + ", analyze: "
-					+ StringUtils.timeString(durAnalyze));
+			out.info("imported " + imported + " new " + StringUtils.quantity(imported, "article"));
+			long newWords = wordMap.getNewWords();
+			out.info("imported " + newWords + " new " + StringUtils.quantity(newWords, "word"));
+			out.info(timer.toString());
 		} catch (Exception e) {
 			throw new ExecutionException(e);
 		}
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/StatsCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/StatsCommand.java
index 774fd43291690fbb0d065166f1ec7f43be745e03..4793f14f216a68e9b6816010c471b8d34a091d6e 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/option/StatsCommand.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/StatsCommand.java
@@ -12,7 +12,7 @@ import de.vipra.cmd.file.Filebase;
 import de.vipra.util.Config;
 import de.vipra.util.StringUtils;
 import de.vipra.util.ex.ConfigException;
-import de.vipra.util.model.Topic;
+import de.vipra.util.model.TopicFull;
 import de.vipra.util.service.DatabaseService;
 
 public class StatsCommand implements Command {
@@ -22,7 +22,7 @@ public class StatsCommand implements Command {
 
 	private Config config;
 	private Filebase filebase;
-	private DatabaseService<Topic> dbTopics;
+	private DatabaseService<TopicFull> dbTopics;
 
 	private void stats() {
 		File modelFile = filebase.getModelFile();
@@ -37,7 +37,7 @@ public class StatsCommand implements Command {
 		try {
 			config = Config.getConfig();
 			filebase = Filebase.getFilebase(config);
-			dbTopics = DatabaseService.getDatabaseService(config, Topic.class);
+			dbTopics = DatabaseService.getDatabaseService(config, TopicFull.class);
 
 			stats();
 		} catch (IOException | ConfigException | FilebaseException e) {
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 d0534bca8a96cb1943cb90951155fcb20a169a83..afc32c130db9ab84115c5be5218d529b8ba4bf25 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
@@ -4,6 +4,7 @@ import java.util.List;
 import java.util.Properties;
 
 import de.vipra.cmd.ex.PreprocessorException;
+import de.vipra.util.Constants;
 import edu.stanford.nlp.ling.CoreAnnotations.SentencesAnnotation;
 import edu.stanford.nlp.ling.CoreAnnotations.TokensAnnotation;
 import edu.stanford.nlp.ling.CoreLabel;
@@ -41,7 +42,8 @@ public class CoreNLPProcessor extends Processor {
 					sb.append(word.word()).append(" ");
 			}
 		}
-		return new ProcessedText(sb.toString().trim());
+		String text = sb.toString().trim().replaceAll(Constants.CHARS_DISALLOWED, "").replaceAll("\\s+", " ");
+		return new ProcessedText(text);
 	}
 
 }
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 fcf6521d6cf66f89724d0e972971f7a94bc1e57e..7b3974e7bf798aa4153843e8bc5fb55ea0a2c488 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
@@ -1,6 +1,5 @@
 package de.vipra.cmd.text;
 
-import java.util.Arrays;
 import java.util.List;
 
 import de.vipra.cmd.ex.PreprocessorException;
diff --git a/vipra-rest/pom.xml b/vipra-rest/pom.xml
index 8b81b7ee714fb55b749b29339048e3261b561fb8..ff073cf971f5db8c048fa8280b9a875cc5d7bc43 100644
--- a/vipra-rest/pom.xml
+++ b/vipra-rest/pom.xml
@@ -34,22 +34,7 @@
 		<dependency>
 			<groupId>org.glassfish.jersey.media</groupId>
 			<artifactId>jersey-media-json-jackson</artifactId>
-			<version>${jerseyVersion}</version>
-		</dependency>
-		<dependency>
-			<groupId>org.glassfish.jersey.test-framework</groupId>
-			<artifactId>jersey-test-framework-core</artifactId>
-			<version>${jerseyVersion}</version>
-		</dependency>
-		<dependency>
-			<groupId>org.glassfish.jersey.test-framework.providers</groupId>
-			<artifactId>jersey-test-framework-provider-simple</artifactId>
-			<version>${jerseyVersion}</version>
-		</dependency>
-		<dependency>
-			<groupId>com.github.fge</groupId>
-			<artifactId>json-patch</artifactId>
-			<version>1.9</version>
+			<version>2.22.1</version>
 		</dependency>
 
 		<!-- Servlet API -->
diff --git a/vipra-rest/src/main/java/de/vipra/rest/provider/ObjectMapperProvider.java b/vipra-rest/src/main/java/de/vipra/rest/provider/ObjectMapperProvider.java
index 9289222c737362b445d2e4318b2686381e424c40..e7f5577aaca3739c57e2515e223ad87e444e8cde 100644
--- a/vipra-rest/src/main/java/de/vipra/rest/provider/ObjectMapperProvider.java
+++ b/vipra-rest/src/main/java/de/vipra/rest/provider/ObjectMapperProvider.java
@@ -5,6 +5,7 @@ import java.text.SimpleDateFormat;
 import javax.ws.rs.ext.ContextResolver;
 import javax.ws.rs.ext.Provider;
 
+import org.bson.types.ObjectId;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -15,9 +16,11 @@ import com.fasterxml.jackson.databind.module.SimpleModule;
 
 import de.vipra.rest.serializer.GenericDeserializer;
 import de.vipra.rest.serializer.GenericSerializer;
+import de.vipra.rest.serializer.ObjectIdDeserializer;
+import de.vipra.rest.serializer.ObjectIdSerializer;
 import de.vipra.util.Constants;
 import de.vipra.util.model.Article;
-import de.vipra.util.model.Topic;
+import de.vipra.util.model.TopicFull;
 
 @Provider
 public class ObjectMapperProvider implements ContextResolver<ObjectMapper> {
@@ -39,8 +42,12 @@ public class ObjectMapperProvider implements ContextResolver<ObjectMapper> {
 		SimpleModule module = new SimpleModule();
 		module.addSerializer(Article.class, new GenericSerializer<Article>(Article.class));
 		module.addDeserializer(Article.class, new GenericDeserializer<Article>(Article.class));
-		module.addSerializer(Topic.class, new GenericSerializer<Topic>(Topic.class));
-		module.addDeserializer(Topic.class, new GenericDeserializer<Topic>(Topic.class));
+
+		module.addSerializer(TopicFull.class, new GenericSerializer<TopicFull>(TopicFull.class));
+		module.addDeserializer(TopicFull.class, new GenericDeserializer<TopicFull>(TopicFull.class));
+
+		module.addSerializer(ObjectId.class, new ObjectIdSerializer());
+		module.addDeserializer(ObjectId.class, new ObjectIdDeserializer());
 
 		final ObjectMapper mapper = new ObjectMapper();
 		mapper.enable(SerializationFeature.INDENT_OUTPUT);
diff --git a/vipra-rest/src/main/java/de/vipra/rest/resource/ArticleResource.java b/vipra-rest/src/main/java/de/vipra/rest/resource/ArticleResource.java
index e3c5d06aa49607cc2c3cf7990dc7f7e33b0e88dd..bf182eaacc59fe0f56f75fa70703944608d1c954 100644
--- a/vipra-rest/src/main/java/de/vipra/rest/resource/ArticleResource.java
+++ b/vipra-rest/src/main/java/de/vipra/rest/resource/ArticleResource.java
@@ -27,12 +27,11 @@ import de.vipra.rest.Messages;
 import de.vipra.rest.PATCH;
 import de.vipra.rest.model.APIError;
 import de.vipra.rest.model.Wrapper;
-import de.vipra.rest.service.ArticleService;
 import de.vipra.util.Config;
-import de.vipra.util.Mongo;
 import de.vipra.util.ex.ConfigException;
 import de.vipra.util.ex.DatabaseException;
 import de.vipra.util.model.Article;
+import de.vipra.util.service.DatabaseService;
 
 @Path("articles")
 public class ArticleResource {
@@ -40,20 +39,19 @@ public class ArticleResource {
 	@Context
 	UriInfo uri;
 
-	Cache<String, Article> articleCache;
-
-	final ArticleService service;
+	final Cache<String, Article> articleCache;
+	final DatabaseService<Article> service;
 
 	public ArticleResource(@Context ServletContext servletContext) throws ConfigException, IOException {
 		Config config = Config.getConfig();
-		Mongo mongo = Mongo.getInstance(config);
-		service = new ArticleService(mongo);
+		service = DatabaseService.getDatabaseService(config, Article.class);
 
 		CacheManager manager = (CacheManager) servletContext.getAttribute("cachemanager");
-		articleCache = manager.getCache("articlecache", String.class, Article.class);
+		Cache<String, Article> articleCache = manager.getCache("articlecache", String.class, Article.class);
 		if (articleCache == null)
 			articleCache = manager.createCache("articlecache",
 					CacheConfigurationBuilder.newCacheConfigurationBuilder().buildConfig(String.class, Article.class));
+		this.articleCache = articleCache;
 	}
 
 	@GET
@@ -61,7 +59,7 @@ public class ArticleResource {
 	public Response getArticles(@QueryParam("skip") @DefaultValue("0") int skip,
 			@QueryParam("limit") @DefaultValue("0") int limit,
 			@QueryParam("sort") @DefaultValue("date") String sortBy) {
-		List<Article> articles = service.getMultiple(uri.getAbsolutePath(), skip, limit, sortBy);
+		List<Article> articles = service.getMultiple(skip, limit, sortBy);
 		Wrapper<List<Article>> res = new Wrapper<>(articles);
 		return Response.ok().entity(res).tag(res.tag()).build();
 	}
diff --git a/vipra-rest/src/main/java/de/vipra/rest/resource/PingResource.java b/vipra-rest/src/main/java/de/vipra/rest/resource/PingResource.java
deleted file mode 100644
index 028c1cafc565077b56471b27eed997bb7c5ad2c4..0000000000000000000000000000000000000000
--- a/vipra-rest/src/main/java/de/vipra/rest/resource/PingResource.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package de.vipra.rest.resource;
-
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
-
-@Path("ping")
-public class PingResource {
-
-	@GET
-	@Produces(MediaType.TEXT_PLAIN)
-	public Response ping() {
-		return Response.ok().entity("running").build();
-	}
-
-}
diff --git a/vipra-rest/src/main/java/de/vipra/rest/resource/TopicResource.java b/vipra-rest/src/main/java/de/vipra/rest/resource/TopicResource.java
index df56b8c3b2273caaf7eb884ba7c8bee123998b3a..44cc2964da7a70befbeba57c8bc4b269fa9543ce 100644
--- a/vipra-rest/src/main/java/de/vipra/rest/resource/TopicResource.java
+++ b/vipra-rest/src/main/java/de/vipra/rest/resource/TopicResource.java
@@ -25,13 +25,11 @@ import de.vipra.rest.Messages;
 import de.vipra.rest.PATCH;
 import de.vipra.rest.model.APIError;
 import de.vipra.rest.model.Wrapper;
-import de.vipra.rest.service.TopicService;
 import de.vipra.util.Config;
-import de.vipra.util.Mongo;
 import de.vipra.util.ex.ConfigException;
 import de.vipra.util.ex.DatabaseException;
-import de.vipra.util.model.Article;
-import de.vipra.util.model.Topic;
+import de.vipra.util.model.TopicFull;
+import de.vipra.util.service.DatabaseService;
 
 @Path("topics")
 public class TopicResource {
@@ -39,28 +37,27 @@ public class TopicResource {
 	@Context
 	UriInfo uri;
 
-	Cache<String, Topic> topicCache;
-
-	TopicService service;
+	final Cache<String, TopicFull> topicCache;
+	final DatabaseService<TopicFull> service;
 
 	public TopicResource(@Context ServletContext servletContext) throws ConfigException, IOException {
 		Config config = Config.getConfig();
-		Mongo mongo = Mongo.getInstance(config);
-		service = new TopicService(mongo);
+		service = DatabaseService.getDatabaseService(config, TopicFull.class);
 
 		CacheManager manager = (CacheManager) servletContext.getAttribute("cachemanager");
-		topicCache = manager.getCache("topiccache", String.class, Topic.class);
+		Cache<String, TopicFull> topicCache = manager.getCache("topiccache", String.class, TopicFull.class);
 		if (topicCache == null)
-			topicCache = manager.createCache("topiccache",
-					CacheConfigurationBuilder.newCacheConfigurationBuilder().buildConfig(String.class, Topic.class));
+			topicCache = manager.createCache("topiccache", CacheConfigurationBuilder.newCacheConfigurationBuilder()
+					.buildConfig(String.class, TopicFull.class));
+		this.topicCache = topicCache;
 	}
 
 	@GET
 	@Produces(APIMediaType.APPLICATION_JSONAPI)
 	public Response getTopics(@QueryParam("skip") @DefaultValue("0") int skip,
 			@QueryParam("limit") @DefaultValue("0") int limit) {
-		List<Topic> topics = service.getMultiple(uri.getAbsolutePath(), skip, limit, null);
-		Wrapper<List<Topic>> res = new Wrapper<>(topics);
+		List<TopicFull> topics = service.getMultiple(skip, limit, null);
+		Wrapper<List<TopicFull>> res = new Wrapper<>(topics);
 		return Response.ok().entity(res).tag(res.tag()).build();
 	}
 
@@ -69,14 +66,14 @@ public class TopicResource {
 	@Consumes(APIMediaType.APPLICATION_JSONAPI)
 	@Path("{id}")
 	public Response getTopic(@PathParam("id") String id) {
-		Wrapper<Topic> res = new Wrapper<>();
+		Wrapper<TopicFull> res = new Wrapper<>();
 		if (id == null || id.trim().length() == 0) {
 			res.addError(new APIError(Response.Status.BAD_REQUEST, "ID is empty",
 					String.format(Messages.BAD_REQUEST, "id cannot be empty")));
 			return Response.status(Response.Status.BAD_REQUEST).entity(res).build();
 		}
 
-		Topic topic = getSingle(id);
+		TopicFull topic = getSingle(id);
 
 		if (topic != null) {
 			res.setData(topic);
@@ -92,9 +89,9 @@ public class TopicResource {
 	@Consumes(APIMediaType.APPLICATION_JSONAPI)
 	@Produces(APIMediaType.APPLICATION_JSONAPI)
 	@Path("{id}")
-	public Response replaceTopic(@PathParam("id") String id, Wrapper<Topic> wrapper) {
-		Topic topic = wrapper.getData();
-		Wrapper<Topic> res = new Wrapper<>();
+	public Response replaceTopic(@PathParam("id") String id, Wrapper<TopicFull> wrapper) {
+		TopicFull topic = wrapper.getData();
+		Wrapper<TopicFull> res = new Wrapper<>();
 		try {
 			service.updateSingle(topic);
 			topicCache.put(id, topic);
@@ -111,15 +108,15 @@ public class TopicResource {
 	@Consumes(APIMediaType.APPLICATION_JSONAPI)
 	@Produces(APIMediaType.APPLICATION_JSONAPI)
 	@Path("{id}")
-	public Response updateTopic(@PathParam("id") String id, Wrapper<Topic> wrapper) {
-		Topic newTopic = wrapper.getData();
-		Topic topic = getSingle(id);
+	public Response updateTopic(@PathParam("id") String id, Wrapper<TopicFull> wrapper) {
+		TopicFull newTopic = wrapper.getData();
+		TopicFull topic = getSingle(id);
 		// TODO implement
 		return null;
 	}
 
-	private Topic getSingle(String id) {
-		Topic topic = topicCache.get(id);
+	private TopicFull getSingle(String id) {
+		TopicFull topic = topicCache.get(id);
 		if (topic == null) {
 			topic = service.getSingle(id);
 			if (topic != null)
diff --git a/vipra-rest/src/main/java/de/vipra/rest/serializer/GenericDeserializer.java b/vipra-rest/src/main/java/de/vipra/rest/serializer/GenericDeserializer.java
index 710073d2687bba690a903960188d9dfd147cfad6..ff9ea7d98237212ef1c0b82d7cffdef0a520781e 100644
--- a/vipra-rest/src/main/java/de/vipra/rest/serializer/GenericDeserializer.java
+++ b/vipra-rest/src/main/java/de/vipra/rest/serializer/GenericDeserializer.java
@@ -8,6 +8,7 @@ import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.core.JsonParseException;
 import com.fasterxml.jackson.core.JsonParser;
@@ -17,7 +18,6 @@ import com.fasterxml.jackson.databind.DeserializationContext;
 import com.fasterxml.jackson.databind.JsonDeserializer;
 
 import de.vipra.util.an.JsonField;
-import de.vipra.util.an.JsonIgnore;
 import de.vipra.util.an.JsonWrap;
 import de.vipra.util.model.Model;
 
@@ -32,7 +32,8 @@ public class GenericDeserializer<T extends Model> extends JsonDeserializer<T> {
 
 		Field[] fields = clazz.getDeclaredFields();
 		for (Field field : fields) {
-			if (Modifier.isPrivate(field.getModifiers())) {
+			int modifiers = field.getModifiers();
+			if (Modifier.isPrivate(modifiers) && !Modifier.isStatic(modifiers)) {
 				field.setAccessible(true);
 
 				JsonIgnore ji = field.getDeclaredAnnotation(JsonIgnore.class);
diff --git a/vipra-rest/src/main/java/de/vipra/rest/serializer/GenericSerializer.java b/vipra-rest/src/main/java/de/vipra/rest/serializer/GenericSerializer.java
index 85311f4a35b009239972ebd794fdbfe7153e81e1..b7c896c0e33cb600c4ac2895fefdc12b55ea7a29 100644
--- a/vipra-rest/src/main/java/de/vipra/rest/serializer/GenericSerializer.java
+++ b/vipra-rest/src/main/java/de/vipra/rest/serializer/GenericSerializer.java
@@ -11,9 +11,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 
-import org.bson.types.ObjectId;
-
-import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.core.JsonGenerator;
 import com.fasterxml.jackson.core.JsonProcessingException;
@@ -41,11 +38,14 @@ public class GenericSerializer<T extends Model> extends JsonSerializer<T> {
 
 		Field[] fields = clazz.getDeclaredFields();
 		for (Field field : fields) {
-			if (Modifier.isPrivate(field.getModifiers())) {
+			int modifiers = field.getModifiers();
+			if (Modifier.isPrivate(modifiers) && !Modifier.isStatic(modifiers)) {
 				field.setAccessible(true);
 
-				JsonIgnore ji = field.getDeclaredAnnotation(JsonIgnore.class);
-				if (ji != null && ji.value())
+				com.fasterxml.jackson.annotation.JsonIgnore ji1 = field
+						.getDeclaredAnnotation(com.fasterxml.jackson.annotation.JsonIgnore.class);
+				de.vipra.util.an.JsonIgnore ji2 = field.getDeclaredAnnotation(de.vipra.util.an.JsonIgnore.class);
+				if ((ji1 != null && ji1.value()) || (ji2 != null && ji2.value()))
 					continue;
 
 				String name = field.getName();
@@ -91,12 +91,8 @@ public class GenericSerializer<T extends Model> extends JsonSerializer<T> {
 				e.printStackTrace();
 			}
 
-			if (v != null) {
-				if (v instanceof ObjectId)
-					v = ((ObjectId) v).toString();
-
+			if (v != null)
 				pathAdd(map, entry.getKey(), v);
-			}
 		}
 
 		serializers.defaultSerializeValue(map, gen);
diff --git a/vipra-rest/src/main/java/de/vipra/rest/serializer/ObjectIdDeserializer.java b/vipra-rest/src/main/java/de/vipra/rest/serializer/ObjectIdDeserializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..4a6f5994ef36e243e63d642c73a76a12781ede36
--- /dev/null
+++ b/vipra-rest/src/main/java/de/vipra/rest/serializer/ObjectIdDeserializer.java
@@ -0,0 +1,21 @@
+package de.vipra.rest.serializer;
+
+import java.io.IOException;
+
+import org.bson.types.ObjectId;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+
+import de.vipra.util.MongoUtils;
+
+public class ObjectIdDeserializer extends JsonDeserializer<ObjectId> {
+
+	@Override
+	public ObjectId deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
+		return MongoUtils.objectId(p.getValueAsString());
+	}
+
+}
diff --git a/vipra-rest/src/main/java/de/vipra/rest/serializer/ObjectIdSerializer.java b/vipra-rest/src/main/java/de/vipra/rest/serializer/ObjectIdSerializer.java
new file mode 100644
index 0000000000000000000000000000000000000000..b6653911281c5436f63fbbc4cb5af93e633905cd
--- /dev/null
+++ b/vipra-rest/src/main/java/de/vipra/rest/serializer/ObjectIdSerializer.java
@@ -0,0 +1,20 @@
+package de.vipra.rest.serializer;
+
+import java.io.IOException;
+
+import org.bson.types.ObjectId;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+
+public class ObjectIdSerializer extends JsonSerializer<ObjectId> {
+
+	@Override
+	public void serialize(ObjectId value, JsonGenerator gen, SerializerProvider serializers)
+			throws IOException, JsonProcessingException {
+		gen.writeString(value.toString());
+	}
+
+}
diff --git a/vipra-rest/src/main/java/de/vipra/rest/service/ArticleService.java b/vipra-rest/src/main/java/de/vipra/rest/service/ArticleService.java
deleted file mode 100644
index b53aaad4e5c95f216f835ca6117c22415c127f30..0000000000000000000000000000000000000000
--- a/vipra-rest/src/main/java/de/vipra/rest/service/ArticleService.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package de.vipra.rest.service;
-
-import java.net.URI;
-import java.util.List;
-
-import de.vipra.util.Mongo;
-import de.vipra.util.model.Article;
-import de.vipra.util.service.DatabaseService;
-
-public class ArticleService extends DatabaseService<Article> {
-
-	public ArticleService(Mongo mongo) {
-		super(mongo, de.vipra.util.model.Article.class);
-	}
-
-	public List<Article> getMultiple(URI base, int skip, int limit, String sortBy) {
-		List<Article> articles = super.getMultiple(skip, limit, sortBy);
-		for (Article article : articles) {
-			// delete data for listing
-			article.setText(null);
-			article.setStats(null);
-			article.setTopics(null);
-		}
-		return articles;
-	}
-
-}
diff --git a/vipra-rest/src/main/java/de/vipra/rest/service/TopicService.java b/vipra-rest/src/main/java/de/vipra/rest/service/TopicService.java
deleted file mode 100644
index 4ec6eb604d23f49d37282bfb6524bcca1ee46285..0000000000000000000000000000000000000000
--- a/vipra-rest/src/main/java/de/vipra/rest/service/TopicService.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package de.vipra.rest.service;
-
-import java.net.URI;
-import java.util.List;
-
-import de.vipra.util.Mongo;
-import de.vipra.util.model.Topic;
-import de.vipra.util.service.DatabaseService;
-
-public class TopicService extends DatabaseService<Topic> {
-
-	public TopicService(Mongo mongo) {
-		super(mongo, de.vipra.util.model.Topic.class);
-	}
-
-	public List<Topic> getMultiple(URI base, int skip, int limit, String sortBy) {
-		List<Topic> topics = super.getMultiple(skip, limit, sortBy);
-		for (Topic topic : topics) {
-			topic.setWords(null);
-		}
-		return topics;
-	}
-
-}
diff --git a/vipra-ui/app/templates/articles/show.hbs b/vipra-ui/app/templates/articles/show.hbs
index 9ee5c2bc361cde649cfa35427a519bb54788a600..bcddff844c6ad60c7bd715799cc4b4ed9b1489c2 100644
--- a/vipra-ui/app/templates/articles/show.hbs
+++ b/vipra-ui/app/templates/articles/show.hbs
@@ -7,49 +7,21 @@
   <dd>{{model.article.date}}</dd>
   <dt>URL</dt>
   <dd><a href="{{model.article.url}}">{{model.article.url}}</a></dd>
+  <dt>Word count</dt>
+  <dd>{{model.article.stats.wordCount}}</dd>
 </dl>
 
 <h3>Topics</h3>
 
-{{#each model.article.topics as |topic|}}
-  {{#topic-link topic=topic}} ({{topic-share topic.count model.article.stats.wordCount}}%){{/topic-link}}
+{{#each model.article.topics as |topicRef|}}
+  [{{#topic-link topic=topicRef.topic}} ({{topic-share topicRef.count model.article.stats.wordCount}}%){{/topic-link}}]
 {{/each}}
 
 <h3>Statistics</h3>
 
 <table>
   <tbody>
-    <tr>
-      <td>Word count</td>
-      <td>{{model.article.stats.wordCount}}</td>
-    </tr>
-    <tr>
-      <td>Unique word count</td>
-      <td>{{model.article.stats.uniqueWordCount}}</td>
-    </tr>
-    <tr>
-      <td>Most frequent words</td>
-      <td>
-        <table>
-          <thead>
-            <tr>
-              <th>Word</th>
-              <th>Count</th>
-              <th>NTF</th>
-            </tr>
-          </thead>
-          <tbody>
-            {{#each model.statsData as |stats|}}
-              <tr>
-                <td>{{stats.word}}</td>
-                <td>{{stats.count}}</td>
-                <td>{{stats.norm}}</td>
-              </tr>
-            {{/each}}
-          </tbody>
-        </table>
-      </td>
-    </tr>
+    
   </tbody>
 </table>
 
diff --git a/vipra-ui/app/templates/topics/index.hbs b/vipra-ui/app/templates/topics/index.hbs
index f7cabdd7052be50106761da749d418fda9acf5f6..a8a4eece3ceafbe0649445c4f7f031a0e6a26227 100644
--- a/vipra-ui/app/templates/topics/index.hbs
+++ b/vipra-ui/app/templates/topics/index.hbs
@@ -1,5 +1,5 @@
 <h2>Found topics</h2>
 
-{{debounced-input placeholder='Filter' size='50' valueBinding='filter' debounce='150'}}
+{{debounced-input placeholder='Filter' size='50' value=filter debounce='150'}}
 
 {{topics-list items=model.topics filter=filter}}
\ No newline at end of file
diff --git a/vipra-ui/app/templates/topics/show/index.hbs b/vipra-ui/app/templates/topics/show/index.hbs
index 0a8dcf51f60d093085587ee44c815954a09f5d6f..e7f210342630bac1cc8bf90b340116afe2218b87 100644
--- a/vipra-ui/app/templates/topics/show/index.hbs
+++ b/vipra-ui/app/templates/topics/show/index.hbs
@@ -1,11 +1,3 @@
 {{#link-to 'topics.show.edit'}}Edit{{/link-to}}
 
-<h2>{{model.topic._name}}</h2>
-
-<h3>Words</h3>
-
-{{#each model.topic.words as |word|}}
-  <span class="word" title="Likeliness: {{word.likeliness}}">{{word.word}}</span>
-{{/each}}
-
-<h3>Articles</h3>
\ No newline at end of file
+<h2>{{model.topic._name}}</h2>
\ No newline at end of file
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 c5eea30ef8a97ba510884f3d40ba5f676078ab3e..22072d5de9984be43d0047df0209f1bd6230cce9 100644
--- a/vipra-util/src/main/java/de/vipra/util/Constants.java
+++ b/vipra-util/src/main/java/de/vipra/util/Constants.java
@@ -41,7 +41,7 @@ public class Constants {
 	 * expression is used to strip text of characters that should not be
 	 * processed.
 	 */
-	public static final String CHARS_DISALLOWED = "[^a-zA-Z0-9]";
+	public static final String CHARS_DISALLOWED = "[^a-zA-Z0-9 ]";
 
 	/**
 	 * The number of words to be used to generate a topic name. The top n words
@@ -127,7 +127,7 @@ public class Constants {
 		CUSTOM("custom"),
 		CORENLP("corenlp"),
 		LUCENE("lucene"),
-		DEFAULT(LUCENE);
+		DEFAULT(CORENLP);
 
 		public final String name;
 
diff --git a/vipra-util/src/main/java/de/vipra/util/ListUtils.java b/vipra-util/src/main/java/de/vipra/util/ListUtils.java
index bddd22abffc7bc8bba3df0f3fe6ab383cb71af75..8daf6a8a59d8b8f9ecee92a336ef7696a2a30970 100644
--- a/vipra-util/src/main/java/de/vipra/util/ListUtils.java
+++ b/vipra-util/src/main/java/de/vipra/util/ListUtils.java
@@ -1,5 +1,6 @@
 package de.vipra.util;
 
+import java.util.ArrayList;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
@@ -21,4 +22,11 @@ public class ListUtils {
 		return strings;
 	}
 
+	public static <T> List<T> toList(Iterable<T> it) {
+		List<T> list = new ArrayList<>();
+		for (T t : it)
+			list.add(t);
+		return list;
+	}
+
 }
diff --git a/vipra-util/src/main/java/de/vipra/util/StringUtils.java b/vipra-util/src/main/java/de/vipra/util/StringUtils.java
index 566ade74a16f1c2e16a8389c8d8f9ee09ab385b4..adeda301024f9a75363d7a010e84b8f183e6222a 100644
--- a/vipra-util/src/main/java/de/vipra/util/StringUtils.java
+++ b/vipra-util/src/main/java/de/vipra/util/StringUtils.java
@@ -124,4 +124,14 @@ public class StringUtils {
 		return null;
 	}
 
+	public static String quantity(Number qty, String singular, String plural) {
+		if (qty.intValue() == 1)
+			return singular;
+		return plural;
+	}
+
+	public static String quantity(Number qty, String singular) {
+		return quantity(qty, singular, singular + "s");
+	}
+
 }
diff --git a/vipra-util/src/main/java/de/vipra/util/Timer.java b/vipra-util/src/main/java/de/vipra/util/Timer.java
index 9ca70c5188281fe10cfd4ac7b05eced4b4fcdbb4..1ec369f9f0b7ba0a748e490aec8a1ef7467e52cd 100644
--- a/vipra-util/src/main/java/de/vipra/util/Timer.java
+++ b/vipra-util/src/main/java/de/vipra/util/Timer.java
@@ -1,11 +1,17 @@
 package de.vipra.util;
 
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
 public class Timer {
 
 	private long start;
+	private Map<String, Long> laps;
 
 	public long start() {
 		start = System.nanoTime();
+		laps = new LinkedHashMap<>();
 		return start;
 	}
 
@@ -19,4 +25,24 @@ public class Timer {
 		return lap;
 	}
 
+	public long lap(String name) {
+		long lap = lap();
+		laps.put(name, lap);
+		return lap;
+	}
+
+	public String toString() {
+		String out = null;
+		if (laps != null && laps.size() > 0) {
+			StringBuilder sb = new StringBuilder();
+			for (Entry<String, Long> e : laps.entrySet()) {
+				sb.append(", ").append(e.getKey()).append(": ").append(StringUtils.timeString(e.getValue()));
+			}
+			out = sb.toString().substring(2);
+		} else {
+			out = super.toString();
+		}
+		return out;
+	}
+
 }
diff --git a/vipra-util/src/main/java/de/vipra/util/an/JsonIgnore.java b/vipra-util/src/main/java/de/vipra/util/an/JsonIgnore.java
index bd89d6f11dc38327f62c74b6963ddf54e2be9249..70e5b17b880d5f318747978acc587cf8a61b64b5 100644
--- a/vipra-util/src/main/java/de/vipra/util/an/JsonIgnore.java
+++ b/vipra-util/src/main/java/de/vipra/util/an/JsonIgnore.java
@@ -6,9 +6,9 @@ import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
 @Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.TYPE)
+@Target(ElementType.FIELD)
 public @interface JsonIgnore {
 
-	public boolean value() default false;
+	public boolean value() default true;
 
-}
+}
\ 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
new file mode 100644
index 0000000000000000000000000000000000000000..422439e7d892fb45e675ce6eec0115310e34e446
--- /dev/null
+++ b/vipra-util/src/main/java/de/vipra/util/an/QueryIgnore.java
@@ -0,0 +1,16 @@
+package de.vipra.util.an;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface QueryIgnore {
+
+	public boolean single() default false;
+
+	public boolean multi() default false;
+
+}
diff --git a/vipra-util/src/main/java/de/vipra/util/model/Article.java b/vipra-util/src/main/java/de/vipra/util/model/Article.java
index 17531569ba14608ae5d199653a8b0ec11fa3c418..72da35029f52ed7f53ba6db80a6059714751dba8 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/Article.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/Article.java
@@ -18,43 +18,43 @@ import de.vipra.util.Constants;
 import de.vipra.util.FileUtils;
 import de.vipra.util.MongoUtils;
 import de.vipra.util.StringUtils;
-import de.vipra.util.an.JsonField;
+import de.vipra.util.an.JsonWrap;
+import de.vipra.util.an.QueryIgnore;
 
+@SuppressWarnings("serial")
 @Entity(value = "articles", noClassnameStored = true)
 public class Article extends Model implements Serializable {
 
-	private static final long serialVersionUID = -3357348905924854240L;
-
 	@Id
 	private ObjectId id;
 
-	@JsonField("attributes.title")
+	@JsonWrap("attributes")
 	private String title;
 
-	@JsonField("attributes.text")
+	@JsonWrap("attributes")
+	@QueryIgnore(multi = true)
 	private String text;
 
-	@JsonField("attributes.url")
+	@JsonWrap("attributes")
 	private String url;
 
-	@JsonField("attributes.date")
+	@JsonWrap("attributes")
 	private Date date;
 
-	@JsonField("attributes.complete")
-	private boolean complete;
-
 	@Embedded
-	@JsonField("attributes.stats")
-	private ArticleStats stats;
+	@JsonWrap("attributes")
+	@QueryIgnore(multi = true)
+	private List<TopicRef> topics;
 
 	@Embedded
-	@JsonField("attributes.topics")
-	private List<TopicRef> topics;
+	@JsonWrap("attributes")
+	@QueryIgnore(multi = true)
+	private ArticleStats stats;
 
-	@JsonField("attributes.created")
+	@JsonWrap("attributes")
 	private Date created = new Date();
 
-	@JsonField("attributes.modified")
+	@JsonWrap("attributes")
 	private Date modified;
 
 	public ObjectId getId() {
@@ -101,22 +101,6 @@ public class Article extends Model implements Serializable {
 		this.date = date;
 	}
 
-	public boolean isComplete() {
-		return complete;
-	}
-
-	public void setComplete(boolean complete) {
-		this.complete = complete;
-	}
-
-	public ArticleStats getStats() {
-		return stats;
-	}
-
-	public void setStats(ArticleStats stats) {
-		this.stats = stats;
-	}
-
 	public void setDate(String date) {
 		SimpleDateFormat df = new SimpleDateFormat(Constants.DATETIME_FORMAT);
 		try {
@@ -132,6 +116,14 @@ public class Article extends Model implements Serializable {
 		this.topics = topics;
 	}
 
+	public ArticleStats getStats() {
+		return stats;
+	}
+
+	public void setStats(ArticleStats stats) {
+		this.stats = stats;
+	}
+
 	public Date getCreated() {
 		return created;
 	}
diff --git a/vipra-util/src/main/java/de/vipra/util/model/ArticleStats.java b/vipra-util/src/main/java/de/vipra/util/model/ArticleStats.java
index 579d5621a7498824adf6ea19ebf9acbc8c749e8a..06a3b1e9f9069fba467b892a8d9809d70cf83401 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/ArticleStats.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/ArticleStats.java
@@ -1,11 +1,8 @@
 package de.vipra.util.model;
 
 import java.io.Serializable;
-import java.util.HashMap;
-import java.util.Map;
 
 import org.bson.types.ObjectId;
-import org.mongodb.morphia.annotations.Embedded;
 import org.mongodb.morphia.annotations.Entity;
 import org.mongodb.morphia.annotations.Id;
 
@@ -13,13 +10,10 @@ import org.mongodb.morphia.annotations.Id;
 public class ArticleStats implements Serializable {
 
 	private static final long serialVersionUID = -4712841724990200627L;
-	
+
 	@Id
 	private ObjectId id;
 	private long wordCount;
-	private long uniqueWordCount;
-	@Embedded
-	private Map<String, TermFrequency> uniqueWords;
 
 	public ObjectId getId() {
 		return id;
@@ -37,57 +31,16 @@ public class ArticleStats implements Serializable {
 		this.wordCount = wordCount;
 	}
 
-	public long getUniqueWordCount() {
-		return uniqueWordCount;
-	}
-
-	public void setUniqueWordCount(long uniqueWordCount) {
-		this.uniqueWordCount = uniqueWordCount;
-	}
-
-	public Map<String, TermFrequency> getUniqueWords() {
-		return uniqueWords;
-	}
-
-	public void setUniqueWords(Map<String, TermFrequency> uniqueWords) {
-		this.uniqueWords = uniqueWords;
-	}
-
-	public static ArticleStats generateFromText(final String text) {
+	public static ArticleStats generateFromText(final String text, final WordMap wordMap) {
 		ArticleStats stats = new ArticleStats();
 		String[] words = text.split("\\s+");
 		stats.setWordCount(words.length);
-		Map<String, TermFrequency> uniqueWords = new HashMap<>();
-		long maxFrequency = 0;
-
-		// loop and count unique words
-		// also remember maximum frequency
-		for (String word : words) {
-			TermFrequency tf = uniqueWords.get(word);
-			if (tf == null) {
-				tf = new TermFrequency();
-			}
-			tf.incrementTermFrequency();
-			if (tf.getTermFrequency() > maxFrequency) {
-				maxFrequency = tf.getTermFrequency();
-			}
-			uniqueWords.put(word, tf);
-		}
-
-		// normalize frequencies
-		for (Map.Entry<String, TermFrequency> entry : uniqueWords.entrySet()) {
-			entry.getValue().normalizeTermFrequency(maxFrequency);
-		}
-
-		stats.setUniqueWordCount(uniqueWords.size());
-		stats.setUniqueWords(uniqueWords);
 		return stats;
 	}
 
 	@Override
 	public String toString() {
-		return ArticleStats.class.getSimpleName() + "[id:" + id + ", wordCount:" + wordCount + ", uniqueWordCount:"
-				+ uniqueWordCount + "]";
+		return ArticleStats.class.getSimpleName() + "[id:" + id + ", wordCount:" + wordCount + "]";
 	}
 
-}
+}
\ No newline at end of file
diff --git a/vipra-util/src/main/java/de/vipra/util/model/Model.java b/vipra-util/src/main/java/de/vipra/util/model/Model.java
index 70baf2257b93c900ef71832336926302149075bd..8fa5aab18b609ce00af81ade8e02059ae472610e 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/Model.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/Model.java
@@ -11,10 +11,9 @@ import org.bson.types.ObjectId;
 
 import de.vipra.util.Constants;
 
+@SuppressWarnings("serial")
 public abstract class Model implements Serializable {
 
-	private static final long serialVersionUID = -1991594352707918633L;
-
 	public URI uri(URI base) {
 		try {
 			return new URI(base.toString() + "/" + getId().toString());
diff --git a/vipra-util/src/main/java/de/vipra/util/model/TermFrequency.java b/vipra-util/src/main/java/de/vipra/util/model/TermFrequency.java
deleted file mode 100644
index 75099a27a65c8fe9e7bbb79fd8302e8809359c0f..0000000000000000000000000000000000000000
--- a/vipra-util/src/main/java/de/vipra/util/model/TermFrequency.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package de.vipra.util.model;
-
-import java.io.Serializable;
-
-import org.mongodb.morphia.annotations.Embedded;
-
-@Embedded
-public class TermFrequency implements Serializable {
-
-	private static final long serialVersionUID = 4042573510472738071L;
-
-	private long termFrequency = 0;
-	private double normalizedTermFrequency = 0;
-
-	public long getTermFrequency() {
-		return termFrequency;
-	}
-
-	public void setTermFrequency(long termFrequency) {
-		this.termFrequency = termFrequency;
-	}
-
-	public double getNormalizedTermFrequency() {
-		return normalizedTermFrequency;
-	}
-
-	public void setNormalizedTermFrequency(double normalizedTermFrequency) {
-		this.normalizedTermFrequency = normalizedTermFrequency;
-	}
-
-	public void normalizeTermFrequency(double max) {
-		setNormalizedTermFrequency(getTermFrequency() / max);
-	}
-
-	public void incrementTermFrequency() {
-		setTermFrequency(getTermFrequency() + 1);
-	}
-
-	@Override
-	public String toString() {
-		return TermFrequency.class.getSimpleName() + "[termFrequency:" + termFrequency + ", normalizedTermFrequency:"
-				+ normalizedTermFrequency + "]";
-	}
-
-}
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 1d9488cd2e2235e8979d82a6fe304e1c1da5aa5b..1e1d7a47af4967da703cde59b19124835c851a6d 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
@@ -3,52 +3,22 @@ package de.vipra.util.model;
 import java.io.File;
 import java.io.IOException;
 import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
 
 import org.bson.types.ObjectId;
-import org.mongodb.morphia.annotations.Embedded;
 import org.mongodb.morphia.annotations.Entity;
 import org.mongodb.morphia.annotations.Id;
-import org.mongodb.morphia.annotations.PrePersist;
 
-import de.vipra.util.Constants;
 import de.vipra.util.MongoUtils;
-import de.vipra.util.StringUtils;
-import de.vipra.util.an.JsonWrap;
 import de.vipra.util.ex.NotImplementedException;
 
+@SuppressWarnings("serial")
 @Entity(value = "topics", noClassnameStored = true)
 public class Topic extends Model implements Serializable {
 
-	private static final long serialVersionUID = 7121629487498450992L;
-
 	@Id
 	private ObjectId id;
-
-	@JsonWrap("attributes")
-	private int index;
-
-	@JsonWrap("attributes")
 	private String name;
 
-	@Embedded
-	@JsonWrap("attributes")
-	private List<TopicWord> words;
-
-	@JsonWrap("attributes")
-	private Date created = new Date();
-
-	@JsonWrap("attributes")
-	private Date modified;
-
-	public Topic() {}
-
-	public Topic(List<TopicWord> words) {
-		this.words = words;
-	}
-
 	public ObjectId getId() {
 		return id;
 	}
@@ -61,14 +31,6 @@ public class Topic extends Model implements Serializable {
 		this.id = MongoUtils.objectId(id);
 	}
 
-	public int getIndex() {
-		return index;
-	}
-
-	public void setIndex(int index) {
-		this.index = index;
-	}
-
 	public String getName() {
 		return name;
 	}
@@ -77,43 +39,6 @@ public class Topic extends Model implements Serializable {
 		this.name = name;
 	}
 
-	public List<TopicWord> getWords() {
-		return words;
-	}
-
-	public void setWords(List<TopicWord> words) {
-		this.words = words;
-	}
-
-	public Date getCreated() {
-		return created;
-	}
-
-	public void setCreated(Date created) {
-		this.created = created;
-	}
-
-	public Date getModified() {
-		return modified;
-	}
-
-	public void setModified(Date modified) {
-		this.modified = modified;
-	}
-
-	public String getNameFromWords() {
-		String name = null;
-		if (words != null && words.size() > 0) {
-			int size = Math.min(Constants.AUTO_TOPIC_WORDS, words.size());
-			List<String> topWords = new ArrayList<>(size);
-			for (int i = 0; i < size; i++) {
-				topWords.add(words.get(i).getWord());
-			}
-			name = StringUtils.join(topWords);
-		}
-		return name;
-	}
-
 	@Override
 	public void fromFile(File file) throws IOException {
 		throw new NotImplementedException();
@@ -124,15 +49,4 @@ public class Topic extends Model implements Serializable {
 		throw new NotImplementedException();
 	}
 
-	@Override
-	public String toString() {
-		return Topic.class.getSimpleName() + "[id:" + id + ", name:" + name + ", created:" + created + ", modified:"
-				+ modified + "]";
-	}
-
-	@PrePersist
-	public void prePersist() {
-		this.modified = new Date();
-	}
-
 }
diff --git a/vipra-util/src/main/java/de/vipra/util/model/TopicFull.java b/vipra-util/src/main/java/de/vipra/util/model/TopicFull.java
new file mode 100644
index 0000000000000000000000000000000000000000..5af0a958ee8598d8f87bed4b5cbb6a3213d064ac
--- /dev/null
+++ b/vipra-util/src/main/java/de/vipra/util/model/TopicFull.java
@@ -0,0 +1,135 @@
+package de.vipra.util.model;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import org.bson.types.ObjectId;
+import org.mongodb.morphia.annotations.Embedded;
+import org.mongodb.morphia.annotations.Entity;
+import org.mongodb.morphia.annotations.Id;
+import org.mongodb.morphia.annotations.PrePersist;
+
+import de.vipra.util.Constants;
+import de.vipra.util.MongoUtils;
+import de.vipra.util.StringUtils;
+import de.vipra.util.an.JsonType;
+import de.vipra.util.an.JsonWrap;
+import de.vipra.util.an.QueryIgnore;
+import de.vipra.util.ex.NotImplementedException;
+
+@SuppressWarnings("serial")
+@JsonType("topic")
+@Entity(value = "topics", noClassnameStored = true)
+public class TopicFull extends Model implements Serializable {
+
+	@Id
+	private ObjectId id;
+
+	@JsonWrap("attributes")
+	private String name;
+
+	@JsonWrap("attributes")
+	private int index;
+
+	@Embedded
+	@JsonWrap("attributes")
+	@QueryIgnore(multi = true)
+	private List<TopicWord> topicWords;
+
+	@JsonWrap("attributes")
+	private Date created = new Date();
+
+	@JsonWrap("attributes")
+	private Date modified;
+
+	public ObjectId getId() {
+		return id;
+	}
+
+	public void setId(ObjectId id) {
+		this.id = id;
+	}
+
+	public void setId(String id) {
+		this.id = MongoUtils.objectId(id);
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public int getIndex() {
+		return index;
+	}
+
+	public void setIndex(int index) {
+		this.index = index;
+	}
+
+	public List<TopicWord> getTopicWords() {
+		return topicWords;
+	}
+
+	public void setTopicWords(List<TopicWord> topicWords) {
+		this.topicWords = topicWords;
+	}
+
+	public Date getCreated() {
+		return created;
+	}
+
+	public void setCreated(Date created) {
+		this.created = created;
+	}
+
+	public Date getModified() {
+		return modified;
+	}
+
+	public void setModified(Date modified) {
+		this.modified = modified;
+	}
+
+	@Override
+	public void fromFile(File file) throws IOException {
+		throw new NotImplementedException();
+	}
+
+	@Override
+	public String toFileString() {
+		throw new NotImplementedException();
+	}
+
+	@Override
+	public String toString() {
+		return TopicFull.class.getSimpleName() + "[id:" + getId() + ", name:" + getName() + ", created:" + created
+				+ ", modified:" + modified + "]";
+	}
+
+	@PrePersist
+	public void prePersist() {
+		this.modified = new Date();
+	}
+
+	public static String getNameFromWords(List<TopicWord> words) {
+		String name = null;
+		if (words != null && words.size() > 0) {
+			int size = Math.min(Constants.AUTO_TOPIC_WORDS, words.size());
+			List<String> topWords = new ArrayList<>(size);
+			for (int i = 0; i < size; i++) {
+				topWords.add(words.get(i).getWord().getWord());
+			}
+			name = StringUtils.join(topWords);
+		}
+		return name;
+	}
+
+}
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 e6b6df22bf13665dbc7b82990bbf34b58002fda1..0380a0dac47eb9e0375ceb7ec47cf70a9454d9f4 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
@@ -4,14 +4,18 @@ import java.io.Serializable;
 
 import org.mongodb.morphia.annotations.Embedded;
 import org.mongodb.morphia.annotations.Reference;
+import org.mongodb.morphia.annotations.Transient;
 
+import de.vipra.util.an.JsonIgnore;
+
+@SuppressWarnings("serial")
 @Embedded
 public class TopicRef implements Comparable<TopicRef>, Serializable {
 
-	private static final long serialVersionUID = 3301635858822787398L;
-
+	@Transient
+	@JsonIgnore
 	private String topicId;
-	@Reference(lazy = true)
+	@Reference
 	private Topic topic;
 	private int count;
 
@@ -33,6 +37,14 @@ public class TopicRef implements Comparable<TopicRef>, Serializable {
 		this.count = count;
 	}
 
+	public Topic getTopic() {
+		return topic;
+	}
+
+	public void setTopic(Topic topic) {
+		this.topic = topic;
+	}
+
 	@Override
 	public int compareTo(TopicRef arg0) {
 		return count - arg0.getCount();
diff --git a/vipra-util/src/main/java/de/vipra/util/model/TopicWord.java b/vipra-util/src/main/java/de/vipra/util/model/TopicWord.java
index 8a1f1f20c20d1f6f950e578b460102d27aaf2a72..5bf9a5cbcea3b5af21eee0060a7c34cdfcac08c8 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/TopicWord.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/TopicWord.java
@@ -3,27 +3,28 @@ package de.vipra.util.model;
 import java.io.Serializable;
 
 import org.mongodb.morphia.annotations.Embedded;
+import org.mongodb.morphia.annotations.Reference;
 
+@SuppressWarnings("serial")
 @Embedded
 public class TopicWord implements Comparable<TopicWord>, Serializable {
 
-	private static final long serialVersionUID = -5409441821591159243L;
-
-	private String word;
+	@Reference
+	private Word word;
 	private double likeliness;
 
 	public TopicWord() {}
 
-	public TopicWord(String word, double likeliness) {
+	public TopicWord(Word word, double likeliness) {
 		this.word = word;
 		this.likeliness = likeliness;
 	}
 
-	public String getWord() {
+	public Word getWord() {
 		return word;
 	}
 
-	public void setWord(String word) {
+	public void setWord(Word word) {
 		this.word = word;
 	}
 
diff --git a/vipra-util/src/main/java/de/vipra/util/model/Word.java b/vipra-util/src/main/java/de/vipra/util/model/Word.java
new file mode 100644
index 0000000000000000000000000000000000000000..2effbd0d5bae56c6ff4ba584bb195b52beed9526
--- /dev/null
+++ b/vipra-util/src/main/java/de/vipra/util/model/Word.java
@@ -0,0 +1,59 @@
+package de.vipra.util.model;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Serializable;
+
+import org.bson.types.ObjectId;
+import org.mongodb.morphia.annotations.Entity;
+import org.mongodb.morphia.annotations.Id;
+
+import de.vipra.util.MongoUtils;
+import de.vipra.util.ex.NotImplementedException;
+
+@SuppressWarnings("serial")
+@Entity(value = "words", noClassnameStored = true)
+public class Word extends Model implements Serializable {
+
+	@Id
+	private ObjectId id;
+	private String word;
+
+	public Word() {}
+
+	public Word(String word) {
+		this.word = word;
+	}
+
+	public ObjectId getId() {
+		return id;
+	}
+
+	public void setId(ObjectId id) {
+		this.id = id;
+	}
+
+	public String getWord() {
+		return word;
+	}
+
+	public void setWord(String word) {
+		this.word = word;
+	}
+
+	@Override
+	public void setId(String id) {
+		this.id = MongoUtils.objectId(id);
+	}
+
+	@Override
+	public void fromFile(File file) throws IOException {
+		throw new NotImplementedException();
+	}
+
+	@Override
+	public String toFileString() {
+		throw new NotImplementedException();
+	}
+
+}
diff --git a/vipra-util/src/main/java/de/vipra/util/model/WordMap.java b/vipra-util/src/main/java/de/vipra/util/model/WordMap.java
new file mode 100644
index 0000000000000000000000000000000000000000..76351d34aa4fa65a6f03142cb87221a86f6f9ba3
--- /dev/null
+++ b/vipra-util/src/main/java/de/vipra/util/model/WordMap.java
@@ -0,0 +1,99 @@
+package de.vipra.util.model;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import de.vipra.util.ex.DatabaseException;
+import de.vipra.util.service.DatabaseService;
+
+public class WordMap extends HashMap<String, Word> {
+
+	private static final long serialVersionUID = 8321873837437524923L;
+	public static final Logger log = LoggerFactory.getLogger(WordMap.class);
+
+	private final DatabaseService<Word> dbWords;
+	private boolean createNow = true;
+	private long newWords = 0;
+
+	public WordMap(DatabaseService<Word> dbWords) {
+		this.dbWords = dbWords;
+		List<Word> words = dbWords.getAll();
+		for (Word word : words)
+			put(word.getWord().toLowerCase(), word);
+	}
+
+	@Override
+	public Word get(Object w) {
+		String strWord = w.toString();
+		Word word = super.get(strWord.toLowerCase());
+		if (word == null) {
+			word = new Word(strWord);
+			createWord(word);
+		}
+		return word;
+	}
+
+	@Override
+	public Word put(String strWord, Word word) {
+		Word currentWord = get(strWord);
+		if (currentWord == null) {
+			if (word == null)
+				word = new Word(strWord);
+			createWord(word);
+			put(strWord, word);
+			currentWord = word;
+		} else {
+			currentWord.setWord(word.getWord());
+			try {
+				dbWords.updateSingle(currentWord);
+			} catch (DatabaseException e) {
+				log.error("could not update word in database", e);
+				throw new RuntimeException(e);
+			}
+		}
+		return currentWord;
+	}
+
+	private Word createWord(Word word) {
+		if (createNow) {
+			try {
+				dbWords.createSingle(word);
+				newWords++;
+			} catch (DatabaseException e) {
+				log.error("could not create word in database", e);
+				throw new RuntimeException(e);
+			}
+		}
+		return word;
+	}
+
+	public Word put(String strWord) {
+		return put(strWord, null);
+	}
+
+	public void create() throws DatabaseException {
+		List<Word> newWords = new ArrayList<>();
+		for (Entry<String, Word> e : this.entrySet())
+			if (e.getValue().getId() == null)
+				newWords.add(e.getValue());
+		dbWords.createMultiple(newWords);
+		this.newWords += newWords.size();
+	}
+
+	public boolean isCreateNow() {
+		return createNow;
+	}
+
+	public void setCreateNow(boolean createNow) {
+		this.createNow = createNow;
+	}
+
+	public long getNewWords() {
+		return newWords;
+	}
+
+}
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 6172a5e2142ceab036ea990dfed87e82e66e207c..ed3f222ac266c81268e5b544225c0bef91b7ac70 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
@@ -1,13 +1,18 @@
 package de.vipra.util.service;
 
+import java.lang.reflect.Field;
+import java.util.ArrayList;
 import java.util.List;
 
+import org.bson.types.ObjectId;
 import org.mongodb.morphia.Datastore;
 import org.mongodb.morphia.query.Query;
 
 import de.vipra.util.Config;
+import de.vipra.util.ListUtils;
 import de.vipra.util.Mongo;
 import de.vipra.util.MongoUtils;
+import de.vipra.util.an.QueryIgnore;
 import de.vipra.util.ex.ConfigException;
 import de.vipra.util.ex.DatabaseException;
 import de.vipra.util.model.Model;
@@ -16,31 +21,70 @@ public class DatabaseService<T extends Model> implements Service<T, DatabaseExce
 
 	private final Datastore datastore;
 	private final Class<T> clazz;
+	private final String[] ignoredFieldsSingle;
+	private final String[] ignoredFieldsMulti;
 
 	public DatabaseService(Mongo mongo, Class<T> clazz) {
 		this.datastore = mongo.getDatastore();
 		this.clazz = clazz;
+
+		List<String> ignoreSingle = new ArrayList<>();
+		List<String> ignoreMulti = new ArrayList<>();
+		Field[] fields = clazz.getDeclaredFields();
+		for (Field field : fields) {
+			QueryIgnore qi = field.getDeclaredAnnotation(QueryIgnore.class);
+			if (qi != null) {
+				if (qi.single())
+					ignoreSingle.add(field.getName());
+				if (qi.multi())
+					ignoreMulti.add(field.getName());
+			}
+		}
+		this.ignoredFieldsSingle = ignoreSingle.toArray(new String[ignoreSingle.size()]);
+		this.ignoredFieldsMulti = ignoreMulti.toArray(new String[ignoreMulti.size()]);
 	}
 
 	@Override
 	public T getSingle(String id) {
-		return datastore.get(clazz, MongoUtils.objectId(id));
+		Query<T> q = datastore.createQuery(clazz).field("_id").equal(new ObjectId(id));
+		if (ignoredFieldsSingle.length > 0)
+			q.retrievedFields(false, ignoredFieldsSingle);
+		return q.get();
 	}
 
-	public List<T> getMultiple(int skip, int limit, String sortBy) {
-		Query<T> q = datastore.createQuery(clazz).offset(skip).limit(limit);
+	@Override
+	public List<T> getMultiple(Integer skip, Integer limit, String sortBy) {
+		Query<T> q = datastore.createQuery(clazz);
+		if (skip != null)
+			q.offset(skip);
+		if (limit != null)
+			q.limit(limit);
 		if (sortBy != null)
-			q = q.order(sortBy);
+			q.order(sortBy);
+		if (ignoredFieldsMulti.length > 0)
+			q.retrievedFields(false, ignoredFieldsMulti);
 		List<T> list = q.asList();
 		return list;
 	}
 
+	@Override
+	public List<T> getAll() {
+		return getMultiple(null, null, null);
+	}
+
 	@Override
 	public T createSingle(T t) throws DatabaseException {
 		datastore.save(t);
 		return t;
 	}
 
+	@Override
+	public List<T> createMultiple(Iterable<T> t) throws DatabaseException {
+		List<T> list = ListUtils.toList(t);
+		datastore.save(list);
+		return list;
+	}
+
 	@Override
 	public long deleteSingle(String id) throws DatabaseException {
 		return datastore.delete(MongoUtils.objectId(id)).getN();
@@ -51,10 +95,12 @@ public class DatabaseService<T extends Model> implements Service<T, DatabaseExce
 		datastore.save(t);
 	}
 
+	@Override
 	public void drop() {
 		datastore.getCollection(clazz).drop();
 	}
 
+	@Override
 	public long count() {
 		return datastore.getCount(clazz);
 	}
diff --git a/vipra-util/src/main/java/de/vipra/util/service/Service.java b/vipra-util/src/main/java/de/vipra/util/service/Service.java
index f3a4129c8edbdf46a58e888b79831efdaa2e0da2..659a57956c81c6c03b1f76269cfe35980beb1110 100644
--- a/vipra-util/src/main/java/de/vipra/util/service/Service.java
+++ b/vipra-util/src/main/java/de/vipra/util/service/Service.java
@@ -1,17 +1,27 @@
 package de.vipra.util.service;
 
+import java.util.List;
+
 import de.vipra.util.model.Model;
 
 public interface Service<T extends Model, E extends Exception> {
 
 	T getSingle(String id) throws E;
 
+	List<T> getMultiple(Integer skip, Integer limit, String sortBy) throws E;
+
+	List<T> getAll() throws E;
+
 	T createSingle(T t) throws E;
 
+	List<T> createMultiple(Iterable<T> t) throws E;
+
 	long deleteSingle(String id) throws E;
 
 	void updateSingle(T t) throws E;
 
-	void drop();
+	void drop() throws E;
+
+	long count() throws E;
 
 }