From 03445c904d0f7ecc7965c91e7aa664b97cf413c9 Mon Sep 17 00:00:00 2001
From: Eike Cochu <eike@cochu.com>
Date: Sun, 3 Apr 2016 16:17:24 +0200
Subject: [PATCH] updated

---
 .../de/vipra/rest/resource/WordResource.java  | 104 ++++++++++++++++++
 .../main/java/de/vipra/cmd/file/Filebase.java |  12 ++
 .../de/vipra/cmd/file/FilebaseWordIndex.java  |  20 +++-
 .../main/java/de/vipra/cmd/lda/Analyzer.java  |   4 +-
 .../vipra/cmd/option/DeleteModelCommand.java  |  23 ++++
 .../de/vipra/cmd/option/ImportCommand.java    |  17 +++
 vipra-ui/app/html/directives/word-link.html   |   2 +-
 vipra-ui/app/html/directives/word-menu.html   |   4 +-
 vipra-ui/app/html/topics/show.html            |   7 +-
 vipra-ui/app/html/words/articles.html         |   2 +-
 vipra-ui/app/html/words/index.html            |  42 +++++++
 vipra-ui/app/html/words/show.html             |   4 +
 vipra-ui/app/html/words/topics.html           |   2 +-
 vipra-ui/app/index.html                       |   3 +
 vipra-ui/app/js/app.js                        |  16 ++-
 vipra-ui/app/js/controllers.js                |  63 +++++++++--
 vipra-ui/app/js/factories.js                  |   4 +
 .../java/de/vipra/util/model/ArticleFull.java |   2 +-
 .../java/de/vipra/util/model/ArticleWord.java |  16 +--
 .../de/vipra/util/model/SequenceWord.java     |  22 ++--
 .../java/de/vipra/util/model/TopicFull.java   |   4 +-
 .../de/vipra/util/model/TopicModelConfig.java |   2 +-
 .../de/vipra/util/model/TopicModelFull.java   |   2 +-
 .../java/de/vipra/util/model/TopicWord.java   |  14 +--
 .../java/de/vipra/util/model/WordFull.java    |  51 +++++++++
 25 files changed, 382 insertions(+), 60 deletions(-)
 create mode 100644 vipra-backend/src/main/java/de/vipra/rest/resource/WordResource.java
 create mode 100644 vipra-ui/app/html/words/index.html
 create mode 100644 vipra-ui/app/html/words/show.html
 create mode 100644 vipra-util/src/main/java/de/vipra/util/model/WordFull.java

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
new file mode 100644
index 00000000..646c64fd
--- /dev/null
+++ b/vipra-backend/src/main/java/de/vipra/rest/resource/WordResource.java
@@ -0,0 +1,104 @@
+package de.vipra.rest.resource;
+
+import java.io.IOException;
+import java.util.List;
+
+import javax.servlet.ServletContext;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+
+import de.vipra.rest.Messages;
+import de.vipra.rest.model.APIError;
+import de.vipra.rest.model.ResponseWrapper;
+import de.vipra.util.Config;
+import de.vipra.util.StringUtils;
+import de.vipra.util.ex.ConfigException;
+import de.vipra.util.model.TopicModel;
+import de.vipra.util.model.WordFull;
+import de.vipra.util.service.MongoService;
+import de.vipra.util.service.Service.QueryBuilder;
+
+@Path("words")
+public class WordResource {
+
+	@Context
+	UriInfo uri;
+
+	final MongoService<WordFull, String> dbWords;
+
+	public WordResource(@Context final ServletContext servletContext) throws ConfigException, IOException {
+		final Config config = Config.getConfig();
+		dbWords = MongoService.getDatabaseService(config, WordFull.class);
+	}
+
+	@GET
+	@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) {
+		final ResponseWrapper<List<WordFull>> res = new ResponseWrapper<>();
+
+		if (res.hasErrors())
+			return res.badRequest();
+
+		try {
+			final QueryBuilder query = QueryBuilder.builder().skip(skip).limit(limit).sortBy(sortBy);
+			if (fields != null && !fields.isEmpty())
+				query.fields(true, StringUtils.getFields(fields));
+
+			if (topicModel != null && !topicModel.isEmpty())
+				query.criteria("topicModel", new TopicModel(topicModel));
+
+			final List<WordFull> words = dbWords.getMultiple(query);
+
+			if ((skip != null && skip > 0) || (limit != null && limit > 0))
+				res.addHeader("total", dbWords.count(null));
+			else
+				res.addHeader("total", words.size());
+
+			return res.ok(words);
+		} catch (final Exception e) {
+			e.printStackTrace();
+			res.addError(new APIError(Response.Status.BAD_REQUEST, "Error", e.getMessage()));
+			return res.badRequest();
+		}
+	}
+
+	@GET
+	@Produces(MediaType.APPLICATION_JSON)
+	@Consumes(MediaType.APPLICATION_JSON)
+	@Path("{id}")
+	public Response getWord(@PathParam("id") final String id, @QueryParam("fields") final String fields) {
+		final ResponseWrapper<WordFull> res = new ResponseWrapper<>();
+		if (id == null || id.trim().length() == 0) {
+			res.addError(new APIError(Response.Status.BAD_REQUEST, "ID is empty", String.format(Messages.BAD_REQUEST, "id cannot be empty")));
+			return res.badRequest();
+		}
+
+		WordFull word;
+		try {
+			word = dbWords.getSingle(id, StringUtils.getFields(fields));
+		} catch (final Exception e) {
+			e.printStackTrace();
+			res.addError(new APIError(Response.Status.BAD_REQUEST, "Error", e.getMessage()));
+			return res.badRequest();
+		}
+
+		if (word != null) {
+			return res.ok(word);
+		} else {
+			res.addError(new APIError(Response.Status.NOT_FOUND, "Resource not found", String.format(Messages.NOT_FOUND, "word", id)));
+			return res.notFound();
+		}
+	}
+
+}
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/file/Filebase.java b/vipra-cmd/src/main/java/de/vipra/cmd/file/Filebase.java
index 09b461eb..09141da0 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/file/Filebase.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/file/Filebase.java
@@ -97,4 +97,16 @@ public class Filebase {
 		windowIndex.sync();
 	}
 
+	public FilebaseIDDateIndex getIdDateIndex() {
+		return idDateIndex;
+	}
+
+	public FilebaseWordIndex getWordIndex() {
+		return wordIndex;
+	}
+
+	public FilebaseWindowIndex getWindowIndex() {
+		return windowIndex;
+	}
+
 }
diff --git a/vipra-cmd/src/main/java/de/vipra/cmd/file/FilebaseWordIndex.java b/vipra-cmd/src/main/java/de/vipra/cmd/file/FilebaseWordIndex.java
index fc1cb496..c939f635 100644
--- a/vipra-cmd/src/main/java/de/vipra/cmd/file/FilebaseWordIndex.java
+++ b/vipra-cmd/src/main/java/de/vipra/cmd/file/FilebaseWordIndex.java
@@ -7,10 +7,12 @@ import java.io.IOException;
 import java.io.OutputStreamWriter;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Set;
 
 import de.vipra.util.Constants;
 import de.vipra.util.CountMap;
@@ -26,10 +28,12 @@ public class FilebaseWordIndex implements Iterable<String> {
 	private final List<String> words;
 	private final Map<String, Integer> wordIndex;
 	private final CountMap<String> wordDocumentCount;
+	private final Set<String> newWords;
 	private int nextIndex = 0;
 
 	public FilebaseWordIndex(final File modelDir) throws IOException {
 		file = new File(modelDir, FILE_NAME);
+		newWords = new HashSet<>();
 		if (file.exists()) {
 			final List<String> lines = FileUtils.readFile(file);
 			words = new ArrayList<>(lines.size());
@@ -51,8 +55,8 @@ public class FilebaseWordIndex implements Iterable<String> {
 	public void sync() throws IOException {
 		if (!dirty)
 			return;
-		BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, false)));
-		for (String word : words) {
+		final BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, false)));
+		for (final String word : words) {
 			out.write(word);
 			out.write(",");
 			out.write(Integer.toString(wordDocumentCount.get(word)));
@@ -63,14 +67,16 @@ public class FilebaseWordIndex implements Iterable<String> {
 	}
 
 	public void countWords(final List<ArticleWord> articleWords) {
-		for (ArticleWord articleWord : articleWords)
-			wordDocumentCount.count(articleWord.getWord());
+		for (final ArticleWord articleWord : articleWords)
+			wordDocumentCount.count(articleWord.getId());
 	}
 
 	public String transform(final String[] words) {
 		final CountMap<String> countMap = new CountMap<>();
-		for (final String word : words)
+		for (final String word : words) {
 			countMap.count(word);
+			newWords.add(word);
+		}
 
 		final StringBuilder sb = new StringBuilder();
 		sb.append(countMap.size());
@@ -107,6 +113,10 @@ public class FilebaseWordIndex implements Iterable<String> {
 		return words.size();
 	}
 
+	public Set<String> getNewWords() {
+		return newWords;
+	}
+
 	@Override
 	public Iterator<String> iterator() {
 		return words.iterator();
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 57775a17..1882c49e 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
@@ -300,11 +300,11 @@ public class Analyzer {
 			final List<TopicWord> newTopicWords = new ArrayList<>(modelConfig.getkTopWords());
 			for (final SequenceWord sequenceWord : topTopicWordsList) {
 				final TopicWord newTopicWord = new TopicWord();
-				newTopicWord.setWord(sequenceWord.getWord());
+				newTopicWord.setId(sequenceWord.getId());
 				final List<Double> sequenceProbabilities = new ArrayList<>(windowCount);
 				final List<Double> sequenceProbabilitiesChange = new ArrayList<>(windowCount);
 				double prevProbability = 0;
-				for (final double probability : probabilities[wordIndex.index(sequenceWord.getWord())]) {
+				for (final double probability : probabilities[wordIndex.index(sequenceWord.getId())]) {
 					sequenceProbabilities.add(probability);
 					sequenceProbabilitiesChange.add(probability - prevProbability);
 					prevProbability = probability;
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 8c771a0a..af9f91b3 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
@@ -3,10 +3,18 @@ package de.vipra.cmd.option;
 import java.io.File;
 import java.util.Arrays;
 
+import org.bson.types.ObjectId;
+
 import de.vipra.util.Config;
 import de.vipra.util.ConsoleUtils;
+import de.vipra.util.model.ArticleFull;
+import de.vipra.util.model.SequenceFull;
+import de.vipra.util.model.TopicFull;
 import de.vipra.util.model.TopicModel;
+import de.vipra.util.model.WindowFull;
+import de.vipra.util.model.WordFull;
 import de.vipra.util.service.MongoService;
+import de.vipra.util.service.Service.QueryBuilder;
 
 public class DeleteModelCommand implements Command {
 
@@ -20,6 +28,11 @@ public class DeleteModelCommand implements Command {
 	public void run() throws Exception {
 		final Config config = Config.getConfig();
 		final MongoService<TopicModel, String> dbTopicModels = MongoService.getDatabaseService(config, TopicModel.class);
+		final MongoService<ArticleFull, ObjectId> dbArticles = MongoService.getDatabaseService(config, ArticleFull.class);
+		final MongoService<TopicFull, ObjectId> dbTopics = MongoService.getDatabaseService(config, TopicFull.class);
+		final MongoService<WindowFull, Integer> dbWindows = MongoService.getDatabaseService(config, WindowFull.class);
+		final MongoService<SequenceFull, ObjectId> dbSequences = MongoService.getDatabaseService(config, SequenceFull.class);
+		final MongoService<WordFull, String> dbWords = MongoService.getDatabaseService(config, WordFull.class);
 
 		for (final String name : names) {
 			final File modelDir = new File(config.getDataDirectory(), name);
@@ -28,7 +41,17 @@ public class DeleteModelCommand implements Command {
 				ConsoleUtils.info("model deleted: " + name);
 			}
 		}
+
 		dbTopicModels.deleteMultiple(Arrays.asList(names));
+
+		for (final String name : names) {
+			final QueryBuilder builder = QueryBuilder.builder().criteria("topicModel", new TopicModel(name));
+			dbArticles.deleteMultiple(builder);
+			dbTopics.deleteMultiple(builder);
+			dbWindows.deleteMultiple(builder);
+			dbSequences.deleteMultiple(builder);
+			dbWords.deleteMultiple(builder);
+		}
 	}
 
 }
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 17a18c6c..c6220dcc 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
@@ -17,6 +17,7 @@ import org.json.simple.parser.ParseException;
 
 import de.vipra.cmd.file.Filebase;
 import de.vipra.cmd.file.FilebaseException;
+import de.vipra.cmd.file.FilebaseWordIndex;
 import de.vipra.cmd.text.ProcessedText;
 import de.vipra.cmd.text.Processor;
 import de.vipra.cmd.text.ProcessorException;
@@ -37,6 +38,7 @@ import de.vipra.util.model.TextEntity;
 import de.vipra.util.model.TopicModel;
 import de.vipra.util.model.TopicModelConfig;
 import de.vipra.util.model.TopicModelFull;
+import de.vipra.util.model.WordFull;
 import de.vipra.util.service.MongoService;
 
 public class ImportCommand implements Command {
@@ -71,6 +73,7 @@ public class ImportCommand implements Command {
 	private Config config;
 	private MongoService<ArticleFull, ObjectId> dbArticles;
 	private MongoService<TopicModelFull, String> dbTopicModels;
+	private MongoService<WordFull, String> dbWords;
 	private TopicModelConfig modelConfig;
 	private SpotlightAnalyzer spotlightAnalyzer;
 	private Filebase filebase;
@@ -260,6 +263,19 @@ public class ImportCommand implements Command {
 		 */
 		filebase.sync();
 
+		/*
+		 * add new words
+		 */
+		final FilebaseWordIndex wordIndex = filebase.getWordIndex();
+		final List<WordFull> newWords = new ArrayList<>(wordIndex.getNewWords().size());
+		final TopicModel topicModelRef = new TopicModel(topicModel.getId());
+		for (final String word : wordIndex.getNewWords()) {
+			final WordFull newWord = new WordFull(word);
+			newWord.setTopicModel(topicModelRef);
+			newWords.add(newWord);
+		}
+		dbWords.createMultiple(newWords);
+
 		/*
 		 * run information
 		 */
@@ -271,6 +287,7 @@ public class ImportCommand implements Command {
 		config = Config.getConfig();
 		dbArticles = MongoService.getDatabaseService(config, ArticleFull.class);
 		dbTopicModels = MongoService.getDatabaseService(config, TopicModelFull.class);
+		dbWords = MongoService.getDatabaseService(config, WordFull.class);
 		processor = new Processor();
 		for (final TopicModelConfig modelConfig : config.getTopicModelConfigs(models)) {
 			importForModel(modelConfig);
diff --git a/vipra-ui/app/html/directives/word-link.html b/vipra-ui/app/html/directives/word-link.html
index 0c2dff7c..73cc32b8 100644
--- a/vipra-ui/app/html/directives/word-link.html
+++ b/vipra-ui/app/html/directives/word-link.html
@@ -1,4 +1,4 @@
 <span>
   <word-menu word="word" />
-  <span ng-bind="word.word"></span>
+  <a ui-sref="words.show({id: word.id})" ng-bind="word.id"></a>
 </span>
\ No newline at end of file
diff --git a/vipra-ui/app/html/directives/word-menu.html b/vipra-ui/app/html/directives/word-menu.html
index fe6a7ef3..103531b1 100644
--- a/vipra-ui/app/html/directives/word-menu.html
+++ b/vipra-ui/app/html/directives/word-menu.html
@@ -3,7 +3,7 @@
     <i class="fa fa-caret-down"></i>
   </a>
   <ul class="dropdown-menu" ng-class="{'dropdown-menu-right':dropdownRight}">
-    <li><a ui-sref="words.topics({word:word.word})">Topics</a></li>
-    <li><a ui-sref="words.articles({word:word.word})">Articles</a></li>
+    <li><a ui-sref="words.show.topics({id:word.id})">Topics</a></li>
+    <li><a ui-sref="words.show.articles({id:word.id})">Articles</a></li>
   </ul>
 </div>
\ No newline at end of file
diff --git a/vipra-ui/app/html/topics/show.html b/vipra-ui/app/html/topics/show.html
index 387f7bda..5c745179 100644
--- a/vipra-ui/app/html/topics/show.html
+++ b/vipra-ui/app/html/topics/show.html
@@ -90,8 +90,8 @@
                 <ul class="list-unstyled">
                   <li ng-repeat="word in topic.words">
                     <div class="checkbox checkbox-condensed" ng-class="{selected:word.selected}">
-                      <input tabindex="0" type="checkbox" ng-model="word.selected" ng-attr-id="{{::word.word}}" ng-change="redrawWordEvolutionChart()">
-                      <label class="check" ng-attr-for="{{::word.word}}">
+                      <input tabindex="0" type="checkbox" ng-model="word.selected" ng-attr-id="{{::word.id}}" ng-change="redrawWordEvolutionChart()">
+                      <label class="check" ng-attr-for="{{::word.id}}">
                         <word-link word="word" />
                       </label>
                     </div>
@@ -123,8 +123,7 @@
             <tbody>
               <tr ng-repeat="word in sequence.words | orderBy:topicsShowModels.seqSortWords">
                 <td>
-                  <word-menu word="word"/>
-                  <span ng-bind="::word.word"></span>
+                  <word-link word="word" />
                 </td>
                 <td ng-bind="word.probability.toFixed(4)"></td>
               </tr>
diff --git a/vipra-ui/app/html/words/articles.html b/vipra-ui/app/html/words/articles.html
index 03dce569..3674b93e 100644
--- a/vipra-ui/app/html/words/articles.html
+++ b/vipra-ui/app/html/words/articles.html
@@ -1,4 +1,4 @@
-<div class="container" ng-cloak ng-hide="!rootModels.topicModel || state.name !== 'words.articles'">
+<div class="container" ng-cloak ng-hide="!rootModels.topicModel || state.name !== 'words.show.articles'">
   <div class="row">
     <div class="col-md-12">
       <div class="page-header">
diff --git a/vipra-ui/app/html/words/index.html b/vipra-ui/app/html/words/index.html
new file mode 100644
index 00000000..16a03a7f
--- /dev/null
+++ b/vipra-ui/app/html/words/index.html
@@ -0,0 +1,42 @@
+<div class="container" ng-cloak ng-hide="!rootModels.topicModel || state.name !== 'words'">
+  <div class="row">
+    <div class="col-md-12 text-center">
+      <pagination total="wordsTotal" page="wordsIndexModels.page" limit="wordsIndexModels.limit" />
+    </div>
+  </div>
+  <div class="row">
+    <div class="col-md-12">
+      <div class="panel panel-default">
+        <div class="panel-heading">
+          Found
+          <ng-pluralize count="wordsTotal||0" when="{0:'no words',1:'1 word',other:'{} words'}"></ng-pluralize> in the database.
+          <span ng-show="wordsTotal">
+          Sort by
+          <ol class="nya-bs-select nya-bs-condensed" ng-model="wordsIndexModels.sortkey">
+            <li value="id" class="nya-bs-option"><a>Word</a></li>
+          </ol>
+          <sort-dir ng-model="wordsIndexModels.sortdir" />
+        </span>
+        </div>
+        <table class="table table-hover table-condensed">
+          <tbody>
+            <tr ng-repeat="word in words">
+              <td>
+              	<word-link word="word" />
+              </td>
+            </tr>
+          </tbody>
+        </table>
+        <div class="panel-footer">
+          Page <span ng-bind="wordsIndexModels.page||1"></span> of <span ng-bind="maxPage||1"></span>
+        </div>
+      </div>
+    </div>
+  </div>
+  <div class="row">
+    <div class="col-md-12 text-center">
+      <pagination total="wordsTotal" page="wordsIndexModels.page" limit="wordsIndexModels.limit" />
+    </div>
+  </div>
+</div>
+<div ng-cloak ui-view></div>
diff --git a/vipra-ui/app/html/words/show.html b/vipra-ui/app/html/words/show.html
new file mode 100644
index 00000000..515f6e9c
--- /dev/null
+++ b/vipra-ui/app/html/words/show.html
@@ -0,0 +1,4 @@
+<div class="container" ng-cloak ng-hide="!rootModels.topicModel || state.name !== 'words.show'">
+	<h1 ng-bind="word.id"></h1>
+</div>
+<div ng-cloak ui-view></div>
diff --git a/vipra-ui/app/html/words/topics.html b/vipra-ui/app/html/words/topics.html
index 46a20f18..f96c9696 100644
--- a/vipra-ui/app/html/words/topics.html
+++ b/vipra-ui/app/html/words/topics.html
@@ -1,4 +1,4 @@
-<div class="container" ng-cloak ng-hide="!rootModels.topicModel || state.name !== 'words.topics'">
+<div class="container" ng-cloak ng-hide="!rootModels.topicModel || state.name !== 'words.show.topics'">
   <div class="row">
     <div class="col-md-12">
       <div class="page-header">
diff --git a/vipra-ui/app/index.html b/vipra-ui/app/index.html
index fb7e6846..ddb769d7 100644
--- a/vipra-ui/app/index.html
+++ b/vipra-ui/app/index.html
@@ -64,6 +64,9 @@
           <li ui-sref-active="active">
             <a tabindex="0" ui-sref="topics"><span class="mnemonic">T</span>opics</a>
           </li>
+          <li ui-sref-active="active">
+            <a tabindex="0" ui-sref="words"><span class="mnemonic">W</span>ords</a>
+          </li>
         </ul>
         <form class="navbar-form navbar-left" role="search" ng-hide="state.name === 'index'">
           <div class="form-group has-feedback">
diff --git a/vipra-ui/app/js/app.js b/vipra-ui/app/js/app.js
index 94aefbb1..64b34a2d 100644
--- a/vipra-ui/app/js/app.js
+++ b/vipra-ui/app/js/app.js
@@ -91,18 +91,24 @@
       // states: words
 
       $stateProvider.state('words', {
-        abstract: true,
-        url: '/words/:word',
-        template: '<ui-view/>'
+        url: '/words?p',
+        templateUrl: 'html/words/index.html',
+        controller: 'WordsIndexController'
       });
 
-      $stateProvider.state('words.topics', {
+      $stateProvider.state('words.show', {
+        url: '/:id',
+        templateUrl: 'html/words/show.html',
+        controller: 'WordsShowController'
+      });
+
+      $stateProvider.state('words.show.topics', {
         url: '/topics',
         templateUrl: 'html/words/topics.html',
         controller: 'WordsTopicsController'
       });
 
-      $stateProvider.state('words.articles', {
+      $stateProvider.state('words.show.articles', {
         url: '/articles',
         templateUrl: 'html/words/articles.html',
         controller: 'WordsArticlesController'
diff --git a/vipra-ui/app/js/controllers.js b/vipra-ui/app/js/controllers.js
index d466d9af..92b9c03c 100644
--- a/vipra-ui/app/js/controllers.js
+++ b/vipra-ui/app/js/controllers.js
@@ -9,8 +9,8 @@
 
   var app = angular.module('vipra.controllers', []);
 
-  app.controller('RootController', ['$scope', '$state', '$location', '$window', 'hotkeys', 'TopicModelFactory',
-    function($scope, $state, $location, $window, hotkeys, TopicModelFactory) {
+  app.controller('RootController', ['$scope', '$state', '$window', 'hotkeys', 'TopicModelFactory',
+    function($scope, $state, $window, hotkeys, TopicModelFactory) {
 
       $scope.rootModels = {
         topicModel: null,
@@ -94,6 +94,15 @@
         }
       });
 
+      hotkeys.add({
+        combo: 'w',
+        description: 'Go to words',
+        callback: function() {
+          if ($scope.rootModels.topicModel && $state.current.name !== 'words')
+            $state.transitionTo('words');
+        }
+      });
+
       hotkeys.add({
         combo: 'm',
         description: 'Choose a topic model',
@@ -803,7 +812,7 @@
             probs.push([new Date($scope.topic.sequences[j].window.startDate).getTime(), prob]);
           }
           evolutions.push({
-            name: word.word,
+            name: word.id,
             data: probs
           });
         }
@@ -904,13 +913,13 @@
   /**
    * Topic Show Articles route
    */
-  app.controller('TopicsArticlesController', ['$scope', '$stateParams', '$location', 'TopicFactory',
-    function($scope, $stateParams, $location, TopicFactory) {
+  app.controller('TopicsArticlesController', ['$scope', '$stateParams', 'TopicFactory',
+    function($scope, $stateParams, TopicFactory) {
 
       $scope.topicsArticlesModels = {
         sortkey: 'title',
         sortdir: true,
-        page: Math.max($location.search().page || 1, 1),
+        page: 1,
         limit: 100
       };
 
@@ -934,10 +943,48 @@
    * Word Controllers
    ****************************************************************************/
 
+  app.controller('WordsIndexController', ['$scope', '$state', 'WordFactory',
+    function($scope, $state, WordFactory) {
+
+      // page was reloaded, choose topic model
+      if (!$scope.rootModels.topicModel && $state.current.name === 'words')
+        $scope.chooseTopicModel();
+
+      $scope.wordsIndexModels = {
+        sortkey: 'id',
+        sortdir: true,
+        page: 1,
+        limit: 100
+      };
+
+      $scope.$watchGroup(['wordsIndexModels.page', 'wordsIndexModels.sortkey', 'wordsIndexModels.sortdir', 'rootModels.topicModel'], 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
+        }, function(data, headers) {
+          $scope.words = data;
+          $scope.wordsTotal = headers("V-Total");
+          $scope.maxPage = Math.ceil($scope.wordsTotal / $scope.wordsIndexModels.limit);
+        });
+      });
+
+    }
+  ]);
+
+  app.controller('WordsShowController', ['$scope',
+    function($scope) {
+
+    }
+  ]);
+
   app.controller('WordsTopicsController', ['$scope', '$state', '$stateParams', 'TopicFactory',
     function($scope, $state, $stateParams, TopicFactory) {
 
-      $scope.word = $stateParams.word;
+      $scope.word = $stateParams.id;
 
       // page was reloaded, choose topic model
       if (!$scope.rootModels.topicModel && $state.current.name === 'words.topics')
@@ -972,7 +1019,7 @@
   app.controller('WordsArticlesController', ['$scope', '$state', '$stateParams', 'ArticleFactory',
     function($scope, $state, $stateParams, ArticleFactory) {
 
-      $scope.word = $stateParams.word;
+      $scope.word = $stateParams.id;
 
       // page was reloaded, choose topic model
       if (!$scope.rootModels.topicModel && $state.current.name === 'words.articles')
diff --git a/vipra-ui/app/js/factories.js b/vipra-ui/app/js/factories.js
index 31f9b64b..b331235d 100644
--- a/vipra-ui/app/js/factories.js
+++ b/vipra-ui/app/js/factories.js
@@ -72,4 +72,8 @@
     return $myResource(Vipra.config.restUrl + '/topicmodels/:id');
   }]);
 
+  app.factory('WordFactory', ['$myResource', function($myResource) {
+    return $myResource(Vipra.config.restUrl + '/words/:id');
+  }]);
+
 })();
\ No newline at end of file
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 96c0407f..3feb126a 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
@@ -175,7 +175,7 @@ public class ArticleFull implements Model<ObjectId>, Serializable {
 
 	public void setTopics(final List<TopicShare> topics) {
 		this.topics = topics;
-		this.topicsCount = topics == null ? 0 : topics.size();
+		topicsCount = topics == null ? 0 : topics.size();
 	}
 
 	public int getTopicsCount() {
diff --git a/vipra-util/src/main/java/de/vipra/util/model/ArticleWord.java b/vipra-util/src/main/java/de/vipra/util/model/ArticleWord.java
index f0fc0cab..7aa0f7be 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/ArticleWord.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/ArticleWord.java
@@ -11,23 +11,23 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 @Embedded
 public class ArticleWord implements Comparable<ArticleWord>, Serializable {
 
-	private String word;
+	private String id;
 
 	private Integer count;
 
 	public ArticleWord() {}
 
-	public ArticleWord(final String word, final int count) {
-		this.word = word;
+	public ArticleWord(final String id, final int count) {
+		this.id = id;
 		this.count = count;
 	}
 
-	public String getWord() {
-		return word;
+	public String getId() {
+		return id;
 	}
 
-	public void setWord(final String word) {
-		this.word = word;
+	public void setId(final String id) {
+		this.id = id;
 	}
 
 	public Integer getCount() {
@@ -45,7 +45,7 @@ public class ArticleWord implements Comparable<ArticleWord>, Serializable {
 
 	@Override
 	public String toString() {
-		return "ArticleWord [word=" + word + ", count=" + count + "]";
+		return "ArticleWord [id=" + id + ", count=" + count + "]";
 	}
 
 }
diff --git a/vipra-util/src/main/java/de/vipra/util/model/SequenceWord.java b/vipra-util/src/main/java/de/vipra/util/model/SequenceWord.java
index 7499f8b8..3e163774 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/SequenceWord.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/SequenceWord.java
@@ -11,23 +11,23 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 @Embedded
 public class SequenceWord implements Comparable<SequenceWord>, Serializable {
 
-	private String word;
+	private String id;
 
 	private Double probability;
 
 	public SequenceWord() {}
 
 	public SequenceWord(final String word, final Double probability) {
-		this.word = word;
+		id = word;
 		this.probability = probability;
 	}
 
-	public String getWord() {
-		return word;
+	public String getId() {
+		return id;
 	}
 
-	public void setWord(final String word) {
-		this.word = word;
+	public void setId(final String id) {
+		this.id = id;
 	}
 
 	public Double getProbability() {
@@ -45,14 +45,14 @@ public class SequenceWord implements Comparable<SequenceWord>, Serializable {
 
 	@Override
 	public String toString() {
-		return "SequenceWord [word=" + word + ", probability=" + probability + "]";
+		return "SequenceWord [id=" + id + ", probability=" + probability + "]";
 	}
 
 	@Override
 	public int hashCode() {
 		final int prime = 31;
 		int result = 1;
-		result = prime * result + ((word == null) ? 0 : word.hashCode());
+		result = prime * result + ((id == null) ? 0 : id.hashCode());
 		return result;
 	}
 
@@ -65,10 +65,10 @@ public class SequenceWord implements Comparable<SequenceWord>, Serializable {
 		if (getClass() != obj.getClass())
 			return false;
 		final SequenceWord other = (SequenceWord) obj;
-		if (word == null) {
-			if (other.word != null)
+		if (id == null) {
+			if (other.id != null)
 				return false;
-		} else if (!word.equals(other.word))
+		} else if (!id.equals(other.id))
 			return false;
 		return true;
 	}
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 c687c3b2..337e3b88 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
@@ -154,7 +154,7 @@ public class TopicFull implements Model<ObjectId>, Serializable {
 		return articlesCount;
 	}
 
-	public void setArticlesCount(int articlesCount) {
+	public void setArticlesCount(final int articlesCount) {
 		this.articlesCount = articlesCount;
 	}
 
@@ -187,7 +187,7 @@ public class TopicFull implements Model<ObjectId>, Serializable {
 			final int size = Math.min(wordsNum, words.size());
 			final List<String> topWords = new ArrayList<>(size);
 			for (int i = 0; i < size; i++) {
-				topWords.add(words.get(i).getWord());
+				topWords.add(words.get(i).getId());
 			}
 			name = StringUtils.join(topWords);
 		}
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 58dc063a..e0dee922 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
@@ -166,7 +166,7 @@ public class TopicModelConfig implements Serializable {
 		return minTopicShare;
 	}
 
-	public void setMinTopicShare(double minTopicShare) {
+	public void setMinTopicShare(final double minTopicShare) {
 		this.minTopicShare = minTopicShare;
 	}
 
diff --git a/vipra-util/src/main/java/de/vipra/util/model/TopicModelFull.java b/vipra-util/src/main/java/de/vipra/util/model/TopicModelFull.java
index 308336f4..a12ee4fe 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/TopicModelFull.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/TopicModelFull.java
@@ -98,7 +98,7 @@ public class TopicModelFull implements Model<String>, Comparable<TopicModelFull>
 		return lastGenerated;
 	}
 
-	public void setLastGenerated(Date lastGenerated) {
+	public void setLastGenerated(final Date lastGenerated) {
 		this.lastGenerated = lastGenerated;
 	}
 
diff --git a/vipra-util/src/main/java/de/vipra/util/model/TopicWord.java b/vipra-util/src/main/java/de/vipra/util/model/TopicWord.java
index 74bbe6cf..6fa9aaf2 100644
--- a/vipra-util/src/main/java/de/vipra/util/model/TopicWord.java
+++ b/vipra-util/src/main/java/de/vipra/util/model/TopicWord.java
@@ -9,18 +9,18 @@ import org.mongodb.morphia.annotations.Embedded;
 @Embedded
 public class TopicWord implements Comparable<TopicWord>, Serializable {
 
-	private String word;
+	private String id;
 
 	private List<Double> sequenceProbabilities;
 
 	private List<Double> sequenceProbabilitiesChange;
 
-	public String getWord() {
-		return word;
+	public String getId() {
+		return id;
 	}
 
-	public void setWord(final String word) {
-		this.word = word;
+	public void setId(final String id) {
+		this.id = id;
 	}
 
 	public List<Double> getSequenceProbabilities() {
@@ -35,13 +35,13 @@ public class TopicWord implements Comparable<TopicWord>, Serializable {
 		return sequenceProbabilitiesChange;
 	}
 
-	public void setSequenceProbabilitiesChange(List<Double> sequenceProbabilitiesChange) {
+	public void setSequenceProbabilitiesChange(final List<Double> sequenceProbabilitiesChange) {
 		this.sequenceProbabilitiesChange = sequenceProbabilitiesChange;
 	}
 
 	@Override
 	public int compareTo(final TopicWord o) {
-		return word.compareTo(o.getWord());
+		return id.compareTo(o.getId());
 	}
 
 }
diff --git a/vipra-util/src/main/java/de/vipra/util/model/WordFull.java b/vipra-util/src/main/java/de/vipra/util/model/WordFull.java
new file mode 100644
index 00000000..504deeef
--- /dev/null
+++ b/vipra-util/src/main/java/de/vipra/util/model/WordFull.java
@@ -0,0 +1,51 @@
+package de.vipra.util.model;
+
+import java.io.Serializable;
+
+import org.mongodb.morphia.annotations.Entity;
+import org.mongodb.morphia.annotations.Id;
+import org.mongodb.morphia.annotations.Reference;
+
+import de.vipra.util.an.QueryIgnore;
+
+@SuppressWarnings("serial")
+@Entity(value = "words", noClassnameStored = true)
+public class WordFull implements Model<String>, Comparable<WordFull>, Serializable {
+
+	@Id
+	private String id;
+
+	@Reference
+	@QueryIgnore(multi = true)
+	private TopicModel topicModel;
+
+	public WordFull() {}
+
+	public WordFull(final String word) {
+		id = word;
+	}
+
+	@Override
+	public String getId() {
+		return id;
+	}
+
+	@Override
+	public void setId(final String id) {
+		this.id = id;
+	}
+
+	public TopicModel getTopicModel() {
+		return topicModel;
+	}
+
+	public void setTopicModel(final TopicModel topicModel) {
+		this.topicModel = topicModel;
+	}
+
+	@Override
+	public int compareTo(final WordFull o) {
+		return id.compareTo(o.getId());
+	}
+
+}
-- 
GitLab