JavaScript Indexes
-
JavaScript indexes are intended for users who prefer to define static indexes using JavaScript instead of C#.
-
The RavenDB JavaScript engine supports ECMAScript 5.1 syntax.
In addition, RavenDB provides a set of predefined JavaScript functions that can be used in JavaScript indexes and in other features such as subscriptions, ETL scripts, set-based patching, and more.
See the full list in Predefined JavaScript functions.
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:
-
Deploy a JavaScript index using the standard creation flow as described in Deploy a static-index.
-
Note that JavaScript indexes can be deployed using a User/Read-Write certificate,
while C# static indexes require a User/Admin certificate or higher. -
To restrict the creation of JavaScript indexes to database admins (and above),
set the Indexing.Static.RequireAdminToDeployJavaScriptIndexes configuration totrue
. -
All other capabilities and features of JavaScript indexes are identical to those of C# indexes.
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();
from index "Employees/ByFirstAndLastName/JS"
where LastName == "King"
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 inAdditionalSources
, 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));
}
}"
};
}
}
public class BlogPost
{
public string Author { get; set; }
public string Title { get; set; }
public string Text { get; set; }
public List<BlogPostComment> Comments { get; set; }
}
public class BlogPostComment
{
public string Author { get; set; }
public string Text { get; set; }
public List<BlogPostComment> Comments { get; set; }
}
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";
}
}
public class Products_ByStock2_JS : AbstractJavaScriptIndexCreationTask
{
public class IndexEntry
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public Products_ByStock2_JS()
{
Maps = new HashSet<string>
{
@"map('Products', function(product) {
// Define a string expression with your condition
const expression = 'product.UnitsInStock < 10';
// Evaluate the string expression at runtime using eval.
const isLowOnStock = eval(expression);
return {
StockIsLow: isLowOnStock
};
});",
};
// 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();
from index "Animals/ByName/JS"
where Name == "Milo"
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();
from index "Products/ByCategory/JS"
order by Count as long desc
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.