1. 程式人生 > >Django的haystack對接elasticsearch伺服器完成全文檢索詳解

Django的haystack對接elasticsearch伺服器完成全文檢索詳解

1. 需求分析

當用戶在搜尋框輸入關鍵字後,我們要為使用者提供相關的搜尋結果。

這種需求依賴資料庫的模糊查詢like關鍵字可以實現,但是like關鍵字的效率極低,而且查詢需要在多個欄位中進行,使用like關鍵字也不方便。

我們引入搜尋引擎來實現全文檢索。全文檢索即在指定的任意欄位中進行檢索查詢。

2. 搜尋引擎原理

通過搜尋引擎進行資料查詢時,搜尋引擎並不是直接在資料庫中進行查詢,而是搜尋引擎會對資料庫中的資料進行一遍預處理,單獨建立起一份索引結構資料。

我們可以將索引結構資料想象成是字典書籍的索引檢索頁,裡面包含了關鍵詞與詞條的對應關係,並記錄詞條的位置。

我們在通過搜尋引擎搜尋時,搜尋引擎將關鍵字在索引資料中進行快速對比查詢,進而找到資料的真實儲存位置。

3. Elasticsearch

它可以快速地儲存、搜尋和分析海量資料。維基百科、Stack Overflow、Github 都採用它。

Elasticsearch 的底層是開源庫 Lucene。但是,你沒法直接用 Lucene,必須自己寫程式碼去呼叫它的介面。Elastic 是 Lucene 的封裝,提供了 REST API 的操作介面,開箱即用。

Elasticsearch 是用Java實現的。

搜尋引擎在對資料構建索引時,需要進行分詞處理。分詞是指將一句話拆解成多個單字或詞,這些字或詞便是這句話的關鍵詞。如

我是中國人。

'我'、'是'、'中'、'國'、'人'、'中國'等都可以是這句話的關鍵詞。

Elasticsearch 不支援對中文進行分詞建立索引,需要配合擴充套件elasticsearch-analysis-ik來實現中文分詞處理。

4. 使用Docker安裝Elasticsearch及其擴充套件

獲取映象,可以通過網路pull

docker image pull delron/elasticsearch-ik:2.4.6-1.0

或者載入提供給大家的映象檔案

連結:https://pan.baidu.com/s/1XmVd845y16Xo9cAOKFI0xg 
提取碼:j4vk 

docker load -i elasticsearch-ik-2.4.6_docker.tar

修改elasticsearch的配置檔案 elasticsearc-2.4.6/config/elasticsearch.yml第54行,更改ip地址為本機ip地址

network.host: 10.211.55.5

建立docker容器執行

docker run -dti --network=host --name=elasticsearch -v /home/python/elasticsearch-2.4.6/config:/usr/share/elasticsearch/config delron/elasticsearch-ik:2.4.6-1.0

5. 使用haystack對接Elasticsearch

Haystack為Django提供了模組化的搜尋。它的特點是統一的,熟悉的API,可以讓你在不修改程式碼的情況下使用不同的搜尋後端(比如 Solr, Elasticsearch, Whoosh, Xapian 等等)。

我們在django中可以通過使用haystack來呼叫Elasticsearch搜尋引擎。

1)安裝

pip install drf-haystack
pip install elasticsearch==2.4.1

drf-haystack是為了在REST framework中使用haystack而進行的封裝(如果在Django中使用haystack,則安裝django-haystack即可)。

2)註冊應用

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

3)配置

在配置檔案中配置haystack使用的搜尋引擎後端

# Haystack
HAYSTACK_CONNECTIONS = {
    'default': {
        'ENGINE': 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine',
        'URL': 'http://10.211.55.5:9200/',  # 此處為elasticsearch執行的伺服器ip地址,埠號固定為9200
        'INDEX_NAME': 'meiduo',  # 指定elasticsearch建立的索引庫的名稱
    },
}

# 當新增、修改、刪除資料時,自動生成索引
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'

注意:

HAYSTACK_SIGNAL_PROCESSOR 的配置保證了在Django執行起來後,有新的資料產生時,haystack仍然可以讓Elasticsearch實時生成新資料的索引

4)建立索引類

通過建立索引類,來指明讓搜尋引擎對哪些欄位建立索引,也就是可以通過哪些欄位的關鍵字來檢索資料。

在goods應用中新建search_indexes.py檔案,用於存放索引類

from haystack import indexes

from goods.models import SKU


class SKUIndex(indexes.SearchIndex, indexes.Indexable):
    """
    SKU索引資料模型類
    """
    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)

在SKUIndex建立的欄位,都可以藉助haystack由elasticsearch搜尋引擎查詢。

其中text欄位我們宣告為document=True,表名該欄位是主要進行關鍵字查詢的欄位, 該欄位的索引值可以由多個數據庫模型類欄位組成,具體由哪些模型類欄位組成,我們用use_template=True表示後續通過模板來指明。其他欄位都是通過model_attr選項指明引用資料庫模型類的特定欄位。

在REST framework中,索引類的欄位會作為查詢結果返回資料的來源。

6)在templates目錄中建立text欄位使用的模板檔案

具體在templates/search/indexes/goods/sku_text.txt檔案中定義

{{ object.name }}
{{ object.caption }}
{{ object.id }}

此模板指明當將關鍵詞通過text引數名傳遞時,可以通過sku的name、caption、id來進行關鍵字索引查詢。

7)手動生成初始索引

python manage.py rebuild_index

8)建立序列化器

在goods/serializers.py中建立haystack序列化器

from drf_haystack.serializers import HaystackSerializer

class SKUSerializer(serializers.ModelSerializer):
    """
    SKU序列化器
    """
    class Meta:
        model = SKU
        fields = ('id', 'name', 'price', 'default_image_url', 'comments')

class SKUIndexSerializer(HaystackSerializer):
    """
    SKU索引結果資料序列化器
    """
    object = SKUSerializer(read_only=True)

    class Meta:
        index_classes = [SKUIndex]
        fields = ('text', 'object')

說明:

  1. 下面的搜尋檢視使用SKUIndexSerializer序列化器用來檢查前端傳入的引數text,並且檢索出資料後再使用這個序列化器返回給前端;

  2. SKUIndexSerializer序列化器中的object欄位是用來向前端返回資料時序列化的欄位。

    Haystack通過Elasticsearch檢索出匹配關鍵詞的搜尋結果後,還會在資料庫中取出完整的資料庫模型類物件,放到搜尋結果的object屬性中,並將結果通過SKUIndexSerializer序列化器進行序列化。所以我們可以通過宣告搜尋結果的object欄位以SKUSerializer序列化的形式進行處理,明確要返回的搜尋結果中每個資料物件包含哪些欄位。

    如,通過上面兩個序列化器,最終的返回結果形式如下:

    [
        {
            "text": "華為 HUAWEI P10 Plus 6GB+128GB 鑽雕藍 移動聯通電信4G手機 雙卡雙待\nwifi雙天線設計!徠卡人像攝影!P10徠卡雙攝拍照,低至2988元!\n11",
            "object": {
                "id": 11,
                "name": "華為 HUAWEI P10 Plus 6GB+128GB 鑽雕藍 移動聯通電信4G手機 雙卡雙待",
                "price": "3788.00",
                "default_image_url": "http://image.meiduo.site:8888/group1/M00/00/02/CtM3BVrRdG6AYdapAAcPaeOqMpA1594598",
                "comments": 2
            }
        },
        {
            "text": "華為 HUAWEI P10 Plus 6GB+128GB 玫瑰金 移動聯通電信4G手機 雙卡雙待\nwifi雙天線設計!徠卡人像攝影!P10徠卡雙攝拍照,低至2988元!\n14",
            "object": {
                "id": 14,
                "name": "華為 HUAWEI P10 Plus 6GB+128GB 玫瑰金 移動聯通電信4G手機 雙卡雙待",
                "price": "3788.00",
                "default_image_url": "http://image.meiduo.site:8888/group1/M00/00/02/CtM3BVrRdMSAaDUtAAVslh9vkK04466364",
                "comments": 1
            }
        }
    ]
    

9)建立檢視

在goods/views.py中建立檢視

from drf_haystack.viewsets import HaystackViewSet

class SKUSearchViewSet(HaystackViewSet):
    """
    SKU搜尋
    """
    index_models = [SKU]

    serializer_class = SKUIndexSerializer

注意:

  • 該檢視會返回搜尋結果的列表資料,所以如果可以為檢視增加REST framework的分頁功能。
  • 我們在實現商品列表頁面時已經定義了全域性的分頁配置,所以此搜尋檢視會使用全域性的分頁配置。

返回的資料舉例如下:

{
    "count": 10,
    "next": "http://api.meiduo.site:8000/skus/search/?page=2&text=%E5%8D%8E",
    "previous": null,
    "results": [
        {
            "text": "華為 HUAWEI P10 Plus 6GB+64GB 鑽雕金 移動聯通電信4G手機 雙卡雙待\nwifi雙天線設計!徠卡人像攝影!P10徠卡雙攝拍照,低至2988元!\n9",
            "id": 9,
            "name": "華為 HUAWEI P10 Plus 6GB+64GB 鑽雕金 移動聯通電信4G手機 雙卡雙待",
            "price": "3388.00",
            "default_image_url": "http://10.211.55.5:8888/group1/M00/00/02/CtM3BVrRcUeAHp9pAARfIK95am88523545",
            "comments": 0
        },
        {
            "text": "華為 HUAWEI P10 Plus 6GB+128GB 鑽雕金 移動聯通電信4G手機 雙卡雙待\nwifi雙天線設計!徠卡人像攝影!P10徠卡雙攝拍照,低至2988元!\n10",
            "id": 10,
            "name": "華為 HUAWEI P10 Plus 6GB+128GB 鑽雕金 移動聯通電信4G手機 雙卡雙待",
            "price": "3788.00",
            "default_image_url": "http://10.211.55.5:8888/group1/M00/00/02/CtM3BVrRchWAMc8rAARfIK95am88158618",
            "comments": 5
        }
    ]
}

10)定義路由

通過REST framework的router來定義路由

router = DefaultRouter()
router.register('skus/search', views.SKUSearchViewSet, base_name='skus_search')

...

urlpatterns += router.urls

11)測試

我們可以GET方法訪問如下連結進行測試

http://api.meiduo.site:8000/skus/search/?text=wifi

bug說明:

如果在配置完haystack並啟動程式後,出現如下異常,是因為drf-haystack還沒有適配最新版本的REST framework框架

 

可以通過修改REST framework框架程式碼,補充_get_count函式定義即可

檔案路徑 虛擬環境下的 lib/python3.6/site-packages/rest_framework/pagination.py

def _get_count(queryset):
    """
    Determine an object count, supporting either querysets or regular lists.
    """
    try:
        return queryset.count()
    except (AttributeError, TypeError):
        return len(queryset)

6. 前端

在任何有提供搜尋框的頁面中,搜尋框的表單都類似如下:

<form method="get" action="/search.html" class="search_con">
    <input type="text" class="input_text fl" name="q" placeholder="搜尋商品">
    <input type="submit" class="input_btn fr" name="" value="搜尋">
</form>

當輸入關鍵字並點選搜獲後,會進入到/search.html?q=關鍵詞

在search.js中將q的關鍵詞傳送給後端/skus/search/介面

var vm = new Vue({
    el: '#app',
    delimiters: ['[[', ']]'], // 修改vue模板符號,防止與django衝突
    data: {
        host: host,
        username: sessionStorage.username || localStorage.username,
        user_id: sessionStorage.user_id || localStorage.user_id,
        token: sessionStorage.token || localStorage.token,
        page: 1, // 當前頁數
        page_size: 6, // 每頁數量
        count: 0,  // 總數量
        skus: [], // 資料
        query: '',  // 查詢關鍵字
        cart_total_count: 0, // 購物車總數量
        cart: [], // 購物車資料
    },
    computed: {
        total_page: function(){  // 總頁數
            return Math.ceil(this.count/this.page_size);
        },
        next: function(){  // 下一頁
            if (this.page >= this.total_page) {
                return 0;
            } else {
                return this.page + 1;
            }
        },
        previous: function(){  // 上一頁
            if (this.page <= 0 ) {
                return 0;
            } else {
                return this.page - 1;
            }
        },
        page_nums: function(){  // 頁碼
            // 分頁頁數顯示計算
            // 1.如果總頁數<=5
            // 2.如果當前頁是前3頁
            // 3.如果當前頁是後3頁,
            // 4.既不是前3頁,也不是後3頁
            var nums = [];
            if (this.total_page <= 5) {
                for (var i=1; i<=this.total_page; i++){
                    nums.push(i);
                }
            } else if (this.page <= 3) {
                nums = [1, 2, 3, 4, 5];
            } else if (this.total_page - this.page <= 2) {
                for (var i=this.total_page; i>this.total_page-5; i--) {
                    nums.push(i);
                }
            } else {
                for (var i=this.page-2; i<this.page+3; i++){
                    nums.push(i);
                }
            }
            return nums;
        }
    },
    mounted: function(){
        this.query = this.get_query_string('q');
        this.get_search_result();
        this.get_cart();
    },
    methods: {
        logout(){
            sessionStorage.clear();
            localStorage.clear();
            location.href = '/login.html';
        },
        // 獲取url路徑引數
        get_query_string: function(name){
            var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
            var r = window.location.search.substr(1).match(reg);
            if (r != null) {
                return decodeURI(r[2]);
            }
            return null;
        },
        // 請求查詢結果
        get_search_result: function(){
            axios.get(this.host+'/skus/search/', {
                    params: {
                        text: this.query,
                        page: this.page,
                        page_size: this.page_size,
                    },
                    responseType: 'json'
                })
                .then(response => {
                    this.skus = [];
                    this.count = response.data.count;
                    var results = response.data.results;
                    for(var i=0; i< results.length; i++){
                        var sku = results[i].object;
                        sku.url = '/goods/' + sku.id + ".html";
                        this.skus.push(sku);
                    }
                })
                .catch(error => {
                    console.log(error.response.data);
                })
        },
        // 點選頁數
        on_page: function(num){
            if (num != this.page){
                this.page = num;
                this.get_search_result();
            }
        },
        // 獲取購物車資料
        get_cart: function(){

        }
    }
});