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.