git.iko.soy/typeable/bloodhound
on master help sign in
Expose low-level scroll API

Changes in this commit: - Put a note on scanSearch's runtime properties - Resolve a "todo" making ScrollId a proper newtype. - Add low level api calls for getInitialScroll and advanceScroll, with which scanSearch is now implemented. 'scanSearch' provides a high level but pretty limited functionality. It scans over the entire search range and appends hits into a list. There are a few problems with this approach: 1. It uses lazy lists and O(n) appends on each page fetched. This probably isn't a big deal for small results sets but for larger ones this could get inefficient. Which brings me to 2. It collects all the results in memory, so in general its not suitable for large data sets anyways. 3. The scroll window was fixed to 1 minute. That's a reasonable default but this isn't a decision that the library can make categorically. 4. *only* scanSearch was exported, making it impossible to perform a scrolling search if you wanted something more efficient. This change leaves scanSearch how it is but adds getInitialScroll and advanceScroll which can be easily used to create a pipes/conduits/what-have-you stream of hits. The only semantic changes are: 1. ScrollId is now a newtype as originally intended by the feature's author. 2. MonadThrow has been introduced into the type signature for the (AFAIK) impossible edge case of the server returning a response that can't be parsed as an EsError.

commit39e49f85f689ea45e356cab56f613d02bae1f54a authorMichael Xavier <michael@michaelxavier.net> dateThu 5 Nov 2015, 10:30 (11 years ago) parentbb72983
2 files changed+52−22
src/Database/Bloodhound/Client.hs+50−20 src/Database/Bloodhound/Types.hs+2−2
src/Database/Bloodhound/Client.hs+50−20
@@ -42,6 +42,8 @@ module Database.Bloodhound.Client
4242 , searchByIndex
4343 , searchByType
4444 , scanSearch
45+ , getInitialScroll
46+ , advanceScroll
4547 , refreshIndex
4648 , mkSearch
4749 , mkAggregateSearch
@@ -78,6 +80,7 @@ import Data.Monoid
7880import Data.Text (Text)
7981import qualified Data.Text as T
8082import qualified Data.Text.Encoding as T
83+import Data.Time.Clock
8184import qualified Data.Vector as V
8285import Network.HTTP.Client
8386import qualified Network.HTTP.Types.Method as NHTM
@@ -608,8 +611,12 @@ searchByType (IndexName indexName)
608611 (MappingName mappingName) = bindM2 dispatchSearch url . return
609612 where url = joinPath [indexName, mappingName, "_search"]
610613
611−scanSearch' :: MonadBH m => IndexName -> MappingName -> Search -> m (Maybe ScrollId)
612−scanSearch' (IndexName indexName) (MappingName mappingName) search = do
614+-- | For a given scearch, request a scroll for efficient streaming of
615+-- search results. Note that the search is put into 'SearchTypeScan'
616+-- mode and thus results will not be sorted. Combine this with
617+-- 'advanceScroll' to efficiently stream through the full result set
618+getInitialScroll :: MonadBH m => IndexName -> MappingName -> Search -> m (Maybe ScrollId)
619+getInitialScroll (IndexName indexName) (MappingName mappingName) search = do
613620 let url = joinPath [indexName, mappingName, "_search"]
614621 search' = search { searchType = SearchTypeScan }
615622 resp' <- bindM2 dispatchSearch url (return search')
@@ -617,31 +624,54 @@ scanSearch' (IndexName indexName) (MappingName mappingName) search = do
617624 msid = maybe Nothing scrollId msr
618625 return msid
619626
620−scroll' :: (FromJSON a, MonadBH m) => Maybe ScrollId -> m ([Hit a], Maybe ScrollId)
627+scroll' :: (FromJSON a, MonadBH m, MonadThrow m) => Maybe ScrollId -> m ([Hit a], Maybe ScrollId)
621628scroll' Nothing = return ([], Nothing)
622629scroll' (Just sid) = do
623− url <- joinPath ["_search/scroll?scroll=1m"]
624− resp' <- post url (Just . L.fromStrict $ T.encodeUtf8 sid)
625− let msr = decode' $ responseBody resp' :: FromJSON a => Maybe (SearchResult a)
626− resp = case msr of
627− Just sr -> (hits $ searchHits sr, scrollId sr)
628− _ -> ([], Nothing)
629− return resp
630−
631−simpleAccumilator :: (FromJSON a, MonadBH m) => [Hit a] -> ([Hit a], Maybe ScrollId) -> m ([Hit a], Maybe ScrollId)
632−simpleAccumilator oldHits (newHits, Nothing) = return (oldHits ++ newHits, Nothing)
633−simpleAccumilator oldHits ([], _) = return (oldHits, Nothing)
634−simpleAccumilator oldHits (newHits, msid) = do
630+ res <- advanceScroll sid 60
631+ case res of
632+ Right SearchResult {..} -> return (hits searchHits, scrollId)
633+ Left _ -> return ([], Nothing)
634+
635+-- | Use the given scroll to fetch the next page of documents. If
636+-- there are still further pages, there will be a value in the
637+-- 'scrollId' field of the 'SearchResult'
638+advanceScroll
639+ :: ( FromJSON a
640+ , MonadBH m
641+ , MonadThrow m
642+ )
643+ => ScrollId
644+ -> NominalDiffTime
645+ -- ^ How long should the snapshot of data be kept around? This timeout is updated every time 'advanceScroll' is used, so don't feel the need to set it to the entire duration of your search processing. Note that durations < 1s will be rounded up. Also note that 'NominalDiffTime' is an instance of Num so literals like 60 will be interpreted as seconds. 60s is a reasonable default.
646+ -> m (Either EsError (SearchResult a))
647+advanceScroll (ScrollId sid) scroll = do
648+ url <- joinPath ["_search/scroll?scroll=" <> scrollTime]
649+ parseEsResponse =<< post url (Just . L.fromStrict $ T.encodeUtf8 sid)
650+ where scrollTime = showText secs <> "s"
651+ secs :: Integer
652+ secs = round scroll
653+
654+simpleAccumulator :: (FromJSON a, MonadBH m, MonadThrow m) => [Hit a] -> ([Hit a], Maybe ScrollId) -> m ([Hit a], Maybe ScrollId)
655+simpleAccumulator oldHits (newHits, Nothing) = return (oldHits ++ newHits, Nothing)
656+simpleAccumulator oldHits ([], _) = return (oldHits, Nothing)
657+simpleAccumulator oldHits (newHits, msid) = do
635658 (newHits', msid') <- scroll' msid
636− simpleAccumilator (oldHits ++ newHits) (newHits', msid')
659+ simpleAccumulator (oldHits ++ newHits) (newHits', msid')
637660
638661-- | 'scanSearch' uses the 'scan&scroll' API of elastic,
639−-- for a given 'IndexName' and 'MappingName',
640−scanSearch :: (FromJSON a, MonadBH m) => IndexName -> MappingName -> Search -> m [Hit a]
662+-- for a given 'IndexName' and 'MappingName'. Note that this will
663+-- consume the entire search result set and will be doing O(n) list
664+-- appends so this may not be suitable for large result sets. In that
665+-- case, 'getInitialScroll' and 'advanceScroll' are good low level
666+-- tools. You should be able to hook them up trivially to conduit,
667+-- pipes, or your favorite streaming IO abstraction of choice. Note
668+-- that ordering on the search would destroy performance and thus is
669+-- ignored.
670+scanSearch :: (FromJSON a, MonadBH m, MonadThrow m) => IndexName -> MappingName -> Search -> m [Hit a]
641671scanSearch indexName mappingName search = do
642− msid <- scanSearch' indexName mappingName search
672+ msid <- getInitialScroll indexName mappingName search
643673 (hits, msid') <- scroll' msid
644− (totalHits, _) <- simpleAccumilator [] (hits, msid')
674+ (totalHits, _) <- simpleAccumulator [] (hits, msid')
645675 return totalHits
646676
647677-- | 'mkSearch' is a helper function for defaulting additional fields of a 'Search'
src/Database/Bloodhound/Types.hs+2−2
@@ -83,7 +83,7 @@ module Database.Bloodhound.Types
8383 , Search(..)
8484 , SearchType(..)
8585 , SearchResult(..)
86− , ScrollId
86+ , ScrollId(..)
8787 , SearchHits(..)
8888 , TrackSortScores
8989 , From(..)
@@ -1332,7 +1332,7 @@ data SearchResult a =
13321332 , aggregations :: Maybe AggregationResults
13331333 , scrollId :: Maybe ScrollId } deriving (Eq, Show)
13341334
1335−type ScrollId = Text -- Fixme: Newtype
1335+newtype ScrollId = ScrollId Text deriving (Eq, Show, Ord, ToJSON, FromJSON)
13361336
13371337type Score = Maybe Double
13381338