Laurent Kempé
Indexer et rechercher vos entités métier à l'aide du Framework Lucene.Net
Conception à l'aide des génériques et de la réflexion d'un moteur de recherche permettant d'indexer et rechercher du contenu dans des entités métier sans les polluer.
Par Laurent Kempé publié le 12/11/2007 à 00:04, lu 3254 fois, 6 pages
 4 | Détails de l'implémentation
Commençons par le plus simple, l'attribut ajouté à notre domaine, rien de bien particulier, il s'agit d'une classe qui hérite de System.Attribute :

using System;

 

namespace innoveo.Blog.Domain.Utils

{

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]

    public sealed class SearchableAttribute : Attribute

    {

    }

}

Rien de bien particulier non plus en ce qui concerne la classe IndexPath, il est toutefois à noter qu'elle fait la distinction entre une application web et les autres types d'applications :

using System;

using System.IO;

using System.Web.Hosting;

 

namespace innoveo.Utils.Lucene

{

    public static class IndexPath

    {

        private static readonly FileInfo indexPath;

 

        /// <summary>

        /// Initializes the <see cref="EntityIndexer"/> class.

        /// </summary>

        static IndexPath()

        {

            string applicationPath;

 

            if (HostingEnvironment.IsHosted)

                applicationPath = HostingEnvironment.MapPath("~/App_Data/");

            else

            {

                DirectoryInfo dirInfo = new DirectoryInfo(Environment.CurrentDirectory);

                applicationPath = dirInfo.Parent.Parent.FullName;

            }

 

            indexPath = new FileInfo(Path.Combine(applicationPath, @"index"));

        }

 

        /// <summary>

        /// Gets the index full path.

        /// </summary>

        /// <value>The index full path.</value>

        public static string FullPath

        {

            get { return indexPath.FullName; }

        }

    }

}

Il est tout à fait envisageable que cette classe évolue afin de pouvoir fournir la possibilité d'avoir plusieurs index, par exemple un par entité. Ce qui n'est pas le cas dans cette version et définit un seul index pour tout type d'entité.
Passons à la classe EntityIndexer qui permet de gérer un index et d'indexer les entités du domaine.
Pour cela elle donne accès à des méthodes permettant de créer et effacer un index :

/// <summary>

/// Deletes the Lucene index.

/// </summary>

public static void DeleteIndex()

{

    DirectoryInfo directory = new DirectoryInfo(IndexPath.FullPath);

 

    if (directory.Exists)

        directory.Delete(true);

}

 

/// <summary>

/// Deletes the Lucene index.

/// </summary>

public static void CreateIndex()

{

    IndexWriter writer = new IndexWriter(IndexPath.FullPath, new StandardAnalyzer(), true);

    writer.Close();

}

Une méthode permettant de connaitre le nombre d'entités indexées :

/// <summary>

/// Gets the indexed entities count.

/// </summary>

/// <value>The indexed entities count.</value>

public static int IndexedEntitiesCount

{

    get

    {

        IndexReader reader = IndexReader.Open(IndexPath.FullPath);

        int docsCount = reader.NumDocs();

        reader.Close();

        return docsCount;

    }

}

Nous avons enfin une méthode qui permet d'indexer les entités :

/// <summary>

/// Indexes the specified entity if some of it's properties are decorated with ATTRIBUTE.

/// </summary>

/// <typeparam name="T">The type of the entity to index</typeparam>

/// <typeparam name="ATTRIBUTE">The type of the Attribute to search for indexing the property.</typeparam>

/// <param name="entity">The entity.</param>

public static void Index<T, ATTRIBUTE>(T entity) where T : class where ATTRIBUTE : Attribute

{

    if (IsSearchableEntity<T, ATTRIBUTE>(entity))

    {

        IndexWriter writer = new IndexWriter(IndexPath.FullPath, new StandardAnalyzer(), false);

 

        writer.AddDocument(EntityDocument.GetDocument<T, ATTRIBUTE>(entity));

 

        writer.Optimize();

        writer.Close();

    }

}

Cette méthode utilise les génériques avec l'élément ATTRIBUTE définissant le type de l'attribut qu'elle doit rechercher dans l'entité du domaine. Elle impose bien entendu que cet élément hérite de la classe System.Attribute. Le type T étant le type de notre entité, par exemple dans notre solution utilisant le Framework le type Post ou Page.
Ce que fait cette méthode peut être décomposé de la façon suivante. Si l'entité contient une propriété décorée par l'attribut servant de métadonnée alors le contenu de cette propriété est ajouté à l'index Lucene.Net en créant un EntityDocument. La détermination de la décoration de l'entité par l'attribut est déléguée à la méthode suivante :

/// <summary>

/// Determines whether the specified entity has some properties decorated with the ATTRIBUTE.

/// </summary>

/// <typeparam name="T">The type of the entity to index</typeparam>

/// <typeparam name="ATTRIBUTE">The type of the TTRIBUTE.</typeparam>

/// <param name="entity">The entity.</param>

/// <returns>

///    <c>true</c> if the specified entity is searchable entity; otherwise, <c>false</c>.

/// </returns>

public static bool IsSearchableEntity<T, ATTRIBUTE>(T entity) where T : class

{

    foreach (PropertyInfo propertyInfo in typeof (T).GetProperties())

    {

        object[] attributes = propertyInfo.GetCustomAttributes(typeof (ATTRIBUTE), true);

 

        if (attributes.Length == 1)

            return true;

    }

 

    return false;

}

Cette méthode itère à l'aide de la réflexion sur toutes les propriétés de l'entité passée en paramètre et vérifie si cette dernière est décorée par un attribut de type ATTRIBUTE. Si elle trouve un attribut décoré elle retourne true sinon false.
Maintenant que nous pouvons déterminer si notre entité du domaine a une de ses propriétés décorées par notre attribut d'indexation, il reste à créer un Document Lucene.Net qui servira à l'indexation. C'est le rôle de la classe EntityDocument.
La classe EntityDocument agit comme une Factory avec la méthode static suivante :

/// <summary>

/// Create a Lucene Document of the specified entity if properties are marked with the ATTRIBUTE.

/// </summary>

/// <typeparam name="T">The business entity type</typeparam>

/// <typeparam name="ATTRIBUTE">The type of the ATTRIBUTE to search for</typeparam>

/// <param name="entity">The business entity.</param>

/// <returns>

/// A Lucene document containing fields if the entity had properties marked as Searchable,

/// otherwise an empty document

/// </returns>

public static Document GetDocument<T, ATTRIBUTE>(T entity) where ATTRIBUTE : Attribute

{

    Document doc = new Document();

 

    if (AddIdFieldToDocument(doc, entity))

        foreach (PropertyInfo propertyInfo in typeof (T).GetProperties())

            AddSearchablePropertyToDocument<T, ATTRIBUTE>(doc, entity, propertyInfo);

 

    return doc;

}

Elle crée tout d'abord un Document Lucene.Net auquel elle va ajouter un field représentant l'identifiant de notre entité du domaine grâce à la méthode AddIdFieldToDocument:

/// <summary>

/// Adds the id field to the Lucene document.

/// </summary>

/// <typeparam name="T">The business entity type</typeparam>

/// <param name="doc">The Lucene document.</param>

/// <param name="entity">The business entity.</param>

/// <returns>

///    <c>true</c> if the id field was added to the Lucene document; otherwise, <c>false</c>.

/// </returns>

private static bool AddIdFieldToDocument<T>(Document doc, T entity)

{

    PropertyInfo idProperty = typeof (T).GetProperty("Id");

 

    if (idProperty == null)

        return false;

 

    string entityId = idProperty.GetValue(entity, null) as string;

 

    if (string.IsNullOrEmpty(entityId))

        return false;

 

    doc.Add(new Field("Id", entityId, Field.Store.YES, Field.Index.UN_TOKENIZED));

 

    return true;

}

Le fait que notre entité métier doit avoir une Id est une restriction par rapport à notre Framework. Ceci pourrait être changé dans une nouvelle version afin de le rendre plus indépendant. Cette restriction nous servira plus tard lors de la recherche d'entité à l'aide de l'index car le contenu de l'entité n'est bien entendu pas sauvegardé dans l'index. Il nous faudra donc une information référençant l'entité dans l'index qui nous permettra de charger l'entité depuis la couche de données. C'est le rôle de l'Id qui est un identifiant unique de notre entité du domaine.
Puis nous ajoutons à notre Document un field par propriété de l'entité qui est décorée :

/// <summary>

/// Adds the searchable property value to the Lucene document.

/// </summary>

/// <typeparam name="T">The business entity type</typeparam>

/// <typeparam name="ATTRIBUTE">The type of the ATTRIBUTE to search for</typeparam>

/// <param name="doc">The Lucene document.</param>

/// <param name="entity">The business entity.</param>

/// <param name="propertyInfo">The property info.</param>

private static void AddSearchablePropertyToDocument<T, ATTRIBUTE>(Document doc, T entity,

                                                                  PropertyInfo propertyInfo)

    where ATTRIBUTE : Attribute

{

    object[] attributes = propertyInfo.GetCustomAttributes(typeof (ATTRIBUTE), true);

 

    if (attributes.Length == 1)

    {

        ATTRIBUTE searchableAttribute = attributes[0] as ATTRIBUTE;

 

        //todo: add validation, the attribute must decorate a property returning a String

 

        if (searchableAttribute != null)

        {

            string toIndex = propertyInfo.GetValue(entity, null) as string;

 

            if (toIndex != null)

                doc.Add(new Field(propertyInfo.Name, new StringReader(toIndex)));

        }

    }

}

Un field Lucene.Net est une paire nom/valeur. Nous utilisons comme nom le nom de la propriété décorée et comme valeur la valeur de cette propriété lue à travers la classe StringReader.
Nous avons donc maintenant vu comment indexer nos entités du domaine. Voilà comment effacer une référence à une entité de l'index :

/// <summary>

/// Removes the entity from the index.

/// </summary>

/// <typeparam name="T">The type of the entity to index</typeparam>

/// <typeparam name="ATTRIBUTE">The type of the TTRIBUTE.</typeparam>

/// <param name="entity">The entity.</param>

public static void RemoveEntityFromIndex<T, ATTRIBUTE>(T entity) where T : class

{

    if (IsSearchableEntity<T, ATTRIBUTE>(entity))

    {

        //todo: check if we can remove the "Id"

        PropertyInfo idProperty = typeof (T).GetProperty("Id");

 

        if (idProperty != null)

        {

            string entityId = idProperty.GetValue(entity, null) as string;

 

            if (!string.IsNullOrEmpty(entityId))

            {

                IndexReader reader = IndexReader.Open(IndexPath.FullPath);

                reader.DeleteDocuments(new Term("Id", entityId));

                reader.Close();

            }

        }

    }

}

Cette méthode utilise tout simplement le fait que notre index contient une référence sur un identifiant unique de notre entité. A l'aide de cette information il efface le Document Lucene.Net de l'index.
Il nous reste encore à voir comment rechercher une entité dans notre index.
Pour cela nous avons définit la classe EntitySearcher avec la méthode Search suivante :

/// <summary>

/// Searches for business entities having indexed properties matching the specified query string.

/// </summary>

/// <param name="queryString">The query string.</param>

/// <param name="queryField">Name of the Lucene field to search.</param>

/// <returns>All business entities matching</returns>

public static IEnumerable<string> Search(string queryString, string queryField)

{

    string queryEscaped = QueryParser.Escape(queryString); 

 

        foreach (Document document in SearchLuceneDocument(queryEscaped, queryField))

            yield return document.Get("Id");

}

Cette méthode prend comme paramètres deux chaines de caractères, l'un représente la chaine servant de requête alors que l'autre spécifie sur quel field Lucene.Net la recherche doit s'effectuer. Elle retourne une collection de chaine de caractères représentant les Id des entités correspondant à la requête. Search utilise une méthode interne à la classe qui, elle, travaille sur des Documents Lucene.Net :

/// <summary>

/// Searches for Lucene document matching the specified query string.

/// </summary>

/// <param name="queryString">The query string.</param>

/// <param name="queryField">Name of the Lucene field to search.</param>

/// <returns>All Lucene document matching</returns>

protected static IEnumerable<Document> SearchLuceneDocument(String queryString, string queryField)

{

    IndexReader reader = IndexReader.Open(IndexPath.FullPath);

 

    Searcher searcher = new IndexSearcher(reader);

 

    QueryParser parser = new QueryParser(queryField, new StandardAnalyzer());

 

    Hits hits = searcher.Search(parser.Parse(queryString));

 

    for (int i = 0; i < hits.Length(); i++)

        yield return hits.Doc(i);

 

    reader.Close();

}

Il ne nous reste plus qu'à voir comment utiliser notre nouveau Framework.
 
» Démarrer une discussion
 
Discussion démarée par teddyalbina le 24/08/2008 à 11:41, 1 commentaire(s).