1{-# LANGUAGE DeriveGeneric #-}
2{-# LANGUAGE OverloadedStrings #-}
3{-# LANGUAGE ScopedTypeVariables #-}
4{-# OPTIONS_GHC -fno-warn-orphans #-}
7import Control.Applicative
8import Control.Exception
10import Control.Monad.Reader
12import qualified Data.HashMap.Strict as HM
14import Data.List.NonEmpty (NonEmpty (..))
15import qualified Data.List.NonEmpty as NE
16import qualified Data.Map.Strict as M
18import Data.Text (Text)
19import qualified Data.Text as T
20import Data.Time.Calendar (Day (..))
21import Data.Time.Clock (UTCTime (..),
23import qualified Data.Vector as V
24import Database.Bloodhound
25import GHC.Generics (Generic)
26import Network.HTTP.Client
27import qualified Network.HTTP.Types.Status as NHTS
28import Prelude hiding (filter)
30import Test.QuickCheck.Property.Monoid (prop_Monoid, eq, T(..))
32import Test.Hspec.QuickCheck (prop)
36testServer = Server "http://localhost:9200"
38testIndex = IndexName "bloodhound-tests-twitter-1"
39testMapping :: MappingName
40testMapping = MappingName "tweet"
42withTestEnv :: BH IO a -> IO a
43withTestEnv = withBH defaultManagerSettings testServer
45validateStatus :: Response body -> Int -> Expectation
46validateStatus resp expected =
47 (NHTS.statusCode $ responseStatus resp)
48 `shouldBe` (expected :: Int)
50createExampleIndex :: BH IO Reply
51createExampleIndex = createIndex (IndexSettings (ShardCount 1) (ReplicaCount 0)) testIndex
52deleteExampleIndex :: BH IO Reply
53deleteExampleIndex = deleteIndex testIndex
55data ServerVersion = ServerVersion Int Int Int deriving (Show, Eq, Ord)
58es14 = ServerVersion 1 4 0
61es13 = ServerVersion 1 3 0
64es12 = ServerVersion 1 2 0
67es11 = ServerVersion 1 1 0
70es10 = ServerVersion 1 0 0
72serverBranch :: ServerVersion -> ServerVersion
73serverBranch (ServerVersion majorVer minorVer patchVer) =
74 ServerVersion majorVer minorVer patchVer
76mkServerVersion :: [Int] -> Maybe ServerVersion
77mkServerVersion [majorVer, minorVer, patchVer] =
78 Just (ServerVersion majorVer minorVer patchVer)
79mkServerVersion _ = Nothing
81getServerVersion :: IO (Maybe ServerVersion)
82getServerVersion = liftM extractVersion (withTestEnv getStatus)
84 version' = T.splitOn "." . number . version
85 toInt = read . T.unpack
86 parseVersion v = map toInt (version' v)
87 extractVersion = join . liftM (mkServerVersion . parseVersion)
89testServerBranch :: IO (Maybe ServerVersion)
90testServerBranch = getServerVersion >>= \v -> return $ liftM serverBranch v
92atleast :: ServerVersion -> IO Bool
93atleast v = testServerBranch >>= \x -> return $ x >= Just (serverBranch v)
95atmost :: ServerVersion -> IO Bool
96atmost v = testServerBranch >>= \x -> return $ x <= Just (serverBranch v)
98is :: ServerVersion -> IO Bool
99is v = testServerBranch >>= \x -> return $ x == Just (serverBranch v)
101when' :: Monad m => m Bool -> m () -> m ()
102when' b f = b >>= \x -> when x f
104data Location = Location { lat :: Double
105 , lon :: Double } deriving (Eq, Generic, Show)
107data Tweet = Tweet { user :: Text
108 , postDate :: UTCTime
111 , location :: Location }
112 deriving (Eq, Generic, Show)
115instance FromJSON Tweet
116instance ToJSON Location
117instance FromJSON Location
119data ParentMapping = ParentMapping deriving (Eq, Show)
121instance ToJSON ParentMapping where
122 toJSON ParentMapping =
123 object ["parent" .= Null ]
125data ChildMapping = ChildMapping deriving (Eq, Show)
127instance ToJSON ChildMapping where
128 toJSON ChildMapping =
130 object ["_parent" .= object ["type" .= ("parent" :: Text)]]
133data TweetMapping = TweetMapping deriving (Eq, Show)
135instance ToJSON TweetMapping where
136 toJSON TweetMapping =
138 object ["properties" .=
139 object [ "user" .= object ["type" .= ("string" :: Text)]
140 -- Serializing the date as a date is breaking other tests, mysteriously.
141 -- , "postDate" .= object [ "type" .= ("date" :: Text)
142 -- , "format" .= ("YYYY-MM-dd`T`HH:mm:ss.SSSZZ" :: Text)]
143 , "message" .= object ["type" .= ("string" :: Text)]
144 , "age" .= object ["type" .= ("integer" :: Text)]
145 , "location" .= object ["type" .= ("geo_point" :: Text)]
149exampleTweet = Tweet { user = "bitemyapp"
151 (ModifiedJulianDay 55000)
152 (secondsToDiffTime 10)
153 , message = "Use haskell!"
155 , location = Location 40.12 (-71.34) }
158otherTweet = Tweet { user = "notmyapp"
160 (ModifiedJulianDay 55000)
161 (secondsToDiffTime 11)
162 , message = "Use haskell!"
164 , location = Location 40.12 (-71.34) }
166resetIndex :: BH IO ()
168 _ <- deleteExampleIndex
169 _ <- createExampleIndex
170 _ <- putMapping testIndex testMapping TweetMapping
173insertData :: BH IO Reply
176 insertData' defaultIndexDocumentSettings
178insertData' :: IndexDocumentSettings -> BH IO Reply
180 r <- indexDocument testIndex testMapping ids exampleTweet (DocId "1")
181 _ <- refreshIndex testIndex
184insertOther :: BH IO ()
186 _ <- indexDocument testIndex testMapping defaultIndexDocumentSettings otherTweet (DocId "2")
187 _ <- refreshIndex testIndex
190searchTweet :: Search -> BH IO (Either String Tweet)
191searchTweet search = do
192 result <- searchTweets search
193 let myTweet = fmap (hitSource . head . hits . searchHits) result
196searchTweets :: Search -> BH IO (Either String (SearchResult Tweet))
197searchTweets search = eitherDecode . responseBody <$> searchByIndex testIndex search
199searchExpectNoResults :: Search -> BH IO ()
200searchExpectNoResults search = do
201 result <- searchTweets search
202 let emptyHits = fmap (hits . searchHits) result
204 emptyHits `shouldBe` Right []
206searchExpectAggs :: Search -> BH IO ()
207searchExpectAggs search = do
208 reply <- searchAll search
209 let isEmpty x = return (M.null x)
210 let result = decode (responseBody reply) :: Maybe (SearchResult Tweet)
212 (result >>= aggregations >>= isEmpty) `shouldBe` Just False
214searchValidBucketAgg :: (BucketAggregation a, FromJSON a, Show a) =>
215 Search -> Text -> (Text -> AggregationResults -> Maybe (Bucket a)) -> BH IO ()
216searchValidBucketAgg search aggKey extractor = do
217 reply <- searchAll search
218 let bucketDocs = docCount . head . buckets
219 let result = decode (responseBody reply) :: Maybe (SearchResult Tweet)
220 let count = result >>= aggregations >>= extractor aggKey >>= \x -> return (bucketDocs x)
222 count `shouldBe` Just 1
224searchTermsAggHint :: [ExecutionHint] -> BH IO ()
225searchTermsAggHint hints = do
226 let terms hint = TermsAgg $ (mkTermsAggregation "user") { termExecutionHint = Just hint }
227 let search hint = mkAggregateSearch Nothing $ mkAggregations "users" $ terms hint
228 forM_ hints $ searchExpectAggs . search
229 forM_ hints (\x -> searchValidBucketAgg (search x) "users" toTerms)
231searchTweetHighlight :: Search -> BH IO (Either String (Maybe HitHighlight))
232searchTweetHighlight search = do
233 result <- searchTweets search
234 let myHighlight = fmap (hitHighlight . head . hits . searchHits) result
237searchExpectSource :: Source -> Either String Value -> BH IO ()
238searchExpectSource src expected = do
240 let query = QueryMatchQuery $ mkMatchQuery (FieldName "_all") (QueryString "haskell")
241 let search = (mkSearch (Just query) Nothing) { source = Just src }
242 reply <- searchAll search
243 let result = eitherDecode (responseBody reply) :: Either String (SearchResult Value)
244 let value = fmap (hitSource . head . hits . searchHits) result
246 value `shouldBe` expected
248data BulkTest = BulkTest { name :: Text } deriving (Eq, Generic, Show)
249instance FromJSON BulkTest
250instance ToJSON BulkTest
252noDuplicates :: Eq a => [a] -> Bool
253noDuplicates xs = nub xs == xs
255instance Arbitrary RegexpFlags where
256 arbitrary = oneof [ pure AllRegexpFlags
258 , SomeRegexpFlags <$> arbitrary
261instance Arbitrary a => Arbitrary (NonEmpty a) where
262 arbitrary = liftA2 (:|) arbitrary arbitrary
264instance Arbitrary RegexpFlag where
265 arbitrary = oneof [ pure AnyString
273arbitraryScore :: Gen Score
274arbitraryScore = fmap getPositive <$> arbitrary
276instance Arbitrary Text where
277 arbitrary = T.pack <$> arbitrary
279instance (Arbitrary k, Arbitrary v, Ord k) => Arbitrary (M.Map k v) where
280 arbitrary = M.fromList <$> arbitrary
282instance Arbitrary IndexName where
283 arbitrary = IndexName <$> arbitrary
285instance Arbitrary MappingName where
286 arbitrary = MappingName <$> arbitrary
288instance Arbitrary DocId where
289 arbitrary = DocId <$> arbitrary
291instance Arbitrary a => Arbitrary (Hit a) where
292 arbitrary = Hit <$> arbitrary
300instance Arbitrary a => Arbitrary (SearchHits a) where
301 arbitrary = sized $ \n -> resize (n `div` 2) $ do
302 tot <- getPositive <$> arbitrary
303 score <- arbitraryScore
305 return $ SearchHits tot score hs
307getSource :: EsResult a -> Maybe a
308getSource = fmap _source . foundResult
313 describe "index create/delete API" $ do
314 it "creates and then deletes the requested index" $ withTestEnv $ do
316 _ <- deleteExampleIndex
317 resp <- createExampleIndex
318 deleteResp <- deleteExampleIndex
320 validateStatus resp 200
321 validateStatus deleteResp 200
324 describe "document API" $ do
325 it "indexes, gets, and then deletes the generated document" $ withTestEnv $ do
327 docInserted <- getDocument testIndex testMapping (DocId "1")
328 let newTweet = eitherDecode
329 (responseBody docInserted) :: Either String (EsResult Tweet)
330 liftIO $ (fmap getSource newTweet `shouldBe` Right (Just exampleTweet))
332 it "produces a parseable result when looking up a bogus document" $ withTestEnv $ do
333 doc <- getDocument testIndex testMapping (DocId "bogus")
334 let noTweet = eitherDecode
335 (responseBody doc) :: Either String (EsResult Tweet)
336 liftIO $ fmap foundResult noTweet `shouldBe` Right Nothing
338 it "can use optimistic concurrency control" $ withTestEnv $ do
339 let ev = ExternalDocVersion minBound
340 let cfg = defaultIndexDocumentSettings { idsVersionControl = ExternalGT ev }
342 res <- insertData' cfg
343 liftIO $ isCreated res `shouldBe` True
344 res' <- insertData' cfg
345 liftIO $ isVersionConflict res' `shouldBe` True
347 it "indexes two documents in a parent/child relationship and checks that the child exists" $ withTestEnv $ do
349 _ <- putMapping testIndex (MappingName "parent") ParentMapping
350 _ <- putMapping testIndex (MappingName "child") ChildMapping
351 _ <- indexDocument testIndex (MappingName "parent") defaultIndexDocumentSettings exampleTweet (DocId "1")
352 let parent = (Just . DocumentParent . DocId) "1"
353 ids = IndexDocumentSettings NoVersionControl parent
354 _ <- indexDocument testIndex (MappingName "child") ids otherTweet (DocId "2")
355 _ <- refreshIndex testIndex
356 exists <- documentExists testIndex (MappingName "child") parent (DocId "2")
357 liftIO $ exists `shouldBe` True
359 describe "template API" $ do
360 it "can create a template" $ withTestEnv $ do
361 let idxTpl = IndexTemplate (TemplatePattern "tweet-*") (Just (IndexSettings (ShardCount 1) (ReplicaCount 1))) [toJSON TweetMapping]
362 resp <- putTemplate idxTpl (TemplateName "tweet-tpl")
363 liftIO $ validateStatus resp 200
365 it "can detect if a template exists" $ withTestEnv $ do
366 exists <- templateExists (TemplateName "tweet-tpl")
367 liftIO $ exists `shouldBe` True
369 it "can delete a template" $ withTestEnv $ do
370 resp <- deleteTemplate (TemplateName "tweet-tpl")
371 liftIO $ validateStatus resp 200
373 it "can detect if a template doesn't exist" $ withTestEnv $ do
374 exists <- templateExists (TemplateName "tweet-tpl")
375 liftIO $ exists `shouldBe` False
377 describe "bulk API" $ do
378 it "inserts all documents we request" $ withTestEnv $ do
380 let firstTest = BulkTest "blah"
381 let secondTest = BulkTest "bloo"
382 let firstDoc = BulkIndex testIndex
383 testMapping (DocId "2") (toJSON firstTest)
384 let secondDoc = BulkCreate testIndex
385 testMapping (DocId "3") (toJSON secondTest)
386 let stream = V.fromList [firstDoc, secondDoc]
388 _ <- refreshIndex testIndex
389 fDoc <- getDocument testIndex testMapping (DocId "2")
390 sDoc <- getDocument testIndex testMapping (DocId "3")
391 let maybeFirst = eitherDecode $ responseBody fDoc :: Either String (EsResult BulkTest)
392 let maybeSecond = eitherDecode $ responseBody sDoc :: Either String (EsResult BulkTest)
394 fmap getSource maybeFirst `shouldBe` Right (Just firstTest)
395 fmap getSource maybeSecond `shouldBe` Right (Just secondTest)
398 describe "query API" $ do
399 it "returns document for term query and identity filter" $ withTestEnv $ do
401 let query = TermQuery (Term "user" "bitemyapp") Nothing
402 let filter = IdentityFilter <&&> IdentityFilter
403 let search = mkSearch (Just query) (Just filter)
404 myTweet <- searchTweet search
406 myTweet `shouldBe` Right exampleTweet
408 it "returns document for terms query and identity filter" $ withTestEnv $ do
410 let query = TermsQuery (NE.fromList [(Term "user" "bitemyapp")])
411 let filter = IdentityFilter <&&> IdentityFilter
412 let search = mkSearch (Just query) (Just filter)
413 myTweet <- searchTweet search
415 myTweet `shouldBe` Right exampleTweet
417 it "returns document for match query" $ withTestEnv $ do
419 let query = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
420 let search = mkSearch (Just query) Nothing
421 myTweet <- searchTweet search
423 myTweet `shouldBe` Right exampleTweet
425 it "returns document for multi-match query" $ withTestEnv $ do
427 let flds = [FieldName "user", FieldName "message"]
428 let query = QueryMultiMatchQuery $ mkMultiMatchQuery flds (QueryString "bitemyapp")
429 let search = mkSearch (Just query) Nothing
430 myTweet <- searchTweet search
432 myTweet `shouldBe` Right exampleTweet
434 it "returns document for bool query" $ withTestEnv $ do
436 let innerQuery = QueryMatchQuery $
437 mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
438 let query = QueryBoolQuery $
439 mkBoolQuery [innerQuery] [] []
440 let search = mkSearch (Just query) Nothing
441 myTweet <- searchTweet search
443 myTweet `shouldBe` Right exampleTweet
445 it "returns document for boosting query" $ withTestEnv $ do
447 let posQuery = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
448 let negQuery = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "notmyapp")
449 let query = QueryBoostingQuery $ BoostingQuery posQuery negQuery (Boost 0.2)
450 let search = mkSearch (Just query) Nothing
451 myTweet <- searchTweet search
453 myTweet `shouldBe` Right exampleTweet
455 it "returns document for common terms query" $ withTestEnv $ do
457 let query = QueryCommonTermsQuery $
458 CommonTermsQuery (FieldName "user")
459 (QueryString "bitemyapp")
460 (CutoffFrequency 0.0001)
461 Or Or Nothing Nothing Nothing Nothing
462 let search = mkSearch (Just query) Nothing
463 myTweet <- searchTweet search
465 myTweet `shouldBe` Right exampleTweet
468 describe "sorting" $ do
469 it "returns documents in the right order" $ withTestEnv $ do
472 let sortSpec = DefaultSortSpec $ mkSort (FieldName "age") Ascending
473 let search = Search Nothing
474 (Just IdentityFilter) (Just [sortSpec]) Nothing Nothing
475 False (From 0) (Size 10) SearchTypeQueryThenFetch Nothing Nothing
476 result <- searchTweets search
477 let myTweet = fmap (hitSource . head . hits . searchHits) result
479 myTweet `shouldBe` Right otherTweet
482 describe "filtering API" $ do
483 it "returns document for composed boolmatch and identity" $ withTestEnv $ do
485 let queryFilter = BoolFilter (MustMatch (Term "user" "bitemyapp") False)
487 let search = mkSearch Nothing (Just queryFilter)
488 myTweet <- searchTweet search
490 myTweet `shouldBe` Right exampleTweet
492 it "returns document for term filter" $ withTestEnv $ do
494 let termFilter = TermFilter (Term "user" "bitemyapp") False
495 let search = mkSearch Nothing (Just termFilter)
496 myTweet <- searchTweet search
498 myTweet `shouldBe` Right exampleTweet
500 it "returns document for existential filter" $ withTestEnv $ do
502 let search = mkSearch Nothing (Just (ExistsFilter (FieldName "user")))
503 myTweet <- searchTweet search
505 myTweet `shouldBe` Right exampleTweet
507 it "returns document for geo boundingbox filter" $ withTestEnv $ do
509 let box = GeoBoundingBox (LatLon 40.73 (-74.1)) (LatLon 40.10 (-71.12))
510 let bbConstraint = GeoBoundingBoxConstraint (FieldName "tweet.location") box False GeoFilterMemory
511 let geoFilter = GeoBoundingBoxFilter bbConstraint
512 let search = mkSearch Nothing (Just geoFilter)
513 myTweet <- searchTweet search
515 myTweet `shouldBe` Right exampleTweet
517 it "doesn't return document for nonsensical boundingbox filter" $ withTestEnv $ do
519 let box = GeoBoundingBox (LatLon 0.73 (-4.1)) (LatLon 0.10 (-1.12))
520 let bbConstraint = GeoBoundingBoxConstraint (FieldName "tweet.location") box False GeoFilterMemory
521 let geoFilter = GeoBoundingBoxFilter bbConstraint
522 let search = mkSearch Nothing (Just geoFilter)
523 searchExpectNoResults search
525 it "returns document for geo distance filter" $ withTestEnv $ do
527 let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
528 let distance = Distance 10.0 Miles
529 let optimizeBbox = OptimizeGeoFilterType GeoFilterMemory
530 let geoFilter = GeoDistanceFilter geoPoint distance SloppyArc optimizeBbox False
531 let search = mkSearch Nothing (Just geoFilter)
532 myTweet <- searchTweet search
534 myTweet `shouldBe` Right exampleTweet
536 it "returns document for geo distance range filter" $ withTestEnv $ do
538 let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
539 let distanceRange = DistanceRange (Distance 0.0 Miles) (Distance 10.0 Miles)
540 let geoFilter = GeoDistanceRangeFilter geoPoint distanceRange
541 let search = mkSearch Nothing (Just geoFilter)
542 myTweet <- searchTweet search
544 myTweet `shouldBe` Right exampleTweet
546 it "doesn't return document for wild geo distance range filter" $ withTestEnv $ do
548 let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
549 let distanceRange = DistanceRange (Distance 100.0 Miles) (Distance 1000.0 Miles)
550 let geoFilter = GeoDistanceRangeFilter geoPoint distanceRange
551 let search = mkSearch Nothing (Just geoFilter)
552 searchExpectNoResults search
554 it "returns document for geo polygon filter" $ withTestEnv $ do
556 let points = [LatLon 40.0 (-70.00),
557 LatLon 40.0 (-72.00),
558 LatLon 41.0 (-70.00),
559 LatLon 41.0 (-72.00)]
560 let geoFilter = GeoPolygonFilter (FieldName "tweet.location") points
561 let search = mkSearch Nothing (Just geoFilter)
562 myTweet <- searchTweet search
564 myTweet `shouldBe` Right exampleTweet
566 it "doesn't return document for bad geo polygon filter" $ withTestEnv $ do
568 let points = [LatLon 40.0 (-70.00),
569 LatLon 40.0 (-71.00),
570 LatLon 41.0 (-70.00),
571 LatLon 41.0 (-71.00)]
572 let geoFilter = GeoPolygonFilter (FieldName "tweet.location") points
573 let search = mkSearch Nothing (Just geoFilter)
574 searchExpectNoResults search
576 it "returns document for ids filter" $ withTestEnv $ do
578 let filter = IdsFilter (MappingName "tweet") [DocId "1"]
579 let search = mkSearch Nothing (Just filter)
580 myTweet <- searchTweet search
582 myTweet `shouldBe` Right exampleTweet
584 it "returns document for Double range filter" $ withTestEnv $ do
586 let filter = RangeFilter (FieldName "age")
587 (RangeDoubleGtLt (GreaterThan 1000.0) (LessThan 100000.0))
588 RangeExecutionIndex False
589 let search = mkSearch Nothing (Just filter)
590 myTweet <- searchTweet search
592 myTweet `shouldBe` Right exampleTweet
594 it "returns document for UTCTime date filter" $ withTestEnv $ do
596 let filter = RangeFilter (FieldName "postDate")
598 (GreaterThanD (UTCTime
599 (ModifiedJulianDay 54000)
600 (secondsToDiffTime 0)))
602 (ModifiedJulianDay 55000)
603 (secondsToDiffTime 11))))
604 RangeExecutionIndex False
605 let search = mkSearch Nothing (Just filter)
606 myTweet <- searchTweet search
608 myTweet `shouldBe` Right exampleTweet
610 it "returns document for regexp filter" $ withTestEnv $ do
612 let filter = RegexpFilter (FieldName "user") (Regexp "bite.*app")
613 AllRegexpFlags (CacheName "test") False (CacheKey "key")
614 let search = mkSearch Nothing (Just filter)
615 myTweet <- searchTweet search
617 myTweet `shouldBe` Right exampleTweet
619 it "doesn't return document for non-matching regexp filter" $ withTestEnv $ do
621 let filter = RegexpFilter (FieldName "user")
622 (Regexp "boy") AllRegexpFlags
623 (CacheName "test") False (CacheKey "key")
624 let search = mkSearch Nothing (Just filter)
625 searchExpectNoResults search
627 it "returns document for query filter, uncached" $ withTestEnv $ do
629 let filter = QueryFilter (TermQuery (Term "user" "bitemyapp") Nothing) True
630 search = mkSearch Nothing (Just filter)
631 myTweet <- searchTweet search
632 liftIO $ myTweet `shouldBe` Right exampleTweet
634 it "returns document for query filter, cached" $ withTestEnv $ do
636 let filter = QueryFilter (TermQuery (Term "user" "bitemyapp") Nothing) False
637 search = mkSearch Nothing (Just filter)
638 myTweet <- searchTweet search
639 liftIO $ myTweet `shouldBe` Right exampleTweet
641 describe "Aggregation API" $ do
642 it "returns term aggregation results" $ withTestEnv $ do
644 let terms = TermsAgg $ mkTermsAggregation "user"
645 let search = mkAggregateSearch Nothing $ mkAggregations "users" terms
646 searchExpectAggs search
647 searchValidBucketAgg search "users" toTerms
649 it "can give collection hint parameters to term aggregations" $ when' (atleast es13) $ withTestEnv $ do
651 let terms = TermsAgg $ (mkTermsAggregation "user") { termCollectMode = Just BreadthFirst }
652 let search = mkAggregateSearch Nothing $ mkAggregations "users" terms
653 searchExpectAggs search
654 searchValidBucketAgg search "users" toTerms
656 it "can give execution hint parameters to term aggregations" $ when' (atmost es11) $ withTestEnv $ do
658 searchTermsAggHint [Map, Ordinals]
660 it "can give execution hint parameters to term aggregations" $ when' (is es12) $ withTestEnv $ do
662 searchTermsAggHint [GlobalOrdinals, GlobalOrdinalsHash, GlobalOrdinalsLowCardinality, Map, Ordinals]
664 it "can give execution hint parameters to term aggregations" $ when' (atleast es12) $ withTestEnv $ do
666 searchTermsAggHint [GlobalOrdinals, GlobalOrdinalsHash, GlobalOrdinalsLowCardinality, Map]
669 it "can execute value_count aggregations" $ withTestEnv $ do
672 let ags = mkAggregations "user_count" (ValueCountAgg (FieldValueCount (FieldName "user"))) <>
673 mkAggregations "bogus_count" (ValueCountAgg (FieldValueCount (FieldName "bogus")))
674 let search = mkAggregateSearch Nothing ags
675 let docCountPair k n = (k, object ["value" .= Number n])
676 res <- searchTweets search
678 fmap aggregations res `shouldBe` Right (Just (M.fromList [ docCountPair "user_count" 2
679 , docCountPair "bogus_count" 0
682 it "can execute filter aggregations" $ withTestEnv $ do
685 let ags = mkAggregations "bitemyapps" (FilterAgg (FilterAggregation (TermFilter (Term "user" "bitemyapp") defaultCache) Nothing)) <>
686 mkAggregations "notmyapps" (FilterAgg (FilterAggregation (TermFilter (Term "user" "notmyapp") defaultCache) Nothing))
687 let search = mkAggregateSearch Nothing ags
688 let docCountPair k n = (k, object ["doc_count" .= Number n])
689 res <- searchTweets search
691 fmap aggregations res `shouldBe` Right (Just (M.fromList [ docCountPair "bitemyapps" 1
692 , docCountPair "notmyapps" 1
694 -- Interaction of date serialization and date histogram aggregation is broken.
695 -- it "returns date histogram aggregation results" $ withTestEnv $ do
697 -- let histogram = DateHistogramAgg $ mkDateHistogram (FieldName "postDate") Minute
698 -- let search = mkAggregateSearch Nothing (mkAggregations "byDate" histogram)
699 -- searchExpectAggs search
700 -- searchValidBucketAgg search "byDate" toDateHistogram
702 -- it "returns date histogram using fractional date" $ withTestEnv $ do
704 -- let periods = [Year, Quarter, Month, Week, Day, Hour, Minute, Second]
705 -- let fractionals = map (FractionalInterval 1.5) [Weeks, Days, Hours, Minutes, Seconds]
706 -- let intervals = periods ++ fractionals
707 -- let histogram = mkDateHistogram (FieldName "postDate")
708 -- let search interval = mkAggregateSearch Nothing $ mkAggregations "byDate" $ DateHistogramAgg (histogram interval)
709 -- let expect interval = searchExpectAggs (search interval)
710 -- let valid interval = searchValidBucketAgg (search interval) "byDate" toDateHistogram
711 -- forM_ intervals expect
712 -- forM_ intervals valid
714 describe "Highlights API" $ do
716 it "returns highlight from query when there should be one" $ withTestEnv $ do
719 let query = QueryMatchQuery $ mkMatchQuery (FieldName "_all") (QueryString "haskell")
720 let testHighlight = Highlights Nothing [FieldHighlight (FieldName "message") Nothing]
722 let search = mkHighlightSearch (Just query) testHighlight
723 myHighlight <- searchTweetHighlight search
725 myHighlight `shouldBe` Right (Just (M.fromList [("message",["Use <em>haskell</em>!"])]))
727 it "doesn't return highlight from a query when it shouldn't" $ withTestEnv $ do
730 let query = QueryMatchQuery $ mkMatchQuery (FieldName "_all") (QueryString "haskell")
731 let testHighlight = Highlights Nothing [FieldHighlight (FieldName "user") Nothing]
733 let search = mkHighlightSearch (Just query) testHighlight
734 myHighlight <- searchTweetHighlight search
736 myHighlight `shouldBe` Right Nothing
738 describe "Source filtering" $ do
740 it "doesn't include source when sources are disabled" $ withTestEnv $ do
743 (Left "key \"_source\" not present")
745 it "includes a source" $ withTestEnv $ do
747 (SourcePatterns (PopPattern (Pattern "message")))
748 (Right (Object (HM.fromList [("message", String "Use haskell!")])))
750 it "includes sources" $ withTestEnv $ do
752 (SourcePatterns (PopPatterns [Pattern "user", Pattern "message"]))
753 (Right (Object (HM.fromList [("user",String "bitemyapp"),("message", String "Use haskell!")])))
755 it "includes source patterns" $ withTestEnv $ do
757 (SourcePatterns (PopPattern (Pattern "*ge")))
758 (Right (Object (HM.fromList [("age", Number 10000),("message", String "Use haskell!")])))
760 it "excludes source patterns" $ withTestEnv $ do
762 (SourceIncludeExclude (Include []) (Exclude [Pattern "l*", Pattern "*ge", Pattern "postDate"]))
763 (Right (Object (HM.fromList [("user",String "bitemyapp")])))
765 describe "ToJSON RegexpFlags" $ do
766 it "generates the correct JSON for AllRegexpFlags" $
767 toJSON AllRegexpFlags `shouldBe` String "ALL"
769 it "generates the correct JSON for NoRegexpFlags" $
770 toJSON NoRegexpFlags `shouldBe` String "NONE"
772 it "generates the correct JSON for SomeRegexpFlags" $
773 let flags = AnyString :| [ Automaton
778 in toJSON (SomeRegexpFlags flags) `shouldBe` String "ANYSTRING|AUTOMATON|COMPLEMENT|EMPTY|INTERSECTION|INTERVAL"
780 prop "removes duplicates from flags" $ \(flags :: RegexpFlags) ->
781 let String str = toJSON flags
782 flagStrs = T.splitOn "|" str
783 in noDuplicates flagStrs
785 describe "omitNulls" $ do
786 it "checks that omitNulls drops list elements when it should" $
787 let dropped = omitNulls $ [ "test1" .= (toJSON ([] :: [Int]))
788 , "test2" .= (toJSON ("some value" :: Text))]
789 in dropped `shouldBe` Object (HM.fromList [("test2", String "some value")])
791 it "checks that omitNulls doesn't drop list elements when it shouldn't" $
792 let notDropped = omitNulls $ [ "test1" .= (toJSON ([1] :: [Int]))
793 , "test2" .= (toJSON ("some value" :: Text))]
794 in notDropped `shouldBe` Object (HM.fromList [ ("test1", Array (V.fromList [Number 1.0]))
795 , ("test2", String "some value")])
796 it "checks that omitNulls drops non list elements when it should" $
797 let dropped = omitNulls $ [ "test1" .= (toJSON Null)
798 , "test2" .= (toJSON ("some value" :: Text))]
799 in dropped `shouldBe` Object (HM.fromList [("test2", String "some value")])
800 it "checks that omitNulls doesn't drop non list elements when it shouldn't" $
801 let notDropped = omitNulls $ [ "test1" .= (toJSON (1 :: Int))
802 , "test2" .= (toJSON ("some value" :: Text))]
803 in notDropped `shouldBe` Object (HM.fromList [ ("test1", Number 1.0)
804 , ("test2", String "some value")])
805 describe "Monoid (SearchHits a)" $ do
806 prop "abides the monoid laws" $ eq $
807 prop_Monoid (T :: T (SearchHits ()))
809 describe "mkDocVersion" $ do
810 prop "can never construct an out of range docVersion" $ \i ->
811 let res = mkDocVersion i
813 Nothing -> property True
814 Just dv -> (dv >= minBound) .&&.
815 (dv <= maxBound) .&&.
816 docVersionNumber dv === i
818 describe "Enum DocVersion" $ do
819 it "follows the laws of Enum, Bounded" $ do
820 evaluate (succ maxBound :: DocVersion) `shouldThrow` anyErrorCall
821 evaluate (pred minBound :: DocVersion) `shouldThrow` anyErrorCall
822 evaluate (toEnum 0 :: DocVersion) `shouldThrow` anyErrorCall
823 evaluate (toEnum 9200000000000000001 :: DocVersion) `shouldThrow` anyErrorCall
824 enumFrom (pred maxBound :: DocVersion) `shouldBe` [pred maxBound, maxBound]
825 enumFrom (pred maxBound :: DocVersion) `shouldBe` [pred maxBound, maxBound]
826 enumFromThen minBound (pred maxBound :: DocVersion) `shouldBe` [minBound, pred maxBound]
828 describe "scan&scroll API" $ do
829 it "returns documents using the scan&scroll API" $ withTestEnv $ do
832 let search = (mkSearch (Just $ MatchAllQuery Nothing) Nothing) { size = (Size 1) }
833 regular_search <- searchTweet search
834 scan_search' <- scanSearch testIndex testMapping search :: BH IO [Hit Tweet]
835 let scan_search = map hitSource scan_search'
837 regular_search `shouldBe` Right exampleTweet -- Check that the size restrtiction is being honored
839 scan_search `shouldMatchList` [exampleTweet, otherTweet]
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list