Indexes: Map Indexes
Map
indexes, sometimes referred to as simple indexes, contain one or more mapping functions to indicate which document fields should be indexed.- After indexing, documents can be searched by the indexed fields.
-
The mapping functions can be considered the core of indexes.
-
In This Page:
Also see:
- Indexing fields from related documents
- Aggregating data with Map-Reduce indexes
- Indexing multiple collections with Multi-Map indexes
- Running calculations and storing the results in the index to reduce query time
Indexing single fields
Let's create an index that will help us search for Employees
by their FirstName
, LastName
, or both.
- First, let's create an index called
Employees/ByFirstAndLastName
Note: The naming separator character "
_
" in your code will become "/" in the index name.
In the following sample, "Employees_ByFirstAndLastName
" will become "Employees/ByFirstAndLastName" in your indexes list.
class Employees_ByFirstAndLastName(AbstractIndexCreationTask):
def __init__(self):
super().__init__()
# ...
class Employees_ByFirstAndLastName(AbstractJavaScriptIndexCreationTask):
def __init__(self):
super().__init__()
# ...
You might notice that we're passing Employee
as a generic parameter to AbstractIndexCreationTask
. This gives our indexing function a strongly-typed syntax. If you are not familiar with AbstractIndexCreationTask
, you can read this article before proceeding.
- The next step is to create the indexing function itself. This is done by setting the
map
property with our function in a parameterless constructor.
class Employees_ByFirstAndLastName(AbstractIndexCreationTask):
def __init__(self):
super().__init__()
self.map = "from employee in docs.Employees select new { FirstName = employee.FirstName, LastName = employee.LastName }"
class Employees_ByFirstAndLastName(AbstractIndexCreationTask):
def __init__(self):
super().__init__()
self.map = "from employee in docs.Employees select new { FirstName = employee.FirstName, LastName = employee.LastName }"
class Employees_ByFirstAndLastName(AbstractJavaScriptIndexCreationTask):
def __init__(self):
super().__init__()
self.maps = {
"""
map('Employees', function (employee){
return {
FirstName : employee.FirstName,
LastName : employee.LastName
};
})
"""
}
- The final step is to deploy it to the server
and issue a query using the session Query method.
To query an index, the name of the index must be called by the query.
If the index isn't called, RavenDB will either use or create an auto index.
employees_1 = list(
session.query_index_type(Employees_ByFirstAndLastName, Employee).where_equals(
"first_name", "Robert"
)
)
employees_2 = list(
session.query_index("Employees/ByFirstAndLastName", Employee).where_equals("first_name", "Robert")
)
from index 'Employees/ByFirstAndLastName'
where FirstName = 'Robert'
This is how our final index looks like:
class Employees_ByFirstAndLastName(AbstractIndexCreationTask):
def __init__(self):
super().__init__()
self.map = "from employee in docs.Employees select new { FirstName = employee.FirstName, LastName = employee.LastName }"
class Employees_ByFirstAndLastName(AbstractJavaScriptIndexCreationTask):
def __init__(self):
super().__init__()
self.maps = {
"""
map('Employees', function (employee){
return {
FirstName : employee.FirstName,
LastName : employee.LastName
};
})
"""
}
Field Types
Please note that indexing capabilities are detected automatically from the returned field type from the indexing function.
For example, if our Employee
has a property named Age
that is an int
, the following indexing function...
from employee in docs.Employees
select new
{
Age = employee.Age
}
map('Employees', function(employee)
{
return {
Age : employee.Age
};
})
...grants us the capability to issue numeric queries (return all the Employees whose Age is more than 30).
Changing the Age
type to a string
will take that capability away from you. The easiest example would be to issue .ToString()
on the Age
field...
from employee in docs.Employees
select new
{
Age = employee.Age.ToString()
}
map('Employees', function(employee)
{
return {
Age : employee.Age.toString()
};
})
Convention
You will probably notice that in the Studio
, this function is a bit different from the one defined in the Employees_ByFirstAndLastName
class:
from employee in docs.Employees
select new
{
FirstName = employee.FirstName,
LastName = employee.LastName
}
The part you should pay attention to is docs.Employees
. This syntax indicates from which collection a server should take the documents for indexing. In our case, documents will be taken from the Employees
collection. To change the collection, you need to change Employees
to the desired collection name or remove it and leave only docs
to index all documents.
Combining multiple fields
Since each index contains a function, you can combine multiple fields into one.
Example I
Index definition:
class Employees_ByFullName(AbstractIndexCreationTask):
class Result:
def __init__(self, full_name: str = None):
self.full_name = full_name
def __init__(self):
super().__init__()
self.map = (
'from employee in docs.Employees select new { full_name = employee.FirstName + " " + employee.LastName }'
)
class Employees_ByFullName(AbstractJavaScriptIndexCreationTask):
class Result:
def __init__(self, full_name: str = None):
self.full_name = full_name
def __init__(self):
super().__init__()
self.maps = {
"""
map('Employees', function (employee){
return {
FullName : employee.FirstName + ' ' + employee.LastName
};
})
"""
}
Query the index:
employees = list(
session.query_index_type(Employees_ByFullName, Employee).where_equals("full_name", "Robert King")
)
from index 'Employees/ByFullName'
where FullName = 'Robert King'
Example II
Information
In this example, the index field Query
combines all values from various Employee fields into one.
The default Analyzer on fields is changed to enable Full-Text Search
operations. The matches no longer need to be exact.
You can read more about analyzers and Full-Text Search
here.
Index definition:
class Companies_ByAddress_Country(AbstractIndexCreationTask):
class Result:
def __init__(self, city: str = None, company: str = None, phone: str = None):
self.city = city
self.company = company
self.phone = phone
def __init__(self):
super().__init__()
self.map = (
'from company in docs.Companies where company.Address.Country == "USA"'
"select new { company = company.Name, city = company.Address.City, phone = company.Phone }"
)
class Employees_Query(AbstractJavaScriptIndexCreationTask):
class Result:
def __init__(self, query: List[str] = None):
self.query = query
def __init__(self):
super().__init__()
self.maps = {
"""
map('Employees', function (employee) {
return {
query : [employee.FirstName,
employee.LastName,
employee.Title,
employee.Address.City]
}
})
"""
}
self.fields = {"query": IndexFieldOptions(indexing=FieldIndexing.SEARCH)}
Query the index:
employees = list(
session.query_index_type(Employees_Query, Employees_Query.Result)
.search("query", "John Doe")
.of_type(Employee)
)
from index 'Employees/Query'
where search(Query, 'John Doe')
Indexing partial field data
Imagine that you would like to return all employees that were born in a specific year.
You can do it by indexing Birthday
from Employee
, then specify the year in Birthday
as you query the index:
Index definition:
class Employees_ByBirthday(AbstractIndexCreationTask):
class Result:
def __init__(self, birthday: datetime.datetime = None):
self.birthday = birthday
@classmethod
def from_json(cls, json_dict: Dict[str, Any]) -> "Employees_ByBirthday.Result":
# import 'Utils' from 'ravendb.tools.utils' to convert C# datetime strings to Python datetime objects
return cls(Utils.string_to_datetime(json_dict["Birthday"]))
class Employees_ByBirthday(AbstractJavaScriptIndexCreationTask):
class Result:
def __init__(self, birthday: datetime.datetime = None):
self.birthday = birthday
@classmethod
def from_json(cls, json_dict: Dict[str, Any]) -> "Employees_ByBirthday.Result":
return cls(json_dict["Birthday"])
def __init__(self):
super().__init__()
self.maps = {
"""
map('Employees', function (employee){
return {
Birthday : employee.Birthday
}
})
"""
}
Query the index:
start_date = datetime.datetime(1963, 1, 1)
end_date = start_date + datetime.timedelta(days=365) - datetime.timedelta(milliseconds=1)
employees = list(
session.query_index_type(Employees_ByBirthday, Employees_ByBirthday.Result)
.where_between("birthday", start_date, end_date)
.of_type(Employee)
)
from index 'Employees/ByBirthday '
where Birthday between '1963-01-01' and '1963-12-31T23:59:59.9990000'
RavenDB gives you the ability to extract field data and to index by it. A different way to achieve our goal will look as follows:
Index definition:
class Employees_ByYearOfBirth(AbstractIndexCreationTask):
class Result:
def __init__(self, year_of_birth: int = None):
self.year_of_birth = year_of_birth
def __init__(self):
super().__init__()
self.map = "from employee in docs.Employees select new { year_of_birth = employee.Birthday.Year }"
class Employees_ByYearOfBirth(AbstractJavaScriptIndexCreationTask):
class Result:
def __init__(self, year_of_birth: int = None):
self.year_of_birth = year_of_birth
@classmethod
def from_json(cls, json_dict: Dict[str, Any]) -> "Employees_ByYearOfBirth.Result":
return cls(json_dict["Birthday"])
def __init__(self):
super().__init__()
self.maps = {
"""
map('Employees', function (employee){
return {
Birthday : employee.Birthday.Year
}
})
"""
}
Query the index:
employees = list(
session.query_index_type(Employees_ByYearOfBirth, Employees_ByYearOfBirth.Result)
.where_equals("year_of_birth", 1963)
.of_type(Employee)
)
from index 'Employees/ByYearOfBirth'
where YearOfBirth = 1963
Filtering data within fields
In the examples above, where_equals
is used in the query to filter the results.
If you consistently want to filter with the same filtering conditions,
you can use where_equals
in the index definition to narrow the index terms that the query must scan.
This can save query-time but narrows the terms available to query.
Example I
For logic that has to do with special import rules that only apply to the USA
where
can be used to filter the Companies collection Address.Country
field.
Thus, we only index documents where company.Address.Country == "USA"
.
Index definition:
class Companies_ByAddress_Country(AbstractIndexCreationTask):
class Result:
def __init__(self, city: str = None, company: str = None, phone: str = None):
self.city = city
self.company = company
self.phone = phone
def __init__(self):
super().__init__()
self.map = (
'from company in docs.Companies where company.Address.Country == "USA"'
"select new { company = company.Name, city = company.Address.City, phone = company.Phone }"
)
Query the index:
orders = list(
session.query_index_type(Companies_ByAddress_Country, Companies_ByAddress_Country.Result).of_type(
Company
)
)
from index 'Companies_ByAddress_Country'
Example II
Imagine a seed company that needs to categorize its customers by latitude-based growing zones.
They can create a different index for each zone and filter their customers in the index with
where (company.Address.Location.Latitude > 20 && company.Address.Location.Latitude < 50)
.
Index definition:
class Companies_ByAddress_Latitude(AbstractIndexCreationTask):
class Result:
def __init__(
self,
latitude: float = None,
longitude: float = None,
company_name: str = None,
company_address: str = None,
company_phone: str = None,
):
self.latitude = latitude
self.longitude = longitude
self.company_name = company_name
self.company_address = company_address
self.company_phone = company_phone
def __init__(self):
super().__init__()
self.map = (
"from company in companies"
"where (company.Address.Location.Latitude > 20 && company.Address.Location.Latitude < 50"
"select new"
"{"
" latitude = company.Address.Location.Latitude,"
" longitude = company.Address.Location.Longitude,"
" company_name = company.Name,"
" company_address = company.Address,"
" company_phone = company.Phone"
"}"
)
Query the index:
orders = list(
session.query_index_type(Companies_ByAddress_Latitude, Companies_ByAddress_Latitude.Result).of_type(
Company
)
)
from index 'Companies_ByAddress_Latitude'
Indexing nested data
If your document contains nested data, e.g. Employee
contains Address
, you can index on its fields by accessing them directly in the index. Let's say that we would like to create an index that returns all employees that were born in a specific Country
:
Index definition:
class Employees_ByCountry(AbstractIndexCreationTask):
class Result:
def __init__(self, country: str = None):
self.country = country
def __init__(self):
super().__init__()
self.map = "from employee in docs.Employees select new { country = employee.Address.Country }"
class Employees_ByCountry(AbstractJavaScriptIndexCreationTask):
class Result:
def __init__(self, country: str = None):
self.country = country
def __init__(self):
super().__init__()
self.maps = {
"""
map('Employees', function (employee){
return {
country : employee.Address.Country
}
})
"""
}
Query the index:
employees = list(
session.query_index_type(Employees_ByCountry, Employees_ByCountry.Result)
.where_equals("country", "USA")
.of_type(Employee)
)
from index 'Employees/ByCountry'
where Country = 'USA'
If a document relationship is represented by the document's ID, you can use the LoadDocument
method to retrieve such a document.
Learn more here.
Indexing Missing Fields
By default, indexes will not index a document that contains none of the specified fields. This behavior can be changed using the Indexing.IndexEmptyEntries configuration option.
The option Indexing.IndexMissingFieldsAsNull
determines whether missing fields in documents are indexed with the value null
, or not indexed at all.