packages feed

shikumi-compile-0.1.0.0: src/Shikumi/Compile/Retriever.hs

{-# LANGUAGE RankNTypes #-}

-- | The minimal retrieval interface and the one trivial implementation shikumi
-- ships. Production retrievers (vector stores, search services) are explicitly out
-- of scope for shikumi (MasterPlan "Out of scope"); this defines the /interface/ a
-- real one would later satisfy.
module Shikumi.Compile.Retriever
  ( Passage (..),
    Retriever (..),
    inMemoryRetriever,
  )
where

import Data.List (sortOn)
import Data.Ord (Down (..))
import Data.Text (Text)
import Data.Text qualified as T
import Effectful (Eff)

-- | A unit of retrievable text.
data Passage = Passage
  { passageId :: !Text,
    text :: !Text
  }
  deriving stock (Eq, Show)

-- | Given a query, return relevant passages ranked best-first. Effect-polymorphic
-- (@forall es@) so the interface is honest about a /real/ retriever running under a
-- program's effect stack — though the trivial implementation below performs no
-- effect, and the RAG compiler runs retrieval purely at compile time (see
-- "Shikumi.Compile.RAG"). A real IO-backed retriever would define its own concrete
-- type with the effects it needs rather than satisfy this @forall es@ signature.
newtype Retriever = Retriever
  { retrieve :: forall es. Text -> Eff es [Passage]
  }

-- | A brute-force in-memory retriever: rank passages by the number of shared
-- (lowercased, whitespace-split) word tokens with the query, and return the top
-- @k@. Not production quality — no embeddings, no stemming, no scoring beyond raw
-- overlap — this is the documented placeholder.
inMemoryRetriever :: Int -> [Passage] -> Retriever
inMemoryRetriever k corpus = Retriever $ \query ->
  let qWords = tokens query
      score p = length (filter (`elem` qWords) (tokens (text p)))
   in pure (take k (sortOn (Down . score) corpus))
  where
    tokens = T.words . T.toLower