git.iko.soy/typeable/bloodhound
on 7d718ef help sign in
993 lines 30 kB history raw
1Bloodhound [![TravisCI](https://travis-ci.org/bitemyapp/bloodhound.svg)](https://travis-ci.org/bitemyapp/bloodhound) [![Hackage](https://img.shields.io/hackage/v/bloodhound.svg?style=flat)](https://hackage.haskell.org/package/bloodhound)
2==========
4![Bloodhound (dog)](./bloodhound.jpg)
6Elasticsearch client and query DSL for Haskell
7==============================================
9Why?
10----
12Search doesn't have to be hard. Let the dog do it.
14Endorsements
15------------
17"Bloodhound makes Elasticsearch almost tolerable!" - Almost-gruntled user
19"ES is a nightmare but Bloodhound at least makes it tolerable." - Same user, later opinion.
21Version compatibility
22---------------------
24Elasticsearch \>=1.0 && \<2.0 is recommended. Bloodhound mostly works with 0.9.x, but I don't recommend it if you expect everything to work. As of Bloodhound 0.3 all \>=1.0 && \<2.0 versions of Elasticsearch work. Some (or even most?) features will work with versions \>=2.0, but it is not officially supported yet.
26Current versions we test against are 1.2.4, 1.3.6, 1.4.1, 1.5.2, 1.6.0, and 1.7.2. We also check that GHC 7.6 and 7.8 both build and pass tests. See our [TravisCI](https://travis-ci.org/bitemyapp/bloodhound) to learn more.
28Stability
29---------
31Bloodhound is stable for production use. I will strive to avoid breaking API compatibility from here on forward, but dramatic features like a type-safe, fully integrated mapping API may require breaking things in the future.
33Testing
34---------
36The TravisCI tests are run using [Stack](http://docs.haskellstack.org/en/stable/README.html). You should use Stack instead of `cabal` to build and test Bloodhound to avoid compatibility problems. You will also need to have an ElasticSearch instance running at `localhost:9200` in order to execute some of the tests. See the "Version compatibility" section above for a list of ElasticSearch versions that are officially validated against in TravisCI.
38Steps to run the tests locally:
39 1. Dig through the [past releases] (https://www.elastic.co/downloads/past-releases) section of the ElasticSearch download page and install the desired ElasticSearch versions.
40 2. Install [Stack] (http://docs.haskellstack.org/en/stable/README.html#how-to-install)
41 3. In your local Bloodhound directory, run `stack setup && stack build`
42 4. Start the desired version of ElasticSearch at `localhost:9200`, which should be the default.
43 5. Run `stack test` in your local Bloodhound directory.
44 6. The unit tests will pass if you re-execute `stack test`, but some of the doctests might fail due to existing data in ElasticSearch. If you want to start with a clean slate, stop your ElasticSearch instance, delete the `data/` folder in the ElasticSearch installation, restart ElasticSearch, and re-run `stack test`.
47Hackage page and Haddock documentation
48======================================
50<http://hackage.haskell.org/package/bloodhound>
52Elasticsearch Tutorial
53======================
55It's not using Bloodhound, but if you need an introduction to or overview of Elasticsearch and how to use it, you can use [this screencast](https://vimeo.com/106463167).
57Examples
58========
60Index Operations
61----------------
63### Create Index
65``` {.haskell}
67-- Formatted for use in ghci, so there are "let"s in front of the decls.
69-- if you see :{ and :}, they're so you can copy-paste
70-- the multi-line examples into your ghci REPL.
72:set -XDeriveGeneric
73:set -XOverloadedStrings
74:{
75import Control.Applicative
76import Database.Bloodhound
77import Data.Aeson
78import Data.Either (Either(..))
79import Data.Maybe (fromJust)
80import Data.Time.Calendar (Day(..))
81import Data.Time.Clock (secondsToDiffTime, UTCTime(..))
82import Data.Text (Text)
83import GHC.Generics (Generic)
84import Network.HTTP.Client
85import qualified Network.HTTP.Types.Status as NHTS
87-- no trailing slashes in servers, library handles building the path.
88let testServer = (Server "http://localhost:9200")
89let testIndex = IndexName "twitter"
90let testMapping = MappingName "tweet"
91let withBH' = withBH defaultManagerSettings testServer
93-- defaultIndexSettings is exported by Database.Bloodhound as well
94let defaultIndexSettings = IndexSettings (ShardCount 3) (ReplicaCount 2)
96-- createIndex returns MonadBH m => m Reply. You can use withBH for
97 one-off commands or you can use runBH to group together commands
98 and to pass in your own HTTP manager for pipelining.
100-- response :: Reply, Reply is a synonym for Network.HTTP.Conduit.Response
101response <- withBH' $ createIndex defaultIndexSettings testIndex
102:}
104```
106### Delete Index
108#### Code
110``` {.haskell}
112-- response :: Reply
113response <- withBH' $ deleteIndex testIndex
115```
117#### Example Response
119``` {.haskell}
121-- print response if it was a success
122Response {responseStatus = Status {statusCode = 200, statusMessage = "OK"}
123 , responseVersion = HTTP/1.1
124 , responseHeaders = [("Content-Type", "application/json; charset=UTF-8")
125 , ("Content-Length", "21")]
126 , responseBody = "{\"acknowledged\":true}"
127 , responseCookieJar = CJ {expose = []}
128 , responseClose' = ResponseClose}
130-- if the index to be deleted didn't exist anyway
131Response {responseStatus = Status {statusCode = 404, statusMessage = "Not Found"}
132 , responseVersion = HTTP/1.1
133 , responseHeaders = [("Content-Type", "application/json; charset=UTF-8")
134 , ("Content-Length","65")]
135 , responseBody = "{\"error\":\"IndexMissingException[[twitter] missing]\",\"status\":404}"
136 , responseCookieJar = CJ {expose = []}
137 , responseClose' = ResponseClose}
139```
141### Refresh Index
143#### Note, you **have** to do this if you expect to read what you just wrote
145``` {.haskell}
147resp <- withBH' $ refreshIndex testIndex
149```
151#### Example Response
153``` {.haskell}
155-- print resp on success
156Response {responseStatus = Status {statusCode = 200, statusMessage = "OK"}
157 , responseVersion = HTTP/1.1
158 , responseHeaders = [("Content-Type", "application/json; charset=UTF-8")
159 , ("Content-Length","50")]
160 , responseBody = "{\"_shards\":{\"total\":10,\"successful\":5,\"failed\":0}}"
161 , responseCookieJar = CJ {expose = []}
162 , responseClose' = ResponseClose}
164```
166Mapping Operations
167------------------
169### Create Mapping
171``` {.haskell}
173-- don't forget imports and the like at the top.
175data TweetMapping = TweetMapping deriving (Eq, Show)
177-- I know writing the JSON manually sucks.
178-- I don't have a proper data type for Mappings yet.
179-- Let me know if this is something you need.
181:{
182instance ToJSON TweetMapping where
183 toJSON TweetMapping =
184 object ["tweet" .=
185 object ["properties" .=
186 object ["location" .=
187 object ["type" .= ("geo_point" :: Text)]]]]
188:}
190resp <- withBH' $ putMapping testIndex testMapping TweetMapping
192```
194### Delete Mapping
196``` {.haskell}
198resp <- withBH' $ deleteMapping testIndex testMapping
200```
202Document Operations
203-------------------
205### Indexing Documents
207``` {.haskell}
209-- don't forget the imports and derive generic setting for ghci
210-- at the beginning of the examples.
212:{
213data Location = Location { lat :: Double
214 , lon :: Double } deriving (Eq, Generic, Show)
216data Tweet = Tweet { user :: Text
217 , postDate :: UTCTime
218 , message :: Text
219 , age :: Int
220 , location :: Location } deriving (Eq, Generic, Show)
222exampleTweet = Tweet { user = "bitemyapp"
223 , postDate = UTCTime
224 (ModifiedJulianDay 55000)
225 (secondsToDiffTime 10)
226 , message = "Use haskell!"
227 , age = 10000
228 , location = Location 40.12 (-71.34) }
230-- automagic (generic) derivation of instances because we're lazy.
231instance ToJSON Tweet
232instance FromJSON Tweet
233instance ToJSON Location
234instance FromJSON Location
235:}
237-- Should be able to toJSON and encode the data structures like this:
238-- λ> toJSON $ Location 10.0 10.0
239-- Object fromList [("lat",Number 10.0),("lon",Number 10.0)]
240-- λ> encode $ Location 10.0 10.0
241-- "{\"lat\":10,\"lon\":10}"
243resp <- withBH' $ indexDocument testIndex testMapping defaultIndexDocumentSettings exampleTweet (DocId "1")
245```
247#### Example Response
249``` {.haskell}
251Response {responseStatus =
252 Status {statusCode = 200, statusMessage = "OK"}
253 , responseVersion = HTTP/1.1, responseHeaders =
254 [("Content-Type","application/json; charset=UTF-8"),
255 ("Content-Length","75")]
256 , responseBody = "{\"_index\":\"twitter\",\"_type\":\"tweet\",\"_id\":\"1\",\"_version\":2,\"created\":false}"
257 , responseCookieJar = CJ {expose = []}, responseClose' = ResponseClose}
259```
261### Deleting Documents
263``` {.haskell}
265resp <- withBH' $ deleteDocument testIndex testMapping (DocId "1")
267```
269### Getting Documents
271``` {.haskell}
273-- n.b., you'll need the earlier imports. responseBody is from http-conduit
275resp <- withBH' $ getDocument testIndex testMapping (DocId "1")
277-- responseBody :: Response body -> body
278let body = responseBody resp
280-- you have two options, you use decode and just get Maybe (EsResult Tweet)
281-- or you can use eitherDecode and get Either String (EsResult Tweet)
283let maybeResult = decode body :: Maybe (EsResult Tweet)
284-- the explicit typing is so Aeson knows how to parse the JSON.
286-- use either if you want to know why something failed to parse.
287-- (string errors, sadly)
288let eitherResult = eitherDecode body :: Either String (EsResult Tweet)
290-- print eitherResult should look like:
291Right (EsResult {_index = "twitter"
292 , _type = "tweet"
293 , _id = "1"
294 , foundResult = Just (EsResultFound { _version = 2
295 , _source = Tweet {user = "bitemyapp"
296 , postDate = 2009-06-18 00:00:10 UTC
297 , message = "Use haskell!"
298 , age = 10000
299 , location = Location {lat = 40.12, lon = -71.34}}})})
301-- _source in EsResultFound is parametric, we dispatch the type by passing in what we expect (Tweet) as a parameter to EsResult.
303-- use the _source record accessor to get at your document
304fmap (fmap _source . foundResult) eitherResult
305Right (Just (Tweet {user = "bitemyapp"
306 , postDate = 2009-06-18 00:00:10 UTC
307 , message = "Use haskell!"
308 , age = 10000
309 , location = Location {lat = 40.12, lon = -71.34}}))
311```
313Bulk Operations
314---------------
316### Bulk create, index
318``` {.haskell}
320-- don't forget the imports and derive generic setting for ghci
321-- at the beginning of the examples.
323:{
324-- Using the earlier Tweet datatype and exampleTweet data
326-- just changing up the data a bit.
327let bulkTest = exampleTweet { user = "blah" }
328let bulkTestTwo = exampleTweet { message = "woohoo!" }
330-- create only bulk operation
331-- BulkCreate :: IndexName -> MappingName -> DocId -> Value -> BulkOperation
332let firstOp = BulkCreate testIndex
333 testMapping (DocId "3") (toJSON bulkTest)
335-- index operation "create or update"
336let sndOp = BulkIndex testIndex
337 testMapping (DocId "4") (toJSON bulkTestTwo)
339-- Some explanation, the final "Value" type that BulkIndex,
340-- BulkCreate, and BulkUpdate accept is the actual document
341-- data that your operation applies to. BulkDelete doesn't
342-- take a value because it's just deleting whatever DocId
343-- you pass.
345-- list of bulk operations
346let stream = [firstDoc, secondDoc]
348-- Fire off the actual bulk request
349-- bulk :: Vector BulkOperation -> IO Reply
350resp <- withBH' $ bulk stream
351:}
353```
355### Encoding individual bulk API operations
357``` {.haskell}
358-- the following functions are exported in Bloodhound so
359-- you can build up bulk operations yourself
360encodeBulkOperations :: V.Vector BulkOperation -> L.ByteString
361encodeBulkOperation :: BulkOperation -> L.ByteString
363-- How to use the above:
364data BulkTest = BulkTest { name :: Text } deriving (Eq, Generic, Show)
365instance FromJSON BulkTest
366instance ToJSON BulkTest
368_ <- insertData
369let firstTest = BulkTest "blah"
370let secondTest = BulkTest "bloo"
371let firstDoc = BulkIndex testIndex
372 testMapping (DocId "2") (toJSON firstTest)
373let secondDoc = BulkCreate testIndex
374 testMapping (DocId "3") (toJSON secondTest)
375let stream = V.fromList [firstDoc, secondDoc] :: V.Vector BulkOperation
377-- to encode yourself
378let firstDocEncoded = encode firstDoc :: L.ByteString
380-- to encode a vector of bulk operations
381let encodedOperations = encodeBulkOperations stream
383-- to insert into a particular server
384-- bulk :: V.Vector BulkOperation -> IO Reply
385_ <- withBH' $ bulk streamp
387```
389Search
390------
392### Querying
394#### Term Query
396``` {.haskell}
398-- exported by the Client module, just defaults some stuff.
399-- mkSearch :: Maybe Query -> Maybe Filter -> Search
400-- mkSearch query filter = Search query filter Nothing False (From 0) (Size 10) Nothing
402let query = TermQuery (Term "user" "bitemyapp") Nothing
404-- AND'ing identity filter with itself and then tacking it onto a query
405-- search should be a null-operation. I include it for the sake of example.
406-- <||> (or/plus) should make it into a search that returns everything.
408let filter = IdentityFilter <&&> IdentityFilter
410-- constructing the search object the searchByIndex function dispatches on.
411let search = mkSearch (Just query) (Just filter)
413-- you can also searchByType and specify the mapping name.
414reply <- withBH' $ searchByIndex testIndex search
416let result = eitherDecode (responseBody reply) :: Either String (SearchResult Tweet)
418λ> fmap (hits . searchHits) result
419Right [Hit {hitIndex = IndexName "twitter"
420 , hitType = MappingName "tweet"
421 , hitDocId = DocId "1"
422 , hitScore = 0.30685282
423 , hitSource = Tweet {user = "bitemyapp"
424 , postDate = 2009-06-18 00:00:10 UTC
425 , message = "Use haskell!"
426 , age = 10000
427 , location = Location {lat = 40.12, lon = -71.34}}}]
429```
431#### Match Query
433``` {.haskell}
435let query = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
436let search = mkSearch (Just query) Nothing
438```
440#### Multi-Match Query
442``` {.haskell}
444let fields = [FieldName "user", FieldName "message"]
445let query = QueryMultiMatchQuery $ mkMultiMatchQuery fields (QueryString "bitemyapp")
446let search = mkSearch (Just query) Nothing
448```
450#### Bool Query
452``` {.haskell}
454let innerQuery = QueryMatchQuery $
455 mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
456let query = QueryBoolQuery $
457 mkBoolQuery [innerQuery] [] []
458let search = mkSearch (Just query) Nothing
460```
462#### Boosting Query
464``` {.haskell}
466let posQuery = QueryMatchQuery $
467 mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
468let negQuery = QueryMatchQuery $
469 mkMatchQuery (FieldName "user") (QueryString "notmyapp")
470let query = QueryBoostingQuery $
471 BoostingQuery posQuery negQuery (Boost 0.2)
473```
475#### Rest of the query/filter types
477Just follow the pattern you've seen here and check the Hackage API documentation.
479### Sorting
481``` {.haskell}
483let sortSpec = DefaultSortSpec $ mkSort (FieldName "age") Ascending
485-- mkSort is a shortcut function that takes a FieldName and a SortOrder
486-- to generate a vanilla DefaultSort.
487-- checkt the DefaultSort type for the full list of customizable options.
489-- From and size are integers for pagination.
491-- When sorting on a field, scores are not computed. By setting TrackSortScores to true, scores will still be computed and tracked.
493-- type Sort = [SortSpec]
494-- type TrackSortScores = Bool
495-- type From = Int
496-- type Size = Int
498-- Search takes Maybe Query
499-- -> Maybe Filter
500-- -> Maybe Sort
501-- -> TrackSortScores
502-- -> From -> Size
503-- -> Maybe [FieldName]
505-- just add more sortspecs to the list if you want tie-breakers.
506let search = Search Nothing (Just IdentityFilter) (Just [sortSpec]) False (From 0) (Size 10) Nothing
508```
510### Field selection
512If you only want certain fields from the source document returned, you can
513set the "fields" field of the Search record.
515``` {.haskell}
517let search' = mkSearch (Just (MatchAllQuery Nothing)) Nothing
518 search = search' { fields = Just [FieldName "updated"] }
520```
522### Filtering
524#### And, Not, and Or filters
526Filters form a monoid and seminearring.
528``` {.haskell}
530instance Monoid Filter where
531 mempty = IdentityFilter
532 mappend a b = AndFilter [a, b] defaultCache
534instance Seminearring Filter where
535 a <||> b = OrFilter [a, b] defaultCache
537-- AndFilter and OrFilter take [Filter] as an argument.
539-- This will return anything, because IdentityFilter returns everything
540OrFilter [IdentityFilter, someOtherFilter] False
542-- This will return exactly what someOtherFilter returns
543AndFilter [IdentityFilter, someOtherFilter] False
545-- Thanks to the seminearring and monoid, the above can be expressed as:
547-- "and"
548IdentityFilter <&&> someOtherFilter
550-- "or"
551IdentityFilter <||> someOtherFilter
553-- Also there is a NotFilter, it only accepts a single filter, not a list.
555NotFilter someOtherFilter False
557```
559#### Identity Filter
561``` {.haskell}
563-- And'ing two Identity
564let queryFilter = IdentityFilter <&&> IdentityFilter
566let search = mkSearch Nothing (Just queryFilter)
568reply <- withBH' $ searchByType testIndex testMapping search
570```
572#### Boolean Filter
574Similar to boolean queries.
576``` {.haskell}
578-- Will return only items whose "user" field contains the term "bitemyapp"
579let queryFilter = BoolFilter (MustMatch (Term "user" "bitemyapp") False)
581-- Will return only items whose "user" field does not contain the term "bitemyapp"
582let queryFilter = BoolFilter (MustNotMatch (Term "user" "bitemyapp") False)
584-- The clause (query) should appear in the matching document.
585-- In a boolean query with no must clauses, one or more should
586-- clauses must match a document. The minimum number of should
587-- clauses to match can be set using the minimum_should_match parameter.
588let queryFilter = BoolFilter (ShouldMatch [(Term "user" "bitemyapp")] False)
590```
592#### Exists Filter
594``` {.haskell}
596-- Will filter for documents that have the field "user"
597let existsFilter = ExistsFilter (FieldName "user")
599```
601#### Geo BoundingBox Filter
603``` {.haskell}
605-- topLeft and bottomRight
606let box = GeoBoundingBox (LatLon 40.73 (-74.1)) (LatLon 40.10 (-71.12))
608let constraint = GeoBoundingBoxConstraint (FieldName "tweet.location") box False GeoFilterMemory
610```
612#### Geo Distance Filter
614``` {.haskell}
616let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
618-- coefficient and units
619let distance = Distance 10.0 Miles
621-- GeoFilterType or NoOptimizeBbox
622let optimizeBbox = OptimizeGeoFilterType GeoFilterMemory
624-- SloppyArc is the usual/default optimization in Elasticsearch today
625-- but pre-1.0 versions will need to pick Arc or Plane.
627let geoFilter = GeoDistanceFilter geoPoint distance SloppyArc optimizeBbox False
629```
631#### Geo Distance Range Filter
633Think of a donut and you won't be far off.
635``` {.haskell}
637let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
639let distanceRange = DistanceRange (Distance 0.0 Miles) (Distance 10.0 Miles)
641let geoFilter = GeoDistanceRangeFilter geoPoint distanceRange
643```
645#### Geo Polygon Filter
647``` {.haskell}
649-- I think I drew a square here.
650let points = [LatLon 40.0 (-70.00),
651 LatLon 40.0 (-72.00),
652 LatLon 41.0 (-70.00),
653 LatLon 41.0 (-72.00)]
655let geoFilter = GeoPolygonFilter (FieldName "tweet.location") points
657```
659#### Document IDs filter
661``` {.haskell}
663-- takes a mapping name and a list of DocIds
664IdsFilter (MappingName "tweet") [DocId "1"]
666```
668#### Range Filter
670``` {.haskell}
672-- RangeFilter :: FieldName
673-- -> RangeValue
674-- -> RangeExecution
675-- -> Cache -> Filter
677let filter = RangeFilter (FieldName "age")
678 (RangeGtLt (GreaterThan 1000.0) (LessThan 100000.0))
679 RangeExecutionIndex False
681```
683``` {.haskell}
685let filter = RangeFilter (FieldName "age")
686 (RangeLte (LessThanEq 100000.0))
687 RangeExecutionIndex False
689```
691##### Date Ranges
693Date ranges are expressed in UTCTime. Date ranges use the same range bound constructors as numerics, except that they end in "D".
695Note that compatibility with ES is tested only down to seconds.
697``` {.haskell}
699let filter = RangeFilter (FieldName "postDate")
700 (RangeDateGtLte
701 (GreaterThanD (UTCTime
702 (ModifiedJulianDay 55000)
703 (secondsToDiffTime 9)))
704 (LessThanEqD (UTCTime
705 (ModifiedJulianDay 55000)
706 (secondsToDiffTime 11))))
707 RangeExecutionIndex False
708```
710#### Regexp Filter
712``` {.haskell}
714-- RegexpFilter
715-- :: FieldName
716-- -> Regexp
717-- -> RegexpFlags
718-- -> CacheName
719-- -> Cache
720-- -> CacheKey
721-- -> Filter
722let filter = RegexpFilter (FieldName "user") (Regexp "bite.*app")
723 AllRegexpFlags (CacheName "test") False (CacheKey "key")
725-- n.b.
726-- data RegexpFlags = AllRegexpFlags
727-- | NoRegexpFlags
728-- | SomeRegexpFlags (NonEmpty RegexpFlag) deriving (Eq, Show)
730-- data RegexpFlag = AnyString
731-- | Automaton
732-- | Complement
733-- | Empty
734-- | Intersection
735-- | Interval deriving (Eq, Show)
737```
739### Aggregations
741#### Adding aggregations to search
743Aggregations can now be added to search queries, or made on their own.
745``` {.haskell}
746type Aggregations = M.Map Text Aggregation
747data Aggregation
748 = TermsAgg TermsAggregation
749 | DateHistogramAgg DateHistogramAggregation
750```
752For convenience, \`\`\`mkAggregations\`\`\` exists, that will create an \`\`\`Aggregations\`\`\` with the aggregation provided.
754For example:
756``` {.haskell}
757 let a = mkAggregations "users" $ TermsAgg $ mkTermsAggregation "user"
758 let search = mkAggregateSearch Nothing a
759```
761Aggregations can be added to an existing search, using the \`\`\`aggBody\`\`\` field
763``` {.haskell}
764 let search = mkSearch (Just (MatchAllQuery Nothing)) Nothing
765 let search' = search {aggBody = Just a}
766```
768Since the \`\`\`Aggregations\`\`\` structure is just a Map Text Aggregation, M.insert can be used to add additional aggregations.
770``` {.haskell}
771 let a' = M.insert "age" (TermsAgg $ mkTermsAggregation "age") a
772```
774#### Extracting aggregations from results
776Aggregations are part of the reply structure of every search, in the
777form of `Maybe AggregationResults`
779``` {.haskell}
780-- Lift decode and response body to be in the IO monad.
781let decode' = liftM decode
782let responseBody' = liftM responseBody
783let reply = withBH' $ searchByIndex testIndex search
784let response = decode' $ responseBody' reply :: IO (Maybe (SearchResult Tweet))
786-- Now that we have our response, we can extract our terms aggregation result -- which is a list of buckets.
788let terms = do { response' <- response; return $ response' >>= aggregations >>= toTerms "users" }
789terms
790Just (Bucket {buckets = [TermsResult {termKey = "bitemyapp", termsDocCount = 1, termsAggs = Nothing}]})
791```
793Note that bucket aggregation results, such as the TermsResult is a member of the type class `BucketAggregation`:
795``` {.haskell}
796class BucketAggregation a where
797 key :: a -> Text
798 docCount :: a -> Int
799 aggs :: a -> Maybe AggregationResults
800```
802You can use the `aggs` function to get any nested results, if there
803were any. For example, if there were a nested terms aggregation keyed
804to "age" in a TermsResult named `termresult` , you would call `aggs
805termresult >>= toTerms "age"`
807#### Terms Aggregation
809``` {.haskell}
810data TermsAggregation
811 = TermsAggregation {term :: Either Text Text,
812 termInclude :: Maybe TermInclusion,
813 termExclude :: Maybe TermInclusion,
814 termOrder :: Maybe TermOrder,
815 termMinDocCount :: Maybe Int,
816 termSize :: Maybe Int,
817 termShardSize :: Maybe Int,
818 termCollectMode :: Maybe CollectionMode,
819 termExecutionHint :: Maybe ExecutionHint,
820 termAggs :: Maybe Aggregations}
821```
823Term Aggregations have two factory functions, `mkTermsAggregation`, and
824`mkTermsScriptAggregation`, and can be used as follows:
826``` {.haskell}
827let ta = TermsAgg $ mkTermsAggregation "user"
828```
830There are of course other options that can be added to a Terms Aggregation, such as the collection mode:
832``` {.haskell}
833let ta = mkTermsAggregation "user"
834let ta' = ta { termCollectMode = Just BreadthFirst }
835let ta'' = TermsAgg ta'
836```
838For more documentation on how the Terms Aggregation works, see <https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html>
840#### Date Histogram Aggregation
842``` {.haskell}
843data DateHistogramAggregation
844 = DateHistogramAggregation {dateField :: FieldName,
845 dateInterval :: Interval,
846 dateFormat :: Maybe Text,
847 datePreZone :: Maybe Text,
848 datePostZone :: Maybe Text,
849 datePreOffset :: Maybe Text,
850 datePostOffset :: Maybe Text,
851 dateAggs :: Maybe Aggregations}
852```
854The Date Histogram Aggregation works much the same as the Terms Aggregation.
856Relevant functions include `mkDateHistogram`, and `toDateHistogram`
858``` {.haskell}
859let dh = DateHistogramAgg (mkDateHistogram (FieldName "postDate") Minute)
860```
862Date histograms also accept a `FractionalInterval`:
864``` {.haskell}
865FractionalInterval :: Float -> TimeInterval -> Interval
866-- TimeInterval is the following:
867data TimeInterval = Weeks | Days | Hours | Minutes | Seconds
868```
870It can be used as follows:
872``` {.haskell}
873let dh = DateHistogramAgg (mkDateHistogram (FieldName "postDate") (FractionalInterval 1.5 Minutes))
874```
876The `DateHistogramResult` is defined as:
878``` {.haskell}
879data DateHistogramResult
880 = DateHistogramResult {dateKey :: Int,
881 dateKeyStr :: Maybe Text,
882 dateDocCount :: Int,
883 dateHistogramAggs :: Maybe AggregationResults}
884```
886It is an instance of `BucketAggregation`, and can have nested aggregations in each bucket.
888Buckets can be extracted from an `AggregationResult` using
889`toDateHistogram name`
891For more information on the Date Histogram Aggregation, see: <https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-datehistogram-aggregation.html>
894Contributors
895============
897* [Chris Allen](https://github.com/bitemyapp)
898* [Liam Atkinson](https://github.com/latkins)
899* [Christopher Guiney](https://github.com/chrisguiney)
900* [Curtis Carter](https://github.com/ccarter)
901* [Michael Xavier](https://github.com/MichaelXavier)
902* [Bob Long](https://github.com/bobjflong)
903* [Maximilian Tagher](https://github.com/MaxGabriel)
904* [Anna Kopp](https://github.com/annakopp)
905* [Matvey B. Aksenov](https://github.com/supki)
906* [Jan-Philip Loos](https://github.com/MaxDaten)
908Possible future functionality
909=============================
911Span Queries
912------------
914Beginning here: <https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-span-first-query.html>
916Function Score Query
917--------------------
919<https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html>
921Node discovery and failover
922---------------------------
924Might require TCP support.
926Support for TCP access to Elasticsearch
927---------------------------------------
929Pretend to be a transport client?
931Bulk cluster-join merge
932-----------------------
934Might require making a lucene index on disk with the appropriate format.
936GeoShapeQuery
937-------------
939<https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geo-shape-query.html>
941GeoShapeFilter
942--------------
944<https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geo-shape-filter.html>
946Geohash cell filter
947-------------------
949<https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geohash-cell-filter.html>
951HasChild Filter
952---------------
954<https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-has-child-filter.html>
956HasParent Filter
957----------------
959<https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-has-parent-filter.html>
961Indices Filter
962--------------
964<https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-indices-filter.html>
966Query Filter
967------------
969<https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-filter.html>
971Script based sorting
972--------------------
974<https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html#_script_based_sorting>
976Collapsing redundantly nested and/or structures
977-----------------------------------------------
979The Seminearring instance, if deeply nested can possibly produce nested structure that is redundant. Depending on how this affects ES performance, reducing this structure might be valuable.
981Runtime checking for cycles in data structures
982----------------------------------------------
984check for n \> 1 occurrences in DFS:
986<http://hackage.haskell.org/package/stable-maps-0.0.5/docs/System-Mem-StableName-Dynamic.html>
988<http://hackage.haskell.org/package/stable-maps-0.0.5/docs/System-Mem-StableName-Dynamic-Map.html>
990Photo Origin
991============
993Photo from HA! Designs: <https://www.flickr.com/photos/hadesigns/>