作者: HelloGitHub-追夢人物

在 django 博客教程中,我們使用了 django-haystack 和 Elasticsearch 進行文章內容的搜索。django-haystack 默認返回的搜索結果是一個類似於 django QuerySet 的對象,需要配合模板系統使用,因爲未被序列化,所以無法直接用於 django-rest-framework 的接口。當然解決方案也很簡單,編寫相應的序列化器將返回結果序列化就可以了。

但是,通過之前的功能我們看到,使用 django-rest-framework 是一個近乎標準化但又枯燥無聊的過程:首先是編寫序列化器用於序列化資源,然後是編寫視圖集,提供對資源各類操作的接口。既然是標準化的東西,肯定已經有人寫好了相關的功能以供複用。此時就要發揮開源社區的力量,去 GitHub 使用關鍵詞 rest haystack 搜索,果然搜到一個 drf-haystack 開源項目,專門用於解決 django-rest-framework 和 haystack 結合使用的問題。因此我們就不再重複造輪子,直接使用開源第三方庫來實現我們的需求。

既然要使用第三方庫,第一步當然是安裝它,進入項目根目錄,運行:

$ pipenv install drf-haystack

由於需要使用到搜索功能,因此需要啓動 Elasticsearch 服務,最簡單的方式就是使用項目中編排的 Elasticsearch 鏡像啓動容器。

項目根目錄下運行如下命令啓動全部項目所需的容器服務:

$ docker-compose -f local.yml up --build

啓動完成後運行 docker ps 命令可以檢查到如下 2 個運行的容器,說明啓動成功:

hellodjango_rest_framework_tutorial_local
hellodjango_rest_framework_tutorial_elasticsearch_local

接着創建一些文章,以便用於搜索測試,可以自己在 admin 後臺添加,當然最簡單的方法是運行項目中的 fake.py 腳本,批量生成測試數據:

$ docker-compose -f local.yml run --rm hellodjango.rest.framework.tutorial.local python -m scripts.fake

測試文章生成後,還要運行下面的命令給文章的內容創建索引,這樣搜索引擎才能根據索引搜索到相應的內容:

$ docker-compose -f local.yml run --rm hellodjango.rest.framework.tutorial.local python manage.py rebuild_index

# 輸出如下
Your choices after this are to restore from backups or rebuild via the `rebuild_index` command.
Are you sure you wish to continue? [y/N] y
Removing all documents from your index because you said so.
All documents removed.
Indexing 201 文章
GET /hellodjango_blog_tutorial/_mapping [status:404 request:0.005s]

注意

如果生成索引時看到如下錯誤:

elasticsearch.exceptions.ConnectionError: ConnectionError(<urllib3.connection.HTTPConnection object at 0x7f25daa83c50>: Failed to establish a new connection:

[Errno -2] Name does not resolve) caused by: NewConnectionError(<urllib3.connection.HTTPConnection object at 0x7f25daa83c50>: Failed to establish a new connection: [Errno -2] Name does not resolve)

這是由於項目配置中 Elasticsearch 服務的 URL 配置出錯導致,解決方法是進入 settings/local.py 配置文件中,將搜索設置改爲下面的內容:

HAYSTACK_CONNECTIONS['default']['URL'] = ' http://elasticsearch.local:9200/ '

因爲這個 URL 地址需和容器編排文件 local.yml 中指定的容器服務名一致 Docker 才能正確解析。

現在萬事具備了,數據庫中已經有了文章,搜索服務已經有了文章的索引,只需要等待客戶端來進行查詢,然後返回結果。所以接下來就進入到 django-rest-framework 標準開發流程:定義序列化器 -> 編寫視圖 -> 配置路由,這樣一個標準的搜索接口就開發出來了。

先來定義序列化器,粗略過一遍 drf-haystack 官方文檔 ,依葫蘆畫瓢創建文章(Post) 的 Serializer

blog/serializers.py

from drf_haystack.serializers import HaystackSerializerMixin


class PostHaystackSerializer(HaystackSerializerMixin, PostListSerializer):
    class Meta(PostListSerializer.Meta):
        search_fields = ["text"]

根據官方文檔的介紹,爲了複用已經定義好用於序列化文章列表的序列化器,我們直接繼承了 PostListSerializer ,同時我們還混入了 HaystackSerializerMixin ,這是 drf-haystack 的混入類,提供搜索結果序列化相關的功能。

另外內部類 Meta 同樣繼承 PostListSerializer.Meta ,這樣就無需重複定義序列化字段列表 fields 。關鍵的地方在這個 search_fields ,這個列表聲明用於搜索的字段(通常都定義爲索引字段),我們在上一部教程設置 django-haystack 時,文章的索引字段設置的名字叫 text,如果對這一塊有疑惑,可以簡單回顧一下 Django Haystack 全文檢索與關鍵詞高亮 中的內容。

然後編寫視圖集,需繼承 HaystackViewSet

blog/views.py

from drf_haystack.viewsets import HaystackViewSet
from .serializers import PostHaystackSerializer

class PostSearchView(HaystackViewSet):
    index_models = [Post]
    serializer_class = PostHaystackSerializer

這個視圖集非常簡單,只需要通過類屬性 index_models 聲明需要搜索的模型,以及搜索結果的序列化器就行了,剩餘的功能均由 HaystackViewSet 內部替我們實現了。

最後是在路由器中註冊視圖集,自動生成 URL 模式:

blogproject/urls.py

router = routers.DefaultRouter()
router.register(r"search", blog.views.PostSearchView, basename="search")

搞定了!一套標準化的 django-restful-framework 開發流程,不過大量工作已由 drf-haystack 在背後替我們完成,我們只寫了非常少量的代碼即實現了一套搜索接口。

來看看搜索效果。我們啓動 Docker 容器,在瀏覽器輸入如下格式的 URL:

http://127.0.0.1:8000/api/search/?text=key-word

將 key-word 替換爲需要搜索的關鍵字,例如將其替換爲 markdown,測試集數據中得到的搜索結果如下:

搜索結果符合預期,但略微有一點不太好的地方,就是沒有高亮的標題和摘要,我們希望將來顯示的結果應該是下面這樣的,因此返回的數據必須支持這樣的顯示:

關鍵詞高亮的實現原理其實非常簡單,通過解析整段文本,將搜索關鍵詞替換爲由 HTML 標籤包裹的富文本,並給這個包裹標籤設置 CSS 樣式,讓其顯示不同的字體顏色就可以了。

瞭解其原理後當然就是實現其功能,不過 django-haystack 已經爲我們造好了輪子,而且在上一部教程的 Django Haystack 全文檢索與關鍵詞高亮 ,我們還對默認的高亮輔助類進行了改造,優化了文章標題被從關鍵字位置截斷的問題,因此我們使用改造後的輔助類來對需要高亮的結果進行處理。

需要高亮的其實是 2 個字段,一個是 title 、一個是 body 。而 body 我們不需要完整的內容,只需要摘出其中一部分作爲搜索結果的摘要即可。這兩個功能,輔助類均已經爲我們提供了,我們只需要調用所需的方法就行。

注意到這裏我們需要對 titlebody 兩個字段進行高亮處理,其基本邏輯其實就是接收 titlebody 的值作爲輸入,高亮處理後再輸出。回顧一下序列化器的序列化字段,其實也是接收某個字段的值作爲輸入,對其進行處理,將其轉化爲可序列化的結果後輸出,和我們需要的邏輯很像。但是,django-rest-framework 並沒有提供這些比較個性化需求的序列化字段,因此接下來我們接觸 drf 的一點高級用法——自定義序列化字段。

自定義序列化字段其實非常的簡單,基本流程分兩步走:

to_representation

以我們的需求爲例。因爲 titlebody 均爲字符型,因此選擇父類序列化字段爲 CharField ,定義一個 HighlightedCharField 字段如下:

from .utils import Highlighter

class HighlightedCharField(CharField):
    def to_representation(self, value):
        value = super().to_representation(value)
        request = self.context["request"]
        query = request.query_params["text"]
        highlighter = Highlighter(query)
        return highlighter.highlight(value)

django-rest-framework 通過調用序列化字段的 to_representation 方法對輸入的值進行序列化,這個方法接收的第一個參數就是需要序列化的值。在我們自定義的邏輯中,首先調用父類 CharFieldto_representation 方法,父類序列化的邏輯是將任何輸入的值都轉爲字符串;接着我們從 context 屬性中取得 request 對象,這個對象就是視圖中的 HTTP 請求對象,但是因爲 django 中 request 對象無法像 flask 那樣從全局獲取,因此 drf 在視圖中將其保存在了序列化器和序列化字段的 context 屬性中以便在視圖外訪問;獲取 request 對象的目的是希望獲取查詢的關鍵字, query_params 屬性是一個類字典對象,用於記錄來自 URL 的查詢參數,例如我們之前測試查詢功能時調用的 URL 爲 /api/search/?text=markdown,所以 query_params 保存了 URL 中的查詢參數,將其封裝爲一個類字段對象 {"text": "markdown"} ,這裏 text 的值就是查詢的關鍵字,我們將它傳給 Highlighter 輔助類,然後調用 highlight 方法將需要序列化的值進行進一步的高亮處理。

序列化字段定義好後,我們就可以在序列化器中用它了:

class PostHaystackSerializer(HaystackSerializerMixin, PostListSerializer):
    title = HighlightedCharField()
    summary = HighlightedCharField(source="body")

    class Meta(PostListSerializer.Meta):
        search_fields = ["text"]
        fields = [
            "id",
            "title",
            "summary",
            "created_time",
            "excerpt",
            "category",
            "author",
            "views",
        ]

title 字段原本使用默認的 CharField 進行序列化,這裏我們重新指定爲自定義的 HighlightedCharField ,這樣序列化後的值就是高亮的格式。

summary 是我們新增的字段,注意我們序列化的對象是文章 Post,但這個對象是沒有 summary 這個屬性的,但是 summary 其實是對屬性 body 序列化後的結果,因此我們通過指定序列化化字段的 source 參數,指定值的來源。

最後別忘了在 fields 中申明全部序列化的字段,主要是把新增的 summary 加進去。

來看看改進後的搜索效果:

注意觀察返回的 title 和 summary,我們搜索的關鍵詞是 markdown,可以看到所有 markdown 關鍵字都被包裹了一個 span 標籤,並且設置了 class 屬性爲 highlighted,只要設置好 css 樣式,頁面所有的 markdown 關鍵詞就會顯示不同的顏色,從而實現搜索關鍵詞高亮的效果了。

當然,我們現在並沒有實際用到這個特性,下一部教程我們將使用 Vue 來開發博客,到時候調用搜索接口拿到搜索結果後就會實際用到了。

關注公衆號加入交流羣

相關文章