bloodhound-1.0.0.0: tests/Test/KnnClearCacheSpec.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Test.KnnClearCacheSpec (spec) where
import Database.Bloodhound.OpenSearch2.Client qualified as OS2.Client
import Database.Bloodhound.OpenSearch2.Requests qualified as OS2Requests
import Database.Bloodhound.OpenSearch3.Client qualified as OS3.Client
import Database.Bloodhound.OpenSearch3.Requests qualified as OS3Requests
import TestsUtils.Common
import TestsUtils.Import
import Prelude
-- | Pure endpoint-shape tests for the k-NN plugin Clear Cache API
-- (@POST /_plugins/_knn/clear_cache\/{index}@) across OpenSearch 2 and 3.
-- The endpoint was added in OpenSearch 2.14, so unlike 'Test.KnnWarmupSpec'
-- there is no OpenSearch 1 variant. These mirror 'Test.NeuralClearCacheSpec's
-- shape tests but assert the @\/_plugins\/_knn\/clear_cache@ path, the POST
-- method, the empty-object body, and OS2\/OS3 cross-backend parity. JSON
-- decoding of the shared 'ShardsResult' response is re-verified here on
-- the clear-cache response shape
-- (@{\"_shards\":{\"total\":...,\"successful\":...,\"failed\":...}}@,
-- live-verified against OS 2.19.5 \/ 3.7.0) so a regression in the
-- pre-existing instance is caught in this spec too.
spec :: Spec
spec = describe "k-NN Clear Cache API" $ do
describe "clearKnnCache endpoint shape (OpenSearch 3)" $ do
it "POSTs to /_plugins/_knn/clear_cache/{index} when given an IndexName" $ do
let req = OS3Requests.clearKnnCache [[qqIndexName|my-index|]]
bhRequestMethod req `shouldBe` "POST"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_knn", "clear_cache", "my-index"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "attaches an empty JSON object as the body" $ do
let req = OS3Requests.clearKnnCache [[qqIndexName|my-index|]]
bhRequestBody req `shouldBe` Just (encode (object []))
it "uses a distinct index in the path when given a different IndexName" $ do
let req = OS3Requests.clearKnnCache [[qqIndexName|other-index|]]
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_knn", "clear_cache", "other-index"]
it "keeps the index name as a single opaque path segment" $ do
let req = OS3Requests.clearKnnCache [[qqIndexName|my-index_42|]]
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_knn", "clear_cache", "my-index_42"]
describe "clearKnnCache endpoint shape (OpenSearch 2)" $ do
it "POSTs to /_plugins/_knn/clear_cache/{index} when given an IndexName" $ do
let req = OS2Requests.clearKnnCache [[qqIndexName|my-index|]]
bhRequestMethod req `shouldBe` "POST"
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_knn", "clear_cache", "my-index"]
getRawEndpointQueries (bhRequestEndpoint req) `shouldBe` []
it "attaches an empty JSON object as the body" $ do
let req = OS2Requests.clearKnnCache [[qqIndexName|my-index|]]
bhRequestBody req `shouldBe` Just (encode (object []))
it "uses a distinct index in the path when given a different IndexName" $ do
let req = OS2Requests.clearKnnCache [[qqIndexName|other-index|]]
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_knn", "clear_cache", "other-index"]
it "keeps the index name as a single opaque path segment" $ do
let req = OS2Requests.clearKnnCache [[qqIndexName|my-index_42|]]
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_knn", "clear_cache", "my-index_42"]
-- ---------------------------------------------------------------- --
-- Multi-index (comma-separated) path rendering: bloodhound-6jy. --
-- The k-NN Clear Cache API documents comma-separated lists and --
-- index patterns in the {index} path segment; the [IndexName] --
-- argument is rendered as a single comma-joined segment, matching --
-- the OpenSearch multi-target syntax. --
-- ---------------------------------------------------------------- --
describe "clearKnnCache multi-index path rendering (OpenSearch 3)" $ do
it "renders [i1,i2,i3] as a single comma-joined path segment" $ do
let req =
OS3Requests.clearKnnCache
[[qqIndexName|index1|], [qqIndexName|index2|], [qqIndexName|index3|]]
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_knn", "clear_cache", "index1,index2,index3"]
it "preserves index name order in the comma-joined segment" $ do
let req =
OS3Requests.clearKnnCache
[[qqIndexName|zebra|], [qqIndexName|alpha|], [qqIndexName|mike|]]
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_knn", "clear_cache", "zebra,alpha,mike"]
it "renders an empty segment for an empty index list (server will error)" $ do
let req = OS3Requests.clearKnnCache []
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_knn", "clear_cache", ""]
describe "clearKnnCache multi-index path rendering (OpenSearch 2)" $ do
it "renders [i1,i2] as a single comma-joined path segment" $ do
let req =
OS2Requests.clearKnnCache [[qqIndexName|index1|], [qqIndexName|index2|]]
getRawEndpoint (bhRequestEndpoint req)
`shouldBe` ["_plugins", "_knn", "clear_cache", "index1,index2"]
describe "clearKnnCache cross-backend parity" $ do
-- OS2 and OS3 must emit the same path, POST method, empty-object body,
-- and empty query string for the same 'IndexName'. (OS1 is excluded:
-- the Clear Cache API was added in OpenSearch 2.14.)
let idx = [qqIndexName|shared-index|]
it "OS2, OS3 produce the same path segments" $ do
let path2 = getRawEndpoint (bhRequestEndpoint (OS2Requests.clearKnnCache [idx]))
path3 = getRawEndpoint (bhRequestEndpoint (OS3Requests.clearKnnCache [idx]))
path2 `shouldBe` path3
it "OS2, OS3 all use POST" $ do
let m2 = bhRequestMethod (OS2Requests.clearKnnCache [idx])
m3 = bhRequestMethod (OS3Requests.clearKnnCache [idx])
m2 `shouldBe` "POST"
m3 `shouldBe` "POST"
it "OS2, OS3 all carry no query string" $ do
let q2 = getRawEndpointQueries (bhRequestEndpoint (OS2Requests.clearKnnCache [idx]))
q3 = getRawEndpointQueries (bhRequestEndpoint (OS3Requests.clearKnnCache [idx]))
q2 `shouldBe` []
q3 `shouldBe` []
it "OS2, OS3 attach identical body bytes" $ do
let b2 = bhRequestBody (OS2Requests.clearKnnCache [idx])
b3 = bhRequestBody (OS3Requests.clearKnnCache [idx])
b2 `shouldBe` b3
describe "clearKnnCache is distinct from warmupKnnIndex" $ do
-- Guards against an accidental copy-paste regression where
-- clearKnnCache silently re-uses the warmup endpoint. The two APIs
-- share the @\/_plugins\/_knn@ prefix and a 'ShardsResult' return
-- type, but since bloodhound-04f.6.2.9 they differ in three ways:
-- method (@POST@ for clear_cache vs @GET@ for warmup), body
-- (empty-object for clear_cache vs absent for warmup), and the
-- @clear_cache@ vs @warmup@ path segment. The path is the most
-- stable distinguishing feature so it is the one pinned here; a
-- method-divergence assertion lives in 'Test.KnnWarmupSpec'.
let idx = [qqIndexName|shared-index|]
it "OS2 clear_cache path differs from warmup path" $ do
let clearPath = getRawEndpoint (bhRequestEndpoint (OS2Requests.clearKnnCache [idx]))
warmupPath = getRawEndpoint (bhRequestEndpoint (OS2Requests.warmupKnnIndex [idx]))
clearPath `shouldNotBe` warmupPath
it "OS3 clear_cache path differs from warmup path" $ do
let clearPath = getRawEndpoint (bhRequestEndpoint (OS3Requests.clearKnnCache [idx]))
warmupPath = getRawEndpoint (bhRequestEndpoint (OS3Requests.warmupKnnIndex [idx]))
clearPath `shouldNotBe` warmupPath
describe "ShardsResult response decoding" $ do
-- Pure unit tests (no OS round-trip): lock the 'FromJSON ShardsResult'
-- decoder against the actual wire shape returned by
-- @POST /_plugins/_knn/clear_cache\/{index}@. Live OS 2.19.5 \/ 3.7.0
-- confirmed the response is the @_shards@ envelope (matching
-- @warmupKnnIndex@, @flushIndex@, @refreshIndex@, @clearIndexCache@),
-- not the @{\"acknowledged\":true}@ envelope the previous
-- 'Acknowledged' return type assumed.
it "decodes {\"_shards\":{\"total\":2,\"successful\":1,\"skipped\":0,\"failed\":0}} into ShardsResult" $ do
let payload = "{\"_shards\":{\"total\":2,\"successful\":1,\"skipped\":0,\"failed\":0}}"
Just decoded = decode payload :: Maybe ShardsResult
decoded `shouldBe` ShardsResult (ShardResult 2 1 0 0)
it "rejects a payload missing the _shards envelope" $ do
let payload = "{\"acknowledged\":true}"
decoded = decode payload :: Maybe ShardsResult
decoded `shouldBe` Nothing
it "round-trips a captured live clear-cache response through the decoder" $ do
let original = "{\"_shards\":{\"total\":2,\"successful\":1,\"failed\":0}}"
Just decoded = decode original :: Maybe ShardsResult
shardTotal (srShards decoded) `shouldBe` 2
-- ---------------------------------------------------------------- --
-- Live round-trip: gated per-OS with os{n}OnlyIT (the --
-- 'Test.MLModelSpec' pattern). The k-NN Clear Cache API was added --
-- in OpenSearch 2.14, so only OS 2 and OS 3 are exercised — OS 1 --
-- is excluded. --
-- --
-- Wire shape live-verified against OS 2.19.5 (port 9204) and OS --
-- 3.7.0 (port 9205) in bead bloodhound-vvj: the response is --
-- @{"_shards":{"total":2,"successful":1,"failed":0}}@ — the --
-- @_shards@ envelope, decoded into 'ShardsResult'. --
-- ---------------------------------------------------------------- --
describe "clearKnnCache live round-trip" $ do
os2It <- runIO os2OnlyIT
os3It <- runIO os3OnlyIT
let assertShards :: ShardsResult -> IO ()
assertShards (ShardsResult sr) = do
shardTotal sr `shouldSatisfy` (>= 1)
shardsFailed sr `shouldBe` 0
os2It "OS2: returns a ShardsResult with at least one shard and no failures" $
withTestEnv $ do
_ <- tryEsError resetOsKnnIndex
result <- OS2.Client.clearKnnCache osKnnTestIndex
liftIO $ assertShards result
os3It "OS3: returns a ShardsResult with at least one shard and no failures" $
withTestEnv $ do
_ <- tryEsError resetOsKnnIndex
result <- OS3.Client.clearKnnCache osKnnTestIndex
liftIO $ assertShards result