Searching
One of the most common functionalities that many real world applications provide is a search feature. Many times it will be enough to apply Where
closure to create a simple condition, for example to get all users whose name equals John Doe
use the code:
IList<User> users = session
.Query<User, Users_ByName>()
.Where(x => x.Name == "John")
.ToList();
IList<User> users = session
.Advanced
.DocumentQuery<User, Users_ByName>()
.WhereEquals(x => x.Name, "John")
.ToList();
QueryResult result = store
.DatabaseCommands
.Query(
"Users/ByName",
new IndexQuery
{
Query = "Name:John"
});
public class Users_ByName : AbstractIndexCreationTask<User>
{
public Users_ByName()
{
Map = users => from user in users
select new
{
user.Name
};
Indexes.Add(x => x.Name, FieldIndexing.Analyzed);
}
}
where User
class is defined as follows:
public class User
{
public string Id { get; set; }
public string Name { get; set; }
public byte Age { get; set; }
public ICollection<string> Hobbies { get; set; }
}
The Where
statement also is good if you want to perform a really simple text field search, for example let's create a query to retrieve users whose name starts with Jo:
IList<User> users = session
.Query<User, Users_ByName>()
.Where(x => x.Name.StartsWith("Jo"))
.ToList();
IList<User> users = session
.Advanced
.DocumentQuery<User, Users_ByName>()
.WhereStartsWith(x => x.Name, "Jo")
.ToList();
QueryResult result = store
.DatabaseCommands
.Query(
"Users/ByName",
new IndexQuery
{
Query = "Name:Jo*"
});
public class Users_ByName : AbstractIndexCreationTask<User>
{
public Users_ByName()
{
Map = users => from user in users
select new
{
user.Name
};
Indexes.Add(x => x.Name, FieldIndexing.Analyzed);
}
}
Eventually all queries are always transformed into a Lucene query. The query like above will be translated into Name:Jo*.
Safe By Default
An attempt to use string.Contains()
method as condition of Where
closure, will throw NotSupportedException
. That is because the search term like *term* (note wildcards at the beginning and at the end) can cause performance issues. Due to Raven's safe-by-default paradigm such operation is forbidden. If you really want to achieve this case, you will find more details in one of the next section below.
Information
Note that that results of a query might be different depending on an analyzer that was applied.
Multiple terms
When you need to do a more complex text searching use Search
extension method (in Raven.Client
namespace). This method allows you to pass a few search terms that will be used in searching process for a particular field. Here is a sample code
that uses Search
extension to get users with name John or Adam:
IList<User> users = session
.Query<User, Users_ByName>()
.Search(x => x.Name, "John Adam")
.ToList();
IList<User> users = session
.Advanced
.DocumentQuery<User, Users_ByName>()
.Search(x => x.Name, "John Adam")
.ToList();
QueryResult result = store
.DatabaseCommands
.Query(
"Users/ByName",
new IndexQuery
{
Query = "Name:(John Adam)"
});
public class Users_ByName : AbstractIndexCreationTask<User>
{
public Users_ByName()
{
Map = users => from user in users
select new
{
user.Name
};
Indexes.Add(x => x.Name, FieldIndexing.Analyzed);
}
}
Each of search terms (separated by space character) will be checked independently. The result documents must match exact one of the passed terms.
The same way you are also able to look for users that have some hobby:
IList<User> users = session
.Query<User, Users_ByHobbies>()
.Search(x => x.Hobbies, "looking for someone who likes sport books computers")
.ToList();
IList<User> users = session
.Advanced
.DocumentQuery<User, Users_ByHobbies>()
.Search(x => x.Hobbies, "looking for someone who likes sport books computers")
.ToList();
QueryResult result = store
.DatabaseCommands
.Query(
"Users/ByHobbies",
new IndexQuery
{
Query = "Name:(looking for someone who likes sport books computers)"
});
public class Users_ByHobbies : AbstractIndexCreationTask<User>
{
public Users_ByHobbies()
{
Map = users => from user in users
select new
{
user.Hobbies
};
Indexes.Add(x => x.Hobbies, FieldIndexing.Analyzed);
}
}
In result you will get users that are interested in sport, books or computers.
Multiple fields
By using Search
extension you are also able to look for by multiple indexed fields. First let's introduce the index:
public class Users_ByNameAndHobbies : AbstractIndexCreationTask<User>
{
public Users_ByNameAndHobbies()
{
Map = users => from user in users
select new
{
user.Name,
user.Hobbies
};
Indexes.Add(x => x.Name, FieldIndexing.Analyzed);
Indexes.Add(x => x.Hobbies, FieldIndexing.Analyzed);
}
}
Now we are able to search by using Name
and Hobbies
properties:
List<User> users = session
.Query<User, Users_ByNameAndHobbies>()
.Search(x => x.Name, "Adam")
.Search(x => x.Hobbies, "sport")
.ToList();
List<User> users = session
.Advanced
.DocumentQuery<User, Users_ByNameAndHobbies>()
.Search(x => x.Name, "Adam")
.Search(x => x.Hobbies, "sport")
.ToList();
QueryResult result = store
.DatabaseCommands
.Query(
"Users/ByNameAndHobbies",
new IndexQuery
{
Query = "Name:(Adam) OR Hobbies:(sport)"
});
Boosting
Indexing in RavenDB is built upon Lucene engine that provides a boosting term mechanism. This feature introduces the relevance level of matching documents based on the terms found. Each search term can be associated with a boost factor that influences the final search results. The higher the boost factor, the more relevant the term will be. RavenDB also supports that, in order to improve your searching mechanism and provide the users with much more accurate results you can specify the boost argument. Let's see the example:
IList<User> users = session
.Query<User, Users_ByHobbies>()
.Search(x => x.Hobbies, "I love sport", boost: 10)
.Search(x => x.Hobbies, "but also like reading books", boost: 5)
.ToList();
IList<User> users = session
.Advanced
.DocumentQuery<User, Users_ByHobbies>()
.Search(x => x.Hobbies, "I love sport")
.Boost(10)
.Search(x => x.Hobbies, "but also like reading books")
.Boost(5)
.ToList();
QueryResult result = store
.DatabaseCommands
.Query(
"Users/ByHobbies",
new IndexQuery
{
Query = "Hobbies:(I love sport)^10 OR Hobbies:(but also like reading books)^5"
});
public class Users_ByHobbies : AbstractIndexCreationTask<User>
{
public Users_ByHobbies()
{
Map = users => from user in users
select new
{
user.Hobbies
};
Indexes.Add(x => x.Hobbies, FieldIndexing.Analyzed);
}
}
The search above will promote users who do sports before book readers and they will be placed at the top of the result list.
Search options
In order to specify the logic of search expression specify the options argument of the Search
method. It is SearchOptions
enum with the following values:
- Or,
- And,
- Not,
- Guess (default).
By default RavenDB attempts to guess and match up the semantics between terms. If there are consecutive searches, they will be OR together, otherwise AND semantic will be used by default.
The following query:
IList<User> users = session
.Query<User, Users_ByNameAgeAndHobbies>()
.Search(x => x.Hobbies, "computers")
.Search(x => x.Name, "James")
.Where(x => x.Age == 20)
.ToList();
IList<User> users = session
.Advanced
.DocumentQuery<User, Users_ByNameAgeAndHobbies>()
.Search(x => x.Hobbies, "computers")
.Search(x => x.Name, "James")
.WhereEquals(x => x.Age, 20)
.ToList();
QueryResult result = store
.DatabaseCommands
.Query(
"Users/ByNameAgeAndHobbies",
new IndexQuery
{
Query = "(Hobbies:(computers) OR Name:(James)) AND Age:20"
});
public class Users_ByNameAgeAndHobbies : AbstractIndexCreationTask<User>
{
public Users_ByNameAgeAndHobbies()
{
Map = users => from user in users
select new
{
user.Name,
user.Age,
user.Hobbies
};
Indexes.Add(x => x.Name, FieldIndexing.Analyzed);
Indexes.Add(x => x.Hobbies, FieldIndexing.Analyzed);
}
}
will be translated into (Hobbies:(computers) Name:(James)) AND (Age:20)
(if there is no boolean operator then OR is used).
You can also specify what exactly the query logic should be. The applied option will influence a query term where it was used. The query as follow:
IList<User> users = session
.Query<User, Users_ByNameAndHobbies>()
.Search(x => x.Name, "Adam")
.Search(x => x.Hobbies, "sport", options: SearchOptions.And)
.ToList();
IList<User> users = session
.Advanced
.DocumentQuery<User, Users_ByNameAndHobbies>()
.Search(x => x.Name, "Adam")
.AndAlso()
.Search(x => x.Hobbies, "sport")
.ToList();
QueryResult result = store
.DatabaseCommands
.Query(
"Users/ByNameAndHobbies",
new IndexQuery
{
Query = "Name:(Adam) AND Hobbies:(sport)"
});
public class Users_ByNameAndHobbies : AbstractIndexCreationTask<User>
{
public Users_ByNameAndHobbies()
{
Map = users => from user in users
select new
{
user.Name,
user.Hobbies
};
Indexes.Add(x => x.Name, FieldIndexing.Analyzed);
Indexes.Add(x => x.Hobbies, FieldIndexing.Analyzed);
}
}
will result in the following Lucene query: Name:(Adam) AND Hobbies:(sport)
If you want to negate the term use SearchOptions.Not
:
IList<User> users = session
.Query<User, Users_ByName>()
.Search(x => x.Name, "James", options: SearchOptions.Not)
.ToList();
IList<User> users = session
.Advanced
.DocumentQuery<User, Users_ByName>()
.Not
.Search(x => x.Name, "James")
.ToList();
QueryResult result = store
.DatabaseCommands
.Query(
"Users/ByName",
new IndexQuery
{
Query = "-Name:(James)"
});
public class Users_ByName : AbstractIndexCreationTask<User>
{
public Users_ByName()
{
Map = users => from user in users
select new
{
user.Name
};
Indexes.Add(x => x.Name, FieldIndexing.Analyzed);
}
}
According to Lucene syntax it will be transformed to the query: -Name:(James)
.
You can treat SearchOptions
values as bit flags and create any combination of the defined enum values, e.g:
IList<User> users = session
.Query<User, Users_ByNameAndHobbies>()
.Search(x => x.Name, "Adam")
.Search(x => x.Hobbies, "sport", options: SearchOptions.Not | SearchOptions.And)
.ToList();
IList<User> users = session
.Advanced
.DocumentQuery<User, Users_ByNameAndHobbies>()
.Search(x => x.Name, "Adam")
.AndAlso()
.Not
.Search(x => x.Hobbies, "sport")
.ToList();
QueryResult result = store
.DatabaseCommands
.Query(
"Users/ByName",
new IndexQuery
{
Query = "Name:(Adam) AND -Hobbies:(sport)"
});
public class Users_ByNameAndHobbies : AbstractIndexCreationTask<User>
{
public Users_ByNameAndHobbies()
{
Map = users => from user in users
select new
{
user.Name,
user.Hobbies
};
Indexes.Add(x => x.Name, FieldIndexing.Analyzed);
Indexes.Add(x => x.Hobbies, FieldIndexing.Analyzed);
}
}
It will produce the following Lucene query: Name:(Adam) AND -Hobbies:(sport)
.
Query escaping
The code examples presented in this section have hard coded searching terms. However in a real use case the user will specify the term. You are able to control the escaping strategy of the provided query by specifying
the EscapeQueryOptions
parameter. It's the enum that can have one of the following values:
- EscapeAll (default),
- AllowPostfixWildcard,
- AllowAllWildcards,
- RawQuery.
By default all special characters contained in the query will be escaped (EscapeAll
) when Query from session is used. However you can add a bit more of flexibility to your searching mechanism.
EscapeQueryOptions.AllowPostfixWildcard
enables searching against a field by using search term that ends with wildcard character:
IList<User> users = session
.Query<User, Users_ByName>()
.Search(x => x.Name, "Jo* Ad*", escapeQueryOptions: EscapeQueryOptions.AllowPostfixWildcard)
.ToList();
IList<User> users = session
.Advanced
.DocumentQuery<User, Users_ByName>()
.Search("Name", "Jo* Ad*", escapeQueryOptions: EscapeQueryOptions.AllowPostfixWildcard)
.ToList();
QueryResult result = store
.DatabaseCommands
.Query(
"Users/ByName",
new IndexQuery
{
Query = "Name:(Jo* Ad*)"
});
public class Users_ByName : AbstractIndexCreationTask<User>
{
public Users_ByName()
{
Map = users => from user in users
select new
{
user.Name
};
Indexes.Add(x => x.Name, FieldIndexing.Analyzed);
}
}
The next option EscapeQueryOptions.AllowAllWildcards
extends the previous one by allowing the wildcard character to be present at the beginning as well as at the end of the search term.
IList<User> users = session
.Query<User, Users_ByName>()
.Search(x => x.Name, "*oh* *da*", escapeQueryOptions: EscapeQueryOptions.AllowAllWildcards)
.ToList();
IList<User> users = session
.Advanced
.DocumentQuery<User, Users_ByName>()
.Search("Name", "*oh* *da*", escapeQueryOptions: EscapeQueryOptions.AllowAllWildcards)
.ToList();
QueryResult result = store
.DatabaseCommands
.Query(
"Users/ByName",
new IndexQuery
{
Query = "Name:(*oh* *da*)"
});
public class Users_ByName : AbstractIndexCreationTask<User>
{
public Users_ByName()
{
Map = users => from user in users
select new
{
user.Name
};
Indexes.Add(x => x.Name, FieldIndexing.Analyzed);
}
}
Warning
RavenDB allows to search by using such queries but you have to be aware that leading wildcards drastically slow down searches.
Consider if you really need to find substrings, most cases looking for words is enough. There are also other alternatives for searching without expensive wildcard matches, e.g. indexing a reversed version of text field or creating a custom analyzer.
The last option makes that the query will not be escaped and the raw term will be relayed to Lucene:
IList<User> users = session
.Query<User, Users_ByName>()
.Search(x => x.Name, "*J?n*", escapeQueryOptions: EscapeQueryOptions.RawQuery)
.ToList();
IList<User> users = session
.Advanced
.DocumentQuery<User, Users_ByName>()
.Search(x => x.Name, "*J?n*", escapeQueryOptions: EscapeQueryOptions.RawQuery)
.ToList();
QueryResult result = store
.DatabaseCommands
.Query(
"Users/ByName",
new IndexQuery
{
Query = "Name:(*J?n*)"
});
public class Users_ByName : AbstractIndexCreationTask<User>
{
public Users_ByName()
{
Map = users => from user in users
select new
{
user.Name
};
Indexes.Add(x => x.Name, FieldIndexing.Analyzed);
}
}
EscapeQueryOptions
Default EscapeQueryOptions
value for Query is EscapeQueryOptions.EscapeAll
.
Default EscapeQueryOptions
value for DocumentQuery is EscapeQueryOptions.RawQuery
.