Wednesday, November 25, 2009

Working with Lucene Search Index in Sitecore 6. Part III - Code examples

If you read my previous posts about Lucene search index, then you already know how to configure it and how it works in Sitecore application.
In this part we will take a look at API to see what can be achieved using the search index.

To search existing index we need to get an index instance somehow. I'm not going to show the code examples that you would write with old search index. If you're intrested in it, check out this article Lecene Search Engine.
Additional layer of API for new search index resides under Sitecore.Search namespace. In order to get a search index object, you would need to use members of SearchManager class.
To get an index by name use this line of code:
Index indx = Search.Manager.GetIndex(index_name);
If you want to use default system index, you can simply call SystemIndex property of SearchManager class. In order to use Sitecore API to look up for some info in the index, you need to created a search context.
It's easy to do by calling CreateSearchContext method of the index object we got previously. It's also possible to create a search context by using one for the constructors of IndexSearchContext class. In this case it will be easy to run search queries by passing a search index instance as a parameter to the search context.
To search information, we need to create a query and run it in the search context. Sitecore API has a few classes that you can use to build search queries. Let's take a look at each of them:
FullTextQuery - this type of query searches the index by "_content" field. All information from text fields (such as "Single-Line Text", "Rich Text", "Multi-Line Text", "text", "rich text", "html", "memo") is stored there. Data in this field are indexed and tokenized. Which means that the search operations running on these data are very efficient.
FieldQuery - this type of query allows you to search any field that was added to the index. By default database crawler adds all item fields to the index.
CombinedQuery - this type of query was designed to allow you create complex queries with additional conditions. For instance to find items which have specific work in title and belong to some category. When you add search queries to this type of query, you need to supply QueryOccurance parameter. It's Enum type that has the following members:
- Must - it's a logical AND operator in Lucene boolean query.
- MustNot - it's a logical NOT operator in Lucene boolean query.
- Should - it's a logical OR operation in Lucene boolean query.
You can read more about this operators in Query Parser Syntax article.

All of these query types are derived from QueryBase class.
There is one thing left until we jump from the theory to some code samples. To run defined queries, you need to use one of Search methods of IndexSearchContext object.
Now let's create a couple of samples to see a real code that goes behind the theory.

Sample 1: Searching text fields.
// Next samples will skip lines with getting the index instance and creating the search context.
// Get an index object
Index indx = SearchManager.GetIndex("my_index");
// Create a search context
using (IndexSearchContext searchContext = indx.CreateSearchContext())
{
// In following examples I will be using QueryBase class to create search queries.
FullTextQuery ftQuery = new FullTextQuery("welcome");
SearchHits results = searchContext.Search(ftQuery);
}

Sample 2: Searching item fields
Let's say we want to find items classified by some category. There is a trick searching by GUIDs so let's say our category is just a string name.
.....
// FieldQuery ctor accepts two parambers. First is a field name. The other one is a value we're looking for.
QueryBase query = FieldQuery("category", "slr");
SearchHits results = searchContext.Search(query);
.....

Sample 3: Searching by multiple conditions
Turned out that your category parameter is not enough to get required results. You client is screaming that there are too many items and business users cannot find ones they are looking for (is there anything they can find?).
Obviously you have some additional fields that can help to find more strict results. Let's say there is a rating field with values from 1 to 5.
That's where CombinedQuery gets into the game.
.....
// CombinedQuery object has Add method that should be used to add search queries to it. That's why we cannot use base class variable here.
CombinedQuery query = new CombinedQuery();
QueryBase catQuery = new FieldQuery("category", "slr");
QueryBase ratQuery = new FieldQuery("rating", "5");
query.Add(catQuery, QueryOccurance.Must);
query.Add(ratQuery, QueryOccurance.Must);
var hits = searchContext.Search(query);
.....

All results are presented as SearchHits object. Now you should use of following methods of SearchHits object to get the results as Sitecore items:
- FetchResults(int, int) - returns search results as SearchResultCollection. First parameter is a start position of an item you want to start fetching results from. Second one is count of items you want to fetch. By calling this function as mentioned below, you can get all results at once:
var results = hits.FetchResults(0, hits.Length);

- Slice(int) - returns all results as IEnumerable collection.

- Slice(int, int) - this method has similar signature to FetchResults but returns results as IEnumerable collection.

Here are a couple of examples the way you can transform SearchHits object into Sitecore items.
Sample 4: using FetchResults
.....
SearchResultCollection results = hits.FetchResults(0, hits.Length);
IEnumerable searchItems = from hit in results
select hit.GetObject();
}
.....

Sample 5: using Slice
.....
IList searchItems = List();
foreach(var hit in hits.Slice(0))
{
ItemUri itemUri = new ItemUri(hit.Url);
if (itemUri != null)
{
Item item = ItemManager.GetItem(itemUri.ItemID, itemUri.Language, itemUri.Version, Factory.GetDatabase(itemUri.DatabaseName));
if (item != null)
{
searchItems.Add(item);
}
}
}
.....

It's worth to mention that some variations of Search method of IndexSearchContext class can accept Lucene.Net.Search.Query as a search query parameter. It becomes very useful when you need to create a complex query which cannot be built with Sitecore query types.

Searching GUIDs.

New search index has lots of useful built-in fields that help to build strict queries.
Besides standard fields it has the following fields that contain GUIDs in ShortID format:
- _links - contains all references to current item

- _path - contains ShortIDs for every parent item in the path relative to current item

- _template - contains GUID of item's tempalte.
NOTE: this field is supposed to have ShortID value instead of GUID one. This field should not be used in combined queries prior to Sitecore 6.2 releases.
If you decide to add custom fields to your search index and they should have GUID values, you need to store them as ShortID in lower case format. Otherwise search will not be able to find any results. The reason why it happens is because Lucene recognizes GUIDs and applies special parsing for them. It works fine if search query has only one field to look into. If it's combined/complex query then it fails to find anything even if it's correct.
So, remember if you need to filter search results by template, you will have to customize DatabaseCrawler to add another field (e.g. _shorttemplateid) to store item template id in ShortID format.

Sample 1: Find all item references

.....
QueryBase query = FieldQuery("_links", ShortID.Encode(item.ID).ToLowerInvariant());
SearchHits results = searchContext.Search(query);
.....

Sample 6: Find all items based on specified template

.....
// Prior to Sitecore 6.2 release, you will need to add and use _shorttemplateid field
QueryBase query = FieldQuery("_template", ShortID.Encode(item.ID).ToLowerInvariant());
SearchHits results = searchContext.Search(query);
.....

Sample 7: Find items that are descendants of a specified one
.....
QueryBase query = FieldQuery("_path", ShortID.Encode(item.ID).ToLowerInvariant());
SearchHits results = searchContext.Search(query);
.....

Sample 8: Find items of a parent and belong to a specific template
.....
CombinedQuery query = new CombinedQuery();
query.Add(new FieldQuery("_shorttemplateid", ShortID.Encode(templateId).ToLowerInvariant()), QueryOccurance.Must);
query.Add(new FieldQuery("_path", ShortID.Encode(parent.ID).ToLowerInvariant())), QueryOccurance.Must);
.....

That's all I wanted to tell about Lucene search index in Sitecore 6. I hope it will help Lucene beginners to better understand the concept and get up to speed with Lucene search index abilities.
Enjoy!

Monday, November 2, 2009

Working with Lucene Search Index in Sitecore 6. Part II - How it works

Here is second part of the Lucene search index overview for Sitecore 6. In this part we'll take a look at configuration settings and talk about how it works.

Sitecore 5 has Lucene engine as well. Let's step one Sitecore version back and see how Lucene works there. In web.config file there is a section /sitecore/indexes that contains Lucene index configuration. When index is configured, it should be added to /sitecore/databases/database/indexes section.
The web database does not have a search index by default. Even if you add it to aforementioned section, it won't work. Why? Because index configuration relies on HistoryEngine functionality. By default the web database does not have it. It's easy to add it though. Just add the HistoryEngine configuration section to the database.
You can find more configuration details from this article on SDN.
This index has the same configuration in Sitecore 6.
In addition to it, Sitecore 6 has a new Lucene index functionality. Which is more reliable and has Sitecore level API on top of Lucene one. In some cases you will still have to use Lucene API. For instance to create range queries.
Configuration settings for new search index located under /sitecore/search section.
The analyzer section defines a Lucene analyzer that is used to analyze and index content.
The categories section is used to categories search results. It's used for content tree search introduced in Sitecore 6. The search box is located right above the content tree in content editor.
The configuration section has indexes definitions with their configurations. An index definition should be created under /sitecore/search/configuration/indexes node.
First two parameters describe the index name and folder name where it should be stored:
<param desc="name">$(id)</param>
<param desc="folder">my_index_folderName</param>
Next setting is the analyzer that should be used for the index:
<Analyzer ref="search/analyzer" />
Lucene StandardAnalyzer covers most of the case scenarios. But it's possible to use any other analyzer if it's needed.
Following setting defines locations for the index:
<locations hint="list:AddCrawler">
It's possible to have multiple locations for one index. Moreover it's even possible to have content from different databases in the same index. Every child of the locations node has its own configuration for a particular part of the content. A name of location node is not predefined. You're welcome to name it the way you want. For example:
<locations hint="list:AddCrawler">
<sdn-site type="Sitecore.Search.Crawlers.DatabaseCrawler, Sitecore.Kernel">
...
</sdn-site>
</locations>
Every location has a database section. It defines indexing database for the location.
Then root section. The database crawler will index content beneath this path.
Next sibling node is the include section. Here it's possible to add templates items of which should be included to the index or excluded from it.
Example:
<include hint="list:IncludeTemplate">
<sampleItem>{76036F5E-CBCE-46D1-AF0A-4143F9B557AA}</sampleItem>
</include>
<include hint="list:ExcludeTemplate">
<layout>{3A45A723-64EE-4919-9D41-02FD40FD1466}</layout>
</include>
It does not make sense to use both of these settings for the one location. Use only one of them.
Next location setting is tags section. Here you can tag indexed content and use it during the search procedure.
Last setting is boost section. Here you have an ability to boost indexed content among other content that belongs to other locations.
And last but not the least, this search index uses the same HistoryEngine mechanism as old one. So, don't forget to copy configuration section from master database to a database where you want to add search index facilities to.

How it all works?
When an action performed on the item, database crawler updates entries in search index for the item. So that information in index is in sync with the one in database. How does it happen if "item:saved", "item:deleted", "item:renamed", "item:copied", "item:moved" do not have event handlers that trigger search index update? Thank to HistoryEngine that was mentioned several times already.
It is HistoryEngine that tracks any changes made to the item and fires appropriate event handler to process it.
IndexingManager is responsible for all operations to the search index. It subscribes to AddEntry event of HistoryEngine and as soon as an entry added to the History table, it triggers a job that updates the search index(es).
In web.config file there are a few settings that belong to indexing functionality.
  • Indexing.UpdateInterval - sets the interval between the IndexingManager checking its queue for pending actions. Default value is 5 minutes.
    What does it mean? If for whatever reason pending job was not executed, the IndexingManager will re-run it if it finds it in pending state after 5 minutes pass.
  • Indexing.UpdateJobThrottle - sets the minimum time to wait between individual index update jobs. Default value 1 second.
    When some operation is performed on the item, you can see this entry in Sitecore log file:
    INFO Starting update of index for the database 'databaseName' ( pending).
    This setting sets the interval between jobs like this. So that it does not overwhelm all CPU time if you're doing massive change to the items.
  • Indexing.ServerSpecificProperties - Indicates if server specific keys should be used for property values (such as 'last updated'). It's off by default.
    This setting is designed for content delivery environments in web farms. As web database is shared, there could be a situation when one server has updated its search indexes and changed History table in the database. Other servers won't update their indexes because HistoryEngine wouldn't indicate there was a change. This setting prevents situations like this.
Well... this is it for now. In next part we will take a look at Sitecore Lucene API and create some search queries with it.
Enjoy!