第15次:商品搜索

实现用户在页面可自由搜索某个商品的功能。

第1步:准备搜索功能用到的库

shell 复制代码
pip install whoosh
pip install jieba
pip install django-haystack
  • whoosh是搜索引擎,对英文支持较好,但对中文效果不佳。
  • jieba为中文分词库,弥补whoosh的缺陷。
  • django-haystack为在django项目中使用搜索引擎的工具应用,通过它可以在不修改代码的情况下使用不同的搜索引擎。

第2步:在settings.py中添加haystack应用

python 复制代码
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'areas',
    'carts',
    'contents',
    'goods',
    'users',
    'orders',
    'payment',
    'verifications',
    'haystack'
]

第3步:在settings.py中新境搜索引擎whoosh的配置项

python 复制代码
HAYSTACK_CONNECTIONS = {
    'default': {
        # 引擎使用whoosh
        'ENGINE': 'haystack.backends.whoosh_cn_backend.WhooshEngine',
        # 索引文件路径
        'PATH': os.path.join(BASE_DIR, 'whoosh_index')
    }
}
# 添加、修改、删除数据时,自动生成索引
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'

第4步:将Whoosh自带的分词组件替换为jieba

在虚拟环境下(.venv\Lib\site-packages\haystack)创建ChineseAnalyzer.py

python 复制代码
import jieba

from whoosh.analysis import Tokenizer, Token

class ChineseTokenizer(Tokenizer):
    """在 Whoosh 搜索引擎中, Token 对象是分词过程中的核心数据结构,主要作用包括:
        1. 存储分词信息 :
        - text : 分词后的实际文本内容
        - original : 原始分词文本(可能与处理后的不同)
        - boost : 权重值,影响搜索结果排序
        2. 记录位置信息 :
        - pos : 词在文本中的位置序号
        - startchar / endchar : 字符级别的起始和结束位置
        3. 控制索引行为 :
        - removestops : 是否移除停用词
        - mode : 控制索引模式(如存储位置信息等)
    """

    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)
        seglist = jieba.cut(value, cut_all=True)

        for w in seglist:
            t.original = t.text = w
            t.boost = 1.0

            if positions:
                t.pos = start_pos + value.find(w)

            if chars:
                t.startchar = start_char + value.find(w)
                t.endchar = start_char + value.find(w) + len(w)

            yield t


def ChineseAnalyzer():
    return ChineseTokenizer()

复制/haystack/backends下的whoosh_backend.py文件,将副本更名为whoosh_cn_backend.py

打开whoosh_cn_backend.py,引入中文分析器文件ChineseAnalyzer.py

analyzer=field_class.analyzer or StemmingAnalyzer(),替换为analyzer = ChineseAnalyzer(),

在goods应用下创建索引类search_indexes.py,代码如下

python 复制代码
from haystack import indexes

from .models import SKU


class SKUIndex(indexes.SearchIndex, indexes.Indexable):
    """索引数据模型类"""
    # 接收索引字段:使用文档定义索引字段,并且使用模板语法渲染
    # document=True表示该字段是主要进行关键字查询的字段,use_template=True表明后续要通过一个数据模板指明需要检索的字段
    text = indexes.CharField(document=True, use_template=True)

    def get_model(self):
        """返回建立索引的模型类"""
        return SKU

    def index_queryset(self, using=None):
        """返回要建立索引的数据查询集"""
        return self.get_model().objects.filter(is_launched=True)

在templates/search/indexes/目录下创建goods目录,在其中创建sku_text.txt,在其中指定索引的属性。

tex 复制代码
{{object.id}}
{{object.name}}
{{object.caption}}

执行下面命令,手动生成初始索引

sh 复制代码
python .\manage.py rebuild_index

Are you sure you wish to continue? [y/N] y

在项目urls.py中增加haystack路由配置项

python 复制代码
path('search/', include('haystack.urls')),

在templates/search/目录下新增搜索结果模板search.html

html 复制代码
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
    <title>小鱼商城-商品搜索</title>
    <link rel="stylesheet" type="text/css" href="{{ static('css/jquery.pagination.css') }}">
    <link rel="stylesheet" type="text/css" href="{{ static('css/reset.css') }}">
    <link rel="stylesheet" type="text/css" href="{{ static('css/main.css') }}">
    <script type="text/javascript" src="{{ static('js/jquery-1.12.4.min.js') }}"></script>
    <script type="text/javascript" src="{{ static('js/vue-2.5.16.js') }}"></script>
    <script type="text/javascript" src="{{ static('js/axios-0.18.0.min.js') }}"></script>
</head>
<body>
<div id="app">
    <div class="header_con">
        <div class="header" v-cloak>
            <div class="welcome fl">欢迎来到小鱼商城!</div>
            <div class="fr">
                <div v-if="username" class="login_btn fl">
                    欢迎您:<em>[[ username ]]</em>
                    <span>|</span>
                    <a href="{{ url('users:logout') }}">退出</a>
                </div>
                <div v-else class="login_btn fl">
                    <a href="{{ url('users:login') }}">登录</a>
                    <span>|</span>
                    <a href="{{ url('users:register') }}">注册</a>
                </div>
                <div class="user_link fl">
                    <span>|</span>
                    <a href="{{ url('users:info') }}">用户中心</a>
                    <span>|</span>
                    {#                    <a href="{{ url('carts:info') }}">我的购物车</a>#}
                    <span>|</span>
                    {#                    <a href="{{ url('users:myorderinfo',args=(1,)) }}">我的订单</a>#}
                </div>
            </div>
        </div>
    </div>
    <div class="search_bar clearfix">
        <a href="{{ url('contents:index') }}" class="logo fl"><img src="{{ static('images/logo.png') }}"></a>
        <div class="search_wrap fl">
            <form method="get" action="/search/" class="search_con">
                <input type="text" class="input_text fl" name="q" placeholder="搜索商品">
                <input type="submit" class="input_btn fr" name="" value="搜索">
            </form>
            <ul class="search_suggest fl">
                <li><a href="#">索尼微单</a></li>
                <li><a href="#">优惠15元</a></li>
                <li><a href="#">美妆个护</a></li>
                <li><a href="#">买2免1</a></li>
            </ul>
        </div>
    </div>
    <div class="main_wrap clearfix">
        <div class="clearfix">
            <ul class="goods_type_list clearfix">
                {% for result in page %}
                    <li>
                        <a href="detail.html">
                            <img src="/static/images/goods/{{ result.object.default_image.url }}.jpg">
                        </a>
                        <h4><a href="detail.html">{{ result.object.name }}</a></h4>
                        <div class="operate">
                            <span class="price">¥{{ result.object.price }}</span>
                            <span>{{ result.object.comments }}评价</span>
                            <!--                        <span class="unit">{{ result.object.sales }}台</span>-->
                            <a href="#" class="add_goods" title="加入购物车"></a>
                        </div>
                    </li>
                    {% else %}
                    <p>没有找到您要查询的商品。</p>
                {% endfor %}
            </ul>
            <div class="pagenation">
                <div id="pagination" class="page"></div>
            </div>
        </div>
    </div>


    <div class="footer">
        <div class="foot_link">
            <a href="#">关于我们</a>
            <span>|</span>
            <a href="#">联系我们</a>
            <span>|</span>
            <a href="#">招聘人才</a>
            <span>|</span>
            <a href="#">友情链接</a>
        </div>
        <p>CopyRight © 2024 北京小鱼商业股份有限公司 All Rights Reserved</p>
        <p>电话:010-****888 京ICP备*******8号</p>
    </div>
</div>
<script type="text/javascript" src="{{ static('js/common.js') }}"></script>
<script type="text/javascript" src="{{ static('js/search.js') }}"></script>
<script type="text/javascript" src="{{ static('js/jquery.pagination.min.js') }}"></script>
<script type="text/javascript">
    $(function () {
        $('#pagination').pagination({
            currentPage: {{ page.number }},
            totalPage: {{ paginator.num_pages }},
            callback: function (current) {
                window.location.href = '/search/?q={{ query }}&page=' + current;
            }
        })
    });
</script>
</body>
</html>

在settings.py中增加HAYSTACK_SEARCH_RESULT_PER_PAGE控制结果页面显示的搜索记录数量。

复制代码
HAYSTACK_SEARCH_RESULTS_PER_PAGE = 5