git.iko.soy/typeable/bloodhound
on master help sign in
Merge pull request #174 from bitemyapp/drop-doctests

Drop doctests

commit17c826612179e32dafc6d6ee47a407f057b3f35f authorChris Allen <cma@bitemyapp.com> dateTue 14 Feb 2017, 12:16 (10 years ago) parent819ee1457549f2
15 files changed+206−1047
.gitignore+1 .travis.yml−1 README.md+14−834 bloodhound.cabal−20 examples/LICENSE+12 examples/README.md+3 examples/Setup.hs+2 examples/Tweet.hs+125 examples/package.yaml+38 src/Database/V1/Bloodhound/Client.hs−180 stack-7.10.yaml+4−2 stack-7.8.yaml+3−1 stack.yaml+4−1 tests/V1/doctests.hs−4 tests/V5/doctests.hs−4
.gitignore+1
@@ -16,3 +16,4 @@ bloodhound.iml
1616.hg/00changelog.i
1717.hg
1818.hgignore
19+examples/bloodhound-examples.cabal
.travis.yml−1
@@ -36,7 +36,6 @@ script:
3636 - stack setup
3737 - stack update
3838 - stack build -j1
39− - stack test bloodhound:doctests --flag bloodhound:$ESFLAG
4039 - stack test bloodhound:tests --test-arguments="--qc-max-success 500" --flag bloodhound:$ESFLAG
4140
4241cache:
README.md+14−834
@@ -21,9 +21,19 @@ Endorsements
2121Version compatibility
2222---------------------
2323
24−Elasticsearch \>=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.
24+As of version 0.13.0.0, Bloodhound has 2 separate module trees for
25+ElasticSearch versions 1 and 5. Import the module that is appropriate
26+for your use case. If you would like to add support for another major
27+version, open a ticket expressing your intend and follow the pattern
28+used for other versions. We weighed the idea of sharing code between
29+versions but it just got too messy, especially considering the
30+instability of the ElasticSearch API. We switched to a model which
31+would allow the persons responsible for a particular protocol version
32+to maintain that version while avoiding conflict with other versions.
33+
34+See our [TravisCI](https://travis-ci.org/bitemyapp/bloodhound) for a
35+listing of ElasticSearch version we test against.
2536
26−Current 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.
2737
2838Stability
2939---------
@@ -41,7 +51,7 @@ Steps to run the tests locally:
4151 3. In your local Bloodhound directory, run `stack setup && stack build`
4252 4. Start the desired version of ElasticSearch at `localhost:9200`, which should be the default.
4353 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`.
54+ 6. The unit tests will pass if you re-execute `stack test`. 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`.
4555
4656
4757Hackage page and Haddock documentation
@@ -57,837 +67,7 @@ It's not using Bloodhound, but if you need an introduction to or overview of Ela
5767Examples
5868========
5969
60−Index Operations
61−----------------
62−
63−### Create Index
64−
65−``` {.haskell}
66−
67−-- Formatted for use in ghci, so there are "let"s in front of the decls.
68−
69−-- if you see :{ and :}, they're so you can copy-paste
70−-- the multi-line examples into your ghci REPL.
71−
72−:set -XDeriveGeneric
73−:set -XOverloadedStrings
74−:{
75−import Control.Applicative
76−import Database.Bloodhound
77−import Data.Aeson
78−import Data.Either (Either(..))
79−import Data.Maybe (fromJust)
80−import Data.Time.Calendar (Day(..))
81−import Data.Time.Clock (secondsToDiffTime, UTCTime(..))
82−import Data.Text (Text)
83−import GHC.Generics (Generic)
84−import Network.HTTP.Client
85−import qualified Network.HTTP.Types.Status as NHTS
86−
87−-- no trailing slashes in servers, library handles building the path.
88−let testServer = (Server "http://localhost:9200")
89−let testIndex = IndexName "twitter"
90−let testMapping = MappingName "tweet"
91−let withBH' = withBH defaultManagerSettings testServer
92−
93−-- defaultIndexSettings is exported by Database.Bloodhound as well
94−let defaultIndexSettings = IndexSettings (ShardCount 3) (ReplicaCount 2)
95−
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.
99−
100−-- response :: Reply, Reply is a synonym for Network.HTTP.Conduit.Response
101−response <- withBH' $ createIndex defaultIndexSettings testIndex
102−:}
103−
104−```
105−
106−### Delete Index
107−
108−#### Code
109−
110−``` {.haskell}
111−
112−-- response :: Reply
113−response <- withBH' $ deleteIndex testIndex
114−
115−```
116−
117−#### Example Response
118−
119−``` {.haskell}
120−
121−-- print response if it was a success
122−Response {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}
129−
130−-- if the index to be deleted didn't exist anyway
131−Response {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}
138−
139−```
140−
141−### Refresh Index
142−
143−#### Note, you **have** to do this if you expect to read what you just wrote
144−
145−``` {.haskell}
146−
147−resp <- withBH' $ refreshIndex testIndex
148−
149−```
150−
151−#### Example Response
152−
153−``` {.haskell}
154−
155−-- print resp on success
156−Response {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}
163−
164−```
165−
166−Mapping Operations
167−------------------
168−
169−### Create Mapping
170−
171−``` {.haskell}
172−
173−-- don't forget imports and the like at the top.
174−
175−data TweetMapping = TweetMapping deriving (Eq, Show)
176−
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.
180−
181−:{
182−instance ToJSON TweetMapping where
183− toJSON TweetMapping =
184− object ["properties" .=
185− object ["location" .=
186− object ["type" .= ("geo_point" :: Text)]]]
187−:}
188−
189−resp <- withBH' $ putMapping testIndex testMapping TweetMapping
190−
191−```
192−
193−### Delete Mapping
194−
195−``` {.haskell}
196−
197−resp <- withBH' $ deleteMapping testIndex testMapping
198−
199−```
200−
201−Document Operations
202−-------------------
203−
204−### Indexing Documents
205−
206−``` {.haskell}
207−
208−-- don't forget the imports and derive generic setting for ghci
209−-- at the beginning of the examples.
210−
211−:{
212−data Location = Location { lat :: Double
213− , lon :: Double } deriving (Eq, Generic, Show)
214−
215−data Tweet = Tweet { user :: Text
216− , postDate :: UTCTime
217− , message :: Text
218− , age :: Int
219− , location :: Location } deriving (Eq, Generic, Show)
220−
221−exampleTweet = 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) }
228−
229−-- automagic (generic) derivation of instances because we're lazy.
230−instance ToJSON Tweet
231−instance FromJSON Tweet
232−instance ToJSON Location
233−instance FromJSON Location
234−:}
235−
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}"
241−
242−resp <- withBH' $ indexDocument testIndex testMapping defaultIndexDocumentSettings exampleTweet (DocId "1")
243−
244−```
245−
246−#### Example Response
247−
248−``` {.haskell}
249−
250−Response {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}
257−
258−```
259−
260−### Deleting Documents
261−
262−``` {.haskell}
263−
264−resp <- withBH' $ deleteDocument testIndex testMapping (DocId "1")
265−
266−```
267−
268−### Getting Documents
269−
270−``` {.haskell}
271−
272−-- n.b., you'll need the earlier imports. responseBody is from http-conduit
273−
274−resp <- withBH' $ getDocument testIndex testMapping (DocId "1")
275−
276−-- responseBody :: Response body -> body
277−let body = responseBody resp
278−
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)
281−
282−let maybeResult = decode body :: Maybe (EsResult Tweet)
283−-- the explicit typing is so Aeson knows how to parse the JSON.
284−
285−-- use either if you want to know why something failed to parse.
286−-- (string errors, sadly)
287−let eitherResult = eitherDecode body :: Either String (EsResult Tweet)
288−
289−-- print eitherResult should look like:
290−Right (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}}})})
299−
300−-- _source in EsResultFound is parametric, we dispatch the type by passing in what we expect (Tweet) as a parameter to EsResult.
301−
302−-- use the _source record accessor to get at your document
303−fmap (fmap _source . foundResult) eitherResult
304−Right (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}}))
309−
310−```
311−
312−Bulk Operations
313−---------------
314−
315−### Bulk create, index
316−
317−``` {.haskell}
318−
319−-- don't forget the imports and derive generic setting for ghci
320−-- at the beginning of the examples.
321−
322−:{
323−-- Using the earlier Tweet datatype and exampleTweet data
324−
325−-- just changing up the data a bit.
326−let bulkTest = exampleTweet { user = "blah" }
327−let bulkTestTwo = exampleTweet { message = "woohoo!" }
328−
329−-- create only bulk operation
330−-- BulkCreate :: IndexName -> MappingName -> DocId -> Value -> BulkOperation
331−let firstOp = BulkCreate testIndex
332− testMapping (DocId "3") (toJSON bulkTest)
333−
334−-- index operation "create or update"
335−let sndOp = BulkIndex testIndex
336− testMapping (DocId "4") (toJSON bulkTestTwo)
337−
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.
343−
344−-- list of bulk operations
345−let stream = [firstDoc, secondDoc]
346−
347−-- Fire off the actual bulk request
348−-- bulk :: Vector BulkOperation -> IO Reply
349−resp <- withBH' $ bulk stream
350−:}
351−
352−```
353−
354−### Encoding individual bulk API operations
355−
356−``` {.haskell}
357−-- the following functions are exported in Bloodhound so
358−-- you can build up bulk operations yourself
359−encodeBulkOperations :: V.Vector BulkOperation -> L.ByteString
360−encodeBulkOperation :: BulkOperation -> L.ByteString
361−
362−-- How to use the above:
363−data BulkTest = BulkTest { name :: Text } deriving (Eq, Generic, Show)
364−instance FromJSON BulkTest
365−instance ToJSON BulkTest
366−
367−_ <- insertData
368−let firstTest = BulkTest "blah"
369−let secondTest = BulkTest "bloo"
370−let firstDoc = BulkIndex testIndex
371− testMapping (DocId "2") (toJSON firstTest)
372−let secondDoc = BulkCreate testIndex
373− testMapping (DocId "3") (toJSON secondTest)
374−let stream = V.fromList [firstDoc, secondDoc] :: V.Vector BulkOperation
375−
376−-- to encode yourself
377−let firstDocEncoded = encode firstDoc :: L.ByteString
378−
379−-- to encode a vector of bulk operations
380−let encodedOperations = encodeBulkOperations stream
381−
382−-- to insert into a particular server
383−-- bulk :: V.Vector BulkOperation -> IO Reply
384−_ <- withBH' $ bulk streamp
385−
386−```
387−
388−Search
389−------
390−
391−### Querying
392−
393−#### Term Query
394−
395−``` {.haskell}
396−
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
400−
401−let query = TermQuery (Term "user" "bitemyapp") Nothing
402−
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.
406−
407−let filter = IdentityFilter <&&> IdentityFilter
408−
409−-- constructing the search object the searchByIndex function dispatches on.
410−let search = mkSearch (Just query) (Just filter)
411−
412−-- you can also searchByType and specify the mapping name.
413−reply <- withBH' $ searchByIndex testIndex search
414−
415−let result = eitherDecode (responseBody reply) :: Either String (SearchResult Tweet)
416−
417−λ> fmap (hits . searchHits) result
418−Right [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}}}]
427−
428−```
429−
430−#### Match Query
431−
432−``` {.haskell}
433−
434−let query = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
435−let search = mkSearch (Just query) Nothing
436−
437−```
438−
439−#### Multi-Match Query
440−
441−``` {.haskell}
442−
443−let fields = [FieldName "user", FieldName "message"]
444−let query = QueryMultiMatchQuery $ mkMultiMatchQuery fields (QueryString "bitemyapp")
445−let search = mkSearch (Just query) Nothing
446−
447−```
448−
449−#### Bool Query
450−
451−``` {.haskell}
452−
453−let innerQuery = QueryMatchQuery $
454− mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
455−let query = QueryBoolQuery $
456− mkBoolQuery [innerQuery] [] []
457−let search = mkSearch (Just query) Nothing
458−
459−```
460−
461−#### Boosting Query
462−
463−``` {.haskell}
464−
465−let posQuery = QueryMatchQuery $
466− mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
467−let negQuery = QueryMatchQuery $
468− mkMatchQuery (FieldName "user") (QueryString "notmyapp")
469−let query = QueryBoostingQuery $
470− BoostingQuery posQuery negQuery (Boost 0.2)
471−
472−```
473−
474−#### Rest of the query/filter types
475−
476−Just follow the pattern you've seen here and check the Hackage API documentation.
477−
478−### Sorting
479−
480−``` {.haskell}
481−
482−let sortSpec = DefaultSortSpec $ mkSort (FieldName "age") Ascending
483−
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.
487−
488−-- From and size are integers for pagination.
489−
490−-- When sorting on a field, scores are not computed. By setting TrackSortScores to true, scores will still be computed and tracked.
491−
492−-- type Sort = [SortSpec]
493−-- type TrackSortScores = Bool
494−-- type From = Int
495−-- type Size = Int
496−
497−-- Search takes Maybe Query
498−-- -> Maybe Filter
499−-- -> Maybe Sort
500−-- -> TrackSortScores
501−-- -> From -> Size
502−-- -> Maybe [FieldName]
503−
504−-- just add more sortspecs to the list if you want tie-breakers.
505−let search = Search Nothing (Just IdentityFilter) (Just [sortSpec]) False (From 0) (Size 10) Nothing
506−
507−```
508−
509−### Field selection
510−
511−If you only want certain fields from the source document returned, you can
512−set the "fields" field of the Search record.
513−
514−``` {.haskell}
515−
516−let search' = mkSearch (Just (MatchAllQuery Nothing)) Nothing
517− search = search' { fields = Just [FieldName "updated"] }
518−
519−```
520−
521−### Filtering
522−
523−#### And, Not, and Or filters
524−
525−Filters form a monoid and seminearring.
526−
527−``` {.haskell}
528−
529−instance Monoid Filter where
530− mempty = IdentityFilter
531− mappend a b = AndFilter [a, b] defaultCache
532−
533−instance Seminearring Filter where
534− a <||> b = OrFilter [a, b] defaultCache
535−
536−-- AndFilter and OrFilter take [Filter] as an argument.
537−
538−-- This will return anything, because IdentityFilter returns everything
539−OrFilter [IdentityFilter, someOtherFilter] False
540−
541−-- This will return exactly what someOtherFilter returns
542−AndFilter [IdentityFilter, someOtherFilter] False
543−
544−-- Thanks to the seminearring and monoid, the above can be expressed as:
545−
546−-- "and"
547−IdentityFilter <&&> someOtherFilter
548−
549−-- "or"
550−IdentityFilter <||> someOtherFilter
551−
552−-- Also there is a NotFilter, it only accepts a single filter, not a list.
553−
554−NotFilter someOtherFilter False
555−
556−```
557−
558−#### Identity Filter
559−
560−``` {.haskell}
561−
562−-- And'ing two Identity
563−let queryFilter = IdentityFilter <&&> IdentityFilter
564−
565−let search = mkSearch Nothing (Just queryFilter)
566−
567−reply <- withBH' $ searchByType testIndex testMapping search
568−
569−```
570−
571−#### Boolean Filter
572−
573−Similar to boolean queries.
574−
575−``` {.haskell}
576−
577−-- Will return only items whose "user" field contains the term "bitemyapp"
578−let queryFilter = BoolFilter (MustMatch (Term "user" "bitemyapp") False)
579−
580−-- Will return only items whose "user" field does not contain the term "bitemyapp"
581−let queryFilter = BoolFilter (MustNotMatch (Term "user" "bitemyapp") False)
582−
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.
587−let queryFilter = BoolFilter (ShouldMatch [(Term "user" "bitemyapp")] False)
588−
589−```
590−
591−#### Exists Filter
592−
593−``` {.haskell}
594−
595−-- Will filter for documents that have the field "user"
596−let existsFilter = ExistsFilter (FieldName "user")
597−
598−```
599−
600−#### Geo BoundingBox Filter
601−
602−``` {.haskell}
603−
604−-- topLeft and bottomRight
605−let box = GeoBoundingBox (LatLon 40.73 (-74.1)) (LatLon 40.10 (-71.12))
606−
607−let constraint = GeoBoundingBoxConstraint (FieldName "tweet.location") box False GeoFilterMemory
608−
609−```
610−
611−#### Geo Distance Filter
612−
613−``` {.haskell}
614−
615−let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
616−
617−-- coefficient and units
618−let distance = Distance 10.0 Miles
619−
620−-- GeoFilterType or NoOptimizeBbox
621−let optimizeBbox = OptimizeGeoFilterType GeoFilterMemory
622−
623−-- SloppyArc is the usual/default optimization in Elasticsearch today
624−-- but pre-1.0 versions will need to pick Arc or Plane.
625−
626−let geoFilter = GeoDistanceFilter geoPoint distance SloppyArc optimizeBbox False
627−
628−```
629−
630−#### Geo Distance Range Filter
631−
632−Think of a donut and you won't be far off.
633−
634−``` {.haskell}
635−
636−let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
637−
638−let distanceRange = DistanceRange (Distance 0.0 Miles) (Distance 10.0 Miles)
639−
640−let geoFilter = GeoDistanceRangeFilter geoPoint distanceRange
641−
642−```
643−
644−#### Geo Polygon Filter
645−
646−``` {.haskell}
647−
648−-- I think I drew a square here.
649−let points = [LatLon 40.0 (-70.00),
650− LatLon 40.0 (-72.00),
651− LatLon 41.0 (-70.00),
652− LatLon 41.0 (-72.00)]
653−
654−let geoFilter = GeoPolygonFilter (FieldName "tweet.location") points
655−
656−```
657−
658−#### Document IDs filter
659−
660−``` {.haskell}
661−
662−-- takes a mapping name and a list of DocIds
663−IdsFilter (MappingName "tweet") [DocId "1"]
664−
665−```
666−
667−#### Range Filter
668−
669−``` {.haskell}
670−
671−-- RangeFilter :: FieldName
672−-- -> RangeValue
673−-- -> RangeExecution
674−-- -> Cache -> Filter
675−
676−let filter = RangeFilter (FieldName "age")
677− (RangeGtLt (GreaterThan 1000.0) (LessThan 100000.0))
678− RangeExecutionIndex False
679−
680−```
681−
682−``` {.haskell}
683−
684−let filter = RangeFilter (FieldName "age")
685− (RangeLte (LessThanEq 100000.0))
686− RangeExecutionIndex False
687−
688−```
689−
690−##### Date Ranges
691−
692−Date ranges are expressed in UTCTime. Date ranges use the same range bound constructors as numerics, except that they end in "D".
693−
694−Note that compatibility with ES is tested only down to seconds.
695−
696−``` {.haskell}
697−
698−let 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−```
708−
709−#### Regexp Filter
710−
711−``` {.haskell}
712−
713−-- RegexpFilter
714−-- :: FieldName
715−-- -> Regexp
716−-- -> RegexpFlags
717−-- -> CacheName
718−-- -> Cache
719−-- -> CacheKey
720−-- -> Filter
721−let filter = RegexpFilter (FieldName "user") (Regexp "bite.*app")
722− AllRegexpFlags (CacheName "test") False (CacheKey "key")
723−
724−-- n.b.
725−-- data RegexpFlags = AllRegexpFlags
726−-- | NoRegexpFlags
727−-- | SomeRegexpFlags (NonEmpty RegexpFlag) deriving (Eq, Show)
728−
729−-- data RegexpFlag = AnyString
730−-- | Automaton
731−-- | Complement
732−-- | Empty
733−-- | Intersection
734−-- | Interval deriving (Eq, Show)
735−
736−```
737−
738−### Aggregations
739−
740−#### Adding aggregations to search
741−
742−Aggregations can now be added to search queries, or made on their own.
743−
744−``` {.haskell}
745−type Aggregations = M.Map Text Aggregation
746−data Aggregation
747− = TermsAgg TermsAggregation
748− | DateHistogramAgg DateHistogramAggregation
749−```
750−
751−For convenience, \`\`\`mkAggregations\`\`\` exists, that will create an \`\`\`Aggregations\`\`\` with the aggregation provided.
752−
753−For example:
754−
755−``` {.haskell}
756− let a = mkAggregations "users" $ TermsAgg $ mkTermsAggregation "user"
757− let search = mkAggregateSearch Nothing a
758−```
759−
760−Aggregations can be added to an existing search, using the \`\`\`aggBody\`\`\` field
761−
762−``` {.haskell}
763− let search = mkSearch (Just (MatchAllQuery Nothing)) Nothing
764− let search' = search {aggBody = Just a}
765−```
766−
767−Since the \`\`\`Aggregations\`\`\` structure is just a Map Text Aggregation, M.insert can be used to add additional aggregations.
768−
769−``` {.haskell}
770− let a' = M.insert "age" (TermsAgg $ mkTermsAggregation "age") a
771−```
772−
773−#### Extracting aggregations from results
774−
775−Aggregations are part of the reply structure of every search, in the
776−form of `Maybe AggregationResults`
777−
778−``` {.haskell}
779−-- Lift decode and response body to be in the IO monad.
780−let decode' = liftM decode
781−let responseBody' = liftM responseBody
782−let reply = withBH' $ searchByIndex testIndex search
783−let response = decode' $ responseBody' reply :: IO (Maybe (SearchResult Tweet))
784−
785−-- Now that we have our response, we can extract our terms aggregation result -- which is a list of buckets.
786−
787−let terms = do { response' <- response; return $ response' >>= aggregations >>= toTerms "users" }
788−terms
789−Just (Bucket {buckets = [TermsResult {termKey = "bitemyapp", termsDocCount = 1, termsAggs = Nothing}]})
790−```
791−
792−Note that bucket aggregation results, such as the TermsResult is a member of the type class `BucketAggregation`:
793−
794−``` {.haskell}
795−class BucketAggregation a where
796− key :: a -> Text
797− docCount :: a -> Int
798− aggs :: a -> Maybe AggregationResults
799−```
800−
801−You can use the `aggs` function to get any nested results, if there
802−were any. For example, if there were a nested terms aggregation keyed
803−to "age" in a TermsResult named `termresult` , you would call `aggs
804−termresult >>= toTerms "age"`
805−
806−#### Terms Aggregation
807−
808−``` {.haskell}
809−data 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−```
821−
822−Term Aggregations have two factory functions, `mkTermsAggregation`, and
823−`mkTermsScriptAggregation`, and can be used as follows:
824−
825−``` {.haskell}
826−let ta = TermsAgg $ mkTermsAggregation "user"
827−```
828−
829−There are of course other options that can be added to a Terms Aggregation, such as the collection mode:
830−
831−``` {.haskell}
832−let ta = mkTermsAggregation "user"
833−let ta' = ta { termCollectMode = Just BreadthFirst }
834−let ta'' = TermsAgg ta'
835−```
836−
837−For more documentation on how the Terms Aggregation works, see <https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html>
838−
839−#### Date Histogram Aggregation
840−
841−``` {.haskell}
842−data 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−```
852−
853−The Date Histogram Aggregation works much the same as the Terms Aggregation.
854−
855−Relevant functions include `mkDateHistogram`, and `toDateHistogram`
856−
857−``` {.haskell}
858−let dh = DateHistogramAgg (mkDateHistogram (FieldName "postDate") Minute)
859−```
860−
861−Date histograms also accept a `FractionalInterval`:
862−
863−``` {.haskell}
864−FractionalInterval :: Float -> TimeInterval -> Interval
865−-- TimeInterval is the following:
866−data TimeInterval = Weeks | Days | Hours | Minutes | Seconds
867−```
868−
869−It can be used as follows:
870−
871−``` {.haskell}
872−let dh = DateHistogramAgg (mkDateHistogram (FieldName "postDate") (FractionalInterval 1.5 Minutes))
873−```
874−
875−The `DateHistogramResult` is defined as:
876−
877−``` {.haskell}
878−data DateHistogramResult
879− = DateHistogramResult {dateKey :: Int,
880− dateKeyStr :: Maybe Text,
881− dateDocCount :: Int,
882− dateHistogramAggs :: Maybe AggregationResults}
883−```
884−
885−It is an instance of `BucketAggregation`, and can have nested aggregations in each bucket.
886−
887−Buckets can be extracted from an `AggregationResult` using
888−`toDateHistogram name`
889−
890−For more information on the Date Histogram Aggregation, see: <https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-datehistogram-aggregation.html>
70+See the [examples](htts://github.com/bitemyapp/bloodhound/tree/master/examples) directory for example code.
89171
89272
89373Contributors
bloodhound.cabal−20
@@ -95,23 +95,3 @@ test-suite tests
9595 unix-compat,
9696 network-uri
9797 default-language: Haskell2010
98−
99−test-suite doctests
100− ghc-options: -threaded -Wall
101− default-language: Haskell2010
102− type: exitcode-stdio-1.0
103− main-is: doctests.hs
104− hs-source-dirs: src
105− if flag(ES5)
106− hs-source-dirs: tests/V5
107− else
108− hs-source-dirs: tests/V1
109− if impl(ghc >= 7.8)
110− build-depends: base,
111− aeson,
112− bloodhound,
113− directory,
114− doctest >= 0.10.1,
115− filepath
116− else
117− buildable: False
examples/LICENSEnew file+12
@@ -0,0 +1,12 @@
1+Copyright (c) 2014, Chris Allen
2+All rights reserved.
3+
4+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5+
6+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7+
8+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9+
10+3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
11+
12+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
examples/README.mdnew file+3
@@ -0,0 +1,3 @@
1+# Bloodhound Examples
2+
3+These examples can be build via `stack build`.
examples/Setup.hsnew file+2
@@ -0,0 +1,2 @@
1+import Distribution.Simple
2+main = defaultMain
examples/Tweet.hsnew file+125
@@ -0,0 +1,125 @@
1+{-# LANGUAGE DeriveGeneric #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Main
4+ ( main
5+ ) where
6+
7+
8+-------------------------------------------------------------------------------
9+import Control.Monad.IO.Class (liftIO)
10+import Data.Aeson (FromJSON (..), defaultOptions,
11+ genericParseJSON, genericToJSON,
12+ object, (.=))
13+import Data.List.NonEmpty (NonEmpty (..))
14+import Data.Text (Text)
15+import Data.Time.Calendar (Day (..))
16+import Data.Time.Clock (UTCTime (..), secondsToDiffTime)
17+import qualified Data.Vector as V
18+import Database.V1.Bloodhound
19+import GHC.Generics (Generic)
20+import Network.HTTP.Client (defaultManagerSettings)
21+-------------------------------------------------------------------------------
22+
23+
24+data TweetMapping = TweetMapping deriving (Eq, Show)
25+
26+instance ToJSON TweetMapping where
27+ toJSON TweetMapping =
28+ object
29+ [ "properties" .=
30+ object ["location" .= object ["type" .= ("geo_point" :: Text)]]
31+ ]
32+
33+
34+-------------------------------------------------------------------------------
35+data Location = Location
36+ { locLat :: Double
37+ , locLon :: Double
38+ } deriving (Eq, Generic, Show)
39+
40+
41+-------------------------------------------------------------------------------
42+data Tweet = Tweet
43+ { user :: Text
44+ , postDate :: UTCTime
45+ , message :: Text
46+ , age :: Int
47+ , location :: Location
48+ } deriving (Eq, Generic, Show)
49+
50+
51+-------------------------------------------------------------------------------
52+exampleTweet :: Tweet
53+exampleTweet =
54+ Tweet
55+ { user = "bitemyapp"
56+ , postDate = UTCTime (ModifiedJulianDay 55000) (secondsToDiffTime 10)
57+ , message = "Use haskell!"
58+ , age = 10000
59+ , location = loc
60+ }
61+ where
62+ loc = Location {locLat = 40.12, locLon = (-71.34)}
63+
64+instance ToJSON Tweet where
65+ toJSON = genericToJSON defaultOptions
66+instance FromJSON Tweet where
67+ parseJSON = genericParseJSON defaultOptions
68+instance ToJSON Location where
69+ toJSON = genericToJSON defaultOptions
70+instance FromJSON Location where
71+ parseJSON = genericParseJSON defaultOptions
72+
73+
74+-------------------------------------------------------------------------------
75+main :: IO ()
76+main = runBH' $ do
77+ -- set up index
78+ _ <- createIndex indexSettings testIndex
79+ True <- indexExists testIndex
80+ _ <- putMapping testIndex testMapping TweetMapping
81+
82+ -- create a tweet
83+ resp <- indexDocument testIndex testMapping defaultIndexDocumentSettings exampleTweet (DocId "1")
84+ liftIO (print resp)
85+ -- Response {responseStatus = Status {statusCode = 201, statusMessage = "Created"}, responseVersion = HTTP/1.1, responseHeaders = [("Content-Type","application/json; charset=UTF-8"),("Content-Length","74")], responseBody = "{\"_index\":\"twitter\",\"_type\":\"tweet\",\"_id\":\"1\",\"_version\":1,\"created\":true}", responseCookieJar = CJ {expose = []}, responseClose' = ResponseClose}
86+
87+ -- bulk load
88+ let stream = V.fromList [BulkIndex testIndex testMapping (DocId "2") (toJSON exampleTweet)]
89+ _ <- bulk stream
90+ -- Bulk loads require an index refresh before new data is loaded.
91+ _ <- refreshIndex testIndex
92+
93+ -- set up some aliases
94+ let aliasName = IndexName "twitter-alias"
95+ let iAlias = IndexAlias testIndex (IndexAliasName aliasName)
96+ let aliasRouting = Nothing
97+ let aliasFiltering = Nothing
98+ let aliasCreate = IndexAliasCreate aliasRouting aliasFiltering
99+ _ <- updateIndexAliases (AddAlias iAlias aliasCreate :| [])
100+ True <- indexExists aliasName
101+
102+ -- create a template so that if we just write into an index named tweet-2017-01-02, for instance, the index will be automatically created with the given mapping. This is a great idea for any ongoing indices because it makes them much easier to manage and rotate.
103+ let idxTpl = IndexTemplate (TemplatePattern "tweet-*") (Just (IndexSettings (ShardCount 1) (ReplicaCount 1))) [toJSON TweetMapping]
104+ let templateName = TemplateName "tweet-tpl"
105+ _ <- putTemplate idxTpl templateName
106+ True <- templateExists templateName
107+
108+ -- do a search
109+ let boost = Nothing
110+ let query = TermQuery (Term "user" "bitemyapp") boost
111+ let search = mkSearch (Just query) boost
112+ _ <- searchByType testIndex testMapping search
113+
114+ -- clean up
115+ _ <- deleteTemplate templateName
116+ _ <- deleteIndex testIndex
117+ False <- indexExists testIndex
118+
119+ return ()
120+ where
121+ testServer = (Server "http://localhost:9200")
122+ runBH' = withBH defaultManagerSettings testServer
123+ testIndex = IndexName "twitter"
124+ testMapping = MappingName "tweet"
125+ indexSettings = IndexSettings (ShardCount 1) (ReplicaCount 0)
examples/package.yamlnew file+38
@@ -0,0 +1,38 @@
1+name: bloodhound-examples
2+version: '0.0.0.0'
3+category: Web
4+author: Chris Allen
5+maintainer: cma@bitemyapp.com
6+copyright: 2017, Chris Allen
7+license: BSD3
8+github: bitemyapp/bloodhound
9+extra-source-files:
10+- README.md
11+dependencies:
12+- base
13+- bloodhound
14+- time
15+- aeson
16+- text
17+- http-client
18+- vector
19+- semigroups
20+- transformers
21+ghc-options:
22+- -Wall
23+- -threaded
24+- -rtsopts
25+- -with-rtsopts=-N
26+when:
27+- condition: flag(werror)
28+ ghc-options: -Werror
29+
30+executables:
31+ tweet-example:
32+ main: Tweet.hs
33+
34+flags:
35+ werror:
36+ description: "Treat warnings as errors"
37+ manual: true
38+ default: false
src/Database/V1/Bloodhound/Client.hs−180
@@ -123,60 +123,8 @@ import Prelude hiding (filter, head)
123123
124124import Database.V1.Bloodhound.Types
125125
126−-- $setup
127−-- >>> :set -XOverloadedStrings
128−-- >>> :set -XDeriveGeneric
129−-- >>> import Database.V1.Bloodhound
130−-- >>> let testServer = (Server "http://localhost:9200")
131−-- >>> let runBH' = withBH defaultManagerSettings testServer
132−-- >>> let testIndex = IndexName "twitter"
133−-- >>> let testMapping = MappingName "tweet"
134−-- >>> let defaultIndexSettings = IndexSettings (ShardCount 1) (ReplicaCount 0)
135−-- >>> data TweetMapping = TweetMapping deriving (Eq, Show)
136−-- >>> _ <- runBH' $ deleteIndex testIndex >> deleteMapping testIndex testMapping
137−-- >>> import GHC.Generics
138−-- >>> import Data.Time.Calendar (Day (..))
139−-- >>> import Data.Time.Clock (UTCTime (..), secondsToDiffTime)
140−-- >>> :{
141−--instance ToJSON TweetMapping where
142−-- toJSON TweetMapping =
143−-- object ["properties" .=
144−-- object ["location" .=
145−-- object ["type" .= ("geo_point" :: Text)]]]
146−--data Location = Location { lat :: Double
147−-- , lon :: Double } deriving (Eq, Generic, Show)
148−--data Tweet = Tweet { user :: Text
149−-- , postDate :: UTCTime
150−-- , message :: Text
151−-- , age :: Int
152−-- , location :: Location } deriving (Eq, Generic, Show)
153−--exampleTweet = Tweet { user = "bitemyapp"
154−-- , postDate = UTCTime
155−-- (ModifiedJulianDay 55000)
156−-- (secondsToDiffTime 10)
157−-- , message = "Use haskell!"
158−-- , age = 10000
159−-- , location = Location 40.12 (-71.34) }
160−--instance ToJSON Tweet where
161−-- toJSON = genericToJSON defaultOptions
162−--instance FromJSON Tweet where
163−-- parseJSON = genericParseJSON defaultOptions
164−--instance ToJSON Location where
165−-- toJSON = genericToJSON defaultOptions
166−--instance FromJSON Location where
167−-- parseJSON = genericParseJSON defaultOptions
168−--data BulkTest = BulkTest { name :: Text } deriving (Eq, Generic, Show)
169−--instance FromJSON BulkTest where
170−-- parseJSON = genericParseJSON defaultOptions
171−--instance ToJSON BulkTest where
172−-- toJSON = genericToJSON defaultOptions
173−-- :}
174−
175126-- | 'mkShardCount' is a straight-forward smart constructor for 'ShardCount'
176127-- which rejects 'Int' values below 1 and above 1000.
177−--
178−-- >>> mkShardCount 10
179−-- Just (ShardCount 10)
180128mkShardCount :: Int -> Maybe ShardCount
181129mkShardCount n
182130 | n < 1 = Nothing
@@ -185,9 +133,6 @@ mkShardCount n
185133
186134-- | 'mkReplicaCount' is a straight-forward smart constructor for 'ReplicaCount'
187135-- which rejects 'Int' values below 0 and above 1000.
188−--
189−-- >>> mkReplicaCount 10
190−-- Just (ReplicaCount 10)
191136mkReplicaCount :: Int -> Maybe ReplicaCount
192137mkReplicaCount n
193138 | n < 0 = Nothing
@@ -271,10 +216,6 @@ post = dispatch NHTM.methodPost
271216-- https://github.com/supki/libjenkins/blob/master/src/Jenkins/Rest/Internal.hs
272217
273218-- | 'getStatus' fetches the 'Status' of a 'Server'
274−--
275−-- >>> serverStatus <- runBH' getStatus
276−-- >>> fmap status (serverStatus)
277−-- Just 200
278219getStatus :: MonadBH m => m (Maybe Status)
279220getStatus = do
280221 response <- get =<< url
@@ -494,12 +435,6 @@ getNodesStats sel = parseEsResponse =<< get =<< url
494435 selToSeg (NodeByAttribute (NodeAttrName a) v) = a <> ":" <> v
495436
496437-- | 'createIndex' will create an index given a 'Server', 'IndexSettings', and an 'IndexName'.
497−--
498−-- >>> response <- runBH' $ createIndex defaultIndexSettings (IndexName "didimakeanindex")
499−-- >>> respIsTwoHunna response
500−-- True
501−-- >>> runBH' $ indexExists (IndexName "didimakeanindex")
502−-- True
503438createIndex :: MonadBH m => IndexSettings -> IndexName -> m Reply
504439createIndex indexSettings (IndexName indexName) =
505440 bindM2 put url (return body)
@@ -508,23 +443,11 @@ createIndex indexSettings (IndexName indexName) =
508443
509444
510445-- | 'deleteIndex' will delete an index given a 'Server', and an 'IndexName'.
511−--
512−-- >>> _ <- runBH' $ createIndex defaultIndexSettings (IndexName "didimakeanindex")
513−-- >>> response <- runBH' $ deleteIndex (IndexName "didimakeanindex")
514−-- >>> respIsTwoHunna response
515−-- True
516−-- >>> runBH' $ indexExists testIndex
517−-- False
518446deleteIndex :: MonadBH m => IndexName -> m Reply
519447deleteIndex (IndexName indexName) =
520448 delete =<< joinPath [indexName]
521449
522450-- | 'updateIndexSettings' will apply a non-empty list of setting updates to an index
523−--
524−-- >>> _ <- runBH' $ createIndex defaultIndexSettings (IndexName "unconfiguredindex")
525−-- >>> response <- runBH' $ updateIndexSettings (BlocksWrite False :| []) (IndexName "unconfiguredindex")
526−-- >>> respIsTwoHunna response
527−-- True
528451updateIndexSettings :: MonadBH m => NonEmpty UpdatableIndexSetting -> IndexName -> m Reply
529452updateIndexSettings updates (IndexName indexName) =
530453 bindM2 put url (return body)
@@ -557,12 +480,6 @@ getIndexSettings (IndexName indexName) = do
557480-- almost completely identical forcemerge API. Adding support to that
558481-- API would be trivial but due to the significant breaking changes,
559482-- this library cannot currently be used with >= 2.0, so that feature was omitted.
560−--
561−-- >>> let ixn = IndexName "unoptimizedindex"
562−-- >>> _ <- runBH' $ deleteIndex ixn >> createIndex defaultIndexSettings ixn
563−-- >>> response <- runBH' $ optimizeIndex (IndexList (ixn :| [])) (defaultIndexOptimizationSettings { maxNumSegments = Just 1, onlyExpungeDeletes = True })
564−-- >>> respIsTwoHunna response
565−-- True
566483optimizeIndex :: MonadBH m => IndexSelection -> IndexOptimizationSettings -> m Reply
567484optimizeIndex ixs IndexOptimizationSettings {..} =
568485 bindM2 post url (return body)
@@ -617,8 +534,6 @@ parseEsResponse reply
617534
618535-- | 'indexExists' enables you to check if an index exists. Returns 'Bool'
619536-- in IO
620−--
621−-- >>> exists <- runBH' $ indexExists testIndex
622537indexExists :: MonadBH m => IndexName -> m Bool
623538indexExists (IndexName indexName) = do
624539 (_, exists) <- existentialQuery =<< joinPath [indexName]
@@ -626,9 +541,6 @@ indexExists (IndexName indexName) = do
626541
627542-- | 'refreshIndex' will force a refresh on an index. You must
628543-- do this if you want to read what you wrote.
629−--
630−-- >>> _ <- runBH' $ createIndex defaultIndexSettings testIndex
631−-- >>> _ <- runBH' $ refreshIndex testIndex
632544refreshIndex :: MonadBH m => IndexName -> m Reply
633545refreshIndex (IndexName indexName) =
634546 bindM2 post url (return Nothing)
@@ -655,15 +567,11 @@ openOrCloseIndexes oci (IndexName indexName) =
655567
656568-- | 'openIndex' opens an index given a 'Server' and an 'IndexName'. Explained in further detail at
657569-- <http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-open-close.html>
658−--
659−-- >>> reply <- runBH' $ openIndex testIndex
660570openIndex :: MonadBH m => IndexName -> m Reply
661571openIndex = openOrCloseIndexes OpenIndex
662572
663573-- | 'closeIndex' closes an index given a 'Server' and an 'IndexName'. Explained in further detail at
664574-- <http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-open-close.html>
665−--
666−-- >>> reply <- runBH' $ closeIndex testIndex
667575closeIndex :: MonadBH m => IndexName -> m Reply
668576closeIndex = openOrCloseIndexes CloseIndex
669577
@@ -684,20 +592,6 @@ listIndices =
684592-- | 'updateIndexAliases' updates the server's index alias
685593-- table. Operations are atomic. Explained in further detail at
686594-- <https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-aliases.html>
687−--
688−-- >>> let src = IndexName "a-real-index"
689−-- >>> let aliasName = IndexName "an-alias"
690−-- >>> let iAlias = IndexAlias src (IndexAliasName aliasName)
691−-- >>> let aliasCreate = IndexAliasCreate Nothing Nothing
692−-- >>> _ <- runBH' $ deleteIndex src
693−-- >>> respIsTwoHunna <$> runBH' (createIndex defaultIndexSettings src)
694−-- True
695−-- >>> runBH' $ indexExists src
696−-- True
697−-- >>> respIsTwoHunna <$> runBH' (updateIndexAliases (AddAlias iAlias aliasCreate :| []))
698−-- True
699−-- >>> runBH' $ indexExists aliasName
700−-- True
701595updateIndexAliases :: MonadBH m => NonEmpty IndexAliasAction -> m Reply
702596updateIndexAliases actions = bindM2 post url (return body)
703597 where url = joinPath ["_aliases"]
@@ -713,9 +607,6 @@ getIndexAliases = parseEsResponse =<< get =<< url
713607-- | 'putTemplate' creates a template given an 'IndexTemplate' and a 'TemplateName'.
714608-- Explained in further detail at
715609-- <https://www.elastic.co/guide/en/elasticsearch/reference/1.7/indices-templates.html>
716−--
717−-- >>> let idxTpl = IndexTemplate (TemplatePattern "tweet-*") (Just (IndexSettings (ShardCount 1) (ReplicaCount 1))) [toJSON TweetMapping]
718−-- >>> resp <- runBH' $ putTemplate idxTpl (TemplateName "tweet-tpl")
719610putTemplate :: MonadBH m => IndexTemplate -> TemplateName -> m Reply
720611putTemplate indexTemplate (TemplateName templateName) =
721612 bindM2 put url (return body)
@@ -723,29 +614,18 @@ putTemplate indexTemplate (TemplateName templateName) =
723614 body = Just $ encode indexTemplate
724615
725616-- | 'templateExists' checks to see if a template exists.
726−--
727−-- >>> exists <- runBH' $ templateExists (TemplateName "tweet-tpl")
728617templateExists :: MonadBH m => TemplateName -> m Bool
729618templateExists (TemplateName templateName) = do
730619 (_, exists) <- existentialQuery =<< joinPath ["_template", templateName]
731620 return exists
732621
733622-- | 'deleteTemplate' is an HTTP DELETE and deletes a template.
734−--
735−-- >>> let idxTpl = IndexTemplate (TemplatePattern "tweet-*") (Just (IndexSettings (ShardCount 1) (ReplicaCount 1))) [toJSON TweetMapping]
736−-- >>> _ <- runBH' $ putTemplate idxTpl (TemplateName "tweet-tpl")
737−-- >>> resp <- runBH' $ deleteTemplate (TemplateName "tweet-tpl")
738623deleteTemplate :: MonadBH m => TemplateName -> m Reply
739624deleteTemplate (TemplateName templateName) =
740625 delete =<< joinPath ["_template", templateName]
741626
742627-- | 'putMapping' is an HTTP PUT and has upsert semantics. Mappings are schemas
743628-- for documents in indexes.
744−--
745−-- >>> _ <- runBH' $ createIndex defaultIndexSettings testIndex
746−-- >>> resp <- runBH' $ putMapping testIndex testMapping TweetMapping
747−-- >>> print resp
748−-- Response {responseStatus = Status {statusCode = 200, statusMessage = "OK"}, responseVersion = HTTP/1.1, responseHeaders = [("Content-Type","application/json; charset=UTF-8"),("Content-Length","21")], responseBody = "{\"acknowledged\":true}", responseCookieJar = CJ {expose = []}, responseClose' = ResponseClose}
749629putMapping :: (MonadBH m, ToJSON a) => IndexName
750630 -> MappingName -> a -> m Reply
751631putMapping (IndexName indexName) (MappingName mappingName) mapping =
@@ -757,12 +637,6 @@ putMapping (IndexName indexName) (MappingName mappingName) mapping =
757637
758638-- | 'deleteMapping' is an HTTP DELETE and deletes a mapping for a given index.
759639-- Mappings are schemas for documents in indexes.
760−--
761−-- >>> _ <- runBH' $ createIndex defaultIndexSettings testIndex
762−-- >>> _ <- runBH' $ putMapping testIndex testMapping TweetMapping
763−-- >>> resp <- runBH' $ deleteMapping testIndex testMapping
764−-- >>> print resp
765−-- Response {responseStatus = Status {statusCode = 200, statusMessage = "OK"}, responseVersion = HTTP/1.1, responseHeaders = [("Content-Type","application/json; charset=UTF-8"),("Content-Length","21")], responseBody = "{\"acknowledged\":true}", responseCookieJar = CJ {expose = []}, responseClose' = ResponseClose}
766640deleteMapping :: MonadBH m => IndexName -> MappingName -> m Reply
767641deleteMapping (IndexName indexName)
768642 (MappingName mappingName) =
@@ -788,10 +662,6 @@ versionCtlParams cfg =
788662-- Elasticsearch. The document itself is simply something we can
789663-- convert into a JSON 'Value'. The 'DocId' will function as the
790664-- primary key for the document.
791−--
792−-- >>> resp <- runBH' $ indexDocument testIndex testMapping defaultIndexDocumentSettings exampleTweet (DocId "1")
793−-- >>> print resp
794−-- Response {responseStatus = Status {statusCode = 201, statusMessage = "Created"}, responseVersion = HTTP/1.1, responseHeaders = [("Content-Type","application/json; charset=UTF-8"),("Content-Length","74")], responseBody = "{\"_index\":\"twitter\",\"_type\":\"tweet\",\"_id\":\"1\",\"_version\":1,\"created\":true}", responseCookieJar = CJ {expose = []}, responseClose' = ResponseClose}
795665indexDocument :: (ToJSON doc, MonadBH m) => IndexName -> MappingName
796666 -> IndexDocumentSettings -> doc -> DocId -> m Reply
797667indexDocument (IndexName indexName)
@@ -816,8 +686,6 @@ updateDocument (IndexName indexName)
816686 body = Just (encode $ object ["doc" .= toJSON patch])
817687
818688-- | 'deleteDocument' is the primary way to delete a single document.
819−--
820−-- >>> _ <- runBH' $ deleteDocument testIndex testMapping (DocId "1")
821689deleteDocument :: MonadBH m => IndexName -> MappingName
822690 -> DocId -> m Reply
823691deleteDocument (IndexName indexName)
@@ -830,10 +698,6 @@ deleteDocument (IndexName indexName)
830698-- index\/update\/delete\/create operations. You pass a 'V.Vector' of 'BulkOperation's
831699-- and a 'Server' to 'bulk' in order to send those operations up to your Elasticsearch
832700-- server to be performed. I changed from [BulkOperation] to a Vector due to memory overhead.
833−--
834−-- >>> let stream = V.fromList [BulkIndex testIndex testMapping (DocId "2") (toJSON (BulkTest "blah"))]
835−-- >>> _ <- runBH' $ bulk stream
836−-- >>> _ <- runBH' $ refreshIndex testIndex
837701bulk :: MonadBH m => V.Vector BulkOperation -> m Reply
838702bulk bulkOps = bindM2 post url (return body)
839703 where url = joinPath ["_bulk"]
@@ -841,10 +705,6 @@ bulk bulkOps = bindM2 post url (return body)
841705
842706-- | 'encodeBulkOperations' is a convenience function for dumping a vector of 'BulkOperation'
843707-- into an 'L.ByteString'
844−--
845−-- >>> let bulkOps = V.fromList [BulkIndex testIndex testMapping (DocId "2") (toJSON (BulkTest "blah"))]
846−-- >>> encodeBulkOperations bulkOps
847−-- "\n{\"index\":{\"_type\":\"tweet\",\"_id\":\"2\",\"_index\":\"twitter\"}}\n{\"name\":\"blah\"}\n"
848708encodeBulkOperations :: V.Vector BulkOperation -> L.ByteString
849709encodeBulkOperations stream = collapsed where
850710 blobs = fmap encodeBulkOperation stream
@@ -863,10 +723,6 @@ mkBulkStreamValue operation indexName mappingName docId =
863723
864724-- | 'encodeBulkOperation' is a convenience function for dumping a single 'BulkOperation'
865725-- into an 'L.ByteString'
866−--
867−-- >>> let bulkOp = BulkIndex testIndex testMapping (DocId "2") (toJSON (BulkTest "blah"))
868−-- >>> encodeBulkOperation bulkOp
869−-- "{\"index\":{\"_type\":\"tweet\",\"_id\":\"2\",\"_index\":\"twitter\"}}\n{\"name\":\"blah\"}"
870726encodeBulkOperation :: BulkOperation -> L.ByteString
871727encodeBulkOperation (BulkIndex (IndexName indexName)
872728 (MappingName mappingName)
@@ -896,8 +752,6 @@ encodeBulkOperation (BulkUpdate (IndexName indexName)
896752-- | 'getDocument' is a straight-forward way to fetch a single document from
897753-- Elasticsearch using a 'Server', 'IndexName', 'MappingName', and a 'DocId'.
898754-- The 'DocId' is the primary key for your Elasticsearch document.
899−--
900−-- >>> yourDoc <- runBH' $ getDocument testIndex testMapping (DocId "1")
901755getDocument :: MonadBH m => IndexName -> MappingName
902756 -> DocId -> m Reply
903757getDocument (IndexName indexName)
@@ -906,8 +760,6 @@ getDocument (IndexName indexName)
906760
907761-- | 'documentExists' enables you to check if a document exists. Returns 'Bool'
908762-- in IO
909−--
910−-- >>> exists <- runBH' $ documentExists testIndex testMapping Nothing (DocId "1")
911763documentExists :: MonadBH m => IndexName -> MappingName
912764 -> Maybe DocumentParent -> DocId -> m Bool
913765documentExists (IndexName indexName) (MappingName mappingName)
@@ -924,30 +776,18 @@ dispatchSearch url search = post url' (Just (encode search))
924776
925777-- | 'searchAll', given a 'Search', will perform that search against all indexes
926778-- on an Elasticsearch server. Try to avoid doing this if it can be helped.
927−--
928−-- >>> let query = TermQuery (Term "user" "bitemyapp") Nothing
929−-- >>> let search = mkSearch (Just query) Nothing
930−-- >>> reply <- runBH' $ searchAll search
931779searchAll :: MonadBH m => Search -> m Reply
932780searchAll = bindM2 dispatchSearch url . return
933781 where url = joinPath ["_search"]
934782
935783-- | 'searchByIndex', given a 'Search' and an 'IndexName', will perform that search
936784-- against all mappings within an index on an Elasticsearch server.
937−--
938−-- >>> let query = TermQuery (Term "user" "bitemyapp") Nothing
939−-- >>> let search = mkSearch (Just query) Nothing
940−-- >>> reply <- runBH' $ searchByIndex testIndex search
941785searchByIndex :: MonadBH m => IndexName -> Search -> m Reply
942786searchByIndex (IndexName indexName) = bindM2 dispatchSearch url . return
943787 where url = joinPath [indexName, "_search"]
944788
945789-- | 'searchByType', given a 'Search', 'IndexName', and 'MappingName', will perform that
946790-- search against a specific mapping within an index on an Elasticsearch server.
947−--
948−-- >>> let query = TermQuery (Term "user" "bitemyapp") Nothing
949−-- >>> let search = mkSearch (Just query) Nothing
950−-- >>> reply <- runBH' $ searchByType testIndex testMapping search
951791searchByType :: MonadBH m => IndexName -> MappingName -> Search
952792 -> m Reply
953793searchByType (IndexName indexName)
@@ -1020,29 +860,16 @@ scanSearch indexName mappingName search = do
1020860-- to Nothing in case you only care about your 'Query' and 'Filter'. Use record update
1021861-- syntax if you want to add things like aggregations or highlights while still using
1022862-- this helper function.
1023−--
1024−-- >>> let query = TermQuery (Term "user" "bitemyapp") Nothing
1025−-- >>> mkSearch (Just query) Nothing
1026−-- Search {queryBody = Just (TermQuery (Term {termField = "user", termValue = "bitemyapp"}) Nothing), filterBody = Nothing, sortBody = Nothing, aggBody = Nothing, highlight = Nothing, trackSortScores = False, from = From 0, size = Size 10, searchType = SearchTypeQueryThenFetch, fields = Nothing, source = Nothing}
1027863mkSearch :: Maybe Query -> Maybe Filter -> Search
1028864mkSearch query filter = Search query filter Nothing Nothing Nothing False (From 0) (Size 10) SearchTypeQueryThenFetch Nothing Nothing
1029865
1030866-- | 'mkAggregateSearch' is a helper function that defaults everything in a 'Search' except for
1031867-- the 'Query' and the 'Aggregation'.
1032−--
1033−-- >>> let terms = TermsAgg $ (mkTermsAggregation "user") { termCollectMode = Just BreadthFirst }
1034−-- >>> terms
1035−-- TermsAgg (TermsAggregation {term = Left "user", termInclude = Nothing, termExclude = Nothing, termOrder = Nothing, termMinDocCount = Nothing, termSize = Nothing, termShardSize = Nothing, termCollectMode = Just BreadthFirst, termExecutionHint = Nothing, termAggs = Nothing})
1036−-- >>> let myAggregation = mkAggregateSearch Nothing $ mkAggregations "users" terms
1037868mkAggregateSearch :: Maybe Query -> Aggregations -> Search
1038869mkAggregateSearch query mkSearchAggs = Search query Nothing Nothing (Just mkSearchAggs) Nothing False (From 0) (Size 0) SearchTypeQueryThenFetch Nothing Nothing
1039870
1040871-- | 'mkHighlightSearch' is a helper function that defaults everything in a 'Search' except for
1041872-- the 'Query' and the 'Aggregation'.
1042−--
1043−-- >>> let query = QueryMatchQuery $ mkMatchQuery (FieldName "_all") (QueryString "haskell")
1044−-- >>> let testHighlight = Highlights Nothing [FieldHighlight (FieldName "message") Nothing]
1045−-- >>> let search = mkHighlightSearch (Just query) testHighlight
1046873mkHighlightSearch :: Maybe Query -> Highlights -> Search
1047874mkHighlightSearch query searchHighlights = Search query Nothing Nothing Nothing (Just searchHighlights) False (From 0) (Size 10) SearchTypeQueryThenFetch Nothing Nothing
1048875
@@ -1050,13 +877,6 @@ mkHighlightSearch query searchHighlights = Search query Nothing Nothing Nothing
1050877-- and size fields for the search. The from parameter defines the offset
1051878-- from the first result you want to fetch. The size parameter allows you to
1052879-- configure the maximum amount of hits to be returned.
1053−--
1054−-- >>> let query = QueryMatchQuery $ mkMatchQuery (FieldName "_all") (QueryString "haskell")
1055−-- >>> let search = mkSearch (Just query) Nothing
1056−-- >>> search
1057−-- Search {queryBody = Just (QueryMatchQuery (MatchQuery {matchQueryField = FieldName "_all", matchQueryQueryString = QueryString "haskell", matchQueryOperator = Or, matchQueryZeroTerms = ZeroTermsNone, matchQueryCutoffFrequency = Nothing, matchQueryMatchType = Nothing, matchQueryAnalyzer = Nothing, matchQueryMaxExpansions = Nothing, matchQueryLenient = Nothing, matchQueryBoost = Nothing})), filterBody = Nothing, sortBody = Nothing, aggBody = Nothing, highlight = Nothing, trackSortScores = False, from = From 0, size = Size 10, searchType = SearchTypeQueryThenFetch, fields = Nothing, source = Nothing}
1058−-- >>> pageSearch (From 10) (Size 100) search
1059−-- Search {queryBody = Just (QueryMatchQuery (MatchQuery {matchQueryField = FieldName "_all", matchQueryQueryString = QueryString "haskell", matchQueryOperator = Or, matchQueryZeroTerms = ZeroTermsNone, matchQueryCutoffFrequency = Nothing, matchQueryMatchType = Nothing, matchQueryAnalyzer = Nothing, matchQueryMaxExpansions = Nothing, matchQueryLenient = Nothing, matchQueryBoost = Nothing})), filterBody = Nothing, sortBody = Nothing, aggBody = Nothing, highlight = Nothing, trackSortScores = False, from = From 10, size = Size 100, searchType = SearchTypeQueryThenFetch, fields = Nothing, source = Nothing}
1060880pageSearch :: From -- ^ The result offset
1061881 -> Size -- ^ The number of results to return
1062882 -> Search -- ^ The current seach
stack-7.10.yaml+4−2
@@ -1,13 +1,15 @@
1−flags: {}
1+flags:
2+ bloodhound-examples:
3+ werror: true
24packages:
35- '.'
6+- './examples'
47extra-deps:
58- http-client-0.5.0
69- aeson-0.11.1.0
710- fail-4.9.0.0
811- http-types-0.9
912- attoparsec-0.13.0.1
10−- doctest-0.10.1
1113- quickcheck-properties-0.1
1214- semigroups-0.18.0.1
1315- uri-bytestring-0.1.9
stack-7.8.yaml+3−1
@@ -1,15 +1,17 @@
11flags:
22 aeson:
33 old-locale: true
4+ bloodhound-examples:
5+ werror: true
46packages:
57- '.'
8+- './examples'
69extra-deps:
710- aeson-0.11.1.0
811- fail-4.9.0.0
912- http-types-0.9
1013- http-client-0.5.0
1114- attoparsec-0.13.0.1
12−- doctest-0.10.1
1315- quickcheck-properties-0.1
1416- semigroups-0.18.0.1
1517- tagged-0.8.3
stack.yaml+4−1
@@ -1,6 +1,9 @@
1−flags: {}
1+flags:
2+ bloodhound-examples:
3+ werror: true
24packages:
35- '.'
6+- './examples'
47extra-deps:
58- quickcheck-properties-0.1
69- vector-0.12.0.0
tests/V1/doctests.hsdeleted−4
@@ -1,4 +0,0 @@
1−import Test.DocTest
2−
3−main :: IO ()
4−main = doctest ["-i src", "Database.V1.Bloodhound"]
tests/V5/doctests.hsdeleted−4
@@ -1,4 +0,0 @@
1−import Test.DocTest
2−
3−main :: IO ()
4−main = doctest ["-i src", "Database.V5.Bloodhound"]