1
0
mirror of https://github.com/stevenferrer/multi-select-facet.git synced 2025-11-23 21:54:45 +02:00

improve search and auto-suggest

- include query term in search
- improved autosuggest
This commit is contained in:
Steven Ferrer
2020-06-30 15:52:46 +08:00
parent 48c79f4be8
commit 8664f33a10
7 changed files with 127 additions and 76 deletions

View File

@@ -100,76 +100,76 @@ func initSolrSchema(ctx context.Context, collection string, solrClient solr.Clie
// auto-suggest field type // auto-suggest field type
fieldTypes := []solrschema.FieldType{ fieldTypes := []solrschema.FieldType{
// // approach #1 // approach #1
// // see: https://blog.griddynamics.com/implementing-autocomplete-with-solr/ // 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/
{ {
Name: "text_suggest", Name: "text_suggest",
Class: "solr.TextField", Class: "solr.TextField",
Stored: true, PositionIncrementGap: "100",
IndexAnalyzer: &solrschema.Analyzer{ IndexAnalyzer: &solrschema.Analyzer{
Tokenizer: &solrschema.Tokenizer{ Tokenizer: &solrschema.Tokenizer{
Class: "solr.WhitespaceTokenizerFactory", Class: "solr.StandardTokenizerFactory",
}, },
Filters: []solrschema.Filter{ Filters: []solrschema.Filter{
{ {
Class: "solr.LowerCaseFilterFactory", Class: "solr.LowerCaseFilterFactory",
}, },
{ {
Class: "solr.ASCIIFoldingFilterFactory", Class: "solr.EdgeNGramFilterFactory",
MinGramSize: 1,
MaxGramSize: 100,
}, },
}, },
}, },
QueryAnalyzer: &solrschema.Analyzer{ QueryAnalyzer: &solrschema.Analyzer{
Tokenizer: &solrschema.Tokenizer{ Tokenizer: &solrschema.Tokenizer{
Class: "solr.WhitespaceTokenizerFactory", Class: "solr.KeywordTokenizerFactory",
}, },
Filters: []solrschema.Filter{ Filters: []solrschema.Filter{
{ {
Class: "solr.LowerCaseFilterFactory", 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 { for _, fieldType := range fieldTypes {
@@ -247,6 +247,27 @@ func initSolrSchema(ctx context.Context, collection string, solrClient solr.Clie
Source: "*_s", Source: "*_s",
Dest: "suggest", 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 { for _, copyField := range copyFields {
@@ -267,7 +288,7 @@ func initSuggestConfig(ctx context.Context, collection string, configClient solr
"name": "suggest", "name": "suggest",
"class": "solr.SuggestComponent", "class": "solr.SuggestComponent",
"suggester": map[string]string{ "suggester": map[string]string{
"name": "mySuggester", "name": "default",
"lookupImpl": "FuzzyLookupFactory", "lookupImpl": "FuzzyLookupFactory",
"dictionaryImpl": "DocumentDictionaryFactory", "dictionaryImpl": "DocumentDictionaryFactory",
"field": "suggest", "field": "suggest",
@@ -285,7 +306,7 @@ func initSuggestConfig(ctx context.Context, collection string, configClient solr
"defaults": map[string]Any{ "defaults": map[string]Any{
"suggest": true, "suggest": true,
"suggest.count": 10, "suggest.count": 10,
"suggest.dictionary": "mySuggester", "suggest.dictionary": "default",
}, },
"components": []string{"suggest"}, "components": []string{"suggest"},
}, },
@@ -301,7 +322,7 @@ func initSuggestConfig(ctx context.Context, collection string, configClient solr
} }
func indexProducts(ctx context.Context, collection string, func indexProducts(ctx context.Context, collection string,
indexClient solrindex.JSONClient) error { indexClient solrindex.Client) error {
b, err := ioutil.ReadFile(dataPath) b, err := ioutil.ReadFile(dataPath)
if err != nil { if err != nil {
return err return err

View File

@@ -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 := Map{
"query": "{!parent tag=top filters=$skuFilters which=$product score=total v=$sku}", "query": "{!parent tag=top filters=$skuFilters which=$product score=total v=$sku}",
"queries": Map{ "queries": Map{

View File

@@ -20,15 +20,9 @@ func (h *suggestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
dict := "mySuggester" dict := "default"
suggestResp, err := h.solrClient.Suggester(). suggestResp, err := h.solrClient.Suggester().Suggest(r.Context(), h.collection,
Suggest(r.Context(), suggester.Request{ suggester.Params{Query: q, Dictionaries: []string{dict}})
Collection: h.collection,
Params: suggester.Params{
Query: q,
Dictionaries: []string{dict},
},
})
if err != nil { if err != nil {
http.Error(w, err.Error(), 500) http.Error(w, err.Error(), 500)
return return

3
go.mod
View File

@@ -3,9 +3,10 @@ module github.com/stevenferrer/multi-select-facet
go 1.14 go 1.14
require ( require (
github.com/davecgh/go-spew v1.1.0
github.com/go-chi/chi v4.1.2+incompatible github.com/go-chi/chi v4.1.2+incompatible
github.com/go-chi/cors v1.1.1 github.com/go-chi/cors v1.1.1
github.com/pkg/errors v0.9.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 golang.org/x/net v0.0.0-20200625001655-4c5254603344 // indirect
) )

8
go.sum
View File

@@ -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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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.1.2 h1:IT57+MJ8gUtl4Iu5E9+EK7pg6+bX0MOn0dz7bSYPeaY=
github.com/stevenferrer/solr-go v0.0.9/go.mod h1:ePa8+6kV1baPpoywPbmcR06wQ0500EGuMntYGxli5Xk= github.com/stevenferrer/solr-go v0.1.2/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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

View File

@@ -6,8 +6,11 @@
<Filters :facets="facets" @changed="onChanged" /> <Filters :facets="facets" @changed="onChanged" />
</div> </div>
<div class="column"> <div class="column">
<Search /> <Search @selected="onSelected" />
<br /> <br />
{{ query }}
<br />
<div class="columns is-multiline is-mobile"> <div class="columns is-multiline is-mobile">
<div <div
v-for="(product, i) in products" v-for="(product, i) in products"
@@ -27,12 +30,14 @@ import Filters from "./components/Filters";
import Product from "./components/Product"; import Product from "./components/Product";
import Search from "./components/Search"; import Search from "./components/Search";
async function search(filters) { async function search(query, filters) {
if (!filters) { if (!filters) {
filters = []; filters = [];
} }
const url = new URL("http://localhost:8081/search"); const url = new URL(
`http://localhost:8081/search?q=${encodeURIComponent(query)}`
);
filters.forEach((filter) => { filters.forEach((filter) => {
url.searchParams.append(filter.param, filter.selected.join(",")); url.searchParams.append(filter.param, filter.selected.join(","));
}); });
@@ -87,12 +92,13 @@ export default {
}, },
data() { data() {
return { return {
query: "",
products: null, products: null,
facets: null, facets: null,
}; };
}, },
async mounted() { async mounted() {
const { products, facets } = await search(); const { products, facets } = await search(this.query);
this.products = products; this.products = products;
this.facets = facets; this.facets = facets;
}, },
@@ -105,7 +111,25 @@ export default {
return { name, param, selected }; 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.products = products;
this.facets = facets; this.facets = facets;
}, },

View File

@@ -8,7 +8,8 @@
icon="search" icon="search"
:loading="isFetching" :loading="isFetching"
@typing="getAsyncData" @typing="getAsyncData"
@select="(option) => (selected = option)" @select="onSelected"
keep-first
clearable clearable
> >
<template slot-scope="props"> <template slot-scope="props">
@@ -31,15 +32,15 @@ export default {
}; };
}, },
methods: { methods: {
getAsyncData: debounce(function(name) { getAsyncData: debounce(function(query) {
if (!name.length) { if (!query.length) {
this.data = []; this.data = [];
return; return;
} }
this.isFetching = true; this.isFetching = true;
this.$http this.$http
.get(`http://localhost:8081/suggest?q=${encodeURIComponent(name)}`) .get(`http://localhost:8081/suggest?q=${encodeURIComponent(query)}`)
.then(({ data }) => { .then(({ data }) => {
this.data = []; this.data = [];
data.suggestions.forEach((item) => this.data.push(item)); data.suggestions.forEach((item) => this.data.push(item));
@@ -52,6 +53,11 @@ export default {
this.isFetching = false; this.isFetching = false;
}); });
}, 500), }, 500),
onSelected(option) {
this.selected = option;
this.$emit("selected", option);
},
}, },
}; };
</script> </script>