offset vs cursor ページネーション 

# はじめに
多くのダイナミックな[データを集約して視覚化するサービス](https://srest.io)に携わっており、検索機能を作成することで興味深い選択問題を出てました。

新しいデータが大量に追加されると、同じ場所(ページ)のデータをリクエストしても異なる結果が返ってくることが判明した。データの分析や比較がより複雑になっちゃう。

通常、誰もがオフセット・ページネーションに慣れているが、常に変化するデータ（挿入、削除）に対してより効果的な方法もある。カーソルのページネーションを使った方がいいかもしれない。両方について説明しよう。

# オフセットページネーション: 
この方法では、結果セットの先頭からスキップするアイテムの数（オフセット）と、各ページで返すアイテムの数を指定します。たとえば、100個のアイテムの結果セットがある場合、1ページ目を取得するためにはオフセットを0、サイズを10に設定します。次に、2ページ目を取得するためにはオフセットを10、サイズを10に設定し、以降も同様です。オフセットページネーションはシンプルですが、大きなオフセット値では効率が低下し、動的なデータセットでは問題が生じることがあります。

# カーソルページネーション: 
この方法では、一意の識別子（カーソル）を使用して次のページの結果を取得します。通常、カーソルは結果セット内の特定のアイテムを指します。次のページをリクエストする際には、前のページで取得した最後のアイテムに対応するカーソルを指定します。この方法は大きなデータセットに対して効率的であり、オフセットを使用しないため、アイテムの挿入や削除がページネーションのパフォーマンスに影響を与えません。

# 可視化
説明を理解するのが難しいかもしれないので、簡単な視覚化を作成しました。

![Screen Recording 2024-04-24 at 17.06.36.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2989047/4b365ea4-abe1-5220-fa3e-bf918ac1774e.gif)

オフセット（左側）の場合、データが追加されるたびに（新しいリクエストが行われるたびに）ドキュメントのインデックスが変化することがわかります。
カーソルを使うことで、変化するデータセットをよりスムーズにナビゲートできる。

<details>
    <summary>可視化を作成するために使用したコード</summary>

``` html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ページネーション視覚化: Offset vs Cursor</title>
<style>
  .document { width: 15px; height: 10px; background-color: blue; margin: 1px; float: left; color: white; text-align: center; font-size: 8px; }
  .container { width: 220px; height: 660px; overflow: hidden; float: left; margin-right: 10px; }
</style>
</head>
<body>
<div class="container" id="offsetContainer"></div>
<div class="container" id="cursorContainer"></div>
<button onclick="next()">+1</button>
<button onclick="previous()">-1</button>
<script>
const documentsOffset = [];
const documentsCursor = [];
const offsetContainer = document.getElementById('offsetContainer');
const cursorContainer = document.getElementById('cursorContainer');
let intervalIdOffset = null;
let intervalIdCursor = null;
const cursorInitialIndices = [];
let counter = 0;
let page = 0;

const next = () => {
  counter += 10;
  page += 10;
};

const previous = () => {
  counter -= 10;
  page -= 10;
  if (page < 0) {
    page = 0;
  }
  if (counter < 0) {
    counter = 0;
  }
};

const createDocuments = () => {
  for (let i = 0; i < 100; i++) {
    addDocument(i + 1, true);  // offset
    addDocument(i + 1, false); // cursor
  }

  for (let i = 0; i < 10; i++) {
    cursorInitialIndices.push(i);
  }
};

const addDocument = (index, isOffset) => {
  const doc = document.createElement('div');
  doc.className = 'document';
  doc.textContent = index;
  if (isOffset) {
    offsetContainer.insertBefore(doc, offsetContainer.firstChild);
    documentsOffset.unshift(doc);
  } else {
    cursorContainer.insertBefore(doc, cursorContainer.firstChild);
    documentsCursor.unshift(doc);
  }
};

const startOffsetPagination = () => {
  const step = 10;

  intervalIdOffset = setInterval(() => {
    clearHighlight(documentsOffset);
    for (let i = 0; i < step && i < documentsOffset.length; i++) {
      documentsOffset[i + page].style.backgroundColor = 'red';
    }
  }, 100);
};

const startCursorPagination = () => {
  const step = 10;

  intervalIdCursor = setInterval(() => {
    clearHighlight(documentsCursor);
    cursorInitialIndices.forEach(index => {
      if (index + counter < documentsCursor.length) {
        documentsCursor[index + counter].style.backgroundColor = 'green';
      }
    });
  }, 100);
};

const clearHighlight = (docs) => {
  docs.forEach(doc => {
    doc.style.backgroundColor = 'blue';
  });
};

createDocuments();
startOffsetPagination();
startCursorPagination();

// 新しいデータの追加を開始し、ライブ環境をシミュレートする
setInterval(() => {
  counter += 1;
  addDocument(documentsOffset.length + 1, true); // offset追加
  addDocument(documentsCursor.length + 1, false); // cursor追加
}, 2000);

</script>
</body>
</html>

```

</details>


# まとめて
要約すると、両方の方法はページネーションを実現しますが、カーソルページネーションは大きなデータセットや動的なデータセットに対して効率的でスケーラブルなため、より好まれます。一方、オフセットページネーションはシンプルですが、大きくて複雑データセットでは効率が悪くなる場合があります。