langchain-hs-0.0.5.0: src/Langchain/VectorStore/SqliteVec.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{- |
Module : Langchain.VectorStore.SqliteVec
Description : SQLite-backed vector store with persistent storage and cosine distance
Copyright : (c) 2026 Tushar Adhatrao
License : MIT
Maintainer : Tushar Adhatrao <tusharadhatrao@gmail.com>
Stability : experimental
Stores document text, JSON metadata, and vector embeddings in a local SQLite database.
-}
module Langchain.VectorStore.SqliteVec
( SqliteVecStore (..)
, newSqliteVecStore
, initSqliteVecSchema
) where
import Control.Exception (try)
import Control.Monad.Except (MonadError, throwError)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Aeson (decode, encode)
import qualified Data.ByteString.Lazy as LBS
import Data.Int (Int64)
import Data.List (sortOn)
import Data.Maybe (fromMaybe)
import Data.Ord (Down (..))
import qualified Data.Text as TS
import qualified Data.Text.Encoding as TE
import qualified Data.Text.Lazy as TL
import Database.SQLite.Simple
import Langchain.Core.Error (LangchainError, vectorStoreError)
import Langchain.DocumentLoader.Core (Document (..))
import Langchain.Embeddings.Core (Embeddings (..))
import Langchain.VectorStore.Core (VectorStore (..))
import Langchain.VectorStore.InMemory (cosineSimilarity)
-- | SQLite vector store container
data SqliteVecStore e = SqliteVecStore
{ sqliteDbPath :: FilePath
, sqliteEmbeddings :: e
}
-- | Construct a new SqliteVecStore and initialize schema
newSqliteVecStore ::
(MonadIO m, MonadError LangchainError m) =>
FilePath ->
e ->
m (SqliteVecStore e)
newSqliteVecStore dbPath emb = do
initSqliteVecSchema dbPath
pure $ SqliteVecStore dbPath emb
-- | Initialize table schema in SQLite database
initSqliteVecSchema :: (MonadIO m, MonadError LangchainError m) => FilePath -> m ()
initSqliteVecSchema dbPath = do
eRes <- liftIO $ try $ withConnection dbPath $ \conn -> do
execute_
conn
"CREATE TABLE IF NOT EXISTS langchain_vectors (\
\ id INTEGER PRIMARY KEY AUTOINCREMENT,\
\ content TEXT NOT NULL,\
\ metadata TEXT NOT NULL,\
\ vector BLOB NOT NULL\
\);"
case eRes of
Left err ->
throwError $
vectorStoreError
(TS.pack $ "Failed to initialize SQLite vector database: " ++ show (err :: IOError))
(Just "SqliteVecStore")
Nothing
Right () -> pure ()
instance (Embeddings e) => VectorStore (SqliteVecStore e) where
addDocuments store docs = do
vectors <- embedDocuments (sqliteEmbeddings store) docs
eRes <- liftIO $ try $ withConnection (sqliteDbPath store) $ \conn -> do
withTransaction conn $ do
mapM_
( \(doc, vec) -> do
let cTxt = TL.unpack (pageContent doc)
mJson = TE.decodeUtf8 $ LBS.toStrict $ encode (metadata doc)
vBytes = LBS.toStrict $ encode (vec :: [Float])
execute
conn
"INSERT INTO langchain_vectors (content, metadata, vector) VALUES (?, ?, ?)"
(cTxt, TS.unpack mJson, vBytes)
)
(zip docs vectors)
case eRes of
Left err ->
throwError $
vectorStoreError
(TS.pack $ "Failed to insert documents into SQLite vector store: " ++ show (err :: IOError))
(Just "SqliteVecStore")
Nothing
Right () -> pure store
delete store ids = do
eRes <- liftIO $ try $ withConnection (sqliteDbPath store) $ \conn -> do
withTransaction conn $ do
mapM_
(\i -> execute conn "DELETE FROM langchain_vectors WHERE id = ?" (Only (i :: Int64)))
ids
case eRes of
Left err ->
throwError $
vectorStoreError
(TS.pack $ "Failed to delete documents from SQLite vector store: " ++ show (err :: IOError))
(Just "SqliteVecStore")
Nothing
Right () -> pure store
similaritySearch store query0 k = do
qVec <- embedQuery (sqliteEmbeddings store) query0
similaritySearchByVector store qVec k
similaritySearchByVector store qVec k = do
rowsRes <- liftIO $ try $ withConnection (sqliteDbPath store) $ \conn -> do
query_ conn "SELECT id, content, metadata, vector FROM langchain_vectors" ::
IO [(Int64, String, String, LBS.ByteString)]
rows <- case rowsRes of
Left err ->
throwError $
vectorStoreError
(TS.pack $ "Failed to query SQLite vector store: " ++ show (err :: IOError))
(Just "SqliteVecStore")
Nothing
Right r -> pure r
let scoredDocs =
[ (score, doc)
| (_, contentStr, metaStr, vBytes) <- rows
, let mbVec = decode (LBS.fromStrict (LBS.toStrict vBytes)) :: Maybe [Float]
, Just vec <- [mbVec]
, let score = cosineSimilarity qVec vec
, let mbMeta = decode (LBS.fromStrict (TE.encodeUtf8 (TS.pack metaStr)))
, let meta = fromMaybe mempty mbMeta
, let doc = Document (TL.pack contentStr) meta
]
topK = take k $ map snd $ sortOn (Down . fst) scoredDocs
pure topK