aux-search/src/ElasticSearch.elm

608 lines
16 KiB
Elm
Raw Normal View History

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
, button
, div
2020-05-11 19:56:10 +00:00
, em
, form
, h1
, h4
, input
2020-05-11 19:56:10 +00:00
, li
2020-05-11 20:42:57 +00:00
, option
2020-05-11 19:56:10 +00:00
, p
2020-05-11 20:42:57 +00:00
, select
, span
, strong
, text
2020-05-11 19:56:10 +00:00
, ul
)
import Html.Attributes
exposing
( class
2020-05-11 19:56:10 +00:00
, classList
, href
, type_
, value
)
import Html.Events
exposing
2020-05-11 19:56:10 +00:00
( custom
, onClick
, onInput
, onSubmit
2020-05-11 19:56:10 +00:00
, preventDefaultOn
)
import Http
import Json.Decode
import Json.Encode
import RemoteData
import Url.Builder
type alias Model a =
2020-05-11 20:42:57 +00:00
{ channel : String
, query : Maybe String
, result : RemoteData.WebData (Result a)
, showDetailsFor : Maybe String
2020-05-11 19:56:10 +00:00
, from : Int
, size : Int
}
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 20:42:57 +00:00
-> Maybe String
2020-05-11 19:56:10 +00:00
-> Maybe Int
-> Maybe Int
-> ( Model a, Cmd msg )
2020-05-11 20:42:57 +00:00
init channel query showDetailsFor from size =
( { channel = Maybe.withDefault "unstable" channel
, 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
}
, Cmd.none
)
-- ---------------------------
-- UPDATE
-- ---------------------------
type Msg a
2020-05-11 19:56:10 +00:00
= NoOp
2020-05-11 20:42:57 +00:00
| ChannelChange String
2020-05-11 19:56:10 +00:00
| QueryInput String
| 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-11 20:42:57 +00:00
ChannelChange channel ->
( { model | channel = channel }
, Cmd.none
)
QueryInput query ->
( { model | query = Just query }
, Cmd.none
)
QuerySubmit ->
( model
2020-05-11 20:42:57 +00:00
, createUrl
path
model.channel
2020-05-11 19:56:10 +00:00
model.query
model.showDetailsFor
0
model.size
|> Browser.Navigation.pushUrl navKey
)
QueryResponse result ->
( { model | result = result }
, Cmd.none
)
ShowDetails selected ->
( model
2020-05-11 20:42:57 +00:00
, createUrl
path
model.channel
model.query
(if model.showDetailsFor == Just selected then
Nothing
else
Just selected
)
2020-05-11 19:56:10 +00:00
model.from
model.size
|> Browser.Navigation.pushUrl navKey
)
2020-05-11 19:56:10 +00:00
createUrl :
String
2020-05-11 20:42:57 +00:00
-> String
2020-05-11 19:56:10 +00:00
-> Maybe String
-> Maybe String
-> Int
-> Int
-> String
2020-05-11 20:42:57 +00:00
createUrl path channel query showDetailsFor from size =
2020-05-11 19:56:10 +00:00
[ Url.Builder.int "from" from
, Url.Builder.int "size" size
2020-05-11 20:42:57 +00:00
, Url.Builder.string "channel" channel
2020-05-11 19:56:10 +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
-> 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 =
div [ class "search-page" ]
2020-05-11 19:56:10 +00:00
[ h1 [ class "page-header" ] [ text title ]
, 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" ]
]
]
2020-05-11 20:42:57 +00:00
, span []
[ strong []
[ text " in " ]
, select
[ onInput (\x -> outMsg (ChannelChange x)) ]
[ option
[ value "unstable" ]
[ text "unstable" ]
, option
[ value "20.03" ]
[ text "20.03" ]
, option
[ value "19.09" ]
[ text "19.09" ]
]
, strong []
[ text " channel." ]
]
]
]
, case model.result of
RemoteData.NotAsked ->
div [] [ text "" ]
RemoteData.Loading ->
div [] [ text "Loading" ]
RemoteData.Success result ->
if result.hits.total.value == 0 then
div []
[ h4 [] [ text <| "No " ++ path ++ " found!" ]
]
else
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
++ "."
)
]
2020-05-11 19:56:10 +00:00
]
, viewPager outMsg model result path
, viewSuccess model.showDetailsFor result
, viewPager outMsg model result path
2020-05-11 19:56:10 +00:00
]
RemoteData.Failure error ->
let
( errorTitle, errorMessage ) =
case error of
Http.BadUrl text ->
( "Bad Url!", text )
Http.Timeout ->
( "Timeout!", "Request to the server timeout." )
Http.NetworkError ->
( "Network Error!", "Please check your network connection." )
Http.BadStatus code ->
( "Bad Status", "Server returned " ++ String.fromInt code )
Http.BadBody text ->
( "Bad Body", text )
in
div [ class "alert alert-error" ]
[ h4 [] [ text errorTitle ]
, text errorMessage
]
]
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
2020-05-11 20:42:57 +00:00
model.channel
2020-05-11 19:56:10 +00:00
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
2020-05-11 20:42:57 +00:00
model.channel
2020-05-11 19:56:10 +00:00
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
2020-05-11 20:42:57 +00:00
model.channel
2020-05-11 19:56:10 +00:00
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
2020-05-11 20:42:57 +00:00
model.channel
2020-05-11 19:56:10 +00:00
model.query
model.showDetailsFor
((result.hits.total.value // model.size) * model.size)
model.size
]
[ text "Last" ]
]
]
-- 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 =
-- Prefix Query
-- example query for "python"
-- {
-- "from": 0,
-- "size": 10,
-- "query": {
-- "bool": {
-- "should": [
-- {
-- "multi_match": {
-- "query": "python",
-- "boost": 1,
-- "fields": [
-- "attr_name.raw",
-- "attr_name"
-- ],
-- "type": "most_fields"
-- }
-- },
-- {
-- "term": {
-- "pname": {
-- "value": "python",
-- "boost": 2
-- }
-- }
-- },
-- {
-- "term": {
-- "pversion": {
-- "value": "python",
-- "boost": 0.2
-- }
-- }
-- },
-- {
-- "term": {
-- "description": {
-- "value": "python",
-- "boost": 0.3
-- }
-- }
-- },
-- {
-- "term": {
-- "longDescription": {
-- "value": "python",
-- "boost": 0.1
-- }
-- }
-- }
-- ]
-- }
-- }
-- }
let
listIn name type_ value =
[ ( name, Json.Encode.list type_ value ) ]
objectIn name value =
[ ( name, Json.Encode.object value ) ]
encodeTerm ( name, boost ) =
[ ( "term"
, Json.Encode.object
[ ( name
, Json.Encode.object
[ ( "value", Json.Encode.string query )
, ( "boost", Json.Encode.float boost )
]
)
]
)
2020-05-11 19:56:10 +00:00
]
in
[ ( "pname", 2.0 )
, ( "pversion", 0.2 )
, ( "description", 0.3 )
, ( "longDescription", 0.1 )
]
|> List.map encodeTerm
|> List.append
[ [ "attr_name.raw"
, "attr_name"
]
|> listIn "fields" Json.Encode.string
|> List.append
[ ( "query", Json.Encode.string query )
, ( "boost", Json.Encode.float 1.0 )
]
|> objectIn "multi_match"
]
|> listIn "should" Json.Encode.object
|> objectIn "bool"
|> objectIn "query"
|> List.append
[ ( "from", Json.Encode.int from )
, ( "size", Json.Encode.int size )
]
|> Json.Encode.object
|> Http.jsonBody
makeRequest :
String
-> String
-> Json.Decode.Decoder a
-> Options
-> String
2020-05-11 19:56:10 +00:00
-> Int
-> Int
-> Cmd (Msg a)
2020-05-11 19:56:10 +00:00
makeRequest field index decodeResultItemSource options query from size =
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
, 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)