Saturday, November 22, 2014

Using Lucene search in DotNetNuke / Evoq Content 7+

Intro

I've recently improved a DNN module to use the new search engine present in the latest version of DotNetNuke. Before, I implemented my own search tool using SQL queries, but it was not very efficient (with LIKEs) and users always found ways not to find what they where looking for (using diacritic and other special characters, using quotes a.s.o...). In short, it was not good ;-)

I googled a bit to find examples, but didn't find many. So instead, I looked up the in the DNN source code. Here is a summary of my findings...

There were 2 areas I wanted to cover:

  • submit items to the DNN search engine (there are examples of that);
  • use the engine to retrieve my items and display them in my module (didn't find any example);


Submitting items to crawl

To use Lucene, you have to use the latest "ModuleSearchBase" base class for your FeatureController instead of implementing the ISearchable interface as in older versions.
You then override the GetModifiedSearchDocuments method and return a list of SearchDocument:

using System;
using DotNetNuke.Entities.Modules;
using DotNetNuke.Services.Search;
using DotNetNuke.Services.Search.Entities;
using DotNetNuke.Services.Search.Internals;

public class FeatureController : ModuleSearchBase
{
    private static readonly int ModuleSearchTypeId = SearchHelper.Instance.GetSearchTypeByName("module").SearchTypeId;

    public override IList GetModifiedSearchDocuments(ModuleInfo oModuleInfo, DateTime beginDate)
    {
        var oDocuments = new List();
        var oDocument = new SearchDocument();
        oDocument.Keywords.Add("keyword1", "value1");
        oDocument.UniqueKey = "some unique key to your app";
        oDocument.Title = "Title of the document";
        oDocument.Body = "Content of the document...";
        oDocument.AuthorUserId = oModuleInfo.LastModifiedByUserID;
        oDocument.ModuleId = oModuleInfo.ModuleID;
        oDocument.ModuleDefId = oModuleInfo.ModuleDefID;
        oDocument.PortalId = oModuleInfo.PortalID;
        oDocument.TabId = oModuleInfo.ParentTab.TabID;
        oDocument.SearchTypeId = ModuleSearchTypeId;
        oDocument.QueryString = "Parameters to pass to the page where my module is inserted into";
        oDocument.ModifiedTimeUtc = DocumentLastModifiedDate;

        // Important, if false, the document will be deleted from the search index
        oDocument.IsActive = true; 

        oDocuments.Add(oDocument);
        return oDocuments;
    }
}

You then run the site crawler job (or wait for it to run automatically) and you should be able to find your documents in the standard search. Clicking on an item will call the page where the module is located and add the querystring you specified for each document.

Note: you should take the "beginDate" parameter into account to only return new and modified documents, otherwise duplicate entries will show up. Unfortunately, if you purge the index in DNN, that date is not updated, and your module will return modified entries instead of returning all of them to refill the index... This is a bug I have reported to support, hopefully it will be fixed someday.

Using the DNN search in a module

A query is very simple to make. You make a new instance of SearchQuery, fill in some data, run it and display the results:

using DotNetNuke.Services.Search.Controllers;
using DotNetNuke.Services.Search.Entities;

protected void btnSearch_Click(object sender, EventArgs e)
{
    var oSearchQuery = new SearchQuery();
    oSearchQuery.KeyWords = "Search query";
    oSearchQuery.ModuleId = this.ModuleId;
    oSearchQuery.TabId = this.TabId;
    oSearchQuery.PortalIds = new List() { this.PortalId };
    oSearchQuery.WildCardSearch = true;

    var results = SearchController.Instance.ModuleSearch(oSearchQuery);

    if (results != null)
    {
        if (results.Results == null || results.Results.Count == 0)
            plhSearchResults.Controls.Add(new LiteralControl("<div class='alert alert-info'>" + LocalizeString("NoSearchResults") + "</div>"));
        else
        {
            plhSearchResults.Controls.Add(new LiteralControl("<ul style='list-style-type: none;'>"));
            foreach (var oRes in results.Results)
                plhSearchResults.Controls.Add(new LiteralControl(string.Format("<li><a href='{0}'>{1}</a><p>{2}</p></li>", oRes.Url, oRes.Title, oRes.Snippet)));
            plhSearchResults.Controls.Add(new LiteralControl("</ul>"));
        }
    }
}

Where plhSearchResults is an asp:placeholder receiving the list of results.

Hope that helps!