bloodhound-1.0.0.0: tests/Test/DocumentsSpec.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Test.DocumentsSpec where
import Control.Concurrent (threadDelay)
import Data.List (isInfixOf)
import Data.Word (Word64)
import Numeric.Natural
import TestsUtils.Common
import TestsUtils.Import
spec :: Spec
spec =
describe "document API" $ do
it "indexes, updates, gets, and then deletes the generated document" $
withTestEnv $ do
_ <- insertData
_ <- updateData
newTweet <- performBHRequest $ getDocument @Tweet testIndex (DocId "1")
liftIO $ getSource newTweet `shouldBe` Just patchedTweet
it "indexes, gets, and then deletes the generated document with a DocId containing a space" $
withTestEnv $ do
_ <- insertWithSpaceInId
newTweet <- performBHRequest $ getDocument @Tweet testIndex (DocId "Hello World")
liftIO $ getSource newTweet `shouldBe` Just exampleTweet
it "produces a parseable result when looking up a bogus document" $
withTestEnv $ do
noTweet <- performBHRequest $ getDocument @Tweet testIndex (DocId "bogus")
liftIO $ foundResult noTweet `shouldBe` Nothing
it "can use optimistic concurrency control" $
withTestEnv $ do
-- ExternalGTE only conflicts when the supplied version is
-- strictly lower than the stored one (@external_gt@, which
-- conflicted on equality, was removed in bloodhound-41e), so
-- seed the doc with a higher version then re-index a stale
-- (lower) one to trigger the 409.
let cfg = defaultIndexDocumentSettings {idsVersionControl = ExternalGTE (ExternalDocVersion (succ minBound))}
let cfgStale = defaultIndexDocumentSettings {idsVersionControl = ExternalGTE (ExternalDocVersion minBound)}
resetIndex
(res, _) <- insertData' cfg
liftIO $ isCreated res `shouldBe` True
insertedConflict <- tryEsError $ insertData' cfgStale
case insertedConflict of
Right (res', _) -> liftIO $ isVersionConflict res' `shouldBe` True
Left e -> liftIO $ errorStatus e `shouldBe` Just 409
it "can use predicates on the response" $
withTestEnv $ do
let cfg = defaultIndexDocumentSettings {idsVersionControl = ExternalGTE (ExternalDocVersion (succ minBound))}
let cfgStale = defaultIndexDocumentSettings {idsVersionControl = ExternalGTE (ExternalDocVersion minBound)}
resetIndex
(res, _) <- insertData' cfg
liftIO $ isCreated res `shouldBe` True
liftIO $ isSuccess res `shouldBe` True
liftIO $ isVersionConflict res `shouldBe` False
res' <- insertData'' cfgStale
liftIO $ isCreated res' `shouldBe` False
liftIO $ isSuccess res' `shouldBe` False
liftIO $ isVersionConflict res' `shouldBe` True
it "indexes two documents in a parent/child relationship and checks that the child exists" $
withTestEnv $ do
resetIndex
_ <- performBHRequest $ putMapping @Value testIndex ConversationMapping
let parentSettings = defaultIndexDocumentSettings {idsJoinRelation = Just (ParentDocument (FieldName "reply_join") (RelationName "message"))}
let childSettings = defaultIndexDocumentSettings {idsJoinRelation = Just (ChildDocument (FieldName "reply_join") (RelationName "reply") (DocId "1"))}
_ <- performBHRequest $ indexDocument testIndex parentSettings exampleTweet (DocId "1")
_ <- performBHRequest $ indexDocument testIndex childSettings otherTweet (DocId "2")
_ <- performBHRequest $ refreshIndex testIndex
let query = QueryHasParentQuery $ HasParentQuery (RelationName "message") (MatchAllQuery Nothing) Nothing Nothing
let search = mkSearch (Just query) Nothing
response <- performBHRequest $ searchByIndex @Value testIndex search
liftIO $
hitsTotal (searchHits response) `shouldBe` Just (HitsTotal 1 HTR_EQ)
it "updates documents by query" $ withTestEnv $ do
_ <- insertData
_ <- insertOther
_ <- insertExtra
let query = (TermQuery (Term "user" "bitemyapp") Nothing)
script =
Script
(Just (ScriptLanguage "painless"))
(ScriptInline "ctx._source.age *= 2")
Nothing
Nothing
_ <- performBHRequest $ updateByQuery @Value testIndex query (Just script)
_ <- performBHRequest $ refreshIndex testIndex
let search = mkSearch (Just query) Nothing
parsed <- searchTweets search
case parsed of
Left e ->
liftIO $ expectationFailure ("Expected tweets as search result but got: " <> show e)
Right sr -> do
let results =
map (fmap age . hitSource) (hits (searchHits sr))
liftIO $
results `shouldBe` [Just $ age exampleTweet * 2, Just $ age tweetWithExtra * 2]
describe "IndexDocumentSettings URI params" $ do
it "op_type=create succeeds when the document does not exist yet" $
withTestEnv $ do
resetIndex
let cfg = defaultIndexDocumentSettings {idsOpType = Just OpCreate}
res <- performBHRequest $ indexDocument testIndex cfg exampleTweet (DocId "op-create-1")
liftIO $ idxDocResult res `shouldBe` "created"
it "op_type=create yields a 409 version-conflict on an existing doc" $
withTestEnv $ do
resetIndex
let cfg = defaultIndexDocumentSettings {idsOpType = Just OpCreate}
_ <- performBHRequest $ indexDocument testIndex cfg exampleTweet (DocId "op-create-2")
secondAttempt <- tryEsError $ dispatch $ indexDocument testIndex cfg exampleTweet (DocId "op-create-2")
case secondAttempt of
Right resp ->
liftIO $ isVersionConflict resp `shouldBe` True
Left e ->
liftIO $ errorStatus e `shouldBe` Just 409
it "refresh=wait_for makes the doc searchable without an explicit refresh" $
withTestEnv $ do
resetIndex
let cfg = defaultIndexDocumentSettings {idsRefresh = Just RefreshWaitFor}
_ <- performBHRequest $ indexDocument testIndex cfg exampleTweet (DocId "wait-for-1")
-- No refreshIndex call here — RefreshWaitFor should make the doc
-- visible to the search below.
let search = mkSearch (Just (TermQuery (Term "user" "bitemyapp") Nothing)) Nothing
result <- searchTweets search
case result of
Left e ->
liftIO $ expectationFailure ("Search failed after refresh=wait_for: " <> show e)
Right sr ->
liftIO $ hitsTotal (searchHits sr) `shouldBe` Just (HitsTotal 1 HTR_EQ)
it "wait_for_active_shards=all succeeds on a single-shard index" $
withTestEnv $ do
resetIndex
let cfg =
defaultIndexDocumentSettings
{ idsWaitForActiveShards = Just AllActiveShards
}
res <- performBHRequest $ indexDocument testIndex cfg exampleTweet (DocId "wfas-1")
liftIO $ idxDocResult res `shouldBe` "created"
it "if_seq_no/if_primary_term rejects a stale sequence number with 409" $
withTestEnv $ do
resetIndex
-- First write succeeds and gives us the current seq_no/primary_term.
let createCfg = defaultIndexDocumentSettings {idsOpType = Just OpCreate}
first' <- performBHRequest $ indexDocument testIndex createCfg exampleTweet (DocId "seqno-1")
-- Now attempt a second write with a deliberately stale seq_no
-- (offset by +1 from the actual one); the server must reject
-- the write with HTTP 409.
let staleSeqNo = fromIntegral (idxDocSeqNo first') + 1
staleTerm = fromIntegral (idxDocPrimaryTerm first')
staleCfg =
defaultIndexDocumentSettings
{ idsIfSeqNo = Just staleSeqNo,
idsIfPrimaryTerm = Just staleTerm
}
conflict <- tryEsError $ dispatch $ indexDocument testIndex staleCfg exampleTweet (DocId "seqno-1")
case conflict of
Right resp ->
liftIO $ isVersionConflict resp `shouldBe` True
Left e ->
liftIO $ errorStatus e `shouldBe` Just 409
it "if_seq_no/if_primary_term accepts the current sequence number" $
withTestEnv $ do
resetIndex
let createCfg = defaultIndexDocumentSettings {idsOpType = Just OpCreate}
first' <- performBHRequest $ indexDocument testIndex createCfg exampleTweet (DocId "seqno-2")
let currSeqNo = fromIntegral (idxDocSeqNo first')
currTerm = fromIntegral (idxDocPrimaryTerm first')
matchingCfg =
defaultIndexDocumentSettings
{ idsIfSeqNo = Just currSeqNo,
idsIfPrimaryTerm = Just currTerm
}
res <- performBHRequest $ indexDocument testIndex matchingCfg exampleTweet (DocId "seqno-2")
liftIO $ idxDocResult res `shouldBe` "updated"
it "if_seq_no without if_primary_term is rejected by the server (not silently dropped)" $
withTestEnv $ do
resetIndex
let halfSetCfg = defaultIndexDocumentSettings {idsIfSeqNo = Just 0}
result <- tryEsError $ dispatch $ indexDocument testIndex halfSetCfg exampleTweet (DocId "seqno-half")
case result of
Right resp ->
-- Server should reject the half-set request rather than
-- silently indexing without OCC.
liftIO $ isSuccess resp `shouldBe` False
Left e ->
liftIO $ errorStatus e `shouldBe` Just 400
describe "GetDocumentOptions URI params" $ do
it "_source=false returns a found document with no source" $
withTestEnv $ do
_ <- insertData
let opts = defaultGetDocumentOptions {gdoSource = Just False}
result <- performBHRequest $ getDocumentWith @Tweet opts testIndex (DocId "1")
-- With _source=false, ES omits the _source key. The parser
-- therefore cannot build an EsResultFound, so foundResult is
-- Nothing even though the document exists (HTTP 200, _id set).
liftIO $ getSource result `shouldBe` Nothing
it "_source=true preserves the full source (byte-identical to default)" $
withTestEnv $ do
_ <- insertData
let opts = defaultGetDocumentOptions {gdoSource = Just True}
result <- performBHRequest $ getDocumentWith @Tweet opts testIndex (DocId "1")
liftIO $ getSource result `shouldBe` Just exampleTweet
it "defaultGetDocumentOptions matches the parameterless getDocument" $
withTestEnv $ do
_ <- insertData
withOpts <- performBHRequest $ getDocumentWith @Tweet defaultGetDocumentOptions testIndex (DocId "1")
withoutOpts <- performBHRequest $ getDocument @Tweet testIndex (DocId "1")
liftIO $ getSource withOpts `shouldBe` getSource withoutOpts
it "routing param targets a routed document" $
withTestEnv $ do
resetIndex
let routed = defaultIndexDocumentSettings {idsJoinRelation = Just (ParentDocument (FieldName "reply_join") (RelationName "message"))}
_ <- performBHRequest $ putMapping @Value testIndex ConversationMapping
_ <- performBHRequest $ indexDocument testIndex routed exampleTweet (DocId "rt-1")
-- Without routing, the GET would miss the shard; with the
-- matching routing it must find the document.
let opts = defaultGetDocumentOptions {gdoRouting = Just "rt-1"}
result <- performBHRequest $ getDocumentWith @Tweet opts testIndex (DocId "rt-1")
liftIO $ foundResult result `shouldSatisfy` isJust
-- bloodhound-65e: the parsed EsResultFound must surface the
-- _routing the document was indexed with, so callers can feed
-- it back into follow-up writes on the same shard.
let Just found = foundResult result
liftIO $ _routing found `shouldBe` Just "rt-1"
it "surfaces _seq_no and _primary_term on a fresh GET (65e)" $
withTestEnv $ do
(_, doc) <- insertData' defaultIndexDocumentSettings
result <- performBHRequest $ getDocument @Tweet testIndex (DocId "1")
let Just found = foundResult result
liftIO $ do
-- _seq_no / _primary_term must round-trip from the
-- IndexedDocument write response to the GET response so
-- callers can use them for if_seq_no / if_primary_term.
_seq_no found `shouldBe` Just (idxDocSeqNo doc)
_primary_term found `shouldBe` Just (idxDocPrimaryTerm doc)
describe "getDocumentSource (GET /{index}/_source/{id})" $ do
it "returns the source of an existing document as Just" $
withTestEnv $ do
_ <- insertData
source <- performBHRequest $ getDocumentSource @Tweet testIndex (DocId "1")
liftIO $ source `shouldBe` Just exampleTweet
it "returns Nothing for a missing document" $
withTestEnv $ do
_ <- insertData
-- The source-only endpoint responds with HTTP 404 (no
-- {\"found\": false} envelope) when the document does not
-- exist. The custom parser must therefore surface
-- 'Nothing' rather than throw an EsError.
source <- performBHRequest $ getDocumentSource @Tweet testIndex (DocId "bogus")
liftIO $ source `shouldBe` Nothing
it "404 surfaces as Right Nothing through tryPerformBHRequest" $
withTestEnv $ do
_ <- insertData
parsed <- tryPerformBHRequest $ getDocumentSource @Tweet testIndex (DocId "bogus")
liftIO $ parsed `shouldBe` Right (Nothing :: Maybe Tweet)
it "defaultGetDocumentSourceOptions matches the parameterless getDocumentSource" $
withTestEnv $ do
_ <- insertData
withOpts <- performBHRequest $ getDocumentSourceWith @Tweet defaultGetDocumentSourceOptions testIndex (DocId "1")
withoutOpts <- performBHRequest $ getDocumentSource @Tweet testIndex (DocId "1")
liftIO $ withOpts `shouldBe` withoutOpts
it "_source_includes filters the returned source fields" $
withTestEnv $ do
_ <- insertData
-- Parse as Value so we can inspect the partial source
-- without requiring every Tweet field to be present
-- (deriveJSON defaultOptions would otherwise reject a
-- source missing the @age@, @message@, etc. fields).
let opts = defaultGetDocumentSourceOptions {gdsoSourceIncludes = Just "user"}
source <- performBHRequest $ getDocumentSourceWith @Value opts testIndex (DocId "1")
liftIO $ source `shouldSatisfy` isJust
case source of
Just v ->
liftIO $ do
show v `shouldSatisfy` isInfixOf "\"user\""
-- With _source_includes=user, the server returns a
-- source object containing only the "user" field.
show v `shouldSatisfy` not . isInfixOf "\"age\""
show v `shouldSatisfy` not . isInfixOf "\"message\""
Nothing ->
liftIO $ expectationFailure "expected a source"
it "routing param targets a routed document" $
withTestEnv $ do
resetIndex
let routed = defaultIndexDocumentSettings {idsJoinRelation = Just (ParentDocument (FieldName "reply_join") (RelationName "message"))}
_ <- performBHRequest $ putMapping @Value testIndex ConversationMapping
_ <- performBHRequest $ indexDocument testIndex routed exampleTweet (DocId "rt-src-1")
-- Without routing, the source GET would miss the shard
-- (HTTP 404 → Nothing); with the matching routing it must
-- find the document.
let opts = defaultGetDocumentSourceOptions {gdsoRouting = Just "rt-src-1"}
source <- performBHRequest $ getDocumentSourceWith @Tweet opts testIndex (DocId "rt-src-1")
liftIO $ source `shouldBe` Just exampleTweet
describe "documentSourceExists (HEAD /{index}/_source/{id})" $ do
it "returns True when the document has a _source" $
withTestEnv $ do
_ <- insertData
present <- performBHRequest $ documentSourceExists testIndex (DocId "1")
liftIO $ present `shouldBe` True
it "returns False for a document that never existed" $
withTestEnv $ do
_ <- insertData
present <- performBHRequest $ documentSourceExists testIndex (DocId "bogus")
liftIO $ present `shouldBe` False
it "returns False after the document is deleted" $
withTestEnv $ do
_ <- insertData
_ <- performBHRequest $ deleteDocument testIndex (DocId "1")
present <- performBHRequest $ documentSourceExists testIndex (DocId "1")
liftIO $ present `shouldBe` False
describe "deleteDocumentWith (DELETE /{index}/_doc/{id} URI params)" $ do
it "refresh=Just RefreshTrue makes the deletion immediately visible" $
withTestEnv $ do
_ <- insertData
-- No manual refreshIndex: the refresh=True URI param should
-- make the delete visible to the immediately-following HEAD.
let opts = defaultDeleteDocumentOptions {ddoRefresh = Just RefreshTrue}
_ <- performBHRequest $ deleteDocumentWith opts testIndex (DocId "1")
present <- performBHRequest $ documentSourceExists testIndex (DocId "1")
liftIO $ present `shouldBe` False
it "if_seq_no/if_primary_term: stale pair yields a 409 version conflict" $
withTestEnv $ do
resetIndex
-- First index: capture the document's initial seq_no and
-- primary_term straight from the 'IndexedDocument' body.
(_, doc) <- insertData' defaultIndexDocumentSettings
let initialSeqNo = fromIntegral (idxDocSeqNo doc) :: Word64
initialPrimaryTerm = fromIntegral (idxDocPrimaryTerm doc) :: Word64
-- Second index of the same id advances seq_no, so the
-- stale pair must no longer match the live document.
_ <- performBHRequest $ indexDocument testIndex defaultIndexDocumentSettings exampleTweet (DocId "1")
let staleOpts =
defaultDeleteDocumentOptions
{ ddoIfSeqNo = Just initialSeqNo,
ddoIfPrimaryTerm = Just initialPrimaryTerm
}
result <-
tryEsError $ dispatch $ deleteDocumentWith staleOpts testIndex (DocId "1")
case result of
Left e -> liftIO $ errorStatus e `shouldBe` Just 409
Right resp -> liftIO $ isVersionConflict resp `shouldBe` True
describe "documentExistsWith (HEAD /{index}/_doc/{id} URI params)" $ do
it "refresh=Just True makes a just-indexed document immediately visible" $
withTestEnv $ do
resetIndex
-- Index without a manual refreshIndex: the refresh=True URI
-- param on the HEAD should force the shard to see the write.
_ <-
performBHRequest $
indexDocument testIndex defaultIndexDocumentSettings exampleTweet (DocId "1")
let opts = defaultDocumentExistsOptions {deoRefresh = Just True}
present <- performBHRequest $ documentExistsWith opts testIndex (DocId "1")
liftIO $ present `shouldBe` True
it "accepts a routing URI parameter and resolves the document" $
withTestEnv $ do
resetIndex
_ <- performBHRequest $ indexDocument testIndex defaultIndexDocumentSettings exampleTweet (DocId "rt-ex-1")
-- testIndex is single-shard, so this is a wire-acceptance
-- smoke test (routing param rendered and accepted, HEAD
-- resolves the document) rather than a shard-selection proof;
-- the routing renderer itself is pinned by the
-- DocumentExistsOptions unit tests.
let opts = defaultDocumentExistsOptions {deoRouting = Just "user-42"}
present <- performBHRequest $ documentExistsWith opts testIndex (DocId "rt-ex-1")
liftIO $ present `shouldBe` True
it "default options are byte-for-byte equivalent to documentExists" $
withTestEnv $ do
_ <- insertData
withOpts <- performBHRequest $ documentExistsWith defaultDocumentExistsOptions testIndex (DocId "1")
withoutOpts <- performBHRequest $ documentExists testIndex (DocId "1")
liftIO $ withOpts `shouldBe` withoutOpts
describe "documentSourceExistsWith (HEAD /{index}/_source/{id} URI params)" $ do
it "refresh=Just True makes a just-indexed document's source immediately visible" $
withTestEnv $ do
resetIndex
_ <-
performBHRequest $
indexDocument testIndex defaultIndexDocumentSettings exampleTweet (DocId "1")
let opts = defaultDocumentSourceExistsOptions {dseoRefresh = Just True}
present <- performBHRequest $ documentSourceExistsWith opts testIndex (DocId "1")
liftIO $ present `shouldBe` True
it "default options are byte-for-byte equivalent to documentSourceExists" $
withTestEnv $ do
_ <- insertData
withOpts <- performBHRequest $ documentSourceExistsWith defaultDocumentSourceExistsOptions testIndex (DocId "1")
withoutOpts <- performBHRequest $ documentSourceExists testIndex (DocId "1")
liftIO $ withOpts `shouldBe` withoutOpts
describe "ByQueryOptions URI params" $ do
it "updateByQueryWith accepts conflicts=proceed + slices=auto" $
withTestEnv $ do
_ <- insertData
_ <- insertExtra
let query = TermQuery (Term "user" "bitemyapp") Nothing
script =
Script
(Just (ScriptLanguage "painless"))
(ScriptInline "ctx._source.age *= 2")
Nothing
Nothing
opts =
defaultByQueryOptions
{ bqoConflicts = Just ConflictsProceed,
bqoWaitForCompletion = Just True,
bqoSlices = Just SlicesAuto,
bqoRefresh = Just RefreshTrue
}
_ <- tryPerformBHRequest $ updateByQueryWith @Value opts testIndex query (Just script)
parsed <- searchTweets (mkSearch (Just query) Nothing)
case parsed of
Left e ->
liftIO $ expectationFailure ("updateByQueryWith failed: " <> show e)
Right sr -> do
let results = map (fmap age . hitSource) (hits (searchHits sr))
liftIO $
results `shouldBe` [Just $ age exampleTweet * 2, Just $ age tweetWithExtra * 2]
it "deleteByQueryWith respects max_docs=1" $
withTestEnv $ do
_ <- insertData
_ <- insertOther
let query = MatchAllQuery Nothing
opts = defaultByQueryOptions {bqoMaxDocs = Just 1, bqoRefresh = Just RefreshTrue}
_ <- tryPerformBHRequest $ deleteByQueryWith @DeletedDocuments opts testIndex query
-- max_docs=1 should delete only one of the two documents.
let search = mkSearch (Just (MatchAllQuery Nothing)) Nothing
parsed <- searchTweets search
case parsed of
Left e ->
liftIO $ expectationFailure ("search after deleteByQueryWith failed: " <> show e)
Right sr ->
liftIO $ hitsTotal (searchHits sr) `shouldBe` Just (HitsTotal 1 HTR_EQ)
it "updateByQueryWith defaultByQueryOptions succeeds (equivalent to updateByQuery)" $
withTestEnv $ do
_ <- insertData
let query = TermQuery (Term "user" "bitemyapp") Nothing
script =
Script
(Just (ScriptLanguage "painless"))
(ScriptInline "ctx._source.age = 99")
Nothing
Nothing
res <- tryPerformBHRequest $ updateByQueryWith @Value defaultByQueryOptions testIndex query (Just script)
liftIO $ res `shouldSatisfy` isRight
it "rethrottleUpdateByQuery unthrottles an in-progress async update_by_query" $
withTestEnv $ do
-- Use a dedicated index so that an in-flight by-query task
-- cannot race with the next test's @insertData -> resetIndex@
-- on the shared @testIndex@. Mirrors the @rethrottleReindex@
-- test's @newIndex@ pattern.
let idx = [qqIndexName|bloodhound-tests-twitter-rethrottle-ubq|]
_ <- tryPerformBHRequest $ deleteIndex idx
_ <-
performBHRequest $
createIndex (IndexSettings (ShardCount 1) (ReplicaCount 0) defaultIndexMappingsLimits) idx
_ <- performBHRequest $ indexDocument idx defaultIndexDocumentSettings exampleTweet (DocId "1")
_ <- performBHRequest $ refreshIndex idx
let query = TermQuery (Term "user" "bitemyapp") Nothing
script =
Script
(Just (ScriptLanguage "painless"))
(ScriptInline "ctx._source.age = 99")
Nothing
Nothing
-- wait_for_completion=false so the request returns
-- immediately with a TaskNodeId instead of blocking.
opts = defaultByQueryOptions {bqoWaitForCompletion = Just False}
taskNodeId <-
assertRight =<< tryPerformBHRequest (updateByQueryWith @TaskNodeId opts idx query (Just script))
-- Race window: the single-doc update_by_query may finish
-- before the rethrottle reaches the server, in which case
-- the response carries an empty node list (the docstring on
-- 'rethrottleUpdateByQuery' spells this out). When the task
-- is still running the rethrottle response lists it
-- (>= 1 node). Either outcome is acceptable here — the
-- test exercises the wire path and decoder regardless.
_ <- performBHRequest $ rethrottleUpdateByQuery taskNodeId RethrottleUnlimited
_ <- waitForByQueryTaskToComplete 10 taskNodeId
_ <- performBHRequest $ refreshIndex idx
let search = mkSearch (Just query) Nothing
searchResult <- assertRight =<< tryPerformBHRequest (searchByIndex @Tweet idx search)
liftIO $
map (fmap age . hitSource) (hits (searchHits searchResult)) `shouldBe` [Just (99 :: Int)]
it "rethrottleDeleteByQuery unthrottles an in-progress async delete_by_query" $
withTestEnv $ do
let idx = [qqIndexName|bloodhound-tests-twitter-rethrottle-dbq|]
_ <- tryPerformBHRequest $ deleteIndex idx
_ <-
performBHRequest $
createIndex (IndexSettings (ShardCount 1) (ReplicaCount 0) defaultIndexMappingsLimits) idx
_ <- performBHRequest $ indexDocument idx defaultIndexDocumentSettings exampleTweet (DocId "1")
_ <- performBHRequest $ refreshIndex idx
let query = TermQuery (Term "user" "bitemyapp") Nothing
opts = defaultByQueryOptions {bqoWaitForCompletion = Just False}
taskNodeId <-
assertRight =<< tryPerformBHRequest (deleteByQueryWith @TaskNodeId opts idx query)
-- Same race window as 'rethrottleUpdateByQuery' above: an
-- empty node list in the response means the delete had
-- already finished. Either outcome exercises the wire path.
_ <- performBHRequest $ rethrottleDeleteByQuery taskNodeId RethrottleUnlimited
_ <- waitForByQueryTaskToComplete 10 taskNodeId
_ <- performBHRequest $ refreshIndex idx
let search = mkSearch (Just query) Nothing
searchResult <- assertRight =<< tryPerformBHRequest (searchByIndex @Tweet idx search)
liftIO $ hitsTotal (searchHits searchResult) `shouldBe` Just (HitsTotal 0 HTR_EQ)
describe "UpdateBody (updateDocumentWith)" $ do
it "UpdateDoc with doc_as_upsert=true creates a missing document" $
withTestEnv $ do
resetIndex
let body =
UpdateDoc
{ ubDoc = toJSON exampleTweet,
ubDocAsUpsert = Just True
}
res <- performBHRequest $ updateDocumentWith testIndex defaultIndexDocumentSettings body (DocId "upsert-1")
liftIO $ idxDocResult res `shouldBe` "created"
-- Verify the document was actually created with the supplied content.
fetched <- performBHRequest $ getDocument @Tweet testIndex (DocId "upsert-1")
liftIO $ getSource fetched `shouldBe` Just exampleTweet
it "UpdateDoc without doc_as_upsert fails on a missing document" $
withTestEnv $ do
resetIndex
let body = mkUpdateDoc (toJSON (exampleTweet {user = "patched"}))
result <- tryEsError $ dispatch $ updateDocumentWith testIndex defaultIndexDocumentSettings body (DocId "no-upsert-1")
case result of
Right resp -> liftIO $ isSuccess resp `shouldBe` False
Left e -> liftIO $ errorStatus e `shouldBe` Just 404
it "UpdateScript modifies an existing document via painless" $
withTestEnv $ do
_ <- insertData
let script =
Script
(Just (ScriptLanguage "painless"))
(ScriptInline "ctx._source.age += 10")
Nothing
Nothing
body = mkUpdateScript script
_ <- performBHRequest $ updateDocumentWith testIndex defaultIndexDocumentSettings body (DocId "1")
fetched <- performBHRequest $ getDocument @Tweet testIndex (DocId "1")
liftIO $ (fmap age . getSource) fetched `shouldBe` Just (age exampleTweet + 10)
it "UpdateScript with upsert creates a missing document via script" $
withTestEnv $ do
resetIndex
let script =
Script
(Just (ScriptLanguage "painless"))
(ScriptInline "ctx._source.counter = 0")
Nothing
Nothing
body =
UpdateScript
{ ubScript = script,
ubUpsert = Just (object ["counter" .= (0 :: Int)]),
ubScriptedUpsert = Just True
}
res <- performBHRequest $ updateDocumentWith testIndex defaultIndexDocumentSettings body (DocId "script-upsert-1")
liftIO $ idxDocResult res `shouldBe` "created"
it "updateDocument (legacy) remains byte-for-byte equivalent to updateDocumentWith (mkUpdateDoc)" $
withTestEnv $ do
_ <- insertData
-- Legacy path: updateDocument with a patch.
let patch = exampleTweet {user = "legacy"}
_ <- performBHRequest $ updateDocument testIndex defaultIndexDocumentSettings patch (DocId "1")
legacyFetched <- performBHRequest $ getDocument @Tweet testIndex (DocId "1")
-- Reset and try the With path with the same patch.
_ <- insertData
_ <- performBHRequest $ updateDocumentWith testIndex defaultIndexDocumentSettings (mkUpdateDoc (toJSON patch)) (DocId "1")
withFetched <- performBHRequest $ getDocument @Tweet testIndex (DocId "1")
liftIO $ getSource legacyFetched `shouldBe` getSource withFetched
assertRight :: (Show a, MonadFail m) => Either a b -> m b
assertRight (Left x) = fail $ "Expected Right, got Left: " <> show x
assertRight (Right x) = pure x
-- | Poll 'getTask' until 'taskResponseCompleted' is true, bounded by a
-- number of 100ms ticks. Mirrors the helper in "Test.ReindexSpec". Used
-- to wait out an asynchronous @*_by_query@ task started with
-- @bqoWaitForCompletion = Just False@.
waitForByQueryTaskToComplete ::
(MonadBH m, MonadFail m) =>
Natural ->
TaskNodeId ->
m (TaskResponse Value)
waitForByQueryTaskToComplete 0 taskNodeId =
fail $ "Timed out waiting for by-query task to complete, taskNodeId = " <> show taskNodeId
waitForByQueryTaskToComplete n taskNodeId = do
task <- assertRight =<< tryPerformBHRequest (getTask taskNodeId)
if taskResponseCompleted task
then pure task
else liftIO (threadDelay 100000) >> waitForByQueryTaskToComplete (n - 1) taskNodeId