diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,3 @@
+# Changelog for word2vec-model
+
+## Unreleased changes
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Filip Graliński (c) 2017
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Filip Graliński nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,16 @@
+# word2vec-model
+
+Reading word2vec binary models (generated with the original tool by Mikolov).
+
+This simple module is only for *reading* word2vec models (it cannot be used
+to *generate* a word2vec model, for this the original word2vec tools should be used).
+
+Note that word2vec binary format is not a proper serialisation format (as it is mostly
+a raw dump of C data. *Caveat emptor*, it might be risky to read a model generated
+on a host with a different architecture.
+
+Example:
+
+    {-# LANGUAGE OverloadedStrings #-}
+    model <- readWord2VecModel "binary.bin"
+    let theMostSimilar = findKNearestToWord w2v 30 "polska"
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/app/Similarity.hs b/app/Similarity.hs
new file mode 100644
--- /dev/null
+++ b/app/Similarity.hs
@@ -0,0 +1,25 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main where
+
+import Data.Word2Vec.Model
+import Data.Text as T
+
+import qualified Data.Conduit.Binary as CB
+import qualified Data.Conduit.Text as CT
+import Data.Conduit
+import Data.Conduit.Combinators as C
+
+import System.Environment
+
+main :: IO ()
+main = do
+  (arg:_) <- getArgs
+  w2v <- readWord2VecModel arg
+  runConduitRes $ stdin .| CT.decodeUtf8Lenient .| CT.lines .|  C.map (getSimilarOnes w2v)
+                                                .| CT.encode CT.utf8 .| stdout
+
+getSimilarOnes :: Word2VecModel -> Text -> Text
+getSimilarOnes w2v w = T.unlines $ Prelude.map (format w) similarOnes
+  where similarOnes = findKNearestToWord w2v 30 w
+        format w (w', d) = T.intercalate "\t" [w, w', (T.pack $ show d)]
diff --git a/app/WordAnalogy.hs b/app/WordAnalogy.hs
new file mode 100644
--- /dev/null
+++ b/app/WordAnalogy.hs
@@ -0,0 +1,26 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main where
+
+import Data.Word2Vec.Model
+import Data.Text as T
+
+import qualified Data.Conduit.Binary as CB
+import qualified Data.Conduit.Text as CT
+import Data.Conduit
+import Data.Conduit.Combinators as C
+
+import System.Environment
+
+main :: IO ()
+main = do
+  (arg:_) <- getArgs
+  w2v <- readWord2VecModel arg
+  runConduitRes $ stdin .| CT.decodeUtf8Lenient .| CT.lines .|  C.map (solveAnalogy w2v)
+                                                .| CT.encode CT.utf8 .| stdout
+
+solveAnalogy :: Word2VecModel -> Text -> Text
+solveAnalogy w2v line = T.unlines $ Prelude.map (format a1 a2 b1) similarOnes
+  where [a1, a2, b1] = T.words line
+        similarOnes = solveWordAnalogy w2v 30 a1 a2 b1
+        format a1 a2 b1 (w', d) = T.intercalate "\t" [a1, a2, b1, w', (T.pack $ show d)]
diff --git a/src/Data/Word2Vec/Model.hs b/src/Data/Word2Vec/Model.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Word2Vec/Model.hs
@@ -0,0 +1,214 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE BangPatterns #-}
+
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  Data.Word2Vec.Model
+-- Copyright   :  Filip Graliński 2017
+-- License     :  BSD-style (see the LICENSE file in the distribution)
+--
+-- Maintainer  :  filipg@amu.edu.pl
+-- Stability   :  experimental
+-- Portability :  ?
+--
+-- Reading word2vec binary models (generated with the original tool by Mikolov).
+--
+-- This simple module is only for /reading/ word2vec models (it cannot be used
+-- to /generate/ a word2vec model, for this the original word2vec tools should be used).
+--
+-- Note that word2vec binary format is not a proper serialisation format (as it is mostly
+-- a raw dump of C data. /Caveat emptor/, it might be risky to read a model generated
+-- on a host with a different architecture.
+--
+-- Example:
+--
+-- @
+--   model <- readWord2VecModel "binary.bin"
+--   let theMostSimilar = findKNearestToWord w2v 30 "bar"
+-- @
+--
+-----------------------------------------------------------------------------
+
+module Data.Word2Vec.Model
+    (
+      -- * Main data structure
+      Word2VecModel
+
+      -- * Basic operations
+    , readWord2VecModel
+
+    , numberOfWords
+    , numberOfDimensions
+
+      -- * Operations on vectors
+    , WVector
+    , buildWVector
+    , getVector
+    , normalizeVector
+
+      -- * Looking for the nearest vector
+      --
+      -- (Like the distance/word-analogy tools in the original word2vec.)
+    , findNearestToWord
+    , findKNearestToWord
+    , findKNearestToVector
+    , solveWordAnalogy
+
+      -- * Helper functions
+    , cosineSimilarity
+    , dotProduct
+    ) where
+
+import qualified Data.HashMap.Strict as DHS
+import qualified Data.Text as T
+import Data.Text.Encoding
+import qualified Data.Vector.Storable as V
+import qualified Data.ByteString.Lazy as BL
+import qualified Data.ByteString.Internal as BS
+import qualified Data.Attoparsec.ByteString.Lazy as AL
+import qualified Data.Attoparsec.ByteString.Char8 as AP
+import Data.Binary
+import Data.Binary.Get
+import Data.Binary.IEEE754
+import Data.List (maximumBy, foldl')
+import Data.Maybe (catMaybes)
+
+-- | Vector stored in a word2vec model with its norm (to speed up
+-- calculating cosine similarities).
+data WVector = WVector (V.Vector Float) -- vector itself
+                       Float            -- norm
+               deriving (Eq, Show)
+
+-- | Word2vec Model
+data Word2VecModel = Word2VecModel Int -- number of words
+                                   Int -- number of dimensions
+                                   !(DHS.HashMap T.Text WVector) -- word-to-vector map
+                     deriving (Eq, Show)
+
+-- | Main function, reading a word2vec binary model into memory.
+readWord2VecModel :: FilePath -> IO (Word2VecModel)
+readWord2VecModel fileName = do
+  contents <- BL.readFile fileName
+  pure $ processWord2VecBinaryModel contents
+
+-- | Get the number of words in the model.
+numberOfWords :: Word2VecModel -> Int
+numberOfWords (Word2VecModel n _ _) = n
+
+-- | Get the number of dimensions.
+numberOfDimensions :: Word2VecModel -> Int
+numberOfDimensions (Word2VecModel _ d _) = d
+
+-- | /(Practically) O(1)/ Get a vector for a given word.
+getVector :: Word2VecModel -> T.Text -> Maybe WVector
+getVector (Word2VecModel _ _ h) w = DHS.lookup w h
+
+processWord2VecBinaryModel :: BL.ByteString -> Word2VecModel
+processWord2VecBinaryModel contents =
+  case AL.parse parseWord2VecBinaryModel contents of
+    AL.Fail _ _ _ -> error "does not look like a word2vec binary model"
+    AL.Done _ !m -> m
+
+parseWord2VecBinaryModel :: AP.Parser Word2VecModel
+parseWord2VecBinaryModel = do
+  nbOfWords <- AP.decimal
+  " "
+  nbOfDimensions <- AP.decimal
+  "\n"
+  entries <- AP.many' (parseWord2VecEntry nbOfDimensions <* "\n")
+  return $ Word2VecModel nbOfWords nbOfDimensions $ DHS.fromList entries
+
+floatSize :: Int
+floatSize = 4
+
+parseWord2VecEntry :: Int -> AP.Parser (T.Text, WVector)
+parseWord2VecEntry nbOfDimensions = do
+  word <- AP.takeWhile1 (not . AP.isSpace)
+  " "
+  floatVectorRaw <- AP.take (floatSize * nbOfDimensions)
+  return (decodeUtf8 word, buildWVector $ bytesToFloats floatVectorRaw)
+
+-- see https://stackoverflow.com/questions/20912582/haskell-bytestring-to-float-array
+bytesToFloats :: BS.ByteString -> V.Vector Float
+bytesToFloats = V.unsafeCast . aux . BS.toForeignPtr
+  where aux (fp,offset,len) = V.unsafeFromForeignPtr fp offset len
+
+-- | Build a word2vec vector from a raw float vector
+--
+-- (Just calculates its norm.)
+buildWVector :: V.Vector Float -> WVector
+buildWVector v = WVector v (norm v)
+
+-- | Normalise a vector with its norm
+normalizeVector :: WVector -> WVector
+normalizeVector (WVector v n) = buildWVector (V.map (/ n) v)
+
+-- | Calculate cosine similarity between two word2vec vectors.
+--
+-- Note that it was called wrongly /cosine distance/ in the original word2vec.
+cosineSimilarity :: WVector -> WVector -> Float
+cosineSimilarity (WVector veca norma) (WVector vecb normb) = (dotProduct veca vecb) / (norma * normb)
+  where norm v = V.sum $ V.map (\e -> e * e) v
+
+-- | Calculate dot product between two word2vec vectors
+dotProduct :: V.Vector Float -> V.Vector Float -> Float
+dotProduct veca vecb = V.sum $ V.zipWith (*) veca vecb
+
+norm :: V.Vector Float -> Float
+norm = sqrt . V.sum . V.map (\e -> e * e)
+
+-- | /O(n) where n is the number of words, assuming k is small/ Find
+-- the top-k most similar words for a given word. (The queried word is
+-- excluded.)
+findKNearestToWord :: Word2VecModel -> Int -> T.Text -> [(T.Text, Float)]
+findKNearestToWord m k w = case getVector m w of
+  Just v -> filter (\(t,_) -> t /= w) $ findKNearestToVector m (k+1) v
+  Nothing -> []
+
+-- | /O(n) where n is the number of words, assuming k is small/ Find
+-- the top-k most similar words for a given vector.
+findKNearestToVector :: Word2VecModel -> Int -> WVector -> [(T.Text, Float)]
+findKNearestToVector m@(Word2VecModel _ _ h) k v = reverse $ catMaybes
+                                                 $ foldl' step (replicate k Nothing)
+                                                 $ map (\(w',v') -> (w', cosineSimilarity v' v)) $ DHS.toList h
+  where step [] v = [Just v] -- not really needed, for completeness
+        step l@(lowest:theRest) v = if isBetter v lowest
+                                    then
+                                      insertInto v theRest
+                                    else
+                                      l
+        insertInto v [] = [Just v]
+        insertInto v l@(lowest:theRest) = if isBetter v lowest
+                                        then
+                                          (lowest:insertInto v theRest)
+                                        else
+                                          (Just v:l)
+        isBetter _ Nothing = True
+        isBetter (_, s1) (Just (_, s2)) = s1 > s2
+
+-- | /O(n) where n is the number of words/ Find the word which is the
+-- most similar to a given word. (The queried word is excluded.)
+findNearestToWord :: Word2VecModel -> T.Text -> Maybe (T.Text, Float)
+findNearestToWord m@(Word2VecModel _ _ h) w = findNearestToWord' h <$> (getVector m w)
+   where findNearestToWord' h v = maximumBy (\(_,p) (_, q) -> p `compare` q)
+                                  $ map (\(w',v') -> (w', cosineSimilarity v' v))
+                                  $ filter (\(w',_) -> w' /= w) $ DHS.toList h
+
+solveWordAnalogy :: Word2VecModel -> Int -> T.Text -> T.Text -> T.Text -> [(T.Text, Float)]
+solveWordAnalogy m k a1 a2 b1 = case targetVector of
+  Just v -> findKNearestToVector m k v
+  Nothing -> []
+  where targetVector = getVectorByAnalogy <$>
+                                (getVector m a1) <*> (getVector m a2) <*> (getVector m b1)
+        getVectorByAnalogy v1 v2 u1 = vadd (normalizeVector u1) (vsubtract (normalizeVector v2)
+                                                                           (normalizeVector v1))
+
+pointWiseOperation :: (Float -> Float -> Float) -> WVector -> WVector -> WVector
+pointWiseOperation fun (WVector v1 _) (WVector v2 _) =
+  buildWVector $ V.zipWith fun v1 v2
+
+vadd :: WVector -> WVector -> WVector
+vadd = pointWiseOperation (+)
+
+vsubtract :: WVector -> WVector -> WVector
+vsubtract = pointWiseOperation (-)
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,67 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+import Test.Hspec
+
+import Data.Word2Vec.Model
+
+import qualified Data.Vector.Storable as V
+
+import qualified Test.HUnit as HU
+
+import Control.Monad
+
+main :: IO ()
+main = hspec $ do
+  describe "basic facilities" $ do
+    it "trivial dot product" $ do
+      dotProduct (V.fromList [1.0, 2.0]) (V.fromList [1.0, 2.0]) `shouldBeAlmost` 5.0
+    it "dot product" $ do
+      dotProduct (V.fromList [0.5, 0.0, -2.0]) (V.fromList [3.0, 5.3, 1.0]) `shouldBeAlmost` (-0.5)
+    it "cosine similarity (dissimilar)" $ do
+      cosineSimilarity (getWVector [0.0, 4.3]) (getWVector [2.7, 0.0]) `shouldBeAlmost` 0.0
+    it "cosine similarity (perfect similarity — trivial)" $ do
+      cosineSimilarity (getWVector [1.0, 2.0]) (getWVector [1.0, 2.0]) `shouldBeAlmost` 1.0
+    it "cosine similarity (perfect similarity)" $ do
+      cosineSimilarity (getWVector [1.0, -3.7]) (getWVector [2.0, -7.4]) `shouldBeAlmost` 1.0
+  describe "reading a small binary model" $ do
+    it "find the most similar (cosine)" $ do
+      model <- readWord2VecModel "test/sample1.bin"
+      let Just (theWord, _) = findNearestToWord model "rębajło"
+      numberOfWords model `shouldBe` 3997
+      numberOfDimensions model `shouldBe` 20
+      theWord `shouldBe` "asesor"
+    it "find K most similar ones (cosine)" $ do
+      model <- readWord2VecModel "test/sample1.bin"
+      (map fst $ findKNearestToWord model 5 "umiał") `shouldBe` ["myśliłem",
+                                                                 "myślili",
+                                                                 "złości",
+                                                                 "złość",
+                                                                 "czas"]
+    it "solve a word analogy puzzle" $ do
+      model <- readWord2VecModel "test/sample1.bin"
+      (map fst $ solveWordAnalogy model 5 "tadeusz" "polska" "zosia") `shouldBe` [
+          "polska",
+          "kochany",
+          "polski",
+          "dziewczyny",
+          "naród" ]
+
+getWVector :: [Float] -> WVector
+getWVector = buildWVector . V.fromList
+
+class AEq a where
+    (=~) :: a -> a -> Bool
+
+instance AEq Float where
+    x =~ y = abs ( x - y ) < (1.0e-4 :: Float)
+
+(@=~?) :: (Show a, AEq a) => a -> a -> HU.Assertion
+(@=~?) actual expected = expected =~ actual HU.@? assertionMsg
+    where
+      assertionMsg = "Expected : " ++ show expected ++
+                     "\nActual   : " ++ show actual
+
+shouldBeAlmost got expected = got @=~? expected
+
+shouldReturnAlmost :: (AEq a, Show a, Eq a) => IO a -> a -> Expectation
+shouldReturnAlmost action expected = action >>= (@=~? expected)
diff --git a/word2vec-model.cabal b/word2vec-model.cabal
new file mode 100644
--- /dev/null
+++ b/word2vec-model.cabal
@@ -0,0 +1,112 @@
+-- This file has been generated from package.yaml by hpack version 0.21.2.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: 89967af707c702f80ea2fdcecf346aeae1f656bba7c4e0957f907f564751b783
+
+name:           word2vec-model
+version:        0.1.0.0
+synopsis:       Reading word2vec binary models
+description:    Please see the README on Github at <https://gonito.net/gitlist/word2vec-model.git/blob/master/README.md>
+homepage:       https://gonito.net/gitlist/word2vec-model.git
+author:         Filip Graliński
+maintainer:     filipg@amu.edu.pl
+copyright:      BSD3
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+cabal-version:  >= 1.10
+
+extra-source-files:
+    ChangeLog.md
+    README.md
+
+source-repository head
+  type: git
+  location: git://gonito.net/word2vec-model.git
+
+library
+  exposed-modules:
+      Data.Word2Vec.Model
+  other-modules:
+      Paths_word2vec_model
+  hs-source-dirs:
+      src
+  build-depends:
+      attoparsec
+    , base >=4.7 && <5
+    , binary
+    , binary-ieee754
+    , bytestring
+    , text
+    , unordered-containers
+    , vector
+  default-language: Haskell2010
+
+executable word2vec-model-similarity
+  main-is: Similarity.hs
+  other-modules:
+      WordAnalogy
+      Paths_word2vec_model
+  hs-source-dirs:
+      app
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      attoparsec
+    , base >=4.7 && <5
+    , binary
+    , binary-ieee754
+    , bytestring
+    , conduit
+    , conduit-combinators
+    , conduit-extra
+    , text
+    , unordered-containers
+    , vector
+    , word2vec-model
+  default-language: Haskell2010
+
+executable word2vec-model-word-analogy
+  main-is: WordAnalogy.hs
+  other-modules:
+      Similarity
+      Paths_word2vec_model
+  hs-source-dirs:
+      app
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      attoparsec
+    , base >=4.7 && <5
+    , binary
+    , binary-ieee754
+    , bytestring
+    , conduit
+    , conduit-combinators
+    , conduit-extra
+    , text
+    , unordered-containers
+    , vector
+    , word2vec-model
+  default-language: Haskell2010
+
+test-suite word2vec-model-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Paths_word2vec_model
+  hs-source-dirs:
+      test
+  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      HUnit
+    , attoparsec
+    , base >=4.7 && <5
+    , binary
+    , binary-ieee754
+    , bytestring
+    , hspec
+    , text
+    , unordered-containers
+    , vector
+    , word2vec-model
+  default-language: Haskell2010
