sphinx-0.0: Text/Search/Sphinx.hs
-- | This is the Haskell version of the Sphinx searchd client.
module Text.Search.Sphinx (Configuration (..), query, defaultConfig) where
import Network
import IO hiding (bracket)
import System
import Control.Exception
import Data.Binary.Get
import Data.Binary.Put
import Data.ByteString.Lazy hiding (pack, length, map)
import Data.ByteString.Lazy.Char8 (pack)
import qualified Data.ByteString.Lazy as BS
import Data.Char (ord, chr)
import Control.Monad
import Data.Int (Int64)
import Prelude hiding (readList)
-- | The configuration for a query
data Configuration = Configuration {
-- | The hostname of the Sphinx daemon
host :: String
-- | The portnumber of the Sphinx daemon
, port :: PortID
-- | How many records to seek from result-set start (default is 0)
, offset :: Int
-- | How many records to return from result-set starting at offset (default is 20)
, limit :: Int
-- | Query matching mode
, mode :: Int
-- | Ranking mode
, ranker :: Int
-- | Match sorting mode
, sort :: Int
-- | Attribute to sort by
, sortBy :: String
-- | Minimum ID to match, 0 means no limit
, minId :: Int
-- | Maximum ID to match, 0 means no limit
, maxId :: Int
-- | Group-by sorting clause (to sort groups in result set with)
, groupSort :: String
}
data AttributeType = AttrNone
| AttrInteger
| AttrTimestamp
| AttrOrdinal
| AttrBool
| AttrFloat
| AttrMulti
attributeType :: Int -> AttributeType
attributeType 0 = AttrNone
attributeType 1 = AttrInteger
attributeType 2 = AttrTimestamp
attributeType 3 = AttrOrdinal
attributeType 4 = AttrBool
attributeType 5 = AttrFloat
attributeType 0X40000000 = AttrMulti
type Connection = (Handle, Configuration)
connect :: Configuration -> IO Connection
connect cfg = do connection <- connectTo (host cfg) (port cfg)
bs <- hGet connection 4
let version = runGet getWord32be bs
myVersion = runPut (putWord32be 1)
hPut connection myVersion
return (connection, cfg)
--sClose connection
num = putWord32be . toEnum
num64 = putWord64be . toEnum
numList ls = do num (length ls)
mapM_ num ls
nums cfg = mapM_ (\x -> num $ x cfg)
num64s cfg = mapM_ (\x -> num64 $ x cfg)
str :: String -> Put
str s = do let bs = pack s
num (Prelude.length s) -- todo: dangerous
putLazyByteString bs
addQuery :: Connection -> String -> String -> String -> Put
addQuery c q i cm = addQuery' c q i cm
addQuery' (handle, cfg) query index comment = do
nums cfg [offset, limit, mode, ranker, sort]
-- pack sortbyLen and sortBy
str (sortBy cfg)
-- pack the queryLen and query encoded in utf-8
str query
-- weights
numList [100, 1]
-- pack len index and index
str index
num 1 -- tODO: id64 range marker
num64s cfg [minId, maxId]
-- pack len filters + filters
num 0 -- todo
-- pack groupfunc
num 0 -- todo
-- pack len groupby + groupby
str "" --todo
-- -- req.append ( pack ( '>2L', self._maxmatches) ) )
nums cfg [const 1000] --todo
-- -- req.append ( len + self._groupsort )
str (groupSort cfg)
-- -- req.append ( pack ( '>LLL', self._cutoff, self._retrycount, self._retrydelay))
nums cfg [const 0, const 0, const 0]
-- -- req.append ( self._groupdistinct)
str "" --todo
-- -- anchor point
num 0 --todo
-- -- per index weights
num 0 --todo
-- pack [max_query_time]
num 0 --todo
-- -- per-field weights
num 0 --todo
-- pack commentLen + comment
str comment
-- | A basic, default configuration.
defaultConfig = Configuration {
port = PortNumber 3312
, host = "127.0.0.1"
, offset = 0
, limit = 20
, mode = sph_match_all
, ranker = sph_rank_proximity_bm25
, sort = sort_relevance
, sortBy = ""
, minId = 0
, maxId = 0
, groupSort = "@group desc"
}
sph_match_all = 0
sph_rank_proximity_bm25 = 0
sort_relevance = 0
cmdSearch = putWord16be 0
verCmdSearch = putWord16be 0x113
query :: Configuration -> String -> IO [Text.Search.Sphinx.SearchResult]
query config s = do
conn <- connect config
let q = addQuery conn s "*" ""
runQueries (fst conn) q 1
runQueries :: Handle -> Put -> Int -> IO [SearchResult]
runQueries conn q numQueries = do
let req = runPut (makeRunQuery q numQueries)
hPut conn req
hFlush conn
getResponse conn 1
makeRunQuery query numQueries = do
cmdSearch
verCmdSearch
num $ fromEnum $ BS.length (runPut query) + 4
num numQueries
query
--getResponse :: Handle -> Int -> IO [SearchResult]
getResponse conn numResults = do
bs <- hGet conn 8
let x@(status, version, len) = runGet f bs
response <- hGet conn (fromIntegral len)
return $ runGet (numResults `times` getResult) response
where
f = do status <- getWord16be
version <- getWord16be
length <- getWord32be
return (status, version, length)
-- todo applicative
getNum :: Get Int
getNum = getWord32be >>= return . fromEnum
getNum64 :: Get Int64
getNum64 = getWord64be >>= return . fromIntegral
readList f = do num <- getNum
num `times` f
type SearchResult = ([(Int64, Int, [Int])], Int, Int, Int, [(ByteString, Int, Int)])
getResult :: Get SearchResult
getResult = do
status <- getNum
-- todo: we suppose the status is OK
fields <- readList readField
attrs <- readList readAttr
matchCount <- getNum
id64 <- getNum
matches <- matchCount `times` readMatch (id64 > 0) (map snd attrs)
[total, totalFound, time, numWords] <- 4 `times` getNum
wrds <- numWords `times` readWord
return $ (matches, total, totalFound, time, wrds)
times = replicateM
readWord = do s <- readStr
[doc, hits] <- 2 `times` getNum
return (s, doc, hits)
readField = readStr
readMatch isId64 attrs = do
doc <- if isId64 then getNum64 else (getNum >>= return . fromIntegral)
weight <- getNum
matchAttrs <- mapM readMatchAttr attrs
return (doc, weight, matchAttrs)
readMatchAttr AttrFloat = error "readMatchAttr for AttrFloat not implemented yet."
readMatchAttr AttrMulti = error "readMatchAttr for AttrFloat not implemented yet."
readMatchAttr _ = getNum
readStr = do len <- getNum
getLazyByteString (fromIntegral len)
readAttr = do
s <- readStr
t <- getNum
return (s, attributeType t)