Blog

Posts written in December 2010.

  • Published on
    -
    4 min read

    Multi Query Search Using Lucene.NET

    Over the last few days I have been doing some research on the best way to implement search functionality for a site I am currently building. The site will consists mainly of news articles. The client wanted a search that would allow a user to search across all fields that related to a news article.

    Originally, I envisaged writing my own SQL to query a few tables within my database to return some search results. But as I delved further into designing the database architecture in the early planning stages, I found that my original (somewhat closed minded) approach wouldn't be flexible nor scalable enough to search and extract all the information I required.

    From what I have researched, the general consensus is to either use SQL Full Text Search or Lucene.NET. Many have favoured the use of Lucene due to its richer querying language and generally more flexible since you have the ability to write a search index tailored to your project. From what I gather, Lucene can work with any type of text data. For example, you not only can index rows in your database but there are also solutions to support indexing physical files in your application. Neat!

    I have written some basic code (below) with a couple methods to get started in creating a search index and carrying out a multi-query search across your whole index. You would further enhance this code to only carry out a full index once all required records have been added. Most implementations of Lucene would use incremental indexing, where documents already in the index are just updated individually, rather than deleting the whole index and building a new one every time. I plan to hook up and optimise my Lucene code into a service that would be scheduled to carry out an incremental index every midnight.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using Lucene;
    using Lucene.Net;
    using Lucene.Net.Store;
    using Lucene.Net.Analysis;
    using Lucene.Net.Analysis.Standard;
    using Lucene.Net.Index;
    using Lucene.Net.Documents;
    using Lucene.Net.QueryParsers;
    using Lucene.Net.Search;
    using System.Configuration; 
    
    namespace MES.DataManager.Search
    {
        public class Lucene
        {
            public static void IndexSite()
            {           
                    //The file location of the index
                    string indexLocation = @ConfigurationManager.AppSettings["SearchIndexPath"];
    
                    Directory searchDirectory = null;
    
                    if (System.IO.Directory.Exists(indexLocation))
                        searchDirectory = FSDirectory.GetDirectory(indexLocation, false);
                    else
                        searchDirectory = FSDirectory.GetDirectory(indexLocation, true); 
    
                    //Create an analyzer to process the text
                    Analyzer searchAnalyser = new StandardAnalyzer(); 
    
                    //Create the index writer with the directory and analyzer.
                    IndexWriter indexWriter = new IndexWriter(searchDirectory, searchAnalyser, true);
    
                    //Iterate through Article table and populate the index
                    foreach (Article a in ArticleBLL.GetArticleDetails())
                    {
                        Document doc = new Document();
    
                        doc.Add(new Field("id", a.ID.ToString(), Field.Store.YES, Field.Index.UN_TOKENIZED, Field.TermVector.YES));
                        doc.Add(new Field("title", a.Title, Field.Store.YES, Field.Index.TOKENIZED, Field.TermVector.YES));
                        doc.Add(new Field("articletype", a.Type.TypeName, Field.Store.YES, Field.Index.TOKENIZED, Field.TermVector.YES)); 
    
                        if (!String.IsNullOrEmpty(a.Summary))
                            doc.Add(new Field("summary", a.Summary, Field.Store.YES, Field.Index.TOKENIZED, Field.TermVector.YES));                
    
                        if (!String.IsNullOrEmpty(a.ByLineShort))
                            doc.Add(new Field("bylineshort", a.ByLineShort, Field.Store.YES, Field.Index.TOKENIZED, Field.TermVector.YES));                    
    
                        if (!String.IsNullOrEmpty(a.ByLineLong))
                            doc.Add(new Field("bylinelong", a.ByLineLong, Field.Store.YES, Field.Index.TOKENIZED, Field.TermVector.YES));                   
    
                        if (!String.IsNullOrEmpty(a.BasicWords))
                            doc.Add(new Field("basicwords", a.BasicWords, Field.Store.YES, Field.Index.TOKENIZED, Field.TermVector.YES));                   
    
                        if (!String.IsNullOrEmpty(a.MediumWords))
                            doc.Add(new Field("mediumwords", a.MediumWords, Field.Store.YES, Field.Index.TOKENIZED, Field.TermVector.YES));                   
    
                        if (!String.IsNullOrEmpty(a.LongWords))
                            doc.Add(new Field("longwords", a.LongWords, Field.Store.YES, Field.Index.TOKENIZED, Field.TermVector.YES));  
    
                        //Write the document to the index
                        indexWriter.AddDocument(doc);
                    }
                              
    
                    //Optimize and close the writer
                    indexWriter.Optimize();
                    indexWriter.Close();         
            }
    
            public static List<CoreArticleDetail> SearchArticles(string searchTerm)
            {
                Analyzer analyzer = new StandardAnalyzer(); 
    
                //Search by multiple fields
                MultiFieldQueryParser parser = new MultiFieldQueryParser(
                                                                    new string[]
                                                                    {
                                                                        "title",
                                                                        "summary",
                                                                        "bylineshort",
                                                                        "bylinelong",
                                                                        "basicwords",
                                                                        "mediumwords",
                                                                        "longwords"
                                                                    },
                                                                    analyzer); 
    
                Query query = parser.Parse(searchTerm); 
    
                //Create an index searcher that will perform the search
                IndexSearcher searcher = new IndexSearcher(@ConfigurationManager.AppSettings["SearchIndexPath"]); 
    
                //Execute the query
                Hits hits = searcher.Search(query);
    
                List<int> articleIDs = new List<int>(); 
    
                //Iterate through index and return all article id’s
                for (int i = 0; i < hits.Length(); i++)
                {
                    Document doc = hits.Doc(i);
    
                    articleIDs.Add(int.Parse(doc.Get("id")));
                } 
    
                return ArticleBLL.GetArticleSearchInformation(articleIDs);
            }
    
        }
    }
    

    As you can see, my example allows you to carry out a search across as many of your fields as you require which I am sure you will find useful. It took a lot of research to find out how to carry out a multi query search. Majority of the examples I found over the internet showed you how to search only one field.

    The main advantage I can see straight away from using Lucene is that since the search data is held on disk, there is hardly any need to query the database. The only downside I can see is problems being caused by the possibility a corrupt index.

    For more information on using Lucene, here are a couple of links that you may find useful to get started (I know I did):

    http://www.codeproject.com/KB/library/IntroducingLucene.aspx http://ifdefined.com/blog/post/Full-Text-Search-in-ASPNET-using-LuceneNET.aspx

  • Published on
    -
    4 min read

    Watermarking Images On The Fly Using ASP.NET

    Watermarking and general image manipulation within the .NET Framework has become quite an easy thing to carry out thanks to the features provided by the System.Drawing namespace. The System.Drawing namespace contains types to help you with…well…drawing and rendering images. I will not be covering the basic use of the System.Drawing class. But feel free to carry out a Google.

    My example consists of using a .NET (aspx) page and a Generic Handler (ashx). The .NET page will allow me to select an image, add a logo to the top left and some text. The Generic Handler will contain all the magic needed to manipulate the image based on selections made within the .NET page. The screenshot (below) shows my basic program in action.

    Image Watermarking

    Firstly, let me start off by showing you the code for the Generic Handler.

    ImageRenderJpeg.ashx

    using System;
    using System.Web;
    using System.Drawing;
    using System.Drawing.Imaging;
    using System.Drawing.Drawing2D;
    
    public class ImageRenderJpeg : IHttpHandler
    {
        public void ProcessRequest(HttpContext context)
        {
            context.Response.ContentType = "image/jpeg";
    
            //Retrieve image details
            string imageUrl = context.Request.QueryString["ImageUrl"].ToString();
            string imageComment = context.Request.QueryString["ImageComment"].ToString();
            string imageIconUrl = context.Request.QueryString["Icon"].ToString();
    
            if (!String.IsNullOrEmpty(imageUrl))
            {
                //Get the location of the image
                Image imagePhoto = Image.FromFile(imageUrl);
    
                // Get dimensions of image
                int imageHeight = imagePhoto.Height;
                int imageWidth = imagePhoto.Width;
    
                //Create a new Bitmap
                Bitmap oBitmap = new Bitmap(imageWidth, imageHeight, PixelFormat.Format24bppRgb);
    
                //Load Background Graphic from Image
                Graphics oGraphics = Graphics.FromImage(oBitmap);
                oGraphics.SmoothingMode = SmoothingMode.HighQuality;
                oGraphics.DrawImage(imagePhoto, new Rectangle(0, 0, imageWidth, imageHeight), 0, 0, imageWidth, imageHeight, GraphicsUnit.Pixel);
    
                //Layer 1: Add an Image Logo to the top left
                if (!String.IsNullOrEmpty(imageIconUrl))
                {
                    Image imageIcon = Image.FromFile(imageIconUrl);
                    oGraphics.DrawImage(imageIcon, new Rectangle(5, 5, 124, 48), 0, 0, imageIcon.Width, imageIcon.Height, GraphicsUnit.Pixel);
    
                    imageIcon.Dispose();
                }
                
                //Layer 2: Add Comment
                if (!String.IsNullOrEmpty(imageComment))
                {
                    Font commentFont = new Font("Arial", 14, FontStyle.Regular); //Font Style
                    StringFormat commentFormat = new StringFormat();
                    commentFormat.Alignment = StringAlignment.Near; //Align text in left of layer
    
                    SolidBrush commentBrush = new SolidBrush(Color.Black); //Font Colour
    
                    oGraphics.FillRectangle(Brushes.Beige, 5, imageHeight - 55, imageWidth - 15, 50); //Create a rectangle with white background
                    oGraphics.DrawString(imageComment, commentFont, commentBrush, new Rectangle(5, imageHeight - 55, imageWidth - 15, 50), commentFormat); //Add comment text inside rectangle
                }
                
                //Layer 3: Add Copyright watermark
                Font watermarkFont = new Font("Arial", 40, FontStyle.Bold); //Font Style
                SolidBrush semiTransBrush = new SolidBrush(Color.LightGray); //Font Colour
                StringFormat watermarkFormat = new StringFormat();
                watermarkFormat.Alignment = StringAlignment.Center; //Align text in center of image
    
                oGraphics.DrawString("Copyright",
                    watermarkFont,
                    semiTransBrush,
                    new PointF(imageWidth / 2, imageHeight / 2), watermarkFormat);
    
                //Dispose of graphic objects
                imagePhoto.Dispose();
                oGraphics.Dispose();
    
                //Output image
                oBitmap.Save(context.Response.OutputStream, ImageFormat.Jpeg);
            }
            else
            {
            }
    
        }
    
        public bool IsReusable
        {
            get
            {
                return false;
            }
        }
    }
    

    You can see that I am manipulating my image based on the query string parameters I pass from my .NET page into my Generic Handler. Hopefully, my code is commented well enough to explain the general overview on what is going on.

    The following code displays how my aspx page parses all the parameters needed to generate an image on the page:

    Default.aspx

    <%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>
    
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head runat="server">
        <title>Image Generator</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            Select an Image:
            <asp:DropDownList ID="ddlImage" runat="server">
                <asp:ListItem Text="*" Value="*">Select Image</asp:ListItem>
                <asp:ListItem Text="The Tumbler" Value="C:\Users\Surinder\Documents\Visual Studio 2010\WebSites\ImageCreator\Images\batmobile_Tumbler.jpg"></asp:ListItem>
                <asp:ListItem Text="Audi TT" Value="C:\Users\Surinder\Documents\Visual Studio 2010\WebSites\ImageCreator\Images\new-audi-tt-coupe.jpg"></asp:ListItem>
                <asp:ListItem Text="Volvo Concept" Value="C:\Users\Surinder\Documents\Visual Studio 2010\WebSites\ImageCreator\Images\volvo-s60-concept-interior1.jpg"></asp:ListItem>
            </asp:DropDownList>
            <br />
            <br />
            Add Logo: <asp:TextBox ID="txtImage" runat="server"></asp:TextBox>
            <br />
            <br />
            Add a comment:
            <asp:TextBox ID="txtComment" runat="server" TextMode="MultiLine" Width="500" Height="50"></asp:TextBox>
            <br />
            <br />
            <asp:Button ID="btnCreateImage" Text="Create Image" runat="server" onclick="btnCreateImage_Click" />
            <br />
            <br />
            <img id="imgRender" alt="Image Render" title="Image Render" runat="server" />        
        </div>
        </form>
    </body>
    </html>
    

    Default.aspx.cs

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Web.UI;
    using System.Web.UI.WebControls;
    
    public partial class _Default : System.Web.UI.Page 
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (ddlImage.SelectedValue == "*")
            {
                imgRender.Visible = false;
            }
            else
            {
                imgRender.Visible = true;
            }
        }
    
        protected void btnCreateImage_Click(object sender, EventArgs e)
        {
            if (ddlImage.SelectedValue != "*")
            {
                //The Image source will be pointed to our Generic Handler to display the image.
                imgRender.Src = String.Format("ImageRenderJpeg.ashx?ImageUrl={0}&ImageComment={1}&Icon={2}", ddlImage.SelectedValue, txtComment.Text, txtImage.Text);
            }
        }
    }