talash-0.3.0: src/Talash/Chunked.hs
-- |
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE TypeFamilies #-}
module Talash.Chunked (module Talash.Chunked , module Export) where
import Control.Concurrent (forkIO)
import Control.Concurrent.MVar
import Data.Bit
import qualified Data.Set as DS
import qualified Data.Text as T
import Data.Text.AhoCorasick.Automaton as Export (CaseSensitivity(..))
import qualified Data.Vector as V
import Data.Vector.Algorithms.Intro (sort)
import qualified Data.Vector.Mutable as MV
import qualified Data.Vector.Sized as SV
import qualified Data.Vector.Unboxed as U
import qualified Data.Vector.Unboxed.Mutable as M
import qualified Data.Vector.Unboxed.Mutable.Sized as MS
import qualified Data.Vector.Unboxed.Sized as S
import Lens.Micro.TH (makeLenses)
import qualified System.IO.Streams as I
import Talash.Core as Export hiding (match , makeMatcher)
import Talash.Intro hiding (splitAt)
import Talash.ScoredMatch as Export
newtype Chunks (n:: Nat) = Chunks { chunks :: V.Vector (SV.Vector n Text)} deriving (Eq , Ord , Show)
type MatchSetSized n = DS.Set (ScoredMatchSized n)
data Ocassion = ChunkSearched | QueryDone | NewQuery | SearchDone deriving (Eq , Ord , Show)
data SearchStateSized (n :: Nat) a = SearchStateSized { _currentQuery :: {-# UNPACK #-} !Text
, _prevQuery :: {-# UNPACK #-} !Text
, _chunkNumber :: {-# UNPACK #-} !Int
, _totalMatches :: {-# UNPACK #-} !Int
, _newMatches :: !Bool
, _done :: !Bool
, _matchSet :: !(MatchSetSized n)}
makeLenses ''SearchStateSized
data SearchFunctions a b = SearchFunctions { _makeMatcher :: Text -> Matcher a
, _match :: forall n. KnownNat n => MatcherSized n a -> Text -> Maybe (MatchFull n)
-- | Given the matcher @m@, the matched string @t@ and the indices of matches in @t@ divide @t@ in alternating strings that are a matches
-- and the gap between these matches. The first of these is always a gap and can be empty. The rest should be non empty.
, _display :: forall n. KnownNat n => (Bool -> Text -> b) -> MatcherSized n a -> Text -> S.Vector n Int -> [b] }
makeLenses ''SearchFunctions
data SearchReport = SearchReport { _ocassion :: Ocassion , _hasNewMatches :: Bool , _nummatches :: Int , _searchedTerm :: Text}
makeLenses ''SearchReport
-- | The constant environment in which the search runs.
data SearchEnv n a b = SearchEnv { _searchFunctions :: SearchFunctions a b -- ^ The functions used to find and display matches.
, _send :: forall n m. (KnownNat n , KnownNat m) => Chunks n -> SearchReport -> MatcherSized m a -> MatchSetSized m -> IO ()
, _maxMatches :: Int
, _candidates :: Chunks n
, _query :: MVar (Maybe Text)
, _allMatches :: M.IOVector (S.Vector n Bit) }
makeLenses ''SearchEnv
{-# INLINABLE (!) #-}
(!) :: KnownNat n => Chunks n -> ChunkIndex -> Text
(!) (Chunks v) (ChunkIndex i j) = V.unsafeIndex (SV.fromSized $ V.unsafeIndex v i) j
{-# INLINE getChunk #-}
getChunk :: Int -> Chunks n -> SV.Vector n Text
getChunk i (Chunks f) = V.unsafeIndex f i
{-# INLINE matchChunk #-}
matchChunk :: forall n m a. (KnownNat n , KnownNat m) => (MatcherSized n a -> Text -> Maybe (MatchFull n)) -> MatcherSized n a -> Int -> SV.Vector m Text
-> S.Vector m Bit -> (S.Vector m Bit , MatchSetSized n)
matchChunk fun m ci v i = runST $ matchChunkM fun m ci v i
{-# INLINEABLE matchChunkM #-}
matchChunkM :: forall n m a s. (KnownNat n , KnownNat m) => (MatcherSized n a -> Text -> Maybe (MatchFull n)) -> MatcherSized n a -> Int -> SV.Vector m Text
-> S.Vector m Bit -> ST s (S.Vector m Bit , MatchSetSized n)
matchChunkM fun m = go
where
go ci v i = doMatching =<< MS.replicate (Bit False)
where
nzero = natVal (Proxy :: Proxy n) == 0
doMatching mbv = freezeAndDone =<< U.ifoldM' collectAndWrite DS.empty (S.fromSized i)
where
umbv = MS.fromSized mbv
freezeAndDone mset = ( , mset) <$> S.unsafeFreeze mbv
collectAndWrite x _ (Bit False) = pure x
collectAndWrite x j (Bit True)
| Nothing <- res = pure x
| Just mtch <- res = unsafeFlipBit umbv j $> DS.insert (conv mtch) x
where
res
| nzero = if SV.unsafeIndex v j == "" then Nothing else fun m . SV.unsafeIndex v $ j
| otherwise = fun m . SV.unsafeIndex v $ j
conv (MatchFull k w) = ScoredMatchSized (Down k) (ChunkIndex ci j) w
{-# INLINABLE resetMatches #-}
resetMatches :: forall n m a b. KnownNat n => SearchEnv n a b -> SearchStateSized m a -> IO ()
resetMatches env state
| T.isInfixOf (state ^. prevQuery) (state ^. currentQuery) = pure ()
| otherwise = M.set (env ^. allMatches) (S.replicate 1)
{-# INLINABLE searchNextChunk #-}
searchNextChunk :: (KnownNat n , KnownNat m) => SearchEnv n a b -> MatcherSized m a -> SearchStateSized m a -> IO (SearchStateSized m a)
searchNextChunk env matcher state = nextstate . getMatches =<< M.read (env ^. allMatches) i
where
i = state ^. chunkNumber
getMatches = matchChunk (env ^. searchFunctions . match) matcher i (getChunk i (env ^. candidates))
nextstate (js , mtchs) = M.write (env ^. allMatches) i js $> (over chunkNumber (+ 1) . updateAndSend . mergedMatches (state ^. matchSet) $ mtchs)
where
mergedMatches curr new = if not (DS.null new) && (DS.size curr < env ^. maxMatches || DS.lookupMax curr > DS.lookupMin new)
then Just . DS.take (env ^. maxMatches) . DS.union curr $ new else Nothing
updateAndSend = over totalMatches (+ DS.size mtchs) . maybe (set newMatches False state) (\mset -> set newMatches True (set matchSet mset state))
matcherLoop :: (KnownNat n , KnownNat m) => SearchEnv n a b -> Text -> Text -> MatcherSized m a -> IO (Maybe Text)
matcherLoop env qry prev matcher = resetMatches env initstate *> loop initstate
where
initstate = SearchStateSized qry prev 0 0 False False DS.empty
loop state = step =<< tryTakeMVar (env ^. query)
where
step x
| Just Nothing <- x = doSend SearchDone $> Nothing
| inrange , Nothing <- x = doSend ChunkSearched *> (loop =<< searchNextChunk env matcher state)
| Just (Just t) <- x = doSend NewQuery $> Just t
| otherwise = doSend QueryDone *> takeMVar (env ^. query)
where
report b = SearchReport b (state ^. newMatches) (state ^. totalMatches) qry
doSend b = (env ^. send) (env ^. candidates) (report b) matcher (state ^. matchSet)
inrange = state ^. chunkNumber < (V.length . chunks $ env ^. candidates)
searchEnv :: KnownNat n => SearchFunctions a b -> Int -> (forall n m. (KnownNat n , KnownNat m) => Chunks n -> SearchReport -> MatcherSized m a -> MatchSetSized m -> IO ())
-> Chunks n -> IO (SearchEnv n a b)
searchEnv funs n sender chks = SearchEnv funs sender n chks <$> newEmptyMVar <*> M.replicate (V.length . chunks $ chks) (S.replicate 1)
searchLoop :: KnownNat n => SearchEnv n a b -> IO ()
searchLoop env = maybe (pure ()) (loop "") =<< takeMVar (env ^. query)
where
loop prev qry
| (Matcher m) <- (env ^. searchFunctions . makeMatcher) qry = maybe (pure ()) (loop qry) =<< matcherLoop env qry prev m
fuzzyFunctions :: CaseSensitivity -> SearchFunctions MatchPart b
fuzzyFunctions c = SearchFunctions (fuzzyMatcher c) fuzzyMatchSized fuzzyMatchPartsAs
orderlessFunctions :: CaseSensitivity -> SearchFunctions Int b
orderlessFunctions c = SearchFunctions (orderlessMatcher c) orderlessMatchSized orderlessMatchPartsAs
{-# INLINABLE makeChunks #-}
makeChunks :: forall n. KnownNat n => V.Vector Text -> Chunks n
makeChunks v = Chunks $ V.generate numchunks (SV.force . fromJust . SV.toSized . chunk)
where
n = fromInteger $ natVal (Proxy :: Proxy n)
(numchunks , remainder) = bimap (+ 1) (+ 1) . divMod (length v - 1) $ n
chunk i
| i + 1 < numchunks = V.slice (i*n) n v
| otherwise = V.slice (i*n) remainder v <> V.replicate (n - remainder) ""
{-# INLINE makeChunksP #-}
makeChunksP :: KnownNat n => Proxy n -> V.Vector Text -> Chunks n
makeChunksP _ = makeChunks
{-# INLINEABLE setToVectorST #-}
setToVectorST :: (a -> b) -> DS.Set a -> ST s (V.Vector b)
setToVectorST f s = go =<< MV.unsafeNew (DS.size s)
where
go mv = foldM_ (\i e -> MV.unsafeWrite mv i (f e) $> i + 1) 0 s *> V.unsafeFreeze mv
startSearcher :: KnownNat n => SearchEnv n a b -> IO ()
startSearcher = void . forkIO . searchLoop
sendQuery :: KnownNat n => SearchEnv n a b -> Text -> IO ()
sendQuery env = putMVar (env ^. query) . Just
stopSearcher :: KnownNat n => SearchEnv n a b -> IO ()
stopSearcher env = putMVar (env ^. query) Nothing
concatChunks :: KnownNat n => Int -> Chunks n -> V.Vector Text
concatChunks i (Chunks c) = V.concatMap SV.fromSized . V.take i $ c
forceChunks :: KnownNat n => Chunks n -> Chunks n
forceChunks (Chunks v) = Chunks . V.force $ v
chunksFromStream :: forall n. KnownNat n => I.InputStream Text -> IO (Chunks n)
chunksFromStream i = Chunks <$> (I.toVector =<< I.mapMaybe (\v -> map SV.force . SV.toSized $ v V.++ V.replicate (n - length v) "") =<< I.chunkVector n i)
where
n = fromInteger $ natVal (Proxy :: Proxy n)
{-# INLINE chunksFromStreamP #-}
chunksFromStreamP :: forall n. KnownNat n => Proxy n -> I.InputStream Text -> IO (Chunks n)
chunksFromStreamP _ = chunksFromStream
{-# INLINABLE chunksFromHandle #-}
chunksFromHandle :: KnownNat n => Proxy n -> Handle -> IO (Chunks n)
chunksFromHandle p = chunksFromStreamP p <=< I.decodeUtf8 <=< I.lines <=< I.handleToInputStream
readVectorHandleWith :: (Text -> Text) -- ^ The function to transform the candidates.
-> (V.Vector Text -> V.Vector Text) -- ^ The function to apply to the constructed vector before compacting.
-> Handle -- ^ The handle to read from
-> IO (V.Vector Text)
readVectorHandleWith f t = map t . I.toVector <=< I.map f <=< I.decodeUtf8 <=< I.lines <=< I.handleToInputStream
fileNamesSorted :: Handle -> IO (V.Vector Text)
fileNamesSorted = readVectorHandleWith (T.takeWhileEnd (/= '/')) (V.uniq . V.modify sort)