Recherche à facette

Contexte : je travaille sur un django oscar : une boutique avec des produits qui ont des caractéristiques.

J’essaye de mettre en place une « recherche à facette » sur les caractéristiques, le truc classique qu’on trouve sur toutes les boutiques en ligne, à gauche, par exemple pour sélectionner une ou plusieurs marques, type, fréquence, format…

J’ai tenté comme Oscar fait, d’utiliser django-haystack avec Apache Solr derrière.

Point positif : ça marche, c’est assez simple à mettre en place, juste besoin de décrire les attributs de produit qu’on utilise et django-haystack s’occupe de parler à Solr (insertion des produits, mise à jour des produits, et recherche), c’est propre.

Souci : je n’arrive à requêter qu’avec des “et”, çàd si j’ai des tshirt bleus, rouges, et noirs, de taille S, M, ou L, je peux faire la requête “bleu et M”, mais pas la requête “bleu et (M ou L)”. J’arrive à faire “bleu et M et L” mais ça ne renvoie (évidemment) rien.

cf. une issue chez django-haystack où j’en parle aussi :

Une idée ?

Ca peut aider ?
https://django-haystack.readthedocs.io/en/master/searchqueryset_api.html#filter-or

J’en doute, enfin j’ignore comment pour le moment.

Ce que je sais c’est que de cette page c’est la méthode narrow qui est utilisée, ici :

for facet in request.query_params.getlist(self.facet_query_params_text):
    if ":" not in facet:
        continue

    field, value = facet.split(":", 1)
    if value:
        queryset = queryset.narrow('%s:"%s"' % (field, queryset.query.clean(value)))

donc au moins on voit que dans le code, clairement, il réduit l’espace de recherche a chaque paramètre, c’est explicite.

Là où c’est moins clair pour moi, même avec filter_or c’est comment régler la priorité des opérateurs : (rouge ou bleu) et (M ou L).

En même temps pour moi tout ce qui est dessous est opaque : c’est la première fois que j’ai un Apache Solr en prod, et c’est haystack qui lui parle, pas moi, donc j’ignore à peu près tout de ce qu’ils se disent :smiley:

Je creuse…

OK je vois la requête solr passer :

http://127.0.0.1:8983/solr/naxt/select/?
    q=%2A%3A%2A&
    fl=%2A+score&
    df=text&
    start=0&
    spellcheck=true&
    spellcheck.collate=true&
    spellcheck.count=1&
    facet=on&
    facet.field=product_class_exact&
    facet.field=category_exact&
    facet.field=brand_exact&
    fq=product_class_exact%3A%22RAM%22&
    fq=django_ct%3A%28catalogue.product%29&
    wt=json

Je pense que je n’ai plus qu’a aller lire la doc de Solr pour voir déjà si c’est possible d’exprimer ce que je veux.

Doc trouvée à propos du paramètre fq.

Et j’ai testé avec un :

fq=(product_class_exact:"RAM" OR product_class_exact:"YOUPI") AND (brand_exact:"YOUPI" OR brand_exact:"TEST")

j’ai bien des résultats de “ram test” qui sortent.

Donc bonne nouvelle : Solr gère. Reste à trouver comment tordre drf-haystack pour qu’il produise ce genre de requêtes :smiley:

Bon je pense que j’ai un truc qui marche, je surcharge FacetMixin de drf-haystack (celui qui fait des narrow tout le temps) avec une version qui “groupe” comme je l’entends :

class FacetMixinMultiSelect(FacetMixin):
    @action(detail=False, methods=["get"], url_path="facets")
    def facets(self, request):
        """Sets up a list route for ``faceted`` results.
        This will add ie ^search/facets/$ to your existing ^search pattern.

        Here we're grouping selected facets by fields so we can OR
        them inside the same fields.

        So:

            selected_facets=color:blue&selected_facets=color:red&selected_facets=size:L

        will translate to:

            (color:blue OR color:red) AND size:L
        """
        queryset = self.filter_facet_queryset(self.get_queryset())

        fields = defaultdict(list)

        for facet in request.query_params.getlist(self.facet_query_params_text):
            if ":" not in facet:
                continue

            field, value = facet.split(":", 1)
            fields[field].append(value)

        for field, values in fields.items():
            queryset = queryset.narrow(
                " OR ".join(
                    '%s:"%s"' % (field, queryset.query.clean(value)) for value in values
                )
            )

        serializer = self.get_facet_serializer(
            queryset.facet_counts(), objects=queryset, many=False
        )
        return Response(serializer.data)