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

Этот модуль посвящен применению инвертированного индекса – распространенного шаблона проектирования для DynamoDB.

Вторичные индексы – ключевые инструменты моделирования данных в DynamoDB. С их помощью можно переформировать данные, чтобы воспользоваться альтернативными шаблонами запросов.

Инвертированный индекс – распространенный шаблон проектирования вторичного индекса с помощью DynamoDB. Инвертированный индекс позволяет создать вторичный индекс, который является инверсией первичного ключа таблицы. Ключ типа HASH для таблицы становится ключом типа RANGE в индексе, а ключ типа RANGE для таблицы становится первичным ключом для индекса.

Инвертированный индекс полезен в двух случаях. Во-первых, он полезен для отправки запроса к «другой» стороне отношения «многие ко многим». Это относится к сущности Friendship. Структура первичного ключа позволяет отправлять запрос ко всем подписчикам определенного пользователя по первичному ключу таблицы. При добавлении инвертированного индекса можно будет найти пользователей, на которых подписан этот пользователь («отслеживаемых пользователей»), посредством оправки запроса к инвертированному индексу.

Инвертированный индекс также полезен для отправки запроса к отношению «один ко многим» сущности, которая сама по себе является субъектом отношения «один ко многим». Пример подобного представлен в таблице в виде сущности Reaction. Может существовать множество реакций на одну фотографию, и каждая реакция будет включать фотографию, к которой эта реакция относится, имя отреагировавшего пользователя и тип реакции. Но поскольку у пользователя может быть много фотографий, главный идентификатор той или иной фотографии указывается в ключе типа RANGE: PHOTO#<USERNAME>#<TIMESTAMP>. Поэтому нельзя использовать первичный ключ для привязки реакций к фотографиям.

Чтобы удовлетворить условия шаблона доступа «Просмотр фотографий и реакций», модель данных размещает идентификатор фотографии для сущности Reaction в ключе типа RANGE. Теперь при отправке запроса к инвертированному индексу можно использовать идентификатор фотографии для выбора этой фотографии и всех сопутствующих реакций в одном обращении. Этот процесс показан в приведенном ниже шаге 2.

Время, необходимое для прохождения модуля: 40 минут


  • Этап 1. Создание разреженного вторичного индекса

    Чтобы создать вторичный индекс, нужно указать первичный ключ индекса, как это делалось раньше во время создания таблицы. Обратите внимание, что первичный ключ глобального вторичного индекса необязательно должен быть уникальным для каждого элемента. Затем DynamoDB копирует объекты в индекс в зависимости от указанных атрибутов, так что запросы к нему можно выполнять так же, как и к таблице.

    В DynamoDB инвертированный индекс – это распространенный шаблон, предусматривающий создание вторичного индекса, который является инверсией первичного ключа таблицы. Ключ типа HASH для таблицы указывается в виде ключа типа RANGE во вторичном индексе, а ключ типа RANGE для таблицы указывается в виде ключа типа HASH во вторичном индексе.

    Создание вторичного индекса напоминает создание таблицы. В скачанном коде в каталоге scripts/ есть файл под названием add_inverted_index.py. Файл содержит следующий код:

    import boto3
    
    dynamodb = boto3.client('dynamodb')
    
    try:
        dynamodb.update_table(
            TableName='quick-photos',
            AttributeDefinitions=[
                {
                    "AttributeName": "PK",
                    "AttributeType": "S"
                },
                {
                    "AttributeName": "SK",
                    "AttributeType": "S"
                }
            ],
            GlobalSecondaryIndexUpdates=[
                {
                    "Create": {
                        "IndexName": "InvertedIndex",
                        "KeySchema": [
                            {
                                "AttributeName": "SK",
                                "KeyType": "HASH"
                            },
                            {
                                "AttributeName": "PK",
                                "KeyType": "RANGE"
                            }
                        ],
                        "Projection": {
                            "ProjectionType": "ALL"
                        },
                        "ProvisionedThroughput": {
                            "ReadCapacityUnits": 5,
                            "WriteCapacityUnits": 5
                        }
                    }
                }
            ],
        )
        print("Table updated successfully.")
    except Exception as e:
        print("Could not update table. Error:")
        print(e)
    

    Когда в первичном ключе таблицы или вторичного индекса используются атрибуты, их необходимо определить в AttributeDefinitions. Затем мы создаем вторичный индекс операцией Create в свойстве GlobalSecondaryIndexUpdates. Для этого вторичного индекса мы указываем имя индекса, схему первичного ключа, предоставляемую пропускную способность и атрибуты, для которых необходимо создать проекцию.

    Обратите внимание, что в DynamoDB инвертированный индекс – это имя шаблона проектирования, а не официальное свойство. Создание инвертированного индекса похоже на создание любого другого вторичного индекса.

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

    python scripts/add_inverted_index.py

    В консоли вы увидите сообщение «Table updated successfully» («Таблица обновлена»).

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

  • Шаг 2. Отправка запроса к инвертированному индексу для поиска реакций на фотографию

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

    Для использования вторичного индекса можно воспользоваться двумя вызовами API: Query и Scan. Для Query требуется указать ключ HASH. Он возвращает целевой результат. Для Scan не требуется указывать ключ HASH . Эта операция выполняется над всей таблицей. Операции Scan не рекомендуется использовать в DynamoDB, кроме определенных случаев, потому что они обращаются к каждому объекту базы данных. Если в таблице очень много данных, то сканирование может длиться долго.

    Для поиска всех реакций на определенную фотографию можно использовать API Query применительно ко вторичному индексу. Как было продемонстрировано в предыдущем модуле, можно использовать этот запрос для извлечения двух типов сущностей посредством одной команды. В рамках этого запроса можно извлечь фотографию и сопутствующие реакции.

    В скачанном коде в каталоге application/ есть файл под названием fetch_photo_and_reactions.py. В скрипте указывается приведенное ниже содержимое.

    import boto3
    
    from entities import Photo, Reaction
    
    dynamodb = boto3.client('dynamodb')
    
    USER = "david25"
    TIMESTAMP = '2019-03-02T09:11:30'
    
    
    def fetch_photo_and_reactions(username, timestamp):
        try:
            resp = dynamodb.query(
                TableName='quick-photos',
                IndexName='InvertedIndex',
                KeyConditionExpression="SK = :sk AND PK BETWEEN :reactions AND :user",
                ExpressionAttributeValues={
                    ":sk": { "S": "PHOTO#{}#{}".format(username, timestamp) },
                    ":user": { "S": "USER$" },
                    ":reactions": { "S": "REACTION#" },
                },
                ScanIndexForward=True
            )
        except Exception as e:
            print("Index is still backfilling. Please try again in a moment.")
            return False
    
        items = resp['Items']
        items.reverse()
    
        photo = Photo(items[0])
        photo.reactions = [Reaction(item) for item in items[1:]]
    
        return photo
    
    
    photo = fetch_photo_and_reactions(USER, TIMESTAMP)
    
    if photo:
        print(photo)
        for reaction in photo.reactions:
            print(reaction)
    

    Функция fetch_photo_and_reactions аналогична функции, которая была бы представлена в приложении. Функция принимает имя пользователя и метку времени и создает запрос применительно к InvertedIndex для поиска фотографии и сопутствующих реакций. Затем она собирает возвращенные элементы в сущность Photo и множество сущностей Reaction, которые можно использовать в приложении.

    python application/fetch_photo_and_reactions.py

    Вы должны увидеть вывод фотографии и пяти ее сопутствующих реакций.

    Photo<david25 -- 2019-03-02T09:11:30>
    Reaction<ylee -- PHOTO#david25#2019-03-02T09:11:30 -- smiley>
    Reaction<kennedyheather -- PHOTO#david25#2019-03-02T09:11:30 -- smiley>
    Reaction<jenniferharris -- PHOTO#david25#2019-03-02T09:11:30 -- +1>
    Reaction<geoffrey32 -- PHOTO#david25#2019-03-02T09:11:30 -- +1>
    Reaction<chasevang -- PHOTO#david25#2019-03-02T09:11:30 -- +1>

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

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

  • Шаг 3. Поиск отслеживаемых пользователей

    В предыдущем шаге вы увидели, как использовать инвертированный индекс для получения данных об отношении «один ко многим» сущности, которая сама по себе являлась субъектом отношения «один ко многим». В этом шаге инвертированный индекс будет использоваться для получения данных о «другой» стороне отношения «многие ко многим».

    Первичный ключ в таблице позволяет найти всех подписчиков определенного пользователя, но не позволяет найти всех пользователей, на которых кто-то подписан. Благодаря инвертированному индексу он переворачивается, и можно найти всех пользователей, отслеживаемых определенным пользователем.

    В скачанном коде в каталоге application/ есть файл под названием find_following_for_user.py. Этот скрипт содержит следующий код:

    import boto3
    
    from entities import Friendship
    
    dynamodb = boto3.client('dynamodb')
    
    USERNAME = "haroldwatkins"
    
    
    def find_following_for_user(username):
        resp = dynamodb.query(
            TableName='quick-photos',
            IndexName='InvertedIndex',
            KeyConditionExpression="SK = :sk",
            ExpressionAttributeValues={
                ":sk": { "S": "#FRIEND#{}".format(username) }
            },
            ScanIndexForward=True
        )
    
        return [Friendship(item) for item in resp['Items']]
    
    
    
    follows = find_following_for_user(USERNAME)
    
    print("Users followed by {}:".format(USERNAME))
    for follow in follows:
        print(follow)
    

    Функция find_following_for_user аналогична функции, которая была бы представлена в приложении. Функция принимает имя пользователя, для которого необходимо найти отслеживаемых пользователей. Затем функция отправляет запрос к инвертированному индексу для поиска всех сущностей Friendship, в которых подписанный пользователь соответствует указанному имени пользователя.

    Выполните скрипт, запустив указанную ниже команду в терминале.

    python application/find_following_for_user.py

    В консоли должен быть выведен список отслеживаемых пользователей, которые соответствуют указанному имени пользователя.

    Users followed by haroldwatkins:
    Friendship<chasevang -- haroldwatkins>
    Friendship<david25 -- haroldwatkins>
    Friendship<frankhall -- haroldwatkins>
    Friendship<geoffrey32 -- haroldwatkins>
    Friendship<jacksonjason -- haroldwatkins>
    Friendship<natasha87 -- haroldwatkins>
    Friendship<nmitchell -- haroldwatkins>
    Friendship<ppierce -- haroldwatkins>
    Friendship<tmartinez -- haroldwatkins>
    Friendship<vpadilla -- haroldwatkins>

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

  • Выводы

    В этом модуле с помощью шаблона инвертированного индекса к таблице был добавлен вторичный индекс. Таким образом мы реализовали два дополнительных шаблона доступа:

    • Просмотр фотографий и реакций (чтение).
    • Просмотр пользователей, отслеживаемых определенным пользователем (чтение).

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