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 *.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 .DS_Store
example/dist .cache
.idea
ignore .node_repl_history
.npm
build/Release
dist dist
package-lock.json elm-stuff/
result
scripts/eval-*
eval-* 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 src-url

View file

@ -1 +1 @@
8 9

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
{ pkgs ? import <nixpkgs> { } { pkgs ? import <nixpkgs> { }
, version ? "0" , version ? pkgs.lib.removeSuffix "\n" (builtins.readFile ./../VERSION)
}: }:
let let
inherit (pkgs.poetry2nix) mkPoetryApplication overrides; inherit (pkgs.poetry2nix) mkPoetryApplication overrides;
@ -13,11 +13,21 @@ mkPoetryApplication {
''; '';
}); });
}); });
nativeBuildInputs = [
pkgs.poetry
];
checkPhase = '' checkPhase = ''
black --diff --check ./import_scripts export PYTHONPATH=$PWD:$PYTHONPATH
flake8 --ignore W503,E501,E265,E203 ./import_scripts black --diff --check import_scripts/ tests/
flake8 --ignore W503,E501,E265,E203 import_scripts/ tests/
mypy import_scripts/ tests/
pytest -vv tests/
''; '';
postInstall = '' postInstall = ''
wrapProgram $out/bin/import-channel --set INDEX_SCHEMA_VERSION "${version}" 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 boto3 # type: ignore
import botocore import botocore # type: ignore
import botocore.client import botocore.client # type: ignore
import click import click
import click_log import click_log # type: ignore
import elasticsearch import elasticsearch # type: ignore
import elasticsearch.helpers import elasticsearch.helpers # type: ignore
import json import json
import logging import logging
import os import os
import os.path import os.path
import pypandoc import pypandoc # type: ignore
import re import re
import requests import requests
import shlex import shlex
import subprocess import subprocess
import sys import sys
import tqdm import tqdm # type: ignore
import typing
import xml.etree.ElementTree import xml.etree.ElementTree
logger = logging.getLogger("import-channel") logger = logging.getLogger("import-channel")
@ -55,6 +56,12 @@ MAPPING = {
"properties": { "properties": {
"type": {"type": "keyword"}, "type": {"type": "keyword"},
# Package fields # Package fields
"package_suggestions": {
"type": "completion",
"analyzer": "lowercase",
"search_analyzer": "lowercase",
"preserve_position_increments": False,
},
"package_hydra_build": { "package_hydra_build": {
"type": "nested", "type": "nested",
"properties": { "properties": {
@ -98,6 +105,12 @@ MAPPING = {
"package_homepage": {"type": "keyword"}, "package_homepage": {"type": "keyword"},
"package_system": {"type": "keyword"}, "package_system": {"type": "keyword"},
# Options fields # Options fields
"option_suggestions": {
"type": "completion",
"analyzer": "lowercase",
"search_analyzer": "lowercase",
"preserve_position_increments": False,
},
"option_name": {"type": "keyword", "normalizer": "lowercase"}, "option_name": {"type": "keyword", "normalizer": "lowercase"},
"option_name_query": {"type": "keyword", "normalizer": "lowercase"}, "option_name_query": {"type": "keyword", "normalizer": "lowercase"},
"option_description": {"type": "text"}, "option_description": {"type": "text"},
@ -109,11 +122,33 @@ MAPPING = {
} }
def split_query(text): def parse_suggestions(text: str) -> typing.List[typing.Dict[str, object]]:
"""Tokenize package attr_name """Tokenize option_name
Example: 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 python37Packages.test_name-test
= index: 0 = index: 0
- python37Packages.test1_name-test2 - python37Packages.test1_name-test2
@ -345,9 +380,10 @@ def get_packages(evaluation, evaluation_builds):
yield dict( yield dict(
type="package", type="package",
package_suggestions=parse_suggestions(attr_name),
package_hydra=hydra, package_hydra=hydra,
package_attr_name=attr_name, 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_attr_set=attr_set,
package_pname=remove_attr_set(data["pname"]), package_pname=remove_attr_set(data["pname"]),
package_pversion=data["version"], package_pversion=data["version"],
@ -411,8 +447,9 @@ def get_options(evaluation):
yield dict( yield dict(
type="option", type="option",
option_suggestions=parse_suggestions(name),
option_name=name, option_name=name,
option_name_query=split_query(name), option_name_query=parse_query(name),
option_description=description, option_description=description,
option_type=option.get("type"), option_type=option.get("type"),
option_default=default, option_default=default,
@ -539,5 +576,3 @@ def run(es_url, channel, force, verbose):
if __name__ == "__main__": if __name__ == "__main__":
run() 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" black = "^19.10b0"
flake8 = "^3.8.3" flake8 = "^3.8.3"
mypy = "^0.780" mypy = "^0.780"
pytest = "^5.4.3"
setuptools = "^47.3.1"
boto3-stubs = "^1.14.6"
[build-system] [build-system]
requires = ["poetry>=0.12"] 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 |> updateWith Home HomeMsg model
( PackagesMsg subMsg, Packages subModel ) -> ( PackagesMsg subMsg, Packages subModel ) ->
Page.Packages.update model.navKey subMsg subModel Page.Packages.update model.navKey model.elasticsearch subMsg subModel
|> updateWith Packages PackagesMsg model |> updateWith Packages PackagesMsg model
( OptionsMsg subMsg, Options subModel ) -> ( OptionsMsg subMsg, Options subModel ) ->
Page.Options.update model.navKey subMsg subModel Page.Options.update model.navKey model.elasticsearch subMsg subModel
|> updateWith Options OptionsMsg model |> updateWith Options OptionsMsg model
( _, _ ) -> ( _, _ ) ->

View file

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

View file

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

View file

@ -2,8 +2,8 @@ module Search exposing
( Model ( Model
, Msg(..) , Msg(..)
, Options , Options
, Result
, ResultItem , ResultItem
, SearchResult
, channelDetailsFromId , channelDetailsFromId
, decodeResult , decodeResult
, init , init
@ -13,8 +13,12 @@ module Search exposing
, view , view
) )
import Array
import Base64 import Base64
import Browser.Dom
import Browser.Navigation import Browser.Navigation
import Debouncer.Messages
import Dict
import Html import Html
exposing exposing
( Html ( Html
@ -25,12 +29,10 @@ import Html
, form , form
, h1 , h1
, h4 , h4
, i
, input , input
, li , li
, option
, p , p
, select
, span
, strong , strong
, text , text
, ul , ul
@ -41,6 +43,7 @@ import Html.Attributes
, class , class
, classList , classList
, href , href
, id
, type_ , type_
, value , value
) )
@ -50,27 +53,50 @@ import Html.Events
, onClick , onClick
, onInput , onInput
, onSubmit , onSubmit
, preventDefaultOn
) )
import Http import Http
import Json.Decode import Json.Decode
import Json.Encode import Json.Encode
import Keyboard
import Keyboard.Events
import RemoteData import RemoteData
import Task
import Url.Builder import Url.Builder
type alias Char =
String
type alias Model a = type alias Model a =
{ channel : String { channel : String
, query : Maybe 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 , show : Maybe String
, from : Int , from : Int
, size : Int , size : Int
} }
type alias Result a = type alias SearchResult a =
{ hits : ResultHits 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 = type alias ResultHitsTotal =
{ value : Int { value : Int
, relation : String -- TODO: this should probably be Enum , relation : String
} }
@ -92,10 +118,19 @@ type alias ResultItem a =
, id : String , id : String
, score : Float , score : Float
, source : a , source : a
, text : Maybe String
, matched_queries : Maybe (List String) , matched_queries : Maybe (List String)
} }
itemHtml : Char -> Html Never
itemHtml char =
div []
[ i [ class "fa fa-rebel" ] []
, text (" " ++ char)
]
init : init :
Maybe String Maybe String
-> Maybe String -> Maybe String
@ -122,7 +157,13 @@ init channel query show from size model =
|> Maybe.withDefault 15 |> Maybe.withDefault 15
in in
( { channel = Maybe.withDefault defaultChannel channel ( { channel = Maybe.withDefault defaultChannel channel
, queryDebounce =
Debouncer.Messages.manual
|> Debouncer.Messages.settleWhenQuietFor (Just <| Debouncer.Messages.fromSeconds 0.6)
|> Debouncer.Messages.toDebouncer
, query = query , query = query
, querySuggest = RemoteData.NotAsked
, querySelectedSuggestion = Nothing
, result = , result =
model model
|> Maybe.map (\x -> x.result) |> Maybe.map (\x -> x.result)
@ -144,19 +185,34 @@ init channel query show from size model =
type Msg a type Msg a
= NoOp = NoOp
| ChannelChange String | ChannelChange String
| QueryInputDebounce (Debouncer.Messages.Msg (Msg a))
| QueryInput String | QueryInput String
| QueryInputSuggestionsSubmit
| QueryInputSuggestionsResponse (RemoteData.WebData (SearchResult a))
| QuerySubmit | QuerySubmit
| QueryResponse (RemoteData.WebData (Result a)) | QueryResponse (RemoteData.WebData (SearchResult a))
| ShowDetails String | ShowDetails String
| SuggestionsMoveDown
| SuggestionsMoveUp
| SuggestionsSelect
| SuggestionsClickSelect String
| SuggestionsClose
update : update :
String String
-> Browser.Navigation.Key -> Browser.Navigation.Key
-> String
-> Options
-> Json.Decode.Decoder a
-> Msg a -> Msg a
-> Model a -> Model a
-> ( Model a, Cmd (Msg 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 case msg of
NoOp -> NoOp ->
( model ( model
@ -187,8 +243,84 @@ update path navKey msg model =
|> Browser.Navigation.pushUrl navKey |> 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 -> 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 , Cmd.none
) )
@ -226,6 +358,147 @@ update path navKey msg model =
|> Browser.Navigation.pushUrl navKey |> 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 : createUrl :
String 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 : view :
String String
-> String -> String
-> Model a -> Model a
-> (String -> Maybe String -> Result a -> Html b) -> (String -> Maybe String -> SearchResult a -> Html b)
-> (Msg a -> b) -> (Msg a -> b)
-> Html b -> Html b
view path title model viewSuccess outMsg = 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" ] div [ class "search-page" ]
[ h1 [ class "page-header" ] [ text title ] [ 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) ] [ form [ onSubmit (outMsg QuerySubmit) ]
[ p [ p
[] []
@ -364,15 +690,56 @@ view path title model viewSuccess outMsg =
[ class "input-append" [ class "input-append"
] ]
[ input [ input
[ type_ "text" ([ type_ "text"
, onInput (\x -> outMsg (QueryInput x)) , onInput (\x -> outMsg (QueryInput x))
, value <| Maybe.withDefault "" model.query , 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" ] , div [ class "btn-group" ]
[ button [ class "btn" ] [ text "Search" ] [ 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 , case model.result of
@ -448,10 +815,10 @@ view path title model viewSuccess outMsg =
viewPager : viewPager :
(Msg a -> b) (Msg a -> b)
-> Model a -> Model a
-> Result a -> SearchResult a
-> String -> String
-> Html b -> Html b
viewPager outMsg model result path = viewPager _ model result path =
ul [ class "pager" ] ul [ class "pager" ]
[ li [ li
[ classList [ classList
@ -652,37 +1019,16 @@ makeRequestBody :
-> String -> String
-> List (List ( String, Json.Encode.Value )) -> List (List ( String, Json.Encode.Value ))
-> Http.Body -> Http.Body
makeRequestBody query from size type_ query_field should_queries = makeRequestBody query from sizeRaw type_ query_field should_queries =
-- TODO: rescore how close the query is to the root of the name let
-- |> List.append -- you can not request more then 10000 results otherwise it will return 404
-- ("""int i = 1; size =
-- for (token in doc['option_name.raw'][0].splitOnToken('.')) { if from + sizeRaw > 10000 then
-- if (token == '""" 10000 - from
-- ++ query
-- ++ """') { else
-- return 10000 - (i * 100); sizeRaw
-- } in
-- 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
Http.jsonBody Http.jsonBody
(Json.Encode.object (Json.Encode.object
[ ( "from" [ ( "from"
@ -718,20 +1064,10 @@ makeRequest :
-> String -> String
-> Json.Decode.Decoder a -> Json.Decode.Decoder a
-> Options -> Options
-> String -> (RemoteData.WebData (SearchResult a) -> Msg a)
-> Int -> Maybe String
-> Int
-> Cmd (Msg a) -> Cmd (Msg a)
makeRequest body index decodeResultItemSource options query from sizeRaw = makeRequest body index decodeResultItemSource options responseMsg tracker =
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.riskyRequest Http.riskyRequest
{ method = "POST" { method = "POST"
, headers = , headers =
@ -741,10 +1077,10 @@ makeRequest body index decodeResultItemSource options query from sizeRaw =
, body = body , body = body
, expect = , expect =
Http.expectJson Http.expectJson
(RemoteData.fromResult >> QueryResponse) (RemoteData.fromResult >> responseMsg)
(decodeResult decodeResultItemSource) (decodeResult decodeResultItemSource)
, timeout = Nothing , timeout = Nothing
, tracker = Nothing , tracker = tracker
} }
@ -754,10 +1090,26 @@ makeRequest body index decodeResultItemSource options query from sizeRaw =
decodeResult : decodeResult :
Json.Decode.Decoder a Json.Decode.Decoder a
-> Json.Decode.Decoder (Result a) -> Json.Decode.Decoder (SearchResult a)
decodeResult decodeResultItemSource = decodeResult decodeResultItemSource =
Json.Decode.map Result Json.Decode.map2 SearchResult
(Json.Decode.field "hits" (decodeResultHits decodeResultItemSource)) (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) 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 : Json.Decode.Decoder a -> Json.Decode.Decoder (ResultItem a)
decodeResultItem decodeResultItemSource = decodeResultItem decodeResultItemSource =
Json.Decode.map5 ResultItem Json.Decode.map6 ResultItem
(Json.Decode.field "_index" Json.Decode.string) (Json.Decode.field "_index" Json.Decode.string)
(Json.Decode.field "_id" Json.Decode.string) (Json.Decode.field "_id" Json.Decode.string)
(Json.Decode.field "_score" Json.Decode.float) (Json.Decode.field "_score" Json.Decode.float)
(Json.Decode.field "_source" decodeResultItemSource) (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))) (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'); const {Elm} = require('./Main');
console.log("WORKS: " + process.env.ELASTICSEARCH_MAPPING_SCHEMA_VERSION);
Elm.Main.init({ Elm.Main.init({
flags: { flags: {
elasticsearchMappingSchemaVersion: parseInt(process.env.ELASTICSEARCH_MAPPING_SCHEMA_VERSION) || 0, elasticsearchMappingSchemaVersion: parseInt(process.env.ELASTICSEARCH_MAPPING_SCHEMA_VERSION) || 0,

View file

@ -30,19 +30,69 @@ header .navbar.navbar-static-top {
} }
.search-page { .search-page {
.search-input { .search-input.with-suggestions-loading {
text-align: center; .input-append div.loader {
.input-append input { display: block;
font-size: 24px;
height: 40px;
width: auto;
max-width: 15em;
} }
.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; font-size: 24px;
height: 50px; height: 50px;
min-width: 4em; min-width: 4em;
} }
}
form > p > strong { form > p > strong {
vertical-align: middle; vertical-align: middle;
font-size: 1.2em; font-size: 1.2em;
@ -84,7 +134,7 @@ header .navbar.navbar-static-top {
.loader, .loader,
.loader:before, .loader:before,
.loader:after { .loader:after {
background: #ffffff; background: transparent;
-webkit-animation: load1 1s infinite ease-in-out; -webkit-animation: load1 1s infinite ease-in-out;
animation: load1 1s infinite ease-in-out; animation: load1 1s infinite ease-in-out;
width: 1em; width: 1em;