a-piece-of-flake-0.0.1: src/PieceOfFlake/Index.hs
module PieceOfFlake.Index where
import Data.HashPSQ qualified as PSQ
import Data.Map.Strict qualified as M
import Data.LruCache qualified as LRU
import Data.LruCache.Internal qualified as LRU
import Data.LruCache (LruCache)
import Data.SearchEngine
( Term,
SearchConfig(documentFeatureValue, SearchConfig, documentKey,
extractDocumentTerms, transformQueryTerm),
SearchRankParameters(paramAutosuggestPostfilterLimit,
SearchRankParameters, paramK1, paramB, paramFieldWeights,
paramFeatureWeights, paramFeatureFunctions,
paramResultsetSoftLimit, paramResultsetHardLimit,
paramAutosuggestPrefilterLimit),
SearchEngine,
FeatureFunction(LogarithmicFunction),
query,
initSearchEngine,
insertDoc,
queryAutosuggest,
ResultsFilter(NoFilter) )
import ListT qualified as L
import NLP.Tokenize.Text ( tokenize )
import PieceOfFlake.Flake
( FlakeUrl,
repoOfFlakeUrl,
PackageInfo(broken, description, license, name, unfree),
MetaFlake(hasNixOsModules, description, packages),
Flake(meta, uploadedAt, FlakeIndexed, FlakeFetched, flakeUrl),
isIndexed )
import PieceOfFlake.CmdArgs ( IndexQueryCacheSize )
import PieceOfFlake.Prelude hiding (pi, Map)
import PieceOfFlake.Stm
( readTQueue, writeTQueue, TQueue, newTQueueIO, atomicalog )
import StmContainers.Map ( insert, listTNonAtomic, lookup, Map )
import PieceOfFlake.Stats
( RepoStatsF(meanTimeInIndexQueue, fetchedFlakes, indexedFlakes,
meanIndexTime, meanSearchTime),
addTimeDif )
import PieceOfFlake.TotalMath ( notZero, divNz, realToFracNz )
type FlakeSearchEngine = SearchEngine (FlakeUrl, MetaFlake) FlakeUrl () ()
data FlakeIndex
= FlakeIndex
{ searchEngine :: TVar FlakeSearchEngine
, indexerQueue :: TQueue FlakeUrl
, indexerQueueLen :: TVar Int
, queryCache :: TVar (LruCache Text UtcBox)
, searchRequestCounter :: TVar Integer
}
mkFlakeIndex :: MonadIO m => Tagged IndexQueryCacheSize Word -> m FlakeIndex
mkFlakeIndex (Tagged cs) = do
liftIO $
FlakeIndex <$>
newTVarIO emptySearchEngine <*>
newTQueueIO <*>
newTVarIO 0 <*>
newTVarIO (LRU.empty $ fromIntegral cs) <*>
newTVarIO 0
packageInfoToTerms :: PackageInfo -> [Term]
packageInfoToTerms pi =
concatMap tokenize pi.description <>
concatMap tokenize pi.license <>
[ toText pi.name ]
<> maybeToList (fmap (\c -> if c then "unfree" else "free") pi.unfree)
<> maybeToList (fmap (\c -> if c then "broken" else "unbroken") pi.broken)
extractTerms :: (FlakeUrl, MetaFlake) -> () -> [Term]
extractTerms (fu, mf) () =
concatMap tokenize (maybeToList mf.description)
<> [repoOfFlakeUrl fu] <> (toText <$> M.keys mf.packages)
<> (concatMap packageInfoToTerms . concatMap M.elems $ M.elems mf.packages)
<> memptyIfFalse mf.hasNixOsModules ["nixosModules"]
emptySearchEngine :: FlakeSearchEngine
emptySearchEngine =
initSearchEngine
SearchConfig
{ documentKey = fst
, extractDocumentTerms = extractTerms
, transformQueryTerm = const
, documentFeatureValue = \_doc () -> 1
}
SearchRankParameters
{ paramK1 = 2 -- weight of repetition
, paramB = const 0.4 -- normalize by length
, paramFieldWeights = const 1
, paramFeatureWeights = const 1
, paramFeatureFunctions = const $ LogarithmicFunction 2
, paramResultsetSoftLimit = 10
, paramResultsetHardLimit = 30
, paramAutosuggestPrefilterLimit = 10
, paramAutosuggestPostfilterLimit = 10
}
data FlakeIndexedSuc
= FlakeIndexedSuc
{ inIndexQueue :: NominalDiffTime
, indexingTook :: NominalDiffTime
}
nothing :: Monad m => m a1 -> m (Maybe a2)
nothing m = do
void m
pure Nothing
consumeIndexQueue :: PoF m => RepoStatsF TVar -> Map FlakeUrl Flake -> FlakeIndex -> m ()
consumeIndexQueue rs fs fi = do
r <- atomicalog $ do
$(logInfo) "Wait for flakes to index for full text search"
fu <- lift $ readTQueue fi.indexerQueue
lift $ modifyTVar' fi.indexerQueueLen (\x -> x - 1)
ql <- lift $ readTVar fi.indexerQueueLen
$(logInfo) $ "Start index flake " <> show fu <> "; index queue " <> show ql
lift (lookup fu fs) >>= \case
Nothing -> do
$(logError) $ "Flake " <> show fu <> " is missing"
pure Nothing
Just f -> indexFlake rs fi fs f
mapM_ (\FlakeIndexedSuc { inIndexQueue, indexingTook } -> do
addTimeDif rs.meanIndexTime indexingTook
addTimeDif rs.meanTimeInIndexQueue inIndexQueue
) r
indexNewFlake :: (MonadLogger (t STM), MonadTrans t) => FlakeIndex -> FlakeUrl -> t STM ()
indexNewFlake fi fu = do
iql <- lift $ do
writeTQueue fi.indexerQueue fu
modifyTVar' fi.indexerQueueLen (1 +)
readTVar fi.indexerQueueLen
$(logInfo) $ "Indexer queue increased to " <> show iql
indexFlake :: (MonadLogger (t STM), MonadTrans t) =>
RepoStatsF TVar -> FlakeIndex -> Map FlakeUrl Flake -> Flake -> t STM (Maybe FlakeIndexedSuc)
indexFlake rs =
indexFlake' $ do
lift $ do
modifyTVar' rs.fetchedFlakes (flip (-) 1)
modifyTVar' rs.indexedFlakes (1 +)
indexFlake' :: (MonadLogger (t STM), MonadTrans t) =>
t STM () -> FlakeIndex -> Map FlakeUrl Flake -> Flake -> t STM (Maybe FlakeIndexedSuc)
indexFlake' onIndexed fi fs f =
case f of
ff@FlakeFetched { flakeUrl, uploadedAt } ->
doAfter uploadedAt $ \ua -> do
beforeIndex <- lift $ getTimeAfter ua
let fu = flakeUrl
ixf = FlakeIndexed fu (mkUtcBox beforeIndex) ff.meta
onIndexed
lift $ do
insert ixf fu fs
modifyTVar' fi.searchEngine (insertDoc (fu, ff.meta))
afterIndex <- lift $ getTimeAfter beforeIndex
$(logInfo) $ "Finished index flake " <> show fu
pure . Just $ FlakeIndexedSuc
{ inIndexQueue = beforeIndex `diffUTCTime` ua
, indexingTook = afterIndex `diffUTCTime` beforeIndex
}
_nff -> do
$(logError) $ "Flake " <> show f.flakeUrl <> " is not in the fetched state"
pure Nothing
loadIndexFromScratch ::
PoF m => RepoStatsF TVar -> FlakeIndex -> Map FlakeUrl Flake -> [(FlakeUrl, Flake)] -> m ()
loadIndexFromScratch rs fi fsm fs = do
started <- getCurrentTime
totalIndexed :: Int <- atomicalog $ do
$(logInfo) "Started init full text search index population"
r <- foldlM go 0 fs
$(logInfo) "Ended init full text search index population"
pure r
dur <- flip diffUTCTime started <$> getTimeAfter started
forM_ (realToFracNz <$> notZero totalIndexed) $ \ti ->
addTimeDif rs.meanIndexTime $ dur `divNz` ti
where
go (i :: Int) = \case
(_, ff@FlakeFetched {}) ->
(i + ) . maybe 0 (const 1) <$> indexFlake' (lift $ modifyTVar' rs.indexedFlakes (1 +)) fi fsm ff
(fu, nff) -> do
lift $ insert nff fu fsm
pure i
data FlakeSearchReq
= FlakeSearchReq
{ searchPattern :: [Text]
, skipBroken :: Bool
} deriving (Show, Eq, Generic)
instance FromJSON FlakeSearchReq
listQueryCache :: PoF m => FlakeIndex -> m [ Text ]
listQueryCache fi =
reverse . fmap (^._1) . sortWith (^._2) . PSQ.toList . LRU.lruQueue <$> readTVarIO fi.queryCache
findFlakes :: PoF m =>
RepoStatsF TVar -> Map FlakeUrl Flake -> FlakeIndex -> FlakeSearchReq -> m [ FlakeUrl ]
findFlakes rs fs fi FlakeSearchReq { searchPattern = ps } =
case concatMap tokenize ps of
[] -> justLoadFirstNFlakes fs 30
pst@(t1:_) -> do
now <- getCurrentTime
atomicalog (fromIdx now t1 pst) <*
(addTimeDif rs.meanSearchTime . (`diffUTCTime` now) =<< getTimeAfter now)
where
fromIdx now t1 pst = do
se <- lift $ do
modifyTVar' fi.searchRequestCounter (1 +)
modifyTVar' fi.queryCache (LRU.insert (unwords ps) (mkUtcBox now))
readTVar fi.searchEngine
$(logInfo) $ "Search flakes by " <> show pst
let r = query se pst `alt` (fmap fst . snd $ queryAutosuggest se NoFilter [] t1)
$(logInfo) $ "Found " <> show (length r) <> " by " <> show ps
pure r
justLoadFirstNFlakes :: MonadIO m => Map FlakeUrl Flake -> Int -> m [ FlakeUrl ]
justLoadFirstNFlakes fs n =
fmap fst . filter (isIndexed . snd) <$> liftIO (L.toList $ L.take (2 * n) (listTNonAtomic fs))