From 8664f33a100e68ba0035f1e7a7773719199d38f8 Mon Sep 17 00:00:00 2001 From: Steven Ferrer Date: Tue, 30 Jun 2020 15:52:46 +0800 Subject: [PATCH] improve search and auto-suggest - include query term in search - improved autosuggest --- cmd/api/main.go | 123 ++++++++++++++++++------------- cmd/api/search_handler.go | 9 +++ cmd/api/suggest_handler.go | 12 +-- go.mod | 3 +- go.sum | 8 +- webapp/src/App.vue | 34 +++++++-- webapp/src/components/Search.vue | 14 +++- 7 files changed, 127 insertions(+), 76 deletions(-) diff --git a/cmd/api/main.go b/cmd/api/main.go index e8fe2a8..15b0532 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -100,76 +100,76 @@ func initSolrSchema(ctx context.Context, collection string, solrClient solr.Clie // auto-suggest field type fieldTypes := []solrschema.FieldType{ - // // approach #1 - // // see: https://blog.griddynamics.com/implementing-autocomplete-with-solr/ - // { - // Name: "text_suggest", - // Class: "solr.TextField", - // PositionIncrementGap: "100", - // IndexAnalyzer: &solrschema.Analyzer{ - // Tokenizer: &solrschema.Tokenizer{ - // Class: "solr.StandardTokenizerFactory", - // }, - // Filters: []solrschema.Filter{ - // { - // Class: "solr.LowerCaseFilterFactory", - // }, - // { - // Class: "solr.EdgeNGramFilterFactory", - // MinGramSize: 1, - // MaxGramSize: 100, - // }, - // }, - // }, - // QueryAnalyzer: &solrschema.Analyzer{ - // Tokenizer: &solrschema.Tokenizer{ - // Class: "solr.KeywordTokenizerFactory", - // }, - // Filters: []solrschema.Filter{ - // { - // Class: "solr.LowerCaseFilterFactory", - // }, - // }, - // }, - // }, - - // approach #2 - // see: https://blog.griddynamics.com/implement-autocomplete-search-for-large-e-commerce-catalogs/ + // approach #1 + // see: https://blog.griddynamics.com/implementing-autocomplete-with-solr/ { - Name: "text_suggest", - Class: "solr.TextField", - Stored: true, + Name: "text_suggest", + Class: "solr.TextField", + PositionIncrementGap: "100", IndexAnalyzer: &solrschema.Analyzer{ Tokenizer: &solrschema.Tokenizer{ - Class: "solr.WhitespaceTokenizerFactory", + Class: "solr.StandardTokenizerFactory", }, Filters: []solrschema.Filter{ { Class: "solr.LowerCaseFilterFactory", }, { - Class: "solr.ASCIIFoldingFilterFactory", + Class: "solr.EdgeNGramFilterFactory", + MinGramSize: 1, + MaxGramSize: 100, }, }, }, QueryAnalyzer: &solrschema.Analyzer{ Tokenizer: &solrschema.Tokenizer{ - Class: "solr.WhitespaceTokenizerFactory", + Class: "solr.KeywordTokenizerFactory", }, Filters: []solrschema.Filter{ { Class: "solr.LowerCaseFilterFactory", }, - { - Class: "solr.ASCIIFoldingFilterFactory", - }, - { - Class: "solr.SynonymGraphFilterFactory", - Synonyms: "synonyms.txt", - }, }, }, }, + + // // approach #2 + // // see: https://blog.griddynamics.com/implement-autocomplete-search-for-large-e-commerce-catalogs/ + // { + // Name: "text_suggest", + // Class: "solr.TextField", + // Stored: true, + // IndexAnalyzer: &solrschema.Analyzer{ + // Tokenizer: &solrschema.Tokenizer{ + // Class: "solr.WhitespaceTokenizerFactory", + // }, + // Filters: []solrschema.Filter{ + // { + // Class: "solr.LowerCaseFilterFactory", + // }, + // { + // Class: "solr.ASCIIFoldingFilterFactory", + // }, + // }, + // }, + // QueryAnalyzer: &solrschema.Analyzer{ + // Tokenizer: &solrschema.Tokenizer{ + // Class: "solr.WhitespaceTokenizerFactory", + // }, + // Filters: []solrschema.Filter{ + // { + // Class: "solr.LowerCaseFilterFactory", + // }, + // { + // Class: "solr.ASCIIFoldingFilterFactory", + // }, + // { + // Class: "solr.SynonymGraphFilterFactory", + // Synonyms: "synonyms.txt", + // }, + // }, + // }, + // }, } for _, fieldType := range fieldTypes { @@ -247,6 +247,27 @@ func initSolrSchema(ctx context.Context, collection string, solrClient solr.Clie Source: "*_s", Dest: "suggest", }, + + { + Source: "name", + Dest: "_text_", + }, + { + Source: "category", + Dest: "_text_", + }, + { + Source: "brand", + Dest: "_text_", + }, + { + Source: "productType", + Dest: "_text_", + }, + { + Source: "*_s", + Dest: "_text_", + }, } for _, copyField := range copyFields { @@ -267,7 +288,7 @@ func initSuggestConfig(ctx context.Context, collection string, configClient solr "name": "suggest", "class": "solr.SuggestComponent", "suggester": map[string]string{ - "name": "mySuggester", + "name": "default", "lookupImpl": "FuzzyLookupFactory", "dictionaryImpl": "DocumentDictionaryFactory", "field": "suggest", @@ -285,7 +306,7 @@ func initSuggestConfig(ctx context.Context, collection string, configClient solr "defaults": map[string]Any{ "suggest": true, "suggest.count": 10, - "suggest.dictionary": "mySuggester", + "suggest.dictionary": "default", }, "components": []string{"suggest"}, }, @@ -301,7 +322,7 @@ func initSuggestConfig(ctx context.Context, collection string, configClient solr } func indexProducts(ctx context.Context, collection string, - indexClient solrindex.JSONClient) error { + indexClient solrindex.Client) error { b, err := ioutil.ReadFile(dataPath) if err != nil { return err diff --git a/cmd/api/search_handler.go b/cmd/api/search_handler.go index c4b3f7b..1b03c74 100644 --- a/cmd/api/search_handler.go +++ b/cmd/api/search_handler.go @@ -128,6 +128,15 @@ func (h *searchHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } + q := r.URL.Query().Get("q") + if len(q) == 0 { + q = "*" + } else { + q = fmt.Sprintf("%q", q) + } + + productFilters = append(productFilters, fmt.Sprintf("{!tag=top}_text_:%s", q)) + query := Map{ "query": "{!parent tag=top filters=$skuFilters which=$product score=total v=$sku}", "queries": Map{ diff --git a/cmd/api/suggest_handler.go b/cmd/api/suggest_handler.go index 8a9b0df..b6f453d 100644 --- a/cmd/api/suggest_handler.go +++ b/cmd/api/suggest_handler.go @@ -20,15 +20,9 @@ func (h *suggestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - dict := "mySuggester" - suggestResp, err := h.solrClient.Suggester(). - Suggest(r.Context(), suggester.Request{ - Collection: h.collection, - Params: suggester.Params{ - Query: q, - Dictionaries: []string{dict}, - }, - }) + dict := "default" + suggestResp, err := h.solrClient.Suggester().Suggest(r.Context(), h.collection, + suggester.Params{Query: q, Dictionaries: []string{dict}}) if err != nil { http.Error(w, err.Error(), 500) return diff --git a/go.mod b/go.mod index 4730869..d4796b1 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,10 @@ module github.com/stevenferrer/multi-select-facet go 1.14 require ( + github.com/davecgh/go-spew v1.1.0 github.com/go-chi/chi v4.1.2+incompatible github.com/go-chi/cors v1.1.1 github.com/pkg/errors v0.9.1 - github.com/stevenferrer/solr-go v0.1.1 + github.com/stevenferrer/solr-go v0.1.2 golang.org/x/net v0.0.0-20200625001655-4c5254603344 // indirect ) diff --git a/go.sum b/go.sum index 6fcc0ff..00b8d54 100644 --- a/go.sum +++ b/go.sum @@ -10,12 +10,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stevenferrer/solr-go v0.0.9 h1:G8PAQAWjbtoVNS0DOQ3g+7Bdp/nUgFCiyqoRsh56aeo= -github.com/stevenferrer/solr-go v0.0.9/go.mod h1:ePa8+6kV1baPpoywPbmcR06wQ0500EGuMntYGxli5Xk= -github.com/stevenferrer/solr-go v0.1.0 h1:gCw5J4szWcExmLwg+JMJjgs2wZI7x6Bgd5hhNoP3rjs= -github.com/stevenferrer/solr-go v0.1.0/go.mod h1:ePa8+6kV1baPpoywPbmcR06wQ0500EGuMntYGxli5Xk= -github.com/stevenferrer/solr-go v0.1.1 h1:xqGrvW9p0q4oVNgH0lj+pImEOISClgdnD+DVjn+RUxw= -github.com/stevenferrer/solr-go v0.1.1/go.mod h1:ePa8+6kV1baPpoywPbmcR06wQ0500EGuMntYGxli5Xk= +github.com/stevenferrer/solr-go v0.1.2 h1:IT57+MJ8gUtl4Iu5E9+EK7pg6+bX0MOn0dz7bSYPeaY= +github.com/stevenferrer/solr-go v0.1.2/go.mod h1:ePa8+6kV1baPpoywPbmcR06wQ0500EGuMntYGxli5Xk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/webapp/src/App.vue b/webapp/src/App.vue index 8631c49..0d7aded 100644 --- a/webapp/src/App.vue +++ b/webapp/src/App.vue @@ -6,8 +6,11 @@
- +
+ {{ query }} +
+
{ url.searchParams.append(filter.param, filter.selected.join(",")); }); @@ -87,12 +92,13 @@ export default { }, data() { return { + query: "", products: null, facets: null, }; }, async mounted() { - const { products, facets } = await search(); + const { products, facets } = await search(this.query); this.products = products; this.facets = facets; }, @@ -105,7 +111,25 @@ export default { return { name, param, selected }; }); - const { products, facets } = await search(filters); + const { products, facets } = await search(this.query, filters); + this.products = products; + this.facets = facets; + }, + async onSelected(option) { + if (!option) return; + + const { term: query } = option; + + this.query = query; + + const { facets: oldFacet } = this; + const filters = oldFacet + .filter(({ selected }) => selected.length > 0) + .map(({ name, param, selected }) => { + return { name, param, selected }; + }); + + const { products, facets } = await search(query, filters); this.products = products; this.facets = facets; }, diff --git a/webapp/src/components/Search.vue b/webapp/src/components/Search.vue index 52cdbf8..11a4534 100644 --- a/webapp/src/components/Search.vue +++ b/webapp/src/components/Search.vue @@ -8,7 +8,8 @@ icon="search" :loading="isFetching" @typing="getAsyncData" - @select="(option) => (selected = option)" + @select="onSelected" + keep-first clearable >