You are previewing Programming Amazon EC2.

Programming Amazon EC2

Cover of Programming Amazon EC2 by Flavia Paganelli... Published by O'Reilly Media, Inc.
O'Reilly logo

SimpleDB

AWS says that SimpleDB is “a highly available, scalable, and flexible nonrelational data store that offloads the work of database administration.” There you have it! In other words, you can store an extreme amount of structured information without worrying about security, data loss, and query performance. And you pay only for what you use.

SimpleDB is not a relational database, but to explain what it is, we will compare it to a relational database since that’s what we know best. SimpleDB is not a database server, so therefore there is no such thing in SimpleDB as a database. In SimpleDB, you create domains to store related items. Items are collections of attributes, or key-value pairs. The attributes can have multiple values. An item can have 256 attributes and a domain can have one billion attributes; together, this may take up to 10 GB of storage.

You can compare a domain to a table, and an item to a record in that table. A traditional relational database imposes the structure by defining a schema. A SimpleDB domain does not require items to be all of the same structure. It doesn’t make sense to have all totally different items in one domain, but you can change the attributes you use over time. As a consequence, you can’t define indexes, but they are implicit: every attribute is indexed automatically for you.

Domains are distinct—they are on their own. Joins, which are the most powerful feature in relational databases, are not possible. You cannot combine the information in two domains with one single query. Joins were introduced to reconstruct normalized data, where normalizing data means ripping it apart to avoid duplication.

Because of the lack of joins, there are two different approaches to handling relations (previously handled by joins). You can either introduce duplication (e.g., store employees in the employer domain and vice versa), or you can use multiple queries and combine the data at the application level. If you have data duplication and if several applications write to your SimpleDB domains, each of them will have to be aware of this when you make changes or add items to maintain consistency. In the second case, each application that reads your data will need to aggregate information from different domains.

There is one other aspect of SimpleDB that is important to understand. If you add or update an item, it does not have to be immediately available. SimpleDB reserves the right to take some time to process the operations you fire at it. This is what is called eventual consistency, and for many kinds of information, it is not a problem.

But in some cases, you need the latest, most up-to-date information. Think of an online auction website like eBay, where people bid for different items. At the moment a purchase is made, it’s important that the right—latest—price is read from the database. To address those situations, SimpleDB introduced two new features in early 2010: consistent read and conditional put/delete. A consistent read guarantees to return values that reflect all previously successful writes. Conditional put/delete guarantees that a certain operation is performed only when one of the attributes exists or has a particular value. With this, you can implement a counter, for example, or implement locking/concurrency.

We have to stress that SimpleDB is a service, and as such, it solves a number of problems for you. Indexing is one we already mentioned. High availability, performance, and infinite scalability are other benefits. You don’t have to worry about replicating your data to protect it against hardware failures, and you don’t have to think of what hardware you are using if you have more load, or how to handle peaks. Also, the software upgrades are taken care of for you.

But even though SimpleDB makes sure your data is safe and highly available by seamlessly replicating it in several data centers, Amazon itself doesn’t provide a way to manually make backups. So if you want to protect your data against your own mistakes and be able to revert back to previous versions, you will have to resort to third-party solutions that can back up SimpleDB data, for example to S3.

Table 4-1 highlights some of the differences between the SimpleDB data store and relational databases.

Tip

SimpleDB is not (yet) part of the AWS Console, but to get an idea of the API that SimpleDB provides, you can play around with the SimpleDB Scratchpad.

Table 4-1. SimpleDB data store versus relational databases

Relational databasesSimpleDB
Tables are organized in databasesNo databases; all domains are loose in your AWS account
Schemas to define table structureNo predefined structure; variable attributes
Tables, records, and columnsDomains, items, and attributes
Columns have only one valueAttributes can have multiple values
You define indexes manuallyAll attributes are automatically indexed
Data is normalized; broken down to its smallest partsData is not always normalized
Joins are used to denormalizeNo joins; either duplicate data or multiple queries
Transactions are used to guarantee data consistencyEventual consistency, consistent read, or conditional put/delete

Use Cases for SimpleDB

So what can you do with SimpleDB? It is different enough from traditional relational databases that you need to approach your problem from other angles, yet it is similar enough to make that extremely difficult.

AWS itself also seems to struggle with this. If you look at the featured use cases, they mention logging, online games, and metadata indexing. Logging is suitable for SimpleDB, but you do have to realize you can’t use SimpleDB for aggregate reporting: there are no aggregate functions such as SUM, AVERAGE, MIN, etc. Metadata indexing is a very good pattern of applying SimpleDB to your problem; you can have data stored in S3 and use SimpleDB domains to store pointers to S3 objects with more information about them (metadata). It is very quick and easy to search and query this metadata.

Another class of problems SimpleDB is perfect for is sharing information between isolated components of your application (decoupling). Where we use SQS for communicating actions or messages, SimpleDB provides a way to share indexed information, i.e., information that can be searched. A SimpleDB item is limited in size, but you can use S3 for storing bigger objects, for example images and videos, and point to them from SimpleDB. You could call this metadata indexing.

Other classes of problems where SimpleDB is useful are:

Loosely coupled systems

This is our favorite use of SimpleDB so far. Loosely coupled components share information, but are otherwise independent.

Suppose you have a big system and you suddenly realize you need to scale by separating components. You have to consider what to do with the data that is shared by the resulting components. SimpleDB has the advantage of having no setup or installation overhead, and it’s not necessary to define the structure of all your data in advance. If the data you have to share is not very complex, SimpleDB is a good choice. Your data will be highly available for all your components, and you will be able to retrieve it quickly (by selecting or searching) thanks to indexing.

Fat clients

For years, everyone has been working on thin clients; the logic of a web application is located on the server side. With Web 2.0, the clients are getting a bit fatter, but still, the server is king. The explosion of smartphone apps and app stores shows that the fat client is making a comeback. These new, smarter apps do a lot themselves, but in the age of the cloud they can’t do everything, of course. SimpleDB is a perfect companion for this type of system: self-contained clients operating on cloud-based information. Not really thick, but certainly not thin: thick-thin-clients.

One advantage of SimpleDB here is that it’s ready to use right away. There is no setup or administration hassle, the data is secure, and, most importantly, SimpleDB provides access through web services that can be called easily from these clients. Typically, many applications take advantage of storing data in the cloud to build different kinds of clients—web, smartphone, desktop—accessing the same data.

Large (linear) datasets

The obvious example of a large, simple dataset in the context of Amazon is books. A quick calculation shows that you can store 31,250,000 books in one domain, if 32 attributes is enough for one book. We couldn’t find the total number of different titles on Amazon.com, but Kindle was reported to offer 500,000 titles in April 2010. Store PDFs in S3, and you are seriously on your way to building a bookstore.

Other examples are music and film—much of the online movie database IMDb’s functionality could be implemented with SimpleDB. If you need to scale and handle variations in your load, SimpleDB makes sure you have enough resources, and guarantees good performance when searching through your data.

Hyperpersonal

A new application of cloud-based structured storage (SimpleDB) can be called hyperpersonal storage. For example, saving the settings or preferences of your applications/devices or your personal ToDo list makes recovery or reinstallation very easy. Everyone has literally hundreds (perhaps even thousands) of these implicit or explicit little lists of personal information.

This is information that is normally not shared with others. It is very suitable for SimpleDB because there is no complex structure behind it.

Scale

Not all applications are created equal, and “some are more equal than others.” Most public web applications would like to have as many users as Amazon.com, but the reality is different. If you are facing the problem of too much load, you have probably already optimized your systems fully. It is time to take more dramatic measures.

As described in Chapter 1, the most drastic thing you can do is eliminate joins. If you get rid of joins, you release part of your information; it becomes isolated and can be used independently of other information. You can then move this free information to SimpleDB, which takes over responsibilities such as scalability and availability while giving you good performance.

Example 1: Storing Users for Kulitzer (Ruby)

On the Web you can buy, sell, and steal, but you can’t usually organize a contest. That is why Kulitzer is one of a kind. Contests are everywhere. There are incredibly prestigious contests like the Cannes Film Festival or the Academy Awards. There are similar prizes in every country and every school. There are other kinds of contests like American Idol, and every year thousands of girls want to be the prom queen.

Kulitzer aims to be one platform for all. If you want to host a contest, you are more than welcome to. But some of these festivals require something special—they want their own Kulitzer. At this moment, we want to meet this demand, but we do not want to invest in developing something like Google Sites where you can just create your own customized contest environment. We will simply set up a separate Kulitzer environment, designed to the wishes of the client.

But the database of users is very important, and we want to keep it on our platform. We believe that, in the end, Kulitzer will be the brand that gives credibility to the contests it hosts. Kulitzer is the reputation system for everyone participating in contests. This means that our main platform and the specials are all built on one user base, with one reputation system, and a recognizable experience for the users (preferences are personal, for example; you take them with you). We are going to move the users to SimpleDB so they can be accessed both by the central Kulitzer and by the separate special Kulitzer environments. Figure 4-5 shows the new architecture. The new components are the SimpleDB users domain and the new EC2 instances for separate contests.

Note

Figure 4-5 does not show the RDS instance used for the database, which we introduced in Chapter 1. With this change, each new special Kulitzer would use an RDS instance of its own for its relational data. In fact, each Kulitzer environment would be run on a different AWS account. To simplify the figure, ELBs are not shown.

Introducing SimpleDB for sharing user accounts

Figure 4-5. Introducing SimpleDB for sharing user accounts

The solution of moving the users to SimpleDB falls into several of the classes of problems described above. It is a large linear dataset we need to scale, and we also deal with disparate (decoupled) systems.

In the examples below, we show how to create an item in a users domain, and then how to retrieve a user item using Ruby.

Adding a user

To add a user, all you need is something like the following. We use the RightAWS Ruby library. The attribute id will be the item name, which is always unique in SimpleDB. When using SimpleDB with RightAWS, we need an additional gem called uuidtools, which you can install with gem install uuidtools:

require 'rubygems'
require 'right_aws'
require 'sdb/active_sdb'

RightAws::ActiveSdb.establish_connection("AKIAIGKECZXA7AEIJLMQ", 
    "w2Y3dx82vcY1YSKbJY51GmfFQn3705ftW4uSBrHn")

# right_aws' simpledb is still alpha. it works, but it feels a bit
# experimental and does not resemble working with SQS. Anyway, you need
# to subclass Base to access a domain. The name of the class will be
# lowercased to domain name.
class Users < RightAws::ActiveSdb::Base
end

# create domain users if it doesn't exist yet,
# same as RightAws::ActiveSdb.create_domain("users")
Users.create_domain

# add Arjan
# note: id is used by right_aws sdb as the name, and names are unique
# in simpledb. but create is sort of idempotent, doens't matter if you
# create it several times, it starts to act as an update.
Users.create(
            'id' => 'arjanvw@gmail.com',
            'login' => 'mushimushi',
            'description' => 
                'total calligraphy enthusiast ink in my veins!!',
            'created_at' => '1241263900',
            'updated_at' => '1283845902',
            'admin' => '1',
            'password' => 
                '33a1c623d4f95b793c2d0fe0cadc34e14f27a664230061135',
            'salt' => 'Koma659Z3Zl8zXmyyyLQ',
            'login_count' => '22',
            'failed_login_count' => '0',
            'last_request_at' => '1271243580',
            'active' => '1',
            'agreed_terms' => '1')

Getting a user

Once you know who you want, it is easy and fast to get everything. This particular part of the RightScale SimpleDB Ruby implementation does not follow the SQS way. SQS is more object oriented and easier to read. But the example below is all it takes to get a particular item. If you want to perform more complex queries, it doesn’t get much more difficult. For example, getting all admins can be expressed with something like administrators = Users.find( :all, :conditions=> [ "['admin'=?]", "1"]):

require 'rubygems'
require 'right_aws'
require 'sdb/active_sdb'

RightAws::ActiveSdb.establish_connection("AKIAIGKECZXA7AEIJLMQ",
    "w2Y3dx82vcY1YSKbJY51GmfFQn3705ftW4uSBrHn")

class Users < RightAws::ActiveSdb::Base
end

# get Arjan
arjan = Users.find('arjanvw@gmail.com')
# reload to get all the attributes
arjan.reload

puts arjan.inspect

Note

As we were writing this book, we found that there are not many alternatives to a good SimpleDB solution for Ruby. We chose to use RightAWS because of consistency with the SQS examples, and because we found it was the most comprehensive of the libraries we saw. The implementation of SimpleDB is a bit “Railsy”: it generates IDs automatically and uses those as the names for the items. You can overwrite that behavior, as we did, at your convenience.

Example 2: Sharing Marvia Accounts and Templates (PHP)

With Marvia, we started to implement some of our ideas regarding quality of service, a finer-grained approach to product differentiation (Example 2: Priority PDF Processing for Marvia (PHP)). We created a priority lane for important jobs using SQS, giving the user the option of preferential treatment. We introduced flexibility at the job level, but we want to give more room for product innovation.

The current service is an all-you-can-eat plan for creating your PDFs. This is interesting, but for some users it is too much. In some periods, they generate tens or even hundreds of PDFs per week, but in other periods they only require the service occasionally. These users are more than willing to spend extra money during their busy weeks if they can lower their recurring costs.

We want to introduce this functionality gradually into our application, starting by implementing a fair-use policy on our entry-level products. For this, we need two things: we need to keep track of actual jobs (number of PDFs created), and we need a phone to inform customers if they are over the limit. If we know users consistently breach the fair use, it is an opportunity to upgrade. So for now we only need to start counting jobs.

At the same time, we are working toward a full-blown API that implements many of the features of the current application and that will be open for third parties to use. Extracting the activity per account information from the application and moving it to a place where it can be shared is a good first step in implementing the API. So the idea is to move accounts to SimpleDB. SimpleDB is easily accessible by different components of the application, and we don’t have to worry about scalability. Figure 4-6 shows the new Marvia ecosystem.

Using SimpleDB in Marvia for sharing user accounts

Figure 4-6. Using SimpleDB in Marvia for sharing user accounts

Adding an account

Adding an item to a domain in SimpleDB is just as easy as sending a message to a queue in SQS. An item always has a name, which is unique within its domain; in this case, we chose the ID of the account as the name. We are going to count the number of PDFs generated for all accounts, but we do want to share if an account is restricted under the fair-use policy of Marvia.

As with the SQS create_queue method, the create_domain method is idempotent (it doesn’t matter if you run it twice; nothing changes). We need to set the region because we want the lowest latency possible and the rest of our infrastructure is in Europe. And instead of having to use JSON to add structure to the message body, we can add multiple attributes as name/value pairs. We’ll have the attribute fair, indicating whether this user account is under the fair-use policy, and PDFs, containing the number of jobs done. We want both attributes to have a singular value, so we have to specify that we don’t want to add, but rather replace, existing attribute/value pairs in the put_attributes method:

<?php
    require_once('/usr/share/php/AWSSDKforPHP/sdk.class.php');

    define('AWS_KEY', 'AKIAIGKECZXA7AEIJLMQ');
    define('AWS_SECRET_KEY', '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');
    define('AWS_ACCOUNT_ID', '457964863276');

    # construct the message (use zero padding to handle
    # simpledb's lexicographic ordering)
    $account = array(
        'fair' => 'yes',
        'PDFs' => sprintf('%05d', '0'));

    $sdb = new AmazonSDB();
    $sdb->set_region($sdb::REGION_EU_W1);

    $accounts = $sdb->create_domain('accounts');
    $accounts->isOK() or
            die('could not create domain accounts');

    $response = $sdb->put_attributes('accounts',
            'jurg@9apps.net', $account, true);

    pr($response->body);

    function pr($var) { print '<pre>'; print_r($var); print '</pre>'; }
?>

Getting an account

If you want to get all attributes of a specific item, you can use the get_attributes method. Just state the domain name and item name, and you will get a CFSimpleXML object with all attributes. This is supposed to be easy, but we don’t find it very straightforward. But if you understand it, you can parse XML with very brief statements, which is convenient compared to other ways of dealing with XML:

<?php
    require_once('/usr/share/php/AWSSDKforPHP/sdk.class.php');

    define('AWS_KEY', 'AKIAIGKECZXA7AEIJLMQ');
    define('AWS_SECRET_KEY', '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');
    define('AWS_ACCOUNT_ID', '457964863276');

    $sdb = new AmazonSDB();
    $sdb->set_region($sdb::REGION_EU_W1);

    $accounts = $sdb->create_domain('accounts');
    $accounts->isOK() or
            die('could not create domain accounts');

    $response = $sdb->get_attributes('accounts',
            'jurg@9apps.net');

    pr($response->body);

    function pr($var) { print '<pre>'; print_r($var); print '</pre>'; }
?>

As output when invoking this script, we will see something like this:

CFSimpleXML Object
(
    [@attributes] => Array
        (
            [ns] => http://sdb.amazonaws.com/doc/2009-04-15/
        )

    [GetAttributesResult] => CFSimpleXML Object
        (
            [Attribute] => Array
                (
                    [0] => CFSimpleXML Object
                        (
                            [Name] => PDFs
                            [Value] => 00000
                        )

                    [1] => CFSimpleXML Object
                        (
                            [Name] => fair
                            [Value] => yes
                        )

                )

        )

    [ResponseMetadata] => CFSimpleXML Object
        (
            [RequestId] => eec8b61d-4107-0f1c-45b2-219f5f0b895a
            [BoxUsage] => 0.0000093282
        )

)

Incrementing the counter

Before the introduction of conditional puts and consistent reads, some things were impossible to guarantee: for example, atomically incrementing the value of an attribute. But with conditional puts, we can do just that. SimpleDB does not have an increment operator, so we have to first get the value of the attribute we want to increment. It is possible that someone else updated that particular attribute. We can try, but the conditional put will fail. Not a problem—we can just try again. (We just introduced a possible race condition, but we are not generating thousands of PDFs a second; if we do 100 a day per account, it is a lot.)

To be sure we have the most up-to-date value for this attribute, we do a consistent read. You can consider a consistent read as a flush for writes (puts)—using consistent reads forces all operations to be propagated over the replicated instances of your data. A regular read can get information from a replicated instance that does not yet have the latest updates in the system. A consistent read can be slower, especially if there are operations to be forced to propagate:

<?php
    require_once('/usr/share/php/AWSSDKforPHP/sdk.class.php');

    define('AWS_KEY', 'AKIAIGKECZXA7AEIJLMQ');
    define('AWS_SECRET_KEY', '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');
    define('AWS_ACCOUNT_ID', '457964863276');

    $sdb = new AmazonSDB();
    # $sdb->set_region($sdb::REGION_EU_W1);

    $accounts = $sdb->create_domain( 'accounts');
    $accounts->isOK() or
            die('could not create domain accounts');

    # we have to be a bit persistent; even though it
    # is unlikely someone else might have incremented
    # during our operation
    do {
        $response = $sdb->get_attributes('accounts',
            'jurg@9apps.net', 'PDFs', array( 'ConsistentRead' => 'true'));
        $PDFs = (int)$response->body->Value(0);

        $account = array('PDFs' => sprintf( '%05d', $PDFs + 1));
        $response = $sdb->put_attributes(
            'accounts',
            'jurg@9apps.net',
            $account,
            true,
            array(
            'Expected.Name' => 'PDFs',
            'Expected.Value' => sprintf( '%05d', $PDFs)));
    } while($response->isOK() === FALSE);
?>

Example 3: SimpleDB in Decaf (Java)

Decaf is a mobile tool, and it offers a unique way of interacting with your AWS applications. If your app is on AWS and you use SimpleDB, there are a couple of interesting things you can learn on the go.

Sometimes you’re not at the office, but you would like to search or browse your items in SimpleDB. Perhaps you’re at a party and you want to know if someone you meet is a Kulitzer user. Or perhaps you run into a customer at a networking event and you want to know how many PDFs have been generated by her.

Another example addresses the competitive thrill seeker in us. If we have accounts in SimpleDB, we might want to monitor the size of that domain. We want to know when we hit 1,000 and, after that, 10,000.

So that is what we are going to do here. We are going to implement a basic SimpleDB browser that can search for and monitor certain attributes.

Tip

If you use the Eclipse development environment for Java, you can try out the AWS Toolkit for Eclipse, which provides some nice graphical tools for managing SimpleDB. You can manage your domains and items, and make select queries in a Scratchpad.

Listing domains

Listing existing domains is simple. The ListDomains action returns a list of domain names. This list has, by default, a maximum of 100 elements (you can change that by adding a parameter in the request), and if there are more than the maximum, you get a token to get the next “page” of results:

import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.simpledb.AmazonSimpleDB;
import com.amazonaws.services.simpledb.AmazonSimpleDBClient;
import com.amazonaws.services.simpledb.model.ListDomainsRequest;
import com.amazonaws.services.simpledb.model.ListDomainsResult;

// ...

// prepare the credentials
String accessKey = "AKIAIGKECZXA7AEIJLMQ";
String secretKey = "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";

// create the SimpleDB service
AmazonSimpleDB sdbService = new AmazonSimpleDBClient(
    new BasicAWSCredentials(accessKey, secretKey));


// set the endpoint for us-east-1 region
sdbService.setEndpoint("https://sdb.amazonaws.com");

String nextToken = null;
ListDomainsRequest request = new ListDomainsRequest();

List<String> domains = new ArrayList<String>();

// get the existing domains for this region
do {
    if (nextToken != null) request = request.withNextToken(nextToken);
    ListDomainsResult result = sdbService.listDomains(request);
    nextToken = result.getNextToken();
    domains.addAll(result.getDomainNames());
} while (nextToken != null);

// show the domains in a list...

Listing items in a domain: select

The way to list the items in a domain is to execute a select. The syntax is similar to that of an SQL select statement, but of course there are no joins; it is only for fetching items from one domain at a time. The results are paginated, as in the case of domains, in a way that each response never exceeds 1 MB.

The following retrieves all the items of a given domain:

import com.amazonaws.services.simpledb.AmazonSimpleDB;
import com.amazonaws.services.simpledb.AmazonSimpleDBClient;
import com.amazonaws.services.simpledb.model.Item;
import com.amazonaws.services.simpledb.model.SelectRequest;
import com.amazonaws.services.simpledb.model.SelectResult;

// ...

// determine which domain we want to list
String domainName = ...

AmazonSimpleDB simpleDBService = ...;

// initialize list of items
List<Item> items = new ArrayList<Item>();

// nextToken == null is the first page
String nextToken = null;

// set the select expression which retrieves all the items from this domain
SelectRequest request = new SelectRequest("select * from " + domainName);
        
do {
    if (nextToken != null) request = request.withNextToken(nextToken);
    // make the request to the service
    SelectResult result = simpleDBService.select(request);

    nextToken = result.getNextToken();
    items.addAll(result.getItems());
} while (nextToken != null);


// show the items...

Note

In these examples, we retrieve all the pages using NextToken and then we show them all to simplify the example. When the number of items is very big and you are showing them to a user, it’s probably best to get a page, show it, then get more pages (if necessary). Otherwise, the user might have to wait a long time to see something. Also, the amount of data might be too big to keep in memory.

Of course, with a select expression, we can do more complex filtering by adding a where clause, listing only certain attributes and sorting the items by one of the attributes. For example, if we have the users table and we are looking for a user whose name is “Juan” and whose surname starts with “P”, we would do something like this:

select name, surname from users 
    where name like 'Juan%' intersection surname like 'P%'

In addition to '*' and a list of attributes, the output of a select can also be count(*) or itemName() to return the name of the item.

Getting domain metadata

If you’d like to know the total number of items in your domain, one way is to use a select:

select count(*) from users

Another option is to retrieve the metadata of the domain, like we do here:

import com.amazonaws.services.simpledb.AmazonSimpleDB;
import com.amazonaws.services.simpledb.AmazonSimpleDBClient;
import com.amazonaws.services.simpledb.model.DomainMetadataRequest;
import com.amazonaws.services.simpledb.model.DomainMetadataResult;

// determine which domain we want to list
String domainName = ...

AmazonSimpleDB simpleDBService = ...;

// prepare the DomainMetadata request for this domain
DomainMetadataRequest request = new DomainMetadataRequest(domainName);
      
DomainMetadataResult result = simpleDBService.domainMetadata(request);
      
// we are interested in the total amount of items
long totalItems = result.getItemCount();

// show results
System.out.println("Domain metadata: " + result);
System.out.println("The domain " + domainName + " has " +
    totalItems + " items.");

The DomainMetadataResult can look similar to this:

{ItemCount: 210, 
 ItemNamesSizeBytes: 3458, 
 AttributeNameCount: 2, 
 AttributeNamesSizeBytes: 18, 
 AttributeValueCount: 245,
 AttributeValuesSizeBytes: 7439,
 Timestamp: 1287263703, }

The best content for your career. Discover unlimited learning on demand for around $1/day.