You've already forked multi-select-facet
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:
123
cmd/api/main.go
123
cmd/api/main.go
@@ -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
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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
3
go.mod
@@ -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
8
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/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=
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user