diff --git a/vipra-backend/src/main/java/de/vipra/rest/provider/RestServletContextListener.java b/vipra-backend/src/main/java/de/vipra/rest/provider/RestServletContextListener.java
index 9f932b398e728f9c6e912c5fbbd109b0ecbf4be0..57099323b0d1e72dcc9024275305635ee48a9478 100644
--- a/vipra-backend/src/main/java/de/vipra/rest/provider/RestServletContextListener.java
+++ b/vipra-backend/src/main/java/de/vipra/rest/provider/RestServletContextListener.java
@@ -8,6 +8,7 @@ import org.apache.logging.log4j.Logger;
 
 import de.vipra.util.IPCServer;
 import de.vipra.ws.WebSocketChain;
+import de.vipra.ws.WebSocketPing;
 
 public class RestServletContextListener implements ServletContextListener {
 
@@ -22,6 +23,10 @@ public class RestServletContextListener implements ServletContextListener {
 		ipcServer.register("websocket", new WebSocketChain());
 		ipcServer.start();
 		log.info("started ipc server");
+
+		final WebSocketPing wsPing = WebSocketPing.getInstance();
+		wsPing.start();
+		log.info("started websocket ping");
 	}
 
 }
diff --git a/vipra-backend/src/main/java/de/vipra/rest/resource/ArticleResource.java b/vipra-backend/src/main/java/de/vipra/rest/resource/ArticleResource.java
index 2fae2a246d87cf7e06d5184f87fdc7e2d5e98023..b0a9e40d41f473eeea6a238064f3303f4099f48b 100644
--- a/vipra-backend/src/main/java/de/vipra/rest/resource/ArticleResource.java
+++ b/vipra-backend/src/main/java/de/vipra/rest/resource/ArticleResource.java
@@ -58,7 +58,7 @@ public class ArticleResource {
 	public Response getArticles(@QueryParam("topicModel") final String topicModel, @QueryParam("skip") final Integer skip,
 			@QueryParam("limit") final Integer limit, @QueryParam("sort") @DefaultValue("date") final String sortBy,
 			@QueryParam("fields") final String fields, @QueryParam("word") final String word, @QueryParam("entity") final String entity,
-			@QueryParam("excerpt") final String excerpt) {
+			@QueryParam("excerpt") final String excerpt, @QueryParam("char") final String startChar, @QueryParam("contains") final String contains) {
 		final ResponseWrapper<List<ArticleFull>> res = new ResponseWrapper<>();
 
 		if (res.hasErrors())
@@ -78,10 +78,16 @@ public class ArticleResource {
 			if (entity != null && !entity.isEmpty())
 				query.eq("entities.entity.id", entity);
 
+			if (startChar != null && !startChar.isEmpty())
+				query.startsWith("title", startChar, true);
+
+			if (contains != null && !contains.isEmpty())
+				query.contains("title", contains, true);
+
 			final List<ArticleFull> articles = dbArticles.getMultiple(query);
 
 			if ((skip != null && skip > 0) || (limit != null && limit > 0))
-				res.addHeader("total", dbArticles.count(query.skip(null).limit(null)));
+				res.addHeader("total", dbArticles.count(query));
 			else
 				res.addHeader("total", articles.size());
 
diff --git a/vipra-backend/src/main/java/de/vipra/rest/resource/InfoResource.java b/vipra-backend/src/main/java/de/vipra/rest/resource/InfoResource.java
index 7a5c47d79593929fe8d0e557f27c7af7aaf22265..7048ced418a21cfb7379c212b20022027b0df2d9 100644
--- a/vipra-backend/src/main/java/de/vipra/rest/resource/InfoResource.java
+++ b/vipra-backend/src/main/java/de/vipra/rest/resource/InfoResource.java
@@ -63,20 +63,27 @@ public class InfoResource {
 
 			// constants
 			info.put("const.windowres", Constants.WINDOW_RESOLUTION);
+			info.put("const.procmode", Constants.PROCESSOR_MODE);
 			info.put("const.importbuf", Constants.IMPORT_BUFFER_MAX);
 			info.put("const.esboosttopics", Constants.ES_BOOST_TOPICS);
 			info.put("const.esboosttitles", Constants.ES_BOOST_TITLES);
 			info.put("const.topicautoname", Constants.TOPIC_AUTO_NAMING_WORDS);
 			info.put("const.ktopics", Constants.K_TOPICS);
+			info.put("const.ktopwords", Constants.K_TOP_WORDS);
 			info.put("const.decaylambda", Constants.RISING_DECAY_LAMBDA);
+			info.put("const.mintopicshare", Constants.MIN_TOPIC_SHARE);
 			info.put("const.minrelprob", Constants.MIN_RELATIVE_PROB);
 			info.put("const.maxsimdocs", Constants.MAX_SIMILAR_DOCUMENTS);
-			info.put("const.maxdiv", Constants.MAX_SIMILAR_DOCUMENTS_DIVERGENCE);
+			info.put("const.maxsimtopics", Constants.MAX_SIMILAR_TOPICS);
+			info.put("const.maxdocdiv", Constants.MAX_SIMILAR_DOCUMENTS_DIVERGENCE);
+			info.put("const.maxtopicdiv", Constants.MAX_SIMILAR_TOPICS_DIVERGENCE);
 			info.put("const.dynminiter", Constants.DYNAMIC_MIN_ITER);
 			info.put("const.dynmaxiter", Constants.DYNAMIC_MAX_ITER);
 			info.put("const.statiter", Constants.STATIC_ITER);
 			info.put("const.docminlength", Constants.DOCUMENT_MIN_LENGTH);
 			info.put("const.docminwordfreq", Constants.DOCUMENT_MIN_WORD_FREQ);
+			info.put("const.spotsupport", Constants.SPOTLIGHT_SUPPORT);
+			info.put("const.spotconf", Constants.SPOTLIGHT_CONFIDENCE);
 			info.put("const.charsdisallow", Constants.CHARS_DISALLOWED);
 			info.put("const.regexemail", Constants.REGEX_EMAIL);
 			info.put("const.regexurl", Constants.REGEX_URL);
@@ -84,6 +91,7 @@ public class InfoResource {
 			info.put("const.regexchar", Constants.REGEX_SINGLECHAR);
 			info.put("const.excerptlength", Constants.EXCERPT_LENGTH);
 			info.put("const.dateformat", Constants.DATETIME_FORMAT);
+
 		} catch (final Exception e) {
 			info.put("error", e.getMessage());
 		}
diff --git a/vipra-backend/src/main/java/de/vipra/rest/resource/TextEntityResource.java b/vipra-backend/src/main/java/de/vipra/rest/resource/TextEntityResource.java
index cbb74e613ca99d5672392c121174f5b2f892d3c7..da867de62ffdf2cf3eae2aa113f8d17f6535238d 100644
--- a/vipra-backend/src/main/java/de/vipra/rest/resource/TextEntityResource.java
+++ b/vipra-backend/src/main/java/de/vipra/rest/resource/TextEntityResource.java
@@ -44,7 +44,7 @@ public class TextEntityResource {
 	@Produces(MediaType.APPLICATION_JSON)
 	public Response getEntities(@QueryParam("topicModel") final String topicModel, @QueryParam("skip") final Integer skip,
 			@QueryParam("limit") final Integer limit, @QueryParam("sort") @DefaultValue("id") final String sortBy,
-			@QueryParam("fields") final String fields) {
+			@QueryParam("fields") final String fields, @QueryParam("char") final String startChar, @QueryParam("contains") final String contains) {
 		final ResponseWrapper<List<TextEntityFull>> res = new ResponseWrapper<>();
 
 		if (res.hasErrors())
@@ -58,10 +58,16 @@ public class TextEntityResource {
 			if (topicModel != null && !topicModel.isEmpty())
 				query.eq("topicModel", new TopicModel(topicModel));
 
+			if (startChar != null && !startChar.isEmpty())
+				query.startsWith("id", startChar, true);
+
+			if (contains != null && !contains.isEmpty())
+				query.contains("id", contains, true);
+
 			final List<TextEntityFull> entities = dbEntities.getMultiple(query);
 
 			if ((skip != null && skip > 0) || (limit != null && limit > 0))
-				res.addHeader("total", dbEntities.count(null));
+				res.addHeader("total", dbEntities.count(query));
 			else
 				res.addHeader("total", entities.size());
 
diff --git a/vipra-backend/src/main/java/de/vipra/rest/resource/TopicResource.java b/vipra-backend/src/main/java/de/vipra/rest/resource/TopicResource.java
index 0aceea0610157e275c05deb666ba389810989bd0..bf0eafe7403364c2ace741b657ccf9c772e02bed 100644
--- a/vipra-backend/src/main/java/de/vipra/rest/resource/TopicResource.java
+++ b/vipra-backend/src/main/java/de/vipra/rest/resource/TopicResource.java
@@ -50,7 +50,8 @@ public class TopicResource {
 	@Produces(MediaType.APPLICATION_JSON)
 	public Response getTopics(@QueryParam("topicModel") final String topicModel, @QueryParam("skip") final Integer skip,
 			@QueryParam("limit") final Integer limit, @QueryParam("sort") @DefaultValue("name") final String sortBy,
-			@QueryParam("fields") final String fields, @QueryParam("word") final String word) {
+			@QueryParam("fields") final String fields, @QueryParam("word") final String word, @QueryParam("char") final String startChar,
+			@QueryParam("contains") final String contains) {
 		final ResponseWrapper<List<TopicFull>> res = new ResponseWrapper<>();
 
 		if (res.hasErrors())
@@ -67,10 +68,16 @@ public class TopicResource {
 			if (word != null && !word.isEmpty())
 				query.eq("words.id", word);
 
+			if (startChar != null && !startChar.isEmpty())
+				query.startsWith("name", startChar, true);
+
+			if (contains != null && !contains.isEmpty())
+				query.contains("name", contains, true);
+
 			final List<TopicFull> topics = dbTopics.getMultiple(query);
 
 			if ((skip != null && skip > 0) || (limit != null && limit > 0))
-				res.addHeader("total", dbTopics.count(query.skip(null).limit(null)));
+				res.addHeader("total", dbTopics.count(query));
 			else
 				res.addHeader("total", topics.size());
 
diff --git a/vipra-backend/src/main/java/de/vipra/rest/resource/WordResource.java b/vipra-backend/src/main/java/de/vipra/rest/resource/WordResource.java
index 572e2a8e6d203c100e1d1843c922b952ca6628c9..4921d0a85d957cb9331f1f7337e9552c813118c7 100644
--- a/vipra-backend/src/main/java/de/vipra/rest/resource/WordResource.java
+++ b/vipra-backend/src/main/java/de/vipra/rest/resource/WordResource.java
@@ -40,7 +40,7 @@ public class WordResource {
 	@Produces(MediaType.APPLICATION_JSON)
 	public Response getWords(@QueryParam("topicModel") final String topicModel, @QueryParam("skip") final Integer skip,
 			@QueryParam("limit") final Integer limit, @QueryParam("sort") @DefaultValue("id") final String sortBy,
-			@QueryParam("fields") final String fields) {
+			@QueryParam("fields") final String fields, @QueryParam("char") final String startChar, @QueryParam("contains") final String contains) {
 		final ResponseWrapper<List<WordFull>> res = new ResponseWrapper<>();
 
 		if (res.hasErrors())
@@ -54,10 +54,16 @@ public class WordResource {
 			if (topicModel != null && !topicModel.isEmpty())
 				query.eq("topicModel", new TopicModel(topicModel));
 
+			if (startChar != null && !startChar.isEmpty())
+				query.startsWith("id", startChar, true);
+
+			if (contains != null && !contains.isEmpty())
+				query.contains("id", contains, true);
+
 			final List<WordFull> words = dbWords.getMultiple(query);
 
 			if ((skip != null && skip > 0) || (limit != null && limit > 0))
-				res.addHeader("total", dbWords.count(null));
+				res.addHeader("total", dbWords.count(query));
 			else
 				res.addHeader("total", words.size());
 
diff --git a/vipra-backend/src/main/java/de/vipra/ws/WebSocket.java b/vipra-backend/src/main/java/de/vipra/ws/WebSocket.java
index 7e31d080e228911d0fb368d58f95b1a84a0db255..ab910cca67f2267f146d89fb505ff4a04c846880 100644
--- a/vipra-backend/src/main/java/de/vipra/ws/WebSocket.java
+++ b/vipra-backend/src/main/java/de/vipra/ws/WebSocket.java
@@ -1,6 +1,7 @@
 package de.vipra.ws;
 
 import java.io.IOException;
+import java.nio.ByteBuffer;
 import java.util.HashSet;
 import java.util.Set;
 
@@ -51,11 +52,42 @@ public class WebSocket {
 	}
 
 	public static void sendMessage(final String message) {
-		for (final Session session : sessions)
+		for (final Session session : sessions) {
 			if (session.isOpen())
 				session.getAsyncRemote().sendText(message);
 			else
 				sessions.remove(session);
+		}
+	}
+
+	public static void sendPing() {
+		final String msg = "ping";
+		final ByteBuffer data = ByteBuffer.wrap(msg.getBytes());
+		for (final Session session : sessions) {
+			if (session.isOpen()) {
+				try {
+					session.getAsyncRemote().sendPing(data);
+				} catch (IllegalArgumentException | IOException e) {
+					sessions.remove(session);
+				}
+			} else
+				sessions.remove(session);
+		}
+	}
+
+	public static void sendPong() {
+		final String msg = "pong";
+		final ByteBuffer data = ByteBuffer.wrap(msg.getBytes());
+		for (final Session session : sessions) {
+			if (session.isOpen()) {
+				try {
+					session.getAsyncRemote().sendPong(data);
+				} catch (IllegalArgumentException | IOException e) {
+					sessions.remove(session);
+				}
+			} else
+				sessions.remove(session);
+		}
 	}
 
 }
diff --git a/vipra-backend/src/main/java/de/vipra/ws/WebSocketPing.java b/vipra-backend/src/main/java/de/vipra/ws/WebSocketPing.java
new file mode 100644
index 0000000000000000000000000000000000000000..d02c358b9cf6957c75ff883380b29792045ce409
--- /dev/null
+++ b/vipra-backend/src/main/java/de/vipra/ws/WebSocketPing.java
@@ -0,0 +1,34 @@
+package de.vipra.ws;
+
+public class WebSocketPing extends Thread {
+
+	private static WebSocketPing instance;
+
+	private WebSocketPing() {}
+
+	@Override
+	public void run() {
+		while (true) {
+			try {
+				sleep(60000);
+			} catch (final InterruptedException e) {
+				return;
+			}
+			WebSocket.sendPong();
+		}
+	}
+
+	@Override
+	public void start() {
+		if (!isAlive())
+			super.start();
+	}
+
+	public static WebSocketPing getInstance() {
+		if (instance == null) {
+			instance = new WebSocketPing();
+		}
+		return instance;
+	}
+
+}
diff --git a/vipra-cmd/runcfg/CMD.launch b/vipra-cmd/runcfg/CMD.launch
index b36db2e1f99f6ae77ceb1a8501b802a2a37c5e24..0e3fecb125ac97797b96daa17da6c22724631ed2 100644
--- a/vipra-cmd/runcfg/CMD.launch
+++ b/vipra-cmd/runcfg/CMD.launch
@@ -11,7 +11,7 @@
 </listAttribute>
 <stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.eclipse.m2e.launchconfig.classpathProvider"/>
 <stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="de.vipra.cmd.Main"/>
-<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-t"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-Ai"/>
 <stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="vipra-cmd"/>
 <stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.eclipse.m2e.launchconfig.sourcepathProvider"/>
 <stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-ea"/>
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 8c8d894f8d1a24438a879975a7a4af537ba8e69b..9502c5cddcfbd7e250e2618a3ed8e69cbac8ef09 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/Main.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/Main.java
@@ -54,7 +54,7 @@ public class Main {
 		}
 	}
 
-	private static void execute(final String[] args) {
+	public static void main(final String[] args) {
 		final CommandLineOptions opts = new CommandLineOptions();
 		try {
 			opts.parse(args);
@@ -113,8 +113,13 @@ public class Main {
 		// run
 
 		if (commands.size() > 0) {
+			boolean locked = false;
 			for (final ListIterator<Command> it = commands.listIterator(); it.hasNext();) {
 				final Command c = it.next();
+				if (c.requiresLock() && !locked) {
+					lock();
+					locked = true;
+				}
 				try {
 					c.run();
 				} catch (final Exception e) {
@@ -130,11 +135,7 @@ public class Main {
 		} else {
 			opts.printHelp();
 		}
-	}
 
-	public static void main(final String[] args) {
-		lock();
-		execute(args);
 		unlock();
 	}
 
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/lda/Analyzer.java b/vipra-cmd/src/main/java/de/vipra/cmd/lda/Analyzer.java
index f7fd26ca3c72b076baf4b55b12abf9b57aa7ebcc..50e92ca4180e1de2f8d39a21e58bc7d0a0539263 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/lda/Analyzer.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/lda/Analyzer.java
@@ -460,6 +460,12 @@ public class Analyzer {
 					}
 				}
 			}
+
+			Collections.sort(similarTopics);
+
+			if (similarTopics.size() > modelConfig.getMaxSimilarTopics())
+				similarTopics.subList(modelConfig.getMaxSimilarTopics(), similarTopics.size()).clear();
+
 			topic1.setSimilarTopics(similarTopics);
 		}
 
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 5cd3ef88af28e20fdabe0ad3e07519d278d5c3b9..1894e665bcf36e08720af5b65e51caabfe5b5427 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
@@ -41,4 +41,9 @@ public class ClearCommand implements Command {
 		clear();
 	}
 
+	@Override
+	public boolean requiresLock() {
+		return true;
+	}
+
 }
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/Command.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/Command.java
index d794b7590ef7f04e0829de76c0fd25cb7617e442..48af4869f603f45cb26506fae08562c2582ea839 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/option/Command.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/Command.java
@@ -4,4 +4,6 @@ public interface Command {
 
 	public void run() throws Exception;
 
+	public boolean requiresLock();
+
 }
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/CreateModelCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/CreateModelCommand.java
index e029235085ae5fa63efbfcc5ea125c62f9a0c362..63ece339285d43ede0e319a5f3e819ae35201643 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/option/CreateModelCommand.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/CreateModelCommand.java
@@ -64,4 +64,9 @@ public class CreateModelCommand implements Command {
 		}
 	}
 
+	@Override
+	public boolean requiresLock() {
+		return false;
+	}
+
 }
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/DeleteModelCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/DeleteModelCommand.java
index 483c320f7e0876197af940e47551196e9a932c2b..9f28494bc172443c5afd1508da6568455df31416 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/option/DeleteModelCommand.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/DeleteModelCommand.java
@@ -57,4 +57,9 @@ public class DeleteModelCommand implements Command {
 		}
 	}
 
+	@Override
+	public boolean requiresLock() {
+		return true;
+	}
+
 }
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/EditModelCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/EditModelCommand.java
index 43cfbf84dba23747cf1f6f4e078f8089d6932a1a..ce25ce87bd6390405535a5d3856c1387835e782f 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/option/EditModelCommand.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/EditModelCommand.java
@@ -73,4 +73,9 @@ public class EditModelCommand implements Command {
 		}
 	}
 
+	@Override
+	public boolean requiresLock() {
+		return true;
+	}
+
 }
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 92b2636e6901e8f9a19e3ad615a6aa905e08ad4d..116b67c27116098da5620b893a71ff0b59fa005b 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
@@ -356,4 +356,9 @@ public class ImportCommand implements Command {
 		ipcClient.close();
 	}
 
+	@Override
+	public boolean requiresLock() {
+		return true;
+	}
+
 }
\ No newline at end of file
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/IndexingCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/IndexingCommand.java
index f2792a843c1d708a7ac0eaf518184aafaa4d4b92..adb8e7c77b450e497c630c21e59a829931e19177 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/option/IndexingCommand.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/IndexingCommand.java
@@ -103,4 +103,9 @@ public class IndexingCommand implements Command {
 		ipcClient.close();
 	}
 
+	@Override
+	public boolean requiresLock() {
+		return true;
+	}
+
 }
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/ListModelsCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/ListModelsCommand.java
index d6f1f4b694f22d2d00f42cb6924a89cb538b0970..4852d0e6f5a0c22333def610f9cf3ff1217b2373 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/option/ListModelsCommand.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/ListModelsCommand.java
@@ -23,4 +23,9 @@ public class ListModelsCommand implements Command {
 					+ " " + entry.getValue().toString());
 	}
 
+	@Override
+	public boolean requiresLock() {
+		return false;
+	}
+
 }
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/MessageCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/MessageCommand.java
index 2eba9b2425a84a92427e67a3e31870b829dd02e3..fa547c651e38327c72c6b18d998a3634fc87a524 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/option/MessageCommand.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/MessageCommand.java
@@ -19,4 +19,9 @@ public class MessageCommand implements Command {
 		client.close();
 	}
 
+	@Override
+	public boolean requiresLock() {
+		return false;
+	}
+
 }
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/ModelingCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/ModelingCommand.java
index 3bf6fa11dbb5cad03fe5b6275182dd8226857011..ca2627cf72af2fc0441832d693291c5a9261a1c0 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/option/ModelingCommand.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/ModelingCommand.java
@@ -1,5 +1,6 @@
 package de.vipra.cmd.option;
 
+import java.io.File;
 import java.io.IOException;
 import java.text.ParseException;
 
@@ -54,6 +55,11 @@ public class ModelingCommand implements Command {
 	@Override
 	public void run() throws Exception {
 		final Config config = Config.getConfig();
+		if (config.getDtmPath() == null || config.getDtmPath().isEmpty())
+			throw new Exception("dtm path not configured, set config variable 'dtmPath'");
+		if (!new File(config.getDtmPath()).exists())
+			throw new Exception("dtm not found at specified path: '" + config.getDtmPath() + "'");
+
 		final IPCClient ipcClient = new IPCClient();
 		for (final TopicModelConfig modelConfig : config.getTopicModelConfigs(models)) {
 			ipcClient.send(new IPCMessage(IPCMessageCode.START_MODELING).message("Started generating model '" + modelConfig.getName() + "'"));
@@ -63,4 +69,9 @@ public class ModelingCommand implements Command {
 		ipcClient.close();
 	}
 
+	@Override
+	public boolean requiresLock() {
+		return true;
+	}
+
 }
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/PrintModelCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/PrintModelCommand.java
index e315b3c080d2484c9d06f7909f5133fa4645ebd4..b335c621ad4b032dac2558134fe47bf5d190ea90 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/option/PrintModelCommand.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/PrintModelCommand.java
@@ -26,4 +26,9 @@ public class PrintModelCommand implements Command {
 		}
 	}
 
+	@Override
+	public boolean requiresLock() {
+		return false;
+	}
+
 }
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/TestCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/TestCommand.java
index d3d51d54a6c927a62741ca13a1a947c345af3852..388bd63338707303d00ad1717569ad04e1d3624e 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/option/TestCommand.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/TestCommand.java
@@ -71,10 +71,15 @@ public class TestCommand implements Command {
 			}
 
 			ConsoleUtils.info("all tests passed");
-		} catch (Exception e) {
+		} catch (final Exception e) {
 			ConsoleUtils.print(Ansi.ansi().fg(Color.RED).a("FAILED").reset().toString());
 			throw e;
 		}
 	}
 
+	@Override
+	public boolean requiresLock() {
+		return false;
+	}
+
 }
diff --git a/vipra-ui/app/html/about.html b/vipra-ui/app/html/about.html
index 4865c49cfd93ec603a9d985d73314d4a3ecb1f18..cd457011730baea8fc4d315c33e8b06d5f99ab93 100644
--- a/vipra-ui/app/html/about.html
+++ b/vipra-ui/app/html/about.html
@@ -85,7 +85,16 @@
           </tr>
           <tr class="well">
             <td colspan="2">
-              Analyzer, text processor and dynamic window resolution.
+              The dynamic topic modeling window resolution to be used. This value is only used, if the selected analyzer supports dynamic topic modeling.
+            </td>
+          </tr>
+          <tr>
+            <th>Processor mode</th>
+            <td ng-bind-template="{{::info.const.procmode}}"></td>
+          </tr>
+          <tr class="well">
+            <td colspan="2">
+              The processor mode defines the processed text output. In text mode, the text is trimmed down, in entity mode, the text is scanned for entities. In mixed mode, the found entities are inserted into the trimmed text.
             </td>
           </tr>
           <tr>
@@ -128,6 +137,15 @@
               The number of topics to be generated in the topic modeling process.
             </td>
           </tr>
+          <tr>
+            <th>K top words</th>
+            <td ng-bind-template="{{::info.const.ktopwords}}"></td>
+          </tr>
+          <tr class="well">
+            <td colspan="2">
+              The maximum number of top words per sequence and topic.
+            </td>
+          </tr>
           <tr>
             <th>Rising decay weight</th>
             <td ng-bind-template="{{::info.const.decaylambda}}"></td>
@@ -137,6 +155,15 @@
               A weight to the rising decay caulculation of topic relevances. The higher this value, the more focus is put on later sequences containing more recent documents.
             </td>
           </tr>
+          <tr>
+            <th>Minimum topic share</th>
+            <td ng-bind-template="{{::info.const.mintopicshare}}"></td>
+          </tr>
+          <tr class="well">
+            <td colspan="2">
+              Minimum topic share for an article. Topics with a smaller share are ignored.
+            </td>
+          </tr>
           <tr>
             <th>Minimum relative probability</th>
             <td ng-bind-template="{{::info.const.minrelprob}}"></td>
@@ -151,20 +178,33 @@
             <th>Maximum similar documents</th>
             <td ng-bind-template="{{::info.const.maxsimdocs}}"></td>
           </tr>
+          <tr>
+            <th>Maximum similar topics</th>
+            <td ng-bind-template="{{::info.const.maxsimtopics}}"></td>
+          </tr>
           <tr class="well">
             <td colspan="2">
-              Maximum number of similar documents for each document.
+              Maximum number of similar documents/topics for each document/topic.
             </td>
           </tr>
           <tr>
-            <th>Maximum divergence</th>
-            <td ng-bind-template="{{::info.const.maxdiv}}"></td>
+            <th>Maximum similar documents divergence</th>
+            <td ng-bind-template="{{::info.const.maxdocdiv}}"></td>
           </tr>
           <tr class="well">
             <td colspan="2">
               Maximum divergence between a document and similar documents. Lower values mean more similar documents (less divergence).
             </td>
           </tr>
+          <tr>
+            <th>Maximum similar topics divergence</th>
+            <td ng-bind-template="{{::info.const.maxtopicdiv}}"></td>
+          </tr>
+          <tr class="well">
+            <td colspan="2">
+              Maximum divergence between a topic and similar topics. Lower values mean more similar topics (less divergence).
+            </td>
+          </tr>
           <tr>
             <th>Dynamic minimum iterations</th>
             <td ng-bind-template="{{::info.const.dynminiter}}"></td>
@@ -200,6 +240,24 @@
               The minimum article word frequency. Words that occurr less than this frequency are stripped from the article.
             </td>
           </tr>
+          <tr>
+            <th>Spotlight support</th>
+            <td ng-bind-template="{{::info.const.spotsupport}}"></td>
+          </tr>
+          <tr class="well">
+            <td colspan="2">
+              Minimum number of dbpedia inlinks for an entity annotation to be accepted.
+            </td>
+          </tr>
+          <tr>
+            <th>Spotlight confidence</th>
+            <td ng-bind-template="{{::info.const.spotconf}}"></td>
+          </tr>
+          <tr class="well">
+            <td colspan="2">
+              Disambiguation confidence. Eliminates top n percent of inconfident annotations. Ranges from 0 to 1.
+            </td>
+          </tr>
           <tr>
             <th>Regex disallowed chars</th>
             <td ng-bind-template="{{::info.const.charsdisallow}}"></td>
diff --git a/vipra-ui/app/html/articles/index.html b/vipra-ui/app/html/articles/index.html
index 88ca1fd8df6ef85e5528a00d36a40fa8b2dea596..7757ce3ec73bb1049a88c45aaaf8304ca22677bd 100644
--- a/vipra-ui/app/html/articles/index.html
+++ b/vipra-ui/app/html/articles/index.html
@@ -5,6 +5,17 @@
     </div>
   </menu-affix>
   <div class="container">
+    <div class="row row-spaced">
+      <div class="col-md-8 text-center">
+        <char-selector ng-model="articlesIndexModels.startChar"/>
+      </div>
+      <div class="col-md-4">
+        <div class="btn-group btn-group-justified">
+          <input type="text" class="form-control" ng-model="articlesIndexModels.contains" placeholder="Filter..." ng-model-options="{debounce:300}">
+          <span class="glyphicon glyphicon-remove-circle searchclear" ng-click="articlesIndexModels.contains=''"></span>
+        </div>
+      </div>
+    </div>
     <div class="row">
       <div class="col-md-12">
         <div class="panel panel-default">
diff --git a/vipra-ui/app/html/directives/char-selector.html b/vipra-ui/app/html/directives/char-selector.html
new file mode 100644
index 0000000000000000000000000000000000000000..c91239cda4f8fa3c786a19a5d2e77384c7c9493a
--- /dev/null
+++ b/vipra-ui/app/html/directives/char-selector.html
@@ -0,0 +1,29 @@
+<div class="char-selector">
+  <a ng-class="{selected:!ngModel}" data-char="">All</a>
+  <a ng-class="{selected:ngModel==='A'}" data-char="A">A</a>
+  <a ng-class="{selected:ngModel==='B'}" data-char="B">B</a>
+  <a ng-class="{selected:ngModel==='C'}" data-char="C">C</a>
+  <a ng-class="{selected:ngModel==='D'}" data-char="D">D</a>
+  <a ng-class="{selected:ngModel==='E'}" data-char="E">E</a>
+  <a ng-class="{selected:ngModel==='F'}" data-char="F">F</a>
+  <a ng-class="{selected:ngModel==='G'}" data-char="G">G</a>
+  <a ng-class="{selected:ngModel==='H'}" data-char="H">H</a>
+  <a ng-class="{selected:ngModel==='I'}" data-char="I">I</a>
+  <a ng-class="{selected:ngModel==='J'}" data-char="J">J</a>
+  <a ng-class="{selected:ngModel==='K'}" data-char="K">K</a>
+  <a ng-class="{selected:ngModel==='L'}" data-char="L">L</a>
+  <a ng-class="{selected:ngModel==='M'}" data-char="M">M</a>
+  <a ng-class="{selected:ngModel==='N'}" data-char="N">N</a>
+  <a ng-class="{selected:ngModel==='O'}" data-char="O">O</a>
+  <a ng-class="{selected:ngModel==='P'}" data-char="P">P</a>
+  <a ng-class="{selected:ngModel==='Q'}" data-char="Q">Q</a>
+  <a ng-class="{selected:ngModel==='R'}" data-char="R">R</a>
+  <a ng-class="{selected:ngModel==='S'}" data-char="S">S</a>
+  <a ng-class="{selected:ngModel==='T'}" data-char="T">T</a>
+  <a ng-class="{selected:ngModel==='U'}" data-char="U">U</a>
+  <a ng-class="{selected:ngModel==='V'}" data-char="V">V</a>
+  <a ng-class="{selected:ngModel==='W'}" data-char="W">W</a>
+  <a ng-class="{selected:ngModel==='X'}" data-char="X">X</a>
+  <a ng-class="{selected:ngModel==='Y'}" data-char="Y">Y</a>
+  <a ng-class="{selected:ngModel==='Z'}" data-char="Z">Z</a>
+</div>
\ No newline at end of file
diff --git a/vipra-ui/app/html/directives/sequence-dropdown.html b/vipra-ui/app/html/directives/sequence-dropdown.html
index a786621254b7d1cc46df10a392a88777f232873a..54e8dc898700cf306553b4aff3b62078a40240e0 100644
--- a/vipra-ui/app/html/directives/sequence-dropdown.html
+++ b/vipra-ui/app/html/directives/sequence-dropdown.html
@@ -2,5 +2,4 @@
   <li class="nya-bs-option" nya-bs-option="sequence in sequences">
     <a ng-bind="sequence.label"></a>
   </li>
-</ol>
-<button class="btn btn-sm btn-default" ng-click="doClear()" ng-show="showClear">Clear</button>
\ No newline at end of file
+</ol>
\ No newline at end of file
diff --git a/vipra-ui/app/html/directives/window-dropdown.html b/vipra-ui/app/html/directives/window-dropdown.html
new file mode 100644
index 0000000000000000000000000000000000000000..78e0e8c834d6423979a359ea07ceaa39fd0a04dd
--- /dev/null
+++ b/vipra-ui/app/html/directives/window-dropdown.html
@@ -0,0 +1,12 @@
+<div class="input-group">
+  <ol class="nya-bs-select form-control" ng-model="ngModel" ng-class="{dropup:showDropup}" disabled="!windows">
+    <li class="nya-bs-option" nya-bs-option="window in windows">
+      <a ng-bind="window.label"></a>
+    </li>
+  </ol>
+  <span class="input-group-btn">
+    <button class="btn btn-default selectclear nooutline" type="button">
+      <span class="glyphicon glyphicon-remove-circle" ng-click="ngModel=''"></span>
+    </button>
+  </span>
+</div>
\ No newline at end of file
diff --git a/vipra-ui/app/html/entities/index.html b/vipra-ui/app/html/entities/index.html
index 2554c3f2d484dc5c29d5653365a8821fef76e22c..ba03287836eb58b78bcd99a8a32d17e250e731b7 100644
--- a/vipra-ui/app/html/entities/index.html
+++ b/vipra-ui/app/html/entities/index.html
@@ -5,6 +5,17 @@
     </div>
   </menu-affix>
   <div class="container">
+    <div class="row row-spaced">
+      <div class="col-md-8 text-center">
+        <char-selector ng-model="entitiesIndexModels.startChar"/>
+      </div>
+      <div class="col-md-4">
+        <div class="btn-group btn-group-justified">
+          <input type="text" class="form-control" ng-model="entitiesIndexModels.contains" placeholder="Filter..." ng-model-options="{debounce:300}">
+          <span class="glyphicon glyphicon-remove-circle searchclear" ng-click="entitiesIndexModels.contains=''"></span>
+        </div>
+      </div>
+    </div>
     <div class="row">
       <div class="col-md-12">
         <div class="panel panel-default">
diff --git a/vipra-ui/app/html/network.html b/vipra-ui/app/html/network.html
index 88bd5c4f92758a44b0ba9e0df462d73b7911ff63..83610c2ce3a9670015e3601530d84e385d2d07ba 100644
--- a/vipra-ui/app/html/network.html
+++ b/vipra-ui/app/html/network.html
@@ -1,25 +1,51 @@
 <div ng-cloak ng-hide="!rootModels.topicModel || state.name !== 'network'">
   <div class="fullsize navpadding">
-    <div class="graph-legend overlay">
-      <div class="checkbox">
-        <input type="checkbox" id="showArticles" ng-model="shown.articles">
-        <label for="showArticles">Articles</label>
+    <div class="graph-legend overlay" ng-class="{collapsed:legendCollapsed}">
+      <i class="collapser fa pointer pull-right" ng-class="{'fa-chevron-up':!legendCollapsed,'fa-chevron-down':legendCollapsed}" ng-click="legendCollapsed=!legendCollapsed"></i>
+      <div ng-hide="legendCollapsed">
+        <div class="checkbox">
+          <input type="checkbox" id="showArticles" ng-model="shown.articles">
+          <label for="showArticles">Articles</label>
+        </div>
+        <div class="checkbox">
+          <input type="checkbox" id="showSimilarArticles" ng-model="shown.similararticles">
+          <label for="showSimilarArticles">Similar Articles</label>
+        </div>
+        <div class="checkbox">
+          <input type="checkbox" id="showTopics" ng-model="shown.topics">
+          <label for="showTopics">Topics</label>
+        </div>
+        <div class="checkbox">
+          <input type="checkbox" id="showWords" ng-model="shown.words">
+          <label for="showWords">Words</label>
+        </div>
+        <div>
+          Window
+          <window-dropdown ng-model="selectedWindow" windows="windows" />
+        </div>
+        <div>
+          <div class="btn-group btn-group-justified">
+            <input class="form-control" type="text" ng-model="searchNodes" ng-model-options="{debounce:300}" placeholder="Search...">
+            <span class="glyphicon glyphicon-remove-circle searchclear" ng-click="searchNodes=''"></span>
+          </div>
+        </div>
+        <div>
+          <table>
+            <tr>
+              <td>
+                <button class="btn btn-default" ng-click="reset()">
+                  Reset
+                </button>
+              </td>
+              <td>
+                <button class="btn btn-default" ng-click="fit()">
+                  Zoom to fit
+                </button>
+              </td>
+            </tr>
+          </table>
+        </div>
       </div>
-      <div class="checkbox">
-        <input type="checkbox" id="showSimilarArticles" ng-model="shown.similararticles">
-        <label for="showSimilarArticles">Similar Articles</label>
-      </div>
-      <div class="checkbox">
-        <input type="checkbox" id="showTopics" ng-model="shown.topics">
-        <label for="showTopics">Topics</label>
-      </div>
-      <div class="checkbox">
-        <input type="checkbox" id="showWords" ng-model="shown.words">
-        <label for="showWords">Words</label>
-      </div>
-      <button class="btn btn-default" ng-click="reset()">
-        Reset
-      </button>
     </div>
     <div class="fullsize" id="visgraph"></div>
   </div>
diff --git a/vipra-ui/app/html/topics/index.html b/vipra-ui/app/html/topics/index.html
index 8d7c207d90629580346c406529651d40c539ffe1..d9e3b962839993ebb690cd659996196d849c2e15 100644
--- a/vipra-ui/app/html/topics/index.html
+++ b/vipra-ui/app/html/topics/index.html
@@ -5,6 +5,17 @@
     </div>
   </menu-affix>
   <div class="container">
+    <div class="row row-spaced">
+      <div class="col-md-8 text-center">
+        <char-selector ng-model="topicsIndexModels.startChar"/>
+      </div>
+      <div class="col-md-4">
+        <div class="btn-group btn-group-justified">
+          <input type="text" class="form-control" ng-model="topicsIndexModels.contains" placeholder="Filter..." ng-model-options="{debounce:300}">
+          <span class="glyphicon glyphicon-remove-circle searchclear" ng-click="topicsIndexModels.contains=''"></span>
+        </div>
+      </div>
+    </div>
     <div class="row">
       <div class="col-md-12">
         <div class="panel panel-default">
diff --git a/vipra-ui/app/html/words/index.html b/vipra-ui/app/html/words/index.html
index 60ca039549f28621b042debb5dfca62bf2afcc3c..34a4cac3bc779ab0756ed5a981e4d4c68bedc537 100644
--- a/vipra-ui/app/html/words/index.html
+++ b/vipra-ui/app/html/words/index.html
@@ -5,6 +5,17 @@
     </div>
   </menu-affix>
   <div class="container">
+    <div class="row row-spaced">
+      <div class="col-md-8 text-center">
+        <char-selector ng-model="wordsIndexModels.startChar"/>
+      </div>
+      <div class="col-md-4">
+        <div class="btn-group btn-group-justified">
+          <input type="text" class="form-control" ng-model="wordsIndexModels.contains" placeholder="Filter..." ng-model-options="{debounce:300}">
+          <span class="glyphicon glyphicon-remove-circle searchclear" ng-click="wordsIndexModels.contains=''"></span>
+        </div>
+      </div>
+    </div>
     <div class="row">
       <div class="col-md-12">
         <div class="panel panel-default">
diff --git a/vipra-ui/app/index.html b/vipra-ui/app/index.html
index 18917c14d81bd60da20b9c08e6e8c83ffb4da535..af9a4e37e24bd80853e73ca7a6e1ae7e042df3b2 100644
--- a/vipra-ui/app/index.html
+++ b/vipra-ui/app/index.html
@@ -40,6 +40,9 @@
           <li ui-sref-active="active">
             <a tabindex="0" ui-sref="explorer"><span class="mnemonic">E</span>xplorer</a>
           </li>
+          <li ui-sref-active="active">
+            <a tabindex="0" ui-sref="network"><span class="mnemonic">N</span>etwork</a>
+          </li>
           <li ui-sref-active="active">
             <a tabindex="0" ui-sref="articles"><span class="mnemonic">A</span>rticles</a>
           </li>
@@ -47,7 +50,7 @@
             <a tabindex="0" ui-sref="topics"><span class="mnemonic">T</span>opics</a>
           </li>
           <li ui-sref-active="active">
-            <a tabindex="0" ui-sref="entities">E<span class="mnemonic">n</span>tities</a>
+            <a tabindex="0" ui-sref="entities">Ent<span class="mnemonic">i</span>ties</a>
           </li>
           <li ui-sref-active="active">
             <a tabindex="0" ui-sref="words"><span class="mnemonic">W</span>ords</a>
diff --git a/vipra-ui/app/js/app.js b/vipra-ui/app/js/app.js
index 25a3d9b9a405691cf9e825e3018dc7f5d347a343..2985c762f6cb395190b4c125c76a5ea83ad2a131 100644
--- a/vipra-ui/app/js/app.js
+++ b/vipra-ui/app/js/app.js
@@ -11,7 +11,6 @@
     'ngResource',
     'ngSanitize',
     'ngAnimate',
-    'ngWebSocket',
     'ui.router',
     'cfp.hotkeys',
     'nya.bootstrap.select',
@@ -43,7 +42,7 @@
       });
 
       $stateProvider.state('network', {
-        url: '/network/:type/:id',
+        url: '/network?type&id',
         templateUrl: 'html/network.html',
         controller: 'NetworkController'
       });
@@ -201,7 +200,7 @@
     }
   ]);
 
-  app.run(['$rootScope', '$state', '$websocket', function($rootScope, $state, $websocket) {
+  app.run(['$rootScope', '$state', 'AlertFactory', function($rootScope, $state, AlertFactory) {
 
     $rootScope.loading = {};
 
@@ -224,11 +223,38 @@
       $rootScope.alerts = [];
     });
 
-    var socket = $websocket(Vipra.config.websocketUrl);
-
-    socket.onMessage(function(message) {
-      console.log(message.data);
-    });
+    $rootScope.handleWSMessage = function(data) {
+      AlertFactory.showAlert(data.message, {time:7000});
+    };
+
+    $rootScope.showMessage = function(msg) {
+      AlertFactory.showAlert(msg, {time:7000});
+    };
+
+    window.vipraWS = function() {
+      var socket = new WebSocket(Vipra.config.websocketUrl);
+
+      socket.onmessage = function(event) {
+        try {
+          var data = JSON.parse(event.data);
+          $rootScope.$apply(function() {
+            $rootScope.handleWSMessage(data);
+          });
+        } catch(e) {
+          $rootScope.$apply(function() {
+            $rootScope.showMessage(event.data);
+          });
+        }
+      };
+
+      socket.onclose = function() {
+        setTimeout(function() {
+          window.vipraWS();
+        }, 5000);
+      };
+    };
+
+    window.vipraWS();
 
   }]);
 
diff --git a/vipra-ui/app/js/controllers.js b/vipra-ui/app/js/controllers.js
index 9f9e5d25eaeff139ba2c62b09e53969cd2a2b5b0..31325a87fe71ff73579aa01d9b3056c57aae6837 100644
--- a/vipra-ui/app/js/controllers.js
+++ b/vipra-ui/app/js/controllers.js
@@ -2,7 +2,7 @@
  * Vipra Application
  * Controllers
  ******************************************************************************/
-/* globals angular, Vipra, moment, vis, console, prompt, randomColor, Highcharts, $ */
+/* globals angular, Vipra, moment, vis, prompt, randomColor, Highcharts, $ */
 (function() {
 
   "use strict";
@@ -107,6 +107,15 @@
         }
       });
 
+      hotkeys.add({
+        combo: 'n',
+        description: 'Go to network',
+        callback: function() {
+          if ($scope.rootModels.topicModel && $state.current.name !== 'network')
+            $state.transitionTo('network');
+        }
+      });
+
       hotkeys.add({
         combo: 'a',
         description: 'Go to articles',
@@ -126,7 +135,7 @@
       });
 
       hotkeys.add({
-        combo: 'n',
+        combo: 'i',
         description: 'Go to entities',
         callback: function() {
           if ($scope.rootModels.topicModel && $state.current.name !== 'entities')
@@ -151,14 +160,6 @@
         }
       });
 
-      hotkeys.add({
-        combo: 'b',
-        description: 'Report a bug',
-        callback: function() {
-          $scope.reportBug();
-        }
-      });
-
       $scope.showCheatSheet = hotkeys.toggleCheatSheet;
     }
   ]);
@@ -241,8 +242,8 @@
   /**
    * Network controller
    */
-  app.controller('NetworkController', ['$scope', '$state', '$stateParams', '$timeout', 'ArticleFactory', 'TopicFactory', 'WordFactory',
-    function($scope, $state, $stateParams, $timeout, ArticleFactory, TopicFactory, WordFactory) {
+  app.controller('NetworkController', ['$scope', '$state', '$stateParams', '$timeout', 'ArticleFactory', 'TopicFactory', 'WordFactory', 'WindowFactory',
+    function($scope, $state, $stateParams, $timeout, ArticleFactory, TopicFactory, WordFactory, WindowFactory) {
 
       var id = 0,
         ids = {},
@@ -253,13 +254,14 @@
         topics: '#DBB234',
         words: '#FFFFFF'
       };
+
       $scope.nodes = new vis.DataSet();
       $scope.edges = new vis.DataSet();
       $scope.data = {
         nodes: $scope.nodes,
         edges: $scope.edges
       };
-      $scope.type = $stateParams.type;
+
       $scope.options = {
         nodes: {
           font: {
@@ -287,6 +289,7 @@
           hideEdgesOnDrag: true
         }
       };
+
       $scope.shown = {
         articles: true,
         similararticles: true,
@@ -294,49 +297,6 @@
         words: true
       };
 
-      var factory;
-      if ($stateParams.type === 'articles')
-        factory = ArticleFactory;
-      else if ($stateParams.type === 'topics')
-        factory = TopicFactory;
-      else if ($stateParams.type === 'word')
-        factory = WordFactory;
-      else {
-        console.log('unknown network type');
-        return;
-      }
-
-      // if the topic model is not set, the page was refreshed
-      // set to true, id of current node decides topic model
-      if (!$scope.rootModels.topicModel)
-        $scope.rootModels.topicModel = true;
-
-      // get root node
-      factory.get({
-        id: $stateParams.id
-      }, function(data) {
-        $scope.rootNode = data;
-
-        // add root node
-        if ($stateParams.type === 'articles')
-          $scope.nodes.add([articleNode(data)]);
-        else if ($stateParams.type === 'topics')
-          $scope.nodes.add([topicNode(data)]);
-        else if ($stateParams.type === 'words')
-          $scope.nodes.add([wordNode(data)]);
-        ids[data.id] = id;
-
-        // create graph
-        var container = document.getElementById("visgraph");
-        $scope.graph = new vis.Network(container, $scope.data, $scope.options);
-        $scope.graph.on('selectNode', $scope.select);
-        $scope.graph.on('doubleClick', $scope.open);
-
-        // take topic model from node
-        if (!angular.isObject($scope.rootModels.topicModel))
-          $scope.rootModels.topicModel = data.topicModel;
-      });
-
       var newNode = function(title, type, show, dbid, color, shape, loader) {
         ids[dbid] = ++id;
         return {
@@ -348,11 +308,34 @@
           dbid: dbid,
           shape: shape || 'dot',
           loader: loader,
+          borderWidth: 1,
+          origColor: {
+            background: color,
+            highlight: {
+              background: color,
+            }
+          },
           color: {
+            border: '#000',
             background: color,
             highlight: {
-              background: color
+              background: color,
             }
+          },
+          font: {
+            color: '#000'
+          }
+        };
+      };
+
+      var newEdge = function(from, to) {
+        return {
+          from: from,
+          to: to,
+          selectionWidth: 1,
+          color: {
+            color: '#333',
+            highlight: '#f00'
           }
         };
       };
@@ -396,20 +379,18 @@
           for (var i = 0; i < result.length; i++) {
             var current = result[i];
             if (ids.hasOwnProperty(current.id)) {
-              if (edgeExists(ids[current.id], node.id))
+              if (node && edgeExists(ids[current.id], node.id))
                 continue;
-              newEdges.push({
-                from: ids[current.id],
-                to: node.id
-              });
-              addEdge(ids[current.id], node.id);
+              if(node) {
+                newEdges.push(newEdge(ids[current.id], node.id));
+                addEdge(ids[current.id], node.id);
+              }
             } else {
               newNodes.push(nodeFunction(current));
-              newEdges.push({
-                from: id,
-                to: node.id
-              });
-              addEdge(id, node.id);
+              if(node) {
+                newEdges.push(newEdge(id, node.id));
+                addEdge(id, node.id);
+              }
             }
           }
           if (newNodes.length)
@@ -497,16 +478,143 @@
         });
       };
 
+      $scope.reset = function() {
+        $state.go($state.current, {}, {
+          reload: true
+        });
+      };
+
+      $scope.fit = function() {
+        $scope.graph.fit({
+          animation: {
+            offset: {x: 0, y: 0},
+            duration: 1000,
+            easingFunction: 'easeInOutQuad'
+          }
+        });
+      };
+
       $scope.$watch('rootModels.topicModel', function(newVal) {
         if ($scope.rootNode && $scope.rootNode.topicModel.id !== newVal.id)
           $state.transitionTo('index');
       });
 
-      $scope.reset = function() {
-        $state.go($state.current, {}, {
-          reload: true
+      $scope.$watch('searchNodes', function() {
+        var nodes = $scope.nodes.get();
+        var i;
+        var updates = [];
+        if($scope.searchNodes) {
+          for(i = 0; i < nodes.length; i++) {
+            if(nodes[i].title.indexOf($scope.searchNodes) != -1) {
+              updates.push({
+                id: nodes[i].id,
+                color: nodes[i].origColor,
+                font: {
+                  color: '#000'
+                }
+              });
+            } else {
+              updates.push({
+                id: nodes[i].id,
+                color: {
+                  background: '#eee'
+                },
+                font: {
+                  color: '#ccc'
+                }
+              });
+            }
+          }
+        } else {
+          for(i = 0; i < nodes.length; i++) {
+            updates.push({
+              id: nodes[i].id,
+              color: nodes[i].origColor,
+              font: {
+                color: '#000'
+              }
+            });
+          }
+        }
+
+        $scope.data.nodes.update(updates);
+      });
+
+      // create graph
+      var container = document.getElementById("visgraph");
+      $scope.graph = new vis.Network(container, $scope.data, $scope.options);
+      $scope.graph.on('selectNode', $scope.select);
+      $scope.graph.on('doubleClick', $scope.open);
+
+      if($stateParams.type) {
+        // if the topic model is not set, the page was refreshed
+        // set to true, id of current node decides topic model
+        if (!$scope.rootModels.topicModel)
+          $scope.rootModels.topicModel = true;
+
+        // type is given, load node
+        var factory;
+        if ($stateParams.type === 'articles')
+          factory = ArticleFactory;
+        else if ($stateParams.type === 'topics')
+          factory = TopicFactory;
+        else if ($stateParams.type === 'word')
+          factory = WordFactory;
+
+        // get root node
+        factory.get({
+          id: $stateParams.id
+        }, function(data) {
+          $scope.rootNode = data;
+
+          // add root node
+          if ($stateParams.type === 'articles')
+            $scope.nodes.add([articleNode(data)]);
+          else if ($stateParams.type === 'topics')
+            $scope.nodes.add([topicNode(data)]);
+          else if ($stateParams.type === 'words')
+            $scope.nodes.add([wordNode(data)]);
+          ids[data.id] = id;
+
+          // take topic model from node
+          if (!angular.isObject($scope.rootModels.topicModel))
+            $scope.rootModels.topicModel = data.topicModel;
         });
-      };
+      } else {
+        $scope.queryTopics = function() {
+          if ($scope.shown.topics) {
+            TopicFactory.query({
+              topicModel: $scope.rootModels.topicModel.id
+            }, function(data) {
+              constructor(data, null, topicNode);
+            });
+          }
+        };
+
+        $scope.$watch('rootModels.topicModel', function() {
+          $scope.queryTopics();
+        });
+
+        // page was reloaded, choose topic model
+        if (!$scope.rootModels.topicModel)
+          $scope.chooseTopicModel();
+        else
+          $scope.queryTopics();
+      }
+
+      $scope.reloadWindows = function() {
+        if(!$scope.rootModels.topicModel) return;
+
+        WindowFactory.query({
+          topicModel: $scope.rootModels.topicModel.id
+        }, function(data) {
+          $scope.windows = data;
+        });
+      }
+
+      $scope.$watch('rootModels.topicModel', function() {
+        $scope.reloadWindows();
+      });
     }
   ]);
 
@@ -759,21 +867,33 @@
         limit: 100
       };
 
-      $scope.$watchGroup(['articlesIndexModels.page', 'articlesIndexModels.sortkey', 'articlesIndexModels.sortdir', 'rootModels.topicModel'], function() {
+      $scope.reloadArticles = function() {
         if (!$scope.rootModels.topicModel) return;
 
         ArticleFactory.query({
           skip: ($scope.articlesIndexModels.page - 1) * $scope.articlesIndexModels.limit,
           limit: $scope.articlesIndexModels.limit,
           sort: ($scope.articlesIndexModels.sortdir ? '' : '-') + $scope.articlesIndexModels.sortkey,
-          topicModel: $scope.rootModels.topicModel.id
+          topicModel: $scope.rootModels.topicModel.id,
+          char: $scope.articlesIndexModels.startChar,
+          contains: $scope.articlesIndexModels.contains
         }, function(data, headers) {
           $scope.articles = data;
           $scope.articlesTotal = headers("V-Total");
           $scope.maxPage = Math.ceil($scope.articlesTotal / $scope.articlesIndexModels.limit);
         });
+      };
+
+      $scope.$watchGroup(['articlesIndexModels.page', 'articlesIndexModels.sortkey', 'articlesIndexModels.sortdir', 'rootModels.topicModel'], function() {
+        $scope.reloadArticles();
       });
 
+      $scope.$watchGroup(['articlesIndexModels.startChar', 'articlesIndexModels.contains'], function() {
+        if($scope.articlesIndexModels.page !== 1)
+          $scope.articlesIndexModels.page = 1;
+        else
+          $scope.reloadArticles();
+      });
     }
   ]);
 
@@ -794,7 +914,6 @@
         id: $stateParams.id
       }, function(data) {
         $scope.article = data;
-        $scope.article.text = $scope.article.text;
         $scope.articleDate = Vipra.formatDate($scope.article.date);
         $scope.articleCreated = Vipra.formatDateTime($scope.article.created);
         $scope.articleModified = Vipra.formatDateTime($scope.article.modified);
@@ -930,21 +1049,33 @@
         limit: 100
       };
 
-      $scope.$watchGroup(['topicsIndexModels.page', 'topicsIndexModels.sortkey', 'topicsIndexModels.sortdir', 'rootModels.topicModel'], function() {
+      $scope.reloadTopics = function() {
         if (!$scope.rootModels.topicModel) return;
 
         TopicFactory.query({
           topicModel: $scope.rootModels.topicModel.id,
           skip: ($scope.topicsIndexModels.page - 1) * $scope.topicsIndexModels.limit,
           limit: $scope.topicsIndexModels.limit,
-          sort: ($scope.topicsIndexModels.sortdir ? '' : '-') + $scope.topicsIndexModels.sortkey
+          sort: ($scope.topicsIndexModels.sortdir ? '' : '-') + $scope.topicsIndexModels.sortkey,
+          char: $scope.topicsIndexModels.startChar,
+          contains: $scope.topicsIndexModels.contains
         }, function(data, headers) {
           $scope.topics = data;
           $scope.topicsTotal = headers("V-Total");
           $scope.maxPage = Math.ceil($scope.topicsTotal / $scope.topicsIndexModels.limit);
         });
+      };
+
+      $scope.$watchGroup(['topicsIndexModels.page', 'topicsIndexModels.sortkey', 'topicsIndexModels.sortdir', 'rootModels.topicModel'], function() {
+        $scope.reloadTopics();
       });
 
+      $scope.$watchGroup(['topicsIndexModels.startChar', 'topicsIndexModels.contains'], function() {
+        if($scope.topicsIndexModels.page !== 1)
+          $scope.topicsIndexModels.page = 1;
+        else
+          $scope.reloadTopics();
+      });
     }
   ]);
 
@@ -1164,21 +1295,33 @@
         limit: 100
       };
 
-      $scope.$watchGroup(['entitiesIndexModels.page', 'entitiesIndexModels.sortkey', 'entitiesIndexModels.sortdir', 'rootModels.topicModel'], function() {
+      $scope.reloadEntities = function() {
         if (!$scope.rootModels.topicModel) return;
 
         EntityFactory.query({
           topicModel: $scope.rootModels.topicModel.id,
           skip: ($scope.entitiesIndexModels.page - 1) * $scope.entitiesIndexModels.limit,
           limit: $scope.entitiesIndexModels.limit,
-          sort: ($scope.entitiesIndexModels.sortdir ? '' : '-') + $scope.entitiesIndexModels.sortkey
+          sort: ($scope.entitiesIndexModels.sortdir ? '' : '-') + $scope.entitiesIndexModels.sortkey,
+          char: $scope.entitiesIndexModels.startChar,
+          contains: $scope.entitiesIndexModels.contains
         }, function(data, headers) {
           $scope.entities = data;
           $scope.entitiesTotal = headers("V-Total");
           $scope.maxPage = Math.ceil($scope.entitiesTotal / $scope.entitiesIndexModels.limit);
         });
+      };
+
+      $scope.$watchGroup(['entitiesIndexModels.page', 'entitiesIndexModels.sortkey', 'entitiesIndexModels.sortdir', 'rootModels.topicModel'], function() {
+        $scope.reloadEntities();
       });
 
+      $scope.$watchGroup(['entitiesIndexModels.startChar', 'entitiesIndexModels.contains'], function() {
+        if($scope.entitiesIndexModels.page !== 1)
+          $scope.entitiesIndexModels.page = 1;
+        else
+          $scope.reloadEntities();
+      });
     }
   ]);
 
@@ -1252,21 +1395,33 @@
         limit: 100
       };
 
-      $scope.$watchGroup(['wordsIndexModels.page', 'wordsIndexModels.sortkey', 'wordsIndexModels.sortdir', 'rootModels.topicModel'], function() {
+      $scope.reloadWords = function() {
         if (!$scope.rootModels.topicModel) return;
-
+        
         WordFactory.query({
           topicModel: $scope.rootModels.topicModel.id,
           skip: ($scope.wordsIndexModels.page - 1) * $scope.wordsIndexModels.limit,
           limit: $scope.wordsIndexModels.limit,
-          sort: ($scope.wordsIndexModels.sortdir ? '' : '-') + $scope.wordsIndexModels.sortkey
+          sort: ($scope.wordsIndexModels.sortdir ? '' : '-') + $scope.wordsIndexModels.sortkey,
+          char: $scope.wordsIndexModels.startChar,
+          contains: $scope.wordsIndexModels.contains
         }, function(data, headers) {
           $scope.words = data;
           $scope.wordsTotal = headers("V-Total");
           $scope.maxPage = Math.ceil($scope.wordsTotal / $scope.wordsIndexModels.limit);
         });
+      };
+
+      $scope.$watchGroup(['wordsIndexModels.page', 'wordsIndexModels.sortkey', 'wordsIndexModels.sortdir', 'rootModels.topicModel'], function() {
+        $scope.reloadWords();
       });
 
+      $scope.$watchGroup(['wordsIndexModels.startChar', 'wordsIndexModels.contains'], function() {
+        if($scope.wordsIndexModels.page !== 1)
+          $scope.wordsIndexModels.page = 1;
+        else
+          $scope.reloadWords();
+      });
     }
   ]);
 
diff --git a/vipra-ui/app/js/directives.js b/vipra-ui/app/js/directives.js
index b41b9c9d6d59b050927625ca46f09cffd693010a..a2989a5ec29695d54d64464868817405150d69ac 100644
--- a/vipra-ui/app/js/directives.js
+++ b/vipra-ui/app/js/directives.js
@@ -266,15 +266,34 @@
             }
           }
         });
-
-        $scope.doClear = function() {
-          delete $scope.ngModel;
-        };
       },
       templateUrl: '/html/directives/sequence-dropdown.html'
     };
   }]);
 
+  app.directive('windowDropdown', [function() {
+    return {
+      scope: {
+        ngModel: '=',
+        windows: '=',
+        dropup: '@'
+      },
+      link: function($scope) {
+        $scope.showDropup = $scope.dropup === 'true';
+
+        $scope.$watch('windows', function(newValue) {
+          if (newValue) {
+            for (var i = 0, w; i < $scope.windows.length; i++) {
+              w = $scope.windows[i];
+              w.label = Vipra.windowLabel(w.startDate, w.windowResolution);
+            }
+          }
+        });
+      },
+      templateUrl: '/html/directives/window-dropdown.html'
+    };
+  }]);
+
   app.directive('sortBy', [function() {
     return {
       restrict: 'A',
@@ -471,4 +490,22 @@
     };
   }]);
 
+  app.directive('charSelector', [function() {
+    return {
+      replace: true,
+      scope: {
+        ngModel: '='
+      },
+      link: function($scope, $elem) {
+        $elem.on('click', 'a', function() {
+          var c = $(this).data('char');
+          $scope.$apply(function() {
+            $scope.ngModel = c;
+          });
+        });
+      },
+      templateUrl: 'html/directives/char-selector.html'
+    };
+  }]);
+
 })();
\ No newline at end of file
diff --git a/vipra-ui/app/js/factories.js b/vipra-ui/app/js/factories.js
index 514dc1cbf7d3404ab0b401d5aa47d7618accb8ff..430f6af6acfa1ee836f697e63d7f83f17dab5943 100644
--- a/vipra-ui/app/js/factories.js
+++ b/vipra-ui/app/js/factories.js
@@ -2,7 +2,7 @@
  * Vipra Application
  * Factories
  ******************************************************************************/
-/* globals angular, Vipra */
+/* globals angular, Vipra, $ */
 (function() {
 
   "use strict";
@@ -84,4 +84,39 @@
     return $myResource(Vipra.config.restUrl + '/entities/:id');
   }]);
 
+  app.factory('WindowFactory', ['$myResource', function($myResource) {
+    return $myResource(Vipra.config.restUrl + '/windows/:id');
+  }]);
+
+  app.factory('AlertFactory', [function() {
+    var alerts = $("#alerts");
+    if(alerts.length === 0) {
+      alerts = $('<div id="alerts" class="alerts"></div>').appendTo('body');
+    }
+
+    function showAlert(msg, config) {
+      config = angular.merge({}, {
+        type: 'info',
+        time: 0,
+        dismissible: true
+      }, config);
+      var classes = 'alert alert-' + config.type;
+      if(config.dismissible) {
+        classes += ' alert-dismissible';
+      }
+      var alert = $('<div class="' + classes + '" role="alert" style="display:none">' +  (config.dismissible ? 
+        '<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>' : '') + 
+        msg + '</div>').appendTo(alerts).fadeIn();
+      if(config.time > 0) {
+        setTimeout(function() {
+          alert.fadeOut(300, function() { $(this).remove(); });
+        }, config.time);
+      }
+    }
+
+    return {
+      showAlert: showAlert
+    };
+  }]);
+
 })();
\ No newline at end of file
diff --git a/vipra-ui/app/less/app.less b/vipra-ui/app/less/app.less
index 4534a0062003ba89e6c407f7b4be94ae7092c26f..cb294ea3bac6b30d38f0a947bae457c4d40970c6 100644
--- a/vipra-ui/app/less/app.less
+++ b/vipra-ui/app/less/app.less
@@ -86,24 +86,37 @@ a:hover {
 }
 
 .graph-legend {
+  @spacing: 5px;
   position: absolute;
   top: 10px;
   left: 10px;
-  font-weight: bold;
-  padding: 10px;
-  background: rgba(255, 255, 255, 0.75);
+  padding: @spacing*2;
+  background: rgba(128, 128, 128, 0.1);
   border-radius: 5px;
-  label {
-    margin: 0;
+  &:not(.collapsed) {
+    width: 220px;
   }
   label + label {
-    padding-left: 5px;
+    padding-left: @spacing;
   }
-  .checkbox:first-child {
-    margin-top: 0;
+  label,
+  .checkbox {
+    margin: 0;
+  }
+  div + div {
+    margin-top: @spacing;
   }
-  .checkbox:last-child {
-    margin-bottom: 0;
+  table,
+  table button {
+    width: 100%;
+    td + td {
+      padding-left: @spacing;
+    }
+  }
+  .collapser {
+    margin: 0;
+    position: relative;
+    z-index: 2;
   }
 }
 
@@ -437,6 +450,20 @@ a:hover {
   color: #ccc;
 }
 
+.selectclear,
+.selectclear:hover,
+.selectclear:active,
+.selectclear:focus,
+.selectclear:active:hover {
+  border-left: 0;
+  padding-left: 4px;
+  padding-right: 4px;
+  color: #ccc;
+  background: #fff;
+  outline: none;
+  border-color: #ccc;
+}
+
 .chart,
 .highcharts-container {
   width: 100% !important;
@@ -522,6 +549,7 @@ entity-menu {
   bottom: 20px;
   left: 20px;
   right: 20px;
+  z-index: 8999;
 
   .alert {
     margin: 0;
@@ -755,6 +783,31 @@ entity-menu {
   display: block !important;
 }
 
+.char-selector {
+  @char-padding: 5px;
+  padding: 5px;
+  text-align: center;
+  font-size: 0;
+  > a {
+    font-size: 14px;
+    padding-right: @char-padding;
+  }
+  > a:last-child {
+    padding-right: 0;
+  }
+  > a + a {
+    padding-left: @char-padding;
+    border-left: 1px solid #ccc;
+  }
+  > a.selected {
+    text-decoration: underline;
+  }
+}
+
+.nooutline:focus {
+  outline: none;
+}
+
 @-moz-keyframes spin {
   100% {
     -moz-transform: rotateY(360deg);
diff --git a/vipra-ui/bower.json b/vipra-ui/bower.json
index 1d60c9d9fd99051f4f9439f28f6a4c7508a8d2c0..e29148b23a4626a8214c37e3867a3656ede6d8bb 100644
--- a/vipra-ui/bower.json
+++ b/vipra-ui/bower.json
@@ -23,7 +23,6 @@
     "angular-resource": "^1.x",
     "angular-sanitize": "^1.x",
     "angular-animate": "^1.x",
-    "angular-websocket": "^1.x",
     "angular-ui-router": "^0.x",
     "highcharts": "^4.x",
     "vis": "^4.x",
diff --git a/vipra-ui/gulpfile.js b/vipra-ui/gulpfile.js
index 6d8a564aa7e222ec94e1718b01b9f25e7b76123a..900439345488c7b7d2d13e5623e2be43b8909ad1 100644
--- a/vipra-ui/gulpfile.js
+++ b/vipra-ui/gulpfile.js
@@ -17,7 +17,6 @@ var assets = {
     'bower_components/angular-resource/angular-resource.min.js',
     'bower_components/angular-sanitize/angular-sanitize.min.js',
     'bower_components/angular-animate/angular-animate.min.js',
-    'bower_components/angular-websocket/angular-websocket.min.js',
     'bower_components/angular-hotkeys/build/hotkeys.min.js',
     'bower_components/angular-ui-router/release/angular-ui-router.min.js',
     'bower_components/bootstrap/dist/js/bootstrap.min.js',
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 55fec90e8ae55c106a1c47135f74398797d36cb7..3c7c8aaafa6bccdcdf0f90ec8df8e4afde9be019 100644
--- a/vipra-util/src/main/java/de/vipra/util/Config.java
+++ b/vipra-util/src/main/java/de/vipra/util/Config.java
@@ -237,7 +237,7 @@ public class Config {
 		return getConfig(false);
 	}
 
-	public static Config getConfig(boolean skipModels) throws ConfigException {
+	public static Config getConfig(final boolean skipModels) throws ConfigException {
 		if (instance == null) {
 			try {
 				InputStream in = null;
diff --git a/vipra-util/src/main/java/de/vipra/util/ConsoleUtils.java b/vipra-util/src/main/java/de/vipra/util/ConsoleUtils.java
index c751672eb648876368351be553c9586b722c349f..089276a528fdc168961cdac9116edb7d5242807e 100644
--- a/vipra-util/src/main/java/de/vipra/util/ConsoleUtils.java
+++ b/vipra-util/src/main/java/de/vipra/util/ConsoleUtils.java
@@ -96,7 +96,7 @@ public class ConsoleUtils {
 	public static int print(final String msg) {
 		return print(System.out, true, msg);
 	}
-	
+
 	public static int printNOLF(final String msg) {
 		return print(System.out, false, msg);
 	}
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 edc7792a412a6f7679412e04ce9e3919b6bc78bf..dbee71c2497d8bb439e35a3ee6342cec4356d292 100644
--- a/vipra-util/src/main/java/de/vipra/util/Constants.java
+++ b/vipra-util/src/main/java/de/vipra/util/Constants.java
@@ -107,6 +107,11 @@ public class Constants {
 	 */
 	public static final int MAX_SIMILAR_DOCUMENTS = 10;
 
+	/**
+	 * Maximum number of similar topics for each topic. Default 10.
+	 */
+	public static final int MAX_SIMILAR_TOPICS = 10;
+
 	/**
 	 * Maximum divergence between a document and similar documents. Lower values
 	 * mean more similar documents (less divergence). Default 0.25.
diff --git a/vipra-util/src/main/java/de/vipra/util/IPCClient.java b/vipra-util/src/main/java/de/vipra/util/IPCClient.java
index 46fd449dca538fd1cc81a9b91e8eef3a08654581..2d6c50e4368c31b5b72526a4c7b7d26ff4c67980 100644
--- a/vipra-util/src/main/java/de/vipra/util/IPCClient.java
+++ b/vipra-util/src/main/java/de/vipra/util/IPCClient.java
@@ -10,21 +10,20 @@ import de.vipra.util.ex.ConfigException;
 
 public class IPCClient implements Closeable {
 
-	private final Socket socket;
-	private final DataOutputStream out;
+	private final Config config = Config.getConfig();
+	private Socket socket;
+	private DataOutputStream out;
 
 	public IPCClient() throws ConfigException, UnknownHostException, IOException {
-		final Config config = Config.getConfig();
-		socket = new Socket(config.getIpcHost(), config.getIpcPort());
-		out = new DataOutputStream(socket.getOutputStream());
+		reconnect();
 	}
 
 	public IPCClient send(final IPCMessage message) throws IOException {
-		if (!socket.isClosed()) {
-			final String messageStr = Config.mapper.writeValueAsString(message);
-			out.writeUTF(messageStr);
-			out.flush();
-		}
+		if (socket.isClosed())
+			reconnect();
+		final String messageStr = Config.mapper.writeValueAsString(message);
+		out.writeUTF(messageStr);
+		out.flush();
 		return this;
 	}
 
@@ -33,4 +32,10 @@ public class IPCClient implements Closeable {
 		socket.close();
 	}
 
+	private void reconnect() throws UnknownHostException, IOException {
+		socket = new Socket(config.getIpcHost(), config.getIpcPort());
+		socket.setKeepAlive(true);
+		out = new DataOutputStream(socket.getOutputStream());
+	}
+
 }
diff --git a/vipra-util/src/main/java/de/vipra/util/LockFile.java b/vipra-util/src/main/java/de/vipra/util/LockFile.java
index 216989340578db7cf236ada2282da927d274efcc..b68de7f6e18dc6f5fc7ed572dbf8eec5bd7fdc2a 100644
--- a/vipra-util/src/main/java/de/vipra/util/LockFile.java
+++ b/vipra-util/src/main/java/de/vipra/util/LockFile.java
@@ -7,10 +7,10 @@ import de.vipra.util.ex.LockFileException;
 
 public class LockFile {
 
-	private File file = new File(PathUtils.tempDir(), "vipra.lock");
+	private final File file = new File(PathUtils.tempDir(), "vipra.lock");
 
 	public void lock() throws LockFileException {
-		if (file.exists())
+		if (isLocked())
 			throw new LockFileException();
 		try {
 			file.createNewFile();
@@ -27,6 +27,10 @@ public class LockFile {
 		}
 	}
 
+	public boolean isLocked() {
+		return file.exists();
+	}
+
 	public String getPath() {
 		return file.getAbsolutePath();
 	}
diff --git a/vipra-util/src/main/java/de/vipra/util/model/TopicModelConfig.java b/vipra-util/src/main/java/de/vipra/util/model/TopicModelConfig.java
index d369d5ef0996cdfe312563eaaea14b38eba17cac..30f28c73f311d43be35c315a5d05c36deb006d07 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/TopicModelConfig.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/TopicModelConfig.java
@@ -33,6 +33,7 @@ public class TopicModelConfig implements Serializable {
 	private int staticIterations = Constants.STATIC_ITER;
 	private int topicAutoNamingWords = Constants.TOPIC_AUTO_NAMING_WORDS;
 	private int maxSimilarDocuments = Constants.MAX_SIMILAR_DOCUMENTS;
+	private int maxSimilarTopics = Constants.MAX_SIMILAR_TOPICS;
 	private int documentMinimumLength = Constants.DOCUMENT_MIN_LENGTH;
 	private int documentMinimumWordFrequency = Constants.DOCUMENT_MIN_WORD_FREQ;
 	private int spotlightSupport = Constants.SPOTLIGHT_SUPPORT;
@@ -55,6 +56,7 @@ public class TopicModelConfig implements Serializable {
 		staticIterations = topicModelConfig.getStaticIterations();
 		topicAutoNamingWords = topicModelConfig.getTopicAutoNamingWords();
 		maxSimilarDocuments = topicModelConfig.getMaxSimilarDocuments();
+		maxSimilarTopics = topicModelConfig.getMaxSimilarTopics();
 		documentMinimumLength = topicModelConfig.getDocumentMinimumLength();
 		documentMinimumWordFrequency = topicModelConfig.getDocumentMinimumWordFrequency();
 		spotlightSupport = topicModelConfig.getSpotlightSupport();
@@ -156,6 +158,14 @@ public class TopicModelConfig implements Serializable {
 		this.maxSimilarDocuments = maxSimilarDocuments;
 	}
 
+	public int getMaxSimilarTopics() {
+		return maxSimilarTopics;
+	}
+
+	public void setMaxSimilarTopics(int maxSimilarTopics) {
+		this.maxSimilarTopics = maxSimilarTopics;
+	}
+
 	public int getSpotlightSupport() {
 		return spotlightSupport;
 	}
diff --git a/vipra-util/src/main/java/de/vipra/util/service/MongoService.java b/vipra-util/src/main/java/de/vipra/util/service/MongoService.java
index b7ea2dce5944d8055f2db4d2ea04012412db4829..26fda9a7414828123c13343956b6e313bba4684a 100644
--- a/vipra-util/src/main/java/de/vipra/util/service/MongoService.java
+++ b/vipra-util/src/main/java/de/vipra/util/service/MongoService.java
@@ -208,7 +208,7 @@ public class MongoService<Type extends Model<IdType>, IdType> {
 			return datastore.getCount(clazz);
 
 		final Query<Type> query = datastore.createQuery(clazz);
-		builder.build(query);
+		builder.build(query, true);
 
 		return datastore.getCount(query);
 	}
diff --git a/vipra-util/src/main/java/de/vipra/util/service/QueryBuilder.java b/vipra-util/src/main/java/de/vipra/util/service/QueryBuilder.java
index 75875fd06c907c51b5119af5afeb4f5f324fe06f..e7a587de93fb9568908b0a09d1b4bd32bae34acb 100644
--- a/vipra-util/src/main/java/de/vipra/util/service/QueryBuilder.java
+++ b/vipra-util/src/main/java/de/vipra/util/service/QueryBuilder.java
@@ -3,17 +3,26 @@ package de.vipra.util.service;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Set;
+import java.util.regex.Pattern;
 
 import org.mongodb.morphia.query.Query;
 
 public class QueryBuilder {
 
 	public static enum CriterionType {
-		EQ,
-		LT,
-		LTE,
-		GT,
-		GTE
+		EQUALS,
+		EQUALS_IGNORECASE,
+		LESSERTHAN,
+		LESSERTHANEQUAL,
+		GREATERTHAN,
+		GREATERTHANEQUAL,
+		STARTSWITH,
+		STARTSWITH_IGNORECASE,
+		ENDSWITH,
+		ENDSWITH_IGNORECASE,
+		CONTAINS,
+		CONTAINS_IGNORECASE,
+		REGEX
 	};
 
 	public static class Criterion {
@@ -29,21 +38,44 @@ public class QueryBuilder {
 
 		public void apply(final Query<?> query) {
 			switch (type) {
-				case EQ:
+				case EQUALS:
 					query.field(field).equal(value);
 					break;
-				case LT:
+				case EQUALS_IGNORECASE:
+					query.field(field).equalIgnoreCase(value);
+					break;
+				case LESSERTHAN:
 					query.field(field).lessThan(value);
 					break;
-				case LTE:
+				case LESSERTHANEQUAL:
 					query.field(field).lessThanOrEq(value);
 					break;
-				case GT:
+				case GREATERTHAN:
 					query.field(field).greaterThan(value);
 					break;
-				case GTE:
+				case GREATERTHANEQUAL:
 					query.field(field).greaterThanOrEq(value);
 					break;
+				case STARTSWITH:
+					query.field(field).startsWith((String) value);
+					break;
+				case STARTSWITH_IGNORECASE:
+					query.field(field).startsWithIgnoreCase((String) value);
+					break;
+				case ENDSWITH:
+					query.field(field).endsWith((String) value);
+					break;
+				case ENDSWITH_IGNORECASE:
+					query.field(field).endsWithIgnoreCase((String) value);
+					break;
+				case CONTAINS:
+					query.field(field).contains((String) value);
+					break;
+				case CONTAINS_IGNORECASE:
+					query.field(field).containsIgnoreCase((String) value);
+					break;
+				case REGEX:
+					query.filter(field, value);
 			}
 		}
 	}
@@ -83,32 +115,48 @@ public class QueryBuilder {
 	}
 
 	public QueryBuilder eq(final String field, final Object value) {
-		if (field != null && !field.isEmpty() && value != null)
-			criteria.add(new Criterion(field, CriterionType.EQ, value));
-		return this;
+		return criterion(field, CriterionType.EQUALS, value);
+	}
+
+	public QueryBuilder eq(final String field, final Object value, final boolean ignoreCase) {
+		return criterion(field, ignoreCase ? CriterionType.EQUALS_IGNORECASE : CriterionType.EQUALS, value);
 	}
 
 	public QueryBuilder lt(final String field, final Object value) {
-		if (field != null && !field.isEmpty() && value != null)
-			criteria.add(new Criterion(field, CriterionType.LT, value));
-		return this;
+		return criterion(field, CriterionType.LESSERTHAN, value);
 	}
 
 	public QueryBuilder lte(final String field, final Object value) {
-		if (field != null && !field.isEmpty() && value != null)
-			criteria.add(new Criterion(field, CriterionType.LTE, value));
-		return this;
+		return criterion(field, CriterionType.LESSERTHANEQUAL, value);
 	}
 
 	public QueryBuilder gt(final String field, final Object value) {
-		if (field != null && !field.isEmpty() && value != null)
-			criteria.add(new Criterion(field, CriterionType.GT, value));
-		return this;
+		return criterion(field, CriterionType.GREATERTHAN, value);
 	}
 
 	public QueryBuilder gte(final String field, final Object value) {
+		return criterion(field, CriterionType.GREATERTHANEQUAL, value);
+	}
+
+	public QueryBuilder startsWith(final String field, final String value, final boolean ignoreCase) {
+		return criterion(field, ignoreCase ? CriterionType.STARTSWITH_IGNORECASE : CriterionType.STARTSWITH, value);
+	}
+
+	public QueryBuilder endsWith(final String field, final String value, final boolean ignoreCase) {
+		return criterion(field, ignoreCase ? CriterionType.ENDSWITH_IGNORECASE : CriterionType.ENDSWITH, value);
+	}
+
+	public QueryBuilder contains(final String field, final String value, final boolean ignoreCase) {
+		return criterion(field, ignoreCase ? CriterionType.CONTAINS_IGNORECASE : CriterionType.CONTAINS, value);
+	}
+
+	public QueryBuilder regex(final String field, final Pattern pattern) {
+		return criterion(field, CriterionType.REGEX, pattern);
+	}
+
+	private QueryBuilder criterion(final String field, final CriterionType type, final Object value) {
 		if (field != null && !field.isEmpty() && value != null)
-			criteria.add(new Criterion(field, CriterionType.GTE, value));
+			criteria.add(new Criterion(field, type, value));
 		return this;
 	}
 
@@ -168,11 +216,15 @@ public class QueryBuilder {
 	}
 
 	public Query<?> build(final Query<?> query) {
-		if (getSkip() != null && getSkip() > 0)
+		return build(query, false);
+	}
+
+	public Query<?> build(final Query<?> query, final boolean counting) {
+		if (!counting && getSkip() != null && getSkip() > 0)
 			query.offset(getSkip());
-		if (getLimit() != null && getLimit() > 0)
+		if (!counting && getLimit() != null && getLimit() > 0)
 			query.limit(getLimit());
-		if (getSortBy() != null)
+		if (!counting && getSortBy() != null)
 			query.order(getSortBy());
 		if (getCriteria() != null)
 			for (final Criterion criterion : getCriteria())