1{-# LANGUAGE DeriveGeneric #-}
5import Database.Bloodhound
7-- import Data.Aeson.Encode.Pretty
8-- import Data.ByteString.Lazy.Char8 (putStrLn)
9import Data.Time.Calendar (Day(..))
10import Data.Time.Clock (secondsToDiffTime, UTCTime(..))
11import Data.Text (Text)
12import GHC.Generics (Generic)
13import Network.HTTP.Conduit
14import qualified Network.HTTP.Types.Status as NHTS
15import Prelude hiding (filter, putStrLn)
19testServer = Server "http://localhost:9200"
21testIndex = IndexName "twitter"
22testMapping :: MappingName
23testMapping = MappingName "tweet"
25validateStatus :: Response body -> Int -> Expectation
26validateStatus resp expected =
27 (NHTS.statusCode $ responseStatus resp)
28 `shouldBe` (expected :: Int)
30createExampleIndex :: IO Reply
31createExampleIndex = createIndex testServer defaultIndexSettings testIndex
32deleteExampleIndex :: IO Reply
33deleteExampleIndex = deleteIndex testServer testIndex
35data Location = Location { lat :: Double
36 , lon :: Double } deriving (Eq, Generic, Show)
38data Tweet = Tweet { user :: Text
42 , location :: Location }
43 deriving (Eq, Generic, Show)
46instance FromJSON Tweet
47instance ToJSON Location
48instance FromJSON Location
50data TweetMapping = TweetMapping deriving (Eq, Show)
52instance ToJSON TweetMapping where
55 object ["properties" .=
56 object ["location" .= object ["type" .= ("geo_point" :: Text)]]]]
59exampleTweet = Tweet { user = "bitemyapp"
61 (ModifiedJulianDay 55000)
62 (secondsToDiffTime 10)
63 , message = "Use haskell!"
65 , location = Location 40.12 (-71.34) }
68otherTweet = Tweet { user = "notmyapp"
70 (ModifiedJulianDay 55000)
71 (secondsToDiffTime 11)
72 , message = "Use haskell!"
74 , location = Location 40.12 (-71.34) }
78 _ <- deleteExampleIndex
79 _ <- createExampleIndex
80 _ <- createMapping testServer testIndex testMapping TweetMapping
81 _ <- indexDocument testServer testIndex testMapping exampleTweet (DocId "1")
82 _ <- refreshIndex testServer testIndex
87 _ <- indexDocument testServer testIndex testMapping otherTweet (DocId "2")
88 _ <- refreshIndex testServer testIndex
91searchTweet :: Search -> IO (Either String Tweet)
92searchTweet search = do
93 reply <- searchByIndex testServer testIndex search
94 let result = eitherDecode (responseBody reply) :: Either String (SearchResult Tweet)
95 let myTweet = fmap (hitSource . head . hits . searchHits) result
98searchExpectNoResults :: Search -> IO ()
99searchExpectNoResults search = do
100 reply <- searchByIndex testServer testIndex search
101 let result = eitherDecode (responseBody reply) :: Either String (SearchResult Tweet)
102 let emptyHits = fmap (hits . searchHits) result
103 emptyHits `shouldBe` Right []
105data BulkTest = BulkTest { name :: Text } deriving (Eq, Generic, Show)
106instance FromJSON BulkTest
107instance ToJSON BulkTest
112 describe "index create/delete API" $ do
113 it "creates and then deletes the requested index" $ do
115 _ <- deleteExampleIndex
116 resp <- createExampleIndex
117 deleteResp <- deleteExampleIndex
118 validateStatus resp 200
119 validateStatus deleteResp 200
122 describe "document API" $ do
123 it "indexes, gets, and then deletes the generated document" $ do
125 docInserted <- getDocument testServer testIndex testMapping (DocId "1")
126 let newTweet = eitherDecode
127 (responseBody docInserted) :: Either String (EsResult Tweet)
128 fmap _source newTweet `shouldBe` Right exampleTweet
131 describe "bulk API" $ do
132 it "inserts all documents we request" $ do
134 let firstTest = BulkTest "blah"
135 let secondTest = BulkTest "bloo"
136 let firstDoc = BulkIndex (IndexName "twitter")
137 (MappingName "tweet") (DocId "2") (object ["name" .= String "blah"])
138 let secondDoc = BulkCreate (IndexName "twitter")
139 (MappingName "tweet") (DocId "3") (object ["name" .= String "bloo"])
140 let stream = [firstDoc, secondDoc]
141 _ <- bulk testServer stream
142 _ <- refreshIndex testServer testIndex
143 fDoc <- getDocument testServer testIndex testMapping (DocId "2")
144 sDoc <- getDocument testServer testIndex testMapping (DocId "3")
145 let maybeFirst = eitherDecode $ responseBody fDoc :: Either String (EsResult BulkTest)
146 let maybeSecond = eitherDecode $ responseBody sDoc :: Either String (EsResult BulkTest)
147 fmap _source maybeFirst `shouldBe` Right firstTest
148 fmap _source maybeSecond `shouldBe` Right secondTest
151 describe "query API" $ do
152 it "returns document for term query and identity filter" $ do
154 let query = TermQuery (Term "user" "bitemyapp") Nothing
155 let filter = IdentityFilter <&&> IdentityFilter
156 let search = mkSearch (Just query) (Just filter)
157 myTweet <- searchTweet search
158 myTweet `shouldBe` Right exampleTweet
160 it "returns document for match query" $ do
162 let query = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
163 let search = mkSearch (Just query) Nothing
164 myTweet <- searchTweet search
165 myTweet `shouldBe` Right exampleTweet
167 it "returns document for multi-match query" $ do
169 let fields = [FieldName "user", FieldName "message"]
170 let query = QueryMultiMatchQuery $ mkMultiMatchQuery fields (QueryString "bitemyapp")
171 let search = mkSearch (Just query) Nothing
172 myTweet <- searchTweet search
173 myTweet `shouldBe` Right exampleTweet
175 describe "sorting" $ do
176 it "returns documents in the right order" $ do
179 let sortSpec = DefaultSortSpec $ mkSort (FieldName "age") Ascending
180 let search = Search Nothing
181 (Just IdentityFilter) (Just [sortSpec])
183 reply <- searchByIndex testServer testIndex search
184 let result = eitherDecode (responseBody reply) :: Either String (SearchResult Tweet)
185 let myTweet = fmap (hitSource . head . hits . searchHits) result
186 myTweet `shouldBe` Right otherTweet
189 describe "filtering API" $ do
190 it "returns document for composed boolmatch and identity" $ do
192 let queryFilter = BoolFilter (MustMatch (Term "user" "bitemyapp") False)
194 let search = mkSearch Nothing (Just queryFilter)
195 myTweet <- searchTweet search
196 myTweet `shouldBe` Right exampleTweet
198 it "returns document for existential filter" $ do
200 let search = mkSearch Nothing (Just (ExistsFilter (FieldName "user")))
201 myTweet <- searchTweet search
202 myTweet `shouldBe` Right exampleTweet
204 it "returns document for geo boundingbox filter" $ do
206 let box = GeoBoundingBox (LatLon 40.73 (-74.1)) (LatLon 40.10 (-71.12))
207 let bbConstraint = GeoBoundingBoxConstraint (FieldName "tweet.location") box False
208 let geoFilter = GeoBoundingBoxFilter bbConstraint GeoFilterMemory
209 let search = mkSearch Nothing (Just geoFilter)
210 myTweet <- searchTweet search
211 myTweet `shouldBe` Right exampleTweet
213 it "doesn't return document for nonsensical boundingbox filter" $ do
215 let box = GeoBoundingBox (LatLon 0.73 (-4.1)) (LatLon 0.10 (-1.12))
216 let bbConstraint = GeoBoundingBoxConstraint (FieldName "tweet.location") box False
217 let geoFilter = GeoBoundingBoxFilter bbConstraint GeoFilterMemory
218 let search = mkSearch Nothing (Just geoFilter)
219 searchExpectNoResults search
221 it "returns document for geo distance filter" $ do
223 let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
224 let distance = Distance 10.0 Miles
225 let optimizeBbox = OptimizeGeoFilterType GeoFilterMemory
226 let geoFilter = GeoDistanceFilter geoPoint distance SloppyArc optimizeBbox False
227 let search = mkSearch Nothing (Just geoFilter)
228 myTweet <- searchTweet search
229 myTweet `shouldBe` Right exampleTweet
231 it "returns document for geo distance range filter" $ do
233 let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
234 let distanceRange = DistanceRange (Distance 0.0 Miles) (Distance 10.0 Miles)
235 let geoFilter = GeoDistanceRangeFilter geoPoint distanceRange
236 let search = mkSearch Nothing (Just geoFilter)
237 myTweet <- searchTweet search
238 myTweet `shouldBe` Right exampleTweet
240 it "doesn't return document for wild geo distance range filter" $ do
242 let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
243 let distanceRange = DistanceRange (Distance 100.0 Miles) (Distance 1000.0 Miles)
244 let geoFilter = GeoDistanceRangeFilter geoPoint distanceRange
245 let search = mkSearch Nothing (Just geoFilter)
246 searchExpectNoResults search
248 it "returns document for geo polygon filter" $ do
250 let points = [LatLon 40.0 (-70.00),
251 LatLon 40.0 (-72.00),
252 LatLon 41.0 (-70.00),
253 LatLon 41.0 (-72.00)]
254 let geoFilter = GeoPolygonFilter (FieldName "tweet.location") points
255 let search = mkSearch Nothing (Just geoFilter)
256 myTweet <- searchTweet search
257 myTweet `shouldBe` Right exampleTweet
259 it "doesn't return document for bad geo polygon filter" $ do
261 let points = [LatLon 40.0 (-70.00),
262 LatLon 40.0 (-71.00),
263 LatLon 41.0 (-70.00),
264 LatLon 41.0 (-71.00)]
265 let geoFilter = GeoPolygonFilter (FieldName "tweet.location") points
266 let search = mkSearch Nothing (Just geoFilter)
267 searchExpectNoResults search
269 it "returns document for ids filter" $ do
271 let filter = IdsFilter (MappingName "tweet") [DocId "1"]
272 let search = mkSearch Nothing (Just filter)
273 myTweet <- searchTweet search
274 myTweet `shouldBe` Right exampleTweet
276 it "returns document for range filter" $ do
278 let filter = RangeFilter (FieldName "age")
279 (Right (RangeLtGt (LessThan 100000.0) (GreaterThan 1000.0)))
280 RangeExecutionIndex False
281 let search = mkSearch Nothing (Just filter)
282 myTweet <- searchTweet search
283 myTweet `shouldBe` Right exampleTweet
285 it "returns document for regexp filter" $ do
287 let filter = RegexpFilter (FieldName "user") (Regexp "bite.*app")
288 (RegexpFlags "ALL") (CacheName "test") False (CacheKey "key")
289 let search = mkSearch Nothing (Just filter)
290 myTweet <- searchTweet search
291 myTweet `shouldBe` Right exampleTweet
293 it "doesn't return document for non-matching regexp filter" $ do
295 let filter = RegexpFilter (FieldName "user")
296 (Regexp "boy") (RegexpFlags "ALL")
297 (CacheName "test") False (CacheKey "key")
298 let search = mkSearch Nothing (Just filter)
299 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