bloodhound-0.20.0.0: src/Database/Bloodhound/Internal/Aggregation.hs
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Database.Bloodhound.Internal.Aggregation where
import Bloodhound.Import
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.KeyMap as X
import qualified Data.Map.Strict as M
import qualified Data.Text as T
import Database.Bloodhound.Internal.Client
import Database.Bloodhound.Internal.Highlight (HitHighlight)
import Database.Bloodhound.Internal.Newtypes
import Database.Bloodhound.Internal.Query
import Database.Bloodhound.Internal.Sort
type Aggregations = M.Map Key Aggregation
emptyAggregations :: Aggregations
emptyAggregations = M.empty
mkAggregations :: Key -> Aggregation -> Aggregations
mkAggregations name aggregation = M.insert name aggregation emptyAggregations
data Aggregation
= TermsAgg TermsAggregation
| CardinalityAgg CardinalityAggregation
| DateHistogramAgg DateHistogramAggregation
| ValueCountAgg ValueCountAggregation
| FilterAgg FilterAggregation
| DateRangeAgg DateRangeAggregation
| MissingAgg MissingAggregation
| TopHitsAgg TopHitsAggregation
| StatsAgg StatisticsAggregation
| SumAgg SumAggregation
deriving (Eq, Show)
instance ToJSON Aggregation where
toJSON (TermsAgg (TermsAggregation term include exclude order minDocCount size shardSize collectMode executionHint termAggs)) =
omitNulls
[ "terms"
.= omitNulls
[ toJSON' term,
"include" .= include,
"exclude" .= exclude,
"order" .= order,
"min_doc_count" .= minDocCount,
"size" .= size,
"shard_size" .= shardSize,
"collect_mode" .= collectMode,
"execution_hint" .= executionHint
],
"aggs" .= termAggs
]
where
toJSON' x = case x of Left y -> "field" .= y; Right y -> "script" .= y
toJSON (CardinalityAgg (CardinalityAggregation field precisionThreshold)) =
object
[ "cardinality"
.= omitNulls
[ "field" .= field,
"precisionThreshold" .= precisionThreshold
]
]
toJSON
( DateHistogramAgg
( DateHistogramAggregation
field
interval
format
preZone
postZone
preOffset
postOffset
dateHistoAggs
)
) =
omitNulls
[ "date_histogram"
.= omitNulls
[ "field" .= field,
"interval" .= interval,
"format" .= format,
"pre_zone" .= preZone,
"post_zone" .= postZone,
"pre_offset" .= preOffset,
"post_offset" .= postOffset
],
"aggs" .= dateHistoAggs
]
toJSON (ValueCountAgg a) = object ["value_count" .= v]
where
v = case a of
(FieldValueCount (FieldName n)) ->
object ["field" .= n]
(ScriptValueCount s) ->
object ["script" .= s]
toJSON (FilterAgg (FilterAggregation filt ags)) =
omitNulls
[ "filter" .= filt,
"aggs" .= ags
]
toJSON (DateRangeAgg a) =
object
[ "date_range" .= a
]
toJSON (MissingAgg (MissingAggregation {..})) =
object ["missing" .= object ["field" .= maField]]
toJSON (TopHitsAgg (TopHitsAggregation mfrom msize msort)) =
omitNulls
[ "top_hits"
.= omitNulls
[ "size" .= msize,
"from" .= mfrom,
"sort" .= msort
]
]
toJSON (StatsAgg (StatisticsAggregation typ field)) =
object [stType .= omitNulls ["field" .= field]]
where
stType
| typ == Basic = "stats"
| otherwise = "extended_stats"
toJSON (SumAgg (SumAggregation (FieldName n))) =
omitNulls ["sum" .= omitNulls ["field" .= n]]
data TopHitsAggregation = TopHitsAggregation
{ taFrom :: Maybe From,
taSize :: Maybe Size,
taSort :: Maybe Sort
}
deriving (Eq, Show)
data MissingAggregation = MissingAggregation
{ maField :: Text
}
deriving (Eq, Show)
data TermsAggregation = TermsAggregation
{ term :: Either Text Text,
termInclude :: Maybe TermInclusion,
termExclude :: Maybe TermInclusion,
termOrder :: Maybe TermOrder,
termMinDocCount :: Maybe Int,
termSize :: Maybe Int,
termShardSize :: Maybe Int,
termCollectMode :: Maybe CollectionMode,
termExecutionHint :: Maybe ExecutionHint,
termAggs :: Maybe Aggregations
}
deriving (Eq, Show)
data CardinalityAggregation = CardinalityAggregation
{ cardinalityField :: FieldName,
precisionThreshold :: Maybe Int
}
deriving (Eq, Show)
data DateHistogramAggregation = DateHistogramAggregation
{ dateField :: FieldName,
dateInterval :: Interval,
dateFormat :: Maybe Text,
-- pre and post deprecated in 1.5
datePreZone :: Maybe Text,
datePostZone :: Maybe Text,
datePreOffset :: Maybe Text,
datePostOffset :: Maybe Text,
dateAggs :: Maybe Aggregations
}
deriving (Eq, Show)
data DateRangeAggregation = DateRangeAggregation
{ draField :: FieldName,
draFormat :: Maybe Text,
draRanges :: NonEmpty DateRangeAggRange
}
deriving (Eq, Show)
instance ToJSON DateRangeAggregation where
toJSON DateRangeAggregation {..} =
omitNulls
[ "field" .= draField,
"format" .= draFormat,
"ranges" .= toList draRanges
]
data DateRangeAggRange
= DateRangeFrom DateMathExpr
| DateRangeTo DateMathExpr
| DateRangeFromAndTo DateMathExpr DateMathExpr
deriving (Eq, Show)
instance ToJSON DateRangeAggRange where
toJSON (DateRangeFrom e) = object ["from" .= e]
toJSON (DateRangeTo e) = object ["to" .= e]
toJSON (DateRangeFromAndTo f t) = object ["from" .= f, "to" .= t]
-- | See <https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-valuecount-aggregation.html> for more information.
data ValueCountAggregation
= FieldValueCount FieldName
| ScriptValueCount Script
deriving (Eq, Show)
-- | Single-bucket filter aggregations. See <https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-filter-aggregation.html#search-aggregations-bucket-filter-aggregation> for more information.
data FilterAggregation = FilterAggregation
{ faFilter :: Filter,
faAggs :: Maybe Aggregations
}
deriving (Eq, Show)
data StatisticsAggregation = StatisticsAggregation
{ statsType :: StatsType,
statsField :: FieldName
}
deriving (Eq, Show)
data StatsType
= Basic
| Extended
deriving (Eq, Show)
newtype SumAggregation = SumAggregation {sumAggregationField :: FieldName}
deriving (Eq, Show)
mkTermsAggregation :: Text -> TermsAggregation
mkTermsAggregation t =
TermsAggregation
(Left t)
Nothing
Nothing
Nothing
Nothing
Nothing
Nothing
Nothing
Nothing
Nothing
mkTermsScriptAggregation :: Text -> TermsAggregation
mkTermsScriptAggregation t = TermsAggregation (Right t) Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
mkDateHistogram :: FieldName -> Interval -> DateHistogramAggregation
mkDateHistogram t i = DateHistogramAggregation t i Nothing Nothing Nothing Nothing Nothing Nothing
mkCardinalityAggregation :: FieldName -> CardinalityAggregation
mkCardinalityAggregation t = CardinalityAggregation t Nothing
mkStatsAggregation :: FieldName -> StatisticsAggregation
mkStatsAggregation = StatisticsAggregation Basic
mkExtendedStatsAggregation :: FieldName -> StatisticsAggregation
mkExtendedStatsAggregation = StatisticsAggregation Extended
type AggregationResults = M.Map Key Value
class BucketAggregation a where
key :: a -> BucketValue
docCount :: a -> Int
aggs :: a -> Maybe AggregationResults
data Bucket a = Bucket
{ buckets :: [a]
}
deriving (Read, Show)
instance (FromJSON a) => FromJSON (Bucket a) where
parseJSON (Object v) =
Bucket
<$> v .: "buckets"
parseJSON _ = mempty
data BucketValue
= TextValue Text
| ScientificValue Scientific
| BoolValue Bool
deriving (Read, Show)
instance FromJSON BucketValue where
parseJSON (String t) = return $ TextValue t
parseJSON (Number s) = return $ ScientificValue s
parseJSON (Bool b) = return $ BoolValue b
parseJSON _ = mempty
data TermInclusion
= TermInclusion Text
| TermPattern Text Text
deriving (Eq, Show)
instance ToJSON TermInclusion where
toJSON (TermInclusion x) = toJSON x
toJSON (TermPattern pattern flags) =
omitNulls
[ "pattern" .= pattern,
"flags" .= flags
]
data TermOrder = TermOrder
{ termSortField :: Text,
termSortOrder :: SortOrder
}
deriving (Eq, Show)
instance ToJSON TermOrder where
toJSON (TermOrder termSortField termSortOrder) =
object [fromText termSortField .= termSortOrder]
data CollectionMode
= BreadthFirst
| DepthFirst
deriving (Eq, Show)
instance ToJSON CollectionMode where
toJSON BreadthFirst = "breadth_first"
toJSON DepthFirst = "depth_first"
data ExecutionHint
= GlobalOrdinals
| Map
deriving (Eq, Show)
instance ToJSON ExecutionHint where
toJSON GlobalOrdinals = "global_ordinals"
toJSON Map = "map"
-- | See <https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#date-math> for more information.
data DateMathExpr
= DateMathExpr DateMathAnchor [DateMathModifier]
deriving (Eq, Show)
instance ToJSON DateMathExpr where
toJSON (DateMathExpr a mods) = String (fmtA a <> mconcat (fmtMod <$> mods))
where
fmtA DMNow = "now"
fmtA (DMDate date) = (T.pack $ showGregorian date) <> "||"
fmtMod (AddTime n u) = "+" <> showText n <> fmtU u
fmtMod (SubtractTime n u) = "-" <> showText n <> fmtU u
fmtMod (RoundDownTo u) = "/" <> fmtU u
fmtU DMYear = "y"
fmtU DMMonth = "M"
fmtU DMWeek = "w"
fmtU DMDay = "d"
fmtU DMHour = "h"
fmtU DMMinute = "m"
fmtU DMSecond = "s"
-- | Starting point for a date range. This along with the 'DateMathModifiers' gets you the date ES will start from.
data DateMathAnchor
= DMNow
| DMDate Day
deriving (Eq, Show)
data DateMathModifier
= AddTime Int DateMathUnit
| SubtractTime Int DateMathUnit
| RoundDownTo DateMathUnit
deriving (Eq, Show)
data DateMathUnit
= DMYear
| DMMonth
| DMWeek
| DMDay
| DMHour
| DMMinute
| DMSecond
deriving (Eq, Show)
data TermsResult = TermsResult
{ termKey :: BucketValue,
termsDocCount :: Int,
termsAggs :: Maybe AggregationResults
}
deriving (Read, Show)
instance FromJSON TermsResult where
parseJSON (Object v) =
TermsResult
<$> v .: "key"
<*> v .: "doc_count"
<*> (pure $ getNamedSubAgg v ["key", "doc_count"])
parseJSON _ = mempty
instance BucketAggregation TermsResult where
key = termKey
docCount = termsDocCount
aggs = termsAggs
data DateHistogramResult = DateHistogramResult
{ dateKey :: Int,
dateKeyStr :: Maybe Text,
dateDocCount :: Int,
dateHistogramAggs :: Maybe AggregationResults
}
deriving (Show)
instance FromJSON DateHistogramResult where
parseJSON (Object v) =
DateHistogramResult
<$> v .: "key"
<*> v .:? "key_as_string"
<*> v .: "doc_count"
<*> ( pure $
getNamedSubAgg
v
[ "key",
"doc_count",
"key_as_string"
]
)
parseJSON _ = mempty
instance BucketAggregation DateHistogramResult where
key = TextValue . showText . dateKey
docCount = dateDocCount
aggs = dateHistogramAggs
data DateRangeResult = DateRangeResult
{ dateRangeKey :: Text,
dateRangeFrom :: Maybe UTCTime,
dateRangeFromAsString :: Maybe Text,
dateRangeTo :: Maybe UTCTime,
dateRangeToAsString :: Maybe Text,
dateRangeDocCount :: Int,
dateRangeAggs :: Maybe AggregationResults
}
deriving (Eq, Show)
instance FromJSON DateRangeResult where
parseJSON = withObject "DateRangeResult" parse
where
parse v =
DateRangeResult
<$> v .: "key"
<*> (fmap posixMS <$> v .:? "from")
<*> v .:? "from_as_string"
<*> (fmap posixMS <$> v .:? "to")
<*> v .:? "to_as_string"
<*> v .: "doc_count"
<*> ( pure $
getNamedSubAgg
v
[ "key",
"from",
"from_as_string",
"to",
"to_as_string",
"doc_count"
]
)
instance BucketAggregation DateRangeResult where
key = TextValue . dateRangeKey
docCount = dateRangeDocCount
aggs = dateRangeAggs
toTerms :: Key -> AggregationResults -> Maybe (Bucket TermsResult)
toTerms = toAggResult
toDateHistogram :: Key -> AggregationResults -> Maybe (Bucket DateHistogramResult)
toDateHistogram = toAggResult
toMissing :: Key -> AggregationResults -> Maybe MissingResult
toMissing = toAggResult
toTopHits :: (FromJSON a) => Key -> AggregationResults -> Maybe (TopHitResult a)
toTopHits = toAggResult
toAggResult :: (FromJSON a) => Key -> AggregationResults -> Maybe a
toAggResult t a = M.lookup t a >>= deserialize
where
deserialize = parseMaybe parseJSON
-- Try to get an AggregationResults when we don't know the
-- field name. We filter out the known keys to try to minimize the noise.
getNamedSubAgg :: Object -> [Key] -> Maybe AggregationResults
getNamedSubAgg o knownKeys = maggRes
where
unknownKeys = X.filterWithKey (\k _ -> k `notElem` knownKeys) o
maggRes
| X.null unknownKeys = Nothing
| otherwise = Just . M.fromList $ X.toList unknownKeys
data MissingResult = MissingResult
{ missingDocCount :: Int
}
deriving (Show)
instance FromJSON MissingResult where
parseJSON = withObject "MissingResult" parse
where
parse v = MissingResult <$> v .: "doc_count"
data TopHitResult a = TopHitResult
{ tarHits :: (SearchHits a)
}
deriving (Eq, Show)
instance (FromJSON a) => FromJSON (TopHitResult a) where
parseJSON (Object v) =
TopHitResult
<$> v .: "hits"
parseJSON _ = fail "Failure in FromJSON (TopHitResult a)"
data HitsTotalRelation = HTR_EQ | HTR_GTE deriving (Eq, Show)
instance FromJSON HitsTotalRelation where
parseJSON (String "eq") = pure HTR_EQ
parseJSON (String "gte") = pure HTR_GTE
parseJSON _ = empty
data HitsTotal = HitsTotal
{ value :: Int,
relation :: HitsTotalRelation
}
deriving (Eq, Show)
instance FromJSON HitsTotal where
parseJSON (Object v) =
HitsTotal
<$> v .: "value"
<*> v .: "relation"
parseJSON _ = empty
instance Semigroup HitsTotal where
(HitsTotal ta HTR_EQ) <> (HitsTotal tb HTR_EQ) = HitsTotal (ta + tb) HTR_EQ
(HitsTotal ta HTR_GTE) <> (HitsTotal tb _) = HitsTotal (ta + tb) HTR_GTE
(HitsTotal ta _) <> (HitsTotal tb HTR_GTE) = HitsTotal (ta + tb) HTR_GTE
data SearchHits a = SearchHits
{ hitsTotal :: HitsTotal,
maxScore :: Score,
hits :: [Hit a]
}
deriving (Eq, Show)
instance (FromJSON a) => FromJSON (SearchHits a) where
parseJSON (Object v) =
SearchHits
<$> v .: "total"
<*> v .: "max_score"
<*> v .: "hits"
parseJSON _ = empty
instance Semigroup (SearchHits a) where
(SearchHits ta ma ha) <> (SearchHits tb mb hb) =
SearchHits (ta <> tb) (max ma mb) (ha <> hb)
instance Monoid (SearchHits a) where
mempty = SearchHits (HitsTotal 0 HTR_EQ) Nothing mempty
mappend = (<>)
type SearchAfterKey = [Aeson.Value]
data Hit a = Hit
{ hitIndex :: IndexName,
hitDocId :: DocId,
hitScore :: Score,
hitSource :: Maybe a,
hitSort :: Maybe SearchAfterKey,
hitFields :: Maybe HitFields,
hitHighlight :: Maybe HitHighlight,
hitInnerHits :: Maybe (X.KeyMap (TopHitResult Value))
}
deriving (Eq, Show)
instance (FromJSON a) => FromJSON (Hit a) where
parseJSON (Object v) =
Hit
<$> v .: "_index"
<*> v .: "_id"
<*> v .: "_score"
<*> v .:? "_source"
<*> v .:? "sort"
<*> v .:? "fields"
<*> v .:? "highlight"
<*> v .:? "inner_hits"
parseJSON _ = empty