1{-# LANGUAGE OverloadedStrings #-}
2{-# LANGUAGE RecordWildCards #-}
3{-# LANGUAGE TupleSections #-}
5-------------------------------------------------------------------------------
7-- Module : Database.Bloodhound.Client
8-- Copyright : (C) 2014, 2018 Chris Allen
9-- License : BSD-style (see the file LICENSE)
10-- Maintainer : Chris Allen <cma@bitemyapp.com>
11-- Stability : provisional
12-- Portability : OverloadedStrings
14-- Client side functions for talking to Elasticsearch servers.
16-------------------------------------------------------------------------------
18module Database.Bloodhound.Client
19 ( -- * Bloodhound client functions
20 -- | The examples in this module assume the following code has been run.
21 -- The :{ and :} will only work in GHCi. You'll only need the data types
22 -- and typeclass instances for the functions that make use of them.
43 -- *** Index Templates
62 , getInitialSortedScroll
73 -- ** Snapshot/Restore
83 -- *** Restoring Snapshots
88 -- ** Request Utilities
89 , encodeBulkOperations
93 -- * Reply-handling tools
101import qualified Blaze.ByteString.Builder as BB
102import Control.Applicative as A
104import Control.Monad.Catch
105import Control.Monad.IO.Class
107import Data.ByteString.Lazy.Builder
108import qualified Data.ByteString.Lazy.Char8 as L
109import Data.Foldable (toList)
110import qualified Data.HashMap.Strict as HM
112import qualified Data.List as LS (filter, foldl')
113import Data.List.NonEmpty (NonEmpty (..))
114import Data.Maybe (catMaybes, fromMaybe, isJust)
116import Data.Text (Text)
117import qualified Data.Text as T
118import qualified Data.Text.Encoding as T
119import Data.Time.Clock
120import qualified Data.Vector as V
121import Network.HTTP.Client
122import qualified Network.HTTP.Types.Method as NHTM
123import qualified Network.HTTP.Types.Status as NHTS
124import qualified Network.HTTP.Types.URI as NHTU
125import qualified Network.URI as URI
126import Prelude hiding (filter, head)
128import Database.Bloodhound.Types
131-- >>> :set -XOverloadedStrings
132-- >>> :set -XDeriveGeneric
133-- >>> import Database.Bloodhound
134-- >>> import Network.HTTP.Client
135-- >>> let testServer = (Server "http://localhost:9200")
136-- >>> let runBH' = withBH defaultManagerSettings testServer
137-- >>> let testIndex = IndexName "twitter"
138-- >>> let testMapping = MappingName "tweet"
139-- >>> let defaultIndexSettings = IndexSettings (ShardCount 1) (ReplicaCount 0)
140-- >>> data TweetMapping = TweetMapping deriving (Eq, Show)
141-- >>> _ <- runBH' $ deleteIndex testIndex
142-- >>> _ <- runBH' $ deleteIndex (IndexName "didimakeanindex")
143-- >>> import GHC.Generics
144-- >>> import Data.Time.Calendar (Day (..))
145-- >>> import Data.Time.Clock (UTCTime (..), secondsToDiffTime)
147--instance ToJSON TweetMapping where
148-- toJSON TweetMapping =
149-- object ["properties" .=
150-- object ["location" .=
151-- object ["type" .= ("geo_point" :: Text)]]]
152--data Location = Location { lat :: Double
153-- , lon :: Double } deriving (Eq, Generic, Show)
154--data Tweet = Tweet { user :: Text
155-- , postDate :: UTCTime
158-- , location :: Location } deriving (Eq, Generic, Show)
159--exampleTweet = Tweet { user = "bitemyapp"
160-- , postDate = UTCTime
161-- (ModifiedJulianDay 55000)
162-- (secondsToDiffTime 10)
163-- , message = "Use haskell!"
165-- , location = Location 40.12 (-71.34) }
166--instance ToJSON Tweet where
167-- toJSON = genericToJSON defaultOptions
168--instance FromJSON Tweet where
169-- parseJSON = genericParseJSON defaultOptions
170--instance ToJSON Location where
171-- toJSON = genericToJSON defaultOptions
172--instance FromJSON Location where
173-- parseJSON = genericParseJSON defaultOptions
174--data BulkTest = BulkTest { name :: Text } deriving (Eq, Generic, Show)
175--instance FromJSON BulkTest where
176-- parseJSON = genericParseJSON defaultOptions
177--instance ToJSON BulkTest where
178-- toJSON = genericToJSON defaultOptions
181-- | 'mkShardCount' is a straight-forward smart constructor for 'ShardCount'
182-- which rejects 'Int' values below 1 and above 1000.
184-- >>> mkShardCount 10
185-- Just (ShardCount 10)
186mkShardCount :: Int -> Maybe ShardCount
190 | otherwise = Just (ShardCount n)
192-- | 'mkReplicaCount' is a straight-forward smart constructor for 'ReplicaCount'
193-- which rejects 'Int' values below 0 and above 1000.
195-- >>> mkReplicaCount 10
196-- Just (ReplicaCount 10)
197mkReplicaCount :: Int -> Maybe ReplicaCount
200 | n > 1000 = Nothing -- ...
201 | otherwise = Just (ReplicaCount n)
203emptyBody :: L.ByteString
209 -> Maybe L.ByteString
211dispatch dMethod url body = do
212 initReq <- liftIO $ parseUrl' url
213 reqHook <- bhRequestHook A.<$> getBHEnv
214 let reqBody = RequestBodyLBS $ fromMaybe emptyBody body
217 $ setRequestIgnoreStatus
218 $ initReq { method = dMethod
220 -- "application/x-ndjson" for bulk
221 ("Content-Type", "application/json") : requestHeaders initReq
222 , requestBody = reqBody }
223 -- req <- liftIO $ reqHook $ setRequestIgnoreStatus $ initReq { method = dMethod
224 -- , requestBody = reqBody }
225 mgr <- bhManager <$> getBHEnv
226 liftIO $ httpLbs req mgr
228joinPath' :: [Text] -> Text
229joinPath' = T.intercalate "/"
231joinPath :: MonadBH m => [Text] -> m Text
233 Server s <- bhServer <$> getBHEnv
234 return $ joinPath' (s:ps)
236appendSearchTypeParam :: Text -> SearchType -> Text
237appendSearchTypeParam originalUrl st = addQuery params originalUrl
238 where stText = "search_type"
240 | st == SearchTypeDfsQueryThenFetch = [(stText, Just "dfs_query_then_fetch")]
241 -- used to catch 'SearchTypeQueryThenFetch', which is also the default
244-- | Severely dumbed down query renderer. Assumes your data doesn't
246addQuery :: [(Text, Maybe Text)] -> Text -> Text
247addQuery q u = u <> rendered
250 T.decodeUtf8 $ BB.toByteString $ NHTU.renderQueryText prependQuestionMark q
251 prependQuestionMark = True
253bindM2 :: (Applicative m, Monad m) => (a -> b -> m c) -> m a -> m b -> m c
254bindM2 f ma mb = join (f <$> ma <*> mb)
256-- | Convenience function that sets up a manager and BHEnv and runs
257-- the given set of bloodhound operations. Connections will be
258-- pipelined automatically in accordance with the given manager
259-- settings in IO. If you've got your own monad transformer stack, you
260-- should use 'runBH' directly.
261withBH :: ManagerSettings -> Server -> BH IO a -> IO a
264 let env = mkBHEnv s mgr
267-- Shortcut functions for HTTP methods
268delete :: MonadBH m => Text -> m Reply
269delete = flip (dispatch NHTM.methodDelete) Nothing
270get :: MonadBH m => Text -> m Reply
271get = flip (dispatch NHTM.methodGet) Nothing
272head :: MonadBH m => Text -> m Reply
273head = flip (dispatch NHTM.methodHead) Nothing
274put :: MonadBH m => Text -> Maybe L.ByteString -> m Reply
275put = dispatch NHTM.methodPut
276post :: MonadBH m => Text -> Maybe L.ByteString -> m Reply
277post = dispatch NHTM.methodPost
279-- indexDocument s ix name doc = put (root </> s </> ix </> name </> doc) (Just encode doc)
280-- http://hackage.haskell.org/package/http-client-lens-0.1.0/docs/Network-HTTP-Client-Lens.html
281-- https://github.com/supki/libjenkins/blob/master/src/Jenkins/Rest/Internal.hs
283-- | 'getStatus' fetches the 'Status' of a 'Server'
285-- >>> serverStatus <- runBH' getStatus
286-- >>> fmap tagline (serverStatus)
287-- Just "You Know, for Search"
288getStatus :: MonadBH m => m (Maybe Status)
290 response <- get =<< url
291 return $ decode (responseBody response)
292 where url = joinPath []
294-- | 'getSnapshotRepos' gets the definitions of a subset of the
295-- defined snapshot repos.
300 => SnapshotRepoSelection
301 -> m (Either EsError [GenericSnapshotRepo])
302getSnapshotRepos sel = fmap (fmap unGSRs) . parseEsResponse =<< get =<< url
304 url = joinPath ["_snapshot", selectorSeg]
305 selectorSeg = case sel of
306 AllSnapshotRepos -> "_all"
307 SnapshotRepoList (p :| ps) -> T.intercalate "," (renderPat <$> (p:ps))
308 renderPat (RepoPattern t) = t
309 renderPat (ExactRepo (SnapshotRepoName t)) = t
312-- | Wrapper to extract the list of 'GenericSnapshotRepo' in the
313-- format they're returned in
314newtype GSRs = GSRs { unGSRs :: [GenericSnapshotRepo] }
317instance FromJSON GSRs where
318 parseJSON = withObject "Collection of GenericSnapshotRepo" parse
320 parse = fmap GSRs . mapM (uncurry go) . HM.toList
321 go rawName = withObject "GenericSnapshotRepo" $ \o ->
322 GenericSnapshotRepo (SnapshotRepoName rawName) <$> o .: "type"
326-- | Create or update a snapshot repo
331 => SnapshotRepoUpdateSettings
332 -- ^ Use 'defaultSnapshotRepoUpdateSettings' if unsure
335updateSnapshotRepo SnapshotRepoUpdateSettings {..} repo =
336 bindM2 put url (return (Just body))
338 url = addQuery params <$> joinPath ["_snapshot", snapshotRepoName gSnapshotRepoName]
340 | repoUpdateVerify = []
341 | otherwise = [("verify", Just "false")]
342 body = encode $ object [ "type" .= gSnapshotRepoType
343 , "settings" .= gSnapshotRepoSettings
345 GenericSnapshotRepo {..} = toGSnapshotRepo repo
349-- | Verify if a snapshot repo is working. __NOTE:__ this API did not
350-- make it into Elasticsearch until 1.4. If you use an older version,
351-- you will get an error here.
357 -> m (Either EsError SnapshotVerification)
358verifySnapshotRepo (SnapshotRepoName n) =
359 parseEsResponse =<< bindM2 post url (return Nothing)
361 url = joinPath ["_snapshot", n, "_verify"]
364deleteSnapshotRepo :: MonadBH m => SnapshotRepoName -> m Reply
365deleteSnapshotRepo (SnapshotRepoName n) = delete =<< url
367 url = joinPath ["_snapshot", n]
370-- | Create and start a snapshot
375 -> SnapshotCreateSettings
377createSnapshot (SnapshotRepoName repoName)
378 (SnapshotName snapName)
379 SnapshotCreateSettings {..} =
380 bindM2 put url (return (Just body))
382 url = addQuery params <$> joinPath ["_snapshot", repoName, snapName]
383 params = [("wait_for_completion", Just (boolQP snapWaitForCompletion))]
384 body = encode $ object prs
385 prs = catMaybes [ ("indices" .=) . indexSelectionName <$> snapIndices
386 , Just ("ignore_unavailable" .= snapIgnoreUnavailable)
387 , Just ("ignore_global_state" .= snapIncludeGlobalState)
388 , Just ("partial" .= snapPartial)
392indexSelectionName :: IndexSelection -> Text
393indexSelectionName AllIndexes = "_all"
394indexSelectionName (IndexList (i :| is)) = T.intercalate "," (renderIndex <$> (i:is))
396 renderIndex (IndexName n) = n
399-- | Get info about known snapshots given a pattern and repo name.
406 -> m (Either EsError [SnapshotInfo])
407getSnapshots (SnapshotRepoName repoName) sel =
408 fmap (fmap unSIs) . parseEsResponse =<< get =<< url
410 url = joinPath ["_snapshot", repoName, snapPath]
411 snapPath = case sel of
412 AllSnapshots -> "_all"
413 SnapshotList (s :| ss) -> T.intercalate "," (renderPath <$> (s:ss))
414 renderPath (SnapPattern t) = t
415 renderPath (ExactSnap (SnapshotName t)) = t
418newtype SIs = SIs { unSIs :: [SnapshotInfo] }
421instance FromJSON SIs where
422 parseJSON = withObject "Collection of SnapshotInfo" parse
424 parse o = SIs <$> o .: "snapshots"
427-- | Delete a snapshot. Cancels if it is running.
428deleteSnapshot :: MonadBH m => SnapshotRepoName -> SnapshotName -> m Reply
429deleteSnapshot (SnapshotRepoName repoName) (SnapshotName snapName) =
432 url = joinPath ["_snapshot", repoName, snapName]
435-- | Restore a snapshot to the cluster See
436-- <https://www.elastic.co/guide/en/elasticsearch/reference/1.7/modules-snapshots.html#_restore>
442 -> SnapshotRestoreSettings
443 -- ^ Start with 'defaultSnapshotRestoreSettings' and customize
444 -- from there for reasonable defaults.
446restoreSnapshot (SnapshotRepoName repoName)
447 (SnapshotName snapName)
448 SnapshotRestoreSettings {..} = bindM2 post url (return (Just body))
450 url = addQuery params <$> joinPath ["_snapshot", repoName, snapName, "_restore"]
451 params = [("wait_for_completion", Just (boolQP snapRestoreWaitForCompletion))]
452 body = encode (object prs)
455 prs = catMaybes [ ("indices" .=) . indexSelectionName <$> snapRestoreIndices
456 , Just ("ignore_unavailable" .= snapRestoreIgnoreUnavailable)
457 , Just ("include_global_state" .= snapRestoreIncludeGlobalState)
458 , ("rename_pattern" .=) <$> snapRestoreRenamePattern
459 , ("rename_replacement" .=) . renderTokens <$> snapRestoreRenameReplacement
460 , Just ("include_aliases" .= snapRestoreIncludeAliases)
461 , ("index_settings" .= ) <$> snapRestoreIndexSettingsOverrides
462 , ("ignore_index_settings" .= ) <$> snapRestoreIgnoreIndexSettings
464 renderTokens (t :| ts) = mconcat (renderToken <$> (t:ts))
465 renderToken (RRTLit t) = t
466 renderToken RRSubWholeMatch = "$0"
467 renderToken (RRSubGroup g) = T.pack (show (rrGroupRefNum g))
475 -> m (Either EsError NodesInfo)
476getNodesInfo sel = parseEsResponse =<< get =<< url
478 url = joinPath ["_nodes", selectionSeg]
479 selectionSeg = case sel of
480 LocalNode -> "_local"
481 NodeList (l :| ls) -> T.intercalate "," (selToSeg <$> (l:ls))
483 selToSeg (NodeByName (NodeName n)) = n
484 selToSeg (NodeByFullNodeId (FullNodeId i)) = i
485 selToSeg (NodeByHost (Server s)) = s
486 selToSeg (NodeByAttribute (NodeAttrName a) v) = a <> ":" <> v
493 -> m (Either EsError NodesStats)
494getNodesStats sel = parseEsResponse =<< get =<< url
496 url = joinPath ["_nodes", selectionSeg, "stats"]
497 selectionSeg = case sel of
498 LocalNode -> "_local"
499 NodeList (l :| ls) -> T.intercalate "," (selToSeg <$> (l:ls))
501 selToSeg (NodeByName (NodeName n)) = n
502 selToSeg (NodeByFullNodeId (FullNodeId i)) = i
503 selToSeg (NodeByHost (Server s)) = s
504 selToSeg (NodeByAttribute (NodeAttrName a) v) = a <> ":" <> v
506-- | 'createIndex' will create an index given a 'Server', 'IndexSettings', and an 'IndexName'.
508-- >>> response <- runBH' $ createIndex defaultIndexSettings (IndexName "didimakeanindex")
509-- >>> respIsTwoHunna response
511-- >>> runBH' $ indexExists (IndexName "didimakeanindex")
513createIndex :: MonadBH m => IndexSettings -> IndexName -> m Reply
514createIndex indexSettings (IndexName indexName) =
515 bindM2 put url (return body)
516 where url = joinPath [indexName]
517 body = Just $ encode indexSettings
519-- | Create an index, providing it with any number of settings. This
520-- is more expressive than 'createIndex' but makes is more verbose
521-- for the common case of configuring only the shard count and
523createIndexWith :: MonadBH m
524 => [UpdatableIndexSetting]
525 -> Int -- ^ shard count
528createIndexWith updates shards (IndexName indexName) =
529 bindM2 put url (return (Just body))
530 where url = joinPath [indexName]
531 body = encode $ object
532 ["settings" .= deepMerge
533 ( HM.singleton "index.number_of_shards" (toJSON shards) :
534 [u | Object u <- toJSON <$> updates]
538-- | 'flushIndex' will flush an index given a 'Server' and an 'IndexName'.
539flushIndex :: MonadBH m => IndexName -> m Reply
540flushIndex (IndexName indexName) = do
541 path <- joinPath [indexName, "_flush"]
544-- | 'deleteIndex' will delete an index given a 'Server' and an 'IndexName'.
546-- >>> _ <- runBH' $ createIndex defaultIndexSettings (IndexName "didimakeanindex")
547-- >>> response <- runBH' $ deleteIndex (IndexName "didimakeanindex")
548-- >>> respIsTwoHunna response
550-- >>> runBH' $ indexExists (IndexName "didimakeanindex")
552deleteIndex :: MonadBH m => IndexName -> m Reply
553deleteIndex (IndexName indexName) =
554 delete =<< joinPath [indexName]
556-- | 'updateIndexSettings' will apply a non-empty list of setting updates to an index
558-- >>> _ <- runBH' $ createIndex defaultIndexSettings (IndexName "unconfiguredindex")
559-- >>> response <- runBH' $ updateIndexSettings (BlocksWrite False :| []) (IndexName "unconfiguredindex")
560-- >>> respIsTwoHunna response
562updateIndexSettings :: MonadBH m => NonEmpty UpdatableIndexSetting -> IndexName -> m Reply
563updateIndexSettings updates (IndexName indexName) =
564 bindM2 put url (return body)
566 url = joinPath [indexName, "_settings"]
567 body = Just (encode jsonBody)
568 jsonBody = Object (deepMerge [u | Object u <- toJSON <$> toList updates])
571getIndexSettings :: (MonadBH m, MonadThrow m) => IndexName
572 -> m (Either EsError IndexSettingsSummary)
573getIndexSettings (IndexName indexName) =
574 parseEsResponse =<< get =<< url
576 url = joinPath [indexName, "_settings"]
578-- | 'forceMergeIndex'
580-- The force merge API allows to force merging of one or more indices through
581-- an API. The merge relates to the number of segments a Lucene index holds
582-- within each shard. The force merge operation allows to reduce the number of
583-- segments by merging them.
585-- This call will block until the merge is complete. If the http connection is
586-- lost, the request will continue in the background, and any new requests will
587-- block until the previous force merge is complete.
589-- For more information see
590-- <https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-forcemerge.html#indices-forcemerge>.
592-- worthwhile comes back in the reply body, so matching on the status
595-- 'forceMergeIndex' with a maxNumSegments of 1 and onlyExpungeDeletes
596-- to True is the main way to release disk space back to the OS being
597-- held by deleted documents.
599-- >>> let ixn = IndexName "unoptimizedindex"
600-- >>> _ <- runBH' $ deleteIndex ixn >> createIndex defaultIndexSettings ixn
601-- >>> response <- runBH' $ forceMergeIndex (IndexList (ixn :| [])) (defaultIndexOptimizationSettings { maxNumSegments = Just 1, onlyExpungeDeletes = True })
602-- >>> respIsTwoHunna response
604forceMergeIndex :: MonadBH m => IndexSelection -> ForceMergeIndexSettings -> m Reply
605forceMergeIndex ixs ForceMergeIndexSettings {..} =
606 bindM2 post url (return body)
607 where url = addQuery params <$> joinPath [indexName, "_forcemerge"]
608 params = catMaybes [ ("max_num_segments",) . Just . showText <$> maxNumSegments
609 , Just ("only_expunge_deletes", Just (boolQP onlyExpungeDeletes))
610 , Just ("flush", Just (boolQP flushAfterOptimize))
612 indexName = indexSelectionName ixs
616deepMerge :: [Object] -> Object
617deepMerge = LS.foldl' go mempty
618 where go acc = LS.foldl' go' acc . HM.toList
619 go' acc (k, v) = HM.insertWith merge k v acc
620 merge (Object a) (Object b) = Object (deepMerge [a, b])
624statusCodeIs :: (Int, Int) -> Reply -> Bool
625statusCodeIs r resp = inRange r $ NHTS.statusCode (responseStatus resp)
627respIsTwoHunna :: Reply -> Bool
628respIsTwoHunna = statusCodeIs (200, 299)
630existentialQuery :: MonadBH m => Text -> m (Reply, Bool)
631existentialQuery url = do
633 return (reply, respIsTwoHunna reply)
636-- | Tries to parse a response body as the expected type @a@ and
637-- failing that tries to parse it as an EsError. All well-formed, JSON
638-- responses from elasticsearch should fall into these two
639-- categories. If they don't, a 'EsProtocolException' will be
640-- thrown. If you encounter this, please report the full body it
641-- reports along with your Elasticsearch verison.
642parseEsResponse :: ( MonadThrow m
646 -> m (Either EsError a)
648 | respIsTwoHunna reply = case eitherDecode body of
649 Right a -> return (Right a)
652 | otherwise = tryParseError "Non-200 status code"
653 where body = responseBody reply
654 tryParseError originalError
655 = case eitherDecode body of
656 Right e -> return (Left e)
657 -- Failed to parse the error message.
658 Left err -> explode ("Original error was: " <> originalError <> " Error parse failure was: " <> err)
659 explode errorMsg = throwM (EsProtocolException (T.pack errorMsg) body)
661-- | 'indexExists' enables you to check if an index exists. Returns 'Bool'
664-- >>> exists <- runBH' $ indexExists testIndex
665indexExists :: MonadBH m => IndexName -> m Bool
666indexExists (IndexName indexName) = do
667 (_, exists) <- existentialQuery =<< joinPath [indexName]
670-- | 'refreshIndex' will force a refresh on an index. You must
671-- do this if you want to read what you wrote.
673-- >>> _ <- runBH' $ createIndex defaultIndexSettings testIndex
674-- >>> _ <- runBH' $ refreshIndex testIndex
675refreshIndex :: MonadBH m => IndexName -> m Reply
676refreshIndex (IndexName indexName) =
677 bindM2 post url (return Nothing)
678 where url = joinPath [indexName, "_refresh"]
680-- | Block until the index becomes available for indexing
681-- documents. This is useful for integration tests in which
682-- indices are rapidly created and deleted.
683waitForYellowIndex :: MonadBH m => IndexName -> m Reply
684waitForYellowIndex (IndexName indexName) = get =<< url
685 where url = addQuery q <$> joinPath ["_cluster","health",indexName]
686 q = [("wait_for_status",Just "yellow"),("timeout",Just "10s")]
688stringifyOCIndex :: OpenCloseIndex -> Text
689stringifyOCIndex oci = case oci of
691 CloseIndex -> "_close"
693openOrCloseIndexes :: MonadBH m => OpenCloseIndex -> IndexName -> m Reply
694openOrCloseIndexes oci (IndexName indexName) =
695 bindM2 post url (return Nothing)
696 where ociString = stringifyOCIndex oci
697 url = joinPath [indexName, ociString]
699-- | 'openIndex' opens an index given a 'Server' and an 'IndexName'. Explained in further detail at
700-- <http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-open-close.html>
702-- >>> reply <- runBH' $ openIndex testIndex
703openIndex :: MonadBH m => IndexName -> m Reply
704openIndex = openOrCloseIndexes OpenIndex
706-- | 'closeIndex' closes an index given a 'Server' and an 'IndexName'. Explained in further detail at
707-- <http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-open-close.html>
709-- >>> reply <- runBH' $ closeIndex testIndex
710closeIndex :: MonadBH m => IndexName -> m Reply
711closeIndex = openOrCloseIndexes CloseIndex
713-- | 'listIndices' returns a list of all index names on a given 'Server'
714listIndices :: (MonadThrow m, MonadBH m) => m [IndexName]
716 parse . responseBody =<< get =<< url
718 url = joinPath ["_cat/indices?format=json"]
719 parse body = either (\msg -> (throwM (EsProtocolException (T.pack msg) body))) return $ do
720 vals <- eitherDecode body
724 case HM.lookup "index" obj of
725 (Just (String txt)) -> Right (IndexName txt)
726 v -> Left $ "indexVal in listIndices failed on non-string, was: " <> show v
727 v -> Left $ "One of the values parsed in listIndices wasn't an object, it was: " <> show v
729-- | 'updateIndexAliases' updates the server's index alias
730-- table. Operations are atomic. Explained in further detail at
731-- <https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-aliases.html>
733-- >>> let src = IndexName "a-real-index"
734-- >>> let aliasName = IndexName "an-alias"
735-- >>> let iAlias = IndexAlias src (IndexAliasName aliasName)
736-- >>> let aliasCreate = IndexAliasCreate Nothing Nothing
737-- >>> _ <- runBH' $ deleteIndex src
738-- >>> respIsTwoHunna <$> runBH' (createIndex defaultIndexSettings src)
740-- >>> runBH' $ indexExists src
742-- >>> respIsTwoHunna <$> runBH' (updateIndexAliases (AddAlias iAlias aliasCreate :| []))
744-- >>> runBH' $ indexExists aliasName
746updateIndexAliases :: MonadBH m => NonEmpty IndexAliasAction -> m Reply
747updateIndexAliases actions = bindM2 post url (return body)
748 where url = joinPath ["_aliases"]
749 body = Just (encode bodyJSON)
750 bodyJSON = object [ "actions" .= toList actions]
752-- | Get all aliases configured on the server.
753getIndexAliases :: (MonadBH m, MonadThrow m)
754 => m (Either EsError IndexAliasesSummary)
755getIndexAliases = parseEsResponse =<< get =<< url
756 where url = joinPath ["_aliases"]
758-- | Delete a single alias, removing it from all indices it
759-- is currently associated with.
760deleteIndexAlias :: MonadBH m => IndexAliasName -> m Reply
761deleteIndexAlias (IndexAliasName (IndexName name)) = delete =<< url
762 where url = joinPath ["_all","_alias",name]
764-- | 'putTemplate' creates a template given an 'IndexTemplate' and a 'TemplateName'.
765-- Explained in further detail at
766-- <https://www.elastic.co/guide/en/elasticsearch/reference/1.7/indices-templates.html>
768-- >>> let idxTpl = IndexTemplate (TemplatePattern "tweet-*") (Just (IndexSettings (ShardCount 1) (ReplicaCount 1))) [toJSON TweetMapping]
769-- >>> resp <- runBH' $ putTemplate idxTpl (TemplateName "tweet-tpl")
770putTemplate :: MonadBH m => IndexTemplate -> TemplateName -> m Reply
771putTemplate indexTemplate (TemplateName templateName) =
772 bindM2 put url (return body)
773 where url = joinPath ["_template", templateName]
774 body = Just $ encode indexTemplate
776-- | 'templateExists' checks to see if a template exists.
778-- >>> exists <- runBH' $ templateExists (TemplateName "tweet-tpl")
779templateExists :: MonadBH m => TemplateName -> m Bool
780templateExists (TemplateName templateName) = do
781 (_, exists) <- existentialQuery =<< joinPath ["_template", templateName]
784-- | 'deleteTemplate' is an HTTP DELETE and deletes a template.
786-- >>> let idxTpl = IndexTemplate (TemplatePattern "tweet-*") (Just (IndexSettings (ShardCount 1) (ReplicaCount 1))) [toJSON TweetMapping]
787-- >>> _ <- runBH' $ putTemplate idxTpl (TemplateName "tweet-tpl")
788-- >>> resp <- runBH' $ deleteTemplate (TemplateName "tweet-tpl")
789deleteTemplate :: MonadBH m => TemplateName -> m Reply
790deleteTemplate (TemplateName templateName) =
791 delete =<< joinPath ["_template", templateName]
793-- | 'putMapping' is an HTTP PUT and has upsert semantics. Mappings are schemas
794-- for documents in indexes.
796-- >>> _ <- runBH' $ createIndex defaultIndexSettings testIndex
797-- >>> resp <- runBH' $ putMapping testIndex testMapping TweetMapping
799-- Response {responseStatus = Status {statusCode = 200, statusMessage = "OK"}, responseVersion = HTTP/1.1, responseHeaders = [("content-type","application/json; charset=UTF-8"),("content-encoding","gzip"),("transfer-encoding","chunked")], responseBody = "{\"acknowledged\":true}", responseCookieJar = CJ {expose = []}, responseClose' = ResponseClose}
800putMapping :: (MonadBH m, ToJSON a) => IndexName
801 -> MappingName -> a -> m Reply
802putMapping (IndexName indexName) (MappingName mappingName) mapping =
803 bindM2 put url (return body)
804 where url = joinPath [indexName, "_mapping", mappingName]
805 -- "_mapping" and mappingName above were originally transposed
806 -- erroneously. The correct API call is: "/INDEX/_mapping/MAPPING_NAME"
807 body = Just $ encode mapping
809versionCtlParams :: IndexDocumentSettings -> [(Text, Maybe Text)]
810versionCtlParams cfg =
811 case idsVersionControl cfg of
812 NoVersionControl -> []
813 InternalVersion v -> versionParams v "internal"
814 ExternalGT (ExternalDocVersion v) -> versionParams v "external_gt"
815 ExternalGTE (ExternalDocVersion v) -> versionParams v "external_gte"
816 ForceVersion (ExternalDocVersion v) -> versionParams v "force"
818 vt = showText . docVersionNumber
819 versionParams v t = [ ("version", Just $ vt v)
820 , ("version_type", Just t)
823-- | 'indexDocument' is the primary way to save a single document in
824-- Elasticsearch. The document itself is simply something we can
825-- convert into a JSON 'Value'. The 'DocId' will function as the
826-- primary key for the document. You are encouraged to generate
827-- your own id's and not rely on Elasticsearch's automatic id
828-- generation. Read more about it here:
829-- https://github.com/bitemyapp/bloodhound/issues/107
831-- >>> resp <- runBH' $ indexDocument testIndex testMapping defaultIndexDocumentSettings exampleTweet (DocId "1")
833-- Response {responseStatus = Status {statusCode = 201, statusMessage = "Created"}, responseVersion = HTTP/1.1, responseHeaders = [("Location","/twitter/tweet/1"),("content-type","application/json; charset=UTF-8"),("content-encoding","gzip"),("transfer-encoding","chunked")], responseBody = "{\"_index\":\"twitter\",\"_type\":\"tweet\",\"_id\":\"1\",\"_version\":1,\"result\":\"created\",\"_shards\":{\"total\":2,\"successful\":1,\"failed\":0},\"created\":true}", responseCookieJar = CJ {expose = []}, responseClose' = ResponseClose}
834indexDocument :: (ToJSON doc, MonadBH m) => IndexName -> MappingName
835 -> IndexDocumentSettings -> doc -> DocId -> m Reply
836indexDocument (IndexName indexName)
837 (MappingName mappingName) cfg document (DocId docId) =
838 bindM2 put url (return body)
839 where url = addQuery params <$> joinPath [indexName, mappingName, docId]
840 parentParams = case idsParent cfg of
842 Just (DocumentParent (DocId p)) -> [ ("parent", Just p) ]
843 params = versionCtlParams cfg ++ parentParams
844 body = Just (encode document)
846-- | 'updateDocument' provides a way to perform an partial update of a
847-- an already indexed document.
848updateDocument :: (ToJSON patch, MonadBH m) => IndexName -> MappingName
849 -> IndexDocumentSettings -> patch -> DocId -> m Reply
850updateDocument (IndexName indexName)
851 (MappingName mappingName) cfg patch (DocId docId) =
852 bindM2 post url (return body)
853 where url = addQuery (versionCtlParams cfg) <$>
854 joinPath [indexName, mappingName, docId, "_update"]
855 body = Just (encode $ object ["doc" .= toJSON patch])
857-- | 'deleteDocument' is the primary way to delete a single document.
859-- >>> _ <- runBH' $ deleteDocument testIndex testMapping (DocId "1")
860deleteDocument :: MonadBH m => IndexName -> MappingName
862deleteDocument (IndexName indexName)
863 (MappingName mappingName) (DocId docId) =
864 delete =<< joinPath [indexName, mappingName, docId]
867-- <http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-bulk.html Elasticsearch's bulk API>
868-- to perform bulk operations. The 'BulkOperation' data type encodes the
869-- index\/update\/delete\/create operations. You pass a 'V.Vector' of 'BulkOperation's
870-- and a 'Server' to 'bulk' in order to send those operations up to your Elasticsearch
871-- server to be performed. I changed from [BulkOperation] to a Vector due to memory overhead.
873-- >>> let stream = V.fromList [BulkIndex testIndex testMapping (DocId "2") (toJSON (BulkTest "blah"))]
874-- >>> _ <- runBH' $ bulk stream
875-- >>> _ <- runBH' $ refreshIndex testIndex
876bulk :: MonadBH m => V.Vector BulkOperation -> m Reply
878 bindM2 post url (return body)
880 url = joinPath ["_bulk"]
881 body = Just $ encodeBulkOperations bulkOps
883-- | 'encodeBulkOperations' is a convenience function for dumping a vector of 'BulkOperation'
884-- into an 'L.ByteString'
886-- >>> let bulkOps = V.fromList [BulkIndex testIndex testMapping (DocId "2") (toJSON (BulkTest "blah"))]
887-- >>> encodeBulkOperations bulkOps
888-- "\n{\"index\":{\"_type\":\"tweet\",\"_id\":\"2\",\"_index\":\"twitter\"}}\n{\"name\":\"blah\"}\n"
889encodeBulkOperations :: V.Vector BulkOperation -> L.ByteString
890encodeBulkOperations stream = collapsed where
892 fmap encodeBulkOperation stream
894 mash (mempty :: Builder) blobs
896 toLazyByteString $ mappend mashedTaters (byteString "\n")
898mash :: Builder -> V.Vector L.ByteString -> Builder
899mash = V.foldl' (\b x -> b <> byteString "\n" <> lazyByteString x)
901mkBulkStreamValue :: Text -> Text -> Text -> Text -> Value
902mkBulkStreamValue operation indexName mappingName docId =
904 object [ "_index" .= indexName
905 , "_type" .= mappingName
908mkBulkStreamValueAuto :: Text -> Text -> Text -> Value
909mkBulkStreamValueAuto operation indexName mappingName =
911 object [ "_index" .= indexName
912 , "_type" .= mappingName]]
914-- | 'encodeBulkOperation' is a convenience function for dumping a single 'BulkOperation'
915-- into an 'L.ByteString'
917-- >>> let bulkOp = BulkIndex testIndex testMapping (DocId "2") (toJSON (BulkTest "blah"))
918-- >>> encodeBulkOperation bulkOp
919-- "{\"index\":{\"_type\":\"tweet\",\"_id\":\"2\",\"_index\":\"twitter\"}}\n{\"name\":\"blah\"}"
920encodeBulkOperation :: BulkOperation -> L.ByteString
921encodeBulkOperation (BulkIndex (IndexName indexName)
922 (MappingName mappingName)
923 (DocId docId) value) = blob
924 where metadata = mkBulkStreamValue "index" indexName mappingName docId
925 blob = encode metadata `mappend` "\n" `mappend` encode value
927encodeBulkOperation (BulkIndexAuto (IndexName indexName)
928 (MappingName mappingName)
930 where metadata = mkBulkStreamValueAuto "index" indexName mappingName
931 blob = encode metadata `mappend` "\n" `mappend` encode value
933encodeBulkOperation (BulkIndexEncodingAuto (IndexName indexName)
934 (MappingName mappingName)
935 encoding) = toLazyByteString blob
936 where metadata = toEncoding (mkBulkStreamValueAuto "index" indexName mappingName)
937 blob = fromEncoding metadata <> "\n" <> fromEncoding encoding
939encodeBulkOperation (BulkCreate (IndexName indexName)
940 (MappingName mappingName)
941 (DocId docId) value) = blob
942 where metadata = mkBulkStreamValue "create" indexName mappingName docId
943 blob = encode metadata `mappend` "\n" `mappend` encode value
945encodeBulkOperation (BulkDelete (IndexName indexName)
946 (MappingName mappingName)
947 (DocId docId)) = blob
948 where metadata = mkBulkStreamValue "delete" indexName mappingName docId
949 blob = encode metadata
951encodeBulkOperation (BulkUpdate (IndexName indexName)
952 (MappingName mappingName)
953 (DocId docId) value) = blob
954 where metadata = mkBulkStreamValue "update" indexName mappingName docId
955 doc = object ["doc" .= value]
956 blob = encode metadata `mappend` "\n" `mappend` encode doc
958encodeBulkOperation (BulkCreateEncoding (IndexName indexName)
959 (MappingName mappingName)
960 (DocId docId) encoding) = toLazyByteString blob
961 where metadata = toEncoding (mkBulkStreamValue "create" indexName mappingName docId)
962 blob = fromEncoding metadata <> "\n" <> fromEncoding encoding
964-- | 'getDocument' is a straight-forward way to fetch a single document from
965-- Elasticsearch using a 'Server', 'IndexName', 'MappingName', and a 'DocId'.
966-- The 'DocId' is the primary key for your Elasticsearch document.
968-- >>> yourDoc <- runBH' $ getDocument testIndex testMapping (DocId "1")
969getDocument :: MonadBH m => IndexName -> MappingName
971getDocument (IndexName indexName)
972 (MappingName mappingName) (DocId docId) =
973 get =<< joinPath [indexName, mappingName, docId]
975-- | 'documentExists' enables you to check if a document exists. Returns 'Bool'
978-- >>> exists <- runBH' $ documentExists testIndex testMapping Nothing (DocId "1")
979documentExists :: MonadBH m => IndexName -> MappingName
980 -> Maybe DocumentParent -> DocId -> m Bool
981documentExists (IndexName indexName) (MappingName mappingName)
982 parent (DocId docId) = do
983 (_, exists) <- existentialQuery =<< url
985 where url = addQuery params <$> joinPath [indexName, mappingName, docId]
986 parentParam = fmap (\(DocumentParent (DocId p)) -> p) parent
987 params = LS.filter (\(_, v) -> isJust v) [("parent", parentParam)]
989dispatchSearch :: MonadBH m => Text -> Search -> m Reply
990dispatchSearch url search = post url' (Just (encode search))
991 where url' = appendSearchTypeParam url (searchType search)
993-- | 'searchAll', given a 'Search', will perform that search against all indexes
994-- on an Elasticsearch server. Try to avoid doing this if it can be helped.
996-- >>> let query = TermQuery (Term "user" "bitemyapp") Nothing
997-- >>> let search = mkSearch (Just query) Nothing
998-- >>> reply <- runBH' $ searchAll search
999searchAll :: MonadBH m => Search -> m Reply
1000searchAll = bindM2 dispatchSearch url . return
1001 where url = joinPath ["_search"]
1003-- | 'searchByIndex', given a 'Search' and an 'IndexName', will perform that search
1004-- against all mappings within an index on an Elasticsearch server.
1006-- >>> let query = TermQuery (Term "user" "bitemyapp") Nothing
1007-- >>> let search = mkSearch (Just query) Nothing
1008-- >>> reply <- runBH' $ searchByIndex testIndex search
1009searchByIndex :: MonadBH m => IndexName -> Search -> m Reply
1010searchByIndex (IndexName indexName) = bindM2 dispatchSearch url . return
1011 where url = joinPath [indexName, "_search"]
1013-- | 'searchByIndices' is a variant of 'searchByIndex' that executes a
1014-- 'Search' over many indices. This is much faster than using
1015-- 'mapM' to 'searchByIndex' over a collection since it only
1016-- causes a single HTTP request to be emitted.
1017searchByIndices :: MonadBH m => NonEmpty IndexName -> Search -> m Reply
1018searchByIndices ixs = bindM2 dispatchSearch url . return
1019 where url = joinPath [renderedIxs, "_search"]
1020 renderedIxs = T.intercalate (T.singleton ',') (map (\(IndexName t) -> t) (toList ixs))
1022-- | 'searchByType', given a 'Search', 'IndexName', and 'MappingName', will perform that
1023-- search against a specific mapping within an index on an Elasticsearch server.
1025-- >>> let query = TermQuery (Term "user" "bitemyapp") Nothing
1026-- >>> let search = mkSearch (Just query) Nothing
1027-- >>> reply <- runBH' $ searchByType testIndex testMapping search
1028searchByType :: MonadBH m => IndexName -> MappingName -> Search
1030searchByType (IndexName indexName)
1031 (MappingName mappingName) = bindM2 dispatchSearch url . return
1032 where url = joinPath [indexName, mappingName, "_search"]
1034-- | For a given search, request a scroll for efficient streaming of
1035-- search results. Note that the search is put into 'SearchTypeScan'
1036-- mode and thus results will not be sorted. Combine this with
1037-- 'advanceScroll' to efficiently stream through the full result set
1039 (FromJSON a, MonadThrow m, MonadBH m) => IndexName ->
1042 m (Either EsError (SearchResult a))
1043getInitialScroll (IndexName indexName) (MappingName mappingName) search' = do
1044 let url = addQuery params <$> joinPath [indexName, mappingName, "_search"]
1045 params = [("scroll", Just "1m")]
1046 sorting = Just [DefaultSortSpec $ mkSort (FieldName "_doc") Descending]
1047 search = search' { sortBody = sorting }
1048 resp' <- bindM2 dispatchSearch url (return search)
1049 parseEsResponse resp'
1051-- | For a given search, request a scroll for efficient streaming of
1052-- search results. Combine this with 'advanceScroll' to efficiently
1053-- stream through the full result set. Note that this search respects
1054-- sorting and may be less efficient than 'getInitialScroll'.
1055getInitialSortedScroll ::
1056 (FromJSON a, MonadThrow m, MonadBH m) => IndexName ->
1059 m (Either EsError (SearchResult a))
1060getInitialSortedScroll (IndexName indexName) (MappingName mappingName) search = do
1061 let url = addQuery params <$> joinPath [indexName, mappingName, "_search"]
1062 params = [("scroll", Just "1m")]
1063 resp' <- bindM2 dispatchSearch url (return search)
1064 parseEsResponse resp'
1066scroll' :: (FromJSON a, MonadBH m, MonadThrow m) => Maybe ScrollId ->
1067 m ([Hit a], Maybe ScrollId)
1068scroll' Nothing = return ([], Nothing)
1069scroll' (Just sid) = do
1070 res <- advanceScroll sid 60
1072 Right SearchResult {..} -> return (hits searchHits, scrollId)
1073 Left _ -> return ([], Nothing)
1075-- | Use the given scroll to fetch the next page of documents. If there are no
1076-- further pages, 'SearchResult.searchHits.hits' will be '[]'.
1084 -- ^ 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.
1085 -> m (Either EsError (SearchResult a))
1086advanceScroll (ScrollId sid) scroll = do
1087 url <- joinPath ["_search", "scroll"]
1088 resp <- post url (Just $ encode scrollObject)
1089 parseEsResponse resp
1090 where scrollTime = showText secs <> "s"
1094 scrollObject = object [ "scroll" .= scrollTime
1095 , "scroll_id" .= sid
1099 (FromJSON a, MonadBH m, MonadThrow m) =>
1101 ([Hit a], Maybe ScrollId) ->
1102 m ([Hit a], Maybe ScrollId)
1103simpleAccumulator oldHits (newHits, Nothing) = return (oldHits ++ newHits, Nothing)
1104simpleAccumulator oldHits ([], _) = return (oldHits, Nothing)
1105simpleAccumulator oldHits (newHits, msid) = do
1106 (newHits', msid') <- scroll' msid
1107 simpleAccumulator (oldHits ++ newHits) (newHits', msid')
1109-- | 'scanSearch' uses the 'scroll' API of elastic,
1110-- for a given 'IndexName' and 'MappingName'. Note that this will
1111-- consume the entire search result set and will be doing O(n) list
1112-- appends so this may not be suitable for large result sets. In that
1113-- case, 'getInitialScroll' and 'advanceScroll' are good low level
1114-- tools. You should be able to hook them up trivially to conduit,
1115-- pipes, or your favorite streaming IO abstraction of choice. Note
1116-- that ordering on the search would destroy performance and thus is
1118scanSearch :: (FromJSON a, MonadBH m, MonadThrow m) => IndexName
1122scanSearch indexName mappingName search = do
1123 initialSearchResult <- getInitialScroll indexName mappingName search
1124 let (hits', josh) = case initialSearchResult of
1125 Right SearchResult {..} -> (hits searchHits, scrollId)
1126 Left _ -> ([], Nothing)
1127 (totalHits, _) <- simpleAccumulator [] (hits', josh)
1130-- | 'mkSearch' is a helper function for defaulting additional fields of a 'Search'
1131-- to Nothing in case you only care about your 'Query' and 'Filter'. Use record update
1132-- syntax if you want to add things like aggregations or highlights while still using
1133-- this helper function.
1135-- >>> let query = TermQuery (Term "user" "bitemyapp") Nothing
1136-- >>> mkSearch (Just query) Nothing
1137-- Search {queryBody = Just (TermQuery (Term {termField = "user", termValue = "bitemyapp"}) Nothing), filterBody = Nothing, searchAfterKey = Nothing, sortBody = Nothing, aggBody = Nothing, highlight = Nothing, trackSortScores = False, from = From 0, size = Size 10, searchType = SearchTypeQueryThenFetch, fields = Nothing, source = Nothing}
1138mkSearch :: Maybe Query -> Maybe Filter -> Search
1139mkSearch query filter = Search query filter Nothing Nothing Nothing False (From 0) (Size 10) SearchTypeQueryThenFetch Nothing Nothing Nothing Nothing Nothing
1141-- | 'mkAggregateSearch' is a helper function that defaults everything in a 'Search' except for
1142-- the 'Query' and the 'Aggregation'.
1144-- >>> let terms = TermsAgg $ (mkTermsAggregation "user") { termCollectMode = Just BreadthFirst }
1146-- TermsAgg (TermsAggregation {term = Left "user", termInclude = Nothing, termExclude = Nothing, termOrder = Nothing, termMinDocCount = Nothing, termSize = Nothing, termShardSize = Nothing, termCollectMode = Just BreadthFirst, termExecutionHint = Nothing, termAggs = Nothing})
1147-- >>> let myAggregation = mkAggregateSearch Nothing $ mkAggregations "users" terms
1148mkAggregateSearch :: Maybe Query -> Aggregations -> Search
1149mkAggregateSearch query mkSearchAggs = Search query Nothing Nothing (Just mkSearchAggs) Nothing False (From 0) (Size 0) SearchTypeQueryThenFetch Nothing Nothing Nothing Nothing Nothing
1151-- | 'mkHighlightSearch' is a helper function that defaults everything in a 'Search' except for
1152-- the 'Query' and the 'Aggregation'.
1154-- >>> let query = QueryMatchQuery $ mkMatchQuery (FieldName "_all") (QueryString "haskell")
1155-- >>> let testHighlight = Highlights Nothing [FieldHighlight (FieldName "message") Nothing]
1156-- >>> let search = mkHighlightSearch (Just query) testHighlight
1157mkHighlightSearch :: Maybe Query -> Highlights -> Search
1158mkHighlightSearch query searchHighlights = Search query Nothing Nothing Nothing (Just searchHighlights) False (From 0) (Size 10) SearchTypeQueryThenFetch Nothing Nothing Nothing Nothing Nothing
1160-- | 'pageSearch' is a helper function that takes a search and assigns the from
1161-- and size fields for the search. The from parameter defines the offset
1162-- from the first result you want to fetch. The size parameter allows you to
1163-- configure the maximum amount of hits to be returned.
1165-- >>> let query = QueryMatchQuery $ mkMatchQuery (FieldName "_all") (QueryString "haskell")
1166-- >>> let search = mkSearch (Just query) Nothing
1168-- Search {queryBody = Just (QueryMatchQuery (MatchQuery {matchQueryField = FieldName "_all", matchQueryQueryString = QueryString "haskell", matchQueryOperator = Or, matchQueryZeroTerms = ZeroTermsNone, matchQueryCutoffFrequency = Nothing, matchQueryMatchType = Nothing, matchQueryAnalyzer = Nothing, matchQueryMaxExpansions = Nothing, matchQueryLenient = Nothing, matchQueryBoost = Nothing})), filterBody = Nothing, sortBody = Nothing, aggBody = Nothing, highlight = Nothing, trackSortScores = False, from = From 0, size = Size 10, searchType = SearchTypeQueryThenFetch, fields = Nothing, source = Nothing}
1169-- >>> pageSearch (From 10) (Size 100) search
1170-- Search {queryBody = Just (QueryMatchQuery (MatchQuery {matchQueryField = FieldName "_all", matchQueryQueryString = QueryString "haskell", matchQueryOperator = Or, matchQueryZeroTerms = ZeroTermsNone, matchQueryCutoffFrequency = Nothing, matchQueryMatchType = Nothing, matchQueryAnalyzer = Nothing, matchQueryMaxExpansions = Nothing, matchQueryLenient = Nothing, matchQueryBoost = Nothing})), filterBody = Nothing, sortBody = Nothing, aggBody = Nothing, highlight = Nothing, trackSortScores = False, from = From 10, size = Size 100, searchType = SearchTypeQueryThenFetch, fields = Nothing, source = Nothing}
1171pageSearch :: From -- ^ The result offset
1172 -> Size -- ^ The number of results to return
1173 -> Search -- ^ The current seach
1174 -> Search -- ^ The paged search
1175pageSearch resultOffset pageSize search = search { from = resultOffset, size = pageSize }
1177parseUrl' :: MonadThrow m => Text -> m Request
1178parseUrl' t = parseRequest (URI.escapeURIString URI.isAllowedInURI (T.unpack t))
1180-- | Was there an optimistic concurrency control conflict when
1181-- indexing a document?
1182isVersionConflict :: Reply -> Bool
1183isVersionConflict = statusCheck (== 409)
1185isSuccess :: Reply -> Bool
1186isSuccess = statusCheck (inRange (200, 299))
1188isCreated :: Reply -> Bool
1189isCreated = statusCheck (== 201)
1191statusCheck :: (Int -> Bool) -> Reply -> Bool
1192statusCheck prd = prd . NHTS.statusCode . responseStatus
1194-- | This is a hook that can be set via the 'bhRequestHook' function
1195-- that will authenticate all requests using an HTTP Basic
1196-- Authentication header. Note that it is *strongly* recommended that
1197-- this option only be used over an SSL connection.
1199-- >> (mkBHEnv myServer myManager) { bhRequestHook = basicAuthHook (EsUsername "myuser") (EsPassword "mypass") }
1200basicAuthHook :: Monad m => EsUsername -> EsPassword -> Request -> m Request
1201basicAuthHook (EsUsername u) (EsPassword p) = return . applyBasicAuth u' p'
1202 where u' = T.encodeUtf8 u
1206boolQP :: Bool -> Text
1208boolQP False = "false"
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list