You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nlpcraft.apache.org by se...@apache.org on 2020/08/09 11:40:10 UTC

[incubator-nlpcraft] branch NLPCRAFT-41 updated: WIP.

This is an automated email from the ASF dual-hosted git repository.

sergeykamov pushed a commit to branch NLPCRAFT-41
in repository https://gitbox.apache.org/repos/asf/incubator-nlpcraft.git


The following commit(s) were added to refs/heads/NLPCRAFT-41 by this push:
     new fca8a2d  WIP.
fca8a2d is described below

commit fca8a2d39705509d1e8d7cde5ec1493615eacb34
Author: Sergey Kamov <se...@apache.org>
AuthorDate: Sun Aug 9 14:39:58 2020 +0300

    WIP.
---
 nlpcraft/src/main/resources/nlpcraft.conf          |   3 +
 .../model/intent/impl/NCIntentScanner.scala        |  66 ++-
 .../tools/suggestions/NCSuggestionsGenerator.scala | 455 ---------------------
 .../test/impl/NCTestAutoModelValidatorImpl.scala   |  97 +----
 .../nlpcraft/probe/mgrs/NCModelDecorator.scala     |   2 +
 .../probe/mgrs/conn/NCConnectionManager.scala      |  16 +-
 .../probe/mgrs/deploy/NCDeployManager.scala        |  40 +-
 .../mgrs/deploy/NCModelHolder.scala}               |  25 +-
 .../nlpcraft/probe/mgrs/model/NCModelManager.scala |  21 +-
 .../org/apache/nlpcraft/server/NCServer.scala      |   8 +-
 .../nlpcraft/server/mdo/NCProbeModelMdo.scala      |   5 +-
 .../opencensus/NCOpenCensusServerStats.scala       |   1 +
 .../nlpcraft/server/probe/NCProbeManager.scala     |  31 +-
 .../nlpcraft/server/rest/NCBasicRestApi.scala      | 135 ++++--
 .../NCSuggestion.scala}                            |  32 +-
 .../server/suggestion/NCSuggestionsManager.scala   | 315 ++++++++++++++
 16 files changed, 602 insertions(+), 650 deletions(-)

diff --git a/nlpcraft/src/main/resources/nlpcraft.conf b/nlpcraft/src/main/resources/nlpcraft.conf
index da972c8..9e181a6 100644
--- a/nlpcraft/src/main/resources/nlpcraft.conf
+++ b/nlpcraft/src/main/resources/nlpcraft.conf
@@ -204,6 +204,9 @@ nlpcraft {
 
         # If Spacy is enabled as a token provider (value 'spacy') - defines Spacy proxy URL.
         # spacy.proxy.url=http://localhost:5002
+
+        # If ContextWord enricher is enabled as a token provider - defines ctxword-server URL.
+        ctxword.url="http://localhost:5000"
     }
 
     # +---------------------+
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/intent/impl/NCIntentScanner.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/model/intent/impl/NCIntentScanner.scala
index ffd8389..507f7ea 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/intent/impl/NCIntentScanner.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/model/intent/impl/NCIntentScanner.scala
@@ -21,6 +21,7 @@ import java.lang.reflect.{InvocationTargetException, Method, ParameterizedType,
 import java.util
 import java.util.function.Function
 
+import com.typesafe.scalalogging.LazyLogging
 import org.apache.nlpcraft.common._
 import org.apache.nlpcraft.model._
 import org.apache.nlpcraft.model.intent.utils.NCDslIntent
@@ -31,7 +32,7 @@ import scala.collection._
 /**
   * Scanner for `NCIntent`, `NCIntentRef` and `NCIntentTerm` annotations.
   */
-object NCIntentScanner {
+object NCIntentScanner extends LazyLogging {
     type Callback = Function[NCIntentMatch, NCResult]
 
     private final val CLS_INTENT = classOf[NCIntent]
@@ -39,6 +40,7 @@ object NCIntentScanner {
     private final val CLS_TERM = classOf[NCIntentTerm]
     private final val CLS_QRY_RES = classOf[NCResult]
     private final val CLS_SLV_CTX = classOf[NCIntentMatch]
+    private final val CLS_SAMPLE = classOf[NCIntentSample]
 
     // Java and scala lists.
     private final val CLS_SCALA_SEQ = classOf[Seq[_]]
@@ -415,4 +417,66 @@ object NCIntentScanner {
             case (intent, m) ⇒ intent → prepareCallback(m, mdl, intent)
         }
         .toMap
+
+    /**
+      * TODO:
+      * @param mdl
+      */
+    @throws[NCE]
+    def scanIntentsSamples(mdl: NCModel): Map[String, Seq[String]] = {
+        var annFound = false
+
+        val res =
+            mdl.getClass.getDeclaredMethods.flatMap(method ⇒ {
+                def mkMethodName: String = s"${method.getDeclaringClass.getName}#${method.getName}(...)"
+
+                val smpAnn = method.getAnnotation(CLS_SAMPLE)
+                val intAnn = method.getAnnotation(CLS_INTENT)
+                val refAnn = method.getAnnotation(CLS_INTENT_REF)
+
+                if (smpAnn != null || intAnn != null || refAnn != null) {
+                    annFound = true
+
+                    def mkIntentId(): String =
+                        if (intAnn != null)
+                            NCIntentDslCompiler.compile(intAnn.value(), mdl.getId).id
+                        else if (refAnn != null)
+                            refAnn.value().trim
+                        else
+                            throw new AssertionError()
+
+                    if (smpAnn != null) {
+                        if (intAnn == null && refAnn == null) {
+                            logger.warn(s"@NCTestSample annotation without corresponding @NCIntent or @NCIntentRef annotations " +
+                                s"in method (ignoring): $mkMethodName")
+
+                            None
+                        }
+                        else {
+                            val samples = smpAnn.value().toList
+
+                            if (samples.isEmpty) {
+                                logger.warn(s"@NCTestSample annotation is empty in method (ignoring): $mkMethodName")
+
+                                None
+                            }
+                            else
+                                Some(mkIntentId() → samples)
+                        }
+                    }
+                    else {
+                        logger.warn(s"@NCTestSample annotation is missing in method (ignoring): $mkMethodName")
+
+                        None
+                    }
+                }
+                else
+                    None
+            }).toMap
+
+        if (!annFound)
+            logger.warn(s"Model '${mdl.getId}' doesn't have any intents.")
+            
+        res
+    }
 }
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/tools/suggestions/NCSuggestionsGenerator.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/model/tools/suggestions/NCSuggestionsGenerator.scala
deleted file mode 100644
index 945d130..0000000
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/tools/suggestions/NCSuggestionsGenerator.scala
+++ /dev/null
@@ -1,455 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.nlpcraft.model.tools.suggestions
-
-import java.util.concurrent.atomic.AtomicInteger
-import java.util.concurrent.{CopyOnWriteArrayList, CountDownLatch, TimeUnit}
-
-import com.google.gson.Gson
-import com.google.gson.reflect.TypeToken
-import org.apache.http.HttpResponse
-import org.apache.http.client.ResponseHandler
-import org.apache.http.client.methods.HttpPost
-import org.apache.http.entity.StringEntity
-import org.apache.http.impl.client.HttpClients
-import org.apache.http.util.EntityUtils
-import org.apache.nlpcraft.common.ascii.NCAsciiTable
-import org.apache.nlpcraft.common.makro.NCMacroParser
-import org.apache.nlpcraft.common.nlp.core.NCNlpPorterStemmer
-import org.apache.nlpcraft.common.util.NCUtils
-import org.apache.nlpcraft.common.version.NCVersion
-import org.apache.nlpcraft.model.NCModelFileAdapter
-
-import java.util.{List ⇒ JList}
-
-import scala.collection.JavaConverters._
-import scala.collection._
-
-case class ParametersHolder(modelPath: String, url: String, limit: Int, minScore: Double, debug: Boolean)
-
-object NCSuggestionsGeneratorImpl {
-    case class Suggestion(word: String, score: Double)
-    case class RequestData(sentence: String, example: String, elementId: String, index: Int)
-    case class RestRequestSentence(text: String, indexes: JList[Int])
-    case class RestRequest(sentences: JList[RestRequestSentence], limit: Int, min_score: Double)
-    case class Word(word: String, stem: String) {
-        require(!word.contains(" "), s"Word cannot contains spaces: $word")
-        require(
-            word.forall(ch ⇒
-                ch.isLetterOrDigit ||
-                    ch == '\'' ||
-                    SEPARATORS.contains(ch)
-            ),
-            s"Unsupported symbols: $word"
-        )
-    }
-
-    private final val GSON = new Gson
-    private final val TYPE_RESP = new TypeToken[JList[JList[Suggestion]]]() {}.getType
-    private final val SEPARATORS = Seq('?', ',', '.', '-', '!')
-    private final val BATCH_SIZE = 20
-
-    private final val HANDLER: ResponseHandler[Seq[Seq[Suggestion]]] =
-        (resp: HttpResponse) ⇒ {
-            val code = resp.getStatusLine.getStatusCode
-            val e = resp.getEntity
-
-            val js = if (e != null) EntityUtils.toString(e) else null
-
-            if (js == null)
-                throw new RuntimeException(s"Unexpected empty response [code=$code]")
-
-            code match {
-                case 200 ⇒
-                    val data: JList[JList[Suggestion]] = GSON.fromJson(js, TYPE_RESP)
-
-                    data.asScala.map(p ⇒ if (p.isEmpty) Seq.empty else p.asScala.tail)
-
-                case 400 ⇒ throw new RuntimeException(js)
-                case _ ⇒ throw new RuntimeException(s"Unexpected response [code=$code, response=$js]")
-            }
-        }
-
-    private def split(s: String): Seq[String] = s.split(" ").toSeq.map(_.trim).filter(_.nonEmpty)
-    private def toStem(s: String): String = split(s).map(NCNlpPorterStemmer.stem).mkString(" ")
-    private def toStemWord(s: String): String = NCNlpPorterStemmer.stem(s)
-
-    private def getAllSlices(seq1: Seq[String], seq2: Seq[String]): Seq[Int] = {
-        val seq = mutable.Buffer.empty[Int]
-
-        var i = seq1.indexOfSlice(seq2)
-
-        while (i >= 0) {
-            seq += i
-
-            i = seq1.indexOfSlice(seq2, i + 1)
-        }
-
-        seq
-    }
-
-    def process(data: ParametersHolder): Unit = {
-        val now = System.currentTimeMillis()
-
-        val mdl = new NCModelFileAdapter(data.modelPath) {}
-
-        val parser = new NCMacroParser()
-
-        if (mdl.getMacros != null)
-            mdl.getMacros.asScala.foreach { case (name, str) ⇒ parser.addMacro(name, str) }
-
-        val examples =
-            mdl.getExamples.asScala.
-                map(ex ⇒ SEPARATORS.foldLeft(ex)((s, ch) ⇒ s.replaceAll(s"\\$ch", s" $ch "))).
-                map(ex ⇒ {
-                    val seq = ex.split(" ")
-
-                    seq → seq.map(toStemWord)
-                }).
-                toMap
-
-        val elemSyns =
-            mdl.getElements.asScala.map(e ⇒ e.getId → e.getSynonyms.asScala.flatMap(parser.expand)).
-                map { case (id, seq) ⇒ id → seq.map(txt ⇒ split(txt).map(p ⇒ Word(p, toStemWord(p)))) }.toMap
-
-        val allReqs =
-            elemSyns.map {
-                case (elemId, syns) ⇒
-                    val normSyns: Seq[Seq[Word]] = syns.filter(_.size == 1)
-                    val synsStems = normSyns.map(_.map(_.stem))
-                    val synsWords = normSyns.map(_.map(_.word))
-
-                    val reqs =
-                        examples.flatMap { case (exampleWords, exampleStems) ⇒
-                            val exampleIdxs = synsStems.flatMap(synStems ⇒ getAllSlices(exampleStems, synStems))
-
-                            def mkRequestData(idx: Int, synStems: Seq[String], synStemsIdx: Int): RequestData = {
-                                val fromIncl = idx
-                                val toExcl = idx + synStems.length
-
-                                RequestData(
-                                    sentence = exampleWords.zipWithIndex.flatMap {
-                                        case (exampleWord, i) ⇒
-                                            i match {
-                                                case x if x == fromIncl ⇒ synsWords(synStemsIdx)
-                                                case x if x > fromIncl && x < toExcl ⇒ Seq.empty
-                                                case _ ⇒ Seq(exampleWord)
-                                            }
-                                    }.mkString(" "),
-                                    example = exampleWords.mkString(" "),
-                                    elementId = elemId,
-                                    index = idx
-                                )
-                            }
-
-                            (for (idx ← exampleIdxs; (synStems, i) ← synsStems.zipWithIndex)
-                                yield mkRequestData(idx, synStems, i)).distinct
-                        }
-
-                    elemId → reqs.toSet
-            }.filter(_._2.nonEmpty)
-
-        val allReqsCnt = allReqs.map(_._2.size).sum
-
-        println(s"Examples count: ${examples.size}")
-        println(s"Synonyms count: ${elemSyns.map(_._2.size).sum}")
-        println(s"Request prepared: $allReqsCnt")
-
-        val allSuggs = new java.util.concurrent.ConcurrentHashMap[String, JList[Suggestion]]()
-        val cdl = new CountDownLatch(1)
-        val debugs = mutable.HashMap.empty[RequestData, Seq[Suggestion]]
-        val cnt = new AtomicInteger(0)
-
-        val client = HttpClients.createDefault
-
-        for ((elemId, reqs) ← allReqs; batch ← reqs.sliding(BATCH_SIZE, BATCH_SIZE).map(_.toSeq)) {
-            NCUtils.asFuture(
-                _ ⇒ {
-                    val post = new HttpPost(data.url)
-
-                    post.setHeader("Content-Type", "application/json")
-                    post.setEntity(
-                        new StringEntity(
-                            GSON.toJson(
-                                RestRequest(
-                                    sentences = batch.map(p ⇒ RestRequestSentence(p.sentence, Seq(p.index).asJava)).asJava,
-                                    min_score = data.minScore,
-                                    limit = data.limit
-                                )
-                            ),
-                            "UTF-8"
-                        )
-                    )
-
-                    val resps: Seq[Seq[Suggestion]] =
-                        try
-                            client.execute(post, HANDLER)
-                        finally
-                            post.releaseConnection()
-
-                    if (data.debug) {
-                        require(reqs.size == resps.size)
-
-                        reqs.zip(resps).foreach { case (req, resp) ⇒ debugs += req → resp}
-                    }
-
-                    val i = cnt.addAndGet(batch.size)
-
-                    println(s"Executed: $i requests.")
-
-                    allSuggs.
-                        computeIfAbsent(elemId, (_: String) ⇒ new CopyOnWriteArrayList[Suggestion]()).
-                        addAll(resps.flatten.asJava)
-
-                    if (i == allReqsCnt)
-                        cdl.countDown()
-                },
-                (e: Throwable) ⇒ {
-                    e.printStackTrace()
-
-                    cdl.countDown()
-                },
-                (_: Unit) ⇒ ()
-            )
-        }
-
-        cdl.await(Long.MaxValue, TimeUnit.MILLISECONDS)
-
-        val allSynsStems = elemSyns.flatMap(_._2).flatten.map(_.stem).toSet
-
-        val nonEmptySuggs = allSuggs.asScala.map(p ⇒ p._1 → p._2.asScala).filter(_._2.nonEmpty)
-
-        val tbl = NCAsciiTable()
-
-        tbl #= (
-            "Element",
-            "Suggestion",
-            "ContextWord Server Score",
-            "Suggested times",
-            "Totla Score",
-        )
-
-        nonEmptySuggs.
-            foreach { case (elemId, elemSuggs) ⇒
-                elemSuggs.
-                    map(sugg ⇒ (sugg, toStem(sugg.word))).
-                    groupBy { case (_, stem) ⇒ stem }.
-                    filter { case (stem, _) ⇒ !allSynsStems.contains(stem) }.
-                    map { case (_, group) ⇒
-                        val seq = group.map { case (sugg, _) ⇒ sugg }.sortBy(-_.score)
-
-                        // Drops repeated.
-                        (seq.head, seq.length)
-                    }.
-                    toSeq.
-                    map { case (sugg, cnt) ⇒ (sugg, cnt, sugg.score * cnt / elemSuggs.size) }.
-                    sortBy { case (_, _, sumFactor) ⇒ -sumFactor }.
-                    zipWithIndex.
-                    foreach { case ((sugg, cnt, sumFactor), sugIdx) ⇒
-                        def f(d: Double): String = "%1.5f" format d
-
-                        tbl += (if (sugIdx == 0) elemId else "", sugg.word, f(sugg.score), cnt, f(sumFactor))
-                    }
-            }
-
-        if (data.debug) {
-            var i = 1
-
-            debugs.groupBy(_._1.example).foreach { case (_, m) ⇒
-                m.toSeq.sortBy(_._1.sentence).foreach { case (req, suggs) ⇒
-                    val s =
-                        split(req.sentence).
-                            zipWithIndex.map { case (w, i) ⇒ if (i == req.index) s"<<<$w>>>" else w }.
-                            mkString(" ")
-
-                    println(
-                        s"$i. " +
-                            s"Request=$s, " +
-                            s"suggestions=[${suggs.map(_.word).mkString(", ")}], " +
-                            s"element=${req.elementId}"
-                    )
-
-                    i = i + 1
-                }
-            }
-        }
-
-        println(s"Suggestions calculated (${(System.currentTimeMillis() - now) / 1000} secs)")
-
-        tbl.render()
-    }
-}
-
-object NCSuggestionsGenerator extends App {
-    private lazy val DFLT_URL: String = "http://localhost:5000/suggestions"
-    private lazy val DFLT_LIMIT: Int = 20
-    private lazy val DFLT_MIN_SCORE: Double = 0
-    private lazy val DFLT_DEBUG: Boolean = false
-
-    /**
-      *
-      * @param msg Optional error message.
-      */
-    private def errorExit(msg: String = null): Unit = {
-        if (msg != null)
-            System.err.println(s"ERROR: $msg")
-        else
-            System.err.println(
-                s"""
-                   |NAME:
-                   |    NCSuggestionsGenerator -- NLPCraft synonyms suggestions generator for given model.
-                   |
-                   |SYNOPSIS:
-                   |    java -cp apache-nlpcraft-incubating-${NCVersion.getCurrent}-all-deps.jar org.apache.nlpcraft.model.tools.suggestions.NCSuggestionsGenerator [PARAMETERS]
-                   |
-                   |DESCRIPTION:
-                   |    This utility generates synonyms suggestions for given NLPCraft model.
-                   |    Note that ContextWord NLP server should be started and accessible parameter URL.
-                   |
-                   |    This Java class can be run from the command line or from an IDE like any other
-                   |    Java application.""".stripMargin
-            )
-
-        System.err.println(
-            s"""
-               |PARAMETERS:
-               |    [--model|-m] model path
-               |        Mandatory file model path.
-               |        It should have one of the following extensions: .js, .json, .yml, or .yaml
-               |
-               |    [--url|-u] url
-               |        Optional ContextWord NLP server URL.
-               |        Default is $DFLT_URL.
-               |
-               |    [--limit|-l] limit
-               |        Optional maximum suggestions per synonyms count value.
-               |        Default is $DFLT_LIMIT.
-               |
-               |    [--score|-c] score
-               |        Optional minimal suggestion score value.
-               |        Default is $DFLT_MIN_SCORE.
-               |
-               |    [--debug|-d] [true|false]
-               |        Optional flag on whether or not to debug output.
-               |        Default is $DFLT_DEBUG.
-               |
-               |    [--help|-h|-?]
-               |        Prints this usage information.
-               |
-               |EXAMPLES:
-               |    java -cp apache-nlpcraft-incubating-${NCVersion.getCurrent}-all-deps.jar org.apache.nlpcraft.model.tools.sqlgen.NCSqlModelGenerator
-               |        -m src/main/scala/org/apache/nlpcraft/examples/weather/weather_model.json
-               |        -u $DFLT_URL
-            """.stripMargin
-        )
-
-        System.exit(1)
-    }
-
-    /**
-      *
-      * @param v
-      * @param name
-      */
-    private def mandatoryParam(v: String, name: String): Unit =
-        if (v == null)
-            throw new IllegalArgumentException(s"Parameter is mandatory and must be set: $name")
-
-    /**
-      *
-      * @param v
-      * @param name
-      * @return
-      */
-    private def parseNum[T](v: String, name: String, extract: String ⇒ T, fromIncl: T, toIncl: T)(implicit e: T ⇒ Number): T = {
-        val t =
-            try
-                extract(v.toLowerCase)
-            catch {
-                case _: NumberFormatException ⇒ throw new IllegalArgumentException(s"Invalid numeric: $name")
-            }
-
-        val td = t.doubleValue()
-
-        if (td < fromIncl.doubleValue() || td > toIncl.doubleValue())
-            throw new IllegalArgumentException(s"Invalid '$name' range. Must be between: $fromIncl and $toIncl")
-
-        t
-    }
-
-    /**
-      *
-      * @param v
-      * @param name
-      * @return
-      */
-    private def parseBoolean(v: String, name: String): Boolean =
-        v.toLowerCase match {
-            case "true" ⇒ true
-            case "false" ⇒ false
-
-            case _ ⇒ throw new IllegalArgumentException(s"Invalid boolean value in: $name $v")
-        }
-
-    /**
-      *
-      * @param cmdArgs
-      * @return
-      */
-    private def parseCmdParameters(cmdArgs: Array[String]): ParametersHolder = {
-        if (cmdArgs.isEmpty || !cmdArgs.intersect(Seq("--help", "-h", "-help", "--?", "-?", "/?", "/help")).isEmpty)
-            errorExit()
-
-        var mdlPath: String = null
-
-        var url = DFLT_URL
-        var limit = DFLT_LIMIT
-        var minScore = DFLT_MIN_SCORE
-        var debug = DFLT_DEBUG
-
-        var i = 0
-
-        try {
-            while (i < cmdArgs.length - 1) {
-                val k = cmdArgs(i).toLowerCase
-                val v = cmdArgs(i + 1)
-
-                k match {
-                    case "--model" | "-m" ⇒ mdlPath = v
-                    case "--url" | "-u" ⇒ url = v
-                    case "--limit" | "-l" ⇒ limit = parseNum(v, k, (s: String) ⇒ s.toInt, 1, Integer.MAX_VALUE)
-                    case "--score" | "-c" ⇒ minScore = parseNum(v, k, (s: String) ⇒ s.toDouble, 0, Integer.MAX_VALUE)
-                    case "--debug" | "-d" ⇒ debug = parseBoolean(v, k)
-
-                    case _ ⇒ throw new IllegalArgumentException(s"Invalid argument: ${cmdArgs(i)}")
-                }
-
-                i = i + 2
-            }
-
-            mandatoryParam(mdlPath, "--model")
-        }
-        catch {
-            case e: Exception ⇒ errorExit(e.getMessage)
-        }
-
-        ParametersHolder(mdlPath, url, limit, minScore, debug)
-    }
-
-    NCSuggestionsGeneratorImpl.process(parseCmdParameters(args))
-}
\ No newline at end of file
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/tools/test/impl/NCTestAutoModelValidatorImpl.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/model/tools/test/impl/NCTestAutoModelValidatorImpl.scala
index b71458a..7eae2f4 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/model/tools/test/impl/NCTestAutoModelValidatorImpl.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/model/tools/test/impl/NCTestAutoModelValidatorImpl.scala
@@ -17,28 +17,20 @@
 
 package org.apache.nlpcraft.model.tools.test.impl
 
-import java.lang.reflect.Method
-
 import com.typesafe.scalalogging.LazyLogging
 import org.apache.nlpcraft.common.ascii.NCAsciiTable
 import org.apache.nlpcraft.common.util.NCUtils
-import org.apache.nlpcraft.model.intent.impl.NCIntentDslCompiler
+import org.apache.nlpcraft.model.NCModel
+import org.apache.nlpcraft.model.intent.impl.NCIntentScanner
 import org.apache.nlpcraft.model.tools.test.NCTestClientBuilder
-import org.apache.nlpcraft.model.{NCIntent, NCIntentRef, NCIntentSample, NCModel}
 import org.apache.nlpcraft.probe.embedded.NCEmbeddedProbe
 
 /**
   * Implementation for `NCTestAutoModelValidator` class.
   */
 private [test] object NCTestAutoModelValidatorImpl extends LazyLogging {
-    case class IntentSamples(intentId: String, samples: List[String])
-    
     private final val PROP_MODELS = "NLPCRAFT_TEST_MODELS"
 
-    private final val CLS_SAMPLE = classOf[NCIntentSample]
-    private final val CLS_INTENT = classOf[NCIntent]
-    private final val CLS_INTENT_REF = classOf[NCIntentRef]
-
     @throws[Exception]
     def isValid: Boolean =
         NCUtils.sysEnv(PROP_MODELS) match {
@@ -66,7 +58,12 @@ private [test] object NCTestAutoModelValidatorImpl extends LazyLogging {
 
     @throws[Exception]
     private def isValid(classes: Seq[Class[_ <: NCModel]]) = {
-        val samples = getSamples(classes.map(cl ⇒ cl → cl.getDeclaredConstructor().newInstance().getId).toMap)
+        val samples =
+            classes.
+                map(_.getDeclaredConstructor().newInstance()).
+                map(mdl ⇒ mdl.getId → NCIntentScanner.scanIntentsSamples(mdl).toMap).
+                toMap.
+                filter(_._2.nonEmpty)
 
         NCEmbeddedProbe.start(classes: _*)
 
@@ -81,7 +78,7 @@ private [test] object NCTestAutoModelValidatorImpl extends LazyLogging {
       * @param samples
       * @return
       */
-    private def process(samples: Map[/*Model ID*/String, List[IntentSamples]]): Boolean = {
+    private def process(samples: Map[/*Model ID*/String, Map[String/*Intent ID*/, Seq[String]/*Samples*/]]): Boolean = {
         case class Result(
             modelId: String,
             intentId: String,
@@ -90,7 +87,7 @@ private [test] object NCTestAutoModelValidatorImpl extends LazyLogging {
             error: Option[String]
         )
         
-        val results = samples.flatMap { case (mdlId, smpList) ⇒
+        val results = samples.flatMap { case (mdlId, samples) ⇒
             val cli = new NCTestClientBuilder().newBuilder.build
     
             cli.open(mdlId)
@@ -100,14 +97,14 @@ private [test] object NCTestAutoModelValidatorImpl extends LazyLogging {
                     val res = cli.ask(txt)
             
                     if (res.isFailed)
-                        Result(mdlId, intentId, txt, false, Some(res.getResultError.get()))
+                        Result(mdlId, intentId, txt, pass = false, Some(res.getResultError.get()))
                     else if (intentId != res.getIntentId)
-                        Result(mdlId, intentId, txt, false, Some(s"Unexpected intent ID '${res.getIntentId}'"))
+                        Result(mdlId, intentId, txt, pass = false, Some(s"Unexpected intent ID '${res.getIntentId}'"))
                     else
-                        Result(mdlId, intentId, txt, true, None)
+                        Result(mdlId, intentId, txt, pass = true, None)
                 }
                 
-                for (smp ← smpList; txt ← smp.samples) yield ask(smp.intentId, txt)
+                for ((intentId, seq) ← samples; txt ← seq) yield ask(intentId, txt)
             }
             finally
                 cli.close()
@@ -139,73 +136,7 @@ private [test] object NCTestAutoModelValidatorImpl extends LazyLogging {
         
         failCnt == 0
     }
-    
-    private def mkMethodName(mtd: Method): String =
-        s"${mtd.getDeclaringClass.getName}#${mtd.getName}(...)"
-
-    /**
-      *
-      * @param mdls
-      * @return
-      */
-    private def getSamples(mdls: Map[Class[_ <: NCModel], String]): Map[/*Model ID*/String, List[IntentSamples]] =
-        mdls.flatMap { case (claxx, mdlId) ⇒
-            var annFound = false
-
-            val mdlData = claxx.getDeclaredMethods.flatMap(method ⇒ {
-                val smpAnn = method.getAnnotation(CLS_SAMPLE)
-                val intAnn = method.getAnnotation(CLS_INTENT)
-                val refAnn = method.getAnnotation(CLS_INTENT_REF)
-
-                if (smpAnn != null || intAnn != null || refAnn != null) {
-                    annFound = true
 
-                    def mkIntentId(): String =
-                        if (intAnn != null)
-                            NCIntentDslCompiler.compile(intAnn.value(), mdlId).id
-                        else if (refAnn != null)
-                            refAnn.value().trim
-                        else
-                            throw new AssertionError()
-
-                    if (smpAnn != null) {
-                        if (intAnn == null && refAnn == null) {
-                            logger.warn(s"@NCTestSample annotation without corresponding @NCIntent or @NCIntentRef annotations " +
-                                s"in method (ignoring): ${mkMethodName(method)}")
-
-                            None
-                        }
-                        else {
-                            val samples = smpAnn.value().toList
-
-                            if (samples.isEmpty) {
-                                logger.warn(s"@NCTestSample annotation is empty in method (ignoring): ${mkMethodName(method)}")
-
-                                None
-                            }
-                            else
-                                Some(IntentSamples(mkIntentId(), samples))
-                        }
-                    }
-                    else {
-                        logger.warn(s"@NCTestSample annotation is missing in method (ignoring): ${mkMethodName(method)}")
-
-                        None
-                    }
-                }
-                else
-                    None
-            }).toList
-
-            if (mdlData.isEmpty) {
-                if (!annFound)
-                    logger.warn(s"Model '$mdlId' doesn't have any intents to test.")
-
-                None
-            }
-            else
-                Some(mdlId → mdlData)
-        }
 
     /**
       *
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/NCModelDecorator.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/NCModelDecorator.scala
index 2cfe417..9805a16 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/NCModelDecorator.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/NCModelDecorator.scala
@@ -32,6 +32,7 @@ import scala.language.implicitConversions
 /**
   *
   * @param model Decorated model.
+  * @param intentsSamples Model examples.
   * @param synonyms Fast-access synonyms map for first phase.
   * @param synonymsDsl Fast-access synonyms map for second phase.
   * @param additionalStopWordsStems Stemmatized additional stopwords.
@@ -41,6 +42,7 @@ import scala.language.implicitConversions
   */
 case class NCModelDecorator(
     model: NCModel,
+    intentsSamples: Map[String, Seq[String]],
     synonyms: Map[String/*Element ID*/, Map[Int/*Synonym length*/, Seq[NCSynonym]]], // Fast access map.
     synonymsDsl: Map[String/*Element ID*/, Map[Int/*Synonym length*/, Seq[NCSynonym]]], // Fast access map.
     additionalStopWordsStems: Set[String],
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/conn/NCConnectionManager.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/conn/NCConnectionManager.scala
index a59eea0..cc6dd1f 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/conn/NCConnectionManager.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/conn/NCConnectionManager.scala
@@ -35,6 +35,7 @@ import org.apache.nlpcraft.probe.mgrs.NCProbeMessage
 import org.apache.nlpcraft.probe.mgrs.cmd.NCCommandManager
 import org.apache.nlpcraft.probe.mgrs.model.NCModelManager
 
+import scala.collection.JavaConverters._
 import scala.collection.mutable
 
 /**
@@ -47,7 +48,7 @@ object NCConnectionManager extends NCService {
     private final val SO_TIMEOUT = 5 * 1000
     // Ping timeout.
     private final val PING_TIMEOUT = 5 * 1000
-    
+
     // Internal probe GUID.
     @volatile private var probeGuid: String = _
     
@@ -234,7 +235,18 @@ object NCConnectionManager extends NCService {
 
                             // util.HashSet created to avoid scala collections serialization error.
                             // Seems to be a Scala bug.
-                            (mdl.getId, mdl.getName, mdl.getVersion, new util.HashSet[String](mdl.getEnabledBuiltInTokens))
+                            (
+                                mdl.getId,
+                                mdl.getName,
+                                mdl.getVersion,
+                                new util.HashSet[String](mdl.getEnabledBuiltInTokens),
+                                mdl.getMacros,
+                                new util.HashMap[String, util.List[String]](
+                                    mdl.getElements.asScala.map(p ⇒ p.getId → p.getSynonyms).toMap.asJava
+                                ),
+                                new util.HashMap[String, util.List[String]]
+                                    (m.intentsSamples.map(p ⇒ p._1 → new util.ArrayList[String](p._2.asJava)).asJava)
+                            )
                         })
                 ), cryptoKey)
     
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/deploy/NCDeployManager.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/deploy/NCDeployManager.scala
index 66706da..cf61a1a 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/deploy/NCDeployManager.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/deploy/NCDeployManager.scala
@@ -18,7 +18,7 @@
 package org.apache.nlpcraft.probe.mgrs.deploy
 
 import java.io._
-import java.util.jar.{JarInputStream ⇒ JIS}
+import java.util.jar.{JarInputStream => JIS}
 
 import io.opencensus.trace.Span
 import org.apache.nlpcraft.common._
@@ -31,7 +31,7 @@ import resource.managed
 
 import scala.collection.JavaConverters._
 import scala.collection.convert.DecorateAsScala
-import scala.collection.mutable
+import scala.collection.{Seq, mutable}
 import scala.collection.mutable.ArrayBuffer
 import scala.util.control.Exception._
 
@@ -39,11 +39,15 @@ import scala.util.control.Exception._
   * Model deployment manager.
   */
 object NCDeployManager extends NCService with DecorateAsScala {
-    @volatile private var models: ArrayBuffer[NCModel] = _
-    @volatile private var modelFactory: NCModelFactory = _
-    
+    private final val CLS_SAMPLE = classOf[NCIntentSample]
+    private final val CLS_INTENT = classOf[NCIntent]
+    private final val CLS_INTENT_REF = classOf[NCIntentRef]
+
     private final val ID_REGEX = "^[_a-zA-Z]+[a-zA-Z0-9:-_]*$"
 
+    @volatile private var models: ArrayBuffer[NCModelHolder] = _
+    @volatile private var modelFactory: NCModelFactory = _
+
     object Config extends NCConfigurable {
         private final val pre = "nlpcraft.probe"
 
@@ -75,7 +79,7 @@ object NCDeployManager extends NCService with DecorateAsScala {
       * @return
       */
     @throws[NCE]
-    private def wrap(mdl: NCModel): NCModel = {
+    private def wrap(mdl: NCModel): NCModelHolder = {
         // Scan for intent annotations in the model class.
         val intents = NCIntentScanner.scan(mdl)
 
@@ -91,16 +95,16 @@ object NCDeployManager extends NCService with DecorateAsScala {
             val solver = new NCIntentSolver(
                 intents.toList.map(x ⇒ (x._1, (z: NCIntentMatch) ⇒ x._2.apply(z)))
             )
-    
-            new NCModelImpl(mdl, solver)
+
+            NCModelHolder(new NCModelImpl(mdl, solver), NCIntentScanner.scanIntentsSamples(mdl).toMap)
         }
         else {
             logger.warn(s"Model has no intents: ${mdl.getId}")
-    
-            new NCModelImpl(mdl, null)
+
+            NCModelHolder(new NCModelImpl(mdl, null), Map.empty)
         }
     }
-    
+
     /**
       *
       * @param clsName Factory class name.
@@ -122,7 +126,7 @@ object NCDeployManager extends NCService with DecorateAsScala {
       * @param clsName Model class name.
       */
     @throws[NCE]
-    private def makeModel(clsName: String): NCModel =
+    private def makeModel(clsName: String): NCModelHolder =
         try
             wrap(
                 makeModelFromSource(
@@ -157,7 +161,7 @@ object NCDeployManager extends NCService with DecorateAsScala {
       * @param jarFile JAR file to extract from.
       */
     @throws[NCE]
-    private def extractModels(jarFile: File): Seq[NCModel] = {
+    private def extractModels(jarFile: File): Seq[NCModelHolder] = {
         val clsLdr = Thread.currentThread().getContextClassLoader
         
         val classes = mutable.ArrayBuffer.empty[Class[_ <: NCModel]]
@@ -197,7 +201,7 @@ object NCDeployManager extends NCService with DecorateAsScala {
     @throws[NCE]
     override def start(parent: Span = null): NCService = startScopedSpan("start", parent) { _ ⇒
         modelFactory = new NCBasicModelFactory
-        models = ArrayBuffer.empty[NCModel]
+        models = ArrayBuffer.empty[NCModelHolder]
 
         // Initialize model factory (if configured).
         Config.modelFactoryType match {
@@ -230,7 +234,9 @@ object NCDeployManager extends NCService with DecorateAsScala {
         }
 
         // Verify models' identities.
-        models.foreach(mdl ⇒ {
+        models.foreach(h ⇒ {
+            val mdl = h.model
+
             val mdlName = mdl.getName
             val mdlId = mdl.getId
             val mdlVer = mdl.getVersion
@@ -259,7 +265,7 @@ object NCDeployManager extends NCService with DecorateAsScala {
                     throw new NCE(s"Model element ID '${elm.getId}' does not match '$ID_REGEX' regex in: $mdlId")
         })
 
-        if (U.containsDups(models.map(_.getId).toList))
+        if (U.containsDups(models.map(_.model.getId).toList))
             throw new NCE("Duplicate model IDs detected.")
         
         super.start()
@@ -280,5 +286,5 @@ object NCDeployManager extends NCService with DecorateAsScala {
       *
       * @return
       */
-    def getModels: Seq[NCModel] = models
+    def getModels: Seq[NCModelHolder] = models
 }
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/mdo/NCProbeModelMdo.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/deploy/NCModelHolder.scala
similarity index 58%
copy from nlpcraft/src/main/scala/org/apache/nlpcraft/server/mdo/NCProbeModelMdo.scala
copy to nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/deploy/NCModelHolder.scala
index 1510c4b..1a94481 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/mdo/NCProbeModelMdo.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/deploy/NCModelHolder.scala
@@ -15,26 +15,13 @@
  * limitations under the License.
  */
 
-package org.apache.nlpcraft.server.mdo
+package org.apache.nlpcraft.probe.mgrs.deploy
 
-import org.apache.nlpcraft.server.mdo.impl._
+import org.apache.nlpcraft.model.NCModel
 
 /**
-  * Probe model MDO.
+  * TODO:
+  * @param model
+  * @param intentSamples
   */
-@NCMdoEntity(sql = false)
-case class NCProbeModelMdo(
-    @NCMdoField id: String,
-    @NCMdoField name: String,
-    @NCMdoField version: String,
-    @NCMdoField enabledBuiltInTokens: Set[String]
-) extends NCAnnotatedMdo[NCProbeModelMdo] {
-    override def hashCode(): Int = s"$id$name".hashCode()
-    
-    override def equals(obj: Any): Boolean = {
-        obj match {
-            case x: NCProbeModelMdo ⇒ x.id == id
-            case _ ⇒ false
-        }
-    }
-}
+case class NCModelHolder(model: NCModel, intentSamples: Map[String, Seq[String]])
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/model/NCModelManager.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/model/NCModelManager.scala
index 2875d20..9f67045 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/model/NCModelManager.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/probe/mgrs/model/NCModelManager.scala
@@ -59,24 +59,24 @@ object NCModelManager extends NCService with DecorateAsScala {
     )
     
     /**
-      * @param mdl Data model.
+      * @param h Data model holder.
       */
-    private def addNewModel(mdl: NCModel): Unit = {
+    private def addNewModel(h: NCModelHolder): Unit = {
         require(Thread.holdsLock(mux))
 
-        checkModelConfig(mdl)
+        checkModelConfig(h.model)
 
         val parser = new NCMacroParser
-        val macros = mdl.getMacros
+        val macros = h.model.getMacros
 
         // Initialize macro parser.
         if (macros != null)
             macros.asScala.foreach(t ⇒ parser.addMacro(t._1, t._2))
-        
-        models += mdl.getId → verifyAndDecorate(mdl, parser)
+
+        models += h.model.getId → verifyAndDecorate(h, parser)
 
         // Init callback on the model.
-        mdl.onInit()
+        h.model.onInit()
     }
 
     @throws[NCE]
@@ -249,12 +249,14 @@ object NCModelManager extends NCService with DecorateAsScala {
     /**
       * Verifies given model and makes a decorator optimized for model enricher.
       *
-      * @param mdl Model to verify and decorate.
+      * @param h Model holder to verify and decorate.
       * @param parser Initialized macro parser.
       * @return Model decorator.
       */
     @throws[NCE]
-    private def verifyAndDecorate(mdl: NCModel, parser: NCMacroParser): NCModelDecorator = {
+    private def verifyAndDecorate(h: NCModelHolder, parser: NCMacroParser): NCModelDecorator = {
+        val mdl = h.model
+
         for (elm ← mdl.getElements)
             checkElement(mdl, elm)
 
@@ -526,6 +528,7 @@ object NCModelManager extends NCService with DecorateAsScala {
 
         NCModelDecorator(
             model = mdl,
+            intentsSamples = h.intentSamples,
             synonyms = mkFastAccessMap(filter(syns, dsl = false)),
             synonymsDsl = mkFastAccessMap(filter(syns, dsl = true)),
             additionalStopWordsStems = addStopWords,
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/NCServer.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/NCServer.scala
index 1418685..7ac8179 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/NCServer.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/NCServer.scala
@@ -45,6 +45,7 @@ import org.apache.nlpcraft.server.proclog.NCProcessLogManager
 import org.apache.nlpcraft.server.query.NCQueryManager
 import org.apache.nlpcraft.server.rest.NCRestManager
 import org.apache.nlpcraft.server.sql.NCSqlManager
+import org.apache.nlpcraft.server.suggestion.NCSuggestionsManager
 import org.apache.nlpcraft.server.tx.NCTxManager
 import org.apache.nlpcraft.server.user.NCUserManager
 
@@ -120,7 +121,10 @@ object NCServer extends App with NCIgniteInstance with LazyLogging with NCOpenCe
                     NCUserManager.start(span)
                     NCCompanyManager.start(span)
                 },
-                () ⇒ NCProbeManager.start(span),
+                () ⇒ {
+                    NCProbeManager.start(span)
+                    NCSuggestionsManager.start(span)
+                },
                 () ⇒ NCFeedbackManager.start(span)
             )
             
@@ -144,6 +148,8 @@ object NCServer extends App with NCIgniteInstance with LazyLogging with NCOpenCe
                 NCRestManager,
                 NCQueryManager,
                 NCFeedbackManager,
+                NCSuggestionsManager,
+                NCProbeManager,
                 NCCompanyManager,
                 NCUserManager,
                 NCServerEnrichmentManager,
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/mdo/NCProbeModelMdo.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/mdo/NCProbeModelMdo.scala
index 1510c4b..0598ff6 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/mdo/NCProbeModelMdo.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/mdo/NCProbeModelMdo.scala
@@ -27,7 +27,10 @@ case class NCProbeModelMdo(
     @NCMdoField id: String,
     @NCMdoField name: String,
     @NCMdoField version: String,
-    @NCMdoField enabledBuiltInTokens: Set[String]
+    @NCMdoField enabledBuiltInTokens: Set[String],
+    @NCMdoField macros: Map[String, String],
+    @NCMdoField elementsSynonyms: Map[String, Seq[String]],
+    @NCMdoField intentsSamples: Map[String, Seq[String]]
 ) extends NCAnnotatedMdo[NCProbeModelMdo] {
     override def hashCode(): Int = s"$id$name".hashCode()
     
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/opencensus/NCOpenCensusServerStats.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/opencensus/NCOpenCensusServerStats.scala
index 7c12e16..b50aa66 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/opencensus/NCOpenCensusServerStats.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/opencensus/NCOpenCensusServerStats.scala
@@ -31,6 +31,7 @@ import io.opencensus.stats._
 trait NCOpenCensusServerStats {
     val M_ASK_LATENCY_MS: MeasureLong = MeasureLong.create("ask_latency", "The latency of '/ask' REST call", "ms")
     val M_CHECK_LATENCY_MS: MeasureLong = MeasureLong.create("check_latency", "The latency of '/check' REST call", "ms")
+    val M_SUGGESTION_LATENCY_MS: MeasureLong = MeasureLong.create("suggestion_latency", "The latency of '/suggestion' REST call", "ms")
     val M_CANCEL_LATENCY_MS: MeasureLong = MeasureLong.create("cancel_latency", "The latency of '/cancel' REST call", "ms")
     val M_SIGNIN_LATENCY_MS: MeasureLong = MeasureLong.create("signin_latency", "The latency of '/signin' REST call", "ms")
     val M_SIGNOUT_LATENCY_MS: MeasureLong = MeasureLong.create("signout_latency", "The latency of '/signout' REST call", "ms")
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/probe/NCProbeManager.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/probe/NCProbeManager.scala
index add4ea4..1eea061 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/probe/NCProbeManager.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/probe/NCProbeManager.scala
@@ -56,8 +56,8 @@ object NCProbeManager extends NCService {
     private[probe] object Config extends NCConfigurable {
         final private val pre = "nlpcraft.server.probe"
 
-        def getDnHostPort = getHostPort(s"$pre.links.downLink")
-        def getUpHostPort = getHostPort(s"$pre.links.upLink")
+        def getDnHostPort: (String, Integer) = getHostPort(s"$pre.links.downLink")
+        def getUpHostPort: (String, Integer) = getHostPort(s"$pre.links.upLink")
 
         def poolSize: Int = getInt(s"$pre.poolSize")
         def reconnectTimeoutMs: Long = getLong(s"$pre.reconnectTimeoutMs")
@@ -574,13 +574,34 @@ object NCProbeManager extends NCService {
                 respond("S2P_PROBE_VERSION_MISMATCH")
             else {
                 val models =
-                    hsMsg.data[List[(String, String, String, java.util.Set[String])]]("PROBE_MODELS").
-                        map { case (mdlId, mdlName, mdlVer, enabledBuiltInToks) ⇒
+                    hsMsg.data[
+                        List[(
+                            String,
+                            String,
+                            String,
+                            java.util.Set[String],
+                            java.util.Map[String, String],
+                            java.util.Map[String, java.util.List[String]],
+                            java.util.Map[String, java.util.List[String]]
+                        )]]("PROBE_MODELS").
+                        map {
+                            case (
+                                mdlId,
+                                mdlName,
+                                mdlVer,
+                                enabledBuiltInToks,
+                                macros,
+                                elemSyns,
+                                intentsSamples
+                            ) ⇒
                             NCProbeModelMdo(
                                 id = mdlId,
                                 name = mdlName,
                                 version = mdlVer,
-                                enabledBuiltInTokens = enabledBuiltInToks.asScala.toSet
+                                enabledBuiltInTokens = enabledBuiltInToks.asScala.toSet,
+                                macros = macros.asScala.toMap,
+                                elementsSynonyms = elemSyns.asScala.map(p ⇒ p._1 → p._2.asScala).toMap,
+                                intentsSamples.asScala.map(p ⇒ p._1 → p._2.asScala).toMap
                             )
                         }.toSet
 
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/rest/NCBasicRestApi.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/rest/NCBasicRestApi.scala
index 71c0678..b2425df 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/rest/NCBasicRestApi.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/rest/NCBasicRestApi.scala
@@ -37,6 +37,7 @@ import org.apache.nlpcraft.server.mdo.{NCQueryStateMdo, NCUserMdo}
 import org.apache.nlpcraft.server.opencensus.NCOpenCensusServerStats
 import org.apache.nlpcraft.server.probe.NCProbeManager
 import org.apache.nlpcraft.server.query.NCQueryManager
+import org.apache.nlpcraft.server.suggestion.NCSuggestionsManager
 import org.apache.nlpcraft.server.user.NCUserManager
 import spray.json.DefaultJsonProtocol._
 import spray.json.{JsValue, RootJsonFormat}
@@ -257,14 +258,14 @@ class NCBasicRestApi extends NCRestApi with LazyLogging with NCOpenCensusTrace w
       * @param acsTkn Access token to check.
       */
     @throws[NCE]
-    protected def authenticate(acsTkn: String): NCUserMdo = authenticate0(acsTkn, false)
+    protected def authenticate(acsTkn: String): NCUserMdo = authenticate0(acsTkn, shouldBeAdmin = false)
 
     /**
       *
       * @param acsTkn Access token to check.
       */
     @throws[NCE]
-    protected def authenticateAsAdmin(acsTkn: String): NCUserMdo = authenticate0(acsTkn, true)
+    protected def authenticateAsAdmin(acsTkn: String): NCUserMdo = authenticate0(acsTkn, shouldBeAdmin = true)
 
     /**
       * Checks length of field value.
@@ -618,7 +619,64 @@ class NCBasicRestApi extends NCRestApi with LazyLogging with NCOpenCensusTrace w
             }
         }
     }
-    
+
+    /**
+      *
+      * @return
+      */
+    protected def suggestion$(): Route = {
+        case class Req(
+            acsTok: String,
+            mdlId: String,
+            limitOpt: Option[Int],
+            minScoreOpt: Option[Double]
+        )
+
+        case class Suggestion(
+            suggestion: String,
+            score: Double,
+            suggestedCount: Int
+        )
+
+        case class Res(
+            status: String,
+            suggestions: Map[String, Seq[Suggestion]]
+        )
+
+        implicit val reqFmt: RootJsonFormat[Req] = jsonFormat4(Req)
+        implicit val fbFmt: RootJsonFormat[Suggestion] = jsonFormat3(Suggestion)
+        implicit val resFmt: RootJsonFormat[Res] = jsonFormat2(Res)
+
+        entity(as[Req]) { req ⇒
+            startScopedSpan(
+                "check$",
+                "mdlId" → req.mdlId,
+                "limit" → req.limitOpt.getOrElse(() ⇒ null),
+                "minScore" → req.minScoreOpt.getOrElse(() ⇒ null),
+                "acsTok" → req.acsTok
+            ) { span ⇒
+                checkLength("acsTok", req.acsTok, 256)
+                checkLength("mdlId", req.mdlId, 32)
+
+                val admin = authenticateAsAdmin(req.acsTok)
+
+                if (!NCProbeManager.getAllProbes(admin.companyId, span).exists(_.models.exists(_.id == req.mdlId)))
+                    throw new NCE(s"Probe not found: ${req.mdlId}")
+
+                val res: Map[String, Seq[Suggestion]] =
+                    NCSuggestionsManager.
+                        suggest(req.mdlId, req.limitOpt, req.minScoreOpt, span).
+                        map { case (elemId, suggs) ⇒
+                            elemId → suggs.map(p ⇒ Suggestion(p.suggestion, p.ctxWorldServerScore, p.suggestedCount))
+                        }.toMap
+
+                complete {
+                    Res(API_OK, res)
+                }
+            }
+        }
+    }
+
     /**
       *
       * @return
@@ -1718,40 +1776,43 @@ class NCBasicRestApi extends NCRestApi with LazyLogging with NCOpenCensusTrace w
                 }
             } ~
             post {
-                withRequestTimeoutResponse(_ ⇒ timeoutResp) {
-                    path(API / "signin") { withLatency(M_SIGNIN_LATENCY_MS, signin$) } ~
-                    path(API / "signout") { withLatency(M_SIGNOUT_LATENCY_MS, signout$) } ~ {
-                    path(API / "cancel") { withLatency(M_CANCEL_LATENCY_MS, cancel$) } ~
-                    path(API / "check") { withLatency(M_CHECK_LATENCY_MS, check$) } ~
-                    path(API / "clear"/ "conversation") { withLatency(M_CLEAR_CONV_LATENCY_MS, clear$Conversation) } ~
-                    path(API / "clear"/ "dialog") { withLatency(M_CLEAR_DIALOG_LATENCY_MS, clear$Dialog) } ~
-                    path(API / "company"/ "add") { withLatency(M_COMPANY_ADD_LATENCY_MS, company$Add) } ~
-                    path(API / "company"/ "get") { withLatency(M_COMPANY_GET_LATENCY_MS, company$Get) } ~
-                    path(API / "company" / "update") { withLatency(M_COMPANY_UPDATE_LATENCY_MS, company$Update) } ~
-                    path(API / "company" / "token" / "reset") { withLatency(M_COMPANY_TOKEN_LATENCY_MS, company$Token$Reset) } ~
-                    path(API / "company" / "delete") { withLatency(M_COMPANY_DELETE_LATENCY_MS, company$Delete) } ~
-                    path(API / "user" / "get") { withLatency(M_USER_GET_LATENCY_MS, user$Get) } ~
-                    path(API / "user" / "add") { withLatency(M_USER_ADD_LATENCY_MS, user$Add) } ~
-                    path(API / "user" / "update") { withLatency(M_USER_UPDATE_LATENCY_MS, user$Update) } ~
-                    path(API / "user" / "delete") { withLatency(M_USER_DELETE_LATENCY_MS, user$Delete) } ~
-                    path(API / "user" / "admin") { withLatency(M_USER_ADMIN_LATENCY_MS, user$Admin) } ~
-                    path(API / "user" / "passwd" / "reset") { withLatency(M_USER_PASSWD_RESET_LATENCY_MS, user$Password$Reset) } ~
-                    path(API / "user" / "all") { withLatency(M_USER_ALL_LATENCY_MS, user$All) } ~
-                    path(API / "feedback"/ "add") { withLatency(M_FEEDBACK_ADD_LATENCY_MS, feedback$Add) } ~
-                    path(API / "feedback"/ "all") { withLatency(M_FEEDBACK_GET_LATENCY_MS, feedback$All) } ~
-                    path(API / "feedback" / "delete") { withLatency(M_FEEDBACK_DELETE_LATENCY_MS, feedback$Delete) } ~
-                    path(API / "probe" / "all") { withLatency(M_PROBE_ALL_LATENCY_MS, probe$All) } ~
-                    path(API / "ask") { withLatency(M_ASK_LATENCY_MS, ask$) } ~
-                    (path(API / "ask" / "sync") &
-                        entity(as[JsValue]) &
-                        optionalHeaderValueByName("User-Agent") &
-                        extractClientIP
-                    ) {
-                        (req, userAgentOpt, rmtAddr) ⇒
-                            onSuccess(withLatency(M_ASK_SYNC_LATENCY_MS, ask$Sync(req, userAgentOpt, rmtAddr))) {
-                                js ⇒ complete(HttpResponse(entity = HttpEntity(ContentTypes.`application/json`, js)))
-                            }
-                    }}
+                withPrecompressedMediaTypeSupport {
+                    withRequestTimeoutResponse(_ ⇒ timeoutResp) {
+                        path(API / "signin") { withLatency(M_SIGNIN_LATENCY_MS, signin$) } ~
+                        path(API / "signout") { withLatency(M_SIGNOUT_LATENCY_MS, signout$) } ~ {
+                        path(API / "cancel") { withLatency(M_CANCEL_LATENCY_MS, cancel$) } ~
+                        path(API / "check") { withLatency(M_CHECK_LATENCY_MS, check$) } ~
+                        path(API / "suggestion") { withLatency(M_SUGGESTION_LATENCY_MS, suggestion$) } ~
+                        path(API / "clear"/ "conversation") { withLatency(M_CLEAR_CONV_LATENCY_MS, clear$Conversation) } ~
+                        path(API / "clear"/ "dialog") { withLatency(M_CLEAR_DIALOG_LATENCY_MS, clear$Dialog) } ~
+                        path(API / "company"/ "add") { withLatency(M_COMPANY_ADD_LATENCY_MS, company$Add) } ~
+                        path(API / "company"/ "get") { withLatency(M_COMPANY_GET_LATENCY_MS, company$Get) } ~
+                        path(API / "company" / "update") { withLatency(M_COMPANY_UPDATE_LATENCY_MS, company$Update) } ~
+                        path(API / "company" / "token" / "reset") { withLatency(M_COMPANY_TOKEN_LATENCY_MS, company$Token$Reset) } ~
+                        path(API / "company" / "delete") { withLatency(M_COMPANY_DELETE_LATENCY_MS, company$Delete) } ~
+                        path(API / "user" / "get") { withLatency(M_USER_GET_LATENCY_MS, user$Get) } ~
+                        path(API / "user" / "add") { withLatency(M_USER_ADD_LATENCY_MS, user$Add) } ~
+                        path(API / "user" / "update") { withLatency(M_USER_UPDATE_LATENCY_MS, user$Update) } ~
+                        path(API / "user" / "delete") { withLatency(M_USER_DELETE_LATENCY_MS, user$Delete) } ~
+                        path(API / "user" / "admin") { withLatency(M_USER_ADMIN_LATENCY_MS, user$Admin) } ~
+                        path(API / "user" / "passwd" / "reset") { withLatency(M_USER_PASSWD_RESET_LATENCY_MS, user$Password$Reset) } ~
+                        path(API / "user" / "all") { withLatency(M_USER_ALL_LATENCY_MS, user$All) } ~
+                        path(API / "feedback"/ "add") { withLatency(M_FEEDBACK_ADD_LATENCY_MS, feedback$Add) } ~
+                        path(API / "feedback"/ "all") { withLatency(M_FEEDBACK_GET_LATENCY_MS, feedback$All) } ~
+                        path(API / "feedback" / "delete") { withLatency(M_FEEDBACK_DELETE_LATENCY_MS, feedback$Delete) } ~
+                        path(API / "probe" / "all") { withLatency(M_PROBE_ALL_LATENCY_MS, probe$All) } ~
+                        path(API / "ask") { withLatency(M_ASK_LATENCY_MS, ask$) } ~
+                        (path(API / "ask" / "sync") &
+                            entity(as[JsValue]) &
+                            optionalHeaderValueByName("User-Agent") &
+                            extractClientIP
+                        ) {
+                            (req, userAgentOpt, rmtAddr) ⇒
+                                onSuccess(withLatency(M_ASK_SYNC_LATENCY_MS, ask$Sync(req, userAgentOpt, rmtAddr))) {
+                                    js ⇒ complete(HttpResponse(entity = HttpEntity(ContentTypes.`application/json`, js)))
+                                }
+                        }}
+                    }
                 }
             }
         )
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/mdo/NCProbeModelMdo.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/suggestion/NCSuggestion.scala
similarity index 58%
copy from nlpcraft/src/main/scala/org/apache/nlpcraft/server/mdo/NCProbeModelMdo.scala
copy to nlpcraft/src/main/scala/org/apache/nlpcraft/server/suggestion/NCSuggestion.scala
index 1510c4b..7ea0d28 100644
--- a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/mdo/NCProbeModelMdo.scala
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/suggestion/NCSuggestion.scala
@@ -15,26 +15,18 @@
  * limitations under the License.
  */
 
-package org.apache.nlpcraft.server.mdo
-
-import org.apache.nlpcraft.server.mdo.impl._
+package org.apache.nlpcraft.server.suggestion
 
 /**
-  * Probe model MDO.
+  * TODO:
+  * @param suggestion
+  * @param ctxWorldServerScore
+  * @param suggestedCount
+  * @param totalScore
   */
-@NCMdoEntity(sql = false)
-case class NCProbeModelMdo(
-    @NCMdoField id: String,
-    @NCMdoField name: String,
-    @NCMdoField version: String,
-    @NCMdoField enabledBuiltInTokens: Set[String]
-) extends NCAnnotatedMdo[NCProbeModelMdo] {
-    override def hashCode(): Int = s"$id$name".hashCode()
-    
-    override def equals(obj: Any): Boolean = {
-        obj match {
-            case x: NCProbeModelMdo ⇒ x.id == id
-            case _ ⇒ false
-        }
-    }
-}
+case class NCSuggestion(
+    suggestion: String,
+    ctxWorldServerScore: Double,
+    suggestedCount: Int,
+    totalScore: Double
+)
diff --git a/nlpcraft/src/main/scala/org/apache/nlpcraft/server/suggestion/NCSuggestionsManager.scala b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/suggestion/NCSuggestionsManager.scala
new file mode 100644
index 0000000..f9d184a
--- /dev/null
+++ b/nlpcraft/src/main/scala/org/apache/nlpcraft/server/suggestion/NCSuggestionsManager.scala
@@ -0,0 +1,315 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.nlpcraft.server.suggestion
+
+import java.util.concurrent.atomic.AtomicInteger
+import java.util.concurrent.{CopyOnWriteArrayList, CountDownLatch, TimeUnit}
+import java.util.{List ⇒ JList}
+
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import io.opencensus.trace.Span
+import org.apache.http.HttpResponse
+import org.apache.http.client.ResponseHandler
+import org.apache.http.client.methods.HttpPost
+import org.apache.http.entity.StringEntity
+import org.apache.http.impl.client.HttpClients
+import org.apache.http.util.EntityUtils
+import org.apache.nlpcraft.common.config.NCConfigurable
+import org.apache.nlpcraft.common.makro.NCMacroParser
+import org.apache.nlpcraft.common.nlp.core.NCNlpPorterStemmer
+import org.apache.nlpcraft.common.util.NCUtils
+import org.apache.nlpcraft.common.{NCE, NCService}
+import org.apache.nlpcraft.server.probe.NCProbeManager
+
+import scala.collection.JavaConverters._
+import scala.collection._
+
+/**
+  * TODO:
+  */
+object NCSuggestionsManager extends NCService  {
+    private final val DFLT_LIMIT: Int = 20
+    private final val DFLT_MIN_SCORE: Double = 0
+
+    private object Config extends NCConfigurable {
+        val urlOpt: Option[String] = getStringOpt("nlpcraft.server.ctxword.url")
+    }
+
+    case class Suggestion(word: String, score: Double)
+    case class RequestData(sentence: String, example: String, elementId: String, index: Int)
+    case class RestRequestSentence(text: String, indexes: JList[Int])
+    case class RestRequest(sentences: JList[RestRequestSentence], limit: Int, min_score: Double)
+    case class Word(word: String, stem: String) {
+        require(!word.contains(" "), s"Word cannot contains spaces: $word")
+        require(
+            word.forall(ch ⇒
+                ch.isLetterOrDigit ||
+                    ch == '\'' ||
+                    SEPARATORS.contains(ch)
+            ),
+            s"Unsupported symbols: $word"
+        )
+    }
+
+    private final val GSON = new Gson
+    private final val TYPE_RESP = new TypeToken[JList[JList[Suggestion]]]() {}.getType
+    private final val SEPARATORS = Seq('?', ',', '.', '-', '!')
+    private final val BATCH_SIZE = 20
+
+    private final val HANDLER: ResponseHandler[Seq[Seq[Suggestion]]] =
+        (resp: HttpResponse) ⇒ {
+            val code = resp.getStatusLine.getStatusCode
+            val e = resp.getEntity
+
+            val js = if (e != null) EntityUtils.toString(e) else null
+
+            if (js == null)
+                throw new RuntimeException(s"Unexpected empty response [code=$code]")
+
+            code match {
+                case 200 ⇒
+                    val data: JList[JList[Suggestion]] = GSON.fromJson(js, TYPE_RESP)
+
+                    data.asScala.map(p ⇒ if (p.isEmpty) Seq.empty else p.asScala.tail)
+
+                case 400 ⇒ throw new RuntimeException(js)
+                case _ ⇒ throw new RuntimeException(s"Unexpected response [code=$code, response=$js]")
+            }
+        }
+
+    private def split(s: String): Seq[String] = s.split(" ").toSeq.map(_.trim).filter(_.nonEmpty)
+    private def toStem(s: String): String = split(s).map(NCNlpPorterStemmer.stem).mkString(" ")
+    private def toStemWord(s: String): String = NCNlpPorterStemmer.stem(s)
+
+    private def getAllSlices(seq1: Seq[String], seq2: Seq[String]): Seq[Int] = {
+        val seq = mutable.Buffer.empty[Int]
+
+        var i = seq1.indexOfSlice(seq2)
+
+        while (i >= 0) {
+            seq += i
+
+            i = seq1.indexOfSlice(seq2, i + 1)
+        }
+
+        seq
+    }
+
+    @throws[NCE]
+    def suggest(
+        mdlId: String, limitOpt: Option[Int], minScoreOpt: Option[Double], parent: Span = null
+    ): Map[String, Seq[NCSuggestion]] =
+        startScopedSpan(
+            "suggest",
+            parent,
+            "modelId" → mdlId,
+            "limit" → limitOpt.getOrElse(() ⇒ null),
+            "minScore" → minScoreOpt.getOrElse(() ⇒ null)
+        ) { _ ⇒
+            var url = Config.urlOpt.getOrElse(throw new NCE("Context word server is not configured"))
+
+            url = s"$url/suggestions"
+
+            val mdl = NCProbeManager.getModel(mdlId)
+
+            val parser = new NCMacroParser()
+
+            if (mdl.macros != null)
+                mdl.macros.foreach { case (name, str) ⇒ parser.addMacro(name, str) }
+
+            val examples =
+                mdl.
+                    intentsSamples.
+                    flatMap(_._2).
+                    map(ex ⇒ SEPARATORS.foldLeft(ex)((s, ch) ⇒ s.replaceAll(s"\\$ch", s" $ch "))).
+                    map(ex ⇒ {
+                        val seq = ex.split(" ")
+
+                        seq → seq.map(toStemWord)
+                    }).
+                    toMap
+
+            val elemSyns =
+                mdl.elementsSynonyms.map { case (elemId, syns) ⇒ elemId → syns.flatMap(parser.expand) }.
+                    map { case (id, seq) ⇒ id → seq.map(txt ⇒ split(txt).map(p ⇒ Word(p, toStemWord(p)))) }
+
+            val allReqs =
+                elemSyns.map {
+                    case (elemId, syns) ⇒
+                        val normSyns: Seq[Seq[Word]] = syns.filter(_.size == 1)
+                        val synsStems = normSyns.map(_.map(_.stem))
+                        val synsWords = normSyns.map(_.map(_.word))
+
+                        val reqs =
+                            examples.flatMap { case (exampleWords, exampleStems) ⇒
+                                val exampleIdxs = synsStems.flatMap(synStems ⇒ getAllSlices(exampleStems, synStems))
+
+                                def mkRequestData(idx: Int, synStems: Seq[String], synStemsIdx: Int): RequestData = {
+                                    val fromIncl = idx
+                                    val toExcl = idx + synStems.length
+
+                                    RequestData(
+                                        sentence = exampleWords.zipWithIndex.flatMap {
+                                            case (exampleWord, i) ⇒
+                                                i match {
+                                                    case x if x == fromIncl ⇒ synsWords(synStemsIdx)
+                                                    case x if x > fromIncl && x < toExcl ⇒ Seq.empty
+                                                    case _ ⇒ Seq(exampleWord)
+                                                }
+                                        }.mkString(" "),
+                                        example = exampleWords.mkString(" "),
+                                        elementId = elemId,
+                                        index = idx
+                                    )
+                                }
+
+                                (for (idx ← exampleIdxs; (synStems, i) ← synsStems.zipWithIndex)
+                                    yield mkRequestData(idx, synStems, i)).distinct
+                            }
+
+                        elemId → reqs.toSet
+                }.filter(_._2.nonEmpty)
+
+            val allReqsCnt = allReqs.map(_._2.size).sum
+
+            logger.info(s"Examples count: ${examples.size}")
+            logger.info(s"Synonyms count: ${elemSyns.map(_._2.size).sum}")
+            logger.info(s"Requests prepared: $allReqsCnt")
+
+            val allSuggs = new java.util.concurrent.ConcurrentHashMap[String, JList[Suggestion]]()
+            val cdl = new CountDownLatch(1)
+            val debugs = mutable.HashMap.empty[RequestData, Seq[Suggestion]]
+            val cnt = new AtomicInteger(0)
+
+            val client = HttpClients.createDefault
+
+            for ((elemId, reqs) ← allReqs; batch ← reqs.sliding(BATCH_SIZE, BATCH_SIZE).map(_.toSeq)) {
+                NCUtils.asFuture(
+                    _ ⇒ {
+                        val post = new HttpPost(url)
+
+                        post.setHeader("Content-Type", "application/json")
+                        post.setEntity(
+                            new StringEntity(
+                                GSON.toJson(
+                                    RestRequest(
+                                        sentences = batch.map(p ⇒ RestRequestSentence(p.sentence, Seq(p.index).asJava)).asJava,
+                                        min_score = minScoreOpt.getOrElse(DFLT_MIN_SCORE),
+                                        limit = limitOpt.getOrElse(DFLT_LIMIT)
+                                    )
+                                ),
+                                "UTF-8"
+                            )
+                        )
+
+                        val resps: Seq[Seq[Suggestion]] =
+                            try
+                                client.execute(post, HANDLER)
+                            finally
+                                post.releaseConnection()
+
+                        require(batch.size == resps.size, s"Batch: ${batch.size}, responses: ${resps.size}")
+
+                        reqs.zip(resps).foreach { case (req, resp) ⇒ debugs += req → resp}
+
+                        val i = cnt.addAndGet(batch.size)
+
+                        logger.info(s"Executed: $i requests.")
+
+                        allSuggs.
+                            computeIfAbsent(elemId, (_: String) ⇒ new CopyOnWriteArrayList[Suggestion]()).
+                            addAll(resps.flatten.asJava)
+
+                        if (i == allReqsCnt)
+                            cdl.countDown()
+                    },
+                    (e: Throwable) ⇒ {
+                        logger.error("Error execution request", e)
+
+                        cdl.countDown()
+                    },
+                    (_: Unit) ⇒ ()
+                )
+            }
+
+            cdl.await(Long.MaxValue, TimeUnit.MILLISECONDS)
+
+            val allSynsStems = elemSyns.flatMap(_._2).flatten.map(_.stem).toSet
+
+            val nonEmptySuggs = allSuggs.asScala.map(p ⇒ p._1 → p._2.asScala).filter(_._2.nonEmpty)
+
+            val res = mutable.HashMap.empty[String, mutable.ArrayBuffer[NCSuggestion]]
+
+            nonEmptySuggs.
+                foreach { case (elemId, elemSuggs) ⇒
+                    elemSuggs.
+                        map(sugg ⇒ (sugg, toStem(sugg.word))).
+                        groupBy { case (_, stem) ⇒ stem }.
+                        filter { case (stem, _) ⇒ !allSynsStems.contains(stem) }.
+                        map { case (_, group) ⇒
+                            val seq = group.map { case (sugg, _) ⇒ sugg }.sortBy(-_.score)
+
+                            // Drops repeated.
+                            (seq.head, seq.length)
+                        }.
+                        toSeq.
+                        map { case (sugg, cnt) ⇒ (sugg, cnt, sugg.score * cnt / elemSuggs.size) }.
+                        sortBy { case (_, _, sumFactor) ⇒ -sumFactor }.
+                        zipWithIndex.
+                        foreach { case ((sugg, cnt, sumFactor), _) ⇒
+                            val seq =
+                                res.get(elemId) match {
+                                    case Some(seq) ⇒ seq
+                                    case None ⇒
+                                        val buf = mutable.ArrayBuffer.empty[NCSuggestion]
+
+                                        res += elemId → buf
+
+                                        buf
+                                }
+
+                            seq += NCSuggestion(sugg.word, sugg.score, cnt, sumFactor)
+                        }
+                }
+
+            logger.whenInfoEnabled({
+                var i = 1
+
+                debugs.groupBy(_._1.example).foreach { case (_, m) ⇒
+                    m.toSeq.sortBy(_._1.sentence).foreach { case (req, suggs) ⇒
+                        val s =
+                            split(req.sentence).
+                                zipWithIndex.map { case (w, i) ⇒ if (i == req.index) s"<<<$w>>>" else w }.
+                                mkString(" ")
+
+                        logger.info(
+                            s"$i. " +
+                                s"Request=$s, " +
+                                s"suggestions=[${suggs.map(_.word).mkString(", ")}], " +
+                                s"element=${req.elementId}"
+                        )
+
+                        i = i + 1
+                    }
+                }
+            })
+
+            res
+        }
+}