Рубрики
Без рубрики

Битовая упаковка или как любить и или или хор

В этой статье мы собираемся исследовать использование «битной упаковки». Это позволяет нам сжать несколько значений в «целочисленное или« двоичное поле »в« MongoDB », потенциально позволяя нам сэкономить на размере документов в хранении и передаче через провод. Он имеет некоторые реальные преимущества, но поставляются с компромиссами, что вы должны быть четко вставлены до его применения.

Автор оригинала: Christian Amor Kvalheim.

Резюме

В этой статье мы собираемся исследовать использование битовая упаковка Отказ Это позволяет нам сжать несколько значений в Целое число или бинарный поле в Монгодб Потенциально позволяя нам сохранять размер документа в хранении и передаче через провод. Он имеет некоторые реальные преимущества, но поставляются с компромиссами, что вы должны быть четко вставлены до его применения.

* Невозможно использовать индексы для запросов BIT Упакоемого поля ввода. * Уменьшить пространство, потребляемое документом.
* Требуются комплексные операторы обновления. * Может эффективно упаковать несколько значений и флаги в одно поле.
* Требует битовых потрясающих знаний. * Меньшие документы означают меньше данных, передаваемых через проволоку.

В общем, вы должны подумать только битовая упаковка Когда небольшой размер документа требуется атрибут, поскольку он вводит дополнительную сложность вашего приложения. Однако, если вам это нужно, это может быть бесценным инструментом.

Обзор

Давайте рассмотрим нашу разведочную упаковку битов с помощью гипотетической модели данных, которые моделируют файловую систему Unix, включая чтение, писать, выполнять биты для пользователя, групп и других. Давайте рассмотрим следующую модель.

{
  name: "peter",
  isDirectory: true,
  path: "/home/user/peter",
  parentPath: "/home/user",
  group: 0,
  user: 10,
  permissions: {
    group: {
      read: true,
      write: false,
      execute: false
    },
    user: {
      read: true,
      write: true,
      execute: true
    },
    all: {
      read: true,
      write: false,
      execute: false
    }
  }
}

В этой модели каждый документ представляет собой каталог или файл. Каждый из записей принадлежит Пользователь и а Группа Отказ В нашей системе мы можем иметь 256 Разные группы, представленные 8 бит и 16384 Отличные пользователи, представленные 14 бит Отказ

Теперь, если мы сможем представлять все эти поля и их значение одним 32-битным целым числом. Мы бы сохранили честный кусочек пространства. Вместо того, чтобы сохранить все пространство для полей Isdirectory , Группа , Пользователь , Разрешения , Разрешения. Группа , Permissions.group.read/write/execute , Разрешения.user , permissions.user.read/write/execute , Разрешения. Все и permissions.all.read/write/execute Мы могли бы создать одно поле, которое содержало все значения, упакованные в одно целое значение 32 бита, называемых Метаданные Отказ

Для Метаданные Поле мы определим каждую группу битов, чтобы содержать сжатую информацию.

1                                     // [0:0]    isDirectory
 11111111                             // [1:9]    Group id
         11111111111111               // [10:23]  User id
                       111            // [24:26]  Group r/w/x
                          111         // [27:29]  User r/w/x
                             111      // [29:31]  All r/w/x
  • Биты [0: 0] определяют Isdirectory значение.
  • Биты [1: 9] кодируют 8 биты Групповой идентификатор Отказ
  • Биты [10:23] кодируют 14 биты Идентификатор пользователя Отказ
  • Биты [24:26] кодируют Группа Прочитайте/Написать/Выполнить флаги Отказ
  • Биты [27:29] кодируют Пользователь чтение/запись/выполнение флагов Отказ
  • Биты [29:31] кодируют Все флаги чтения/записи/выполнения Отказ

Полученный Метаданные поле будет 32 Битовое значение, которое будет представлять флаги.

{
  name: "peter",
  path: "/home/user/peter",
  parentPath: "/home/user",
  metadata: 0b11111111111111111111111111111111
}

Давайте посмотрим, как изменение влияет на размер получаемого документа MongoDB. Для этого мы собираемся использовать метод в оболочке под названием Объект .ssonsize который позволяет нам пройти объект JS и получить BSON Binary размер в байтах.

Оригинал 244
Бит упакован 93

Как мы видим, битовая упаковка уменьшает размер получаемого документа довольно немного. Тренование, конечно, более сложный код.

Изменение метаданных

Итак, как мы изменим разрешения на чтение или изменение идентификатора пользователя или группы? Для этого нам нужно использовать Обновить оператор $ Bit выполнять и и или Операции на поле метаданных.

Мы собираемся использовать класс обертки под названием Вход Упростить модификацию Метаданные поле.

class Entry {
  constructor(collection, _id, name, path, parentPath, metadata) {
    this.collection = collection;
    this._id = _id;
    this.name = name;
    this.path = path;
    this.parentPath = parentPath;
    this.metadata = metadata;
  }

  setGroupId(id) {
    // Create AND and OR bitmasks and update metadata field
    updateMask(this.collection, this._id,
      NumberInt(
        parseInt(`10000000011111111111111111111111`, 2)),
      NumberInt(
        parseInt(`0${createIntegerMask(id, 8)}00000000000000000000000`, 2))
    );
  }

  setUserId(id) {
    // Create AND and OR bitmasks and update metadata field
    updateMask(this.collection, this._id,
      NumberInt(
        parseInt(`11111111100000000000000111111111`, 2)),
      NumberInt(
        parseInt(`000000000${createIntegerMask(id, 14)}000000000`, 2))
    );
  }

  setGroupBits(read, write, execute) {
    // Get generated AND and OR flags
    var [andBits, orBits] = generateFlagBitMaps(read, write, execute);
    // Create AND and OR bitmasks and update metadata field
    updateMask(this.collection, this._id,
      NumberInt(
        parseInt(`11111111111111111111111${andBits}111111`, 2)),
      NumberInt(
        parseInt(`00000000000000000000000${orBits}000000`, 2))
    );
  }

  setUserBits(read, write, execute) {
    // Get generated AND and OR flags
    var [andBits, orBits] = generateFlagBitMaps(read, write, execute);
    // Create AND and OR bitmasks and update metadata field
    updateMask(this.collection, this._id,
      NumberInt(
        parseInt(`11111111111111111111111111${andBits}111`, 2)),
      NumberInt(
        parseInt(`00000000000000000000000000${orBits}000`, 2))
    );
  }

  setAllBits(read, write, execute) {
    // Get generated AND and OR flags
    var [andBits, orBits] = generateFlagBitMaps(read, write, execute);
    // Create AND and OR bitmasks and update metadata field
    updateMask(this.collection, this._id,
      NumberInt(
        parseInt(`11111111111111111111111111111${andBits}`, 2)),
      NumberInt(
        parseInt(`00000000000000000000000000000${orBits}`, 2))
    );
  }

  save() {
    this.collection.updateOne({
      _id: this._id
    }, {
      $set: {
        name: this.name, 
        path: this.path, 
        parentPath: this.parentPath, 
        metadata: this.metadata  
      }
    }, { upsert: true });
  }
}

Класс содержит несколько методов.

Устанавливает групповую часть поля метаданных. setgroupid (id)
Устанавливает пользовательскую часть поля метаданных. setuserid (id)
Устанавливает биты для чтения, записи и выполнения для групповых разрешений, прохождение undefined или NULL пропустит бит. setgroupbits (прочитать, писать, execute)
Устанавливает биты считывания, записи и выполнения для пользовательских разрешений, прохождение undefined или NULL пропустит бит. setuserbits (прочитать, писать, execute)
Устанавливает биты для чтения, записи и выполнения для всех разрешений, прохождение undefined или null пропустит бит. setallbits (чтение, запись, execute)
Вставляет или обновления документа спасти()

Мы собираемся рассекать два метода для нашего дела. Первый, будучи setuserid (id) метод и второй, будучи setusersbits (прочитать, писать, выполнять) метод.

Давайте начнем с поиска setuserid (id) метод.

setUserId(id) {
  // Create AND and OR bitmasks and update metadata field
  updateMask(this.collection, this._id,
    NumberInt(parseInt(`11111111100000000000000111111111`, 2)),
    NumberInt(parseInt(`000000000${createIntegerMask(id, 14)}0000000000`, 2))
  );
}

Мы называем два общих метода. Первый называется CreateIntEgermask а второй называется UpdateMask Отказ

CreateIntEgermask Метод принимает значение, переданное в и « » биты Разрешение поля.

UpdateMask Метод занимает Коллекция экземпляр, _id Значение целевого документа и две маски. Первый, будучи И маска и второе существо Или маска.

Давайте посмотрим на каждого из них, начиная с createIntegermask (ID, 14) Отказ

function createIntegerMask(id, bitResolution) {
  let bitsString = id.toString(2);
  if (bitsString.length > bitResolution) {
    throw Error(`value id must be between 0 and ${Math.pow(2, bitResolution) - 1}`);
  }

  // Ensure `bitResolution` bit string
  let missingValues = "";
  for(let i = 0; i < (bitResolution - bitsString.length); i++) {
    missingValues = missingValues + "0";
  }

  // Pad the bitString with 0s
  return missingValues + bitsString;  
}

CreateIntEgermask Метод будет генерировать Строка длины BitResolution это бинарный Представление стоимости ID Отказ Чтобы получить возвращенную строку точно BitResolution Длинный этот метод прокладывает переднюю часть битовой строки с 0 s до конца длины конца BitResolution длинный.

Другими словами, говорят, что мы проходим в ID Значение 1 с BitResolution установить 4 Отказ Это будет превращено в следующие Строка Представительство 0001 Отказ

Теперь давайте вернемся к сезонному методу.

setUserId(id) {
  // Create AND and OR bitmasks and update metadata field
  updateMask(this.collection, this._id,
    NumberInt(parseInt(`11111111100000000000000111111111`, 2)),
    NumberInt(parseInt(`000000000${createIntegerMask(id, 14)}0000000000`, 2))
  );
}

Помните битовую упаковку схемы, которую мы определили.

1                                     // [0:0]    isDirectory
 11111111                             // [1:9]    Group id
         11111111111111               // [10:23]  User id
                       111            // [24:26]  Group r/w/x
                          111         // [27:29]  User r/w/x
                             111      // [29:31]  All r/w/x
  • Биты [0: 0] определяют Isdirectory значение.
  • Биты [1: 9] кодируют 8 биты Групповой идентификатор Отказ
  • Биты [10:23] кодируют 14 биты Идентификатор пользователя Отказ
  • Биты [24:26] кодируют Группа Прочитайте/Написать/Выполнить флаги Отказ
  • Биты [27:29] кодируют Пользователь чтение/запись/выполнение флагов Отказ
  • Биты [29:31] кодируют Все флаги чтения/записи/выполнения Отказ

Мы хотим обновить биты в позиции [10:23] с новым UserID передается как ID параметр. Чтобы сделать его более ощутимым, позволяет предположить ID имеет значение 1 Отказ

Мы создаем две маски здесь.

11111111100000000000000111111111 А ТАКЖЕ
000000000 $ {createictegermask (ID, 14)} 0000000000 ИЛИ ЖЕ

И Маска имеет биты на позициях [10:23] установить 0 и Или Маска имеет то же самое [10:23] Биты, установленные в двоичное представление ID значение.

Так что же делает UpdateMask на самом деле сделать. Давайте взглянем.

function updateMask(collection, _id, andMask, orMask) {
  // Update the fields
  collection.updateOne({
    _id: _id
  }, {
    $bit: {
      metadata: {
        and: andMask, 
        or: orMask
      }
    }
  });
}

Не так много, как вы можете видеть. Он в основном обновил документ с _id поле, равное прошло в _id параметр. Однако волшебство лежит в $ бит оператор.

$ бит Оператор позволяет нам применить три побитовый Операции и , или и Хор Отказ Что три операции делают на Метаданные поле? Чтобы понять это, нам нужно понять, что a побитовый и , или или Хор делает.

И операция

Давайте начнем с поиска того, как И Работа работает на одном битовом поле. Это наиболее легко выражено, используя Правда Таблица.

0 0 0
0 1 0
1 0 0
1 1 1

Оба А и B должно быть 1 для И Операция для возврата 1 Отказ Если какая-то из двух сторон – 0 Результатом также 0 Отказ

Мы можем использовать И Операции для очистки битов, предоставляя маску, где мы хотим очищенные биты, установлено на 0 Отказ

Давайте возьмем пример существующего значения и применяя маску, используя И Отказ

10111101 Оригинальное значение
11100111 И маска
10100101 Результирующее значение

В И Маска Мы устанавливаем биты, где мы хотим сохранить оригинальную настройку бита в 1 и те, которые мы хотим очистить до 0 Отказ Если вы посмотрите на стол, вы можете увидеть, что 1 Обеспечить не изменение для соответствующего бита в исходном значении, в то время как 0 всегда будет очистить это.

Или операция

Далее давайте посмотрим на то, как Или побитовые работы работает. Давайте посмотрим на Правда Таблица для Или Отказ

0 0 0
0 1 1
1 0 1
1 1 1

Мы можем видеть из таблицы, что если Маска Битовое значение установлено на 1 Полученное значение бита всегда будет 1 Отказ

Мы можем использовать Или Операция для обеспечения определенных битов установлена на конечное значение.

Давайте возьмем пример существующего значения и применяя маску, используя Или Отказ

10100101 Оригинальное значение
10000 Или маска
10110101 Результирующее значение

В Или Маска Мы устанавливаем биты, где мы хотим сохранить оригинальную настройку бита в 0 и те, которые мы хотим установить 1 Отказ Если вы посмотрите на стол, вы можете увидеть, что 0 Обеспечить не изменение для соответствующего бита в исходном значении, в то время как 1 всегда будет настроить это.

Работа XOR

Наконец-то давайте посмотрим на Хор Операция или Эксклюзивный или Отказ Вы можете думать о Хор как способ обратить ценность одного бита. Давайте посмотрим на Правда Таблица для Хор Отказ

0 0 0
0 1 1
1 0 1
1 1 0

Мы можем видеть из таблицы, что если Маска бит установлен на 1 Он будет переворачивать значение оригинала к этому обратно. То есть как 0 станет 1 и а 1 станет 0 Отказ

Мы можем использовать Хор перевернуть флаг от 0 к 1 или 1 к 0 Отказ

Давайте возьмем пример существующего значения и применяя маску, используя Или Отказ

10110101 Оригинальное значение
11000 Хорская маска
10101101 Результирующее значение

Мы можем видеть, что значение 10 Получает его биты перевернуты и становится 01 Отказ

$ битовая работа в setuser (id)

Давайте посмотрим на $ бит операция мы делаем в Обновить утверждение.

$bit: {
  metadata: {
    and: andMask, 
    or: orMask
  }
}

Что именно это делает? Позволяет использовать полный пример для setuserid (id) Метод, где ID равно 1 Отказ

11111111100000001110000111111111 Оригинальное значение
11111111100000000000000111111111 И маска
11111111100000000000000111111111 Результирующее значение
11111111100000000000001111111111 Или маска
11111111100000000000001111111111 Результирующее значение

Первый И Шаг, устанавливает все биты в UserID к 0 , чтобы очистить 14 бит значение.

Далее Или Маска устанавливает все биты на 1 где его маска имеет немного, установленную на 1 При этом применяя новый UserID к этому пространству в Метаданные поле.

Операция позволяет нам применить набор битов в определенное место в 32 бит Длинное целое число, сохранение таким образом ГРУПИД как 14 бит Значение внутри Метаданные поле на позициях [10:23] из 32 Битовое целочисленное значение.

Setuserbits (чтение, запись, выполнение) функция

setuserbits Функция позволяет нам установить три флаги читать , Написать и Выполнить для Пользователь на Метаданные поле.

setUserBits(read, write, execute) {
  // Get generated AND and OR flags
  var [andBits, orBits] = generateFlagBitMaps(read, write, execute);
  // Create AND and OR bitmasks and update metadata field
  updateMask(this.collection, this._id,
    NumberInt(parseInt(`11111111111111111111111111${andBits}111`, 2)),
    NumberInt(parseInt(`00000000000000000000000000${orBits}000`, 2))
  );
}

Оптимизация производительности

Одна вещь, которая мучительно очевидна, в настоящее время мы должны выполнить обновление за каждый раз, когда мы хотим изменить поле в упаковке Метаданные поле. Что если мы могли бы установить несколько полей значений одновременно?

Давайте изменим Вход Класс, чтобы позволить нам выполнить несколько обновлений поля в одном операции.

class TurboEntry {
  constructor(collection, _id, name, path, parentPath, metadata) {
    this.collection = collection;
    this._id = _id;
    this.name = name;
    this.path = path;
    this.parentPath = parentPath;
    this.metadata = metadata;

    // Contains the masks we are going to apply
    this.updateMasks = {
      and: null,
      or: null
    }
  }

  applyMask(andMask, orMask) {
    if (!this.updateMasks.and) {
      this.updateMasks.and = andMask;
    } else {
      this.updateMasks.and = this.updateMasks.and & andMask;
    }

    if (!this.updateMasks.or) {
      this.updateMasks.or = orMask;
    } else {
      this.updateMasks.or = this.updateMasks.or | orMask;
    }
  }

  setGroupId(id) {
    this.applyMask(
      parseInt(`10000000011111111111111111111111`, 2),
      parseInt(`0${createIntegerMask(id, 8)}00000000000000000000000`, 2)
    )
  }

  setUserId(id) {
    // Create AND and OR bitmasks and update metadata field
    this.applyMask(
      parseInt(`11111111100000000000000111111111`, 2),
      parseInt(`000000000${createIntegerMask(id, 14)}000000000`, 2)
    );
  }

  setGroupBits(read, write, execute) {
    // Get generated AND and OR flags
    var [andBits, orBits] = generateFlagBitMaps(read, write, execute);
    // Create AND and OR bitmasks and update metadata field
    this.applyMask(
      parseInt(`11111111111111111111111${andBits}111111`, 2),
      parseInt(`00000000000000000000000${orBits}000000`, 2)
    );
  }

  setUserBits(read, write, execute) {
    // Get generated AND and OR flags
    var [andBits, orBits] = generateFlagBitMaps(read, write, execute);
    // Create AND and OR bitmasks and update metadata field
    this.applyMask(
      parseInt(`11111111111111111111111111${andBits}111`, 2),
      parseInt(`00000000000000000000000000${orBits}000`, 2)
    );
  }

  setAllBits(read, write, execute) {
    // Get generated AND and OR flags
    var [andBits, orBits] = generateFlagBitMaps(read, write, execute);
    // Create AND and OR bitmasks and update metadata field
    this.applyMask(
      parseInt(`11111111111111111111111111111${andBits}`, 2),
      parseInt(`00000000000000000000000000000${orBits}`, 2)
    );
  }

  updateMetadata() {
    // If no mask applied do nothing
    if (!this.updateMasks.and || !this.updateMasks.or) {
      return;
    }

    // Update the fields
    this.collection.updateOne({
      _id: this._id
    }, {
      $bit: {
        metadata: {
          and: NumberInt(this.updateMasks.and), 
          or: NumberInt(this.updateMasks.or),
        }
      }
    });
  }

  save() {
    this.collection.updateOne({
      _id: this._id
    }, {
      $set: {
        name: this.name, 
        path: this.path, 
        parentPath: this.parentPath, 
        metadata: this.metadata  
      }
    }, { upsert: true });
  }
}

Давайте посмотрим на основные изменения здесь. Сначала мы держим нынешнее объединение и и или Маски в переменной экземпляра UpdateMasks Отказ

// Contains the masks we are going to apply
this.updateMasks = {
  and: null,
  or: null
}

UpdateMasks Переменная содержит и и или Поле входа, изначально устанавливается на NULL. Давайте посмотрим на setallbits Метод нового Вход класс.

setAllBits(read, write, execute) {
  // Get generated AND and OR flags
  var [andBits, orBits] = generateFlagBitMaps(read, write, execute);
  // Create AND and OR bitmasks and update metadata field
  this.applyMask(
    parseInt(`11111111111111111111111111111${andBits}`, 2),
    parseInt(`00000000000000000000000000000${orBits}`, 2)
  );
}

Мы видим, что это выглядит в основном то же самое. Единственным существенным изменением является то, что мы называем ApplyMask Метод, проходящий в битах маски. Давайте посмотрим на ApplyMask метод.

applyMask(andMask, orMask) {
  if (!this.updateMasks.and) {
    this.updateMasks.and = andMask;
  } else {
    this.updateMasks.and = this.updateMasks.and & andMask;
  }

  if (!this.updateMasks.or) {
    this.updateMasks.or = orMask;
  } else {
    this.updateMasks.or = this.updateMasks.or | orMask;
  }
}

Как мы видим ApplyMask Метод возьми Андмасс и Ормасс Отказ Если в существующих масок в UpdateMasks

Если у нас уже есть маски в UpdateMasks Переменная экземпляра Мы И и маска в UpdateMasks с прохожденным в Андмасс Параметр, чтобы объединить их. Тогда мы …| Или или Маска в UpdateMasks Переменная с прошедшим в Ормасс параметр. Эти две операции объединяют две маски вместе, убедившись, что мы влияем только на биты Метаданные поле мы планируем модифицировать.

Наконец, чтобы обновить Метаданные поле мы создаем новый метод под названием UpdateMetadata Отказ

updateMetadata() {
  // If no mask applied do nothing
  if (!this.updateMasks.and || !this.updateMasks.or) {
    return;
  }

  // Update the fields
  this.collection.updateOne({
    _id: this._id
  }, {
    $bit: {
      metadata: {
        and: NumberInt(this.updateMasks.and), 
        or: NumberInt(this.updateMasks.or),
      }
    }
  });
}

Если есть маски, присутствующие в UpdateMasks Переменная экземпляра Мы применяем их к Метаданные поле, используя $ бит Оператор как раньше.

Запрос на метаданных

Теперь, когда мы знаем, как изменить поля, упакованные в Метаданные Поле позволяет посмотреть, как мы можем запросить разные поля. Мы собираемся создать простой класс (просто сосредоточиться на аспектах запроса), чтобы позволить нам запросить полей, упакованные в Метаданные Отказ Прежде чем мы сделаем, нам нужно понять, какие ограничения при запросе на Метаданные поле.

  1. Мы можем только выполнять операции равенства. То есть мы можем сопоставить на Групповой идентификатор быть равным 5 Но не на Групповой идентификатор больше чем 5 Отказ
  2. Запросы на Метаданные Поле не может использовать любые Индексы поэтому мы должны обеспечить Метаданные поле – последняя часть запроса, используя другие Критерии сузить количество Метаданные поля, которые должны быть проверены.

Итак, давайте посмотрим, какие операции доступны нам для выполнения побитовый Запросы.

$ bitsalllear. Соответствует числовым или двоичным значениям, в которых набор битовых позиций имеет значение 0.
$ bitsallset. Соответствует числовым или двоичным значениям, в которых набор битовых позиций имеет значение 1.
$ bitsanyclear. Соответствует числовым или двоичным значениям, в которых любая бита из набора битовых позиций имеет значение 0.
$ bitsalllear. Соответствует числовым или двоичным значениям, в которых любой бит из набора битовых позиций имеет значение 1.

Что это значит на практике? Давайте посмотрим на каждый из операторов, в свою очередь, используя примеры. Сначала нам нужен образец документа с двоичным значением.

db.bittest.insertOne({
  metadata: 0b011000011110110
});

$ bitsalllear.

Давайте выполним простое запрос, где мы собираемся совпадать со второй группой 4 биты.

db.bittest.findOne({
  metadata: {
    $bitsAllClear: NumberInt(0b0000111100000000)
  }
});

Это будет соответствовать вставленным документе выше, как вторая группа биты В Метаданные поле все настроено на 0 Отказ

$ bitsallset.

Давайте выполним простой операцию запроса, где мы собираемся соответствовать третьей группе 4 биты.

db.bittest.findOne({
  metadata: {
    $bitsAllSet: NumberInt(0b0000000011110000)
  }
});

Это будет соответствовать вставленным документе выше, как третья группа биты В Метаданные поле все настроено на 0 Отказ

$ bitsanyclear.

Давайте выполним простое операцию запроса, где мы собираемся соответствовать первой группе 4 биты.

db.bittest.findOne({
  metadata: {
    $bitsAnyClear: NumberInt(0b1111000000000000)
  }
});

Это будет соответствовать вставленным документе выше, как третья группа биты В Метаданные Поле содержит два бита, установленные на 0 Отказ

$ bitsanyset.

Давайте выполним простой операцию запроса, где мы собираемся соответствовать третьей группе 4 биты.

db.bittest.findOne({
  metadata: {
    $bitsAnySet: NumberInt(0b0000000000001111)
  }
});

Это будет соответствовать вставленным документе выше, как третья группа биты В Метаданные Поле содержит два бита, установленные на 1 Отказ

Найдите записи с группой со всеми разрешениями

В этом запросе мы стремимся получить все записи, где Метаданные поле содержит Групповой идентификатор равно 5 и Все разрешения чтения читаются Отказ

db.entries.find({
  metadata: {
    $bitsAllSet:   NumberInt(0b00000010100000000000000000000100),
    $bitsAllClear: NumberInt(0b00000001000000000000000000000000)
  }
}).toArray();

Ценность 5 выражается как двоичное значение 0b101 Отказ Для правильного соответствия нам нужно комбинировать $ bitsallset и $ bitsalleglear операторы. $ bitsallset Маска содержит Групповой идентификатор Значение 5 выражается как 0b101 Отказ $ bitsalleglear Содержит обратный шаблон бита 0b101 который является 0b010 Отказ Таким образом, мы обеспечиваем идеальное совпадение на бит-шаблон. Только использовать $ bitsallset не проверил, был ли средний бит установлен на 1 или 0 Отказ Вот почему мы должны проверить как для битов, установленных на 1 а также биты, установленные на 0 Отказ Точно так же мы устанавливаем Все разрешения читают Флаг к 1 , пока Написать и выполнить Флаги устанавливаются на 0 Отказ Это означает, что мы будем соответствовать любому документу, где Все разрешения читают Флаг устанавливается, и мы не заботимся о значении Написать и выполнить Флаги.

Выполнение запроса

Давайте создадим простой класс, который упростит запрос данных из Метаданные поле.

class QueryClass {
  constructor(collection) {
    this.collection = collection;
    this.allSetBits = 0b0000000000000000;
    this.allClearBits = 0b0000000000000000;  
  }

  groupId(id) {
    this.allSetBits = this.allSetBits | parseInt(`0${createIntegerQueryMask(id, 8)}00000000000000000000000`, 2);
    this.allClearBits = this.allClearBits | parseInt(`0${createIntegerQueryMask(id, 8, true)}00000000000000000000000`, 2);
    return this;
  }

  userId(id) {
    this.allSetBits = this.allSetBits | parseInt(`000000000${createIntegerQueryMask(id, 14)}000000000`, 2);
    this.allClearBits = this.allClearBits | parseInt(`000000000${createIntegerQueryMask(id, 14, true)}000000000`, 2);
    return this;
  }

  groupPermissions(read, write, execute) {
    let [mask, reverseMask] = createPermissionsQueryMasks(read, write, execute);
    this.allSetBits = this.allSetBits | parseInt(`00000000000000000000000${mask}000000`, 2);
    this.allClearBits = this.allClearBits | parseInt(`00000000000000000000000${reverseMask}000000`, 2);
    return this;
  }

  userPermissions(read, write, execute) {
    let [mask, reverseMask] = createPermissionsQueryMasks(read, write, execute);
    this.allSetBits = this.allSetBits | parseInt(`00000000000000000000000000${mask}000`, 2);
    this.allClearBits = this.allClearBits | parseInt(`00000000000000000000000000${reverseMask}000`, 2);
    return this;
  }

  allPermissions(read, write, execute) {
    let [mask, reverseMask] = createPermissionsQueryMasks(read, write, execute);
    this.allSetBits = this.allSetBits | parseInt(`00000000000000000000000000000${mask}`, 2);
    this.allClearBits = this.allClearBits | parseInt(`00000000000000000000000000000${reverseMask}`, 2);
    return this;
  }

  find() {
    return this.collection.find({
      metadata: {
        $bitsAllSet: NumberInt(this.allSetBits),
        $bitsAllClear: NumberInt(this.allClearBits)
      }
    }).toArray();
  }
}

QueryClass Позволяет нам построить запрос, который выполняет равенство совпадений на двоичных упакованных полях в Метаданные поле. Мы можем цеплять запрос, как это.

new QueryClass(db.entries)
  .groupId(5)
  .allPermissions(true)
  .find();

Это соответствует любому документу, где Метаданные Упакованное поле ГРУПИД равно 5 и Все Разрешение читать (игнорирование значений написать и Execute Флаги).

Давайте посмотрим, что GroupID (ID) и AllPermissions (читать, писать, выполнить) Методы делают, начиная с GroupID (ID) один.

groupId(id) {
  this.allSetBits = this.allSetBits | parseInt(`0${createIntegerMask(id, 8)}00000000000000000000000`, 2);
  this.allClearBits = this.allClearBits | parseInt(`0${createIntegerMask(id, 8, true)}00000000000000000000000`, 2);
  return this;
}

Мы знаем ГРУПИД закодирован в [1: 9] биты. Нам нужно создать две маски, $ bitsallset Маска создана, вызывая createintegerquerymask Метод, проходящий в ID и длина ГРУПИД поле, которое является 8 биты. $ BitAllcearear Маска создана, вызывая createintegerquerymask метод, но прохождение правдой после ID И длина битов, рассказывая к способу отменить биты маски перед их возвратом.

createintegerquerymask.

Давайте посмотрим на что createintegerquerymask делает.

function createIntegerQueryMask(id, bitResolution, reverse) {
  let bitsString = id.toString(2);
  if (bitsString.length > bitResolution) throw Error(`value id must be between 0 and ${Math.pow(2, bitResolution) - 1}`);
  
  // Reverse the bit String
  if (reverse) {
    let bitStringArray = [];
    for (let i = 0; i < bitsString.length; i++) {
      bitStringArray[i] = bitsString[i] == "0" ? "1" : "0";
    }  

    // Save the reverse string
    bitsString = bitStringArray.join('');
  }

  // Ensure `bitResolution` bit string
  let missingValues = "";
  for(let i = 0; i < (bitResolution - bitsString.length); i++) {
    missingValues = missingValues + (reverse ? "1" : "0");
  }

  // Pad the bitString with 0s
  return missingValues + bitsString;
}

createintegerquerymask Метод будет генерировать Строка длины BitResolution это бинарный Представление стоимости ID Отказ Чтобы получить возвращенную строку точно BitResolution Длинный этот метод прокладывает переднюю часть битовой строки с 0 s (или 1 s Если мы установили обратный параметр на true ) до длины конца не будет BitResolution длинный.

Другими словами, говорят, что мы проходим в ID Значение 1 с BitResolution установить 4 Отказ Это будет превращено в следующие Строка Представительство 0001 Отказ

Если мы установим параметр Обратный к правда Возвращенная битовая маска для ID Значение будет изменено (это используется для обеспечения AllClarbits сопоставлять на биты в ID Это ноль). Это означает, что если ID установлен на 5 , с BitResolution 4 Возвращенное значение для ID В бит-маске будет 1010 Как 3 последних бита перевернуты, и мы подуваем 1 на фронт, чтобы убедиться, что мы совпадаем только на значениях, где биты перед ID Значение установлено на 0 В Метаданные поле.

groupId(id) {
  this.allSetBits = this.allSetBits | parseInt(`0${createIntegerMask(id, 8)}00000000000000000000000`, 2);
  this.allClearBits = this.allClearBits | parseInt(`0${createIntegerMask(id, 8, true)}00000000000000000000000`, 2);
  return this;
}

После звонка createentegermask (ID, 8) Мы вставляем соответствующую битовую маску в полный 32 Бита битовая маска на позициях [1: 9] Отказ Затем мы разбираем битовую маску к целым числу и Или с Это. Всеssetbits Отказ Как мы помним Или Установит любые биты на 1 В результате, когда битовая маска справа содержит 1 в данном положении бит. Это будет слияние Две битовые маски, позволяющие соответствуют нескольким полям.

Затем мы сделаем то же самое для Это. ALLCEARBITS Использование обратного ID битовая маска.

CreatePermissionsQueryMasks

Теперь давайте посмотрим на то, как мы запрашиваем для отдельных флагов. Давайте посмотрим на Все разрешения (прочитать, писать, выполнить) метод.

allPermissions(read, write, execute) {
  let [mask, reverseMask] = createPermissionsQueryMasks(read, write, execute);
  this.allSetBits = this.allSetBits | parseInt(`00000000000000000000000000000${mask}`, 2);
  this.allClearBits = this.allClearBits | parseInt(`00000000000000000000000000000${reverseMask}`, 2);
  return this;
}

Так же, как для createintegerquerymask Нам нужно создать $ bitsallset и а $ bitsalleglear Маска для нашего запроса, а затем Или Каждая из масок с значениями экземпляра Это. Всеssetbits и Это. ALLCEARBITS Отказ Давайте посмотрим на что CreatePermissionsQuerymasks (прочитайте, пишите, выполнить) делает.

function createPermissionsQueryMasks(read, write, execute) {
  // Create $bitsAllSet mask
  let mask = [
    read == true ? "1" : "0",
    write == true ? "1" : "0",
    execute == true ? "1" : "0"
  ];

  // Create $bitsAllClear mask
  let reverseMask = [
    read == true || read == null ? "0" : "1",
    write == true || write == null ? "0" : "1",
    execute == true || execute == null ? "0" : "1"
  ]

  return [mask.join(''), reverseMask.join('')];
}

Для каждого из прошедшихся флагов читать , Написать или Выполнить Мы проверяем, равно ли значение равно правдам и добавьте его значение для массива битов, представляющих флаги.

Если мы позвоним методом с правда, ложь, правда Мы получаем Маска [«1», «0», «1»] Отказ

Далее мы создаем обратное маску одним небольшим исключением. Если любой из читать, писать или выполнить Параметры устанавливаются на null Мы устанавливаем соответствующую запись маски в 0 Как мы не хотим совпадать с этим значением флага. Наконец мы вернем массив Маска и Реверсмасс Отказ

Метод findone.

Давайте посмотрим на Findone Метод реальный быстрый.

find() {
  return this.collection.find({
    metadata: {
      $bitsAllSet: NumberInt(this.allSetBits),
      $bitsAllClear: NumberInt(this.allClearBits)
    }
  }).toArray();
}

Как мы можем видеть все, что он делает его, используя Collection.findone Метод против Метаданные поле, используя $ bitsallset Оператор установлен на объединенную битую маску Это. Всеssetbits а также оператор $ BitAllcearear Оператор установлен на объединенную битую маску Это. ALLCEARBITS Отказ

Настройка некоторых тестовых документов

Давайте вставьте некоторые тестовые документы, которые мы можем запросить.

db.entries.insertMany([{
    _id: 1,
    name: "peter",
    path: "/home/user/rowan",
    parentPath: "/home/rowan",
    metadata: NumberInt(0b00000010100000000000001111111100)
}, {
    _id: 2,
    name: "peter",
    path: "/home/user/paul",
    parentPath: "/home/paul",
    metadata: NumberInt(0b00111010100000000000001111111100)
}])

Выполнение запроса

Наконец, запустите запрос, показанный ранее, где мы совпадаем против всех документов, где Группа и то Все разрешение на чтение верно Отказ

new QueryClass(db.entries)
  .groupId(5)
  .allPermissions(true)
  .find();

Как и ожидалось, мы извлекаем документ с помощью _id Отказ Документ содержит следующие значения поля метаданных.

0                                     // [0:0]    isDirectory   = false
 00000101                             // [1:9]    Group id      = 5
         00000000000001               // [10:23]  User id       = 1
                       111            // [24:26]  Group r/w/x   = true/true/true
                          111         // [27:29]  User r/w/x    = true/true/true
                             100      // [29:31]  All r/w/x     = true/false/false

Мы можем видеть, что Группа И что читать Флаг в Все разрешения установлен на правда также.

Мы видим, что управление упаковкой битов довольно сложна по сравнению с обычными значениями базы данных, но мы можем добиться значительных преимуществ экономии космического пространства при необходимости. Это сказал, что вы должны подумать дважды, используя его, если космические проблемы не являются основными проблемами для вас.