Revert "Features: 1) Add suggest, price, and quantity fields to ProductDocument; 2) Introduce prepare_suggest method for enhancing autocomplete functionality; 3) Enhance search with filters, function scoring, and result aggregation; 4) Add new analyzers and token filters for synonyms and stopwords."
This reverts commit 97829d23a6.
This commit is contained in:
parent
352b5f14ef
commit
41b6c1aa07
2 changed files with 81 additions and 129 deletions
|
|
@ -1,9 +1,12 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.http import Http404
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_elasticsearch_dsl import fields
|
from django_elasticsearch_dsl import fields
|
||||||
from django_elasticsearch_dsl.registries import registry
|
from django_elasticsearch_dsl.registries import registry
|
||||||
from elasticsearch.dsl import SF, Q, Search
|
from elasticsearch import NotFoundError
|
||||||
|
from elasticsearch.dsl import Q, Search
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
|
|
||||||
from core.models import Brand, Category, Product
|
from core.models import Brand, Category, Product
|
||||||
|
|
@ -30,120 +33,88 @@ SMART_FIELDS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def process_query(
|
def process_query(query: str = "", request: Request | None = None):
|
||||||
query: str = "", request: Request | None = None, filters: dict | None = None
|
"""
|
||||||
):
|
Perform a lenient, typo‑tolerant, multi‑index search.
|
||||||
|
|
||||||
|
* Full‑text with fuzziness for spelling mistakes
|
||||||
|
* `bool_prefix` for edge‑ngram autocomplete / “icontains”
|
||||||
|
"""
|
||||||
if not query:
|
if not query:
|
||||||
raise ValueError("no search term provided.")
|
raise ValueError(_("no search term provided."))
|
||||||
filters = filters or {}
|
|
||||||
base_q = Q(
|
|
||||||
"bool",
|
|
||||||
should=[
|
|
||||||
Q(
|
|
||||||
"multi_match",
|
|
||||||
query=query,
|
|
||||||
fields=SMART_FIELDS,
|
|
||||||
fuzziness="AUTO",
|
|
||||||
operator="and",
|
|
||||||
),
|
|
||||||
Q(
|
|
||||||
"multi_match",
|
|
||||||
query=query,
|
|
||||||
fields=[f for f in SMART_FIELDS if f.endswith(".auto")],
|
|
||||||
type="bool_prefix",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
minimum_should_match=1,
|
|
||||||
)
|
|
||||||
functions = [
|
|
||||||
SF("field_value_factor", field="quantity", modifier="log1p", missing=0),
|
|
||||||
SF("field_value_factor", field="rating", modifier="sqrt", missing=0),
|
|
||||||
SF("exp", field="created_at", origin="now", scale="30d"), # newness boost
|
|
||||||
]
|
|
||||||
fq = [
|
|
||||||
Q("function_score", query=base_q, functions=functions, boost_mode="sum"),
|
|
||||||
]
|
|
||||||
if "category" in filters:
|
|
||||||
fq.append(Q("term", category__slug=filters["category"]))
|
|
||||||
if "brand" in filters:
|
|
||||||
fq.append(Q("term", brand__slug=filters["brand"]))
|
|
||||||
if "price_min" in filters or "price_max" in filters:
|
|
||||||
range_q = {}
|
|
||||||
if "price_min" in filters:
|
|
||||||
range_q["gte"] = filters["price_min"]
|
|
||||||
if "price_max" in filters:
|
|
||||||
range_q["lte"] = filters["price_max"]
|
|
||||||
fq.append(Q("range", price=range_q))
|
|
||||||
|
|
||||||
search = (
|
query = query.strip()
|
||||||
Search(index=["products", "categories", "brands", "posts"])
|
try:
|
||||||
.query(base_q)
|
q = Q(
|
||||||
.filter(*fq)
|
"bool",
|
||||||
.extra(size=20)
|
should=[
|
||||||
.highlight("description", fragment_size=150)
|
Q(
|
||||||
.extra(
|
"multi_match",
|
||||||
aggs={
|
query=query,
|
||||||
"by_category": {"terms": {"field": "category.keyword"}},
|
fields=SMART_FIELDS,
|
||||||
"by_brand": {"terms": {"field": "brand.keyword"}},
|
fuzziness="AUTO",
|
||||||
"price_stats": {"stats": {"field": "price"}},
|
operator="and",
|
||||||
}
|
),
|
||||||
)
|
Q(
|
||||||
)
|
"multi_match",
|
||||||
response = search.execute()
|
query=query,
|
||||||
|
fields=[f for f in SMART_FIELDS if f.endswith(".auto")],
|
||||||
results: dict = {"products": [], "categories": [], "brands": [], "posts": []}
|
type="bool_prefix",
|
||||||
for hit in response.hits:
|
),
|
||||||
obj_uuid = getattr(hit, "uuid", None) or hit.meta.id
|
],
|
||||||
obj_name = getattr(hit, "name", None) or getattr(hit, "title", None) or "N/A"
|
minimum_should_match=1,
|
||||||
obj_slug = ""
|
|
||||||
raw_slug = getattr(hit, "slug", None)
|
|
||||||
if raw_slug:
|
|
||||||
obj_slug = raw_slug
|
|
||||||
elif hit.meta.index == "brands":
|
|
||||||
obj_slug = slugify(obj_name)
|
|
||||||
elif hit.meta.index == "categories":
|
|
||||||
obj_slug = slugify(f"{obj_name}")
|
|
||||||
|
|
||||||
image_url = None
|
|
||||||
idx = hit.meta.index
|
|
||||||
if idx == "products" and request:
|
|
||||||
prod = get_object_or_404(Product, uuid=obj_uuid)
|
|
||||||
first = prod.images.order_by("priority").first()
|
|
||||||
if first and first.image:
|
|
||||||
image_url = request.build_absolute_uri(first.image.url)
|
|
||||||
elif idx == "brands" and request:
|
|
||||||
brand = get_object_or_404(Brand, uuid=obj_uuid)
|
|
||||||
if brand.small_logo:
|
|
||||||
image_url = request.build_absolute_uri(brand.small_logo.url)
|
|
||||||
elif idx == "categories" and request:
|
|
||||||
cat = get_object_or_404(Category, uuid=obj_uuid)
|
|
||||||
if cat.image:
|
|
||||||
image_url = request.build_absolute_uri(cat.image.url)
|
|
||||||
|
|
||||||
results[idx].append(
|
|
||||||
{
|
|
||||||
"uuid": str(obj_uuid),
|
|
||||||
"name": obj_name,
|
|
||||||
"slug": obj_slug,
|
|
||||||
"image": image_url,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
facets = {
|
search = (
|
||||||
"categories": [
|
Search(index=["products", "categories", "brands", "posts"])
|
||||||
(b.key, b.doc_count) for b in response.aggregations.by_category.buckets
|
.query(q)
|
||||||
],
|
.extra(size=100)
|
||||||
"brands": [
|
)
|
||||||
(b.key, b.doc_count) for b in response.aggregations.by_brand.buckets
|
response = search.execute()
|
||||||
],
|
|
||||||
"price": {
|
|
||||||
"min": response.aggregations.price_stats.min,
|
|
||||||
"max": response.aggregations.price_stats.max,
|
|
||||||
"avg": response.aggregations.price_stats.avg,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return {"results": results, "facets": facets}
|
results: dict = {"products": [], "categories": [], "brands": [], "posts": []}
|
||||||
|
for hit in response.hits:
|
||||||
|
obj_uuid = getattr(hit, "uuid", None) or hit.meta.id
|
||||||
|
obj_name = (
|
||||||
|
getattr(hit, "name", None) or getattr(hit, "title", None) or "N/A"
|
||||||
|
)
|
||||||
|
obj_slug = ""
|
||||||
|
raw_slug = getattr(hit, "slug", None)
|
||||||
|
if raw_slug:
|
||||||
|
obj_slug = raw_slug
|
||||||
|
elif hit.meta.index == "brands":
|
||||||
|
obj_slug = slugify(obj_name)
|
||||||
|
elif hit.meta.index == "categories":
|
||||||
|
obj_slug = slugify(f"{obj_name}")
|
||||||
|
|
||||||
|
image_url = None
|
||||||
|
idx = hit.meta.index
|
||||||
|
if idx == "products" and request:
|
||||||
|
prod = get_object_or_404(Product, uuid=obj_uuid)
|
||||||
|
first = prod.images.order_by("priority").first()
|
||||||
|
if first and first.image:
|
||||||
|
image_url = request.build_absolute_uri(first.image.url)
|
||||||
|
elif idx == "brands" and request:
|
||||||
|
brand = get_object_or_404(Brand, uuid=obj_uuid)
|
||||||
|
if brand.small_logo:
|
||||||
|
image_url = request.build_absolute_uri(brand.small_logo.url)
|
||||||
|
elif idx == "categories" and request:
|
||||||
|
cat = get_object_or_404(Category, uuid=obj_uuid)
|
||||||
|
if cat.image:
|
||||||
|
image_url = request.build_absolute_uri(cat.image.url)
|
||||||
|
|
||||||
|
results[idx].append(
|
||||||
|
{
|
||||||
|
"uuid": str(obj_uuid),
|
||||||
|
"name": obj_name,
|
||||||
|
"slug": obj_slug,
|
||||||
|
"image": image_url,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return results
|
||||||
|
except NotFoundError:
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
|
||||||
LANGUAGE_ANALYZER_MAP = {
|
LANGUAGE_ANALYZER_MAP = {
|
||||||
|
|
@ -192,8 +163,6 @@ COMMON_ANALYSIS = {
|
||||||
"encoder": "double_metaphone",
|
"encoder": "double_metaphone",
|
||||||
"replace": False,
|
"replace": False,
|
||||||
},
|
},
|
||||||
"synonym_filter": {"type": "synonym", "synonyms_path": "analysis/synonyms.txt"},
|
|
||||||
"english_stop": {"type": "stop", "stopwords": "_english_"},
|
|
||||||
},
|
},
|
||||||
"analyzer": {
|
"analyzer": {
|
||||||
"autocomplete": {
|
"autocomplete": {
|
||||||
|
|
@ -208,10 +177,6 @@ COMMON_ANALYSIS = {
|
||||||
"tokenizer": "standard",
|
"tokenizer": "standard",
|
||||||
"filter": ["lowercase", "asciifolding", "ngram_filter"],
|
"filter": ["lowercase", "asciifolding", "ngram_filter"],
|
||||||
},
|
},
|
||||||
"synonym_analyzer": {
|
|
||||||
"tokenizer": "standard",
|
|
||||||
"filter": ["lowercase", "asciifolding", "synonym_filter"],
|
|
||||||
},
|
|
||||||
"name_phonetic": {
|
"name_phonetic": {
|
||||||
"tokenizer": "standard",
|
"tokenizer": "standard",
|
||||||
"filter": ["lowercase", "asciifolding", "double_metaphone"],
|
"filter": ["lowercase", "asciifolding", "double_metaphone"],
|
||||||
|
|
|
||||||
|
|
@ -52,12 +52,6 @@ class _BaseDoc(ActiveOnlyMixin, Document):
|
||||||
|
|
||||||
|
|
||||||
class ProductDocument(_BaseDoc):
|
class ProductDocument(_BaseDoc):
|
||||||
suggest = fields.CompletionField(
|
|
||||||
analyzer="autocomplete",
|
|
||||||
search_analyzer="autocomplete_search",
|
|
||||||
)
|
|
||||||
price = fields.FloatField(attr="price")
|
|
||||||
quantity = fields.IntegerField(attr="quantity")
|
|
||||||
rating = fields.FloatField(attr="rating")
|
rating = fields.FloatField(attr="rating")
|
||||||
|
|
||||||
class Index(_BaseDoc.Index):
|
class Index(_BaseDoc.Index):
|
||||||
|
|
@ -67,13 +61,6 @@ class ProductDocument(_BaseDoc):
|
||||||
model = Product
|
model = Product
|
||||||
fields = ["uuid"]
|
fields = ["uuid"]
|
||||||
|
|
||||||
def prepare_suggest(self, instance):
|
|
||||||
terms = [instance.name]
|
|
||||||
if instance.brand:
|
|
||||||
terms.append(instance.brand.name)
|
|
||||||
terms.append(instance.category.name)
|
|
||||||
return {"input": terms, "weight": int(instance.quantity)}
|
|
||||||
|
|
||||||
|
|
||||||
_add_multilang_fields(ProductDocument)
|
_add_multilang_fields(ProductDocument)
|
||||||
registry.register_document(ProductDocument)
|
registry.register_document(ProductDocument)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue