通過擴展Django數據庫API支持全文搜索
本文為翻譯內容,原文請查看 http://www.mercurytide.com/
介紹Django
Django是一個開源的Web應用程序框架,引用創作者的話來說就是:“鼓勵快速開發和干凈、實效的設計”。它由Python編寫,并提供各種組件以創 建高質量的Web應用程序,包括一個ORM(object-relational mapper)框架、一個支持全部特性的模板系統、URL分發器、會話管理、安全認證和其他更多的東西。
一個對框架有價值的衡量標準是:對符合開發者需求的擴展是否簡單。這里我們將介紹一下Django的數據庫API,并通過擴展讓Django支持MySQL的全文搜索功能來演示它的靈活性。
這篇文章包含的源代碼已經發布在公共區域了。
Django的數據庫API
Django的ORM 提供了一個 豐富的API 用 來創建數據庫查詢。這個API可以讓應用程序開發人員從SQL語句的細節之中隔離開來,但是依然支持高效率方式的復雜選擇標準的查詢。這個方法的另一個值 得注意的好處就是它完全避免了“SQL注入攻擊”的可能性,同時應用程序開發人員不再需要直接向SQL語句中插入數據值。
對象從數據庫中獲取之后形成一個 “QuerySet” 查詢集合, 它是一個SQL選擇語句的提取結果。QuerySet提供了一些減少結果集合到對象集合的匹配/轉換細節的方法,其中一些方法就像一個SQL語句中的 WHERE條件子句--實際上,在幕后,一個QuerySet創造了一個SQL陳述作為它的方法被調用了。QuerySet實例是從一個模型類的管理器 (model class's Manager)實例中獲得的,通常情況下這些實例被叫做 objects 。這里有一些使用QuerySet的例子:
# 獲取包含所有文章的查詢集合
articles = Article.objects.all()
# 只包含今年(2007年)之前寫的文章
articles = articles.filter(posted_date__lt='2007-01-01')
# 但是我寫的文章除外
articles = articles.exclude(author__exact='Richard')
# 最后,把他們按照點擊率和發表日期進行升序排序
articles = articles.order_by('rating', 'posted_date')
QuerySets 可以非常方便的被過濾、排序, 同時他們被延遲求值:這些動作操作QuerySets內部的SQL語句,且這些語句不會被執行,除非你通過迭代器或者分片嘗試訪問QuerySet的記錄。
# 獲取前五篇文章, 只有這個時候才訪問數據庫
a = articles[:5]
為了擴展這個接口,我們將開發一個管理器子類和一個查詢集合子類,但在這之前我們暫時描述一下MySQL的全文搜索。
MySQL和全文搜索
MySQL擁有一個內建的全文搜索引擎,但是沒有專門的庫(例如 Lucene 和 Xapian )那么強大,它整合了數據庫引擎和查詢語法,所以在數據庫驅動的應用程序中很容易使用。這里我們只看看它對“自然語言”查詢的支持,其他細節特性請查看 MySql 文檔 。
通過在數據表的一個字段或者一些列字段上創建全文索引就可以激活 MySQL 的全文搜索功能了。不管行數據被寫入、更新,或者刪除,這些索引都將自動更新,所以搜索出來的結果都是最新的,永遠不會過期。創建索引的語句可以這樣使用:
CREATE FULLTEXT INDEX 索引名 ON 需要索引的表 (表中需要索引的列名)
搜索用 MATCH...AGAINST 表達式指定搜索的列名和搜索的文本來操作:
MATCH(要搜索的列名) AGAINST (要搜索的文本)
對于自然語言的查詢,使用這個表達式的方法可能向下面這樣:
SELECT title, MATCH(title, text) AGAINST ('Django framework')
AS `relevance`
FROM fulltext_article
WHERE MATCH(title, text) AGAINST ('Django framework')
這將返回匹配文本”Django and framework“的所有文章的標題(title)和適當分數(relevance score),默認情況下他們按照 relevance 降序排序。別擔心這個語句中重復的 MATCH...AGAINST 表達式 -- MySQL將會發現它們是一樣的,且只會執行這個搜索一次。需要特別注意的是,被傳到 MATCH 中的列一定要和數據庫中創建索引的列保持一致。
擴展數據庫API支持搜索
Django的設計原理之一就是”一致性、連貫性”,這個對于擴展是很重要的。為了讓全文搜索接口和Django的其他部分保持一致, 這個擴展應該使用 Manager 與 QuerySets, 那樣的話它就可以以同樣的方式使用了。實際上,也就是說程序員可以像下面這樣編寫語句:
Article.objects.search('Django Jazz Guitar')
Article.objects.filter(posted_date__gt='2006-07-01').search('Django Python')
這些語句將返回被過濾之后的 QuerySet 自身。為了達到這一個目的,我們將開發一個叫做 SearchQuerySet 的 QuerySet 子類來提供 search() 方法,并創建 MATCH...AGAINST 表達式填充到 SQL 語句中;創建一個叫做 SearchManager 的 Manager 子類返回 SearchQuerySet 子類。因為這個管理類為了便于使用也提供了許多 QuerySet 的方法,所以,為了保持一致,SearchQuerySet 也應該提供一個 search() 方法。這里是代碼:
from django.db import models, backend
class SearchQuerySet(models.query.QuerySet):
def __inti__(self, model=None, fields=None):
super(SearchQuerySet, self).__init__(model)
self._search_fields = fields
def search(self, query):
meta = self.model._meta
# 從模型中獲取數據表名稱和列名稱
# 用 'table_name'.'column_name' 的風格
columns = [meta.get_field(name, many_to_many=False).column
for name in self._search_fields]
full_names = ["%s.%s" %
(backend.quote_name(meta.db_table), backend.quote_name(column))
for column in columns]
# 創造 MATCH...AGAINST 表達式
fulltext_columns = ", ".join(full_names)
match_expr = ("MATCH(%s) AGAINST (%%s)" % fulltext_columns)
# 添加額外的 SELECT 和 WHERE 選項
return self.extra(select={'relevance': match_expr},
where=[match_expr],
params=[query, query])
class SearchManager(models.Manager):
def __init__(self, fields):
super(SearchManager, self).__init__()
self.search_fields = fields
def get_query_set(self):
return SearchQuerySet(self.model, self._search_fields)
def search(self, query):
return self.get_query_set().search(query)
這里,SearchQuerySet.search() 向 Django 要了表和列的名字,創建了一個 MATCH..AGAINST 表達式, 然后在一行中添加了查詢的 SELECT 和 WHERE 子句。
很方便,所有的實際工作都讓 Django 自己做了。模型類中的 _meta 對象存儲了關于模型和它的字段的所有“元信息”, 但是這里我們僅僅需要表名和字段名(當SearchQuerySet實例被初始化的時候,它被告知哪些將被搜索)。
QuerySet.extra() 方法提供了一個簡單的方式去添加擴展列、WHERE子句表達式,表到 QuerySet的內部 SQL 語句的引用: select 選擇參數把列名映射到一個表達式(想想 “SELECT expression AS alias”), where 參數是 WHERE 子句組成的表達式列表,而 params 參數是SQL 語句中內嵌字符串 '%s' 的替換值列表。
SearchManager 又怎么樣呢?像上面所說的,它必須返回一個 SearchQuerySet 實例來替換普通的 QuerySets。幸運地是,Manager 類已經編寫了這個方法,它有一個 get_query_set() 方法用來返回一個適當的 QuerySet 子類的實例, 且無論合適一個管理類需要創建一個新的 QuerySet 實例的時候,它都會被調用,那么重寫這個 get_query_set() 方法讓它返回一個 SearchQuerySet 就顯得微不足道了。當創建一個 SearchQuerySet 實例的時候,它傳入待搜索的字段,這些已經在它自己的構造器中提供好了。我們也想讓 SearchManager 實現一個 search() 便利的方法,但是我們其實這是負責代理 SearchQuerySet.search() 就行了。
使用搜索組件
我們將演示這些子類的使用方法。這里有一個表現發表在一個Web站點上的文章的簡單模型;所以它可以被搜索,我們創建一個 SearchManager 實例并把它指派到 objects :
from django.db import models
from fulltext.search import SearchManager
class Article(models.Model):
posted_date = models.DateField(db_index=True)
title = models.CharField(maxlength=100)
text = models.TextField()
# 用一個 SearchManager 獲取對象,并告訴他哪些字段需要被搜索
objects = SearchManager(('title', 'text'))
class Admin:
pass
def __str__(self):
return "%s (%s) " % (self.title, self.posted_date)
文章都有一個標題,一個主要部分正文,和一個它們被發表的日期。我們將在數據庫中為標題(title)和正文(text)列定義一個 FULLTEXT INDEX,并傳遞一個相應字段名稱的元組給 SearchManager 實例。這里是創建索引的 SQL 語句。
CREATE FULLTEXT INDEX fulltext_article_title_text
ON fulltext_article (title, text);
假設一個 Django 工程,一個應用程序包含了文章模型,一個填充了合適的文章數據的數據庫,那么全文搜索就可以在 Python 的交互式解釋器終輕松演示了:
# 這里有多少篇文章?
len(Article.objects.all())
# 找到關于 frameworks 的文章
Article.objects.search('framework')
# 顯示這些文章的適當分數
[(a, a.relevance)
for a in Article.objects.search('framework')]
# 把這些文章的搜索結果限制在發表日期為六月份以前
Article.objects.search('framework').filter(posted_date__lt='2006-06-01')
# 注意,filter() 也返回一個 SearchQuerySet:
Article.objects.filter(posted_date__lt='2006-06-01').search('framework')
最終的注意點
現在,我想讓你知道一個秘密:從2006年6月份起,Django 已經支持一個搜索操作符(search operator)用來查詢 MySQL 的全文搜索了![]()
# 這里使用布爾搜索模式,不是自然語言查詢
Article.objects.filter(title__search='+Django -Rails')
盡管如此,這篇文章所演示的技術特性依然可以用于創建數據庫API擴展上用來支持任何SQL特性,不論是支持全文搜索,分組、聚合查詢還是任何其他SQL特性或者特定的數據庫擴展。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
