第 11 篇:基於 drf-haystack 的文章搜尋介面

削微寒發表於2020-07-03

作者: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 的一點高階用法——自定義序列化欄位。

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

  1. 從 drf 官方提供的序列化欄位中找一個資料型別最為接近的作為父類。
  2. 重寫 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 來開發部落格,到時候呼叫搜尋介面拿到搜尋結果後就會實際用到了。


關注公眾號加入交流群

相關文章