git.iko.soy/typeable/bloodhound
on master help sign in
Modularized V5 test suite
commit103aac64319a0eea82067a758a24e38d30dfaac2 authorChris Allen <cma@bitemyapp.com> dateThu 8 Feb 2018, 19:34 (9 years ago) parent87d539d
17 files changed+1342−1179
bloodhound.cabal+15−2 tests/V5/Test/Aggregation.hs+147 tests/V5/Test/BulkAPI.hs+95 tests/V5/Test/Common.hs+288 tests/V5/Test/Documents.hs+51 tests/V5/Test/Highlights.hs+34 tests/V5/Test/Import.hs+27−1 tests/V5/Test/Indices.hs+152 tests/V5/Test/JSON.hs+47−8 tests/V5/Test/Query.hs+115 tests/V5/Test/Script.hs+35 tests/V5/Test/Snapshots.hs+201 tests/V5/Test/Sorting.hs+22 tests/V5/Test/SourceFiltering.hs+38 tests/V5/Test/Suggest.hs+23 tests/V5/Test/Templates.hs+26 tests/V5/tests.hs+26−1168
bloodhound.cabal+15−2
@@ -7,7 +7,7 @@ license: BSD3
77license-file: LICENSE
88author: Chris Allen
99maintainer: cma@bitemyapp.com
10−copyright: 2015, Chris Allen
10+copyright: 2018 Chris Allen
1111category: Database, Search
1212build-type: Simple
1313cabal-version: >=1.10
@@ -81,10 +81,23 @@ test-suite bloodhound-tests
8181 hs-source-dirs: tests/V1
8282 else
8383 hs-source-dirs: tests/V5
84− other-modules: Test.ApproxEq
84+ other-modules: Test.Aggregation
85+ Test.ApproxEq
86+ Test.BulkAPI
87+ Test.Common
88+ Test.Documents
8589 Test.Generators
90+ Test.Highlights
8691 Test.Import
92+ Test.Indices
8793 Test.JSON
94+ Test.Query
95+ Test.Script
96+ Test.Snapshots
97+ Test.Sorting
98+ Test.SourceFiltering
99+ Test.Suggest
100+ Test.Templates
88101 build-depends: base,
89102 bloodhound,
90103 bytestring,
tests/V5/Test/Aggregation.hsnew file+147
@@ -0,0 +1,147 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+module Test.Aggregation where
4+
5+import Test.Common
6+import Test.Import
7+
8+import Control.Error (fmapL, note)
9+import qualified Data.Map as M
10+import qualified Database.V5.Bloodhound
11+
12+spec :: Spec
13+spec =
14+ describe "Aggregation API" $ do
15+ it "returns term aggregation results" $ withTestEnv $ do
16+ _ <- insertData
17+ let terms = TermsAgg $ mkTermsAggregation "user"
18+ let search = mkAggregateSearch Nothing $ mkAggregations "users" terms
19+ searchExpectAggs search
20+ searchValidBucketAgg search "users" toTerms
21+
22+ it "return sub-aggregation results" $ withTestEnv $ do
23+ _ <- insertData
24+ let subaggs = mkAggregations "age_agg" . TermsAgg $ mkTermsAggregation "age"
25+ agg = TermsAgg $ (mkTermsAggregation "user") { termAggs = Just subaggs}
26+ search = mkAggregateSearch Nothing $ mkAggregations "users" agg
27+ reply <- searchByIndex testIndex search
28+ let result = decode (responseBody reply) :: Maybe (SearchResult Tweet)
29+ usersAggResults = result >>= aggregations >>= toTerms "users"
30+ subAggResults = usersAggResults >>= (listToMaybe . buckets) >>= termsAggs >>= toTerms "age_agg"
31+ subAddResultsExists = isJust subAggResults
32+ liftIO $ (subAddResultsExists) `shouldBe` True
33+
34+ it "returns cardinality aggregation results" $ withTestEnv $ do
35+ _ <- insertData
36+ let cardinality = CardinalityAgg $ mkCardinalityAggregation $ FieldName "user"
37+ let search = mkAggregateSearch Nothing $ mkAggregations "users" cardinality
38+ let search' = search { Database.V5.Bloodhound.from = From 0, size = Size 0 }
39+ searchExpectAggs search'
40+ let docCountPair k n = (k, object ["value" .= Number n])
41+ res <- searchTweets search'
42+ liftIO $
43+ fmap aggregations res `shouldBe` Right (Just (M.fromList [ docCountPair "users" 1]))
44+
45+ it "returns stats aggregation results" $ withTestEnv $ do
46+ _ <- insertData
47+ let stats = StatsAgg $ mkStatsAggregation $ FieldName "age"
48+ let search = mkAggregateSearch Nothing $ mkAggregations "users" stats
49+ let search' = search { Database.V5.Bloodhound.from = From 0, size = Size 0 }
50+ searchExpectAggs search'
51+ let statsAggRes k n = (k, object [ "max" .= Number n
52+ , "avg" .= Number n
53+ , "count" .= Number 1
54+ , "min" .= Number n
55+ , "sum" .= Number n])
56+ res <- searchTweets search'
57+ liftIO $
58+ fmap aggregations res `shouldBe` Right (Just (M.fromList [ statsAggRes "users" 10000]))
59+
60+ it "can give collection hint parameters to term aggregations" $ when' (atleast es13) $ withTestEnv $ do
61+ _ <- insertData
62+ let terms = TermsAgg $ (mkTermsAggregation "user") { termCollectMode = Just BreadthFirst }
63+ let search = mkAggregateSearch Nothing $ mkAggregations "users" terms
64+ searchExpectAggs search
65+ searchValidBucketAgg search "users" toTerms
66+
67+ -- One of these fails with 1.7.3
68+ it "can give execution hint parameters to term aggregations" $ when' (atmost es11) $ withTestEnv $ do
69+ _ <- insertData
70+ searchTermsAggHint [Map, Ordinals]
71+
72+ it "can give execution hint parameters to term aggregations" $ when' (is es12) $ withTestEnv $ do
73+ _ <- insertData
74+ searchTermsAggHint [GlobalOrdinals, GlobalOrdinalsHash, GlobalOrdinalsLowCardinality, Map, Ordinals]
75+
76+ it "can give execution hint parameters to term aggregations" $ when' (atleast es12) $ withTestEnv $ do
77+ _ <- insertData
78+ searchTermsAggHint [GlobalOrdinals, GlobalOrdinalsHash, GlobalOrdinalsLowCardinality, Map]
79+ -- One of the above.
80+
81+ it "can execute value_count aggregations" $ withTestEnv $ do
82+ _ <- insertData
83+ _ <- insertOther
84+ let ags = mkAggregations "user_count" (ValueCountAgg (FieldValueCount (FieldName "user"))) <>
85+ mkAggregations "bogus_count" (ValueCountAgg (FieldValueCount (FieldName "bogus")))
86+ let search = mkAggregateSearch Nothing ags
87+ let docCountPair k n = (k, object ["value" .= Number n])
88+ res <- searchTweets search
89+ liftIO $
90+ fmap aggregations res `shouldBe` Right (Just (M.fromList [ docCountPair "user_count" 2
91+ , docCountPair "bogus_count" 0
92+ ]))
93+
94+ it "can execute date_range aggregations" $ withTestEnv $ do
95+ let now = fromGregorian 2015 3 14
96+ let ltAMonthAgo = UTCTime (fromGregorian 2015 3 1) 0
97+ let ltAWeekAgo = UTCTime (fromGregorian 2015 3 10) 0
98+ let oldDoc = exampleTweet { postDate = ltAMonthAgo }
99+ let newDoc = exampleTweet { postDate = ltAWeekAgo }
100+ _ <- indexDocument testIndex testMapping defaultIndexDocumentSettings oldDoc (DocId "1")
101+ _ <- indexDocument testIndex testMapping defaultIndexDocumentSettings newDoc (DocId "2")
102+ _ <- refreshIndex testIndex
103+ let thisMonth = DateRangeFrom (DateMathExpr (DMDate now) [SubtractTime 1 DMMonth])
104+ let thisWeek = DateRangeFrom (DateMathExpr (DMDate now) [SubtractTime 1 DMWeek])
105+ let agg = DateRangeAggregation (FieldName "postDate") Nothing (thisMonth :| [thisWeek])
106+ let ags = mkAggregations "date_ranges" (DateRangeAgg agg)
107+ let search = mkAggregateSearch Nothing ags
108+ res <- searchTweets search
109+ liftIO $ hitsTotal . searchHits <$> res `shouldBe` Right 2
110+ let bucks = do magrs <- fmapL show (aggregations <$> res)
111+ agrs <- note "no aggregations returned" magrs
112+ rawBucks <- note "no date_ranges aggregation" $ M.lookup "date_ranges" agrs
113+ parseEither parseJSON rawBucks
114+ let fromMonthT = UTCTime (fromGregorian 2015 2 14) 0
115+ let fromWeekT = UTCTime (fromGregorian 2015 3 7) 0
116+ liftIO $ buckets <$> bucks `shouldBe` Right [ DateRangeResult "2015-02-14T00:00:00.000Z-*"
117+ (Just fromMonthT)
118+ (Just "2015-02-14T00:00:00.000Z")
119+ Nothing
120+ Nothing
121+ 2
122+ Nothing
123+ , DateRangeResult "2015-03-07T00:00:00.000Z-*"
124+ (Just fromWeekT)
125+ (Just "2015-03-07T00:00:00.000Z")
126+ Nothing
127+ Nothing
128+ 1
129+ Nothing
130+ ]
131+
132+ it "returns date histogram aggregation results" $ withTestEnv $ do
133+ _ <- insertData
134+ let histogram = DateHistogramAgg $ mkDateHistogram (FieldName "postDate") Minute
135+ let search = mkAggregateSearch Nothing (mkAggregations "byDate" histogram)
136+ searchExpectAggs search
137+ searchValidBucketAgg search "byDate" toDateHistogram
138+
139+ it "can execute missing aggregations" $ withTestEnv $ do
140+ _ <- insertData
141+ _ <- insertExtra
142+ let ags = mkAggregations "missing_agg" (MissingAgg (MissingAggregation "extra"))
143+ let search = mkAggregateSearch Nothing ags
144+ let docCountPair k n = (k, object ["doc_count" .= Number n])
145+ res <- searchTweets search
146+ liftIO $
147+ fmap aggregations res `shouldBe` Right (Just (M.fromList [docCountPair "missing_agg" 1]))
tests/V5/Test/BulkAPI.hsnew file+95
@@ -0,0 +1,95 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+module Test.BulkAPI where
4+
5+import Test.Common
6+import Test.Import
7+
8+import qualified Data.Vector as V
9+import qualified Lens.Micro.Aeson as LMA
10+
11+newtype BulkTest =
12+ BulkTest { name :: Text }
13+ deriving (Eq, Show)
14+
15+instance ToJSON BulkTest where
16+ toJSON (BulkTest name') =
17+ object ["name" .= name']
18+
19+instance FromJSON BulkTest where
20+ parseJSON = withObject "BulkTest" parse
21+ where
22+ parse o = do
23+ t <- o .: "name"
24+ BulkTest <$> parseJSON t
25+
26+spec :: Spec
27+spec =
28+ describe "Bulk API" $ do
29+ it "inserts all documents we request" $ withTestEnv $ do
30+ _ <- insertData
31+ let firstTest = BulkTest "blah"
32+ let secondTest = BulkTest "bloo"
33+ let thirdTest = BulkTest "graffle"
34+ let fourthTest = BulkTest "garabadoo"
35+ let fifthTest = BulkTest "serenity"
36+ let firstDoc = BulkIndex testIndex
37+ testMapping (DocId "2") (toJSON firstTest)
38+ let secondDoc = BulkCreate testIndex
39+ testMapping (DocId "3") (toJSON secondTest)
40+ let thirdDoc = BulkCreateEncoding testIndex
41+ testMapping (DocId "4") (toEncoding thirdTest)
42+ let fourthDoc = BulkIndexAuto testIndex
43+ testMapping (toJSON fourthTest)
44+ let fifthDoc = BulkIndexEncodingAuto testIndex
45+ testMapping (toEncoding fifthTest)
46+ let stream = V.fromList [firstDoc, secondDoc, thirdDoc, fourthDoc, fifthDoc]
47+ _ <- bulk stream
48+ -- liftIO $ pPrint bulkResp
49+ _ <- refreshIndex testIndex
50+ -- liftIO $ pPrint refreshResp
51+ fDoc <- getDocument testIndex testMapping (DocId "2")
52+ sDoc <- getDocument testIndex testMapping (DocId "3")
53+ tDoc <- getDocument testIndex testMapping (DocId "4")
54+ -- note that we cannot query for fourthDoc and fifthDoc since we
55+ -- do not know their autogenerated ids.
56+ let maybeFirst =
57+ eitherDecode
58+ $ responseBody fDoc
59+ :: Either String (EsResult BulkTest)
60+ let maybeSecond =
61+ eitherDecode
62+ $ responseBody sDoc
63+ :: Either String (EsResult BulkTest)
64+ let maybeThird =
65+ eitherDecode
66+ $ responseBody tDoc
67+ :: Either String (EsResult BulkTest)
68+ -- liftIO $ pPrint [maybeFirst, maybeSecond, maybeThird]
69+ liftIO $ do
70+ fmap getSource maybeFirst `shouldBe` Right (Just firstTest)
71+ fmap getSource maybeSecond `shouldBe` Right (Just secondTest)
72+ fmap getSource maybeThird `shouldBe` Right (Just thirdTest)
73+ -- Since we can't get the docs by doc id, we check for their existence in
74+ -- a match all query.
75+ let query = MatchAllQuery Nothing
76+ let search = mkSearch (Just query) Nothing
77+ resp <- searchByIndex testIndex search
78+ parsed <- parseEsResponse resp :: BH IO (Either EsError (SearchResult Value))
79+ case parsed of
80+ Left e ->
81+ liftIO $ expectationFailure ("Expected a script-transformed result but got: " <> show e)
82+ (Right sr) -> do
83+ liftIO $
84+ hitsTotal (searchHits sr) `shouldBe` 6
85+ let nameList :: [Text]
86+ nameList =
87+ (hits (searchHits sr))
88+ ^.. traverse
89+ . to hitSource
90+ . _Just
91+ . LMA.key "name"
92+ . _String
93+ liftIO $
94+ nameList
95+ `shouldBe` ["blah","bloo","graffle","garabadoo","serenity"]
tests/V5/Test/Common.hsnew file+288
@@ -0,0 +1,288 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+{-# LANGUAGE TemplateHaskell #-}
3+
4+module Test.Common where
5+
6+import Test.Import
7+
8+import qualified Data.Map as M
9+import qualified Data.Version as Vers
10+import qualified Network.HTTP.Types.Status as NHTS
11+
12+testServer :: Server
13+testServer = Server "http://localhost:9200"
14+testIndex :: IndexName
15+testIndex = IndexName "bloodhound-tests-twitter-1"
16+testMapping :: MappingName
17+testMapping = MappingName "tweet"
18+
19+withTestEnv :: BH IO a -> IO a
20+withTestEnv = withBH defaultManagerSettings testServer
21+
22+data Location = Location { lat :: Double
23+ , lon :: Double } deriving (Eq, Show)
24+
25+data Tweet = Tweet { user :: Text
26+ , postDate :: UTCTime
27+ , message :: Text
28+ , age :: Int
29+ , location :: Location
30+ , extra :: Maybe Text }
31+ deriving (Eq, Show)
32+
33+$(deriveJSON defaultOptions ''Location)
34+$(deriveJSON defaultOptions ''Tweet)
35+
36+data ParentMapping = ParentMapping deriving (Eq, Show)
37+
38+instance ToJSON ParentMapping where
39+ toJSON ParentMapping =
40+ object ["properties" .=
41+ object [ "user" .= object ["type" .= ("string" :: Text)
42+ , "fielddata" .= True
43+ ]
44+ -- Serializing the date as a date is breaking other tests, mysteriously.
45+ -- , "postDate" .= object [ "type" .= ("date" :: Text)
46+ -- , "format" .= ("YYYY-MM-dd`T`HH:mm:ss.SSSZZ" :: Text)]
47+ , "message" .= object ["type" .= ("string" :: Text)]
48+ , "age" .= object ["type" .= ("integer" :: Text)]
49+ , "location" .= object ["type" .= ("geo_point" :: Text)]
50+ , "extra" .= object ["type" .= ("keyword" :: Text)]
51+ ]]
52+
53+es13 :: Vers.Version
54+es13 = Vers.Version [1, 3, 0] []
55+
56+es12 :: Vers.Version
57+es12 = Vers.Version [1, 2, 0] []
58+
59+es11 :: Vers.Version
60+es11 = Vers.Version [1, 1, 0] []
61+
62+es14 :: Vers.Version
63+es14 = Vers.Version [1, 4, 0] []
64+
65+es15 :: Vers.Version
66+es15 = Vers.Version [1, 5, 0] []
67+
68+es16 :: Vers.Version
69+es16 = Vers.Version [1, 6, 0] []
70+
71+es20 :: Vers.Version
72+es20 = Vers.Version [2, 0, 0] []
73+
74+es50 :: Vers.Version
75+es50 = Vers.Version [5, 0, 0] []
76+
77+getServerVersion :: IO (Maybe Vers.Version)
78+getServerVersion = fmap extractVersion <$> withTestEnv getStatus
79+ where
80+ extractVersion = versionNumber . number . version
81+
82+createExampleIndex :: (MonadBH m) => m Reply
83+createExampleIndex =
84+ createIndex (IndexSettings (ShardCount 1) (ReplicaCount 0)) testIndex
85+
86+deleteExampleIndex :: (MonadBH m) => m Reply
87+deleteExampleIndex =
88+ deleteIndex testIndex
89+
90+validateStatus :: Show body => Response body -> Int -> Expectation
91+validateStatus resp expected =
92+ if actual == expected
93+ then return ()
94+ else expectationFailure ("Expected " <> show expected <> " but got " <> show actual <> ": " <> show body)
95+ where
96+ actual = NHTS.statusCode (responseStatus resp)
97+ body = responseBody resp
98+
99+data ChildMapping = ChildMapping deriving (Eq, Show)
100+
101+instance ToJSON ChildMapping where
102+ toJSON ChildMapping =
103+ object ["_parent" .= object ["type" .= ("parent" :: Text)]
104+ , "properties" .=
105+ object [ "user" .= object ["type" .= ("string" :: Text)
106+ , "fielddata" .= True
107+ ]
108+ -- Serializing the date as a date is breaking other tests, mysteriously.
109+ -- , "postDate" .= object [ "type" .= ("date" :: Text)
110+ -- , "format" .= ("YYYY-MM-dd`T`HH:mm:ss.SSSZZ" :: Text)]
111+ , "message" .= object ["type" .= ("string" :: Text)]
112+ , "age" .= object ["type" .= ("integer" :: Text)]
113+ , "location" .= object ["type" .= ("geo_point" :: Text)]
114+ , "extra" .= object ["type" .= ("keyword" :: Text)]
115+ ]]
116+
117+data TweetMapping = TweetMapping deriving (Eq, Show)
118+
119+instance ToJSON TweetMapping where
120+ toJSON TweetMapping =
121+ object ["tweet" .=
122+ object ["properties" .=
123+ object [ "user" .= object [ "type" .= ("string" :: Text)
124+ , "fielddata" .= True
125+ ]
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)]
132+ , "extra" .= object ["type" .= ("keyword" :: Text)]
133+ ]]]
134+
135+exampleTweet :: Tweet
136+exampleTweet = Tweet { user = "bitemyapp"
137+ , postDate = UTCTime
138+ (ModifiedJulianDay 55000)
139+ (secondsToDiffTime 10)
140+ , message = "Use haskell!"
141+ , age = 10000
142+ , location = Location 40.12 (-71.34)
143+ , extra = Nothing }
144+
145+tweetWithExtra :: Tweet
146+tweetWithExtra = Tweet { user = "bitemyapp"
147+ , postDate = UTCTime
148+ (ModifiedJulianDay 55000)
149+ (secondsToDiffTime 10)
150+ , message = "Use haskell!"
151+ , age = 10000
152+ , location = Location 40.12 (-71.34)
153+ , extra = Just "blah blah" }
154+
155+newAge :: Int
156+newAge = 31337
157+
158+newUser :: Text
159+newUser = "someotherapp"
160+
161+tweetPatch :: Value
162+tweetPatch =
163+ object [ "age" .= newAge
164+ , "user" .= newUser
165+ ]
166+
167+patchedTweet :: Tweet
168+patchedTweet = exampleTweet{age = newAge, user = newUser}
169+
170+otherTweet :: Tweet
171+otherTweet = Tweet { user = "notmyapp"
172+ , postDate = UTCTime
173+ (ModifiedJulianDay 55000)
174+ (secondsToDiffTime 11)
175+ , message = "Use haskell!"
176+ , age = 1000
177+ , location = Location 40.12 (-71.34)
178+ , extra = Nothing }
179+
180+resetIndex :: BH IO ()
181+resetIndex = do
182+ _ <- deleteExampleIndex
183+ _ <- createExampleIndex
184+ _ <- putMapping testIndex testMapping TweetMapping
185+ return ()
186+
187+insertData :: BH IO Reply
188+insertData = do
189+ resetIndex
190+ insertData' defaultIndexDocumentSettings
191+
192+insertData' :: IndexDocumentSettings -> BH IO Reply
193+insertData' ids = do
194+ r <- indexDocument testIndex testMapping ids exampleTweet (DocId "1")
195+ _ <- refreshIndex testIndex
196+ return r
197+
198+updateData :: BH IO Reply
199+updateData = do
200+ r <- updateDocument testIndex testMapping defaultIndexDocumentSettings tweetPatch (DocId "1")
201+ _ <- refreshIndex testIndex
202+ return r
203+
204+insertOther :: BH IO ()
205+insertOther = do
206+ _ <- indexDocument testIndex testMapping defaultIndexDocumentSettings otherTweet (DocId "2")
207+ _ <- refreshIndex testIndex
208+ return ()
209+
210+insertExtra :: BH IO ()
211+insertExtra = do
212+ _ <- indexDocument testIndex testMapping defaultIndexDocumentSettings tweetWithExtra (DocId "4")
213+ _ <- refreshIndex testIndex
214+ return ()
215+
216+insertWithSpaceInId :: BH IO ()
217+insertWithSpaceInId = do
218+ _ <- indexDocument testIndex testMapping defaultIndexDocumentSettings exampleTweet (DocId "Hello World")
219+ _ <- refreshIndex testIndex
220+ return ()
221+
222+searchTweet :: Search -> BH IO (Either EsError Tweet)
223+searchTweet search = do
224+ result <- searchTweets search
225+ let myTweet :: Either EsError Tweet
226+ myTweet = grabFirst result
227+ return myTweet
228+
229+searchTweets :: Search -> BH IO (Either EsError (SearchResult Tweet))
230+searchTweets search = parseEsResponse =<< searchByIndex testIndex search
231+
232+searchExpectNoResults :: Search -> BH IO ()
233+searchExpectNoResults search = do
234+ result <- searchTweets search
235+ let emptyHits = fmap (hits . searchHits) result
236+ liftIO $
237+ emptyHits `shouldBe` Right []
238+
239+searchExpectAggs :: Search -> BH IO ()
240+searchExpectAggs search = do
241+ reply <- searchByIndex testIndex search
242+ let isEmpty x = return (M.null x)
243+ let result = decode (responseBody reply) :: Maybe (SearchResult Tweet)
244+ liftIO $
245+ (result >>= aggregations >>= isEmpty) `shouldBe` Just False
246+
247+searchValidBucketAgg :: (BucketAggregation a, FromJSON a, Show a) =>
248+ Search -> Text -> (Text -> AggregationResults -> Maybe (Bucket a)) -> BH IO ()
249+searchValidBucketAgg search aggKey extractor = do
250+ reply <- searchByIndex testIndex search
251+ let bucketDocs = docCount . head . buckets
252+ let result = decode (responseBody reply) :: Maybe (SearchResult Tweet)
253+ let count = result >>= aggregations >>= extractor aggKey >>= \x -> return (bucketDocs x)
254+ liftIO $
255+ count `shouldBe` Just 1
256+
257+searchTermsAggHint :: [ExecutionHint] -> BH IO ()
258+searchTermsAggHint hints = do
259+ let terms hint = TermsAgg $ (mkTermsAggregation "user") { termExecutionHint = Just hint }
260+ let search hint = mkAggregateSearch Nothing $ mkAggregations "users" $ terms hint
261+ forM_ hints $ searchExpectAggs . search
262+ forM_ hints (\x -> searchValidBucketAgg (search x) "users" toTerms)
263+
264+searchTweetHighlight :: Search -> BH IO (Either EsError (Maybe HitHighlight))
265+searchTweetHighlight search = do
266+ result <- searchTweets search
267+ let myHighlight = fmap (hitHighlight . head . hits . searchHits) result
268+ return myHighlight
269+
270+searchExpectSource :: Source -> Either EsError Value -> BH IO ()
271+searchExpectSource src expected = do
272+ _ <- insertData
273+ let query = QueryMatchQuery $ mkMatchQuery (FieldName "_all") (QueryString "haskell")
274+ let search = (mkSearch (Just query) Nothing) { source = Just src }
275+ reply <- searchByIndex testIndex search
276+ result <- parseEsResponse reply
277+ let value = grabFirst result
278+ liftIO $
279+ value `shouldBe` expected
280+
281+atleast :: Vers.Version -> IO Bool
282+atleast v = getServerVersion >>= \x -> return $ x >= Just v
283+
284+atmost :: Vers.Version -> IO Bool
285+atmost v = getServerVersion >>= \x -> return $ x <= Just v
286+
287+is :: Vers.Version -> IO Bool
288+is v = getServerVersion >>= \x -> return $ x == Just v
tests/V5/Test/Documents.hsnew file+51
@@ -0,0 +1,51 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+module Test.Documents where
4+
5+import Test.Common
6+import Test.Import
7+
8+spec :: Spec
9+spec =
10+ describe "document API" $ do
11+ it "indexes, updates, gets, and then deletes the generated document" $ withTestEnv $ do
12+ _ <- insertData
13+ _ <- updateData
14+ docInserted <- getDocument testIndex testMapping (DocId "1")
15+ let newTweet = eitherDecode
16+ (responseBody docInserted) :: Either String (EsResult Tweet)
17+ liftIO $ (fmap getSource newTweet `shouldBe` Right (Just patchedTweet))
18+
19+ it "indexes, gets, and then deletes the generated document with a DocId containing a space" $ withTestEnv $ do
20+ _ <- insertWithSpaceInId
21+ docInserted <- getDocument testIndex testMapping (DocId "Hello World")
22+ let newTweet = eitherDecode
23+ (responseBody docInserted) :: Either String (EsResult Tweet)
24+ liftIO $ (fmap getSource newTweet `shouldBe` Right (Just exampleTweet))
25+
26+ it "produces a parseable result when looking up a bogus document" $ withTestEnv $ do
27+ doc <- getDocument testIndex testMapping (DocId "bogus")
28+ let noTweet = eitherDecode
29+ (responseBody doc) :: Either String (EsResult Tweet)
30+ liftIO $ fmap foundResult noTweet `shouldBe` Right Nothing
31+
32+ it "can use optimistic concurrency control" $ withTestEnv $ do
33+ let ev = ExternalDocVersion minBound
34+ let cfg = defaultIndexDocumentSettings { idsVersionControl = ExternalGT ev }
35+ resetIndex
36+ res <- insertData' cfg
37+ liftIO $ isCreated res `shouldBe` True
38+ res' <- insertData' cfg
39+ liftIO $ isVersionConflict res' `shouldBe` True
40+
41+ it "indexes two documents in a parent/child relationship and checks that the child exists" $ withTestEnv $ do
42+ resetIndex
43+ _ <- putMapping testIndex (MappingName "child") ChildMapping
44+ _ <- putMapping testIndex (MappingName "parent") ParentMapping
45+ _ <- indexDocument testIndex (MappingName "parent") defaultIndexDocumentSettings exampleTweet (DocId "1")
46+ let parent = (Just . DocumentParent . DocId) "1"
47+ ids = IndexDocumentSettings NoVersionControl parent
48+ _ <- indexDocument testIndex (MappingName "child") ids otherTweet (DocId "2")
49+ _ <- refreshIndex testIndex
50+ exists <- documentExists testIndex (MappingName "child") parent (DocId "2")
51+ liftIO $ exists `shouldBe` True
tests/V5/Test/Highlights.hsnew file+34
@@ -0,0 +1,34 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+module Test.Highlights where
4+
5+import Test.Common
6+import Test.Import
7+
8+import qualified Data.Map as M
9+
10+spec :: Spec
11+spec =
12+ describe "Highlights API" $ do
13+
14+ it "returns highlight from query when there should be one" $ withTestEnv $ do
15+ _ <- insertData
16+ _ <- insertOther
17+ let query = QueryMatchQuery $ mkMatchQuery (FieldName "message") (QueryString "haskell")
18+ let testHighlight = Highlights Nothing [FieldHighlight (FieldName "message") Nothing]
19+
20+ let search = mkHighlightSearch (Just query) testHighlight
21+ myHighlight <- searchTweetHighlight search
22+ liftIO $
23+ myHighlight `shouldBe` Right (Just (M.fromList [("message",["Use <em>haskell</em>!"])]))
24+
25+ it "doesn't return highlight from a query when it shouldn't" $ withTestEnv $ do
26+ _ <- insertData
27+ _ <- insertOther
28+ let query = QueryMatchQuery $ mkMatchQuery (FieldName "message") (QueryString "haskell")
29+ let testHighlight = Highlights Nothing [FieldHighlight (FieldName "user") Nothing]
30+
31+ let search = mkHighlightSearch (Just query) testHighlight
32+ myHighlight <- searchTweetHighlight search
33+ liftIO $
34+ myHighlight `shouldBe` Right Nothing
tests/V5/Test/Import.hs+27−1
@@ -1,5 +1,8 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
13module Test.Import
24 ( module X
5+ , module Test.Import
36 ) where
47
58
@@ -11,12 +14,13 @@ import Control.Monad.Reader as X
1114import Data.Aeson as X
1215import Data.Aeson.TH as X
1316import Data.Aeson.Types as X (parseEither)
17+import Data.Maybe as X
1418import Data.List.NonEmpty as X (NonEmpty(..))
1519import Data.Monoid as X
1620import Data.Ord as X (comparing)
1721import Data.Proxy as X
1822import Data.Text as X (Text)
19−import Data.Time.Calendar as X (Day(..))
23+import Data.Time.Calendar as X (Day(..), fromGregorian)
2024import Data.Time.Clock as X
2125import Data.Typeable as X
2226import Database.V5.Bloodhound as X hiding (key)
@@ -30,3 +34,25 @@ import Test.Hspec.QuickCheck as X (prop)
3034import Test.QuickCheck as X hiding (Result, Success)
3135import Test.QuickCheck.Property.Monoid as X (T (..), eq, prop_Monoid)
3236import Text.Pretty.Simple as X (pPrint)
37+
38+import qualified Data.List as L
39+
40+noDuplicates :: Eq a => [a] -> Bool
41+noDuplicates xs = L.nub xs == xs
42+
43+getSource :: EsResult a -> Maybe a
44+getSource = fmap _source . foundResult
45+
46+grabFirst :: Either EsError (SearchResult a) -> Either EsError a
47+grabFirst r =
48+ case fmap (hitSource . head . hits . searchHits) r of
49+ (Left e) -> Left e
50+ (Right Nothing) -> Left (EsError 500 "Source was missing")
51+ (Right (Just x)) -> Right x
52+
53+when' :: Monad m => m Bool -> m () -> m ()
54+when' b f = b >>= \x -> when x f
55+
56+headMay :: [a] -> Maybe a
57+headMay (x : _) = Just x
58+headMay _ = Nothing
tests/V5/Test/Indices.hsnew file+152
@@ -0,0 +1,152 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+module Test.Indices where
4+
5+import Test.Common
6+import Test.Import
7+
8+import qualified Data.List as L
9+import qualified Data.List.NonEmpty as NE
10+import qualified Data.Map as M
11+spec :: Spec
12+spec = do
13+ describe "Index create/delete API" $ do
14+ it "creates and then deletes the requested index" $ withTestEnv $ do
15+ -- priming state.
16+ _ <- deleteExampleIndex
17+ resp <- createExampleIndex
18+ deleteResp <- deleteExampleIndex
19+ liftIO $ do
20+ validateStatus resp 200
21+ validateStatus deleteResp 200
22+
23+ describe "Index aliases" $ do
24+ let aname = IndexAliasName (IndexName "bloodhound-tests-twitter-1-alias")
25+ let alias = IndexAlias (testIndex) aname
26+ let create = IndexAliasCreate Nothing Nothing
27+ let action = AddAlias alias create
28+ it "handles the simple case of aliasing an existing index" $ do
29+ withTestEnv $ do
30+ resetIndex
31+ resp <- updateIndexAliases (action :| [])
32+ liftIO $ validateStatus resp 200
33+ let cleanup = withTestEnv (updateIndexAliases (RemoveAlias alias :| []))
34+ (do aliases <- withTestEnv getIndexAliases
35+ let expected = IndexAliasSummary alias create
36+ case aliases of
37+ Right (IndexAliasesSummary summs) ->
38+ L.find ((== alias) . indexAliasSummaryAlias) summs `shouldBe` Just expected
39+ Left e -> expectationFailure ("Expected an IndexAliasesSummary but got " <> show e)) `finally` cleanup
40+ it "allows alias deletion" $ do
41+ aliases <- withTestEnv $ do
42+ resetIndex
43+ resp <- updateIndexAliases (action :| [])
44+ liftIO $ validateStatus resp 200
45+ _ <- deleteIndexAlias aname
46+ getIndexAliases
47+ -- let expected = IndexAliasSummary alias create
48+ case aliases of
49+ Right (IndexAliasesSummary summs) ->
50+ L.find ( (== aname)
51+ . indexAlias
52+ . indexAliasSummaryAlias
53+ ) summs
54+ `shouldBe` Nothing
55+ Left e -> expectationFailure ("Expected an IndexAliasesSummary but got " <> show e)
56+
57+ describe "Index Listing" $ do
58+ it "returns a list of index names" $ withTestEnv $ do
59+ _ <- createExampleIndex
60+ ixns <- listIndices
61+ liftIO (ixns `shouldContain` [testIndex])
62+
63+ describe "Index Settings" $ do
64+ it "persists settings" $ withTestEnv $ do
65+ _ <- deleteExampleIndex
66+ _ <- createExampleIndex
67+ let updates = BlocksWrite False :| []
68+ updateResp <- updateIndexSettings updates testIndex
69+ liftIO $ validateStatus updateResp 200
70+ getResp <- getIndexSettings testIndex
71+ liftIO $
72+ getResp `shouldBe` Right (IndexSettingsSummary
73+ testIndex
74+ (IndexSettings (ShardCount 1) (ReplicaCount 0))
75+ (NE.toList updates))
76+
77+ it "allows total fields to be set" $ when' (atleast es50) $ withTestEnv $ do
78+ _ <- deleteExampleIndex
79+ _ <- createExampleIndex
80+ let updates = MappingTotalFieldsLimit 2500 :| []
81+ updateResp <- updateIndexSettings updates testIndex
82+ liftIO $ validateStatus updateResp 200
83+ getResp <- getIndexSettings testIndex
84+ liftIO $
85+ getResp `shouldBe` Right (IndexSettingsSummary
86+ testIndex
87+ (IndexSettings (ShardCount 1) (ReplicaCount 0))
88+ (NE.toList updates))
89+
90+ it "accepts customer analyzers" $ when' (atleast es50) $ withTestEnv $ do
91+ _ <- deleteExampleIndex
92+ let analysis = Analysis
93+ (M.singleton "ex_analyzer"
94+ ( AnalyzerDefinition
95+ (Just (Tokenizer "ex_tokenizer"))
96+ (map TokenFilter
97+ [ "ex_filter_lowercase","ex_filter_uppercase","ex_filter_apostrophe"
98+ , "ex_filter_reverse","ex_filter_snowball"
99+ , "ex_filter_shingle"
100+ ]
101+ )
102+ )
103+ )
104+ (M.singleton "ex_tokenizer"
105+ ( TokenizerDefinitionNgram
106+ ( Ngram 3 4 [TokenLetter,TokenDigit])
107+ )
108+ )
109+ (M.fromList
110+ [ ("ex_filter_lowercase",TokenFilterDefinitionLowercase (Just Greek))
111+ , ("ex_filter_uppercase",TokenFilterDefinitionUppercase Nothing)
112+ , ("ex_filter_apostrophe",TokenFilterDefinitionApostrophe)
113+ , ("ex_filter_reverse",TokenFilterDefinitionReverse)
114+ , ("ex_filter_snowball",TokenFilterDefinitionSnowball English)
115+ , ("ex_filter_shingle",TokenFilterDefinitionShingle (Shingle 3 3 True False " " "_"))
116+ ]
117+ )
118+ updates = [AnalysisSetting analysis]
119+ createResp <- createIndexWith (updates ++ [NumberOfReplicas (ReplicaCount 0)]) 1 testIndex
120+ liftIO $ validateStatus createResp 200
121+ getResp <- getIndexSettings testIndex
122+ liftIO $
123+ getResp `shouldBe` Right (IndexSettingsSummary
124+ testIndex
125+ (IndexSettings (ShardCount 1) (ReplicaCount 0))
126+ updates
127+ )
128+
129+ it "accepts default compression codec" $ when' (atleast es50) $ withTestEnv $ do
130+ _ <- deleteExampleIndex
131+ let updates = [CompressionSetting CompressionDefault]
132+ createResp <- createIndexWith (updates ++ [NumberOfReplicas (ReplicaCount 0)]) 1 testIndex
133+ liftIO $ validateStatus createResp 200
134+ getResp <- getIndexSettings testIndex
135+ liftIO $ getResp `shouldBe` Right
136+ (IndexSettingsSummary testIndex (IndexSettings (ShardCount 1) (ReplicaCount 0)) updates)
137+
138+ it "accepts best compression codec" $ when' (atleast es50) $ withTestEnv $ do
139+ _ <- deleteExampleIndex
140+ let updates = [CompressionSetting CompressionBest]
141+ createResp <- createIndexWith (updates ++ [NumberOfReplicas (ReplicaCount 0)]) 1 testIndex
142+ liftIO $ validateStatus createResp 200
143+ getResp <- getIndexSettings testIndex
144+ liftIO $ getResp `shouldBe` Right
145+ (IndexSettingsSummary testIndex (IndexSettings (ShardCount 1) (ReplicaCount 0)) updates)
146+
147+
148+ describe "Index Optimization" $ do
149+ it "returns a successful response upon completion" $ withTestEnv $ do
150+ _ <- createExampleIndex
151+ resp <- forceMergeIndex (IndexList (testIndex :| [])) defaultForceMergeIndexSettings
152+ liftIO $ validateStatus resp 200
tests/V5/Test/JSON.hs+47−8
@@ -1,19 +1,17 @@
1+{-# LANGUAGE OverloadedStrings #-}
12{-# LANGUAGE ScopedTypeVariables #-}
23
34module Test.JSON where
45
5−import Database.V5.Bloodhound
6+import Test.Import
67
7−import Data.Aeson
8−import Data.Aeson.Types (parseEither)
98import qualified Data.ByteString.Lazy.Char8 as BL8
109import qualified Data.List as L
10+import Data.List.NonEmpty (NonEmpty(..))
1111import qualified Data.List.NonEmpty as NE
12−import Data.Monoid
13−import Data.Typeable
14−import Test.Hspec
15−import Test.Hspec.QuickCheck
16−import Test.QuickCheck
12+import qualified Data.HashMap.Strict as HM
13+import qualified Data.Text as T
14+import qualified Data.Vector as V
1715
1816import Test.ApproxEq
1917import Test.Generators
@@ -52,6 +50,47 @@ propApproxJSON _ = prop testName $ \(a :: a) ->
5250
5351spec :: Spec
5452spec = do
53+ describe "ToJSON RegexpFlags" $ do
54+ it "generates the correct JSON for AllRegexpFlags" $
55+ toJSON AllRegexpFlags `shouldBe` String "ALL"
56+
57+ it "generates the correct JSON for NoRegexpFlags" $
58+ toJSON NoRegexpFlags `shouldBe` String "NONE"
59+
60+ it "generates the correct JSON for SomeRegexpFlags" $
61+ let flags = AnyString :| [ Automaton
62+ , Complement
63+ , Empty
64+ , Intersection
65+ , Interval ]
66+ in toJSON (SomeRegexpFlags flags) `shouldBe` String "ANYSTRING|AUTOMATON|COMPLEMENT|EMPTY|INTERSECTION|INTERVAL"
67+
68+ prop "removes duplicates from flags" $ \(flags :: RegexpFlags) ->
69+ let String str = toJSON flags
70+ flagStrs = T.splitOn "|" str
71+ in noDuplicates flagStrs
72+
73+ describe "omitNulls" $ do
74+ it "checks that omitNulls drops list elements when it should" $
75+ let dropped = omitNulls $ [ "test1" .= (toJSON ([] :: [Int]))
76+ , "test2" .= (toJSON ("some value" :: Text))]
77+ in dropped `shouldBe` Object (HM.fromList [("test2", String "some value")])
78+
79+ it "checks that omitNulls doesn't drop list elements when it shouldn't" $
80+ let notDropped = omitNulls $ [ "test1" .= (toJSON ([1] :: [Int]))
81+ , "test2" .= (toJSON ("some value" :: Text))]
82+ in notDropped `shouldBe` Object (HM.fromList [ ("test1", Array (V.fromList [Number 1.0]))
83+ , ("test2", String "some value")])
84+ it "checks that omitNulls drops non list elements when it should" $
85+ let dropped = omitNulls $ [ "test1" .= (toJSON Null)
86+ , "test2" .= (toJSON ("some value" :: Text))]
87+ in dropped `shouldBe` Object (HM.fromList [("test2", String "some value")])
88+ it "checks that omitNulls doesn't drop non list elements when it shouldn't" $
89+ let notDropped = omitNulls $ [ "test1" .= (toJSON (1 :: Int))
90+ , "test2" .= (toJSON ("some value" :: Text))]
91+ in notDropped `shouldBe` Object (HM.fromList [ ("test1", Number 1.0)
92+ , ("test2", String "some value")])
93+
5594 describe "Exact isomorphism JSON instances" $ do
5695 propJSON (Proxy :: Proxy Version)
5796 propJSON (Proxy :: Proxy IndexName)
tests/V5/Test/Query.hsnew file+115
@@ -0,0 +1,115 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+module Test.Query where
4+
5+import Test.Common
6+import Test.Import
7+
8+import qualified Data.HashMap.Strict as HM
9+
10+spec :: Spec
11+spec =
12+ describe "query API" $ do
13+ it "returns document for term query and identity filter" $ withTestEnv $ do
14+ _ <- insertData
15+ let query = TermQuery (Term "user" "bitemyapp") Nothing
16+ let filter' = Filter $ MatchAllQuery Nothing
17+ let search = mkSearch (Just query) (Just filter')
18+ myTweet <- searchTweet search
19+ liftIO $
20+ myTweet `shouldBe` Right exampleTweet
21+
22+ it "handles constant score queries" $ withTestEnv $ do
23+ _ <- insertData
24+ let query = TermsQuery "user" ("bitemyapp" :| [])
25+ let cfQuery = ConstantScoreQuery query (Boost 1.0)
26+ let filter' = Filter $ MatchAllQuery Nothing
27+ let search = mkSearch (Just cfQuery) (Just filter')
28+ myTweet <- searchTweet search
29+ liftIO $
30+ myTweet `shouldBe` Right exampleTweet
31+
32+ it "returns document for terms query and identity filter" $ withTestEnv $ do
33+ _ <- insertData
34+ let query = TermsQuery "user" ("bitemyapp" :| [])
35+ let filter' = Filter $ MatchAllQuery Nothing
36+ let search = mkSearch (Just query) (Just filter')
37+ myTweet <- searchTweet search
38+ liftIO $
39+ myTweet `shouldBe` Right exampleTweet
40+
41+ it "returns document for match query" $ withTestEnv $ do
42+ _ <- insertData
43+ let query = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
44+ let search = mkSearch (Just query) Nothing
45+ myTweet <- searchTweet search
46+ liftIO $
47+ myTweet `shouldBe` Right exampleTweet
48+
49+ it "returns document for multi-match query" $ withTestEnv $ do
50+ _ <- insertData
51+ let flds = [FieldName "user", FieldName "message"]
52+ let query = QueryMultiMatchQuery $ mkMultiMatchQuery flds (QueryString "bitemyapp")
53+ let search = mkSearch (Just query) Nothing
54+ myTweet <- searchTweet search
55+ liftIO $
56+ myTweet `shouldBe` Right exampleTweet
57+
58+ it "returns document for multi-match query with a custom tiebreaker" $ withTestEnv $ do
59+ _ <- insertData
60+ let tiebreaker = Just $ Tiebreaker 0.3
61+ flds = [FieldName "user", FieldName "message"]
62+ multiQuery' = mkMultiMatchQuery flds (QueryString "bitemyapp")
63+ query = QueryMultiMatchQuery $ multiQuery' { multiMatchQueryTiebreaker = tiebreaker }
64+ search = mkSearch (Just query) Nothing
65+ myTweet <- searchTweet search
66+ liftIO $
67+ myTweet `shouldBe` Right exampleTweet
68+
69+ it "returns document for bool query" $ withTestEnv $ do
70+ _ <- insertData
71+ let innerQuery = QueryMatchQuery $
72+ mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
73+ let query = QueryBoolQuery $
74+ mkBoolQuery [innerQuery] [] [] []
75+ let search = mkSearch (Just query) Nothing
76+ myTweet <- searchTweet search
77+ liftIO $
78+ myTweet `shouldBe` Right exampleTweet
79+
80+ it "returns document for boosting query" $ withTestEnv $ do
81+ _ <- insertData
82+ let posQuery = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
83+ let negQuery = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "notmyapp")
84+ let query = QueryBoostingQuery $ BoostingQuery posQuery negQuery (Boost 0.2)
85+ let search = mkSearch (Just query) Nothing
86+ myTweet <- searchTweet search
87+ liftIO $
88+ myTweet `shouldBe` Right exampleTweet
89+
90+ it "returns document for common terms query" $ withTestEnv $ do
91+ _ <- insertData
92+ let query = QueryCommonTermsQuery $
93+ CommonTermsQuery (FieldName "user")
94+ (QueryString "bitemyapp")
95+ (CutoffFrequency 0.0001)
96+ Or Or Nothing Nothing Nothing Nothing
97+ let search = mkSearch (Just query) Nothing
98+ myTweet <- searchTweet search
99+ liftIO $
100+ myTweet `shouldBe` Right exampleTweet
101+
102+ it "returns document for for inline template query" $ withTestEnv $ do
103+ _ <- insertData
104+ let innerQuery = QueryMatchQuery $
105+ mkMatchQuery (FieldName "{{userKey}}")
106+ (QueryString "{{bitemyappKey}}")
107+ templateParams = TemplateQueryKeyValuePairs $ HM.fromList
108+ [ ("userKey", "user")
109+ , ("bitemyappKey", "bitemyapp")
110+ ]
111+ templateQuery = QueryTemplateQueryInline $
112+ TemplateQueryInline innerQuery templateParams
113+ search = mkSearch (Just templateQuery) Nothing
114+ myTweet <- searchTweet search
115+ liftIO $ myTweet `shouldBe` Right exampleTweet
tests/V5/Test/Script.hsnew file+35
@@ -0,0 +1,35 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+module Test.Script where
4+
5+import Test.Common
6+import Test.Import
7+
8+import qualified Data.HashMap.Strict as HM
9+import qualified Data.Map as M
10+
11+spec :: Spec
12+spec =
13+ describe "Script" $ do
14+ it "returns a transformed document based on the script field" $ withTestEnv $ do
15+ _ <- insertData
16+ let query = MatchAllQuery Nothing
17+ sfv = toJSON $
18+ Script
19+ (Just (ScriptLanguage "painless"))
20+ (Just (ScriptInline "doc['age'].value * 2"))
21+ Nothing
22+ Nothing
23+ sf = ScriptFields $
24+ HM.fromList [("test1", sfv)]
25+ search' = mkSearch (Just query) Nothing
26+ search = search' { scriptFields = Just sf }
27+ resp <- searchByIndex testIndex search
28+ parsed <- parseEsResponse resp :: BH IO (Either EsError (SearchResult Value))
29+ case parsed of
30+ Left e ->
31+ liftIO $ expectationFailure ("Expected a script-transformed result but got: " <> show e)
32+ Right sr -> do
33+ let Just results =
34+ hitFields (head (hits (searchHits sr)))
35+ liftIO $ results `shouldBe` (HitFields (M.fromList [("test1", [Number 20000.0])]))
tests/V5/Test/Snapshots.hsnew file+201
@@ -0,0 +1,201 @@
1+{-# LANGUAGE RecordWildCards #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+
4+module Test.Snapshots where
5+
6+import Test.Common
7+import Test.Import
8+
9+import Data.Maybe (fromMaybe)
10+import qualified Data.List as L
11+import qualified Data.HashMap.Strict as HM
12+import qualified Data.Text as T
13+import qualified Data.Vector as V
14+import qualified Network.HTTP.Types.Method as NHTM
15+import qualified Network.URI as URI
16+
17+import Test.Generators ()
18+
19+spec :: Spec
20+spec = do
21+ describe "FsSnapshotRepo" $ do
22+ prop "SnapshotRepo laws" $ \fsr ->
23+ fromGSnapshotRepo (toGSnapshotRepo fsr) === Right (fsr :: FsSnapshotRepo)
24+
25+ describe "Snapshot repos" $ do
26+ it "always parses all snapshot repos API" $ when' canSnapshot $ withTestEnv $ do
27+ res <- getSnapshotRepos AllSnapshotRepos
28+ liftIO $ case res of
29+ Left e -> expectationFailure ("Expected a right but got Left " <> show e)
30+ Right _ -> return ()
31+
32+ it "finds an existing list of repos" $ when' canSnapshot $ withTestEnv $ do
33+ let r1n = SnapshotRepoName "bloodhound-repo1"
34+ let r2n = SnapshotRepoName "bloodhound-repo2"
35+ withSnapshotRepo r1n $ \r1 ->
36+ withSnapshotRepo r2n $ \r2 -> do
37+ repos <- getSnapshotRepos (SnapshotRepoList (ExactRepo r1n :| [ExactRepo r2n]))
38+ liftIO $ case repos of
39+ Right xs -> do
40+ let srt = L.sortBy (comparing gSnapshotRepoName)
41+ srt xs `shouldBe` srt [r1, r2]
42+ Left e -> expectationFailure (show e)
43+
44+ it "creates and updates with updateSnapshotRepo" $ when' canSnapshot $ withTestEnv $ do
45+ let r1n = SnapshotRepoName "bloodhound-repo1"
46+ withSnapshotRepo r1n $ \r1 -> do
47+ let Just (String dir) = HM.lookup "location" (gSnapshotRepoSettingsObject (gSnapshotRepoSettings r1))
48+ let noCompression = FsSnapshotRepo r1n (T.unpack dir) False Nothing Nothing Nothing
49+ resp <- updateSnapshotRepo defaultSnapshotRepoUpdateSettings noCompression
50+ liftIO (validateStatus resp 200)
51+ Right [roundtrippedNoCompression] <- getSnapshotRepos (SnapshotRepoList (ExactRepo r1n :| []))
52+ liftIO (roundtrippedNoCompression `shouldBe` toGSnapshotRepo noCompression)
53+
54+ -- verify came around in 1.4 it seems
55+ it "can verify existing repos" $ when' canSnapshot $ when' (atleast es14) $ withTestEnv $ do
56+ let r1n = SnapshotRepoName "bloodhound-repo1"
57+ withSnapshotRepo r1n $ \_ -> do
58+ res <- verifySnapshotRepo r1n
59+ liftIO $ case res of
60+ Right (SnapshotVerification vs)
61+ | null vs -> expectationFailure "Expected nonempty set of verifying nodes"
62+ | otherwise -> return ()
63+ Left e -> expectationFailure (show e)
64+
65+ describe "Snapshots" $ do
66+ it "always parses all snapshots API" $ when' canSnapshot $ withTestEnv $ do
67+ let r1n = SnapshotRepoName "bloodhound-repo1"
68+ withSnapshotRepo r1n $ \_ -> do
69+ res <- getSnapshots r1n AllSnapshots
70+ liftIO $ case res of
71+ Left e -> expectationFailure ("Expected a right but got Left " <> show e)
72+ Right _ -> return ()
73+
74+ it "can parse a snapshot that it created" $ when' canSnapshot $ withTestEnv $ do
75+ let r1n = SnapshotRepoName "bloodhound-repo1"
76+ withSnapshotRepo r1n $ \_ -> do
77+ let s1n = SnapshotName "example-snapshot"
78+ withSnapshot r1n s1n $ do
79+ res <- getSnapshots r1n (SnapshotList (ExactSnap s1n :| []))
80+ liftIO $ case res of
81+ Right [snap]
82+ | snapInfoState snap == SnapshotSuccess &&
83+ snapInfoName snap == s1n -> return ()
84+ | otherwise -> expectationFailure (show snap)
85+ Right [] -> expectationFailure "There were no snapshots"
86+ Right snaps -> expectationFailure ("Expected 1 snapshot but got" <> show (length snaps))
87+ Left e -> expectationFailure (show e)
88+
89+ describe "Snapshot restore" $ do
90+ it "can restore a snapshot that we create" $ when' canSnapshot $ withTestEnv $ do
91+ let r1n = SnapshotRepoName "bloodhound-repo1"
92+ withSnapshotRepo r1n $ \_ -> do
93+ let s1n = SnapshotName "example-snapshot"
94+ withSnapshot r1n s1n $ do
95+ let settings = defaultSnapshotRestoreSettings { snapRestoreWaitForCompletion = True }
96+ -- have to close an index to restore it
97+ resp1 <- closeIndex testIndex
98+ liftIO (validateStatus resp1 200)
99+ resp2 <- restoreSnapshot r1n s1n settings
100+ liftIO (validateStatus resp2 200)
101+
102+ it "can restore and rename" $ when' canSnapshot $ withTestEnv $ do
103+ let r1n = SnapshotRepoName "bloodhound-repo1"
104+ withSnapshotRepo r1n $ \_ -> do
105+ let s1n = SnapshotName "example-snapshot"
106+ withSnapshot r1n s1n $ do
107+ let pat = RestoreRenamePattern "bloodhound-tests-twitter-(\\d+)"
108+ let replace = RRTLit "restored-" :| [RRSubWholeMatch]
109+ let expectedIndex = IndexName "restored-bloodhound-tests-twitter-1"
110+ oldEnoughForOverrides <- liftIO (atleast es15)
111+ let overrides = RestoreIndexSettings { restoreOverrideReplicas = Just (ReplicaCount 0) }
112+ let settings = defaultSnapshotRestoreSettings { snapRestoreWaitForCompletion = True
113+ , snapRestoreRenamePattern = Just pat
114+ , snapRestoreRenameReplacement = Just replace
115+ , snapRestoreIndexSettingsOverrides = if oldEnoughForOverrides
116+ then Just overrides
117+ else Nothing
118+ }
119+ -- have to close an index to restore it
120+ let go = do
121+ resp <- restoreSnapshot r1n s1n settings
122+ liftIO (validateStatus resp 200)
123+ exists <- indexExists expectedIndex
124+ liftIO (exists `shouldBe` True)
125+ go `finally` deleteIndex expectedIndex
126+
127+-- | Get configured repo paths for snapshotting. Note that by default
128+-- this is not enabled and if we are over es 1.5, we won't be able to
129+-- test snapshotting. Note that this can and should be part of the
130+-- client functionality in a much less ad-hoc incarnation.
131+getRepoPaths :: IO [FilePath]
132+getRepoPaths = withTestEnv $ do
133+ bhe <- getBHEnv
134+ let Server s = bhServer bhe
135+ let tUrl = s <> "/" <> "_nodes"
136+ initReq <- parseRequest (URI.escapeURIString URI.isAllowedInURI (T.unpack tUrl))
137+ let req = setRequestIgnoreStatus $ initReq { method = NHTM.methodGet }
138+ Right (Object o) <- parseEsResponse =<< liftIO (httpLbs req (bhManager bhe))
139+ return $ fromMaybe mempty $ do
140+ Object nodes <- HM.lookup "nodes" o
141+ Object firstNode <- snd <$> headMay (HM.toList nodes)
142+ Object settings <- HM.lookup "settings" firstNode
143+ Object path <- HM.lookup "path" settings
144+ Array repo <- HM.lookup "repo" path
145+ return [ T.unpack t | String t <- V.toList repo]
146+
147+-- | 1.5 and earlier don't care about repo paths
148+canSnapshot :: IO Bool
149+canSnapshot = do
150+ caresAboutRepos <- atleast es16
151+ repoPaths <- getRepoPaths
152+ return (not caresAboutRepos || not (null (repoPaths)))
153+
154+withSnapshotRepo
155+ :: ( MonadMask m
156+ , MonadBH m
157+ )
158+ => SnapshotRepoName
159+ -> (GenericSnapshotRepo -> m a)
160+ -> m a
161+withSnapshotRepo srn@(SnapshotRepoName n) f = do
162+ repoPaths <- liftIO getRepoPaths
163+ -- we'll use the first repo path if available, otherwise system temp
164+ -- dir. Note that this will fail on ES > 1.6, so be sure you use
165+ -- @when' canSnapshot@.
166+ case repoPaths of
167+ (firstRepoPath:_) -> withTempDirectory firstRepoPath (T.unpack n) $ \dir -> bracket (alloc dir) free f
168+ [] -> withSystemTempDirectory (T.unpack n) $ \dir -> bracket (alloc dir) free f
169+ where
170+ alloc dir = do
171+ liftIO (setFileMode dir mode)
172+ let repo = FsSnapshotRepo srn "bloodhound-tests-backups" True Nothing Nothing Nothing
173+ resp <- updateSnapshotRepo defaultSnapshotRepoUpdateSettings repo
174+ liftIO (validateStatus resp 200)
175+ return (toGSnapshotRepo repo)
176+ mode = ownerModes `unionFileModes` groupModes `unionFileModes` otherModes
177+ free GenericSnapshotRepo {..} = do
178+ resp <- deleteSnapshotRepo gSnapshotRepoName
179+ liftIO (validateStatus resp 200)
180+
181+
182+withSnapshot
183+ :: ( MonadMask m
184+ , MonadBH m
185+ )
186+ => SnapshotRepoName
187+ -> SnapshotName
188+ -> m a
189+ -> m a
190+withSnapshot srn sn = bracket_ alloc free
191+ where
192+ alloc = do
193+ resp <- createSnapshot srn sn createSettings
194+ liftIO (validateStatus resp 200)
195+ -- We'll make this synchronous for testing purposes
196+ createSettings = defaultSnapshotCreateSettings { snapWaitForCompletion = True
197+ , snapIndices = Just (IndexList (testIndex :| []))
198+ -- We don't actually need to back up any data
199+ }
200+ free = do
201+ deleteSnapshot srn sn
tests/V5/Test/Sorting.hsnew file+22
@@ -0,0 +1,22 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+module Test.Sorting where
4+
5+import Test.Common
6+import Test.Import
7+
8+spec :: Spec
9+spec =
10+ describe "sorting" $ do
11+ it "returns documents in the right order" $ withTestEnv $ do
12+ _ <- insertData
13+ _ <- insertOther
14+ let sortSpec = DefaultSortSpec $ mkSort (FieldName "age") Ascending
15+ let search = Search Nothing
16+ Nothing (Just [sortSpec]) Nothing Nothing
17+ False (From 0) (Size 10) SearchTypeQueryThenFetch Nothing Nothing
18+ Nothing Nothing
19+ result <- searchTweets search
20+ let myTweet = grabFirst result
21+ liftIO $
22+ myTweet `shouldBe` Right otherTweet
tests/V5/Test/SourceFiltering.hsnew file+38
@@ -0,0 +1,38 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+module Test.SourceFiltering where
4+
5+import Test.Common
6+import Test.Import
7+
8+import qualified Data.HashMap.Strict as HM
9+
10+spec :: Spec
11+spec =
12+ describe "Source filtering" $ do
13+
14+ it "doesn't include source when sources are disabled" $ withTestEnv $ do
15+ searchExpectSource
16+ NoSource
17+ (Left (EsError 500 "Source was missing"))
18+
19+ it "includes a source" $ withTestEnv $ do
20+ searchExpectSource
21+ (SourcePatterns (PopPattern (Pattern "message")))
22+ (Right (Object (HM.fromList [("message", String "Use haskell!")])))
23+
24+ it "includes sources" $ withTestEnv $ do
25+ searchExpectSource
26+ (SourcePatterns (PopPatterns [Pattern "user", Pattern "message"]))
27+ (Right (Object (HM.fromList [("user",String "bitemyapp"),("message", String "Use haskell!")])))
28+
29+ it "includes source patterns" $ withTestEnv $ do
30+ searchExpectSource
31+ (SourcePatterns (PopPattern (Pattern "*ge")))
32+ (Right (Object (HM.fromList [("age", Number 10000),("message", String "Use haskell!")])))
33+
34+ it "excludes source patterns" $ withTestEnv $ do
35+ searchExpectSource
36+ (SourceIncludeExclude (Include [])
37+ (Exclude [Pattern "l*", Pattern "*ge", Pattern "postDate", Pattern "extra"]))
38+ (Right (Object (HM.fromList [("user",String "bitemyapp")])))
tests/V5/Test/Suggest.hsnew file+23
@@ -0,0 +1,23 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+module Test.Suggest where
4+
5+import Test.Common
6+import Test.Import
7+
8+spec :: Spec
9+spec =
10+ describe "Suggest" $ do
11+ it "returns a search suggestion using the phrase suggester" $ withTestEnv $ do
12+ _ <- insertData
13+ let query = QueryMatchNoneQuery
14+ phraseSuggester = mkPhraseSuggester (FieldName "message")
15+ namedSuggester = Suggest "Use haskel" "suggest_name" (SuggestTypePhraseSuggester phraseSuggester)
16+ search' = mkSearch (Just query) Nothing
17+ search = search' { suggestBody = Just namedSuggester }
18+ expectedText = Just "use haskell"
19+ resp <- searchByIndex testIndex search
20+ parsed <- parseEsResponse resp :: BH IO (Either EsError (SearchResult Tweet))
21+ case parsed of
22+ Left e -> liftIO $ expectationFailure ("Expected an search suggestion but got " <> show e)
23+ Right sr -> liftIO $ (suggestOptionsText . head . suggestResponseOptions . head . nsrResponses <$> suggest sr) `shouldBe` expectedText
tests/V5/Test/Templates.hsnew file+26
@@ -0,0 +1,26 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+module Test.Templates where
4+
5+import Test.Common
6+import Test.Import
7+
8+spec :: Spec
9+spec =
10+ describe "template API" $ do
11+ it "can create a template" $ withTestEnv $ do
12+ let idxTpl = IndexTemplate (TemplatePattern "tweet-*") (Just (IndexSettings (ShardCount 1) (ReplicaCount 1))) [toJSON TweetMapping]
13+ resp <- putTemplate idxTpl (TemplateName "tweet-tpl")
14+ liftIO $ validateStatus resp 200
15+
16+ it "can detect if a template exists" $ withTestEnv $ do
17+ exists <- templateExists (TemplateName "tweet-tpl")
18+ liftIO $ exists `shouldBe` True
19+
20+ it "can delete a template" $ withTestEnv $ do
21+ resp <- deleteTemplate (TemplateName "tweet-tpl")
22+ liftIO $ validateStatus resp 200
23+
24+ it "can detect if a template doesn't exist" $ withTestEnv $ do
25+ exists <- templateExists (TemplateName "tweet-tpl")
26+ liftIO $ exists `shouldBe` False
tests/V5/tests.hs+26−1168
@@ -18,428 +18,40 @@
1818#endif
1919module Main where
2020
21+import Test.Common
2122import Test.Import
2223
23−import Control.Error hiding (Script)
24−import qualified Data.HashMap.Strict as HM
25−import Data.List (nub)
26−import qualified Data.List as L
27−import Data.List.NonEmpty (NonEmpty (..))
28−import qualified Data.List.NonEmpty as NE
29−import qualified Data.Map.Strict as M
30−import Data.Text (Text)
31−import qualified Data.Text as T
32−import Data.Time.Calendar (Day (..), fromGregorian)
33−import Data.Time.Clock (UTCTime (..),
34− secondsToDiffTime)
35−import qualified Data.Vector as V
36−import qualified Data.Version as Vers
37−import qualified Database.V5.Bloodhound
38−import qualified Lens.Micro.Aeson as LMA
39−import qualified Network.HTTP.Types.Method as NHTM
40−import qualified Network.HTTP.Types.Status as NHTS
41−import qualified Network.URI as URI
4224import Prelude hiding (filter)
4325
26+import qualified Test.Aggregation as Aggregation
27+import qualified Test.BulkAPI as Bulk
28+import qualified Test.Documents as Documents
29+import qualified Test.Highlights as Highlights
30+import qualified Test.Indices as Indices
4431import qualified Test.JSON as JSON
45−
46−testServer :: Server
47−testServer = Server "http://localhost:9200"
48−testIndex :: IndexName
49−testIndex = IndexName "bloodhound-tests-twitter-1"
50−testMapping :: MappingName
51−testMapping = MappingName "tweet"
52−
53−withTestEnv :: BH IO a -> IO a
54−withTestEnv = withBH defaultManagerSettings testServer
55−
56−validateStatus :: Show body => Response body -> Int -> Expectation
57−validateStatus resp expected =
58− if actual == expected
59− then return ()
60− else expectationFailure ("Expected " <> show expected <> " but got " <> show actual <> ": " <> show body)
61− where
62− actual = NHTS.statusCode (responseStatus resp)
63− body = responseBody resp
64−
65−createExampleIndex :: (MonadBH m) => m Reply
66−createExampleIndex = createIndex (IndexSettings (ShardCount 1) (ReplicaCount 0)) testIndex
67−deleteExampleIndex :: (MonadBH m) => m Reply
68−deleteExampleIndex = deleteIndex testIndex
69−
70−es13 :: Vers.Version
71−es13 = Vers.Version [1, 3, 0] []
72−
73−es12 :: Vers.Version
74−es12 = Vers.Version [1, 2, 0] []
75−
76−es11 :: Vers.Version
77−es11 = Vers.Version [1, 1, 0] []
78−
79−es14 :: Vers.Version
80−es14 = Vers.Version [1, 4, 0] []
81−
82−es15 :: Vers.Version
83−es15 = Vers.Version [1, 5, 0] []
84−
85−es16 :: Vers.Version
86−es16 = Vers.Version [1, 6, 0] []
87−
88−es20 :: Vers.Version
89−es20 = Vers.Version [2, 0, 0] []
90−
91−es50 :: Vers.Version
92−es50 = Vers.Version [5, 0, 0] []
93−
94−getServerVersion :: IO (Maybe Vers.Version)
95−getServerVersion = fmap extractVersion <$> withTestEnv getStatus
96− where
97− extractVersion = versionNumber . number . version
98−
99−-- | Get configured repo paths for snapshotting. Note that by default
100−-- this is not enabled and if we are over es 1.5, we won't be able to
101−-- test snapshotting. Note that this can and should be part of the
102−-- client functionality in a much less ad-hoc incarnation.
103−getRepoPaths :: IO [FilePath]
104−getRepoPaths = withTestEnv $ do
105− bhe <- getBHEnv
106− let Server s = bhServer bhe
107− let tUrl = s <> "/" <> "_nodes"
108− initReq <- parseRequest (URI.escapeURIString URI.isAllowedInURI (T.unpack tUrl))
109− let req = setRequestIgnoreStatus $ initReq { method = NHTM.methodGet }
110− Right (Object o) <- parseEsResponse =<< liftIO (httpLbs req (bhManager bhe))
111− return $ fromMaybe mempty $ do
112− Object nodes <- HM.lookup "nodes" o
113− Object firstNode <- snd <$> headMay (HM.toList nodes)
114− Object settings <- HM.lookup "settings" firstNode
115− Object path <- HM.lookup "path" settings
116− Array repo <- HM.lookup "repo" path
117− return [ T.unpack t | String t <- V.toList repo]
118−
119−-- | 1.5 and earlier don't care about repo paths
120−canSnapshot :: IO Bool
121−canSnapshot = do
122− caresAboutRepos <- atleast es16
123− repoPaths <- getRepoPaths
124− return (not caresAboutRepos || not (null (repoPaths)))
125−
126−atleast :: Vers.Version -> IO Bool
127−atleast v = getServerVersion >>= \x -> return $ x >= Just v
128−
129−atmost :: Vers.Version -> IO Bool
130−atmost v = getServerVersion >>= \x -> return $ x <= Just v
131−
132−is :: Vers.Version -> IO Bool
133−is v = getServerVersion >>= \x -> return $ x == Just v
134−
135−when' :: Monad m => m Bool -> m () -> m ()
136−when' b f = b >>= \x -> when x f
137−
138−data Location = Location { lat :: Double
139− , lon :: Double } deriving (Eq, Show)
140−
141−data Tweet = Tweet { user :: Text
142− , postDate :: UTCTime
143− , message :: Text
144− , age :: Int
145− , location :: Location
146− , extra :: Maybe Text }
147− deriving (Eq, Show)
148−
149−$(deriveJSON defaultOptions ''Location)
150−$(deriveJSON defaultOptions ''Tweet)
151−
152−data ParentMapping = ParentMapping deriving (Eq, Show)
153−
154−instance ToJSON ParentMapping where
155− toJSON ParentMapping =
156− object ["properties" .=
157− object [ "user" .= object ["type" .= ("string" :: Text)
158− , "fielddata" .= True
159− ]
160− -- Serializing the date as a date is breaking other tests, mysteriously.
161− -- , "postDate" .= object [ "type" .= ("date" :: Text)
162− -- , "format" .= ("YYYY-MM-dd`T`HH:mm:ss.SSSZZ" :: Text)]
163− , "message" .= object ["type" .= ("string" :: Text)]
164− , "age" .= object ["type" .= ("integer" :: Text)]
165− , "location" .= object ["type" .= ("geo_point" :: Text)]
166− , "extra" .= object ["type" .= ("keyword" :: Text)]
167− ]]
168−
169−data ChildMapping = ChildMapping deriving (Eq, Show)
170−
171−instance ToJSON ChildMapping where
172− toJSON ChildMapping =
173− object ["_parent" .= object ["type" .= ("parent" :: Text)]
174− , "properties" .=
175− object [ "user" .= object ["type" .= ("string" :: Text)
176− , "fielddata" .= True
177− ]
178− -- Serializing the date as a date is breaking other tests, mysteriously.
179− -- , "postDate" .= object [ "type" .= ("date" :: Text)
180− -- , "format" .= ("YYYY-MM-dd`T`HH:mm:ss.SSSZZ" :: Text)]
181− , "message" .= object ["type" .= ("string" :: Text)]
182− , "age" .= object ["type" .= ("integer" :: Text)]
183− , "location" .= object ["type" .= ("geo_point" :: Text)]
184− , "extra" .= object ["type" .= ("keyword" :: Text)]
185− ]]
186−
187−data TweetMapping = TweetMapping deriving (Eq, Show)
188−
189−instance ToJSON TweetMapping where
190− toJSON TweetMapping =
191− object ["tweet" .=
192− object ["properties" .=
193− object [ "user" .= object [ "type" .= ("string" :: Text)
194− , "fielddata" .= True
195− ]
196− -- Serializing the date as a date is breaking other tests, mysteriously.
197− -- , "postDate" .= object [ "type" .= ("date" :: Text)
198− -- , "format" .= ("YYYY-MM-dd`T`HH:mm:ss.SSSZZ" :: Text)]
199− , "message" .= object ["type" .= ("string" :: Text)]
200− , "age" .= object ["type" .= ("integer" :: Text)]
201− , "location" .= object ["type" .= ("geo_point" :: Text)]
202− , "extra" .= object ["type" .= ("keyword" :: Text)]
203− ]]]
204−
205−exampleTweet :: Tweet
206−exampleTweet = Tweet { user = "bitemyapp"
207− , postDate = UTCTime
208− (ModifiedJulianDay 55000)
209− (secondsToDiffTime 10)
210− , message = "Use haskell!"
211− , age = 10000
212− , location = Location 40.12 (-71.34)
213− , extra = Nothing }
214−
215−tweetWithExtra :: Tweet
216−tweetWithExtra = Tweet { user = "bitemyapp"
217− , postDate = UTCTime
218− (ModifiedJulianDay 55000)
219− (secondsToDiffTime 10)
220− , message = "Use haskell!"
221− , age = 10000
222− , location = Location 40.12 (-71.34)
223− , extra = Just "blah blah" }
224−
225−newAge :: Int
226−newAge = 31337
227−
228−newUser :: Text
229−newUser = "someotherapp"
230−
231−tweetPatch :: Value
232−tweetPatch =
233− object [ "age" .= newAge
234− , "user" .= newUser
235− ]
236−
237−patchedTweet :: Tweet
238−patchedTweet = exampleTweet{age = newAge, user = newUser}
239−
240−otherTweet :: Tweet
241−otherTweet = Tweet { user = "notmyapp"
242− , postDate = UTCTime
243− (ModifiedJulianDay 55000)
244− (secondsToDiffTime 11)
245− , message = "Use haskell!"
246− , age = 1000
247− , location = Location 40.12 (-71.34)
248− , extra = Nothing }
249−
250−resetIndex :: BH IO ()
251−resetIndex = do
252− _ <- deleteExampleIndex
253− _ <- createExampleIndex
254− _ <- putMapping testIndex testMapping TweetMapping
255− return ()
256−
257−insertData :: BH IO Reply
258−insertData = do
259− resetIndex
260− insertData' defaultIndexDocumentSettings
261−
262−insertData' :: IndexDocumentSettings -> BH IO Reply
263−insertData' ids = do
264− r <- indexDocument testIndex testMapping ids exampleTweet (DocId "1")
265− _ <- refreshIndex testIndex
266− return r
267−
268−updateData :: BH IO Reply
269−updateData = do
270− r <- updateDocument testIndex testMapping defaultIndexDocumentSettings tweetPatch (DocId "1")
271− _ <- refreshIndex testIndex
272− return r
273−
274−insertOther :: BH IO ()
275−insertOther = do
276− _ <- indexDocument testIndex testMapping defaultIndexDocumentSettings otherTweet (DocId "2")
277− _ <- refreshIndex testIndex
278− return ()
279−
280−insertExtra :: BH IO ()
281−insertExtra = do
282− _ <- indexDocument testIndex testMapping defaultIndexDocumentSettings tweetWithExtra (DocId "4")
283− _ <- refreshIndex testIndex
284− return ()
285−
286−insertWithSpaceInId :: BH IO ()
287−insertWithSpaceInId = do
288− _ <- indexDocument testIndex testMapping defaultIndexDocumentSettings exampleTweet (DocId "Hello World")
289− _ <- refreshIndex testIndex
290− return ()
291−
292−searchTweet :: Search -> BH IO (Either EsError Tweet)
293−searchTweet search = do
294− result <- searchTweets search
295− let myTweet :: Either EsError Tweet
296− myTweet = grabFirst result
297− return myTweet
298−
299−searchTweets :: Search -> BH IO (Either EsError (SearchResult Tweet))
300−searchTweets search = parseEsResponse =<< searchByIndex testIndex search
301−
302−searchExpectNoResults :: Search -> BH IO ()
303−searchExpectNoResults search = do
304− result <- searchTweets search
305− let emptyHits = fmap (hits . searchHits) result
306− liftIO $
307− emptyHits `shouldBe` Right []
308−
309−searchExpectAggs :: Search -> BH IO ()
310−searchExpectAggs search = do
311− reply <- searchByIndex testIndex search
312− let isEmpty x = return (M.null x)
313− let result = decode (responseBody reply) :: Maybe (SearchResult Tweet)
314− liftIO $
315− (result >>= aggregations >>= isEmpty) `shouldBe` Just False
316−
317−searchValidBucketAgg :: (BucketAggregation a, FromJSON a, Show a) =>
318− Search -> Text -> (Text -> AggregationResults -> Maybe (Bucket a)) -> BH IO ()
319−searchValidBucketAgg search aggKey extractor = do
320− reply <- searchByIndex testIndex search
321− let bucketDocs = docCount . head . buckets
322− let result = decode (responseBody reply) :: Maybe (SearchResult Tweet)
323− let count = result >>= aggregations >>= extractor aggKey >>= \x -> return (bucketDocs x)
324− liftIO $
325− count `shouldBe` Just 1
326−
327−searchTermsAggHint :: [ExecutionHint] -> BH IO ()
328−searchTermsAggHint hints = do
329− let terms hint = TermsAgg $ (mkTermsAggregation "user") { termExecutionHint = Just hint }
330− let search hint = mkAggregateSearch Nothing $ mkAggregations "users" $ terms hint
331− forM_ hints $ searchExpectAggs . search
332− forM_ hints (\x -> searchValidBucketAgg (search x) "users" toTerms)
333−
334−searchTweetHighlight :: Search -> BH IO (Either EsError (Maybe HitHighlight))
335−searchTweetHighlight search = do
336− result <- searchTweets search
337− let myHighlight = fmap (hitHighlight . head . hits . searchHits) result
338− return myHighlight
339−
340−searchExpectSource :: Source -> Either EsError Value -> BH IO ()
341−searchExpectSource src expected = do
342− _ <- insertData
343− let query = QueryMatchQuery $ mkMatchQuery (FieldName "_all") (QueryString "haskell")
344− let search = (mkSearch (Just query) Nothing) { source = Just src }
345− reply <- searchByIndex testIndex search
346− result <- parseEsResponse reply
347− let value = grabFirst result
348− liftIO $
349− value `shouldBe` expected
350−
351−withSnapshotRepo
352− :: ( MonadMask m
353− , MonadBH m
354− )
355− => SnapshotRepoName
356− -> (GenericSnapshotRepo -> m a)
357− -> m a
358−withSnapshotRepo srn@(SnapshotRepoName n) f = do
359− repoPaths <- liftIO getRepoPaths
360− -- we'll use the first repo path if available, otherwise system temp
361− -- dir. Note that this will fail on ES > 1.6, so be sure you use
362− -- @when' canSnapshot@.
363− case repoPaths of
364− (firstRepoPath:_) -> withTempDirectory firstRepoPath (T.unpack n) $ \dir -> bracket (alloc dir) free f
365− [] -> withSystemTempDirectory (T.unpack n) $ \dir -> bracket (alloc dir) free f
366− where
367− alloc dir = do
368− liftIO (setFileMode dir mode)
369− let repo = FsSnapshotRepo srn "bloodhound-tests-backups" True Nothing Nothing Nothing
370− resp <- updateSnapshotRepo defaultSnapshotRepoUpdateSettings repo
371− liftIO (validateStatus resp 200)
372− return (toGSnapshotRepo repo)
373− mode = ownerModes `unionFileModes` groupModes `unionFileModes` otherModes
374− free GenericSnapshotRepo {..} = do
375− resp <- deleteSnapshotRepo gSnapshotRepoName
376− liftIO (validateStatus resp 200)
377−
378−
379−withSnapshot
380− :: ( MonadMask m
381− , MonadBH m
382− )
383− => SnapshotRepoName
384− -> SnapshotName
385− -> m a
386− -> m a
387−withSnapshot srn sn = bracket_ alloc free
388− where
389− alloc = do
390− resp <- createSnapshot srn sn createSettings
391− liftIO (validateStatus resp 200)
392− -- We'll make this synchronous for testing purposes
393− createSettings = defaultSnapshotCreateSettings { snapWaitForCompletion = True
394− , snapIndices = Just (IndexList (testIndex :| []))
395− -- We don't actually need to back up any data
396− }
397− free = do
398− deleteSnapshot srn sn
399−
400−
401−
402−newtype BulkTest =
403− BulkTest { name :: Text }
404− deriving (Eq, Show)
405−
406−instance ToJSON BulkTest where
407− toJSON (BulkTest name) =
408− object ["name" .= name]
409−
410−instance FromJSON BulkTest where
411− parseJSON = withObject "BulkTest" parse
412− where
413− parse o = do
414− t <- o .: "name"
415− BulkTest <$> parseJSON t
416−
417−noDuplicates :: Eq a => [a] -> Bool
418−noDuplicates xs = nub xs == xs
419−
420−
421−getSource :: EsResult a -> Maybe a
422−getSource = fmap _source . foundResult
423−
424−grabFirst :: Either EsError (SearchResult a) -> Either EsError a
425−grabFirst r =
426− case fmap (hitSource . head . hits . searchHits) r of
427− (Left e) -> Left e
428− (Right Nothing) -> Left (EsError 500 "Source was missing")
429− (Right (Just x)) -> Right x
32+import qualified Test.Query as Query
33+import qualified Test.Script as Script
34+import qualified Test.Snapshots as Snapshots
35+import qualified Test.Sorting as Sorting
36+import qualified Test.SourceFiltering as SourceFiltering
37+import qualified Test.Suggest as Suggest
38+import qualified Test.Templates as Templates
43039
43140main :: IO ()
43241main = hspec $ do
42+ Aggregation.spec
43+ Bulk.spec
44+ Documents.spec
45+ Highlights.spec
46+ Indices.spec
43347 JSON.spec
434− describe "index create/delete API" $ do
435− it "creates and then deletes the requested index" $ withTestEnv $ do
436− -- priming state.
437− _ <- deleteExampleIndex
438− resp <- createExampleIndex
439− deleteResp <- deleteExampleIndex
440− liftIO $ do
441− validateStatus resp 200
442− validateStatus deleteResp 200
48+ Query.spec
49+ Script.spec
50+ Snapshots.spec
51+ Sorting.spec
52+ SourceFiltering.spec
53+ Suggest.spec
54+ Templates.spec
44355
44456 describe "error parsing" $ do
44557 it "can parse EsErrors for < 2.0" $ when' (atmost es16) $ withTestEnv $ do
@@ -452,484 +64,6 @@ main = hspec $ do
45264 let errorResp = eitherDecode (responseBody res)
45365 liftIO (errorResp `shouldBe` Right (EsError 404 "no such index"))
45466
455− describe "document API" $ do
456− it "indexes, updates, gets, and then deletes the generated document" $ withTestEnv $ do
457− _ <- insertData
458− _ <- updateData
459− docInserted <- getDocument testIndex testMapping (DocId "1")
460− let newTweet = eitherDecode
461− (responseBody docInserted) :: Either String (EsResult Tweet)
462− liftIO $ (fmap getSource newTweet `shouldBe` Right (Just patchedTweet))
463−
464− it "indexes, gets, and then deletes the generated document with a DocId containing a space" $ withTestEnv $ do
465− _ <- insertWithSpaceInId
466− docInserted <- getDocument testIndex testMapping (DocId "Hello World")
467− let newTweet = eitherDecode
468− (responseBody docInserted) :: Either String (EsResult Tweet)
469− liftIO $ (fmap getSource newTweet `shouldBe` Right (Just exampleTweet))
470−
471− it "produces a parseable result when looking up a bogus document" $ withTestEnv $ do
472− doc <- getDocument testIndex testMapping (DocId "bogus")
473− let noTweet = eitherDecode
474− (responseBody doc) :: Either String (EsResult Tweet)
475− liftIO $ fmap foundResult noTweet `shouldBe` Right Nothing
476−
477− it "can use optimistic concurrency control" $ withTestEnv $ do
478− let ev = ExternalDocVersion minBound
479− let cfg = defaultIndexDocumentSettings { idsVersionControl = ExternalGT ev }
480− resetIndex
481− res <- insertData' cfg
482− liftIO $ isCreated res `shouldBe` True
483− res' <- insertData' cfg
484− liftIO $ isVersionConflict res' `shouldBe` True
485−
486− it "indexes two documents in a parent/child relationship and checks that the child exists" $ withTestEnv $ do
487− resetIndex
488− _ <- putMapping testIndex (MappingName "child") ChildMapping
489− _ <- putMapping testIndex (MappingName "parent") ParentMapping
490− _ <- indexDocument testIndex (MappingName "parent") defaultIndexDocumentSettings exampleTweet (DocId "1")
491− let parent = (Just . DocumentParent . DocId) "1"
492− ids = IndexDocumentSettings NoVersionControl parent
493− _ <- indexDocument testIndex (MappingName "child") ids otherTweet (DocId "2")
494− _ <- refreshIndex testIndex
495− exists <- documentExists testIndex (MappingName "child") parent (DocId "2")
496− liftIO $ exists `shouldBe` True
497−
498− describe "template API" $ do
499− it "can create a template" $ withTestEnv $ do
500− let idxTpl = IndexTemplate (TemplatePattern "tweet-*") (Just (IndexSettings (ShardCount 1) (ReplicaCount 1))) [toJSON TweetMapping]
501− resp <- putTemplate idxTpl (TemplateName "tweet-tpl")
502− liftIO $ validateStatus resp 200
503−
504− it "can detect if a template exists" $ withTestEnv $ do
505− exists <- templateExists (TemplateName "tweet-tpl")
506− liftIO $ exists `shouldBe` True
507−
508− it "can delete a template" $ withTestEnv $ do
509− resp <- deleteTemplate (TemplateName "tweet-tpl")
510− liftIO $ validateStatus resp 200
511−
512− it "can detect if a template doesn't exist" $ withTestEnv $ do
513− exists <- templateExists (TemplateName "tweet-tpl")
514− liftIO $ exists `shouldBe` False
515−
516− describe "bulk API" $ do
517− it "inserts all documents we request" $ withTestEnv $ do
518− _ <- insertData
519− let firstTest = BulkTest "blah"
520− let secondTest = BulkTest "bloo"
521− let thirdTest = BulkTest "graffle"
522− let fourthTest = BulkTest "garabadoo"
523− let fifthTest = BulkTest "serenity"
524− let firstDoc = BulkIndex testIndex
525− testMapping (DocId "2") (toJSON firstTest)
526− let secondDoc = BulkCreate testIndex
527− testMapping (DocId "3") (toJSON secondTest)
528− let thirdDoc = BulkCreateEncoding testIndex
529− testMapping (DocId "4") (toEncoding thirdTest)
530− let fourthDoc = BulkIndexAuto testIndex
531− testMapping (toJSON fourthTest)
532− let fifthDoc = BulkIndexEncodingAuto testIndex
533− testMapping (toEncoding fifthTest)
534− let stream = V.fromList [firstDoc, secondDoc, thirdDoc, fourthDoc, fifthDoc]
535− _ <- bulk stream
536− -- liftIO $ pPrint bulkResp
537− _ <- refreshIndex testIndex
538− -- liftIO $ pPrint refreshResp
539− fDoc <- getDocument testIndex testMapping (DocId "2")
540− sDoc <- getDocument testIndex testMapping (DocId "3")
541− tDoc <- getDocument testIndex testMapping (DocId "4")
542− -- note that we cannot query for fourthDoc and fifthDoc since we
543− -- do not know their autogenerated ids.
544− let maybeFirst =
545− eitherDecode
546− $ responseBody fDoc
547− :: Either String (EsResult BulkTest)
548− let maybeSecond =
549− eitherDecode
550− $ responseBody sDoc
551− :: Either String (EsResult BulkTest)
552− let maybeThird =
553− eitherDecode
554− $ responseBody tDoc
555− :: Either String (EsResult BulkTest)
556− -- liftIO $ pPrint [maybeFirst, maybeSecond, maybeThird]
557− liftIO $ do
558− fmap getSource maybeFirst `shouldBe` Right (Just firstTest)
559− fmap getSource maybeSecond `shouldBe` Right (Just secondTest)
560− fmap getSource maybeThird `shouldBe` Right (Just thirdTest)
561− -- Since we can't get the docs by doc id, we check for their existence in
562− -- a match all query.
563− let query = MatchAllQuery Nothing
564− let search = mkSearch (Just query) Nothing
565− resp <- searchByIndex testIndex search
566− parsed <- parseEsResponse resp :: BH IO (Either EsError (SearchResult Value))
567− case parsed of
568− Left e ->
569− liftIO $ expectationFailure ("Expected a script-transformed result but got: " <> show e)
570− (Right sr) -> do
571− liftIO $
572− hitsTotal (searchHits sr) `shouldBe` 6
573− let nameList :: [Text]
574− nameList =
575− (hits (searchHits sr))
576− ^.. traverse
577− . to hitSource
578− . _Just
579− . LMA.key "name"
580− . _String
581− liftIO $
582− nameList
583− `shouldBe` ["blah","bloo","graffle","garabadoo","serenity"]
584−
585−
586− describe "query API" $ do
587− it "returns document for term query and identity filter" $ withTestEnv $ do
588− _ <- insertData
589− let query = TermQuery (Term "user" "bitemyapp") Nothing
590− let filter = Filter $ MatchAllQuery Nothing
591− let search = mkSearch (Just query) (Just filter)
592− myTweet <- searchTweet search
593− liftIO $
594− myTweet `shouldBe` Right exampleTweet
595−
596− it "handles constant score queries" $ withTestEnv $ do
597− _ <- insertData
598− let query = TermsQuery "user" ("bitemyapp" :| [])
599− let cfQuery = ConstantScoreQuery query (Boost 1.0)
600− let filter = Filter $ MatchAllQuery Nothing
601− let search = mkSearch (Just cfQuery) (Just filter)
602− myTweet <- searchTweet search
603− liftIO $
604− myTweet `shouldBe` Right exampleTweet
605−
606− it "returns document for terms query and identity filter" $ withTestEnv $ do
607− _ <- insertData
608− let query = TermsQuery "user" ("bitemyapp" :| [])
609− let filter = Filter $ MatchAllQuery Nothing
610− let search = mkSearch (Just query) (Just filter)
611− myTweet <- searchTweet search
612− liftIO $
613− myTweet `shouldBe` Right exampleTweet
614−
615− it "returns document for match query" $ withTestEnv $ do
616− _ <- insertData
617− let query = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
618− let search = mkSearch (Just query) Nothing
619− myTweet <- searchTweet search
620− liftIO $
621− myTweet `shouldBe` Right exampleTweet
622−
623− it "returns document for multi-match query" $ withTestEnv $ do
624− _ <- insertData
625− let flds = [FieldName "user", FieldName "message"]
626− let query = QueryMultiMatchQuery $ mkMultiMatchQuery flds (QueryString "bitemyapp")
627− let search = mkSearch (Just query) Nothing
628− myTweet <- searchTweet search
629− liftIO $
630− myTweet `shouldBe` Right exampleTweet
631−
632− it "returns document for multi-match query with a custom tiebreaker" $ withTestEnv $ do
633− _ <- insertData
634− let tiebreaker = Just $ Tiebreaker 0.3
635− flds = [FieldName "user", FieldName "message"]
636− multiQuery' = mkMultiMatchQuery flds (QueryString "bitemyapp")
637− query = QueryMultiMatchQuery $ multiQuery' { multiMatchQueryTiebreaker = tiebreaker }
638− search = mkSearch (Just query) Nothing
639− myTweet <- searchTweet search
640− liftIO $
641− myTweet `shouldBe` Right exampleTweet
642−
643− it "returns document for bool query" $ withTestEnv $ do
644− _ <- insertData
645− let innerQuery = QueryMatchQuery $
646− mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
647− let query = QueryBoolQuery $
648− mkBoolQuery [innerQuery] [] [] []
649− let search = mkSearch (Just query) Nothing
650− myTweet <- searchTweet search
651− liftIO $
652− myTweet `shouldBe` Right exampleTweet
653−
654− it "returns document for boosting query" $ withTestEnv $ do
655− _ <- insertData
656− let posQuery = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
657− let negQuery = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "notmyapp")
658− let query = QueryBoostingQuery $ BoostingQuery posQuery negQuery (Boost 0.2)
659− let search = mkSearch (Just query) Nothing
660− myTweet <- searchTweet search
661− liftIO $
662− myTweet `shouldBe` Right exampleTweet
663−
664− it "returns document for common terms query" $ withTestEnv $ do
665− _ <- insertData
666− let query = QueryCommonTermsQuery $
667− CommonTermsQuery (FieldName "user")
668− (QueryString "bitemyapp")
669− (CutoffFrequency 0.0001)
670− Or Or Nothing Nothing Nothing Nothing
671− let search = mkSearch (Just query) Nothing
672− myTweet <- searchTweet search
673− liftIO $
674− myTweet `shouldBe` Right exampleTweet
675−
676− it "returns document for for inline template query" $ withTestEnv $ do
677− _ <- insertData
678− let innerQuery = QueryMatchQuery $
679− mkMatchQuery (FieldName "{{userKey}}")
680− (QueryString "{{bitemyappKey}}")
681− templateParams = TemplateQueryKeyValuePairs $ HM.fromList
682− [ ("userKey", "user")
683− , ("bitemyappKey", "bitemyapp")
684− ]
685− templateQuery = QueryTemplateQueryInline $
686− TemplateQueryInline innerQuery templateParams
687− search = mkSearch (Just templateQuery) Nothing
688− myTweet <- searchTweet search
689− liftIO $ myTweet `shouldBe` Right exampleTweet
690−
691−
692− describe "sorting" $ do
693− it "returns documents in the right order" $ withTestEnv $ do
694− _ <- insertData
695− _ <- insertOther
696− let sortSpec = DefaultSortSpec $ mkSort (FieldName "age") Ascending
697− let search = Search Nothing
698− Nothing (Just [sortSpec]) Nothing Nothing
699− False (From 0) (Size 10) SearchTypeQueryThenFetch Nothing Nothing
700− Nothing Nothing
701− result <- searchTweets search
702− let myTweet = grabFirst result
703− liftIO $
704− myTweet `shouldBe` Right otherTweet
705−
706− describe "Aggregation API" $ do
707− it "returns term aggregation results" $ withTestEnv $ do
708− _ <- insertData
709− let terms = TermsAgg $ mkTermsAggregation "user"
710− let search = mkAggregateSearch Nothing $ mkAggregations "users" terms
711− searchExpectAggs search
712− searchValidBucketAgg search "users" toTerms
713−
714− it "return sub-aggregation results" $ withTestEnv $ do
715− _ <- insertData
716− let subaggs = mkAggregations "age_agg" . TermsAgg $ mkTermsAggregation "age"
717− agg = TermsAgg $ (mkTermsAggregation "user") { termAggs = Just subaggs}
718− search = mkAggregateSearch Nothing $ mkAggregations "users" agg
719− reply <- searchByIndex testIndex search
720− let result = decode (responseBody reply) :: Maybe (SearchResult Tweet)
721− usersAggResults = result >>= aggregations >>= toTerms "users"
722− subAggResults = usersAggResults >>= (listToMaybe . buckets) >>= termsAggs >>= toTerms "age_agg"
723− subAddResultsExists = isJust subAggResults
724− liftIO $ (subAddResultsExists) `shouldBe` True
725−
726− it "returns cardinality aggregation results" $ withTestEnv $ do
727− _ <- insertData
728− let cardinality = CardinalityAgg $ mkCardinalityAggregation $ FieldName "user"
729− let search = mkAggregateSearch Nothing $ mkAggregations "users" cardinality
730− let search' = search { Database.V5.Bloodhound.from = From 0, size = Size 0 }
731− searchExpectAggs search'
732− let docCountPair k n = (k, object ["value" .= Number n])
733− res <- searchTweets search'
734− liftIO $
735− fmap aggregations res `shouldBe` Right (Just (M.fromList [ docCountPair "users" 1]))
736−
737− it "returns stats aggregation results" $ withTestEnv $ do
738− _ <- insertData
739− let stats = StatsAgg $ mkStatsAggregation $ FieldName "age"
740− let search = mkAggregateSearch Nothing $ mkAggregations "users" stats
741− let search' = search { Database.V5.Bloodhound.from = From 0, size = Size 0 }
742− searchExpectAggs search'
743− let statsAggRes k n = (k, object [ "max" .= Number n
744− , "avg" .= Number n
745− , "count" .= Number 1
746− , "min" .= Number n
747− , "sum" .= Number n])
748− res <- searchTweets search'
749− liftIO $
750− fmap aggregations res `shouldBe` Right (Just (M.fromList [ statsAggRes "users" 10000]))
751−
752− it "can give collection hint parameters to term aggregations" $ when' (atleast es13) $ withTestEnv $ do
753− _ <- insertData
754− let terms = TermsAgg $ (mkTermsAggregation "user") { termCollectMode = Just BreadthFirst }
755− let search = mkAggregateSearch Nothing $ mkAggregations "users" terms
756− searchExpectAggs search
757− searchValidBucketAgg search "users" toTerms
758−
759− -- One of these fails with 1.7.3
760− it "can give execution hint parameters to term aggregations" $ when' (atmost es11) $ withTestEnv $ do
761− _ <- insertData
762− searchTermsAggHint [Map, Ordinals]
763−
764− it "can give execution hint parameters to term aggregations" $ when' (is es12) $ withTestEnv $ do
765− _ <- insertData
766− searchTermsAggHint [GlobalOrdinals, GlobalOrdinalsHash, GlobalOrdinalsLowCardinality, Map, Ordinals]
767−
768− it "can give execution hint parameters to term aggregations" $ when' (atleast es12) $ withTestEnv $ do
769− _ <- insertData
770− searchTermsAggHint [GlobalOrdinals, GlobalOrdinalsHash, GlobalOrdinalsLowCardinality, Map]
771− -- One of the above.
772−
773− it "can execute value_count aggregations" $ withTestEnv $ do
774− _ <- insertData
775− _ <- insertOther
776− let ags = mkAggregations "user_count" (ValueCountAgg (FieldValueCount (FieldName "user"))) <>
777− mkAggregations "bogus_count" (ValueCountAgg (FieldValueCount (FieldName "bogus")))
778− let search = mkAggregateSearch Nothing ags
779− let docCountPair k n = (k, object ["value" .= Number n])
780− res <- searchTweets search
781− liftIO $
782− fmap aggregations res `shouldBe` Right (Just (M.fromList [ docCountPair "user_count" 2
783− , docCountPair "bogus_count" 0
784− ]))
785−
786− it "can execute date_range aggregations" $ withTestEnv $ do
787− let now = fromGregorian 2015 3 14
788− let ltAMonthAgo = UTCTime (fromGregorian 2015 3 1) 0
789− let ltAWeekAgo = UTCTime (fromGregorian 2015 3 10) 0
790− let oldDoc = exampleTweet { postDate = ltAMonthAgo }
791− let newDoc = exampleTweet { postDate = ltAWeekAgo }
792− _ <- indexDocument testIndex testMapping defaultIndexDocumentSettings oldDoc (DocId "1")
793− _ <- indexDocument testIndex testMapping defaultIndexDocumentSettings newDoc (DocId "2")
794− _ <- refreshIndex testIndex
795− let thisMonth = DateRangeFrom (DateMathExpr (DMDate now) [SubtractTime 1 DMMonth])
796− let thisWeek = DateRangeFrom (DateMathExpr (DMDate now) [SubtractTime 1 DMWeek])
797− let agg = DateRangeAggregation (FieldName "postDate") Nothing (thisMonth :| [thisWeek])
798− let ags = mkAggregations "date_ranges" (DateRangeAgg agg)
799− let search = mkAggregateSearch Nothing ags
800− res <- searchTweets search
801− liftIO $ hitsTotal . searchHits <$> res `shouldBe` Right 2
802− let bucks = do magrs <- fmapL show (aggregations <$> res)
803− agrs <- note "no aggregations returned" magrs
804− rawBucks <- note "no date_ranges aggregation" $ M.lookup "date_ranges" agrs
805− parseEither parseJSON rawBucks
806− let fromMonthT = UTCTime (fromGregorian 2015 2 14) 0
807− let fromWeekT = UTCTime (fromGregorian 2015 3 7) 0
808− liftIO $ buckets <$> bucks `shouldBe` Right [ DateRangeResult "2015-02-14T00:00:00.000Z-*"
809− (Just fromMonthT)
810− (Just "2015-02-14T00:00:00.000Z")
811− Nothing
812− Nothing
813− 2
814− Nothing
815− , DateRangeResult "2015-03-07T00:00:00.000Z-*"
816− (Just fromWeekT)
817− (Just "2015-03-07T00:00:00.000Z")
818− Nothing
819− Nothing
820− 1
821− Nothing
822− ]
823−
824− it "returns date histogram aggregation results" $ withTestEnv $ do
825− _ <- insertData
826− let histogram = DateHistogramAgg $ mkDateHistogram (FieldName "postDate") Minute
827− let search = mkAggregateSearch Nothing (mkAggregations "byDate" histogram)
828− searchExpectAggs search
829− searchValidBucketAgg search "byDate" toDateHistogram
830−
831− it "can execute missing aggregations" $ withTestEnv $ do
832− _ <- insertData
833− _ <- insertExtra
834− let ags = mkAggregations "missing_agg" (MissingAgg (MissingAggregation "extra"))
835− let search = mkAggregateSearch Nothing ags
836− let docCountPair k n = (k, object ["doc_count" .= Number n])
837− res <- searchTweets search
838− liftIO $
839− fmap aggregations res `shouldBe` Right (Just (M.fromList [docCountPair "missing_agg" 1]))
840−
841− describe "Highlights API" $ do
842−
843− it "returns highlight from query when there should be one" $ withTestEnv $ do
844− _ <- insertData
845− _ <- insertOther
846− let query = QueryMatchQuery $ mkMatchQuery (FieldName "message") (QueryString "haskell")
847− let testHighlight = Highlights Nothing [FieldHighlight (FieldName "message") Nothing]
848−
849− let search = mkHighlightSearch (Just query) testHighlight
850− myHighlight <- searchTweetHighlight search
851− liftIO $
852− myHighlight `shouldBe` Right (Just (M.fromList [("message",["Use <em>haskell</em>!"])]))
853−
854− it "doesn't return highlight from a query when it shouldn't" $ withTestEnv $ do
855− _ <- insertData
856− _ <- insertOther
857− let query = QueryMatchQuery $ mkMatchQuery (FieldName "message") (QueryString "haskell")
858− let testHighlight = Highlights Nothing [FieldHighlight (FieldName "user") Nothing]
859−
860− let search = mkHighlightSearch (Just query) testHighlight
861− myHighlight <- searchTweetHighlight search
862− liftIO $
863− myHighlight `shouldBe` Right Nothing
864−
865− describe "Source filtering" $ do
866−
867− it "doesn't include source when sources are disabled" $ withTestEnv $ do
868− searchExpectSource
869− NoSource
870− (Left (EsError 500 "Source was missing"))
871−
872− it "includes a source" $ withTestEnv $ do
873− searchExpectSource
874− (SourcePatterns (PopPattern (Pattern "message")))
875− (Right (Object (HM.fromList [("message", String "Use haskell!")])))
876−
877− it "includes sources" $ withTestEnv $ do
878− searchExpectSource
879− (SourcePatterns (PopPatterns [Pattern "user", Pattern "message"]))
880− (Right (Object (HM.fromList [("user",String "bitemyapp"),("message", String "Use haskell!")])))
881−
882− it "includes source patterns" $ withTestEnv $ do
883− searchExpectSource
884− (SourcePatterns (PopPattern (Pattern "*ge")))
885− (Right (Object (HM.fromList [("age", Number 10000),("message", String "Use haskell!")])))
886−
887− it "excludes source patterns" $ withTestEnv $ do
888− searchExpectSource
889− (SourceIncludeExclude (Include [])
890− (Exclude [Pattern "l*", Pattern "*ge", Pattern "postDate", Pattern "extra"]))
891− (Right (Object (HM.fromList [("user",String "bitemyapp")])))
892−
893− describe "ToJSON RegexpFlags" $ do
894− it "generates the correct JSON for AllRegexpFlags" $
895− toJSON AllRegexpFlags `shouldBe` String "ALL"
896−
897− it "generates the correct JSON for NoRegexpFlags" $
898− toJSON NoRegexpFlags `shouldBe` String "NONE"
899−
900− it "generates the correct JSON for SomeRegexpFlags" $
901− let flags = AnyString :| [ Automaton
902− , Complement
903− , Empty
904− , Intersection
905− , Interval ]
906− in toJSON (SomeRegexpFlags flags) `shouldBe` String "ANYSTRING|AUTOMATON|COMPLEMENT|EMPTY|INTERSECTION|INTERVAL"
907−
908− prop "removes duplicates from flags" $ \(flags :: RegexpFlags) ->
909− let String str = toJSON flags
910− flagStrs = T.splitOn "|" str
911− in noDuplicates flagStrs
912−
913− describe "omitNulls" $ do
914− it "checks that omitNulls drops list elements when it should" $
915− let dropped = omitNulls $ [ "test1" .= (toJSON ([] :: [Int]))
916− , "test2" .= (toJSON ("some value" :: Text))]
917− in dropped `shouldBe` Object (HM.fromList [("test2", String "some value")])
918−
919− it "checks that omitNulls doesn't drop list elements when it shouldn't" $
920− let notDropped = omitNulls $ [ "test1" .= (toJSON ([1] :: [Int]))
921− , "test2" .= (toJSON ("some value" :: Text))]
922− in notDropped `shouldBe` Object (HM.fromList [ ("test1", Array (V.fromList [Number 1.0]))
923− , ("test2", String "some value")])
924− it "checks that omitNulls drops non list elements when it should" $
925− let dropped = omitNulls $ [ "test1" .= (toJSON Null)
926− , "test2" .= (toJSON ("some value" :: Text))]
927− in dropped `shouldBe` Object (HM.fromList [("test2", String "some value")])
928− it "checks that omitNulls doesn't drop non list elements when it shouldn't" $
929− let notDropped = omitNulls $ [ "test1" .= (toJSON (1 :: Int))
930− , "test2" .= (toJSON ("some value" :: Text))]
931− in notDropped `shouldBe` Object (HM.fromList [ ("test1", Number 1.0)
932− , ("test2", String "some value")])
93367 describe "Monoid (SearchHits a)" $ do
93468 prop "abides the monoid laws" $ eq $
93569 prop_Monoid (T :: T (SearchHits ()))
@@ -943,112 +77,6 @@ main = hspec $ do
94377 (dv <= maxBound) .&&.
94478 docVersionNumber dv === i
94579
946− describe "FsSnapshotRepo" $ do
947− prop "SnapshotRepo laws" $ \fsr ->
948− fromGSnapshotRepo (toGSnapshotRepo fsr) === Right (fsr :: FsSnapshotRepo)
949−
950− describe "snapshot repos" $ do
951− it "always parses all snapshot repos API" $ when' canSnapshot $ withTestEnv $ do
952− res <- getSnapshotRepos AllSnapshotRepos
953− liftIO $ case res of
954− Left e -> expectationFailure ("Expected a right but got Left " <> show e)
955− Right _ -> return ()
956−
957− it "finds an existing list of repos" $ when' canSnapshot $ withTestEnv $ do
958− let r1n = SnapshotRepoName "bloodhound-repo1"
959− let r2n = SnapshotRepoName "bloodhound-repo2"
960− withSnapshotRepo r1n $ \r1 ->
961− withSnapshotRepo r2n $ \r2 -> do
962− repos <- getSnapshotRepos (SnapshotRepoList (ExactRepo r1n :| [ExactRepo r2n]))
963− liftIO $ case repos of
964− Right xs -> do
965− let srt = L.sortBy (comparing gSnapshotRepoName)
966− srt xs `shouldBe` srt [r1, r2]
967− Left e -> expectationFailure (show e)
968−
969− it "creates and updates with updateSnapshotRepo" $ when' canSnapshot $ withTestEnv $ do
970− let r1n = SnapshotRepoName "bloodhound-repo1"
971− withSnapshotRepo r1n $ \r1 -> do
972− let Just (String dir) = HM.lookup "location" (gSnapshotRepoSettingsObject (gSnapshotRepoSettings r1))
973− let noCompression = FsSnapshotRepo r1n (T.unpack dir) False Nothing Nothing Nothing
974− resp <- updateSnapshotRepo defaultSnapshotRepoUpdateSettings noCompression
975− liftIO (validateStatus resp 200)
976− Right [roundtrippedNoCompression] <- getSnapshotRepos (SnapshotRepoList (ExactRepo r1n :| []))
977− liftIO (roundtrippedNoCompression `shouldBe` toGSnapshotRepo noCompression)
978−
979− -- verify came around in 1.4 it seems
980− it "can verify existing repos" $ when' canSnapshot $ when' (atleast es14) $ withTestEnv $ do
981− let r1n = SnapshotRepoName "bloodhound-repo1"
982− withSnapshotRepo r1n $ \_ -> do
983− res <- verifySnapshotRepo r1n
984− liftIO $ case res of
985− Right (SnapshotVerification vs)
986− | null vs -> expectationFailure "Expected nonempty set of verifying nodes"
987− | otherwise -> return ()
988− Left e -> expectationFailure (show e)
989−
990− describe "snapshots" $ do
991− it "always parses all snapshots API" $ when' canSnapshot $ withTestEnv $ do
992− let r1n = SnapshotRepoName "bloodhound-repo1"
993− withSnapshotRepo r1n $ \_ -> do
994− res <- getSnapshots r1n AllSnapshots
995− liftIO $ case res of
996− Left e -> expectationFailure ("Expected a right but got Left " <> show e)
997− Right _ -> return ()
998−
999− it "can parse a snapshot that it created" $ when' canSnapshot $ withTestEnv $ do
1000− let r1n = SnapshotRepoName "bloodhound-repo1"
1001− withSnapshotRepo r1n $ \_ -> do
1002− let s1n = SnapshotName "example-snapshot"
1003− withSnapshot r1n s1n $ do
1004− res <- getSnapshots r1n (SnapshotList (ExactSnap s1n :| []))
1005− liftIO $ case res of
1006− Right [snap]
1007− | snapInfoState snap == SnapshotSuccess &&
1008− snapInfoName snap == s1n -> return ()
1009− | otherwise -> expectationFailure (show snap)
1010− Right [] -> expectationFailure "There were no snapshots"
1011− Right snaps -> expectationFailure ("Expected 1 snapshot but got" <> show (length snaps))
1012− Left e -> expectationFailure (show e)
1013−
1014− describe "snapshot restore" $ do
1015− it "can restore a snapshot that we create" $ when' canSnapshot $ withTestEnv $ do
1016− let r1n = SnapshotRepoName "bloodhound-repo1"
1017− withSnapshotRepo r1n $ \_ -> do
1018− let s1n = SnapshotName "example-snapshot"
1019− withSnapshot r1n s1n $ do
1020− let settings = defaultSnapshotRestoreSettings { snapRestoreWaitForCompletion = True }
1021− -- have to close an index to restore it
1022− resp1 <- closeIndex testIndex
1023− liftIO (validateStatus resp1 200)
1024− resp2 <- restoreSnapshot r1n s1n settings
1025− liftIO (validateStatus resp2 200)
1026−
1027− it "can restore and rename" $ when' canSnapshot $ withTestEnv $ do
1028− let r1n = SnapshotRepoName "bloodhound-repo1"
1029− withSnapshotRepo r1n $ \_ -> do
1030− let s1n = SnapshotName "example-snapshot"
1031− withSnapshot r1n s1n $ do
1032− let pat = RestoreRenamePattern "bloodhound-tests-twitter-(\\d+)"
1033− let replace = RRTLit "restored-" :| [RRSubWholeMatch]
1034− let expectedIndex = IndexName "restored-bloodhound-tests-twitter-1"
1035− oldEnoughForOverrides <- liftIO (atleast es15)
1036− let overrides = RestoreIndexSettings { restoreOverrideReplicas = Just (ReplicaCount 0) }
1037− let settings = defaultSnapshotRestoreSettings { snapRestoreWaitForCompletion = True
1038− , snapRestoreRenamePattern = Just pat
1039− , snapRestoreRenameReplacement = Just replace
1040− , snapRestoreIndexSettingsOverrides = if oldEnoughForOverrides
1041− then Just overrides
1042− else Nothing
1043− }
1044− -- have to close an index to restore it
1045− let go = do
1046− resp <- restoreSnapshot r1n s1n settings
1047− liftIO (validateStatus resp 200)
1048− exists <- indexExists expectedIndex
1049− liftIO (exists `shouldBe` True)
1050− go `finally` deleteIndex expectedIndex
1051−
105280 describe "getNodesInfo" $ do
105381 it "fetches the responding node when LocalNode is used" $ withTestEnv $ do
105482 res <- getNodesInfo LocalNode
@@ -1079,7 +107,7 @@ main = hspec $ do
1079107 enumFrom (pred maxBound :: DocVersion) `shouldBe` [pred maxBound, maxBound]
1080108 enumFromThen minBound (pred maxBound :: DocVersion) `shouldBe` [minBound, pred maxBound]
1081109
1082− describe "scan&scroll API" $ do
110+ describe "Scan & Scroll API" $ do
1083111 it "returns documents using the scan&scroll API" $ withTestEnv $ do
1084112 _ <- insertData
1085113 _ <- insertOther
@@ -1091,173 +119,3 @@ main = hspec $ do
1091119 regular_search `shouldBe` Right exampleTweet -- Check that the size restrtiction is being honored
1092120 liftIO $
1093121 scan_search `shouldMatchList` [Just exampleTweet, Just otherTweet]
1094−
1095− describe "index aliases" $ do
1096− let aname = IndexAliasName (IndexName "bloodhound-tests-twitter-1-alias")
1097− let alias = IndexAlias (testIndex) aname
1098− let create = IndexAliasCreate Nothing Nothing
1099− let action = AddAlias alias create
1100− it "handles the simple case of aliasing an existing index" $ do
1101− withTestEnv $ do
1102− resetIndex
1103− resp <- updateIndexAliases (action :| [])
1104− liftIO $ validateStatus resp 200
1105− let cleanup = withTestEnv (updateIndexAliases (RemoveAlias alias :| []))
1106− (do aliases <- withTestEnv getIndexAliases
1107− let expected = IndexAliasSummary alias create
1108− case aliases of
1109− Right (IndexAliasesSummary summs) ->
1110− L.find ((== alias) . indexAliasSummaryAlias) summs `shouldBe` Just expected
1111− Left e -> expectationFailure ("Expected an IndexAliasesSummary but got " <> show e)) `finally` cleanup
1112− it "allows alias deletion" $ do
1113− aliases <- withTestEnv $ do
1114− resetIndex
1115− resp <- updateIndexAliases (action :| [])
1116− liftIO $ validateStatus resp 200
1117− _ <- deleteIndexAlias aname
1118− getIndexAliases
1119− -- let expected = IndexAliasSummary alias create
1120− case aliases of
1121− Right (IndexAliasesSummary summs) ->
1122− L.find ( (== aname)
1123− . indexAlias
1124− . indexAliasSummaryAlias
1125− ) summs
1126− `shouldBe` Nothing
1127− Left e -> expectationFailure ("Expected an IndexAliasesSummary but got " <> show e)
1128−
1129− describe "Index Listing" $ do
1130− it "returns a list of index names" $ withTestEnv $ do
1131− _ <- createExampleIndex
1132− ixns <- listIndices
1133− liftIO (ixns `shouldContain` [testIndex])
1134−
1135− describe "Index Settings" $ do
1136− it "persists settings" $ withTestEnv $ do
1137− _ <- deleteExampleIndex
1138− _ <- createExampleIndex
1139− let updates = BlocksWrite False :| []
1140− updateResp <- updateIndexSettings updates testIndex
1141− liftIO $ validateStatus updateResp 200
1142− getResp <- getIndexSettings testIndex
1143− liftIO $
1144− getResp `shouldBe` Right (IndexSettingsSummary
1145− testIndex
1146− (IndexSettings (ShardCount 1) (ReplicaCount 0))
1147− (NE.toList updates))
1148−
1149− it "allows total fields to be set" $ when' (atleast es50) $ withTestEnv $ do
1150− _ <- deleteExampleIndex
1151− _ <- createExampleIndex
1152− let updates = MappingTotalFieldsLimit 2500 :| []
1153− updateResp <- updateIndexSettings updates testIndex
1154− liftIO $ validateStatus updateResp 200
1155− getResp <- getIndexSettings testIndex
1156− liftIO $
1157− getResp `shouldBe` Right (IndexSettingsSummary
1158− testIndex
1159− (IndexSettings (ShardCount 1) (ReplicaCount 0))
1160− (NE.toList updates))
1161−
1162− it "accepts customer analyzers" $ when' (atleast es50) $ withTestEnv $ do
1163− _ <- deleteExampleIndex
1164− let analysis = Analysis
1165− (M.singleton "ex_analyzer"
1166− ( AnalyzerDefinition
1167− (Just (Tokenizer "ex_tokenizer"))
1168− (map TokenFilter
1169− [ "ex_filter_lowercase","ex_filter_uppercase","ex_filter_apostrophe"
1170− , "ex_filter_reverse","ex_filter_snowball"
1171− , "ex_filter_shingle"
1172− ]
1173− )
1174− )
1175− )
1176− (M.singleton "ex_tokenizer"
1177− ( TokenizerDefinitionNgram
1178− ( Ngram 3 4 [TokenLetter,TokenDigit])
1179− )
1180− )
1181− (M.fromList
1182− [ ("ex_filter_lowercase",TokenFilterDefinitionLowercase (Just Greek))
1183− , ("ex_filter_uppercase",TokenFilterDefinitionUppercase Nothing)
1184− , ("ex_filter_apostrophe",TokenFilterDefinitionApostrophe)
1185− , ("ex_filter_reverse",TokenFilterDefinitionReverse)
1186− , ("ex_filter_snowball",TokenFilterDefinitionSnowball English)
1187− , ("ex_filter_shingle",TokenFilterDefinitionShingle (Shingle 3 3 True False " " "_"))
1188− ]
1189− )
1190− updates = [AnalysisSetting analysis]
1191− createResp <- createIndexWith (updates ++ [NumberOfReplicas (ReplicaCount 0)]) 1 testIndex
1192− liftIO $ validateStatus createResp 200
1193− getResp <- getIndexSettings testIndex
1194− liftIO $
1195− getResp `shouldBe` Right (IndexSettingsSummary
1196− testIndex
1197− (IndexSettings (ShardCount 1) (ReplicaCount 0))
1198− updates
1199− )
1200−
1201− it "accepts default compression codec" $ when' (atleast es50) $ withTestEnv $ do
1202− _ <- deleteExampleIndex
1203− let updates = [CompressionSetting CompressionDefault]
1204− createResp <- createIndexWith (updates ++ [NumberOfReplicas (ReplicaCount 0)]) 1 testIndex
1205− liftIO $ validateStatus createResp 200
1206− getResp <- getIndexSettings testIndex
1207− liftIO $ getResp `shouldBe` Right
1208− (IndexSettingsSummary testIndex (IndexSettings (ShardCount 1) (ReplicaCount 0)) updates)
1209−
1210− it "accepts best compression codec" $ when' (atleast es50) $ withTestEnv $ do
1211− _ <- deleteExampleIndex
1212− let updates = [CompressionSetting CompressionBest]
1213− createResp <- createIndexWith (updates ++ [NumberOfReplicas (ReplicaCount 0)]) 1 testIndex
1214− liftIO $ validateStatus createResp 200
1215− getResp <- getIndexSettings testIndex
1216− liftIO $ getResp `shouldBe` Right
1217− (IndexSettingsSummary testIndex (IndexSettings (ShardCount 1) (ReplicaCount 0)) updates)
1218−
1219−
1220− describe "Index Optimization" $ do
1221− it "returns a successful response upon completion" $ withTestEnv $ do
1222− _ <- createExampleIndex
1223− resp <- forceMergeIndex (IndexList (testIndex :| [])) defaultForceMergeIndexSettings
1224− liftIO $ validateStatus resp 200
1225−
1226− describe "Suggest" $ do
1227− it "returns a search suggestion using the phrase suggester" $ withTestEnv $ do
1228− _ <- insertData
1229− let query = QueryMatchNoneQuery
1230− phraseSuggester = mkPhraseSuggester (FieldName "message")
1231− namedSuggester = Suggest "Use haskel" "suggest_name" (SuggestTypePhraseSuggester phraseSuggester)
1232− search' = mkSearch (Just query) Nothing
1233− search = search' { suggestBody = Just namedSuggester }
1234− expectedText = Just "use haskell"
1235− resp <- searchByIndex testIndex search
1236− parsed <- parseEsResponse resp :: BH IO (Either EsError (SearchResult Tweet))
1237− case parsed of
1238− Left e -> liftIO $ expectationFailure ("Expected an search suggestion but got " <> show e)
1239− Right sr -> liftIO $ (suggestOptionsText . head . suggestResponseOptions . head . nsrResponses <$> suggest sr) `shouldBe` expectedText
1240−
1241− describe "Script" $ do
1242− it "returns a transformed document based on the script field" $ withTestEnv $ do
1243− _ <- insertData
1244− let query = MatchAllQuery Nothing
1245− sfv = toJSON $
1246− Script
1247− (Just (ScriptLanguage "painless"))
1248− (Just (ScriptInline "doc['age'].value * 2"))
1249− Nothing
1250− Nothing
1251− sf = ScriptFields $
1252− HM.fromList [("test1", sfv)]
1253− search' = mkSearch (Just query) Nothing
1254− search = search' { scriptFields = Just sf }
1255− resp <- searchByIndex testIndex search
1256− parsed <- parseEsResponse resp :: BH IO (Either EsError (SearchResult Value))
1257− case parsed of
1258− Left e ->
1259− liftIO $ expectationFailure ("Expected a script-transformed result but got: " <> show e)
1260− Right sr -> do
1261− let Just results =
1262− hitFields (head (hits (searchHits sr)))
1263− liftIO $ results `shouldBe` (HitFields (M.fromList [("test1", [Number 20000.0])]))