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:

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 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));
                    }
                }"
        };
    }
}
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.