在 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
我們不需要完整的內容,只需要摘出其中一部分作為搜尋結果的摘要即可。這兩個功能,輔助類均已經為我們提供了,我們只需要呼叫所需的方法就行。
注意到這裡我們需要對 title
、body
兩個欄位進行高亮處理,其基本邏輯其實就是接收 title
、body
的值作為輸入,高亮處理後再輸出。回顧一下序列化器的序列化欄位,其實也是接收某個欄位的值作為輸入,對其進行處理,將其轉化為可序列化的結果後輸出,和我們需要的邏輯很像。但是,django-rest-framework 並沒有提供這些比較個性化需求的序列化欄位,因此接下來我們接觸 drf 的一點高階用法——自定義序列化欄位。
自定義序列化欄位其實非常的簡單,基本流程分兩步走:
- 從 drf 官方提供的序列化欄位中找一個資料型別最為接近的作為父類。
- 重寫
to_representation
方法,加入自己的序列化邏輯。
以我們的需求為例。因為 title
、body
均為字元型,因此選擇父類序列化欄位為 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
方法對輸入的值進行序列化,這個方法接收的第一個引數就是需要序列化的值。在我們自定義的邏輯中,首先呼叫父類 CharField
的 to_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 來開發部落格,到時候呼叫搜尋介面拿到搜尋結果後就會實際用到了。
關注公眾號加入交流群