Skip to main content

How Ottoman Works

This section is for those who want to understand how Ottoman works in depth.

Key Generation Layer

Ottoman provides an abstraction layer to handle the keys that will be used to store/access the documents on the Database Server.

Developers will only have to work with the document ids while ottoman handles the keys automatically for them.

keyGenerator function

The default keyGenerator function is used to generate all keys by Ottoman in your Couchbase datastore.

const keyGenerator = ({ metadata }) => `${ metadata.modelName }`

Using the default keyGenerator function that Ottoman provides and assuming your modelName is 'User', the key for your document would look like:

  • User::0477024c

::: tip Notice This resulted key is a combination of the prefix as provided by the default keyGenerator function (${metadata.modelName}) appended with an ID (0477024c). :::

Override keyGenerator function

The keyGenerator function allows you to only override the prefix for a key, or completely remove the prefix such that the key always matches the ID of the document generated.

const keyGenerator = ({ metadata }) => `${ metadata.scopeName }`
const User = model('User', schema, { keyGenerator, scopeName: 'myScope' })

In this example we are overriding the keyGenerator function and replacing the ${metadata.modelName} with ${metadata.scopeName}. Using this override, the key for your document would look like:

  • myScope::0477024c

To understand how ID differs from keys in Ottoman we need to explore creating a model, understand how Ottoman deals with IDs which affect your key and then how to retrieve your document by ID.

Defining a Model

...
const userSchema = new Schema({ name: string });
const User = model('User', userSchema);
  1. Set your rules in the Schema.
  2. Now you can create your Model with the Schema defined.

Creating a Document

Let see how Ottoman handles a new document creation.

How to Use

::: tip Notice Using Ottoman you only need to think about id in order to execute CRUD Operations over documents. All the key management will be automatically handled by Ottoman. :::

Retrieving a Document

Ottoman provides a findById method at the Model level to retrieve a document by id. See the picture below to understand how it works.

How to Use

Ottoman vs NodeJS SDK

In this section we will be reviewing some methods of the Ottoman Model, and comparing them with their respective implementation in the Couchbase NodeJs SDK.

The environment on which these tests were run has the following characteristics:

InfoProperties
ProcessorIntel(R) Core(TM) i7-9750H CPU @ 2.60GHz
RAM16.0 GB
System type64-bit operating system, x64-based processor
StorageSSD 500GB
SOWindows 10
CouchbaseEnterprise Edition 7.0.0
Couchbase Node.js SDKv3.2.2
Ottomanv2.0.0-beta.9
About metrics

For metrics we run each example around a thousand times and take the average tests run time and heapUsed*

*heapUsed: is the actual memory used during the process execution, according to the documentation, the memory used by "C++ objects bound to JavaScript objects managed by V8"

Ottoman Schema and Model Definitions

We will use the data model corresponding to the type of airport collection, within the travel-sample bucket and scope inventory.

// Define Geolocation Schema will be part of Airport Schema
const GeolocationSchema = new Schema({
alt: Number,
lat: { type: Number, required: true },
lon: { type: Number, required: true },
accuracy: String,
});

// Define Airport Schema
const AirportSchema = new Schema({
airportname: { type: String, required: true },
city: { type: String, required: true },
country: { type: String, required: true },
faa: String,
geo: GeolocationSchema,
icao: String,
tz: { type: String, required: true },
});

Our model would look like this:

// Define Airport model instance
const AirportModel = model(
'airport', // Model name (collection)
AirportSchema, // Schema defined
{
modelKey: 'type', // Ottoman by default use `_type`
scopeName: 'inventory', // Collection scope
keyGeneratorDelimiter: '_', // By default Ottoman use ::
});

Ottoman vs SDK Connection

Create our Ottoman instance and connect:

const ottoman = new Ottoman();
await ottoman.connect({
connectionString: 'couchbase://127.0.0.1',
bucketName: 'travel-sample',
username: 'Administrator',
password: 'password',
});

Couchbase NodeJs SDK cluster definition:

const cluster = new couchbase.Cluster('couchbase://127.0.0.1', {
username: 'Administrator',
password: 'password',
});

const bucket = cluster.bucket('travel-sample');
const collection = bucket.scope('inventory').collection('airport');

Model find

  • Let's see a simple filterless search implementation.

SDK:

const query = `
SELECT *
FROM \`travel-sample\`.\`inventory\`.\`airport\`
WHERE type = "airport"
LIMIT 1;
`;
try {
const result = await cluster.query(query);
console.log('Result:', JSON.stringify(result.rows, null, 2));
} catch(error) {
console.error('Query failed: ', error);
}

Ottoman:

try {
const result = await AirportModel.find({}, { limit: 1 });
console.log('Result:', JSON.stringify(result.rows, null, 2));
} catch(error) {
console.error('Query failed: ', error);
}

Output:

Result: [
{
"airport": {
"id": 1254,
"type": "airport",
"airportname": "Calais Dunkerque",
"city": "Calais",
"country": "France",
"faa": "CQF",
"icao": "LFAC",
"tz": "Europe/Paris",
"geo": {
"lat": 50.962097,
"lon": 1.954764,
"alt": 12
}
}
}
]
Metrics
LibraryTime (ms)Heap Use (MB)
Ottoman6.89.1
NodeJs SDK69.6
  • Now using some conditions and sort:

SDK:

const query = `
SELECT airportname
FROM \`travel-sample\`.\`inventory\`.\`airport\`
WHERE
country = "France" AND
airportname LIKE "Aix%" AND
type = "airport"
ORDER BY airportname ASC
`;
try {
const result = await cluster.query(query);
console.log('Result:', JSON.stringify(result.rows, null, 2));
} catch(error) {
console.error('Query failed: ', error);
}

Ottoman:

try {
const result = await AirportModel.find(
{ country: 'France', airportname: { $like: 'Aix%' } },
{ select: ['airportname'], sort: { airportname: 'ASC' } },
);
console.log('Result:', JSON.stringify(result.rows, null, 2));
} catch (error) {
console.error('Query failed: ', error);
}

Output:

Result: [
{
"airportname": "Aix Les Bains"
},
{
"airportname": "Aix Les Milles"
}
]
Metrics
LibraryTime (ms)Heap Use (MB)
Ottoman3.479.1
NodeJs SDK3.439.78

Model findOneAndUpdate

SDK:

const query = `
SELECT META().id as id
FROM \`travel-sample\`.\`inventory\`.\`airport\`
WHERE airportname LIKE "Aix Les Bains%"
AND type = "airport"
LIMIT 1
`;

try {
const response = await cluster.query(query);
const key = response.rows[0].id;
const { cas, value } = await collection.get(key);

await collection.replace(key,
{ ...value, airportname: 'Aix Les Bains Updated' },
{ cas }
);
const result = await collection.get(key);
console.log('Result:', JSON.stringify(result.content, null, 2));
} catch (error) {
console.error('Query failed: ', error);
}

Ottoman:

try {
const result = await AirportModel.findOneAndUpdate(
{ airportname: { $like: 'Aix Les Bains%' } },
{ airportname: 'Aix Les Bains Updated' },
{ new: true }, // To get updated object
);
console.log('Result:', JSON.stringify(result, null, 2));
} catch (error) {
console.error('Query failed: ', error);
}

Output:

Result: {
"airportname": "Aix Les Bains",
"city": "Chambery",
"country": "France",
"faa": "CMF",
"geo": {
"lat": 45.63805,
"lon": 5.880225,
"alt": 779
},
"icao": "LFLB",
"id": 1329,
"tz": "Europe/Paris",
"type": "airport"
}
Metrics
LibraryTime (ms)Heap Use (MB)
Ottoman10.0810.49
NodeJs SDK11.067.95

Model findOneAndRemove

SDK:

const query = `
SELECT META().id as id
FROM \`travel-sample\`.\`inventory\`.\`airport\`
WHERE airportname LIKE "Aix Les Bains%"
AND type = "airport"
LIMIT 1
`;

try {
const response = await cluster.query(query);
const key = response.rows[0].id;
const { cas } = await collection.get(key);
const result = await collection.remove(key, { cas });
console.log(`RESULT: `, result);
} catch (error) {
console.error('Query failed: ', error);
}

Output:

RESULT:  MutationResult {
cas: CbCas { '0': <Buffer 00 00 09 f4 de 87 a2 16> },
token: undefined
}

Ottoman:

In Ottoman using findOneAndRemove method we will have the document deleted as a result:

try {
const result = await AirportModel.findOneAndRemove(
{ airportname: { $like: 'Aix Les Bains%' } }
);
console.log('Result:', JSON.stringify(result, null, 2));
} catch (error) {
console.error('Query failed: ', error);
}

Output:

Result: {
"airportname": "Aix Les Bains",
"city": "Chambery",
"country": "France",
"faa": "CMF",
"geo": {
"lat": 45.63805,
"lon": 5.880225,
"alt": 779
},
"icao": "LFLB",
"id": 1329,
"tz": "Europe/Paris"
}
Metrics
LibraryTime (ms)Heap Use (MB)
Ottoman11.4411.047
NodeJs SDK14.867.95*

Bulk Operations

Bulk operations describes how to operate on more than one document at the same time, this is a way to increasing network performance in to pipeline operations with Batching Operations. As Node.js inherently performs all operations in an asynchronous manner, no special implementation is required in order to enable batching. Simply perform a number of operations simultaneously, and they will be batched on the network. This happens internally and is highly efficient. Let's see in action with Ottoman and Node SDK:

Create Many Documents

For our example implementation we will use bulkAirportDocs array as defined next:

const bulkAirportDocs = [
{
airportname: 'Airport test 1',
city: 'City A1',
country: 'Country A1',
faa: 'CA1',
geo: {
lat: 0.0,
lon: 0.0,
alt: 0,
},
icao: 'CAC1',
tz: 'Europe/Paris',
},
{
airportname: 'Airport test 2',
city: 'City A2',
country: 'Country A2',
faa: 'CA2',
geo: {
lat: 0.0,
lon: 0.0,
alt: 0,
},
icao: 'CAC2',
tz: 'Europe/Paris',
},
];

SDK

In the next example we will be using Insert operation for write a JSON document with a given ID (key) to the database applying batching operation strategy:

// Note that this behaviour extends to the world of async/await,
// such that following would be automatically batched across the network:
try {
const result = await Promise.all(
bulkAirportDocs.map(async (doc) => {
await collection.insert(`airport_${ doc.id }`, doc);
const newDoc = await collection.get(`airport_${ doc.id }`);
return newDoc.content;
}),
);
console.log(`RESULT: `, JSON.stringify(result, null, 2));
} catch(error) {
console.error('Query failed: ', error);
}

Output:

[
{
"id": "b6027254-7071-4e76-9303-aa18f3b93bcd",
"airportname": "Airport test 1",
"city": "City A1",
"country": "Country A1",
"faa": "CA1",
"geo": {
"lat": 0,
"lon": 0,
"alt": 0
},
"icao": "CAC1",
"tz": "Europe/Paris"
},
{
"id": "d4331304-e321-45cb-89d2-23530240b9bd",
"airportname": "Airport test 2",
"city": "City A2",
"country": "Country A2",
"faa": "CA2",
"geo": {
"lat": 0,
"lon": 0,
"alt": 0
},
"icao": "CAC2",
"tz": "Europe/Paris"
}
]

OTTOMAN

The way to do the same example above in Ottoman is as simple as using Model's createMany function as we can see below:

try {
const result = await AirportModel.createMany(bulkAirportDocs);
console.log(JSON.stringify(result, null, 2));
} catch (error) {
console.error('Query failed: ', error);
}

Output:

{
"status": "SUCCESS",
"message": {
"success": 2,
"match_number": 2,
"errors": [],
"data": [
{
"airportname": "Airport test 1",
"city": "City A1",
"country": "Country A1",
"faa": "CA1",
"geo": {
"lat": 0,
"lon": 0,
"alt": 0
},
"icao": "CAC1",
"tz": "Europe/Paris",
"id": "b6027254-7071-4e76-9303-aa18f3b93bcd",
"type": "airport"
},
{
"airportname": "Airport test 2",
"city": "City A2",
"country": "Country A2",
"faa": "CA2",
"geo": {
"lat": 0,
"lon": 0,
"alt": 0
},
"icao": "CAC2",
"tz": "Europe/Paris",
"id": "d4331304-e321-45cb-89d2-23530240b9bd",
"type": "airport"
}
]
}
}

Conclusions

As can be appreciated for most of the search operations through filters in the Couchbase SDK, we must define our N1QL sentences while Ottoman internally takes care of it. Ottoman provides a method of interacting with a database using an object-oriented language, although it also allows the direct use of N1QL queries. Developers can interact with the database without becoming an expert in N1QL, which can save time and keep code streamlined, especially when is not that familiar with N1QL language, eliminating repetitive code and reducing the time taken in repetitive development tasks such as changes to the object model would be made in one place. The Ottoman abstraction layer allows us to focus exclusively on the use of the ODM and leave the management and interaction of the low-level database to it, however, insulates the developer from that layer, at times can potentially make it difficult to solve low-level problems.