Ngrams.hs 19.1 KB
Newer Older
1
{-|
2
Module      : Gargantext.Database.Schema.Ngrams
3
Description : Ngram connection to the Database
4 5 6 7 8 9
Copyright   : (c) CNRS, 2017-Present
License     : AGPL + CECILL v3
Maintainer  : team@gargantext.org
Stability   : experimental
Portability : POSIX

10 11
Ngrams connection to the Database.

12 13
-}

14 15 16 17 18
{-# LANGUAGE Arrows                     #-}
{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE FunctionalDependencies     #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
19 20
{-# LANGUAGE MultiParamTypeClasses  #-}
{-# LANGUAGE NoImplicitPrelude      #-}
21 22
{-# LANGUAGE OverloadedStrings      #-}
{-# LANGUAGE QuasiQuotes            #-}
23
{-# LANGUAGE RankNTypes             #-}
24
{-# LANGUAGE TemplateHaskell        #-}
25

26
module Gargantext.Database.Schema.Ngrams where
27

28
import Data.Aeson (FromJSON, FromJSONKey)
29
import Control.Lens (makeLenses, view, over)
30
import Control.Monad (mzero)
31
import Data.Aeson
32
import Data.ByteString.Internal (ByteString)
Alexandre Delanoë's avatar
Alexandre Delanoë committed
33
import Data.Map (Map, fromList, lookup, fromListWith)
34
import Data.Profunctor.Product.TH (makeAdaptorAndInstance)
Alexandre Delanoë's avatar
Alexandre Delanoë committed
35
import Data.Set (Set)
36
import Data.Text (Text, splitOn)
37
import Database.PostgreSQL.Simple ((:.)(..))
38 39
import Database.PostgreSQL.Simple.FromRow (fromRow, field)
import Database.PostgreSQL.Simple.SqlQQ (sql)
40 41
import Database.PostgreSQL.Simple.ToField (toField, ToField)
import Database.PostgreSQL.Simple.FromField (FromField, fromField)
42
import Database.PostgreSQL.Simple.ToRow   (toRow)
43
import Database.PostgreSQL.Simple.Types (Values(..), QualifiedIdentifier(..))
44
import Debug.Trace (trace)
45
import GHC.Generics (Generic)
46 47
import Gargantext.Core.Types -- (fromListTypeId, ListType, NodePoly(Node))
import Gargantext.Database.Config (nodeTypeId,userMaster)
48
import Gargantext.Database.Root (getRoot)
49
import Gargantext.Database.Types.Node (NodeType)
50
import Gargantext.Database.Schema.Node (getListsWithParentId, getCorporaWithParentId)
51
import Gargantext.Database.Utils (Cmd, runPGSQuery, runOpaQuery, formatPGSQuery)
52
import Gargantext.Prelude
53
import Opaleye hiding (FromField)
54
import Prelude (Enum, Bounded, minBound, maxBound, Functor)
55
import qualified Data.Set as DS
56
import qualified Database.PostgreSQL.Simple as PGS
57

58 59 60 61 62

type NgramsTerms = Text
type NgramsId    = Int
type Size        = Int

63
data NgramsPoly id terms n = NgramsDb { ngrams_id    :: id
Alexandre Delanoë's avatar
Alexandre Delanoë committed
64 65 66
                                      , ngrams_terms :: terms
                                      , ngrams_n     :: n
                                      } deriving (Show)
67

68
type NgramsWrite = NgramsPoly (Maybe (Column PGInt4))
69 70 71
                                   (Column PGText)
                                   (Column PGInt4)

72 73 74 75 76 77 78
type NgramsRead  = NgramsPoly (Column PGInt4)
                              (Column PGText)
                              (Column PGInt4)

type NgramsReadNull = NgramsPoly (Column (Nullable PGInt4))
                                 (Column (Nullable PGText))
                                 (Column (Nullable PGInt4))
79

80
type NgramsDb = NgramsPoly Int Text Int
81

82 83
$(makeAdaptorAndInstance "pNgramsDb"    ''NgramsPoly)
-- $(makeLensesWith abbreviatedFields   ''NgramsPoly)
84

85 86
ngramsTable :: Table NgramsWrite NgramsRead
ngramsTable = Table "ngrams" (pNgramsDb NgramsDb { ngrams_id    = optional "id"
Alexandre Delanoë's avatar
Alexandre Delanoë committed
87 88 89 90 91
                                                 , ngrams_terms = required "terms"
                                                 , ngrams_n     = required "n"
                                                 }
                              )

92 93
queryNgramsTable :: Query NgramsRead
queryNgramsTable = queryTable ngramsTable
94

95 96
dbGetNgramsDb :: Cmd err [NgramsDb]
dbGetNgramsDb = runOpaQuery queryNgramsTable
97

98 99 100 101 102 103
-- | Main Ngrams Types
-- | Typed Ngrams
-- Typed Ngrams localize the context of the ngrams
-- ngrams in source field of document has Sources Type
-- ngrams in authors field of document has Authors Type
-- ngrams in text (title or abstract) of documents has Terms Type
104
data NgramsType = Authors | Institutes | Sources | NgramsTerms
105 106 107 108
  deriving (Eq, Show, Ord, Enum, Bounded, Generic)

instance FromJSON NgramsType
instance FromJSONKey NgramsType
109 110
instance ToJSON NgramsType
instance ToJSONKey NgramsType
111

112 113 114 115 116 117 118
newtype NgramsTypeId = NgramsTypeId Int
  deriving (Eq, Show, Ord, Num)

instance ToField NgramsTypeId where
  toField (NgramsTypeId n) = toField n

instance FromField NgramsTypeId where
119 120
  fromField fld mdata = do
    n <- fromField fld mdata
121 122 123
    if (n :: Int) > 0 then return $ NgramsTypeId n
                      else mzero

124 125 126
pgNgramsType :: NgramsType -> Column PGInt4
pgNgramsType = pgNgramsTypeId . ngramsTypeId

127 128 129 130
pgNgramsTypeId :: NgramsTypeId -> Column PGInt4
pgNgramsTypeId (NgramsTypeId n) = pgInt4 n

ngramsTypeId :: NgramsType -> NgramsTypeId
131 132 133 134
ngramsTypeId Authors     = 1
ngramsTypeId Institutes  = 2
ngramsTypeId Sources     = 3
ngramsTypeId NgramsTerms = 4
135

136
fromNgramsTypeId :: NgramsTypeId -> Maybe NgramsType
137 138
fromNgramsTypeId id = lookup id $ fromList [(ngramsTypeId nt,nt) | nt <- [minBound .. maxBound] :: [NgramsType]]

139 140 141 142
------------------------------------------------------------------------
-- | TODO put it in Gargantext.Text.Ngrams
data Ngrams = Ngrams { _ngramsTerms :: Text
                     , _ngramsSize  :: Int
143 144
           } deriving (Generic, Show, Eq, Ord)

145
makeLenses ''Ngrams
146
instance PGS.ToRow Ngrams where
147 148
  toRow (Ngrams t s) = [toField t, toField s]

149 150 151
text2ngrams :: Text -> Ngrams
text2ngrams txt = Ngrams txt $ length $ splitOn " " txt

152 153 154
-------------------------------------------------------------------------
-- | TODO put it in Gargantext.Text.Ngrams
-- Named entity are typed ngrams of Terms Ngrams
155 156 157
data NgramsT a =
  NgramsT { _ngramsType :: NgramsType
          , _ngramsT    :: a
158
          } deriving (Generic, Show, Eq, Ord)
159

160
makeLenses ''NgramsT
161 162 163

instance Functor NgramsT where
  fmap = over ngramsT
164
-----------------------------------------------------------------------
165 166 167 168
data NgramsIndexed =
  NgramsIndexed
  { _ngrams   :: Ngrams
  , _ngramsId :: NgramsId
169
  } deriving (Show, Generic, Eq, Ord)
170

171 172
makeLenses ''NgramsIndexed
------------------------------------------------------------------------
173 174 175 176
data NgramIds =
  NgramIds
  { ngramId    :: Int
  , ngramTerms :: Text
177
  } deriving (Show, Generic, Eq, Ord)
178

179
instance PGS.FromRow NgramIds where
180
  fromRow = NgramIds <$> field <*> field
181

182
----------------------
183 184 185
withMap :: Map NgramsTerms NgramsId -> NgramsTerms -> NgramsId
withMap m n = maybe (panic "withMap: should not happen") identity (lookup n m)

186
indexNgramsT :: Map NgramsTerms NgramsId -> NgramsT Ngrams -> NgramsT NgramsIndexed
187 188 189 190 191 192 193 194
indexNgramsT = fmap . indexNgramsWith . withMap

indexNgrams :: Map NgramsTerms NgramsId -> Ngrams -> NgramsIndexed
indexNgrams = indexNgramsWith . withMap

-- NP: not sure we need it anymore
indexNgramsTWith :: (NgramsTerms -> NgramsId) -> NgramsT Ngrams -> NgramsT NgramsIndexed
indexNgramsTWith = fmap . indexNgramsWith
195

196 197
indexNgramsWith :: (NgramsTerms -> NgramsId) -> Ngrams -> NgramsIndexed
indexNgramsWith f n = NgramsIndexed n (f $ _ngramsTerms n)
198

199
-- TODO-ACCESS: access must not be checked here but when insertNgrams is called.
200
insertNgrams :: [Ngrams] -> Cmd err (Map NgramsTerms NgramsId)
201 202
insertNgrams ns = fromList <$> map (\(NgramIds i t) -> (t, i)) <$> (insertNgrams' ns)

203
-- TODO-ACCESS: access must not be checked here but when insertNgrams' is called.
204 205
insertNgrams' :: [Ngrams] -> Cmd err [NgramIds]
insertNgrams' ns = runPGSQuery queryInsertNgrams (PGS.Only $ Values fields ns)
206 207
  where
    fields = map (\t -> QualifiedIdentifier Nothing t) ["text", "int4"]
208

209 210
insertNgrams_Debug :: [(NgramsTerms, Size)] -> Cmd err ByteString
insertNgrams_Debug ns = formatPGSQuery queryInsertNgrams (PGS.Only $ Values fields ns)
211 212
  where
    fields = map (\t -> QualifiedIdentifier Nothing t) ["text", "int4"]
213

214
----------------------
215
queryInsertNgrams :: PGS.Query
216 217 218 219 220 221 222 223
queryInsertNgrams = [sql|
    WITH input_rows(terms,n) AS (?)
    , ins AS (
       INSERT INTO ngrams (terms,n)
       SELECT * FROM input_rows
       ON CONFLICT (terms) DO NOTHING -- unique index created here
       RETURNING id,terms
       )
224

225 226 227 228 229 230 231
    SELECT id, terms
    FROM   ins
    UNION  ALL
    SELECT c.id, terms
    FROM   input_rows
    JOIN   ngrams c USING (terms);     -- columns of unique index
           |]
232 233 234


-- | Ngrams Table
235 236 237
-- TODO: the way we are getting main Master Corpus and List ID is not clean
-- TODO: if ids are not present -> create
-- TODO: Global Env State Monad to keep in memory the ids without retrieving it each time
238 239 240
getNgramsTableDb :: NodeType -> NgramsType
                 -> NgramsTableParamUser
                 -> Limit -> Offset
241
                 -> Cmd err [NgramsTableData]
242
getNgramsTableDb nt ngrt ntp limit_ offset_ = do
243 244
  
  
245
  maybeRoot <- head <$> getRoot userMaster
246 247 248 249
  let path = "Garg.Db.Ngrams.getTableNgrams: "
  let masterRootId = maybe (panic $ path <> "no userMaster Tree") (view node_id) maybeRoot
  -- let errMess = panic "Error"

250
  corpusMasterId <- maybe (panic "error master corpus") (view node_id) <$> head <$> getCorporaWithParentId masterRootId
251
  
252
  listMasterId   <- maybe (panic "error master list") (view node_id) <$> head <$> getListsWithParentId corpusMasterId
253
  
254
  getNgramsTableData nt ngrt ntp (NgramsTableParam listMasterId corpusMasterId) limit_ offset_
255 256

data NgramsTableParam =
257 258
     NgramsTableParam { _nt_listId     :: NodeId
                      , _nt_corpusId   :: NodeId
259 260 261 262 263
                      }

type NgramsTableParamUser   = NgramsTableParam
type NgramsTableParamMaster = NgramsTableParam

264

265 266 267 268 269 270
data NgramsTableData = NgramsTableData { _ntd_id        :: Int
                                         , _ntd_parent_id :: Maybe Int
                                         , _ntd_terms     :: Text
                                         , _ntd_n         :: Int
                                         , _ntd_listType  :: Maybe ListType
                                         , _ntd_weight    :: Double
271 272 273 274
                                         } deriving (Show)



275
getNgramsTableData :: NodeType -> NgramsType
276 277
                   -> NgramsTableParamUser -> NgramsTableParamMaster 
                   -> Limit -> Offset
278 279
                   -> Cmd err [NgramsTableData]
getNgramsTableData nodeT ngrmT (NgramsTableParam ul uc) (NgramsTableParam ml mc) limit_ offset_ =
280
  trace ("Ngrams table params: " <> show params) <$>
281
  map (\(i,p,t,n,lt,w) -> NgramsTableData i p t n (fromListTypeId lt) w) <$>
282 283 284 285 286 287 288 289 290
    runPGSQuery querySelectTableNgramsTrees params
      where
        nodeTId = nodeTypeId   nodeT
        ngrmTId = ngramsTypeId ngrmT
        params  = (ul,ml,uc,mc,nodeTId,ngrmTId) :. (limit_, offset_)

getNgramsTableDataDebug :: PGS.ToRow a => a -> Cmd err ByteString
getNgramsTableDataDebug = formatPGSQuery querySelectTableNgramsTrees

291

292
querySelectTableNgrams :: PGS.Query
293 294
querySelectTableNgrams = [sql|

295
    WITH tableUser AS (
296 297 298 299 300
      SELECT ngs.terms, ngs.n, list.list_type, corp.weight FROM ngrams ngs
        JOIN nodes_ngrams list ON list.ngrams_id = ngs.id
        JOIN nodes_ngrams corp ON corp.ngrams_id = ngs.id
        JOIN nodes_nodes  nn   ON nn.node2_id    = corp.node_id
        JOIN nodes        n    ON n.id           = corp.node_id
301 302
      
      WHERE list.node_id     = ?   -- User listId
303
        AND nn.node1_id      = ?   -- User CorpusId or AnnuaireId
304 305
        AND n.typename       = ?   -- both type of childs (Documents or Contacts)
        AND corp.ngrams_type = ?   -- both type of ngrams (Authors or Terms or...)
306
        AND list.parent_id   IS NULL
307 308
    )
    , tableMaster AS (
309 310 311
      SELECT ngs.terms, ngs.n, list.list_type, corp.weight FROM ngrams ngs
        JOIN nodes_ngrams list ON list.ngrams_id = ngs.id
        JOIN nodes_ngrams corp ON corp.ngrams_id = ngs.id
312 313 314 315 316 317 318 319
        JOIN nodes        n    ON n.id          = corp.node_id
        JOIN nodes_nodes  nn   ON nn.node2_id  = n.id
        
      WHERE list.node_id     = ?   -- Master listId
        AND n.parent_id      = ?   -- Master CorpusId or AnnuaireId
        AND n.typename       = ?   -- Master childs (Documents or Contacts)
        AND corp.ngrams_type = ?   -- both type of ngrams (Authors or Terms?)
        AND nn.node1_id      = ?   -- User CorpusId or AnnuaireId
320
        AND list.parent_id   IS NULL
321 322 323 324
    )
    
  SELECT COALESCE(tu.terms,tm.terms) AS terms
       , COALESCE(tu.n,tm.n)         AS n
325
       , COALESCE(tu.list_type,tm.list_type) AS ngrams_type
326 327
       , SUM(COALESCE(tu.weight,tm.weight)) AS weight
  FROM tableUser tu RIGHT JOIN tableMaster tm ON tu.terms = tm.terms
328 329
  GROUP BY tu.terms,tm.terms,tu.n,tm.n,tu.list_type,tm.list_type
  ORDER BY 1,2
330 331
  LIMIT ?
  OFFSET ?;
332 333 334

  |]

335 336 337 338

querySelectTableNgramsTrees :: PGS.Query
querySelectTableNgramsTrees = [sql|

339 340 341
-- DROP FUNCTION tree_start(integer,integer,integer,integer,integer,integer,integer,integer);
-- DROP FUNCTION tree_end(integer,integer,integer,integer,integer,integer);
-- DROP FUNCTION tree_ngrams(integer,integer,integer,integer,integer,integer,integer,integer);
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382

CREATE OR REPLACE FUNCTION public.tree_start(luid INT, lmid INT,cuid INT, cmid INT, tdoc INT, tngrams INT, lmt INT, ofst INT)
 RETURNS TABLE (id INT, parent_id INT, terms VARCHAR(255), n int, list_type int, weight float8) AS $$
BEGIN
    RETURN QUERY
    WITH tableUser AS (
        SELECT list.id, list.parent_id, ngs.terms, ngs.n, list.list_type, corp.weight FROM ngrams ngs
          JOIN nodes_ngrams list ON list.ngrams_id = ngs.id
          JOIN nodes_ngrams corp ON corp.ngrams_id = ngs.id
          JOIN nodes_nodes  nn   ON nn.node2_id    = corp.node_id
          JOIN nodes        n    ON n.id           = corp.node_id
        
        WHERE list.node_id     = luid   -- User listId
          AND nn.node1_id      = cuid   -- User CorpusId or AnnuaireId
          AND n.typename       = tdoc   -- both type of childs (Documents or Contacts)
          AND corp.ngrams_type = tngrams   -- both type of ngrams (Authors or Terms or...)
          AND list.parent_id   IS NULL
      ),
      tableMaster AS (
        SELECT list.id, list.parent_id, ngs.terms, ngs.n, list.list_type, corp.weight FROM ngrams ngs
          JOIN nodes_ngrams list ON list.ngrams_id = ngs.id
          JOIN nodes_ngrams corp ON corp.ngrams_id = ngs.id
          JOIN nodes        n    ON n.id          = corp.node_id
          JOIN nodes_nodes  nn   ON nn.node2_id  = n.id
          
        WHERE list.node_id     = lmid   -- Master listId
          AND n.parent_id      = cmid   -- Master CorpusId or AnnuaireId
          AND n.typename       = tdoc   -- Master childs (Documents or Contacts)
          AND corp.ngrams_type = tngrams -- both type of ngrams (Authors or Terms1)
          AND nn.node1_id      = cuid    -- User CorpusId or AnnuaireId
          AND list.parent_id   IS NULL
      )
      
      SELECT COALESCE(tu.id,tm.id) AS id
           , COALESCE(tu.parent_id,tm.parent_id) AS parent_id
           , COALESCE(tu.terms,tm.terms) AS terms
           , COALESCE(tu.n,tm.n)         AS n
           , COALESCE(tu.list_type,tm.list_type) AS ngrams_type
           , SUM(COALESCE(tu.weight,tm.weight)) AS weight
      FROM tableUser tu RIGHT JOIN tableMaster tm ON tu.terms = tm.terms
      GROUP BY tu.id,tm.id,tu.parent_id,tm.parent_id,tu.terms,tm.terms,tu.n,tm.n,tu.list_type,tm.list_type
383
      ORDER BY 3
384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451
      LIMIT lmt
      OFFSET ofst
      ;
END $$
LANGUAGE plpgsql ;

CREATE OR REPLACE FUNCTION public.tree_end(luid INT, lmid INT,cuid INT, cmid INT, tdoc INT, tngrams INT)
 RETURNS TABLE (id INT, parent_id INT, terms VARCHAR(255), n int, list_type int, weight float8) AS $$
BEGIN
    RETURN QUERY
      WITH tableUser2 AS (
          SELECT list.id, list.parent_id, ngs.terms, ngs.n, list.list_type, corp.weight FROM ngrams ngs
            JOIN nodes_ngrams list ON list.ngrams_id = ngs.id
            JOIN nodes_ngrams corp ON corp.ngrams_id = ngs.id
            JOIN nodes_nodes  nn   ON nn.node2_id    = corp.node_id
            JOIN nodes        n    ON n.id           = corp.node_id
          
          WHERE list.node_id     = luid   -- User listId
            AND nn.node1_id      = cuid   -- User CorpusId or AnnuaireId
            AND n.typename       = tdoc   -- both type of childs (Documents or Contacts)
            AND corp.ngrams_type = tngrams  -- both type of ngrams (Authors or Terms or...)
        )
        , tableMaster2 AS (
          SELECT list.id, list.parent_id, ngs.terms, ngs.n, list.list_type, corp.weight FROM ngrams ngs
            JOIN nodes_ngrams list ON list.ngrams_id = ngs.id
            JOIN nodes_ngrams corp ON corp.ngrams_id = ngs.id
            JOIN nodes        n    ON n.id          = corp.node_id
            JOIN nodes_nodes  nn   ON nn.node2_id  = n.id
            
          WHERE list.node_id     = lmid   -- Master listId
            AND n.parent_id      = cmid   -- Master CorpusId or AnnuaireId
            AND n.typename       = tdoc   -- Master childs (Documents or Contacts)
            AND corp.ngrams_type = tngrams   -- both type of ngrams (Authors or Terms1)
            AND nn.node1_id      = cuid   -- User CorpusId or AnnuaireId
        )
        SELECT COALESCE(tu.id,tm.id) as id
             , COALESCE(tu.parent_id,tm.parent_id) as parent_id
             , COALESCE(tu.terms,tm.terms) AS terms
             , COALESCE(tu.n,tm.n)         AS n
             , COALESCE(tu.list_type,tm.list_type) AS list_type
             , SUM(COALESCE(tu.weight,tm.weight)) AS weight
        FROM tableUser2 tu RIGHT JOIN tableMaster2 tm ON tu.terms = tm.terms
        GROUP BY tu.id,tm.id,tu.parent_id,tm.parent_id,tu.terms,tm.terms,tu.n,tm.n,tu.list_type,tm.list_type
    ;
END $$
LANGUAGE plpgsql ;


CREATE OR REPLACE FUNCTION public.tree_ngrams(luid INT, lmid INT,cuid INT, cmid INT, tdoc INT, tngrams INT, lmt INT, ofst INT)
 RETURNS TABLE (id INT, parent_id INT, terms VARCHAR(255), n int, list_type int, weight float8) AS $$
BEGIN 
 RETURN QUERY WITH RECURSIVE
    ngrams_tree (id,parent_id,terms,n,list_type,weight) AS (
     SELECT ts.id,ts.parent_id,ts.terms,ts.n,ts.list_type,ts.weight FROM tree_start($1,$2,$3,$4,$5,$6,$7,$8) ts
      UNION
     SELECT te.id,te.parent_id,te.terms,te.n,te.list_type,te.weight FROM tree_end($1,$2,$3,$4,$5,$6) as te
     INNER JOIN ngrams_tree ON te.parent_id = ngrams_tree.id
     )
    SELECT * from ngrams_tree;
END $$
LANGUAGE plpgsql ;

select * from tree_ngrams(?,?,?,?,?,?,?,?)

  |]



452 453
type ListIdUser   = NodeId
type ListIdMaster = NodeId
454

455
type MapToChildren = Map Text (Set Text)
456
type MapToParent   = Map Text Text
457

458 459 460
getNgramsGroup :: ListIdUser -> ListIdMaster -> Cmd err (MapToParent, MapToChildren)
getNgramsGroup lu lm = do
  groups <- runPGSQuery querySelectNgramsGroup (lu,lm)
461
  let mapChildren = fromListWith (<>) $ map (\(a,b) -> (a, DS.singleton b)) groups
462
  let mapParent   = fromListWith (<>) $ map (\(a,b) -> (b, a)) groups
463
  pure (mapParent, mapChildren)
Alexandre Delanoë's avatar
Alexandre Delanoë committed
464

465
querySelectNgramsGroup :: PGS.Query
466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482
querySelectNgramsGroup = [sql|
    WITH groupUser AS (
      SELECT n1.terms AS t1, n2.terms AS t2 FROM nodes_ngrams_ngrams nnn
        JOIN ngrams n1 ON n1.id = nnn.ngram1_id
        JOIN ngrams n2 ON n2.id = nnn.ngram2_id
        WHERE
        nnn.node_id = ? -- User listId
      ),
      groupMaster AS (
      SELECT n1.terms AS t1, n2.terms AS t2 FROM nodes_ngrams_ngrams nnn
        JOIN ngrams n1 ON n1.id = nnn.ngram1_id
        JOIN ngrams n2 ON n2.id = nnn.ngram2_id
        WHERE
        nnn.node_id = ? -- Master listId
      )
    SELECT COALESCE(gu.t1,gm.t1) AS ngram1_id
         , COALESCE(gu.t2,gm.t2) AS ngram2_id
483
      FROM groupUser gu LEFT JOIN groupMaster gm ON gu.t1 = gm.t1
484
  |]
485