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 4adc7dd66711659edcc03dca4ab6098c67fb6671..eaa25a59d9aeab06b8aa517065498dcb79526901 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/Main.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/Main.java
@@ -39,7 +39,7 @@ import de.vipra.cmd.option.ConfigCommand;
 import de.vipra.cmd.option.ImportCommand;
 import de.vipra.cmd.option.StatsCommand;
 import de.vipra.cmd.option.TestCommand;
-import de.vipra.cmd.option.TopicModelingCommand;
+import de.vipra.cmd.option.ModelingCommand;
 
 public class Main {
 
@@ -111,7 +111,7 @@ public class Main {
 			commands.add(new ImportCommand(cline.getOptionValues(OPT_IMPORT)));
 
 		if (cline.hasOption(OPT_MODELING))
-			commands.add(new TopicModelingCommand());
+			commands.add(new ModelingCommand());
 
 		if (cline.hasOption(OPT_STATS))
 			commands.add(new StatsCommand());
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/file/FilebaseVocabulary.java b/vipra-cmd/src/main/java/de/vipra/cmd/file/FilebaseVocabulary.java
index 2f0ec9317e270c07e9dcf7725ad494d29a620676..8f8b3b6f693714599fb35afdd65b6f54609d1ec2 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/file/FilebaseVocabulary.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/file/FilebaseVocabulary.java
@@ -4,8 +4,10 @@ import java.io.Closeable;
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 
 import de.vipra.util.Constants;
 import de.vipra.util.FileUtils;
@@ -14,13 +16,19 @@ public class FilebaseVocabulary implements Closeable, Iterable<String> {
 
 	private File file;
 	private List<String> vocables;
+	private Map<String, Integer> vocablesMap;
+	private int nextIndex = 0;
 
 	public FilebaseVocabulary(File file) throws IOException {
 		this.file = file;
 		if (file.exists()) {
 			vocables = new ArrayList<>(FileUtils.readFile(file));
+			vocablesMap = new HashMap<>(vocables.size() + 200);
+			for (String vocable : vocables)
+				vocablesMap.put(vocable, nextIndex++);
 		} else {
-			vocables = new ArrayList<>();
+			vocables = new ArrayList<>(500);
+			vocablesMap = new HashMap<>(500);
 		}
 	}
 
@@ -34,19 +42,20 @@ public class FilebaseVocabulary implements Closeable, Iterable<String> {
 
 	public void addVocabulary(String[] text) {
 		for (String word : text) {
-			// TODO fix this
-			if (!vocables.contains(word)) {
+			if (!vocablesMap.containsKey(word)) {
+				vocablesMap.put(word, nextIndex++);
 				vocables.add(word);
 			}
 		}
 	}
 
 	public int index(String word) {
-		return vocables.indexOf(word);
+		Integer index = vocablesMap.get(word);
+		return index == null ? -1 : index;
 	}
 
 	public int size() {
-		return vocables.size();
+		return vocablesMap.size();
 	}
 
 	@Override
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 d75179fb82bc047f8b18ee07f864ceac165eec6e..bb34530d1eb20afecea39c1aa838379033ce1a7b 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
@@ -5,6 +5,7 @@ import java.io.FileReader;
 import java.io.FilenameFilter;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
 
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
@@ -188,7 +189,7 @@ public class ImportCommand implements Command {
 		 * save words
 		 */
 		out.info("saving words");
-		List<Word> importedWords = wordMap.getNewWords();
+		Set<Word> importedWords = wordMap.getNewWords();
 		timer.lap("saving topic refs and indexing");
 		wordMap.create();
 		timer.lap("saving words");
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/option/TopicModelingCommand.java b/vipra-cmd/src/main/java/de/vipra/cmd/option/ModelingCommand.java
similarity index 96%
rename from vipra-cmd/src/main/java/de/vipra/cmd/option/TopicModelingCommand.java
rename to vipra-cmd/src/main/java/de/vipra/cmd/option/ModelingCommand.java
index bc0b184169c92588a04eafdfef78860787f83a2d..435a7ab6cbdff194157c0b0ecd551e5a203b361a 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/option/TopicModelingCommand.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/option/ModelingCommand.java
@@ -6,6 +6,7 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
+import java.util.Set;
 
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
@@ -32,9 +33,9 @@ import de.vipra.util.model.TopicRef;
 import de.vipra.util.model.Word;
 import de.vipra.util.service.DatabaseService;
 
-public class TopicModelingCommand implements Command {
+public class ModelingCommand implements Command {
 
-	public static final Logger log = LogManager.getLogger(TopicModelingCommand.class);
+	public static final Logger log = LogManager.getLogger(ModelingCommand.class);
 	public static final Logger out = LogManager.getLogger("shellout");
 
 	private Config config;
@@ -147,7 +148,7 @@ public class TopicModelingCommand implements Command {
 		 * save words
 		 */
 		out.info("saving words");
-		List<Word> importedWords = wordMap.getNewWords();
+		Set<Word> importedWords = wordMap.getNewWords();
 		timer.lap("saving topic refs and indexing");
 		wordMap.create();
 		timer.lap("saving words");
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 b6e59868a9aa29ba534eb8ca28be23915ec6d275..8264c003b526dab5e279b2244816fed990890f8f 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
@@ -9,7 +9,9 @@ import org.bson.types.ObjectId;
 import de.vipra.cmd.file.Filebase;
 import de.vipra.util.Config;
 import de.vipra.util.StringUtils;
-import de.vipra.util.model.TopicFull;
+import de.vipra.util.model.Article;
+import de.vipra.util.model.Topic;
+import de.vipra.util.model.Word;
 import de.vipra.util.service.DatabaseService;
 
 public class StatsCommand implements Command {
@@ -19,21 +21,25 @@ public class StatsCommand implements Command {
 
 	private Config config;
 	private Filebase filebase;
-	private DatabaseService<TopicFull, ObjectId> dbTopics;
+	private DatabaseService<Article, ObjectId> dbArticles;
+	private DatabaseService<Topic, ObjectId> dbTopics;
+	private DatabaseService<Word, String> dbWords;
 
 	private void stats() {
 		File modelFile = filebase.getModelFile();
 		out.info("filebase size: " + StringUtils.humanReadableByteCount(modelFile.length(), true));
-		out.info("# of articles: " + filebase.getIndex().size());
-		out.info("# of words   : " + filebase.getVocab().size());
+		out.info("# of articles: " + dbArticles.count());
 		out.info("# of topics  : " + dbTopics.count());
+		out.info("# of words   : " + dbWords.count());
 	}
 
 	@Override
 	public void run() throws Exception {
 		config = Config.getConfig();
 		filebase = Filebase.getFilebase(config);
-		dbTopics = DatabaseService.getDatabaseService(config, TopicFull.class);
+		dbArticles = DatabaseService.getDatabaseService(config, Article.class);
+		dbTopics = DatabaseService.getDatabaseService(config, Topic.class);
+		dbWords = DatabaseService.getDatabaseService(config, Word.class);
 
 		stats();
 	}
diff --git a/vipra-cmd/src/main/resources/config.properties b/vipra-cmd/src/main/resources/config.properties
index 822a7005c3cff53f490e50623dc41cdffc9e80ec..0778073f0a04af174ec97a6263bcf433f71a90d6 100644
--- a/vipra-cmd/src/main/resources/config.properties
+++ b/vipra-cmd/src/main/resources/config.properties
@@ -2,5 +2,5 @@ db.host=localhost
 db.port=27017
 db.name=test
 tm.processor=corenlp
-tm.analyzer=dynnmf
+tm.analyzer=jgibb
 tm.saveallwords=false
\ No newline at end of file
diff --git a/vipra-ui/app/html/articles/detail/network.html b/vipra-ui/app/html/articles/detail/network.html
new file mode 100644
index 0000000000000000000000000000000000000000..5ba521ab1a741ba18346473f29372a4b24b949fe
--- /dev/null
+++ b/vipra-ui/app/html/articles/detail/network.html
@@ -0,0 +1 @@
+<div class="graph" vis-graph="graph" vis-data="data" vis-options="options" vis-select="select" vis-dblclick="open"></div>
\ No newline at end of file
diff --git a/vipra-ui/app/html/articles/show.html b/vipra-ui/app/html/articles/detail/show.html
similarity index 84%
rename from vipra-ui/app/html/articles/show.html
rename to vipra-ui/app/html/articles/detail/show.html
index 13c354fed285907d6d71498d6aa9e97483a918bf..98016a43904610c24ec3cf3a32012795d834053f 100644
--- a/vipra-ui/app/html/articles/show.html
+++ b/vipra-ui/app/html/articles/detail/show.html
@@ -34,7 +34,13 @@
       <th>Word count</th>
       <td ng-bind="::article.stats.wordCount"></td>
     </tr>
+    <tr>
+      <th>Links</th>
+      <td>
+        <a ui-sref="articles.detail.network({id:article.id})">Network graph</a>
+      </td>
+    </tr>
   </tbody>
 </table>
 
-<p ng-bind="::article.text"></p>
\ No newline at end of file
+<p ng-bind-html="::article.text"></p>
\ No newline at end of file
diff --git a/vipra-ui/app/html/articles/index.html b/vipra-ui/app/html/articles/index.html
index 5560bfaff51ace0ddeb7c818e2d572d2fa7270fd..5f2bea4bbf5967fda77744308451f1b7ed6e9e0c 100644
--- a/vipra-ui/app/html/articles/index.html
+++ b/vipra-ui/app/html/articles/index.html
@@ -4,7 +4,7 @@
 
 <ul class="list-unstyled">
   <li ng-repeat="article in articles">
-    <a ui-sref="articles.show({id: article.id})" ng-bind="::article.title"></a>
+    <a ui-sref="articles.detail.show({id: article.id})" ng-bind="::article.title"></a>
   </li>
 </ul>
 
diff --git a/vipra-ui/app/html/index.html b/vipra-ui/app/html/index.html
index 336e7d2f348a602b8c83c30c000aec72297eac34..a1b589f3c65b813c7c7dc0ea28614f0356c3afbf 100644
--- a/vipra-ui/app/html/index.html
+++ b/vipra-ui/app/html/index.html
@@ -11,7 +11,7 @@
       <h4>Latest articles</h4>
       <ul class="list-unstyled">
         <li class="ellipsize" ng-repeat="article in latestArticles">
-          <a ui-sref="articles.show({id:article.id})" ng-bind="article.title"></a>
+          <a ui-sref="articles.detail.show({id:article.id})" ng-bind="article.title"></a>
         </li>
       </ul>
     </div>
@@ -50,7 +50,7 @@
       <h4>Results <query-time/></h4>
       <ul class="list-unstyled search-results">
         <li class="search-result" ng-repeat="article in searchResults">
-          <a ui-sref="articles.show({id:article.id})" ng-bind="article.title"></a>
+          <a ui-sref="articles.detail.show({id:article.id})" ng-bind="article.title"></a>
           <p>
             <span class="text" ng-bind="article.text"></span>
             <br>
diff --git a/vipra-ui/app/js/app.js b/vipra-ui/app/js/app.js
index 7b23dad661694e9b82da097c324e87b7244b3f73..29554595c0f76352ac2df53efea8b433c2e5eaee 100644
--- a/vipra-ui/app/js/app.js
+++ b/vipra-ui/app/js/app.js
@@ -1,11 +1,12 @@
-/*
+/******************************************************************************
  * Vipra Application
  * Main application file
- */
+ ******************************************************************************/
 (function() {
 
   var app = angular.module('vipra.app', [
     'ngResource',
+    'ngSanitize',
     'ui.router',
     'vipra.controllers',
     'vipra.directives',
@@ -42,12 +43,24 @@
       controller: 'ArticlesIndexController'
     });
 
-    $stateProvider.state('articles.show', {
+    $stateProvider.state('articles.detail', {
       url: '/:id',
-      templateUrl: tplBase + '/articles/show.html',
+      abstract: true,
+      template: '<ui-view/>'
+    });
+
+    $stateProvider.state('articles.detail.show', {
+      url: '',
+      templateUrl: tplBase + '/articles/detail/show.html',
       controller: 'ArticlesShowController'
     });
 
+    $stateProvider.state('articles.detail.network', {
+      url: '/network',
+      templateUrl: tplBase + '/articles/detail/network.html',
+      controller: 'ArticlesNetworkController'
+    });
+
     // states: topics
 
     $stateProvider.state('topics', {
diff --git a/vipra-ui/app/js/controllers.js b/vipra-ui/app/js/controllers.js
index e92e1afe610cf70eeb8e4945dcf77bd5a8b115bb..8fb6620453bc6f76036551fff831a172f13ed35a 100644
--- a/vipra-ui/app/js/controllers.js
+++ b/vipra-ui/app/js/controllers.js
@@ -1,7 +1,7 @@
-/*
+/******************************************************************************
  * Vipra Application
  * Controllers
- */
+ ******************************************************************************/
 (function() {
 
   var app = angular.module('vipra.controllers', [
@@ -49,7 +49,7 @@
   }]);
 
   /*
-   * ARTICLES
+   * Article Controllers
    */
 
   app.controller('ArticlesIndexController', ['$scope', '$stateParams', 'ArticleFactory',
@@ -83,8 +83,134 @@
 
   }]);
 
+  app.controller('ArticlesNetworkController', ['$scope', '$state', '$stateParams', 'ArticleFactory', 'TopicFactory',
+    function($scope, $state, $stateParams, ArticleFactory, TopicFactory) {
+
+    ArticleFactory.get({id: $stateParams.id}, function(response) {
+      $scope.article = response.data;
+
+      $scope.nodes = new vis.DataSet();
+      $scope.edges = new vis.DataSet();
+      $scope.data = {
+        nodes: $scope.nodes,
+        edges: $scope.edges
+      };
+      $scope.options = {
+        nodes: {
+          font: {
+            size: 11
+          },
+          scaling: {
+            label: true
+          },
+          shadow: true
+        },
+        layout: {
+          randomSeed: 1
+        },
+        physics: {
+          barnesHut: {
+            centralGravity: 0.1,
+            springLength: 200,
+            springConstant: 0.01,
+            avoidOverlap: 0.1
+          }
+        }
+      };
+
+      // initial network
+      var id = 0,
+          ids = {},
+          nodes = [],
+          edges = [],
+          topics = $scope.article.topics,
+          articleColor = '#BBC9D2',
+          topicColor = '#DBB234';
+      
+      // root node
+      nodes.push({id: ++id, title: $scope.article.title, label: $scope.article.title.ellipsize(20), type: 'article', article: $scope.article.id, loaded: true, color: {background: articleColor}});
+      ids[$scope.article.id] = id;
+      
+      // child nodes
+      for(var i = 0; i < topics.length; i++) {
+        var topic = topics[i].topic;
+        nodes.push({id: ++id, label: topic.name, type: 'topic', topic: topic.id, color: {background: topicColor}});
+        edges.push({from: 1, to: id});
+        ids[topic.id] = id;
+      }
+
+      $scope.nodes.add(nodes);
+      $scope.edges.add(edges);
+      
+      // on node select
+      $scope.select = function(props) {
+        var node = $scope.nodes.get(props.nodes[0]);
+        if(node && !node.loaded) {
+          if(node.type === 'topic') {
+            // node is topic, load topic to get articles
+            TopicFactory.get({id:node.topic}, function(res) {
+              if(res.data && res.data.articles) {
+                var articles = res.data.articles;
+                var newNodes = [];
+                var newEdges = [];
+                for(var i = 0; i < articles.length; i++) {
+                  var article = articles[i];
+                  if(!ids.hasOwnProperty(article.id)) {
+                    newNodes.push({id: ++id, title: article.title, label: article.title.ellipsize(20), type: 'article', article: article.id, color: {background: articleColor}});
+                    newEdges.push({from:node.id, to:id});
+                    ids[article.id] = id;
+                  }
+                }
+                if(newNodes.length)
+                  $scope.nodes.add(newNodes);
+                if(newEdges.length)
+                  $scope.edges.add(newEdges);
+              }
+            });
+          } else if(node.type === 'article') {
+            // node is article, load article to get topics
+            ArticleFactory.get({id:node.article}, function(res) {
+              if(res.data && res.data.topics) {
+                var topics = res.data.topics;
+                var newNodes = [];
+                var newEdges = [];
+                for(var i = 0; i < topics.length; i++) {
+                  var topic = topics[i].topic;
+                  if(!ids.hasOwnProperty(topic.id)) {
+                    newNodes.push({id: ++id, title: topic.name, label: topic.name.ellipsize(20), type: 'topic', topic: topic.id, color: {background: topicColor}});
+                    newEdges.push({from:node.id, to:id});
+                    ids[topic.id] = id;
+                  }
+                }
+                if(newNodes.length)
+                  $scope.nodes.add(newNodes);
+                if(newEdges.length)
+                  $scope.edges.add(newEdges);
+              }
+            });
+          }
+          node.loaded = true;
+          $scope.nodes.update(node);
+        }
+      };
+
+      // on node open
+      $scope.open = function(props) {
+        var node = $scope.nodes.get(props.nodes[0]);
+        if(node) {
+          if(node.type === 'article') {
+            $state.transitionTo('articles.detail.show', {id:node.article});
+          } else if(node.type === 'topic') {
+            $state.transitionTo('topics.show', {id:node.topic});
+          }
+        }
+      };
+    });
+
+  }]);
+
   /*
-   * TOPICS
+   * Topic Controllers
    */
 
   app.controller('TopicsIndexController', ['$scope', 'TopicFactory',
@@ -112,7 +238,7 @@
   }]);
 
   /*
-   * WORDS
+   * Word Controllers
    */
 
   app.controller('WordsIndexController', ['$scope', 'WordFactory',
@@ -139,7 +265,7 @@
   }]);
 
   /*
-   * DIRECTIVES
+   * Directive Controllers
    */
 
    app.controller('PaginationController', ['$scope',
diff --git a/vipra-ui/app/js/directives.js b/vipra-ui/app/js/directives.js
index f37e49881e07ca87873cdada2a9004faa51ff42e..519f1d347c19d9caea1086b2ad96bd82304f6847 100644
--- a/vipra-ui/app/js/directives.js
+++ b/vipra-ui/app/js/directives.js
@@ -1,7 +1,7 @@
-/*
+/******************************************************************************
  * Vipra Application
  * Directives
- */
+ ******************************************************************************/
 (function() {
 
   var app = angular.module('vipra.directives', [
@@ -30,7 +30,7 @@
       restrict: 'E',
       replace: true,
       transclude: true,
-      template: '<a class="article-link" ui-sref="articles.show({id:article.id})"><span ng-bind="article.title"></span><ng-transclude/></a>'
+      template: '<a class="article-link" ui-sref="articles.detail.show({id:article.id})"><span ng-bind="article.title"></span><ng-transclude/></a>'
     }
   });
 
@@ -56,4 +56,23 @@
     };
   });
 
+  app.directive('visGraph', function() {
+    return {
+      scope: {
+        visGraph: '=',
+        visData: '=',
+        visOptions: '=',
+        visSelect: '&',
+        visDblclick: '&'
+      },
+      link: function($scope, $element) {
+        $scope.$watchGroup(['visData', 'visOptions'], function() {
+          $scope.visGraph = new vis.Network($element[0], $scope.visData, $scope.visOptions);
+          $scope.visGraph.on('selectNode', $scope.visSelect() || function() {});
+          $scope.visGraph.on('doubleClick', $scope.visDblclick() || function() {});
+        });
+      }
+    };
+  });
+
 })();
\ No newline at end of file
diff --git a/vipra-ui/app/js/factories.js b/vipra-ui/app/js/factories.js
index b056ecc2c96b13e8c0ba3e3e5730b18cec350085..45284805d380db3e8573aadfb9bf07640d2d6c49 100644
--- a/vipra-ui/app/js/factories.js
+++ b/vipra-ui/app/js/factories.js
@@ -1,7 +1,7 @@
-/*
+/******************************************************************************
  * Vipra Application
  * Factories
- */
+ ******************************************************************************/
 (function() {
 
   var app = angular.module('vipra.factories', []);
diff --git a/vipra-ui/app/js/filters.js b/vipra-ui/app/js/filters.js
index 2be8bfc07f40c33666e2d8d6ad3bd69bd3b02759..ae98ac22d553e57fa2b1f32afc2cfe40ddbd84ed 100644
--- a/vipra-ui/app/js/filters.js
+++ b/vipra-ui/app/js/filters.js
@@ -1,7 +1,7 @@
-/*
+/******************************************************************************
  * Vipra Application
  * Filters
- */
+ ******************************************************************************/
 (function() {
 
   var app = angular.module('vipra.filters', []);
diff --git a/vipra-ui/app/js/helpers.js b/vipra-ui/app/js/helpers.js
index 48126a269a9dbd79815d8f523ac660d6f5e40f44..cf43be3f05b0a0a0358d1f86f828f1f207938160 100644
--- a/vipra-ui/app/js/helpers.js
+++ b/vipra-ui/app/js/helpers.js
@@ -1,7 +1,7 @@
-/*
+/******************************************************************************
  * Vipra Application
  * Helpers
- */
+ ******************************************************************************/
 (function() {
 
   window.formatDate = function(date) {
@@ -19,4 +19,11 @@
     return Math.round(input * 100);
   };
 
+  String.prototype.ellipsize = function(max) {
+    if(this.length > max) {
+      return this.substring(0, max) + '...';
+    }
+    return this;
+  };
+
 })();
\ No newline at end of file
diff --git a/vipra-ui/app/js/services.js b/vipra-ui/app/js/services.js
index 1fba5a4cc0a7ee15de94b109aac433a14f5e8fb3..5f8e95703ea98dc8ae766127dd7a21420231b528 100644
--- a/vipra-ui/app/js/services.js
+++ b/vipra-ui/app/js/services.js
@@ -1,7 +1,7 @@
-/*
+/******************************************************************************
  * Vipra Application
  * Services
- */
+ ******************************************************************************/
 (function() {
 
   var app = angular.module('vipra.services', []);
diff --git a/vipra-ui/app/less/app.less b/vipra-ui/app/less/app.less
index 537ff232c056c30fa94ca042ca28010f6b8d7442..0391c33412f22484b8026fafe0a0f6dc685f428a 100644
--- a/vipra-ui/app/less/app.less
+++ b/vipra-ui/app/less/app.less
@@ -99,6 +99,14 @@ body {
   }
 }
 
+.graph {
+  position: absolute;
+  top: 50px;
+  left: 0;
+  right: 0;
+  bottom: 50px;
+}
+
 .noselect {
   -webkit-touch-callout: none;
   -webkit-user-select: none;
diff --git a/vipra-ui/bower.json b/vipra-ui/bower.json
index 6104b60a698abadab8bf78da635179af4ee58d4a..ade95723df59097006fdbde8f1c96f560d36d234 100644
--- a/vipra-ui/bower.json
+++ b/vipra-ui/bower.json
@@ -19,8 +19,10 @@
   "dependencies": {
     "bootstrap": "~3.3.6",
     "jquery": "^2.2.0",
-    "angular": "^1.4.9",
-    "angular-resource": "^1.4.9",
-    "angular-ui-router": "^0.2.17"
+    "angular": "^1.5.0",
+    "angular-resource": "^1.5.0",
+    "angular-ui-router": "^0.2.17",
+    "angular-sanitize": "^1.5.0",
+    "highcharts": "^4.2.2"
   }
 }
diff --git a/vipra-ui/gulpfile.js b/vipra-ui/gulpfile.js
index 20da5abcda1dd2966ca625304963f444ad1ecd22..87ee3e2591413a6ac17a0650df08a19c643f9848 100644
--- a/vipra-ui/gulpfile.js
+++ b/vipra-ui/gulpfile.js
@@ -2,6 +2,7 @@ var gulp = require('gulp'),
     less = require('gulp-less'),
     concat = require('gulp-concat'),
     uglify = require('gulp-uglify'),
+    plumber = require('gulp-plumber'),
     cssnano = require('gulp-cssnano'),
     webserver = require('gulp-webserver');
 
@@ -10,11 +11,15 @@ var assets = {
     'bower_components/jquery/dist/jquery.min.js',
     'bower_components/angular/angular.min.js',
     'bower_components/angular-resource/angular-resource.min.js',
+    'bower_components/angular-sanitize/angular-sanitize.min.js',
     'bower_components/angular-ui-router/release/angular-ui-router.min.js',
-    'bower_components/bootstrap/dist/js/bootstrap.min.js'
+    'bower_components/bootstrap/dist/js/bootstrap.min.js',
+    'bower_components/highcharts/highcharts.js',
+    'bower_components/vis/dist/vis.min.js'
   ],
   css: [
-    'bower_components/bootstrap/dist/css/bootstrap.min.css'
+    'bower_components/bootstrap/dist/css/bootstrap.min.css',
+    'bower_components/vis/dist/vis.min.css'
   ],
   fonts: [
     'bower_components/bootstrap/dist/fonts/*'
@@ -24,12 +29,14 @@ var assets = {
 
 gulp.task('less', function() {
   gulp.src('app/less/**/*.less')
+      .pipe(plumber())
       .pipe(less())
       .pipe(gulp.dest('public/css'));
 });
 
 gulp.task('js', function() {
   gulp.src('app/js/**/*.js')
+      .pipe(plumber())
       .pipe(concat('app.js'))
       .pipe(gulp.dest('public/js'));
 });
@@ -54,11 +61,9 @@ gulp.task('public', function() {
 gulp.task('assets', function() {
   gulp.src(assets.js)
       .pipe(concat('vendor.js'))
-      .pipe(uglify())
       .pipe(gulp.dest('public/js'));
   gulp.src(assets.css)
       .pipe(concat('vendor.css'))
-      .pipe(cssnano())
       .pipe(gulp.dest('public/css'));
   gulp.src(assets.fonts)
       .pipe(gulp.dest('public/fonts'));
diff --git a/vipra-ui/package.json b/vipra-ui/package.json
index bc23c8b4d62be957580b6e7a5dfaf1625b1739ff..5be5d8d240dd5417d10174aa7cac157778319a31 100644
--- a/vipra-ui/package.json
+++ b/vipra-ui/package.json
@@ -10,6 +10,7 @@
     "gulp-cssnano": "^2.1.0",
     "gulp-include": "^2.1.0",
     "gulp-less": "^3.0.5",
+    "gulp-plumber": "^1.0.1",
     "gulp-sourcemaps": "^1.6.0",
     "gulp-uglify": "^1.5.1",
     "gulp-webserver": "^0.9.1"
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 4b3a78deff351ce1b7d9cdddf3587c6835324e8f..b03c69994b630fb669fff1ff2f8eddaca5c8ce52 100644
--- a/vipra-util/src/main/java/de/vipra/util/Config.java
+++ b/vipra-util/src/main/java/de/vipra/util/Config.java
@@ -233,6 +233,12 @@ public class Config {
 		pw.flush();
 	}
 
+	public String hash() {
+		String config = databaseHost + databasePort + databaseName + processor + analyzer + windowResolution
+				+ saveAllWords;
+		return DigestUtils.md5(config);
+	}
+
 	public static File getGenericDataDir() {
 		File base = PathUtils.appDataDir();
 		return new File(base, Constants.FB_DIR);
diff --git a/vipra-util/src/main/java/de/vipra/util/DigestUtils.java b/vipra-util/src/main/java/de/vipra/util/DigestUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..3fa20e86387c1fcedc655cb972c3e0712ce4351f
--- /dev/null
+++ b/vipra-util/src/main/java/de/vipra/util/DigestUtils.java
@@ -0,0 +1,26 @@
+package de.vipra.util;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import javax.xml.bind.annotation.adapters.HexBinaryAdapter;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DigestUtils {
+
+	public static final Logger log = LoggerFactory.getLogger(DigestUtils.class);
+
+	public static String md5(String in) {
+		MessageDigest md = null;
+		try {
+			md = MessageDigest.getInstance("MD5");
+			return (new HexBinaryAdapter()).marshal(md.digest(in.getBytes()));
+		} catch (NoSuchAlgorithmException e) {
+			log.error("md5 algorithm not available");
+			return null;
+		}
+	}
+
+}
diff --git a/vipra-util/src/main/java/de/vipra/util/WordMap.java b/vipra-util/src/main/java/de/vipra/util/WordMap.java
index 8d11d931316e97d6c3f4d0a0181047b7e9acfb6b..574f3ac0de3fd84f0f2e5fb0d35976b1689bce5b 100644
--- a/vipra-util/src/main/java/de/vipra/util/WordMap.java
+++ b/vipra-util/src/main/java/de/vipra/util/WordMap.java
@@ -2,9 +2,11 @@ package de.vipra.util;
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Set;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -19,13 +21,13 @@ public class WordMap {
 
 	private final DatabaseService<Word, String> dbWords;
 	private final Map<String, Word> wordMap;
-	private final List<Word> newWords;
+	private final Set<Word> newWords;
 	private boolean createNow = false;
 
 	public WordMap(DatabaseService<Word, String> dbWords) {
 		this.dbWords = dbWords;
 		this.wordMap = new HashMap<>();
-		this.newWords = new ArrayList<>();
+		this.newWords = new HashSet<>();
 		List<Word> words = dbWords.getAll();
 		for (Word word : words)
 			wordMap.put(word.getWord().toLowerCase(), word);
@@ -76,7 +78,7 @@ public class WordMap {
 		this.createNow = createNow;
 	}
 
-	public List<Word> getNewWords() {
+	public Set<Word> getNewWords() {
 		return newWords;
 	}
 
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 fc65f56b500b6d1229d99a4af88d036a44706a63..36a13c525e6ffa5fee8fda9036920d2db05e89d0 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
@@ -41,4 +41,23 @@ public class Article implements Model<ObjectId>, Serializable {
 		this.title = title;
 	}
 
+	@Override
+	public boolean equals(Object o) {
+		if (o == null)
+			return false;
+		if (!(o instanceof Article))
+			return false;
+		Article a = (Article) o;
+		if (id == null)
+			return a.getId() == null;
+		return id.equals(a.getId());
+	}
+
+	@Override
+	public int hashCode() {
+		if (id == null)
+			return super.hashCode();
+		return id.hashCode();
+	}
+
 }
diff --git a/vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java b/vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java
index d2effd84b4e28315107e7e23a7546aacea6c60ed..4380c73381077fca6bee3b2e643552bc71ff2d8b 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/ArticleFull.java
@@ -238,4 +238,23 @@ public class ArticleFull extends FileModel<ObjectId> implements Serializable {
 				+ ", created:" + created + ", modified:" + modified + "]";
 	}
 
+	@Override
+	public boolean equals(Object o) {
+		if (o == null)
+			return false;
+		if (!(o instanceof ArticleFull))
+			return false;
+		ArticleFull a = (ArticleFull) o;
+		if (id == null)
+			return a.getId() == null;
+		return id.equals(a.getId());
+	}
+
+	@Override
+	public int hashCode() {
+		if (id == null)
+			return super.hashCode();
+		return id.hashCode();
+	}
+
 }
\ No newline at end of file
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 760510f2032673fe697efb38b4d712f9d7c8bf3d..133be4d4b0ba5d5d4ec3e70f328474a9049772fd 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
@@ -52,4 +52,23 @@ public class Topic implements Model<ObjectId>, Serializable {
 		this.name = name;
 	}
 
+	@Override
+	public boolean equals(Object o) {
+		if (o == null)
+			return false;
+		if (!(o instanceof Topic))
+			return false;
+		Topic a = (Topic) o;
+		if (id == null)
+			return a.getId() == null;
+		return id.equals(a.getId());
+	}
+
+	@Override
+	public int hashCode() {
+		if (id == null)
+			return super.hashCode();
+		return id.hashCode();
+	}
+
 }
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
index f004d83788a70049a290eb3049588d1d8346d6c0..99267ff4caaea60305efaf4b1289605be326ad04 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/TopicFull.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/TopicFull.java
@@ -127,4 +127,23 @@ public class TopicFull implements Model<ObjectId>, Serializable {
 		return name;
 	}
 
+	@Override
+	public boolean equals(Object o) {
+		if (o == null)
+			return false;
+		if (!(o instanceof TopicFull))
+			return false;
+		TopicFull a = (TopicFull) o;
+		if (id == null)
+			return a.getId() == null;
+		return id.equals(a.getId());
+	}
+
+	@Override
+	public int hashCode() {
+		if (id == null)
+			return super.hashCode();
+		return id.hashCode();
+	}
+
 }
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
index 7b5c4ed688ebfd12b1facebe2d666648cdfed9c6..268a8dc2b899e10b7ea842fd3bac22892ae12809 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/Word.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/Word.java
@@ -114,4 +114,23 @@ public class Word implements Model<String>, Serializable {
 			this.created = new Date();
 	}
 
+	@Override
+	public boolean equals(Object o) {
+		if (o == null)
+			return false;
+		if (!(o instanceof Word))
+			return false;
+		Word w = (Word) o;
+		if (id == null)
+			return w.getId() == null;
+		return this.id.equals(w.getId());
+	}
+
+	@Override
+	public int hashCode() {
+		if (id == null)
+			return super.hashCode();
+		return id.hashCode();
+	}
+
 }