Suggestions for search field (#74)

This commit is contained in:
adisbladis 2020-07-02 14:27:49 +02:00 committed by GitHub
parent b06bac6187
commit 1f8939b3af
Failed to generate hash of commit
19 changed files with 1152 additions and 158 deletions

45
.gitignore vendored
View file

@ -1,35 +1,20 @@
# Logs
logs
*.log
.idea
.cache
npm-debug.log*
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
node_modules
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
# elm-package generated files
elm-stuff/
# elm-repl generated files
repl-temp-*
.DS_Store
example/dist
ignore
.cache
.idea
.node_repl_history
.npm
build/Release
dist
package-lock.json
result
scripts/eval-*
elm-stuff/
eval-*
ignore
import-scripts/import_scripts/__pycache__/
import-scripts/tests/__pycache__/
logs
node_modules
npm-debug.log*
package-lock.json
repl-temp-*
result
src-url

View file

@ -1 +1 @@
8
9

View file

@ -1,5 +1,5 @@
{ pkgs ? import <nixpkgs> { }
, version ? "0"
, version ? pkgs.lib.removeSuffix "\n" (builtins.readFile ./VERSION)
}:
let
package = builtins.fromJSON (builtins.readFile ./package.json);

View file

@ -10,11 +10,21 @@
version = "1.1.3";
};
"ohanhi/keyboard" = {
sha256 = "10sbq8v2kydnc3lkydl367g36q2b0xizxl031xyakrgl4zlh07ic";
version = "2.0.1";
};
"truqu/elm-base64" = {
sha256 = "12w68b4idbs2vn0gm0lj354pm745jb7n0fj69408mpvh5r1z4m1b";
version = "2.0.4";
};
"elm/regex" = {
sha256 = "0lijsp50w7n1n57mjg6clpn9phly8vvs07h0qh2rqcs0f1jqvsa2";
version = "1.0.0";
};
"elm/html" = {
sha256 = "1n3gpzmpqqdsldys4ipgyl1zacn0kbpc3g4v3hdpiyfjlgh8bf3k";
version = "1.0.0";
@ -25,6 +35,16 @@
version = "1.0.2";
};
"Gizra/elm-debouncer" = {
sha256 = "009yw0rb418ar2a458ilr25m8gxrxsv5nvs3ld3l6sy12v12n0yn";
version = "2.0.0";
};
"Skinney/keyboard-events" = {
sha256 = "10qjlpa4byk78sra071w4ghc7b9p2brnppx7aqyy9cmbrmp5nf86";
version = "2.0.1";
};
"elm/core" = {
sha256 = "0gyk7lx3b6vx2jlfbxdsb4xffn0wdvg5yxldq50jr2kk5dzc2prj";
version = "1.0.4";
@ -60,11 +80,6 @@
version = "1.0.5";
};
"elm/regex" = {
sha256 = "0lijsp50w7n1n57mjg6clpn9phly8vvs07h0qh2rqcs0f1jqvsa2";
version = "1.0.0";
};
"rtfeldman/elm-hex" = {
sha256 = "1y0aa16asvwdqmgbskh5iba6psp43lkcjjw9mgzj3gsrg33lp00d";
version = "1.0.0";
@ -75,6 +90,11 @@
version = "1.1.0";
};
"elm-community/list-extra" = {
sha256 = "1rvr1c8cfb3dwf3li17l9ziax6d1fshkliasspnw6rviva38lw34";
version = "8.2.4";
};
"elm/time" = {
sha256 = "0vch7i86vn0x8b850w1p69vplll1bnbkp8s383z7pinyg94cm2z1";
version = "1.0.0";
@ -90,6 +110,11 @@
version = "1.2.2";
};
"elm/svg" = {
sha256 = "1cwcj73p61q45wqwgqvrvz3aypjyy3fw732xyxdyj6s256hwkn0k";
version = "1.0.1";
};
"elm/random" = {
sha256 = "138n2455wdjwa657w6sjq18wx2r0k60ibpc4frhbqr50sncxrfdl";
version = "1.0.0";

View file

@ -6,7 +6,9 @@
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"Gizra/elm-debouncer": "2.0.0",
"NoRedInk/elm-json-decode-pipeline": "1.0.0",
"Skinney/keyboard-events": "2.0.1",
"elm/browser": "1.0.2",
"elm/core": "1.0.4",
"elm/html": "1.0.0",
@ -16,6 +18,7 @@
"elm/url": "1.0.0",
"hecrj/html-parser": "2.3.4",
"krisajenkins/remotedata": "6.0.1",
"ohanhi/keyboard": "2.0.1",
"truqu/elm-base64": "2.0.4"
},
"indirect": {
@ -24,6 +27,7 @@
"elm/parser": "1.1.0",
"elm/time": "1.0.0",
"elm/virtual-dom": "1.0.2",
"elm-community/list-extra": "8.2.4",
"rtfeldman/elm-hex": "1.0.0"
}
},
@ -32,7 +36,8 @@
"elm-explorations/test": "1.2.2"
},
"indirect": {
"elm/random": "1.0.0"
"elm/random": "1.0.0",
"elm/svg": "1.0.1"
}
}
}

View file

@ -18,14 +18,13 @@
poetry2nix.overlay
];
};
version = pkgs.lib.removeSuffix "\n" (builtins.readFile "${self}/VERSION");
in
{
import_scripts = import ./import-scripts {
inherit pkgs version;
inherit pkgs;
};
frontend = import ./. {
inherit pkgs version;
inherit pkgs;
};
};
in

View file

@ -1,5 +1,5 @@
{ pkgs ? import <nixpkgs> { }
, version ? "0"
, version ? pkgs.lib.removeSuffix "\n" (builtins.readFile ./../VERSION)
}:
let
inherit (pkgs.poetry2nix) mkPoetryApplication overrides;
@ -13,11 +13,21 @@ mkPoetryApplication {
'';
});
});
nativeBuildInputs = [
pkgs.poetry
];
checkPhase = ''
black --diff --check ./import_scripts
flake8 --ignore W503,E501,E265,E203 ./import_scripts
export PYTHONPATH=$PWD:$PYTHONPATH
black --diff --check import_scripts/ tests/
flake8 --ignore W503,E501,E265,E203 import_scripts/ tests/
mypy import_scripts/ tests/
pytest -vv tests/
'';
postInstall = ''
wrapProgram $out/bin/import-channel --set INDEX_SCHEMA_VERSION "${version}"
'';
shellHook = ''
cd import-scripts/
export PYTHONPATH=$PWD:$PYTHONPATH
'';
}

View file

@ -1,21 +1,22 @@
import boto3
import botocore
import botocore.client
import boto3 # type: ignore
import botocore # type: ignore
import botocore.client # type: ignore
import click
import click_log
import elasticsearch
import elasticsearch.helpers
import click_log # type: ignore
import elasticsearch # type: ignore
import elasticsearch.helpers # type: ignore
import json
import logging
import os
import os.path
import pypandoc
import pypandoc # type: ignore
import re
import requests
import shlex
import subprocess
import sys
import tqdm
import tqdm # type: ignore
import typing
import xml.etree.ElementTree
logger = logging.getLogger("import-channel")
@ -55,6 +56,12 @@ MAPPING = {
"properties": {
"type": {"type": "keyword"},
# Package fields
"package_suggestions": {
"type": "completion",
"analyzer": "lowercase",
"search_analyzer": "lowercase",
"preserve_position_increments": False,
},
"package_hydra_build": {
"type": "nested",
"properties": {
@ -98,6 +105,12 @@ MAPPING = {
"package_homepage": {"type": "keyword"},
"package_system": {"type": "keyword"},
# Options fields
"option_suggestions": {
"type": "completion",
"analyzer": "lowercase",
"search_analyzer": "lowercase",
"preserve_position_increments": False,
},
"option_name": {"type": "keyword", "normalizer": "lowercase"},
"option_name_query": {"type": "keyword", "normalizer": "lowercase"},
"option_description": {"type": "text"},
@ -109,11 +122,33 @@ MAPPING = {
}
def split_query(text):
"""Tokenize package attr_name
def parse_suggestions(text: str) -> typing.List[typing.Dict[str, object]]:
"""Tokenize option_name
Example:
services.nginx.extraConfig
- services.nginx.extraConfig
- services.nginx.
- services.
"""
results: typing.List[typing.Dict[str, object]] = [
{"input": text, "weight": 1000 - (((len(text.split(".")) - 1) * 10))},
]
for i in range(len(text.split(".")) - 1):
result = {
"input": ".".join(text.split(".")[: -(i + 1)]) + ".",
"weight": 1000 - ((len(text.split(".")) - 2 - i) * 10) + 1,
}
results.append(result)
return results
def parse_query(text):
"""Tokenize package attr_name
Example package:
python37Packages.test_name-test
= index: 0
- python37Packages.test1_name-test2
@ -345,9 +380,10 @@ def get_packages(evaluation, evaluation_builds):
yield dict(
type="package",
package_suggestions=parse_suggestions(attr_name),
package_hydra=hydra,
package_attr_name=attr_name,
package_attr_name_query=list(split_query(attr_name)),
package_attr_name_query=list(parse_query(attr_name)),
package_attr_set=attr_set,
package_pname=remove_attr_set(data["pname"]),
package_pversion=data["version"],
@ -411,8 +447,9 @@ def get_options(evaluation):
yield dict(
type="option",
option_suggestions=parse_suggestions(name),
option_name=name,
option_name_query=split_query(name),
option_name_query=parse_query(name),
option_description=description,
option_type=option.get("type"),
option_default=default,
@ -539,5 +576,3 @@ def run(es_url, channel, force, verbose):
if __name__ == "__main__":
run()
# vi:ft=python

File diff suppressed because one or more lines are too long

View file

@ -25,6 +25,9 @@ ipdb = "^0.13.2"
black = "^19.10b0"
flake8 = "^3.8.3"
mypy = "^0.780"
pytest = "^5.4.3"
setuptools = "^47.3.1"
boto3-stubs = "^1.14.6"
[build-system]
requires = ["poetry>=0.12"]

View file

@ -0,0 +1,52 @@
module Example exposing (fuzzTest, unitTest, viewTest)
import Expect exposing (Expectation)
import Fuzz exposing (Fuzzer, int, list, string)
import Main exposing (..)
import Test exposing (..)
import Test.Html.Query as Query
import Test.Html.Selector exposing (tag, text)
{-| See <https://github.com/elm-community/elm-test>
-}
unitTest : Test
unitTest =
describe "simple unit test"
[ test "Inc adds one" <|
\() ->
update Inc (Model 0 "")
|> Tuple.first
|> .counter
|> Expect.equal 1
]
{-| See <https://github.com/elm-community/elm-test>
-}
fuzzTest : Test
fuzzTest =
describe "simple fuzz test"
[ fuzz int "Inc ALWAYS adds one" <|
\ct ->
update Inc (Model ct "")
|> Tuple.first
|> .counter
|> Expect.equal (ct + 1)
]
{-| see <https://github.com/eeue56/elm-html-test>
-}
viewTest : Test
viewTest =
describe "Testing view function"
[ test "Button has the expected text" <|
\() ->
Model 0 ""
|> view
|> Query.fromHtml
|> Query.findAll [ tag "button" ]
|> Query.first
|> Query.has [ text "+ 1" ]
]

View file

@ -0,0 +1,104 @@
import pytest # type: ignore
@pytest.mark.parametrize(
"text,expected",
[
(
"services.grafana.analytics.reporting.enable",
[
{"input": "services.grafana.analytics.reporting.enable", "weight": 960},
{"input": "services.grafana.analytics.reporting.", "weight": 971},
{"input": "services.grafana.analytics.", "weight": 981},
{"input": "services.grafana.", "weight": 991},
{"input": "services.", "weight": 1001},
],
),
(
"services.nginx.extraConfig",
[
{"input": "services.nginx.extraConfig", "weight": 980},
{"input": "services.nginx.", "weight": 991},
{"input": "services.", "weight": 1001},
],
),
(
"python37Packages.test1_name-test2",
[
{"input": "python37Packages.test1_name-test2", "weight": 990},
{"input": "python37Packages.", "weight": 1001},
],
),
],
)
def test_parse_suggestions(text, expected):
import import_scripts.channel
assert import_scripts.channel.parse_suggestions(text) == expected
@pytest.mark.parametrize(
"text,expected",
[
(
"services.nginx.extraConfig",
[
"services.nginx.extraConfig",
"services.nginx.extra",
"services.nginx",
"services",
"nginx.extraConfig",
"nginx.extra",
"nginx",
"extraConfig",
"extra",
"Config",
],
),
(
"python37Packages.test1_name-test2",
[
"python37Packages.test1_name-test2",
"python37Packages.test1_name-test",
"python37Packages.test1_name",
"python37Packages.test1",
"python37Packages.test",
"python37Packages",
"python37",
"python",
"37Packages.test1_name-test2",
"37Packages.test1_name-test",
"37Packages.test1_name",
"37Packages.test1",
"37Packages.test",
"37Packages",
"37",
"Packages.test1_name-test2",
"Packages.test1_name-test",
"Packages.test1_name",
"Packages.test1",
"Packages.test",
"Packages",
"test1_name-test2",
"test1_name-test",
"test1_name",
"test1",
"test",
"1_name-test2",
"1_name-test",
"1_name",
"1",
"name-test2",
"name-test",
"name",
"test2",
"test",
"2",
],
),
],
)
def test_parse_query(text, expected):
import import_scripts.channel
assert sorted(import_scripts.channel.parse_query(text)) == sorted(expected)

Binary file not shown.

View file

@ -236,11 +236,11 @@ update msg model =
|> updateWith Home HomeMsg model
( PackagesMsg subMsg, Packages subModel ) ->
Page.Packages.update model.navKey subMsg subModel
Page.Packages.update model.navKey model.elasticsearch subMsg subModel
|> updateWith Packages PackagesMsg model
( OptionsMsg subMsg, Options subModel ) ->
Page.Options.update model.navKey subMsg subModel
Page.Options.update model.navKey model.elasticsearch subMsg subModel
|> updateWith Options OptionsMsg model
( _, _ ) ->

View file

@ -87,13 +87,20 @@ type Msg
= SearchMsg (Search.Msg ResultItemSource)
update : Browser.Navigation.Key -> Msg -> Model -> ( Model, Cmd Msg )
update navKey msg model =
update : Browser.Navigation.Key -> Search.Options -> Msg -> Model -> ( Model, Cmd Msg )
update navKey options msg model =
case msg of
SearchMsg subMsg ->
let
( newModel, newCmd ) =
Search.update "options" navKey subMsg model
Search.update
"options"
navKey
"option"
options
decodeResultItemSource
subMsg
model
in
( newModel, Cmd.map SearchMsg newCmd )
@ -115,7 +122,7 @@ view model =
viewSuccess :
String
-> Maybe String
-> Search.Result ResultItemSource
-> Search.SearchResult ResultItemSource
-> Html Msg
viewSuccess channel show result =
div [ class "search-result" ]
@ -380,9 +387,8 @@ makeRequest options channel queryRaw from size =
("latest-" ++ String.fromInt options.mappingSchemaVersion ++ "-" ++ channel)
decodeResultItemSource
options
query
from
size
Search.QueryResponse
(Just "query-options")
|> Cmd.map SearchMsg

View file

@ -122,13 +122,20 @@ type Msg
= SearchMsg (Search.Msg ResultItemSource)
update : Browser.Navigation.Key -> Msg -> Model -> ( Model, Cmd Msg )
update navKey msg model =
update : Browser.Navigation.Key -> Search.Options -> Msg -> Model -> ( Model, Cmd Msg )
update navKey options msg model =
case msg of
SearchMsg subMsg ->
let
( newModel, newCmd ) =
Search.update "packages" navKey subMsg model
Search.update
"packages"
navKey
"package"
options
decodeResultItemSource
subMsg
model
in
( newModel, Cmd.map SearchMsg newCmd )
@ -150,7 +157,7 @@ view model =
viewSuccess :
String
-> Maybe String
-> Search.Result ResultItemSource
-> Search.SearchResult ResultItemSource
-> Html Msg
viewSuccess channel show result =
div [ class "search-result" ]
@ -495,9 +502,8 @@ makeRequest options channel queryRaw from size =
("latest-" ++ String.fromInt options.mappingSchemaVersion ++ "-" ++ channel)
decodeResultItemSource
options
query
from
size
Search.QueryResponse
(Just "query-packages")
|> Cmd.map SearchMsg

View file

@ -2,8 +2,8 @@ module Search exposing
( Model
, Msg(..)
, Options
, Result
, ResultItem
, SearchResult
, channelDetailsFromId
, decodeResult
, init
@ -13,8 +13,12 @@ module Search exposing
, view
)
import Array
import Base64
import Browser.Dom
import Browser.Navigation
import Debouncer.Messages
import Dict
import Html
exposing
( Html
@ -25,12 +29,10 @@ import Html
, form
, h1
, h4
, i
, input
, li
, option
, p
, select
, span
, strong
, text
, ul
@ -41,6 +43,7 @@ import Html.Attributes
, class
, classList
, href
, id
, type_
, value
)
@ -50,27 +53,50 @@ import Html.Events
, onClick
, onInput
, onSubmit
, preventDefaultOn
)
import Http
import Json.Decode
import Json.Encode
import Keyboard
import Keyboard.Events
import RemoteData
import Task
import Url.Builder
type alias Char =
String
type alias Model a =
{ channel : String
, query : Maybe String
, result : RemoteData.WebData (Result a)
, queryDebounce : Debouncer.Messages.Debouncer (Msg a)
, querySuggest : RemoteData.WebData (SearchResult a)
, querySelectedSuggestion : Maybe String
, result : RemoteData.WebData (SearchResult a)
, show : Maybe String
, from : Int
, size : Int
}
type alias Result a =
type alias SearchResult a =
{ hits : ResultHits a
, suggest : Maybe (SearchSuggest a)
}
type alias SearchSuggest a =
{ query : Maybe (List (SearchSuggestQuery a))
}
type alias SearchSuggestQuery a =
{ text : String
, offset : Int
, length : Int
, options : List (ResultItem a)
}
@ -83,7 +109,7 @@ type alias ResultHits a =
type alias ResultHitsTotal =
{ value : Int
, relation : String -- TODO: this should probably be Enum
, relation : String
}
@ -92,10 +118,19 @@ type alias ResultItem a =
, id : String
, score : Float
, source : a
, text : Maybe String
, matched_queries : Maybe (List String)
}
itemHtml : Char -> Html Never
itemHtml char =
div []
[ i [ class "fa fa-rebel" ] []
, text (" " ++ char)
]
init :
Maybe String
-> Maybe String
@ -122,7 +157,13 @@ init channel query show from size model =
|> Maybe.withDefault 15
in
( { channel = Maybe.withDefault defaultChannel channel
, queryDebounce =
Debouncer.Messages.manual
|> Debouncer.Messages.settleWhenQuietFor (Just <| Debouncer.Messages.fromSeconds 0.6)
|> Debouncer.Messages.toDebouncer
, query = query
, querySuggest = RemoteData.NotAsked
, querySelectedSuggestion = Nothing
, result =
model
|> Maybe.map (\x -> x.result)
@ -144,19 +185,34 @@ init channel query show from size model =
type Msg a
= NoOp
| ChannelChange String
| QueryInputDebounce (Debouncer.Messages.Msg (Msg a))
| QueryInput String
| QueryInputSuggestionsSubmit
| QueryInputSuggestionsResponse (RemoteData.WebData (SearchResult a))
| QuerySubmit
| QueryResponse (RemoteData.WebData (Result a))
| QueryResponse (RemoteData.WebData (SearchResult a))
| ShowDetails String
| SuggestionsMoveDown
| SuggestionsMoveUp
| SuggestionsSelect
| SuggestionsClickSelect String
| SuggestionsClose
update :
String
-> Browser.Navigation.Key
-> String
-> Options
-> Json.Decode.Decoder a
-> Msg a
-> Model a
-> ( Model a, Cmd (Msg a) )
update path navKey msg model =
update path navKey result_type options decodeResultItemSource msg model =
let
requestQuerySuggestionsTracker =
"query-" ++ result_type ++ "-suggestions"
in
case msg of
NoOp ->
( model
@ -187,8 +243,84 @@ update path navKey msg model =
|> Browser.Navigation.pushUrl navKey
)
QueryInputDebounce subMsg ->
Debouncer.Messages.update
(update path navKey result_type options decodeResultItemSource)
{ mapMsg = QueryInputDebounce
, getDebouncer = .queryDebounce
, setDebouncer = \debouncer m -> { m | queryDebounce = debouncer }
}
subMsg
model
QueryInput query ->
( { model | query = Just query }
update path
navKey
result_type
options
decodeResultItemSource
(QueryInputDebounce (Debouncer.Messages.provideInput QueryInputSuggestionsSubmit))
{ model
| query = Just query
, querySuggest = RemoteData.NotAsked
, querySelectedSuggestion = Nothing
}
|> Tuple.mapSecond
(\cmd ->
if RemoteData.isLoading model.querySuggest then
Cmd.batch
[ cmd
, Http.cancel requestQuerySuggestionsTracker
]
else
cmd
)
QueryInputSuggestionsSubmit ->
let
body =
Http.jsonBody
(Json.Encode.object
[ ( "from", Json.Encode.int 0 )
, ( "size", Json.Encode.int 0 )
, ( "suggest"
, Json.Encode.object
[ ( "query"
, Json.Encode.object
[ ( "text", Json.Encode.string (Maybe.withDefault "" model.query) )
, ( "completion"
, Json.Encode.object
[ ( "field", Json.Encode.string (result_type ++ "_suggestions") )
, ( "skip_duplicates", Json.Encode.bool True )
, ( "size", Json.Encode.int 1000 )
]
)
]
)
]
)
]
)
in
( { model
| querySuggest = RemoteData.Loading
, querySelectedSuggestion = Nothing
}
, makeRequest
body
("latest-" ++ String.fromInt options.mappingSchemaVersion ++ "-" ++ model.channel)
decodeResultItemSource
options
QueryInputSuggestionsResponse
(Just requestQuerySuggestionsTracker)
)
QueryInputSuggestionsResponse querySuggest ->
( { model
| querySuggest = querySuggest
, querySelectedSuggestion = Nothing
}
, Cmd.none
)
@ -226,6 +358,147 @@ update path navKey msg model =
|> Browser.Navigation.pushUrl navKey
)
SuggestionsMoveDown ->
( { model
| querySelectedSuggestion =
getMovedSuggestion
model.query
model.querySuggest
model.querySelectedSuggestion
(\x -> x + 1)
}
, scrollToSelected "dropdown-menu"
)
SuggestionsMoveUp ->
( { model
| querySelectedSuggestion =
getMovedSuggestion
model.query
model.querySuggest
model.querySelectedSuggestion
(\x -> x - 1)
}
, scrollToSelected "dropdown-menu"
)
SuggestionsSelect ->
case model.querySelectedSuggestion of
Just selected ->
update path
navKey
result_type
options
decodeResultItemSource
(SuggestionsClickSelect selected)
model
Nothing ->
( model
, Cmd.none
)
SuggestionsClickSelect selected ->
( { model
| querySuggest = RemoteData.NotAsked
, querySelectedSuggestion = Nothing
, query = Just selected
}
, Task.attempt (\_ -> QueryInputSuggestionsSubmit) (Task.succeed ())
)
SuggestionsClose ->
( { model
| querySuggest = RemoteData.NotAsked
, querySelectedSuggestion = Nothing
}
, Cmd.none
)
scrollToSelected :
String
-> Cmd (Msg a)
scrollToSelected id =
let
scroll y =
Browser.Dom.setViewportOf id 0 y
|> Task.onError (\_ -> Task.succeed ())
in
Task.sequence
[ Browser.Dom.getElement (id ++ "-selected")
|> Task.map (\x -> ( x.element.y, x.element.height ))
, Browser.Dom.getElement id
|> Task.map (\x -> ( x.element.y, x.element.height ))
, Browser.Dom.getViewportOf id
|> Task.map (\x -> ( x.viewport.y, x.viewport.height ))
]
|> Task.andThen
(\x ->
case x of
( elementY, elementHeight ) :: ( viewportY, viewportHeight ) :: ( viewportScrollTop, _ ) :: [] ->
let
scrollTop =
scroll (viewportScrollTop + (elementY - viewportY))
scrollBottom =
scroll (viewportScrollTop + (elementY - viewportY) + (elementHeight - viewportHeight))
in
if elementHeight > viewportHeight then
scrollTop
else if elementY < viewportY then
scrollTop
else if elementY + elementHeight > viewportY + viewportHeight then
scrollBottom
else
Task.succeed ()
_ ->
Task.succeed ()
)
|> Task.attempt (\_ -> NoOp)
getMovedSuggestion :
Maybe String
-> RemoteData.WebData (SearchResult a)
-> Maybe String
-> (Int -> Int)
-> Maybe String
getMovedSuggestion query querySuggest querySelectedSuggestion moveIndex =
let
suggestions =
getSuggestions query querySuggest
|> List.filterMap .text
getIndex key =
suggestions
|> List.indexedMap (\i a -> ( a, i ))
|> Dict.fromList
|> Dict.get key
|> Maybe.map moveIndex
|> Maybe.map
(\x ->
if x < 0 then
x + List.length suggestions
else
x
)
getKey index =
suggestions
|> Array.fromList
|> Array.get index
in
querySelectedSuggestion
|> Maybe.andThen getIndex
|> Maybe.withDefault 0
|> getKey
createUrl :
String
@ -321,17 +594,70 @@ channels =
]
getSuggestions :
Maybe String
-> RemoteData.WebData (SearchResult a)
-> List (ResultItem a)
getSuggestions query querySuggest =
let
maybeList f x =
x
|> Maybe.map f
|> Maybe.withDefault []
in
case querySuggest of
RemoteData.Success result ->
result.suggest
|> maybeList (\x -> x.query |> maybeList (List.map .options))
|> List.concat
|> List.filter (\x -> x.text /= query)
_ ->
[]
view :
String
-> String
-> Model a
-> (String -> Maybe String -> Result a -> Html b)
-> (String -> Maybe String -> SearchResult a -> Html b)
-> (Msg a -> b)
-> Html b
view path title model viewSuccess outMsg =
let
suggestions =
getSuggestions model.query model.querySuggest
viewSuggestion x =
li
[]
[ a
([ href "#" ]
|> List.append
(x.text
|> Maybe.map (\text -> [ onClick <| outMsg (SuggestionsClickSelect text) ])
|> Maybe.withDefault []
)
|> List.append
(if x.text == model.querySelectedSuggestion then
[ id "dropdown-menu-selected" ]
else
[]
)
)
[ text <| Maybe.withDefault "" x.text ]
]
in
div [ class "search-page" ]
[ h1 [ class "page-header" ] [ text title ]
, div [ class "search-input" ]
, div
[ classList
[ ( "search-input", True )
, ( "with-suggestions", RemoteData.isSuccess model.querySuggest && List.length suggestions > 0 )
, ( "with-suggestions-loading", RemoteData.isLoading model.querySuggest )
]
]
[ form [ onSubmit (outMsg QuerySubmit) ]
[ p
[]
@ -364,15 +690,56 @@ view path title model viewSuccess outMsg =
[ class "input-append"
]
[ input
[ type_ "text"
([ type_ "text"
, onInput (\x -> outMsg (QueryInput x))
, value <| Maybe.withDefault "" model.query
]
|> List.append
(if RemoteData.isSuccess model.querySuggest && List.length suggestions > 0 then
[ Keyboard.Events.custom Keyboard.Events.Keydown
{ preventDefault = True
, stopPropagation = True
}
([ ( Keyboard.ArrowDown, SuggestionsMoveDown )
, ( Keyboard.ArrowUp, SuggestionsMoveUp )
, ( Keyboard.Tab, SuggestionsMoveDown )
, ( Keyboard.Enter, SuggestionsSelect )
, ( Keyboard.Escape, SuggestionsClose )
]
|> List.map (\( k, m ) -> ( k, outMsg m ))
)
]
else if RemoteData.isNotAsked model.querySuggest then
[ Keyboard.Events.custom Keyboard.Events.Keydown
{ preventDefault = True
, stopPropagation = True
}
([ ( Keyboard.ArrowDown, QueryInputSuggestionsSubmit )
, ( Keyboard.ArrowUp, QueryInputSuggestionsSubmit )
]
|> List.map (\( k, m ) -> ( k, outMsg m ))
)
]
else
[]
)
)
[]
, div [ class "loader" ] []
, div [ class "btn-group" ]
[ button [ class "btn" ] [ text "Search" ]
]
]
, ul
[ id "dropdown-menu", class "dropdown-menu" ]
(if RemoteData.isSuccess model.querySuggest && List.length suggestions > 0 then
List.map viewSuggestion suggestions
else
[]
)
]
]
, case model.result of
@ -448,10 +815,10 @@ view path title model viewSuccess outMsg =
viewPager :
(Msg a -> b)
-> Model a
-> Result a
-> SearchResult a
-> String
-> Html b
viewPager outMsg model result path =
viewPager _ model result path =
ul [ class "pager" ]
[ li
[ classList
@ -652,37 +1019,16 @@ makeRequestBody :
-> String
-> List (List ( String, Json.Encode.Value ))
-> Http.Body
makeRequestBody query from size type_ query_field should_queries =
-- TODO: rescore how close the query is to the root of the name
-- |> List.append
-- ("""int i = 1;
-- for (token in doc['option_name.raw'][0].splitOnToken('.')) {
-- if (token == '"""
-- ++ query
-- ++ """') {
-- return 10000 - (i * 100);
-- }
-- i++;
-- }
-- return 10;
-- """
-- |> stringIn "source"
-- |> objectIn "script"
-- |> objectIn "script_score"
-- |> objectIn "function_score"
-- |> objectIn "rescore_query"
-- |> List.append ("total" |> stringIn "score_mode")
-- |> List.append ("total" |> stringIn "score_mode")
-- |> objectIn "query"
-- |> List.append [ ( "window_size", Json.Encode.int 1000 ) ]
-- |> objectIn "rescore"
-- )
-- |> List.append
-- [ ( "from", Json.Encode.int from )
-- , ( "size", Json.Encode.int size )
-- ]
-- |> Json.Encode.object
-- |> Http.jsonBody
makeRequestBody query from sizeRaw type_ query_field should_queries =
let
-- you can not request more then 10000 results otherwise it will return 404
size =
if from + sizeRaw > 10000 then
10000 - from
else
sizeRaw
in
Http.jsonBody
(Json.Encode.object
[ ( "from"
@ -718,20 +1064,10 @@ makeRequest :
-> String
-> Json.Decode.Decoder a
-> Options
-> String
-> Int
-> Int
-> (RemoteData.WebData (SearchResult a) -> Msg a)
-> Maybe String
-> Cmd (Msg a)
makeRequest body index decodeResultItemSource options query from sizeRaw =
let
-- you can not request more then 10000 results otherwise it will return 404
size =
if from + sizeRaw > 10000 then
10000 - from
else
sizeRaw
in
makeRequest body index decodeResultItemSource options responseMsg tracker =
Http.riskyRequest
{ method = "POST"
, headers =
@ -741,10 +1077,10 @@ makeRequest body index decodeResultItemSource options query from sizeRaw =
, body = body
, expect =
Http.expectJson
(RemoteData.fromResult >> QueryResponse)
(RemoteData.fromResult >> responseMsg)
(decodeResult decodeResultItemSource)
, timeout = Nothing
, tracker = Nothing
, tracker = tracker
}
@ -754,10 +1090,26 @@ makeRequest body index decodeResultItemSource options query from sizeRaw =
decodeResult :
Json.Decode.Decoder a
-> Json.Decode.Decoder (Result a)
-> Json.Decode.Decoder (SearchResult a)
decodeResult decodeResultItemSource =
Json.Decode.map Result
Json.Decode.map2 SearchResult
(Json.Decode.field "hits" (decodeResultHits decodeResultItemSource))
(Json.Decode.maybe (Json.Decode.field "suggest" (decodeSuggest decodeResultItemSource)))
decodeSuggest : Json.Decode.Decoder a -> Json.Decode.Decoder (SearchSuggest a)
decodeSuggest decodeResultItemSource =
Json.Decode.map SearchSuggest
(Json.Decode.maybe (Json.Decode.field "query" (Json.Decode.list (decodeSuggestQuery decodeResultItemSource))))
decodeSuggestQuery : Json.Decode.Decoder a -> Json.Decode.Decoder (SearchSuggestQuery a)
decodeSuggestQuery decodeResultItemSource =
Json.Decode.map4 SearchSuggestQuery
(Json.Decode.field "text" Json.Decode.string)
(Json.Decode.field "offset" Json.Decode.int)
(Json.Decode.field "length" Json.Decode.int)
(Json.Decode.field "options" (Json.Decode.list (decodeResultItem decodeResultItemSource)))
decodeResultHits : Json.Decode.Decoder a -> Json.Decode.Decoder (ResultHits a)
@ -777,9 +1129,10 @@ decodeResultHitsTotal =
decodeResultItem : Json.Decode.Decoder a -> Json.Decode.Decoder (ResultItem a)
decodeResultItem decodeResultItemSource =
Json.Decode.map5 ResultItem
Json.Decode.map6 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)
(Json.Decode.maybe (Json.Decode.field "text" Json.Decode.string))
(Json.Decode.maybe (Json.Decode.field "matched_queries" (Json.Decode.list Json.Decode.string)))

View file

@ -4,7 +4,6 @@ require("./index.scss");
const {Elm} = require('./Main');
console.log("WORKS: " + process.env.ELASTICSEARCH_MAPPING_SCHEMA_VERSION);
Elm.Main.init({
flags: {
elasticsearchMappingSchemaVersion: parseInt(process.env.ELASTICSEARCH_MAPPING_SCHEMA_VERSION) || 0,

View file

@ -30,19 +30,69 @@ header .navbar.navbar-static-top {
}
.search-page {
.search-input {
text-align: center;
.input-append input {
font-size: 24px;
height: 40px;
width: auto;
max-width: 15em;
.search-input.with-suggestions-loading {
.input-append div.loader {
display: block;
}
.input-append button {
}
.search-input.with-suggestions {
ul.dropdown-menu {
display: block;
}
.input-append input {
border-radius: 4px 0 0 0;
}
}
.search-input {
position: relative;
ul.dropdown-menu {
font-size: 18px;
width: 25.6em;
height: auto;
max-height: 200px;
border-radius: 0;
margin-top: -10px;
border-top: 0;
overflow-y: scroll;
li > a {
font-size: 14px;
}
li > a#dropdown-menu-selected {
color: #fff;
text-decoration: none;
background-color: #0081c2;
background-image: -moz-linear-gradient(top,#08c,#0077b3);
background-image: -webkit-gradient(linear,0 0,0 100%,from(#08c),to(#0077b3));
background-image: -webkit-linear-gradient(top,#08c,#0077b3);
background-image: -o-linear-gradient(top,#08c,#0077b3);
background-image: linear-gradient(to bottom,#08c,#0077b3);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0077b3',GradientType=0);
}
}
.input-append {
position: relative;
input {
font-size: 18px;
height: 40px;
width: 25em;
}
div.loader {
display: none;
z-index: 100;
position: absolute;
margin: 0;
top: 37px;
right: 125px;
font-size: 6px;
color: #999999;
}
button {
font-size: 24px;
height: 50px;
min-width: 4em;
}
}
form > p > strong {
vertical-align: middle;
font-size: 1.2em;
@ -84,7 +134,7 @@ header .navbar.navbar-static-top {
.loader,
.loader:before,
.loader:after {
background: #ffffff;
background: transparent;
-webkit-animation: load1 1s infinite ease-in-out;
animation: load1 1s infinite ease-in-out;
width: 1em;