bloodhound-1.0.0.0: src/Database/Bloodhound/Dynamic/Client.hs
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- |
-- Module : Database.Bloodhound.Dynamic.Client
-- Copyright : (C) 2014, 2018 Chris Allen
-- License : BSD-style (see the file LICENSE)
-- Maintainer : Chris Allen <cma@bitemyapp.com>
-- Stability : provisional
-- Portability : GHC
--
-- Dynamically route the query depending on the backend.
--
-- @
-- withFetchedBackendType $ \backend ->
-- pitSearch backend index search
-- @
module Database.Bloodhound.Dynamic.Client
( module Reexport,
guessBackendType,
withFetchedBackendType,
pitSearch,
pitSearchWith,
listAllPITs,
)
where
import Data.Aeson
import Data.Maybe (fromMaybe, listToMaybe)
import Data.Versions qualified as Versions
import Database.Bloodhound.Client.Cluster
import Database.Bloodhound.Common.Client as Reexport
import Database.Bloodhound.Common.Types
import Database.Bloodhound.ElasticSearch7.Client qualified as ClientES7
import Database.Bloodhound.ElasticSearch8.Client qualified as ClientES8
import Database.Bloodhound.ElasticSearch9.Client qualified as ClientES9
import Database.Bloodhound.OpenSearch2.Client qualified as ClientOS2
import Database.Bloodhound.OpenSearch2.Types (PITInfo (pitInfoId, pitInfoKeepAlive))
import Database.Bloodhound.OpenSearch3.Client qualified as ClientOS3
import Database.Bloodhound.OpenSearch3.Types qualified as OS3Types
import Lens.Micro (toListOf)
import Prelude hiding (filter, head)
-- | Try to guess the current 'BackendType'
guessBackendType :: NodeInfo -> Maybe BackendType
guessBackendType nodeInfo =
case listToMaybe $ toListOf Versions.major $ versionNumber $ nodeInfoESVersion nodeInfo of
Just 7 -> Just ElasticSearch7
Just 1 -> Just OpenSearch1
Just 2 -> Just OpenSearch2
_ -> Nothing
-- | Fetch the currently running backend and run backend-dependent code
withFetchedBackendType :: (MonadBH m) => (forall backend. SBackendType backend -> m a) -> m a
withFetchedBackendType f = do
nodeInfo <- getNodesInfo LocalNode
let backend = fromMaybe Dynamic $ listToMaybe (nodesInfo nodeInfo) >>= guessBackendType
withDynamicBH backend $ StaticBH . f
-- | 'pitSearch' uses the point in time (PIT) API, for a given
-- 'IndexName'. Requires Elasticsearch >=7.10 or OpenSearch >=2. The supplied
-- 'KeepAlive' duration is forwarded to every PIT open\/extend call. Note that
-- this will consume the entire search result set and will be doing O(n) list
-- appends so this may not be suitable for large result sets. In that case, the
-- point in time API should be used directly with `openPointInTime` and
-- `closePointInTime`.
--
-- This is a convenience wrapper around 'pitSearchWith' that lifts the
-- single 'IndexName' into an 'IndexPattern'. To search across multiple
-- comma-joined indices or a wildcard pattern, use 'pitSearchWith'
-- directly with an 'IndexPattern'.
--
-- Note that 'pitSearch' utilizes the 'search_after' parameter under the hood,
-- which requires a non-empty 'sortBody' field in the provided 'Search' value.
-- Otherwise, 'pitSearch' will fail to return all matching documents.
--
-- For more information see
-- <https://docs.opensearch.org/latest/search-plugins/point-in-time/>.
pitSearch ::
forall a m backend.
(FromJSON a, MonadBH m) =>
SBackendType backend ->
IndexName ->
KeepAlive ->
Search ->
m [Hit a]
pitSearch backend indexName keepAlive search =
pitSearchWith backend (IndexPattern (unIndexName indexName)) keepAlive search
-- | 'pitSearchWith' is the parameterized variant of 'pitSearch': it
-- takes an 'IndexPattern' as the target, which may be a single index
-- name, a comma-separated list of indices, or a wildcard pattern
-- (e.g. @\"logs-*\"@, @\"logs-2024,logs-2025\"@). Otherwise behaves
-- exactly like 'pitSearch'. Requires Elasticsearch >=7.10 or OpenSearch
-- >=2; OpenSearch 1 lacks a PIT API and throws. See
-- 'openPointInTimeWith' for the target rendering.
--
-- For more information see
-- <https://docs.opensearch.org/latest/search-plugins/point-in-time/>.
pitSearchWith ::
forall a m backend.
(FromJSON a, MonadBH m) =>
SBackendType backend ->
IndexPattern ->
KeepAlive ->
Search ->
m [Hit a]
pitSearchWith backend indexPattern keepAlive search =
case backend of
SElasticSearch7 -> unsafePerformBH @'ElasticSearch7 $ ClientES7.pitSearchWith indexPattern keepAlive search
SElasticSearch8 -> unsafePerformBH @'ElasticSearch8 $ ClientES8.pitSearchWith indexPattern keepAlive search
SElasticSearch9 -> unsafePerformBH @'ElasticSearch9 $ ClientES9.pitSearchWith indexPattern keepAlive search
SOpenSearch1 -> throwEsError $ EsError Nothing "pitSearch is not supported by OpenSearch1"
SOpenSearch2 -> unsafePerformBH @'OpenSearch2 $ ClientOS2.pitSearchWith indexPattern keepAlive search
SOpenSearch3 -> unsafePerformBH @'OpenSearch3 $ ClientOS3.pitSearchWith indexPattern keepAlive search
-- | 'listAllPITs' lists every currently-open point in time on the
-- cluster, normalizing the OpenSearch response into a common
-- 'PITSummary'. Only OpenSearch 2 and 3 support a list-all-PITs endpoint
-- (@GET /_search\/point_in_time\/_all@); OpenSearch 1 lacks a PIT API
-- entirely and throws.
--
-- Elasticsearch throws on /every/ major version: the ES point-in-time
-- API only ever documented @POST \/{index}\/_pit@ (open) and
-- @DELETE \/_pit@ (close). @GET \/_pit@ was never an ES operation —
-- verified live (bloodhound-cfv), the server returns @405 Method Not
-- Allowed@ with @Allow: POST,DELETE@ on ES 7.17.25, 8.19.16 and 9.4.2.
-- Use the nodes stats API (@GET \/_nodes\/stats\/indices\/search@) for
-- an aggregate count of open search contexts on ES, or migrate to
-- OpenSearch for per-PIT listing.
--
-- For more information see the OpenSearch docs:
-- <https://docs.opensearch.org/latest/search-plugins/point-in-time/#list-all-pit-ids>.
listAllPITs ::
forall m backend.
(MonadBH m) =>
SBackendType backend ->
m [PITSummary]
listAllPITs backend =
case backend of
SElasticSearch7 -> throwEsError $ EsError Nothing "listAllPITs is not supported by any Elasticsearch version (GET /_pit returns 405 on ES 7.17/8.x/9.x; only POST/DELETE are documented)"
SElasticSearch8 -> throwEsError $ EsError Nothing "listAllPITs is not supported by any Elasticsearch version (GET /_pit returns 405 on ES 7.17/8.x/9.x; only POST/DELETE are documented)"
SElasticSearch9 -> throwEsError $ EsError Nothing "listAllPITs is not supported by any Elasticsearch version (GET /_pit returns 405 on ES 7.17/8.x/9.x; only POST/DELETE are documented)"
SOpenSearch1 -> throwEsError $ EsError Nothing "listAllPITs is not supported by OpenSearch1"
SOpenSearch2 -> summarizeOS2 <$> (unsafePerformBH @'OpenSearch2 ClientOS2.listAllPITs)
SOpenSearch3 -> summarizeOS3 <$> (unsafePerformBH @'OpenSearch3 ClientOS3.listAllPITs)
-- | Collapse an OpenSearch 2 'PITInfo' into the backend-agnostic
-- 'PITSummary'. The OS list-all-PITs response surfaces @keep_alive@ (as a
-- millisecond count, parsed into a 'KeepAlive'), so it is 'Just';
-- @creation_time@ is dropped.
summarizeOS2 :: [PITInfo] -> [PITSummary]
summarizeOS2 pits =
[PITSummary (pitInfoId p) (Just (pitInfoKeepAlive p)) | p <- pits]
-- | Collapse an OpenSearch 3 'PITInfo' into the backend-agnostic
-- 'PITSummary'. Same field semantics as 'summarizeOS2'.
summarizeOS3 :: [OS3Types.PITInfo] -> [PITSummary]
summarizeOS3 pits =
[PITSummary (OS3Types.pitInfoId p) (Just (OS3Types.pitInfoKeepAlive p)) | p <- pits]