C Sample App Backend Tutorial

This page will walk you through how some of the queries may be constructed using the C SDK. The sample app uses the C API with a C++ JSON library (json-cpp), though you are free to use your own.

This tutorial focuses on querying through N1QL and FTS rather than views. If you want information about using views, see Views.

Walking Through the API

The following sections lead you through the primary functions of the sample application.

This shows you how to use the various features and services of Couchbase including: connecting to a cluster and bucket, key/value iteraction, document query through N1QL and full text searches.

Configure and Bootstrap the SDK

The first step is to let the application connect to your cluster and obtain a reference to a lcb_t (the Bucket is the entry point for the whole storage API).

Connecting to a Bucket

lcb_t instance;
lcb_create_st options;
memset(&options, 0, sizeof options);
options.version = 3;
options.v.v3.connstr = "couchbase://localhost/travel-sample";
/* Username and password for Couchbase 5.0+ */
options.v.v3.username = "mark";
options.v.v3.passwd = "secret";
lcb_create(&instance, &options);
lcb_connect(instance);
lcb_wait(instance);
if (lcb_get_bootstrap_status(instance) != LCB_SUCCESS) {
    printf("Error while bootstrapping: %s\n", lcb_strerror(NULL, lcb_get_bootstrap_status(instance)));
    // .. handle error further..
}
You could make the bootstrap process safer by providing a list of hostnames/IPs to bootstrap from (in case the one node you provided for bootstrap is unavailable when creating the Cluster reference). In production the best practice is to provide at least 3 nodes in the boostrap list. You can separate the list of hostnames in the connection string using a comma, e.g. couchbase://host1,host2,host3

The lcb_create function creates the raw library handle. The lcb_connect function starts the connection attempt. The lcb_wait function must be called to actually wait for the connection to complete. Finally, the status of the initial bootstrapping connection can be obtained using lcb_get_bootstrap_status. If successful, it will return LCB_SUCCESS; otherwise a textual description of the error can be obtained using the lcb_strerror function.

Manage Users using Key/Value API

Couchbase Server is a document-oriented database which provides access to your data both through its document ID (for high performance access), as well as through views, N1QL (as powerful query languages) and FTS.

Creating New Users

Next, prepare the content for the new user (as a JSON object) and the associated document (in order to give it an ID):

Json::Value doc(Json::objectValue);
std::string username = get_user();
doc["type"] = "user";
doc["name"] = username;
// make_pass_hash() is just a stub.
doc["password"] = make_pass_hash(password);

std::string docbuf(Json::FastWriter().write(doc));
std::string docid(std::string("user::") + username);

lcb_CMDSTORE scmd = { 0 };
LCB_CMD_SET_KEY(&scmd, docid.c_str(), docid.size());
LCB_CMD_SET_VALUE(&scmd, docbuf.c_str(), docbuf.size());
// Ensure you've set a callback using lcb_install_callback3(instance, LCB_CALLBACK_STORE, your_callback);
The "user::" prefix is arbitrary to this application, this is just a convention that the app uses to obtain unique keys and have additional information in it, but the key could have been anything (even sequence numbers or UUIDs).

Now it is time to use the Couchbase Server key/value API to store the document. Note that you need to install a callback to check the result of the storage operation (using lcb_install_callback3), in case it failed.

lcb_store3(instance, NULL, &scmd);

When it comes to storing a document, you have three choices of operation, which you can set using the lcb_CMDSTORE::operation field

  • LCB_ADD will only work if no document currently exists for the given ID, otherwise a LCB_KEY_EEXISTS will be received in the callback.

  • LCB_REPLACE on the contrary will only work if the document does already exist (otherwise a LCB_KEY_ENOENT will be received in the callback).

  • LCB_SET will always work, replacing or creating the document as needed. This is the default operation.

Checking Login by Getting the User’s Document

std::string docid(std::string("user::") + username);
lcb_CMDGET cmd = { 0 };
LCB_CMD_SET_KEY(&cmd, docid.c_str(), docid.size());
lcb_get3(instance, NULL, &cmd);
lcb_wait(instance);

Most of the action takes place in the callback:

static void get_callback(lcb_t, int, const lcb_RESPGET* resp) {
    if (resp->rc == LCB_KEY_ENOENT) {
        // User doesn't exist. Handle this!
    } else if (resp->rc != LCB_SUCCESS) {
        // Other error!?
    }

    Json::Value value;
    const char *encoded = reinterpret_cast<const char *>(resp->value);
    // Decode JSON
    if (!Json::Reader().parse(encoded, encoded + resp->nvalue, value)) {
        // Parse error!
    }

    std::string input_pass = get_input_pass(resp->cookie);
    if (make_pass_hash(input_pass) != value["password"]) {
        // Password doesn't match!
    }
    // More handling here
}

A First N1QL Query: Finding Airports

In the SDK, there is a lcb_n1ql_query function that can issue N1QL queries against Couchbase. The function accepts an lcb_CMDN1QL structure which contains the encoded query. You can use the lcb_N1QLPARAMS structure and its associated functions to help you construct the encoded query. If you’re using C++ (as the sample application is), it might be simpler to simply encode the query per the specification.

N1QL is a super-set of SQL, so if you’re familiar with SQL you’ll feel at ease.

Only the airport names are required for this part of the application, therefore just the airport name from relevant documents in the bucket should be selected. As the application needs to filter relevant document on a criteria that depends on the input length, the SELECT and FROM clauses are performed first:

std::string stmt("SELECT airportname FROM ");
stmt.append("`").append("travel-sample").append("`"); // Backticks, because '-' in the bucket name must be escaped
stmt.append(" WHERE ");

Then the correct fields can be chosen to look into, depending on the length of the input.

std::string query_arg;
if (params.size() == 3) {
    stmt.append("faa = $1");
    query_arg = params;
} else if (params.size() == 4) {
    stmt.append("icao = $1");
    query_arg = params;
} else {
    stmt.append("airportname LIKE $1");
    query_arg = "%" + params + "%";
}

// Now encode everything
Json::Value query(Json::objectValue);
query["statement"] = stmt;
query["args"].append(query_arg);

std::string encoded(Json::FastWriter().write(query));

Then the statement is actually executed:

lcb_CMDN1QL cmd = { 0 };
cmd.query = query.c_str();
cmd.nquery = query.size();
cmd.callback = query_callback; // We'll show this function soon
if (lcb_n1ql_query(instance, NULL, &cmd) != LCB_SUCCESS) {
    // Handle error
}
lcb_wait(instance);

query_callback then handles the results.

static void query_callback(lcb_t, int, const lcb_RESPN1QL *resp) {
    if (resp->rc != LCB_SUCCESS) {
        // Problem! Handle this
    }
    if (resp->rflags & LCB_RESP_F_FINAL) {
        // Last response in sequence. All rows have already been received
    }

    // Normal response:
    Json::Value json;
    // Decode the row as JSON
    Json::Reader().parse(resp->row, resp->row + resp->nrow, json);

    std::cout << json["airportname"] << std::endl;
}

The query callback is invoked once for each result row received. It is invoked one last time with the LCB_RESP_F_FINAL flag set (in the response’s rflags field) as a terminator to indicate that no more rows remain.

More Complex Queries: Finding Routes

In this service, there are two more complex queries. The first aims at transforming the human-readable airport name for the departure and arrival airports to FAA codes:

SELECT faa AS fromAirport FROM `travel-sample` WHERE airportname = "Los Angeles Intl"
UNION SELECT faa AS toAirport FROM `travel-sample` WHERE airportname = "San Francisco Intl"

The second aims at constructing the result set of available flight paths that connect the two airports:

SELECT a.name, s.flight, s.utc, r.sourceairport, r.destinationairport, r.equipment
FROM `travel-sample` AS r
UNNEST r.schedule AS s
JOIN `travel-sample` AS a ON KEYS r.airlineid
WHERE r.sourceairport = "LAX" AND r.destinationairport = "SFO" AND s.day = 6
ORDER BY a.name ASC

A specificity of N1QL that can be seen in the second statement is UNNEST. It extracts a sub-JSON object and puts it at the same root level as the bucket, so its possible to do joins on each element in this sub-JSON as if they were entries in a left-hand side bucket.

Indexing the Data: N1QL & GSI

Index management is a bit more advanced (and is already done when loading the sample), so now that you’ve learned the bsaics of N1QL, you can have a look at it. For N1QL to work, you must first ensure that at least a Primary Index has been created. For that you can issue the query:

CREATE PRIMARY INDEX ON `travel-sample`

Refer to the above example on how to execute this query from the SDK. You’ll still need a callback, though there will be no result rows (but the final callback will be invoked always).

You can also create secondary indexes on specific fields of the document, for better performance:

CREATE INDEX `def_username` ON `travel-sample`(username)

In this case, give a name to your index (def_username), specify the target bucket (travel-sample) AND the field(s) in the JSON to index (username).

Full Text Search: Finding Hotels

In this service, hotels are searched for using more fuzzy criterias, like the content of the address or the description of a hotel. This is done using Full Text Search (FTS). When some results match the specified criteria, only the relevant data for each result to be displayed in the UI is fetched using the subdocument API.

To find a hotel based on its location and its description, first a JSON query body is created:

Json::Value query(Json::objectValue);
query["query"]["conjuncts"] = Json::Value(Json::arrayValue);
Json::Value typeQuery(Json::objectValue);
typeQuery["term"] = "hotel";
typeQuery["field"] = "type";
query["query"]["conjuncts"].append(typeQuery)

A conjunction query allows you to combine multiple FTS queries into one, in an AND fashion. This query always includes an exact match criteria that restricts it to the hotel data type (as reflected in the type field of the JSON document).

If the user provided a location keyword, a second component is added to the FTS query that will look for that keyword in several address-related fields of the document. This is done in an OR fashion, using disjuncts:

if (!location.empty() && location != "*") {
    Json::Value locationQuery(Json::objectValue);
    Json::Value disjuncts(Json::objectValue);
    disjuncts["disjuncts"] = Json::Value(Json::arrayValue);
    Json::Value matchPhrase(Json::objectValue);

    matchPhrase["match_phrase"] = location
    std::array<const char*, 4> fields({"country", "city", "state", "address"});
    for (const auto ptr : fields) {
        matchPhrase["field"] = ptr;
        disjuncts.append(matchPhrase);
    }
    query["query"]["conjuncts"].append(disjuncts);
}
if (!description.empty() && description != "*") {
    Json::Value disjuncts(Json::objectValue);
    std::array<const char *, 2> fields({"description", "name"});
    for (const auto field : fields) {
        Json::Value matchPhrase(Json::objectValue);
        matchPhrase["field"] = field;
        matchPhrase["match_phrase"] = description;
    }
    query["query"]["conjuncts"].append(disjuncts);
}

Before the query is executed, you can limit the number of results to be returned:

query["size"] = 100;

The compound FTS query is now ready to be executed.

lcb_CMDFTS cmd = { 0 };
std::string buf(Json::FastWriter().write(query));
cmd.query = buf.c_str();
cmd.nquery = buf.size();
cmd.callback = search_callback; // Defined later
lcb_fts_query(instance, NULL, &cmd);
lcb_wait(instance);

The second step of working with hotels is done inside the callback. The callback is very similar to the N1QL callback.

The FTS are iterated over in the callback, and the document corresponding to each result is fetched. In actuality, only the parts of the document that will be displayed in the UI are required. This is where the sub-document API comes in.

The sub-document API allows you to fetch or mutate only a set of paths inside a JSON document, without having to send the whole document back and forth. This can save network bandwidth if the document is large and the parts that we’re interested in are small. The callback iterates over each result of the FTS search then triggers a subdoc call:

static void search_callback(lcb_t instance, int, const lcb_RESPFTS* resp) {
    if (resp->rc != LCB_SUCCESS) {
        // ...
    }
    if (resp->rflags & LCB_RESP_F_FINAL) {
        // ...
    }

    Json::Value row;
    Json::Reader().parse(resp->row, resp->row + resp->nrow, row);
    std::string docid(row["id"]);
    // Fetch the various subdoc fields:
    std::vector<lcb_SDSPEC> specs;
    std::array<const char *, 6> fields({"country", "city", "state", "address", "name", "description"});
    for (auto field : fields) {
        lcb_SDSPEC spec = { 0 };
        spec.sdcmd = LCB_SDCMD_GET;
        LCB_SDSPEC_SET_PATH(&spec, field, strlen(field));
        specs.push_back(spec);
    }
    lcb_CMDSUBDOC cmd = { 0 };
    LCB_CMD_SET_KEY(&cmd, docid.c_str(), docid.size());
    cmd.specs = &specs[0];
    cmd.nspecs = specs.size();
    lcb_subdoc3(instance, NULL, &cmd);
    // Note, the above requires that the subdoc callback has been installed.
}
static void subdoc_callback(lcb_t, int, const lcb_RESPSUBDOC *resp) {
    if (resp->rc != LCB_SUCCESS) {
        // Couldn't get hotel description!
    }
    // Fields are retrieved in order; so the first field is 'country',
    // the second is 'city', and so on:
    lcb_SDENTRY ent;
    size_t iter = 0;
    lcb_sdresult_next(resp, &ent, &iter);
    printf("Country is: %.*s\n", ent.value, ent.nvalue);
    // and so on..
}