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 TweetMapping = TweetMapping deriving (Eq, Show)
121instance ToJSON TweetMapping where
122 toJSON TweetMapping =
124 object ["properties" .=
125 object [ "user" .= object ["type" .= ("string" :: Text)]
126 -- Serializing the date as a date is breaking other tests, mysteriously.
127 -- , "postDate" .= object [ "type" .= ("date" :: Text)
128 -- , "format" .= ("YYYY-MM-dd`T`HH:mm:ss.SSSZZ" :: Text)]
129 , "message" .= object ["type" .= ("string" :: Text)]
130 , "age" .= object ["type" .= ("integer" :: Text)]
131 , "location" .= object ["type" .= ("geo_point" :: Text)]
135exampleTweet = Tweet { user = "bitemyapp"
137 (ModifiedJulianDay 55000)
138 (secondsToDiffTime 10)
139 , message = "Use haskell!"
141 , location = Location 40.12 (-71.34) }
144otherTweet = Tweet { user = "notmyapp"
146 (ModifiedJulianDay 55000)
147 (secondsToDiffTime 11)
148 , message = "Use haskell!"
150 , location = Location 40.12 (-71.34) }
152resetIndex :: BH IO ()
154 _ <- deleteExampleIndex
155 _ <- createExampleIndex
156 _ <- putMapping testIndex testMapping TweetMapping
159insertData :: BH IO Reply
162 insertData' defaultIndexDocumentSettings
164insertData' :: IndexDocumentSettings -> BH IO Reply
166 r <- indexDocument testIndex testMapping ids exampleTweet (DocId "1")
167 _ <- refreshIndex testIndex
170insertOther :: BH IO ()
172 _ <- indexDocument testIndex testMapping defaultIndexDocumentSettings otherTweet (DocId "2")
173 _ <- refreshIndex testIndex
176searchTweet :: Search -> BH IO (Either String Tweet)
177searchTweet search = do
178 result <- searchTweets search
179 let myTweet = fmap (hitSource . head . hits . searchHits) result
182searchTweets :: Search -> BH IO (Either String (SearchResult Tweet))
183searchTweets search = eitherDecode . responseBody <$> searchByIndex testIndex search
185searchExpectNoResults :: Search -> BH IO ()
186searchExpectNoResults search = do
187 result <- searchTweets search
188 let emptyHits = fmap (hits . searchHits) result
190 emptyHits `shouldBe` Right []
192searchExpectAggs :: Search -> BH IO ()
193searchExpectAggs search = do
194 reply <- searchAll search
195 let isEmpty x = return (M.null x)
196 let result = decode (responseBody reply) :: Maybe (SearchResult Tweet)
198 (result >>= aggregations >>= isEmpty) `shouldBe` Just False
200searchValidBucketAgg :: (BucketAggregation a, FromJSON a, Show a) =>
201 Search -> Text -> (Text -> AggregationResults -> Maybe (Bucket a)) -> BH IO ()
202searchValidBucketAgg search aggKey extractor = do
203 reply <- searchAll search
204 let bucketDocs = docCount . head . buckets
205 let result = decode (responseBody reply) :: Maybe (SearchResult Tweet)
206 let count = result >>= aggregations >>= extractor aggKey >>= \x -> return (bucketDocs x)
208 count `shouldBe` Just 1
210searchTermsAggHint :: [ExecutionHint] -> BH IO ()
211searchTermsAggHint hints = do
212 let terms hint = TermsAgg $ (mkTermsAggregation "user") { termExecutionHint = Just hint }
213 let search hint = mkAggregateSearch Nothing $ mkAggregations "users" $ terms hint
214 forM_ hints $ searchExpectAggs . search
215 forM_ hints (\x -> searchValidBucketAgg (search x) "users" toTerms)
217searchTweetHighlight :: Search -> BH IO (Either String (Maybe HitHighlight))
218searchTweetHighlight search = do
219 result <- searchTweets search
220 let myHighlight = fmap (hitHighlight . head . hits . searchHits) result
223searchExpectSource :: Source -> Either String Value -> BH IO ()
224searchExpectSource src expected = do
226 let query = QueryMatchQuery $ mkMatchQuery (FieldName "_all") (QueryString "haskell")
227 let search = (mkSearch (Just query) Nothing) { source = Just src }
228 reply <- searchAll search
229 let result = eitherDecode (responseBody reply) :: Either String (SearchResult Value)
230 let value = fmap (hitSource . head . hits . searchHits) result
232 value `shouldBe` expected
234data BulkTest = BulkTest { name :: Text } deriving (Eq, Generic, Show)
235instance FromJSON BulkTest
236instance ToJSON BulkTest
238noDuplicates :: Eq a => [a] -> Bool
239noDuplicates xs = nub xs == xs
241instance Arbitrary RegexpFlags where
242 arbitrary = oneof [ pure AllRegexpFlags
244 , SomeRegexpFlags <$> arbitrary
247instance Arbitrary a => Arbitrary (NonEmpty a) where
248 arbitrary = liftA2 (:|) arbitrary arbitrary
250instance Arbitrary RegexpFlag where
251 arbitrary = oneof [ pure AnyString
259arbitraryScore :: Gen Score
260arbitraryScore = fmap getPositive <$> arbitrary
262instance Arbitrary Text where
263 arbitrary = T.pack <$> arbitrary
265instance (Arbitrary k, Arbitrary v, Ord k) => Arbitrary (M.Map k v) where
266 arbitrary = M.fromList <$> arbitrary
268instance Arbitrary IndexName where
269 arbitrary = IndexName <$> arbitrary
271instance Arbitrary MappingName where
272 arbitrary = MappingName <$> arbitrary
274instance Arbitrary DocId where
275 arbitrary = DocId <$> arbitrary
277instance Arbitrary a => Arbitrary (Hit a) where
278 arbitrary = Hit <$> arbitrary
286instance Arbitrary a => Arbitrary (SearchHits a) where
287 arbitrary = sized $ \n -> resize (n `div` 2) $ do
288 tot <- getPositive <$> arbitrary
289 score <- arbitraryScore
291 return $ SearchHits tot score hs
293getSource :: EsResult a -> Maybe a
294getSource = fmap _source . foundResult
299 describe "index create/delete API" $ do
300 it "creates and then deletes the requested index" $ withTestEnv $ do
302 _ <- deleteExampleIndex
303 resp <- createExampleIndex
304 deleteResp <- deleteExampleIndex
306 validateStatus resp 200
307 validateStatus deleteResp 200
310 describe "document API" $ do
311 it "indexes, gets, and then deletes the generated document" $ withTestEnv $ do
313 docInserted <- getDocument testIndex testMapping (DocId "1")
314 let newTweet = eitherDecode
315 (responseBody docInserted) :: Either String (EsResult Tweet)
316 liftIO $ (fmap getSource newTweet `shouldBe` Right (Just exampleTweet))
318 it "produces a parseable result when looking up a bogus document" $ withTestEnv $ do
319 doc <- getDocument testIndex testMapping (DocId "bogus")
320 let noTweet = eitherDecode
321 (responseBody doc) :: Either String (EsResult Tweet)
322 liftIO $ fmap foundResult noTweet `shouldBe` Right Nothing
324 it "can use optimistic concurrency control" $ withTestEnv $ do
325 let ev = ExternalDocVersion minBound
326 let cfg = defaultIndexDocumentSettings { idsVersionControl = ExternalGT ev }
328 res <- insertData' cfg
329 liftIO $ isCreated res `shouldBe` True
330 res' <- insertData' cfg
331 liftIO $ isVersionConflict res' `shouldBe` True
333 describe "template API" $ do
334 it "can create a template" $ withTestEnv $ do
335 let idxTpl = IndexTemplate (TemplatePattern "tweet-*") (Just (IndexSettings (ShardCount 1) (ReplicaCount 1))) [toJSON TweetMapping]
336 resp <- putTemplate idxTpl (TemplateName "tweet-tpl")
337 liftIO $ validateStatus resp 200
339 it "can detect if a template exists" $ withTestEnv $ do
340 exists <- templateExists (TemplateName "tweet-tpl")
341 liftIO $ exists `shouldBe` True
343 it "can delete a template" $ withTestEnv $ do
344 resp <- deleteTemplate (TemplateName "tweet-tpl")
345 liftIO $ validateStatus resp 200
347 it "can detect if a template doesn't exist" $ withTestEnv $ do
348 exists <- templateExists (TemplateName "tweet-tpl")
349 liftIO $ exists `shouldBe` False
351 describe "bulk API" $ do
352 it "inserts all documents we request" $ withTestEnv $ do
354 let firstTest = BulkTest "blah"
355 let secondTest = BulkTest "bloo"
356 let firstDoc = BulkIndex testIndex
357 testMapping (DocId "2") (toJSON firstTest)
358 let secondDoc = BulkCreate testIndex
359 testMapping (DocId "3") (toJSON secondTest)
360 let stream = V.fromList [firstDoc, secondDoc]
362 _ <- refreshIndex testIndex
363 fDoc <- getDocument testIndex testMapping (DocId "2")
364 sDoc <- getDocument testIndex testMapping (DocId "3")
365 let maybeFirst = eitherDecode $ responseBody fDoc :: Either String (EsResult BulkTest)
366 let maybeSecond = eitherDecode $ responseBody sDoc :: Either String (EsResult BulkTest)
368 fmap getSource maybeFirst `shouldBe` Right (Just firstTest)
369 fmap getSource maybeSecond `shouldBe` Right (Just secondTest)
372 describe "query API" $ do
373 it "returns document for term query and identity filter" $ withTestEnv $ do
375 let query = TermQuery (Term "user" "bitemyapp") Nothing
376 let filter = IdentityFilter <&&> IdentityFilter
377 let search = mkSearch (Just query) (Just filter)
378 myTweet <- searchTweet search
380 myTweet `shouldBe` Right exampleTweet
382 it "returns document for terms query and identity filter" $ withTestEnv $ do
384 let query = TermsQuery (NE.fromList [(Term "user" "bitemyapp")])
385 let filter = IdentityFilter <&&> IdentityFilter
386 let search = mkSearch (Just query) (Just filter)
387 myTweet <- searchTweet search
389 myTweet `shouldBe` Right exampleTweet
391 it "returns document for match query" $ withTestEnv $ do
393 let query = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
394 let search = mkSearch (Just query) Nothing
395 myTweet <- searchTweet search
397 myTweet `shouldBe` Right exampleTweet
399 it "returns document for multi-match query" $ withTestEnv $ do
401 let flds = [FieldName "user", FieldName "message"]
402 let query = QueryMultiMatchQuery $ mkMultiMatchQuery flds (QueryString "bitemyapp")
403 let search = mkSearch (Just query) Nothing
404 myTweet <- searchTweet search
406 myTweet `shouldBe` Right exampleTweet
408 it "returns document for bool query" $ withTestEnv $ do
410 let innerQuery = QueryMatchQuery $
411 mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
412 let query = QueryBoolQuery $
413 mkBoolQuery [innerQuery] [] []
414 let search = mkSearch (Just query) Nothing
415 myTweet <- searchTweet search
417 myTweet `shouldBe` Right exampleTweet
419 it "returns document for boosting query" $ withTestEnv $ do
421 let posQuery = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
422 let negQuery = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "notmyapp")
423 let query = QueryBoostingQuery $ BoostingQuery posQuery negQuery (Boost 0.2)
424 let search = mkSearch (Just query) Nothing
425 myTweet <- searchTweet search
427 myTweet `shouldBe` Right exampleTweet
429 it "returns document for common terms query" $ withTestEnv $ do
431 let query = QueryCommonTermsQuery $
432 CommonTermsQuery (FieldName "user")
433 (QueryString "bitemyapp")
434 (CutoffFrequency 0.0001)
435 Or Or Nothing Nothing Nothing Nothing
436 let search = mkSearch (Just query) Nothing
437 myTweet <- searchTweet search
439 myTweet `shouldBe` Right exampleTweet
442 describe "sorting" $ do
443 it "returns documents in the right order" $ withTestEnv $ do
446 let sortSpec = DefaultSortSpec $ mkSort (FieldName "age") Ascending
447 let search = Search Nothing
448 (Just IdentityFilter) (Just [sortSpec]) Nothing Nothing
449 False (From 0) (Size 10) SearchTypeQueryThenFetch Nothing Nothing
450 result <- searchTweets search
451 let myTweet = fmap (hitSource . head . hits . searchHits) result
453 myTweet `shouldBe` Right otherTweet
456 describe "filtering API" $ do
457 it "returns document for composed boolmatch and identity" $ withTestEnv $ do
459 let queryFilter = BoolFilter (MustMatch (Term "user" "bitemyapp") False)
461 let search = mkSearch Nothing (Just queryFilter)
462 myTweet <- searchTweet search
464 myTweet `shouldBe` Right exampleTweet
466 it "returns document for term filter" $ withTestEnv $ do
468 let termFilter = TermFilter (Term "user" "bitemyapp") False
469 let search = mkSearch Nothing (Just termFilter)
470 myTweet <- searchTweet search
472 myTweet `shouldBe` Right exampleTweet
474 it "returns document for existential filter" $ withTestEnv $ do
476 let search = mkSearch Nothing (Just (ExistsFilter (FieldName "user")))
477 myTweet <- searchTweet search
479 myTweet `shouldBe` Right exampleTweet
481 it "returns document for geo boundingbox filter" $ withTestEnv $ do
483 let box = GeoBoundingBox (LatLon 40.73 (-74.1)) (LatLon 40.10 (-71.12))
484 let bbConstraint = GeoBoundingBoxConstraint (FieldName "tweet.location") box False GeoFilterMemory
485 let geoFilter = GeoBoundingBoxFilter bbConstraint
486 let search = mkSearch Nothing (Just geoFilter)
487 myTweet <- searchTweet search
489 myTweet `shouldBe` Right exampleTweet
491 it "doesn't return document for nonsensical boundingbox filter" $ withTestEnv $ do
493 let box = GeoBoundingBox (LatLon 0.73 (-4.1)) (LatLon 0.10 (-1.12))
494 let bbConstraint = GeoBoundingBoxConstraint (FieldName "tweet.location") box False GeoFilterMemory
495 let geoFilter = GeoBoundingBoxFilter bbConstraint
496 let search = mkSearch Nothing (Just geoFilter)
497 searchExpectNoResults search
499 it "returns document for geo distance filter" $ withTestEnv $ do
501 let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
502 let distance = Distance 10.0 Miles
503 let optimizeBbox = OptimizeGeoFilterType GeoFilterMemory
504 let geoFilter = GeoDistanceFilter geoPoint distance SloppyArc optimizeBbox False
505 let search = mkSearch Nothing (Just geoFilter)
506 myTweet <- searchTweet search
508 myTweet `shouldBe` Right exampleTweet
510 it "returns document for geo distance range filter" $ withTestEnv $ do
512 let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
513 let distanceRange = DistanceRange (Distance 0.0 Miles) (Distance 10.0 Miles)
514 let geoFilter = GeoDistanceRangeFilter geoPoint distanceRange
515 let search = mkSearch Nothing (Just geoFilter)
516 myTweet <- searchTweet search
518 myTweet `shouldBe` Right exampleTweet
520 it "doesn't return document for wild geo distance range filter" $ withTestEnv $ do
522 let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
523 let distanceRange = DistanceRange (Distance 100.0 Miles) (Distance 1000.0 Miles)
524 let geoFilter = GeoDistanceRangeFilter geoPoint distanceRange
525 let search = mkSearch Nothing (Just geoFilter)
526 searchExpectNoResults search
528 it "returns document for geo polygon filter" $ withTestEnv $ do
530 let points = [LatLon 40.0 (-70.00),
531 LatLon 40.0 (-72.00),
532 LatLon 41.0 (-70.00),
533 LatLon 41.0 (-72.00)]
534 let geoFilter = GeoPolygonFilter (FieldName "tweet.location") points
535 let search = mkSearch Nothing (Just geoFilter)
536 myTweet <- searchTweet search
538 myTweet `shouldBe` Right exampleTweet
540 it "doesn't return document for bad geo polygon filter" $ withTestEnv $ do
542 let points = [LatLon 40.0 (-70.00),
543 LatLon 40.0 (-71.00),
544 LatLon 41.0 (-70.00),
545 LatLon 41.0 (-71.00)]
546 let geoFilter = GeoPolygonFilter (FieldName "tweet.location") points
547 let search = mkSearch Nothing (Just geoFilter)
548 searchExpectNoResults search
550 it "returns document for ids filter" $ withTestEnv $ do
552 let filter = IdsFilter (MappingName "tweet") [DocId "1"]
553 let search = mkSearch Nothing (Just filter)
554 myTweet <- searchTweet search
556 myTweet `shouldBe` Right exampleTweet
558 it "returns document for Double range filter" $ withTestEnv $ do
560 let filter = RangeFilter (FieldName "age")
561 (RangeDoubleGtLt (GreaterThan 1000.0) (LessThan 100000.0))
562 RangeExecutionIndex False
563 let search = mkSearch Nothing (Just filter)
564 myTweet <- searchTweet search
566 myTweet `shouldBe` Right exampleTweet
568 it "returns document for UTCTime date filter" $ withTestEnv $ do
570 let filter = RangeFilter (FieldName "postDate")
572 (GreaterThanD (UTCTime
573 (ModifiedJulianDay 54000)
574 (secondsToDiffTime 0)))
576 (ModifiedJulianDay 55000)
577 (secondsToDiffTime 11))))
578 RangeExecutionIndex False
579 let search = mkSearch Nothing (Just filter)
580 myTweet <- searchTweet search
582 myTweet `shouldBe` Right exampleTweet
584 it "returns document for regexp filter" $ withTestEnv $ do
586 let filter = RegexpFilter (FieldName "user") (Regexp "bite.*app")
587 AllRegexpFlags (CacheName "test") False (CacheKey "key")
588 let search = mkSearch Nothing (Just filter)
589 myTweet <- searchTweet search
591 myTweet `shouldBe` Right exampleTweet
593 it "doesn't return document for non-matching regexp filter" $ withTestEnv $ do
595 let filter = RegexpFilter (FieldName "user")
596 (Regexp "boy") AllRegexpFlags
597 (CacheName "test") False (CacheKey "key")
598 let search = mkSearch Nothing (Just filter)
599 searchExpectNoResults search
601 it "returns document for query filter, uncached" $ withTestEnv $ do
603 let filter = QueryFilter (TermQuery (Term "user" "bitemyapp") Nothing) True
604 search = mkSearch Nothing (Just filter)
605 myTweet <- searchTweet search
606 liftIO $ myTweet `shouldBe` Right exampleTweet
608 it "returns document for query filter, cached" $ withTestEnv $ do
610 let filter = QueryFilter (TermQuery (Term "user" "bitemyapp") Nothing) False
611 search = mkSearch Nothing (Just filter)
612 myTweet <- searchTweet search
613 liftIO $ myTweet `shouldBe` Right exampleTweet
615 describe "Aggregation API" $ do
616 it "returns term aggregation results" $ withTestEnv $ do
618 let terms = TermsAgg $ mkTermsAggregation "user"
619 let search = mkAggregateSearch Nothing $ mkAggregations "users" terms
620 searchExpectAggs search
621 searchValidBucketAgg search "users" toTerms
623 it "can give collection hint parameters to term aggregations" $ when' (atleast es13) $ withTestEnv $ do
625 let terms = TermsAgg $ (mkTermsAggregation "user") { termCollectMode = Just BreadthFirst }
626 let search = mkAggregateSearch Nothing $ mkAggregations "users" terms
627 searchExpectAggs search
628 searchValidBucketAgg search "users" toTerms
630 it "can give execution hint parameters to term aggregations" $ when' (atmost es11) $ withTestEnv $ do
632 searchTermsAggHint [Map, Ordinals]
634 it "can give execution hint parameters to term aggregations" $ when' (is es12) $ withTestEnv $ do
636 searchTermsAggHint [GlobalOrdinals, GlobalOrdinalsHash, GlobalOrdinalsLowCardinality, Map, Ordinals]
638 it "can give execution hint parameters to term aggregations" $ when' (atleast es12) $ withTestEnv $ do
640 searchTermsAggHint [GlobalOrdinals, GlobalOrdinalsHash, GlobalOrdinalsLowCardinality, Map]
643 it "can execute value_count aggregations" $ withTestEnv $ do
646 let ags = mkAggregations "user_count" (ValueCountAgg (FieldValueCount (FieldName "user"))) <>
647 mkAggregations "bogus_count" (ValueCountAgg (FieldValueCount (FieldName "bogus")))
648 let search = mkAggregateSearch Nothing ags
649 let docCountPair k n = (k, object ["value" .= Number n])
650 res <- searchTweets search
652 fmap aggregations res `shouldBe` Right (Just (M.fromList [ docCountPair "user_count" 2
653 , docCountPair "bogus_count" 0
656 it "can execute filter aggregations" $ withTestEnv $ do
659 let ags = mkAggregations "bitemyapps" (FilterAgg (FilterAggregation (TermFilter (Term "user" "bitemyapp") defaultCache) Nothing)) <>
660 mkAggregations "notmyapps" (FilterAgg (FilterAggregation (TermFilter (Term "user" "notmyapp") defaultCache) Nothing))
661 let search = mkAggregateSearch Nothing ags
662 let docCountPair k n = (k, object ["doc_count" .= Number n])
663 res <- searchTweets search
665 fmap aggregations res `shouldBe` Right (Just (M.fromList [ docCountPair "bitemyapps" 1
666 , docCountPair "notmyapps" 1
668 -- Interaction of date serialization and date histogram aggregation is broken.
669 -- it "returns date histogram aggregation results" $ withTestEnv $ do
671 -- let histogram = DateHistogramAgg $ mkDateHistogram (FieldName "postDate") Minute
672 -- let search = mkAggregateSearch Nothing (mkAggregations "byDate" histogram)
673 -- searchExpectAggs search
674 -- searchValidBucketAgg search "byDate" toDateHistogram
676 -- it "returns date histogram using fractional date" $ withTestEnv $ do
678 -- let periods = [Year, Quarter, Month, Week, Day, Hour, Minute, Second]
679 -- let fractionals = map (FractionalInterval 1.5) [Weeks, Days, Hours, Minutes, Seconds]
680 -- let intervals = periods ++ fractionals
681 -- let histogram = mkDateHistogram (FieldName "postDate")
682 -- let search interval = mkAggregateSearch Nothing $ mkAggregations "byDate" $ DateHistogramAgg (histogram interval)
683 -- let expect interval = searchExpectAggs (search interval)
684 -- let valid interval = searchValidBucketAgg (search interval) "byDate" toDateHistogram
685 -- forM_ intervals expect
686 -- forM_ intervals valid
688 describe "Highlights API" $ do
690 it "returns highlight from query when there should be one" $ withTestEnv $ do
693 let query = QueryMatchQuery $ mkMatchQuery (FieldName "_all") (QueryString "haskell")
694 let testHighlight = Highlights Nothing [FieldHighlight (FieldName "message") Nothing]
696 let search = mkHighlightSearch (Just query) testHighlight
697 myHighlight <- searchTweetHighlight search
699 myHighlight `shouldBe` Right (Just (M.fromList [("message",["Use <em>haskell</em>!"])]))
701 it "doesn't return highlight from a query when it shouldn't" $ withTestEnv $ do
704 let query = QueryMatchQuery $ mkMatchQuery (FieldName "_all") (QueryString "haskell")
705 let testHighlight = Highlights Nothing [FieldHighlight (FieldName "user") Nothing]
707 let search = mkHighlightSearch (Just query) testHighlight
708 myHighlight <- searchTweetHighlight search
710 myHighlight `shouldBe` Right Nothing
712 describe "Source filtering" $ do
714 it "doesn't include source when sources are disabled" $ withTestEnv $ do
717 (Left "key \"_source\" not present")
719 it "includes a source" $ withTestEnv $ do
721 (SourcePatterns (PopPattern (Pattern "message")))
722 (Right (Object (HM.fromList [("message", String "Use haskell!")])))
724 it "includes sources" $ withTestEnv $ do
726 (SourcePatterns (PopPatterns [Pattern "user", Pattern "message"]))
727 (Right (Object (HM.fromList [("user",String "bitemyapp"),("message", String "Use haskell!")])))
729 it "includes source patterns" $ withTestEnv $ do
731 (SourcePatterns (PopPattern (Pattern "*ge")))
732 (Right (Object (HM.fromList [("age", Number 10000),("message", String "Use haskell!")])))
734 it "excludes source patterns" $ withTestEnv $ do
736 (SourceIncludeExclude (Include []) (Exclude [Pattern "l*", Pattern "*ge", Pattern "postDate"]))
737 (Right (Object (HM.fromList [("user",String "bitemyapp")])))
739 describe "ToJSON RegexpFlags" $ do
740 it "generates the correct JSON for AllRegexpFlags" $
741 toJSON AllRegexpFlags `shouldBe` String "ALL"
743 it "generates the correct JSON for NoRegexpFlags" $
744 toJSON NoRegexpFlags `shouldBe` String "NONE"
746 it "generates the correct JSON for SomeRegexpFlags" $
747 let flags = AnyString :| [ Automaton
752 in toJSON (SomeRegexpFlags flags) `shouldBe` String "ANYSTRING|AUTOMATON|COMPLEMENT|EMPTY|INTERSECTION|INTERVAL"
754 prop "removes duplicates from flags" $ \(flags :: RegexpFlags) ->
755 let String str = toJSON flags
756 flagStrs = T.splitOn "|" str
757 in noDuplicates flagStrs
759 describe "omitNulls" $ do
760 it "checks that omitNulls drops list elements when it should" $
761 let dropped = omitNulls $ [ "test1" .= (toJSON ([] :: [Int]))
762 , "test2" .= (toJSON ("some value" :: Text))]
763 in dropped `shouldBe` Object (HM.fromList [("test2", String "some value")])
765 it "checks that omitNulls doesn't drop list elements when it shouldn't" $
766 let notDropped = omitNulls $ [ "test1" .= (toJSON ([1] :: [Int]))
767 , "test2" .= (toJSON ("some value" :: Text))]
768 in notDropped `shouldBe` Object (HM.fromList [ ("test1", Array (V.fromList [Number 1.0]))
769 , ("test2", String "some value")])
770 it "checks that omitNulls drops non list elements when it should" $
771 let dropped = omitNulls $ [ "test1" .= (toJSON Null)
772 , "test2" .= (toJSON ("some value" :: Text))]
773 in dropped `shouldBe` Object (HM.fromList [("test2", String "some value")])
774 it "checks that omitNulls doesn't drop non list elements when it shouldn't" $
775 let notDropped = omitNulls $ [ "test1" .= (toJSON (1 :: Int))
776 , "test2" .= (toJSON ("some value" :: Text))]
777 in notDropped `shouldBe` Object (HM.fromList [ ("test1", Number 1.0)
778 , ("test2", String "some value")])
779 describe "Monoid (SearchHits a)" $ do
780 prop "abides the monoid laws" $ eq $
781 prop_Monoid (T :: T (SearchHits ()))
783 describe "mkDocVersion" $ do
784 prop "can never construct an out of range docVersion" $ \i ->
785 let res = mkDocVersion i
787 Nothing -> property True
788 Just dv -> (dv >= minBound) .&&.
789 (dv <= maxBound) .&&.
790 docVersionNumber dv === i
792 describe "Enum DocVersion" $ do
793 it "follows the laws of Enum, Bounded" $ do
794 evaluate (succ maxBound :: DocVersion) `shouldThrow` anyErrorCall
795 evaluate (pred minBound :: DocVersion) `shouldThrow` anyErrorCall
796 evaluate (toEnum 0 :: DocVersion) `shouldThrow` anyErrorCall
797 evaluate (toEnum 9200000000000000001 :: DocVersion) `shouldThrow` anyErrorCall
798 enumFrom (pred maxBound :: DocVersion) `shouldBe` [pred maxBound, maxBound]
799 enumFrom (pred maxBound :: DocVersion) `shouldBe` [pred maxBound, maxBound]
800 enumFromThen minBound (pred maxBound :: DocVersion) `shouldBe` [minBound, pred maxBound]
802 describe "scan&scroll API" $ do
803 it "returns documents using the scan&scroll API" $ withTestEnv $ do
806 let search = (mkSearch (Just $ MatchAllQuery Nothing) Nothing) { size = (Size 1) }
807 regular_search <- searchTweet search
808 scan_search' <- scanSearch testIndex testMapping search :: BH IO [Hit Tweet]
809 let scan_search = map hitSource scan_search'
811 regular_search `shouldBe` Right exampleTweet -- Check that the size restrtiction is being honored
813 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