© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
D. MiličićIntroducing RavenDBhttps://doi.org/10.1007/978-1-4842-8919-8_5

5. Map Indexes

Dejan Miličić1  
(1)
Novi Sad, Serbia
 

The previous chapter introduced the index, which almost all databases use to speed up and optimize queries. In this chapter, we will show how to take control of indexes. Instead of relying on RavenDB to create them automatically, you can specify them explicitly as “static” indexes. Along with ways to index one or multiple collections, you will also learn how to handle referenced documents and create stored, dynamic, and computed fields. Finally, we will cover techniques to index hierarchical data models in your database.

Static Indexes

In the previous chapter, we saw how RavenDB does not allow queries against raw data. Queries are always executed over indexes, and if you do not have one, RavenDB’s query optimizer will create one for you.

Executing query
from Employees where FirstName = 'Nancy'

will trigger the creation of the Auto/Employees/ByFirstName automatic index. Once created, this index will remain active after the query results are generated, and RavenDB will use it for all future filtering and ordering queries over the same field.

Raven Query Language allows you to query indexes directly, as shown in Listing 5-1
from index 'Auto/Employees/ByFirstName' where FirstName = 'Nancy'
Listing 5-1

Querying index directly

Such query will produce the same result as a previous query.

Hence, you can either use from [Collection] form and let the query optimizer choose for you or go with from index '[Index_name]' and take control over the selection of the index used to deliver results.

You can take control even further. Instead of relying on a query optimizer to create indexes, you can define it yourself. Such indexes are called static indexes.

Static Map Index

You can inspect the content of the Auto/Employees/ByFirstName automatic index by writing a query:
from index 'Auto/Employees/ByFirstName'
and then selecting Settings ➤ Show raw index entries instead of matching documents as visible in Figure 5-1.

A screenshot displays 1 query, results with columns preview, first name, and id, save button, search, settings drop-down box with 2 enabled among 4 toggle buttons, play, and delete documents button.

Figure 5-1

Content of Automatic Index

You can create a static index with the same content from Figure 5-1. Select the List of Indexes option in the left column, and then click on the New index button. You are now looking at a screen where you can define a new index, shown in Figure 5-2.

A screenshot displays a dialog box with save and cancel buttons at the top, a text box for index name, a drop-down button for deployment mode, and a list box for maps placed at the bottom.

Figure 5-2

Create a New Index Dialog

You first need to select a name for the new index. A convention is to go with the names of the form
[COLLECTION]/By[FIELD]

and you can already see it with Auto/Employees/ByFirstName.

Hence, use Employees/ByFirstName for the name.

Content of Map Index is a JavaScript function map that will take documents from the collection and return an object for every document; these objects will be index entries. Accordingly, the name map, since we are mapping documents to index entries . Listing 5-2 shows such mapping function.
map("Employees", function(emp) {
    return {
        FirstName: emp.FirstName
    }
})
Listing 5-2

Content of Employees/ByFirstName index

Clicking the Save button will create a new index, and it will be listed along with other automatic and static ones, as shown in Figure 5-3.

A screenshot of the indexes list with 2 sections, companies with 141 entries and employees with 9 entries on 2 sublists, query search, drop-down menu for index status, delete, play, lock, and add new index buttons.

Figure 5-3

List of Indexes

You can see that the automatic index Auto/Employees/ByFirstName contains nine entries, the same as the Employees/ByFirstName you just created. And if you query it
from index 'Employees/ByFirstName' where FirstName = 'Nancy'

you will get identical results.

Static Index Analysis

Let’s analyze all elements of an index from Listing 5-2. Looking at the top-level structure, you can see this map index has the following form:
map("[COLLECTION]", function(doc) {...})

The first argument is a collection name. One by one, the document will be fetched from that collection and passed to the JavaScript function, which is the second argument.

Looking at the function itself
function(emp) {
    return {
        FirstName: emp.FirstName
    }
}
you can see it accepts a document from the collection and returns JavaScript object literal:
{
    FirstName: emp.FirstName
}

This object literal will be indexing entry.

Index can be somewhat simplified if we replace traditional function expression with a compact alternative called arrow function expression as shown in Listing 5-3.
map("Employees", emp => {
    return {
        FirstName: emp.FirstName
    }
})
Listing 5-3

A simplified version of the Employees/ByFirstName index

Expanding Map Index

The structure of the index entries will limit fields we can filter. Attempting to execute the following query
from index 'Employees/ByFirstName' where LastName = 'Davolio'
will result in an error:
The field 'LastName' is not indexed, cannot query/sort on fields that are not indexed
Your first reaction might be to create new Employees/ByLastName index, as shown in Listing 5-4.
map("Employees", emp => {
    return {
        LastName: emp.LastName
    }
})
Listing 5-4

Employees/ByLastName index

This index now can be queried
from index 'Employees/ByLastName' where LastName = 'Davolio'

and will produce results .

However, instead of adding one more index, we can expand Employees/ByFirstName index with LastName field. Following conventions we previously introduced, we can name it Employees/ByFirstNameByLastName and define its content as shown in Listing 5-5.
map("Employees", emp => {
    return {
        FirstName: emp.FirstName,
        LastName: emp.LastName
    }
})
Listing 5-5

Employees/ByFirstNameByLastName index

Inspecting raw content of this index reveals FirstName and LastName indexed for ID of every document, as visible in Figure 5-4.

A screenshot of the index employees slash by first name by last name index displays a table of raw index entries with 4 columns and tabs on top namely delete documents, statistics, and export as C S V.

Figure 5-4

Raw Content of Index Employees/ByFirstNameByLastName

You can query this new index both on the FirstName field
from index 'Employees/ByFirstNameByLastName' where FirstName = 'Nancy'
and on LastName field:
from index 'Employees/ByFirstNameByLastName' where LastName = 'Davolio'.

This way, you can use just one index to cover multiple fields of documents in one collection.

Indexing References

In the previous section, you saw how to create an index from the field that contained values – employees were indexed by their first name. Let’s look at a slightly different scenario. If you open one of the orders, you will see they have references pointing to Company and Employee, as shown in Listing 5-6:
{
    "Company": "companies/65-A",
    "Employee": "employees/1-A",
    "Freight": 8.53,
    "Lines": [
...
Listing 5-6

Order orders/830-A

Following the same approach as we did for index Employees/ByFirstName, you can index Orders by the Employee field, presented in Listing 5-7:
map("Orders", order => {
    return {
        Employee: order.Employee
    }
})
Listing 5-7

Orders/ByEmployee index

Looking at indexing terms of this index, you can see identifiers of all employees extracted and indexed, as shown in Figure 5-5 .

A screenshot of the index terms for orders slash by employee with 2 sections, i d and employee that has 9 loaded data, employees slash 1-a to 9-a.

Figure 5-5

Index Terms for Orders/ByEmployee Index

Nancy Davolio’s ID is employees/1-A, so you can query Orders/ByEmployees to get all of her orders:
from index 'Orders/ByEmployee' where Employee = 'employees/1-a'

However, it would be much more natural if you could query by Nancy Davolio’s first name – that would not force you to check what her ID is. That would require an index to read an employee’s id, load that document, and read FirstName property.

RavenDB provides function load that you can use to achieve that. Improved version of Oders/ByEmployee index is shown in Listing 5-8 :
map("Orders", order => {
    var employee = load(order.Employee, 'Employees');
    return {
        EmployeeName: employee.FirstName
    }
})
Listing 5-8

Orders/ByEmployeeName index

You can now use this index to search orders by employee’s first name:
from index 'Orders/ByEmployeeName' where EmployeeName = 'Nancy'
Inspecting Listing 5-8, you can see the usage of the load function :
load(order.Employee, 'Employees')

The first argument is the document’s ID, and the second one is the document’s collection.

This index will react to changes over Orders document collection and recalculate. Every time you call the load function in an index, RavenDB will note that your index depends not only on the collection it is indexing but also on the collection from which referenced documents are loaded. Since we are loading Employee documents, changes to that collection will also trigger updates to the index .

Stored Fields

In the previous chapter, we introduced different types of indexes: nonclustered (holding only references to documents), clustered (storing whole documents), and covering ones (including a selected collection of fields). Static indexes we developed so far in this chapter are nonclustered. You can easily inspect that by running the following query:
from index 'Employees/ByFirstNameByLastName'
After getting employee documents, click on Settings and select the option Show stored index fields only. You will get a result shown in Figure 5-6 .

A screenshot displays a query, a settings menu with 2 enabled among 3 toggle buttons, search query, and save and play buttons. The results panel displays no data during the preview of stored index fields.

Figure 5-6

No Stored Fields for Index Employees/ByFirstNameByLastName

As you can see, an index does not contain any stored fields. So, when you run the query with a projection
from index 'Employees/ByFirstNameByLastName'
where FirstName = 'Nancy'
select FirstName, LastName

the database engine will first perform filtering based on an index, which will result in single ID employees/1-A. After that, the engine will make one more round trip to the storage to fetch this document and read its FirstName and LastName properties.

You can convert your nonclustered index into covering one by instructing RavenDB to store specific fields. Going back to the Index edit form and looking at the bottom of the screen, you can see the Fields tab in Figure 5-7 .

A screenshot with 2 sections, maps with 6 lines of syntax and buttons add map and add reduction and reduce with 4 tabs and buttons add field and add default field options in the fields tab.

Figure 5-7

Fields Tab on Edit Index Page

This tab shows no fields added to the index. Add fields FirstName and LastName by clicking on Add field button, and populate both of their names, and select value Yes in the Store option, as shown in Figure 5-8.

A screenshot with 4 tabs namely fields, configuration, additional assemblies, and additional sources. The fields tab has 2 buttons, add field and add default field options, to add details under last name and first name.

Figure 5-8

Adding FirstName and LastName as Stored Fields in the Index

After saving the changed index definition, repeating the query from Figure 5-6 will reveal stored fields of the Employees/ByFirstNameByLastName index, as presented in Figure 5-9.

A screenshot displays a query, settings drop-down box with 2 enabled among 3 toggle buttons, search query, save, play, and delete documents buttons. The results panel displays a table with 4 columns.

Figure 5-9

Stored Fields for Index Employees/ByFirstNameByLastName

As a result of the change you just made, the database engine will be able to create projections consisting of FirstName and LastName with just one trip to the storage. Index entry fetched via filtering operation will contain these two fields, which will be available immediately.

We already mentioned that covering indexes are a good compromise since they require fewer round trips to produce results but, at the same time, are not storing the complete document. With RavenDB Studio, you can now inspect actual allocated storage for all of your indexes and collections. By clicking on the Stats icon on the left edge and then selecting Storage report, you will be able to inspect allocation details, as shown in Figure 5-10.

A screenshot of the storage report displays the allocation details and various fields of the index storage when the icon Stats is clicked.

Figure 5-10

Storage Report

You can add or remove various fields from the index storage and check the impact of your changes in the Storage report.

Computed Fields

So far, you have created several static indexes that include the value of the fields. In some cases, like the Employees/ByFirstName index, the field value is read directly from processed documents. In others, like Orders/ByEmployeeName, a referenced document was loaded, and then the content of its field was indexed.

However, it is also possible to create index fields containing a value that does not exist in any documents. Such fields are called computed fields. We usually calculate their value based on one or more other fields.

Example of an index with computed field is shown in Listing 5-9.
map("Orders", order => {
    var employee = load(order.Employee, 'Employees');
    var total = 0;
    for (var i = 0; i < order.Lines.length; i++) {
        var line = order.Lines[i];
        total += line.Quantity * line.PricePerUnit * (1 - line.Discount);
    }
    return {
        EmployeeName: employee.FirstName,
        Total: total
    }
})
Listing 5-9

Index Orders/ByEmployeeNameByTotal with Computed Field Total

Index defined in Listing 5-9 is based on Orders/ByEmployeeName but expanded with field Total. Its value represents total monetary value of the order, and it is computed in the following way :
var total = 0;
for (var i = 0; i < order.Lines.length; i++) {
    var line = order.Lines[i];
    total += line.Quantity * line.PricePerUnit * (1 - line.Discount);
}

This is a relatively standard piece of imperative JS code. It iterates over Order.Lines array, and for every line, the total is quantity * price * (1 – discount). Line totals are accumulated, and after lines are processed, variable total holds calculated total for Order.

You can use all features of JavaScript languages, like loops, functions, etc. You can even extract code into JS functions that will be called from the map function of an index, as shown in Listing 5-10.
function GetTotal(order) {
    var total = 0;
    for (var i = 0; i < order.Lines.length; i++) {
        var line = order.Lines[i];
        total += line.Quantity * line.PricePerUnit * (1 - line.Discount);
    }
    return total;
}
map("Orders", order => {
    var employee = load(order.Employee, 'Employees');
    return {
        EmployeeName: employee.FirstName,
        Total: GetTotal(order)
    }
})
Listing 5-10

Index Orders/ByEmployeeNameByTotal with functionality extracted into JS function

It is also possible to use declarative style of JS, as shown in Listing 5-11.
function GetTotal(order) {
    return order.Lines.reduce((partial_sum, l) => partial_sum + (l.Quantity * l.PricePerUnit) * (1 - l.Discount), 0)
}
map("Orders", order => {
    var employee = load(order.Employee, 'Employees');
    return {
        EmployeeName: employee.FirstName,
        Total: GetTotal(order)
    }
})
Listing 5-11

Index Orders/ByEmployeeNameByTotal with declarative-style JS function

It is also possible to embed business logic and business rules of your domain into index. Listing 5-12 gives one such example .
function GetTotal(order) {
    var total = 0;
    for (var i = 0; i < order.Lines.length; i++) {
        var line = order.Lines[i];
        total += line.Quantity * line.PricePerUnit * (1 - line.Discount);
    }
    if (order.ShipTo.Country == "Poland") {
        total = 0.9 * total;
    }
    return total;
}
map("Orders", order => {
    var employee = load(order.Employee, 'Employees');
    return {
        EmployeeName: employee.FirstName,
        Total: GetTotal(order)
    }
})
Listing 5-12

Index Orders/ByEmployeeNameByTotal expanded with business rules

We are giving a 10% discount to orders shipped to Poland, and that rule is embedded into the index as part of a computed field calculation .

Hence, now you can execute a query:
from index 'Orders/ByEmployeeNameByTotal'
where Total > 15000

to get all orders with a total monetary value of over 15,000.

So, with computed fields, you can create new properties calculated on existing properties with the application of possibly very complex logic. You can offload a significant portion of your business logic to a database if needed .

Dynamic Fields

As we mentioned earlier, RavenDB is a schemaless database. You do not have to define schema upfront, and you will be able to accept various semi-structured or even unstructured data. The moment you know least about your domain is the very moment when the project starts. The ability to postpone decisions about data structures you will use is a significant advantage. It will not just speed up development, but it will also provide much-needed flexibility as additional requirements arrive along the way of implementation and inevitably after you launch your product .

In other cases, you have very well-defined requirements, but the data you need to store inside a database is inherently heterogeneous. A typical situation is a need for globalization of your application. Expanding your business to a new part of the world often means learning about concepts you forgot to cover and modifying your application and database to take them into account.

Take the concept of an address as an example. Various countries around the world have different ways of addressing locations. It is enough to read multiple administrative division terms for countries worldwide – state, county, province, district, prefecture, emirate, canton, municipality, circuit, raion , oblast – to comprehend the full complexity of modeling such heterogeneous data.

Schemaless JSON as a data format for your documents in RavenDB will provide you with much-needed flexibility for expanding entities that you store. However, you need to propagate these changes to queries and the application itself. And, since queries are using indexes, you will also have to expand the RavenDB index definition. Once it’s changed, this will trigger recomputation of the whole index, which can be both computation-heavy and time-consuming.

RavenDB provides a handy solution for situations like this one in the form of dynamic fields . Instead of explicitly stating field names and their values, you can programmatically define them. Such an index can accommodate different document structures. Also, dynamic fields will prevent complete recalculation of the index when a document with new fields arrives at the database. Listing 5-13 shows an index with dynamic fields generated for the employee’s address.
function CreateDynamicFields(addr) {
    var ret = [];
    for (const property in addr) {
        ret.push(createField(property, addr[property], { indexing: 'Exact', storage: false, termVector: null }))
    }
    return ret;
}
map("Employees", emp => {
    return {
        _: CreateDynamicFields(emp.Address)
    }
})
Listing 5-13

Employees/DynamicFields index

As you can see from the map definition, this index is processing all employees and will create dynamic fields for their addresses . Listing 5-14 presents one sample address.
{
    "Line1": "4726 - 11th Ave. N.E.",
    "Line2": null,
    "City": "Seattle",
    "Region": "WA",
    "PostalCode": "98105",
    "Country": "USA",
    "Location": {
        "Latitude": 47.66416419999999,
        "Longitude": -122.3160148
    }
}
Listing 5-14

Address literal of employees/8-A

Figure 5-11 shows collections of index terms for addresses of this form.

A screenshot of index terms for employees slash dynamic fields namely i d with 9 loaded data, line 1, line 2, city with 5 loaded data, region, postal code with 9 loaded data, country, and location.

Figure 5-11

Index Terms for Employees/DynamicFields Index

Each property of the address (like Line1, Line2, City, etc.) is one collection of terms. Values of such group are extracted from all occurrences of that property on any of the employee documents.

Let’s look at the definition of the index. You can see that definition of the dynamic field has a peculiar form:
_: CreateDynamicFields(emp.Address)

Underscore as an object literal name is just a convention. Figure 5-11 shows that a collection with the name _ will be created, but it does not have content.

Function that generates set of dynamic fields has the following definition :
function CreateDynamicFields(addr) {
    var ret = [];
    for (const property in addr) {
        ret.push(createField(property, addr[property], { indexing: 'Exact', storage: false, termVector: null }))
    }
    return ret;
}

An empty array is defined. The indexing engine will iterate over all address’ properties and add a new dynamic field for every one of them .

RavenDB has built-in JS function:
createField(name, value, {options}).

You can use it to create a dynamic field name:property.

Note that the CreateDynamicFields function is entirely generic – no exact field names are stated. It can process any object literal passed to it, extract all property names and their values, and create dynamic fields for every property it finds.

Furthermore, modifying employee documents will trigger an index update, and a new field will be added to the indexing terms. As an exercise, open employee with id employees/8-A and expand its Address with the following property:
"Continent": "North America"

After saving changes, verify that a new index term collection with the name Continent has been created.

Overall, dynamic fields are a tool for creating flexible indexes which can process heterogeneous data and, at the same time, adapt to future changes your domain model might introduce .

Fanout Index

Indexes we created so far were processing documents and were creating one indexing entry per document. For example, index Employees/ByFirstName will take all nine employee documents, read their first name, and output one index entry per document. Figure 5-12 shows a summary for this index.

A screenshot of the index employees with a checkbox, employees slash by first name, displays 9 entries up to date with normal state, normal priority, and unlocked mode.

Figure 5-12

Summary for Index Employees/ByFirstName

Summary shows nine entries, one per document. The number of index entries equals number of documents in the collection.

It is possible to write an index which would output not one but multiple entries per processed documents. Such index is called fanout index. Listing 5-15 shows an example of such index.
map('Orders', order => {
    var res = [];
    order.Lines.forEach(l => {
        res.push({
            ProductName: l.ProductName
        })
    });
    return res;
})
Listing 5-15

Orders/ByProductName index

Fanout indexes can produce dozens, even hundreds, of entries per document. Index in Listing 5-15 iterates over all lines in every order and creates one indexing entry for each line. Every such entry contains a product name. Hence, for every order document , the index will create as many entries as there are order lines.

You are now able to find all orders containing the line with a specified product name:
from index 'Orders/ByProductName' where ProductName = 'chocolade'
Figure 5-13 shows that inspecting indexing entries for a specific order can reveal all of its entries and help you understand the fanout principle.

A screenshot displays a query from index orders slash by product name where i d = orders slash 154-a. The results panel has a table with 3 columns namely preview, product name, and i d and delete documents button.

Figure 5-13

Indexing Entries for Orders/154-a

Going back to the index overview, you will see that this index contains 2,155 entries for 830 processed orders.

In the same manner, you can index all documents with embedded values .

Multi-Map Index

Up to this point, all indexes we defined were processing a single collection, loading properties of its documents, and extracting their values into indexing entries. However, it is not uncommon for documents in different collections to have the same properties . A typical example of this would be an address – in our sample dataset, Employees, Companies, and Suppliers all have this property.

Both Companies and Suppliers are legal entities, and it is not hard to imagine a business scenario where you would like to search both of them by city. One possible solution would be to create Companies/ByCity and Suppliers/ByCity indexes. However, you would have to perform two searches to get total results:
from index 'Companies/ByCity' where City = 'paris'
from index 'Suppliers/ByCity' where City = 'paris'

RavenDB provides a way to merge these two indexes into one. Such an index is called the multi-map index.

For our scenario, defining such an index is not harder than defining a single-map index. First, you need to choose a name. Since two collections are scanned, you can go with a name that uses business functionality description instead of collection names, such as Search/ByCity. Listing 5-16 shows the definition of this index.
map("Companies", company => {
    return {
        City: company.Address.City
    }
})
map("Suppliers", supplier => {
    return {
        City: supplier.Address.City
    }
})
Listing 5-16

Search/ByCity Index

You will add the first map as you usually do. Then, click on Add map button. One more text area will open so that you can add a second map definition . Your multi-map index is ready for querying, as shown in Figure 5-14.

A screenshot displays a query from index search slash by city where city = paris. The results panel has a table with 7 columns, preview, i d, name, external I d, contact, address, and phone number, and delete documents button.

Figure 5-14

Querying Multi-Map Search/ByCity Index

The results you got are all companies and suppliers located in Paris.

You can expand this index with one more map that would process Employees . We will leave this as an exercise for you.

Indexing Hierarchical Data

Besides simple linear relations, your applications will inevitably need to represent and store more complex setups. One such structure is the hierarchy – chain of documents pointing to the next one sequentially. Typical representatives of hierarchical data are genealogical trees or threads of comments on a blog post.

Our sample database contains one example of hierarchical data, as shown in Listing 5-17.
{
    "LastName": "Dodsworth",
    "FirstName": "Anne",
    ...
    "ReportsTo": "employees/5-A",
    ...
Listing 5-17

ReportsTo property of employees/9-A document

Every employee has ReportsTo property that contains the identifier of another employee. In this chapter, you already saw examples of how to index related documents, so it is easy to come up with an index presented in Listing 5-18.
map("Employees", emp => {
    return {
        Manager : load(emp.ReportsTo, "Employees").FirstName
    }
})
Listing 5-18

Employees/ByReportsToFirstName index

Looking at index terms for this index, you can see it contains just two names: Andrew and Steven. Querying index for both of these names
from index 'Employees/ByReportsToFirstName' where Manager = 'Andrew'
from index 'Employees/ByReportsToFirstName' where Manager = 'Steven'

will reveal the structure of Northwind Traders company: Anne, Michael, and Robert report directly to Steven. Everyone else, including Steven, reports directly to Andrew. This hierarchy is also revealing itself in employee titles. Andrew is “Vice President, Sales” and Steven is “Sales Manager.” All other employees bear “Sales Representative” and “Inside Sales Coordinator.”

An important thing to notice here is Andrew’s ReportsTo property:
"ReportsTo": null
This property is null, and Employees/ByReportsToFirstName will execute the following line:
load(null, "Employees").FirstName

However, unlike most programming languages, the null reference here will not create any problems. RavenDB will recognize an attempt to load a nonexisting document and handle that gracefully – it will not generate any indexing entries for Andrew. If you check the overview for this index, you will see that it has eight entries for nine processed employees.

The hierarchy we just computed is not a complete one – just direct superiors were fetched. After fetching a direct manager for every employee, we can continue climbing up the hierarchy tree until we reach the topmost manager in the hierarchy. Listing 5-19 defines an index that collects every employee’s direct and indirect managers .
map("Employees", empl => {
    if (empl.ReportsTo == null) return null;
    var managerNames = [];
    while (true) {
        empl = load(empl.ReportsTo, "Employees");
        if (empl == null)
            break;
        managerNames.push(empl.FirstName);
    }
    return {
        Manager: managerNames
    }
})
Listing 5-19

Employees/ByManagers index

This index contains the same terms as a previous one (Employees/ByReportsToFirstName), but indexing entries are different, as shown in Figure 5-15.

A screenshot displays a query from index employees slash by managers where manager = steven. The results panel has a table with 3 columns, preview, manager, and i d, statistics tab, and delete documents button.

Figure 5-15

Indexing Entries for Employees/ByManagers Index

As you can see, we queried for all employees where a manager is Steven, and we got all three of them. However, index entries for these employees contain both Steven and Andrew – as we were looping up the hierarchy in Listing 5-19, Steven was added to an array , followed by Andrew, loaded as Steven’s reporting officer.

Recursive nature of this index can be expressed in a declarative manner. RavenDB provides JS function recurse of the form
recurse(start_document, func(doc) -> doc)
which will start from a specified document and apply a func in a recursive way – returned document will be fed as an input to func. This chain of calls, defined in Listing 5-20, will continue up to the point when func returns null.
map("Employees", empl => {
    var reportsTo = load(empl.ReportsTo, "Employees");
    return recurse(reportsTo, x => load(x.ReportsTo, "Employees"))
        .map(boss => {
            return { Managers: boss.FirstName };
        });
})
Listing 5-20

Recursive version of Employees/ByManagers index

Both imperative and declarative versions of this index are equal in the result. You will be able to query hierarchy by any of the values in the chain .

Summary

In this chapter, we introduced static indexes to define an index explicitly. You saw ways to index one or multiple collections and specify dynamic fields, stored fields, and computed fields of such index. We presented techniques for indexing references to other documents and approaches to handle hierarchical data structures.

The next chapter will show how to create indexes performing data aggregations.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.10.246