1{-# LANGUAGE DeriveGeneric #-}
5import Database.Bloodhound
7import Data.Time.Calendar (Day(..))
8import Data.Time.Clock (secondsToDiffTime, UTCTime(..))
10import GHC.Generics (Generic)
11import Network.HTTP.Client
12import qualified Network.HTTP.Types.Status as NHTS
13import Prelude hiding (filter, putStrLn)
17testServer = Server "http://localhost:9200"
19testIndex = IndexName "bloodhound-tests-twitter-1"
20testMapping :: MappingName
21testMapping = MappingName "tweet"
23validateStatus :: Response body -> Int -> Expectation
24validateStatus resp expected =
25 (NHTS.statusCode $ responseStatus resp)
26 `shouldBe` (expected :: Int)
28createExampleIndex :: IO Reply
29createExampleIndex = createIndex testServer defaultIndexSettings testIndex
30deleteExampleIndex :: IO Reply
31deleteExampleIndex = deleteIndex testServer testIndex
33data Location = Location { lat :: Double
34 , lon :: Double } deriving (Eq, Generic, Show)
36data Tweet = Tweet { user :: Text
40 , location :: Location }
41 deriving (Eq, Generic, Show)
44instance FromJSON Tweet
45instance ToJSON Location
46instance FromJSON Location
48data TweetMapping = TweetMapping deriving (Eq, Show)
50instance ToJSON TweetMapping where
53 object ["properties" .=
54 object ["location" .= object ["type" .= ("geo_point" :: Text)]]]]
57exampleTweet = Tweet { user = "bitemyapp"
59 (ModifiedJulianDay 55000)
60 (secondsToDiffTime 10)
61 , message = "Use haskell!"
63 , location = Location 40.12 (-71.34) }
66otherTweet = Tweet { user = "notmyapp"
68 (ModifiedJulianDay 55000)
69 (secondsToDiffTime 11)
70 , message = "Use haskell!"
72 , location = Location 40.12 (-71.34) }
76 _ <- deleteExampleIndex
77 _ <- createExampleIndex
78 _ <- putMapping testServer testIndex testMapping TweetMapping
79 _ <- indexDocument testServer testIndex testMapping exampleTweet (DocId "1")
80 _ <- refreshIndex testServer testIndex
85 _ <- indexDocument testServer testIndex testMapping otherTweet (DocId "2")
86 _ <- refreshIndex testServer testIndex
89searchTweet :: Search -> IO (Either String Tweet)
90searchTweet search = do
91 reply <- searchByIndex testServer testIndex search
92 let result = eitherDecode (responseBody reply) :: Either String (SearchResult Tweet)
93 let myTweet = fmap (hitSource . head . hits . searchHits) result
96searchExpectNoResults :: Search -> IO ()
97searchExpectNoResults search = do
98 reply <- searchByIndex testServer testIndex search
99 let result = eitherDecode (responseBody reply) :: Either String (SearchResult Tweet)
100 let emptyHits = fmap (hits . searchHits) result
101 emptyHits `shouldBe` Right []
103data BulkTest = BulkTest { name :: Text } deriving (Eq, Generic, Show)
104instance FromJSON BulkTest
105instance ToJSON BulkTest
110 describe "index create/delete API" $ do
111 it "creates and then deletes the requested index" $ do
113 _ <- deleteExampleIndex
114 resp <- createExampleIndex
115 deleteResp <- deleteExampleIndex
116 validateStatus resp 200
117 validateStatus deleteResp 200
120 describe "document API" $ do
121 it "indexes, gets, and then deletes the generated document" $ do
123 docInserted <- getDocument testServer testIndex testMapping (DocId "1")
124 let newTweet = eitherDecode
125 (responseBody docInserted) :: Either String (EsResult Tweet)
126 fmap _source newTweet `shouldBe` Right exampleTweet
129 describe "bulk API" $ do
130 it "inserts all documents we request" $ do
132 let firstTest = BulkTest "blah"
133 let secondTest = BulkTest "bloo"
134 let firstDoc = BulkIndex testIndex
135 testMapping (DocId "2") (object ["name" .= String "blah"])
136 let secondDoc = BulkCreate testIndex
137 testMapping (DocId "3") (object ["name" .= String "bloo"])
138 let stream = [firstDoc, secondDoc]
139 _ <- bulk testServer stream
140 _ <- refreshIndex testServer testIndex
141 fDoc <- getDocument testServer testIndex testMapping (DocId "2")
142 sDoc <- getDocument testServer testIndex testMapping (DocId "3")
143 let maybeFirst = eitherDecode $ responseBody fDoc :: Either String (EsResult BulkTest)
144 let maybeSecond = eitherDecode $ responseBody sDoc :: Either String (EsResult BulkTest)
145 fmap _source maybeFirst `shouldBe` Right firstTest
146 fmap _source maybeSecond `shouldBe` Right secondTest
149 describe "query API" $ do
150 it "returns document for term query and identity filter" $ do
152 let query = TermQuery (Term "user" "bitemyapp") Nothing
153 let filter = IdentityFilter <&&> IdentityFilter
154 let search = mkSearch (Just query) (Just filter)
155 myTweet <- searchTweet search
156 myTweet `shouldBe` Right exampleTweet
158 it "returns document for match query" $ do
160 let query = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
161 let search = mkSearch (Just query) Nothing
162 myTweet <- searchTweet search
163 myTweet `shouldBe` Right exampleTweet
165 it "returns document for multi-match query" $ do
167 let fields = [FieldName "user", FieldName "message"]
168 let query = QueryMultiMatchQuery $ mkMultiMatchQuery fields (QueryString "bitemyapp")
169 let search = mkSearch (Just query) Nothing
170 myTweet <- searchTweet search
171 myTweet `shouldBe` Right exampleTweet
173 it "returns document for bool query" $ do
175 let innerQuery = QueryMatchQuery $
176 mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
177 let query = QueryBoolQuery $
178 mkBoolQuery (Just innerQuery) Nothing Nothing
179 let search = mkSearch (Just query) Nothing
180 myTweet <- searchTweet search
181 myTweet `shouldBe` Right exampleTweet
183 it "returns document for boosting query" $ do
185 let posQuery = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
186 let negQuery = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "notmyapp")
187 let query = QueryBoostingQuery $ BoostingQuery posQuery negQuery (Boost 0.2)
188 let search = mkSearch (Just query) Nothing
189 myTweet <- searchTweet search
190 myTweet `shouldBe` Right exampleTweet
192 it "returns document for common terms query" $ do
194 let query = QueryCommonTermsQuery $
195 CommonTermsQuery (FieldName "user")
196 (QueryString "bitemyapp")
197 (CutoffFrequency 0.0001)
198 Or Or Nothing Nothing Nothing Nothing
199 let search = mkSearch (Just query) Nothing
200 myTweet <- searchTweet search
201 myTweet `shouldBe` Right exampleTweet
204 describe "sorting" $ do
205 it "returns documents in the right order" $ do
208 let sortSpec = DefaultSortSpec $ mkSort (FieldName "age") Ascending
209 let search = Search Nothing
210 (Just IdentityFilter) (Just [sortSpec])
212 reply <- searchByIndex testServer testIndex search
213 let result = eitherDecode (responseBody reply) :: Either String (SearchResult Tweet)
214 let myTweet = fmap (hitSource . head . hits . searchHits) result
215 myTweet `shouldBe` Right otherTweet
218 describe "filtering API" $ do
219 it "returns document for composed boolmatch and identity" $ do
221 let queryFilter = BoolFilter (MustMatch (Term "user" "bitemyapp") False)
223 let search = mkSearch Nothing (Just queryFilter)
224 myTweet <- searchTweet search
225 myTweet `shouldBe` Right exampleTweet
227 it "returns document for existential filter" $ do
229 let search = mkSearch Nothing (Just (ExistsFilter (FieldName "user")))
230 myTweet <- searchTweet search
231 myTweet `shouldBe` Right exampleTweet
233 it "returns document for geo boundingbox filter" $ do
235 let box = GeoBoundingBox (LatLon 40.73 (-74.1)) (LatLon 40.10 (-71.12))
236 let bbConstraint = GeoBoundingBoxConstraint (FieldName "tweet.location") box False
237 let geoFilter = GeoBoundingBoxFilter bbConstraint GeoFilterMemory
238 let search = mkSearch Nothing (Just geoFilter)
239 myTweet <- searchTweet search
240 myTweet `shouldBe` Right exampleTweet
242 it "doesn't return document for nonsensical boundingbox filter" $ do
244 let box = GeoBoundingBox (LatLon 0.73 (-4.1)) (LatLon 0.10 (-1.12))
245 let bbConstraint = GeoBoundingBoxConstraint (FieldName "tweet.location") box False
246 let geoFilter = GeoBoundingBoxFilter bbConstraint GeoFilterMemory
247 let search = mkSearch Nothing (Just geoFilter)
248 searchExpectNoResults search
250 it "returns document for geo distance filter" $ do
252 let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
253 let distance = Distance 10.0 Miles
254 let optimizeBbox = OptimizeGeoFilterType GeoFilterMemory
255 let geoFilter = GeoDistanceFilter geoPoint distance SloppyArc optimizeBbox False
256 let search = mkSearch Nothing (Just geoFilter)
257 myTweet <- searchTweet search
258 myTweet `shouldBe` Right exampleTweet
260 it "returns document for geo distance range filter" $ do
262 let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
263 let distanceRange = DistanceRange (Distance 0.0 Miles) (Distance 10.0 Miles)
264 let geoFilter = GeoDistanceRangeFilter geoPoint distanceRange
265 let search = mkSearch Nothing (Just geoFilter)
266 myTweet <- searchTweet search
267 myTweet `shouldBe` Right exampleTweet
269 it "doesn't return document for wild geo distance range filter" $ do
271 let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
272 let distanceRange = DistanceRange (Distance 100.0 Miles) (Distance 1000.0 Miles)
273 let geoFilter = GeoDistanceRangeFilter geoPoint distanceRange
274 let search = mkSearch Nothing (Just geoFilter)
275 searchExpectNoResults search
277 it "returns document for geo polygon filter" $ do
279 let points = [LatLon 40.0 (-70.00),
280 LatLon 40.0 (-72.00),
281 LatLon 41.0 (-70.00),
282 LatLon 41.0 (-72.00)]
283 let geoFilter = GeoPolygonFilter (FieldName "tweet.location") points
284 let search = mkSearch Nothing (Just geoFilter)
285 myTweet <- searchTweet search
286 myTweet `shouldBe` Right exampleTweet
288 it "doesn't return document for bad geo polygon filter" $ do
290 let points = [LatLon 40.0 (-70.00),
291 LatLon 40.0 (-71.00),
292 LatLon 41.0 (-70.00),
293 LatLon 41.0 (-71.00)]
294 let geoFilter = GeoPolygonFilter (FieldName "tweet.location") points
295 let search = mkSearch Nothing (Just geoFilter)
296 searchExpectNoResults search
298 it "returns document for ids filter" $ do
300 let filter = IdsFilter (MappingName "tweet") [DocId "1"]
301 let search = mkSearch Nothing (Just filter)
302 myTweet <- searchTweet search
303 myTweet `shouldBe` Right exampleTweet
305 it "returns document for range filter" $ do
307 let filter = RangeFilter (FieldName "age")
308 (Right (RangeLtGt (LessThan 100000.0) (GreaterThan 1000.0)))
309 RangeExecutionIndex False
310 let search = mkSearch Nothing (Just filter)
311 myTweet <- searchTweet search
312 myTweet `shouldBe` Right exampleTweet
314 it "returns document for regexp filter" $ do
316 let filter = RegexpFilter (FieldName "user") (Regexp "bite.*app")
317 (RegexpFlags "ALL") (CacheName "test") False (CacheKey "key")
318 let search = mkSearch Nothing (Just filter)
319 myTweet <- searchTweet search
320 myTweet `shouldBe` Right exampleTweet
322 it "doesn't return document for non-matching regexp filter" $ do
324 let filter = RegexpFilter (FieldName "user")
325 (Regexp "boy") (RegexpFlags "ALL")
326 (CacheName "test") False (CacheKey "key")
327 let search = mkSearch Nothing (Just filter)
328 searchExpectNoResults search
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list