الطوابع الزمنية المقسّمة إلى أجزاء

إذا كانت المجموعة تحتوي على مستندات تحتوي على قيم مفهرَسة تسلسلية، يقتصر معدل الكتابة في Cloud Firestore على 500 عملية كتابة في الثانية. توضّح هذه الصفحة كيفية تقسيم حقل مستند للتغلب على هذا الحدّ. أولاً، لنحدد ما نعنيه بـ "الحقول المفهرَسة التسلسلية" ونوضّح الحالات التي ينطبق فيها هذا الحدّ.

الحقول المفهرَسة تسلسليًا

"الحقول المفهرَسة التسلسلية" تعني أي مجموعة من المستندات تحتوي على حقل مفهرَس يزداد أو ينخفض بشكلٍ منتظم. في العديد من الحالات، يعني ذلك حقل timestamp، ولكن يمكن لأي قيمة حقل متزايدة أو متناقصة بشكل أحادي سمته أن تؤدي إلى تجاوز الحد الأقصى لعمليات الكتابة الذي يبلغ 500 عملية كتابة في الثانية.

على سبيل المثال، ينطبق الحدّ الأقصى على مجموعة من user مستندًا يتضمّن حقلًا مفهرَسًا userid إذا كان التطبيق يحدّد قيم userid على النحو التالي:

  • 1281, 1282, 1283, 1284, 1285, ...

من ناحية أخرى، لا تؤدي جميع حقول timestamp إلى تطبيق هذا الحدّ. إذا كان حقل timestamp يتتبّع قيمًا موزّعة عشوائيًا، لن يتم تطبيق حدّ الكتابة. ولا يهمّ أيضًا القيمة الفعلية للحقل، ما يهمّ فقط هو أنّ الحقل يزداد أو ينخفض بشكلٍ أحادي. على سبيل المثال، يؤدي كلا مجموعتَي قيم الحقول المتزايدة بشكلٍ أحادي إلى بدء الحدّ الأقصى للكتابة:

  • 100000, 100001, 100002, 100003, ...
  • 0, 1, 2, 3, ...

تقسيم حقل طابع زمني

لنفترض أنّ تطبيقك يستخدم حقل timestamp متزايدًا بشكلٍ أحادي. إذا كان تطبيقك لا يستخدم الحقل timestamp في أي طلبات بحث، يمكنك إزالة الحدّ الأقصى المسموح به وهو 500 عملية كتابة في الثانية من خلال عدم فهرسة حقل الطابع الزمني. إذا كنت تحتاج إلى استخدام حقل timestamp في طلبات البحث، يمكنك تجاوز الحدّ الأقصى من خلال استخدام طوابع زمنية مجزّأة:

  1. أضِف حقل shard بجانب حقل timestamp. استخدِم 1..n قيمة مختلفة للحقل shard. يؤدي ذلك إلى رفع الحدّ الأقصى للكتابة للمجموعة إلى 500*n، ولكن عليك تجميع n طلب بحث.
  2. عدِّل منطق الكتابة لتحديد قيمة shard بشكل عشوائي لكل مستند.
  3. عدِّل طلبات البحث لتجميع مجموعات النتائج المجزّأة.
  4. أوقِف الفهارس التي تتضمّن حقلًا واحدًا لكلٍّ من الحقل shard والحقل timestamp. احذف الفهارس المركبة الحالية التي تحتوي على الحقل timestamp.
  5. أنشئ فهارس مركبة جديدة لتتوافق مع طلبات البحث المعدّلة. إنّ ترتيب الحقول في الفهرس مهم، ويجب أن يأتي الحقل shard قبل الحقل timestamp. يجب أن تتضمّن أيّ فهارس تتضمّن الحقل timestamp الحقل shard أيضًا.

يجب تنفيذ الطوابع الزمنية المجزّأة فقط في حالات الاستخدام التي تتضمن معدلات كتابة مستمرة تزيد عن 500 عملية كتابة في الثانية. وإلا، هذا تحسين مبكر. يؤدي تقسيم حقل timestamp إلى إزالة القيود المفروضة على عمليات الإضافة التي تبلغ 500 عملية في الثانية، ولكن مع التأثير السلبي المتمثل في الحاجة إلى تجميعات طلبات البحث من جهة العميل.

توضِّح الأمثلة التالية كيفية تقسيم حقل timestamp وكيفية إجراء طلب بحث في مجموعة نتائج مقسّمة.

مثال على نموذج البيانات وطلبات البحث

على سبيل المثال، تخيل تطبيقًا لتحليلinstruments المالية في الوقت الفعلي تقريبًا، مثل العملات والأسهم العادية وصناديق المؤشرات المتداولة. يكتب هذا التطبيق المستندات في مجموعة instruments على النحو التالي:

Node.js
async function insertData() {
  const instruments = [
    {
      symbol: 'AAA',
      price: {
        currency: 'USD',
        micros: 34790000
      },
      exchange: 'EXCHG1',
      instrumentType: 'commonstock',
      timestamp: Timestamp.fromMillis(
          Date.parse('2019-01-01T13:45:23.010Z'))
    },
    {
      symbol: 'BBB',
      price: {
        currency: 'JPY',
        micros: 64272000000
      },
      exchange: 'EXCHG2',
      instrumentType: 'commonstock',
      timestamp: Timestamp.fromMillis(
          Date.parse('2019-01-01T13:45:23.101Z'))
    },
    {
      symbol: 'Index1 ETF',
      price: {
        currency: 'USD',
        micros: 473000000
      },
      exchange: 'EXCHG1',
      instrumentType: 'etf',
      timestamp: Timestamp.fromMillis(
          Date.parse('2019-01-01T13:45:23.001Z'))
    }
  ];

  const batch = fs.batch();
  for (const inst of instruments) {
    const ref = fs.collection('instruments').doc();
    batch.set(ref, inst);
  }

  await batch.commit();
}

يُجري هذا التطبيق طلبات البحث والطلبات التالية حسب الحقل timestamp:

Node.js
function createQuery(fieldName, fieldOperator, fieldValue, limit = 5) {
  return fs.collection('instruments')
      .where(fieldName, fieldOperator, fieldValue)
      .orderBy('timestamp', 'desc')
      .limit(limit)
      .get();
}

function queryCommonStock() {
  return createQuery('instrumentType', '==', 'commonstock');
}

function queryExchange1Instruments() {
  return createQuery('exchange', '==', 'EXCHG1');
}

function queryUSDInstruments() {
  return createQuery('price.currency', '==', 'USD');
}
insertData()
    .then(() => {
      const commonStock = queryCommonStock()
          .then(
              (docs) => {
                console.log('--- queryCommonStock: ');
                docs.forEach((doc) => {
                  console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`);
                });
              }
          );
      const exchange1Instruments = queryExchange1Instruments()
          .then(
              (docs) => {
                console.log('--- queryExchange1Instruments: ');
                docs.forEach((doc) => {
                  console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`);
                });
              }
          );
      const usdInstruments = queryUSDInstruments()
          .then(
              (docs) => {
                console.log('--- queryUSDInstruments: ');
                docs.forEach((doc) => {
                  console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`);
                });
              }
          );
      return Promise.all([commonStock, exchange1Instruments, usdInstruments]);
    });

بعد إجراء بعض الأبحاث، تقرّر أنّ التطبيق سيتلقّى ما بين 1,000 و1,500 تعديل على بيانات الأجهزة في الثانية. ويتجاوز ذلك عدد عمليات الكتابة المسموح به في الثانية، والذي يبلغ 500 عملية للمجموعات التي تحتوي على مستندات تتضمّن حقول طابع زمني مفهرَسة. لزيادة معدل نقل البيانات للكتابة، تحتاج إلى 3 قيم للشريحة، MAX_INSTRUMENT_UPDATES/500 = 3. يستخدم هذا المثال قيم الشريحة x y وz. يمكنك أيضًا استخدام أرقام أو أحرف أخرى لقيم الشريحة.

إضافة حقل شريحة

أضِف حقل shard إلى مستنداتك. اضبط الحقل shard على القيم x أو y أو z التي ترفع الحد الأقصى لعمليات الكتابة في المجموعة إلى 1,500 عملية كتابة في الثانية.

Node.js
// Define our 'K' shard values
const shards = ['x', 'y', 'z'];
// Define a function to help 'chunk' our shards for use in queries.
// When using the 'in' query filter there is a max number of values that can be
// included in the value. If our number of shards is higher than that limit
// break down the shards into the fewest possible number of chunks.
function shardChunks() {
  const chunks = [];
  let start = 0;
  while (start < shards.length) {
    const elements = Math.min(MAX_IN_VALUES, shards.length - start);
    const end = start + elements;
    chunks.push(shards.slice(start, end));
    start = end;
  }
  return chunks;
}

// Add a convenience function to select a random shard
function randomShard() {
  return shards[Math.floor(Math.random() * Math.floor(shards.length))];
}
async function insertData() {
  const instruments = [
    {
      shard: randomShard(),  // add the new shard field to the document
      symbol: 'AAA',
      price: {
        currency: 'USD',
        micros: 34790000
      },
      exchange: 'EXCHG1',
      instrumentType: 'commonstock',
      timestamp: Timestamp.fromMillis(
          Date.parse('2019-01-01T13:45:23.010Z'))
    },
    {
      shard: randomShard(),  // add the new shard field to the document
      symbol: 'BBB',
      price: {
        currency: 'JPY',
        micros: 64272000000
      },
      exchange: 'EXCHG2',
      instrumentType: 'commonstock',
      timestamp: Timestamp.fromMillis(
          Date.parse('2019-01-01T13:45:23.101Z'))
    },
    {
      shard: randomShard(),  // add the new shard field to the document
      symbol: 'Index1 ETF',
      price: {
        currency: 'USD',
        micros: 473000000
      },
      exchange: 'EXCHG1',
      instrumentType: 'etf',
      timestamp: Timestamp.fromMillis(
          Date.parse('2019-01-01T13:45:23.001Z'))
    }
  ];

  const batch = fs.batch();
  for (const inst of instruments) {
    const ref = fs.collection('instruments').doc();
    batch.set(ref, inst);
  }

  await batch.commit();
}

طلب البحث عن الطابع الزمني المجزّأ

تتطلّب إضافة حقل shard تعديل طلبات البحث لتجميع النتائج المجزّأة:

Node.js
function createQuery(fieldName, fieldOperator, fieldValue, limit = 5) {
  // For each shard value, map it to a new query which adds an additional
  // where clause specifying the shard value.
  return Promise.all(shardChunks().map(shardChunk => {
        return fs.collection('instruments')
            .where('shard', 'in', shardChunk)  // new shard condition
            .where(fieldName, fieldOperator, fieldValue)
            .orderBy('timestamp', 'desc')
            .limit(limit)
            .get();
      }))
      // Now that we have a promise of multiple possible query results, we need
      // to merge the results from all of the queries into a single result set.
      .then((snapshots) => {
        // Create a new container for 'all' results
        const docs = [];
        snapshots.forEach((querySnapshot) => {
          querySnapshot.forEach((doc) => {
            // append each document to the new all container
            docs.push(doc);
          });
        });
        if (snapshots.length === 1) {
          // if only a single query was returned skip manual sorting as it is
          // taken care of by the backend.
          return docs;
        } else {
          // When multiple query results are returned we need to sort the
          // results after they have been concatenated.
          // 
          // since we're wanting the `limit` newest values, sort the array
          // descending and take the first `limit` values. By returning negated
          // values we can easily get a descending value.
          docs.sort((a, b) => {
            const aT = a.data().timestamp;
            const bT = b.data().timestamp;
            const secondsDiff = aT.seconds - bT.seconds;
            if (secondsDiff === 0) {
              return -(aT.nanoseconds - bT.nanoseconds);
            } else {
              return -secondsDiff;
            }
          });
          return docs.slice(0, limit);
        }
      });
}

function queryCommonStock() {
  return createQuery('instrumentType', '==', 'commonstock');
}

function queryExchange1Instruments() {
  return createQuery('exchange', '==', 'EXCHG1');
}

function queryUSDInstruments() {
  return createQuery('price.currency', '==', 'USD');
}
insertData()
    .then(() => {
      const commonStock = queryCommonStock()
          .then(
              (docs) => {
                console.log('--- queryCommonStock: ');
                docs.forEach((doc) => {
                  console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`);
                });
              }
          );
      const exchange1Instruments = queryExchange1Instruments()
          .then(
              (docs) => {
                console.log('--- queryExchange1Instruments: ');
                docs.forEach((doc) => {
                  console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`);
                });
              }
          );
      const usdInstruments = queryUSDInstruments()
          .then(
              (docs) => {
                console.log('--- queryUSDInstruments: ');
                docs.forEach((doc) => {
                  console.log(`doc = ${util.inspect(doc.data(), {depth: 4})}`);
                });
              }
          );
      return Promise.all([commonStock, exchange1Instruments, usdInstruments]);
    });

تعديل تعريفات الفهرس

لإزالة قيد 500 عملية كتابة في الثانية، احذف الفهرس المفرد الحالي والفهرس المركب الذي يستخدم الحقل timestamp.

حذف تعريفات الفهرس المركب

وحدة تحكُّم Firebase

  1. افتح صفحة Cloud Firestore الفهارس المركبة في وحدة تحكّم Firebase.

    الانتقال إلى "الفهرس المركب"

  2. لكل فهرس يحتوي على الحقل timestamp، انقر على الزر ثمّ انقر على حذف.

وحدة تحكّم Google Cloud Platform

  1. في وحدة تحكّم Google Cloud، انتقِل إلى صفحة قواعد البيانات.

    الانتقال إلى قواعد البيانات

  2. اختَر قاعدة البيانات المطلوبة من قائمة قواعد البيانات.

  3. في قائمة التنقّل، انقر على الفهارس، ثمّ انقر على علامة التبويب مركب.

  4. استخدِم حقل الفلترة للبحث عن تعريفات الفهرس التي تحتوي على الحقل timestamp.

  5. بالنسبة إلى كل فهرس من هذه الفهارس، انقر على الزر ثم على حذف.

Firebase CLI

  1. إذا لم يسبق لك إعداد Firebase CLI، اتّبِع هذه التعليمات لتثبيت CLI وتنفيذ الأمر firebase init. أثناء تنفيذ الأمر init، احرص على اختيار Firestore: Deploy rules and create indexes for Firestore.
  2. أثناء الإعداد، تنزِّل أداة Firebase CLI تعريفات الفهرس الحالية إلىملف يُسمى تلقائيًا firestore.indexes.json.
  3. أزِل أيّ تعريفات فهرس تحتوي على الحقل timestamp، على سبيل المثال:

    {
    "indexes": [
      // Delete composite index definition that contain the timestamp field
      {
        "collectionGroup": "instruments",
        "queryScope": "COLLECTION",
        "fields": [
          {
            "fieldPath": "exchange",
            "order": "ASCENDING"
          },
          {
            "fieldPath": "timestamp",
            "order": "DESCENDING"
          }
        ]
      },
      {
        "collectionGroup": "instruments",
        "queryScope": "COLLECTION",
        "fields": [
          {
            "fieldPath": "instrumentType",
            "order": "ASCENDING"
          },
          {
            "fieldPath": "timestamp",
            "order": "DESCENDING"
          }
        ]
      },
      {
        "collectionGroup": "instruments",
        "queryScope": "COLLECTION",
        "fields": [
          {
            "fieldPath": "price.currency",
            "order": "ASCENDING"
          },
          {
            "fieldPath": "timestamp",
            "order": "DESCENDING"
          }
        ]
      },
     ]
    }
    
  4. يمكنك نشر تعريفات الفهرس المعدَّلة باتّباع الخطوات التالية:

    firebase deploy --only firestore:indexes
    

تعديل تعريفات فهرس الحقل الواحد

وحدة تحكُّم Firebase

  1. افتح صفحة Cloud Firestore فهارس الحقول الفردية في وحدة تحكُّم Firebase.

    الانتقال إلى "فهرسات الحقول الفردية"

  2. انقر على إضافة إعفاء.

  3. بالنسبة إلى معرّف المجموعة، أدخِل instruments. بالنسبة إلى مسار الحقل، أدخِل timestamp.

  4. ضمن نطاق طلب البحث، اختَر كلّ من المجموعة و مجموعة المجموعات.

  5. انقر على التالي.

  6. فعِّل جميع إعدادات الفهرس على إيقاف. انقر على حفظ.

  7. كرِّر الخطوات نفسها للحقل shard.

وحدة تحكّم Google Cloud Platform

  1. في وحدة تحكّم Google Cloud، انتقِل إلى صفحة قواعد البيانات.

    الانتقال إلى قواعد البيانات

  2. اختَر قاعدة البيانات المطلوبة من قائمة قواعد البيانات.

  3. في قائمة التنقّل، انقر على الفهارس، ثم على علامة التبويب حقل واحد.

  4. انقر على علامة التبويب حقل واحد.

  5. انقر على إضافة إعفاء.

  6. بالنسبة إلى معرّف المجموعة، أدخِل instruments. بالنسبة إلى مسار الحقل، أدخِل timestamp.

  7. ضمن نطاق طلب البحث، اختَر كلّ من المجموعة و مجموعة المجموعات.

  8. انقر على التالي.

  9. فعِّل جميع إعدادات الفهرس على إيقاف. انقر على حفظ.

  10. كرِّر الخطوات نفسها للحقل shard.

Firebase CLI

  1. أضِف ما يلي إلى قسم fieldOverrides في ملف تعريفات الفهرس:

    {
     "fieldOverrides": [
       // Disable single-field indexing for the timestamp field
       {
         "collectionGroup": "instruments",
         "fieldPath": "timestamp",
         "indexes": []
       },
     ]
    }
    
  2. يمكنك نشر تعريفات الفهرس المعدَّلة باتّباع الخطوات التالية:

    firebase deploy --only firestore:indexes
    

إنشاء مؤشرات مركبة جديدة

بعد إزالة جميع الفهارس السابقة التي تحتوي على timestamp، حدِّد الفهارس الجديدة التي يتطلبها تطبيقك. يجب أن يتضمّن أي فهرس يحتوي على الحقل timestamp الحقل shard أيضًا. على سبيل المثال، لتلبية متطلبات طلبات البحث أعلاه، أضِف الفهارس التالية:

التجميع الحقول المفهرَسة نطاق طلب البحث
آلات موسيقية shard، price.currency، الطابع الزمني التجميع
آلات موسيقية شريحة، عملية تبادل، طابع زمني التجميع
آلات موسيقية shard, instrumentType, timestamp التجميع

رسائل الخطأ

يمكنك إنشاء هذه الفهارس من خلال تنفيذ طلبات البحث المعدّلة.

يعرض كلّ طلب بحث رسالة خطأ تتضمّن رابطًا لإنشاء ملف الفهرس المطلوب في "وحدة تحكّم Firebase".

Firebase CLI

  1. أضِف الفهارس التالية إلى ملف تعريف الفهرس:

     {
       "indexes": [
       // New indexes for sharded timestamps
         {
           "collectionGroup": "instruments",
           "queryScope": "COLLECTION",
           "fields": [
             {
               "fieldPath": "shard",
               "order": "DESCENDING"
             },
             {
               "fieldPath": "exchange",
               "order": "ASCENDING"
             },
             {
               "fieldPath": "timestamp",
               "order": "DESCENDING"
             }
           ]
         },
         {
           "collectionGroup": "instruments",
           "queryScope": "COLLECTION",
           "fields": [
             {
               "fieldPath": "shard",
               "order": "DESCENDING"
             },
             {
               "fieldPath": "instrumentType",
               "order": "ASCENDING"
             },
             {
               "fieldPath": "timestamp",
               "order": "DESCENDING"
             }
           ]
         },
         {
           "collectionGroup": "instruments",
           "queryScope": "COLLECTION",
           "fields": [
             {
               "fieldPath": "shard",
               "order": "DESCENDING"
             },
             {
               "fieldPath": "price.currency",
               "order": "ASCENDING"
             },
             {
               "fieldPath": "timestamp",
               "order": "DESCENDING"
             }
           ]
         },
       ]
     }
    
  2. يمكنك نشر تعريفات الفهرس المعدَّلة باتّباع الخطوات التالية:

    firebase deploy --only firestore:indexes
    

فهم الحد الأقصى لعمليات الكتابة في الحقول المفهرَسة التسلسلية

يرجع الحدّ الأقصى لمعدل الكتابة للحقول المفهرَسة التسلسلية إلى كيفية تخزين Cloud Firestore لقيم الفهرس وتوسيع نطاق عمليات الكتابة في الفهرس. لكل عملية تسجيل في Cloud Firestore، يحدِّد Cloud Firestore إدخال مفتاح/قيمة يربط اسم المستند بقيمة كل حقل مفهرَس. Cloud Firestore تنظِّم إدخالات الفهرس هذه في مجموعات من البيانات تُعرف باسم اللوحات. يستضيف كل خادم Cloud Firestore جهازًا لوحيًا واحدًا أو أكثر. عندما يصبح عدد عمليات الكتابة إلى لوحة بيانات معيّنة مرتفعًا جدًا، يتم توسيع نطاق Cloud Firestore أفقياً من خلال تقسيم اللوحة إلى لوحات بيانات أصغر ونشر اللوحات الجديدة على خوادم Cloud Firestore مختلفة.

Cloud Firestore تضع إدخالات الفهرس القريبة من حيث الترتيب الأبجدي على الجهاز اللوحي نفسه. إذا كانت قيم الفهرس في لوحة قريبة جدًا من بعضها، مثل حقول الطابع الزمني، لا يمكن Cloud Firestore تقسيم اللوحة بفعالية إلى لوحات أصغر. يؤدي ذلك إلى إنشاء نقطة اتصال يتلقّى فيها جهاز لوحي واحد عددًا كبيرًا جدًا من الزيارات، وتصبح عمليات القراءة والكتابة في نقطة الاتصال هذه أبطأ.

من خلال تقسيم حقل الطابع الزمني، يمكنك السماح لـ Cloud Firestore بتقسيم أعباء العمل بكفاءة على عدة أجهزة لوحية. على الرغم من أنّ قيم حقل الطابع الزمني قد تظل قريبة من بعضها، توفّر قيمة الشريحة والمؤشر المتسلسلة Cloud Firestore مساحة كافية بين إدخالات الفهرس لتقسيم الإدخالات بين أقسام متعددة.

الخطوات التالية