Skip to main content

JavaScript Indexes

Creating and deploying a JavaScript index

Creating a JavaScript index:

  • To create a JavaScript index, define a class that inherits from AbstractJavaScriptIndexCreationTask.
  • This base class itself inherits from AbstractIndexCreationTask, which is the base class for all C# indexes.
public class Documents_ByName_JS : AbstractJavaScriptIndexCreationTask
{
Maps = new HashSet<string>()
{
// Define a map function:
@"map(<CollectionName>, function(doc) {
return {
Name: doc.Name
// ...
}
})",

// ...
};
}

Deploying a JavaScript index:

Map index

  • A map index contains a single map function.
    To define an index that uses multiple map functions, see the section on Multi-Map indexes below.

  • The map function is written as a string and specifies what content from the documents will be indexed.

Example I - Map index - basic

The following index indexes the FirstName and LastName of employees from the Employees collection.

public class Employees_ByFirstAndLastName_JS : AbstractJavaScriptIndexCreationTask
{
public class IndexEntry
{
public string FirstName { get; set; }
public string LastName { get; set; }
}

public Employees_ByFirstAndLastName_JS()
{
Maps = new HashSet<string>
{
// Define the 'map' function:
// Index content from documents in the 'Employees' collection
@"map('Employees', function (employee) {

// Provide your JavaScript code here
// Return an object that defines the index-entry:
// ==============================================

return {
// Define the index-fields:
// ========================

FirstName: employee.FirstName,
LastName: employee.LastName
};
})",
};
}
}

Query the index:
Once the index is deployed, you can query for Employee documents based on the indexed name fields.

List<Employee> employees = session
// Query the map index
.Query<Employees_ByFirstAndLastName_JS.IndexEntry,
Employees_ByFirstAndLastName_JS>()
.Where(x => x.LastName == "King")
.OfType<Employee>()
.ToList();

Example II - Map index - with additional sources

  • The following index indexes the names of all comment authors (including nested replies) for each BlogPost document.

  • It uses getNames, a recursive helper defined in AdditionalSources, to traverse every comment level and accumulate author names.

public class BlogPosts_ByCommentAuthor_JS : AbstractJavaScriptIndexCreationTask
{
public class IndexEntry
{
public string[] Authors { get; set; }
}

public BlogPosts_ByCommentAuthor_JS()
{
Maps = new HashSet<string>()
{
@"map('BlogPosts', function(post) {
const names = [];

// Get names of authors from the additional source code:
if (post.Comments) {
post.Comments.forEach(x => getNames(x, names));
}

return {
Authors: names
};
})"
};

AdditionalSources = new Dictionary<string, string>
{
["The getNames method"] = @"
function getNames(comment, names) {
names.push(comment.Author);

if (comment.Comments) {
comment.Comments.forEach(x => getNames(x, names));
}
}"
};
}
}

Example III - Map index - with inline string compilation

  • To define a JavaScript index using inline string compilation,
    you must set the Indexing.AllowStringCompilation configuration key to true.

  • The following indexes use inline string compilation to evaluate whether each product’s UnitsInStock is low.

public class Products_ByStock1_JS : AbstractJavaScriptIndexCreationTask
{
public class IndexEntry
{
public string FirstName { get; set; }
public string LastName { get; set; }
}

public Products_ByStock1_JS()
{
Maps = new HashSet<string>
{
@"map('Products', function(product) {
// Define a string expression to check for low stock.
const functionBody = 'return product.UnitsInStock < 10';

// Create a dynamic function that evaluates the expression at runtime.
const dynamicFunc = new Function(""product"", functionBody);

return {
StockIsLow: dynamicFunc(product)
};
});",
};

// Enable string‑compilation so this index can execute the inline script
Configuration["Indexing.AllowStringCompilation"] = "true";
}
}

Learn more about Map indexes here.

Multi-Map index

  • A Multi-Map index allows indexing data from multiple collections.

  • For example, the following index processes documents from both the Cats and Dogs collections.

public class Animals_ByName_JS : AbstractJavaScriptIndexCreationTask
{
public class IndexEntry
{
public string Name { get; set; }
}

public Animals_ByName_JS()
{
Maps = new HashSet<string>()
{
// Define a map function on the 'Cats' collection
@"map('Cats', function(c) { return { Name: c.Name }})",

// Define a map function on the 'Dogs' collection
@"map('Dogs', function(d) { return { Name: d.Name }})"
};
}
}

Query the index:
Once the index is deployed, querying it will return matching documents from both the Cats and Dogs collections.

var animalsNamedMilo = session
// Query the multi-map index
.Query<Animals_ByName_JS.IndexEntry, Animals_ByName_JS>()
.Where(x => x.Name == "Milo")
.ToList();

Learn more about Multi-Map indexes here.

Map-Reduce index

  • A Map-Reduce index allows you to perform complex data aggregations.

  • In the Map stage, the index processes documents and extracts relevant data using the defined mapping function(s).

  • In the Reduce stage, the map results are aggregated to produce the final output.

Example I

The following index counts the number of products per category by grouping on the category name.

public class Products_ByCategory_JS : AbstractJavaScriptIndexCreationTask
{
public class IndexEntry
{
public string Category { get; set; }
public int Count { get; set; }
}

public Products_ByCategory_JS()
{
// The Map stage:
// For each product document -
// * load its related Category document using the 'load' function,
// * extract the category name, and return a count of 1.
Maps = new HashSet<string>()
{
@"map('Products', function(p) {
return {
Category: load(p.Category, 'Categories').Name,
Count: 1
}
})"
};

// The Reduce stage:
// * group the mapped results by Category
// * and count the number of products in each category.
Reduce = @"groupBy(x => x.Category).aggregate(g => {
return {
Category: g.key,
Count: g.values.reduce((count, val) => val.Count + count, 0)
};
})";
}
}

Query the index:
Once the index is deployed, you can query for the total number of products per category,
and optionally, order the results by product count in descending order.

var topCategories = session
// Query the map-reduce index
.Query<Products_ByCategory_JS.IndexEntry, Products_ByCategory_JS>()
.OrderByDescending(x => x.Count)
.ToList();

Example II

The following index calculates how many items were sold and the total sales amount for each product and month.

public class ProductSales_ByMonth_JS : AbstractJavaScriptIndexCreationTask
{
public class IndexEntry
{
public string Product { get; set; }
public DateTime Month { get; set; }
public int Count { get; set; }
public decimal Total { get; set; }
}

public ProductSales_ByMonth_JS()
{
// The Map stage:
// For each order, emit one entry per line with:
// * the product,
// * the first day of the order’s month,
// * a count of 1,
// * and the line’s total value.
Maps = new HashSet<string>()
{
@"map('orders', function(order) {
var res = [];
var orderDate = new Date(order.OrderedAt);

order.Lines.forEach(l => {
res.push({
Product: l.Product,
Month: new Date(orderDate.getFullYear(), orderDate.getMonth(), 1),
Count: 1,
Total: (l.Quantity * l.PricePerUnit) * (1- l.Discount)
})
});

return res;
})"
};

// The Reduce stage:
// Group by product and month, then sum up counts and totals.
Reduce = @"
groupBy(x => ({Product: x.Product, Month: x.Month}))
.aggregate(g => {
return {
Product: g.key.Product,
Month: g.key.Month,
Count: g.values.reduce((sum, x) => x.Count + sum, 0),
Total: g.values.reduce((sum, x) => x.Total + sum, 0)
}
})";

// Output the reduce results into a dedicated collection
OutputReduceToCollection = "MonthlyProductSales";
PatternReferencesCollectionName = "MonthlyProductSales/References";
PatternForOutputReduceToCollectionReferences = "sales/monthly/{Month}";
}
}

Learn more about Map-Reduce indexes here.