@@ -29,6 +29,7 @@
2929module Database.Bloodhound.Types
3030 ( defaultCache
3131 , defaultIndexSettings
32+ , defaultIndexDocumentSettings
3233 , mkSort
3334 , showText
3435 , unpackId
@@ -41,6 +42,8 @@ module Database.Bloodhound.Types
4142 , mkTermsAggregation
4243 , mkTermsScriptAggregation
4344 , mkDateHistogram
45+ , mkDocVersion
46+ , docVersionNumber
4447 , toTerms
4548 , toDateHistogram
4649 , omitNulls
@@ -53,9 +56,16 @@ module Database.Bloodhound.Types
5356 , Existence(..)
5457 , NullValue(..)
5558 , IndexSettings(..)
59+ , IndexTemplate(..)
5660 , Server(..)
5761 , Reply
5862 , EsResult(..)
63+ , EsResultFound(..)
64+ , DocVersion
65+ , ExternalDocVersion(..)
66+ , VersionControl(..)
67+ , DocumentParent(..)
68+ , IndexDocumentSettings(..)
5969 , Query(..)
6070 , Search(..)
6171 , SearchType(..)
@@ -95,7 +105,10 @@ module Database.Bloodhound.Types
95105 , RegexpFlags(..)
96106 , RegexpFlag(..)
97107 , FieldName(..)
108+ , Script(..)
98109 , IndexName(..)
110+ , TemplateName(..)
111+ , TemplatePattern(..)
99112 , MappingName(..)
100113 , DocId(..)
101114 , CacheName(..)
@@ -191,6 +204,8 @@ module Database.Bloodhound.Types
191204 , Bucket(..)
192205 , BucketAggregation(..)
193206 , TermsAggregation(..)
207+ , ValueCountAggregation(..)
208+ , FilterAggregation(..)
194209 , DateHistogramAggregation(..)
195210
196211 , Highlights(..)
@@ -217,13 +232,16 @@ import Control.Monad.Writer
217232import Data.Aeson
218233import Data.Aeson.Types (Pair, emptyObject, parseMaybe)
219234import qualified Data.ByteString.Lazy.Char8 as L
220−import Data.List (nub)
235+import Data.List (nub, foldl')
221236import Data.List.NonEmpty (NonEmpty (..), toList)
237+import qualified Data.HashMap.Strict as HM (union)
222238import qualified Data.Map.Strict as M
239+import Data.Maybe
223240import Data.Text (Text)
224241import qualified Data.Text as T
225242import Data.Time.Clock (UTCTime)
226243import qualified Data.Vector as V
244+import GHC.Enum
227245import GHC.Generics (Generic)
228246import Network.HTTP.Client
229247import qualified Network.HTTP.Types.Method as NHTM
@@ -341,6 +359,20 @@ data FieldType = GeoPointType
341359data FieldDefinition =
342360 FieldDefinition { fieldType :: FieldType } deriving (Eq, Show)
343361
362+{-| An 'IndexTemplate' defines a template that will automatically be
363+ applied to new indices created. The templates include both
364+ 'IndexSettings' and mappings, and a simple 'TemplatePattern' that
365+ controls if the template will be applied to the index created.
366+ Specify mappings as follows: @[toJSON TweetMapping, ...]@
367+
368+ https://www.elastic.co/guide/en/elasticsearch/reference/1.7/indices-templates.html
369+-}
370+data IndexTemplate =
371+ IndexTemplate { templatePattern :: TemplatePattern
372+ , templateSettings :: Maybe IndexSettings
373+ , templateMappings :: [Value]
374+ }
375+
344376data MappingField =
345377 MappingField { mappingFieldName :: FieldName
346378 , fieldDefinition :: FieldDefinition } deriving (Eq, Show)
@@ -368,14 +400,95 @@ data BulkOperation =
368400 | BulkUpdate IndexName MappingName DocId Value deriving (Eq, Show)
369401
370402{-| 'EsResult' describes the standard wrapper JSON document that you see in
371− successful Elasticsearch responses.
403+ successful Elasticsearch lookups or lookups that couldn't find the document.
372404-}
373405data EsResult a = EsResult { _index :: Text
374406 , _type :: Text
375407 , _id :: Text
376− , _version :: Int
377− , found :: Maybe Bool
378− , _source :: a } deriving (Eq, Show)
408+ , foundResult :: Maybe (EsResultFound a)} deriving (Eq, Show)
409+
410+
411+{-| 'EsResultFound' contains the document and its metadata inside of an
412+ 'EsResult' when the document was successfully found.
413+-}
414+data EsResultFound a = EsResultFound { _version :: DocVersion
415+ , _source :: a } deriving (Eq, Show)
416+
417+
418+
419+{-| 'DocVersion' is an integer version number for a document between 1
420+and 9.2e+18 used for <<https://www.elastic.co/guide/en/elasticsearch/guide/current/optimistic-concurrency-control.html optimistic concurrency control>>.
421+-}
422+newtype DocVersion = DocVersion {
423+ docVersionNumber :: Int
424+ } deriving (Eq, Show, Ord, ToJSON)
425+
426+-- | Smart constructor for in-range doc version
427+mkDocVersion :: Int -> Maybe DocVersion
428+mkDocVersion i
429+ | i >= (docVersionNumber minBound) && i <= (docVersionNumber maxBound) =
430+ Just $ DocVersion i
431+ | otherwise = Nothing
432+
433+
434+{-| 'ExternalDocVersion' is a convenience wrapper if your code uses its
435+own version numbers instead of ones from ES.
436+-}
437+newtype ExternalDocVersion = ExternalDocVersion DocVersion
438+ deriving (Eq, Show, Ord, Bounded, Enum, ToJSON)
439+
440+{-| 'VersionControl' is specified when indexing documents as a
441+optimistic concurrency control.
442+-}
443+data VersionControl = NoVersionControl
444+ -- ^ Don't send a version. This is a pure overwrite.
445+ | InternalVersion DocVersion
446+ -- ^ Use the default ES versioning scheme. Only
447+ -- index the document if the version is the same
448+ -- as the one specified. Only applicable to
449+ -- updates, as you should be getting Version from
450+ -- a search result.
451+ | ExternalGT ExternalDocVersion
452+ -- ^ Use your own version numbering. Only index
453+ -- the document if the version is strictly higher
454+ -- OR the document doesn't exist. The given
455+ -- version will be used as the new version number
456+ -- for the stored document. N.B. All updates must
457+ -- increment this number, meaning there is some
458+ -- global, external ordering of updates.
459+ | ExternalGTE ExternalDocVersion
460+ -- ^ Use your own version numbering. Only index
461+ -- the document if the version is equal or higher
462+ -- than the stored version. Will succeed if there
463+ -- is no existing document. The given version will
464+ -- be used as the new version number for the
465+ -- stored document. Use with care, as this could
466+ -- result in data loss.
467+ | ForceVersion ExternalDocVersion
468+ -- ^ The document will always be indexed and the
469+ -- given version will be the new version. This is
470+ -- typically used for correcting errors. Use with
471+ -- care, as this could result in data loss.
472+ deriving (Show, Eq, Ord)
473+
474+{-| 'DocumentParent' is used to specify a parent document.
475+-}
476+newtype DocumentParent = DocumentParent DocId
477+ deriving (Eq, Show)
478+
479+{-| 'IndexDocumentSettings' are special settings supplied when indexing
480+a document. For the best backwards compatiblity when new fields are
481+added, you should probably prefer to start with 'defaultIndexDocumentSettings'
482+-}
483+data IndexDocumentSettings =
484+ IndexDocumentSettings { idsVersionControl :: VersionControl
485+ , idsParent :: Maybe DocumentParent
486+ } deriving (Eq, Show)
487+
488+{-| Reasonable default settings. Chooses no version control and no parent.
489+-}
490+defaultIndexDocumentSettings :: IndexDocumentSettings
491+defaultIndexDocumentSettings = IndexDocumentSettings NoVersionControl Nothing
379492
380493{-| 'Sort' is a synonym for a list of 'SortSpec's. Sort behavior is order
381494 dependent with later sorts acting as tie-breakers for earlier sorts.
@@ -471,6 +584,14 @@ newtype Server = Server Text deriving (Eq, Show)
471584-}
472585newtype IndexName = IndexName Text deriving (Eq, Generic, Show)
473586
587+{-| 'TemplateName' is used to describe which template to query/create/delete
588+-}
589+newtype TemplateName = TemplateName Text deriving (Eq, Show, Generic)
590+
591+{-| 'TemplatePattern' represents a pattern which is matched against index names
592+-}
593+newtype TemplatePattern = TemplatePattern Text deriving (Eq, Show, Generic)
594+
474595{-| 'MappingName' is part of mappings which are how ES describes and schematizes
475596 the data in the indices.
476597-}
@@ -491,6 +612,12 @@ newtype QueryString = QueryString Text deriving (Eq, Generic, Show)
491612-}
492613newtype FieldName = FieldName Text deriving (Eq, Show)
493614
615+
616+{-| 'Script' is often used in place of 'FieldName' to specify more
617+complex ways of extracting a value from a document.
618+-}
619+newtype Script = Script { scriptText :: Text } deriving (Eq, Show)
620+
494621{-| 'CacheName' is used in 'RegexpFilter' for describing the
495622 'CacheKey' keyed caching behavior.
496623-}
@@ -607,7 +734,8 @@ data Search = Search { queryBody :: Maybe Query
607734 , trackSortScores :: TrackSortScores
608735 , from :: From
609736 , size :: Size
610− , searchType :: SearchType } deriving (Eq, Show)
737+ , searchType :: SearchType
738+ , fields :: Maybe [FieldName] } deriving (Eq, Show)
611739
612740data SearchType = SearchTypeQueryThenFetch
613741 | SearchTypeDfsQueryThenFetch
@@ -997,6 +1125,7 @@ data Filter = AndFilter [Filter] Cache
9971125 | LimitFilter Int
9981126 | MissingFilter FieldName Existence NullValue
9991127 | PrefixFilter FieldName PrefixValue Cache
1128+ | QueryFilter Query Cache
10001129 | RangeFilter FieldName RangeValue RangeExecution Cache
10011130 | RegexpFilter FieldName Regexp RegexpFlags CacheName Cache CacheKey
10021131 | TermFilter Term Cache
@@ -1204,7 +1333,9 @@ data Interval = Year
12041333 | FractionalInterval Float TimeInterval deriving (Eq, Show)
12051334
12061335data Aggregation = TermsAgg TermsAggregation
1207− | DateHistogramAgg DateHistogramAggregation deriving (Eq, Show)
1336+ | DateHistogramAgg DateHistogramAggregation
1337+ | ValueCountAgg ValueCountAggregation
1338+ | FilterAgg FilterAggregation deriving (Eq, Show)
12081339
12091340
12101341data TermsAggregation = TermsAggregation { term :: Either Text Text
@@ -1230,6 +1361,13 @@ data DateHistogramAggregation = DateHistogramAggregation { dateField :: Fie
12301361 , dateAggs :: Maybe Aggregations
12311362 } deriving (Eq, Show)
12321363
1364+-- | See <https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-valuecount-aggregation.html> for more information.
1365+data ValueCountAggregation = FieldValueCount FieldName
1366+ | ScriptValueCount Script deriving (Eq, Show)
1367+
1368+-- | 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.
1369+data FilterAggregation = FilterAggregation { faFilter :: Filter
1370+ , faAggs :: Maybe Aggregations} deriving (Eq, Show)
12331371
12341372mkTermsAggregation :: Text -> TermsAggregation
12351373mkTermsAggregation t = TermsAggregation (Left t) Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
@@ -1303,6 +1441,13 @@ instance ToJSON Aggregation where
13031441 "post_offset" .= postOffset
13041442 ],
13051443 "aggs" .= dateHistoAggs ]
1444+ toJSON (ValueCountAgg a) = object ["value_count" .= v]
1445+ where v = case a of
1446+ (FieldValueCount (FieldName n)) -> object ["field" .= n]
1447+ (ScriptValueCount (Script s)) -> object ["script" .= s]
1448+ toJSON (FilterAgg (FilterAggregation filt ags)) =
1449+ omitNulls [ "filter" .= filt
1450+ , "aggs" .= ags]
13061451
13071452type AggregationResults = M.Map Text Value
13081453
@@ -1442,6 +1587,13 @@ instance ToJSON Filter where
14421587 object [fieldName .= fieldValue
14431588 , "_cache" .= cache]]
14441589
1590+ toJSON (QueryFilter query False) =
1591+ object ["query" .= toJSON query ]
1592+ toJSON (QueryFilter query True) =
1593+ object ["fquery" .=
1594+ object [ "query" .= toJSON query
1595+ , "_cache" .= True ]]
1596+
14451597 toJSON (RangeFilter (FieldName fieldName) rangeValue rangeExecution cache) =
14461598 object ["range" .=
14471599 object [ fieldName .= object (rangeValueToPair rangeValue)
@@ -1882,6 +2034,8 @@ instance ToJSON IgnoreTermFrequency
18822034instance ToJSON MaxQueryTerms
18832035instance ToJSON TypeName
18842036instance ToJSON IndexName
2037+instance ToJSON TemplateName
2038+instance ToJSON TemplatePattern
18852039instance ToJSON BoostTerms
18862040instance ToJSON MaxWordLength
18872041instance ToJSON MinWordLength
@@ -1918,22 +2072,43 @@ instance FromJSON Status where
19182072
19192073
19202074instance ToJSON IndexSettings where
1921− toJSON (IndexSettings s r) = object ["settings" .= object ["shards" .= s, "replicas" .= r]]
1922−
2075+ toJSON (IndexSettings s r) = object ["settings" .=
2076+ object ["index" .=
2077+ object ["number_of_shards" .= s, "number_of_replicas" .= r]
2078+ ]
2079+ ]
2080+
2081+instance ToJSON IndexTemplate where
2082+ toJSON (IndexTemplate p s m) = merge
2083+ (object [ "template" .= p
2084+ , "mappings" .= foldl' merge (object []) m
2085+ ])
2086+ (toJSON s)
2087+ where
2088+ merge (Object o1) (Object o2) = toJSON $ HM.union o1 o2
2089+ merge o Null = o
2090+ merge _ _ = undefined
19232091
19242092instance (FromJSON a) => FromJSON (EsResult a) where
1925− parseJSON (Object v) = EsResult <$>
1926− v .: "_index" <*>
1927− v .: "_type" <*>
1928− v .: "_id" <*>
1929− v .: "_version" <*>
1930− v .:? "found" <*>
1931− v .: "_source"
2093+ parseJSON jsonVal@(Object v) = do
2094+ found <- v .:? "found" .!= False
2095+ fr <- if found
2096+ then parseJSON jsonVal
2097+ else return Nothing
2098+ EsResult <$> v .: "_index" <*>
2099+ v .: "_type" <*>
2100+ v .: "_id" <*>
2101+ pure fr
19322102 parseJSON _ = empty
19332103
2104+instance (FromJSON a) => FromJSON (EsResultFound a) where
2105+ parseJSON (Object v) = EsResultFound <$>
2106+ v .: "_version" <*>
2107+ v .: "_source"
2108+ parseJSON _ = empty
19342109
19352110instance ToJSON Search where
1936− toJSON (Search query sFilter sort searchAggs highlight sTrackSortScores sFrom sSize _) =
2111+ toJSON (Search query sFilter sort searchAggs highlight sTrackSortScores sFrom sSize _ sFields) =
19372112 omitNulls [ "query" .= query
19382113 , "filter" .= sFilter
19392114 , "sort" .= sort
@@ -1941,7 +2116,8 @@ instance ToJSON Search where
19412116 , "highlight" .= highlight
19422117 , "from" .= sFrom
19432118 , "size" .= sSize
1944− , "track_scores" .= sTrackSortScores]
2119+ , "track_scores" .= sTrackSortScores
2120+ , "fields" .= sFields]
19452121
19462122
19472123instance ToJSON FieldHighlight where
@@ -2188,3 +2364,26 @@ instance FromJSON ShardResult where
21882364 v .: "successful" <*>
21892365 v .: "failed"
21902366 parseJSON _ = empty
2367+
2368+
2369+instance FromJSON DocVersion where
2370+ parseJSON v = do
2371+ i <- parseJSON v
2372+ maybe (fail "DocVersion out of range") return $ mkDocVersion i
2373+
2374+instance Bounded DocVersion where
2375+ minBound = DocVersion 1
2376+ maxBound = DocVersion 9200000000000000000 -- 9.2e+18
2377+
2378+instance Enum DocVersion where
2379+ succ x
2380+ | x /= maxBound = DocVersion (succ $ docVersionNumber x)
2381+ | otherwise = succError "DocVersion"
2382+ pred x
2383+ | x /= minBound = DocVersion (pred $ docVersionNumber x)
2384+ | otherwise = predError "DocVersion"
2385+ toEnum i =
2386+ fromMaybe (error $ show i ++ " out of DocVersion range") $ mkDocVersion i
2387+ fromEnum = docVersionNumber
2388+ enumFrom = boundedEnumFrom
2389+ enumFromThen = boundedEnumFromThen