diff --git a/Changelog.md b/Changelog.md
new file mode 100644
--- /dev/null
+++ b/Changelog.md
@@ -0,0 +1,6 @@
+# Change log for esqueleto text search 
+
+## Version 1.0.0.3 
+
+initial version, uploaded from:
+https://github.com/creichert/esqueleto-textsearch
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2015 Alberto Valverde Gonzalez
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+# esqueleto-fts
+PostgreSQL full text search for Esqueleto
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/esqueleto-textsearch.cabal b/esqueleto-textsearch.cabal
new file mode 100644
--- /dev/null
+++ b/esqueleto-textsearch.cabal
@@ -0,0 +1,62 @@
+name:               esqueleto-textsearch
+version:            1.0.0.3
+synopsis:           PostgreSQL full text search for Esqueleto
+description:        PostgreSQL text search functions for Esqueleto.
+homepage:           https://github.com/SupercedeTech/esqueleto-textsearch-ii
+license:            MIT
+license-file:       LICENSE
+author:             Alberto Valverde González
+maintainer:         info@supercede.com
+copyright:          2015 Alberto Valverde González
+category:           Database
+build-type:         Simple
+extra-source-files: README.md
+                    Changelog.md
+cabal-version:      >=1.10
+
+source-repository head
+  type: git
+  location: https://github.com/SupercedeTech/esqueleto-textsearch-ii 
+
+library
+  exposed-modules:
+    Database.Esqueleto.TextSearch
+    Database.Esqueleto.TextSearch.Language
+    Database.Esqueleto.TextSearch.Types
+
+  build-depends:
+      base                   >=4.9   && <5
+    , data-default           <0.8
+    , esqueleto              >=3.2 && < 3.6
+    , parsec                 <3.2
+    , persistent             >=2.8.2 && <2.15
+    , persistent-postgresql  >=2.10  && <2.15
+    , text                   >=1.2   && <2.1
+
+  hs-source-dirs:   src
+  default-language: Haskell2010
+  ghc-options:      -Wall -fwarn-incomplete-uni-patterns
+
+test-suite spec
+  type:             exitcode-stdio-1.0
+  ghc-options:      -Wall -fwarn-incomplete-uni-patterns
+  hs-source-dirs:   test
+  main-is:          Spec.hs
+  other-modules:    Database.Esqueleto.TextSearchSpec
+  build-depends:
+      base
+    , esqueleto
+    , esqueleto-textsearch
+    , hspec
+    , HUnit
+    , monad-control
+    , monad-logger
+    , persistent
+    , persistent-postgresql
+    , persistent-template
+    , QuickCheck
+    , resourcet
+    , text
+    , transformers
+
+  default-language: Haskell2010
diff --git a/src/Database/Esqueleto/TextSearch.hs b/src/Database/Esqueleto/TextSearch.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Esqueleto/TextSearch.hs
@@ -0,0 +1,7 @@
+module Database.Esqueleto.TextSearch (
+    module Database.Esqueleto.TextSearch.Language
+  , module Database.Esqueleto.TextSearch.Types
+) where
+
+import Database.Esqueleto.TextSearch.Language
+import Database.Esqueleto.TextSearch.Types
diff --git a/src/Database/Esqueleto/TextSearch/Language.hs b/src/Database/Esqueleto/TextSearch/Language.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Esqueleto/TextSearch/Language.hs
@@ -0,0 +1,70 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Database.Esqueleto.TextSearch.Language
+  ( (@@.)
+  , to_tsvector
+  , to_tsquery
+  , plainto_tsquery
+  , ts_rank
+  , ts_rank_cd
+  , setweight
+  ) where
+
+import Data.String (IsString)
+import Data.Text (Text)
+import Database.Esqueleto (SqlExpr, Value)
+#if MIN_VERSION_esqueleto(3,5,0)
+import Database.Esqueleto.Internal.Internal (unsafeSqlBinOp, unsafeSqlFunction)
+#else
+import Database.Esqueleto.Internal.Sql (unsafeSqlBinOp, unsafeSqlFunction)
+#endif
+import Database.Esqueleto.TextSearch.Types
+
+(@@.)
+  :: SqlExpr (Value TsVector)
+  -> SqlExpr (Value (TsQuery Lexemes))
+  -> SqlExpr (Value Bool)
+(@@.) = unsafeSqlBinOp "@@"
+
+to_tsvector
+  :: IsString a
+  => SqlExpr (Value RegConfig)
+  -> SqlExpr (Value a)
+  -> SqlExpr (Value TsVector)
+to_tsvector a b = unsafeSqlFunction "to_tsvector" (a, b)
+
+to_tsquery
+  :: SqlExpr (Value RegConfig)
+  -> SqlExpr (Value (TsQuery Words))
+  -> SqlExpr (Value (TsQuery Lexemes) )
+to_tsquery a b = unsafeSqlFunction "to_tsquery" (a, b)
+
+plainto_tsquery
+  :: SqlExpr (Value RegConfig)
+  -> SqlExpr (Value Text)
+  -> SqlExpr (Value (TsQuery Lexemes))
+plainto_tsquery a b = unsafeSqlFunction "plainto_tsquery" (a, b)
+
+ts_rank
+  :: SqlExpr (Value Weights)
+  -> SqlExpr (Value TsVector)
+  -> SqlExpr (Value (TsQuery Lexemes))
+  -> SqlExpr (Value [NormalizationOption])
+  -> SqlExpr (Value Double)
+ts_rank a b c d = unsafeSqlFunction "ts_rank" (a, b, c, d)
+
+ts_rank_cd
+  :: SqlExpr (Value Weights)
+  -> SqlExpr (Value TsVector)
+  -> SqlExpr (Value (TsQuery Lexemes))
+  -> SqlExpr (Value [NormalizationOption])
+  -> SqlExpr (Value Double)
+ts_rank_cd a b c d = unsafeSqlFunction "ts_rank_cd" (a, b, c, d)
+
+setweight
+  :: SqlExpr (Value TsVector)
+  -> SqlExpr (Value Weight)
+  -> SqlExpr (Value TsVector)
+setweight a b = unsafeSqlFunction "setweight" (a, b)
diff --git a/src/Database/Esqueleto/TextSearch/Types.hs b/src/Database/Esqueleto/TextSearch/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Database/Esqueleto/TextSearch/Types.hs
@@ -0,0 +1,243 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE KindSignatures #-}
+{-# LANGUAGE StandaloneDeriving #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+module Database.Esqueleto.TextSearch.Types (
+    TsQuery (..)
+  , Words
+  , Lexemes
+  , TsVector
+  , RegConfig
+  , NormalizationOption (..)
+  , Weight (..)
+  , Weights (..)
+  , Position (..)
+  , word
+  , queryToText
+  , textToQuery
+  , def
+) where
+
+import Control.Applicative (pure, many, optional, (<$>), (*>), (<*), (<|>))
+import Data.Bits ((.|.), (.&.))
+import Data.Int (Int64)
+import Data.List (foldl')
+import Data.Monoid ((<>))
+import Data.String (IsString(fromString))
+import Text.Printf (printf)
+import Text.Parsec (
+  ParseError, runParser, char, eof, between, choice, spaces, satisfy, many1)
+import qualified Text.Parsec.Expr as P
+
+import Data.Default (Default(def))
+import Data.Text (Text, singleton)
+import Data.Text.Encoding (decodeUtf8, encodeUtf8)
+import Data.Text.Lazy (toStrict)
+import Data.Text.Lazy.Builder (Builder, toLazyText, fromText)
+import Database.Persist
+import Database.Persist.Postgresql
+
+data NormalizationOption
+  = NormNone
+  | Norm1LogLength
+  | NormLength
+  | NormMeanHarmDist
+  | NormUniqueWords
+  | Norm1LogUniqueWords
+  | Norm1Self
+  deriving (Eq, Show, Enum, Bounded)
+
+normToInt :: NormalizationOption -> Int64
+normToInt n
+  | fromEnum n == 0 = 0
+  | otherwise       = 2 ^ (fromEnum n - 1)
+
+instance PersistField [NormalizationOption] where
+  toPersistValue = PersistInt64 . foldl' (.|.) 0 . map normToInt
+  fromPersistValue (PersistInt64 n) = Right $ foldl' go [] [minBound..maxBound]
+    where go acc v = case normToInt v .&. n of
+                      0 -> acc
+                      _ -> v:acc
+  fromPersistValue f
+    = Left $
+      "TextSearch/[NormalizationOption]: Unexpected Persist field: " <> tShow f
+instance PersistFieldSql [NormalizationOption] where
+  sqlType = const SqlInt32
+
+data Weight
+  = Highest
+  | High
+  | Medium
+  | Low
+  deriving (Eq, Show)
+
+weightToChar :: Weight -> Char
+weightToChar Highest = 'A'
+weightToChar High    = 'B'
+weightToChar Medium  = 'C'
+weightToChar Low     = 'D'
+
+instance PersistField Weight where
+  toPersistValue = PersistText . singleton . weightToChar
+  fromPersistValue (PersistText "A") = Right Highest
+  fromPersistValue (PersistText "B") = Right High
+  fromPersistValue (PersistText "C") = Right Medium
+  fromPersistValue (PersistText "D") = Right Low
+  fromPersistValue (PersistText _)
+    = Left "TextSearch/Weight: Unexpected character"
+  fromPersistValue f
+    = Left $ "TextSearch/Weight: Unexpected Persist field: " <> tShow f
+instance PersistFieldSql Weight where
+  sqlType = const (SqlOther "char")
+
+data Weights
+  = Weights { dWeight :: !Double
+            , cWeight :: !Double
+            , bWeight :: !Double
+            , aWeight :: !Double
+            } deriving (Eq, Show)
+
+instance Default Weights where
+  def = Weights 0.1 0.2 0.4 1.0
+
+instance PersistField Weights where
+  toPersistValue (Weights d c b a)
+    --FIXME: persistent-postgresql should handle this properly
+    = PersistDbSpecific $ fromString $ (printf "{%f,%f,%f,%f}" d c b a)
+  fromPersistValue (PersistList [d, c, b, a])
+    = Weights <$> fromPersistValue d
+              <*> fromPersistValue c
+              <*> fromPersistValue b
+              <*> fromPersistValue a
+  fromPersistValue (PersistList _)
+    = Left "TextSearch/Weights: Expected a length-4 float array"
+  fromPersistValue f
+    = Left $ "TextSearch/Weights: Unexpected Persist field: " <> tShow f
+
+instance PersistFieldSql Weights where
+  sqlType = const (SqlOther "float4[4]")
+
+data QueryType = Words | Lexemes
+type Lexemes = 'Lexemes
+type Words = 'Words
+
+data Position = Prefix | Infix deriving (Show, Eq)
+
+data TsQuery (a :: QueryType) where
+  Lexeme :: Position -> [Weight] -> Text -> TsQuery Lexemes
+  Word   :: Position -> [Weight] -> Text -> TsQuery Words
+  (:&)   :: TsQuery a -> TsQuery a -> TsQuery a
+  (:|)   :: TsQuery a -> TsQuery a -> TsQuery a
+  Not    :: TsQuery a -> TsQuery a
+
+infixr 3 :&
+infixr 2 :|
+
+deriving instance Show (TsQuery a)
+deriving instance Eq (TsQuery a)
+
+instance PersistField (TsQuery Words) where
+  toPersistValue = PersistDbSpecific . encodeUtf8 . queryToText
+  fromPersistValue (PersistDbSpecific _)
+    = Left "TextSearch/TsQuery: Cannot parse (TsQuery Words)"
+  fromPersistValue f
+    = Left $ "TextSearch/TsQuery: Unexpected Persist field: " <> tShow f
+
+instance PersistField (TsQuery Lexemes) where
+  toPersistValue = PersistDbSpecific . encodeUtf8 . queryToText
+  fromPersistValue (PersistDbSpecific bs)
+    = case textToQuery (decodeUtf8 bs) of
+        Right q -> Right q
+        Left  e -> Left $ "Could not parse TsQuery: " <> tShow e
+  fromPersistValue f
+    = Left $ "TextSearch/TsQuery: Unexpected Persist field: " <> tShow f
+
+instance PersistFieldSql (TsQuery Words) where
+  sqlType = const (SqlOther "tsquery")
+
+instance PersistFieldSql (TsQuery Lexemes) where
+  sqlType = const (SqlOther "tsquery")
+
+instance a~Words => IsString (TsQuery a) where
+  fromString = word . fromString
+
+word :: Text -> TsQuery Words
+word = Word Infix []
+
+
+queryToText :: TsQuery a -> Text
+queryToText = toStrict . toLazyText . build . unsafeAsLexeme
+  where
+    build :: TsQuery Lexemes -> Builder
+    build (Lexeme Infix [] s)    = "'" <> fromText s <> "'"
+    build (Lexeme Infix ws s)    = "'" <> fromText s <> "':"  <> buildWeights ws
+    build (Lexeme Prefix ws s)   = "'" <> fromText s <> "':*" <> buildWeights ws
+    build (a :& b)               = parens a <> "&" <> parens b
+    build (a :| b)               = parens a <> "|" <> parens b
+    build (Not q)                = "!" <> parens q
+    buildWeights                 = fromText . fromString . map weightToChar
+    unsafeAsLexeme :: TsQuery a -> TsQuery Lexemes
+    unsafeAsLexeme q@Lexeme{}    = q
+    unsafeAsLexeme (Word p ws s) = Lexeme p ws s
+    unsafeAsLexeme (a :& b)      = unsafeAsLexeme a :& unsafeAsLexeme b
+    unsafeAsLexeme (a :| b)      = unsafeAsLexeme a :| unsafeAsLexeme b
+    unsafeAsLexeme (Not q)       = Not (unsafeAsLexeme q)
+    parens a@Lexeme{}            = build a
+    parens a                     = "(" <> build a <> ")"
+
+textToQuery :: Text -> Either ParseError (TsQuery Lexemes)
+textToQuery = runParser (expr <* eof) () ""
+  where
+    expr    = spaced (P.buildExpressionParser table (spaced term))
+    term    = parens expr
+          <|> lexeme
+    table   = [ [P.Prefix (char '!' *> pure Not)]
+              , [P.Infix  (char '&' *> pure (:&)) P.AssocRight]
+              , [P.Infix  (char '|' *> pure (:|)) P.AssocRight]
+              ]
+    lexeme = do
+      s   <- fromString <$> quoted (many1 (satisfy (/='\'')))
+      _   <- optional (char ':')
+      pos <- char '*' *> pure Prefix <|> pure Infix
+      ws  <- many weight
+      return $ Lexeme pos ws s
+    weight = choice [ char 'A' *> pure Highest
+                    , char 'B' *> pure High
+                    , char 'C' *> pure Medium
+                    , char 'D' *> pure Low]
+    spaced = between (optional spaces) (optional spaces)
+    quoted = between (char '\'') (char '\'')
+    parens = between (char '(') (char ')')
+
+newtype TsVector = TsVector {unTsVector::Text} deriving (Eq, Show, IsString)
+
+instance Default TsVector where
+  def = TsVector ""
+
+instance PersistField TsVector where
+  toPersistValue = PersistDbSpecific . encodeUtf8 . unTsVector
+  fromPersistValue (PersistDbSpecific bs) = Right $ TsVector $ decodeUtf8 bs
+  fromPersistValue f
+    = Left $ "TextSearch/TsVector: Unexpected Persist field: " <> tShow f
+
+instance PersistFieldSql TsVector where
+  sqlType = const (SqlOther "tsvector")
+
+
+newtype RegConfig = RegConfig {unRegConfig::Text} deriving (Eq, Show, IsString)
+
+instance PersistField RegConfig where
+  toPersistValue = PersistDbSpecific . encodeUtf8 . unRegConfig
+  fromPersistValue (PersistDbSpecific bs) = Right $ RegConfig $ decodeUtf8 bs
+  fromPersistValue f
+    = Left $ "TextSearch/RegConfig: Unexpected Persist field: " <> tShow f
+
+instance PersistFieldSql RegConfig where
+  sqlType = const (SqlOther "regconfig")
+
+tShow :: Show a => a -> Text
+tShow = fromString . show
diff --git a/test/Database/Esqueleto/TextSearchSpec.hs b/test/Database/Esqueleto/TextSearchSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Database/Esqueleto/TextSearchSpec.hs
@@ -0,0 +1,353 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE ConstraintKinds #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE StandaloneDeriving #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+module Database.Esqueleto.TextSearchSpec (main, spec) where
+
+import Control.Monad (forM_)
+import Data.Maybe (fromJust)
+import Data.Text (Text, pack)
+
+import Control.Monad.IO.Class (MonadIO(liftIO))
+import Control.Monad.Logger (MonadLogger(..), runStderrLoggingT)
+import Control.Monad.Trans.Control (MonadBaseControl)
+import Control.Monad.Trans.Resource (MonadThrow, ResourceT, runResourceT)
+import Database.Esqueleto
+       ( SqlExpr
+       , Value(..)
+       , from
+       , select
+       , set
+       , unValue
+       , update
+       , val
+       , where_
+       , (=.)
+       , (^.)
+       )
+import Database.Persist (PersistField(..), entityKey, get, insert)
+import Database.Persist.Postgresql
+       ( ConnectionString
+       , SqlPersistT
+       , runMigration
+       , runSqlConn
+       , transactionUndo
+       , withPostgresqlConn
+       )
+import Database.Persist.TH
+       (mkMigrate, mkPersist, persistUpperCase, share, sqlSettings)
+import Test.Hspec (Spec, describe, hspec, it, shouldBe)
+import Test.QuickCheck
+       (Arbitrary(..), choose, elements, listOf, listOf1, oneof, property)
+
+import Database.Esqueleto.TextSearch
+
+connString :: ConnectionString
+connString = "host=localhost port=5432 user=test dbname=test password=test"
+
+-- Test schema
+share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistUpperCase|
+  Article
+    title      Text
+    content    Text
+    textsearch TsVector
+    deriving Eq Show
+
+  WeightsModel
+    weights     Weights
+    deriving Eq Show
+
+  WeightModel
+    weight     Weight
+    deriving Eq Show
+
+  RegConfigModel
+    config     RegConfig
+    deriving Eq Show
+
+  QueryModel
+    query     (TsQuery Lexemes)
+    deriving Eq Show
+|]
+
+
+main :: IO ()
+main = hspec spec
+
+to_etsvector :: SqlExpr (Value Text) -> SqlExpr (Value TsVector)
+to_etsvector = to_tsvector (val "english")
+
+spec :: Spec
+spec = do
+  describe "TsVector" $ do
+    it "can be persisted and retrieved" $ run $ do
+      let article = Article "some title" "some content" def
+      arId <- insert article
+      update  $ \a -> do
+        set a [ArticleTextsearch =. to_etsvector (a^.ArticleContent)]
+      ret <- fromJust <$> get arId
+      liftIO $ articleTextsearch ret /= def `shouldBe` True
+
+    it "can be persisted and retrieved with weight" $ run $ do
+      let article = Article "some title" "some content" def
+      arId <- insert article
+      update  $ \a -> do
+        set a [  ArticleTextsearch
+              =. setweight (to_etsvector (a^.ArticleContent)) (val Highest)
+              ]
+      ret <- fromJust <$> get arId
+      liftIO $ articleTextsearch ret /= def `shouldBe` True
+
+  describe "Weight" $ do
+    it "can be persisted and retrieved" $ run $ do
+      forM_ [Low, Medium, High, Highest] $ \w -> do
+        let m = WeightModel w
+        wId <- insert m
+        ret <- get wId
+        liftIO $ ret `shouldBe` Just m
+
+  describe "Weights" $ do
+    it "can be persisted and retrieved" $ run $ do
+      let m = WeightsModel $ Weights 0.5 0.6 0.7 0.8
+      wsId <- insert m
+      ret <- get wsId
+      liftIO $ ret `shouldBe` Just m
+
+  describe "RegConfig" $ do
+    it "can be persisted and retrieved" $ run $ do
+      forM_ ["english", "spanish"] $ \c -> do
+        let m = RegConfigModel c
+        wId <- insert m
+        ret <- get wId
+        liftIO $ ret `shouldBe` Just m
+
+
+  describe "TsQuery" $ do
+    it "can be persisted and retrieved" $ run $ do
+      let qm = QueryModel (lexm "foo" :& lexm "bar")
+      qId <- insert qm
+      ret <- fromJust <$> get qId
+      liftIO $ qm `shouldBe` ret
+
+    describe "to_tsquery" $ do
+      it "converts words to lexemes" $ run $ do
+        lqs <- select $ return $
+          to_tsquery (val "english") (val ("supernovae" :& "rats"))
+        let lq = unValue $ head lqs
+        liftIO $ lq `shouldBe` (lexm "supernova" :& lexm "rat")
+
+    describe "plainto_tsquery" $ do
+      it "converts text to lexemes" $ run $ do
+        lqs <- select $ return $
+          plainto_tsquery (val "english") (val "rats in supernovae")
+        let lq = unValue $ head lqs
+        liftIO $ lq `shouldBe` (lexm "rat" :& lexm "supernova")
+
+    describe "queryToText" $ do
+      it "can serialize infix lexeme" $
+        queryToText (lexm "foo") `shouldBe` "'foo'"
+      it "can serialize infix lexeme with weights" $
+        queryToText (Lexeme Infix [Highest,Low] "foo") `shouldBe` "'foo':AD"
+      it "can serialize prefix lexeme" $
+        queryToText (Lexeme Prefix [] "foo") `shouldBe` "'foo':*"
+      it "can serialize prefix lexeme with weights" $
+        queryToText (Lexeme Prefix [Highest,Low] "foo") `shouldBe` "'foo':*AD"
+      it "can serialize AND" $
+        queryToText ("foo" :& "bar" :& "car") `shouldBe` "'foo'&('bar'&'car')"
+      it "can serialize OR" $
+        queryToText ("foo" :| "bar") `shouldBe` "'foo'|'bar'"
+      it "can serialize Not" $
+        queryToText (Not "bar") `shouldBe` "!'bar'"
+
+    describe "textToQuery" $ do
+      describe "infix lexeme" $ do
+        it "can parse it" $
+          textToQuery "'foo'" `shouldBe` Right (lexm "foo")
+        it "can parse it surrounded by spaces" $
+          textToQuery " 'foo' " `shouldBe` Right (lexm "foo")
+
+      describe "infix lexeme with weights" $ do
+        it "can parse it" $
+          textToQuery "'foo':AB"
+            `shouldBe` Right (Lexeme Infix [Highest,High] "foo")
+        it "can parse it surrounded by spaces" $
+          textToQuery " 'foo':AB "
+            `shouldBe` Right (Lexeme Infix [Highest,High] "foo")
+
+      describe "prefix lexeme" $ do
+        it "can parse it" $
+          textToQuery "'foo':*" `shouldBe` Right (Lexeme Prefix [] "foo")
+        it "can parse it surrounded byb spaces" $
+          textToQuery " 'foo':* " `shouldBe` Right (Lexeme Prefix [] "foo")
+
+      describe "prefix lexeme with weights" $ do
+        it "can parse it" $
+          textToQuery "'foo':*AB" `shouldBe`
+            Right (Lexeme Prefix [Highest,High] "foo")
+        it "can parse it surrounded by spaces" $
+          textToQuery " 'foo':*AB " `shouldBe`
+            Right (Lexeme Prefix [Highest,High] "foo")
+
+      describe "&" $ do
+        it "can parse it" $
+          textToQuery "'foo'&'bar'" `shouldBe`
+            Right (lexm "foo" :& lexm "bar")
+        it "can parse it surrounded by spaces" $ do
+          textToQuery "'foo' & 'bar'" `shouldBe`
+            Right (lexm "foo" :& lexm "bar")
+          textToQuery "'foo'& 'bar'" `shouldBe`
+            Right (lexm "foo" :& lexm "bar")
+          textToQuery "'foo' &'bar'" `shouldBe`
+            Right (lexm "foo" :& lexm "bar")
+          textToQuery " 'foo'&'bar' " `shouldBe`
+            Right (lexm "foo" :& lexm "bar")
+        it "can parse several" $
+          textToQuery "'foo'&'bar'&'car'" `shouldBe`
+            Right (lexm "foo" :& lexm "bar" :& lexm "car")
+
+      describe "|" $ do
+        it "can parse it" $
+          textToQuery "'foo'|'bar'" `shouldBe` Right (lexm "foo" :| lexm "bar")
+        it "can parse several" $
+          textToQuery "'foo'|'bar'|'car'" `shouldBe`
+            Right (lexm "foo" :| lexm "bar" :| lexm "car")
+
+      describe "mixed |s and &s" $ do
+        it "respects precedence" $ do
+          textToQuery "'foo'|'bar'&'car'" `shouldBe`
+            Right (lexm "foo" :| lexm "bar" :& lexm "car")
+          textToQuery "'foo'&'bar'|'car'" `shouldBe`
+            Right (lexm "foo" :& lexm "bar" :| lexm "car")
+
+      describe "!" $ do
+        it "can parse it" $
+          textToQuery "!'foo'" `shouldBe` Right (Not (lexm "foo"))
+
+      describe "! and &" $ do
+        it "can parse it" $ do
+          textToQuery "!'foo'&'car'" `shouldBe`
+            Right (Not (lexm "foo") :& lexm "car")
+          textToQuery "!('foo'&'car')" `shouldBe`
+            Right (Not (lexm "foo" :& lexm "car"))
+        it "can parse it surrounded by spaces" $ do
+          textToQuery "!'foo' & 'car'" `shouldBe`
+            Right (Not (lexm "foo") :& lexm "car")
+          textToQuery "!( 'foo' & 'car' )" `shouldBe`
+            Right (Not (lexm "foo" :& lexm "car"))
+
+    describe "textToQuery . queryToText" $ do
+      it "is isomorphism" $ property $ \q ->
+        (textToQuery . queryToText) q `shouldBe` Right q
+
+  describe "@@" $ do
+    it "works as expected" $ run $ do
+      let article = Article "some title" "some content" def
+      arId <- insert article
+      update  $ \a -> do
+        set a [ArticleTextsearch =. to_etsvector (a^.ArticleContent)]
+      let query = to_tsquery (val "english") (val "content")
+      result <- select $ from $ \a -> do
+        where_ $ (a^. ArticleTextsearch) @@. query
+        return a
+      liftIO $ do
+        length result `shouldBe` 1
+        map entityKey result `shouldBe` [arId]
+      let query2 = to_tsquery (val "english") (val "foo")
+      result2 <- select $ from $ \a -> do
+        where_ $ (a^. ArticleTextsearch) @@. query2
+        return a
+      liftIO $ length result2 `shouldBe` 0
+
+  describe "ts_rank_cd" $ do
+    it "works as expected" $ run $ do
+      let vector  = to_tsvector (val "english") (val content)
+          content = "content" :: Text
+          query   = to_tsquery (val "english") (val "content")
+          norm    = val []
+      ret <- select $ return $ ts_rank_cd (val def) vector query norm
+      liftIO $ map unValue ret `shouldBe` [0.1]
+
+  describe "ts_rank" $ do
+    it "works as expected" $ run $ do
+      let vector  = to_tsvector (val "english") (val content)
+          content = "content" :: Text
+          query   = to_tsquery (val "english") (val "content")
+          norm    = val []
+      ret <- select $ return $ ts_rank (val def) vector query norm
+      liftIO $ unValue (head ret) `shouldBe` 6.079271e-2
+
+  describe "NormalizationOption" $ do
+    describe "fromPersistValue . toPersistValue" $ do
+      let isEqual [] []         = True
+          isEqual [NormNone] [] = True
+          isEqual [] [NormNone] = True
+          isEqual a  b          = a == b
+          toRight (Right a)     = a
+          toRight _             = error "unexpected Left"
+      it "is isomorphism" $ property $ \(q :: [NormalizationOption]) ->
+        isEqual ((toRight . fromPersistValue . toPersistValue) q) q
+          `shouldBe` True
+
+instance {-# OVERLAPPING #-} Arbitrary [NormalizationOption] where
+    arbitrary = (:[]) <$> elements [minBound..maxBound]
+
+instance a ~ Lexemes => Arbitrary (TsQuery a) where
+  arbitrary = query 0
+    where
+      maxDepth :: Int
+      maxDepth = 10
+      query d
+        | d<maxDepth  = oneof [lexeme, and_ d, or_ d, not_ d]
+        | otherwise   = lexeme
+      lexeme    = Lexeme <$> arbitrary <*> weights <*> lexString
+      weights   = listOf arbitrary
+      and_ d    = (:&) <$> query (d+1) <*> query (d+1)
+      or_  d    = (:|) <$> query (d+1) <*> query (d+1)
+      not_ d    = Not  <$> query (d+1)
+      lexString = pack <$> listOf1 (oneof $ [ choose ('a','z')
+                                            , choose ('A','Z')
+                                            , choose ('0','9')
+                                            ] ++ map pure "-_&|ñçáéíóú")
+
+instance Arbitrary Position where
+  arbitrary = oneof [pure Infix, pure Prefix]
+
+instance Arbitrary Weight where
+  arbitrary = oneof [pure Highest, pure High, pure Medium, pure Low]
+
+
+lexm :: Text -> TsQuery Lexemes
+lexm = Lexeme Infix []
+
+type RunDbMonad m
+  = (MonadBaseControl IO m, MonadIO m, MonadLogger m, MonadThrow m)
+
+run :: (forall m. RunDbMonad m => SqlPersistT (ResourceT m) a) -> IO a
+run act
+  = runStderrLoggingT
+  . runResourceT
+  . withPostgresqlConn connString
+  . runSqlConn
+  $ (initializeDB >> act >>= \ret -> transactionUndo >> return ret)
+
+
+initializeDB
+  :: (forall m. RunDbMonad m
+  => SqlPersistT (ResourceT m) ())
+initializeDB  = do
+  runMigration migrateAll
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,7 @@
+module Main where
+
+import qualified Database.Esqueleto.TextSearchSpec
+
+
+main :: IO ()
+main = Database.Esqueleto.TextSearchSpec.main
