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
dynamic facet configurations
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
# Multi-Select Facet Example
|
||||
An example of multi-select facet using [Solr](https://lucene.apache.org/solr), [Vue](https://vuejs.org) and [Go](http://go.dev/).
|
||||
|
||||
This example uses [Buefy](http://buefy.org/) components and [solr-go](https://github.com/stevenferrer/solr-go) for interacting with [Solr](https://lucene.apache.org/solr).
|
||||
|
||||
## Running the example
|
||||
Use docker-compose maybe?
|
||||
|
||||
## Contributing
|
||||
Please feel free to improve this by [sending a PR](https://github.com/stevenferrer/multi-select-facet/pulls) or [opening an issue](https://github.com/stevenferrer/multi-select-facet/issues).
|
||||
|
||||
@@ -1,276 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/stevenferrer/solr-go"
|
||||
)
|
||||
|
||||
type facet struct {
|
||||
Name string `json:"name"`
|
||||
Buckets []bucket `json:"buckets"`
|
||||
}
|
||||
|
||||
type bucket struct {
|
||||
Val string `json:"val"`
|
||||
SkuCount int `json:"skuCount"`
|
||||
ProductCount int `json:"productCount"`
|
||||
}
|
||||
|
||||
type product struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"`
|
||||
ProductType string `json:"productType"`
|
||||
Brand string `json:"brand"`
|
||||
}
|
||||
|
||||
type facetConfig struct {
|
||||
field, facet string
|
||||
vals []string
|
||||
}
|
||||
|
||||
type searchHandler struct {
|
||||
solrClient solr.Client
|
||||
}
|
||||
|
||||
func (h *searchHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
categories := strings.Split(r.URL.Query().Get("categories"), ",")
|
||||
productTypes := strings.Split(r.URL.Query().Get("productTypes"), ",")
|
||||
brands := strings.Split(r.URL.Query().Get("brands"), ",")
|
||||
colorFamilies := strings.Split(r.URL.Query().Get("colorFamilies"), ",")
|
||||
simCardSlots := strings.Split(r.URL.Query().Get("simCardSlots"), ",")
|
||||
operatingSystems := strings.Split(r.URL.Query().Get("operatingSystems"), ",")
|
||||
storageCapacities := strings.Split(r.URL.Query().Get("storageCapacities"), ",")
|
||||
|
||||
facetsMap := M{}
|
||||
|
||||
catsFacetCfg := facetConfig{
|
||||
facet: "categories",
|
||||
field: "category",
|
||||
vals: categories,
|
||||
}
|
||||
|
||||
productTypsFacetCfg := facetConfig{
|
||||
facet: "productTypes",
|
||||
field: "productType",
|
||||
vals: productTypes,
|
||||
}
|
||||
|
||||
brandsFacetCfg := facetConfig{
|
||||
facet: "brands",
|
||||
field: "brand",
|
||||
vals: brands,
|
||||
}
|
||||
|
||||
filters := []string{}
|
||||
for _, facetCfg := range []facetConfig{catsFacetCfg, productTypsFacetCfg, brandsFacetCfg} {
|
||||
tagVals := []string{}
|
||||
for _, val := range facetCfg.vals {
|
||||
if val == "" {
|
||||
continue
|
||||
}
|
||||
tagVals = append(tagVals, facetCfg.field+":"+val)
|
||||
}
|
||||
|
||||
if len(tagVals) == 0 {
|
||||
filters = append(filters, fmt.Sprintf("{!tag=top}%s:*", facetCfg.field))
|
||||
} else {
|
||||
filters = append(filters, fmt.Sprintf("{!tag=top}%s", strings.Join(tagVals, " OR ")))
|
||||
}
|
||||
|
||||
facetsMap[facetCfg.facet] = M{
|
||||
"facet": M{
|
||||
"productCount": "uniqueBlock(_root_)",
|
||||
},
|
||||
"field": facetCfg.field,
|
||||
"limit": -1,
|
||||
"type": "terms",
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
colorFamiliesFctCfg := facetConfig{
|
||||
facet: "colorFamilies",
|
||||
field: "colorFamily_s",
|
||||
vals: colorFamilies,
|
||||
}
|
||||
|
||||
simCardSlotsFctCfg := facetConfig{
|
||||
facet: "simCardSlots",
|
||||
field: "simCardSlots_s",
|
||||
vals: simCardSlots,
|
||||
}
|
||||
|
||||
operatingSystemsFctCfg := facetConfig{
|
||||
facet: "operatingSystems",
|
||||
field: "operatingSystem_s",
|
||||
vals: operatingSystems,
|
||||
}
|
||||
|
||||
storageCapacitiesFctCfg := facetConfig{
|
||||
facet: "storageCapacities",
|
||||
field: "storageCapacity_s",
|
||||
vals: storageCapacities,
|
||||
}
|
||||
|
||||
childFqs := []string{}
|
||||
for _, fctCfg := range []facetConfig{colorFamiliesFctCfg, simCardSlotsFctCfg, operatingSystemsFctCfg, storageCapacitiesFctCfg} {
|
||||
tagVals := []string{}
|
||||
for _, val := range fctCfg.vals {
|
||||
if val == "" {
|
||||
continue
|
||||
}
|
||||
tagVals = append(tagVals, fctCfg.field+":"+val)
|
||||
}
|
||||
|
||||
if len(tagVals) == 0 {
|
||||
childFqs = append(childFqs, fmt.Sprintf("{!tag=%s}%s:*", fctCfg.field, fctCfg.field))
|
||||
} else {
|
||||
childFqs = append(childFqs, fmt.Sprintf("{!tag=%s}%s", fctCfg.field, strings.Join(tagVals, " OR ")))
|
||||
}
|
||||
|
||||
facetsMap[fctCfg.facet] = M{
|
||||
"domain": M{
|
||||
"excludeTags": "top",
|
||||
"filter": []string{
|
||||
fmt.Sprintf("{!filters param=$child.fq excludeTags=%s v=$child.query}", fctCfg.field),
|
||||
"{!child of=$parent.fq filters=$fq v=$parent.fq}",
|
||||
},
|
||||
},
|
||||
"type": "terms",
|
||||
"field": fctCfg.field,
|
||||
"limit": -1,
|
||||
"facet": M{
|
||||
"productCount": "uniqueBlock(_root_)",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
query := M{
|
||||
"query": "{!parent tag=top filters=$child.fq which=$parent.fq score=total v=$child.query}",
|
||||
"queries": M{
|
||||
"parent.fq": "docType:product",
|
||||
"child.query": "docType:sku",
|
||||
"child.fq": childFqs,
|
||||
},
|
||||
"filter": filters,
|
||||
"fields": "*",
|
||||
"facet": facetsMap,
|
||||
}
|
||||
|
||||
// b, err := json.MarshalIndent(query, "", " ")
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
|
||||
// fmt.Println(string(b))
|
||||
|
||||
queryRespone, err := h.solrClient.Query().Query(r.Context(), collection, query)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
|
||||
var facets = []facet{}
|
||||
for k, v := range queryRespone.Facets {
|
||||
if k == "count" {
|
||||
continue
|
||||
}
|
||||
|
||||
vv, ok := v.(map[string]interface{})
|
||||
if !ok {
|
||||
http.Error(w, "buckets is not map[string]interface{}", 500)
|
||||
}
|
||||
|
||||
bucks, ok := vv["buckets"].([]interface{})
|
||||
if !ok {
|
||||
http.Error(w, "buckets not found", 500)
|
||||
}
|
||||
|
||||
buckets := []bucket{}
|
||||
for _, bk := range bucks {
|
||||
buck, ok := bk.(map[string]interface{})
|
||||
if !ok {
|
||||
http.Error(w, "buck not map[string]interface{}", 500)
|
||||
}
|
||||
|
||||
productCount, ok := buck["productCount"].(float64)
|
||||
if !ok {
|
||||
http.Error(w, "product count not found", 500)
|
||||
}
|
||||
|
||||
skuCount, ok := buck["count"].(float64)
|
||||
if !ok {
|
||||
http.Error(w, "sku count not found", 500)
|
||||
}
|
||||
|
||||
val, ok := buck["val"].(string)
|
||||
if !ok {
|
||||
http.Error(w, "val not found", 500)
|
||||
}
|
||||
|
||||
buckets = append(buckets, bucket{
|
||||
Val: val,
|
||||
SkuCount: int(skuCount),
|
||||
ProductCount: int(productCount),
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
facets = append(facets, facet{
|
||||
Name: k,
|
||||
Buckets: buckets,
|
||||
})
|
||||
}
|
||||
|
||||
var products = []product{}
|
||||
for _, doc := range queryRespone.Response.Docs {
|
||||
id, ok := doc["id"].(string)
|
||||
if !ok {
|
||||
http.Error(w, "id not found", 500)
|
||||
}
|
||||
|
||||
name, ok := doc["name"].([]interface{})
|
||||
if !ok {
|
||||
http.Error(w, "name not found", 500)
|
||||
}
|
||||
|
||||
category, ok := doc["category"].([]interface{})
|
||||
if !ok {
|
||||
http.Error(w, "category not found", 500)
|
||||
}
|
||||
|
||||
brand, ok := doc["brand"].([]interface{})
|
||||
if !ok {
|
||||
http.Error(w, "brand not found", 500)
|
||||
}
|
||||
|
||||
productType, ok := doc["productType"].(string)
|
||||
if !ok {
|
||||
http.Error(w, "productType not found", 500)
|
||||
}
|
||||
|
||||
products = append(products, product{
|
||||
ID: id,
|
||||
Name: name[0].(string),
|
||||
Category: category[0].(string),
|
||||
Brand: brand[0].(string),
|
||||
ProductType: productType,
|
||||
})
|
||||
}
|
||||
|
||||
resp := M{
|
||||
"products": products,
|
||||
"facets": facets,
|
||||
"queryResponse": queryRespone,
|
||||
}
|
||||
|
||||
w.Header().Add("content-type", "application/json")
|
||||
err = json.NewEncoder(w).Encode(resp)
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(500), 500)
|
||||
}
|
||||
}
|
||||
0
api/.gitignore → cmd/api/.gitignore
vendored
0
api/.gitignore → cmd/api/.gitignore
vendored
@@ -17,16 +17,20 @@ import (
|
||||
solrschema "github.com/stevenferrer/solr-go/schema"
|
||||
)
|
||||
|
||||
type M = map[string]interface{}
|
||||
// Any is a convenience type for interface{}
|
||||
type Any = interface{}
|
||||
|
||||
// Map is a convenience type for map[string]interface{}
|
||||
type Map = map[string]Any
|
||||
|
||||
const (
|
||||
collection = "multi-select-demo"
|
||||
dataPath = "phones.json"
|
||||
dataPath = "products.json"
|
||||
)
|
||||
|
||||
func main() {
|
||||
initSchema := flag.Bool("init-schema", false, "initialize solr schema")
|
||||
index := flag.Bool("index", false, "index products")
|
||||
collection := flag.String("collection", "multi-select-demo", "specify the name of collection")
|
||||
initSchema := flag.Bool("initialize-schema", false, "initialize solr schema")
|
||||
index := flag.Bool("index-data", false, "index the data")
|
||||
flag.Parse()
|
||||
|
||||
solrClient := solr.NewClient("localhost", 8983)
|
||||
@@ -34,7 +38,7 @@ func main() {
|
||||
ctx := context.Background()
|
||||
if *initSchema {
|
||||
log.Print("initializing solr schema...")
|
||||
err := initSolrSchema(ctx, solrClient.Schema())
|
||||
err := initSolrSchema(ctx, *collection, solrClient.Schema())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -42,7 +46,7 @@ func main() {
|
||||
|
||||
if *index {
|
||||
log.Println("indexing products...")
|
||||
err := indexProducts(ctx, solrClient.Index())
|
||||
err := indexProducts(ctx, *collection, solrClient.Index())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -63,6 +67,7 @@ func main() {
|
||||
|
||||
// search handler
|
||||
r.Method(http.MethodGet, "/search", &searchHandler{
|
||||
collection: *collection,
|
||||
solrClient: solrClient,
|
||||
})
|
||||
|
||||
@@ -74,7 +79,8 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func initSolrSchema(ctx context.Context, schemaClient solrschema.Client) error {
|
||||
func initSolrSchema(ctx context.Context, collection string,
|
||||
schemaClient solrschema.Client) error {
|
||||
// define the fields
|
||||
fields := []solrschema.Field{
|
||||
{
|
||||
@@ -116,31 +122,29 @@ func initSolrSchema(ctx context.Context, schemaClient solrschema.Client) error {
|
||||
}
|
||||
}
|
||||
|
||||
// define copy field
|
||||
err := schemaClient.AddCopyField(ctx, collection, solrschema.CopyField{
|
||||
Source: "*",
|
||||
Dest: "_text_",
|
||||
})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "add copy field")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func indexProducts(ctx context.Context, indexClient solrindex.JSONClient) error {
|
||||
func indexProducts(ctx context.Context, collection string,
|
||||
indexClient solrindex.JSONClient) error {
|
||||
b, err := ioutil.ReadFile(dataPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var docs []M
|
||||
var docs []Map
|
||||
err = json.Unmarshal(b, &docs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = indexClient.AddMultiple(ctx, collection, docs)
|
||||
err = indexClient.AddDocs(ctx, collection, docs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// commit updates
|
||||
err = indexClient.Commit(ctx, collection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -186,5 +186,63 @@
|
||||
"storageCapacity_s": "128GB"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "7",
|
||||
"name": "Amazon Kindle Paperwhite - 10th Gen",
|
||||
"brand": "Amazon",
|
||||
"category": "Electronic Devices",
|
||||
"productType": "E-reader",
|
||||
"docType": "product",
|
||||
"skus": [
|
||||
{
|
||||
"id": "70",
|
||||
"docType": "sku",
|
||||
"colorFamily_s": "Black",
|
||||
"storageCapacity_s": "8GB",
|
||||
"operatingSystem_s": "Linux"
|
||||
},
|
||||
{
|
||||
"id": "71",
|
||||
"docType": "sku",
|
||||
"colorFamily_s": "Black",
|
||||
"storageCapacity_s": "32GB",
|
||||
"operatingSystem_s": "Linux"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "8",
|
||||
"name": "Kobo Clara HD 6\" Carta E Ink Touchscreen E-Reader",
|
||||
"brand": "Kobo",
|
||||
"category": "Electronic Devices",
|
||||
"productType": "E-reader",
|
||||
"docType": "product",
|
||||
"skus": [
|
||||
{
|
||||
"id": "80",
|
||||
"docType": "sku",
|
||||
"colorFamily_s": "Black",
|
||||
"storageCapacity_s": "8GB",
|
||||
"operatingSystem_s": "Linux"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "9",
|
||||
"name": "Likebook Mars E-Reader, 7.8' Carta Touch Screen",
|
||||
"brand": "Likebook Mars",
|
||||
"category": "Electronic Devices",
|
||||
"productType": "E-reader",
|
||||
"docType": "product",
|
||||
"skus": [
|
||||
{
|
||||
"id": "90",
|
||||
"docType": "sku",
|
||||
"colorFamily_s": "Black",
|
||||
"storageCapacity_s": "16GB",
|
||||
"operatingSystem_s": "Android"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
66
cmd/api/sample-query.json
Normal file
66
cmd/api/sample-query.json
Normal file
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"query": "{!parent tag=top filters=$skuFilters which=$product score=total v=$sku}",
|
||||
"queries": {
|
||||
"product": "docType:product",
|
||||
"sku": "docType:sku",
|
||||
"skuFilters": [
|
||||
"{!tag=colorFamily_s}colorFamily_s:Black",
|
||||
"{!tag=operatingSystem_s}operatingSystem_s:*",
|
||||
"{!tag=storageCapacity_s}storageCapacity_s:*"
|
||||
]
|
||||
},
|
||||
"filter": ["{!tag=top}productType:*", "{!tag=top}brand:Amazon"],
|
||||
"facet": {
|
||||
"Brand": {
|
||||
"facet": { "productCount": "uniqueBlock(_root_)" },
|
||||
"field": "brand",
|
||||
"limit": -1,
|
||||
"type": "terms"
|
||||
},
|
||||
"Color Family": {
|
||||
"domain": {
|
||||
"excludeTags": "top",
|
||||
"filter": [
|
||||
"{!filters param=$skuFilters excludeTags=colorFamily_s v=$sku}",
|
||||
"{!child of=$product filters=$fq v=$product}"
|
||||
]
|
||||
},
|
||||
"facet": { "productCount": "uniqueBlock(_root_)" },
|
||||
"field": "colorFamily_s",
|
||||
"limit": -1,
|
||||
"type": "terms"
|
||||
},
|
||||
"Operating System": {
|
||||
"domain": {
|
||||
"excludeTags": "top",
|
||||
"filter": [
|
||||
"{!filters param=$skuFilters excludeTags=operatingSystem_s v=$sku}",
|
||||
"{!child of=$product filters=$fq v=$product}"
|
||||
]
|
||||
},
|
||||
"facet": { "productCount": "uniqueBlock(_root_)" },
|
||||
"field": "operatingSystem_s",
|
||||
"limit": -1,
|
||||
"type": "terms"
|
||||
},
|
||||
"Product Type": {
|
||||
"facet": { "productCount": "uniqueBlock(_root_)" },
|
||||
"field": "productType",
|
||||
"limit": -1,
|
||||
"type": "terms"
|
||||
},
|
||||
"Storage Capacity": {
|
||||
"domain": {
|
||||
"excludeTags": "top",
|
||||
"filter": [
|
||||
"{!filters param=$skuFilters excludeTags=storageCapacity_s v=$sku}",
|
||||
"{!child of=$product filters=$fq v=$product}"
|
||||
]
|
||||
},
|
||||
"facet": { "productCount": "uniqueBlock(_root_)" },
|
||||
"field": "storageCapacity_s",
|
||||
"limit": -1,
|
||||
"type": "terms"
|
||||
}
|
||||
}
|
||||
}
|
||||
265
cmd/api/search_handler.go
Normal file
265
cmd/api/search_handler.go
Normal file
@@ -0,0 +1,265 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/stevenferrer/solr-go"
|
||||
"github.com/stevenferrer/solr-go/query"
|
||||
)
|
||||
|
||||
// searchHandler is the search handler
|
||||
type searchHandler struct {
|
||||
collection string
|
||||
solrClient solr.Client
|
||||
}
|
||||
|
||||
// facetConfig is a facet configuration
|
||||
type facetConfig struct {
|
||||
// field is the field name
|
||||
field,
|
||||
// facet is the facet name
|
||||
facet,
|
||||
// param is the url query param to be used for retrieving filter values
|
||||
urlParam string
|
||||
// isChild set to true to indicate that this is a child facet
|
||||
isChild bool
|
||||
}
|
||||
|
||||
var facetConfigs = []facetConfig{
|
||||
// {
|
||||
// facet: "Category",
|
||||
// field: "category",
|
||||
// urlParam: "categories",
|
||||
// },
|
||||
{
|
||||
facet: "Product Type",
|
||||
field: "productType",
|
||||
urlParam: "productTypes",
|
||||
},
|
||||
{
|
||||
facet: "Brand",
|
||||
field: "brand",
|
||||
urlParam: "brands",
|
||||
},
|
||||
{
|
||||
facet: "Color Family",
|
||||
field: "colorFamily_s",
|
||||
urlParam: "colorFamilies",
|
||||
isChild: true,
|
||||
},
|
||||
// {
|
||||
// facet: "Sim Card Slots",
|
||||
// field: "simCardSlots_s",
|
||||
// urlParam: "simCardSlots",
|
||||
// isChild: true,
|
||||
// },
|
||||
{
|
||||
facet: "Operating System",
|
||||
field: "operatingSystem_s",
|
||||
urlParam: "operatingSystems",
|
||||
isChild: true,
|
||||
},
|
||||
{
|
||||
facet: "Storage Capacity",
|
||||
field: "storageCapacity_s",
|
||||
urlParam: "storageCapacities",
|
||||
isChild: true,
|
||||
},
|
||||
}
|
||||
|
||||
func (h *searchHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
facetsMap := Map{}
|
||||
productFilters, skuFilters := []string{}, []string{}
|
||||
for _, fctCfg := range facetConfigs {
|
||||
tagVals := []string{}
|
||||
filterVals := strings.Split(r.URL.Query().Get(fctCfg.urlParam), ",")
|
||||
for _, val := range filterVals {
|
||||
if val == "" {
|
||||
continue
|
||||
}
|
||||
tagVals = append(tagVals, fctCfg.field+":"+val)
|
||||
}
|
||||
|
||||
if !fctCfg.isChild {
|
||||
// product filters
|
||||
if len(tagVals) > 0 {
|
||||
productFilters = append(productFilters, fmt.Sprintf("{!tag=top}%s", strings.Join(tagVals, " OR ")))
|
||||
} else {
|
||||
productFilters = append(productFilters, fmt.Sprintf("{!tag=top}%s:*", fctCfg.field))
|
||||
}
|
||||
|
||||
facetsMap[fctCfg.facet] = Map{
|
||||
"facet": Map{
|
||||
"productCount": "uniqueBlock(_root_)",
|
||||
},
|
||||
"field": fctCfg.field,
|
||||
"limit": -1,
|
||||
"type": "terms",
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// sku filters
|
||||
if len(tagVals) > 0 {
|
||||
skuFilters = append(skuFilters, fmt.Sprintf("{!tag=%s}%s", fctCfg.field, strings.Join(tagVals, " OR ")))
|
||||
} else {
|
||||
skuFilters = append(skuFilters, fmt.Sprintf("{!tag=%s}%s:*", fctCfg.field, fctCfg.field))
|
||||
}
|
||||
|
||||
facetsMap[fctCfg.facet] = Map{
|
||||
"domain": Map{
|
||||
"excludeTags": "top",
|
||||
"filter": []string{
|
||||
fmt.Sprintf("{!filters param=$skuFilters excludeTags=%s v=$sku}", fctCfg.field),
|
||||
"{!child of=$product filters=$filter v=$product}",
|
||||
},
|
||||
},
|
||||
"type": "terms",
|
||||
"field": fctCfg.field,
|
||||
"limit": -1,
|
||||
"facet": Map{
|
||||
"productCount": "uniqueBlock(_root_)",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
query := Map{
|
||||
"query": "{!parent tag=top filters=$skuFilters which=$product score=total v=$sku}",
|
||||
"queries": Map{
|
||||
"product": "docType:product",
|
||||
"sku": "docType:sku",
|
||||
"skuFilters": skuFilters,
|
||||
},
|
||||
"filter": productFilters,
|
||||
"facet": facetsMap,
|
||||
}
|
||||
|
||||
queryResp, err := h.solrClient.Query().Query(r.Context(), h.collection, query)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
// extract products and facets from query response
|
||||
resp, err := buildResp(queryResp)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
resp["query"] = query
|
||||
|
||||
w.Header().Add("content-type", "application/json")
|
||||
err = json.NewEncoder(w).Encode(resp)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
}
|
||||
|
||||
func buildResp(queryResp *query.Response) (Map, error) {
|
||||
var facets = []Map{}
|
||||
for name, v := range queryResp.Facets {
|
||||
if name == "count" {
|
||||
continue
|
||||
}
|
||||
|
||||
vv, ok := v.(Map)
|
||||
if !ok {
|
||||
return nil, errors.New("v is not M")
|
||||
}
|
||||
|
||||
bucks, ok := vv["buckets"].([]Any)
|
||||
if !ok {
|
||||
return nil, errors.New("vv is not []Any")
|
||||
}
|
||||
|
||||
buckets := []Map{}
|
||||
for _, bk := range bucks {
|
||||
buck, ok := bk.(Map)
|
||||
if !ok {
|
||||
return nil, errors.New("bucket is not M")
|
||||
}
|
||||
|
||||
productCount, ok := buck["productCount"].(float64)
|
||||
if !ok {
|
||||
return nil, errors.New("productCount is not float64")
|
||||
}
|
||||
|
||||
skuCount, ok := buck["count"].(float64)
|
||||
if !ok {
|
||||
return nil, errors.New("count is not float64")
|
||||
}
|
||||
|
||||
val, ok := buck["val"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("val is not string")
|
||||
}
|
||||
|
||||
buckets = append(buckets, map[string]interface{}{
|
||||
"val": val,
|
||||
"skuCount": int(skuCount),
|
||||
"productCount": int(productCount),
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
var param string
|
||||
for _, fctCfg := range facetConfigs {
|
||||
if fctCfg.facet == name {
|
||||
param = fctCfg.urlParam
|
||||
}
|
||||
}
|
||||
|
||||
facets = append(facets, Map{
|
||||
"name": name,
|
||||
"param": param,
|
||||
"buckets": buckets,
|
||||
})
|
||||
}
|
||||
|
||||
var products = []Map{}
|
||||
for _, doc := range queryResp.Response.Docs {
|
||||
id, ok := doc["id"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("id not found or is not string")
|
||||
}
|
||||
|
||||
name, ok := doc["name"].([]Any)
|
||||
if !ok {
|
||||
return nil, errors.New("name not found or is not []Any")
|
||||
}
|
||||
|
||||
category, ok := doc["category"].([]Any)
|
||||
if !ok {
|
||||
return nil, errors.New("category not found or is not []Any")
|
||||
}
|
||||
|
||||
brand, ok := doc["brand"].([]Any)
|
||||
if !ok {
|
||||
return nil, errors.New("brand not found or is not []Any")
|
||||
}
|
||||
|
||||
productType, ok := doc["productType"].(string)
|
||||
if !ok {
|
||||
return nil, errors.New("productType not found or is not string")
|
||||
}
|
||||
|
||||
products = append(products, Map{
|
||||
"id": id,
|
||||
"name": name[0].(string),
|
||||
"category": category[0].(string),
|
||||
"brand": brand[0].(string),
|
||||
"productType": productType,
|
||||
})
|
||||
}
|
||||
|
||||
return Map{
|
||||
"products": products,
|
||||
"facets": facets,
|
||||
}, nil
|
||||
}
|
||||
28
cmd/api/simplified-query.json
Normal file
28
cmd/api/simplified-query.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"query": "{!parent tag=top filters=$skuFilters which=docType:product v=docType:sku}",
|
||||
"queries": {
|
||||
"skuFilters": ["{!tag=colorFamily_s}colorFamily_s:Black"]
|
||||
},
|
||||
"filter": ["{!tag=top}brand:Amazon"],
|
||||
"facet": {
|
||||
"Brand": {
|
||||
"facet": { "productCount": "uniqueBlock(_root_)" },
|
||||
"field": "brand",
|
||||
"limit": -1,
|
||||
"type": "terms"
|
||||
},
|
||||
"Color Family": {
|
||||
"domain": {
|
||||
"excludeTags": "top",
|
||||
"filter": [
|
||||
"{!filters param=$skuFilters excludeTags=colorFamily_s v=$sku}",
|
||||
"{!child of=docType:product filters=$filter v=docType:product}"
|
||||
]
|
||||
},
|
||||
"facet": { "productCount": "uniqueBlock(_root_)" },
|
||||
"field": "colorFamily_s",
|
||||
"limit": -1,
|
||||
"type": "terms"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
module github.com/stevenferrer/multi-select-facet/api
|
||||
module github.com/stevenferrer/multi-select-facet
|
||||
|
||||
go 1.14
|
||||
|
||||
@@ -7,6 +7,6 @@ require (
|
||||
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.0.4
|
||||
github.com/stevenferrer/solr-go v0.0.6
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 // indirect
|
||||
)
|
||||
@@ -13,9 +13,12 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stevenferrer/solr-go v0.0.4 h1:XsUtbAdlX9txVUNWNPwdLoXb7lWBEGGcD4ZLs33ACdE=
|
||||
github.com/stevenferrer/solr-go v0.0.4/go.mod h1:gatY+DJUzNG3NdONghbJa2JJw6ykdECbiYXtiBy7ykY=
|
||||
github.com/stevenferrer/solr-go v0.0.6 h1:pFludjKdCmQJxJTwmszwRfAvoqDRG/gcYzf3u+rG5wE=
|
||||
github.com/stevenferrer/solr-go v0.0.6/go.mod h1:ePa8+6kV1baPpoywPbmcR06wQ0500EGuMntYGxli5Xk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
@@ -27,3 +30,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -30,7 +30,7 @@ async function search(filters) {
|
||||
|
||||
const url = new URL("http://localhost:8081/search");
|
||||
filters.forEach((filter) => {
|
||||
url.searchParams.append(filter.name, filter.selected.join(","));
|
||||
url.searchParams.append(filter.param, filter.selected.join(","));
|
||||
});
|
||||
|
||||
let response = await fetch(url, {
|
||||
@@ -49,7 +49,7 @@ async function search(filters) {
|
||||
facets: facets
|
||||
.map((facet) => {
|
||||
let selected = [];
|
||||
let filter = filters.find((filter) => filter.name === facet.name);
|
||||
let filter = filters.find((filter) => filter.param === facet.param);
|
||||
if (filter) {
|
||||
selected = filter.selected;
|
||||
}
|
||||
@@ -96,8 +96,8 @@ export default {
|
||||
const { facets: oldFacet } = this;
|
||||
const filters = oldFacet
|
||||
.filter(({ selected }) => selected.length > 0)
|
||||
.map(({ name, selected }) => {
|
||||
return { name, selected };
|
||||
.map(({ name, param, selected }) => {
|
||||
return { name, param, selected };
|
||||
});
|
||||
|
||||
const { products, facets } = await search(filters);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="(facet, i) in facets" :key="i">
|
||||
<h1 class="title">{{ facet.name }}</h1>
|
||||
<h1 class="is-size-5">{{ facet.name }}</h1>
|
||||
<div class="fields">
|
||||
<div v-for="(bucket, j) in facet.buckets.slice(0, 5)" :key="j">
|
||||
<b-checkbox
|
||||
|
||||
@@ -10,8 +10,9 @@
|
||||
<div class="product-title">
|
||||
<span class="is-size-6">{{ data.name }}</span
|
||||
><br />
|
||||
<span class="has-text-grey">{{ data.brand }}</span>
|
||||
</div>
|
||||
|
||||
<span class="has-text-grey">{{ data.brand }}</span>
|
||||
<br />
|
||||
<span class="is-size-6 has-text-success">PHP 1,501.30 </span>
|
||||
<br />
|
||||
@@ -24,6 +25,24 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
|
||||
.product-title {
|
||||
display: -webkit-box;
|
||||
height: 2em;
|
||||
-webkit-line-clamp: 3;
|
||||
line-height: 1em;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
|
||||
Reference in New Issue
Block a user