{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} module Database.V1.Bloodhound.Internal.Aggregation where import Bloodhound.Import import qualified Data.HashMap.Strict as HM import qualified Data.Map.Strict as M import qualified Data.Text as T import Database.V1.Bloodhound.Internal.Client import Database.V1.Bloodhound.Internal.Highlight (HitHighlight) import Database.V1.Bloodhound.Internal.Newtypes import Database.V1.Bloodhound.Internal.Query import Database.V1.Bloodhound.Internal.Sort type Aggregations = M.Map Text Aggregation emptyAggregations :: Aggregations emptyAggregations = M.empty mkAggregations :: Text -> 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 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 (Script 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 ] ] 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) data DateRangeAggRange = DateRangeFrom DateMathExpr | DateRangeTo DateMathExpr | DateRangeFromAndTo DateMathExpr DateMathExpr deriving (Eq, Show) -- | See for more information. data ValueCountAggregation = FieldValueCount FieldName | ScriptValueCount Script deriving (Eq, Show) -- | Single-bucket filter aggregations. See for more information. data FilterAggregation = FilterAggregation { faFilter :: Filter , faAggs :: Maybe Aggregations} 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 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 [termSortField .= termSortOrder] data ExecutionHint = Ordinals | GlobalOrdinals | GlobalOrdinalsHash | GlobalOrdinalsLowCardinality | Map deriving (Eq, Show) instance ToJSON ExecutionHint where toJSON Ordinals = "ordinals" toJSON GlobalOrdinals = "global_ordinals" toJSON GlobalOrdinalsHash = "global_ordinals_hash" toJSON GlobalOrdinalsLowCardinality = "global_ordinals_low_cardinality" toJSON Map = "map" -- | See 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 CollectionMode = BreadthFirst | DepthFirst deriving (Eq, Show) type AggregationResults = M.Map Text Value class BucketAggregation a where key :: a -> BucketValue docCount :: a -> Int aggs :: a -> Maybe AggregationResults data BucketValue = TextValue Text | ScientificValue Scientific | BoolValue Bool deriving (Show) data Bucket a = Bucket { buckets :: [a]} deriving (Show) data TermsResult = TermsResult { termKey :: BucketValue , termsDocCount :: Int , termsAggs :: Maybe AggregationResults } deriving (Show) data DateHistogramResult = DateHistogramResult { dateKey :: Int , dateKeyStr :: Maybe Text , dateDocCount :: Int , dateHistogramAggs :: Maybe AggregationResults } deriving (Show) data DateRangeResult = DateRangeResult { dateRangeKey :: Text , dateRangeFrom :: Maybe UTCTime , dateRangeFromAsString :: Maybe Text , dateRangeTo :: Maybe UTCTime , dateRangeToAsString :: Maybe Text , dateRangeDocCount :: Int , dateRangeAggs :: Maybe AggregationResults } deriving (Show, Eq) toTerms :: Text -> AggregationResults -> Maybe (Bucket TermsResult) toTerms = toAggResult toDateHistogram :: Text -> AggregationResults -> Maybe (Bucket DateHistogramResult) toDateHistogram = toAggResult toMissing :: Text -> AggregationResults -> Maybe MissingResult toMissing = toAggResult toTopHits :: (FromJSON a) => Text -> AggregationResults -> Maybe (TopHitResult a) toTopHits = toAggResult toAggResult :: (FromJSON a) => Text -> AggregationResults -> Maybe a toAggResult t a = M.lookup t a >>= deserialize where deserialize = parseMaybe parseJSON instance BucketAggregation TermsResult where key = termKey docCount = termsDocCount aggs = termsAggs instance BucketAggregation DateHistogramResult where key = TextValue . showText . dateKey docCount = dateDocCount aggs = dateHistogramAggs instance BucketAggregation DateRangeResult where key = TextValue . dateRangeKey docCount = dateRangeDocCount aggs = dateRangeAggs instance (FromJSON a) => FromJSON (Bucket a) where parseJSON (Object v) = Bucket <$> v .: "buckets" parseJSON _ = mempty instance FromJSON BucketValue where parseJSON (String t) = return $ TextValue t parseJSON (Number s) = return $ ScientificValue s parseJSON (Bool b) = return $ BoolValue b parseJSON _ = mempty instance FromJSON MissingResult where parseJSON = withObject "MissingResult" parse where parse v = MissingResult <$> v .: "doc_count" instance FromJSON TermsResult where parseJSON (Object v) = TermsResult <$> v .: "key" <*> v .: "doc_count" <*> (pure $ getNamedSubAgg v ["key", "doc_count"]) parseJSON _ = mempty 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 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 (FromJSON a) => FromJSON (TopHitResult a) where parseJSON (Object v) = TopHitResult <$> v .: "hits" parseJSON _ = fail "Failure in FromJSON (TopHitResult a)" data MissingResult = MissingResult { missingDocCount :: Int } deriving (Show) data TopHitResult a = TopHitResult { tarHits :: (SearchHits a) } deriving Show data SearchHits a = SearchHits { hitsTotal :: Int , maxScore :: Score , hits :: [Hit a] } deriving (Eq, Show) 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 0 Nothing mempty mappend = (<>) data Hit a = Hit { hitIndex :: IndexName , hitType :: MappingName , hitDocId :: DocId , hitScore :: Score , hitSource :: Maybe a , hitHighlight :: Maybe HitHighlight } deriving (Eq, Show) -- 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 -> [Text] -> Maybe AggregationResults getNamedSubAgg o knownKeys = maggRes where unknownKeys = HM.filterWithKey (\k _ -> k `notElem` knownKeys) o maggRes | HM.null unknownKeys = Nothing | otherwise = Just . M.fromList $ HM.toList unknownKeys instance ToJSON CollectionMode where toJSON BreadthFirst = "breadth_first" toJSON DepthFirst = "depth_first" instance ToJSON DateRangeAggregation where toJSON DateRangeAggregation {..} = omitNulls [ "field" .= draField , "format" .= draFormat , "ranges" .= toList draRanges ] instance (FromJSON a) => FromJSON (SearchHits a) where parseJSON (Object v) = SearchHits <$> v .: "total" <*> v .: "max_score" <*> v .: "hits" parseJSON _ = empty 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 ] instance (FromJSON a) => FromJSON (Hit a) where parseJSON (Object v) = Hit <$> v .: "_index" <*> v .: "_type" <*> v .: "_id" <*> v .: "_score" <*> v .:? "_source" <*> v .:? "highlight" parseJSON _ = empty