Solr結(jié)果分頁(yè)

2018-11-29 14:36 更新

在大多數(shù)搜索應(yīng)用程序中,“top” 匹配結(jié)果(按分?jǐn)?shù)或其他標(biāo)準(zhǔn)排序)將顯示給某些用戶(hù)。

在許多應(yīng)用程序中,這些排序結(jié)果的用戶(hù)界面以“頁(yè)面”顯示給用戶(hù),其中包含固定數(shù)量的匹配結(jié)果,而用戶(hù)通常不會(huì)查看經(jīng)過(guò)前幾頁(yè)結(jié)果的結(jié)果。

基本分頁(yè)

在 Solr 中,使用 start 和 rows 參數(shù)支持這種基本的分頁(yè)搜索,通過(guò)使用 queryResultCache 和根據(jù)預(yù)期的頁(yè)面大小調(diào)整 queryResultWindowSize 配置選項(xiàng),可以調(diào)整這種常見(jiàn)行為的性能。

基本分頁(yè)示例

談到基礎(chǔ)的分頁(yè),最簡(jiǎn)單的方法就是將所需的頁(yè)碼乘以每頁(yè)的行數(shù) (將第一頁(yè)的頁(yè)碼視為 "0")。如在以下偽代碼中所示:

function fetch_solr_page($page_number, $rows_per_page) {
  $start = $page_number * $rows_per_page
  $params = [ q = $some_query, rows = $rows_per_page, start = $start ]
  return fetch_solr($params)
}

索引更新對(duì)基本分頁(yè)的影響

Solr 請(qǐng)求中指定的 start 參數(shù)指示客戶(hù)端希望 Solr 用作當(dāng)前“頁(yè)面”開(kāi)頭的完整排序匹配列表中的絕對(duì) “偏移量”。

如果索引修改 (如添加或刪除文檔) 影響與查詢(xún)匹配的有序文檔的順序,則會(huì)在客戶(hù)端的兩個(gè)請(qǐng)求之間發(fā)生,從而導(dǎo)致后續(xù)頁(yè)的結(jié)果,那么這些修改可能會(huì)產(chǎn)生在多個(gè)頁(yè)上返回的同一文檔,或者當(dāng)結(jié)果集收縮或增大時(shí),文檔被 "跳過(guò)"。

例如,考慮一個(gè)包含 26 個(gè)文檔的索引,如下所示:

ID 名稱(chēng)

1

A

2

B

...

...

26

Z


后跟以下請(qǐng)求和索引修改交錯(cuò):

  • 客戶(hù)請(qǐng)求
     q=:&rows=5&start=0&sort=name asc 
    帶有 1-5 的 ID 的文檔將返回給客戶(hù)端
  • ID 為 3 的文檔被刪除
  • 客戶(hù)端請(qǐng)求“page#2” 使用
    q=:&rows=5&start=5&sort=name asc

    文件 7-11 將被退回;
    已跳過(guò)文檔 6,因?yàn)樗F(xiàn)在是所有匹配結(jié)果的排序集合中的第5個(gè)文檔 - 它將在 “page#1” 的新請(qǐng)求上返回。

  • 現(xiàn)在添加了ID為90,91以及92的3頁(yè)新的文件;這三個(gè)文件都有一個(gè)名稱(chēng)
  • 客戶(hù)端請(qǐng)求“第3頁(yè)”使用:
    q=:&rows=5&start=10&sort=name asc
    文檔9、10和11已在 page #2 和 page #3 中返回,因?yàn)樗鼈円频搅伺判蚪Y(jié)果列表中的更遠(yuǎn)的后面。

在典型的情況下,從索引更改對(duì)分頁(yè)搜索的影響不會(huì)顯著影響用戶(hù)體驗(yàn) - 因?yàn)樗鼈冊(cè)谙喈?dāng)靜態(tài)的集合中極少發(fā)生,或者是因?yàn)橛脩?hù)認(rèn)識(shí)到數(shù)據(jù)集合不斷發(fā)展并期望看到文檔在結(jié)果集中上下移動(dòng)。

“深度分頁(yè)”的性能問(wèn)題

在某些情況下,Solr 搜索的結(jié)果不適用于簡(jiǎn)單的分頁(yè)用戶(hù)界面。

當(dāng)您希望從 Solr 中獲取大量的排序結(jié)果,并將其輸入到外部系統(tǒng)中時(shí),為 startor rows 參數(shù)使用非常大的值可能是非常低效的。分頁(yè)使用 start 和 rows 不僅要求 Solr 計(jì)算(和排序)在內(nèi)存中應(yīng)為當(dāng)前頁(yè)面提取的所有匹配文檔,而且還需要在以前的頁(yè)面上出現(xiàn)的所有文檔。

雖然請(qǐng)求 start=0&rows=1000000 可能顯然是低效率的,因?yàn)樗?Solr 維護(hù)和排序一百萬(wàn)份文檔,同樣 start=999000&rows=1000,由于同樣的原因,請(qǐng)求同樣是低效的。Solr 無(wú)法計(jì)算出排序順序中的哪個(gè)匹配文檔是 999001 個(gè)結(jié)果,而無(wú)需先確定前 999000 個(gè)匹配排序結(jié)果是什么。

如果索引是分布式的(在 SolrCloud 模式下運(yùn)行時(shí)常見(jiàn)),則從每個(gè)分片中檢索一百萬(wàn)個(gè)文檔。對(duì)于十個(gè)分片索引,必須檢索和排序一千萬(wàn)個(gè)條目以找出與這些查詢(xún)參數(shù)匹配的 1000 個(gè)文檔。

獲取大量排序結(jié)果:Cursor

作為增加 “start” 參數(shù)以請(qǐng)求后續(xù)頁(yè)的排序結(jié)果的替代方法,Solr 支持使用 “Cursor” 掃描結(jié)果。

Solr 中的 Cursor 是一個(gè)邏輯概念,不涉及在服務(wù)器上緩存任何狀態(tài)信息。而是使用返回給客戶(hù)端的最后一個(gè)文檔的排序值來(lái)計(jì)算表示排序值的有序空間中的邏輯點(diǎn)的“mark”。這個(gè)“mark”可以在隨后的請(qǐng)求參數(shù)中指定,告訴 Solr 在哪里繼續(xù)。

使用 Cursor

要在 Solr 中使用 Cursor ,請(qǐng)指定具有 \* 值的 cursorMark 參數(shù)。您可以把這 start=0 看作是告訴 Solr “在我的排序結(jié)果開(kāi)始處開(kāi)始”的一種方法,它也告訴 Solr 您想使用一個(gè) Cursor。

除了返回前 N 個(gè)排序結(jié)果(可以使用 rows 參數(shù)控制 N )之外,Solr 響應(yīng)還將包括一個(gè)名為 nextCursorMark 的編碼字符串。然后從響應(yīng)中取 nextCursorMark 字符串值,并將其作為cursorMark 參數(shù)傳遞回 Solr 作為下一個(gè)請(qǐng)求。您可以重復(fù)這個(gè)過(guò)程,直到您已經(jīng)獲取盡可能多的文檔,或者直到返回的 nextCursorMark 與已指定的 cursorMark 匹配為止,這表示沒(méi)有更多的結(jié)果。

使用 Cursor 時(shí)的約束

在 Solr 請(qǐng)求中使用 cursorMark 參數(shù)時(shí)需要注意一些重要的約束條件:

  1. cursorMark 和 start 是互斥的參數(shù)。
    您的請(qǐng)求必須不包含 start 參數(shù),或者必須使用值 “0” 指定。
  2. sort 子句必須包含 uniqueKey 字段(asc 或者 desc)。
    如果 id 是您的 uniqueKey 字段,那么類(lèi)似 id asc、name asc、id desc 的參數(shù)將工作正常,但 name asc 本身不會(huì)
  3. 排序包括基于日期數(shù)學(xué)的函數(shù),涉及與 NOW 相關(guān)的計(jì)算將導(dǎo)致混淆的結(jié)果,因?yàn)槊總€(gè)文檔將在每個(gè)后續(xù)請(qǐng)求中獲得新的排序值。這很容易導(dǎo)致永遠(yuǎn)不會(huì)結(jié)束的 Cursor,并且不斷地返回相同的文檔 - 即使文檔從不更新。在這種情況下,為所有 Cursor 請(qǐng)求中的 "NOW" 請(qǐng)求參數(shù)選擇和重用一個(gè)固定值。

游標(biāo)標(biāo)記值是根據(jù)結(jié)果中每個(gè)文檔的排序值計(jì)算出來(lái)的,這意味著如果多個(gè)具有相同排序值的文檔中的一個(gè)是結(jié)果頁(yè)面上的最后一個(gè)文檔,則會(huì)產(chǎn)生相同的 Cursor 標(biāo)記值。在這種情況下,使用 cursorMark 的后續(xù)請(qǐng)求將不知道具有相同標(biāo)記值的哪個(gè)文檔應(yīng)該被跳過(guò)。要求將 uniqueKey 字段作為排序標(biāo)準(zhǔn)中的一個(gè)子句使用,可以確保返回一個(gè)確定性排序,并且每個(gè) cursorMark值都將標(biāo)識(shí)文檔序列中的一個(gè)唯一點(diǎn)。

Cursor 示例

獲取所有文檔

此處顯示的偽代碼顯示了使用 Cursor 獲取與查詢(xún)匹配的所有文檔時(shí)涉及的基本邏輯:

// when fetching all docs, you might as well use a simple id sort
// unless you really need the docs to come back in a specific order
$params = [ q => $some_query, sort => 'id asc', rows => $r, cursorMark => '*' ]
$done = false
while (not $done) {
  $results = fetch_solr($params)
  // do something with $results
  if ($params[cursorMark] == $results[nextCursorMark]) {
    $done = true
  }
  $params[cursorMark] = $results[nextCursorMark]
}

使用 SolrJ,這個(gè)偽代碼將是:

SolrQuery q = (new SolrQuery(some_query)).setRows(r).setSort(SortClause.asc("id"));
String cursorMark = CursorMarkParams.CURSOR_MARK_START;
boolean done = false;
while (! done) {
  q.set(CursorMarkParams.CURSOR_MARK_PARAM, cursorMark);
  QueryResponse rsp = solrServer.query(q);
  String nextCursorMark = rsp.getNextCursorMark();
  doCustomProcessingOfResults(rsp);
  if (cursorMark.equals(nextCursorMark)) {
    done = true;
  }
  cursorMark = nextCursorMark;
}

如果您想用 curl 手工完成,請(qǐng)求的順序看起來(lái)是這樣的:

$ curl '...&rows=10&sort=id+asc&cursorMark=*'
{
  "response":{"numFound":32,"start":0,"docs":[
    // ... 10 docs here ...
  ]},
  "nextCursorMark":"AoEjR0JQ"}
$ curl '...&rows=10&sort=id+asc&cursorMark=AoEjR0JQ'
{
  "response":{"numFound":32,"start":0,"docs":[
    // ... 10 more docs here ...
  ]},
  "nextCursorMark":"AoEpVkRCREIxQTE2"}
$ curl '...&rows=10&sort=id+asc&cursorMark=AoEpVkRCREIxQTE2'
{
  "response":{"numFound":32,"start":0,"docs":[
    // ... 10 more docs here ...
  ]},
  "nextCursorMark":"AoEmbWF4dG9y"}
$ curl '...&rows=10&sort=id+asc&cursorMark=AoEmbWF4dG9y'
{
  "response":{"numFound":32,"start":0,"docs":[
    // ... 2 docs here because we've reached the end.
  ]},
  "nextCursorMark":"AoEpdmlld3Nvbmlj"}
$ curl '...&rows=10&sort=id+asc&cursorMark=AoEpdmlld3Nvbmlj'
{
  "response":{"numFound":32,"start":0,"docs":[
    // no more docs here, and note that the nextCursorMark
    // matches the cursorMark param we used
  ]},
  "nextCursorMark":"AoEpdmlld3Nvbmlj"}

獲取前 N 個(gè)文檔,基于 Post 處理

由于從 Solr 的角度來(lái)看,游標(biāo)是無(wú)狀態(tài)的,所以一旦您確定有足夠的信息,您的客戶(hù)端代碼就可以停止獲取額外的結(jié)果:

while (! done) {
  q.set(CursorMarkParams.CURSOR_MARK_PARAM, cursorMark);
  QueryResponse rsp = solrServer.query(q);
  String nextCursorMark = rsp.getNextCursorMark();
  boolean hadEnough = doCustomProcessingOfResults(rsp);
  if (hadEnough || cursorMark.equals(nextCursorMark)) {
    done = true;
  }
  cursorMark = nextCursorMark;
}

索引更新如何影響 Cursor

與基本分頁(yè)不同,Cursor 分頁(yè)不依賴(lài)于在完成的匹配文檔的排序列表中使用絕對(duì)“偏移量”。相反,請(qǐng)求中指定的 cursorMark 將根據(jù)該文檔的絕對(duì)排序值封裝返回的上一個(gè)文檔的相對(duì)位置信息。這意味著,與基本分頁(yè)相比,使用 Cursor 時(shí),索引修改的影響要小得多??紤]在討論基本分頁(yè)時(shí)所描述的相同示例索引:

ID 名稱(chēng)

1

A

2

B

...

...

26

Z

  • 客戶(hù)端請(qǐng)求:
    q=:&rows=5&start=0&sort=name asc, id asc&cursorMark=*

    帶有 1-5 的 ID 的文檔將返回給客戶(hù)端

  • ID 為 3 的文檔被刪除
  • 客戶(hù)端使用前一個(gè)響應(yīng)中的 nextCursorMark 請(qǐng)求5個(gè)以上的文檔
    文檔6-10將被返回 - 刪除已經(jīng)返回的文檔不會(huì)影響 Cursor 的相對(duì)位置
  • 現(xiàn)在添加了ID為90,91以及92的3頁(yè)新的文檔;這三個(gè)文檔都有一個(gè)名稱(chēng)。
  • 客戶(hù)端使用前一個(gè)響應(yīng)中的 nextCursorMark 請(qǐng)求5個(gè)以上的文檔
    文檔 11-15 將被返回 - 添加已通過(guò)排序值的新文檔不會(huì)影響 Cursor 的相對(duì)位置
  • ID 為1的文檔更新為將其 “name” 更改為 Q 
  • ID 為17的文檔更新為將其 “name” 更改為 A
  • 客戶(hù)端使用前一個(gè)響應(yīng)中的 nextCursorMark 請(qǐng)求5個(gè)以上的文檔
    生成的文檔以 16、1、18、19、20的順序排列;
    由于文檔1的排序值已更改, 使其位于 Cursor 位置之后, 因此文檔將兩次返回給客戶(hù)端;
    由于文檔17的排序值已經(jīng)改變,所以在 Cursor 位置之前,文檔已被“跳過(guò)”,并且不會(huì)因?yàn)?nbsp;Cursor 繼續(xù)進(jìn)行而返回給客戶(hù)端

簡(jiǎn)而言之:當(dāng)獲取與使用 cursorMark 匹配的查詢(xún)的所有結(jié)果時(shí),索引修改的唯一方式可能導(dǎo)致被跳過(guò)的文檔或返回兩次,如果文檔的排序值發(fā)生更改。

確保文檔永遠(yuǎn)不會(huì)被返回的一種方法是將 uniqueKey 字段用作主要(因此是唯一有效的)排序標(biāo)準(zhǔn)。

在這種情況下,您將保證每個(gè)文檔只返回一次,無(wú)論它如何在使用 Cursor 時(shí)被修改。

“拖放” Cursor

由于 Cursor 請(qǐng)求是無(wú)狀態(tài)的,并且 cursorMark 值封裝了從搜索返回的上一個(gè)文檔的絕對(duì)排序值,所以可以“繼續(xù)”從已經(jīng)達(dá)到其結(jié)尾的 Cursor 獲取附加結(jié)果。如果添加新文檔(或更新現(xiàn)有文檔)到結(jié)果的末尾。

您可以把它看作類(lèi)似于在 Unix 中使用 “tail -f” 的東西。如何在索引中添加/更新文檔時(shí),如果有 "時(shí)間戳" 字段記錄,則最常見(jiàn)的示例是如何使用此方法。客戶(hù)端應(yīng)用程序可以使用匹配查詢(xún)的文檔的 sort=timestamp asc, id asc 連續(xù)輪詢(xún) Cursor,并且在添加或更新符合請(qǐng)求條件的文檔時(shí)總是會(huì)收到通知。

另一個(gè)常見(jiàn)的例子是,當(dāng)您創(chuàng)建新文檔時(shí) uniqueKey 值始終增加,并且您可以使用 sort=id asc 連續(xù)輪詢(xún)游標(biāo)以獲得有關(guān)新文檔的通知。

拖放 Cursor 的偽代碼只是我們?cè)缙谔幚砼c查詢(xún)匹配的所有文檔的一個(gè)小修改:

while (true) {
  $doneForNow = false
  while (not $doneForNow) {
    $results = fetch_solr($params)
    // do something with $results
    if ($params[cursorMark] == $results[nextCursorMark]) {
      $doneForNow = true
    }
    $params[cursorMark] = $results[nextCursorMark]
  }
  sleep($some_configured_delay)
}

對(duì)于某些特殊情況,/ export 處理程序可以是一個(gè)選擇。

以上內(nèi)容是否對(duì)您有幫助:
在線(xiàn)筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)