git.iko.soy/typeable/bloodhound
on c432993 help sign in
992 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 ["properties" .=
185 object ["location" .=
186 object ["type" .= ("geo_point" :: Text)]]]
187:}
189resp <- withBH' $ putMapping testIndex testMapping TweetMapping
191```
193### Delete Mapping
195``` {.haskell}
197resp <- withBH' $ deleteMapping testIndex testMapping
199```
201Document Operations
202-------------------
204### Indexing Documents
206``` {.haskell}
208-- don't forget the imports and derive generic setting for ghci
209-- at the beginning of the examples.
211:{
212data Location = Location { lat :: Double
213 , lon :: Double } deriving (Eq, Generic, Show)
215data Tweet = Tweet { user :: Text
216 , postDate :: UTCTime
217 , message :: Text
218 , age :: Int
219 , location :: Location } deriving (Eq, Generic, Show)
221exampleTweet = Tweet { user = "bitemyapp"
222 , postDate = UTCTime
223 (ModifiedJulianDay 55000)
224 (secondsToDiffTime 10)
225 , message = "Use haskell!"
226 , age = 10000
227 , location = Location 40.12 (-71.34) }
229-- automagic (generic) derivation of instances because we're lazy.
230instance ToJSON Tweet
231instance FromJSON Tweet
232instance ToJSON Location
233instance FromJSON Location
234:}
236-- Should be able to toJSON and encode the data structures like this:
237-- λ> toJSON $ Location 10.0 10.0
238-- Object fromList [("lat",Number 10.0),("lon",Number 10.0)]
239-- λ> encode $ Location 10.0 10.0
240-- "{\"lat\":10,\"lon\":10}"
242resp <- withBH' $ indexDocument testIndex testMapping defaultIndexDocumentSettings exampleTweet (DocId "1")
244```
246#### Example Response
248``` {.haskell}
250Response {responseStatus =
251 Status {statusCode = 200, statusMessage = "OK"}
252 , responseVersion = HTTP/1.1, responseHeaders =
253 [("Content-Type","application/json; charset=UTF-8"),
254 ("Content-Length","75")]
255 , responseBody = "{\"_index\":\"twitter\",\"_type\":\"tweet\",\"_id\":\"1\",\"_version\":2,\"created\":false}"
256 , responseCookieJar = CJ {expose = []}, responseClose' = ResponseClose}
258```
260### Deleting Documents
262``` {.haskell}
264resp <- withBH' $ deleteDocument testIndex testMapping (DocId "1")
266```
268### Getting Documents
270``` {.haskell}
272-- n.b., you'll need the earlier imports. responseBody is from http-conduit
274resp <- withBH' $ getDocument testIndex testMapping (DocId "1")
276-- responseBody :: Response body -> body
277let body = responseBody resp
279-- you have two options, you use decode and just get Maybe (EsResult Tweet)
280-- or you can use eitherDecode and get Either String (EsResult Tweet)
282let maybeResult = decode body :: Maybe (EsResult Tweet)
283-- the explicit typing is so Aeson knows how to parse the JSON.
285-- use either if you want to know why something failed to parse.
286-- (string errors, sadly)
287let eitherResult = eitherDecode body :: Either String (EsResult Tweet)
289-- print eitherResult should look like:
290Right (EsResult {_index = "twitter"
291 , _type = "tweet"
292 , _id = "1"
293 , foundResult = Just (EsResultFound { _version = 2
294 , _source = Tweet {user = "bitemyapp"
295 , postDate = 2009-06-18 00:00:10 UTC
296 , message = "Use haskell!"
297 , age = 10000
298 , location = Location {lat = 40.12, lon = -71.34}}})})
300-- _source in EsResultFound is parametric, we dispatch the type by passing in what we expect (Tweet) as a parameter to EsResult.
302-- use the _source record accessor to get at your document
303fmap (fmap _source . foundResult) eitherResult
304Right (Just (Tweet {user = "bitemyapp"
305 , postDate = 2009-06-18 00:00:10 UTC
306 , message = "Use haskell!"
307 , age = 10000
308 , location = Location {lat = 40.12, lon = -71.34}}))
310```
312Bulk Operations
313---------------
315### Bulk create, index
317``` {.haskell}
319-- don't forget the imports and derive generic setting for ghci
320-- at the beginning of the examples.
322:{
323-- Using the earlier Tweet datatype and exampleTweet data
325-- just changing up the data a bit.
326let bulkTest = exampleTweet { user = "blah" }
327let bulkTestTwo = exampleTweet { message = "woohoo!" }
329-- create only bulk operation
330-- BulkCreate :: IndexName -> MappingName -> DocId -> Value -> BulkOperation
331let firstOp = BulkCreate testIndex
332 testMapping (DocId "3") (toJSON bulkTest)
334-- index operation "create or update"
335let sndOp = BulkIndex testIndex
336 testMapping (DocId "4") (toJSON bulkTestTwo)
338-- Some explanation, the final "Value" type that BulkIndex,
339-- BulkCreate, and BulkUpdate accept is the actual document
340-- data that your operation applies to. BulkDelete doesn't
341-- take a value because it's just deleting whatever DocId
342-- you pass.
344-- list of bulk operations
345let stream = [firstDoc, secondDoc]
347-- Fire off the actual bulk request
348-- bulk :: Vector BulkOperation -> IO Reply
349resp <- withBH' $ bulk stream
350:}
352```
354### Encoding individual bulk API operations
356``` {.haskell}
357-- the following functions are exported in Bloodhound so
358-- you can build up bulk operations yourself
359encodeBulkOperations :: V.Vector BulkOperation -> L.ByteString
360encodeBulkOperation :: BulkOperation -> L.ByteString
362-- How to use the above:
363data BulkTest = BulkTest { name :: Text } deriving (Eq, Generic, Show)
364instance FromJSON BulkTest
365instance ToJSON BulkTest
367_ <- insertData
368let firstTest = BulkTest "blah"
369let secondTest = BulkTest "bloo"
370let firstDoc = BulkIndex testIndex
371 testMapping (DocId "2") (toJSON firstTest)
372let secondDoc = BulkCreate testIndex
373 testMapping (DocId "3") (toJSON secondTest)
374let stream = V.fromList [firstDoc, secondDoc] :: V.Vector BulkOperation
376-- to encode yourself
377let firstDocEncoded = encode firstDoc :: L.ByteString
379-- to encode a vector of bulk operations
380let encodedOperations = encodeBulkOperations stream
382-- to insert into a particular server
383-- bulk :: V.Vector BulkOperation -> IO Reply
384_ <- withBH' $ bulk streamp
386```
388Search
389------
391### Querying
393#### Term Query
395``` {.haskell}
397-- exported by the Client module, just defaults some stuff.
398-- mkSearch :: Maybe Query -> Maybe Filter -> Search
399-- mkSearch query filter = Search query filter Nothing False (From 0) (Size 10) Nothing
401let query = TermQuery (Term "user" "bitemyapp") Nothing
403-- AND'ing identity filter with itself and then tacking it onto a query
404-- search should be a null-operation. I include it for the sake of example.
405-- <||> (or/plus) should make it into a search that returns everything.
407let filter = IdentityFilter <&&> IdentityFilter
409-- constructing the search object the searchByIndex function dispatches on.
410let search = mkSearch (Just query) (Just filter)
412-- you can also searchByType and specify the mapping name.
413reply <- withBH' $ searchByIndex testIndex search
415let result = eitherDecode (responseBody reply) :: Either String (SearchResult Tweet)
417λ> fmap (hits . searchHits) result
418Right [Hit {hitIndex = IndexName "twitter"
419 , hitType = MappingName "tweet"
420 , hitDocId = DocId "1"
421 , hitScore = 0.30685282
422 , hitSource = Tweet {user = "bitemyapp"
423 , postDate = 2009-06-18 00:00:10 UTC
424 , message = "Use haskell!"
425 , age = 10000
426 , location = Location {lat = 40.12, lon = -71.34}}}]
428```
430#### Match Query
432``` {.haskell}
434let query = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
435let search = mkSearch (Just query) Nothing
437```
439#### Multi-Match Query
441``` {.haskell}
443let fields = [FieldName "user", FieldName "message"]
444let query = QueryMultiMatchQuery $ mkMultiMatchQuery fields (QueryString "bitemyapp")
445let search = mkSearch (Just query) Nothing
447```
449#### Bool Query
451``` {.haskell}
453let innerQuery = QueryMatchQuery $
454 mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
455let query = QueryBoolQuery $
456 mkBoolQuery [innerQuery] [] []
457let search = mkSearch (Just query) Nothing
459```
461#### Boosting Query
463``` {.haskell}
465let posQuery = QueryMatchQuery $
466 mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
467let negQuery = QueryMatchQuery $
468 mkMatchQuery (FieldName "user") (QueryString "notmyapp")
469let query = QueryBoostingQuery $
470 BoostingQuery posQuery negQuery (Boost 0.2)
472```
474#### Rest of the query/filter types
476Just follow the pattern you've seen here and check the Hackage API documentation.
478### Sorting
480``` {.haskell}
482let sortSpec = DefaultSortSpec $ mkSort (FieldName "age") Ascending
484-- mkSort is a shortcut function that takes a FieldName and a SortOrder
485-- to generate a vanilla DefaultSort.
486-- checkt the DefaultSort type for the full list of customizable options.
488-- From and size are integers for pagination.
490-- When sorting on a field, scores are not computed. By setting TrackSortScores to true, scores will still be computed and tracked.
492-- type Sort = [SortSpec]
493-- type TrackSortScores = Bool
494-- type From = Int
495-- type Size = Int
497-- Search takes Maybe Query
498-- -> Maybe Filter
499-- -> Maybe Sort
500-- -> TrackSortScores
501-- -> From -> Size
502-- -> Maybe [FieldName]
504-- just add more sortspecs to the list if you want tie-breakers.
505let search = Search Nothing (Just IdentityFilter) (Just [sortSpec]) False (From 0) (Size 10) Nothing
507```
509### Field selection
511If you only want certain fields from the source document returned, you can
512set the "fields" field of the Search record.
514``` {.haskell}
516let search' = mkSearch (Just (MatchAllQuery Nothing)) Nothing
517 search = search' { fields = Just [FieldName "updated"] }
519```
521### Filtering
523#### And, Not, and Or filters
525Filters form a monoid and seminearring.
527``` {.haskell}
529instance Monoid Filter where
530 mempty = IdentityFilter
531 mappend a b = AndFilter [a, b] defaultCache
533instance Seminearring Filter where
534 a <||> b = OrFilter [a, b] defaultCache
536-- AndFilter and OrFilter take [Filter] as an argument.
538-- This will return anything, because IdentityFilter returns everything
539OrFilter [IdentityFilter, someOtherFilter] False
541-- This will return exactly what someOtherFilter returns
542AndFilter [IdentityFilter, someOtherFilter] False
544-- Thanks to the seminearring and monoid, the above can be expressed as:
546-- "and"
547IdentityFilter <&&> someOtherFilter
549-- "or"
550IdentityFilter <||> someOtherFilter
552-- Also there is a NotFilter, it only accepts a single filter, not a list.
554NotFilter someOtherFilter False
556```
558#### Identity Filter
560``` {.haskell}
562-- And'ing two Identity
563let queryFilter = IdentityFilter <&&> IdentityFilter
565let search = mkSearch Nothing (Just queryFilter)
567reply <- withBH' $ searchByType testIndex testMapping search
569```
571#### Boolean Filter
573Similar to boolean queries.
575``` {.haskell}
577-- Will return only items whose "user" field contains the term "bitemyapp"
578let queryFilter = BoolFilter (MustMatch (Term "user" "bitemyapp") False)
580-- Will return only items whose "user" field does not contain the term "bitemyapp"
581let queryFilter = BoolFilter (MustNotMatch (Term "user" "bitemyapp") False)
583-- The clause (query) should appear in the matching document.
584-- In a boolean query with no must clauses, one or more should
585-- clauses must match a document. The minimum number of should
586-- clauses to match can be set using the minimum_should_match parameter.
587let queryFilter = BoolFilter (ShouldMatch [(Term "user" "bitemyapp")] False)
589```
591#### Exists Filter
593``` {.haskell}
595-- Will filter for documents that have the field "user"
596let existsFilter = ExistsFilter (FieldName "user")
598```
600#### Geo BoundingBox Filter
602``` {.haskell}
604-- topLeft and bottomRight
605let box = GeoBoundingBox (LatLon 40.73 (-74.1)) (LatLon 40.10 (-71.12))
607let constraint = GeoBoundingBoxConstraint (FieldName "tweet.location") box False GeoFilterMemory
609```
611#### Geo Distance Filter
613``` {.haskell}
615let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
617-- coefficient and units
618let distance = Distance 10.0 Miles
620-- GeoFilterType or NoOptimizeBbox
621let optimizeBbox = OptimizeGeoFilterType GeoFilterMemory
623-- SloppyArc is the usual/default optimization in Elasticsearch today
624-- but pre-1.0 versions will need to pick Arc or Plane.
626let geoFilter = GeoDistanceFilter geoPoint distance SloppyArc optimizeBbox False
628```
630#### Geo Distance Range Filter
632Think of a donut and you won't be far off.
634``` {.haskell}
636let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
638let distanceRange = DistanceRange (Distance 0.0 Miles) (Distance 10.0 Miles)
640let geoFilter = GeoDistanceRangeFilter geoPoint distanceRange
642```
644#### Geo Polygon Filter
646``` {.haskell}
648-- I think I drew a square here.
649let points = [LatLon 40.0 (-70.00),
650 LatLon 40.0 (-72.00),
651 LatLon 41.0 (-70.00),
652 LatLon 41.0 (-72.00)]
654let geoFilter = GeoPolygonFilter (FieldName "tweet.location") points
656```
658#### Document IDs filter
660``` {.haskell}
662-- takes a mapping name and a list of DocIds
663IdsFilter (MappingName "tweet") [DocId "1"]
665```
667#### Range Filter
669``` {.haskell}
671-- RangeFilter :: FieldName
672-- -> RangeValue
673-- -> RangeExecution
674-- -> Cache -> Filter
676let filter = RangeFilter (FieldName "age")
677 (RangeGtLt (GreaterThan 1000.0) (LessThan 100000.0))
678 RangeExecutionIndex False
680```
682``` {.haskell}
684let filter = RangeFilter (FieldName "age")
685 (RangeLte (LessThanEq 100000.0))
686 RangeExecutionIndex False
688```
690##### Date Ranges
692Date ranges are expressed in UTCTime. Date ranges use the same range bound constructors as numerics, except that they end in "D".
694Note that compatibility with ES is tested only down to seconds.
696``` {.haskell}
698let filter = RangeFilter (FieldName "postDate")
699 (RangeDateGtLte
700 (GreaterThanD (UTCTime
701 (ModifiedJulianDay 55000)
702 (secondsToDiffTime 9)))
703 (LessThanEqD (UTCTime
704 (ModifiedJulianDay 55000)
705 (secondsToDiffTime 11))))
706 RangeExecutionIndex False
707```
709#### Regexp Filter
711``` {.haskell}
713-- RegexpFilter
714-- :: FieldName
715-- -> Regexp
716-- -> RegexpFlags
717-- -> CacheName
718-- -> Cache
719-- -> CacheKey
720-- -> Filter
721let filter = RegexpFilter (FieldName "user") (Regexp "bite.*app")
722 AllRegexpFlags (CacheName "test") False (CacheKey "key")
724-- n.b.
725-- data RegexpFlags = AllRegexpFlags
726-- | NoRegexpFlags
727-- | SomeRegexpFlags (NonEmpty RegexpFlag) deriving (Eq, Show)
729-- data RegexpFlag = AnyString
730-- | Automaton
731-- | Complement
732-- | Empty
733-- | Intersection
734-- | Interval deriving (Eq, Show)
736```
738### Aggregations
740#### Adding aggregations to search
742Aggregations can now be added to search queries, or made on their own.
744``` {.haskell}
745type Aggregations = M.Map Text Aggregation
746data Aggregation
747 = TermsAgg TermsAggregation
748 | DateHistogramAgg DateHistogramAggregation
749```
751For convenience, \`\`\`mkAggregations\`\`\` exists, that will create an \`\`\`Aggregations\`\`\` with the aggregation provided.
753For example:
755``` {.haskell}
756 let a = mkAggregations "users" $ TermsAgg $ mkTermsAggregation "user"
757 let search = mkAggregateSearch Nothing a
758```
760Aggregations can be added to an existing search, using the \`\`\`aggBody\`\`\` field
762``` {.haskell}
763 let search = mkSearch (Just (MatchAllQuery Nothing)) Nothing
764 let search' = search {aggBody = Just a}
765```
767Since the \`\`\`Aggregations\`\`\` structure is just a Map Text Aggregation, M.insert can be used to add additional aggregations.
769``` {.haskell}
770 let a' = M.insert "age" (TermsAgg $ mkTermsAggregation "age") a
771```
773#### Extracting aggregations from results
775Aggregations are part of the reply structure of every search, in the
776form of `Maybe AggregationResults`
778``` {.haskell}
779-- Lift decode and response body to be in the IO monad.
780let decode' = liftM decode
781let responseBody' = liftM responseBody
782let reply = withBH' $ searchByIndex testIndex search
783let response = decode' $ responseBody' reply :: IO (Maybe (SearchResult Tweet))
785-- Now that we have our response, we can extract our terms aggregation result -- which is a list of buckets.
787let terms = do { response' <- response; return $ response' >>= aggregations >>= toTerms "users" }
788terms
789Just (Bucket {buckets = [TermsResult {termKey = "bitemyapp", termsDocCount = 1, termsAggs = Nothing}]})
790```
792Note that bucket aggregation results, such as the TermsResult is a member of the type class `BucketAggregation`:
794``` {.haskell}
795class BucketAggregation a where
796 key :: a -> Text
797 docCount :: a -> Int
798 aggs :: a -> Maybe AggregationResults
799```
801You can use the `aggs` function to get any nested results, if there
802were any. For example, if there were a nested terms aggregation keyed
803to "age" in a TermsResult named `termresult` , you would call `aggs
804termresult >>= toTerms "age"`
806#### Terms Aggregation
808``` {.haskell}
809data TermsAggregation
810 = TermsAggregation {term :: Either Text Text,
811 termInclude :: Maybe TermInclusion,
812 termExclude :: Maybe TermInclusion,
813 termOrder :: Maybe TermOrder,
814 termMinDocCount :: Maybe Int,
815 termSize :: Maybe Int,
816 termShardSize :: Maybe Int,
817 termCollectMode :: Maybe CollectionMode,
818 termExecutionHint :: Maybe ExecutionHint,
819 termAggs :: Maybe Aggregations}
820```
822Term Aggregations have two factory functions, `mkTermsAggregation`, and
823`mkTermsScriptAggregation`, and can be used as follows:
825``` {.haskell}
826let ta = TermsAgg $ mkTermsAggregation "user"
827```
829There are of course other options that can be added to a Terms Aggregation, such as the collection mode:
831``` {.haskell}
832let ta = mkTermsAggregation "user"
833let ta' = ta { termCollectMode = Just BreadthFirst }
834let ta'' = TermsAgg ta'
835```
837For more documentation on how the Terms Aggregation works, see <https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html>
839#### Date Histogram Aggregation
841``` {.haskell}
842data DateHistogramAggregation
843 = DateHistogramAggregation {dateField :: FieldName,
844 dateInterval :: Interval,
845 dateFormat :: Maybe Text,
846 datePreZone :: Maybe Text,
847 datePostZone :: Maybe Text,
848 datePreOffset :: Maybe Text,
849 datePostOffset :: Maybe Text,
850 dateAggs :: Maybe Aggregations}
851```
853The Date Histogram Aggregation works much the same as the Terms Aggregation.
855Relevant functions include `mkDateHistogram`, and `toDateHistogram`
857``` {.haskell}
858let dh = DateHistogramAgg (mkDateHistogram (FieldName "postDate") Minute)
859```
861Date histograms also accept a `FractionalInterval`:
863``` {.haskell}
864FractionalInterval :: Float -> TimeInterval -> Interval
865-- TimeInterval is the following:
866data TimeInterval = Weeks | Days | Hours | Minutes | Seconds
867```
869It can be used as follows:
871``` {.haskell}
872let dh = DateHistogramAgg (mkDateHistogram (FieldName "postDate") (FractionalInterval 1.5 Minutes))
873```
875The `DateHistogramResult` is defined as:
877``` {.haskell}
878data DateHistogramResult
879 = DateHistogramResult {dateKey :: Int,
880 dateKeyStr :: Maybe Text,
881 dateDocCount :: Int,
882 dateHistogramAggs :: Maybe AggregationResults}
883```
885It is an instance of `BucketAggregation`, and can have nested aggregations in each bucket.
887Buckets can be extracted from an `AggregationResult` using
888`toDateHistogram name`
890For more information on the Date Histogram Aggregation, see: <https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-datehistogram-aggregation.html>
893Contributors
894============
896* [Chris Allen](https://github.com/bitemyapp)
897* [Liam Atkinson](https://github.com/latkins)
898* [Christopher Guiney](https://github.com/chrisguiney)
899* [Curtis Carter](https://github.com/ccarter)
900* [Michael Xavier](https://github.com/MichaelXavier)
901* [Bob Long](https://github.com/bobjflong)
902* [Maximilian Tagher](https://github.com/MaxGabriel)
903* [Anna Kopp](https://github.com/annakopp)
904* [Matvey B. Aksenov](https://github.com/supki)
905* [Jan-Philip Loos](https://github.com/MaxDaten)
907Possible future functionality
908=============================
910Span Queries
911------------
913Beginning here: <https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-span-first-query.html>
915Function Score Query
916--------------------
918<https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html>
920Node discovery and failover
921---------------------------
923Might require TCP support.
925Support for TCP access to Elasticsearch
926---------------------------------------
928Pretend to be a transport client?
930Bulk cluster-join merge
931-----------------------
933Might require making a lucene index on disk with the appropriate format.
935GeoShapeQuery
936-------------
938<https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geo-shape-query.html>
940GeoShapeFilter
941--------------
943<https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geo-shape-filter.html>
945Geohash cell filter
946-------------------
948<https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-geohash-cell-filter.html>
950HasChild Filter
951---------------
953<https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-has-child-filter.html>
955HasParent Filter
956----------------
958<https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-has-parent-filter.html>
960Indices Filter
961--------------
963<https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-indices-filter.html>
965Query Filter
966------------
968<https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-filter.html>
970Script based sorting
971--------------------
973<https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html#_script_based_sorting>
975Collapsing redundantly nested and/or structures
976-----------------------------------------------
978The 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.
980Runtime checking for cycles in data structures
981----------------------------------------------
983check for n \> 1 occurrences in DFS:
985<http://hackage.haskell.org/package/stable-maps-0.0.5/docs/System-Mem-StableName-Dynamic.html>
987<http://hackage.haskell.org/package/stable-maps-0.0.5/docs/System-Mem-StableName-Dynamic-Map.html>
989Photo Origin
990============
992Photo from HA! Designs: <https://www.flickr.com/photos/hadesigns/>