Indexing Polymorphic Data



The challenge

Let's assume, for example, that we have the following inheritance hierarchy:

Figure 1: Polymorphic indexes


By default:
When saving a Cat document, it will be assigned to the "Cats" collection,
while a Dog document will be placed in the "Dogs" collection.

If we intend to create a simple Map-index for Cat documents based on their names, we would write:

class Cats_ByName extends AbstractJavaScriptIndexCreationTask {
    constructor() {
        super();

        // Index the 'name' field from the CATS collection
        this.map('Cats', cat => {
            return {
                name: cat.name
            };
        });
    }
}
class Animal {
    constructor(name) {
        this.name = name;
    }
}

class Cat extends Animal { }

And for Dogs:

class Dogs_ByName extends AbstractJavaScriptIndexCreationTask {
    constructor() {
        super();

        // Index the 'name' field from the DOGS collection
        this.map('Dogs', dog => {
            return {
                name: dog.name
            };
        });
    }
}
class Animal {
    constructor(name) {
        this.name = name;
    }
}

class Dog extends Animal { }

The challenge:
Querying each index results in documents only from the specific collection the index was defined for.
However, what if we need to query across ALL animal collections?

Possible solutions

Multi-Map Index:


Writing a Multi-map index enables getting results from all collections the index was defined for.

class CatsAndDogs_ByName extends AbstractJavaScriptMultiMapIndexCreationTask  {
    constructor() {
        super();

        // Index documents from the CATS collection
        this.map('Cats', cat => {
            return {
                name: cat.name
            };
        });

        // Index documents from the DOGS collection
        this.map('Dogs', dog => {
            return {
                name: dog.name
            };
        });
    }
}

Query the Multi-map index:

const catsAndDogs = await session
    // Query the index
    .query({ indexName: "CatsAndDogs/ByName" })
    // Look for all Cats or Dogs that are named 'Mitzy' :))
    .whereEquals("name", "Mitzy")
    .all();

// Results will include matching documents from the CATS and DOGS collection
from index "CatsAndDogs/ByName"
where name == "Mitzy"

Polymorphic index:


Another option is to create a polymorphic-index.

Use method WhereEntityIs within your index definition to index documents from all collections
listed in the method.

class CatsAndDogs_ByName extends AbstractCsharpIndexCreationTask {
    constructor() {
        super();

        // Index documents from both the CATS collection and the DOGS collection
        this.map = `from animal in docs.WhereEntityIs("Cats", "Dogs")
                    select new {
                        animal.name
                    }`;
    }
}

Query the polymorphic-index:

const catsAndDogs = await session
    // Query the index
    .query({ indexName: "CatsAndDogs/ByName" })
    // Look for all Cats or Dogs that are named 'Mitzy' :))
    .whereEquals("name", "Mitzy")
    .all();

// Results will include matching documents from the CATS and DOGS collection
from index "CatsAndDogs/ByName"
where name == "Mitzy"

Customize collection:


This option involves customizing the collection name that is assigned to documents created from
subclasses of the Animal class.

This is done by setting the findCollectionName convention on the document store.

const documentStore = new DocumentStore(["serverUrl_1", "serverUrl_2", "..."], "DefaultDB");

// Customize the findCollectionName convention 
documentStore.conventions.findCollectionName = (type) => {
    const typeName = type.name;

    // Documents created from a 'Cat' or a 'Dog' entity will be assinged the "Animals" collection
    if (typeName === "Cat" || typeName === "Dog") {
        return "Animals";
    }

    // All other documents will be assgined the default collection name
    return DocumentConventions.defaultGetCollectionName(type);
}

With the above convention in place, whenever a Cat or a Dog entity is saved, its document will be assigned the "Animals" collection instead of the default "Cats" or "Dogs" collection.

Now you can define a Map-index on the "Animals" collection:

class Animals_ByName extends AbstractJavaScriptIndexCreationTask {
    constructor() {
        super();

        // Index documents from the ANIMALS collection
        this.map('Animals', animal => {
            return {
                name: animal.name
            };
        });
    }
}

Query the index:

const animals = await session
    // Query the index
    .query({ indexName: "Animals/ByName" })
    // Look for all Animals that are named 'Mitzy' :))
    .whereEquals("name", "Mitzy")
    .all();

// Results will include matching documents from the ANIMALS collection
from index "Animals/ByName"
where name == "Mitzy"