bloodhound-1.0.0.0: tests/Test/TermsEnumSpec.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# OPTIONS_GHC -Wno-incomplete-uni-patterns -Wno-x-partial #-}
module Test.TermsEnumSpec (spec) where
import Data.Aeson.Key qualified as AK
import Data.Aeson.KeyMap qualified as KM
import Data.ByteString.Lazy.Char8 qualified as BL8
import Data.Text qualified as T
import Database.Bloodhound.Client.Cluster (BackendType (..))
import Database.Bloodhound.ElasticSearch7.Client qualified as ClientES7
import Database.Bloodhound.ElasticSearch7.Requests qualified as Requests
import Database.Bloodhound.ElasticSearch7.Types qualified as ES7Types
import Database.Bloodhound.ElasticSearch8.Client qualified as ClientES8
import Database.Bloodhound.ElasticSearch9.Client qualified as ClientES9
import TestsUtils.Common
import TestsUtils.Import
spec :: Spec
spec = do
describe "TermsEnumRequest JSON" $ do
it "encodes a field-only request as {\"field\":\"user\"}" $
let req = ES7Types.defaultTermsEnumRequest (FieldName "user")
Object o = toJSON req
in KM.toList o `shouldBe` [(AK.fromString "field", String "user")]
it "includes optional fields when provided and always carries field" $
let req =
(ES7Types.defaultTermsEnumRequest (FieldName "user"))
{ ES7Types.termsEnumRequestString = Just "bit",
ES7Types.termsEnumRequestSize = Just 5,
ES7Types.termsEnumRequestTimeout = Just "1s",
ES7Types.termsEnumRequestCaseInsensitive = Just True,
ES7Types.termsEnumRequestIndexFilter = Just (MatchAllQuery Nothing),
ES7Types.termsEnumRequestSearchAfter = Just "last-term"
}
Object o = toJSON req
in do
KM.lookup (AK.fromString "field") o `shouldBe` Just (String "user")
KM.lookup (AK.fromString "string") o `shouldBe` Just (String "bit")
KM.lookup (AK.fromString "size") o `shouldBe` Just (toJSON (5 :: Int))
KM.lookup (AK.fromString "timeout") o `shouldBe` Just (String "1s")
KM.lookup (AK.fromString "case_insensitive") o `shouldBe` Just (Bool True)
KM.member (AK.fromString "index_filter") o `shouldBe` True
KM.lookup (AK.fromString "index_filter") o `shouldBe` Just (toJSON (MatchAllQuery Nothing))
KM.lookup (AK.fromString "search_after") o `shouldBe` Just (String "last-term")
-- Removed fields copied by mistake from _field_caps: the
-- server silently ignores them. Their absence is the fix.
KM.member (AK.fromString "searchable") o `shouldBe` False
KM.member (AK.fromString "aggregatable") o `shouldBe` False
describe "TermsEnumOptions params" $ do
it "renders nothing for defaultTermsEnumOptions" $
ES7Types.termsEnumOptionsParams ES7Types.defaultTermsEnumOptions `shouldBe` []
it "renders allow_no_indices as a boolean string" $
let opts = ES7Types.defaultTermsEnumOptions {ES7Types.teoAllowNoIndices = Just False}
in lookup "allow_no_indices" (ES7Types.termsEnumOptionsParams opts) `shouldBe` Just (Just "false")
it "renders ignore_unavailable as a boolean string" $
let opts = ES7Types.defaultTermsEnumOptions {ES7Types.teoIgnoreUnavailable = Just True}
in lookup "ignore_unavailable" (ES7Types.termsEnumOptionsParams opts) `shouldBe` Just (Just "true")
it "renders expand_wildcards comma-joined" $
let opts =
ES7Types.defaultTermsEnumOptions
{ ES7Types.teoExpandWildcards = Just [ExpandWildcardsOpen, ExpandWildcardsClosed]
}
in lookup "expand_wildcards" (ES7Types.termsEnumOptionsParams opts)
`shouldBe` Just (Just "open,closed")
describe "TermsEnumResponse JSON" $ do
it "decodes the canonical ES doc example" $
let bytes =
BL8.pack
"{\"_shards\":{\"total\":1,\"successful\":1,\"skipped\":0,\"failed\":0},\"terms\":[\"bitemyapp\",\"notmyapp\"],\"complete\":true}"
Just resp = decode bytes :: Maybe ES7Types.TermsEnumResponse
in do
ES7Types.termsEnumResponseComplete resp `shouldBe` True
ES7Types.termsEnumResponseTerms resp
`shouldBe` [ES7Types.TermValue "bitemyapp", ES7Types.TermValue "notmyapp"]
ES7Types.termsEnumResponseShards resp `shouldSatisfy` isJust
it "defaults missing terms to []" $
let bytes = BL8.pack "{\"complete\":true}"
Just resp = decode bytes :: Maybe ES7Types.TermsEnumResponse
in ES7Types.termsEnumResponseTerms resp `shouldBe` []
it "defaults missing complete to False (conservative)" $
let bytes = BL8.pack "{\"terms\":[\"foo\"]}"
Just resp = decode bytes :: Maybe ES7Types.TermsEnumResponse
in ES7Types.termsEnumResponseComplete resp `shouldBe` False
it "treats missing _shards as Nothing" $
let bytes = BL8.pack "{\"terms\":[\"foo\"],\"complete\":true}"
Just resp = decode bytes :: Maybe ES7Types.TermsEnumResponse
in ES7Types.termsEnumResponseShards resp `shouldBe` Nothing
it "decodes an empty terms array" $
let bytes = BL8.pack "{\"terms\":[],\"complete\":false}"
Just resp = decode bytes :: Maybe ES7Types.TermsEnumResponse
in ES7Types.termsEnumResponseTerms resp `shouldBe` []
describe "Terms Enum endpoint shape" $ do
-- Pure checks against the BHRequest shape; no live backend needed.
it "GETs /{index}/_terms_enum with a body" $ do
let req = Requests.getTermsEnum (Just [[qqIndexName|twitter|]]) (FieldName "user") Nothing
bhRequestMethod req `shouldBe` "GET"
getRawEndpoint (bhRequestEndpoint req) `shouldBe` ["twitter", "_terms_enum"]
bhRequestBody req `shouldSatisfy` isJust
it "encodes the field (and optional string) into the request body" $ do
let req = Requests.getTermsEnum (Just [[qqIndexName|twitter|]]) (FieldName "user") (Just "bit")
Just body = bhRequestBody req
Just (Object o) = decode body :: Maybe Value
in KM.lookup (AK.fromString "field") o `shouldBe` Just (String "user")
it "comma-joins a non-empty index list into the leading segment" $ do
let req = Requests.getTermsEnum (Just [[qqIndexName|twitter-1|], [qqIndexName|twitter-2|]]) (FieldName "user") Nothing
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["twitter-1,twitter-2", "_terms_enum"]
it "treats Just [] the same as Nothing (bare _terms_enum path)" $ do
let reqNo = Requests.getTermsEnum Nothing (FieldName "user") Nothing
reqEmpty = Requests.getTermsEnum (Just []) (FieldName "user") Nothing
getRawEndpoint (bhRequestEndpoint reqNo)
`shouldBe` getRawEndpoint (bhRequestEndpoint reqEmpty)
it "honours TermsEnumOptions in the query string" $ do
let req =
Requests.getTermsEnumWith
(Just [[qqIndexName|twitter|]])
( ES7Types.defaultTermsEnumOptions
{ ES7Types.teoAllowNoIndices = Just True
}
)
(ES7Types.defaultTermsEnumRequest (FieldName "user"))
getRawEndpointQueries (bhRequestEndpoint req)
`shouldBe` [("allow_no_indices", Just "true")]
describe "Terms Enum endpoint" $
backendSpecific [ElasticSearch7, ElasticSearch8, ElasticSearch9] $ do
-- _terms_enum only enumerates keyword / numeric / ip / boolean
-- fields; the test mapping's "user" is an analyzed text field and
-- returns no terms, so the live assertions target the "extra"
-- keyword field populated by 'insertExtra'.
it "returns the indexed term for a keyword field" $
withTestEnv $ do
_ <- insertData
_ <- insertExtra
resp <- getTermsEnumForBackend (Just [testIndex]) (FieldName "extra") Nothing
liftIO $
ES7Types.termsEnumResponseTerms resp
`shouldSatisfy` elem (ES7Types.TermValue "blah blah")
it "narrows results with the string prefix" $
withTestEnv $ do
_ <- insertData
_ <- insertExtra
resp <- getTermsEnumForBackend (Just [testIndex]) (FieldName "extra") (Just "blah")
liftIO $ do
let terms = ES7Types.termsEnumResponseTerms resp
terms `shouldSatisfy` all (\(ES7Types.TermValue t) -> "blah" `T.isPrefixOf` t)
terms `shouldSatisfy` elem (ES7Types.TermValue "blah blah")
it "returns no terms for a non-matching prefix" $
withTestEnv $ do
_ <- insertData
_ <- insertExtra
resp <- getTermsEnumForBackend (Just [testIndex]) (FieldName "extra") (Just "zzzzz")
liftIO $
ES7Types.termsEnumResponseTerms resp `shouldBe` []
-- | Dispatch to the right getTermsEnum client based on the detected
-- backend. The wire format is identical across ES 7.10+, 8.x and 9.x;
-- the dispatch exists only because each version has its own
-- 'WithBackend' constraint.
getTermsEnumForBackend ::
Maybe [IndexName] ->
FieldName ->
Maybe Text ->
BH IO ES7Types.TermsEnumResponse
getTermsEnumForBackend mIndices field mString = do
backend <- liftIO detectBackendType
case backend of
Just ElasticSearch7 -> ClientES7.getTermsEnum mIndices field mString
Just ElasticSearch8 -> ClientES8.getTermsEnum mIndices field mString
Just ElasticSearch9 -> ClientES9.getTermsEnum mIndices field mString
_ -> ClientES8.getTermsEnum mIndices field mString