2020-05-08 13:24:58 +00:00
|
|
|
module ElasticSearch exposing
|
|
|
|
( Model
|
|
|
|
, Msg(..)
|
|
|
|
, Options
|
|
|
|
, Result
|
|
|
|
, ResultItem
|
|
|
|
, decodeResult
|
|
|
|
, init
|
|
|
|
, makeRequest
|
|
|
|
, update
|
|
|
|
, view
|
|
|
|
)
|
|
|
|
|
|
|
|
import Base64
|
|
|
|
import Browser.Navigation
|
|
|
|
import Html
|
|
|
|
exposing
|
|
|
|
( Html
|
2020-05-11 19:56:10 +00:00
|
|
|
, a
|
2020-05-08 13:24:58 +00:00
|
|
|
, button
|
|
|
|
, div
|
2020-05-11 19:56:10 +00:00
|
|
|
, em
|
2020-05-08 13:24:58 +00:00
|
|
|
, form
|
|
|
|
, h1
|
|
|
|
, input
|
2020-05-11 19:56:10 +00:00
|
|
|
, li
|
|
|
|
, p
|
|
|
|
, pre
|
2020-05-08 13:24:58 +00:00
|
|
|
, text
|
2020-05-11 19:56:10 +00:00
|
|
|
, ul
|
2020-05-08 13:24:58 +00:00
|
|
|
)
|
|
|
|
import Html.Attributes
|
|
|
|
exposing
|
|
|
|
( class
|
2020-05-11 19:56:10 +00:00
|
|
|
, classList
|
|
|
|
, href
|
2020-05-08 13:24:58 +00:00
|
|
|
, type_
|
|
|
|
, value
|
|
|
|
)
|
|
|
|
import Html.Events
|
|
|
|
exposing
|
2020-05-11 19:56:10 +00:00
|
|
|
( custom
|
|
|
|
, onClick
|
|
|
|
, onInput
|
2020-05-08 13:24:58 +00:00
|
|
|
, onSubmit
|
2020-05-11 19:56:10 +00:00
|
|
|
, preventDefaultOn
|
2020-05-08 13:24:58 +00:00
|
|
|
)
|
|
|
|
import Http
|
|
|
|
import Json.Decode
|
|
|
|
import Json.Encode
|
|
|
|
import RemoteData
|
|
|
|
import Url.Builder
|
|
|
|
|
|
|
|
|
|
|
|
type alias Model a =
|
|
|
|
{ query : Maybe String
|
|
|
|
, result : RemoteData.WebData (Result a)
|
|
|
|
, showDetailsFor : Maybe String
|
2020-05-11 19:56:10 +00:00
|
|
|
, from : Int
|
|
|
|
, size : Int
|
2020-05-08 13:24:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type alias Result a =
|
|
|
|
{ hits : ResultHits a
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type alias ResultHits a =
|
|
|
|
{ total : ResultHitsTotal
|
|
|
|
, max_score : Maybe Float
|
|
|
|
, hits : List (ResultItem a)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type alias ResultHitsTotal =
|
|
|
|
{ value : Int
|
|
|
|
, relation : String -- TODO: this should probably be Enum
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type alias ResultItem a =
|
|
|
|
{ index : String
|
|
|
|
, id : String
|
|
|
|
, score : Float
|
|
|
|
, source : a
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
init :
|
|
|
|
Maybe String
|
|
|
|
-> Maybe String
|
2020-05-11 19:56:10 +00:00
|
|
|
-> Maybe Int
|
|
|
|
-> Maybe Int
|
2020-05-08 13:24:58 +00:00
|
|
|
-> ( Model a, Cmd msg )
|
2020-05-11 19:56:10 +00:00
|
|
|
init query showDetailsFor from size =
|
2020-05-08 13:24:58 +00:00
|
|
|
( { query = query
|
|
|
|
, result = RemoteData.NotAsked
|
|
|
|
, showDetailsFor = showDetailsFor
|
2020-05-11 19:56:10 +00:00
|
|
|
, from = Maybe.withDefault 0 from
|
|
|
|
, size = Maybe.withDefault 15 size
|
2020-05-08 13:24:58 +00:00
|
|
|
}
|
|
|
|
, Cmd.none
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- ---------------------------
|
|
|
|
-- UPDATE
|
|
|
|
-- ---------------------------
|
|
|
|
|
|
|
|
|
|
|
|
type Msg a
|
2020-05-11 19:56:10 +00:00
|
|
|
= NoOp
|
|
|
|
| QueryInput String
|
2020-05-08 13:24:58 +00:00
|
|
|
| QuerySubmit
|
|
|
|
| QueryResponse (RemoteData.WebData (Result a))
|
|
|
|
| ShowDetails String
|
|
|
|
|
|
|
|
|
|
|
|
update :
|
|
|
|
String
|
|
|
|
-> Browser.Navigation.Key
|
|
|
|
-> Msg a
|
|
|
|
-> Model a
|
|
|
|
-> ( Model a, Cmd (Msg a) )
|
|
|
|
update path navKey msg model =
|
|
|
|
case msg of
|
2020-05-11 19:56:10 +00:00
|
|
|
NoOp ->
|
|
|
|
( model
|
|
|
|
, Cmd.none
|
|
|
|
)
|
|
|
|
|
2020-05-08 13:24:58 +00:00
|
|
|
QueryInput query ->
|
|
|
|
( { model | query = Just query }
|
|
|
|
, Cmd.none
|
|
|
|
)
|
|
|
|
|
|
|
|
QuerySubmit ->
|
|
|
|
( model
|
2020-05-11 19:56:10 +00:00
|
|
|
, createUrl path
|
|
|
|
model.query
|
|
|
|
model.showDetailsFor
|
|
|
|
0
|
|
|
|
model.size
|
2020-05-08 13:24:58 +00:00
|
|
|
|> Browser.Navigation.pushUrl navKey
|
|
|
|
)
|
|
|
|
|
|
|
|
QueryResponse result ->
|
|
|
|
( { model | result = result }
|
|
|
|
, Cmd.none
|
|
|
|
)
|
|
|
|
|
|
|
|
ShowDetails selected ->
|
2020-05-11 19:33:20 +00:00
|
|
|
( model
|
|
|
|
, createUrl path
|
|
|
|
model.query
|
|
|
|
(if model.showDetailsFor == Just selected then
|
|
|
|
Nothing
|
|
|
|
|
|
|
|
else
|
|
|
|
Just selected
|
|
|
|
)
|
2020-05-11 19:56:10 +00:00
|
|
|
model.from
|
|
|
|
model.size
|
2020-05-11 19:33:20 +00:00
|
|
|
|> Browser.Navigation.pushUrl navKey
|
2020-05-08 13:24:58 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2020-05-11 19:56:10 +00:00
|
|
|
createUrl :
|
|
|
|
String
|
|
|
|
-> Maybe String
|
|
|
|
-> Maybe String
|
|
|
|
-> Int
|
|
|
|
-> Int
|
|
|
|
-> String
|
|
|
|
createUrl path query showDetailsFor from size =
|
|
|
|
[ Url.Builder.int "from" from
|
|
|
|
, Url.Builder.int "size" size
|
|
|
|
]
|
2020-05-08 13:24:58 +00:00
|
|
|
|> List.append
|
|
|
|
(query
|
|
|
|
|> Maybe.map
|
|
|
|
(\x ->
|
|
|
|
[ Url.Builder.string "query" x ]
|
|
|
|
)
|
|
|
|
|> Maybe.withDefault []
|
|
|
|
)
|
|
|
|
|> List.append
|
|
|
|
(showDetailsFor
|
|
|
|
|> Maybe.map
|
|
|
|
(\x ->
|
|
|
|
[ Url.Builder.string "showDetailsFor" x
|
|
|
|
]
|
|
|
|
)
|
|
|
|
|> Maybe.withDefault []
|
|
|
|
)
|
|
|
|
|> Url.Builder.absolute [ path ]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- VIEW
|
|
|
|
|
|
|
|
|
|
|
|
view :
|
2020-05-11 19:56:10 +00:00
|
|
|
String
|
|
|
|
-> String
|
2020-05-08 13:24:58 +00:00
|
|
|
-> Model a
|
|
|
|
-> (Maybe String -> Result a -> Html b)
|
|
|
|
-> (Msg a -> b)
|
|
|
|
-> Html b
|
2020-05-11 19:56:10 +00:00
|
|
|
view path title model viewSuccess outMsg =
|
2020-05-08 13:24:58 +00:00
|
|
|
div [ class "search-page" ]
|
2020-05-11 19:56:10 +00:00
|
|
|
[ h1 [ class "page-header" ] [ text title ]
|
2020-05-08 13:24:58 +00:00
|
|
|
, div [ class "search-input" ]
|
|
|
|
[ form [ onSubmit (outMsg QuerySubmit) ]
|
|
|
|
[ div [ class "input-append" ]
|
|
|
|
[ input
|
|
|
|
[ type_ "text"
|
|
|
|
, onInput (\x -> outMsg (QueryInput x))
|
|
|
|
, value <| Maybe.withDefault "" model.query
|
|
|
|
]
|
|
|
|
[]
|
|
|
|
, div [ class "btn-group" ]
|
|
|
|
[ button [ class "btn" ] [ text "Search" ]
|
|
|
|
]
|
|
|
|
]
|
|
|
|
]
|
|
|
|
]
|
|
|
|
, case model.result of
|
|
|
|
RemoteData.NotAsked ->
|
|
|
|
div [] [ text "NotAsked" ]
|
|
|
|
|
|
|
|
RemoteData.Loading ->
|
|
|
|
div [] [ text "Loading" ]
|
|
|
|
|
|
|
|
RemoteData.Success result ->
|
2020-05-11 19:56:10 +00:00
|
|
|
div []
|
|
|
|
[ p []
|
|
|
|
[ em []
|
|
|
|
[ text
|
|
|
|
("Showing results "
|
|
|
|
++ String.fromInt model.from
|
|
|
|
++ "-"
|
|
|
|
++ String.fromInt
|
|
|
|
(if model.from + model.size > result.hits.total.value then
|
|
|
|
result.hits.total.value
|
|
|
|
|
|
|
|
else
|
|
|
|
model.from + model.size
|
|
|
|
)
|
|
|
|
++ " of "
|
|
|
|
++ String.fromInt result.hits.total.value
|
|
|
|
++ "."
|
|
|
|
)
|
|
|
|
]
|
|
|
|
]
|
|
|
|
, viewPager outMsg model result path
|
|
|
|
, viewSuccess model.showDetailsFor result
|
|
|
|
, viewPager outMsg model result path
|
|
|
|
]
|
2020-05-08 13:24:58 +00:00
|
|
|
|
|
|
|
RemoteData.Failure error ->
|
|
|
|
div []
|
|
|
|
[ text "Error!"
|
|
|
|
|
|
|
|
--, pre [] [ text (Debug.toString error) ]
|
|
|
|
]
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2020-05-11 19:56:10 +00:00
|
|
|
viewPager :
|
|
|
|
(Msg a -> b)
|
|
|
|
-> Model a
|
|
|
|
-> Result a
|
|
|
|
-> String
|
|
|
|
-> Html b
|
|
|
|
viewPager outMsg model result path =
|
|
|
|
ul [ class "pager" ]
|
|
|
|
[ li
|
|
|
|
[ classList
|
|
|
|
[ ( "disabled", model.from == 0 )
|
|
|
|
]
|
|
|
|
]
|
|
|
|
[ a
|
|
|
|
[ if model.from == 0 then
|
|
|
|
href "#disabled"
|
|
|
|
|
|
|
|
else
|
|
|
|
href <|
|
|
|
|
createUrl
|
|
|
|
path
|
|
|
|
model.query
|
|
|
|
model.showDetailsFor
|
|
|
|
0
|
|
|
|
model.size
|
|
|
|
]
|
|
|
|
[ text "First" ]
|
|
|
|
]
|
|
|
|
, li
|
|
|
|
[ classList
|
|
|
|
[ ( "disabled", model.from == 0 )
|
|
|
|
]
|
|
|
|
]
|
|
|
|
[ a
|
|
|
|
[ href <|
|
|
|
|
if model.from - model.size < 0 then
|
|
|
|
"#disabled"
|
|
|
|
|
|
|
|
else
|
|
|
|
createUrl
|
|
|
|
path
|
|
|
|
model.query
|
|
|
|
model.showDetailsFor
|
|
|
|
(model.from - model.size)
|
|
|
|
model.size
|
|
|
|
]
|
|
|
|
[ text "Previous" ]
|
|
|
|
]
|
|
|
|
, li
|
|
|
|
[ classList
|
|
|
|
[ ( "disabled", model.from + model.size >= result.hits.total.value )
|
|
|
|
]
|
|
|
|
]
|
|
|
|
[ a
|
|
|
|
[ href <|
|
|
|
|
if model.from + model.size >= result.hits.total.value then
|
|
|
|
"#disabled"
|
|
|
|
|
|
|
|
else
|
|
|
|
createUrl
|
|
|
|
path
|
|
|
|
model.query
|
|
|
|
model.showDetailsFor
|
|
|
|
(model.from + model.size)
|
|
|
|
model.size
|
|
|
|
]
|
|
|
|
[ text "Next" ]
|
|
|
|
]
|
|
|
|
, li
|
|
|
|
[ classList
|
|
|
|
[ ( "disabled", model.from + model.size >= result.hits.total.value )
|
|
|
|
]
|
|
|
|
]
|
|
|
|
[ a
|
|
|
|
[ href <|
|
|
|
|
if model.from + model.size >= result.hits.total.value then
|
|
|
|
"#disabled"
|
|
|
|
|
|
|
|
else
|
|
|
|
createUrl
|
|
|
|
path
|
|
|
|
model.query
|
|
|
|
model.showDetailsFor
|
|
|
|
((result.hits.total.value // model.size) * model.size)
|
|
|
|
model.size
|
|
|
|
]
|
|
|
|
[ text "Last" ]
|
|
|
|
]
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2020-05-08 13:24:58 +00:00
|
|
|
|
|
|
|
-- API
|
|
|
|
|
|
|
|
|
|
|
|
type alias Options =
|
|
|
|
{ url : String
|
|
|
|
, username : String
|
|
|
|
, password : String
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-05-11 19:56:10 +00:00
|
|
|
makeRequestBody :
|
|
|
|
String
|
|
|
|
-> String
|
|
|
|
-> Int
|
|
|
|
-> Int
|
|
|
|
-> Http.Body
|
|
|
|
makeRequestBody field query from size =
|
2020-05-08 13:24:58 +00:00
|
|
|
let
|
|
|
|
stringIn name value =
|
|
|
|
[ ( name, Json.Encode.string value ) ]
|
|
|
|
|
|
|
|
objectIn name object =
|
|
|
|
[ ( name, Json.Encode.object object ) ]
|
|
|
|
in
|
2020-05-11 10:52:07 +00:00
|
|
|
-- Prefix Query
|
|
|
|
-- {
|
2020-05-11 19:56:10 +00:00
|
|
|
-- ""
|
2020-05-11 10:52:07 +00:00
|
|
|
-- "query": {
|
|
|
|
-- "prefix": {
|
|
|
|
-- "user": {
|
|
|
|
-- "value": ""
|
|
|
|
-- }
|
|
|
|
-- }
|
|
|
|
-- }
|
|
|
|
-- }
|
|
|
|
--query
|
|
|
|
-- |> stringIn "value"
|
|
|
|
-- |> objectIn field
|
|
|
|
-- |> objectIn "prefix"
|
|
|
|
-- |> objectIn "query"
|
|
|
|
-- |> Json.Encode.object
|
|
|
|
-- |> Http.jsonBody
|
|
|
|
--
|
|
|
|
-- Wildcard Query
|
|
|
|
-- {
|
|
|
|
-- "query": {
|
|
|
|
-- "wildcard": {
|
|
|
|
-- "<field>": {
|
|
|
|
-- "value": "*<value>*",
|
|
|
|
-- }
|
|
|
|
-- }
|
|
|
|
-- }
|
|
|
|
-- }
|
|
|
|
("*" ++ query ++ "*")
|
|
|
|
|> stringIn "value"
|
2020-05-08 13:24:58 +00:00
|
|
|
|> objectIn field
|
2020-05-11 10:52:07 +00:00
|
|
|
|> objectIn "wildcard"
|
2020-05-08 13:24:58 +00:00
|
|
|
|> objectIn "query"
|
2020-05-11 19:56:10 +00:00
|
|
|
|> List.append
|
|
|
|
[ ( "from", Json.Encode.int from )
|
|
|
|
, ( "size", Json.Encode.int size )
|
|
|
|
]
|
2020-05-08 13:24:58 +00:00
|
|
|
|> Json.Encode.object
|
|
|
|
|> Http.jsonBody
|
|
|
|
|
|
|
|
|
|
|
|
makeRequest :
|
|
|
|
String
|
|
|
|
-> String
|
|
|
|
-> Json.Decode.Decoder a
|
|
|
|
-> Options
|
|
|
|
-> String
|
2020-05-11 19:56:10 +00:00
|
|
|
-> Int
|
|
|
|
-> Int
|
2020-05-08 13:24:58 +00:00
|
|
|
-> Cmd (Msg a)
|
2020-05-11 19:56:10 +00:00
|
|
|
makeRequest field index decodeResultItemSource options query from size =
|
2020-05-08 13:24:58 +00:00
|
|
|
Http.riskyRequest
|
|
|
|
{ method = "POST"
|
|
|
|
, headers =
|
|
|
|
[ Http.header "Authorization" ("Basic " ++ Base64.encode (options.username ++ ":" ++ options.password))
|
|
|
|
]
|
|
|
|
, url = options.url ++ "/" ++ index ++ "/_search"
|
2020-05-11 19:56:10 +00:00
|
|
|
, body = makeRequestBody field query from size
|
2020-05-08 13:24:58 +00:00
|
|
|
, expect =
|
|
|
|
Http.expectJson
|
|
|
|
(RemoteData.fromResult >> QueryResponse)
|
|
|
|
(decodeResult decodeResultItemSource)
|
|
|
|
, timeout = Nothing
|
|
|
|
, tracker = Nothing
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
-- JSON
|
|
|
|
|
|
|
|
|
|
|
|
decodeResult :
|
|
|
|
Json.Decode.Decoder a
|
|
|
|
-> Json.Decode.Decoder (Result a)
|
|
|
|
decodeResult decodeResultItemSource =
|
|
|
|
Json.Decode.map Result
|
|
|
|
(Json.Decode.field "hits" (decodeResultHits decodeResultItemSource))
|
|
|
|
|
|
|
|
|
|
|
|
decodeResultHits : Json.Decode.Decoder a -> Json.Decode.Decoder (ResultHits a)
|
|
|
|
decodeResultHits decodeResultItemSource =
|
|
|
|
Json.Decode.map3 ResultHits
|
|
|
|
(Json.Decode.field "total" decodeResultHitsTotal)
|
|
|
|
(Json.Decode.field "max_score" (Json.Decode.nullable Json.Decode.float))
|
|
|
|
(Json.Decode.field "hits" (Json.Decode.list (decodeResultItem decodeResultItemSource)))
|
|
|
|
|
|
|
|
|
|
|
|
decodeResultHitsTotal : Json.Decode.Decoder ResultHitsTotal
|
|
|
|
decodeResultHitsTotal =
|
|
|
|
Json.Decode.map2 ResultHitsTotal
|
|
|
|
(Json.Decode.field "value" Json.Decode.int)
|
|
|
|
(Json.Decode.field "relation" Json.Decode.string)
|
|
|
|
|
|
|
|
|
|
|
|
decodeResultItem : Json.Decode.Decoder a -> Json.Decode.Decoder (ResultItem a)
|
|
|
|
decodeResultItem decodeResultItemSource =
|
|
|
|
Json.Decode.map4 ResultItem
|
|
|
|
(Json.Decode.field "_index" Json.Decode.string)
|
|
|
|
(Json.Decode.field "_id" Json.Decode.string)
|
|
|
|
(Json.Decode.field "_score" Json.Decode.float)
|
|
|
|
(Json.Decode.field "_source" decodeResultItemSource)
|