bloodhound/tests/tests.hs

385 lines
15 KiB
Haskell
Raw Normal View History

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ScopedTypeVariables #-}
2014-04-07 03:29:46 +04:00
module Main where
import Control.Applicative
import Data.Aeson
import Data.List (nub)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time.Calendar (Day (..))
import Data.Time.Clock (UTCTime (..), secondsToDiffTime)
import Database.Bloodhound
import GHC.Generics (Generic)
import Network.HTTP.Client
import qualified Network.HTTP.Types.Status as NHTS
import Prelude hiding (filter, putStrLn)
import Test.Hspec
import Test.Hspec.QuickCheck (prop)
import Test.QuickCheck
testServer :: Server
testServer = Server "http://localhost:9200"
testIndex :: IndexName
testIndex = IndexName "bloodhound-tests-twitter-1"
testMapping :: MappingName
testMapping = MappingName "tweet"
validateStatus :: Response body -> Int -> Expectation
validateStatus resp expected =
(NHTS.statusCode $ responseStatus resp)
`shouldBe` (expected :: Int)
createExampleIndex :: IO Reply
2014-04-07 03:29:46 +04:00
createExampleIndex = createIndex testServer defaultIndexSettings testIndex
deleteExampleIndex :: IO Reply
2014-04-07 03:29:46 +04:00
deleteExampleIndex = deleteIndex testServer testIndex
data Location = Location { lat :: Double
, lon :: Double } deriving (Eq, Generic, Show)
2014-04-10 09:25:17 +04:00
data Tweet = Tweet { user :: Text
2014-04-07 03:29:46 +04:00
, postDate :: UTCTime
, message :: Text
, age :: Int
, location :: Location }
2014-04-07 03:29:46 +04:00
deriving (Eq, Generic, Show)
instance ToJSON Tweet
2014-04-07 03:29:46 +04:00
instance FromJSON Tweet
instance ToJSON Location
instance FromJSON Location
data TweetMapping = TweetMapping deriving (Eq, Show)
instance ToJSON TweetMapping where
toJSON TweetMapping =
object ["tweet" .=
2014-06-15 06:27:27 +04:00
object ["properties" .=
object ["location" .= object ["type" .= ("geo_point" :: Text)]]]]
2014-04-07 03:29:46 +04:00
exampleTweet :: Tweet
2014-04-07 04:53:53 +04:00
exampleTweet = Tweet { user = "bitemyapp"
2014-04-07 07:51:30 +04:00
, postDate = UTCTime
(ModifiedJulianDay 55000)
2014-04-07 04:53:53 +04:00
(secondsToDiffTime 10)
, message = "Use haskell!"
, age = 10000
, location = Location 40.12 (-71.34) }
otherTweet :: Tweet
2014-04-15 12:10:47 +04:00
otherTweet = Tweet { user = "notmyapp"
, postDate = UTCTime
(ModifiedJulianDay 55000)
(secondsToDiffTime 11)
, message = "Use haskell!"
, age = 1000
, location = Location 40.12 (-71.34) }
2014-04-10 09:25:17 +04:00
insertData :: IO ()
insertData = do
_ <- deleteExampleIndex
_ <- createExampleIndex
_ <- putMapping testServer testIndex testMapping TweetMapping
_ <- indexDocument testServer testIndex testMapping exampleTweet (DocId "1")
_ <- refreshIndex testServer testIndex
2014-04-10 09:25:17 +04:00
return ()
2014-04-15 12:10:47 +04:00
insertOther :: IO ()
insertOther = do
_ <- indexDocument testServer testIndex testMapping otherTweet (DocId "2")
2014-04-15 12:10:47 +04:00
_ <- refreshIndex testServer testIndex
return ()
searchTweet :: Search -> IO (Either String Tweet)
searchTweet search = do
2014-04-11 01:49:15 +04:00
reply <- searchByIndex testServer testIndex search
2014-04-10 09:25:17 +04:00
let result = eitherDecode (responseBody reply) :: Either String (SearchResult Tweet)
let myTweet = fmap (hitSource . head . hits . searchHits) result
return myTweet
searchExpectNoResults :: Search -> IO ()
searchExpectNoResults search = do
reply <- searchByIndex testServer testIndex search
let result = eitherDecode (responseBody reply) :: Either String (SearchResult Tweet)
let emptyHits = fmap (hits . searchHits) result
emptyHits `shouldBe` Right []
2014-04-12 11:56:33 +04:00
data BulkTest = BulkTest { name :: Text } deriving (Eq, Generic, Show)
instance FromJSON BulkTest
instance ToJSON BulkTest
noDuplicates :: Eq a => [a] -> Bool
noDuplicates xs = nub xs == xs
instance Arbitrary RegexpFlags where
arbitrary = oneof [ pure AllRegexpFlags
, pure NoRegexpFlags
, SomeRegexpFlags <$> arbitrary
]
instance Arbitrary a => Arbitrary (NonEmpty a) where
arbitrary = liftA2 (:|) arbitrary arbitrary
instance Arbitrary RegexpFlag where
arbitrary = oneof [ pure AnyString
, pure Automaton
, pure Complement
, pure Empty
, pure Intersection
, pure Interval
]
main :: IO ()
main = hspec $ do
2014-04-10 10:41:21 +04:00
describe "index create/delete API" $ do
it "creates and then deletes the requested index" $ do
-- priming state.
_ <- deleteExampleIndex
resp <- createExampleIndex
deleteResp <- deleteExampleIndex
validateStatus resp 200
validateStatus deleteResp 200
2014-04-10 09:25:17 +04:00
2014-04-07 03:29:46 +04:00
describe "document API" $ do
it "indexes, gets, and then deletes the generated document" $ do
2014-04-10 10:41:21 +04:00
_ <- insertData
docInserted <- getDocument testServer testIndex testMapping (DocId "1")
let newTweet = eitherDecode
(responseBody docInserted) :: Either String (EsResult Tweet)
2014-04-10 09:25:17 +04:00
fmap _source newTweet `shouldBe` Right exampleTweet
2014-04-12 11:56:33 +04:00
describe "bulk API" $ do
it "inserts all documents we request" $ do
_ <- insertData
let firstTest = BulkTest "blah"
let secondTest = BulkTest "bloo"
let firstDoc = BulkIndex testIndex
testMapping (DocId "2") (toJSON firstTest)
let secondDoc = BulkCreate testIndex
testMapping (DocId "3") (toJSON secondTest)
2014-04-12 11:56:33 +04:00
let stream = [firstDoc, secondDoc]
_ <- bulk testServer stream
2014-04-12 11:56:33 +04:00
_ <- refreshIndex testServer testIndex
fDoc <- getDocument testServer testIndex testMapping (DocId "2")
sDoc <- getDocument testServer testIndex testMapping (DocId "3")
let maybeFirst = eitherDecode $ responseBody fDoc :: Either String (EsResult BulkTest)
let maybeSecond = eitherDecode $ responseBody sDoc :: Either String (EsResult BulkTest)
fmap _source maybeFirst `shouldBe` Right firstTest
fmap _source maybeSecond `shouldBe` Right secondTest
2014-04-12 06:58:38 +04:00
describe "query API" $ do
it "returns document for term query and identity filter" $ do
_ <- insertData
let query = TermQuery (Term "user" "bitemyapp") Nothing
let filter = IdentityFilter <&&> IdentityFilter
let search = mkSearch (Just query) (Just filter)
myTweet <- searchTweet search
myTweet `shouldBe` Right exampleTweet
it "returns document for match query" $ do
_ <- insertData
let query = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
let search = mkSearch (Just query) Nothing
myTweet <- searchTweet search
myTweet `shouldBe` Right exampleTweet
2014-04-27 13:57:34 +04:00
it "returns document for multi-match query" $ do
_ <- insertData
let fields = [FieldName "user", FieldName "message"]
let query = QueryMultiMatchQuery $ mkMultiMatchQuery fields (QueryString "bitemyapp")
let search = mkSearch (Just query) Nothing
myTweet <- searchTweet search
myTweet `shouldBe` Right exampleTweet
2014-04-27 15:00:08 +04:00
it "returns document for bool query" $ do
_ <- insertData
let innerQuery = QueryMatchQuery $
mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
let query = QueryBoolQuery $
mkBoolQuery [innerQuery] [] []
2014-04-27 15:00:08 +04:00
let search = mkSearch (Just query) Nothing
myTweet <- searchTweet search
myTweet `shouldBe` Right exampleTweet
2014-04-27 16:22:12 +04:00
it "returns document for boosting query" $ do
_ <- insertData
let posQuery = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "bitemyapp")
let negQuery = QueryMatchQuery $ mkMatchQuery (FieldName "user") (QueryString "notmyapp")
let query = QueryBoostingQuery $ BoostingQuery posQuery negQuery (Boost 0.2)
let search = mkSearch (Just query) Nothing
myTweet <- searchTweet search
myTweet `shouldBe` Right exampleTweet
2014-04-27 22:25:50 +04:00
it "returns document for common terms query" $ do
_ <- insertData
let query = QueryCommonTermsQuery $
CommonTermsQuery (FieldName "user")
(QueryString "bitemyapp")
(CutoffFrequency 0.0001)
Or Or Nothing Nothing Nothing Nothing
let search = mkSearch (Just query) Nothing
myTweet <- searchTweet search
myTweet `shouldBe` Right exampleTweet
2014-04-15 12:10:47 +04:00
describe "sorting" $ do
it "returns documents in the right order" $ do
_ <- insertData
_ <- insertOther
let sortSpec = DefaultSortSpec $ mkSort (FieldName "age") Ascending
let search = Search Nothing
(Just IdentityFilter) (Just [sortSpec])
False 0 10
reply <- searchByIndex testServer testIndex search
let result = eitherDecode (responseBody reply) :: Either String (SearchResult Tweet)
let myTweet = fmap (hitSource . head . hits . searchHits) result
myTweet `shouldBe` Right otherTweet
describe "filtering API" $ do
it "returns document for composed boolmatch and identity" $ do
_ <- insertData
let queryFilter = BoolFilter (MustMatch (Term "user" "bitemyapp") False)
<&&> IdentityFilter
2014-04-12 03:29:10 +04:00
let search = mkSearch Nothing (Just queryFilter)
myTweet <- searchTweet search
myTweet `shouldBe` Right exampleTweet
2014-07-01 19:01:32 +04:00
it "returns document for term filter" $ do
_ <- insertData
let termFilter = TermFilter (Term "user" "bitemyapp") False
let search = mkSearch Nothing (Just termFilter)
myTweet <- searchTweet search
myTweet `shouldBe` Right exampleTweet
it "returns document for existential filter" $ do
_ <- insertData
2014-04-12 03:29:10 +04:00
let search = mkSearch Nothing (Just (ExistsFilter (FieldName "user")))
myTweet <- searchTweet search
myTweet `shouldBe` Right exampleTweet
it "returns document for geo boundingbox filter" $ do
2014-04-10 09:25:17 +04:00
_ <- insertData
let box = GeoBoundingBox (LatLon 40.73 (-74.1)) (LatLon 40.10 (-71.12))
let bbConstraint = GeoBoundingBoxConstraint (FieldName "tweet.location") box False GeoFilterMemory
let geoFilter = GeoBoundingBoxFilter bbConstraint
2014-04-12 03:29:10 +04:00
let search = mkSearch Nothing (Just geoFilter)
myTweet <- searchTweet search
2014-04-10 09:25:17 +04:00
myTweet `shouldBe` Right exampleTweet
2014-04-11 01:49:15 +04:00
it "doesn't return document for nonsensical boundingbox filter" $ do
2014-04-11 01:49:15 +04:00
_ <- insertData
let box = GeoBoundingBox (LatLon 0.73 (-4.1)) (LatLon 0.10 (-1.12))
let bbConstraint = GeoBoundingBoxConstraint (FieldName "tweet.location") box False GeoFilterMemory
let geoFilter = GeoBoundingBoxFilter bbConstraint
2014-04-12 03:29:10 +04:00
let search = mkSearch Nothing (Just geoFilter)
searchExpectNoResults search
2014-04-11 04:01:42 +04:00
it "returns document for geo distance filter" $ do
2014-04-11 04:01:42 +04:00
_ <- insertData
2014-04-12 03:29:10 +04:00
let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
2014-04-11 04:01:42 +04:00
let distance = Distance 10.0 Miles
let optimizeBbox = OptimizeGeoFilterType GeoFilterMemory
let geoFilter = GeoDistanceFilter geoPoint distance SloppyArc optimizeBbox False
2014-04-12 03:29:10 +04:00
let search = mkSearch Nothing (Just geoFilter)
2014-04-11 04:01:42 +04:00
myTweet <- searchTweet search
myTweet `shouldBe` Right exampleTweet
it "returns document for geo distance range filter" $ do
_ <- insertData
2014-04-12 03:29:10 +04:00
let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
let distanceRange = DistanceRange (Distance 0.0 Miles) (Distance 10.0 Miles)
let geoFilter = GeoDistanceRangeFilter geoPoint distanceRange
2014-04-12 03:29:10 +04:00
let search = mkSearch Nothing (Just geoFilter)
myTweet <- searchTweet search
myTweet `shouldBe` Right exampleTweet
it "doesn't return document for wild geo distance range filter" $ do
_ <- insertData
2014-04-12 03:29:10 +04:00
let geoPoint = GeoPoint (FieldName "tweet.location") (LatLon 40.12 (-71.34))
let distanceRange = DistanceRange (Distance 100.0 Miles) (Distance 1000.0 Miles)
let geoFilter = GeoDistanceRangeFilter geoPoint distanceRange
2014-04-12 03:29:10 +04:00
let search = mkSearch Nothing (Just geoFilter)
searchExpectNoResults search
it "returns document for geo polygon filter" $ do
_ <- insertData
let points = [LatLon 40.0 (-70.00),
LatLon 40.0 (-72.00),
LatLon 41.0 (-70.00),
LatLon 41.0 (-72.00)]
2014-04-12 03:29:10 +04:00
let geoFilter = GeoPolygonFilter (FieldName "tweet.location") points
let search = mkSearch Nothing (Just geoFilter)
myTweet <- searchTweet search
myTweet `shouldBe` Right exampleTweet
it "doesn't return document for bad geo polygon filter" $ do
_ <- insertData
let points = [LatLon 40.0 (-70.00),
LatLon 40.0 (-71.00),
LatLon 41.0 (-70.00),
LatLon 41.0 (-71.00)]
2014-04-12 03:29:10 +04:00
let geoFilter = GeoPolygonFilter (FieldName "tweet.location") points
let search = mkSearch Nothing (Just geoFilter)
searchExpectNoResults search
2014-04-11 05:04:24 +04:00
it "returns document for ids filter" $ do
_ <- insertData
let filter = IdsFilter (MappingName "tweet") [DocId "1"]
2014-04-12 03:29:10 +04:00
let search = mkSearch Nothing (Just filter)
2014-04-11 05:04:24 +04:00
myTweet <- searchTweet search
myTweet `shouldBe` Right exampleTweet
it "returns document for range filter" $ do
_ <- insertData
2014-04-12 03:29:10 +04:00
let filter = RangeFilter (FieldName "age")
(Right (RangeLtGt (LessThan 100000.0) (GreaterThan 1000.0)))
RangeExecutionIndex False
2014-04-12 03:29:10 +04:00
let search = mkSearch Nothing (Just filter)
myTweet <- searchTweet search
myTweet `shouldBe` Right exampleTweet
2014-04-11 10:08:36 +04:00
it "returns document for regexp filter" $ do
_ <- insertData
2014-04-12 03:29:10 +04:00
let filter = RegexpFilter (FieldName "user") (Regexp "bite.*app")
AllRegexpFlags (CacheName "test") False (CacheKey "key")
2014-04-12 03:29:10 +04:00
let search = mkSearch Nothing (Just filter)
2014-04-11 10:08:36 +04:00
myTweet <- searchTweet search
myTweet `shouldBe` Right exampleTweet
2014-04-11 10:10:06 +04:00
it "doesn't return document for non-matching regexp filter" $ do
_ <- insertData
2014-04-12 03:29:10 +04:00
let filter = RegexpFilter (FieldName "user")
(Regexp "boy") AllRegexpFlags
2014-04-12 03:29:10 +04:00
(CacheName "test") False (CacheKey "key")
let search = mkSearch Nothing (Just filter)
2014-04-11 10:10:06 +04:00
searchExpectNoResults search
2014-08-15 23:41:03 +04:00
describe "ToJSON RegexpFlags" $ do
it "generates the correct JSON for AllRegexpFlags" $
toJSON AllRegexpFlags `shouldBe` String "ALL"
it "generates the correct JSON for NoRegexpFlags" $
toJSON NoRegexpFlags `shouldBe` String "NONE"
it "generates the correct JSON for SomeRegexpFlags" $
let flags = AnyString :| [ Automaton
, Complement
, Empty
, Intersection
, Interval ]
in toJSON (SomeRegexpFlags flags) `shouldBe` String "ANYSTRING|AUTOMATON|COMPLEMENT|EMPTY|INTERSECTION|INTERVAL"
prop "removes duplicates from flags" $ \(flags :: RegexpFlags) ->
let String str = toJSON flags
flagStrs = T.splitOn "|" str
in noDuplicates flagStrs