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/05/10 16:50:19 UTC

[incubator-nlpcraft] 02/02: SQL test model refactoring. SQL developer tests added, initial version.

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

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

commit dc1157a203ffc3d14bbefd8b514be74533a9cd00
Author: Sergey Kamov <se...@apache.org>
AuthorDate: Sun May 10 19:50:08 2020 +0300

    SQL test model refactoring. SQL developer tests added, initial version.
---
 .../apache/nlpcraft/examples/sql/SqlModel.scala    |   5 +-
 .../nlpcraft/examples/sql/SqlModelTest.scala       |  92 ++++++++++
 .../nlpcraft/examples/sql/db/SqlBuilder.scala      |  97 +++++-----
 .../nlpcraft/examples/sql/db/sample_questions.txt  |   3 -
 .../apache/nlpcraft/examples/sql/sql_model.yaml    |   2 +-
 .../nlpcraft/examples/sql/NCSqlModelSpec.scala     | 200 +++++++++++++++++++++
 .../mgrs/nlp/enrichers/NCEnricherBaseSpec.scala    |  10 +-
 7 files changed, 355 insertions(+), 54 deletions(-)

diff --git a/src/main/scala/org/apache/nlpcraft/examples/sql/SqlModel.scala b/src/main/scala/org/apache/nlpcraft/examples/sql/SqlModel.scala
index 5c79878..bce080a 100644
--- a/src/main/scala/org/apache/nlpcraft/examples/sql/SqlModel.scala
+++ b/src/main/scala/org/apache/nlpcraft/examples/sql/SqlModel.scala
@@ -145,7 +145,7 @@ class SqlModel extends NCModelFileAdapter("org/apache/nlpcraft/examples/sql/sql_
             groupBy { case (col, _) ⇒ col }.
             map { case (col, seq) ⇒ SqlInCondition(col, seq.map { case (_, value) ⇒ value})}.toSeq
 
-    private def select0(
+    protected def select0(
         ext: NCSqlExtractor,
         tabs: Seq[NCToken],
         cols: Seq[NCToken],
@@ -191,7 +191,7 @@ class SqlModel extends NCModelFileAdapter("org/apache/nlpcraft/examples/sql/sql_
         }
     }
 
-
+    // General case.
     @NCIntent(
         "intent=commonReport conv=true " +
         "term(tabs)={groups @@ 'table'}[0,7] " +
@@ -232,6 +232,7 @@ class SqlModel extends NCModelFileAdapter("org/apache/nlpcraft/examples/sql/sql_
         )
     }
 
+    // Case with implicit sort.
     @NCIntent(
         "intent=customSortReport conv=true " +
         "term(sort)={id == 'sort:best' || id == 'sort:worst'} " +
diff --git a/src/main/scala/org/apache/nlpcraft/examples/sql/SqlModelTest.scala b/src/main/scala/org/apache/nlpcraft/examples/sql/SqlModelTest.scala
index feb9892..f6294f1 100644
--- a/src/main/scala/org/apache/nlpcraft/examples/sql/SqlModelTest.scala
+++ b/src/main/scala/org/apache/nlpcraft/examples/sql/SqlModelTest.scala
@@ -589,6 +589,98 @@ class SqlModelTest {
                   |LIMIT
                   |  25
                   """.stripMargin
+            ),
+            Case(
+                Seq(
+                    "What are the best performing products for the last quarter?"
+                ),
+                """SELECT
+                  |  orders.freight,
+                  |  orders.order_date,
+                  |  orders.order_id,
+                  |  orders.required_date,
+                  |  products.product_id,
+                  |  products.product_name,
+                  |  products.quantity_per_unit,
+                  |  categories.category_id,
+                  |  categories.category_name,
+                  |  categories.description,
+                  |  customers.customer_id,
+                  |  customers.company_name,
+                  |  customers.contact_name,
+                  |  employees.employee_id,
+                  |  employees.last_name,
+                  |  employees.first_name,
+                  |  order_details.unit_price,
+                  |  order_details.quantity,
+                  |  order_details.discount,
+                  |  shippers.shipper_id,
+                  |  shippers.company_name,
+                  |  shippers.phone,
+                  |  suppliers.supplier_id,
+                  |  suppliers.company_name,
+                  |  suppliers.contact_name
+                  |FROM
+                  |  order_details
+                  |  INNER JOIN orders ON order_details.order_id = orders.order_id
+                  |  INNER JOIN products ON order_details.product_id = products.product_id
+                  |  LEFT JOIN suppliers ON products.supplier_id = suppliers.supplier_id
+                  |  LEFT JOIN categories ON products.category_id = categories.category_id
+                  |  LEFT JOIN customers ON orders.customer_id = customers.customer_id
+                  |  LEFT JOIN shippers ON orders.ship_via = shippers.shipper_id
+                  |  LEFT JOIN employees ON orders.employee_id = employees.employee_id
+                  |WHERE
+                  |  orders.order_date >= ?
+                  |  AND orders.order_date <= ?
+                  |ORDER BY
+                  |  orders.freight DESC
+                  |LIMIT
+                  |  1000
+                  """.stripMargin
+            ),
+            Case(
+                Seq(
+                    "What are the least performing categories for the last quarter?"
+                ),
+                """SELECT
+                  |  orders.freight,
+                  |  orders.order_date,
+                  |  categories.category_id,
+                  |  categories.category_name,
+                  |  categories.description,
+                  |  orders.order_id,
+                  |  orders.required_date,
+                  |  customers.customer_id,
+                  |  customers.company_name,
+                  |  customers.contact_name,
+                  |  employees.employee_id,
+                  |  employees.last_name,
+                  |  employees.first_name,
+                  |  order_details.unit_price,
+                  |  order_details.quantity,
+                  |  order_details.discount,
+                  |  products.product_id,
+                  |  products.product_name,
+                  |  products.quantity_per_unit,
+                  |  shippers.shipper_id,
+                  |  shippers.company_name,
+                  |  shippers.phone
+                  |FROM
+                  |  products
+                  |  LEFT JOIN categories ON products.category_id = categories.category_id
+                  |  INNER JOIN order_details ON order_details.product_id = products.product_id
+                  |  INNER JOIN orders ON order_details.order_id = orders.order_id
+                  |  LEFT JOIN customers ON orders.customer_id = customers.customer_id
+                  |  LEFT JOIN shippers ON orders.ship_via = shippers.shipper_id
+                  |  LEFT JOIN employees ON orders.employee_id = employees.employee_id
+                  |WHERE
+                  |  orders.order_date >= ?
+                  |  AND orders.order_date <= ?
+                  |ORDER BY
+                  |  orders.freight ASC
+                  |LIMIT
+                  |  1000
+                  """.stripMargin
             )
         )
     }
diff --git a/src/main/scala/org/apache/nlpcraft/examples/sql/db/SqlBuilder.scala b/src/main/scala/org/apache/nlpcraft/examples/sql/db/SqlBuilder.scala
index 46970c3..181efb6 100644
--- a/src/main/scala/org/apache/nlpcraft/examples/sql/db/SqlBuilder.scala
+++ b/src/main/scala/org/apache/nlpcraft/examples/sql/db/SqlBuilder.scala
@@ -85,23 +85,15 @@ case class SqlBuilder(schema: NCSqlSchema) extends LazyLogging {
     private def sql(tabs: Seq[NCSqlTable]): String = {
         val names = tabs.map(_.getTable)
 
-        println("names-" + names.mkString("|"))
-        println("joins=\n" + schemaJoins.map(p ⇒ s"${p.getFromTable} - ${p.getToTable}").mkString("\n"))
-        println()
-
-        val refs = names.
-            flatMap(t ⇒ schemaJoins.filter(j ⇒ j.getFromTable == t && names.contains(j.getToTable))).
-            sortBy(_.getFromTable).
-            zipWithIndex.map { case (join, idx) ⇒
-        }
-
         names.size match {
             case 0 ⇒ throw new AssertionError(s"Unexpected empty tables")
             case 1 ⇒ names.head
             case _ ⇒
+                val used = mutable.HashSet.empty[String]
+
                 val refs = names.
                     flatMap(t ⇒ schemaJoins.filter(j ⇒ j.getFromTable == t && names.contains(j.getToTable))).
-                    sortBy(_.getFromTable).
+                    sortBy(j ⇒ Math.min(names.indexOf(j.getFromTable), names.indexOf(j.getToTable))).
                     zipWithIndex.
                     map { case (join, idx) ⇒
                         val fromCols = join.getFromColumns.asScala
@@ -113,14 +105,33 @@ case class SqlBuilder(schema: NCSqlSchema) extends LazyLogging {
                         val fromTab = join.getFromTable
                         val toTab = join.getToTable
 
-                        println(s"fromTab=$fromTab, toTab=$toTab")
-
                         val onCondition = fromCols.zip(toCols).
                             map { case (fromCol, toCol) ⇒ s"$fromTab.$fromCol = $toTab.$toCol" }.mkString(" AND ")
 
-                        val from = if (idx == 0) fromTab else ""
+                        if (idx == 0) {
+                            used += fromTab
+                            used += toTab
 
-                        s"$from ${sql(join.getType)} $toTab ON $onCondition"
+                            s"$fromTab ${sql(join.getType)} $toTab ON $onCondition"
+                        }
+                        else {
+                            val fromAdded = used.add(fromTab)
+                            val toAdded = used.add(toTab)
+
+                            if (fromAdded) {
+                                val reverseType =
+                                    join.getType match {
+                                        case LEFT ⇒ RIGHT
+                                        case RIGHT ⇒ LEFT
+                                        case _ ⇒ join.getType
+                                    }
+                                s"${sql(reverseType)} $fromTab ON $onCondition"
+                            }
+                            else if (toAdded)
+                                s"${sql(join.getType)} $toTab ON $onCondition"
+                            else
+                                ""
+                        }
                     }
 
                 if (refs.length != names.length - 1)
@@ -215,25 +226,21 @@ case class SqlBuilder(schema: NCSqlSchema) extends LazyLogging {
         )
 
     private def findDateColumn(initTabs: Seq[NCSqlTable]): Option[NCSqlColumn] =
-        freeDateRangeOpt match {
-            case Some(_) ⇒
-                schemaTabs.sortBy(t ⇒ {
-                    // The simple algorithm, which tries to find most suitable date type column for free date condition.
-                    // Higher priority for tables which were detected initially.
-                    val weight1 = if (initTabs.contains(t)) 0 else 1
-                    // Higher priority for tables don't have references from another.
-                    // Probably these tables can have more specific records.
-                    val weight2 = if (!schemaJoins.exists(_.getToTable == t.getTable)) 0 else 1
-
-                    (weight1, weight2)
-                }).flatMap(_.getDefaultDate.asScala).toStream.headOption match {
-                    case Some(col) ⇒ Some(col)
-                    case None ⇒
-                        logger.warn("Free date condition ignored without throwing exceptions.")
-
-                        None
-                }
-            case None ⇒ None
+        schemaTabs.sortBy(t ⇒ {
+            // The simple algorithm, which tries to find most suitable date type column for free date condition.
+            // Higher priority for tables which were detected initially.
+            val weight1 = if (initTabs.contains(t)) 0 else 1
+            // Higher priority for tables which don't have references from another.
+            // Probably these tables can have more specific records.
+            val weight2 = if (!schemaJoins.exists(_.getToTable == t.getTable)) 0 else 1
+
+            (weight1, weight2)
+        }).flatMap(_.getDefaultDate.asScala).toStream.headOption match {
+            case Some(col) ⇒ Some(col)
+            case None ⇒
+                logger.warn("Free date condition ignored without throwing exceptions.")
+
+                None
         }
 
     private def extendConditions(
@@ -318,20 +325,24 @@ case class SqlBuilder(schema: NCSqlSchema) extends LazyLogging {
         condsNorm.foreach(cond ⇒ { tabsNorm += schemaTabsByNames(cond.column.getTable); colsNorm += cond.column })
         sortsNorm.foreach(sort ⇒ { tabsNorm += schemaTabsByNames(sort.getColumn.getTable); colsNorm += sort.getColumn })
 
-        val freeDateColOpt = findDateColumn(tabsNorm)
-
-        freeDateColOpt.toSeq.foreach(col ⇒ { tabsNorm += schemaTabsByNames(col.getTable); colsNorm += col })
+        var freeDateColOpt =
+            if (freeDateRangeOpt.isDefined) tabsNorm.flatMap(_.getDefaultDate.asScala).toStream.headOption else None
 
         tabsNorm = tabsNorm.distinct
-        colsNorm = colsNorm.distinct
-        condsNorm = condsNorm.distinct
-        sortsNorm = sortsNorm.distinct
-
-        println("tabsNorm="+tabsNorm.map(_.getTable).mkString("|"))
 
         val extTabs = extendTables(tabsNorm)
 
-        println("extTabs="+extTabs.map(_.getTable).mkString("|"))
+        if (freeDateColOpt.isEmpty && freeDateRangeOpt.isDefined) {
+            freeDateColOpt = findDateColumn(tabsNorm)
+
+            freeDateColOpt.toSeq.foreach(col ⇒ { tabsNorm += schemaTabsByNames(col.getTable); colsNorm += col })
+
+            tabsNorm = tabsNorm.distinct
+        }
+
+        colsNorm = colsNorm.distinct
+        condsNorm = condsNorm.distinct
+        sortsNorm = sortsNorm.distinct
 
         val extCols = extendColumns(colsNorm, extTabs, freeDateColOpt)
         val (extConds, extParams) = extendConditions(condsNorm, extTabs, tabsNorm, freeDateColOpt)
diff --git a/src/main/scala/org/apache/nlpcraft/examples/sql/db/sample_questions.txt b/src/main/scala/org/apache/nlpcraft/examples/sql/db/sample_questions.txt
index cb807be..a946c6a 100644
--- a/src/main/scala/org/apache/nlpcraft/examples/sql/db/sample_questions.txt
+++ b/src/main/scala/org/apache/nlpcraft/examples/sql/db/sample_questions.txt
@@ -13,6 +13,3 @@ What are the best/least performing products/categories/employee/territory/region
   What are the least performing region for the last quarter?
 
 What are the top 5 best/worst customers in CA for the last 90 days?
-
-What are the busiest shipper/supplier in CA for the last 90 days?
-What is the average/maximum/minimum unit price/discount/quantity in CA for the last 90 days?
\ No newline at end of file
diff --git a/src/main/scala/org/apache/nlpcraft/examples/sql/sql_model.yaml b/src/main/scala/org/apache/nlpcraft/examples/sql/sql_model.yaml
index f0daadc..ee317c2 100644
--- a/src/main/scala/org/apache/nlpcraft/examples/sql/sql_model.yaml
+++ b/src/main/scala/org/apache/nlpcraft/examples/sql/sql_model.yaml
@@ -1472,7 +1472,7 @@ elements:
   groups:
     - "Sort criteria"
   synonyms:
-    - "worst {performing|*}"
+    - "{worst|least} {performing|*}"
 enabledBuiltInTokens:
   - "nlpcraft:date"
   - "nlpcraft:num"
diff --git a/src/test/scala/org/apache/nlpcraft/examples/sql/NCSqlModelSpec.scala b/src/test/scala/org/apache/nlpcraft/examples/sql/NCSqlModelSpec.scala
new file mode 100644
index 0000000..695a2ae
--- /dev/null
+++ b/src/test/scala/org/apache/nlpcraft/examples/sql/NCSqlModelSpec.scala
@@ -0,0 +1,200 @@
+/*
+ * 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.examples.sql
+
+import java.util
+
+import org.apache.nlpcraft.examples.sql.db.SqlServer
+import org.apache.nlpcraft.model.NCElement
+import org.apache.nlpcraft.probe.mgrs.nlp.enrichers.NCTestSortTokenType._
+import org.apache.nlpcraft.probe.mgrs.nlp.enrichers.{NCDefaultTestModel, NCEnricherBaseSpec, NCTestDateToken => dte, NCTestLimitToken => lim, NCTestNlpToken => nlp, NCTestSortToken => srt, NCTestUserToken => usr}
+import org.junit.jupiter.api.{AfterEach, BeforeEach, Test}
+
+class NCSqlModelWrapper extends NCDefaultTestModel {
+    private val delegate = new SqlModel()
+
+    override def getElements: util.Set[NCElement] = delegate.getElements
+
+    override def getMacros: util.Map[String, String] = delegate.getMacros
+
+    override def getExamples: util.Set[String] = delegate.getExamples
+}
+
+class NCSqlModelSpec extends NCEnricherBaseSpec {
+    override def getModelClass: Option[Class[_ <: NCDefaultTestModel]] = Some(classOf[NCSqlModelWrapper])
+
+    @BeforeEach
+    override def setUp(): Unit = {
+        SqlServer.start()
+
+        super.setUp()
+    }
+
+    @AfterEach
+    override def tearDown(): Unit = {
+        super.tearDown()
+
+        SqlServer.stop()
+    }
+
+    /**
+      *
+      * @throws Exception
+      */
+    @Test
+    def test(): Unit = {
+        runBatch(
+            _ ⇒ checkExists(
+                txt = "order date",
+                usr(text = "order date", id = "col:date")
+            ),
+            _ ⇒ checkExists(
+                txt = "show me the order dates",
+                nlp(text = "show me the", isStop = true),
+                usr(text = "order dates", id = "col:date")
+            ),
+            _ ⇒ checkExists(
+                txt = "list dates of orders",
+                nlp(text = "list", isStop = true),
+                usr(text = "dates orders", id = "col:date"),
+                nlp(text = "of")
+            ),
+            _ ⇒ checkExists(
+                txt = "orders for last month",
+                usr(text = "orders", id = "tbl:orders"),
+                dte(text = "for last month")
+            ),
+            _ ⇒ checkExists(
+                txt = "shippers data",
+                usr(text = "shippers", id = "tbl:shippers"),
+                nlp(text = "data")
+            ),
+            _ ⇒ checkExists(
+                txt = "show me orders with freight more than 10 for last year",
+                nlp(text = "show me", isStop = true),
+                srt(text = "orders with", typ = BY_ONLY, note = "condition:num", index = 2),
+                usr(text = "freight more than 10", id = "condition:num"),
+                dte(text = "for last year")
+            ),
+            _ ⇒ checkExists(
+                txt = "territories data",
+                usr(text = "territories", id = "tbl:territories"),
+                nlp(text = "data")
+            ),
+            _ ⇒ checkExists(
+                txt = "employees territories",
+                usr(text = "employees territories", id = "tbl:employee_territories")
+            ),
+            _ ⇒ checkExists(
+                txt = "10 suppliers",
+                lim(text = "10", limit=10, index =1, note="tbl:suppliers"),
+                usr(text = "suppliers", id = "tbl:suppliers")
+            ),
+            // TODO: ? best ?variant
+            _ ⇒ checkExists(
+                txt = "last year Exotic Liquids orders",
+                dte(text="last year"),
+                usr(text = "Exotic Liquids", id = "condition:value"),
+                srt(text = "orders", typ = SUBJ_ONLY, note = "condition:value", index = 1)
+            ),
+            _ ⇒ checkExists(
+                txt = "give me the orders sorted by ship date",
+                nlp(text = "give me the", isStop = true),
+                usr(text = "orders", id = "tbl:orders"),
+                srt(text = "sorted by", typ = BY_ONLY, note = "col:date", index = 3),
+                usr(text = "ship date", id = "col:date"),
+            ),
+            _ ⇒ checkExists(
+                txt = "give me the orders sorted by ship date",
+                nlp(text = "give me the", isStop = true),
+                usr(text = "orders", id = "tbl:orders"),
+                srt(text = "sorted by", typ = BY_ONLY, note = "col:date", index = 3),
+                usr(text = "ship date", id = "col:date"),
+            ),
+            _ ⇒ checkExists(
+                txt = "give me the orders sorted by ship date asc",
+                nlp(text = "give me the", isStop = true),
+                usr(text = "orders", id = "tbl:orders"),
+                srt(text = "sorted by", typ = BY_ONLY, note = "col:date", index = 3, asc = true),
+                usr(text = "ship date", id = "col:date"),
+                nlp(text = "asc", isStop = true)
+            ),
+            _ ⇒ checkExists(
+                txt = "give me the orders sorted by date",
+                nlp(text = "give me the", isStop = true),
+                usr(text = "orders date", id = "col:date"),
+                nlp(text = "sorted"),
+                nlp(text = "by")
+            ),
+            // TODO: winner?
+            _ ⇒ checkExists(
+                txt = "What are the top orders for the last 2 weeks sorted by order quantity?",
+                lim(text = "What are the top", limit = 10, index = 1, note = "tbl:orders", asc = false),
+                usr(text = "orders", id = "tbl:orders"),
+                dte(text = "for last 2 weeks"),
+                nlp(text = "the", isStop = true),
+                srt(text = "sorted by", typ = BY_ONLY, note = "tbl:orders", index = 5),
+                usr(text = "order", id = "tbl:orders"),
+                nlp(text = "quantity"),
+                nlp(text = "?", isStop = true)
+            ),
+            // TODO: limit top
+            _ ⇒ checkExists(
+                txt = "What are the top 25 orders for the last 2 weeks sorted by order quantity?",
+                nlp(text = "What are the", isStop = true),
+                nlp(text = "top"),
+                lim(text = "25", limit = 25, index = 3, note = "tbl:orders"),
+                usr(text = "orders", id = "tbl:orders"),
+                dte(text = "for last 2 weeks"),
+                nlp(text = "the", isStop = true),
+                srt(text = "sorted by", typ = BY_ONLY, note = "col:num", index = 7),
+                usr(text = "order quantity", id = "col:num"),
+                nlp(text = "?", isStop = true)
+            ),
+            // TODO: add.
+//            What are the best performing products for the last quarter?
+//            What are the best performing categories for the last quarter?
+//            What are the best performing employee for the last quarter?
+//            What are the best performing territory for the last quarter?
+//            What are the best performing region for the last quarter?
+//
+//            What are the least performing products for the last quarter?
+//            What are the least performing categories for the last quarter?
+//            What are the least performing employee for the last quarter?
+//            What are the least performing territory for the last quarter?
+//            What are the least performing region for the last quarter?
+            _ ⇒ checkExists(
+                txt = "What are the best performing products for the last quarter?",
+                nlp(text = "What are the", isStop = true),
+                usr(text = "best performing", id = "sort:best"),
+                usr(text = "products", id = "tbl:products"),
+                dte(text = "for last quarter"),
+                nlp(text = "the ?", isStop = true)
+            ),
+            _ ⇒ checkExists(
+                txt = "What are the least performing categories for the last quarter?",
+                nlp(text = "What are the", isStop = true),
+                usr(text = "least performing", id = "sort:worst"),
+                usr(text = "categories", id = "tbl:categories"),
+                dte(text = "for last quarter"),
+                nlp(text = "the ?", isStop = true)
+            )
+
+        )
+    }
+}
diff --git a/src/test/scala/org/apache/nlpcraft/probe/mgrs/nlp/enrichers/NCEnricherBaseSpec.scala b/src/test/scala/org/apache/nlpcraft/probe/mgrs/nlp/enrichers/NCEnricherBaseSpec.scala
index cd436ad..aeb2d80 100644
--- a/src/test/scala/org/apache/nlpcraft/probe/mgrs/nlp/enrichers/NCEnricherBaseSpec.scala
+++ b/src/test/scala/org/apache/nlpcraft/probe/mgrs/nlp/enrichers/NCEnricherBaseSpec.scala
@@ -32,7 +32,7 @@ class NCEnricherBaseSpec {
     def getModelClass: Option[Class[_ <: NCDefaultTestModel]] = Some(classOf[NCDefaultTestModel])
 
     @BeforeEach
-    private[enrichers] def setUp(): Unit = {
+    protected def setUp(): Unit = {
         val mdlId = NCDefaultTestModel.ID
 
         getModelClass match {
@@ -50,7 +50,7 @@ class NCEnricherBaseSpec {
     }
 
     @AfterEach
-    private[enrichers] def tearDown(): Unit = {
+    protected def tearDown(): Unit = {
         if (client != null)
             client.close()
 
@@ -63,7 +63,7 @@ class NCEnricherBaseSpec {
       * @param txt
       * @param expToks
       */
-    private[enrichers] def checkExists(txt: String, expToks: NCTestToken*): Unit = {
+    protected def checkExists(txt: String, expToks: NCTestToken*): Unit = {
         val res = client.ask(txt)
 
         if (res.isFailed)
@@ -86,7 +86,7 @@ class NCEnricherBaseSpec {
       * @param txt
       * @param expToks
       */
-    private[enrichers] def checkAll(txt: String, expToks: Seq[NCTestToken]*): Unit = {
+    protected def checkAll(txt: String, expToks: Seq[NCTestToken]*): Unit = {
         val res = client.ask(txt)
 
         if (res.isFailed)
@@ -113,7 +113,7 @@ class NCEnricherBaseSpec {
       *
       * @param tests
       */
-    private[enrichers] def runBatch(tests: Unit ⇒ Unit*): Unit = {
+    protected def runBatch(tests: Unit ⇒ Unit*): Unit = {
         val errs = tests.zipWithIndex.flatMap { case (test, i) ⇒
             try {
                 test.apply(())