1. 程式人生 > >[CareerCup] 10.7 Simplified Search Engine 簡單的搜尋引擎

[CareerCup] 10.7 Simplified Search Engine 簡單的搜尋引擎

10.7 Imagine a web server for a simplified search engine. This system has 100 machines to respond to search queries, which may then call out using processSearch(string query) to another cluster of machines to actually get the result. The machine which responds to a given query is chosen at random, so you can not guarantee that the same machine will always respond to the same request. The method processSearch is very expensive. Design a caching mechanism for the most recent queries. Be sure to explain how you would update the cache when data changes.

這道題說假設有一個簡單搜尋引擎的網路伺服器,系統共有100個機子來響應檢索,可以用processSearch(string query)來得到其他機子上的結果,每臺機子響應檢索是隨機的,不保證每個機子都會響應到同一個請求。processSearch方法非常昂貴,設計一個快取機制來應對近期檢索。根據書中描述,我們先來做一些假設:

1. 與其說根據需要呼叫processSearch,倒不如設定所有的檢索處理髮生在第一個被呼叫的機子上。

2. 我們需要快取的檢索是非常大量的。

3. 機器之間的呼叫很快。

4. 檢索的結果是一個有序的URL連結串列,每個URL由50個字元的標題和200個字元的概要組成。

5. 最常訪問的檢索會一直出現的快取器中。

系統需求:

主要需要實現下列兩個功能:

1. 高效查詢當給定了一個關鍵字時

2. 新資料會代替舊資料的位置

我們還需要更新和清楚快取當搜尋結果改變了。由於一些檢索非常的常見病永久的在快取器中,我們不能等快取器自然失效。

步驟一:設計單個系統的存存器

我們可以混合使用連結串列和雜湊表來實現,我們建立一個連結串列,當某個節點被訪問了,自動將其移到開頭,這樣連結串列的末尾就是最老的資料。我們用雜湊表來建立檢索和連結串列中節點的對映,這樣不僅可以讓我們高效的返回快取的結果,而且可以把節點移到連結串列前段,參見程式碼如下:

class
Node { public: Node *pre; Node *next; vector<string> results; string query; Node(string q, vector<string> res) { results = res; query = q; } }; class Cache { public: const static int MAX_SIZE = 10; Node *head, *tail; unordered_map<string, Node*> m; int size = 0; Cache() {} void moveToFront(Node *node) { if (node == head) return; removeFromLinkedList(node); node->next = head; if (head != nullptr) { head->pre = node; } head = node; ++size; if (tail == nullptr) { tail = node; } } void moveToFront(string query) { if (m.find(query) == m.end()) return; moveToFront(m[query]); } void removeFromLinkedList(Node *node) { if (node == nullptr) return; if (node->next != nullptr || node->pre != nullptr) { --size; } Node *pre = node->pre; if (pre != nullptr) { pre->next = node->next; } Node *next = node->next; if (next != nullptr) { next->pre = pre; } if (node == head) { head = next; } if (node == tail) { tail = pre; } node->next = nullptr; node->pre = nullptr; } vector<string> getResults(string query) { if (m.find(query) == m.end()) return vector<string>(); Node *node = m[query]; moveToFront(node); return node->results; } void insertResults(string query, vector<string> results) { if (m.find(query) != m.end()) { Node *node = m[query]; node->results = results; moveToFront(node); return; } Node *node = new Node(query, results); moveToFront(node); m[query] = node; if (size > MAX_SIZE) { for (unordered_map<string, Node*>::iterator it = m.begin(); it != m.end(); ++it) { if (it->first == tail->query) m.erase(it); } removeFromLinkedList(tail); } } };

步驟二: 擴充套件到多個機子

對於多個機子,我們有許多中選擇:

選擇1:每個機子有自己的快取器,這種方法的好處是快速,因為沒有機子間的呼叫,但是缺點是不高效

選擇2:每個機子都有快取器的拷貝,當新專案新增到快取器,傳送給所有的機器,設計目的是為了讓常用檢索存在於所有機子上,缺點是快取器的空間有限,無法儲存大量資料

選擇3:每個機器儲存快取器的一部分,當機器i需要得到一個檢索的結果,它需要找出哪個機子有這個結果,併到那個機子上取結果。但是問題是機子i怎麼知道那個機子上有結果,我們可以用雜湊表來建立對映,hash(query)%N,能快速知道哪個機子有想要的結果。

步驟三:更新結果當內容改變

有些檢索很常用,所以會永遠存在快取器中,我們需要一些機制能更新結果,當其內容改變了,快取器中的結果頁應該相應變換,主要有下列三種情況:

1. URL的內容改變了

2. 當網頁的排行改變了,那麼結果的順序也變了

3. 對於特定的檢索有了新的頁面

對於1和2,我們建立單獨的雜湊表告訴我們哪一個檢索和哪個URL之間有對映,這個可以在不同的機子上分別完成,但是可能會需要很多資料。另外,如果資料不需要即時重新整理,我們也可以週期性的來更新快取器。對於3,我們可以實現一個自動超時機制,我們設定一個時間段,如果在這個時間段裡沒有檢索,不管它之間被檢索的有多頻繁,我們都清除它,這樣保證了所有資料都會被週期性的更新。

步驟四:進一步地增強

一個優化是當某個檢索特別頻繁時,比如一個檢索佔了1%的比重時,我們與其讓機器i傳送請求給機器j,倒不如在機器i上將結果存在自己的快取器中。

再有就是我們將檢索根據雜湊值分別不同機器,而不是隨機分配。

另外一個優化就是之前提到的自動超時Automatic Time Out機制,就是x分鐘後自動清除資料,但是有時候我們對不同的資料希望設定不同的x值,這樣每一個URL都有一個超時值基於此頁面過去被更新的頻率。