Query Syntax

Queries are a powerful tool for enabling your players to find the most relevant opponents when using the Matchmaker and the most suitable multiplayer matches when listing available matches. A query expression defines the user’s criteria for finding their opponents or available matches - think of it as a big AND statement - with the query then inspecting the Matchmaker properties or match label, respectively, to deliver the results.

Querying the match label is only possible if the label is a JSON value.

Queries are composed of one or more query terms using the syntax field:value. A query can contain any number of terms with each term separated by a space, for example field:value field1:value1.

The same syntax can be used for any value type, for example:

  • Strings: region:europe
  • Numbers: level:10
  • Numeric Ranges: rank:<=5
  • Date Ranges: created:>"2021-12-25"

See the Operators section below to learn more about writing query terms, and Boosting for details about ordering results based on the weight given to individual query terms.

Matching

The standard syntax matches the whole value, meaning there must be equality between the query value and returned result, not just similarity. For example, a query of mode:free would not return matches with a mode of freeforall.

Queries also inspect individual elements inside an array for a match. For example, an indexed value: {"field": [5, 10, 15]} will be returned as a match for any query including a term field:5, field:10, or field:15.

Operators

There are three available operators when creating your query terms: SHOULD, MUST, and MUST NOT.

SHOULD

Should is the default operator used for query terms. In practice it means that the results should have the given value, but it is not a strict requirement.

For example, if you are searching for opponents using a query of region:europe the results will include good results - meaning opponents in the Europe region - if available but will also include opponents from any other region if not enough good results can be found.

MUST

The MUST operator, indicated by a + at the start of a query term, enforces a string requirement of the given value. Using the same example as above but with the MUST operator (+region:europe) means that only opponents from the Europe region will be returned in the result and, if not enough are available, no result is returned.

MUST NOT

The MUST NOT operator is indicated by a - at the start of the query term. It enforces a string requirement for the exclusion of the query term, i.e. any returned result will absolutely not have that value.

For example, if a user doesn’t want to see any matches with a level above 10, the query term could look like -level:>10.

Ranges

Queries can contain both numeric and date ranges as terms.

Numeric ranges can be used in query terms with the >, >=, <, and <= operators. These same operators can be used for date ranges with the distinction that the date be in quotes, for example created:>"2022-01-01" to return results created after January 1, 2022.

For date range queries, we recommend using UTC seconds/milliseconds where possible.

Boosting

Using the operators detailed above you can craft queries with multiple terms to describe if a given result is returned or not returned. In addition, you can optionally define an ordering for the returned results by “boosting” the relative importance of the terms in your query.

This is done using ^ and an arbitrary booster number at the end of your query term(s).

For example, if using the query mode:freeforall^2 mode:capturetheflag to search for matches, your results will include both Free for All and Capture the Flag matches, but the Free for All results will be listed higher (i.e. be a “better” match) than the Capture the Flag results.

Note that since we didn’t use the + operator this query may also return results that are neither Free for All or Capture the Flag matches, but those results will be lowest of all.

Escaping

The following character set must be escaped for accurate query results: +-=&|><!(){}[]^\"~*?:\\/ .

Note that the space character is included. For example, if you want to query for a “Capture The Flag” match use mode:Capture\ The\ Flag as opposed to mode:Capture The Flag. The latter query may still be valid but will produce the wrong results as it is searching for a match with a mode of “Capture”, and which contains “The” anywhere, and “Flag” anywhere.

Examples

Find a friend’s match

If the match label contains an array players with the user IDs of all players currently in the match, we can use the following query to find any match a given friend is a part of:

Server
1
2
3
4
5
6
7
8
local nk = require("nakama")

local query = "+label.players:<friend_user_id>"
local matches = nk.match_list(query)

for _, match in ipairs(matches) do
  nk.logger_info(string.format("Match id %s", match.match_id))
end
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
query := "+label.players:<friend_user_id>"
matches, err := nk.MatchList(ctx, query)

if err != nil {
    logger.WithField("err", err).Error("Match listings error.")
    return
}

for _, match := range matches {
    logger.Info("Match id %s", match.MatchId)
}
Server
1
2
3
4
5
6
7
8
function findFriendMatch(context: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama) {
  const query = "+label.players:<friend_user_id>";
  var matches = nk.matchList(query);

  matches.forEach(match => {
    logger.info("Match id '%s'", match.matchId);
  });
}

Find an open match

In this example, the match label contains an open value to indicate whether or not the match is accepting new players. This value would be updated in the match handler once the appropriate criteria has been met (e.g. enough players have joined or indicated that they are ready to begin).

Server
1
2
3
4
5
6
7
8
local nk = require("nakama")

local query = "+label.open:true label.game_mode:deathmatch"
local matches = nk.match_list(query)

for _, match in ipairs(matches) do
  nk.logger_info(string.format("Match id %s", match.match_id))
end
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
query := "+label.open:true label.game_mode:deathmatch"
matches, err := nk.MatchList(ctx, query)

if err != nil {
    logger.WithField("err", err).Error("Match listings error.")
    return
}

for _, match := range matches {
    logger.Info("Match id %s", match.MatchId)
}
Server
1
2
3
4
5
6
7
8
function findOpenMatches(context: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama) {
  const query = "+label.open:true label.game_mode:deathmatch";
  var matches = nk.matchList(query);

  matches.forEach(match => {
    logger.info("Match id '%s'", match.matchId);
  });
}

Matchmake around player level

In this example, we create a matchmaker ticket for a player with Skill level of 15 looking for 2-4 opponents with levels between 13 and 17:

Client
1
2
3
4
5
6
7
8
9
const query = "+properties.skill:>=13 +properties.skill:<=17";
const minCount = 2;
const maxCount = 4;

const numericProperties = {
  skill: 15
};

var ticket = await socket.addMatchmaker(query, minCount, maxCount, numericProperties);
Client
1
2
3
4
5
6
7
var query = "+properties.skill:>=13 +properties.skill:<=17";

var numericProperties = new Dictionary<string, int>() {
    {"skill", 15}
};

var matchmakerTicket = await socket.AddMatchmakerAsync(query, 2, 4, numericProperties);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
auto successCallback = [](const NMatchmakerTicket& ticket)
{
    std::cout << "Matchmaker ticket: " << ticket.ticket << std::endl;
};

int32_t minCount = 2;
int32_t maxCount = 4;
string query = "+properties.skill:>=13 +properties.skill:<=17";
NStringDoubleMap numericProperties;

numericProperties.emplace("skill", 15.0);

rtClient->addMatchmaker(minCount, maxCount, query, numericProperties, successCallback);
Client
1
2
3
4
5
6
7
8
9
String query = "+properties.skill:>=13 +properties.skill:<=17";
int minCount = 2;
int maxCount = 4;

Map<String, Double> numericProperties = new HashMap<String, Double>() {{
    put("skill", 15.0);
}};

MatchmakerTicket matchmakerTicket = socket.addMatchmaker(query, minCount, maxCount, numericProperties).get();
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var query = "+properties.skill:>=13 +properties.skill:<=17"
var numeric_properties = { "skill": 15 }

var matchmaker_ticket : NakamaRTAPI.MatchmakerTicket = yield(
    socket.add_matchmaker_async(query, 2, 4, numeric_properties),
    "completed"
)

if matchmaker_ticket.is_exception():
    print("An error occurred: %s" % matchmaker_ticket)
    return

print("Got ticket: %s" % [matchmaker_ticket])

Code snippet for this language Defold has not been found. Please choose another language to show equivalent examples.
Code snippet for this language cURL has not been found. Please choose another language to show equivalent examples.
Code snippet for this language REST has not been found. Please choose another language to show equivalent examples.

Boosting regional preference

In this example our player is located in the Europe region and prefers to matchmake with other players in the same region, followed by those in the Asia and Africa regions:

Client
1
2
3
4
5
6
7
8
9
const query = "properties.region:europe^3 properties.region:asia^2 properties.region:africa";
const minCount = 2;
const maxCount = 4;

const stringProperties = {
  region: "europe"
};

var ticket = await socket.addMatchmaker(query, minCount, maxCount, stringProperties);
Client
1
2
3
4
5
6
7
var query = "properties.region:europe^3 properties.region:asia^2 properties.region:africa";

var stringProperties = new Dictionary<string, string>() {
    {"region", "europe"}
};

var matchmakerTicket = await socket.AddMatchmakerAsync(query, 2, 4, stringProperties);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
auto successCallback = [](const NMatchmakerTicket& ticket)
{
    std::cout << "Matchmaker ticket: " << ticket.ticket << std::endl;
};

int32_t minCount = 2;
int32_t maxCount = 4;
string query = "properties.region:europe^3 properties.region:asia^2 properties.region:africa";
NStringMap stringProperties;

stringProperties.emplace("region", "europe");

rtClient->addMatchmaker(minCount, maxCount, query, stringProperties, successCallback);
Client
1
2
3
4
5
6
7
8
9
String query = "properties.region:europe^3 properties.region:asia^2 properties.region:africa";
int minCount = 2;
int maxCount = 4;

Map<String, String> stringProperties = new HashMap<String, String>() {{
    put("region", "europe");
}};

MatchmakerTicket matchmakerTicket = socket.addMatchmaker(query, minCount, maxCount, stringProperties).get();
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var query = "properties.region:europe^3 properties.region:asia^2 properties.region:africa"
var string_properties = { "region": "europe" }

var matchmaker_ticket : NakamaRTAPI.MatchmakerTicket = yield(
    socket.add_matchmaker_async(query, 2, 4, string_properties),
    "completed"
)

if matchmaker_ticket.is_exception():
    print("An error occurred: %s" % matchmaker_ticket)
    return

print("Got ticket: %s" % [matchmaker_ticket])

Code snippet for this language Defold has not been found. Please choose another language to show equivalent examples.
Code snippet for this language cURL has not been found. Please choose another language to show equivalent examples.
Code snippet for this language REST has not been found. Please choose another language to show equivalent examples.