1{-# LANGUAGE DeriveGeneric #-}
3module Database.Bloodhound.Client
17import Control.Applicative
18import Control.Monad (liftM)
20import Data.Aeson.TH (deriveJSON)
21import qualified Data.ByteString.Lazy.Char8 as L
22import Data.List (intercalate)
23import Data.Maybe (fromMaybe)
24import Data.Text (Text)
25import Data.Time.Clock (UTCTime)
26import GHC.Generics (Generic)
27import Network.HTTP.Conduit
28import qualified Network.HTTP.Types.Method as NHTM
29import qualified Network.HTTP.Types.Status as NHTS
31data Version = Version { number :: Text
33 , build_timestamp :: UTCTime
34 , build_snapshot :: Bool
35 , lucene_version :: Text } deriving (Show, Generic)
37data (FromJSON a, ToJSON a) => Status a =
42 , tagline :: Text } deriving (Show)
44instance ToJSON Version
45instance FromJSON Version
47instance (FromJSON a, ToJSON a) => FromJSON (Status a) where
48 parseJSON (Object v) = Status <$>
56-- instance ToJSON (Status a)
57-- instance FromJSON (Status a)
59-- bloo <- (liftM responseBody $ parseUrl "http://localhost:9200/" >>= \r -> withManager (httpLbs r))
62-- rootPath = rollUp $ ["/"]
65-- pathFromType :: String -> String -> String
66-- pathFromType index docType = "/" ++ index ++ docType
68-- -- Kinda hate this too.
69-- rollUp :: [String] -> String
70-- rollUp = Prelude.concat
72main = simpleHttp "http://localhost:9200/events/event/_search?q=hostname:localhost&size=1" >>= L.putStrLn
74-- data Response = Response { blah :: Text } deriving (Show)
76-- indexDocument :: ToJSON a => a -> IO Response
77-- indexDocument doc = ToJSON a
79-- nomenclature is because you're dropping the type baggage. All of this is compile-time erased.
80-- newtype ShardCount = ShardCount { unShardCount :: Int } deriving (Show, Generic)
81-- newtype ReplicaCount = ReplicaCount { unReplicaCount :: Int } deriving (Show, Generic)
83newtype ShardCount = ShardCount Int deriving (Show, Generic)
84newtype ReplicaCount = ReplicaCount Int deriving (Show, Generic)
86mkShardCount :: Int -> Maybe ShardCount
89 | n > 1000 = Nothing -- seriously, what the fuck?
90 | otherwise = Just (ShardCount n)
92mkReplicaCount :: Int -> Maybe ReplicaCount
95 | n > 1000 = Nothing -- ...
96 | otherwise = Just (ReplicaCount n)
98-- IndexSettings <$> mkShardCount 10 <*> mkReplicaCount 20
101 IndexSettings { indexShards :: ShardCount
102 , indexReplicas :: ReplicaCount } deriving (Show)
104instance ToJSON IndexSettings where
105 toJSON (IndexSettings s r) = object ["settings" .= object ["shards" .= s, "replicas" .= r]]
107instance ToJSON ReplicaCount
108instance ToJSON ShardCount
110defaultIndexSettings = IndexSettings (ShardCount 3) (ReplicaCount 2)
112data Strategy = RoundRobinStrat | RandomStrat | HeadStrat deriving (Show)
114newtype Server = Server String deriving (Show)
116-- data Errors = IndexAlreadyExistsError | GenericError deriving (Show)
118-- data ElasticsearchError = ElasticsearchError
120-- , error :: Text } deriving (Show, Generic)
122-- instance FromJSON ElasticsearchError
124-- errorForResponse resp = do
125-- let (sts, err) = decode (responseBody resp) :: ElasticsearchError
126-- if T.isInfixOf "IndexAlreadyExistsException" error
127-- then IndexAlreadyExistsError
130type Reply = Network.HTTP.Conduit.Response L.ByteString
131type Method = NHTM.Method
133responseIsError :: Reply -> Bool
134responseIsError resp = NHTS.statusCode (responseStatus resp) > 299
138dispatch :: String -> Method -> Maybe L.ByteString
140dispatch url method body = do
141 initReq <- parseUrl url
142 let reqBody = RequestBodyLBS $ fromMaybe emptyBody body
143 let req = initReq { method = method
144 , requestBody = reqBody
145 , checkStatus = \_ _ _ -> Nothing}
146 withManager $ httpLbs req
148type IndexName = String
150joinPath :: [String] -> String
151joinPath = intercalate "/"
153getStatus :: Server -> IO (Maybe (Status Version))
154getStatus (Server server) = do
155 request <- parseUrl $ joinPath [server]
156 response <- withManager $ httpLbs request
157 return $ decode (responseBody response)
159createIndex :: Server -> IndexSettings -> IndexName -> IO Reply
160createIndex (Server server) indexSettings indexName =
161 dispatch url method body where
162 url = joinPath [server, indexName]
163 method = NHTM.methodPut
164 body = Just $ encode indexSettings
166deleteIndex :: Server -> IndexName -> IO Reply
167deleteIndex (Server server) indexName =
168 dispatch url method body where
169 url = joinPath [server, indexName]
170 method = NHTM.methodDelete
173respIsTwoHunna :: Reply -> Bool
174respIsTwoHunna resp = NHTS.statusCode (responseStatus resp) == 200
176existentialQuery url = do
177 reply <- dispatch url NHTM.methodHead Nothing
178 return (reply, respIsTwoHunna reply)
180indexExists :: Server -> IndexName -> IO Bool
181indexExists (Server server) indexName = do
182 (reply, exists) <- existentialQuery url
184 url = joinPath [server, indexName]
186data OpenCloseIndex = OpenIndex | CloseIndex deriving (Show)
188stringifyOCIndex oci = case oci of
190 CloseIndex -> "_close"
192openOrCloseIndexes :: OpenCloseIndex -> Server -> IndexName -> IO Reply
193openOrCloseIndexes oci (Server server) indexName =
194 dispatch url method body where
195 ociString = stringifyOCIndex oci
196 url = joinPath [server, indexName, ociString]
197 method = NHTM.methodPost
200openIndex :: Server -> IndexName -> IO Reply
201openIndex = openOrCloseIndexes OpenIndex
203closeIndex :: Server -> IndexName -> IO Reply
204closeIndex = openOrCloseIndexes CloseIndex
206type MappingName = String
208createMapping :: ToJSON a => Server -> IndexName
209 -> MappingName -> a -> IO Reply
210createMapping (Server server) indexName mappingName mapping =
211 dispatch url method body where
212 url = joinPath [server, indexName, mappingName, "_mapping"]
213 method = NHTM.methodPut
214 body = Just $ encode mapping
216deleteMapping :: Server -> IndexName -> MappingName -> IO Reply
217deleteMapping (Server server) indexName mappingName =
218 dispatch url method body where
219 url = joinPath [server, indexName, mappingName, "_mapping"]
220 method = NHTM.methodDelete
223type DocumentID = String
225indexDocument :: ToJSON doc => Server -> IndexName -> MappingName
226 -> doc -> DocumentID -> IO Reply
227indexDocument (Server server) indexName mappingName document docId =
228 dispatch url method body where
229 url = joinPath [server, indexName, mappingName, docId]
230 method = NHTM.methodPut
231 body = Just (encode document)
233deleteDocument :: Server -> IndexName -> MappingName
234 -> DocumentID -> IO Reply
235deleteDocument (Server server) indexName mappingName docId =
236 dispatch url method body where
237 url = joinPath [server, indexName, mappingName, docId]
238 method = NHTM.methodDelete
241data EsResult a = EsResult { _index :: Text
246 , _source :: a } deriving (Eq, Show)
248instance (FromJSON a, ToJSON a) => FromJSON (EsResult a) where
249 parseJSON (Object v) = EsResult <$>
258getDocument :: Server -> IndexName -> MappingName
259 -> DocumentID -> IO Reply
260getDocument (Server server) indexName mappingName docId =
261 dispatch url method body where
262 url = joinPath [server, indexName, mappingName, docId]
263 method = NHTM.methodGet
266documentExists :: Server -> IndexName -> MappingName
267 -> DocumentID -> IO Bool
268documentExists (Server server) indexName mappingName docId = do
269 (reply, exists) <- existentialQuery url
271 url = joinPath [server, indexName, mappingName, docId]
273-- data Analyzer = AStandardAnalyzer StandardAnalyzer
274-- | SimpleAnalyzer -- just "simple"
275-- | WhitespaceAnalyzer
283-- data StandardAnalyzer =
285-- { stopwords :: Maybe [Text] -- default []
286-- , max_token_length :: Maybe Int -- default 255
289-- DefaultField -> "default_field": ""
290-- Fields -> "fields": []
292type QueryString = Text
294-- author:"John Smith"
295-- for book.title and book.date containing quick OR brown, book.\*:(quick brown)
296-- field title has no value or doesn't exist, _missing_:title
297-- field title has any non-null value, _exists:title
299data BooleanOperator = AND | OR deriving (Show)
301type DisMax = Bool -- "use_dis_max"
302newtype TieBreaker = TieBreaker Int deriving (Show)
303data QueryField = DefaultField Text | Fields [Text] DisMax TieBreaker deriving (Show)
305data QueryStringQuery =
306 QueryStringQuery { query :: QueryString
307 , field :: Maybe QueryField -- default _all
308 , defaultOperator :: Maybe BooleanOperator -- default OR
309 , analyzer :: Maybe Text -- analyzer name
310 , allowLeadingWildcard :: Maybe Bool -- default true
311 , lowercaseExpandedTerms :: Maybe Bool -- default true
312 , enablePositionIncrements :: Maybe Bool -- default true
313 , fuzzyMaxExpansions :: Maybe Int -- default 50
314 , fuzziness :: Maybe Text -- Fuzziness -- default AUTO, add type later
315 , fuzzyPrefixLength :: Maybe Int -- default 0
316 , phraseSlop :: Maybe Int -- default 0, 0 means exact phrase matches
317 , boost :: Maybe Double -- default 1.0
318 , analyzeWildcard :: Maybe Bool -- default false, true forces wildcard analysis
319 , autoGeneratePhraseQueries :: Maybe Bool -- default false
320 , minimumShouldMatch :: Maybe Text -- # "should" clauses in the boolean query should match
321 -- Text to handle weird % and other cases. Needs type
322 , lenient :: Maybe Bool -- default false, true shuts off format based failures
323 , locale :: Maybe Text -- default ROOT, locale used for string conversions
326emptyQueryStringQuery = QueryStringQuery "" Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
327queryStringQuery query = emptyQueryStringQuery { query = query }
332type Cache = Bool -- caching on/off
333data Filter = AndFilter [Filter] Cache
334 | OrFilter [Filter] Cache
335 | BoolFilter BoolMatch Cache
336 | ExistsFilter FieldName -- always cached
337 | GeoBoundingBoxFilter GeoBoundingBoxConstraint GeoFilterType Cache
338 | GeoDistanceFilter GeoConstraint Distance Cache
341instance ToJSON Filter where
342 toJSON (AndFilter filters cache) =
343 object ["and" .= fmap toJSON filters
345 toJSON (OrFilter filters cache) =
346 object ["or" .= fmap toJSON filters
348 toJSON (ExistsFilter fieldName) =
349 object ["exists" .= object
350 ["field" .= fieldName]]
351 toJSON (BoolFilter boolMatch cache) =
352 object ["bool" .= toJSON boolMatch
356data Term = Term { termField :: Text
357 , termValue :: Text } deriving (Show)
359instance ToJSON Term where
360 toJSON (Term field value) = object ["term" .= object
363data BoolMatch = MustMatch Term
365 | ShouldMatch [Term] deriving (Show)
367instance ToJSON BoolMatch where
368 toJSON (MustMatch term) = object ["must" .= toJSON term]
369 toJSON (MustNotMatch term) = object ["must_not" .= toJSON term]
370 toJSON (ShouldMatch terms) = object ["should" .= fmap toJSON terms]
372-- "memory" or "indexed"
373data GeoFilterType = GeoFilterMemory | GeoFilterIndexed deriving (Show)
375data LatLon = LatLon { lat :: Double, lon :: Double } deriving (Show)
377 GeoBoundingBox { topLeft :: LatLon
378 , bottomRight :: LatLon } deriving (Show)
380data GeoBoundingBoxConstraint =
381 GeoBoundingBoxConstraint { geoBBField :: FieldName
382 , constraintBox :: GeoBoundingBox
386 GeoConstraint { geoField :: FieldName
387 , latLon :: LatLon } deriving (Show)
389data DistanceUnits = Miles
397 | NauticalMiles deriving (Show)
399data DistanceType = Arc | SloppyArc | Plane deriving (Show)
402data OptimizeBbox = GeoFilterType | NoOptimizeBbox deriving (Show)
405 Distance { coefficient :: Double
406 , unit :: DistanceUnits } deriving (Show)
408data FromJSON a => SearchResults a =
409 SearchResults { took :: Int
411 , shards :: ShardResults
412 , searchHits :: SearchHits a } deriving (Show)
416data FromJSON a => SearchHits a =
417 SearchHits { hitsTotal :: Int
419 , hits :: [Hits a] } deriving (Show)
421data FromJSON a => Hits a =
422 Hits { hitsIndex :: IndexName
423 , hitsType :: MappingName
424 , hitDocumentID :: DocumentID
426 , hitSource :: a } deriving (Show)
429 ShardResults { shardTotal :: Int
430 , shardsSuccessful :: Int
431 , shardsFailed :: Int } deriving (Show)
433-- This is turning into a fractal of horror
434-- data Fuzziness = FDateFuzziness DateFuzziness |
436-- data Query = Query { query :: () } deriving (Show)
437-- search :: Server -> IndexName -> Maybe MappingName
438-- -> Query -> IO Reply
441-- getStatus :: String -> IO (Maybe (Status Version))
442-- getStatus server = do
443-- request <- parseUrl $ server ++ rootPath
444-- response <- withManager $ httpLbs request
445-- return $ (decode $ responseBody response)
447-- mkServer -- (Maybe Server) for URL parsing?
449-- data Cluster = Cluster { targets :: [Server]
450-- , strategy :: Strategy } deriving (Show)
452-- target :: Cluster -> Server
453-- target (Cluster targets HeadStrat) = head targets
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list