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

إذا كانت المجموعة تحتوي على مستندات ذات قيم مفهرَسة متسلسلة، يحدّ 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 على النحو التالي:

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. في وحدة تحكُّم Firebase، انتقِل إلى علامة التبويب قواعد البيانات ومساحة التخزين > Firestore > الفهارس المركّبة.

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

  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. في وحدة تحكُّم Firebase، انتقِل إلى قواعد البيانات ومساحة التخزين > Firestore > علامة التبويب فهارس الحقول الفردية.

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

  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 أيضًا. على سبيل المثال، لدعم طلبات البحث أعلاه، أضِف الفهارس التالية:

التجميع الحقول التي تمت فهرستها نطاق طلب البحث
آلات موسيقية ‫ جزء، و price.currency، وطابع زمني واحد () التجميع
آلات موسيقية جزء واحد () وعملية تبادل واحدة () وطابع زمني واحد () التجميع
آلات موسيقية ‫ جزء، instrumentType، طابع زمني واحد () التجميع

رسائل الخطأ

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

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

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 مساحة كافية بين إدخالات الفهرس لتقسيم الإدخالات بين أجهزة لوحية متعددة.

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