packages feed

hunt-searchengine (empty) → 0.3.0.0

raw patch · 57 files changed

+11303/−0 lines, 57 filesdep +HUnitdep +QuickCheckdep +aesonsetup-changed

Dependencies added: HUnit, QuickCheck, aeson, aeson-pretty, base, binary, bytestring, containers, data-default, data-r-tree, data-stringmap, deepseq, directory, dlist, filepath, ghc-heap-view, hslogger, hunt-searchengine, hxt-regex-xmlschema, monad-parallel, mtl, murmur-hash, old-locale, parsec, random, test-framework, test-framework-hunit, test-framework-quickcheck2, text, time, transformers, unordered-containers, vector

Files

+ LICENSE view
@@ -0,0 +1,22 @@+The MIT License (MIT)++Copyright (c) 2007 Timo B. Hübel, Sebastian M. Schlatt,+              2014 Chris Reumann, Ulf Sauer++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.
+ Setup.lhs view
@@ -0,0 +1,8 @@+#!/usr/bin/runhaskell++> module Main where++> import Distribution.Simple++> main :: IO ()+> main = defaultMain
+ hunt-searchengine.cabal view
@@ -0,0 +1,189 @@+name:          hunt-searchengine+version:       0.3.0.0+license:       MIT+license-file:  LICENSE+author:        Chris Reumann, Ulf Sauer, Uwe Schmidt+copyright:     Chris Reumann, Ulf Sauer, Uwe Schmidt+maintainer:    Chris Reumann, Ulf Sauer, Uwe Schmidt+stability:     experimental+category:      Text, Data+synopsis:      A search and indexing engine.+homepage:      http://github.com/hunt-framework/+description:   The Hunt-Searchengine library provides a toolkit to+               create fast and flexible searchengines.+cabal-version: >=1.8+build-type:    Simple+-- tested-with:   ghc-7.6.3++-- extra-source-files:+--     README++-- enable with cabal test -ftest-strict+flag test-strict+  default:      False+  manual:       True++source-repository head+  type:        git+  location:    https://github.com/hunt-framework/hunt-searchengine.git++library++  build-depends: base                   >= 4.5  && < 5+               , aeson                  >= 0.6+               , aeson-pretty           >= 0.7+               , binary                 >= 0.5  && < 1+               , bytestring             < 1+               , containers             >= 0.5+               , data-r-tree            >= 0.0.5.0+               , data-stringmap         >= 1.0.1.1 && < 2+               , data-default+               , deepseq                >= 1.2+               , dlist+               , filepath               >= 1+               , hslogger               >= 1    && < 2+               , hxt-regex-xmlschema    >= 9.1+               , monad-parallel         >= 0.7+               , mtl                    >= 1.1  && < 3+               , murmur-hash+               , parsec                 >= 2.1  && < 4+               , text                   >= 1    && < 1.2+               , time                   >= 1.4  && < 2+               , transformers           >= 0.3+               , unordered-containers   >= 0.2+               , vector                 >= 0.10++  exposed-modules:+    Control.Concurrent.XMVar++    Data.Bijection+    Data.Bijection.Instances+    Data.IntMap.BinTree.Strict+    Data.IntSet.Cache+    Data.LimitedPriorityQueue+    Data.Text.Binary+    Data.Typeable.Binary++    GHC.Fingerprint.Binary+    GHC.Stats.Json++    Hunt.ClientInterface++    Hunt.Common+    Hunt.Common.ApiDocument+    Hunt.Common.BasicTypes+    Hunt.Common.DocDesc+    Hunt.Common.DocId+    Hunt.Common.DocIdMap+    Hunt.Common.DocIdSet+    Hunt.Common.Document+    Hunt.Common.Occurrences+    Hunt.Common.Positions+    Hunt.Common.RawResult+    Hunt.Common.IntermediateValue++    Hunt.Index.Schema+    Hunt.Index.Schema.Analyze+    Hunt.Index.Schema.Normalize.Date+    Hunt.Index.Schema.Normalize.Position+    Hunt.Index.Schema.Normalize.Int++    Hunt.Index+    Hunt.Index.IndexImpl+    Hunt.Index.PrefixTreeIndex+    Hunt.Index.PrefixTreeIndex2Dim+    Hunt.Index.RTreeIndex+    Hunt.Index.InvertedIndex++    Hunt.Index.Proxy.KeyIndex++    Hunt.DocTable+    Hunt.DocTable.HashedDocTable++    Hunt.ContextIndex++    Hunt.Interpreter+    Hunt.Interpreter.BasicCommand+    Hunt.Interpreter.Command++    Hunt.Query.Fuzzy+    Hunt.Query.Intermediate+    Hunt.Query.Language.Grammar+    Hunt.Query.Language.Parser+    Hunt.Query.Language.Builder+    Hunt.Query.Processor+    Hunt.Query.Ranking+    Hunt.Query.Result++    Hunt.Utility+    Hunt.Utility.Log+    Hunt.Utility.Output++  hs-source-dirs: src++  ghc-options: -Wall -funbox-strict-fields -fwarn-tabs -threaded++  extensions: MultiParamTypeClasses+              FlexibleContexts+              OverloadedStrings+              TypeFamilies+              ConstraintKinds+              KindSignatures+              CPP++test-suite Hunt-Tests+  hs-source-dirs:       test+  main-is:              Hunt.hs+  type:                 exitcode-stdio-1.0+  ghc-options:          -Wall+  extensions:           OverloadedStrings++  build-depends: base+               , containers+               , HUnit+               , QuickCheck+               , test-framework+               , test-framework-hunit+               , test-framework-quickcheck2+               , text+               , old-locale+               , time+               , aeson+               , binary+               , hunt-searchengine+               , mtl+               , random+               , directory+               , data-default+               , data-r-tree+               , monad-parallel++test-suite Hunt-Strictness+   hs-source-dirs:       test+   main-is:              Strictness.hs+   type:                 exitcode-stdio-1.0+   ghc-options:          -Wall+   extensions:           OverloadedStrings++   if !flag(test-strict)+     buildable: False+   else+     build-depends: base+                  , containers+                  , deepseq+                  , ghc-heap-view                >= 0.5+                  , HUnit+                  , hunt-searchengine+                  , mtl+                  , QuickCheck+                  , random                       >= 1.0+                  , test-framework+                  , test-framework-hunit+                  , test-framework-quickcheck2+                  , text+                  , aeson+                  , unordered-containers+                  , time+                  , old-locale+                  , monad-parallel+                  , data-default
+ src/Control/Concurrent/XMVar.hs view
@@ -0,0 +1,94 @@+-- ----------------------------------------------------------------------------+{- |+  An 'MVar' variation that only blocks for modification.+  Readers are never blocked but write access is carried out in sequence.++  This is done with two 'MVar's.+  While modification is done, the readers use the old value.+  When the modification is done, the old (unmodified) value is replaced with the new one.+  For this to work, the writers have to block each other which is done with the second 'MVar'.+  This process is encapsulated in 'modifyXMVar' and 'modifyXMVar_'.++  /Note/: This may increase the memory usage since there may be two value present at a time.+          This is intended to be used with (big) data structures where small changes are made.+-}+-- ----------------------------------------------------------------------------++module Control.Concurrent.XMVar+  ( XMVar+  , newXMVar+  , readXMVar, modifyXMVar, modifyXMVar_+  , takeXMVarWrite, putXMVarWrite+  , takeXMVarLock, putXMVarLock+  )+where++import           Control.Concurrent.MVar+import           Control.Exception++-- ------------------------------------------------------------++-- | An 'MVar' variation that only blocks for modification.+--   It consists of two 'MVar's. One for the value which can always be read and the second one+--   to block writers so that modifications are done sequentially.+data XMVar a = XMVar (MVar a) (MVar ())++-- ------------------------------------------------------------++-- | Create a new 'XMVar' with the supplied value.+newXMVar :: a -> IO (XMVar a)+newXMVar v = do+  m <- newMVar v+  l <- newMVar ()+  return $ XMVar m l++-- | Read the value.+readXMVar :: XMVar a -> IO a+readXMVar (XMVar m _)+  = readMVar m++-- | Modify the content.+modifyXMVar :: XMVar a -> (a -> IO (a, b)) -> IO b+modifyXMVar (XMVar m l) f+  = mask $ \restore -> do+    _ <- takeMVar l+    v <- readMVar m+    (v',a) <- restore (f v) `onException` putMVar l ()+    _ <- swapMVar m v'+    putMVar l ()+    return a++-- | Like 'modifyXMVar' but without a return value.+modifyXMVar_ :: XMVar a -> (a -> IO a) -> IO ()+modifyXMVar_ (XMVar m l) f+  = mask $ \restore -> do+    _  <- takeMVar l+    v  <- readMVar m+    v' <- restore (f v) `onException` putMVar l ()+    _  <- swapMVar m v'+    putMVar l ()++-- | Locks for writes and reads the value. Readers do not block each other.+--   'modifyXMVar' encapsulates 'takeXMVarWrite' and 'putXMVarWrite' and also handles exceptions.+takeXMVarWrite :: XMVar a -> IO a+takeXMVarWrite (XMVar m l)+  = takeMVar l >> readMVar m++-- | Replaces the value (since it was locked for potential writers) and unlocks writers.+putXMVarWrite :: XMVar a -> a -> IO ()+putXMVarWrite (XMVar m l) v+  = swapMVar m v >> putMVar l ()++-- | Locks for both reads and writes ('MVar' behaviour).+--   This may be useful to save space because the old @a@ does not have to be kept in memory for+--   read access. Note that references to the old @a@ might still lead to memory leaks/issues.+takeXMVarLock :: XMVar a -> IO a+takeXMVarLock (XMVar m l)+  = takeMVar l >> takeMVar m++-- | Replaces the value (since it was locked for potential writers) and unlocks writers.+putXMVarLock :: XMVar a -> a -> IO ()+putXMVarLock (XMVar m l) v+  = putMVar m v >> putMVar l ()++-- ------------------------------------------------------------
+ src/Data/Bijection.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE MultiParamTypeClasses #-}++-- ----------------------------------------------------------------------------+{- |+'Bijection' instances represent a bijection between two types and allow conversion to and from.+-}+-- ----------------------------------------------------------------------------++module Data.Bijection where++-- | Bijection between two types @a@ and @b@.+--   'to' and 'from' represent the bijective function.+--+-- For a proper bijection between x and y, two instances need to be defined (@Bijection x y@ and+-- @Bijection y x@).+class Bijection a b where+  to   :: a -> b+  from :: b -> a++-- This works, but requires XFlexibleInstances XUndecidableInstances which causes errors not being+-- checked by the compiler. Probably because the constraint is not smaller than the head (Paterson-+-- Condition?), which is allowed with XUndecidableInstances.+{-+-- | Only one way needs to be defined.+instance Bijection a b => Bijection b a where+  to   = from+  from = to+-}++-- ------------------------------------------------------------
+ src/Data/Bijection/Instances.hs view
@@ -0,0 +1,30 @@+{-# OPTIONS -fno-warn-orphans     #-}+{-# LANGUAGE FlexibleInstances     #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverlappingInstances  #-}++-- ----------------------------------------------------------------------------+{- |+  Default 'Bijection' instances.+-}+-- ----------------------------------------------------------------------------++module Data.Bijection.Instances where++import           Data.Text++import           Data.Bijection++-- ------------------------------------------------------------++-- | 'Text' to 'String'.+instance Bijection Text String where+  to   = unpack+  from = pack++-- | 'String' to 'Text'.+instance Bijection String Text where+  to   = pack+  from = unpack++-- ------------------------------------------------------------
+ src/Data/IntMap/BinTree/Strict.hs view
@@ -0,0 +1,700 @@+{-# LANGUAGE BangPatterns       #-}+{-# LANGUAGE CPP                #-}+{-# LANGUAGE DeriveDataTypeable #-}++module Data.IntMap.BinTree.Strict where++import           Control.Applicative (Applicative (..), (<$>))+import           Control.DeepSeq+import           Control.Monad++import           Data.Binary         (Binary (..), getWord8)+import qualified Data.Foldable       as F+import qualified Data.IntSet         as S+import qualified Data.List           as L+import           Data.Traversable    (Traversable (..))+import           Data.Typeable+import           Data.Word           (Word8)++import           Prelude             hiding (foldl, foldr, lookup, map, null)++moduleName :: String+moduleName = "Data.IntMap.BinTree.Strict"++error' :: String -> a+error' = error . ((moduleName ++": ") ++)++-- ------------------------------------------------------------++type Key = Int++type IntMap v = Tree v++data Tree v = Empty+            | Node !Key !v !(Tree v) !(Tree v)+            | Lt   !Key !v !(Tree v)            -- Lt, Gt and Leaf are special cases+            | Gt   !Key !v           !(Tree v)  -- of Node to get rid of the empty trees+            | Leaf !Key !v                      -- Empty occurs only as root, never as subtree+              deriving (Show, Typeable)         -- This saves 20% of space, average size of+                                                -- objects is 4 instead of 5 words++-- ------------------------------------------------------------+--+-- instances for NFData and Binary++instance Eq v => Eq (Tree v) where+    (==) = equal++instance NFData v => NFData (Tree v) where+    rnf (Node _k v l r) = rnf v `seq` rnf l `seq` rnf r+    rnf (Lt   _k v l  ) = rnf v `seq` rnf l+    rnf (Gt   _k v   r) = rnf v `seq`             rnf r+    rnf (Leaf _k v    ) = rnf v+    rnf Empty           = ()++instance Binary v => Binary (Tree v) where+    put (Empty       ) = put (0::Word8)+    put (Node k v l r) = put (1::Word8) >> put k >> put v >> put l >> put r+    put (Lt   k v l  ) = put (2::Word8) >> put k >> put v >> put l+    put (Gt   k v   r) = put (3::Word8) >> put k >> put v          >> put r+    put (Leaf k v    ) = put (4::Word8) >> put k >> put v++    get = do tag <- getWord8+             case tag of+               0 -> return Empty+               1 -> Node <$> get <*> get <*> get <*> get+               2 -> Lt   <$> get <*> get <*> get+               3 -> Gt   <$> get <*> get         <*> get+               4 -> Leaf <$> get <*> get+               _ -> error' "error in \"get\" while decoding BinTree"++instance Functor Tree where+    fmap = map++instance F.Foldable Tree where+    foldr  = foldr+    foldl  = foldl+    foldr' = foldr'+    foldl' = foldl'++    {-# INLINE foldr    #-}+    {-# INLINE foldl    #-}+    {-# INLINE foldr'   #-}+    {-# INLINE foldl'   #-}++instance Traversable Tree where+    traverse f (Node k v l r) = Node k <$> f v <*> traverse f l <*> traverse f r+    traverse f (Lt   k v l  ) = Lt   k <$> f v <*> traverse f l+    traverse f (Gt   k v   r) = Gt   k <$> f v                  <*> traverse f r+    traverse f (Leaf k v    ) = Leaf k <$> f v+    traverse _ (Empty       ) = pure Empty++traverseWithKey :: Applicative t => (Key -> a -> t b) -> Tree a -> t (Tree b)+traverseWithKey f+    = go+    where+      go Empty = pure Empty+      go t     = mkNode k <$> f k v <*> go l <*> go r+                 where+                   (k, v, l, r) = unNode t++--       = (pure DIM <*>) . IM.traverseWithKey f . unDIM++-- ------------------------------------------------------------+--+-- the smart constructor generating Lt, Gt and Leaf nodes++mkNode :: Key -> v -> Tree v -> Tree v -> Tree v+mkNode k v Empty Empty = Leaf k v+mkNode k v Empty r     = Gt   k v   r+mkNode k v l     Empty = Lt   k v l+mkNode k v l     r     = Node k v l r++{-# INLINE mkNode #-}+++-- the smart destructor for normalization of Lt, Gt and Leaf nodes++unNode :: Tree v -> (Key, v, Tree v, Tree v)+unNode (Node k v l r) = (k, v, l,     r    )+unNode (Lt   k v l  ) = (k, v, l,     Empty)+unNode (Gt   k v   r) = (k, v, Empty, r    )+unNode (Leaf k v    ) = (k, v, Empty, Empty)+unNode  Empty         = error' "\"unNode\" with empty tree"++{-# INLINE unNode #-}++-- ------------------------------------------------------------+--+-- the work horses++split' :: Key -> Tree v -> (Maybe v, Tree v, Tree v)+split' _ Empty+    = (Nothing, Empty, Empty)++split' k t+    = case compare k k' of+        LT -> let (v', l', r') = split' k l+              in  (v', l', mkNode k' v r' r)++        EQ -> (Just v, l, r)++        GT -> let (v', l', r') = split' k r+              in  (v', mkNode k' v l l', r')+      where+        (k', v, l, r) = unNode t++join' :: Maybe (Key, v) -> Tree v -> Tree v -> Tree v+join' (Just (k, v)) t1 t2+    = mkNode k v t1 t2++join' Nothing t1 t2+    = case minViewWithKey t2 of                 -- get smallest key in t2+        Nothing          -> t1                  -- and return t1+        Just ((k, v), r) -> mkNode k v t1 r     -- or take that key as new root++{- disabled due to unbalancing of result tree++join' Nothing t1 t2+    = go t1+      where+        go Empty = t2                           -- insert whole t2 at the rightmost node in t1+        go t     = mkNode k v l (go r)          -- balancing ???+                   where+                     (k, v, l, r) = unNode t+-- -}++{-# INLINE join' #-}++-- ------------------------------------------------------------+{-+-- lookup, insert and delete with split' and join'+-- insert and delete change the root+-- and can lead to stronger unbalancing than trad. insert and delete++lookup :: Key -> Tree v -> Maybe v+lookup k t+    = v+      where+        (v, _l, _r) = split' k t++insertWith :: (v -> v -> v) -> Key -> v -> Tree v -> Tree v+insertWith f k v t+    = join' (Just (k, f' v')) l r+      where+        (v', l, r)    = split' k t+        f' Nothing    =       v+        f' (Just v'') = f v'' v++delete :: Key -> Tree v -> Tree v+delete k t+    = join' Nothing l r+      where+        (_, l, r) = split' k t++{-# INLINE lookup     #-}+{-# INLINE insertWith #-}+{-# INLINE delete     #-}++-- -}+-- ------------------------------------------------------------+-- {-+-- traditional lookup, insert and remove++lookup :: Key -> Tree v -> Maybe v+lookup k+    = go+      where+        go Empty = Nothing+        go t     = case compare k k' of+                     LT -> go l+                     EQ -> Just v+                     GT -> go r+                   where+                     (k', v, l, r) = unNode t++insertWith :: (v -> v -> v) -> Key -> v -> Tree v -> Tree v+insertWith f k v+    = ins+    where+      ins Empty = mkNode k v Empty Empty+      ins t     = case compare k k' of+                    LT -> mkNode k' v'      (ins l)      r+                    EQ -> mkNode k (f v v')      l       r+                    GT -> mkNode k' v'           l  (ins r)+                  where+                    (k', v', l, r) = unNode t++delete :: Key -> Tree v -> Tree v+delete k+    = del+    where+      del Empty = Empty+      del t     = case compare k k' of+                    LT -> mkNode k' v' (del l) r+                    EQ -> case minViewWithKey r of+                            Nothing               -> Empty+                            Just ((k'', v''), r') -> mkNode k'' v'' l r'+                    GT -> mkNode k' v' l (del r)+                  where+                    (k', v', l, r) = unNode t++{-# INLINE lookup          #-}+{-# INLINE insertWith      #-}+{-# INLINE delete          #-}++-- -}+-- ------------------------------------------------------------+--+-- derived lookup and insert functions++find :: Key -> Tree v -> v+find k = maybe notThere id . lookup k+    where+      notThere = error' ( "error in find: key "+                         ++ show k+                         ++ " is not an element of the map"+                       )++findWithDefault :: v -> Key -> Tree v -> v+findWithDefault v k = maybe v id . lookup k++member :: Key -> Tree v -> Bool+member k = maybe False (const True) . lookup k++notMember :: Key -> Tree v -> Bool+notMember k = maybe True (const False) . lookup k+++insert :: Key -> v -> Tree v -> Tree v+insert = insertWith const+++{-# INLINE find            #-}+{-# INLINE findWithDefault #-}+{-# INLINE member          #-}+{-# INLINE notMember       #-}+{-# INLINE insert          #-}++-- ------------------------------------------------------------+--+-- primitive operations++empty :: Tree v+empty = Empty++null :: Tree v -> Bool+null Empty = True+null _     = False++size :: Tree v -> Int+size = foldl' (\ cnt _ -> cnt + 1) 0++-- | retuns the size of a tree or Nothing if @size t > limit@+--+-- limits the computation time to O(limit), not O(size)++sizeWithLimit :: Int -> Tree v -> Maybe Int+sizeWithLimit limit+    = go 0+    where+      go !i Empty+          = return i+      go !i t+          | i == limit+              = mzero+          | otherwise+              = do i' <- go (i+1) l+                   go i' r+          where+            (_k, _v, l, r) = unNode t++-- ------------------------------------------------------------++union :: Tree v -> Tree v -> Tree v+union = unionWith const++unionWith :: (v -> v -> v) -> Tree v -> Tree v -> Tree v+unionWith op+    = unionWithKey (const op)++unionWithKey' :: (Key -> v -> v -> v) -> Tree v -> Tree v -> Tree v+unionWithKey' _  x1 Empty+    = x1+unionWithKey' f x1 x2+    = uni x1 x2+    where+      uni Empty t2 = t2+      uni t1    t2 = join' (Just (k, v')) (uni l l') (uni r r')+                     where+                       (k, v, l, r) = unNode t1+                       (m', l', r') = split' k t2+                       v'           = maybe v (f k v) m'++unionsWith :: (v -> v -> v) -> [Tree v] -> Tree v+unionsWith f = L.foldl' (\ acc t -> unionWith f acc t) empty++{-# INLINE union         #-}+{-# INLINE unionWith     #-}+{-# INLINE unionWithKey' #-}++-- ------------------------------------------------------------++difference :: Tree a -> Tree b -> Tree a+difference = differenceWith (const (const Nothing))++differenceWith :: (a -> b -> Maybe a) -> Tree a -> Tree b -> Tree a+differenceWith op+    = differenceWithKey (const op)++differenceWithKey' :: (Key -> a -> b -> Maybe a) -> Tree a -> Tree b -> Tree a+differenceWithKey' _ x1 Empty+    = x1++differenceWithKey' f x1 x2+    = diff x1 x2+    where+      diff Empty _  = Empty+      diff t1    t2 = join' v' (diff l l') (diff r r')+                      where+                        (k, v, l, r) = unNode t1+                        (m', l', r') = split' k t2+                        v'           = case m' of+                                          Nothing -> Just (k, v)+                                          Just x  -> case f k v x of+                                                       Nothing -> Nothing+                                                       Just y  -> Just (k, y)++{-# INLINE difference         #-}+{-# INLINE differenceWith     #-}+{-# INLINE differenceWithKey' #-}++-- ------------------------------------------------------------++intersection :: Tree a -> Tree b -> Tree a+intersection = intersectionWith const++intersectionWith :: (a -> b -> c) -> Tree a -> Tree b -> Tree c+intersectionWith op+    = intersectionWithKey (const op)++intersectionWithKey' :: (Key -> a -> b -> c) -> Tree a -> Tree b -> Tree c+intersectionWithKey' _  _ Empty+    = Empty+intersectionWithKey' f x1 x2+    = intersect x1 x2+    where+      intersect Empty _  = Empty+      intersect t1    t2 = join' kv' (intersect l l') (intersect r r')+                           where+                             (k, v, l, r) = unNode t1+                             (m', l', r') = split' k t2+                             kv'          = (\ y -> (k, f k v y)) <$> m'++{-# INLINE intersection         #-}+{-# INLINE intersectionWith     #-}+{-# INLINE intersectionWithKey' #-}++equal :: Eq v => Tree v -> Tree v -> Bool+equal Empty Empty = True+equal Empty _t2   = False+equal _t1   Empty = False+equal t1 t2       =    Just v1 == m2+                    && l1 `equal` l2+                    && r1 `equal` r2+                    where+                      (k1, v1, l1, r1) = unNode t1+                      (m2,     l2, r2) = split' k1 t2++-- ------------------------------------------------------------+--+-- maps++map :: (a -> b) -> Tree a -> Tree b+map f = mapWithKey (const f)++mapWithKey :: (Key -> a -> b) -> Tree a -> Tree b+mapWithKey f+    = mp+    where+      mp Empty = Empty+      mp t     = mkNode k (f k v) (mp l) (mp r)+                 where+                   (k, v, l, r) = unNode t++{-# INLINE map                  #-}+{-# INLINE mapWithKey           #-}++-- ------------------------------------------------------------+-- filter++filter :: (a -> Bool) -> Tree a -> Tree a+filter p = filterWithKey (\ _k v -> p v)++filterWithKey :: (Key -> a -> Bool) -> Tree a -> Tree a+filterWithKey p+    = go+    where+      go Empty = Empty+      go t     = join' res (go l) (go r)+                 where+                   (k, v, l, r)    = unNode t+                   res | p k v     = Just (k, v)+                       | otherwise = Nothing++{-# INLINE filter               #-}+{-# INLINE filterWithKey        #-}++-- ------------------------------------------------------------+--+-- foldr's++foldr :: (a -> b -> b) -> b -> Tree a -> b+foldr op = foldrWithKey (const op)++foldr' :: (a -> b -> b) -> b -> Tree a -> b+foldr' op = foldrWithKey' (const op)++foldrWithKey :: (Key -> a -> b -> b) -> b -> Tree a -> b+foldrWithKey f+    = fold+    where+      fold acc Empty = acc+      fold acc t     = fold (f k v (fold acc r)) l+                       where+                         (k, v, l, r) = unNode t++foldrWithKey' :: (Key -> a -> b -> b) -> b -> Tree a -> b+foldrWithKey' f+    = fold+    where+      fold !acc Empty = acc+      fold !acc t     = fold (f k v (fold acc r)) l+                        where+                          (k, v, l, r) = unNode t++-- ------------------------------------------------------------+--+-- foldl's++foldl :: (b -> a -> b) -> b -> Tree a -> b+foldl op = foldlWithKey (\ x _k v -> op x v)++foldl' :: (b -> a -> b) -> b -> Tree a -> b+foldl' op = foldlWithKey (\ x _k v -> op x v)++foldlWithKey :: (b -> Key -> a -> b) -> b -> Tree a -> b+foldlWithKey f+    = fold+    where+      fold acc Empty = acc+      fold acc t     = fold (f (fold acc l) k v) r+                       where+                         (k, v, l, r) = unNode t++foldlWithKey' :: (b -> Key -> a -> b) -> b -> Tree a -> b+foldlWithKey' f+    = fold+    where+      fold !acc Empty = acc+      fold !acc t     = fold (f (fold acc l) k v) r+                        where+                          (k, v, l, r) = unNode t++{-# INLINE foldr        #-}+{-# INLINE foldrWithKey #-}+{-# INLINE foldl        #-}+{-# INLINE foldlWithKey #-}++-- ------------------------------------------------------------++fromList :: [(Key, v)] -> Tree v+fromList = L.foldl' (\ acc (k, v) -> insert k v acc) Empty++fromSet :: (Key -> v) -> S.IntSet -> Tree v+fromSet f = fromAscList . L.map (\ k -> (k, f k)) . S.elems++fromAscList :: [(Key, v)] -> Tree v+fromAscList = toTr 0 Empty++-- accumulates a balanced tree from a list+-- by scanning the list just once+-- the input tree t has depth i++toTr :: Int -> Tree v -> [(Key, v)] -> Tree v+toTr _ t [] = t+toTr i t (x : xs)+    | L.null xs1  = t'+    | otherwise = toTr (i + 1) t' xs1+    where+      (r, xs1) = scan i xs+      t'       = join' (Just x) t r++-- builds a tree of depth i from the first 2^i - 1 elements of xs+-- and returns the tree and the remaining list++scan :: Int -> [(Key, v)] -> (Tree v, [(Key, v)])+scan 0 xs       = (Empty, xs)+scan _ []       = (Empty, [])+scan i xs+    | L.null xs1  = (l, [])+    | otherwise = (join' (Just x) l r, xs3)+    where+      (l,  xs1) = scan (i - 1) xs+      (x : xs2) = xs1+      (r,  xs3) = scan (i - 1) xs2++-- ------------------------------------------------------------+--+-- conversions to lists++toAscList :: Tree v -> [(Key, v)]+toAscList = foldrWithKey (\ k v res -> (k, v) : res) []++toList :: Tree v -> [(Key, v)]+toList = toAscList++assocs :: Tree v -> [(Key, v)]+assocs = toAscList++elems :: Tree v -> [v]+elems = foldr (:) []++keys :: Tree v -> [Key]+keys = foldrWithKey (\ k _v r -> k : r) []++{-# INLINE toAscList #-}+{-# INLINE toList #-}+{-# INLINE assocs #-}+{-# INLINE elems #-}+{-# INLINE keys  #-}++-- ------------------------------------------------------------+--+-- min/max++minView :: Tree v -> Maybe (v, Tree v)+minView t = first snd <$> minViewWithKey t++minViewWithKey :: Tree v -> Maybe ((Key, v), Tree v)+minViewWithKey Empty = Nothing+minViewWithKey x     = Just $ go x+    where+      go t = case l of+               Empty -> ((k, v), r)+               _     -> substLeft $ go l+          where+            (k, v, l, r) = unNode t+            substLeft (kv, l') = (kv, mkNode k v l' r)++maxView :: Tree v -> Maybe (v, Tree v)+maxView t = first snd <$> maxViewWithKey t++maxViewWithKey :: Tree v -> Maybe ((Key, v), Tree v)+maxViewWithKey Empty = Nothing+maxViewWithKey x     = Just $ go x+    where+      go t = case r of+               Empty -> ((k, v), l)+               _     -> substRight $ go r+          where+            (k, v, l, r) = unNode t+            substRight (kv, r') = (kv, mkNode k v l r')++{-# INLINE minView        #-}+{-# INLINE maxView        #-}+{-# INLINE minViewWithKey #-}+{-# INLINE maxViewWithKey #-}++first :: (a -> c) -> (a, b) -> (c, b)+first f (x,y) = (f x,y)++{-# INLINE first #-}++-- ------------------------------------------------------------++unionWithKey        :: (Key -> v -> v -> v) -> Tree v -> Tree v -> Tree v+intersectionWithKey :: (Key -> a -> b -> c) -> Tree a -> Tree b -> Tree c+differenceWithKey   :: (Key -> a -> b -> Maybe a) -> Tree a -> Tree b -> Tree a++-- {-+unionWithKey        = unionWithKey'+intersectionWithKey = intersectionWithKey'+differenceWithKey   = differenceWithKey'++-- -}++{- union, interect and diff operations with assertions++unionWithKey op+    = assertBin "unionWithKey" keys keys keys union'keys $ unionWithKey' op+      where+        union'keys x y = L.sort $ x `L.union` y++differenceWithKey op+    = assertBin "differenceWithKeys" keys keys keys diff'keys $ differenceWithKey' op+      where+        diff'keys x y = L.sort $ x L.\\ y++intersectionWithKey op+    = assertBin "intersectionWithKey" keys keys keys intersection'keys $ intersectionWithKey' op+      where+        intersection'keys x y = L.sort $ x `L.intersect` y++assertBin :: (Eq r, Show r) =>+             String -> (a -> r) -> (b -> r) -> (c -> r) -> (r -> r -> r) ->+             (a -> b -> c) -> (a -> b -> c)+assertBin opn retrX retrY retrR op' op x y+    = assert msg' retrR res' res+    where+      res  = x  `op`  y+      res' = x' `op'` y'+      x'   = retrX x+      y'   = retrY y+      msg' = unlines [ "operation: " ++ opn+                     , "arg1:      " ++ show x'+                     , "arg2:      " ++ show y'+                     ]++assert :: (Eq r, Show r) => String -> (a -> r) -> r -> a -> a+assert args retr exp' res+    | exp' == res' = res+    | otherwise    = error msg+    where+      res' = retr res+      msg  = unlines [ "assertion failed"+                     , args+                     , "expected:  " ++ show exp'+                     , "got:       " ++ show res'+                     ]+-- -}++-- ------------------------------------------------------------+{-++main :: IO ()+main = return ()++fromList' :: [Key] -> Tree String+fromList' = fromAscList . L.map (\ x -> (x, ""))+fromList'' :: [Key] -> Tree String+fromList'' = fromList . L.map (\ x -> (x, show $ x+1))++s1 :: Tree String+s1 = fromList' [2,4..10]+s2 :: Tree String+s2 = fromList' [1,3..10]+s3 :: Tree String+s3 = fromList'' [0,3..10]+++arg1 = fromList' [-9019555248142772964,-6161526110399673733,-5962998868550245357,-5723094145863444326,-5358586818353638861,-4957792663758460317,-1038248931000888297,1160546946948583692,1945733972938829115,2251145106674145554,3000418586234587927,4084681012873670518,5287472037333007190,7296040159437125633,8079593536333253132,8492044343351169705]+arg2 = fromList' [-9019555248142772964,-6161526110399673733,-5358586818353638861,-4957792663758460317,-1038248931000888297,1160546946948583692,1945733972938829115,2251145106674145554,3000418586234587927,4084681012873670518,5287472037333007190,7296040159437125633,8079593536333253132,8492044343351169705]++expected = fromList'  [-9019555248142772964,-6161526110399673733,-5358586818353638861,-4957792663758460317,-1038248931000888297,1160546946948583692,1945733972938829115,2251145106674145554,3000418586234587927,4084681012873670518,5287472037333007190,7296040159437125633,8079593536333253132,8492044343351169705]+got = fromList'       [-9019555248142772964,-6161526110399673733,-5358586818353638861,-4957792663758460317,-1038248931000888297]+++-- -}++-- ------------------------------------------------------------
+ src/Data/IntSet/Cache.hs view
@@ -0,0 +1,42 @@+-- ----------------------------------------------------------------------------+{- |+A cache for single element 'IntSet's with elements 0 <= i < cacheSize (1024)+These single element sets occur very frequently in position sets+in occurrence maps, so sharing becomes available with this cache.++Usage: substitute all singleton calls with @cacheAt@ calls+and all @fromList@ calls with @IS.unions . map cacheAt@.+-}+-- ----------------------------------------------------------------------------++module Data.IntSet.Cache+  ( cache+  , cacheAt+  )+where+import           Control.DeepSeq++import qualified Data.IntSet     as S+import qualified Data.Vector     as V++-- ------------------------------------------------------------++-- | Size of the cache.+cacheSize :: Int+cacheSize = 1024++-- | Initialize the 'IntSet' cache.+cache :: V.Vector S.IntSet+cache = id $!! V.generate cacheSize S.singleton++-- | A (cached) 'IntSet' singleton.+cacheAt :: Int -> S.IntSet+cacheAt i+    | 0 <= i+      &&+      i < cacheSize+          = id $! cache V.! i+    | otherwise+        = id $! S.singleton i++-- ------------------------------------------------------------
+ src/Data/LimitedPriorityQueue.hs view
@@ -0,0 +1,137 @@+-- ----------------------------------------------------------------------------++{- |+   Intermediate data structure for sorting and paging a result set++   When sorting a result set by priority and knowing how many+   results are requested and which "page" of the result set+   is requested, a priority queue with limited capacity can be used+   for efficient sorting and paging.++-}++module Data.LimitedPriorityQueue+    ( Queue+    , mkQueue+    , insert+    , reduce+    , toList+    , fromList+    , pageList+    )+where++import           Prelude hiding (drop, take)++-- ----------------------------------------------------------------------------++data Queue v+    = Q { _capacity :: !Int+        , _size     :: !Int+        , _elems    :: (Heap v)+        }+      deriving (Show)++data Heap v+    = E+    | T v (Heap v) (Heap v)+      deriving (Show)++-- | Create an empty priority queue with a limited capacity.+-- If capacity is < 0, no limit is set++mkQueue :: Int -> Queue v+mkQueue c+    | c >= 0+        = Q c 0 E+    | otherwise+        = Q maxBound 0 E++-- | Insert an element if there is space in the queue+-- or if the element is larger than the smallest element.++insert :: Ord v => v -> Queue v -> Queue v+insert x q@(Q c s h)+    | s < c                     -- capacity not reached: insert+        = Q c (s + 1) (merge h (T x E E))+    | x' >= x                   -- queue full, score < min score: don't insert+        = q+    | otherwise                 -- queue full, score >= min: throw min away and insert+        = Q c s (merge (merge l r) (T x E E))+    where+      (T x' l r) = h++-- | Reduce size and capacity of queue+-- by throwing away small elements.++reduce :: Ord v => Int -> Queue v -> Queue v+reduce i (Q _ s h)+    | i >= s+        = Q i s h+    | otherwise -- i < s+        = Q i i (drop (s - i) h)++toList :: Ord v => Int -> Int -> Queue v -> [v]+toList start len (Q _ s h)+    | len < 0                   -- no limit on the length, take all except the first elems+        = take (s - start) h+    | len' < s                  -- more elements in queue than needed, drop some elems+        = take len $ drop (s - len') h+    | otherwise                 -- less elements in queue, take them+        = take (len - (len' - s)) h+      where+        len' = start + len++fromList :: Ord v => Int -> [v] -> Queue v+fromList c+    = foldl (\ q x -> insert x q) (mkQueue c)++-- | Take a list of scored values, sort it and return a page of the result.+--+-- @pageList 10 5 xs == take 5 . drop 10 . sortBy snd $ xs@+--+-- If the length is set to @-1@ no limit on the page length is set.++pageList :: Ord v => Int -> Int -> [v] -> [v]+pageList start len+    = toList start len . fromList len'+    where+      len'+          | len >= 0  = start + len+          | otherwise = len++-- ----------------------------------------------------------------------------+-- skew heap operations++take :: Ord v => Int -> Heap v -> [v]+take i0 h0+    = take' i0 h0 []+    where+      take' i (T x l r) acc+          | i <= 0+              = acc+          | otherwise+              = take' (i - 1) (merge l r) (x : acc)+      take' _ E acc+            = acc++drop :: Ord v => Int -> Heap v -> Heap v+drop i h+    | i <= 0+        = h+    | otherwise+        = drop (i - 1) (merge l r)+    where+      (T _x l r) = h++merge :: Ord v => Heap v -> Heap v -> Heap v+merge E q2 = q2+merge q1 E = q1+merge q1@(T x1 l1 r1) q2@(T x2 l2 r2)+    | x1 <= x2+        = T x1 r1 (merge l1 q2)+    | otherwise+        = T x2 r2 (merge l2 q1)++-- ----------------------------------------------------------------------------+
+ src/Data/Text/Binary.hs view
@@ -0,0 +1,23 @@+{-# OPTIONS -fno-warn-orphans #-}++-- ----------------------------------------------------------------------------+{- |+Default 'Binary' instance for 'Text' using UTF-8.+-}+-- ----------------------------------------------------------------------------++module Data.Text.Binary+where++import           Control.Monad      (liftM)+import           Data.Binary        (Binary (..))+import           Data.Text+import           Data.Text.Encoding++-- ------------------------------------------------------------++instance Binary Text where+  put = put . encodeUtf8+  get = liftM decodeUtf8 get++-- ------------------------------------------------------------
+ src/Data/Typeable/Binary.hs view
@@ -0,0 +1,28 @@+{-# OPTIONS -fno-warn-orphans #-}++-- ----------------------------------------------------------------------------+{- |+'Binary' instance for 'TypeRep'.+-}+-- ----------------------------------------------------------------------------++module Data.Typeable.Binary where++import           Control.Applicative++import           Data.Binary+import           Data.Typeable.Internal++import           GHC.Fingerprint.Binary ()++-- ------------------------------------------------------------++instance Binary TypeRep where+  put (TypeRep fp tyCon ts) = put fp >> put tyCon >> put ts+  get = TypeRep <$> get <*> get <*> get++instance Binary TyCon where+  put (TyCon hash package modul name) = put hash >> put package >> put modul >> put name+  get = TyCon <$> get <*> get <*> get <*> get++-- ------------------------------------------------------------
+ src/GHC/Fingerprint/Binary.hs view
@@ -0,0 +1,23 @@+{-# OPTIONS -fno-warn-orphans #-}++-- ----------------------------------------------------------------------------+{- |+'Binary' instance for 'Fingerprint'.+-}+-- ----------------------------------------------------------------------------++module GHC.Fingerprint.Binary where++import           Control.Applicative++import           Data.Binary++import           GHC.Fingerprint.Type++-- ------------------------------------------------------------++instance Binary Fingerprint where+  put (Fingerprint hi lo) = put hi >> put lo+  get = Fingerprint <$> get <*> get++-- ------------------------------------------------------------
+ src/GHC/Stats/Json.hs view
@@ -0,0 +1,40 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS -fno-warn-orphans #-}++-- ----------------------------------------------------------------------------+{- |+'ToJSON' instance for 'GCStats'.+-}+-- ----------------------------------------------------------------------------++module GHC.Stats.Json where++import           Data.Aeson++import           GHC.Stats++-- ------------------------------------------------------------++instance ToJSON GCStats where+  toJSON o = object+    [ "bytesAllocated"          .= bytesAllocated o+    , "numGcs"                  .= numGcs o+    , "maxBytesUsed"            .= maxBytesUsed o+    , "numByteUsageSamples"     .= numByteUsageSamples o+    , "cumulativeBytesUsed"     .= cumulativeBytesUsed o+    , "bytesCopied"             .= bytesCopied o+    , "currentBytesUsed"        .= currentBytesUsed o+    , "currentBytesSlop"        .= currentBytesSlop o+    , "maxBytesSlop"            .= maxBytesSlop o+    , "peakMegabytesAllocated"  .= peakMegabytesAllocated o+    , "mutatorCpuSeconds"       .= mutatorCpuSeconds o+    , "mutatorWallSeconds"      .= mutatorWallSeconds o+    , "gcCpuSeconds"            .= gcCpuSeconds o+    , "gcWallSeconds"           .= gcWallSeconds o+    , "cpuSeconds"              .= cpuSeconds o+    , "wallSeconds"             .= wallSeconds o+    , "parTotBytesCopied"       .= parTotBytesCopied o+    , "parMaxBytesCopied"       .= parMaxBytesCopied o+    ]++-- ------------------------------------------------------------
+ src/Hunt/ClientInterface.hs view
@@ -0,0 +1,520 @@+{- |+  Module     : Hunt.ClientInterface+  License    : MIT++  Maintainer : Uwe Schmidt+  Stability  : experimental+  Portability: none portable++  Common data types and and smart constructors+  for calling a hunt server from a client.++  Values of the Command datatype and its component types, e.g+  Query, ApiDocument, and others+  can be constructed with the "smart" construtors+  defined in this module++  The module is intended to be imported qualified,+  eg like @import qualified Hunt.ClientInterface as HI@.++-}++-- ----------------------------------------------------------------------------++module Hunt.ClientInterface+    (+    -- * types used in commands+      Command+    , ApiDocument(..)   -- also used in results+    , Huntable(..)+    , Content+    , Context+    , ContextSchema+    , Description+    , IndexMap+    , RegEx+    , StatusCmd+    , URI+    , Weight++    -- * types used in results+    , CmdError(..)+    , CmdRes(..)+    , CmdResult(..)+    , LimitedResult(..)+    , Score++    -- * command construction+    , cmdSearch+    , cmdCompletion+    , cmdSelect+    , cmdInsertDoc+    , cmdUpdateDoc+    , cmdDeleteDoc+    , cmdDeleteDocsByQuery+    , cmdLoadIndex+    , cmdStoreIndex+    , cmdInsertContext+    , cmdDeleteContext+    , cmdStatus+    , cmdSequence+    , cmdNOOP++    -- ** configuration options for search and completion+    , setSelectedFields+    , setMaxResults+    , setResultOffset+    , setWeightIncluded++    -- ** Misc+    , createContextCommands+++    -- * ApiDocument construction, configuration and access+    , mkApiDoc+    , setDescription+    , getDescription+    , addDescription+    , remDescription+    , changeDescription+    , lookupDescription+    , lookupDescriptionText+    , setIndex+    , addToIndex+    , getFromIndex+    , changeIndex+    , setDocWeight++    -- ** Misc+    , listToApiDoc+    , insertCmdsToDocuments++    -- ** description construction+    , mkDescription+    , mapToDescr+    , insDescription+    , emptyDescription+    , fromDescription++    -- * Queries+    , Query+    -- ** query parsing+    , parseQuery+    -- ** query construction+    , module Hunt.Query.Language.Builder+++    -- ** pretty printing+    , printQuery++    -- ** query completion+    , completeQueries++    -- * schema definition+    , mkSchema+    , setCxNoDefault+    , setCxWeight+    , setCxRegEx+    , setCxUpperCase+    , setCxLowerCase+    , setCxZeroFill+    , setCxText+    , setCxInt+    , setCxDate+    , setCxPosition++    -- * Weights and Scores+    , noScore+    , defScore+    , mkScore+    , getScore++--    -- * Output to server and file+--    , sendCmdToServer+    , sendCmdToFile+--    , defaultServer+    )+where++import           Control.Applicative         ((<$>))+import           Data.Aeson                  (FromJSON (..), ToJSON (..), Value(..))+import           Data.Default+import           Data.List                   (nub)+import qualified Data.Map.Strict             as SM+import qualified Data.Map.Lazy               as LM+import           Data.Text                   (Text)+import qualified Data.Text                   as T++import           Hunt.Common.ApiDocument     (ApiDocument (..), IndexMap,+                                              LimitedResult (..),+                                              emptyApiDocDescr,+                                              emptyApiDocIndexMap)+import           Hunt.Common.BasicTypes      (Content, Context, Description,+                                              RegEx, Score, URI, Weight,+                                              defScore, getScore, mkScore,+                                              noScore)+import qualified Hunt.Common.DocDesc         as DD+import           Hunt.Index.Schema+import           Hunt.Interpreter.Command+import           Hunt.Query.Language.Builder+import           Hunt.Query.Language.Grammar+import           Hunt.Query.Language.Parser  (parseQuery)+import           Hunt.Utility.Output         (outputValue)++-- ------------------------------------------------------------+-- lookup commands++class Huntable x where+  huntURI        :: x -> URI++  huntIndexMap   :: x -> IndexMap+  huntIndexMap _ = emptyApiDocIndexMap++  huntDescr      :: x -> Description+  huntDescr _    = emptyApiDocDescr++  toApiDocument  :: x -> ApiDocument+  toApiDocument  x = setDescription (huntDescr x) $+                     setIndex (huntIndexMap x) $+                     mkApiDoc $ (huntURI x)+++-- | create simple search command++cmdSearch :: Query -> Command+cmdSearch q+    = Search { icQuery    = q+             , icOffsetSR = 0+             , icMaxSR    = (-1)        -- unlimited+             , icWeight   = False+             , icFields   = Nothing+             }++-- | Create simple completion command++cmdCompletion :: Query -> Command+cmdCompletion q+    = Completion { icPrefixCR = q+                 , icMaxCR    = (-1)    -- unlimited+                 }++cmdSelect :: Query -> Command+cmdSelect = Select++-- ------------------------------------------------------------+-- modifying commands++-- | insert document++cmdInsertDoc :: ApiDocument -> Command+cmdInsertDoc = Insert++-- | update document++cmdUpdateDoc :: ApiDocument -> Command+cmdUpdateDoc = Update++-- | delete document identified by an URI++cmdDeleteDoc :: URI -> Command+cmdDeleteDoc = Delete++-- | delete all documents idenitfied by a query++cmdDeleteDocsByQuery :: Query -> Command+cmdDeleteDocsByQuery = DeleteByQuery++-- ------------------------------------------------------------+-- index schema manipulation++cmdInsertContext :: Context -> ContextSchema -> Command+cmdInsertContext cx sc+    = InsertContext { icIContext = cx+                    , icSchema   = sc+                    }++cmdDeleteContext :: Context -> Command+cmdDeleteContext cx+    = DeleteContext { icDContext = cx }++-- ------------------------------------------------------------+-- index persistance++cmdLoadIndex :: FilePath -> Command+cmdLoadIndex = LoadIx++cmdStoreIndex :: FilePath -> Command+cmdStoreIndex = StoreIx++-- ------------------------------------------------------------+-- status and control commands++cmdStatus :: StatusCmd -> Command+cmdStatus = Status++cmdSequence :: [Command] -> Command+cmdSequence []  = cmdNOOP+cmdSequence [c] = c+cmdSequence cs  = Sequence cs++cmdNOOP :: Command+cmdNOOP = NOOP++-- ------------------------------------------------------------++-- | configure search and completion command: set the max # of results++setMaxResults :: Int -> Command -> Command+setMaxResults mx q@Search{}+    = q { icMaxSR    = mx }+setMaxResults mx q@Completion{}+    = q { icMaxCR    = mx }+setMaxResults _ q+    = q++-- | configure search command: set the starting offset of the result list+setResultOffset :: Int -> Command -> Command+setResultOffset off q@Search{}+    = q { icOffsetSR = off }+setResultOffset _ q+    = q++-- | configure search command: set the list of attributes of the document decription+-- to be included in the result list+--+-- example: @setSelectedFields ["title", "date"]@ restricts the documents+-- attributes to these to fields++setSelectedFields :: [Text] -> Command -> Command+setSelectedFields fs q@Search{}+    = q { icFields = Just fs }++setSelectedFields _ q+    = q++-- |  configure search command: include document weight in result list++setWeightIncluded :: Command -> Command+setWeightIncluded q@Search{}+    = q { icWeight = True }+setWeightIncluded q+    = q+++-- | create InsertContext Commands by a list of Insert Commands+-- These contexts are not optimized and shoudn't be used in production code.+createContextCommands :: [ApiDocument] -> Command+createContextCommands docs = cmdSequence cmds+  where+    names = nub $ docs >>= (LM.keys . adIndex)+    cmds = (\name -> cmdInsertContext name mkSchema) <$> names+-- ------------------------------------------------------------++-- | build an api document with an uri as key and a description+-- map as contents++mkApiDoc :: URI -> ApiDocument+mkApiDoc u+    = ApiDocument+      { adUri   = u+      , adIndex = emptyApiDocIndexMap+      , adDescr = emptyApiDocDescr+      , adWght  = noScore+      }++-- | add an index map containing the text parts to be indexed++setDescription :: Description -> ApiDocument -> ApiDocument+setDescription descr d+    = d { adDescr = descr }++getDescription :: ApiDocument -> Description+getDescription = adDescr++lookupDescription :: FromJSON v => Text -> ApiDocument -> Maybe v+lookupDescription k+    = DD.lookup k . adDescr++lookupDescriptionText :: Text -> ApiDocument -> Text+lookupDescriptionText k+    = DD.lookupText k . adDescr++addDescription :: ToJSON v => Text -> v -> ApiDocument -> ApiDocument+addDescription k v+    = changeDescription $ DD.insert k v++remDescription :: Text -> ApiDocument -> ApiDocument+remDescription k+    = changeDescription $ DD.delete k++changeDescription :: (Description -> Description) -> ApiDocument -> ApiDocument+changeDescription f a = a { adDescr = f . adDescr $ a }++-- | add an index map containing the text parts to be indexed++setIndex :: IndexMap -> ApiDocument -> ApiDocument+setIndex im d+    = d { adIndex = im }++addToIndex :: Context -> Content -> ApiDocument -> ApiDocument+addToIndex cx ct d+    | T.null ct = d+    | otherwise = changeIndex (SM.insert cx ct) d++getFromIndex :: Context -> ApiDocument -> Text+getFromIndex cx d+    = maybe "" id . SM.lookup cx . adIndex $ d++changeIndex :: (IndexMap -> IndexMap) -> ApiDocument -> ApiDocument+changeIndex f a = a { adIndex = f $ adIndex a }++-- | add a document weight++setDocWeight :: Score -> ApiDocument -> ApiDocument+setDocWeight w d+    = d { adWght = w }++-- | wrapper for building an ApiDocument by lists+listToApiDoc+  :: Text -- ^ The uri+  -> [(Text, Text)] -- ^ The index+  -> [(Text, Text)] -- ^ The description+  -> ApiDocument+listToApiDoc uri k v = setDescription (mkDescription v) $ setIndex (LM.fromList k) $ mkApiDoc $ uri+++-- ------------------------------------------------------------+-- document description++-- build a document description from a list of key-value pairs with+-- simple text values++mkDescription :: [(Text, Text)] -> Description+mkDescription+    = DD.fromList . filter (not . T.null . snd)++mapToDescr :: LM.Map Text Text -> DD.DocDesc+mapToDescr src = mkDescription $ LM.toList src++-- insert a key-value pair with an arbitrary value into+-- a document description++insDescription :: ToJSON v => Text -> v -> Description -> Description+insDescription+    = DD.insert++emptyDescription :: Description+emptyDescription = DD.empty++fromDescription :: Description ->  [(Text, Value)]+fromDescription = DD.toList++insertCmdsToDocuments :: Command -> [ApiDocument]+insertCmdsToDocuments (Insert d)    = [d]+insertCmdsToDocuments (Sequence cs) = cs >>= insertCmdsToDocuments+insertCmdsToDocuments _             = []++-- ------------------------------------------------------------+++++-- ------------------------------------------------------------+-- context schema construction++-- | the default schema: context type is text, no normalizers,+-- weigth is 1.0, context is always searched by queries without context spec++mkSchema :: ContextSchema+mkSchema = def++-- | prevent searching in context, when not explicitly set in query++setCxNoDefault :: ContextSchema -> ContextSchema+setCxNoDefault sc+    = sc { cxDefault = False }++-- | set the regex for splitting a text into words++setCxWeight :: Float -> ContextSchema -> ContextSchema+setCxWeight w sc+    = sc { cxWeight = mkScore w }++-- | set the regex for splitting a text into words++setCxRegEx :: RegEx -> ContextSchema -> ContextSchema+setCxRegEx re sc+    = sc { cxRegEx = Just re }++-- | add a text normalizer for transformation into uppercase++setCxUpperCase :: ContextSchema -> ContextSchema+setCxUpperCase sc+    = sc { cxNormalizer = cnUpperCase : cxNormalizer sc }++-- | add a text normalizer for transformation into lowercase++setCxLowerCase :: ContextSchema -> ContextSchema+setCxLowerCase sc+    = sc { cxNormalizer = cnLowerCase : cxNormalizer sc }++-- | add a text normalizer for transformation into lowercase++setCxZeroFill :: ContextSchema -> ContextSchema+setCxZeroFill sc+    = sc { cxNormalizer = cnZeroFill : cxNormalizer sc }++-- | set the type of a context to text++setCxText :: ContextSchema -> ContextSchema+setCxText sc+    = sc { cxType = ctText }++-- | set the type of a context to Int++setCxInt :: ContextSchema -> ContextSchema+setCxInt sc+    = sc { cxType = ctInt }++-- | set the type of a context to Date++setCxDate :: ContextSchema -> ContextSchema+setCxDate sc+    = sc { cxType = ctDate }++-- | set the type of a context to Int++setCxPosition :: ContextSchema -> ContextSchema+setCxPosition sc+    = sc { cxType = ctPosition }++-- ------------------------------------------------------------++completeQueries :: Query -> [Text] -> [Query]+completeQueries (QWord t _)         comps = (\c -> QWord t (c))    <$> comps+completeQueries (QFullWord t _)     comps = (\c -> QFullWord t (c))<$> comps+completeQueries (QPhrase t _)       comps = (\c -> QPhrase t (c))  <$> comps+completeQueries (QContext cxs q)    comps = (QContext cxs)              <$> (completeQueries q comps)+completeQueries (QBinary op q1 q2)  comps = (QBinary op q1)             <$> (completeQueries q2 comps)+completeQueries (QSeq    op qs)     comps = (QSeq op)                   <$> (completeLast qs)+  where+  completeLast [] = []+  completeLast [q] = sequence [completeQueries q comps]+  completeLast (q:qs') = (q :)  <$> completeLast qs'+completeQueries (QBoost w q)        comps = (QBoost w)                  <$> (completeQueries q comps)+completeQueries (QRange t1 t2)      _     = [QRange t1 t2] -- TODO++-- ------------------------------------------------------------++-- client output++-- | send command as JSON into a file+--+-- the JSON is pretty printed with aeson-pretty,+-- @""@ and @"-"@ are used for output to stdout++sendCmdToFile :: String -> Command -> IO ()+sendCmdToFile fn cmd+    = outputValue fn cmd++-- ------------------------------------------------------------
+ src/Hunt/Common.hs view
@@ -0,0 +1,39 @@+{- |+  Module     : Hunt.Index.Common+  License    : MIT++  Maintainer : Ulf Sauer+  Stability  : experimental+  Portability: none portable++  Common data types shared by all index types and a unified interface for+  all different index types. This module defines the common interfaces of+  indexes and their document tables as well as full-text caches.+-}+-- ----------------------------------------------------------------------------++module Hunt.Common+  (+    module Hunt.Common.BasicTypes+  , module Hunt.Common.DocId+  , module Hunt.Common.DocIdMap+  , module Hunt.Common.DocIdSet+  , module Hunt.Common.Document+  , module Hunt.Common.Occurrences+  , module Hunt.Common.Positions+  , module Hunt.Common.RawResult+  , module Hunt.Common.ApiDocument+  , module Hunt.Index.Schema+  )+where++import           Hunt.Common.ApiDocument (ApiDocument (..))+import           Hunt.Common.BasicTypes+import           Hunt.Common.DocId+import           Hunt.Common.DocIdMap    (DocIdMap)+import           Hunt.Common.DocIdSet    (DocIdSet)+import           Hunt.Common.Document    (Document (..))+import           Hunt.Common.Occurrences (Occurrences)+import           Hunt.Common.Positions   (Positions)+import           Hunt.Common.RawResult+import           Hunt.Index.Schema
+ src/Hunt/Common/ApiDocument.hs view
@@ -0,0 +1,179 @@+{-# LANGUAGE OverloadedStrings #-}++-- ----------------------------------------------------------------------------++{- |+  Document format for the interpreter and JSON API.+  It includes the document description, the index data and additional metadata.+-}++-- ----------------------------------------------------------------------------++module Hunt.Common.ApiDocument where++import           Control.Applicative+import           Control.DeepSeq+import           Control.Monad          (mzero)++import           Data.Aeson+import           Data.Binary            (Binary (..))+import           Data.Map.Strict        (Map ())+import qualified Data.Map.Strict        as M+import           Data.Text              (Text)+import qualified Data.Text              as T+import           Data.Text.Binary       ()++import           Hunt.Common.BasicTypes+import qualified Hunt.Common.DocDesc    as DD++import           Hunt.Utility.Log++-- ------------------------------------------------------------++-- | The document accepted by the interpreter and JSON API.++data ApiDocument  = ApiDocument+    { adUri   :: URI              -- ^ The unique identifier.+    , adIndex :: IndexMap         -- ^ The data to index according to schema associated with the context.+    , adDescr :: Description      -- ^ The document description (a simple key-value map).+    , adWght  :: Score            -- ^ An optional document boost, (internal default is @1.0@).+    }+    deriving (Show)++-- | Context map+type IndexMap = Map Context Content++-- | Multiple 'ApiDocument's.+type ApiDocuments = [ApiDocument]++-- | Text analysis function+type AnalyzerFunction = Text -> [(Position, Text)]++-- | Types of analyzer+data AnalyzerType = DefaultAnalyzer+                    deriving (Show)+++-- | Paginated result with an offset and chunk size.+data LimitedResult x = LimitedResult+    { lrResult :: [x] -- ^ The list with at most 'lrMax' elements.+    , lrOffset :: Int -- ^ The offset of the result.+    , lrMax    :: Int -- ^ The limit for the result.+    , lrCount  :: Int -- ^ The size of the complete result.+    }+    deriving (Show, Eq)++instance NFData x => NFData (LimitedResult x) where+  rnf (LimitedResult r o m c) = r `seq` o `seq` m `seq` c `seq` ()++-- ------------------------------------------------------------++-- | Create a paginated result with an offset and a chunk size.+--   The result also includes the size of the complete result.++mkLimitedResult :: Int -> Int -> [x] -> LimitedResult x+mkLimitedResult offset mx xs = LimitedResult+  { lrResult = ( if mx < 0+                 then id+                 else take mx+               ) . drop offset $ xs+  , lrOffset = offset+  , lrMax    = mx+  , lrCount  = length xs+  }+++-- | Empty index content.+emptyApiDocIndexMap :: IndexMap+emptyApiDocIndexMap = M.empty++-- | Empty 'Document' description.+emptyApiDocDescr :: Description+emptyApiDocDescr = DD.empty++-- | Empty 'ApiDocument'.+emptyApiDoc :: ApiDocument+emptyApiDoc = ApiDocument "" emptyApiDocIndexMap emptyApiDocDescr noScore++-- ------------------------------------------------------------++instance NFData ApiDocument where+  --default++-- ------------------------------------------------------------++instance Binary ApiDocument where+  put (ApiDocument a b c d)+      = put a >> put b >> put c >> put d+  get = ApiDocument <$> get <*> get <*> get <*> get++-- ------------------------------------------------------------++instance LogShow ApiDocument where+  logShow o = "ApiDocument {adUri = \"" ++ (T.unpack . adUri $ o) ++ "\", ..}"++-- ------------------------------------------------------------+-- JSON instances+-- ------------------------------------------------------------++instance (ToJSON x) => ToJSON (LimitedResult x) where+   toJSON (LimitedResult res offset mx cnt) = object+    [ "result" .= res+    , "offset" .= offset+    , "max"    .= mx+    , "count"  .= cnt+    ]++instance (FromJSON x) => FromJSON (LimitedResult x) where+  parseJSON (Object v) = do+    res    <- v .: "result"+    offset <- v .: "offset"+    mx     <- v .: "max"+    cnt    <- v .: "count"+    return $ LimitedResult res offset mx cnt+  parseJSON _ = mzero++instance FromJSON ApiDocument where+  parseJSON (Object o) = do+    parsedUri <- o    .: "uri"+    indexMap  <- o    .:? "index"       .!= emptyApiDocIndexMap+    descrMap  <- o    .:? "description" .!= emptyApiDocDescr+    weight    <- mkScore <$>+                 o    .:? "weight"      .!= 0.0+    return ApiDocument+      { adUri    = parsedUri+      , adIndex  = indexMap+      , adDescr  = descrMap+      , adWght   = weight+      }+  parseJSON _ = mzero++instance ToJSON ApiDocument where+  toJSON (ApiDocument u im dm wt)+      = object $+        ( maybe [] (\ x -> ["weight" .= x]) $ getScore wt )+        +++        ( if M.null im+          then []+          else ["index" .= im]+        )+        +++        ( if DD.null dm+          then []+          else ["description" .= dm]+        )+        +++        ["uri" .= u]++instance FromJSON AnalyzerType where+  parseJSON (String s) =+    case s of+      "default" -> return DefaultAnalyzer+      _         -> mzero+  parseJSON _ = mzero++instance ToJSON AnalyzerType where+  toJSON (DefaultAnalyzer) =+    "default"++-- ------------------------------------------------------------
+ src/Hunt/Common/BasicTypes.hs view
@@ -0,0 +1,171 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings          #-}++-- ----------------------------------------------------------------------------++{- |+  Common types used within Hunt.+-}++-- ----------------------------------------------------------------------------++module Hunt.Common.BasicTypes+    ( module Hunt.Common.BasicTypes+    , Monoid(..)+    , (<>)+    )+where++import           Control.Applicative+import           Control.Monad       (mzero)+import           Control.DeepSeq++import           Data.Aeson+import           Data.Binary         hiding (Word)+import           Data.Map+import           Data.Monoid         (Monoid (..), (<>))+import           Data.Text++import           Hunt.Common.DocDesc (DocDesc)++import           Prelude             as P++-- ------------------------------------------------------------++-- | The URI describing the location of the original document.+type URI          = Text++-- | The description of a document is a generic key value map.+type Description  = DocDesc++-- | The title of a document.+type Title        = Text++-- | The content of a document.+type Content      = Text++-- | The position of a word in the document.+type Position     = Int++-- | The name of a context.+type Context      = Text++-- | A single word.+type Word         = Text++-- | Positions of Words for each context.+type Words        = Map Context WordList++-- | Positions of words in the document.+type WordList     = Map Word [Position]++-- | Text index+data TextSearchOp = Case | NoCase | PrefixCase | PrefixNoCase+  deriving (Eq, Show)++-- | Weight (for ranking).+type Weight       = Score++-- | Regular expression.+type RegEx        = Text++-- | The score of a hit (either a document hit or a word hit).+-- type Score        = Float++-- ------------------------------------------------------------++-- | Weight or score of a documents,+-- @0.0@ indicates: not set, so there is no need to work with Maybe's+--  wrapped in newtype to not mix up with Score's and Weight's in documents++newtype Score = SC {unScore :: Float}+    deriving (Eq, Ord, Num, Fractional, Show)++instance NFData Score where+  rnf (SC f) = f `seq` ()++noScore :: Score+noScore = SC 0.0++mkScore :: Float -> Score+mkScore x+    | x > 0.0   = SC x+    | otherwise = noScore++getScore :: Score -> Maybe Float+getScore (SC 0.0) = Nothing+getScore (SC x  ) = Just x++defScore :: Score+defScore = SC 1.0++toDefScore :: Score -> Score+toDefScore (SC 0.0) = defScore+toDefScore sc       = sc++fromDefScore :: Score -> Score+fromDefScore (SC 1.0) = noScore+fromDefScore sc       = sc++accScore :: [Score] -> Score+accScore [] = defScore+accScore xs = mkScore $ sum (P.map unScore xs) / fromIntegral (P.length xs)++-- the Monoid instance is used to accumulate scores+-- in query results, so tune it here when sum is not appropriate++instance Monoid Score where+    mempty = noScore+    mappend = (+)++-- ------------------------------------------------------------+-- JSON instances+-- ------------------------------------------------------------++instance FromJSON Score where+    parseJSON x = mkScore <$> parseJSON x++instance ToJSON Score where+    toJSON (SC x) = toJSON x++instance FromJSON TextSearchOp where+    parseJSON (String s)+        = case s of+            "case"         -> return Case+            "noCase"       -> return NoCase+            "prefixCase"   -> return PrefixCase+            "prefixNoCase" -> return PrefixNoCase+            _              -> mzero+    parseJSON _ = mzero++instance ToJSON TextSearchOp where+  toJSON o = case o of+    Case         -> "case"+    NoCase       -> "noCase"+    PrefixCase   -> "prefixCase"+    PrefixNoCase -> "prefixNoCase"++-- ------------------------------------------------------------+-- Binary instances+-- ------------------------------------------------------------++instance Binary Score where+    put (SC x) = put x+    get = SC <$> get++instance Binary TextSearchOp where+  put (Case)         = put (0 :: Word8)+  put (NoCase)       = put (1 :: Word8)+  put (PrefixCase)   = put (2 :: Word8)+  put (PrefixNoCase) = put (3 :: Word8)++  get = do+    t <- get :: Get Word8+    case t of+      0 -> return Case+      1 -> return NoCase+      2 -> return PrefixCase+      3 -> return PrefixNoCase+      _ -> fail "enum out of bounds: TextSearchOp"++-- ------------------------------------------------------------
+ src/Hunt/Common/DocDesc.hs view
@@ -0,0 +1,118 @@+{-# LANGUAGE DeriveDataTypeable         #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++module Hunt.Common.DocDesc+where++import           Prelude             hiding (lookup)++import           Control.Arrow       (second)+import           Control.DeepSeq+import           Control.Monad       (mzero)++import           Data.Aeson          (FromJSON (..), Object, Result (..),+                                      ToJSON (..), Value (..), decode, encode,+                                      fromJSON)+import           Data.Binary         (Binary (..))+import qualified Data.HashMap.Strict as HM+import           Data.Maybe          (fromMaybe)+import           Data.Text           (Text)+import           Data.Text.Binary    ()+import           Data.Typeable++-- ------------------------------------------------------------+-- {- the new DocDesc with JSON values as attributes++newtype DocDesc+  = DocDesc { unDesc :: Object }+  deriving (Eq, Show, NFData, Typeable)++-- | Smart constructor for document descriptions.++mkDocDesc :: Object -> DocDesc+mkDocDesc o+    = DocDesc $!! o++instance Binary DocDesc where+    put = put . encode . unDesc+    get = do bs <- get+             case decode bs of+               Nothing -> fail "DocDesc.get: error in decoding from JSON"+               Just x  -> return $! (mkDocDesc x)++instance ToJSON DocDesc where+    toJSON = Object . unDesc++instance FromJSON DocDesc where+    parseJSON (Object o) = return $! (mkDocDesc o)+    parseJSON _          = mzero++-- | The empty description.++empty :: DocDesc+empty = mkDocDesc HM.empty++-- | Check if document description is empty.++null :: DocDesc -> Bool+null (DocDesc m) = HM.null m++-- | Insert key value pair into description.++insert :: ToJSON v => Text -> v -> DocDesc -> DocDesc+insert k v (DocDesc m)+    = mkDocDesc $ HM.insert k (toJSON v) m++-- | Remove a key value pair++delete :: Text -> DocDesc -> DocDesc+delete k (DocDesc m)+    = mkDocDesc $ HM.delete k m++-- | Union of two descriptions.++union :: DocDesc -> DocDesc -> DocDesc+union (DocDesc m1) (DocDesc m2)+    = mkDocDesc $ HM.union m1 m2++-- | restrict a DocDesc map to a set of fields++restrict :: [Text] -> DocDesc -> DocDesc+restrict ks (DocDesc m)+    = mkDocDesc $ HM.filterWithKey sel m+      where+        sel k _v = k `elem` ks++deleteNull :: DocDesc -> DocDesc+deleteNull (DocDesc m)+    = mkDocDesc $ HM.filter notNull m+      where+        notNull Null = False+        notNull _    = True++lookupValue :: Text -> DocDesc -> Value+lookupValue k (DocDesc m)+    = fromMaybe Null $ HM.lookup k m++lookup :: FromJSON v => Text -> DocDesc -> Maybe v+lookup k d+    = toMaybe . fromJSON . lookupValue k $ d+    where+      toMaybe (Success v) = Just v+      toMaybe _              = Nothing++lookupText :: Text -> DocDesc -> Text+lookupText k d+    = fromMaybe "" . lookup k $ d++-- | Create a document description from as list++fromList :: ToJSON v => [(Text, v)] -> DocDesc+fromList l = mkDocDesc $ HM.fromList (map (second toJSON) l)++-- | Create a list from a document description.++toList :: DocDesc -> [(Text, Value)]+toList (DocDesc m) = HM.toList m++-- ------------------------------------------------------------
+ src/Hunt/Common/DocId.hs view
@@ -0,0 +1,98 @@+{-# LANGUAGE BangPatterns      #-}+{-# LANGUAGE OverloadedStrings #-}++-- ----------------------------------------------------------------------------++{- |+  Module     : Hunt.Index.Common.DocId+  Copyright  : Copyright (C) 2014 Uwe Schmidt+  License    : MIT++  Maintainer : Uwe Schmidt (uwe@fh-wedel.de)++  The document identifier type DocId and the newtype DocId' with Show and ToJSOn instances+-}++-- ----------------------------------------------------------------------------++module Hunt.Common.DocId+where++import           Control.Applicative++import           Data.Aeson+import           Data.Binary          (Binary (..))+import qualified Data.Binary          as B+import           Data.Digest.Murmur64++-- ------------------------------------------------------------+{-+-- | The unique identifier of a document.+type DocId = Int++-- ------------------------------------------------------------++-- | Create the null-identifier.+mkNull :: DocId+mkNull = 0++-- | Create the first identifier.+mkFirst :: DocId+mkFirst = 1++-- | Create a 'DocId' from an 'Integer'.+fromInteger :: Integer -> DocId+fromInteger = fromIntegral++-- ------------------------------------------------------------++{-# INLINE mkNull #-}+{-# INLINE mkFirst #-}+{-# INLINE fromInteger #-}++-- -}+-- ------------------------------------------------------------++-- the wrapped DocId+-- currently only used for JSON debug output++newtype DocId = DocId {unDocId :: Int}+    deriving (Eq, Ord)++instance Show DocId where+    show = toHex . unDocId++instance Binary DocId where+    put = put . unDocId+    get = DocId <$> get++instance ToJSON DocId where+    toJSON (DocId i) = toJSON $ toHex i++mkDocId :: Binary a => a -> DocId+mkDocId = DocId . fromIntegral . asWord64 . hash64 . B.encode++toHex :: Int -> String+toHex !y = "0x" ++ toX 16 "" y+    where+      toX :: Int -> String -> Int -> String+      toX !0 !acc _ = acc+      toX !n !acc x = toX (n - 1) (d : acc) x'+          where+            (!x', !r) = x `divMod` 16+            !d | r < 10    = toEnum (fromEnum '0' + r)+               | otherwise = toEnum (fromEnum 'a' + r - 10)++fromHex :: String -> Maybe Int+fromHex i@('0' : 'x' : xs)+    | length xs == 16+      &&+      all (`elem` "0123456789abcdef") xs+        = Just . read $ i+    | otherwise+        = Nothing++fromHex _+    = Nothing++-- ------------------------------------------------------------
+ src/Hunt/Common/DocIdMap.hs view
@@ -0,0 +1,316 @@+{-# LANGUAGE CPP                        #-}+{-# LANGUAGE DeriveDataTypeable         #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings          #-}++-- ----------------------------------------------------------------------------++{- |+  Module     : Hunt.Index.Common.DocIdMap+  Copyright  : Copyright (C) 2012 Sebastian M. Schlatt, Timo B. Huebel, Uwe Schmidt+  License    : MIT+  Maintainer : Uwe Schmidt++  Efficient Map implementation for 'DocId's.+-}++-- ----------------------------------------------------------------------------++module Hunt.Common.DocIdMap+  ( DocIdMap(..)+  , empty+  , singleton+  , null+  , member+  , lookup+  , insert+  , delete+  , insertWith+  , size+  , sizeWithLimit+  , union+  , intersection+  , difference+  , diffWithSet+  , unionWith+  , intersectionWith+  , differenceWith+  , unionsWith+  , map+  , filter+  , filterWithKey+  , mapWithKey+  , traverseWithKey+  , foldr+  , foldrWithKey+  , foldl+  , fromList+  , fromDocIdSet+  , fromAscList+  , toList+  , keys+  , elems+  )+where++import           Prelude                    hiding (filter, foldl, foldr,+                                             lookup, map, null)+import qualified Prelude                    as P++import           Control.Applicative        (Applicative (..), (<$>))+import           Control.Arrow              (first)+import           Control.DeepSeq+import           Control.Monad              (foldM, mzero)++import           Data.Aeson+import           Data.Binary                (Binary (..))+import           Data.Foldable              hiding (fold, foldl, foldr, toList)+import qualified Data.HashMap.Strict        as HM+import qualified Data.IntMap.BinTree.Strict as IM+import qualified Data.List                  as L+import           Data.Monoid                (Monoid (..), (<>))+import qualified Data.Text                  as T+import           Data.Typeable++import           Hunt.Common.DocId+import           Hunt.Common.DocIdSet       (DocIdSet (..), toIntSet)++-- ------------------------------------------------------------++-- | An efficient Map implementation for 'DocId's.++newtype DocIdMap v+  = DIM { unDIM :: IM.IntMap v }+  deriving (Eq, Show, Foldable, {-Traversable,-} Functor, NFData, Typeable)++-- ------------------------------------------------------------++instance Monoid v => Monoid (DocIdMap v) where+    mempty  = DIM IM.empty+    mappend = unionWith (<>)++instance Binary v => Binary (DocIdMap v) where+  put = put . unDIM+  get = get >>= return . DIM++instance ToJSON v => ToJSON (DocIdMap v) where+  toJSON = object . L.map toJ . IM.toList . unDIM+    where+    toJ (k, v) = (T.pack . toHex $ k) .= toJSON v++instance FromJSON v => FromJSON (DocIdMap v) where+  parseJSON (Object o) = DIM <$> foldM parsePair IM.empty (HM.toList o)+    where+    parsePair res (k, v)+      = case fromHex . T.unpack $ k of+          Nothing -> mzero+          Just k' -> do+            v' <- parseJSON v+            return $ IM.insert k' v' res++  parseJSON _          = mzero++-- ------------------------------------------------------------++liftDIM                 :: (IM.IntMap v -> IM.IntMap r) ->+                           DocIdMap v -> DocIdMap r+liftDIM f               = DIM . f . unDIM++liftDIM2                :: (IM.IntMap v -> IM.IntMap w -> IM.IntMap x) ->+                           DocIdMap v -> DocIdMap w -> DocIdMap x+liftDIM2 f x y          = DIM $ f (unDIM x) (unDIM y)++-- | The empty map.+empty                   :: DocIdMap v+empty                   = DIM $ IM.empty++-- | A map with a single element.+singleton               :: DocId -> v -> DocIdMap v+singleton d v           = insert d v empty++-- | Is the map empty?+null                    :: DocIdMap v -> Bool+null                    = IM.null . unDIM++-- | Is the 'DocId' member of the map?+member                  :: DocId -> DocIdMap v -> Bool+member x                = IM.member (unDocId x) . unDIM++-- | Lookup the value at a 'DocId' in the map.++--   The function will return the corresponding value as @('Just' value)@,+--   or 'Nothing' if the 'DocId' isn't in the map.+lookup                  :: DocId -> DocIdMap v -> Maybe v+lookup x                = IM.lookup (unDocId x) . unDIM++-- | Insert a 'DocId' and value in the map.+--   If the 'DocId' is already present in the map, the associated value is replaced with the supplied+--   value. 'insert' is equivalent to 'insertWith' 'const'.+insert                  :: DocId -> v -> DocIdMap v -> DocIdMap v+insert x y              = liftDIM $ IM.insert (unDocId x) y++-- | Delete a 'DocId' and its value from the map.+--   When the 'DocId' is not a member of the map, the original map is returned.+delete                  :: DocId -> DocIdMap v -> DocIdMap v+delete x                = liftDIM $ IM.delete (unDocId x)++-- | Insert with a function, combining new value and old value.+--   @insertWith f docId value mp@ will insert the pair @(docId, value)@ into @mp@ if @docId@ does+--   not exist in the map. If the 'DocId' does exist, the function will insert the pair+--   @(docId, f new_value old_value)@.+insertWith              :: (v -> v -> v) -> DocId -> v -> DocIdMap v -> DocIdMap v+insertWith f x y        = liftDIM $ IM.insertWith f (unDocId x) y++-- | The number of elements in the map.+size                    :: DocIdMap v -> Int+size                    = IM.size . unDIM++-- | The number of elements limited up to a maximum+sizeWithLimit           :: Int -> DocIdMap v -> Maybe Int+sizeWithLimit limit     = IM.sizeWithLimit limit . unDIM++-- | The (left-biased) union of two maps.+--   It prefers the first map when duplicate 'DocId' are encountered,+--   i.e. @(union == unionWith const)@.+union                   :: DocIdMap v -> DocIdMap v -> DocIdMap v+union                   = liftDIM2 $ IM.union++-- | The (left-biased) intersection of two maps (based on 'DocId's).+intersection            :: DocIdMap v -> DocIdMap v -> DocIdMap v+intersection            = liftDIM2 $ IM.intersection++-- | Difference between two maps (based on 'DocId's).+difference              :: DocIdMap v -> DocIdMap w -> DocIdMap v+difference              = liftDIM2 $ IM.difference++-- | Difference between the map and a set of 'DocId's.+diffWithSet             :: DocIdMap v -> DocIdSet -> DocIdMap v+diffWithSet m s         = m `difference` (DIM $ IM.fromSet (const ()) (unDIS s))++-- | The union with a combining function.+unionWith               :: (v -> v -> v) -> DocIdMap v -> DocIdMap v -> DocIdMap v+unionWith f             = liftDIM2 $ IM.unionWith f++-- | The intersection with a combining function.+intersectionWith        :: (v -> v -> v) -> DocIdMap v -> DocIdMap v -> DocIdMap v+intersectionWith f      = liftDIM2 $ IM.intersectionWith f++-- | Difference with a combining function.+differenceWith          :: (v -> v -> Maybe v) -> DocIdMap v -> DocIdMap v -> DocIdMap v+differenceWith f        = liftDIM2 $ IM.differenceWith f++-- | The union of a list of maps, with a combining operation.+unionsWith              :: (v -> v -> v) -> [DocIdMap v] -> DocIdMap v+unionsWith f            = DIM . IM.unionsWith f . P.map unDIM++-- | Map a function over all values in the map.+map                     :: (v -> r) -> DocIdMap v -> DocIdMap r+map f                   = liftDIM $ IM.map f++-- | Map a function over all values in the map.+mapWithKey              :: (DocId -> v -> r) -> DocIdMap v -> DocIdMap r+mapWithKey f            = liftDIM $ IM.mapWithKey (f . DocId)++-- | Filter all values that satisfy some predicate.+filter                  :: (v -> Bool) -> DocIdMap v -> DocIdMap v+filter p                = liftDIM $ IM.filter p++-- | Filter all 'DocId's/values that satisfy some predicate.+filterWithKey           :: (DocId -> v -> Bool) -> DocIdMap v -> DocIdMap v+filterWithKey p         = liftDIM $ IM.filterWithKey (p . DocId)++-- | @'traverseWithKey' f s == 'fromList' <$> 'traverse' (\(k, v) -> (,) k <$> f k v) ('toList' m)@+--   That is, behaves exactly like a regular 'traverse' except that the traversing+--   function also has access to the 'DocId' associated with a value.+--+--   > traverseWithKey (\k v -> if odd k then Just (succ v) else Nothing) (fromList [(1, 'a'), (5, 'e')]) == Just (fromList [(1, 'b'), (5, 'f')])+--   > traverseWithKey (\k v -> if odd k then Just (succ v) else Nothing) (fromList [(2, 'c')])           == Nothing+traverseWithKey         :: Applicative t => (DocId -> a -> t b) -> DocIdMap a -> t (DocIdMap b)+traverseWithKey f       = (pure DIM <*>) . IM.traverseWithKey (f . DocId) . unDIM++-- | Fold the values in the map using the given right-associative binary operator, such that+--   @'foldr' f z == 'Prelude.foldr' f z . 'elems'@.+--+-- For example,+--+-- > elems map = foldr (:) [] map+--+-- > let f a len = len + (length a)+-- > foldr f 0 (fromList [(5,"a"), (3,"bbb")]) == 4+foldr                   :: (v -> b -> b) -> b -> DocIdMap v -> b+foldr f u               = IM.foldr f u . unDIM++-- | Fold the 'DocId's and values in the map using the given right-associative+-- binary operator, such that+-- @'foldrWithKey' f z == 'Prelude.foldr' ('uncurry' f) z . 'toAscList'@.+--+-- For example,+--+-- > keys map = foldrWithKey (\k x ks -> k:ks) [] map+--+-- > let f k a result = result ++ "(" ++ (show k) ++ ":" ++ a ++ ")"+-- > foldrWithKey f "Map: " (fromList [(5,"a"), (3,"b")]) == "Map: (5:a)(3:b)"+foldrWithKey            :: (DocId -> v -> b -> b) -> b -> DocIdMap v -> b+foldrWithKey f u        = IM.foldrWithKey (f . DocId) u . unDIM++foldl                   :: (b -> v -> b) -> b -> DocIdMap v -> b+foldl f u               = IM.foldl f u . unDIM++-- | Create a map from a list of 'DocId'\/value pairs.+fromList                :: [(DocId, v)] -> DocIdMap v+fromList                = DIM . IM.fromList . L.map (first unDocId)++-- | Create a map from a set of 'DocId' values+fromDocIdSet            :: (Int -> v) -> DocIdSet -> DocIdMap v+fromDocIdSet f s        = DIM $ IM.fromSet f (toIntSet s)++-- | Build a map from a list of 'DocId'\/value pairs where the 'DocId's are in ascending order.+fromAscList             :: [(DocId, v)] -> DocIdMap v+fromAscList             = DIM . IM.fromAscList . L.map (first unDocId)++-- | Convert the map to a list of 'DocId'\/value pairs.+--   Subject to list fusion.+toList                  :: DocIdMap v -> [(DocId, v)]+toList                  = L.map (first DocId) . IM.toList . unDIM++-- | Return all 'DocId's of the map in ascending order.+--   Subject to list fusion.+keys                    :: DocIdMap v -> [DocId]+keys                    = L.map DocId . IM.keys . unDIM++-- | Return all elements of the map in the ascending order of their 'DocId's.+--   Subject to list fusion.+elems                   :: DocIdMap v -> [v]+elems                   = IM.elems . unDIM++-- ------------------------------------------------------------++{-# INLINE liftDIM #-}+{-# INLINE liftDIM2 #-}+{-# INLINE empty #-}+{-# INLINE singleton #-}+{-# INLINE null #-}+{-# INLINE member #-}+{-# INLINE lookup #-}+{-# INLINE insert #-}+{-# INLINE delete #-}+{-# INLINE insertWith #-}+{-# INLINE size #-}+{-# INLINE union #-}+{-# INLINE difference #-}+{-# INLINE unionWith #-}+{-# INLINE intersectionWith #-}+{-# INLINE differenceWith #-}+{-# INLINE unionsWith #-}+{-# INLINE map #-}+{-# INLINE filter #-}+{-# INLINE filterWithKey #-}+{-# INLINE mapWithKey #-}+{-# INLINE foldr #-}+{-# INLINE foldrWithKey #-}+{-# INLINE fromList #-}+{-# INLINE toList #-}+{-# INLINE keys #-}+{-# INLINE elems #-}++-- ------------------------------------------------------------
+ src/Hunt/Common/DocIdSet.hs view
@@ -0,0 +1,105 @@+{-# LANGUAGE DeriveDataTypeable         #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++-- ----------------------------------------------------------------------------++{- |+  Module     : Hunt.Index.Common.DocIdSet+  Copyright  : Copyright (C) 2014 Uwe Schmidt+  License    : MIT+  Maintainer : Uwe Schmidt++  Efficient Set implementation for 'DocId's.+-}++-- ----------------------------------------------------------------------------++module Hunt.Common.DocIdSet+  ( DocIdSet(..)+  , singleton+  , null+  , member+  , fromList+  , toIntSet+  , toList+  , difference+  , union+  , intersection+  )+where++import           Prelude           hiding (null)++import           Control.DeepSeq+import           Control.Monad     (mzero)++import           Data.Aeson+import qualified Data.IntSet       as S+import qualified Data.List         as L+import           Data.Monoid       (Monoid (..))+import           Data.Typeable+import           Data.Binary       (Binary (..))+import           Hunt.Common.DocId++-- ------------------------------------------------------------+--+-- the wrapped DocId set++newtype DocIdSet = DIS { unDIS :: S.IntSet }+    deriving (Eq, Show, NFData, Typeable)++instance Binary DocIdSet where+  put = put . unDIS+  get = get >>= return . DIS++instance Monoid DocIdSet where+    mempty+        = DIS S.empty+    mappend (DIS s1) (DIS s2)+        = DIS (S.union s1 s2)++instance ToJSON DocIdSet where+    toJSON = toJSON . L.map DocId . S.toList . unDIS++instance FromJSON DocIdSet where+    parseJSON x = do l <- parseJSON x+                     case fromL l of+                       Nothing -> mzero+                       Just s  -> return $ DIS s+        where+          fromL :: [String] -> Maybe S.IntSet+          fromL = L.foldr ins (Just S.empty)+              where+                ins _ Nothing   = Nothing+                ins xs (Just s) = case fromHex xs of+                                    Nothing -> Nothing+                                    Just i  -> Just $ S.insert i s++difference :: DocIdSet -> DocIdSet -> DocIdSet+difference (DIS s1) (DIS s2) = DIS $ S.difference s1 s2++union :: DocIdSet -> DocIdSet -> DocIdSet+union (DIS s1) (DIS s2) = DIS $ S.union s1 s2++intersection :: DocIdSet -> DocIdSet -> DocIdSet+intersection (DIS s1) (DIS s2) = DIS $ S.intersection s1 s2++fromList :: [DocId] -> DocIdSet+fromList = DIS . S.fromList . L.map unDocId++toList :: DocIdSet -> [DocId]+toList = L.map DocId . S.toList . unDIS++toIntSet :: DocIdSet -> S.IntSet+toIntSet = unDIS++singleton :: DocId -> DocIdSet+singleton = DIS . S.singleton . unDocId++null :: DocIdSet -> Bool+null = S.null . unDIS++member :: DocId -> DocIdSet -> Bool+member x s = unDocId x `S.member` unDIS s++-- ------------------------------------------------------------
+ src/Hunt/Common/Document.hs view
@@ -0,0 +1,97 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}++-- ----------------------------------------------------------------------------++{- |+  The document representation.++  This includes the++  * URI for identification,+  * the description for the data itself+  * the weight used in ranking and+  * optionally a score+-}++-- ----------------------------------------------------------------------------++module Hunt.Common.Document+where++import           Control.Applicative+import           Control.DeepSeq++import           Data.Aeson+import           Data.Binary             (Binary (..))+import           Data.Text               as T+import           Data.Text.Binary        ()++import           Hunt.Common.ApiDocument+import           Hunt.Common.BasicTypes+import qualified Hunt.Common.DocDesc     as DD+import           Hunt.Utility.Log++-- ------------------------------------------------------------++-- | The document representation.+data Document = Document+  { uri   :: ! URI         -- ^ Unique identifier of the document.+  , desc  :: ! Description -- ^ Description of the document (simple key-value store).+  , wght  :: ! Score       -- ^ Weight used in ranking (default @1.0@).+  }+  deriving (Show, Eq)++emptyDocument :: Document+emptyDocument = Document "" DD.empty defScore++-- ------------------------------------------------------------+-- JSON instances implemented with ApiDocument+-- ------------------------------------------------------------++toApiDocument :: Document -> ApiDocument+toApiDocument (Document u d w)+    = ApiDocument u emptyApiDocIndexMap d (fromDefScore w)++fromApiDocument :: ApiDocument -> Document+fromApiDocument (ApiDocument u _ix d w)+    = Document u d (toDefScore w)++instance ToJSON Document where+    toJSON = toJSON . toApiDocument++instance FromJSON Document where+    parseJSON o = fromApiDocument <$> parseJSON o++-- ------------------------------------------------------------++instance Binary Document where+  put (Document u d w) = put u >> put d >> put w+  get = Document <$> get <*> get <*> get++instance NFData Document where+  rnf (Document t d _w) = rnf t `seq` rnf d++-- ------------------------------------------------------------++-- | Simple bijection between @e@ and 'Document' for compression.+class (NFData e) => DocumentWrapper e where+  -- | Get the document.+  unwrap :: e -> Document+  -- | Create e from document.+  wrap   :: Document -> e+  -- | Update the wrapped document.+  -- @update f = wrap . f . unwrap@.+  update :: (Document -> Document) -> e -> e+  update f = wrap . f . unwrap++instance DocumentWrapper Document where+  unwrap = id+  wrap   = id++-- ------------------------------------------------------------++instance LogShow Document where+  logShow o = "Document {uri = \"" ++ (T.unpack . uri $ o) ++ "\", ..}"++-- ------------------------------------------------------------
+ src/Hunt/Common/IntermediateValue.hs view
@@ -0,0 +1,66 @@+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE FlexibleInstances    #-}++module Hunt.Common.IntermediateValue where++import           Control.Arrow           (second)+import           Control.DeepSeq++import           Data.Aeson+import           Data.Binary             (Binary)+import qualified Data.List               as L++import           Hunt.Common.Occurrences (Occurrences)+import qualified Hunt.Common.Occurrences as Occ+import           Hunt.Common.DocIdSet    (DocIdSet (..))+import qualified Hunt.Common.DocIdSet    as DS+import qualified Hunt.Common.DocIdMap    as DM++-- This type represents the interface for a value to the engine+-- To have an easy first implementaton the intermediate is+-- basically the same as Occurrences, but that can be adjusted later+newtype IntermediateValue = IntermediateValue+  { unIntermediate :: Occurrences+  }+  deriving (Show, Eq)++mkIntermediateValue :: Occurrences -> IntermediateValue+mkIntermediateValue o = IntermediateValue $! o++instance NFData IntermediateValue where+  rnf (IntermediateValue occ) = rnf occ++instance ToJSON IntermediateValue where+  toJSON x = toJSON (fromIntermediate x :: Occurrences)++fromIntermediates :: IndexValue u => [(x, IntermediateValue)] -> [(x, u)]+fromIntermediates xs = L.map (second fromIntermediate) xs++fromScoredIntermediates :: IndexValue u => [(x, (s, IntermediateValue))] -> [(x, (s,u))]+fromScoredIntermediates xs = L.map (\(x,u) -> (x, second fromIntermediate $ u)) xs++toIntermediates :: IndexValue u => [(x,u)] -> [(x, IntermediateValue)]+toIntermediates xs = L.map (second toIntermediate) xs++class (Binary x, NFData x) => IndexValue x where+  toIntermediate   :: x -> IntermediateValue+  fromIntermediate :: IntermediateValue -> x+  mergeValues      :: x -> x -> x+  diffValues       :: DocIdSet -> x -> Maybe x++instance IndexValue Occurrences where+  toIntermediate   x = mkIntermediateValue x+  fromIntermediate x = unIntermediate $! x+  mergeValues      = Occ.merge+  diffValues s m   = let z = Occ.diffWithSet m s in+                     if Occ.null z then Nothing else Just z++mapToSet :: Occurrences -> DocIdSet+mapToSet = DS.fromList . DM.keys++instance IndexValue DocIdSet where+  toIntermediate     = mkIntermediateValue . Occ.fromDocIdSet+  fromIntermediate   = mapToSet . fromIntermediate+  mergeValues        = DS.union+  diffValues s1 s2   = let r = DS.difference s2 s1 in+                       if DS.null r then Nothing else Just r
+ src/Hunt/Common/Occurrences.hs view
@@ -0,0 +1,118 @@+-- ----------------------------------------------------------------------------++{- |+  Module     : Hunt.Common.Occurrences+  Copyright  : Copyright (C) 2011 Sebastian M. Schlatt, Timo B. Huebel, Uwe Schmidt+  License    : MIT++  Maintainer : Timo B. Huebel (tbh@holumbus.org)+  Stability  : experimental+  Portability: none portable++  Occurrences of words within the index.+  A word occurs in document at specific locations.+-}++-- ----------------------------------------------------------------------------++module Hunt.Common.Occurrences+where++import           Prelude                hiding (subtract)++import           Hunt.Common.BasicTypes+import           Hunt.Common.DocId+import           Hunt.Common.DocIdMap   (DocIdMap)+import           Hunt.Common.DocIdSet   (DocIdSet)+import qualified Hunt.Common.DocIdMap   as DM+import           Hunt.Common.Positions  (Positions)+import qualified Hunt.Common.Positions  as Pos++-- ------------------------------------------------------------++-- | The occurrences of words in documents.+--   A mapping from document ids to the positions in the document.+type Occurrences        = DocIdMap Positions++-- ------------------------------------------------------------++-- | Create an empty set of positions.+empty                   :: Occurrences+empty                   = DM.empty++-- | Create Occurrences from a 'DocIdSet'+--   Since the 'DocIdSet' contains no position information, we+--   assume position one for each 'DocId'+fromDocIdSet            :: DocIdSet -> Occurrences+fromDocIdSet s          = DM.fromDocIdSet (\_ -> Pos.singleton 1) s++-- | Create a single dcid set with a single position.+singleton               :: DocId -> Position -> Occurrences+singleton d p           = singleton' d [p]++-- | Create a single dcid set with a set of.+singleton'              :: DocId -> [Position] -> Occurrences+singleton' d ps         = DM.insert d (Pos.fromList ps) DM.empty++-- | Test on empty set of positions.+null                    :: Occurrences -> Bool+null                    = DM.null++-- | Determine the number of positions in a set of occurrences.+size                    :: Occurrences -> Int+size                    = DM.foldr ((+) . Pos.size) 0++-- | Add a position to occurrences.+insert                  :: DocId -> Position -> Occurrences -> Occurrences+insert d p              = DM.insertWith Pos.union d (Pos.singleton p)++-- | Add multiple positions to occurrences+insert'                 :: DocId -> Positions -> Occurrences -> Occurrences+insert' d ps occs       = Pos.foldr (insert d) occs ps++-- | Remove a position from occurrences.+deleteOccurrence        :: DocId -> Position -> Occurrences -> Occurrences+deleteOccurrence d p    = subtract (DM.singleton d (Pos.singleton p))++-- | Delete a document (by 'DocId') from occurrences.+delete                  :: DocId -> Occurrences -> Occurrences+delete                  = DM.delete++-- | Changes the DocIDs of the occurrences.+update                  :: (DocId -> DocId) -> Occurrences -> Occurrences+update f                = DM.foldrWithKey+                          (\ d ps res -> DM.insertWith Pos.union (f d) ps res) empty++-- | Merge two occurrences.+merge                   :: Occurrences -> Occurrences -> Occurrences+merge                   = DM.unionWith Pos.union++-- | Merge occurrences+merges                  :: [Occurrences] -> Occurrences+merges                  = DM.unionsWith Pos.union++-- | Difference of occurrences.+difference              :: Occurrences -> Occurrences -> Occurrences+difference              = DM.difference++-- | Remove Set of DocIds from Occurrences+diffWithSet             :: Occurrences -> DocIdSet -> Occurrences+diffWithSet             = DM.diffWithSet++-- | Subtract occurrences from some other occurrences.+subtract                :: Occurrences -> Occurrences -> Occurrences+subtract                = DM.differenceWith subtractPositions+  where+  subtractPositions p1 p2+    = if Pos.null diffPos+        then Nothing+        else Just diffPos+    where+    diffPos = Pos.difference p1 p2++intersectOccurrences    :: (Positions -> Positions -> Positions) ->+                           Occurrences -> Occurrences -> Occurrences+intersectOccurrences pf os1 os2+    = DM.filter (not . Pos.null) $ DM.intersectionWith pf os1 os2++-- ------------------------------------------------------------
+ src/Hunt/Common/Positions.hs view
@@ -0,0 +1,120 @@+{-# LANGUAGE DeriveDataTypeable         #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++-- ----------------------------------------------------------------------------++{- |+  Positions within document.+-}++-- ----------------------------------------------------------------------------++module Hunt.Common.Positions where++import           Control.Applicative    ((<$>))+import           Control.DeepSeq++import           Data.Aeson+import           Data.Binary            as B+import qualified Data.IntSet            as IS+import           Data.IntSet.Cache      as IS+import           Data.Maybe             (fromMaybe)+import           Data.Monoid            ()+import           Data.Typeable++import           Hunt.Common.BasicTypes++-- ------------------------------------------------------------++-- | The positions of the word in the document.++newtype Positions+    = PS {unPS :: IS.IntSet}+      deriving (Eq, Ord, Read, Show, Typeable, NFData, Monoid)++-- ------------------------------------------------------------++instance B.Binary Positions where+    put = B.put . toAscList+    get = fromList <$> B.get++instance ToJSON Positions where+    toJSON = toJSON . unPS++-- ------------------------------------------------------------++-- | Empty positions.+empty                :: Positions+empty                = PS IS.empty++-- | Positions with one element.+singleton            :: Position -> Positions+singleton            = PS . IS.cacheAt+--singleton            = PS . IS.singleton++-- | Test whether it is the empty positions.+null                 :: Positions -> Bool+null                 = IS.null . unPS++-- | Whether the 'Position' is part of 'Positions'.+member               :: Position -> Positions -> Bool+member p             = IS.member p . unPS++-- | Converts 'Positions' to a list of 'Position's in ascending order.+toAscList            :: Positions -> [Position]+toAscList            = IS.toAscList . unPS++-- | Constructs Positions from a list of 'Position's.+fromList             :: [Position] -> Positions+fromList             = PS . IS.unions . map IS.cacheAt+--fromList             = PS . IS.fromList++-- | Number of 'Position's.+size                 :: Positions -> Int+size                 = IS.size . unPS++-- | The union of two 'Positions'.+union                :: Positions -> Positions -> Positions+union s1 s2          = PS $ (unPS s1) `IS.union` (unPS s2)++-- | The union of two 'Positions'.+intersection         :: Positions -> Positions -> Positions+intersection s1 s2   = PS $ (unPS s1) `IS.intersection` (unPS s2)++-- | The union of two 'Positions'.+difference           :: Positions -> Positions -> Positions+difference s1 s2     = PS $ (unPS s1) `IS.difference` (unPS s2)++-- | A fold over Positions+foldr                :: (Position -> r -> r) -> r -> Positions -> r+foldr op e           = IS.foldr op e . unPS++-- | intersection with a "shifted" 2. set with elements decremented by a displacement @d@+-- before the element test+--+-- useful when searching for sequences of words (phrases)++intersectionWithDispl :: Int -> Positions -> Positions -> Positions+intersectionWithDispl d (PS s1) (PS s2)+    = PS $ IS.filter member' s1+      where+        member' i = (i + d) `IS.member` s2++-- | intersction with "fuzzy" element test. All elements @e1@ for which an element @e2@ in @s2@+-- is found with @e2 - e1 `elem` [lb..ub]@ remain in set @s1@.+--+-- Useful for context search with sequences of words.+-- This generatizes 'intersectionWithDispl'+--+-- Law: @intersectionWithIntervall d d == intersectionWithDispl d@++intersectionWithIntervall :: Int -> Int -> Positions -> Positions -> Positions+intersectionWithIntervall lb ub (PS s1) (PS s2)+    = PS $ IS.filter member' s1+    where+      member' i = minElem <= i + ub+          where+            (_ls, gt) = IS.split  (i + lb - 1) s2+            minElem   = fromMaybe (i + ub + 1) $ fst <$> IS.minView gt++-- ------------------------------------------------------------
+ src/Hunt/Common/RawResult.hs view
@@ -0,0 +1,52 @@+-- ----------------------------------------------------------------------------++{- |+  Module     : Hunt.Index.Common.RawResult+  Copyright  : Copyright (C) 2011 Sebastian M. Schlatt, Timo B. Huebel, Uwe Schmidt+  License    : MIT++  Maintainer : Timo B. Huebel (tbh@holumbus.org)+  Stability  : experimental+  Portability: none portable++  The raw result of index queries.+-}++-- ----------------------------------------------------------------------------++module Hunt.Common.RawResult+where++import           Data.Map                (Map)+import qualified Data.Map                as M++import           Hunt.Common.BasicTypes+import           Hunt.Common.DocIdMap    (DocIdMap)+import qualified Hunt.Common.DocIdMap    as DM+import           Hunt.Common.Occurrences+import           Hunt.Common.Positions++-- ------------------------------------------------------------++-- | The raw result returned when searching the index.+type RawResult = [(Word, Occurrences)]++type RawScoredResult = [(Word, (Score, Occurrences))]++-- ------------------------------------------------------------++-- | Transform the raw result into a tree structure ordered by word.+resultByWord :: Context -> RawResult -> Map Word (Map Context Occurrences)+resultByWord c+  = M.fromList . map (\ (w, o) -> (w, M.singleton c o))++-- | Transform the raw result into a tree structure ordered by document.+resultByDocument  :: Context -> RawResult -> DocIdMap (Map Context (Map Word Positions))+resultByDocument c os+  = DM.map transform $+        DM.unionsWith (flip $ (:) . head) (map insertWords os)+  where+  insertWords (w, o) = DM.map (\p -> [(w, p)]) o+  transform w        = M.singleton c (M.fromList w)++-- ------------------------------------------------------------
+ src/Hunt/ContextIndex.hs view
@@ -0,0 +1,483 @@+{-# LANGUAGE FlexibleContexts      #-}+{-# LANGUAGE FlexibleInstances     #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE Rank2Types            #-}+{-# LANGUAGE TypeFamilies          #-}+{-# LANGUAGE UndecidableInstances  #-}++-- ----------------------------------------------------------------------------+{- |+  The context index introduces contexts and combines the index, document table and schema.+-}+-- ----------------------------------------------------------------------------++module Hunt.ContextIndex+  (+    -- * Construction+    empty++    -- * Contexts and Schema+  , insertContext+  , deleteContext+  , foreachContext+  , contexts+  , contextsM+  , hasContext+  , hasContextM++    -- * Queries+  , lookupRangeCx+  , lookupAllWithCx+  , searchWithCx+  , searchWithCxsNormalized+  , searchWithCxSc+  , lookupRangeCxSc++    -- * Insert\/Delete Documents+  , insertList+                                 -- XXX: these functions should be internal+                                 -- we export them to be able to test them+                                 -- is there a bedder approach to achieve this?+  , createDocTableFromPartition  -- only used in tests+  , unionDocTables               -- only used in tests+  , modifyWithDescription+  , delete+  , deleteDocsByURI+  , decodeCxIx+  , member++    -- * Types+  , ContextIndex (..)+  , ContextMap (..)+  , IndexRep+  , mkContextMap+  , mapToSchema+  )+where+{-+import           Debug.Trace                   (traceShow)+-- -}+import           Prelude+import qualified Prelude                       as P++import           Control.Applicative           (Applicative, (<$>), (<*>))+import           Control.Arrow+import           Control.Monad+import qualified Control.Monad.Parallel        as Par++import           Data.Binary                   (Binary (..))+import           Data.Binary.Get+import           Data.ByteString.Lazy          (ByteString)+import           Data.Map                      (Map)+import qualified Data.Map                      as M+import           Data.Maybe+import           Data.Set                      (Set)+import qualified Data.Set                      as S+import           Data.Text                     (Text)++import           Hunt.Common+import qualified Hunt.Common.DocDesc           as DocDesc+import qualified Hunt.Common.DocIdSet          as DS+import qualified Hunt.Common.Document          as Doc+import           Hunt.Common.IntermediateValue+import qualified Hunt.Common.Occurrences       as Occ++import           Hunt.DocTable                 (DocTable)+import qualified Hunt.DocTable                 as Dt+import qualified Hunt.Index                    as Ix+import           Hunt.Index.IndexImpl          (IndexImpl)+import qualified Hunt.Index.IndexImpl          as Impl+import           Hunt.Utility++-- ------------------------------------------------------------++-- | Context index introduces contexts and combines the major components of Hunt.++data ContextIndex dt = ContextIndex+  { ciIndex :: !ContextMap -- ^ Indexes associated to contexts.+  , ciDocs  :: !dt         -- ^ Document table.+  }++empty :: DocTable dt => ContextIndex dt+empty = ContextIndex emptyContextMap Dt.empty++-- | Contexts with associated heterogeneous index implementations.++type IndexRep = (ContextSchema, Impl.IndexImpl)++newtype ContextMap+  = ContextMap { cxMap :: Map Context IndexRep }+  deriving (Show)++-- | Empty context map.+emptyContextMap :: ContextMap+emptyContextMap = mkContextMap $ M.empty+++-- | Strict smart constructor for the 'ContextMap'.++mkContextMap :: Map Context IndexRep -> ContextMap+mkContextMap x = ContextMap $! x++-- | Get 'Schema' from 'ContextMap'+mapToSchema :: ContextMap -> Schema+mapToSchema (ContextMap m) = M.map fst m++-- ------------------------------------------------------------+-- Binary / Serialization+-- ------------------------------------------------------------++getContextMap :: [IndexImpl] -> Get ContextMap+getContextMap ts+  = do+    impls <- Impl.gets' ts+    s     <- get+    return . mkContextMap . M.fromDistinctAscList $ map (mergeGets s) impls+    where+      mergeGets s (c, i) = (c, (getS c s, i))+      getS c s = fromMaybe (error "deserializating failed: context schema is missing")+               $ lookup c s++instance Binary ContextMap where+  put = put . cxMap+  get = get >>= return . mkContextMap+++-- | Deserialize a 'ContextIndex' with the list of available index implementations and a+--   map of available 'ContextSchema'.+--+--   /Note/: The serialized index implementations have to  be in the list of available types,+--           otherwise this will fail. The serialized schemas have to be in the list of+--           available 'ContextSchema', otherwise this will fail as well.++decodeCxIx :: (Binary dt, DocTable dt) => [IndexImpl] -> ByteString -> ContextIndex dt+decodeCxIx ts = runGet (get' ts)++get' :: Binary dt => [IndexImpl] -> Get (ContextIndex dt)+get' ts = ContextIndex <$> (getContextMap ts) <*> get++instance Binary dt => Binary (ContextIndex dt) where+  get = error "existential types cannot be deserialized this way. Use special get' functions"+  put (ContextIndex (ContextMap a) b)+    = put (M.map snd a) >>  -- convert to 'IndexImpl' and serialize+      put (M.map fst a) >>  -- convert to 'Schema' and serialize+      put b                 -- put 'DocTable'++-- ------------------------------------------------------------++{-+-- | Insert a Document and Words.+--+--   /Note/: For multiple inserts, use the more efficient 'insertList'.+insert :: (Par.MonadParallel m, Applicative m, DocTable dt)+       => Dt.DValue dt -> Words -> ContextIndex dt -> m (ContextIndex dt)+insert doc wrds ix = insertList [(doc,wrds)] ix+-}++--   This is more efficient than using fold and with 'insert'.+-- | Insert multiple documents and words.++insertList :: (Par.MonadParallel m, Applicative m, DocTable dt) =>+              [(Dt.DValue dt, Words)] ->+              ContextIndex dt -> m (ContextIndex dt)++insertList docAndWords (ContextIndex ix docTable)+    = do -- insert to doctable and generate docId+         tablesAndWords <- Par.mapM createDocTableFromPartition+                         $ partitionListByLength 20 docAndWords+         -- union doctables and docid-words pairs+         (newDt, docIdsAndWords) <- unionDocTables tablesAndWords docTable+         -- insert words to index+         newIx <- batchAddWordsM docIdsAndWords ix+         return $! ContextIndex newIx newDt++-- takes list of documents with wordlist. creates new 'DocTable' and+-- inserts each document of the list into it.+createDocTableFromPartition :: (Par.MonadParallel m, DocTable dt) =>+                               [(Dt.DValue dt, Words)] -> m (dt, [(DocId, Words)])+createDocTableFromPartition ds+    = foldM toDocTable (Dt.empty, []) ds+    where+      toDocTable (dt, resIdsAndWords) (doc, ws)+        = do (dId, dt') <- Dt.insert doc dt+             return (dt', (dId, ws):resIdsAndWords)++-- takes list of doctables with lists of docid-words pairs attached+-- unions the doctables to one big doctable and concats the docid-words+-- pairs to one list+unionDocTables :: (DocTable dt, Par.MonadParallel m) =>+                  [(dt, [(DocId, Words)])] -> dt -> m (dt, [(DocId, Words)])+unionDocTables tablesAndWords oldDt+    = do step <- Par.mapM unionDtsAndWords $ mkPairs tablesAndWords+         case step of+                 []      -> return (Dt.empty, [])+                 [(d,w)] -> do n <- Dt.union oldDt d+                               return (n, w)+                 xs      -> unionDocTables xs oldDt+    where+      unionDtsAndWords ((dt1, ws1), (dt2, ws2))+        = do dt <- Dt.union dt1 dt2+             return (dt, ws1 ++ ws2)++      mkPairs []       = []+      mkPairs (a:[])   = [(a,(Dt.empty,[]))]+      mkPairs (a:b:xs) = (a,b):mkPairs xs++{-+-- XXX: this should not work well atm+-- | Modify documents and index data.++modify :: (Par.MonadParallel m, Applicative m, DocTable dt)+       => (Dt.DValue dt -> m (Dt.DValue dt))+       -> Words -> DocId -> ContextIndex dt -> m (ContextIndex dt)+modify f wrds dId (ContextIndex ii dt s) = do+  newDocTable <- Dt.adjust f dId dt+  newIndex    <- addWordsM wrds dId ii+  return $ ContextIndex newIndex newDocTable s+-- -}++-- | Delete a set of documents by 'URI'.+deleteDocsByURI :: (Par.MonadParallel m, Applicative m, DocTable dt)+                => Set URI -> ContextIndex dt -> m (ContextIndex dt)+deleteDocsByURI us ixx@(ContextIndex _ix dt) = do+  docIds <- liftM (DS.fromList . catMaybes) . mapM (flip Dt.lookupByURI dt) . S.toList $ us+  delete docIds ixx+++-- | Delete a set of documents by 'DocId'.+delete :: (Par.MonadParallel m, Applicative m, DocTable dt)+       => DocIdSet -> ContextIndex dt -> m (ContextIndex dt)+delete dIds cix@(ContextIndex ix dt)+    | DS.null dIds+        = return cix+    | otherwise+        = do newIx <- delete' dIds ix+             newDt <- Dt.difference dIds dt+             return $ ContextIndex newIx newDt+++-- | Is the document part of the index?+member :: (Monad m, Applicative m, DocTable dt)+       => URI -> ContextIndex dt -> m Bool+member u (ContextIndex _ii dt) = do+  mem <- Dt.lookupByURI u dt+  return $ isJust mem++-- ------------------------------------------------------------++-- | Modify the description of a document and add words+--   (occurrences for that document) to the index.++modifyWithDescription :: (Par.MonadParallel m, Applicative m, DocTable dt) =>+                         Score -> Description -> Words -> DocId -> ContextIndex dt ->+                         m (ContextIndex dt)+modifyWithDescription weight descr wrds dId (ContextIndex ii dt)+    = do newDocTable <- Dt.adjust mergeDescr dId dt+         newIndex    <- batchAddWordsM [(dId,wrds)] ii+         return $ ContextIndex newIndex newDocTable+    where+      -- M.union is left-biased+      -- flip to use new values for existing keys+      -- no flip to keep old values+      --+      -- Null values in new descr will remove associated attributes+      mergeDescr+          = return . Doc.update (updateWeight . updateDescr)+          where+            updateWeight d+                | weight == noScore = d+                | otherwise         = d {wght = weight}++            updateDescr d           = -- trc "updateDescr res=" $+                                      d {desc = DocDesc.deleteNull $+                                                flip DocDesc.union d' descr'+                                        }+                                      where+                                        d'     = -- trc "updateDescr old=" $+                                                 desc d+                                        descr' = -- trc "updateDescr new=" $+                                                 descr++-- trc :: Show a => String -> a -> a+-- trc msg x = traceShow (msg, x) x++-- ------------------------------------------------------------+-- Helper+-- ------------------------------------------------------------++-- | Adds words associated to a document to the index.+--++-- | Add words for a document to the 'Index'.+--+--   /Note/: Adds words to /existing/ 'Context's.++batchAddWordsM :: (Functor m, Par.MonadParallel m) =>+                  [(DocId, Words)] -> ContextMap -> m ContextMap+batchAddWordsM [] ix+    = return ix++batchAddWordsM vs (ContextMap m)+    = mkContextMap <$>+      mapWithKeyMP ( \cx (s, impl) -> do i <- foldinsertList cx impl+                                         return (s, i)+                   ) m+    where+      foldinsertList :: (Functor m, Monad m) =>+                        Context -> IndexImpl -> m IndexImpl+      foldinsertList cx (Impl.IndexImpl impl)+          = Impl.mkIndex <$>+            Ix.insertListM (contentForCx cx vs) impl++-- | Computes the words and occurrences out of a list for one context++contentForCx :: Context -> [(DocId, Words)] -> [(Word, IntermediateValue)]+contentForCx cx vs+    = concatMap (invert . second (getWlForCx cx)) $ vs+          where+            invert (did, wl)+                = map (second (toIntermediate . Occ.singleton' did)) $ M.toList wl+            getWlForCx cx' ws'+                = fromMaybe M.empty (M.lookup cx' ws')++----------------------------------------------------------------------------+-- addWords/batchAddWords functions+----------------------------------------------------------------------------++mapWithKeyMP :: (Par.MonadParallel m, Ord k) => (k -> a -> m b) -> M.Map k a -> m (M.Map k b)+mapWithKeyMP f m =+  (Par.mapM (\(k, a) -> do+                  b <- f k a+                  return (k, b)+                ) $ M.toList m) >>=+    return . M.fromList++----------------------------------------------------------------------------++-- | Inserts a new context.+--+insertContext :: Context -> Impl.IndexImpl -> ContextSchema+              -> ContextIndex dt -> ContextIndex dt+insertContext c ix schema (ContextIndex m dt)+    = ContextIndex  m' dt+    where+    m' = insertContext' c schema ix m++--   /Note/: Does nothing if the context already exists.+insertContext' :: Context -> ContextSchema -> Impl.IndexImpl -> ContextMap -> ContextMap+insertContext' c s ix (ContextMap m) = mkContextMap $ M.insertWith (const id) c (s, ix) m++-- | Removes context (including the index and the schema).+deleteContext :: Context -> ContextIndex dt -> ContextIndex dt+deleteContext c (ContextIndex ix dt) = ContextIndex (deleteContext' c ix) dt++-- | Removes context (includes the index, but not the schema).+deleteContext' :: Context -> ContextMap -> ContextMap+deleteContext' cx (ContextMap m) = mkContextMap $ M.delete cx m++delete' :: Par.MonadParallel m => DocIdSet -> ContextMap -> m ContextMap+delete' dIds (ContextMap m)+  = mapWithKeyMP (\_ impl -> adjust' impl) m >>= return . mkContextMap+--  = TV.mapM adjust' m >>= return . mkContextMap+  where+  adjust' (s, Impl.IndexImpl ix) = Ix.deleteDocsM dIds ix >>= \i -> return (s, Impl.mkIndex i)++{- not yet used++-- | Search query in all context.+search :: Monad m => TextSearchOp -> Text -> ContextMap -> m [(Context, [(Text, v)])]+search op k (ContextMap m)+  = liftM M.toList $ TV.mapM search' m+  where+  search' (Impl.IndexImpl ix) = Ix.searchM op k ix+-- -}++-- | Range query in a context between first and second key.+lookupRangeCx :: Monad m => Context -> Text -> Text -> ContextMap -> m [(Text, IntermediateValue)]+lookupRangeCx c k1 k2 cm+    = lookupIndex c cm $ Ix.lookupRangeM k1 k2++-- | Dump a context+lookupAllWithCx :: Monad m => Context -> ContextMap -> m [(Text, IntermediateValue)]+lookupAllWithCx cx cm+    = lookupIndex cx cm $ Ix.toListM++-- | Search query in a context.+searchWithCx :: Monad m => TextSearchOp -> Context -> Text -> ContextMap -> m [(Text, IntermediateValue)]+searchWithCx op cx w cm+    = lookupIndex cx cm $ Ix.searchM op w+++-- | Search over a list of contexts and words+searchWithCxsNormalized :: (Functor m, Monad m) =>+                           TextSearchOp -> [(Context, Text)] -> ContextMap ->+                           m [(Context, [(Text, IntermediateValue)])]+searchWithCxsNormalized op cxws cm+    = P.mapM (uncurry search') cxws+    where+      search' cx w+          = (\ x -> (cx, x))+            <$> (lookupIndex cx cm $ Ix.searchM op w)++-- | Search query with scored results+-- XXX TODO: this function should return intermediates and query processor should work with those+searchWithCxSc :: Monad m =>+                  TextSearchOp -> Context -> Text -> ContextMap -> m [(Text, (Score, Occurrences))]+searchWithCxSc op cx w cm+    = (lookupIndex cx cm $ Ix.searchMSc op w) >>= return . fromScoredIntermediates++-- | Range query in a context between first and second key.+-- XXX TODO: this function should return intermediates and query processor should work with those+lookupRangeCxSc :: Monad m => Context -> Text -> Text -> ContextMap -> m [(Text, (Score, Occurrences))]+lookupRangeCxSc c k1 k2 cm+    = (lookupIndex c cm $ Ix.lookupRangeMSc k1 k2) >>= return . fromScoredIntermediates++-- ------------------------------------------------------------++-- | lookup an index by a context and then search this index for a word+-- result is always a list of values.+--+-- This pattern is used in all search variants++lookupIndex :: Monad m =>+               Context -> ContextMap ->+               (forall i . Impl.IndexImplCon i => i -> m [r]) ->+               m [r]+lookupIndex cx (ContextMap m) search+    = case M.lookup cx m of+        Just (_, Impl.IndexImpl cm)+            -> search cm+        Nothing+            -> return []++foreachContext :: (Functor m, Monad m) =>+                  [Context] ->+                  (Context -> m res) ->+                  m [(Context, res)]+foreachContext cxs action+    = P.mapM action' cxs+      where+        action' cx+            = (\ r -> (cx, r)) <$> action cx++-- ------------------------------------------------------------++-- | All contexts of the index.+contextsM :: (Monad m, DocTable dt)+         => ContextIndex dt -> m [Context]+contextsM (ContextIndex ix _) = return $ contexts ix++-- | Contexts/keys of 'ContextMap'.+contexts :: ContextMap -> [Context]+contexts (ContextMap m) = M.keys m++-- | Check if the context exists.+hasContext :: Context -> ContextMap -> Bool+hasContext c (ContextMap m) = M.member c m++-- | Does the context exist?+hasContextM :: (Monad m, DocTable dt)+           => Context -> ContextIndex dt -> m Bool+hasContextM c (ContextIndex ix _) = return $ hasContext c ix++
+ src/Hunt/DocTable.hs view
@@ -0,0 +1,141 @@+{-# LANGUAGE FlexibleContexts  #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TypeFamilies      #-}++-- ----------------------------------------------------------------------------+{- |+  The document table interface.+-}+-- ----------------------------------------------------------------------------++module Hunt.DocTable+where++import           Prelude                hiding (filter, lookup, map, null)++import           Control.Applicative    (Applicative, (<$>))+import           Control.Monad++import           Data.Aeson+import           Data.Maybe             (catMaybes, fromJust)+import           Data.Set               (Set)+import qualified Data.Set               as S++import           Hunt.Common.BasicTypes+import           Hunt.Common.DocId+import           Hunt.Common.DocIdMap   (DocIdMap (..))+import qualified Hunt.Common.DocIdMap   as DM+import           Hunt.Common.DocIdSet   (DocIdSet)+import qualified Hunt.Common.DocIdSet   as DS+import           Hunt.Common.Document   (Document,+                                         DocumentWrapper (wrap, unwrap))++-- ------------------------------------------------------------++-- | The document table type class which needs to be implemented to be used by the 'Interpreter'.+--   The type parameter @i@ is the implementation.+--   The implementation must have a value type parameter.++class (DocumentWrapper (DValue i)) => DocTable i where+    -- | The value type of the document table.+    type DValue i :: *++    -- | Test whether the document table is empty.+    null            :: (Monad m) => i -> m Bool++    -- | Returns the number of unique documents in the table.+    size            :: (Monad m) => i -> m Int++    -- | Lookup a document by its ID.+    lookup          :: (Monad m) => DocId -> i -> m (Maybe (DValue i))++    -- | Lookup the 'DocId' of a document by an 'URI'.+    lookupByURI     :: (Monad m) => URI -> i -> m (Maybe DocId)++    -- | Union of two disjoint document tables. It is assumed, that the+    --   DocIds and the document 'URI's of both indexes are disjoint.+    union           :: (Monad m) => i -> i -> m i++    -- | Test whether the 'DocId's of both tables are disjoint.+    disjoint        :: (Monad m) => i -> i -> m Bool++    -- | Insert a document into the table. Returns a tuple of the 'DocId' for that document and the+    --   new table. If a document with the same 'URI' is already present, its id will be returned+    --   and the table is returned unchanged.+    insert          :: (Monad m) => DValue i -> i -> m (DocId, i)++    -- | Update a document with a certain 'DocId'.+    update          :: (Monad m) => DocId -> DValue i -> i -> m i++    -- | Update a document by 'DocId' with the result of the provided function.+    adjust          :: (Monad m) => (DValue i -> m (DValue i)) -> DocId -> i -> m i+    adjust f did d =+        maybe (return d) (upd d did <=< f) =<< lookup did d+        --maybe d (update d did . f) $ lookup d did+        where upd i docid v = update docid v i++    -- | Update a document by 'URI' with the result of the provided function.+    adjustByURI     :: (Monad m) => (DValue i -> m (DValue i)) -> URI -> i -> m i+    adjustByURI f uri d+        = maybe (return d) (flip (adjust f) d) =<< lookupByURI uri d++    -- | Removes the document with the specified 'DocId' from the table.+    delete          :: (Monad m) => DocId -> i -> m i++    -- | Removes the document with the specified 'URI' from the table.+    deleteByURI     :: (Monad m) => URI -> i -> m i+    deleteByURI u ds+        = maybe (return ds) (flip delete ds) =<< lookupByURI u ds++    -- | Deletes a set of documents by 'DocId' from the table.+    difference      :: (Monad m) => DocIdSet -> i -> m i++    -- | Deletes a set of documents by 'URI' from the table.+    differenceByURI :: (Monad m) => Set URI -> i -> m i+    differenceByURI uris d = do -- XXX: eliminate S.toList?+        ids <- liftM (DS.fromList . catMaybes) . mapM (flip lookupByURI d) . S.toList $ uris+        difference ids d++    -- | Map a function over all values of the document table.+    map             :: (Monad m) => (DValue i -> DValue i) -> i -> m i++    -- | Filters all documents that satisfy the predicate.+    filter          :: (Monad m) => (DValue i -> Bool) -> i -> m i++    -- | Convert document table to a 'DocIdMap'.+    toMap           :: (Monad m) => i -> m (DocIdMap (DValue i))++    -- | Empty 'DocTable'.+    empty           :: i+++restrict :: (Functor m, Monad m, Applicative m, DocTable i) => DocIdSet -> i -> m i+restrict is dt+    = foldM ins empty $ DS.toList is+      where+        ins m i = do v <- fromJust <$> lookup i dt+                     update i v m++-- ------------------------------------------------------------++-- | JSON dump of the document table.+toJSON'DocTable :: (Functor m, Monad m, Applicative m, DocTable i) => i -> m Value+toJSON'DocTable dt+    = do didm <- DM.map unwrap <$> toMap dt+         return $ toJSON didm++-- | JSON import of the document table.+fromJSON'DocTable :: (Functor m, Monad m, Applicative m, DocTable i) => Value -> m i+fromJSON'DocTable v+    = foldM ins empty $ dm'+      where+        ins res (did, doc) = update did doc res++        dm :: DocIdMap Document+        dm = case fromJSON v of+               Error _   -> DM.empty+               Success m -> m++        dm'= DM.toList . DM.map wrap $ dm++-- ------------------------------------------------------------
+ src/Hunt/DocTable/HashedDocTable.hs view
@@ -0,0 +1,220 @@+{-# LANGUAGE FlexibleContexts  #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TypeFamilies      #-}++-- ----------------------------------------------------------------------------++{- |+  Module     : Hunt.Index.HashedDocTable+  Copyright  : Copyright (C) 2012 Uwe Schmidt+  License    : MIT++  Maintainer : Uwe Schmidt (uwe@fh-wedel.de)+  Stability  : experimental++  A more space efficient substitute for Hunt.Index.Documents+  and a more flexible implementation than Hunt.Index.CompactDocuments.++  DocIds are computed by a hash function, so the inverse map from 'URI's to 'DocId's+  is substituted by the hash function.++  MurmurHash2 64-bit is used as the hash function.+  https://sites.google.com/site/murmurhash/++  It is a fast non-cryptographic hash function with good performance and+  hash distribution properties.+  http://programmers.stackexchange.com/a/145633+-}++-- ----------------------------------------------------------------------------++module Hunt.DocTable.HashedDocTable+    (+      -- * Documents type+      Documents (..)+    , DocMap++      -- * Conversion+    , fromMap+    )+where++import           Data.Binary            (Binary (..))++import           Control.DeepSeq++import           Hunt.Common.BasicTypes+import           Hunt.Common.DocId      (DocId, mkDocId)+import           Hunt.Common.DocIdMap   (DocIdMap)+import qualified Hunt.Common.DocIdMap   as DM+import           Hunt.Common.DocIdSet   (DocIdSet)+import           Hunt.Common.Document   (Document (..), DocumentWrapper (..))+import           Hunt.DocTable+import           Hunt.Utility++-- ------------------------------------------------------------++-- | The table which is used to map a document to an artificial id and vice versa.+type DocMap e+  = DocIdMap e++-- | The 'DocTable' implementation. Maps 'DocId's to 'Document's.+newtype Documents e+  = Documents { idToDoc :: DocMap e }     -- ^ A mapping from a document id to+                                          --   the document itself.+  deriving (Eq, Show)++mkDocuments :: NFData e => DocMap e -> Documents e+mkDocuments m = Documents $! m++-- ------------------------------------------------------------++instance NFData e => NFData (Documents e) where+  rnf (Documents e) = rnf e++instance (DocumentWrapper e, Binary e) => Binary (Documents e) where+  put = put . idToDoc+  get = get >>= return . mkDocuments++--- ------------------------------------------------------------++-- | An empty document table.+empty' :: (DocTable (Documents e), DocumentWrapper e) => Documents e+empty' = mkDocuments DM.empty++-- | Build a 'DocTable' from a 'DocIdMap' (maps 'DocId's to 'Document's)+fromMap :: (DocTable (Documents e), DocumentWrapper e) =>+          (Document -> e) -> DocIdMap Document -> Documents e+fromMap = fromMap'++-- ------------------------------------------------------------++instance (DocumentWrapper e) =>+         DocTable (Documents e) where+  type DValue (Documents e) = e+  null        = return . null'++  -- Returns the number of unique documents in the table.+  size        = return . size'++  -- Lookup a document by its id.+  lookup      = return .:: lookup'++  -- Lookup the id of a document by an URI.+  lookupByURI = return .:: lookupByURI'++  -- Union of two disjoint document tables. It is assumed, that the DocIds and the document uris+  -- of both indexes are disjoint. If only the sets of uris are disjoint, the DocIds can be made+  -- disjoint by adding maxDocId of one to the DocIds of the second, e.g. with editDocIds+  union       = return .:: unionDocs'++  -- Test whether the doc ids of both tables are disjoint.+  disjoint    = return .:: disjoint'++  -- Insert a document into the table. Returns a tuple of the id for that document and the+  -- new table. If a document with the same URI is already present, its id will be returned+  -- and the table is returned unchanged.+  insert      = return .:: insert'++  -- Update a document with a certain DocId.+  update      = return .::: update'++  -- Removes the document with the specified id from the table.+  delete      = return .:: delete'++  -- Deletes a set of Docs by Id from the table.+  difference  = return .:: difference'++  -- Update documents (through mapping over all documents).+  map         = return .:: map'++  -- Filters all documents that satisfy the predicate.+  filter      = return .:: filter'++  -- Convert document table to a single map.+  toMap       = return . toMap'++  -- | Empty 'DocTable'.+  empty       = empty'++-- ------------------------------------------------------------++null'       :: (DocumentWrapper e) => Documents e -> Bool+null'+  = DM.null . idToDoc++size'       :: (DocumentWrapper e) => Documents e -> Int+size'+  = DM.size . idToDoc++lookup'     :: (Monad m, DocumentWrapper e) => DocId -> Documents e -> m e+lookup' i d+  = maybe (fail "") return+    . DM.lookup i+    . idToDoc+    $ d++lookupByURI' :: (Monad m, DocumentWrapper e) => URI -> Documents e -> m DocId+lookupByURI' u d+  = maybe (fail "") (const $ return i)+    . DM.lookup i+    . idToDoc+    $ d+  where+  i = mkDocId u++disjoint'   :: (DocumentWrapper e) => Documents e -> Documents e -> Bool+disjoint' dt1 dt2+  = DM.null $ DM.intersection (idToDoc dt1) (idToDoc dt2)++unionDocs'  :: (DocumentWrapper e) => Documents e -> Documents e -> Documents e+unionDocs' dt1 dt2+  | disjoint' dt1 dt2+      = unionDocs'' dt1 dt2+  | otherwise+      = error+        "HashedDocTable.unionDocs: doctables are not disjoint"+  where+  unionDocs'' :: (DocumentWrapper e) => Documents e -> Documents e -> Documents e+  unionDocs'' dt1' dt2'+    = mkDocuments $ idToDoc dt1' `DM.union` idToDoc dt2'+++insert'     :: (DocumentWrapper e) => e -> Documents e -> (DocId, Documents e)+insert' d ds+  = maybe reallyInsert (const (newId, ds)) (lookup' newId ds)+  where+  newId+      = mkDocId . uri . unwrap $ d+  reallyInsert+      = (newId, mkDocuments $ DM.insert newId d $ idToDoc ds)++update'     :: (DocumentWrapper e) => DocId -> e -> Documents e -> Documents e+update' i d ds+  = mkDocuments $ DM.insert i d $ idToDoc ds++delete'     :: (DocumentWrapper e) => DocId -> Documents e -> Documents e+delete' d ds+  = mkDocuments $ DM.delete d $ idToDoc ds++difference' :: (DocumentWrapper e) => DocIdSet -> Documents e -> Documents e+difference' s ds+  = mkDocuments $ idToDoc ds `DM.diffWithSet` s++map'        :: (DocumentWrapper e) => (e -> e) -> Documents e -> Documents e+map' f d+  = mkDocuments $ DM.map f (idToDoc d)++filter'     :: (DocumentWrapper e) => (e -> Bool) -> Documents e -> Documents e+filter' p d+  = mkDocuments $ DM.filter p (idToDoc d)++fromMap'    :: (DocumentWrapper e) => (Document -> e) -> DocIdMap Document -> Documents e+fromMap' f itd+  = mkDocuments $ DM.map f itd++toMap'      :: Documents e -> DocIdMap e+toMap'+  = idToDoc++-- ------------------------------------------------------------
+ src/Hunt/Index.hs view
@@ -0,0 +1,224 @@+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE RankNTypes                #-}+{-# LANGUAGE TypeFamilies              #-}+{-# LANGUAGE ConstraintKinds           #-}+{-# LANGUAGE FlexibleInstances         #-}+{-# LANGUAGE UndecidableInstances      #-}+{-# LANGUAGE MultiParamTypeClasses     #-}++-- ----------------------------------------------------------------------------+{- |+  The index interface.+-}+-- ----------------------------------------------------------------------------+++module Hunt.Index+where++import           Prelude                hiding (map)++import           GHC.Exts               (Constraint)++import           Control.Arrow          (second)++import qualified Data.List              as L++import           Hunt.Common.BasicTypes+import           Hunt.Common.DocId+import           Hunt.Common.DocIdSet   (DocIdSet)+import qualified Hunt.Common.DocIdSet   as DS+import           Hunt.Common.IntermediateValue++-- ------------------------------------------------------------++-- | The index type class which needs to be implemented to be used by the 'Interpreter'.+--   The type parameter @i@ is the implementation.+--   The implementation must have a value type parameter.+class (IndexValue (IVal i)) => Index i where+  -- | The key type of the index.+  type IKey i :: *+  type IVal i :: *++  type ICon i :: Constraint+  type ICon i = ()++  -- | General lookup function.+  search        :: ICon i => TextSearchOp -> IKey i -> i -> [(IKey i, IntermediateValue)]++  searchSc      :: ICon i => TextSearchOp -> IKey i -> i -> [(IKey i, (Score, IntermediateValue))]+  searchSc op k ix = addDefScore $ search op k ix++  -- | Search within a range of two keys.+  lookupRange   :: ICon i => IKey i -> IKey i -> i -> [(IKey i, IntermediateValue)]++  lookupRangeSc :: ICon i => IKey i -> IKey i -> i -> [(IKey i, (Score, IntermediateValue))]+  lookupRangeSc k1 k2 ix+                = addDefScore $ lookupRange k1 k2 ix++  -- | Insert occurrences.+  --   This is more efficient than folding with 'insert'.+  insertList    :: ICon i =>+                   [(IKey i, IntermediateValue)] -> i -> i++  -- | Insert occurrences.+  insert        :: ICon i =>+                   IKey i -> IntermediateValue -> i -> i+  insert   k v  = insertList [(k,v)]++  -- | Delete as batch job.+  --   This is more efficient than folding with 'delete'.+  deleteDocs    :: ICon i => DocIdSet -> i -> i++  -- | Delete occurrences.+  delete        :: ICon i => DocId -> i -> i+  delete        = deleteDocs . DS.singleton++  -- | Empty index.+  empty         :: ICon i => i++  -- | Convert an index to a list.+  --   Can be used for easy conversion between different index implementations.+  toList        :: ICon i => i -> [(IKey i, IntermediateValue)]++  -- | Convert a list of key-value pairs to an index.+  fromList      :: ICon i => [(IKey i, IntermediateValue)] -> i++  -- | Merge two indexes with a combining function.+  unionWith     :: ICon i+                => (IVal i -> IVal i -> IVal i)+                -> i -> i -> i++  --   Merge two indexes with combining functions.+  --   The second index may have another value type than the first one.+  --   Conversion and merging of the indexes is done in a single step.+  --   This is much more efficient than mapping the second index and calling 'unionWith'.+  --  unionWithConv :: (ICon i, ICon i2)+  --                => IVal i)2 -> IVal i) -> (v -> v2 -> IVal i)+  --                -> i -> i2 -> i++  -- TODO: non-rigid map+  -- | Map a function over the values of the index.+  map           :: ICon i+                => (IVal i -> IVal i)+                -> i -> i+  map f = mapMaybe (Just . f)++  -- | Updates a value or deletes it if the result of the function is 'Nothing'.+  mapMaybe      :: ICon i+                => (IVal i -> Maybe (IVal i))+                -> i -> i++  -- | Keys of the index.+  keys          :: ICon i+                => i -> [IKey i]++-- ------------------------------------------------------------++-- | Monadic version of 'Index'.+--   'Index' instances are automatically instance of this type class.+class Monad m => IndexM m i where+  -- | The key type of the index.+  type IKeyM     i :: *++  -- | The value type of the index.+  type IValM     i :: *++  type IConM     i :: Constraint+  type IConM     i = ()++  -- | Monadic version of 'search'.+  searchM      :: IConM i => TextSearchOp -> IKeyM i -> i -> m [(IKeyM i, IntermediateValue)]++  -- | Monadic version of 'search' with (default) scoring.+  searchMSc     :: IConM i => TextSearchOp -> IKeyM i -> i -> m [(IKeyM i, (Score, IntermediateValue))]+  searchMSc op k ix+                = searchM op k ix >>= return . addDefScore++  -- | Monadic version of 'lookupRangeM'.+  lookupRangeM :: IConM i => IKeyM i -> IKeyM i -> i -> m [(IKeyM i, IntermediateValue)]++  lookupRangeMSc :: IConM i => IKeyM i -> IKeyM i -> i -> m [(IKeyM i, (Score, IntermediateValue))]+  lookupRangeMSc k1 k2 ix+                = lookupRangeM k1 k2 ix >>= return . addDefScore++  -- | Monadic version of 'insertList'.+  insertListM  :: IConM i =>+                  [(IKeyM i, IntermediateValue)] -> i -> m (i)++  -- | Monadic version of 'insert'.+  insertM      :: IConM i =>+                  IKeyM i -> IntermediateValue -> i -> m (i)+  insertM k v  = insertListM [(k,v)]++  -- | Monadic version of 'deleteDocs'.+  deleteDocsM  :: IConM i => DocIdSet -> i -> m (i)++  -- | Monadic version of 'delete'.+  deleteM      :: IConM i => DocId -> i -> m (i)+  deleteM k i  = deleteDocsM (DS.singleton k) i++  -- | Monadic version of 'empty'.+  emptyM       :: IConM i => m (i)++  -- | Monadic version of 'toList'.+  toListM      :: IConM i => i -> m [(IKeyM i, IntermediateValue)]++  -- | Monadic version of 'fromList'.+  fromListM    :: IConM i => [(IKeyM i, IntermediateValue)] -> m (i)++  -- | Monadic version of 'unionWith'.+  unionWithM   :: IConM i =>+                  (IValM i -> IValM i -> IValM i) ->+                  i -> i -> m (i)++  --  Monadic version of 'unionWithConv'.+  -- unionWithConvM :: (IConM i, Monad m, IConM i2)+  --               => IVal i)2 -> IVal i) -> (v -> v2 -> IVal i)+  --               -> i -> i2 -> m (i)++  -- | Monadic version of 'map'.+  mapM         :: IConM i+               => (IValM i -> IValM i)+               -> i -> m (i)+  mapM f = mapMaybeM (Just . f)++  -- | Monadic version of 'mapMaybe'.+  mapMaybeM    :: IConM i+               => (IValM i -> Maybe (IValM i))+               -> i -> m (i)++  -- | Monadic version of 'keys'.+  keysM        :: IConM i+               => i -> m [IKeyM i]++-- ------------------------------------------------------------++instance (Index i, Monad m) => IndexM m i where+  type IKeyM i             = IKey i+  type IValM i             = IVal i+  type IConM i             = ICon i++  searchM   op s i           = return $  search op s i+  searchMSc op s i           = return $  searchSc op s i+  lookupRangeM   l u i       = return $  lookupRange   l u i+  lookupRangeMSc l u i       = return $  lookupRangeSc l u i+  insertListM  vs i          = return $! insertList vs i+  deleteDocsM ds i           = return $! deleteDocs ds i+  insertM k v i              = return $! insert k v i+  deleteM k i                = return $! delete k i+  emptyM                     = return $! empty+  toListM i                  = return $  toList i+  fromListM l                = return $! fromList l+  unionWithM f i1 i2         = return $! unionWith f i1 i2+--  unionWithConvM f1 f2 i1 i2 = return $! unionWithConv f1 f2 i1 i2+  mapM f i                   = return $! map f i+  mapMaybeM f i              = return $! mapMaybe f i+  keysM i                    = return $  keys i++-- ------------------------------------------------------------++addDefScore :: [(a, b)] -> [(a, (Score, b))]+addDefScore = L.map (second (\ x -> (defScore, x)))++-- ------------------------------------------------------------
+ src/Hunt/Index/IndexImpl.hs view
@@ -0,0 +1,103 @@+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE TypeFamilies              #-}++-- ----------------------------------------------------------------------------+{- |+  Heterogeneous indexes using @ExistentialQuantification@.++  Indexes have to implement the 'Index' type class as well as obey other constraints defined in+  'IndexImplCon'.++  /Note/: Due to the nature of @ExistentialQuantification@, deserialization is tricky because the+  concrete implementation is not known.+  This is circumvented by storing the type representation of the index. Deserialization is possible+  if there is a matching type representation in the set of available types.+  This requires the use of the custom get functions.+-}+-- ----------------------------------------------------------------------------++module Hunt.Index.IndexImpl where++import           Control.Applicative         ((<$>))+import           Control.DeepSeq+import           Control.Monad++import           Data.Binary+import qualified Data.List                   as L+import           Data.Text                   (Text)+import           Data.Text.Binary            ()+import           Data.Typeable+import           Data.Typeable.Binary        ()++import           Hunt.Common.BasicTypes+import           Hunt.Common.IntermediateValue+import           Hunt.Index++-- ------------------------------------------------------------++-- | Constraint for index implementations.+type IndexImplCon i+  = ( Index i+    , Show i+    , ICon i+    , IndexValue (IVal i)+    , Binary i+    , Typeable i+    , IKey i ~ Text+    )++-- ------------------------------------------------------------++-- | Index using @ExistentialQuantification@ to allow heterogeneous index containers.+data IndexImpl+  = forall i. IndexImplCon i => IndexImpl { ixImpl :: i }++-- ------------------------------------------------------------++instance Show IndexImpl where+  show (IndexImpl v) = show v++-- FIXME: not 'rnf v `seq` ()'. is it supposed to be that way?+instance NFData IndexImpl where+  rnf (IndexImpl v) = v `seq` ()++-- ------------------------------------------------------------+-- Serialization++-- | FIXME: actually implement instance+instance Binary IndexImpl where+  put (IndexImpl i) = put (typeOf i) >> put i+  get = error "existential types cannot be deserialized this way. Use special get' functions"++-- TODO: refactor++-- | Deserialize a set of 'IndexImpl's. Requires a set of available index implementations.+--+--   /Note/: This will fail if a used index implementation is not provided.+gets' :: [IndexImpl] -> Get [(Context, IndexImpl)]+gets' ts = do+  n <- get :: Get Int+  go [] n+  where+  go xs 0 = return $! reverse xs+  go xs i = do+    x <- liftM2 (,) get (get' ts)+    x `seq` go (x:xs) (i-1)++-- | Deserialize an 'IndexImpl'. Requires a set of available index implementations.+--+--   /Note/: This will fail if a used index implementation is not provided.+get' :: [IndexImpl] -> Get (IndexImpl)+get' ts = do+  t <- get :: Get TypeRep+  case L.find (\(IndexImpl i) -> t == typeOf i) ts of+    Just (IndexImpl x) -> IndexImpl <$> get `asTypeOf` return x+    Nothing            -> error $ "Unable to load index of type: " -- ++ show t++-- ------------------------------------------------------------++-- | Wrap an index using @ExistentialQuantification@ to allow heterogeneous containers.+mkIndex :: IndexImplCon i => i -> IndexImpl+mkIndex i = IndexImpl $! i++-- ------------------------------------------------------------
+ src/Hunt/Index/InvertedIndex.hs view
@@ -0,0 +1,224 @@+{-# LANGUAGE ConstraintKinds            #-}+{-# LANGUAGE DeriveDataTypeable         #-}+{-# LANGUAGE FlexibleContexts           #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverlappingInstances       #-}+{-# LANGUAGE TypeFamilies               #-}+{-# LANGUAGE UndecidableInstances        #-}++-- ----------------------------------------------------------------------------+{- |+  Top-level index implementations with 'Text' keys for++    * text++    * integers++    * dates and++    * geographic positions.+-}+-- ----------------------------------------------------------------------------++module Hunt.Index.InvertedIndex+{-+  ( InvertedIndex (..)+  , InvertedIndexDate+  , InvertedIndexInt+  , InvertedIndexPosition+  , InvertedIndexRTree+  )+-- -}+where++import           Prelude                              as P++import           Control.DeepSeq++import           Data.Bijection.Instances             ()+import           Data.Binary                          (Binary (..))+import qualified Data.List                            as L+import           Data.Text                            (Text)+import qualified Data.Text                            as T+import           Data.Typeable++import           Hunt.Common.BasicTypes+import           Hunt.Common.Occurrences              (Occurrences)+import           Hunt.Index                           as Ix+import           Hunt.Index.PrefixTreeIndex+import qualified Hunt.Index.PrefixTreeIndex2Dim       as PT2D++import           Hunt.Index.Proxy.KeyIndex+++-- ------------------------------------------------------------+-- Inverted index using text key+-- ------------------------------------------------------------++-- | Text index using a 'StringMap'-implementation.+newtype InvertedIndex+  = InvIx { invIx :: KeyProxyIndex Text (DmPrefixTree Occurrences) }+  deriving (Eq, Show, NFData, Typeable)++mkInvIx :: KeyProxyIndex Text (DmPrefixTree Occurrences)+        -> InvertedIndex+mkInvIx x = InvIx $! x++-- ------------------------------------------------------------++instance Binary InvertedIndex where+  put = put . invIx+  get = get >>= return . mkInvIx++-- ------------------------------------------------------------++instance Index InvertedIndex where+  type IKey InvertedIndex = Word+  type IVal InvertedIndex = Occurrences++  insertList wos (InvIx i)+    = mkInvIx $ insertList wos i++  deleteDocs docIds (InvIx i)+    = mkInvIx $ deleteDocs docIds i++  empty+    = mkInvIx $ empty++  fromList l+    = mkInvIx $ fromList l++  toList (InvIx i)+    = toList i++  search t k (InvIx i)+    = search t k i++  -- here the scoring of found word rel. to searched word is added+  searchSc t k m+      = L.map scoreWord $ search t k m+        where+          dist+              = similar k+          scoreWord (w, r)+              = (w, (dist w, r))+++  lookupRange k1 k2 (InvIx i)+    = lookupRange k1 k2 i++  -- for lookupRangeSc the default scoring (all 1.0) is done+  --+  -- no better scoring known++  unionWith op (InvIx i1) (InvIx i2)+    = mkInvIx $ unionWith op i1 i2++{-+  unionWithConv to f (InvIx i1) (InvIx i2)+    = mkInvIx $ unionWithConv to f i1 i2+-}++  map f (InvIx i)+    = mkInvIx $ Ix.map f i++  mapMaybe f (InvIx i)+    = mkInvIx $ Ix.mapMaybe f i++  keys (InvIx i)+    = keys i+++-- ------------------------------------------------------------++-- | a simple similarity heuristic for scoring words found+-- when doing a fuzzy or prefix search++similar :: Text -> Text -> Score+similar s f+    = -- traceShow ("similar"::Text, s, f, r) $+      r+    where+      r = similar' s f++similar' :: Text -> Text -> Score+similar' searched found+    | searched == found+        = 1.0+    | ls == lf+        = 0.75+    | ls < lf                     -- reduce score by length of found word+        = 0.5 * (fromIntegral ls / fromIntegral lf)+    | otherwise                   -- make similar total+        = noScore+    where+      ls = T.length searched+      lf = T.length found++-- ------------------------------------------------------------+-- Inverted index using 2-dimensional lookup+-- ------------------------------------------------------------++-- | Text index with 2-dimensional lookup using a 'StringMap'-implementation.+newtype InvertedIndex2Dim+  = InvIx2D { invIx2D :: KeyProxyIndex Text (PT2D.DmPrefixTree Occurrences) }+  deriving (Eq, Show, NFData, Typeable)++mkInvIx2D :: KeyProxyIndex Text (PT2D.DmPrefixTree Occurrences)+        -> InvertedIndex2Dim+mkInvIx2D x = InvIx2D $! x++-- ------------------------------------------------------------++instance Binary InvertedIndex2Dim where+  put = put . invIx2D+  get = get >>= return . mkInvIx2D++-- ------------------------------------------------------------++instance Index InvertedIndex2Dim where+  type IKey InvertedIndex2Dim = Word+  type IVal InvertedIndex2Dim = Occurrences++  insertList wos (InvIx2D i)+    = mkInvIx2D $ insertList wos i++  deleteDocs docIds (InvIx2D i)+    = mkInvIx2D $ deleteDocs docIds i++  empty+    = mkInvIx2D $ empty++  fromList l+    = mkInvIx2D $ fromList l++  toList (InvIx2D i)+    = toList i++  search t k (InvIx2D i)+    = search t k i++  -- TODO: searchSc and lookupRangeSc implementation similar to InvertedIndexInt and InvertedIndex++  lookupRange k1 k2 (InvIx2D i)+    = lookupRange k1 k2 i++  unionWith op (InvIx2D i1) (InvIx2D i2)+    = mkInvIx2D $ unionWith op i1 i2++{-+  unionWithConv to f (InvIx2D i1) (InvIx2D i2)+    = mkInvIx2D $ unionWithConv to f i1 i2+-}++  map f (InvIx2D i)+    = mkInvIx2D $ Ix.map f i++  mapMaybe f (InvIx2D i)+    = mkInvIx2D $ Ix.mapMaybe f i++  keys (InvIx2D i)+    = keys i+++
+ src/Hunt/Index/PrefixTreeIndex.hs view
@@ -0,0 +1,407 @@+{-# LANGUAGE DeriveDataTypeable         #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE TypeFamilies               #-}++-- ----------------------------------------------------------------------------+{- |+  Text index using the 'DocIdMap' based on the 'StringMap' implementation.+-}+-- ----------------------------------------------------------------------------++module Hunt.Index.PrefixTreeIndex+    ( DmPrefixTree (..)+    , SimplePrefixTreeIndex (..)+    , PrefixTreeIndexInt (..)+    , PrefixTreeIndexDate (..)+    )+where++import           Control.DeepSeq++import           Data.Binary            (Binary (..))+import qualified Data.List              as L+import qualified Data.StringMap.Strict  as SM+import           Data.Typeable+import           Data.Bijection+import           Data.Bijection.Instances ()++import           Data.Text              (Text)+import qualified Data.Text              as T+import           Data.Maybe             (fromMaybe)++import           Text.Read              (readMaybe)++import           Hunt.Common.BasicTypes+import           Hunt.Common.DocIdSet   (DocIdSet)+import           Hunt.Common.IntermediateValue+import           Hunt.Index+import qualified Hunt.Index             as Ix+import           Hunt.Index.Proxy.KeyIndex++import           Hunt.Utility++import qualified Hunt.Index.Schema.Normalize.Date     as Date+import qualified Hunt.Index.Schema.Normalize.Int      as Int+++++-- import           Debug.Trace++-- ------------------------------------------------------------++-- | Text index using 'DocIdMap' based on the 'StringMap' implementation.+--   Note that the value parameter is on the type of the 'DocIdMap' value and not the 'DocIdSet'+--   itself.++newtype DmPrefixTree v+  = DmPT { dmPT :: SM.StringMap v}+  deriving (Eq, Show, NFData, Typeable)++mkDmPT :: NFData v => SM.StringMap v -> DmPrefixTree v+mkDmPT v = DmPT $! v++-- ------------------------------------------------------------++instance IndexValue v => Binary (DmPrefixTree v) where+  put = put . dmPT+  get = get >>= return . mkDmPT++-- ------------------------------------------------------------++instance (IndexValue v) => Index (DmPrefixTree v)  where+  type IKey (DmPrefixTree v) = SM.Key+  type IVal (DmPrefixTree v) = v++  insertList kvs (DmPT pt) =+    mkDmPT $ L.foldl' (\ m' (k', v') -> SM.insertWith mergeValues k' v' m') pt (fromIntermediates kvs)++    {- this is a nice try, but does not do what it should do,+       at least for [("a", occ1), ("a", occ2)]++       mkDmPT $ SM.unionWith op pt (SM.fromList kvs)+    -}++  deleteDocs ks (DmPT pt)+    = mkDmPT $ SM.mapMaybe (diffValues ks) pt++  empty+    = mkDmPT $ SM.empty++  fromList+    = mkDmPT . SM.fromList . fromIntermediates++  toList (DmPT pt)+    = toIntermediates $ SM.toList pt++  search t k (DmPT pt)+    = toIntermediates $ case t of+        Case         -> case SM.lookup k pt of+                          Nothing -> []+                          Just xs -> [(k,xs)]+        NoCase       -> luCase k pt+        PrefixCase   -> pfCase k pt+        PrefixNoCase -> pfNoCase k pt+    where+    toL      = SM.toListShortestFirst+    luCase   = toL .:: SM.lookupNoCase+    pfCase   = toL .:: SM.prefixFilter+    pfNoCase = toL .:: SM.prefixFilterNoCase++  lookupRange k1 k2 (DmPT pt)+    = toIntermediates . SM.toList $ SM.lookupRange k1 k2 pt++  unionWith op (DmPT pt1) (DmPT pt2)+    = mkDmPT $ SM.unionWith op pt1 pt2++{-+  unionWithConv to f (DmPT i1) (DmPT i2)+    = liftM mkDmPT $ unionWithConv to f i1 i2+-}++  map f (DmPT pt)+    = mkDmPT $ SM.map f pt++  mapMaybe f (DmPT pt)+    = mkDmPT $ SM.mapMaybe f pt++  keys (DmPT pt)+    = SM.keys pt++-- ------------------------------------------------------------+-- Simple minimal PrefixTreeIndex based on the 'StringMap'+-- ------------------------------------------------------------++-- | Integer index using a 'StringMap'-implementation.+newtype SimplePrefixTreeIndex+  = SimplePTIx { simplePTIx :: KeyProxyIndex Text (DmPrefixTree DocIdSet) }+  deriving (Eq, Show, NFData, Typeable)++mkSimplePTIx :: KeyProxyIndex Text (DmPrefixTree DocIdSet) -> SimplePrefixTreeIndex+mkSimplePTIx x = SimplePTIx $! x++-- ------------------------------------------------------------++instance Binary SimplePrefixTreeIndex where+  put = put . simplePTIx+  get = get >>= return . mkSimplePTIx++-- ------------------------------------------------------------++instance Index SimplePrefixTreeIndex where+  type IKey SimplePrefixTreeIndex = Text+  type IVal SimplePrefixTreeIndex = DocIdSet++  insertList wos (SimplePTIx i)+    = mkSimplePTIx $ insertList wos i++  deleteDocs docIds (SimplePTIx i)+    = mkSimplePTIx $ deleteDocs docIds i++  empty+    = mkSimplePTIx $ empty++  fromList l+    = mkSimplePTIx $ fromList l++  toList (SimplePTIx i)+    = toList i++  search t k (SimplePTIx i)+    = search t k i++  searchSc t k m+      = L.map scoreWord $ search t k m+        where+          dist+              = similarInt k+          scoreWord (w, r)+              = (w, (dist w, r))++  lookupRange k1 k2 (SimplePTIx i)+    = lookupRange k1 k2 i++  lookupRangeSc k1 k2 m+    = L.map scoreWord $ lookupRange k1 k2 m+      where+        dist+            = similarRangeInt k1 k2+        scoreWord (w, r)+            = (w, (dist w, r))++  unionWith op (SimplePTIx i1) (SimplePTIx i2)+    = mkSimplePTIx $ unionWith op i1 i2++--  unionWithConv to' f (SimplePTIx i1) (SimplePTIx i2)+--    = mkSimplePTIx $ unionWithConv to' f i1 i2++  map f (SimplePTIx i)+    = mkSimplePTIx $ Ix.map f i++  mapMaybe f (SimplePTIx i)+    = mkSimplePTIx $ Ix.mapMaybe f i++  keys (SimplePTIx i)+    = keys i+++-- ------------------------------------------------------------+-- PrefixTree index using int proxy for numeric data+-- ------------------------------------------------------------++-- | Newtype to allow integer normalization 'Bijection' instance.+newtype UnInt = UnInt { unInt :: Text }+  deriving (Show, Eq, NFData)++instance Bijection UnInt Text where+  to   = Int.denormalizeFromText . unInt+  from = UnInt . Int.normalizeToText++instance Bijection Text UnInt where+  to   = UnInt+  from = unInt++-- ------------------------------------------------------------++-- | Integer index using a 'StringMap'-implementation.+newtype PrefixTreeIndexInt+  = InvIntIx { invIntIx :: KeyProxyIndex Text (KeyProxyIndex UnInt (KeyProxyIndex Text (DmPrefixTree DocIdSet))) }+  deriving (Eq, Show, NFData, Typeable)++mkInvIntIx :: KeyProxyIndex Text (KeyProxyIndex UnInt (KeyProxyIndex Text (DmPrefixTree DocIdSet))) -> PrefixTreeIndexInt+mkInvIntIx x = InvIntIx $! x++-- ------------------------------------------------------------++instance Binary PrefixTreeIndexInt where+  put = put . invIntIx+  get = get >>= return . InvIntIx++-- ------------------------------------------------------------++instance Index PrefixTreeIndexInt where+  type IKey PrefixTreeIndexInt = Text+  type IVal PrefixTreeIndexInt = DocIdSet++  insertList wos (InvIntIx i)+    = mkInvIntIx $ insertList wos i++  deleteDocs docIds (InvIntIx i)+    = mkInvIntIx $ deleteDocs docIds i++  empty+    = mkInvIntIx $ empty++  fromList l+    = mkInvIntIx $ fromList l++  toList (InvIntIx i)+    = toList i++  search t k (InvIntIx i)+    = search t k i++  searchSc t k m+      = L.map scoreWord $ search t k m+        where+          dist+              = similarInt k+          scoreWord (w, r)+              = (w, (dist w, r))++  lookupRange k1 k2 (InvIntIx i)+    = lookupRange k1 k2 i++  lookupRangeSc k1 k2 m+    = L.map scoreWord $ lookupRange k1 k2 m+      where+        dist+            = similarRangeInt k1 k2+        scoreWord (w, r)+            = (w, (dist w, r))++  unionWith op (InvIntIx i1) (InvIntIx i2)+    = mkInvIntIx $ unionWith op i1 i2++--  unionWithConv to' f (InvIntIx i1) (InvIntIx i2)+--    = mkInvIntIx $ unionWithConv to' f i1 i2++  map f (InvIntIx i)+    = mkInvIntIx $ Ix.map f i++  mapMaybe f (InvIntIx i)+    = mkInvIntIx $ Ix.mapMaybe f i++  keys (InvIntIx i)+    = keys i+++similarInt :: Text -> Text -> Score+similarInt searched found+    = fromMaybe noScore $+      do s <- readMaybe $ T.unpack searched+         f <- readMaybe $ T.unpack found+         return $ similarFloat (fromIntegral (s::Int)) (fromIntegral (f::Int))++similarRangeInt :: Text -> Text -> Text -> Score+similarRangeInt lbt ubt found+    = fromMaybe noScore $+      do lb <- readMaybe $ T.unpack lbt+         ub <- readMaybe $ T.unpack ubt+         f  <- readMaybe $ T.unpack found+         return $ similarFloat+                    (fromIntegral ((lb::Int) + (ub::Int)) / 2.0)+                    (fromIntegral (f::Int))++similarFloat :: Float -> Float -> Score+similarFloat mu+    = mkScore . bellCurve (sigma mu) mu++sigma :: Float -> Float+sigma x+    = abs x `max` 10.0 / 10.0++-- | Gaussian bell curve for scoring+bellCurve :: Float -> (Float -> Float -> Float)+bellCurve sigma'+    = \ mu x -> exp (- (x - mu) ^ _2 / sigma2'2)+    where+      _2 :: Int+      _2 = 2+      sigma2'2 = 2.0 * sigma' ^ _2++-- ------------------------------------------------------------+-- inverted index using date proxy for dates+-- ------------------------------------------------------------++-- | Newtype to allow date normalization 'Bijection' instance.+newtype UnDate = UnDate { unDate :: Text }+  deriving (Show, Eq, NFData)++instance Bijection UnDate Text where+  to   = Date.denormalize . unDate+  from = UnDate . Date.normalize++instance Bijection Text UnDate where+  to   = UnDate+  from = unDate++-- ------------------------------------------------------------++-- | Date index using a 'StringMap'-implementation.+newtype PrefixTreeIndexDate+  = InvDateIx { invDateIx :: KeyProxyIndex Text (KeyProxyIndex UnDate (KeyProxyIndex Text (DmPrefixTree DocIdSet))) }+  deriving (Eq, Show, NFData, Typeable)++mkInvDateIx :: KeyProxyIndex Text (KeyProxyIndex UnDate (KeyProxyIndex Text (DmPrefixTree DocIdSet))) -> PrefixTreeIndexDate+mkInvDateIx x = InvDateIx $! x++-- ------------------------------------------------------------++instance Binary PrefixTreeIndexDate where+  put = put . invDateIx+  get = get >>= return . mkInvDateIx++-- ------------------------------------------------------------++instance Index PrefixTreeIndexDate where+  type IKey PrefixTreeIndexDate = Word+  type IVal PrefixTreeIndexDate = DocIdSet++  insertList wos (InvDateIx i)+    = mkInvDateIx $ insertList wos i++  deleteDocs docIds (InvDateIx i)+    = mkInvDateIx $ deleteDocs docIds i++  empty+    = mkInvDateIx $ empty++  fromList l+    = mkInvDateIx $ fromList l++  toList (InvDateIx i)+    = toList i++  search t k (InvDateIx i)+    = search t k i++  -- TODO: searchSc and lookupRangeSc implementation similar to PrefixTreeIndexInt and InvertedIndex++  lookupRange k1 k2 (InvDateIx i)+    = lookupRange k1 k2 i++  unionWith op (InvDateIx i1) (InvDateIx i2)+    = mkInvDateIx $ unionWith op i1 i2++--  unionWithConv to' f (InvDateIx i1) (InvDateIx i2)+--    = mkInvDateIx $ unionWithConv to' f i1 i2++  map f (InvDateIx i)+    = mkInvDateIx $ Ix.map f i++  mapMaybe f (InvDateIx i)+    = mkInvDateIx $ Ix.mapMaybe f i++  keys (InvDateIx i)+    = Ix.keys i
+ src/Hunt/Index/PrefixTreeIndex2Dim.hs view
@@ -0,0 +1,188 @@+{-# LANGUAGE DeriveDataTypeable         #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE TypeFamilies               #-}++-- ----------------------------------------------------------------------------+{- |+  Text index using the 'DocIdMap' based on the 'StringMap' implementation.+-}+-- ----------------------------------------------------------------------------++module Hunt.Index.PrefixTreeIndex2Dim+( DmPrefixTree(..)+, PrefixTreeIndexPosition+)+where++import           Control.DeepSeq++import           Data.Binary               (Binary (..))+import           Data.Typeable++import qualified Data.StringMap.Dim2Search as SM2+import qualified Data.StringMap.Strict     as SM+import           Data.Bijection+import           Data.Bijection.Instances  ()+import           Data.Text                 (Text)++import           Hunt.Common.BasicTypes+import           Hunt.Common.DocIdSet+import           Hunt.Common.IntermediateValue+import           Hunt.Index+import qualified Hunt.Index                as Ix+import           Hunt.Index.Proxy.KeyIndex++import qualified Hunt.Index.Schema.Normalize.Position as Pos++import           Hunt.Utility++-- ------------------------------------------------------------++-- | Text index using 'DocIdMap' based on the 'StringMap' implementation.+--   Note that the value parameter is on the type of the 'DocIdMap' value and not the 'DocIdSet'+--   itself.+newtype DmPrefixTree v+  = DmPT { dmPT :: SM.StringMap v }+  deriving (Eq, Show, NFData, Typeable)++mkDmPT :: NFData v => SM.StringMap v -> DmPrefixTree v+mkDmPT v = DmPT $! v++-- ------------------------------------------------------------++instance IndexValue v => Binary (DmPrefixTree v) where+  put = put . dmPT+  get = get >>= return . mkDmPT++-- ------------------------------------------------------------++instance IndexValue v => Index (DmPrefixTree v) where+  type IKey (DmPrefixTree v) = SM.Key+  type IVal (DmPrefixTree v) = v++  insertList kvs (DmPT pt) =+    mkDmPT $ SM.unionWith mergeValues pt (SM.fromList . fromIntermediates $ kvs)++  deleteDocs ks (DmPT pt)+    = mkDmPT $ SM.mapMaybe (diffValues ks) pt++  empty+    = mkDmPT $ SM.empty++  fromList+    = mkDmPT . SM.fromList . fromIntermediates++  toList (DmPT pt)+    = toIntermediates . SM.toList $ pt++  search t k (DmPT pt)+    = toIntermediates $ case t of+        Case         -> case SM.lookup k pt of+                          Nothing -> []+                          Just xs -> [(k,xs)]+        NoCase       -> luCase k pt+        PrefixCase   -> pfCase k pt+        PrefixNoCase -> pfNoCase k pt+    where+    toL      = SM.toListShortestFirst+    luCase   = toL .:: SM.lookupNoCase+    pfCase   = toL .:: SM.prefixFilter+    pfNoCase = toL .:: SM.prefixFilterNoCase++  lookupRange k1 k2 (DmPT pt)+    = toIntermediates . SM.toList $ SM2.lookupRange k1 k2 pt++  unionWith op (DmPT pt1) (DmPT pt2)+    = mkDmPT $ SM.unionWith op pt1 pt2++{-+  unionWithConv to f (DmPT i1) (DmPT i2)+    = liftM mkDmPT $ unionWithConv to f i1 i2+-}++  map f (DmPT pt)+    = mkDmPT $ SM.map f pt++  mapMaybe f (DmPT pt)+    = mkDmPT $ SM.mapMaybe f pt++  keys (DmPT pt)+    = SM.keys pt++-- ------------------------------------------------------------+-- Inverted index using position proxy for geo coordinates+-- ------------------------------------------------------------++-- | Newtype to allow position normalization 'Bijection' instance.+newtype UnPos = UnPos { unPos :: Text }+  deriving (Show, Eq, NFData)++instance Bijection UnPos Text where+  to   = Pos.denormalize . unPos+  from = UnPos . Pos.normalize++instance Bijection Text UnPos where+  to   = UnPos+  from = unPos++-- ------------------------------------------------------------++-- ------------------------------------------------------------++-- | Geo-position index using a 'StringMap'-implementation.+--+newtype PrefixTreeIndexPosition+  = InvPosIx { invPosIx :: KeyProxyIndex Text (KeyProxyIndex UnPos (KeyProxyIndex Text (DmPrefixTree DocIdSet))) }+  deriving (Eq, Show, NFData, Typeable)++mkInvPosIx :: KeyProxyIndex Text (KeyProxyIndex UnPos (KeyProxyIndex Text (DmPrefixTree DocIdSet))) -> PrefixTreeIndexPosition+mkInvPosIx x = InvPosIx $! x++-- ------------------------------------------------------------++instance  Binary PrefixTreeIndexPosition where+  put = put . invPosIx+  get = get >>= return . mkInvPosIx++-- ------------------------------------------------------------++instance Index PrefixTreeIndexPosition where+  type IKey PrefixTreeIndexPosition = Word+  type IVal PrefixTreeIndexPosition = DocIdSet++  insertList wos (InvPosIx i)+    = mkInvPosIx $ insertList wos i++  deleteDocs docIds (InvPosIx i)+    = mkInvPosIx $ deleteDocs docIds i++  empty+    = mkInvPosIx $ Ix.empty++  fromList l+    = mkInvPosIx $ Ix.fromList l++  toList (InvPosIx i)+    = Ix.toList i++  search t k (InvPosIx i)+    = search t k i++  lookupRange k1 k2 (InvPosIx i)+    = lookupRange k1 k2 i++  unionWith op (InvPosIx i1) (InvPosIx i2)+    = mkInvPosIx $ unionWith op i1 i2++--  unionWithConv to' f (InvPosIx i1) (InvPosIx i2)+--    = mkInvPosIx $ unionWithConv to' f i1 i2++  map f (InvPosIx i)+    = mkInvPosIx $ Ix.map f i++  mapMaybe f (InvPosIx i)+    = mkInvPosIx $ Ix.mapMaybe f i++  keys (InvPosIx i)+    = keys i+
+ src/Hunt/Index/Proxy/KeyIndex.hs view
@@ -0,0 +1,102 @@+{-# LANGUAGE FlexibleContexts           #-}+{-# LANGUAGE FlexibleInstances          #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverlappingInstances       #-}+{-# LANGUAGE TypeFamilies               #-}+{-# LANGUAGE UndecidableInstances       #-}++-- ----------------------------------------------------------------------------++{- |+  Key conversion proxy.+  Wraps an index to expose the desired key type.+  The conversion is defined by the 'Bijection' implementation.++  This can be used for simple conversions like @Text@ to @String@ or normalization and compression.+-}++-- ----------------------------------------------------------------------------++module Hunt.Index.Proxy.KeyIndex+( KeyProxyIndex (..)+)+where++import           Prelude                       as P++import           Control.Applicative           ((<$>))+import           Control.Arrow                 (first)+import           Control.DeepSeq++import           Data.Bijection+import           Data.Binary                   (Binary (..))++import           Hunt.Index+import qualified Hunt.Index                    as Ix+import           Hunt.Common.IntermediateValue (IndexValue)+-- ------------------------------------------------------------++-- | Key conversion proxy.+--   @toType@ is the desired/exposed key type, followed by the wrapped index type.+--   There has to be a corresponding 'Bijection' instance:+--+--   >  instance Bijection (IKey impl v) toType where ...+newtype KeyProxyIndex toType impl+  = KeyProxyIx { keyProxyIx :: impl }+  deriving (Eq, Show, NFData)++-- | Wrap an index in a key conversion proxy.+mkKeyProxyIx :: impl -> KeyProxyIndex toType impl+mkKeyProxyIx v = KeyProxyIx $! v++-- ------------------------------------------------------------++instance Binary (impl) => Binary (KeyProxyIndex toType impl) where+  put = put . keyProxyIx+  get = get >>= return . mkKeyProxyIx++-- ------------------------------------------------------------++instance (IndexValue (IVal impl)) => Index (KeyProxyIndex toType impl) where+  type IKey      (KeyProxyIndex toType impl) = toType+  type IVal      (KeyProxyIndex toType impl) = IVal impl+  type ICon      (KeyProxyIndex toType impl) = ( Index impl+                                               , ICon impl+                                               , Bijection (IKey impl) toType+                                               )++  insertList kvs (KeyProxyIx i)+    = mkKeyProxyIx $ insertList (P.map (first from) kvs) i++  deleteDocs ks (KeyProxyIx i)+    = mkKeyProxyIx $ deleteDocs ks i++  empty+    = mkKeyProxyIx $ empty++  fromList l+    = mkKeyProxyIx . fromList $ P.map (first from) l++  toList (KeyProxyIx i)+    = first to <$> toList i++  search t k (KeyProxyIx i)+    = first to <$> search t (from k) i++  lookupRange k1 k2 (KeyProxyIx i)+    = first to <$> lookupRange (from k1) (from k2) i++  unionWith op (KeyProxyIx i1) (KeyProxyIx i2)+    = mkKeyProxyIx $ unionWith op i1 i2++--  unionWithConv t f (KeyProxyIx i1) (KeyProxyIx i2)+--    = mkKeyProxyIx $ unionWithConv t f i1 i2++  map f (KeyProxyIx i)+    = mkKeyProxyIx $ Ix.map f i++  mapMaybe f (KeyProxyIx i)+    = mkKeyProxyIx $ Ix.mapMaybe f i++  keys (KeyProxyIx i)+    = P.map to $ keys i
+ src/Hunt/Index/RTreeIndex.hs view
@@ -0,0 +1,180 @@+{-# LANGUAGE DeriveDataTypeable         #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE TypeFamilies               #-}++-- ----------------------------------------------------------------------------+{- |+  Text index using the 'DocIdMap' based on the 'StringMap' implementation.+-}+-- ----------------------------------------------------------------------------++module Hunt.Index.RTreeIndex+(   RTreeIndex(..)+  , SimpleRTreeIndex(..)+  , readPosition+  , showPosition+)+where++import           Control.DeepSeq++import           Data.Binary                          (Binary (..))+import qualified Data.List                            as L+import           Data.Monoid                          ((<>))+import qualified Data.RTree.Strict                    as RT+import           Data.RTree.MBB+import           Data.Text                            (Text)+import qualified Data.Text                            as T (pack, unpack)+import           Data.Typeable+import           Data.Bijection++import           Hunt.Index+import qualified Hunt.Index                           as Ix+import           Hunt.Index.Proxy.KeyIndex+import           Hunt.Common.IntermediateValue+import           Hunt.Common.DocIdSet                 (DocIdSet)+import           Hunt.Index.Schema.Normalize.Position (position)++import           Text.Parsec+++-- ------------------------------------------------------------++-- | Index adapter for 'Data.RTree' data structure++newtype RTreeIndex v+  = DmRT { dmRT :: RT.RTree v }+  deriving (Eq, Show, NFData, Typeable)++mkDmRT :: RT.RTree v -> RTreeIndex v+mkDmRT v = DmRT $! v++-- ------------------------------------------------------------++instance IndexValue x => Binary (RTreeIndex x) where+  put = put . dmRT+  get = get >>= return . mkDmRT++-- ------------------------------------------------------------++instance IndexValue v => Index (RTreeIndex v) where+  type IKey (RTreeIndex v) = RT.MBB+  type IVal (RTreeIndex v) = v++  insertList kvs (DmRT rt) =+    mkDmRT $ L.foldl' (\ m' (k', v') -> RT.insertWith mergeValues k' v' m') rt (fromIntermediates kvs)++    {- same problem as in PrefixTreeIndex, the k' in kvs don't need to be unique++       mkDmRT $ RT.unionWith op rt (RT.fromList kvs)+    -}++  deleteDocs ks (DmRT rt)+    = mkDmRT $ RT.mapMaybe (diffValues ks) rt++  empty+    = mkDmRT $ RT.empty++  fromList+    = mkDmRT . RT.fromList . fromIntermediates++  toList (DmRT rt)+    = toIntermediates . RT.toList $ rt++  -- MBBs don't have any case or prefix+  search _ k (DmRT rt)+    = toIntermediates $ RT.lookupRangeWithKey k rt++  lookupRange k1 k2 (DmRT rt)+    = toIntermediates $ RT.lookupRangeWithKey (unionMBB k1 k2) rt++  unionWith op (DmRT rt1) (DmRT rt2)+    = mkDmRT $ RT.unionWith op rt1 rt2++  map f (DmRT rt)+    = mkDmRT $ fmap f rt++  -- prohibitiv expensive.+  mapMaybe f (DmRT rt)+    = mkDmRT $ RT.mapMaybe f rt++  keys (DmRT rt)+    = RT.keys rt++-- ------------------------------------------------------------++readPosition :: Text -> MBB+readPosition t+  = case parse position "" $ T.unpack t of+     Right (la,lo) -> mbb la lo la lo+     Left _        -> error "readPosition positon: invalid"++showPosition :: MBB -> Text+showPosition (MBB la lo _ _ ) = T.pack (show la) <> "-" <> T.pack (show lo)++-- ------------------------------------------------------------+-- Index using RTree for indexing positions and bounding boxes+-- ------------------------------------------------------------++-- | Newtype to allow date normalization 'Bijection' instance.++instance Bijection MBB Text where+  to   = showPosition+  from = readPosition++-- ------------------------------------------------------------++-- | Date index using a 'StringMap'-implementation.+newtype SimpleRTreeIndex+  = InvRTreeIx { invRTreeIx :: KeyProxyIndex Text (RTreeIndex DocIdSet)}+  deriving (Eq, Show, NFData, Typeable)++mkInvRTreeIx :: KeyProxyIndex Text (RTreeIndex DocIdSet) -> SimpleRTreeIndex+mkInvRTreeIx x = InvRTreeIx $! x++-- ------------------------------------------------------------++instance Binary SimpleRTreeIndex where+  put = put . invRTreeIx+  get = get >>= return . mkInvRTreeIx++-- ------------------------------------------------------------++instance Index SimpleRTreeIndex where+  type IKey SimpleRTreeIndex = Text+  type IVal SimpleRTreeIndex = DocIdSet++  insertList wos (InvRTreeIx i)+    = mkInvRTreeIx $ insertList wos i++  deleteDocs docIds (InvRTreeIx i)+    = mkInvRTreeIx $ deleteDocs docIds i++  empty+    = mkInvRTreeIx $ empty++  fromList l+    = mkInvRTreeIx $ fromList l++  toList (InvRTreeIx i)+    = toList i++  search t k (InvRTreeIx i)+    = search t k i++  lookupRange k1 k2 (InvRTreeIx i)+    = lookupRange k1 k2 i++  unionWith op (InvRTreeIx i1) (InvRTreeIx i2)+    = mkInvRTreeIx $ unionWith op i1 i2++  map f (InvRTreeIx i)+    = mkInvRTreeIx $ Ix.map f i++  mapMaybe f (InvRTreeIx i)+    = mkInvRTreeIx $ Ix.mapMaybe f i++  keys (InvRTreeIx i)+    = keys i++
+ src/Hunt/Index/Schema.hs view
@@ -0,0 +1,326 @@+{-# OPTIONS -fno-warn-orphans  #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}++-- ----------------------------------------------------------------------------+{- |+  Schema for the 'ContextIndex'.++  Every context has a type (e.g. text, int, date, position) and additional schema information.+  This includes how keys are splitted and normalized when inserted and searched for.+-}+-- ----------------------------------------------------------------------------++module Hunt.Index.Schema+  (+    -- * Types+    Schema+  , ContextSchema (..)+  , ContextType (..)+  , ContextTypes+  , CValidator (..)+  , CNormalizer (..)+  , normalize'++    -- * Default Context Types+  , ctText+  , ctTextSimple+  , ctInt+  , ctDate+  , ctPosition+  , ctPositionRTree++  -- * Default Normalizers+  , cnUpperCase+  , cnLowerCase+  , cnZeroFill+  )+where++import           Control.Applicative+import           Control.Monad                        (mzero)++import           Data.Aeson+import           Data.Binary                          hiding (Word)+import           Data.Default+import qualified Data.List                            as L+import           Data.Map                             hiding (null)+import           Data.Maybe                           (isNothing)+import           Data.Text                            hiding (null)+import qualified Data.Text                            as T+import           Data.Text.Binary                     ()++import           Hunt.Common.BasicTypes+import qualified Hunt.Index                           as Ix+import           Hunt.Index.IndexImpl                 (IndexImpl, mkIndex)+import           Hunt.Index.InvertedIndex+import           Hunt.Index.PrefixTreeIndex           ( PrefixTreeIndexInt+                                                      , PrefixTreeIndexDate+                                                      , SimplePrefixTreeIndex )+import           Hunt.Index.PrefixTreeIndex2Dim       ( PrefixTreeIndexPosition )++import           Hunt.Index.RTreeIndex++import qualified Hunt.Index.Schema.Normalize.Date     as Date+import qualified Hunt.Index.Schema.Normalize.Int      as Int+import qualified Hunt.Index.Schema.Normalize.Position as Pos+import           Hunt.Utility++-- ------------------------------------------------------------++-- | The global schema assigning schema information to each context.+type Schema+  = Map Context ContextSchema++-- | The context schema information. Every context schema has a type and additional to adjust the+--   behavior.+--+--   The regular expression splits the text into words which are then transformed by the given+--   normalizations functions (e.g. to lower case).++data ContextSchema = ContextSchema+  {+    -- | Optional regex to override the default given by context type.+    cxRegEx      :: Maybe RegEx+    -- | Normalizers to apply on keys.+  , cxNormalizer :: [CNormalizer]+    -- | Context weight to boost results.+  , cxWeight     :: Weight+    -- | Whether the context is searched in queries without context-specifier.+  , cxDefault    :: Bool+    -- | The type of the index (e.g. text, int, date, geo-position).+  , cxType       :: ContextType+  }+  deriving Show++-- ------------------------------------------------------------++instance Default ContextSchema where+  def = ContextSchema Nothing [] 1.0 True def++-- ------------------------------------------------------------++-- | Set of context types.+type ContextTypes = [ContextType]++-- | A general context type like text or int.+data ContextType = CType+  {+    -- | Name used in the (JSON) API.+    ctName     :: Text+    -- | Default regex to split words.+  , ctRegEx    :: RegEx+    -- | Validation function for keys.+  , ctValidate :: CValidator+    -- | The index implementation used for this type.+  , ctIxImpl   :: IndexImpl+  }+  deriving Show++-- ------------------------------------------------------------++instance Default ContextType where+  def = ctText++-- ------------------------------------------------------------++-- | Text context type.+ctText :: ContextType+ctText = CType+  { ctName     = "text"+  , ctRegEx    = "\\w*"+  , ctValidate = def+  , ctIxImpl   = def+  }++-- | Special text context type+--   smaller index but not phrase queries possible+--   (due to not storing the words positions)+ctTextSimple :: ContextType+ctTextSimple = CType+  { ctName     = "text-small"+  , ctRegEx    = "\\w*"+  , ctValidate = def+  , ctIxImpl   = simplePT+  }++-- | Int context type.+ctInt :: ContextType+ctInt = CType+  { ctName     = "int"+  , ctRegEx    = "([-]?[0-9]*)"+  , ctValidate = CValidator $ Int.isInt+  , ctIxImpl   = intInv+  }++-- | Date context type.+ctDate :: ContextType+ctDate = CType+  { ctName     = "date"+  , ctRegEx    = "[0-9]{4}-((0[1-9])|(1[0-2]))-((0[1-9])|([12][0-9])|(3[01]))"+  , ctValidate = CValidator $ Date.isAnyDate . unpack+  , ctIxImpl   = dateInv+  }++-- | Geographic position context type.+ctPosition :: ContextType+ctPosition = CType+  { ctName     = "position"+  , ctRegEx    = "-?(90(\\.0*)?|[1-8]?[0-9](\\.[0-9]*)?)--?((180(\\.0*)?)|(1[0-7][0-9])|([1-9]?[0-9]))(\\.[0-9]*)?"+  , ctValidate = CValidator $ Pos.isPosition+  , ctIxImpl   = positionInv+  }++ctPositionRTree :: ContextType+ctPositionRTree = CType+  { ctName     = "position-rtree"+  , ctRegEx    = "-?(90(\\.0*)?|[1-8]?[0-9](\\.[0-9]*)?)--?((180(\\.0*)?)|(1[0-7][0-9])|([1-9]?[0-9]))(\\.[0-9]*)?"+  , ctValidate = CValidator $ Pos.isPosition+  , ctIxImpl   = positionRTree+  }+++-- ------------------------------------------------------------+-- IndexImpls+-- ------------------------------------------------------------++instance Default IndexImpl where+  def = defaultInv++-- ------------------------------------------------------------++-- | Default (text) index implementation.+defaultInv :: IndexImpl+defaultInv = mkIndex (Ix.empty :: InvertedIndex)++-- | Simpler (text) index, which still enables prefix search,+--   but not phrase search anymore. Useful for cases where+--   word positions are not relevant+simplePT :: IndexImpl+simplePT = mkIndex (Ix.empty :: SimplePrefixTreeIndex)++-- | Int index implementation.+intInv :: IndexImpl+intInv = mkIndex (Ix.empty :: PrefixTreeIndexInt)++-- | Date index implementation.+dateInv :: IndexImpl+dateInv = mkIndex (Ix.empty :: PrefixTreeIndexDate)++-- | Geographic position index implementation based on 'StringMap'+positionInv :: IndexImpl+positionInv = mkIndex (Ix.empty :: PrefixTreeIndexPosition)++-- | Geographic position index implementation based on 'RTree'+positionRTree :: IndexImpl+positionRTree = mkIndex (Ix.empty :: SimpleRTreeIndex)+++-- ------------------------------------------------------------+-- Validator+-- ------------------------------------------------------------++-- | Validation function for single words.+data CValidator = CValidator { validate :: Word -> Bool }++-- ------------------------------------------------------------++instance Default CValidator where+  def = CValidator $ const True++-- XXX: maybe add name to validator type as well+instance Show CValidator where+  show _ = "CValidator"++-- ------------------------------------------------------------+-- Normalizer+-- ------------------------------------------------------------++-- | Normalizer for words\/keys of an index.+data CNormalizer = CNormalizer+  { cnName    :: Text         -- ^ Name used in (JSON) API.+  , normalize :: Text -> Text -- ^ Normalization function.+  }++-- | Apply the normalizers to a word.+normalize' :: [CNormalizer] -> Word -> Word+normalize' ns  = L.foldl' (\f2 (CNormalizer _ f1) -> f1 . f2) id $ ns++-- ------------------------------------------------------------++instance Show CNormalizer where+  show = unpack . cnName++instance Default CNormalizer where+  def = CNormalizer "" id++-- | Uppercase normalizer \"UpperCase\".+cnUpperCase :: CNormalizer+cnUpperCase = CNormalizer "UpperCase" T.toUpper++-- | Lowercase normalizer \"LowerCase\".+cnLowerCase :: CNormalizer+cnLowerCase = CNormalizer "LowerCase" T.toLower++-- | Int normalizer \"ZeroFill\" to preserve int ordering on strings.+cnZeroFill :: CNormalizer+cnZeroFill = CNormalizer "ZeroFill" Int.normalizeToText++-- ------------------------------------------------------------+-- JSON instances+-- ------------------------------------------------------------++-- | /Note/: This is only partial (de-)serialization.+--   The other components are environment-dependent+--   and cannot be (de-)serialized. We serialize the name+--   and identify the other compontens of the type later.++instance FromJSON ContextType where+  parseJSON (String s) = return $ def { ctName = s }+  parseJSON _          = mzero++instance ToJSON ContextType where+  toJSON (CType n _ _ _) = String n++instance FromJSON CNormalizer where+  parseJSON (String s) = return $ def { cnName = s }+  parseJSON _          = mzero++instance ToJSON CNormalizer where+  toJSON (CNormalizer n _) = String n++instance FromJSON ContextSchema where+  parseJSON (Object o) = do+    r  <- o .:? "regexp"+    n  <- o .:? "normalizers" .!= []+    w  <- o .:? "weight"      .!= 1.0+    d  <- o .:? "default"     .!= True+    ct <- o .:  "type"+    return $ ContextSchema r n w d ct++  parseJSON _ = mzero++instance ToJSON ContextSchema where+  toJSON (ContextSchema r n w d ct) = object' $+    [ "type"        .== ct+    , "weight"      .=? w .\. (== 1.0)+    , "regexp"      .=? r .\. isNothing+    , "normalizers" .=? n .\. null+    , "default"     .=? d .\. id+    ]++-- ------------------------------------------------------------+-- Binary instances+-- ------------------------------------------------------------++instance Binary ContextSchema where+  get = ContextSchema <$> get <*> get <*> get <*> get <*> get+  put (ContextSchema a b c d e) = put a >> put b >> put c >> put d >> put e++instance Binary ContextType where+  put (CType n _ _ _) = put n+  get = get >>= \n -> return $ def { ctName = n }++instance Binary CNormalizer where+  put (CNormalizer n _) = put n+  get = get >>= \n -> return $ def { cnName = n }
+ src/Hunt/Index/Schema/Analyze.hs view
@@ -0,0 +1,99 @@+-- ----------------------------------------------------------------------------+{- |+  Analyzer for index data.+  Creates raw index data by splitting and normalizing the 'ApiDocument' index data as defined in+  the schema.+-}+-- ----------------------------------------------------------------------------++module Hunt.Index.Schema.Analyze+  ( toDocAndWords+  , toDocAndWords'+  , normalize+  , scanTextRE+  )+where++import           Data.DList                  (DList)+import qualified Data.DList                  as DL+import           Data.Map                    (Map)+import qualified Data.Map                    as M+import           Data.Maybe                  (fromJust, fromMaybe)+import           Data.Text                   (Text)+import qualified Data.Text                   as T++import           Text.Regex.XMLSchema.String++import           Hunt.Common.ApiDocument+import           Hunt.Common.BasicTypes+import           Hunt.Common.Document        (Document (..),+                                              DocumentWrapper (..))+import           Hunt.Index.Schema++-- ------------------------------------------------------------++-- | Extracts the 'Document' ('DocumentWrapper') and raw index data from an 'ApiDocument' in+--   compliance with the schema.+--+--   /Note/: Contexts mentioned in the 'ApiDocument' need to exist.++toDocAndWords :: DocumentWrapper e => Schema -> ApiDocument -> (e, Score, Words)+toDocAndWords s+    = ( \ (d, dw, ws) -> (wrap d, dw, ws) )+      . toDocAndWords' s++-- | Extracts the 'Document' and raw index data from an 'ApiDocument' in compliance with the schema.+--+--   /Note/: Contexts mentioned in the ApiDoc need to exist.++toDocAndWords' :: Schema -> ApiDocument -> (Document, Score, Words)+toDocAndWords' schema apiDoc+    = (doc, weight, ws)+    where+      indexMap = adIndex apiDoc+      descrMap = adDescr apiDoc+      weight   = adWght  apiDoc+      doc = Document+            { uri   = adUri apiDoc+            , desc  = descrMap+            , wght  = toDefScore weight+            }+      ws = M.mapWithKey+           ( \ context content ->+                 let (ContextSchema rex normalizers _ _ cType)+                         = fromJust $ M.lookup context schema+                     (CType _ defRex validator _)+                         = cType+                     scan+                         = filter (validate validator) . scanTextRE (fromMaybe defRex rex)+                 in+                   toWordList scan (normalize' normalizers) content+           )+           indexMap++-- | Construct a 'WordList' from text using the splitting and normalization function.++toWordList :: (Text -> [Word]) -> (Word -> Word) -> Text -> WordList+toWordList scan norm+    = M.map DL.toList+      . foldr insert M.empty+      . zip [1..]+      . map norm+      . scan+    where+      insert :: (Position, Word) -> Map Word (DList Position) -> Map Word (DList Position)+      insert (p, w)+          = M.alter (return . maybe (DL.singleton p) (`DL.snoc` p)) w++-- | Tokenize a text with a regular expression for words.+--+--  > scanTextRE "[^ \t\n\r]*" == Data.Text.words+--+--   Grammar: <http://www.w3.org/TR/xmlschema11-2/#regexs>+scanTextRE :: RegEx -> Text -> [Word]+scanTextRE wRex+    = map T.pack+      . tokenize (T.unpack wRex)+      . T.unpack++-- ------------------------------------------------------------
+ src/Hunt/Index/Schema/Normalize/Date.hs view
@@ -0,0 +1,433 @@+{-# LANGUAGE OverloadedStrings #-}++-- ----------------------------------------------------------------------------++{- |+  Normalization and validation for date.++  Days are represented here as in ISO 8601:2000 Second Edition:+      ISO (International Organization for Standardization).+      Representations of dates and times, second edition, 2000-12-15.++   NOT as in+   ISO 8601+      ISO (International Organization for Standardization).+      Representations of dates and times, 1988-06-15.++   The main difference is dealing with year 0.+   in the older ISO standard, this is excluded and+   "-0001" is the representation of year 1 Before Common Era "-1 BCE".+   In the latter standard "0000" represents "-1 BCE" and "-0001" represents "-2 BCE"+-}+-- source hxt-xmlschema/src/Text/XML/HXT/XMLSchema/W3CDataTypeCheck.hs++-- ----------------------------------------------------------------------------++module Hunt.Index.Schema.Normalize.Date+  ( normalize, denormalize+  , isAnyDate+  , isAnyDate'+  , isTime+  , nullDay+  , showGYearMonth+  , showGYear+  , showGMonthDay+  , showGMonth+  , showGDay+  , showTime+  )+where++import           Control.Applicative+import           Control.Monad++import           Data.List+import           Data.Maybe+import           Data.Text                   (Text)+import qualified Data.Text                   as T++import           Data.Char                   (isDigit)+import           Data.Function               (on)+import           Data.Ratio                  ((%))+import           Data.Time                   (Day, DiffTime, UTCTime (..),+                                              addUTCTime, fromGregorian)++import           Text.Regex.XMLSchema.String++import           Hunt.Utility++-- ------------------------------------------------------------++-- | Normalize a date representation to store in the index or search for.+normalize :: Text -> Text+normalize t = fromMaybe t+    (T.pack . normDateRep . showDateTime . toUTC <$> (readAnyDateM . T.unpack $ t))+  where+  -- XXX: no proper support for dates before year 0 (1 BCE) this way+  normDateRep :: String -> String+  normDateRep s+    = if head' s == Just '-'+      then ('-':) . fil . tail $ s+      else fil s+    where fil = filter (not . (`elem` "-T:"))++-- | Function takes normalized Date and transforms it back a readable form.+--   We don't transform it back to the original representation, since that+--   is never used, but to a general readable date format+denormalize :: Text -> Text+denormalize t = T.concat [y1, y2, "-", m, "-", d, " ", h, ":", i, ":", s]+  where+  [y1,y2,m,d,h,i,s] = T.chunksOf 2 t++-- ------------------------------------------------------------++-- | Checks if the string is a date representation (syntactically).+isAnyDate :: String -> Bool+isAnyDate s = any ($ s) $ map fst safeDateReaders++-- XXX: generally (showDate . readAnyDate) /= id+-- | Same as 'isAnyDate' but also checks if @(showDate . readAnyDate)@ produces the same result.+--   /NOTE/: excludes dates before year 0 (1 BCE).+isAnyDate' :: String -> Bool+isAnyDate' s = fromMaybe False $ do+  d <- readAnyDateM s+  guard $ ((==s) . showDate . readAnyDate) s+  return . not . isPrefixOf "-" . showDate . toUTC $ d++-- | Unsafe 'readAnyDateM'.+readAnyDate :: String -> Date+readAnyDate = fromJust . readAnyDateM++-- | Try to read a date.+readAnyDateM :: String -> Maybe Date+readAnyDateM s = head' . catMaybes . map readDateM $ safeDateReaders+  where+  readDateM :: (String -> Bool, String -> Date) -> Maybe Date+  readDateM (v, r) = guard (v s) >> return (r s)++-- | Tuples of date validator and reader.+safeDateReaders :: [(String -> Bool, String -> Date)]+safeDateReaders = zip validators readers+  where+  validators :: [String -> Bool]+  validators = [  isDateTime,   isDate,   isGYearMonth,   isGYear,   isGMonthDay,   isGMonth,   isGDay]+  readers    :: [String -> Date]+  readers    = [readDateTime, readDate, readGYearMonth, readGYear, readGMonthDay, readGMonth, readGDay]++-- ------------------------------------------------------------++-- source hxt-xmlschema/src/Text/XML/HXT/XMLSchema/W3CDataTypeCheck.hs++-- ----------------------------------------+--+-- Days are represented here+-- as in ISO 8601:2000 Second Edition:+--    ISO (International Organization for Standardization).+--    Representations of dates and times, second edition, 2000-12-15.+--+-- NOT as in+-- ISO 8601+--    ISO (International Organization for Standardization).+--    Representations of dates and times, 1988-06-15.+--+-- The main difference is dealing with year 0.+-- in the older ISO standard, this is excluded and+-- "-0001" is the representation of year 1 Before Common Era "-1 BCE".+-- In the latter standard "0000" represents "-1 BCE" and "-0001" represents "-2 BCE"++data Date =+    Date { _dUTCTime :: UTCTime+         , _dTZ      :: MaybeTimeZone+         }+    deriving (Show)++type MaybeTimeZone = Maybe Seconds++type Seconds = Int++instance Eq Date where+    (==) = (==) `on` toUTCTime++instance Ord Date where+    compare = compare `on` toUTCTime++mkDateTime :: Day -> DiffTime -> MaybeTimeZone -> Date+mkDateTime d t z+    = Date (UTCTime d t) z++toUTCTime :: Date -> UTCTime+toUTCTime (Date d Nothing) = d+toUTCTime (Date d (Just tz)) = addUTCTime (fromInteger . toInteger $ tz) d++-- | Convert to UTC time. Eliminates the timezone offset.+toUTC :: Date -> Date+toUTC d = Date (toUTCTime d) Nothing -- to UTC++-- ------------------------------------------------------------++isDateTime, isDate, isTime, isGYearMonth, isGYear, isGMonthDay, isGMonth, isGDay :: String -> Bool+[isDateTime, isDate, isTime, isGYearMonth, isGYear, isGMonthDay, isGMonth, isGDay]+    = map matchRE rexDates+++rexDates :: [Regex]+rexDates+    = map rex [dateTime, date, time, gYearMonth, gYear, gMonthDay, gMonth, gDay]+    where+      dateTime   = ymd ++ "T" ++ hms ++ tz+      time       =               hms ++ tz+      date       = ymd               ++ tz+      gYearMonth = ym                ++ tz+      gYear      = y                 ++ tz++      gMonthDay  = "--" ++ m2 ++ "-" ++ t2 ++ tz+      gMonth     = "--" ++ m2              ++ tz+      gDay       = "--"       ++ "-" ++ t2 ++ tz++      y     = "-?" ++ y4'+      ym    = y           ++ "-" ++ m2+      ymd   = ym                       ++ "-" ++ t2++      hms   = alt (h2 ++ ":" ++ i2 ++ ":" ++ s2 ++ fr)+                  ("24:00:00" ++ opt ".0+")             -- 24:00 is legal++      tz    = opt (alt tz0 "Z")+      tz0   = (alt "\\-" "\\+") ++ tz1+      tz1   = alt (h13 ++ ":" ++ i2) "14:00:00"++      m2    = alt "0[1-9]" "1[0-2]"                     -- Month+      t2    = alt "0[1-9]" (alt "[12][0-9]" "3[01]")    -- Tag+      h2    = alt "[01][0-9]" "2[0-3]"                  -- Hour+      i2    = "[0-5][0-9]"                              -- mInute+      s2    = i2                                        -- Seconds++{-                                                      -- this conforms to ISO 8601 from 1988+      y1    = "000[1-9]"                                -- "0000" isn't a year, "-0001" represents "-1 BCE"+      y2    = "00[1-9][0-9]"                            -- leading 0-s are only allowd for year < 1000+      y3    = "0[1-9][0-9]{2}"+      y4    = "[1-9][0-9]{3,}"+      y4'   = alt y4 $ alt y3 $ alt y2 y1+-- -}++-- {-                                                   -- this conforms to ISO 8601 Second Edition from 2000+      y4    = "[0-9]{4}"                                -- year "0000" is legal and represents "-1 BCE"+      y4'   = opt "[1-9][0-9]*" ++ y4+-- -}++      fr    = opt ".[0-9]+"++      h13   = alt "0[0-9]" "1[0-3]"++      opt x     = "(" ++ x ++ ")?"+      alt x1 x2 = "((" ++ x1 ++ ")|(" ++ x2 ++ "))"+++-- ------------------------------------------------------------++readDate+  , readGYearMonth+  , readGYear+  , readGMonthDay+  , readGMonth+  , readGDay :: String -> Date++readDate       = readDate' readYearMonthDayS+readGYearMonth = readDate' readYearMonthS+readGYear      = readDate' readYearS+readGMonthDay  = readDate' readMonthDayS+readGMonth     = readDate' readMonthS+readGDay       = readDate' readDayS+++readTimeZone :: String -> MaybeTimeZone+readTimeZone ""+    = Nothing+readTimeZone "Z"+    = Just 0+readTimeZone (s : xs)+    = Just .+      ( if s == '-' then negate else id ) .+      readZone $ xs+    where+      readZone s'+          = 60 * (60 * read hs + read ms)+          where+            (hs, (_ : ms)) = span (/= ':') s'++readYearMonthDayS :: String -> (Day, String)+readYearMonthDayS s0+    = (fromGregorian (sign $ read year) (read month) (read day), rest)+    where+      (sign,          s ) = if head s0 == '-'+                            then (negate, tail s0)+                            else (id,          s0)+      (year,  (_ : rest1)) = span (/= '-') s+      (month, (_ : rest2)) = span (/= '-') rest1+      (day,        rest  ) = span isDigit rest2++readYearMonthS :: String -> (Day, String)+readYearMonthS s0+    = (fromGregorian (sign $ read year) (read month) 1, rest)+    where+      (sign,          s ) = if head s0 == '-'+                            then (negate, tail s0)+                            else (id,          s0)+      (year,  (_ : rest1)) = span (/= '-') s+      (month,      rest  ) = span isDigit rest1++readYearS :: String -> (Day, String)+readYearS s0+    = (fromGregorian (sign $ read year) 1 1, rest)+    where+      (sign,          s ) = if head s0 == '-'+                            then (negate, tail s0)+                            else (id,          s0)+      (year,       rest  ) = span isDigit s++readMonthDayS :: String -> (Day, String)+readMonthDayS s0+    = (fromGregorian 1 (read month) (read day), rest)+    where+      (month, (_ : rest1)) = span isDigit . drop 2 $ s0+      (day,        rest  ) = span isDigit rest1++readMonthS :: String -> (Day, String)+readMonthS s0+    = (fromGregorian 1 (read month) 1, rest)+    where+      (month,       rest ) = span isDigit . drop 2 $ s0++readDayS :: String -> (Day, String)+readDayS s0+    = (fromGregorian 1 1 (read day), rest)+    where+      (day,         rest ) = span isDigit . drop 3 $ s0++readHourMinSec :: String -> DiffTime+readHourMinSec s+    = fromInteger (60 * (60 * read hours + read minutes))+      ++      fromRational (readDecimal seconds)+    where+      (hours,   (_ :    rest)) = span (/= ':') s+      (minutes, (_ : seconds)) = span (/= ':') rest++readDateTime :: String -> Date+readDateTime s+    = mkDateTime day (readHourMinSec time) (readTimeZone zone)+    where+      (day,  (_ : rest)) = readYearMonthDayS s+      (time,       zone) = span (\ x -> isDigit x || x `elem` ":.") rest+++readDate' :: (String -> (Day, String)) -> String -> Date+readDate' read' s+    = mkDateTime day nullTime (readTimeZone zone)+    where+      (day, zone) = read' s++nullTime :: DiffTime+nullTime = fromInteger 0++nullDay :: Day+nullDay = fromGregorian 1 1 1++-- ----------------------------------------++-- | Reads a decimal from a string++readDecimal :: String -> Rational+readDecimal ('+':s) = readDecimal' s+readDecimal ('-':s) = negate $ readDecimal' s+readDecimal      s  = readDecimal' s++-- | Helper function to read a decimal from a string+readDecimal' :: String -> Rational+readDecimal' s+  | f == 0    = (n % 1)+  | otherwise = (n % 1) + (f % (10 ^ (toInteger $ length fs)))+  where+  (ns, fs') = span (/= '.') s+  fs = drop 1 fs'++  f :: Integer+  f | null fs   = 0+    | otherwise = read fs+  n :: Integer+  n | null ns   = 0+    | otherwise = read ns++-- --------------------+-- the show must go on++showDateTime :: Date -> String+showDateTime (Date d tz)+    = ymd ++ "T" ++ hms ++ showTimeZone tz+    where+      (ymd : hms : _) = words . show $ d++showDate' :: (String -> String) -> Date -> String+showDate' fmt (Date d tz)+    = fmt ymd ++ showTimeZone tz+    where+      (ymd : _) = words . show $ d++dropRev :: Int -> String -> String+dropRev i = reverse . drop i . reverse++showDate :: Date -> String+showDate = showDate' $ id++showGYearMonth :: Date -> String+showGYearMonth = showDate' $ dropRev 3++showGYear :: Date -> String+showGYear = showDate' $ dropRev 6++showGMonthDay :: Date -> String+showGMonthDay = showDate' $ ('-' :) . reverse . take 6 . reverse++showGMonth :: Date -> String+showGMonth = showDate' $ ('-' :) . reverse . take 3 . drop 3 . reverse++showGDay :: Date -> String+showGDay = showDate' $ ('-' :) . ('-' :) . reverse . take 3 . reverse++-- it's+showTime :: Date -> String+showTime (Date d tz)+    = hms ++ showTimeZone tz+    where+      (_ymd : hms : _) = words . show $ d++showTimeZone :: MaybeTimeZone -> String+showTimeZone Nothing+    = ""+showTimeZone (Just s)+    | s == 0    = "Z"+    | s >  0    = '+' : showHourMin s+    | otherwise = '-' : showHourMin (negate s)++showHourMin :: Int -> String+showHourMin s0+    = showDec 2 (s `div` 60) ++ ":" ++ showDec 2 (s `mod` 60)+    where+      s = s0 `div` 60++showDec :: Int -> Int -> String+showDec n = reverse . toStr n+    where+      toStr 0 _ = ""+      toStr l i = show (i `mod` 10) ++ toStr (l-1) (i `div` 10)++-- ------------------------------------------------------------++-- | Creates a regex from a string+rex :: String -> Regex+rex regex+  | isZero ex = error $ "syntax error in regexp " ++ show regex ++ "."+  | otherwise = ex+  where+  ex = parseRegex regex++-- ------------------------------------------------------------
+ src/Hunt/Index/Schema/Normalize/Int.hs view
@@ -0,0 +1,93 @@+{-# LANGUAGE OverloadedStrings #-}++-- ----------------------------------------------------------------------------+{- |+  Normalization and validation for integer.+-}+-- ----------------------------------------------------------------------------++module Hunt.Index.Schema.Normalize.Int+  ( normalizeToText, denormalizeFromText+  , normalizeToInt, denormalizeFromInt+  , isInt, integer+  )+where++import           Data.Text                     (Text)+import qualified Data.Text                     as T++import           Control.Applicative           hiding ((<|>))+import           Hunt.Common.BasicTypes+import           Text.ParserCombinators.Parsec++-- ------------------------------------------------------------+-- Normalize Int to actual Int+-- ------------------------------------------------------------++-- | Normalize an integer to an 'Int'.+normalizeToInt :: Text -> Int+normalizeToInt = getInt++-- | Denormalize an integer.+denormalizeFromInt :: Int -> Text+denormalizeFromInt = T.pack . show++-- ------------------------------------------------------------+-- Normalize Int as Text+-- ------------------------------------------------------------++-- | Normalize a /valid/ integer to a 'Text' representation preserving ordering.+normalizeToText' :: Word -> Word+normalizeToText' i+  = T.concat [pfx, zeros, nr]+  where+  (pfx,nr) = if T.take 1 i == "-"+             then ("0", T.drop 1 i)+             else ("1", i)+  elems    = 20 - T.length nr+  zeros    = T.replicate elems "0"++-- | Normalize an integer to a 'Text' representation preserving ordering.+normalizeToText :: Text -> Text+normalizeToText t+  = if isInt t+    then normalizeToText' t+    else error "normalizeToText: invalid input"++-- | Denormalize a value transformed with 'normalize'.+denormalizeFromText :: Text -> Text+denormalizeFromText i+  = sign raw+  where+  sign = if T.take 1 i == "1" then id else ('-' `T.cons`)+  raw  = T.dropWhile (== '0') $ T.drop 1 i++-- ------------------------------------------------------------+-- Validate int+-- ------------------------------------------------------------++-- | Parse a /valid/ integer.+--+--   /Note/: This will fail on invalid integers.+getInt :: Text -> Int+getInt int = case parse integer "" $ T.unpack int of+  Right pint -> pint+  _          -> error "getInt: invalid input"++-- | Validate if the text represents a valid integer.+--   Needs to be within 'Int' range.+isInt :: Text -> Bool+isInt int = case parse integer "" $ T.unpack int of+  Right pint -> int == (T.pack . show $ pint)+  _          -> False++-- | Parse a simple integer representation.+integer :: Parser Int+integer = rd <$> (plus <|> minus <|> number)+  where+  rd     = read :: String -> Int+  plus   = char '+' *> number+  minus  = (:) <$> char '-' <*> number+  number = many1 digit++-- ------------------------------------------------------------
+ src/Hunt/Index/Schema/Normalize/Position.hs view
@@ -0,0 +1,199 @@+-- ----------------------------------------------------------------------------+{- |+  Normalization and validation for geographic positions.+-}+-- ----------------------------------------------------------------------------++module Hunt.Index.Schema.Normalize.Position+  ( normalize, denormalize+  , isPosition, position+  )+where++import           Numeric++import           Control.Applicative           hiding ((<|>))++import           Data.Text                     (Text)+import qualified Data.Text                     as T+++import           Text.Parsec+import           Text.ParserCombinators.Parsec++import           Hunt.Utility++-- ------------------------------------------------------------+-- validator+-- ------------------------------------------------------------++-- | Checks if value is a valid position.+--   A valid position has a format like "double-double"/"latitude-longitude".+isPosition :: Text -> Bool+isPosition pos = isRight . parse position "" $ T.unpack pos++-- | Parse a coordinate.+position :: Parser (Double, Double)+position = do+  lat  <- latitude+  _ <- char '-'+  long <- longitude+  return (lat,long)++-- | Parse a latitude value.+latitude :: Parser Double+latitude = do+  pos <- double+  if pos > -90 && pos < 90+    then return pos+    else fail "latitude out of bounds"++-- | Parse a longitude value.+longitude :: Parser Double+longitude = do+  pos <- double+  if pos > -180 && pos < 180+    then return pos+    else fail "longitude out of bounds"++-- ------------------------------------------------------------+-- normalizer+-- ------------------------------------------------------------++-- | Normalizes valid position+--   A valid position has a format like "double-double"/"latitude-longitude".+normalize :: Text -> Text+normalize pos+  = case parse position "" $ T.unpack pos of+     Right (la,lo) -> T.pack $ intersectPos (format la) (format lo)+     Left _        -> error "normalize positon: invalid position"+  where+  format   :: Double -> String+  format v = dec2bin $ round (v * 10000000)+++-- | Denormalizes internal position into valid position.+--   A valid position has a format like "double-double"/"latitude-longitude".+denormalize :: Text -> Text+denormalize pos+  = T.pack . show' d1 . ('-':) . show' d2 $ ""+  where+  (d1,d2) = ( fromIntegral i1 / 10000000+            , fromIntegral i2 / 10000000+            ) :: (Double,Double)+  (i1,i2) = ( bin2dec odds+            , bin2dec evens+            )++  (odds,evens) = oddsAndEvens sPos++  oddsAndEvens o = case o of+    (x1:x2:xs) -> let (y1,y2) = oddsAndEvens xs in (x1:y1,x2:y2)+    (x1:xs)    -> let (y1,y2) = oddsAndEvens xs in (x1:y1,y2)+    []         -> ([],[])++  sPos = T.unpack pos++  show' = showFFloat (Just 7)++-- ------------------------------------------------------------+-- normalizer helper+-- ------------------------------------------------------------++-- | Decimal representation of a binary encoded string.+--+--   /Note/: The input needs to be valid.+--           @length input >= 2 && all (`elem` "01") input@+bin2dec :: String -> Int+bin2dec (s:i)+  = sign dec+    where+      dec = fst . head . readInt 2 isbc c2b $ i+      sign = if s == '0' then negate else id+bin2dec []+  = error "bin2dec: empty String"++-- | Convert Integer to Binary and normalize result+--   with leading zeros to a length of 32 characters.+--   The first character is the sign: 0 -> negative.+--+--   /Note/: The input needs to be valid.+dec2bin :: Integer -> String+dec2bin i = sign . zeros . bin $ ""+  where+  sign, zeros, bin :: ShowS+  (sign,n) = if i < 0 then (('0':),-i) else (('1':),i)+  bin      = showIntAtBase 2 b2c n+  zeros s  = foldr (\ _ xs -> '0' : xs) s [1..elems]+    where elems = 31 - length s++-- | Merge two lists starting with an element of the first list, then one+--   of the second list and so on.+--   Works like @concat . zipWith (\a b -> a:b:[])@, but pads with zeros.+intersectPos :: String -> String -> String+intersectPos (x:xs) (y:ys) = x   : y   : intersectPos xs ys+intersectPos (x:xs) []     = x   : '0' : intersectPos xs []+intersectPos []     (y:ys) = '0' : y   : intersectPos [] ys+intersectPos _      _      = []++{-+-- ShowS version with accumulator - unnecessary+intersectPos :: String -> String -> ShowS+intersectPos la lo = foldPos' la lo id+    where+    foldPos' (x:xs) (y:ys) a = foldPos' xs ys (\s -> a (x   : y   : s))+    foldPos' (x:xs) []     a = foldPos' xs "" (\s -> a (x   : '0' : s))+    foldPos' []     (y:ys) a = foldPos' "" ys (\s -> a ('0' : y   : s))+    foldPos' []    []      a = a+-}++-- ------------------------------------------------------------+-- Int to binary and vice versa+-- ------------------------------------------------------------++-- | Binary integer to character.+b2c :: Int -> Char+b2c i = case i of+  0 -> '0'+  1 -> '1'+  _ -> error "b2c i with i `notElem` [0,1]"++-- | Character to binary integer.+c2b :: Char -> Int+c2b o = case o of+  '0' -> 0+  '1' -> 1+  _ -> error "c2b c with c `notElem` \"01\""++-- | Is the character a binary number.+isbc :: Char -> Bool+isbc = (`elem` "01")++-- ------------------------------------------------------------+-- parser helper+-- ------------------------------------------------------------++number :: Parser String+number = many1 digit++plus :: Parser String+plus = char '+' *> number++minus :: Parser String+minus = char '-' <:> number++integer :: Parser String+integer = plus <|> minus <|> number++double :: Parser Double+double = fmap rd $ integer <++> decimal+    where rd      = read :: String -> Double+          decimal = option "" $ char '.' <:> number++(<++>) :: Applicative f => f [a] -> f [a] -> f [a]+(<++>) a b = (++) <$> a <*> b++(<:>) :: Applicative f => f a -> f [a] -> f [a]+(<:>) a b = (:) <$> a <*> b++-- ------------------------------------------------------------
+ src/Hunt/Interpreter.hs view
@@ -0,0 +1,820 @@+{-# LANGUAGE ConstraintKinds            #-}+{-# LANGUAGE FlexibleInstances          #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses      #-}+{-# LANGUAGE OverloadedStrings          #-}+{-# LANGUAGE RankNTypes                 #-}+{-# LANGUAGE TypeFamilies               #-}++-- ----------------------------------------------------------------------------+{- |+  The interpreter to evaluate 'Command's.+-}+-- ----------------------------------------------------------------------------++module Hunt.Interpreter+  ( -- * Initialization+    initHunt+    -- * Running Commands+  , runCmd+  , execCmd+  , runHunt+    -- * Types+  , Hunt+  , HuntT (..)+  , HuntEnv (..)+  , DefHuntEnv+  )+where++import           Control.Applicative+import           Control.Arrow                 (second)+import           Control.Concurrent.XMVar+import           Control.Monad.Error+import           Control.Monad.Reader++import           Data.Aeson                    (ToJSON (..), object, (.=))+import           Data.Binary                   (Binary, encodeFile)+import qualified Data.ByteString.Lazy          as BL+import           Data.Default+import qualified Data.List                     as L+import qualified Data.Map                      as M+import           Data.Set                      (Set)+import qualified Data.Set                      as S+import           Data.Text                     (Text)+import qualified Data.Text                     as T+import qualified Data.Traversable              as TV++import           Hunt.Common+import           Hunt.Common.ApiDocument       as ApiDoc+import qualified Hunt.Common.DocDesc           as DocDesc+import qualified Hunt.Common.DocIdSet          as DocIdSet+import           Hunt.ContextIndex             (ContextIndex (..), ContextMap)+import qualified Hunt.ContextIndex             as CIx+import           Hunt.DocTable                 (DValue, DocTable)+import qualified Hunt.DocTable                 as DocTable+import           Hunt.DocTable.HashedDocTable+import qualified Hunt.Index                    as Ix+import           Hunt.Index.IndexImpl          (IndexImpl (..), mkIndex)+import           Hunt.Index.Schema.Analyze+import           Hunt.Interpreter.BasicCommand+import           Hunt.Interpreter.Command      (Command)+import           Hunt.Interpreter.Command      hiding (Command (..))+import           Hunt.Query.Intermediate       (ScoredDocs, ScoredWords,+                                                UnScoredDocs, toDocIdSet,+                                                toDocsResult, RankedDoc(..),+                                                toDocumentResultPage,+                                                toWordsResult)+import           Hunt.Query.Language.Grammar+import           Hunt.Query.Processor          (ProcessConfig (..),+                                                initProcessor,+                                                processQueryScoredDocs,+                                                processQueryScoredWords,+                                                processQueryUnScoredDocs)+import           Hunt.Query.Ranking+import           Hunt.Utility                  (showText)+import           Hunt.Utility.Log++import           System.IO.Error               (isAlreadyInUseError,+                                                isDoesNotExistError,+                                                isFullError, isPermissionError,+                                                tryIOError)+import qualified System.Log.Logger             as Log++import           GHC.Stats                     (getGCStats, getGCStatsEnabled)+import           GHC.Stats.Json                ()++{- OLD+import           Data.Function                 (on)+import           Data.List                     (sortBy)+import qualified Hunt.Common.DocIdMap          as DocIdMap+import           Hunt.Common.Document          (DocumentWrapper, setScore, unwrap)+import           Hunt.Utility+import           Hunt.Query.Result             (DocInfo (..), Result (..), WordInfo (..),+                                                WordInfoAndHits (..))+import           Hunt.Query.Processor          (processQuery,+                                                processQueryDocIds)+-- -}+-- ------------------------------------------------------------+--+-- the semantic domains (datatypes for interpretation)+--+-- HuntEnv, Index, ...++-- ------------------------------------------------------------+--+-- the indexer used in the interpreter+-- this should be a generic interpreter in the end+-- but right now its okay to have the indexer+-- replaceable by a type declaration++-- ------------------------------------------------------------+-- Logging++-- TODO: manage exports++-- | Name of the module for logging purposes.+modName :: String+modName = "Hunt.Interpreter"++-- | Log a message at 'DEBUG' priority.+debugM :: MonadIO m => String -> m ()+debugM = liftIO . Log.debugM modName+{-+-- | Log a message at 'WARNING' priority.+warningM :: MonadIO m => String -> m ()+warningM = liftIO . Log.warningM modName+-}+-- | Log a message at 'ERROR' priority.+errorM :: MonadIO m => String -> m ()+errorM = liftIO . Log.errorM modName++{-+-- | Log formated values that get inserted into a context+debugContext :: Context -> Words -> IO ()+debugContext c ws = debugM $ concat ["insert in ", T.unpack c, show . M.toList $ fromMaybe M.empty $ M.lookup c ws]+-- -}++-- ------------------------------------------------------------++-- | The Hunt state and environment.+--   Initialize with default values with 'initHunt'.+data HuntEnv dt = HuntEnv+  { -- | The context index (indexes, document table and schema).+    --   Stored in an 'XMVar' so that read access is always possible.+    huntIndex       :: DocTable dt => XMVar (ContextIndex dt)+    -- | Ranking configuration.+  , huntRankingCfg  :: RankConfig (DValue dt)+    -- | Available context types.+  , huntTypes       :: ContextTypes+    -- | Available normalizers.+  , huntNormalizers :: [CNormalizer]+    -- | Query processor configuration.+  , huntQueryCfg    :: ProcessConfig+  }++-- | Default Hunt environment type.+type DefHuntEnv = HuntEnv (Documents Document)++-- | Initialize the Hunt environment with default values.+initHunt :: DocTable dt => IO (HuntEnv dt)+initHunt = initHuntEnv CIx.empty defaultRankConfig contextTypes normalizers def++-- | Default context types.+contextTypes :: ContextTypes+contextTypes = [ctText, ctInt, ctDate, ctPosition, ctTextSimple, ctPositionRTree]++-- | Default normalizers.+normalizers :: [CNormalizer]+normalizers = [cnUpperCase, cnLowerCase, cnZeroFill]++-- | Initialize the Hunt environment.+initHuntEnv :: DocTable dt+           => ContextIndex dt+           -> RankConfig (DValue dt)+           -> ContextTypes+           -> [CNormalizer]+           -> ProcessConfig+           -> IO (HuntEnv dt)+initHuntEnv ixx rnk opt ns qc = do+  ixref <- newXMVar ixx+  return $ HuntEnv ixref rnk opt ns qc++-- ------------------------------------------------------------+-- Command evaluation monad+-- ------------------------------------------------------------++-- | The Hunt transformer monad. Allows a custom monad to be embedded to combine with other DSLs.++newtype HuntT dt m a+    = HuntT { runHuntT :: ReaderT (HuntEnv dt) (ErrorT CmdError m) a }+      deriving+      (Applicative, Monad, MonadIO, Functor, MonadReader (HuntEnv dt), MonadError CmdError)++instance MonadTrans (HuntT dt) where+  lift = HuntT . lift . lift++-- | The Hunt monad on 'IO'.+type Hunt dt = HuntT dt IO++-- ------------------------------------------------------------++-- | Run the Hunt monad with the supplied environment/state.++runHunt :: DocTable dt => HuntT dt m a -> HuntEnv dt -> m (Either CmdError a)+runHunt env = runErrorT . runReaderT (runHuntT env)++-- | Run the command the supplied environment/state.+runCmd :: (DocTable dt, Binary dt) => HuntEnv dt -> Command -> IO (Either CmdError CmdResult)+runCmd env cmd+  = runErrorT . runReaderT (runHuntT . execCmd $ cmd) $ env++-- | Get the context index.+askIx :: DocTable dt => Hunt dt (ContextIndex dt)+askIx = do+  ref <- asks huntIndex+  liftIO $ readXMVar ref++-- FIXME: io exception-safe?+-- | Modify the context index.+modIx :: DocTable dt+      => (ContextIndex dt -> Hunt dt (ContextIndex dt, a)) -> Hunt dt a+modIx f = do+  ref <- asks huntIndex+  ix <- liftIO $ takeXMVarWrite ref+  (i',a) <- f ix `catchError` putBack ref ix+  liftIO $ putXMVarWrite ref i'+  return a+  where+  putBack ref i e = do+    liftIO $ putXMVarWrite ref i+    throwError e++-- | Modify the context index.+--   Locks the index for reads and writes to prevent excessive memory usage.+--+--   /Note/: This does not fix the memory issues on load entirely because the old index might+--   still be referenced by a concurrent read operation.+modIxLocked :: DocTable dt+            => (ContextIndex dt -> Hunt dt (ContextIndex dt, a)) -> Hunt dt a+modIxLocked f = do+  ref <- asks huntIndex+  ix <- liftIO $ takeXMVarLock ref+  (i',a) <- f ix `catchError` putBack ref ix+  liftIO $ putXMVarLock ref i'+  return a+  where+  putBack ref i e = do+    liftIO $ putXMVarLock ref i+    throwError e++-- | Do something with the context index.+withIx :: DocTable dt => (ContextIndex dt -> Hunt dt a) -> Hunt dt a+withIx f+  = askIx >>= f++-- | Get the type of a context.+askType :: DocTable dt => Text -> Hunt dt ContextType+askType cn = do+  ts <- asks huntTypes+  case L.find (\t -> cn == ctName t) ts of+    Just t -> return t+    _      -> throwResError 410 ("used unavailable context type: " `T.append` cn)++-- | Get the normalizer of a context.+askNormalizer :: DocTable dt => Text -> Hunt dt CNormalizer+askNormalizer cn = do+  ts <- asks huntNormalizers+  case L.find (\t -> cn == cnName t) ts of+    Just t -> return t+    _      -> throwResError 410 ("used unavailable normalizer: " `T.append` cn)++-- | Get the index.+askIndex :: DocTable dt => Text -> Hunt dt IndexImpl+askIndex cn = ctIxImpl <$> askType cn++-- | Throw an error in the Hunt monad.+throwResError :: DocTable dt => Int -> Text -> Hunt dt a+throwResError n msg+    = do errorM $ unwords [show n, T.unpack msg]+         throwError $ ResError n msg++-- ------------------------------------------------------------++-- | Execute the command in the Hunt monad.+execCmd :: (Binary dt, DocTable dt) => Command -> Hunt dt CmdResult+execCmd+  = execBasicCmd . toBasicCommand++-- XXX: kind of obsolete now+-- | Execute the \"low-level\" command in the Hunt monad.++execBasicCmd :: (Binary dt, DocTable dt) => BasicCommand -> Hunt dt CmdResult+execBasicCmd cmd@(InsertList _) = do+  debugM $ "Exec: InsertList [..]"+  execCmd' cmd++execBasicCmd cmd = do+  debugM $ "Exec: " ++ logShow cmd+  execCmd' cmd+++-- | Use 'execBasicCmd'.+--+--   Dispatches basic commands to corresponding functions.++execCmd' :: (Binary dt, DocTable dt) => BasicCommand -> Hunt dt CmdResult+execCmd' (Search q offset mx wg fields)+  = withIx $ execSearch q offset mx wg fields++execCmd' (Completion q mx)+    = withIx $ execCompletion q mx+--  = withIx $ execSearch' (wrapCompletion mx) q++execCmd' (Select q)+  = withIx $ execSelect q++execCmd' (Sequence cs)+  = execSequence cs++execCmd' NOOP+  = return ResOK  -- keep alive test++execCmd' (Status sc)+  = execStatus sc++execCmd' (InsertList docs)+  = modIx $ execInsertList docs++execCmd' (Update doc)+  = modIx $ execUpdate doc++execCmd' (DeleteDocs uris)+  = modIx $ execDeleteDocs uris++execCmd' (DeleteByQuery q)+  = modIx $ execDeleteByQuery q++execCmd' (StoreIx filename)+  = withIx $ execStore filename++execCmd' (LoadIx filename)+  = execLoad filename++execCmd' (InsertContext cx ct)+  = modIx $ execInsertContext cx ct++execCmd' (DeleteContext cx)+  = modIx $ execDeleteContext cx++-- ------------------------------------------------------------++-- | Execute a sequence of commands.+--   The sequence will be aborted if a command fails, but the previous commands will be permanent.++execSequence :: (DocTable dt, Binary dt)=> [BasicCommand] -> Hunt dt CmdResult+execSequence []       = execBasicCmd NOOP+execSequence [c]      = execBasicCmd c+execSequence (c : cs) = execBasicCmd c >> execSequence cs++-- | Insert a context with associated schema.+execInsertContext :: DocTable dt+                  => Context+                  -> ContextSchema+                  -> ContextIndex dt+                  -> Hunt dt (ContextIndex dt, CmdResult)+execInsertContext cx ct ixx+  = do+    -- check if context already exists+    contextExists        <- CIx.hasContextM cx ixx+    unless' (not contextExists)+           409 $ "context already exists: " `T.append` cx++    -- check if type exists in this interpreter instance+    cType                <- askType . ctName . cxType  $ ct+    impl                 <- askIndex . ctName . cxType $ ct+    norms                <- mapM (askNormalizer . cnName) $ cxNormalizer ct++    -- create new index instance and insert it with context+    return $ ( CIx.insertContext cx (newIx impl) (newSchema cType norms) ixx+             , ResOK+             )+  where+  newIx :: IndexImpl -> IndexImpl+  newIx (IndexImpl i) = mkIndex $ Ix.empty `asTypeOf` i+  newSchema cType norms= (ct { cxType = cType, cxNormalizer = norms })++-- | Deletes the context and the schema associated with it.+execDeleteContext :: DocTable dt+                  => Context+                  -> ContextIndex dt+                  -> Hunt dt (ContextIndex dt, CmdResult)+execDeleteContext cx ixx+  = return (CIx.deleteContext cx ixx, ResOK)++-- | Inserts an 'ApiDocument' into the index.+--+-- /Note/: All contexts mentioned in the 'ApiDocument' need to exist.+-- Documents/URIs must not exist.++execInsertList :: DocTable dt+                => [ApiDocument] -> ContextIndex dt -> Hunt dt (ContextIndex dt, CmdResult)+execInsertList docs ixx@(ContextIndex ix _dt)+    = do -- existence check for all referenced contexts in all docs+         checkContextsExistence contexts ixx++         -- check no duplicates in docs+         checkDuplicates duplicates++         -- apidoc should not exist+         mapM_ (flip (checkApiDocExistence False) ixx) docs++         -- all checks done, do the real work+         ixx' <- lift $ CIx.insertList docsAndWords ixx+         return (ixx', ResOK)+    where+      -- compute all contexts in all docs+      contexts+          = M.keys+            . M.unions+            . L.map (M.map (const ()) . adIndex)+            $ docs++      -- convert ApiDocuments to Documents, delete null values,+      -- and break index data into words by applying the scanner+      -- given by the schema spec for the appropriate contexts+      docsAndWords+          = L.map ( (\ (d, _dw, ws) -> (d, ws))+                    . toDocAndWords (CIx.mapToSchema ix)+                    . (\ d -> d {adDescr = DocDesc.deleteNull $ adDescr d})+                  )+            $ docs++      -- compute duplicate URIs by building a frequency table+      -- and looking for entries with counts @> 1@+      duplicates+          = M.keys+            . M.filter (> 1)+            . L.foldl ins M.empty+            . L.map adUri+            $ docs+            where+              ins m k = M.insertWith (+) k (1::Int) m++      -- check and throw error concerning duplicate URIs+      checkDuplicates xs+          = unless' (L.null xs)+              409 $ "duplicate URIs found in document list:" <> showText xs+++-- | Updates an 'ApiDocument'.+--+-- /Note/: All contexts mentioned in the 'ApiDocument' need to exist.+-- Documents/URIs need to exist.++execUpdate :: DocTable dt+           => ApiDocument -> ContextIndex dt -> Hunt dt (ContextIndex dt, CmdResult)++execUpdate doc ixx@(ContextIndex ix dt)+    = do checkContextsExistence contexts ixx+         docIdM <- lift $ DocTable.lookupByURI (uri docs) dt+         case docIdM of+           Just docId+               -> do ixx' <- lift+                             $ CIx.modifyWithDescription (adWght doc) (desc docs) ws docId ixx+                     return (ixx', ResOK)+           Nothing+               -> throwResError 409 $ "document for update not found: " `T.append` uri docs+    where+      contexts+          = M.keys $ adIndex doc+      (docs, _dw, ws)+          = toDocAndWords (CIx.mapToSchema ix) doc+++-- | Test whether the contexts are present and otherwise throw an error.++checkContextsExistence :: DocTable dt+                       => [Context] -> ContextIndex dt -> Hunt dt ()+checkContextsExistence cs ixx+    = do ixxContexts        <- S.fromList <$> CIx.contextsM ixx+         let docContexts     = S.fromList cs+         let invalidContexts = S.difference docContexts ixxContexts+         unless' (S.null invalidContexts)+           409 ( "mentioned context(s) are not present: "+                 <> (showText . S.toList $ invalidContexts)+               )++-- | Test whether the document (URI) already exists+-- (or does not exist depending on the first argument).+--+-- Throws an error if it exists and the first argument is @False@ and vice versa.++checkApiDocExistence :: DocTable dt+                     => Bool -> ApiDocument -> ContextIndex dt -> Hunt dt ()+checkApiDocExistence switch apidoc ixx+    = do let u = adUri apidoc+         mem <- CIx.member u ixx+         unless' (switch == mem)+           409 ( ( if mem+                   then "document already exists: "+                   else "document does not exist: "+                 ) <> u+               )++execSearch :: DocTable dt =>+              Query ->+              Int -> Int ->+              Bool -> Maybe [Text] ->+              ContextIndex dt ->+              Hunt dt CmdResult++execSearch q offset mx wg fields (ContextIndex ix dt)+    = do debugM ("execSearch: " ++ show q)+         cfg    <- asks huntQueryCfg+         scDocs <- liftHunt $+                   runQueryScoredDocsM ix cfg q+         formatPage <$> toDocsResult dt scDocs+    where+      formatPage ds+          = ResSearch $+            LimitedResult+            { lrResult = ds'+            , lrOffset = offset+            , lrMax    = mx+            , lrCount  = length ds+            }+          where+            ds' = map (mkSelect wg fields)+                  . toDocumentResultPage offset mx+                  $ ds++execCompletion :: DocTable dt =>+                  Query ->+                  Int ->+                  ContextIndex dt -> Hunt dt CmdResult+execCompletion q mx (ContextIndex ix _dt)+    = do debugM ("execCompletion: " ++ show q)+         cfg     <- asks huntQueryCfg+         scWords <- liftHunt $+                    runQueryScoredWordsM ix cfg q+         return $ ResSuggestion $ toWordsResult mx scWords+++execSelect :: DocTable dt => Query -> ContextIndex dt -> Hunt dt CmdResult+execSelect q (ContextIndex ix dt)+    = do debugM ("execSelect: " ++ show q)+         res <- liftHunt $ runQueryUnScoredDocsM ix queryConfigDocIds q+         dt' <- DocTable.restrict (toDocIdSet res) dt+         djs <- DocTable.toJSON'DocTable dt'+         return $ ResGeneric djs++-- | Build a selection function for choosing,+-- which parts of a document are contained in the result.+--+-- The 1. param determines, whether the weight of the document is included in the result.+-- The 2. is the list of the description keys, if @Nothing@ is given the complete desc is included.++mkSelect :: Bool -> Maybe [Text] -> (RankedDoc -> RankedDoc)+mkSelect withWeight fields+    = mkSelW withWeight . mkSelF fields+      where+        mkSelW True      = id+        mkSelW False     = RD . second (\d -> d { wght = 1.0 }) . unRD++        mkSelF Nothing   = id+        mkSelF (Just fs) = RD . second (\d -> d {desc = DocDesc.restrict fs (desc d)}) . unRD+++-- | Delete a set of documents.++execDeleteDocs :: DocTable dt => Set URI -> ContextIndex dt -> Hunt dt (ContextIndex dt, CmdResult)+execDeleteDocs d ix+    = do ix' <- lift $ CIx.deleteDocsByURI d ix+         return (ix', ResOK)++-- | Delete all documents matching the query.++execDeleteByQuery :: DocTable dt => Query -> ContextIndex dt -> Hunt dt (ContextIndex dt, CmdResult)+execDeleteByQuery q ixx@(ContextIndex ix _dt)+    = do debugM ("execDeleteByQuery: " ++ show q)+         ds <- toDocIdSet <$>+               (liftHunt $ runQueryUnScoredDocsM ix queryConfigDocIds q)+         if DocIdSet.null ds+           then do debugM "DeleteByQuery: Query result set empty"+                   return (ixx, ResOK)+           else do debugM $ "DeleteByQuery: " ++ show ds+                   ix' <- lift $ CIx.delete ds ixx+                   return (ix', ResOK)++-- ------------------------------------------------------------++-- TODO: catch exceptions:+--       http://hackage.haskell.org/package/base/docs/System-IO.html#v:openFile++-- | Serialize a value to a file.+execStore :: (Binary a, DocTable dt) =>+             FilePath -> a -> Hunt dt CmdResult+execStore filename x = do+  res <- liftIO . tryIOError $ encodeFile filename x+  case res of+      Left  e+          | isAlreadyInUseError e -> throwResError 409 $ "Cannot store index: file is already in use"+          | isPermissionError   e -> throwResError 403 $ "Cannot store index: no access permission to file"+          | isFullError         e -> throwResError 500 $ "Cannot store index: device is full"+          | otherwise             -> throwResError 500 $ showText $ e+      Right _ -> return ResOK++-- TODO: XMVar functions probably not suited for this, locking for load reasonable++-- | Load a context index.+--   The deserialization is more specific because of the existentially typed index.++--   This operation locks the index, otherwise two potentially large indexes could be present at a+--   time. This is still possible if a read operation lasts as long as loading the index.++execLoad :: (Binary dt, DocTable dt) => FilePath -> Hunt dt CmdResult+execLoad filename = do+  ts <- asks huntTypes+  let ix = map ctIxImpl ts+  modIxLocked $ \_ -> do+    ixh@(ContextIndex ixs _) <- decodeFile' ix filename+    ls <- TV.mapM reloadSchema $ CIx.cxMap ixs+    return (ixh{ ciIndex = CIx.mkContextMap ls }, ResOK)+  where+  decodeFile' ts f = do+    res <- liftIO . tryIOError $ CIx.decodeCxIx ts <$> BL.readFile f+    case res of+      Left  e+          | isAlreadyInUseError e -> throwResError 409 $ "Cannot load index: file already in use"+          | isDoesNotExistError e -> throwResError 404 $ "Cannot load index: file does not exist"+          | isPermissionError   e -> throwResError 403 $ "Cannot load index: no access permission to file"+          | otherwise             -> throwResError 500 $ showText e+      Right r -> return r++  reloadSchema (s,ix) = do+    cxt <- askType . ctName . cxType $ s+    ns  <- mapM (askNormalizer . cnName) (cxNormalizer s)+    return $ ( s { cxType       = cxt+                 , cxNormalizer = ns+                 }+             , ix )++-- ------------------------------------------------------------++-- the query interpreters++-- for scored docs (DocIdMap with scores)++runQueryScoredDocsM :: ContextMap+                    -> ProcessConfig+                    -> Query+                    -> IO (Either CmdError ScoredDocs)+runQueryScoredDocsM ix cfg q+    = processQueryScoredDocs st q+      where+        st = initProcessor cfg ix+++-- for unscored docs (DocIdSet), usually called with 'queryConfigDocIds'++runQueryUnScoredDocsM :: ContextMap+                    -> ProcessConfig+                    -> Query+                    -> IO (Either CmdError UnScoredDocs)+runQueryUnScoredDocsM ix cfg q+    = processQueryUnScoredDocs st q+      where+        st = initProcessor cfg ix+++-- for scored docs (DocIdMap with scores++runQueryScoredWordsM :: ContextMap+                     -> ProcessConfig+                     -> Query+                     -> IO (Either CmdError ScoredWords)+runQueryScoredWordsM ix cfg q+    = processQueryScoredWords st q+      where+        st = initProcessor cfg ix+++-- | Query config for \"delete by query\".++queryConfigDocIds :: ProcessConfig+queryConfigDocIds = ProcessConfig def True 0 0++liftHunt :: IO (Either CmdError r) -> Hunt dt r+liftHunt cmd+    = lift cmd >>= either throwError return++-- ------------------------------------------------------------++-- | Get status information about the server\/index, e.g. garbage collection statistics.+execStatus :: DocTable dt => StatusCmd -> Hunt dt CmdResult+execStatus StatusGC+  = do+    statsEnabled <- liftIO getGCStatsEnabled+    if statsEnabled+      then (ResGeneric . toJSON) <$>+           liftIO getGCStats+      else throwResError 501 ("GC stats not enabled. Use `+RTS -T -RTS' to enable them." :: Text)++execStatus StatusDocTable+    = withIx dumpDocTable+      where+        dumpDocTable (ContextIndex _ix dt)+            = ResGeneric <$>+              DocTable.toJSON'DocTable dt++execStatus (StatusContext cx)+    = withIx dumpContext+      where+        dumpContext (ContextIndex ix _dt)+            = (ResGeneric . object . map (uncurry (.=))) <$>+              CIx.lookupAllWithCx cx ix++execStatus (StatusIndex {- context -})+  = withIx _dumpIndex+    where+      -- context = "type"+      _dumpIndex (ContextIndex _ix _dt)+          = throwResError 501 $ "status of Index not yet implemented"++-- ------------------------------------------------------------++-- | Throw an error unless the first argument is @True@, and otherwise do nothing.+unless' :: DocTable dt+       => Bool -> Int -> Text -> Hunt dt ()+unless' b code text = unless b $ throwResError code text++-- ------------------------------------------------------------++{- OLD+askDocTable :: DocTable dt => Hunt dt dt+askDocTable = askIx >>= return . ciDocs++-- | Get the context weightings.+askContextsWeights :: DocTable dt => Hunt dt ContextWeights -- (M.Map Context Weight)+askContextsWeights+  = withIx (\(ContextIndex _ _ schema) -> return $ M.map cxWeight schema)+-- -}+++{- OLD+-- | Run a query.+runQueryM       :: DocTable dt+                => ContextMap Occurrences+                -> Schema+                -> ProcessConfig+                -> dt+                -> Query+                -> IO (Either CmdError (Result (DValue dt)))+runQueryM ix s cfg dt q = processQuery st dt q+  where+  st = initProcessor cfg ix s++runQueryDocIdsM :: ContextMap Occurrences+                -> Schema+                -> Query+                -> IO (Either CmdError DocIdSet)+runQueryDocIdsM ix s q+    = processQueryDocIds st q+      where+        st = initProcessor queryConfigDocIds ix s+-- -}+-- ------------------------------------------------------------++{- old stuff, but still used in completions++-- | Search the index.+--   Requires a result transformation function, e.g. 'wrapSearch' or 'wrapCompletion'.+execSearch' :: (DocTable dt, e ~ DValue dt)+            => (Result e -> CmdResult)+            -> Query+            -> ContextIndex dt+            -> Hunt dt CmdResult+execSearch' f q (ContextIndex ix dt s)+  = do+    cfg <- asks huntQueryCfg+    r   <- lift $ runQueryM ix s cfg dt q+    rc  <- asks huntRankingCfg+    cw  <- askContextsWeights+    case r of+      Left  err -> throwError err+      Right res -> do -- debugM ("doc  ranking: " ++ show (docHits  res))+                      -- debugM ("word ranking: " ++ show (wordHits res))+                      res' <- rank rc dt cw $ res+                      -- debugM ("doc  result : " ++ show (docHits  res'))+                      -- debugM ("word result : " ++ show (wordHits res'))+                      return (f res')++-- FIXME: signature to result+-- | Wrap the query result for search.+wrapSearch :: (DocumentWrapper e) => (Document -> Document) -> Int -> Int -> Result e -> CmdResult+wrapSearch select offset mx+  = ResSearch+    . mkLimitedResult offset mx+--    . map fst -- remove score from result+    . map (uncurry (flip setScore))+    . map (first select)+    . sortBy (descending `on` snd) -- sort by score+    . map (\(_did, (di, _dch)) -> (unwrap . document $ di, docScore di))+    . DocIdMap.toList+    . docHits++-- | Wrap the query result for auto-completion.+wrapCompletion :: Int -> Result e -> CmdResult+wrapCompletion mx+  = ResCompletion+    . take mx+    . map (\(word,_score,terms') -> (word, terms')) -- remove score from result+    . sortBy (descending `on` (\(_,score',_) -> score')) -- sort by score+    . map (\(c, (WIH wi _wch)) -> (c, wordScore wi, terms wi))+    . M.toList+    . wordHits++-- -}++-- ------------------------------------------------------------
+ src/Hunt/Interpreter/BasicCommand.hs view
@@ -0,0 +1,117 @@+{-# LANGUAGE OverloadedStrings #-}++-- ----------------------------------------------------------------------------+{- |+  Basic \"low-level\" commands that are directly interpreted.++  "Hunt.Interpreter.Command" defines the \"high-level\" commands accepted by the (JSON) API.+-}+-- ----------------------------------------------------------------------------++module Hunt.Interpreter.BasicCommand+  ( BasicCommand (..)+  , StatusCmd (..)+  )+where++import           Control.Applicative+import           Control.Monad               (mzero)++import           Data.Aeson+import qualified Data.Aeson                  as JS (Value (..))+import           Data.Set                    (Set)+import           Data.Text                   (Text)++import           Hunt.Common.ApiDocument+import           Hunt.Common.BasicTypes+import           Hunt.Index.Schema+import           Hunt.Query.Language.Grammar (Query (..))++import           Hunt.Utility.Log++-- ------------------------------------------------------------++-- | The \"low-level\" commands that are actually interpreted.+data BasicCommand+  -- | Search query with pagination.+  = Search        { icQuery    :: Query+                  , icOffsetSR :: Int+                  , icMaxSR    :: Int+                  , icWeight   :: Bool+                  , icFields   :: Maybe [Text]+                  }++  -- | Auto-completion query with a limit.+  | Completion    { icPrefixCR :: Query+                  , icMaxCR    :: Int+                  }++  -- | Raw query without any ranking, scoring and ordering+  | Select        { icQuery :: Query }++  -- | Insert documents.+  | InsertList    { icDocs :: [ApiDocument] }++  -- | Update the document description.+  | Update        { icDoc :: ApiDocument }++  -- | Delete documents by 'URI'.+  | DeleteDocs   { icUris :: Set URI }++  -- | Delete all documents of the query result.+  | DeleteByQuery { icQueryD :: Query }++  -- | Insert a context and the associated schema.+  | InsertContext { icICon   :: Context+                  , icSchema :: ContextSchema+                  }+  -- | Delete a context.+  | DeleteContext { icDCon :: Context }++  -- | Deserialize the index.+  | LoadIx        { icPath :: FilePath }+  -- | Serialize the index.+  | StoreIx       { icPath :: FilePath }++  -- | Query general information about the server/index.+  | Status        { icStatus :: StatusCmd }++  -- | Sequence commands.+  | Sequence      { icCmdSeq :: [BasicCommand] }+  -- | No operation. Can be used in control flow and as an alive test.+  | NOOP+  deriving (Show)++-- | Available status commands.+data StatusCmd+  = StatusGC              -- ^ Garbage collection statistics.+  | StatusDocTable        -- ^ Document table JSON dump.+  | StatusIndex           -- ^ Index JSON dump.+  | StatusContext Context -- ^ Index context dump+    deriving (Show)++-- ------------------------------------------------------------+-- XXX: maybe duplicate StatusCmd to keep this module clean++instance ToJSON StatusCmd where+  toJSON StatusGC          = JS.String "gc"+  toJSON StatusDocTable    = JS.String "doctable"+  toJSON StatusIndex       = JS.String "index"+  toJSON (StatusContext c) = object ["context" .= c] -- JS.String $ "context/" `T.append` c++instance FromJSON StatusCmd where+  parseJSON (Object o)             = StatusContext <$> o .: "context"+  parseJSON (JS.String "gc"      ) = return StatusGC+  parseJSON (JS.String "doctable") = return StatusDocTable+  parseJSON (JS.String "index"   ) = return StatusIndex+  parseJSON _                      = mzero++-- ------------------------------------------------------------++instance LogShow BasicCommand where+  logShow (InsertList docs) = "InsertList " ++ show (map adUri docs)+  logShow (Update doc) = "Update {icDoc = " ++ logShow doc ++ "\", ..}"+  logShow (Sequence _) = "Sequence"+  logShow o = show o++-- ------------------------------------------------------------
+ src/Hunt/Interpreter/Command.hs view
@@ -0,0 +1,321 @@+{-# LANGUAGE OverloadedStrings #-}++-- ----------------------------------------------------------------------------+{- |+  \"High-level\" commands that are accepted by the (JSON) API.++  These commands are translated with 'toBasicCommand' to 'BasicCommand's which can be interpreted.+-}+-- ----------------------------------------------------------------------------++module Hunt.Interpreter.Command+  ( Command (..)+  , StatusCmd (..)+  , CmdResult (..)+  , CmdError (..)+  , CmdRes (..)+  , toBasicCommand+  )+where++import           Control.Applicative+import           Control.Monad                 (mzero)+import           Control.Monad.Error           (Error (..))+import           Control.DeepSeq++import           Data.Aeson+import           Data.List+import qualified Data.Set                      as S+import           Data.Text                     (Text)+import qualified Data.Text                     as T++import           Hunt.Common.ApiDocument+import           Hunt.Common.BasicTypes+import           Hunt.Index.Schema+import           Hunt.Query.Language.Grammar   (Query (..))+import           Hunt.Query.Intermediate       (RankedDoc)++import           Hunt.Interpreter.BasicCommand (BasicCommand, StatusCmd (..))+import qualified Hunt.Interpreter.BasicCommand as Cmd++import           Hunt.Utility.Log++-- ------------------------------------------------------------++-- | The \"high-level\" commands accepted by the 'Interpreter' \/ JSON API.+--   These are translated to 'BasicCommand's.++data Command+  -- | Search query with pagination.+  = Search        { icQuery    :: Query+                  , icOffsetSR :: Int+                  , icMaxSR    :: Int+                  , icWeight   :: Bool+                  , icFields   :: Maybe [Text]+                  }+  -- | Auto-completion query with a limit.+  | Completion    { icPrefixCR :: Query+                  , icMaxCR    :: Int+                  }+  -- | Raw query without any ranking, scoring and ordering+  | Select        { icQuery :: Query }++  -- | Insert a document.+  | Insert        { icDoc :: ApiDocument }++  -- | Update a documents' description.+  | Update        { icDoc :: ApiDocument }++  -- | Delete a documents by 'URI'.+  | Delete        { icUri :: URI }++  -- | Delete all documents of the query result.+  | DeleteByQuery { icQueryD :: Query }++  -- | Insert a context and the associated schema.+  | InsertContext { icIContext :: Context+                  , icSchema   :: ContextSchema+                  }+  -- | Delete a context.+  | DeleteContext { icDContext :: Context }++  -- | Deserialize the index.+  | LoadIx        { icPath :: FilePath }++  -- | Serialize the index.+  | StoreIx       { icPath :: FilePath }++  -- | Query general information about the server/index.+  | Status        { icStatus :: StatusCmd }++  -- | Sequence commands.+  | Sequence      { icCmdSeq :: [Command] }++  -- | No operation. Can be used in control flow and as an alive test.+  | NOOP+  deriving (Show)++-- ------------------------------------------------------------++-- | The result of an interpreted command.++data CmdResult+  -- | The command was processed successfully.+  = ResOK++  -- | The search results.+  | ResSearch       { crRes :: LimitedResult RankedDoc }++  -- | The auto-completion results.+  | ResCompletion   { crWords :: [(Text, [Text])] }++  -- | The simplified completion result++  | ResSuggestion   { crSugg :: [(Text, Score)] }++  -- | A generic JSON result.+  | ResGeneric      { crGen :: Value }+  deriving (Show, Eq)++instance NFData CmdResult where+  rnf (ResSearch r)     = r `seq` ()+  rnf (ResCompletion c) = c `seq` ()+  rnf (ResSuggestion s) = s `seq` ()+  rnf _                 = ()++-- | An error during processing of the command.+--   This includes a error code and a message.++data CmdError+  = ResError+    { ceCode :: Int  -- ^ Error code.+    , ceMsg  :: Text -- ^ Message describing the error.+    } deriving (Show)++instance NFData CmdError where+  rnf (ResError i t) = t `seq` i `seq` ()++-- ------------------------------------------------------------++-- | auxiliary type for parsing JSON CmdResult's of various kinds+--+-- usefull in hunt applications, not used within the hunt server++newtype CmdRes a = CmdRes { unCmdRes :: a }+    deriving (Show)++instance (FromJSON a) => FromJSON (CmdRes a) where+    parseJSON (Object o) = do+      c <- o .: "code"+      case (c :: Int) of+        0 -> CmdRes <$> o .: "msg"+        _ -> mzero+    parseJSON _ = mzero++-- ------------------------------------------------------------++instance LogShow Command where+  logShow (Insert doc) = "Insert {icDoc = " ++ logShow doc ++ "\", ..}"+  logShow (Update doc) = "Update {icDoc = " ++ logShow doc ++ "\", ..}"+  logShow (Sequence _) = "Sequence"+  logShow o = show o++-- ------------------------------------------------------------+-- JSON instances+-- ------------------------------------------------------------++instance ToJSON Command where+  toJSON o = case o of+    Search q ofs mx wght sel+                      -> object . cmd "search"         $+                         [ "query"  .= q+                         , "offset" .= ofs              -- the start of the result list+                         , "max"    .= mx               -- max length of result list+                         ]+                         +++                         ( if wght                      -- doc weight included in result (yes/no)+                           then [ "weight" .= wght ]+                           else []+                         )+                         ++                             -- descr fields included in result+                         maybe [] (\ fs -> [ "fields" .= fs ]) sel++    Completion s mx   -> object . cmd "completion"     $ [ "text"     .= s, "max" .= mx ]+    Select q          -> object . cmd "select"         $ [ "query"    .= q ]+    Insert d          -> object . cmd "insert"         $ [ "document" .= d ]+    Update d          -> object . cmd "update"         $ [ "document" .= d ]+    Delete u          -> object . cmd "delete"         $ [ "uri"      .= u ]+    DeleteByQuery q   -> object . cmd "delete-by-query"$ [ "query"    .= q ]+    InsertContext c s -> object . cmd "insert-context" $ [ "context"  .= c, "schema" .= s ]+    DeleteContext c   -> object . cmd "delete-context" $ [ "context"  .= c ]+    LoadIx  f         -> object . cmd "load"           $ [ "path"     .= f ]+    StoreIx f         -> object . cmd "store"          $ [ "path"     .= f ]+    Status  sc        -> object . cmd "status"         $ [ "status"   .= sc ]+    NOOP              -> object . cmd "noop"           $ []+    Sequence cs       -> toJSON cs+    where+    cmd c = (:) ("cmd" .= (c :: Text))++instance FromJSON Command where+  parseJSON (Object o) = do+    c <- o .: "cmd"+    case (c :: Text) of+      "search"         -> Search        <$> o .:  "query"+                                        <*> o .:? "offset" .!= 0+                                        <*> o .:? "max"    .!= (-1)+                                        <*> o .:? "weight" .!= False+                                        <*> o .:? "fields"++      "completion"     -> Completion    <$> o .: "text"+                                        <*> o .: "max"++      "select"         -> Select        <$> o .: "query"+      "insert"         -> Insert        <$> o .: "document"+      "update"         -> Update        <$> o .: "document"+      "delete"         -> Delete        <$> o .: "uri"+      "delete-by-query"-> DeleteByQuery <$> o .: "query"++      "insert-context" -> InsertContext <$> o .: "context"+                                        <*> o .: "schema"++      "delete-context" -> DeleteContext <$> o .: "context"+      "load"           -> LoadIx        <$> o .: "path"+      "store"          -> StoreIx       <$> o .: "path"+      "noop"           -> return NOOP+      "status"         -> Status        <$> o .: "status"+      _                -> mzero++  parseJSON v          =  Sequence      <$> parseJSON v++instance ToJSON CmdResult where+  toJSON o = case o of+    ResOK           -> object . code 0 $ []+    ResSearch r     -> object . code 0 $ [ "res" .= r ]+    ResCompletion w -> object . code 0 $ [ "res" .= w ]+    ResSuggestion r -> object . code 0 $ [ "res" .= r ]+    ResGeneric v    -> object . code 0 $ [ "res" .= v ]+    where+    code i = (:) ("code" .= (i :: Int))++instance Error CmdError where+  strMsg s = ResError 500 . T.pack $ "internal server error: " ++ s++instance ToJSON CmdError where+  toJSON (ResError c m) = object+    [ "code" .= c+    , "msg"  .= m+    ]++instance FromJSON CmdError where+  parseJSON (Object o) = ResError <$> o .: "code"+                                  <*> o .: "msg"+  parseJSON _ = mzero++-- ------------------------------------------------------------++-- TODO: - flattening of 'Sequence's?++-- | Transform the supported input command into lower level commands which are actually interpreted.+--+--   Transformations:+--+--     - Multiple 'Cmd.Delete's into a single 'DeleteDocs'.+--+--     - Multiple 'Cmd.Insert's into a single or multiple 'InsertList's.+toBasicCommand :: Command -> BasicCommand+toBasicCommand (Sequence cs) = Cmd.Sequence $ opt cs+  where+  opt :: [Command] -> [BasicCommand]+  opt cs' = concatMap optGroup $ groupBy equalHeads cs'+  -- requires the commands to be grouped by constructor+  optGroup :: [Command] -> [BasicCommand]+  -- groups of delete to DeleteDocs+  optGroup cs'@(Delete{}:_)+    = [foldl (\(Cmd.DeleteDocs us) (Delete u)+                -> Cmd.DeleteDocs (S.insert u us)) (Cmd.DeleteDocs S.empty) cs']+  -- groups of Insert to InsertList+  -- TODO: remove constant+  optGroup cs'@(Insert{}:_)+    = [splitBatch maxBound $ foldl (\(Cmd.InsertList us) (Insert u)+                -> Cmd.InsertList (u:us)) (Cmd.InsertList []) cs']+  optGroup cs'@(Sequence{}:_)+    = map toBasicCommand cs'+  optGroup cs'+    = map toBasicCommand cs'+  -- group by constructor+  -- NOTE: add constructors to use in optGroup+  equalHeads :: Command -> Command -> Bool+  equalHeads Delete{}   Delete{}   = True+  equalHeads Insert{}   Insert{}   = True+  equalHeads Sequence{} Sequence{} = True+  equalHeads _ _                   = False++toBasicCommand (Delete u)          = Cmd.DeleteDocs $ S.singleton u+toBasicCommand (DeleteByQuery q)   = Cmd.DeleteByQuery q+toBasicCommand (Search a b c d e)  = Cmd.Search a b c d e+toBasicCommand (Completion a b)    = Cmd.Completion a b+toBasicCommand (Select a)          = Cmd.Select a+toBasicCommand (Insert a)          = Cmd.InsertList [a]+toBasicCommand (Update a)          = Cmd.Update a+toBasicCommand (InsertContext a b) = Cmd.InsertContext a b+toBasicCommand (DeleteContext a)   = Cmd.DeleteContext a+toBasicCommand (LoadIx a)          = Cmd.LoadIx a+toBasicCommand (StoreIx a)         = Cmd.StoreIx a+toBasicCommand (Status a)          = Cmd.Status a+toBasicCommand (NOOP)              = Cmd.NOOP++-- | Splits big batch inserts to smaller ones with at most @n@ elements.+--   This can avoid running out of memory for large 'InsertList's.+splitBatch :: Int -> BasicCommand -> BasicCommand+splitBatch n (Cmd.InsertList xs)+    = Cmd.Sequence $ map Cmd.InsertList $ splitEvery n xs+splitBatch _ cmd+    = cmd++splitEvery :: Int -> [a] -> [[a]]+splitEvery _ [] = []+splitEvery n list = first : splitEvery n rest+  where+  (first,rest) = splitAt n list++-- ------------------------------------------------------------
+ src/Hunt/Query/Fuzzy.hs view
@@ -0,0 +1,226 @@+{-# LANGUAGE OverloadedStrings #-}++-- ----------------------------------------------------------------------------++{- |+  Module     : Hunt.Query.Fuzzy+  Copyright  : Copyright (C) 2007, 2008 Timo B. Huebel+  License    : MIT++  Maintainer : Timo B. Huebel (tbh@holumbus.org)+  Stability  : experimental+  Portability: portable+  Version    : 0.2++  The unique Holumbus mechanism for generating fuzzy sets.+-}++-- ----------------------------------------------------------------------------++module Hunt.Query.Fuzzy+  (+  -- * Fuzzy types+  FuzzySet+  , Replacements+  , Replacement+  , FuzzyScore+  , FuzzyConfig (..)++  -- * Predefined replacements+  , englishReplacements+  , germanReplacements++  -- * Generation+  , fuzz++  -- * Conversion+  , toList+  )+where++import           Data.Binary+import           Data.Default+import           Data.Function+import           Data.List+import           Data.Maybe       (fromMaybe)++import           Control.Applicative++import           Data.Map         (Map)+import qualified Data.Map         as M++import           Data.Text        (Text)+import qualified Data.Text        as T+import           Data.Text.Binary ()++-- ------------------------------------------------------------++-- | A set of string which have been "fuzzed" with an associated score.+type FuzzySet = Map Text FuzzyScore++-- | Some replacements which can be applied to a string to generate a 'FuzzySet'. The scores of+-- the replacements will be normalized to a maximum of 1.0.+type Replacements = [Replacement]++-- | A single replacements, where the first will be replaced by the second and vice versa in+-- the target string. The score indicates the amount of fuzzines that one single application+-- of this replacement in just one direction will cause on the target string.+type Replacement = ((Text,Text), FuzzyScore)++-- | The score indicating an amount of fuzziness.+type FuzzyScore = Float++-- | The configuration of a fuzzy query.+data FuzzyConfig+  = FuzzyConfig+    { applyReplacements  :: Bool         -- ^ Indicates whether the replacements should be applied.+    , applySwappings     :: Bool         -- ^ Indicates whether the swapping of adjacent characters should be applied.+    , maxFuzziness       :: FuzzyScore   -- ^ The maximum allowed fuzziness.+    , customReplacements :: Replacements -- ^ The replacements that should be applied.+    }+  deriving (Show)++-- ------------------------------------------------------------++instance Default FuzzyConfig where+  def = FuzzyConfig True True 1.0 englishReplacements++instance Binary FuzzyConfig where+  put (FuzzyConfig r s m f) = put r >> put s >> put m >> put f+  get = FuzzyConfig <$> get <*> get <*> get <*> get++-- ------------------------------------------------------------++-- | Some default replacements for the english language.+englishReplacements :: Replacements+englishReplacements =+  [ (("l", "ll"), 0.2)+  , (("t", "tt"), 0.2)+  , (("r", "rr"), 0.2)+  , (("e", "ee"), 0.2)+  , (("o", "oo"), 0.2)+  , (("s", "ss"), 0.2)++  , (("g", "ck"), 0.4)+  , (("k", "ck"), 0.4)+  , (("ea", "ee"), 0.4)+  , (("ou", "oo"), 0.4)+  , (("ou", "au"), 0.4)+  , (("ou", "ow"), 0.4)++  , (("s", "c"), 0.6)+  , (("uy", "ye"), 0.6)+  , (("y", "ey"), 0.6)+  , (("kn", "n"), 0.6)+  ]++-- | Some default replacements for the german language.+germanReplacements :: Replacements+germanReplacements =+  [ (("l", "ll"), 0.2)+  , (("t", "tt"), 0.2)+  , (("n", "nn"), 0.2)+  , (("r", "rr"), 0.2)+  , (("i", "ie"), 0.2)+  , (("ei", "ie"), 0.2)+  , (("k", "ck"), 0.2)++  , (("d", "t"), 0.4)+  , (("b", "p"), 0.4)+  , (("g", "k"), 0.4)+  , (("g", "ch"), 0.4)+  , (("c", "k"), 0.4)+  , (("s", "z"), 0.4)+  , (("u", "ou"), 0.4)++  , (("ü", "ue"), 0.1)+  , (("ä", "ae"), 0.1)+  , (("ö", "oe"), 0.1)+  , (("ß", "ss"), 0.1)+  ]++-- | Continue fuzzing a string with the an explicitly specified list of replacements until+-- a given score threshold is reached.+fuzz :: FuzzyConfig -> Text -> FuzzySet+fuzz cfg s = M.delete s (fuzz' (fuzzLimit cfg 0.0 s))+  where+  fuzz' :: FuzzySet -> FuzzySet+  fuzz' fs = if M.null more then fs else M.unionWith min fs (fuzz' more)+    where+    -- The current score is doubled on every recursive call, because fuzziness increases exponentially.+    more = M.foldrWithKey (\sm sc res -> M.unionWith min res (fuzzLimit cfg (sc + sc) sm)) M.empty fs++-- | Fuzz a string and limit the allowed score to a given threshold.+fuzzLimit :: FuzzyConfig -> FuzzyScore -> Text -> FuzzySet+fuzzLimit cfg sc s = if sc <= th then M.filter (<= th) (fuzzInternal cfg sc s) else M.empty+  where+  th = maxFuzziness cfg++-- | Fuzz a string with an list of explicitly specified replacements and combine the scores+-- with an initial score.+fuzzInternal :: FuzzyConfig -> FuzzyScore -> Text -> FuzzySet+fuzzInternal cfg sc s = M.unionWith min replaced swapped+  where+  replaced = let rs = customReplacements cfg in if applyReplacements cfg+             then foldr (\r res -> M.unionWith min res (applyFuzz (replace rs r) sc s)) M.empty rs+             else M.empty+  swapped = if applySwappings cfg+            then applyFuzz swap sc s+            else M.empty++-- | Applies a fuzzy function to a string. An initial score is combined with the new score+-- for the replacement.+applyFuzz :: (Text -> Text -> [(Text, FuzzyScore)]) -> FuzzyScore -> Text -> FuzzySet+applyFuzz f sc s = apply (init $ T.inits s) (init $ T.tails s)+  where+  apply :: [Text] -> [Text] -> FuzzySet+  apply [] _ = M.empty+  apply _ [] = M.empty+  apply (pr:prs) (su:sus) = M.unionsWith min $ apply prs sus:map create (f pr su)+    where+    create (fuzzed, score) = M.singleton fuzzed (sc + score * calcWeight (T.length pr) (T.length s))++-- | Apply a replacement in both directions to the suffix of a string and return the complete+-- string with a score, calculated from the replacement itself and the list of replacements.+replace :: Replacements -> Replacement -> Text -> Text -> [(Text, FuzzyScore)]+replace rs ((r1, r2), s) prefix suffix = replace' r1 r2 ++ replace' r2 r1+  where+  replace' tok sub = if replaced == suffix then [] else [(prefix `T.append` replaced, score)]+    where+    replaced = replaceFirst tok sub suffix+    score = s / (snd $ maximumBy (compare `on` snd) rs)++-- | Swap the first two characters of the suffix and return the complete string with a score or+-- Nothing if there are not enough characters to swap.+swap :: Text -> Text -> [(Text, FuzzyScore)]+swap prefix str = fromMaybe [] $ do+  (s1, suffix0) <- T.uncons str+  (s2, suffix)  <- T.uncons suffix0+  return [(prefix `T.append` (s2 `T.cons` s1 `T.cons` suffix), 1.0)]++-- | Calculate the weighting factor depending on the position in the string and it's total length.+calcWeight :: Int -> Int -> FuzzyScore+calcWeight pos len = (l - p) / l+  where+  p = fromIntegral pos+  l = fromIntegral len++-- | Searches a prefix and replaces it with a substitute in a list.+replaceFirst :: Text -> Text -> Text -> Text+replaceFirst xs' ys' zs'+    = case () of+      _ | T.null xs' -> ys' `T.append` zs'+        | T.null zs' -> T.empty+        | otherwise -> let (x, xs) = (T.head xs', T.tail xs')+                           (y, ys) = (T.head ys', T.tail ys')+                           (z, zs) = (T.head zs', T.tail zs')+                       in if x == z && xs' `T.isPrefixOf` zs' then+                            if T.null ys' then replaceFirst xs T.empty zs+                            else y `T.cons` replaceFirst xs ys zs+                          else zs'++-- | Transform a fuzzy set into a list (ordered by score).+toList :: FuzzySet -> [ (Text, FuzzyScore) ]+toList = sortBy (compare `on` snd) . M.toList++-- ------------------------------------------------------------
+ src/Hunt/Query/Intermediate.hs view
@@ -0,0 +1,657 @@+{-# LANGUAGE FlexibleContexts           #-}+{-# LANGUAGE FlexibleInstances          #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses      #-}+{-# LANGUAGE OverloadedStrings          #-}+{-# LANGUAGE TypeFamilies               #-}++-- ----------------------------------------------------------------------------+{- |+  The intermediate query results which have to be merged for the various combinatorial operations.++  'toResult' creates the final result which includes the document (and word) hits.+-}+-- ----------------------------------------------------------------------------++module Hunt.Query.Intermediate+    ( ScoredResult(..)+    , Aggregate(..)++    , ScoredDocs+    , ScoredWords+    , ScoredContexts+    , ScoredOccs+    , ScoredRawDocs+    , ScoredCx+    , UnScoredDocs+    , RankedDoc (..)++    , toScoredDocs+    , boostAndAggregateCx+    , fromCxRawResults+    , fromRawResult+    , limitRawResult+    , limitCxRawResults+    , contextWeights+    , filterByDocSet+    , toDocIdSet+    , toDocsResult+    , toDocumentResultPage+    , toWordsResult++    , evalSequence+    , evalFollow+    , evalNear+    , evalOr+    , evalAnd+    , evalAndNot+    , evalBoost+    , evalPrim+    )+where++import           Prelude                   hiding (null)++import           Control.Applicative       hiding (empty)+import           Control.Arrow             (second, (***))++import qualified Data.LimitedPriorityQueue as Q+import qualified Data.List                 as L+import           Data.Map                  (Map)+import qualified Data.Map                  as M+import           Data.Maybe+import           Data.Aeson+import qualified Data.HashMap.Strict       as HM+import           Hunt.Query.Result         hiding (null)++import           Hunt.Common+import qualified Hunt.Common.DocIdMap      as DM+import qualified Hunt.Common.DocIdSet      as DS+import           Hunt.Common.Document      (DocumentWrapper (..))+import qualified Hunt.Common.Occurrences   as Occ+import qualified Hunt.Common.Positions     as Pos+import           Hunt.DocTable             (DocTable)+import qualified Hunt.DocTable             as Dt++-- import           Debug.Trace++-- ------------------------------------------------------------+--+-- The Monoid @mempty@ acts as empty result,+-- (<>) is the union of scored results++class Monoid a => ScoredResult a where+    boost            :: Score -> a -> a+    nullSC           :: a -> Bool+    differenceSC     :: a -> a -> a+    intersectSC      :: a -> a -> a+    intersectDisplSC :: Int -> a -> a -> a+    intersectFuzzySC :: Int -> Int -> a -> a -> a++-- ------------------------------------------------------------++-- The result type for document search, every doc is associated with a score++newtype ScoredDocs+    = SDS (DocIdMap Score)+      deriving (Show)++instance Monoid ScoredDocs where+    mempty+        = SDS DM.empty+    mappend (SDS m1) (SDS m2)+        = SDS $ DM.unionWith (<>) m1 m2++instance ScoredResult ScoredDocs where+    boost b (SDS m1)+        = SDS $ DM.map (b *) m1++    nullSC (SDS m1)+        = DM.null m1++    differenceSC (SDS m1) (SDS m2)+        = SDS $ DM.difference m1 m2++    intersectSC (SDS m1) (SDS m2)+        = SDS $ DM.intersectionWith (+) m1 m2++    intersectDisplSC _disp+        = intersectSC++    intersectFuzzySC _lb _ub+        = intersectSC++toScoredDocs :: Occurrences -> ScoredDocs+toScoredDocs os+    = SDS $ DM.map toScore os+      where+        toScore = mkScore . fromIntegral . Pos.size++-- ------------------------------------------------------------++-- | The result type for unscored search of documents+--+-- used when all matching docs must be processed,+-- e.g. in DeleteByQuery commands++newtype UnScoredDocs+    = UDS DS.DocIdSet+      deriving (Show, Monoid)++instance ScoredResult UnScoredDocs where+    boost _b uds+        = uds++    nullSC (UDS s)+        = DS.null s++    differenceSC (UDS s1) (UDS s2)+        = UDS $ DS.difference s1 s2++    intersectSC (UDS s1) (UDS s2)+        = UDS $ DS.intersection s1 s2++    intersectDisplSC _disp+        = intersectSC++    intersectFuzzySC _lb _ub+        = intersectSC++toUnScoredDocs :: Occurrences -> UnScoredDocs+toUnScoredDocs os+    = UDS . DS.fromList . DM.keys $ os++toDocIdSet :: UnScoredDocs -> DS.DocIdSet+toDocIdSet (UDS ds) = ds++-- ------------------------------------------------------------++-- The result type for word search,+-- this is the required result type for completions search++newtype ScoredWords+    = SWS (Map Word Score)+      deriving (Show)++type ScoredContexts = ScoredWords++instance Monoid ScoredWords where+    mempty+        = SWS M.empty++    mappend (SWS m1) (SWS m2)+        = SWS $ M.unionWith (<>) m1 m2++instance ScoredResult ScoredWords where+    boost b (SWS m1)+        = SWS $ M.map (b *) m1++    nullSC (SWS m1)+        = M.null m1++    differenceSC (SWS m1) (SWS m2)+        = SWS $ M.difference m1 m2++    intersectSC (SWS m1) (SWS m2)+        = SWS $ M.intersectionWith (+) m1 m2++    intersectDisplSC _disp+        = intersectSC++    intersectFuzzySC _lb _ub+        = intersectSC++-- ------------------------------------------------------------++data ScoredOccs+    = SCO Score Occurrences+      deriving (Show)++instance Monoid ScoredOccs where+    mempty+        = SCO defScore DM.empty+    mappend (SCO s1 d1) (SCO s2 d2)+        = SCO ((s1 + s2) / 2.0) (DM.unionWith Pos.union d1 d2)++instance ScoredResult ScoredOccs where+    boost b (SCO s d)+        = SCO (b * s) d++    nullSC (SCO _s d)+        = DM.null d++    differenceSC (SCO s1 d1) (SCO _s2 d2)+        = SCO s1 (DM.difference d1 d2)++    intersectSC (SCO s1 d1) (SCO s2 d2)+        = SCO (s1 + s2) (Occ.intersectOccurrences Pos.union d1 d2)++    intersectDisplSC disp (SCO s1 d1) (SCO s2 d2)+        = SCO (s1 + s2) (Occ.intersectOccurrences (Pos.intersectionWithDispl disp) d1 d2)++    intersectFuzzySC lb ub (SCO s1 d1) (SCO s2 d2)+        = SCO (s1 + s2) (Occ.intersectOccurrences (Pos.intersectionWithIntervall lb ub) d1 d2)++-- ------------------------------------------------------------++-- A result type for searching phrases and context search,+-- every word hit or word sequence hit is+-- associated with a score and a DocIdMap containing+-- the docs and position.+--+-- The positions are neccessary in phrase and context search,+-- afterwards the positions can be accumulated into a score,+-- e.g. by counting the occurences+--+-- A RawResult can easily be converted into this type++newtype ScoredRawDocs+    = SRD [([Word], ScoredOccs)]+      deriving (Show)++instance Monoid ScoredRawDocs where+    mempty+        = SRD []+    mappend (SRD xs1) (SRD xs2)+        = SRD $ xs1 ++ xs2++instance ScoredResult ScoredRawDocs where+    boost b (SRD xs)+        = SRD $ L.map (second (boost b)) xs++    nullSC (SRD xs)+        = L.null xs++    differenceSC (SRD xs1) (SRD xs2)+        = srd $ [(ws1, SCO sc1 (diff occ1 xs2)) | (ws1, SCO sc1 occ1) <- xs1]+          where+            diff occ xs+                = L.foldl op occ xs+                  where+                    op occ' (_ws2, SCO _sc2 occ2)+                        = DM.difference occ' occ2++    intersectSC+        = intersectSRD intersectSC++    intersectDisplSC d+        = intersectSRD $ intersectDisplSC d++    intersectFuzzySC lb ub+        = intersectSRD $ intersectFuzzySC lb ub++-- ------------------------------------------------------------+--+-- auxiliary functions for ScoredRawDocs++-- | generalized intersection operator for ScoredRawDocs++intersectSRD :: (ScoredOccs -> ScoredOccs -> ScoredOccs)+              -> ScoredRawDocs -> ScoredRawDocs -> ScoredRawDocs+intersectSRD interSRD (SRD xs1) (SRD xs2)+    = srd $ [ (ws1 ++ ws2, interSRD socc1 socc2)+            | (ws1, socc1) <- xs1+            , (ws2, socc2) <- xs2+            ]++-- | smart constructor for ScoredRawDoc removing empty entries++srd :: [([Word], ScoredOccs)] -> ScoredRawDocs+srd+    = SRD . L.filter (not . nullSC . snd)++filterByDocSet :: UnScoredDocs -> ScoredRawDocs -> ScoredRawDocs+filterByDocSet (UDS ds) (SRD xs)+    = SRD $ concatMap filterDocs xs+      where+        filterDocs (ws, SCO sc occ)+            | Occ.null occ'+                = []+            | otherwise+                = [(ws, SCO sc occ')]+            where+              occ' = DM.filterWithKey p occ+                  where+                    p k _v = k `DS.member` ds++-- ------------------------------------------------------------++-- | Add the Context dimension to a scored result++newtype ScoredCx a+    = SCX (Map Context a)+      deriving (Show)++instance Functor ScoredCx where+    fmap f (SCX m) = SCX $ M.map f m++instance Monoid a => Monoid (ScoredCx a) where+    mempty+        = SCX M.empty+    mappend (SCX m1) (SCX m2)+        = SCX $ M.unionWith (<>) m1 m2++instance ScoredResult a => ScoredResult (ScoredCx a) where+    boost b (SCX m)+        = SCX $ M.map (boost b) m++    nullSC (SCX m)+        = M.null m++    differenceSC+        = binopSCX differenceSC++    intersectSC+        = binopSCX intersectSC++    intersectDisplSC d+        = binopSCX $ intersectDisplSC d++    intersectFuzzySC lb ub+        = binopSCX $ intersectFuzzySC lb ub+++-- | The final op for a search result: boost the partial results of the contexts+-- with the context weights from the schema and aggregate them+-- by throwing away the contexts++boostAndAggregateCx :: ScoredResult a => ScoredContexts -> ScoredCx a -> a+boostAndAggregateCx (SWS sm) (SCX m)+    = M.foldlWithKey op mempty m+      where+        op res k x+            = boost b x <> res+              where+                b = fromMaybe defScore $ M.lookup k sm++contextWeights :: Schema -> ScoredContexts+contextWeights s+    = SWS $ M.map cxWeight s++-- ------------------------------------------------------------+--+-- auxiliary functions for scored context results++binopSCX :: ScoredResult a => (a -> a -> a) -> (ScoredCx a -> ScoredCx a -> ScoredCx a)+binopSCX op (SCX m1) (SCX m2)+    = scx $ M.mapWithKey op' m1+          where+            op' cx1 sr1+                = sr1 `op` sr2+                  where+                    sr2 = fromMaybe mempty $ M.lookup cx1 m2+++-- | smart constructore for ScoredCx removing empty contexts++scx :: ScoredResult a => Map Word a -> ScoredCx a+scx = SCX . M.filter (not . nullSC)++-- ------------------------------------------------------------+--+-- conversions from RawResult into scored results++type CxRawResults = [(Context, RawScoredResult)]++fromCxRawResults :: CxRawResults -> ScoredCx ScoredRawDocs+fromCxRawResults crs+    = -- traceShow ("fromCxRawResults:"::String, crs, res) $+      res+      where+        res = mconcat $ L.map (uncurry $ fromRawResult) crs++fromRawResult :: Context -> RawScoredResult -> ScoredCx ScoredRawDocs+fromRawResult cx rr+    = SCX $ M.singleton cx sr+      where+        sr = SRD $ L.map toScored rr+            where+              toScored (w, (sc, occ))+                  = ([w], SCO sc occ)++limitCxRawResults :: Int -> CxRawResults -> CxRawResults+limitCxRawResults mx+    = L.map (second $ limitRawResult mx)++limitRawResult :: Int -> RawScoredResult -> RawScoredResult+limitRawResult maxDocs rs+    | maxDocs <= 0 = rs+    | otherwise = takeDocs maxDocs rs+    where+      takeDocs _  xs@[]+          = xs+      takeDocs _  xs@[_]+          = xs+      takeDocs mx (r@(_w, (_sc, occ)) : xs)+          = case DM.sizeWithLimit mx occ of+              Nothing -> [r]+              Just s  -> let mx' = mx - s in+                         if mx' <= 0+                            then [r]+                            else r : takeDocs mx' xs++-- | convert a set of scored doc ids into a list of documents containing the score+--+-- This list may be further sorted by score, partitioned into pages or ...++toDocsResult :: (Applicative m, Monad m, DocTable dt) =>+                dt -> ScoredDocs -> m [RankedDoc]+toDocsResult dt (SDS m)+    = mapM toDoc (DM.toList m)+      where+        toDoc (did, sc)+            = (toD . fromJust') <$> Dt.lookup did dt+            where+              toD d'+                  = RD (wght d * sc, d)+                  where+                    d = unwrap d'+        fromJust' (Just x) = x+        fromJust' Nothing  = error "Intermediate.toDocsResult: undefined"+-- ----------------------------------------+-- ranking of documents++-- define a total ordering over documents by taking score and uri into account+-- this is used for ranking (only)+--+-- XXX TODO: maybe move to Common modules, since this type is now the regular+--           return type++newtype RankedDoc = RD {unRD :: (Score, Document)}+  deriving Show++instance Eq RankedDoc where+    (RD (sc1,d1)) == (RD (sc2,d2))+        = sc1 == sc2+          &&+          uri   d1 == uri   d2++instance Ord RankedDoc where+    (RD (sc1,d1)) `compare` (RD (sc2,d2))+        = case sc1 `compare` sc2 of+            EQ -> uri d2 `compare` uri d1+            r  -> r++instance ToJSON RankedDoc where+    toJSON (RD (c,d))+      = addScore $ toJSON d+      where+        addScore (Object hm)+          = Object $ HM.insert "score" (toJSON c) hm+        addScore _+          = error "toJSON rankedDoc: propably a bug introduced with #75"++toDocumentResultPage :: Int -> Int -> [RankedDoc] -> [RankedDoc]+toDocumentResultPage = Q.pageList++-- ----------------------------------------+-- ranking of completions++data RankedWord = RW Score Word+                   deriving Eq++instance Ord RankedWord where+    (RW s1 w1) `compare` (RW s2 w2)+        = case s1 `compare` s2 of+            EQ -> w2 `compare` w1+            r  -> r++toWordsResult :: Int -> ScoredWords -> [(Word, Score)]+toWordsResult len (SWS m)+    = map (\ (RW s w) -> (w, s))+      . Q.toList 0 len+      . M.foldWithKey (\ w s -> Q.insert (RW s w)) (Q.mkQueue len)+      $ m++-- ------------------------------------------------------------+--+-- aggregating (raw) results for various types of scored results++class Aggregate a b where+    aggregate :: a -> b++-- | allow no aggregation++instance Aggregate a a where+    aggregate = id++-- | aggregate scored docs to a single score by summing up the scores and throw away the DocIds++instance Aggregate ScoredDocs Score where+    aggregate (SDS m)+        = DM.foldr (<>) defScore m+++-- | aggregate scored occurences by counting the positions per doc and boost the+-- result with the score.+--+-- The positions in a doc are thrown away, so all kinds of phrase search become impossible++instance Aggregate ScoredOccs ScoredDocs where+    aggregate (SCO sc occ)+        = SDS $ DM.map toScore occ+          where+            toScore = (sc *) . mkScore . fromIntegral . Pos.size++-- | aggregate scored occurrences to unscored docs by throwing away the score+instance Aggregate ScoredOccs UnScoredDocs where+    aggregate (SCO _sc occ)+        = toUnScoredDocs occ++-- | aggregate scored occurences to a score by aggregating first the positions and snd the doc ids+--+-- used in computing the score of word in completion search++instance Aggregate ScoredOccs Score where+    aggregate = agg2 . agg1+        where+          agg1 :: ScoredOccs -> ScoredDocs+          agg1 = aggregate+          agg2 :: ScoredDocs -> Score+          agg2 = aggregate++-- | aggregate scored raw results by throwing away the word hits and aggregating+-- all document hits by throwing away the positions.+--+-- The function for computing the scores of documents in a query++instance Aggregate ScoredRawDocs ScoredDocs where+    aggregate (SRD xs)+        = L.foldl (<>) mempty+          $ L.map (aggregate . snd) xs++-- | aggregate scored raw results into an unscored result by throwing away+-- the words, scores and occurrences++instance Aggregate ScoredRawDocs UnScoredDocs where+    aggregate (SRD xs)+        = L.foldl (<>) mempty+          $ L.map (aggregate . snd) xs++-- | aggregate the scored raw results by computing the score of all doc per word+--+-- Used in completion search.+-- The sequence of words (in a phrase) is cut to the last word (@L.last@). For this last one+-- the completions are computed.++instance Aggregate ScoredRawDocs ScoredWords where+    aggregate (SRD xs)+        = SWS+          $ L.foldl (\ res (w, sc) -> M.insertWith (+) w sc res) M.empty+          $ L.map (L.last *** aggregate) xs++-- | Lifting aggregation to scored context results++instance Aggregate a b => Aggregate (ScoredCx a) (ScoredCx b) where+    aggregate (SCX m)+        = SCX $ M.map aggregate m++-- ------------------------------------------------------------++-- query operator evaluation++-- | combine a sequence of results from a phrase query into a single result+-- and aggregate this result into a simpler structure,+-- e.g. a @ScoredDocs@ or @ScoredWords@ value+--+-- The arguments must be of this unaggregated from, still containing word+-- positions, else sequences of words are not detected++evalSequence :: (ScoredResult r, Aggregate ScoredRawDocs r) =>+                (ScoredCx ScoredRawDocs -> ScoredCx r) ->+                [ScoredCx ScoredRawDocs] -> ScoredCx r+evalSequence _aggr []+    = mempty+evalSequence aggr [r1]+    = aggr r1+evalSequence aggr (r1 : rs)+    = aggr $ L.foldl op r1 (L.zip [(1::Int)..] rs)+      where+        op acc (d, r2) = intersectDisplSC d acc r2++evalFollow :: (ScoredResult r, Aggregate ScoredRawDocs r) =>+              (ScoredCx ScoredRawDocs -> ScoredCx r) ->+              Int ->+              [ScoredCx ScoredRawDocs] -> ScoredCx r+evalFollow _aggr _d []+    = mempty+evalFollow aggr _d [r1]+    = aggr r1+evalFollow aggr dp (r1 : rs)+    = aggr $ L.foldl op r1 (L.zip [dp, (2 * dp) ..] rs)+      where+        op acc (d, r2) = intersectFuzzySC 1 d acc r2++evalNear :: (ScoredResult r, Aggregate ScoredRawDocs r) =>+            (ScoredCx ScoredRawDocs -> ScoredCx r) ->+            Int ->+           [ScoredCx ScoredRawDocs] -> ScoredCx r+evalNear _aggr _d []+    = mempty+evalNear aggr _d [r1]+    = aggr r1+evalNear aggr dp (r1 : rs)+    = aggr $ L.foldl op r1 (L.zip [dp, (2 * dp) ..] rs)+      where+        op acc (d, r2) = intersectFuzzySC (-d) d acc r2++evalOr, evalAnd, evalAndNot :: ScoredResult a => [a] -> a+evalOr     = evalBinary (<>)+evalAnd    = evalBinary intersectSC+evalAndNot = evalBinary differenceSC++evalBinary :: ScoredResult a => (a -> a -> a) -> [a] -> a+evalBinary _ []+    = mempty+evalBinary _ [r]+    = r+evalBinary op rs+    = L.foldl1 op rs++evalBoost :: ScoredResult a => Score -> a -> a+evalBoost = boost++evalPrim :: Aggregate (ScoredCx ScoredRawDocs) (ScoredCx r) => ScoredCx ScoredRawDocs -> ScoredCx r+evalPrim = aggregate++-- ------------------------------------------------------------
+ src/Hunt/Query/Language/Builder.hs view
@@ -0,0 +1,232 @@++module Hunt.Query.Language.Builder+  (   qWord+    , qWordNoCase+    , qFullWord+    , qFullWordNoCase+    , qPhrase+    , qPhraseNoCase+    , qPrefixPhrase+    , qPrefixPhraseNoCase+    , qRange+    , qAnd+    , qAnds+    , qOr+    , qOrs+    , qAndNot+    , qAndNots+    , qNext+    , qNexts+    , qFollow+    , qFollows+    , qNear+    , qNears+    , setNoCaseSearch+    , setFuzzySearch+    , setContext+    , setContexts+    , setBoost+    , withinContexts    -- deprecated+    , withinContext     -- deprecated+    , withBoost         -- deprecated+    , qContext+    )+where++import           Hunt.Query.Language.Grammar+import           Data.Text                   (Text)+import qualified Data.Text                   as T++import           Hunt.Common.BasicTypes      (Context, Weight)++-- query construction++-- | case sensitive prefix search of a single word++qWord :: Text -> Query+qWord = QWord QCase++-- | case insensitive prefix search of a single word++qWordNoCase :: Text -> Query+qWordNoCase = QWord QNoCase++-- | exact case sensitive search of a single word++qFullWord :: Text -> Query+qFullWord = QFullWord QCase++-- | exact, but case insensitive search of a single word++qFullWordNoCase :: Text -> Query+qFullWordNoCase = QFullWord QNoCase++-- --------------------+--+-- phrase search++qPhrase' :: (Text -> Query) -> Text -> Query+qPhrase' qf t+    = case T.words t of+        [w] -> qf w+        ws  -> qNexts $ map qf ws++-- | exact search of a sequence of space separated words.+-- For each word in the sequence, an exact word search is performed.++qPhrase :: Text -> Query+qPhrase = qPhrase' qFullWord++-- | exact, but case insenitive search of a sequence of space separated words.+-- For each word in the sequence, a word search is performed.++qPhraseNoCase :: Text -> Query+qPhraseNoCase = qPhrase' qFullWordNoCase++-- | prefix search of a sequence of space separated words.+-- For each word in the sequence, a prefix search is performed.++qPrefixPhrase :: Text -> Query+qPrefixPhrase = qPhrase' qWordNoCase++-- | prefix search of a sequence of space separated words.+-- For each word in the sequence, a prefix search is performed.++qPrefixPhraseNoCase :: Text -> Query+qPrefixPhraseNoCase = qPhrase' qWordNoCase++-- --------------------++-- | search a range of words or an intervall for numeric contexts+qRange :: Text -> Text -> Query+qRange = QRange++-- | shortcut for case sensitive context search+qContext :: Context -> Text -> Query+qContext c w = QContext [c] $ QWord QCase w++-- | and query+qAnd :: Query -> Query -> Query+qAnd q1 q2 = qAnds [q1, q2]++--  | multiple @and@ queries. The list must not be emtpy+qAnds :: [Query] -> Query+qAnds = mkAssocSeq And++-- | or query+qOr :: Query -> Query -> Query+qOr q1 q2 = qOrs [q1, q2]++--  | multiple @or@ queries. The list must not be emtpy+qOrs :: [Query] -> Query+qOrs = mkAssocSeq Or++-- | and not query+qAndNot :: Query -> Query -> Query+qAndNot q1 q2 = qAndNots [q1, q2]++--  | multiple @and-not@ queries. The list must not be emtpy+-- TODO handle left associativity++qAndNots :: [Query] -> Query+qAndNots = mkLeftAssocSeq AndNot++-- | neighborhood queries. The list must not be empty+--+-- TODO: a better name for qNext and qNexts, qPhrase is already used++qNext :: Query -> Query -> Query+qNext q1 q2 = qNexts [q1, q2]++qNexts :: [Query] -> Query+qNexts = mkAssocSeq Phrase++qFollow :: Int -> Query -> Query -> Query+qFollow d q1 q2 = qFollows d [q1, q2]++qFollows :: Int -> [Query] -> Query+qFollows d = mkAssocSeq (Follow d)++qNear :: Int -> Query -> Query -> Query+qNear d q1 q2 = qNears d [q1, q2]++qNears :: Int -> [Query] -> Query+qNears d = mkAssocSeq (Near d)++collectAssocs :: BinOp -> [Query] -> [Query]+collectAssocs op qs+    = concatMap subqs qs+    where+      subqs (QSeq op' qs')+          | op == op'+              = qs'+      subqs q'+          = [q']++mkAssocSeq :: BinOp -> [Query] -> Query+mkAssocSeq op qs+    = remSingle $ QSeq op (collectAssocs op qs)++mkLeftAssocSeq :: BinOp -> [Query] -> Query+mkLeftAssocSeq op qs+    = remSingle $ QSeq op qs'+    where+      qs' = case qs of+              (QSeq op' qs1 : qs2)+                  | op == op'+                      -> qs1 ++ qs2+              _       -> qs++remSingle :: Query -> Query+remSingle (QSeq _ [q])+    = q+remSingle q+    = q+++-- ------------------------------------------------------------+-- configure simple search queries++-- | case insensitve search, only sensible for word and phrase queries++setNoCaseSearch :: Query -> Query+setNoCaseSearch (QWord     _ w) = QWord     QNoCase w+setNoCaseSearch (QFullWord _ w) = QFullWord QNoCase w+setNoCaseSearch (QPhrase   _ w) = QPhrase   QNoCase w+setNoCaseSearch q             = q++-- | fuzzy search, only sensible for word and phrase queries++setFuzzySearch :: Query -> Query+setFuzzySearch (QWord     _ w) = QWord     QFuzzy w+setFuzzySearch (QFullWord _ w) = QFullWord QFuzzy w+setFuzzySearch (QPhrase   _ w) = QPhrase   QFuzzy w+setFuzzySearch q             = q++-- | restrict search to list of contexts++setContexts :: [Context] -> Query -> Query+setContexts = QContext++withinContexts :: [Context] -> Query -> Query+withinContexts = QContext+{-# DEPRECATED withinContexts "Don't use this, use setContexts" #-}++-- | restrict search to a single context++setContext :: Context -> Query -> Query+setContext cx = withinContexts [cx]++withinContext :: Context -> Query -> Query+withinContext cx = setContexts [cx]+{-# DEPRECATED withinContext "Don't use this, use setContext" #-}+++-- | boost the search results by a factor++setBoost :: Weight -> Query -> Query+setBoost = QBoost++withBoost :: Weight -> Query -> Query+withBoost = QBoost+{-# DEPRECATED withBoost "Don't use this, use setBoost" #-}    
+ src/Hunt/Query/Language/Grammar.hs view
@@ -0,0 +1,378 @@+{-# LANGUAGE OverloadedStrings #-}++-- ----------------------------------------------------------------------------+{- |+  The query language.++  'Query' specifies the complete grammar.++  "Hunt.Query.Language.Parser" provides a parser for plain text queries.+-}+-- ----------------------------------------------------------------------------++module Hunt.Query.Language.Grammar+    ( -- * Query data types+      Query (..)+    , BinOp (..)+    , TextSearchType (..)+    , escapeChar+    , notWordChar++    -- * Optimizing+    , optimize+    , checkWith+    , extractTerms+    -- * Pretty printing+    , printQuery+    )+where++import           Control.Applicative+import           Control.Monad++import           Data.Aeson+import           Data.Binary+import           Data.Text              (Text)+import qualified Data.Text              as T+import           Data.Text.Binary       ()++import           Hunt.Common.BasicTypes as BTy++import           Text.Read              (readMaybe)++-- ------------------------------------------------------------++-- TODO: the constructors QPhrase and QBinary can be removed+-- they can be represented by QFullWord and QSeq.+--+-- Currently these operators are transformed during query evaluation+-- on the fly into QFullWord and QSeq.++-- | The query language.+data Query+  = QWord     TextSearchType Text  -- ^ prefix search for a word+  | QFullWord TextSearchType Text  -- ^ search for a complete word+  | QPhrase   TextSearchType Text  -- ^ Phrase search.+  | QContext  [Context] Query      -- ^ Restrict a query to a list of contexts.+  | QBinary   BinOp Query Query    -- ^ Combine two queries with a binary operation.+  | QSeq      BinOp [Query]+  | QBoost    Weight Query         -- ^ Weight for query.+  | QRange    Text Text            -- ^ Range query.+  deriving (Eq, Show)++-- | The search opeation.+data TextSearchType+  = QCase   -- ^ Case-sensitive search.+  | QNoCase -- ^ Case-insensitive search.+  | QFuzzy  -- ^ Fuzzy search. See "Hunt.Query.Fuzzy" for details.+            --   The query processor allows additional configuration with+            --   'Hunt.Query.Processor.ProcessConfig'.+  deriving (Eq, Show)++-- | A binary operation.+data BinOp+  = And        -- ^ Intersect two queries.+  | Or         -- ^ Union two queries.+  | AndNot     -- ^ Filter a query by another.+  | Phrase     -- ^ Search for a sequence of words+  | Follow Int -- ^ Search a word followed another word within a distance+  | Near   Int -- ^ search a word followed or preceded another word within a distance+  deriving (Eq, Show)++-- ------------------------------------------------------------+-- JSON instances+-- ------------------------------------------------------------++instance ToJSON Query where+  toJSON o = case o of+    QWord op w        -> object . ty "word"     $ [ "op" .= op, "word"   .= w ]+    QFullWord op w    -> object . ty "fullword" $ [ "op" .= op, "word"   .= w ]+    QPhrase op s      -> object . ty "phrase"   $ [ "op" .= op, "phrase" .= s ]+    QContext c q      -> object . ty "context"  $ [ "contexts" .= c , "query" .= q ]+    QBinary op q1 q2  -> object . ty' op        $ [ "query1" .= q1, "query2" .= q2 ]+    QSeq    op qs     -> object . ty "seq"      $ [ "op" .= op, "args" .= qs ]+    QBoost  w q       -> object . ty "boost"    $ [ "weight" .= w, "query" .= q ]+    QRange l u        -> object . ty "range"    $ [ "lower" .= l, "upper" .= u ]+    where+    ty' t = (:) ("type" .= t)+    ty  t = ty' (t :: Text)++instance FromJSON Query where+  parseJSON (Object o) = do+    t <- o .: "type"+    case (t :: Text) of+      "word"+          -> QWord     <$> (o .: "op") <*> (o .: "word")+      "fullword"+          -> QFullWord <$> (o .: "op") <*> (o .: "word")+      "phrase"+          -> QPhrase   <$> (o .: "op") <*> (o .: "phrase")+      "context"+          -> QContext  <$> (o .: "contexts") <*> (o .: "query")+      "boost"+          -> QBoost    <$> (o .: "weight") <*> (o .: "query")+      "range"+          -> QRange    <$> (o .: "lower") <*> (o .: "upper")+      "and"+          -> bin And+      "or"+          -> bin Or+      "and not"+          -> bin AndNot+      "seq"+          -> QSeq      <$> (o .: "op") <*> (o .: "args")+      _   -> mzero+    where+    bin op+        = QBinary op   <$> (o .: "query1") <*> (o .: "query2")+  parseJSON _ = mzero+++instance ToJSON TextSearchType where+  toJSON o = case o of+    QCase   -> "case"+    QNoCase -> "nocase"+    QFuzzy  -> "fuzzy"++instance FromJSON TextSearchType where+  parseJSON (String s)+    = case s of+      "case"   -> return QCase+      "nocase" -> return QNoCase+      "fuzzy"  -> return QFuzzy+      _         -> mzero+  parseJSON _ = mzero++instance ToJSON BinOp where+  toJSON o = case o of+    And      -> "and"+    Or       -> "or"+    AndNot   -> "and not"+    Phrase   -> "phrase"+    Follow d -> String $ "follow " <> T.pack (show d)+    Near   d -> String $ "near "   <> T.pack (show d)++instance FromJSON BinOp where+  parseJSON (String s)+    = case T.words s of+      ["and"]        -> return And+      ["or"]         -> return Or+      ["and", "not"] -> return AndNot+      ["phrase"]     -> return Phrase+      ["follow", d]  -> maybe mzero (return . Follow) . readMaybe . T.unpack $ d+      ["near", d]    -> maybe mzero (return . Near  ) . readMaybe . T.unpack $ d+      _              -> mzero+  parseJSON _ = mzero++-- ------------------------------------------------------------+-- Binary instances+-- ------------------------------------------------------------++instance Binary Query where+  put (QWord op s)       = put (0 :: Word8) >> put op >> put s+  put (QFullWord op s)   = put (7 :: Word8) >> put op >> put s+  put (QPhrase op s)     = put (1 :: Word8) >> put op >> put s+  put (QContext c q)     = put (2 :: Word8) >> put c  >> put q+  put (QBinary o q1 q2)  = put (4 :: Word8) >> put o  >> put q1 >> put q2+  put (QSeq    o qs)     = put (8 :: Word8) >> put o  >> put qs+  put (QBoost w q)       = put (5 :: Word8) >> put w  >> put q+  put (QRange l u)       = put (6 :: Word8) >> put l  >> put u++  get = do+    tag <- getWord8+    case tag of+      0 -> QWord     <$> get <*> get+      7 -> QFullWord <$> get <*> get+      1 -> QPhrase   <$> get <*> get+      2 -> QContext  <$> get <*> get+      4 -> QBinary   <$> get <*> get <*> get+      8 -> QSeq      <$> get <*> get+      5 -> QBoost    <$> get <*> get+      6 -> QRange    <$> get <*> get+      _ -> fail "Error while decoding Query"+++instance Binary TextSearchType where+  put QCase   = put (0 :: Word8)+  put QNoCase = put (1 :: Word8)+  put QFuzzy  = put (2 :: Word8)++  get = do+    tag <- getWord8+    case tag of+      0 -> return QCase+      1 -> return QNoCase+      2 -> return QFuzzy+      _ -> fail "Error while decoding BinOp"++instance Binary BinOp where+  put And        = put (0 :: Word8)+  put Or         = put (1 :: Word8)+  put AndNot     = put (2 :: Word8)+  put Phrase     = put (3 :: Word8)+  put (Follow d) = put (4 :: Word8) >> put d+  put (Near   d) = put (5 :: Word8) >> put d++  get = do+    tag <- getWord8+    case tag of+      0 -> return And+      1 -> return Or+      2 -> return AndNot+      3 -> return Phrase+      4 -> Follow <$> get+      5 -> Near   <$> get+      _ -> fail "Error while decoding BinOp"++-- ------------------------------------------------------------++-- | Characters that cannot occur in a word (and have to be escaped).+notWordChar :: String+notWordChar = escapeChar : "\"')([]^ \n\r\t"++-- | The character an escape sequence starts with.+escapeChar :: Char+escapeChar = '\\'++-- | Minor query optimizations.+--+--   /Note/: This can affect the ranking.+optimize :: Query -> Query+-- Same prefix in AND query (case-insensitive)+optimize q@(QBinary And (QWord QNoCase q1) (QWord QNoCase q2))+  | T.toLower q1 `T.isPrefixOf` T.toLower q2 = QWord QNoCase q2+  | T.toLower q2 `T.isPrefixOf` T.toLower q1 = QWord QNoCase q1+  | otherwise = q+-- Same prefix in AND query (case-sensitive)+optimize q@(QBinary And (QWord QCase q1) (QWord QCase q2))+  | q1 `T.isPrefixOf` q2 = QWord QCase q2+  | q2 `T.isPrefixOf` q1 = QWord QCase q1+  | otherwise = q+-- Same prefix in OR query (case-insensitive)+optimize q@(QBinary Or (QWord QNoCase q1) (QWord QNoCase q2))+  | T.toLower q1 `T.isPrefixOf` T.toLower q2 = QWord QNoCase q1+  | T.toLower q2 `T.isPrefixOf` T.toLower q1 = QWord QNoCase q2+  | otherwise = q+-- Same prefix in OR query (case-sensitive)+optimize q@(QBinary Or (QWord QCase q1) (QWord QCase q2))+  | q1 `T.isPrefixOf` q2 = QWord QCase q1+  | q2 `T.isPrefixOf` q1 = QWord QCase q2+  | otherwise = q+-- recursive application+optimize (QBinary And    q1 q2) = QBinary And    (optimize q1) (optimize q2)+optimize (QBinary Or     q1 q2) = QBinary Or     (optimize q1) (optimize q2)+optimize (QBinary AndNot q1 q2) = QBinary AndNot (optimize q1) (optimize q2)+optimize (QContext cs q)        = QContext cs (optimize q)+optimize (QBoost w q)           = QBoost w (optimize q)++optimize q                      = q++-- | Check if the query arguments comply with some custom predicate.+checkWith                         :: (Text -> Bool) -> Query -> Bool+checkWith f (QWord _ s)           = f s+checkWith f (QFullWord _ s)       = f s+checkWith f (QPhrase _ s)         = f s+checkWith f (QBinary _ q1 q2)     = checkWith f q1 && checkWith f q2+checkWith f (QSeq _ qs)           = and $ map (checkWith f) qs+checkWith f (QContext _ q)        = checkWith f q+checkWith f (QBoost _ q)          = checkWith f q+checkWith f (QRange s1 s2)        = f s1 && f s2++-- | Returns a list of all terms in the query.+extractTerms                      :: Query -> [Text]+extractTerms (QWord _ s)           = [s]+extractTerms (QFullWord _ s)       = [s]+extractTerms (QContext _ q)        = extractTerms q+extractTerms (QBinary _ q1 q2)     = extractTerms q1 ++ extractTerms q2+extractTerms _                     = []++-- ------------------------------------------------------------++-- | Renders a text representation of a Query.++printQuery :: Query -> Text+printQuery (QWord QNoCase w)+    = printWord w++printQuery (QWord QCase w)+    = "!" <> printWord w++printQuery (QWord QFuzzy w)+    = "~" <> printWord w++printQuery (QFullWord QNoCase w)+    = printPhrase w++printQuery (QFullWord QCase w)+    = "!" <> printPhrase w++printQuery (QFullWord QFuzzy w)+    = "~" <> printPhrase w++printQuery (QPhrase _ w)+    = printPhrase w++printQuery (QContext [] w)+    = printQPar w++printQuery (QContext cs' w)+    = printCs <> ":(" <> (printQPar w) <> ")"+      where+        printCs = foldr1 (\l r -> l <> "," <> r) cs'++printQuery (QBinary o l r)+    = (printQPar l) <> (printOp o) <> (printQPar r)++printQuery (QSeq _ [])+    = ""+printQuery (QSeq _ [q])+    = printQuery q+printQuery (QSeq o qs)+    = foldr1 (\ res arg -> res <> printOp o <> arg) $+      map printQPar qs++printQuery (QBoost w q)+    = (printQPar q) <> "^" <> (T.pack $ show $ unScore $ toDefScore $ w)++printQuery (QRange l u)+    = "[" <> l <> " TO " <> u <> "]"++printOp :: BinOp -> Text+printOp And        = " " -- the token AND is not required.+printOp Or         = " OR "+printOp AndNot     = " AND NOT "+printOp Phrase     = " ++ "+printOp (Follow d) = " FOLLOW " <> (T.pack $ show d) <> " "+printOp (Near   d) = " NEAR "   <> (T.pack $ show d) <> " "++-- | Maybe render paranthesis.+printQPar :: Query -> Text+printQPar q@QWord{}     = printQuery q+printQPar q@QFullWord{} = printQuery q+printQPar q@QPhrase{}   = printQuery q+printQPar q@QRange{}    = printQuery q+printQPar q@QContext{}  = printQuery q+printQPar q             = "(" <> (printQuery q) <> ")"++printPhrase :: Text -> Text+printPhrase w+    = "\"" <> escapeWord toBeQuoted w <> "\""+    where+      toBeQuoted = (== '\"')++printWord :: Text -> Text+printWord w+    | T.any toBeQuoted w = "'" <> escapeWord (== '\'') w <> "'"+    | otherwise          = w+    where+      toBeQuoted c = elem c $ notWordChar++escapeWord :: (Char -> Bool) -> Text -> Text+escapeWord p t+    = T.concatMap esc t+      where+        esc c+            | p c = T.pack ('\\' : c : [])+            | otherwise = T.singleton c++-- ------------------------------------------------------------
+ src/Hunt/Query/Language/Parser.hs view
@@ -0,0 +1,322 @@+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC  -fno-warn-unused-do-bind #-}++-- ----------------------------------------------------------------------------+{- |+  Module     : Hunt.Query.Language.Parser+  Copyright  : Copyright (C) 2007, 2008 Timo B. Huebel+  License    : MIT++  Maintainer : Timo B. Huebel (tbh@holumbus.org)+  Stability  : experimental+  Portability: portable+  Version    : 0.2++  The Hunt query parser, based on the famous Parsec library.++  The parser implements a default syntax for the query grammar which exposes+  all possible query types and operators to the user.++  Syntax:++  [@AND@, @OR@, @AND NOT@] = combinatory queries++  [@!w@]                   = case sensitive prefix query e.g.: @!car@ or @!Car@++  [@~w@]                   = fuzzy word query            e.g.: @~car@ or @~cra@++  [@\"...\"@]              = phrase query, performs an exact search for a single word++  [@(...)@]                = brackets++  [@c:w@]                  = context sensitive queries   e.g.: @(who:Rudi Voeller)@++  [@c1,c2:w@]              = multi context queries       e.g.: @(content,who,title:Rudi Voeller)@++  [@\[... TO ...\]@]       = range queries               e.g.: @[2014-02-10 TO 2012-02-16]@++  [@w\^b@]                 = query boosting              e.g.: @toList OR toAscList^1.5@+-}+-- ----------------------------------------------------------------------------++module Hunt.Query.Language.Parser+  (+  -- * Parsing+  parseQuery+  )+where++import           Control.Applicative         hiding (many, (<|>))++import           Data.Text                   (Text)+import qualified Data.Text                   as T++import           Text.Parsec+import           Text.Parsec.String++import           Hunt.Query.Language.Builder+import           Hunt.Common.BasicTypes        (mkScore)+import           Hunt.Query.Language.Grammar++-- ------------------------------------------------------------++-- | Parse a query using the default syntax provided by the Hunt framework.+parseQuery :: String -> Either Text Query+parseQuery = result . parse query ""+  where+  result (Left err) = Left (T.pack . show $ err)+  result (Right q)  = Right q++-- | A query may always be surrounded by whitespace+query :: Parser Query+query+    = do spaces+         res <- orQuery'+         spaces >> eof+         return res++orQuery' :: Parser Query+orQuery'+    = do q1 <- andQuery'+         qs <- many (orOp1 >> andQuery')+         return $ qOrs (q1 : qs)+    where+      orOp1+          = try orOp'+          where+            orOp' = spaces >> string "OR" >> spaces1++andQuery' :: Parser Query+andQuery'+    = do q1 <- neighborQuery+         qs <- many $+               do op <- andOp1+                  q  <- neighborQuery+                  return (op, q)+         return $ foldl (\ res (op', q') -> op' res q') q1 qs+    where+      andOp1+          = try andNotOp'+            <|> try andOp'+          where+            andNotOp'+                = do spaces >> string "AND" >> spaces >> string "NOT" >> spaces1+                     return qAndNot+            andOp' = do spaces >> string "AND" >> spaces1+                        return qAnd+++neighborQuery :: Parser Query+neighborQuery+    = do q1 <- contextSeqQuery+         qs <- many $+               do op <- neiOp+                  q  <- contextSeqQuery+                  return (op, q)+         return $ foldl (\ res (op', q') -> op' res q') q1 qs+    where+      neiOp+          = try nextOp+            <|> try nearOp+            <|> try followOp+          where+            nextOp+                = do spaces >> string "++" >> spaces1+                     return qNext+            nearOp+                = do spaces >> string "NEAR" >> spaces+                     d <- read <$> many1 digit+                     spaces1+                     return $ qNear d++            followOp+                = do spaces >> string "FOLLOW" >> spaces+                     d <- read <$> many1 digit+                     spaces1+                     return $ qNear d++contextSeqQuery :: Parser Query+contextSeqQuery+    = do q1 <- boostQuery+         qs <- many $+               try (spaces1 >> boostQuery)+         return $ foldl qAnd q1 qs++boostQuery :: Parser Query+boostQuery+    = do q <- contextQuery+         tryBoost q+++-- | Parse a context query.+contextQuery :: Parser Query+contextQuery+    = do cs <- try contextSpec <|> return []+         q  <- primaryQuery+         case cs of+           [] -> return q+           _  -> return (QContext cs q)+    where+      contextSpec+          = do cs <- contexts+               spaces >> char ':' >> spaces+               return cs++primaryQuery :: Parser Query+primaryQuery+    = parQuery+      <|>+      rangeQuery+      <|>+      caseQuery+      <|>+      fuzzyQuery+      <|>+      noCaseQuery++-- | Parse a query surrounded by parentheses.+parQuery :: Parser Query+parQuery+    = do char '(' >> spaces+         q <- orQuery'+         spaces >> char ')'+         return q++-- | Parse a range query.+rangeQuery :: Parser Query+rangeQuery+    = do char '[' >> spaces+         l <- word+         spaces1 >> string "TO" >> spaces1+         u <- word+         spaces >> char ']' >> spaces+         return $ QRange (T.pack l) (T.pack u)++-- | Parse a case-sensitive query.+caseQuery :: Parser Query+caseQuery+    = do char '!' >> spaces+         ( phraseQuery qPhrase+           <|>+           wordQuery qWord+           <|>+           quotedWordQuery qWord )++-- | Parse a fuzzy query.+fuzzyQuery :: Parser Query+fuzzyQuery+    =do char '~' >> spaces+        ( wordQuery (setFuzzySearch . qWord)+          <|>+          quotedWordQuery (setFuzzySearch . qWord) )++noCaseQuery :: Parser Query+noCaseQuery+    = phraseQuery qPhraseNoCase+      <|>+      quotedWordQuery qWordNoCase+      <|>+      wordQuery qPrefixPhraseNoCase++-- | Parse a word query.+wordQuery :: (Text -> Query) -> Parser Query+wordQuery c = word >>= return . c . T.pack++quotedWordQuery :: (Text -> Query) -> Parser Query+quotedWordQuery c = quotedWord >>= return . c . T.pack++-- | Parse a phrase query.+phraseQuery :: (Text -> Query) -> Parser Query+phraseQuery c = phrase >>= return . c . T.pack++-- | Parse a word.+word :: Parser String+word = try $+       do w <- many1 (escapedChar <|> wordChar)+          if w `elem` ["OR", "AND", "++", "NEAR", "FOLLOW"]+            then parserZero+            else return w+    where+      wordChar :: Parser Char+      wordChar = noneOf notWordChar++-- | Parse an escape sequence. @\@ followed by the character, e.g. @\"@.+escapedChar :: Parser Char+escapedChar = char escapeChar *> decodeChar++-- | Parse a single valid escape character, e.g. @"@.+decodeChar :: Parser Char+decodeChar = choice (zipWith decode notWordChar notWordChar)+    where decode c r = r <$ char c++escaped :: Char -> Parser Char+escaped c+    = do char escapeChar+         (char c <|> return escapeChar)++-- | Parse a phrase.+phrase :: Parser String+phrase+    = do char '"'+         p <- many1 phraseChar+         char '"'+         return p+    where+      phraseChar+          = escaped '\"' <|> noneOf "\""++quotedWord :: Parser String+quotedWord+    = do char '\''+         p <- many1 quotedWordChar+         char '\''+         return p+    where+      quotedWordChar+          = escaped '\'' <|> noneOf "'"++-- | Parse a boosted query.+tryBoost :: Query -> Parser Query+tryBoost q = try boost <|> return q+  where+  boost = do+          char '^'+          b <- simplePositiveFloat+          return (QBoost (mkScore b) q)+++-- | Parse a list of contexts.+contexts :: Parser [Text]+contexts = context `sepBy1` char ','++-- | Parse a context.+context :: Parser Text+context = do spaces+             c <- many1 alphaNum+             spaces+             return (T.pack c)++-- | Parse at least on white space character.+spaces1 :: Parser ()+spaces1 = skipMany1 space++-- | Parse a simple positive number.+simplePositiveNumber :: Parser String+simplePositiveNumber = many1 digit++-- | Parse a simple positive float. The decimal point with following numbers is optional.+simplePositiveFloat :: Parser Float+simplePositiveFloat = fmap read $ simplePositiveNumber <++> decimal+  where decimal  = option "" $ char '.' <:> simplePositiveNumber++-- ------------------------------------------------------------++-- |Applicative concat @(++)@.+(<++>) :: Applicative f => f [a] -> f [a] -> f [a]+(<++>) a b = (++) <$> a <*> b++-- Applicative cons @(:)@.+(<:>) :: Applicative f => f a -> f [a] -> f [a]+(<:>) a b = (:) <$> a <*> b++-- ------------------------------------------------------------
+ src/Hunt/Query/Processor.hs view
@@ -0,0 +1,661 @@+{-# LANGUAGE ConstraintKinds            #-}+{-# LANGUAGE FlexibleContexts           #-}+{-# LANGUAGE FlexibleInstances          #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses      #-}+{-# LANGUAGE OverloadedStrings          #-}+{-# LANGUAGE RankNTypes                 #-}+{-# LANGUAGE TypeFamilies               #-}++-- ----------------------------------------------------------------------------+{- |+  The query processor to perform 'Query's.++  'processQuery' executes the query and generates the unranked result.+  The result can be ranked with the default 'Hunt.Query.Ranking.rank' function.+-}+-- ----------------------------------------------------------------------------++module Hunt.Query.Processor+    ( processQueryScoredDocs+    , processQueryUnScoredDocs+    , processQueryScoredWords+    , initProcessor++    , ProcessConfig (..)+    , ProcessEnv+    )++where++import           Control.Applicative+import           Control.Monad.Error+import           Control.Monad.Reader++import           Data.Binary                 (Binary)+import qualified Data.Binary                 as Bin+import           Data.Default+import qualified Data.List                   as L+import qualified Data.Map                    as M+import           Data.Maybe+import           Data.Text                   (Text)+import qualified Data.Text                   as T++import           Hunt.Common+import           Hunt.ContextIndex           (ContextMap)+import qualified Hunt.ContextIndex           as CIx+import           Hunt.Interpreter.Command    (CmdError (..))+import           Hunt.Query.Fuzzy            (FuzzyConfig)+import           Hunt.Query.Intermediate+import           Hunt.Query.Language.Grammar+import           Hunt.Utility                (showText)++import qualified System.Log.Logger           as Log++-- import           Debug.Trace++-- ------------------------------------------------------------+-- Logging+-- ------------------------------------------------------------++-- | Name of the module for logging purposes.+modName :: String+modName = "Hunt.Query.Processor"++-- | Log a message at 'DEBUG' priority.+debugM :: String -> IO ()+debugM = Log.debugM modName++-- ------------------------------------------------------------+-- Configuration and state for the query processor+-- ------------------------------------------------------------++-- | Query processor configuration.+data ProcessConfig+    = ProcessConfig+      { fuzzyConfig   :: ! FuzzyConfig -- ^ The configuration for fuzzy queries.+      , optimizeQuery :: ! Bool        -- ^ Optimize the query before processing (default: @False@).+      , wordLimit     :: ! Int         -- ^ The maximum number of words used from a prefix. @0@ = no limit (default: @100@).+      , docLimit      :: ! Int         -- ^ The maximum number of documents taken into account. @0@ = no limit (default: @500@).+      }++-- ------------------------------------------------------------++instance Default ProcessConfig where+    def = ProcessConfig def False 100 500++-- ------------------------------------------------------------++instance Binary ProcessConfig where+    put (ProcessConfig fc o l d)+        = Bin.put fc >> Bin.put o >> Bin.put l >> Bin.put d+    get+        = ProcessConfig <$> Bin.get <*> Bin.get <*> Bin.get <*> Bin.get++-- ------------------------------------------------------------++-- | The internal state of the query processor.+data ProcessEnv+    = ProcessEnv+      { psConfig   :: ! ProcessConfig  -- ^ The configuration for the query processor.+      , psContexts :: ! [Context]      -- ^ The current list of contexts.+      , psIndex    ::   ContextMap     -- ^ The index to search.+      }++-- ------------------------------------------------------------+-- Processor monad+-- ------------------------------------------------------------++type QueryIndex+    = ContextMap++-- | the processor monad+newtype ProcessorT m a+    = PT { runProcessor :: ReaderT ProcessEnv (ErrorT CmdError m) a+         }+      deriving (Applicative, Monad, MonadIO, Functor, MonadReader ProcessEnv, MonadError CmdError)++instance MonadTrans ProcessorT where+    lift = PT . lift . lift++type Processor+    = ProcessorT IO++-- ------------------------------------------------------------+-- Helper+-- ------------------------------------------------------------++queryError ::   Int -> Text -> Processor a+queryError n msg+    = throwError $ ResError n msg++getContexts ::   Processor [Context]+getContexts+    = asks psContexts++getIx :: Processor QueryIndex+getIx+    = asks psIndex++getSchema :: Processor Schema+getSchema+    = getIx >>= return . (M.map fst) . CIx.cxMap++-- | Get the schema associated with that context/index.+--+--   /Note/: This fails if the schema does not exist.++getContextSchema ::   Context -> Processor ContextSchema+getContextSchema c+    = do schema <- getSchema+         case M.lookup c schema of+           Just cs -> return cs+           _       -> queryError 420 ("Context does not exist in schema: " <> c)+++-- | Normalizes the search value with respect to the schema context type.+--   First runs the validator that throws an error for invalid values,+--   then runs the normalizers associated with the context.++normQueryCx :: Context -> Text -> Processor (Maybe Text)+normQueryCx c t+    = do s <- getContextSchema c+         -- apply context type validator+         if (validate . ctValidate . cxType $ s) t+           then+               do liftIO . debugM . debugMsg $ s+                  -- apply context schema normalizer+                  return . Just . norm $ s+           else+               return Nothing+    where+      norm s+          = normalize' (cxNormalizer s) t++      debugMsg s+          = T.unpack $ T.concat [ "query normalizer: ", c, ": [", t, "=>", norm s, "]"]++-- | Initialize the state of the processor.+initProcessor :: ProcessConfig -> QueryIndex -> ProcessEnv+initProcessor cfg ix+    = ProcessEnv cfg cxs ix+    where+      s = CIx.mapToSchema ix+      cxs = filter (\c -> fromMaybe False $ M.lookup c s >>= return . cxDefault)+            $ CIx.contexts ix++-- ------------------------------------------------------------++processQueryUnScoredDocs :: ProcessEnv -> Query -> IO (Either CmdError UnScoredDocs)+processQueryUnScoredDocs = processQueryScoredResult evalUnScoredDocs++-- | evaluate a query into a UnScoredDocs result+--+-- all info about contexts, words and positions and the score of docs+-- are removed by the aggregation.+--+-- This evaluator is called by commands which need to compute just a set of documents,+-- e.g. DeleteByQuery. When calling evalUnScoredDocs the 'ProcessEnv' value+-- should be configured such that the limit for document to taken into account+-- is set to infinity (represented as @0@ in the config), else the result set+-- may not be complete.+--+-- In that case it becomes easy to build a query witch acts as a denial of service+-- attack, because the intermediate results become too large to be processed.+-- So these queries must be used in applications rather carefully, e.g. a user should+-- not be able to construct these types of queries by filling in some input fields in a web interface.++evalUnScoredDocs :: Query -> Processor UnScoredDocs+evalUnScoredDocs q+    | isPrimaryQuery q+        = do res <- forallCx (evalPrimary q)+             aggregateToScoredResult res++evalUnScoredDocs (QRange lb ub)+    = do res <- forallCx (evalRange lb ub)+         aggregateToScoredResult res++evalUnScoredDocs (QSeq op qs)+    | isLocalCxOp op+        = do res <- forallCxLocal+                    ( evalSeq' op                     -- do the position aware operation+                      <$> mapM evalScoredRawDocs qs ) -- switch to the raw evaluator due to positions+             aggregateToScoredResult res              -- and aggregate res to UnScoredDocs+    | otherwise+        = evalSeq op                                  -- do the result combination+          <$> mapM evalUnScoredDocs qs                  -- for the args stay in evaluator++evalUnScoredDocs (QContext cxs q)+    = withCxs cxs $ evalUnScoredDocs q++evalUnScoredDocs (QBoost _w q)+    = evalUnScoredDocs q++evalUnScoredDocs q@QPhrase{}              -- QPhrase is transformed into QFullWord or QSeq Phrase+    = normQuery q >>= evalUnScoredDocs++evalUnScoredDocs q@QBinary{}              -- QBin is transformed into QSeq+    = normQuery q >>= evalUnScoredDocs++evalUnScoredDocs q+    = queryEvalError q++-- ------------------------------------------------------------++processQueryScoredDocs :: ProcessEnv -> Query -> IO (Either CmdError ScoredDocs)+processQueryScoredDocs = processQueryScoredResult evalScoredDocs'++-- | evaluate a query into a ScoredDocs result+--+-- all info about contexts, words and positions is removed by the aggregation,+-- just a set of DocIds and associated scores is computed++evalScoredDocs :: Query -> Processor ScoredDocs+evalScoredDocs q+    | isPrimaryQuery q+        = do res <- forallCx (evalPrimary q)+             aggregateToScoredResult res++evalScoredDocs (QRange lb ub)+    = do res <- forallCx (evalRange lb ub)+         aggregateToScoredResult res++evalScoredDocs (QSeq op qs)+    | isLocalCxOp op+        = do res <- forallCxLocal+                    ( evalSeq' op                     -- do the position aware operation+                      <$> mapM evalScoredRawDocs' qs ) -- switch to the raw evaluator due to positions+             aggregateToScoredResult res                -- and aggregate res to ScoredDocs+    | otherwise+        = evalSeq op                                  -- do the result combination+          <$> mapM evalScoredDocs' qs                  -- for the args stay in evaluator++evalScoredDocs (QContext cxs q)+    = withCxs cxs $ evalScoredDocs' q++evalScoredDocs (QBoost w q)+    = boost w <$> evalScoredDocs' q++evalScoredDocs q@QPhrase{}              -- QPhrase is transformed into QFullWord or QSeq Phrase+    = normQuery q >>= evalScoredDocs++evalScoredDocs q@QBinary{}              -- QBin is transformed into QSeq+    = normQuery q >>= evalScoredDocs++evalScoredDocs q+    = queryEvalError q++-- --------------------+-- {- switch off trace++evalScoredDocs' :: Query -> Processor ScoredDocs+evalScoredDocs' = evalScoredDocs+{-# INLINE evalScoredDocs' #-}++-- -}+{- switch on trace the evaluation++evalScoredDocs' :: Query -> Processor ScoredDocs+evalScoredDocs' q = trc <$> evalScoredDocs q+    where+      trc res = traceShow ("evalScoredDocs: "::String,q  ) $+                traceShow ("evalScoredDocs: "::String,res) $ res+-- -}+-- ------------------------------------------------------------++processQueryScoredWords :: ProcessEnv -> Query -> IO (Either CmdError ScoredWords)+processQueryScoredWords = processQueryScoredResult evalScoredWords'++-- | evaluate a query into a ScoredWords result+--+-- all info about contexts, words and positions is removed by the aggregation,+-- just a set of words and associated scores is computed.+--+-- the words found are suggestions for the last primitive prefix in the query++evalScoredWords :: Query -> Processor ScoredWords+evalScoredWords q+    | isPrimaryQuery q+        = do res <- forallCx (evalPrimary q)+             aggregateToScoredResult res++evalScoredWords (QRange lb ub)+    = do res <- forallCx (evalRange lb ub)+         aggregateToScoredResult res++evalScoredWords (QSeq Or qs)                           -- for completions just search+    = evalScoredWords' (last qs)                       -- for rightmost subquery++evalScoredWords (QSeq AndNot qs)                       -- for completions just search+    = evalScoredWords' (last qs)                       -- for rightmost subquery++evalScoredWords (QSeq And qs)                          -- for completions+    = do docs <- evalUnScoredDocs  (mkQ $ init qs)     -- eval the set of docs for all but the last q+         res  <- evalScoredRawDocs (      last qs)     -- eval the last q as ScoredRawDos+         aggregateToScoredResult                       -- aggregate to ScoredWords+           $ fmap (filterByDocSet docs) res            -- restrict result to docs found+      where+        mkQ [q'] = q'+        mkQ qs'  = QSeq And qs'++evalScoredWords (QSeq op qs)+    | isLocalCxOp op+        = do res <- forallCxLocal+                    ( evalSeq' op                      -- do the position aware operation+                      <$> mapM evalScoredRawDocs' qs ) -- switch to the raw evaluator due to positions+             aggregateToScoredResult res               -- and aggregate res to ScoredWords++evalScoredWords (QContext cxs q)+    = withCxs cxs $ evalScoredWords' q++evalScoredWords (QBoost w q)+    = boost w <$> evalScoredWords' q++evalScoredWords q@QPhrase{}              -- QPhrase is transformed into QFullWord or QSeq Phrase+    = normPhraseQuery q >>= evalScoredWords++evalScoredWords q@QBinary{}              -- QBin is transformed into QSeq+    = normQuery q >>= evalScoredWords++evalScoredWords q+    = queryEvalError q++-- --------------------+-- {- switch off trace++evalScoredWords' :: Query -> Processor ScoredWords+evalScoredWords' = evalScoredWords+{-# INLINE evalScoredWords' #-}++-- -}+{- switch on trace of evaluation++evalScoredWords' :: Query -> Processor ScoredWords+evalScoredWords' q = trc <$> evalScoredWords q+    where+      trc res = traceShow ("evalScoredWords: "::String,q  ) $+                traceShow ("evalScoredWords: "::String,res) $ res+-- -}+-- ------------------------------------------------------------++-- | evaluate a query into a context aware raw docs result+--+-- This evaluator is called from 'evalScoredDocs', 'evalUnScoredDocs' and 'evalScoredWords'+-- in the case of Phrase-, Follow- and Near-queries.+--+-- No info about contexts, words and positions is removed by the aggregation.+-- 'evalScoredRawDocs' always runs in a single context at a time,+-- because the position information does not have any meaning across contexts+--+-- Therefore QContext subqueries become meaningless within Phrase-, Follow- and Near-queries++evalScoredRawDocs :: Query -> Processor (ScoredCx ScoredRawDocs)+evalScoredRawDocs q+    | isPrimaryQuery q+        = forallCx (evalPrimary q)++evalScoredRawDocs (QRange lb ub)+    = forallCx (evalRange lb ub)++evalScoredRawDocs (QSeq op qs)+    = evalSeq' op+      <$> mapM evalScoredRawDocs' qs++evalScoredRawDocs (QContext cxs q)+    = restrictCxs cxs $ evalScoredRawDocs' q++evalScoredRawDocs (QBoost w q)+    = boost w <$> evalScoredRawDocs' q++evalScoredRawDocs q@QPhrase{}              -- QPhrase is transformed into QFullWord or QSeq Phrase+    = normQuery q >>= evalScoredRawDocs++evalScoredRawDocs q@QBinary{}              -- QBin is transformed into QSeq+    = normQuery q >>= evalScoredRawDocs++evalScoredRawDocs q+    = queryEvalError q++-- --------------------+-- {- switch off trace++evalScoredRawDocs' :: Query -> Processor (ScoredCx ScoredRawDocs)+evalScoredRawDocs' = evalScoredRawDocs+{-# INLINE evalScoredRawDocs' #-}++-- -}+{- switch on trace of evaluation++evalScoredRawDocs' :: Query -> Processor (ScoredCx ScoredRawDocs)+evalScoredRawDocs' q = trc <$> evalScoredRawDocs q+    where+      trc res = traceShow ("evalScoredRawDocs: "::String,q  ) $+                traceShow ("evalScoredRawDocs: "::String,res) $ res+-- -}+-- ------------------------------------------------------------++-- run a query evaluation++processQueryScoredResult :: (q -> Processor r) -> ProcessEnv -> q -> IO (Either CmdError r)+processQueryScoredResult eval st q+    = runErrorT . runReaderT (runProcessor $ eval q) $ st+++-- ------------------------------------------------------------+--+-- query normalization: transform "old" queries into generalized new form++normQuery :: Query -> Processor Query+normQuery (QPhrase op w)+    = return . QSeq Phrase . L.map (QFullWord op) $ T.words w++normQuery q@(QBinary op _q1 _q2)+    = return . QSeq op . collect op q $ []+      where+        collect+            | isAssocOp     op = collectAssoc+            | isLeftAssocOp op = collectLeftAssoc+            | otherwise        = \ _op q' -> (q':)++normQuery q+    = return q+++normPhraseQuery :: Query -> Processor Query+normPhraseQuery (QPhrase op w)+    = return . QSeq Phrase $ normPhrase (T.words w) []+    where+      normPhrase (x:[]) r = r ++ [QWord op x]+      normPhrase (x:xs) r = normPhrase xs $+                            r ++ [QFullWord op x]+      normPhrase []     r = r+normPhraseQuery q+    = return q++++collectAssoc :: BinOp -> Query -> [Query] -> [Query]+collectAssoc op (QBinary op' q1 q2)+    | op == op'+        = collectAssoc op q1 . collectAssoc op q2+collectAssoc _ q+    = (q :)++collectLeftAssoc :: BinOp -> Query -> [Query] -> [Query]+collectLeftAssoc op (QBinary op' q1 q2)+    | op == op'+        = collectAssoc op q1 . (q2 :)+collectLeftAssoc _ q+    = (q :)++isAssocOp :: BinOp -> Bool+isAssocOp AndNot = False+isAssocOp _      = True++isLeftAssocOp :: BinOp -> Bool          -- currently AndNot is left assoc+isLeftAssocOp = not . isAssocOp         -- all others are assoc ops++isLocalCxOp :: BinOp -> Bool+isLocalCxOp Phrase   = True+isLocalCxOp Follow{} = True+isLocalCxOp Near{}   = True+isLocalCxOp _        = False++-- ------------------------------------------------------------++-- eval query combinators++evalSeq :: (ScoredResult r) => BinOp -> [r] -> r+evalSeq Or     = evalOr+evalSeq And    = evalAnd+evalSeq AndNot = evalAndNot+evalSeq _op    = const mempty++evalSeq' :: BinOp -> [ScoredCx ScoredRawDocs] -> ScoredCx ScoredRawDocs+evalSeq' Phrase     = evalSequence id+evalSeq' (Follow d) = evalFollow   id d+evalSeq' (Near   d) = evalNear     id d+evalSeq' op         = evalSeq op++-- | restrict the current context for a query+--+-- used in evaluating phrase-, follow- and near- queries+-- there it's meaningless to use other than the current contexts++restrictCxs :: [Context] -> Processor r -> Processor r+restrictCxs cxs p+    = do cxs0 <- getContexts+         withCxs cxs $                                           -- check for legal contexts+                 local (setCx $ cxs0 `L.intersect` L.nub cxs) p  -- restrict contexts to cxs+    where+      setCx cxs' cfg+          = cfg {psContexts = cxs'}++-- | set the context for a part of a query evaluation++withCxs :: [Context] -> Processor r -> Processor r+withCxs cxs p+    = do icxs <- invalidContexts <$> getSchema+         if L.null icxs+            then local setCx $ p+            else queryError 404+                 $ "mentioned context(s) do not exist: "+                   <> showText icxs+    where+      invalidContexts sc+          = filter (\ c -> not (c `M.member` sc)) cxs+      setCx cfg+          = cfg {psContexts = L.nub cxs}++-- | execute a command (query) for all contexts and give the current context+-- as extra argument++forallCx :: (ScoredResult r) => (Context -> Processor r) -> Processor r+forallCx action+    = do cxs <- getContexts+         res <- mapM action cxs+         return $ mconcat res++-- | execute a command for all contexts, but restrict the set of contexts+-- to the current context before executing the command+--+-- So the loop over all contexts (forallCx) called in the evaluation of primary+-- queries is switched off++forallCxLocal :: (ScoredResult r) => Processor r -> Processor r+forallCxLocal action+    = do cxs <- getContexts+         res <- mapM f' cxs+         return $ mconcat res+    where+      f' cx+          = withCxs [cx] action++isPrimaryQuery :: Query -> Bool+isPrimaryQuery QWord{}     = True+isPrimaryQuery QFullWord{} = True+isPrimaryQuery _           = False++queryEvalError :: Query -> Processor r+queryEvalError q+    = queryError 501 $ "Hunt.Query.Processor: query can't be evaluated " <> showText q++-- ------------------------------------------------------------++aggregateToScoredResult :: (Aggregate a (ScoredCx b), ScoredResult b) =>+                           a -> ProcessorT IO b+aggregateToScoredResult res+    = do cxScores <- contextWeights <$> getSchema+         return $ boostAndAggregateCx cxScores (aggregate res)+++-- eval basic queries++evalPrimary :: Query -> Context -> Processor (ScoredCx ScoredRawDocs)+evalPrimary (QWord QCase w) cx+    = searchCx PrefixCase w cx++evalPrimary (QWord QNoCase w) cx+    = searchCx PrefixNoCase w cx++evalPrimary (QWord QFuzzy w) cx         -- TODO: QFuzzy is processed like nocase search+    = searchCx PrefixNoCase w cx++evalPrimary (QFullWord QCase w) cx+    = searchCx Case w cx++evalPrimary (QFullWord QNoCase w) cx+    = searchCx NoCase w cx++evalPrimary (QFullWord QFuzzy w) cx     -- TODO: QFuzzy is processed like nocase search+    = searchCx NoCase w cx++evalPrimary q _cx+    = queryError 501 $ "evalPrimary: not a primary query: " <> showText q++searchCx :: TextSearchOp -> Word -> Context -> Processor (ScoredCx ScoredRawDocs)+searchCx op w' cx+    = do mw <- normQueryCx cx w'                         -- normalize the word with respect to context+         case mw of+           Nothing  -> return $ fromCxRawResults []      -- if normalization not possible, return empty result+           (Just w) -> searchCx' w+    where+      searchCx' w+        = do+          limit <- asks (docLimit . psConfig)            -- get the max. # of docs+          ix    <- getIx                                 -- get the context search index+          rawr  <- limitRawResult limit+                   <$> CIx.searchWithCxSc op cx w ix     -- do the real search and limit result+          return $ fromCxRawResults [(cx, rawr)]+                                                         -- convert the result to a ScoredResult+                                                         -- the score comes from a similarity test++evalRange :: Word -> Word -> Context -> Processor (ScoredCx ScoredRawDocs)+evalRange lb0 ub0 cx+    = do mlb    <- normQueryCx cx lb0                         -- normalize the word with respect to context+         mub    <- normQueryCx cx ub0                         -- normalize the word with respect to context+         case (mlb, mub) of+           (Just lb, Just ub) -> evalRange' lb ub+           _                  -> return $ fromCxRawResults [] -- if one of the words fails validation, return empty result+    where+      evalRange' lb ub+        = do+          limit <- asks (docLimit . psConfig)+          ix    <- getIx+          rawr  <- limitRawResult limit+                   <$> CIx.lookupRangeCxSc cx lb ub ix+          return $ fromCxRawResults [(cx, rawr)]++-- ------------------------------------------------------------++{-+-- | Log a message at 'WARNING' priority.+warningM :: String -> IO ()+warningM = Log.warningM modName++-- | Log a message at 'ERROR' priority.+errorM :: String -> IO ()+errorM = Log.errorM modName+-}++-- ------------------------------------------------------------
+ src/Hunt/Query/Ranking.hs view
@@ -0,0 +1,231 @@+-- ----------------------------------------------------------------------------+{- |+  Module     : Hunt.Query.Ranking+  Copyright  : Copyright (C) 2007, 2008 Timo B. Huebel+  License    : MIT++  Maintainer : Timo B. Huebel (tbh@holumbus.org)+  Stability  : experimental+  Portability: portable+  Version    : 0.3++  The ranking mechanism for Hunt.++  Customized ranking functions for both documents and suggested words can be+  provided by the user. Some predefined ranking functions are avaliable, too.++-}+-- ----------------------------------------------------------------------------++module Hunt.Query.Ranking+  (+  -- * Ranking types+    RankConfig (..)+  , DocRanking+  , WordRanking+  , ContextWeights++  -- * Ranking+  , rank++  -- * Predefined document rankings+  , docRankByCount+  --, docRankWeightedByCount++  -- * Predefined word rankings+  , wordRankByCount+  , wordRankBySimilarity+  --, wordRankWeightedByCount++  , defaultRankConfig+  )+where++import           Prelude               hiding (foldr)++import           Control.Applicative++--import           Data.Foldable+--import           Data.Function+import           Data.Maybe++import qualified Data.List             as L+import           Data.Map              (Map)+import qualified Data.Map              as M+import           Data.Text             (Text)+import qualified Data.Text             as T++import           Hunt.Common+import qualified Hunt.Common.DocIdMap  as DM+import           Hunt.Common.Document  (DocumentWrapper (..))+import           Hunt.Common.Positions as Pos+import           Hunt.DocTable         as Dt+import           Hunt.Query.Result++import           Hunt.Utility++-- import           Debug.Trace++-- ------------------------------------------------------------++-- | The configuration of the ranking mechanism.+data RankConfig e+  = RankConfig+    { docRanking  :: DocRanking e   -- ^ A function to determine the score of a document.+    , wordRanking :: WordRanking    -- ^ A function to determine the score of a word.+    }++-- | The signature of a function to determine the score of a document.+type DocRanking e = ContextWeights -> DocId -> Score -> DocInfo e -> DocContextHits -> Score++-- TODO: add ContextWeights+-- | The signature of a function to determine the score of a word.+type WordRanking  = Word -> WordInfo -> WordContextHits -> Score++-- | Weights for the contexts (optional).+type ContextWeights = Map Context Weight++-- ------------------------------------------------------------++-- | The configuration of the ranking mechanism.+defaultRankConfig :: DocumentWrapper e => RankConfig e+defaultRankConfig+    = RankConfig+      { docRanking  = docRankByCount+      , wordRanking = wordRankBySimilarity+      -- , wordRanking = wordRankByCount+      }++-- ------------------------------------------------------------++-- | Rank the result with custom ranking functions (and the given context weights).++rank :: (DocTable dt, Monad m, Applicative m) =>+        RankConfig e -> dt -> ContextWeights -> Result e -> m (Result e)++rank (RankConfig fd fw {-ld lw-}) dt cw r+  = do sdh <- scoredDocHits+       return $ Result sdh scoredWordHits+  where+    scoredDocHits+        = DM.traverseWithKey+          (\k (di, dch) ->+               do+                 kw <- maybe defScore (wght . unwrap) <$> Dt.lookup k dt+                 return $ ( setDocScore (fd cw k kw di dch) di+                          , dch+                          )+          ) $ docHits r++    scoredWordHits+        = M.mapWithKey+          ( \ k (WIH wi wch) -> WIH (setWordScore (fw k wi wch) wi) wch )+          $ wordHits r++-- ------------------------------------------------------------++-- | Rank documents by count and multiply occurrences with+--   their respective context weights (default @1.0@).+--   Pass an empty map to discard context weights.+--+-- @docRankByCount :: ContextWeights -> DocId -> Score -> DocInfo e -> DocContextHits -> Score@++docRankByCount :: DocRanking e+docRankByCount cw _ docWeight di h+    = docBoost di * docWeight * scHits+    where+      scHits = M.foldrWithKey+               (\cx -> (cxw cx *)+                       .::+                       flip (M.foldr (\h2 r2 -> mkScore (fromIntegral (Pos.size h2)) + r2))+               ) noScore h++      cxw cx = fromMaybe defScore $ M.lookup cx cw++-- | Rank words by count.+--+-- @wordRankByCount :: Word -> WordInfo -> WordContextHits -> Score@+wordRankByCount :: WordRanking+wordRankByCount _w _i h+  = countHits h++wordRankBySimilarity :: WordRanking+wordRankBySimilarity wordFound (WordInfo searchTerms sc) wch+    = cnt * toDefScore sc * simBoost+    where+      cnt = countHits wch++      simBoost :: Score+      simBoost+          | L.null searchTerms            -- make simBoost total+              = noScore+          | otherwise+              = maximum (L.map (similar wordFound) searchTerms)++      similar :: Text -> Text -> Score+      similar found searched+          | searched == found+              = boostExactHit+          | ls == lf+              = boostSameLength+          | ls < lf                     -- reduce score by length of found word+              = fromIntegral ls / fromIntegral lf+          | otherwise                   -- make similar total+              = noScore+          where+            boostExactHit   = 10.0+            boostSameLength =  5.0      -- NoCase hits++            ls = T.length searched+            lf = T.length found++countHits :: WordContextHits -> Score+countHits wch+    = M.foldr op 0.0 wch+    where+      op x res = DM.foldr (+) 0.0 x + res++-- ------------------------------------------------------------++-- The old weighting mechanism+--   - used a list of context weights+--   - normalizes the weights to a maximum of 1.0+--     - not sure if this is really necessary or the users responsibility+--     - the computation should be done /once/ every time the weights are set, not with every query+--   - seems overall cumbersome++{-+-- | Rank documents by context-weighted count. The weights will be normalized to a maximum of 1.0.+--   Contexts with no weight (or a weight of zero) will be ignored.+docRankWeightedByCount :: [(Context, Score)] -> DocId -> DocInfo e -> DocContextHits -> Score+docRankWeightedByCount ws _ _+  =  M.foldrWithKey (calcWeightedScore ws) 0.0++-- | Rank words by context-weighted count. The weights will be normalized to a maximum of 1.0.+--   Contexts with no weight (or a weight of zero) will be ignored.+wordRankWeightedByCount :: [(Context, Score)] -> Word -> WordInfo -> WordContextHits -> Score+wordRankWeightedByCount ws _ _+  = M.foldrWithKey (calcWeightedScore ws) 0.0++-- | Calculate the weighted score of occurrences of a word.+calcWeightedScore :: Foldable f => [(Context, Score)] -> Context -> f Positions -> Score -> Score+calcWeightedScore ws c h r+  = maybe r (\w -> r + ((w / mw) * count)) $ lookupWeight c ws+  where+  count = fromIntegral $ foldl' (flip $ (+) . Pos.size) 0 h+  mw    = snd $ L.maximumBy (compare `on` snd) ws++-- | Find the weight of a context in a list of weights. If the context was not found or it's+--   weight is equal to zero, 'Nothing' will be returned.+lookupWeight :: Context -> [(Context, Score)] -> Maybe Score+lookupWeight _ []     = Nothing+lookupWeight c (x:xs)+  = if fst x == c+    then+      if snd x /= 0.0+      then Just (snd x)+      else Nothing+    else lookupWeight c xs+-}++-- ------------------------------------------------------------
+ src/Hunt/Query/Result.hs view
@@ -0,0 +1,178 @@+{-# LANGUAGE StandaloneDeriving #-}++-- ----------------------------------------------------------------------------+{- |+  Module     : Hunt.Query.Result+  Copyright  : Copyright (C) 2007 Timo B. Huebel+  License    : MIT++  Maintainer : Timo B. Huebel (tbh@holumbus.org)+  Stability  : experimental+  Portability: portable++  The data type for results of Holumbus queries.++  The result of a query is defined in terms of two partial results,+  the documents containing the search terms and the words which+  are possible completions of the search terms.+-}+-- ----------------------------------------------------------------------------++module Hunt.Query.Result+  (+  -- * Result data types+    Result(..)+  , DocHits+  , DocContextHits+  , DocWordHits+  , WordHits+  , WordContextHits+  , WordDocHits+  , DocInfo(..)+  , WordInfo(..)+  , WordInfoAndHits(..)+  , Score+  , Weight+  , Boost+  , DocBoosts++  -- * Construction+  , emptyResult++  -- * Query+  , null+  , sizeDocHits+  , sizeWordHits+  , maxScoreDocHits+  , maxScoreWordHits+  , getDocuments++  -- * Transform+  , setDocScore+  , setWordScore+  )+where++import           Prelude              hiding (null)++import qualified Data.List            as L+import           Data.Map             (Map)+import qualified Data.Map             as M+import           Data.Text            (Text)++import           Hunt.Common+import qualified Hunt.Common.DocIdMap as DM++-- ------------------------------------------------------------++-- | The combined result type for Holumbus queries.+data Result e+  = Result+    { docHits  :: DocHits e  -- ^ The documents matching the query.+    , wordHits :: WordHits   -- ^ The words which are completions of the query terms.+    }+deriving instance Show e => Show (Result e)+++-- | Information about an document.+data DocInfo e+    = DocInfo+      { document :: e      -- ^ The document itself.+      , docBoost :: Weight -- ^ The document weight, init with @1.0@+      , docScore :: Score  -- ^ The score for the document (initial score for all documents is @0.0@).+      }++instance Show (DocInfo e) where+    show (DocInfo _d b s) = "DocInfo d " ++ show b ++ " " ++ show s++-- | Information about a word.+data WordInfo+    = WordInfo+      { terms     :: Terms -- ^ The search terms that led to this very word.+      , wordScore :: Score -- ^ The frequency of the word in the document for a context.+      }+    deriving (Eq, Show)++instance Monoid WordInfo where+    mempty+        = WordInfo [] noScore+    mappend (WordInfo t1 s1) (WordInfo t2 s2)+        = WordInfo (t1 `L.union` t2) (s1 <> s2)++data WordInfoAndHits+    = WIH WordInfo WordContextHits+      deriving (Eq, Show)++instance Monoid WordInfoAndHits where+    mempty+        = WIH mempty M.empty+    mappend (WIH wi1 wh1) (WIH wi2 wh2)+        = WIH (wi1 <> wi2) (M.unionWith (DM.unionWith (+)) wh1 wh2)++-- XXX: a list for now - maybe useful for testing+-- | Boosting of a single document.+type Boost           = Score++-- | Document boosting.+type DocBoosts       = DocIdMap Score++-- | A mapping from a document to it's score and the contexts where it was found.+type DocHits e       = DocIdMap (DocInfo e, DocContextHits)++-- | A mapping from a context to the words of the document that were found in this context.+type DocContextHits  = Map Context DocWordHits++-- | A mapping from a word of the document in a specific context to it's positions.+type DocWordHits     = Map Word Positions++-- | A mapping from a word to it's score and the contexts where it was found.+type WordHits        = Map Word WordInfoAndHits++-- | A mapping from a context to the documents that contain the word that were found in this context.+type WordContextHits = Map Context WordDocHits++-- | A mapping from a document containing the word to the positions of the word.+type WordDocHits     = DocBoosts++-- | The original search terms entered by the user.+type Terms           = [Text]++-- ------------------------------------------------------------++-- | Create an empty result.+emptyResult :: Result e+emptyResult = Result DM.empty M.empty++-- | Query the number of documents in a result.+sizeDocHits :: Result e -> Int+sizeDocHits = DM.size . docHits++-- | Query the number of documents in a result.+sizeWordHits :: Result e -> Int+sizeWordHits = M.size . wordHits++-- | Query the maximum score of the documents.+maxScoreDocHits :: Result e -> Score+maxScoreDocHits = DM.foldr (\(di, _) r -> max (docScore di) r) 0.0 . docHits++-- | Query the maximum score of the words.+maxScoreWordHits :: Result e -> Score+maxScoreWordHits = M.foldr (\ (WIH wi _) r -> max (wordScore wi) r) 0.0 . wordHits++-- | Test if the result contains anything.+null :: Result e -> Bool+null = DM.null . docHits++-- | Set the score in a document info.+setDocScore :: Score -> DocInfo e -> DocInfo e+setDocScore s di = di { docScore = s }++-- | Set the score in a word info.+setWordScore :: Score -> WordInfo -> WordInfo+setWordScore s wi = wi { wordScore = s }++-- | Extract all documents from a result.+getDocuments :: Result e -> [e]+getDocuments r = map (document . fst . snd) . DM.toList $ docHits r++-- ------------------------------------------------------------
+ src/Hunt/Utility.hs view
@@ -0,0 +1,234 @@+{-# LANGUAGE BangPatterns   #-}+{-# LANGUAGE EmptyDataDecls #-}++-- ----------------------------------------------------------------------------+{- |+  General utitlity functions.+-}+-- ----------------------------------------------------------------------------++module Hunt.Utility+  (+    -- * Function Composition+    (.::), (.:::)++    -- * Safe / Total Functions+  , head'++    -- * Strict Functions+  , foldM'++    -- * Monadic Functions+  , foldlWithKeyM, foldrWithKeyM+  , whenM++    -- * Maybe Helper+  , catMaybesSet++    -- * Either Helper+  , isLeft, isRight+  , fromLeft, fromRight++    -- * List Helper+  , unbox, unboxM, isSingleton+  , split+  , partitionListByLength, partitionListByCount+  , descending++    -- * String Helper+  , strip, stripl, stripr, stripWith+  , escape++    -- * Text Helper+  , showText++    -- * Aeson Helper+  , object', (.=?), (.==), (.\.)++  -- * Types+  , TypeDummy+  )+where++import           Control.Monad    (when)++import           Data.Aeson       hiding (decode)+import           Data.Aeson.Types+import           Data.Char+import qualified Data.Foldable    as FB+import qualified Data.List        as L+import           Data.Map         (Map)+import qualified Data.Map         as M+import           Data.Maybe       (fromJust)+import           Data.Set         (Set)+import qualified Data.Set         as S+import           Data.Text        (Text)+import qualified Data.Text        as T++import           Numeric          (showHex)++-- ------------------------------------------------------------++-- | A dummy type without constructor.+data TypeDummy++-- ------------------------------------------------------------++showText :: Show a => a -> Text+showText = T.pack . show++-- | The boob operator.+(.::) :: (c -> d) -> (a -> b -> c) -> a -> b -> d+(.::) = (.).(.)++-- | The Total Recall operator.+(.:::) :: (d -> e) -> (a -> b -> c -> d) -> a -> b -> c -> e+(.:::) = (.).(.).(.)++-- | Safe 'head'.+head' :: [a] -> Maybe a+head' xs = if null xs then Nothing else Just $ head xs++-- | 'Data.Maybe.catMaybes' on a 'Set' instead of a List.+catMaybesSet :: Ord a => Set (Maybe a) -> [a]+catMaybesSet = S.toList . S.map fromJust . S.delete Nothing++-- | Test if 'Either' is a 'Left' value.+isLeft :: Either a b -> Bool+isLeft = either (const True) (const False)++-- | Test if 'Either' is a 'Right' value.+isRight :: Either a b -> Bool+isRight = not . isLeft++-- | Unwraps a 'Left' value - fails on 'Right'.+fromLeft :: Either a b -> a+fromLeft = either id (error "Hunt.Utility.fromLeft: Right")++-- | Unwraps a 'Right' value - fails on 'Left'.+fromRight :: Either a b -> b+fromRight = either (error "Hunt.Utility.fromRight: Left") id++-- | Unbox a singleton.+--+--   /Note/: This fails if the list is not a singleton.+unbox :: [a] -> a+unbox [e] = e+unbox _   = error "unbox: []"++-- | Unbox a singleton in a safe way with 'Maybe'.+unboxM :: [a] -> Maybe a+unboxM [e] = Just e+unboxM _   = Nothing++-- | Test if the list contains a single element.+isSingleton :: [a] -> Bool+isSingleton [_] = True+isSingleton _   = False++-- | Split a string into seperate strings at a specific character sequence.+split :: Eq a => [a] -> [a] -> [[a]]+split _ []       = [[]]+split at w@(x:xs) = maybe ((x:r):rs) ((:) [] . split at) (L.stripPrefix at w)+                    where (r:rs) = split at xs++-- | Removes leading and trailing whitespace from a string.+strip :: String -> String+strip = stripWith isSpace++-- | Removes leading whitespace from a string.+stripl :: String -> String+stripl = dropWhile isSpace++-- | Removes trailing whitespace from a string.+stripr :: String -> String+stripr = reverse . dropWhile isSpace . reverse++-- | Strip leading and trailing elements matching a predicate.+stripWith :: (a -> Bool) -> [a] -> [a]+stripWith f = reverse . dropWhile f . reverse . dropWhile f++-- | partition the list of input data into a list of input data lists of+--   approximately the same specified length+partitionListByLength :: Int -> [a] -> [[a]]+partitionListByLength _ [] = []+partitionListByLength count l  = take count l : partitionListByLength count (drop count l)++-- | partition the list of input data into a list of a specified number of input data lists with+--   approximately the same length+partitionListByCount :: Int -> [a] -> [[a]]+partitionListByCount = partition+  where+  partition 0 _ = []+  partition sublists l+    = let next = (length l `div` sublists)+      in  if next == 0  then [l]+                        else take next l : partition (sublists -1) (drop next l)++-- | To use with 'sortBy'.+descending :: Ord a => a -> a -> Ordering+descending = flip compare++-- | Escapes non-alphanumeric or space characters in a String+escape :: String -> String+escape []     = []+escape (c:cs)+  = if isAlphaNum c || isSpace c+      then c : escape cs+      else '%' : showHex (fromEnum c) "" ++ escape cs++-- | Strict version of 'foldM'.+foldM' :: (Monad m) => (a -> b -> m a) -> a -> [b] -> m a+foldM' _ acc [] = return acc+foldM' f acc (x:xs) = do+  !acc' <- f acc x+  foldM' f acc' xs++-- | 'FB.foldrM' for 'Map' with key.+foldrWithKeyM :: (Monad m) => (k -> a -> b -> m b) -> b -> Map k a -> m b+foldrWithKeyM f b = FB.foldrM (uncurry f) b . M.toList++-- | 'FB.foldlM' for 'Map' with key.+foldlWithKeyM :: (Monad m) => (b -> k -> a -> m b) -> b -> Map k a -> m b+foldlWithKeyM f b = FB.foldlM f' b . M.toList+  where f' a = uncurry (f a)++-- | Monadic 'when'.+whenM :: Monad m => m Bool -> m () -> m ()+whenM bm f = bm >>= \b -> when b f++-- ------------------------------------------------------------+-- Aeson helper+-- ------------------------------------------------------------++-- | Like 'Data.Aeson.object' to use with '.=?', '.==' and '.\.'.+--   This allows omitting (default) values in 'ToJSON' instances.+--+-- > toJSON (Example maybe list float) = object' $+-- >   [ "maybe" .=? maybe .\. isNothing+-- >   , "list"  .=? list  .\. null+-- >   , "float" .=? float .\. (== 1.0)+-- >   ]+object' :: [[Pair]] -> Value+object' = object . Prelude.concat++-- | Like '.=' but allows omitting of (default) values in 'ToJSON' instances.+--+--   See 'object''.+(.=?) :: ToJSON a => Text -> (a, a -> Bool) -> [Pair]+name .=? (value, cond) = if cond value then [] else [ name .= value ]++-- | '.==' for 'object''.+--+--   See 'object''.+(.==) :: ToJSON a => Text -> a -> [Pair]+name .== value = [ name .= value ]++-- | Use with '.=?' to specify which value to omit.+--+--   See 'object''.+(.\.) :: ToJSON a => a -> (a -> Bool) -> (a, a -> Bool)+v .\. c = (v,c)+infixl 8 .=?++-- ------------------------------------------------------------
+ src/Hunt/Utility/Log.hs view
@@ -0,0 +1,20 @@+{-# LANGUAGE FlexibleInstances, UndecidableInstances, OverlappingInstances #-}++-- ----------------------------------------------------------------------------+{- |+  Logging auxiliary functions.++  The 'LogShow' is an alternative 'Show' instance for a custom representation in logs.+  It uses the 'Show' instance by default.+-}+-- ----------------------------------------------------------------------------++module Hunt.Utility.Log where++-- | Alternative 'Show' instance for a custom representation in logs.+--   It uses the 'Show' instance by default.+class LogShow e where+  logShow :: e -> String++instance Show e => LogShow e where+  logShow = show
+ src/Hunt/Utility/Output.hs view
@@ -0,0 +1,72 @@+{-# LANGUAGE FlexibleContexts      #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings     #-}++-- ------------------------------------------------------------++module Hunt.Utility.Output+where+import           Control.Monad.IO.Class++import           Data.Aeson+import           Data.Aeson.Encode.Pretty+import qualified Data.ByteString.Lazy       as LB+import           Data.Monoid+import           Data.Text                  ()++import           System.FilePath            ()++-- ------------------------------------------------------------++--+outputValue :: (Functor m, MonadIO m, ToJSON c) => String -> c -> m ()+outputValue fn c+    = liftIO (jsonOutput True toFile c)+    where+      toFile bs+          | fn == ""+            ||+            fn == "-"+                = LB.putStr bs+          | otherwise+              = LB.writeFile fn bs++-- TODO: merge with Hunt.Server.Client.handleJsonResponse+--evalOkRes :: MonadIO m => Maybe LB.ByteString -> m ()+--evalOkRes Nothing+--    = return ()+--evalOkRes (Just bs)+--    | isOkMsg bs = return ()+--    | otherwise  = liftIO . ioError . userError $+--                  "server error: \"ok\" expected, but got " ++ show (LC.unpack bs)+--    where+--      isOkMsg s = maybe False ((== "ok") . unCmdRes) js+--          where+--            js :: Maybe (CmdRes Text)+--            js = decode s++--evalErrRes :: MonadIO m => Int -> LB.ByteString -> m a+--evalErrRes rc bs+--    = liftIO . ioError . userError $+--      unwords ["server error: rc=", show rc, "msg=", msg ce]+--    where+--      ce :: Maybe CmdError+--      ce = decode bs++--      msg Nothing  = "result is not a JSON error message"+--      msg (Just e) = show e++-- ------------------------------------------------------------++jsonOutput :: (ToJSON c) => Bool -> (LB.ByteString -> IO a) -> c -> IO a+jsonOutput pretty io x+    = io $ (if pretty then encodePretty' encConfig else encode) x+      where+        encConfig :: Config+        encConfig+            = Config { confIndent = 2+                     , confCompare+                         = keyOrder ["description", "index", "uri"]+                           `mappend`+                           compare+                     }
+ test/Hunt.hs view
@@ -0,0 +1,27 @@+{-# LANGUAGE ConstraintKinds           #-}+{-# LANGUAGE FlexibleContexts          #-}+{-# LANGUAGE FlexibleInstances         #-}+{-# LANGUAGE OverloadedStrings         #-}+{-# LANGUAGE TypeFamilies              #-}+{-# LANGUAGE TypeSynonymInstances      #-}+{-# LANGUAGE RankNTypes                #-}+{-# LANGUAGE ExistentialQuantification #-}++module Main where++import           Hunt.AnalyzerTests+import           Hunt.IndexTests+import           Hunt.InterpreterTests+import           Hunt.QueryParserTests+--import           Hunt.RankingTests++import           Test.Framework++main :: IO ()+main = defaultMain+       $ analyzerTests+       ++ contextTypeTests+       ++ interpreterTests+       ++ queryParserTests+--     XXX refactor ranking tests to be conform with new ranking+--       ++ rankingTests
+ test/Strictness.hs view
@@ -0,0 +1,20 @@+{-# LANGUAGE ConstraintKinds           #-}+{-# LANGUAGE FlexibleContexts          #-}+{-# LANGUAGE FlexibleInstances         #-}+{-# LANGUAGE OverloadedStrings         #-}+{-# LANGUAGE TypeFamilies              #-}+{-# LANGUAGE TypeSynonymInstances      #-}+{-# LANGUAGE RankNTypes                #-}+{-# LANGUAGE ExistentialQuantification #-}++module Main where++import           Hunt.Strict.DocTable+import           Hunt.Strict.Index+import           Hunt.Strict.ContextIndex++import           Test.Framework++main :: IO ()+main = defaultMain $ docTableTests ++ indexTests ++ contextIndexTests+