packages feed

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