Django Product Catalog and Full-Text Search Integration

Product Catalog View Implementation

The following class-based view handles the display of product listings within a specific category. It manages sorting options, pagination logic, and retrieves the current cart count for authenticated users.

class ProductCatalogView(View):
    def get(self, request, category_id, page_num):
        # Retrieve sorting preference from query parameters
        ordering = request.GET.get('ordering', 'default')
        
        # Validate sorting option
        if ordering not in ('price', 'sales'):
            ordering = 'default'

        # Fetch the specific category or redirect if missing
        try:
            current_category = GoodsCategory.objects.get(id=category_id)
        except GoodsCategory.DoesNotExist:
            return redirect(reverse('goods:index'))

        # Retrieve all categories for navigation
        all_categories = GoodsCategory.objects.all()

        # Fetch newest arrivals for the sidebar recommendation
        featured_items = GoodsSKU.objects.filter(
            category=current_category
        ).order_by('-create_time')[:2]

        # Apply sorting logic to the product queryset
        if ordering == 'price':
            product_list = GoodsSKU.objects.filter(
                category=current_category
            ).order_by('price')
        elif ordering == 'sales':
            product_list = GoodsSKU.objects.filter(
                category=current_category
            ).order_by('sales')
        else:
            product_list = GoodsSKU.objects.filter(
                category=current_category
            )

        # Calculate cart item count for authenticated users
        cart_count = 0
        redis_conn = get_redis_connection('default')
        if request.user.is_authenticated:
            user_cart = redis_conn.hgetall('cart_%s' % request.user.id)
            for quantity in user_cart.values():
                cart_count += int(quantity)

        # Configure pagination
        items_per_page = 10
        paginator = Paginator(product_list, items_per_page)
        page_num = int(page_num)
        
        try:
            current_page_items = paginator.page(page_num)
        except EmptyPage:
            current_page_items = paginator.page(1)

        # Determine pagination range display logic
        total_pages = paginator.num_pages
        page_range = []

        if total_pages <= 5:
            page_range = range(1, total_pages + 1)
        elif page_num <= 3:
            page_range = range(1, 6)
        elif total_pages - page_num < 3:
            page_range = range(total_pages - 4, total_pages + 1)
        else:
            page_range = range(page_num - 2, page_num + 3)

        context = {
            'category': current_category,
            'categories': all_categories,
            'page_items': current_page_items,
            'featured_items': featured_items,
            'page_range': page_range,
            'ordering': ordering,
            'cart_count': cart_count
        }

        return render(request, 'catalog.html', context)

URL Configuration for Catalog

Map the view to a URL pattern that accepts category and page parameters.

path('catalog/<int:category_id>/<int:page_num>/', 
     ProductCatalogView.as_view(), 
     name='catalog'),

Catalog Template Structure

The template extends the base layout and renders the product grid, sorting options, and pagination controls.

{% extends "base.html" %}
{% load static %}

{% block title %}Product Catalog{% endblock %}

{% block body %}
    <div class="nav-container">
        <div class="nav-bar clearfix">
            <div class="category-menu fl">
                <h1>All Categories</h1>
                <ul class="submenu">
                    {% for cat in categories %}
                    <li>
                        <a href="{% url 'goods:catalog' cat.id 1 %}" 
                           class="{{ cat.logo }}">
                            {{ cat.name }}
                        </a>
                    </li>
                    {% endfor %}
                </ul>
            </div>
        </div>
    </div>

    <div class="breadcrumb-trail">
        <a href="{% url 'goods:index' %}">Home</a>
        <span>></span>
        <a href="{% url 'goods:catalog' category.id 1 %}">{{ category.name }}</a>
    </div>

    <div class="content-wrap clearfix">
        <div class="sidebar fl clearfix">
            <div class="new-arrivals">
                <h3>New Arrivals</h3>
                <ul>
                    {% for item in featured_items %}
                    <li>
                        <a href="{% url 'goods:detail' item.id %}">
                            <img src="{{ item.default_image.url }}">
                        </a>
                        <h4>
                            <a href="{% url 'goods:detail' item.id %}">
                                {{ item.name }}
                            </a>
                        </h4>
                        <div class="price-tag">${{ item.price }}</div>
                    </li>
                    {% endfor %}
                </ul>
            </div>
        </div>

        <div class="main-content fr clearfix">
            <div class="sorting-bar">
                <a href="{% url 'goods:catalog' category.id 1 %}?ordering=default" 
                   {% if ordering == "default" %}class="active"{% endif %}>Default</a>
                <a href="{% url 'goods:catalog' category.id 1 %}?ordering=price" 
                   {% if ordering == "price" %}class="active"{% endif %}>Price</a>
                <a href="{% url 'goods:catalog' category.id 1 %}?ordering=sales" 
                   {% if ordering == "sales" %}class="active"{% endif %}>Popularity</a>
            </div>

            <ul class="product-grid clearfix">
                {% for item in page_items %}
                <li>
                    <a href="{% url 'goods:detail' item.id %}">
                        <img src="{{ item.default_image.url }}">
                    </a>
                    <h4>
                        <a href="{% url 'goods:detail' item.id %}">
                            {{ item.name }}
                        </a>
                    </h4>
                    <div class="actions">
                        <span class="price">${{ item.price }}</span>
                        <span class="unit">{{ item.price }}/{{ item.unit }}</span>
                        <a href="#" class="btn-add-cart" title="Add to Cart"></a>
                    </div>
                </li>
                {% endfor %}
            </ul>

            <div class="pagination">
                {% if page_items.has_previous %}
                <a href="?page={{ page_items.previous_page_number }}&ordering={{ ordering }}">Prev</a>
                {% endif %}
                
                {% for p in page_range %}
                <a href="?page={{ p }}&ordering={{ ordering }}" 
                   {% if p == page_items.number %}class="active"{% endif %}>
                   {{ p }}
                </a>
                {% endfor %}
                
                {% if page_items.has_next %}
                <a href="?page={{ page_items.next_page_number }}&ordering={{ ordering }}">Next</a>
                {% endif %}
            </div>
        </div>
    </div>
{% endblock %}

Full-Text Search Engine Setup

Full-text search provides better efficiency than standard database fuzzy matching and supports tokenization for languages like Chinese. The implementation utilizes Haystack as the framework, Whoosh as the backend engine, and Jieba for Chinese segmentation.

Install the required packages within the virtual environment:

pip install django-haystack
pip install whoosh
pip install jieba

Configuration Settings

Add haystack to the installed applications in settings.py.

INSTALLED_APPS = [
    # ...
    'haystack',
]

Configure the search engine connection and signal processor to ensure indexes update automatically when data changes.

# Haystack configuration
HAYSTACK_CONNECTIONS = {
    'default': {
        'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
        'PATH': os.path.join(BASE_DIR, 'search_index'),
    }
}

# Enable real-time indexing
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'

# Control results per page
HAYSTACK_SEARCH_RESULTS_PER_PAGE = 10

Define Search Indexes

Create a search_indexes.py file within the application directory (e.g., goods). This defines how models map to the search index.

from haystack import indexes
from goods.models import GoodsSKU


class ProductSKUIndex(indexes.SearchIndex, indexes.Indexable):
    text = indexes.CharField(document=True, use_template=True)

    def get_model(self):
        return GoodsSKU

    def index_queryset(self, using=None):
        return self.get_model().objects.all()

Create the template file used by Haystack to determine which fields are indexed. The path must match the app name and model name: templates/search/indexes/goods/goodssku_text.txt.

{{ object.name }}
{{ object.title }}
{{ object.desc }}

Generate the initial index files using the management command:

python manage.py rebuild_index

Search Interface Implementation

Include the Haystack URLs in the projects' main URL configuration.

import haystack.urls

urlpatterns = [
    # ...
    path('search/', include(haystack.urls)),
]

Update the search form in the base template to submit queries to the search endpoint.

<div class="search-box fl">
    <form action="/search/" method="get">
        <input type="text" class="search-input fl" name="q" placeholder="Search products">
        <input type="submit" class="search-btn fr" value="Go">
    </form>
</div>

Search Results Template

Create templates/search/search.html to render the query results. Haystack passes a page object containing the results.

{% extends 'base.html' %}
{% load static %}

{% block title %}Search Results{% endblock %}

{% block search_bar %}
    <div class="search-header clearfix">
        <a href="{% url 'goods:index' %}" class="logo fl">
            <img src="{% static 'images/logo.png' %}">
        </a>
        <div class="page-title fl">|&nbsp;&nbsp;&nbsp;&nbsp;Search Results</div>
        <div class="search-box fr">
            <form action="/search/" method="get">
                <input type="text" class="search-input fl" name="q" placeholder="Search products">
                <input type="submit" class="search-btn fr" value="Go">
            </form>
        </div>
    </div>
{% endblock %}

{% block body %}
    <div class="main-wrap clearfix">
        <ul class="product-grid clearfix">
        {% for result in page %}
            <li>
                <a href="{% url 'goods:detail' result.object.id %}">
                    <img src="{{ result.object.default_image.url }}">
                </a>
                <h4>
                    <a href="{% url 'goods:detail' result.object.id %}">
                        {{ result.object.name }}
                    </a>
                </h4>
                <div class="actions">
                    <span class="price">${{ result.object.price }}</span>
                    <span class="unit">{{ result.object.price }}/{{ result.object.unit }}</span>
                </div>
            </li>
        {% empty %}
            <p>No products found matching your query.</p>
        {% endfor %}
        </ul>

        {% if page.has_previous or page.has_next %}
        <div class="pagination">
            {% if page.has_previous %}
            <a href="/search/?q={{ query }}&page={{ page.previous_page_number }}">Prev</a>
            {% endif %}
            |
            {% if page.has_next %}
            <a href="/search/?q={{ query }}&page={{ page.next_page_number }}">Next</a>
            {% endif %}
        </div>
        {% endif %}
    </div>
{% endblock %}

Chinese Tokenization Configuration

The default Whoosh engine does not handle Chinese characters effectively. A custom enalyzer using Jieba must be configured.

Create a file named JiebaTokenizer.py in the haystack backend directory (or within you're project if configuring a custom backend path).

import jieba
from whoosh.analysis import Tokenizer, Token

class JiebaSegmentTokenizer(Tokenizer):
    def __call__(self, value, positions=False, chars=False,
                 keeporiginal=False, removestops=True,
                 start_pos=0, start_char=0, mode='', **kwargs):
        t = Token(positions, chars, removestops=removestops, mode=mode, **kwargs)
        segments = jieba.cut(value, cut_all=True)
        
        for word in segments:
            t.original = t.text = word
            t.boost = 1.0
            if positions:
                t.pos = start_pos + value.find(word)
            if chars:
                t.startchar = start_char + value.find(word)
                t.endchar = start_char + value.find(word) + len(word)
            yield t

def JiebaAnalyzer():
    return JiebaSegmentTokenizer()

Copy the original whoosh_backend.py to a new file named custom_whoosh_backend.py within the same directory. Modify the import and analyzer usage.

# Inside custom_whoosh_backend.py
from .JiebaTokenizer import JiebaAnalyzer

# Locate the schema definition
# Change:
# analyzer=StemmingAnalyzer()
# To:
analyzer=JiebaAnalyzer()

Update settings.py to point to the custom backend.

HAYSTACK_CONNECTIONS = {
    'default': {
        'ENGINE': 'path.to.custom_whoosh_backend.WhooshEngine',
        'PATH': os.path.join(BASE_DIR, 'search_index'),
    }
}

Rebuild the index to apply the new tokenization rules.

python manage.py rebuild_index

Tags: Django full-text-search Haystack Whoosh E-commerce

Posted on Fri, 17 Jul 2026 17:05:54 +0000 by kronikel