Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

By using my Skybrud.Umbraco.GridData package (which introduces a strongly typed model for the Grid), indexing the new Grid in Umbraco 7.2 is quite easy.

At Skybrud.dk we typically create a class named ExamineIndexer that takes care of the Examine related events. This class should be initalized during Umbraco startup like this:

using Umbraco.Core;

namespace FanoeTest {

    public class Startup : ApplicationEventHandler {

        private static ExamineIndexer _examineIndexer;

        protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) {
            // Register events for Examine
            _examineIndexer = new ExamineIndexer();
        }

    }

}

The ExamineIndexer class it self will now look like the example below. The example is very specific, since only certain document types where we know that the content property holds a Grid value are handled.

The example could be modified to use the Content Service for finding all properties that holds a Grid value. But since the Content Service uses the database, it may slow the performance when building your index.

using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using Examine;
using Examine.Providers;
using Skybrud.Umbraco.GridData;
using Skybrud.Umbraco.GridData.Values;
using Umbraco.Core.Logging;

namespace FanoeTest {
   
    public class ExamineIndexer {

        public ExamineIndexer() {

            BaseIndexProvider externalIndexer = ExamineManager.Instance.IndexProviderCollection["ExternalIndexer"];

            externalIndexer.GatheringNodeData += OnExamineGatheringNodeData;

        }

        private void OnExamineGatheringNodeData(object sender, IndexingNodeDataEventArgs e) {

            try {

                string nodeTypeAlias = e.Fields["nodeTypeAlias"];

                LogHelper.Info<ExamineIndexer>("Gathering node data for node #" + e.NodeId + " (type: " + nodeTypeAlias + ")");

                if (nodeTypeAlias == "Home" || nodeTypeAlias == "LandingPage" || nodeTypeAlias == "TextPage" || nodeTypeAlias == "BlogPost") {

                    string value;

                    if (e.Fields.TryGetValue("content", out value)) {

                        LogHelper.Info<ExamineIndexer>("Node has \"content\" value\"");

                        GridDataModel grid = GridDataModel.Deserialize(e.Fields["content"]);

                        StringBuilder combined = new StringBuilder();

                        foreach (GridControl ctrl in grid.GetAllControls()) {

                            switch (ctrl.Editor.Alias) {

                                case "rte": {

                                    // Get the HTML value
                                    string html = ctrl.GetValue<GridControlRichTextValue>().Value;

                                    // Strip any HTML tags so we only have text
                                    string text = Regex.Replace(html, "<.*?>", "");

                                    // Extra decoding may be necessary
                                    text = HttpUtility.HtmlDecode(text);

                                    // Now append the text
                                    combined.AppendLine(text);
                                    
                                    break;
                                
                                }
                                
                                case "media": {
                                    GridControlMediaValue media = ctrl.GetValue<GridControlMediaValue>();
                                    combined.AppendLine(media.Caption);
                                    break;
                                }

                                case "headline":
                                case "quote": {
                                    combined.AppendLine(ctrl.GetValue<GridControlTextValue>().Value);
                                    break;
                                }

                            }

                        }

                        e.Fields["content"] = combined.ToString();

                    } else {

                        LogHelper.Info<ExamineIndexer>("Node has no \"content\" value\"");

                    }

                }

            } catch (Exception ex) {

                LogHelper.Error<ExamineIndexer>("MAYDAY! MAYDAY! MAYDAY!", ex);
            
            }

        }

    }

}
@theDiverDK

This comment has been minimized.

Copy link

@theDiverDK theDiverDK commented Jan 9, 2015

Hi Anders

Thanks for the example code, looking forward to check it out, and the package.

I hope you can help with a question.

I would like to put some boost on things like the 'headline' or a part of the RTE field?

Thanks in advance :)

@abjerner

This comment has been minimized.

Copy link
Owner Author

@abjerner abjerner commented Jan 9, 2015

I'm not sure that I follow. You wan't search results to have a higher priority if the query matches something in either the header or a richtext field?

I'm not an Examine expert, but I think that you can add those to their own Examine field(s), and then apply a higher priority to those fields when doing the search. I'm not sure how to do so, but I think I have seen in accomplished before.

@rasmusfjord

This comment has been minimized.

Copy link

@rasmusfjord rasmusfjord commented Jan 11, 2015

Looks awesome Anders.

Cant wait to being able to search in grids.
@theDiverDK Dunno if im misunderstanding you, but cant you just use .boost() or is it on content inside an RTE ?

@theDiverDK

This comment has been minimized.

Copy link

@theDiverDK theDiverDK commented Jan 12, 2015

Hi Anders and Rasmus

A bit hard to describe, and I am a complete Examine newbee :)

In a grid on our website, there could be some cell's containing 'Headline' another containing 'RTE' text and so on.

What i would like is that the text in the 'Headline' cell get's boostet compared to the 'RTE' cell content.

But not sure how to do it, as far as i can see the above example code, just returns all the grid's content as a single long string.

Hope this explains what i am trying to do? :)

@abjerner

This comment has been minimized.

Copy link
Owner Author

@abjerner abjerner commented Jan 14, 2015

The trick is to split the controls of the grid into different fields depending on the type of the control.

In the example below, values of rte and headline are added to the contentFocus field, while remaining values are added to the contentNormal field.

using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using Examine;
using Examine.Providers;
using Skybrud.Umbraco.GridData;
using Skybrud.Umbraco.GridData.Values;
using Umbraco.Core.Logging;

namespace FanoeTest {

    public class ExamineIndexer {

        public ExamineIndexer() {

            BaseIndexProvider externalIndexer = ExamineManager.Instance.IndexProviderCollection["ExternalIndexer"];

            externalIndexer.GatheringNodeData += OnExamineGatheringNodeData;

        }

        private void OnExamineGatheringNodeData(object sender, IndexingNodeDataEventArgs e) {

            try {

                string nodeTypeAlias = e.Fields["nodeTypeAlias"];

                LogHelper.Info<ExamineIndexer>("Gathering node data for node #" + e.NodeId + " (type: " + nodeTypeAlias + ")");

                if (nodeTypeAlias == "Home" || nodeTypeAlias == "LandingPage" || nodeTypeAlias == "TextPage" || nodeTypeAlias == "BlogPost") {

                    string value;

                    if (e.Fields.TryGetValue("content", out value)) {

                        LogHelper.Info<ExamineIndexer>("Node has \"content\" value\"");

                        GridDataModel grid = GridDataModel.Deserialize(e.Fields["content"]);

                        StringBuilder contentFocus = new StringBuilder();
                        StringBuilder contentNormal = new StringBuilder();

                        foreach (GridControl ctrl in grid.GetAllControls()) {

                            switch (ctrl.Editor.Alias) {

                                case "rte": {

                                    // Get the HTML value
                                    string html = ctrl.GetValue<GridControlRichTextValue>().Value;

                                    // Strip any HTML tags so we only have text
                                    string text = Regex.Replace(html, "<.*?>", "");

                                    // Extra decoding may be necessary
                                    text = HttpUtility.HtmlDecode(text);

                                    // Now append the text
                                    contentFocus.AppendLine(text);

                                    break;

                                }

                                case "media": {
                                    GridControlMediaValue media = ctrl.GetValue<GridControlMediaValue>();
                                    contentNormal.AppendLine(media.Caption);
                                    break;
                                }

                                case "headline":
                                    contentFocus.AppendLine(ctrl.GetValue<GridControlTextValue>().Value);
                                    break;

                                case "quote": {
                                    contentNormal.AppendLine(ctrl.GetValue<GridControlTextValue>().Value);
                                    break;
                                }

                            }

                        }

                        e.Fields["contentFocus"] = contentFocus.ToString();
                        e.Fields["contentNormal"] = contentNormal.ToString();

                    } else {

                        LogHelper.Info<ExamineIndexer>("Node has no \"content\" value\"");

                    }

                }

            } catch (Exception ex) {

                LogHelper.Error<ExamineIndexer>("MAYDAY! MAYDAY! MAYDAY!", ex);

            }

        }

    }

}

Then your search query could then look something like contentFocus:"hest"^2 contentNormal:"hest". The contentFocus field will then have twice the importance of the contentNormal field.

That should do the job ;)

@theDiverDK

This comment has been minimized.

Copy link

@theDiverDK theDiverDK commented Jan 15, 2015

Thanks Anders

Cool way to solve my problem :) I can see i need to dig deeper with Examine, to try to understand better how it is working :)

@AussieInSeattle

This comment has been minimized.

Copy link

@AussieInSeattle AussieInSeattle commented Feb 23, 2015

Nice work!

Can you provide an example for extending the Property Converters you've built in - as an example I have a custom headline grid editor that has value.title and value.subhead values - I know I could access them via JObject, but would prefer the strongly typed model approach. One issue I've also seen with custom grid editors is that sometimes value.subhead will actually be null (subhead wont even exist) if the user never enters anything into that textarea, so not sure if your Value Converter approach handles that?

Thanks,
Matt

@abjerner

This comment has been minimized.

Copy link
Owner Author

@abjerner abjerner commented Mar 19, 2015

Hi Matt. Sorry for not answering sooner - I hadn't seen your comment.

From what I understand, the JSON for your value looks like:

{
    "title": "Here is my title",
    "subhead": "Here is my sub header."
}

Implementing a class for your value could then look like below. We map the individual properties using JSON.net.

using Newtonsoft.Json;
using Skybrud.Umbraco.GridData.Interfaces;

public class GridControlHeaderValue : IGridControlValue {

    [JsonProperty("title")]
    public string Title { get; set; }

    [JsonProperty("subhead")]
    public string SubHeader { get; set; }

}

Then you need to register your editor so that the Grid model will automatically parse your value. Assuming that the alias of your editor is header, that can be done by the following line during startup:

using Newtonsoft.Json.Linq;
using Skybrud.Umbraco.GridData;
using Umbraco.Core;

public class GridStartup : ApplicationEventHandler {

    protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) {

        GridContext.Current["header"] = token => token.ToObject<GridControlHeaderValue>();

    }

}

If you don't like the lambda syntax, the snippet below does exactly the same:

using Newtonsoft.Json.Linq;
using Skybrud.Umbraco.GridData;
using Umbraco.Core;

public class GridStartup : ApplicationEventHandler {

    protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) {

        GridContext.Current["header"] = delegate(JToken token) {
            return token.ToObject<GridControlHeaderValue>();
        };

    }

}

On the less serious side, if you like Easter eggs, this is also possible :bowtie:

public class GridStartup : ApplicationEventHandler {

    protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) {

        PiggyBank.OneLittlePiggy["header"] = token => token.ToObject<GridControlHeaderValue>();

    }

}

I hope this answers your question. If not - and I'm to slow to answer - feel free to create an issue here on GitHub or ping me on Twitter 😏

@nvmy13t

This comment has been minimized.

Copy link

@nvmy13t nvmy13t commented May 18, 2015

Anders,

I am trying to follow your example for creating a new property converter (similar to Matt's question above). When I try to register my editor in ApplicationStarted, I get an error "The name 'GridContext' does not exist in the current context." Can you provide any guidance?

Thanks,

Mike

@asaliyog

This comment has been minimized.

Copy link

@asaliyog asaliyog commented Jul 29, 2015

Hi All,
I am trying to Index my custom grid editor data for search and following above example but stuck with an strange issue-
BaseIndexProvider externalIndexer = ExamineManager.Instance.IndexProviderCollection["ExternalIndexer"];

above line of code throwing an exception -Additional information: Exception has been thrown by the target of an invocation ,"Exception has been thrown by the target of an invocation. (Y:\Projects\testProject\testProject.Web\config\ExamineSettings.config line 18)"}

and my examinSetting.config file is-

Any help will be appreciated

Thanks

@pbne04

This comment has been minimized.

Copy link

@pbne04 pbne04 commented Dec 2, 2015

Hello,

I am trying to index my grid data, but after updating to Umbraco 7.3.3 (from 7.2.2), I am getting some exceptions when using GridDataModel.Deserialize(e.Fields["myGrid"])

The error + stacktrace is as follows:

Object reference not set to an instance of an object.
   at Skybrud.Umbraco.GridData.Howdy.ReplaceEditorObjectFromConfig(GridControl control)
   at Skybrud.Umbraco.GridData.GridControl.Parse(GridArea area, JObject obj)
   at Skybrud.Umbraco.GridData.GridArea.<>c__DisplayClass4.<Parse>b__2(JObject x)
   at Skybrud.Umbraco.GridData.Extensions.Json.JObjectExtensionMethods.<>c__DisplayClass1`1.<GetArray>b__0(JObject child)
   at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
   at System.Linq.Buffer`1..ctor(IEnumerable`1 source)
   at System.Linq.Enumerable.ToArray[TSource](IEnumerable`1 source)
   at Skybrud.Umbraco.GridData.Extensions.Json.JObjectExtensionMethods.GetArray[T](JObject obj, String propertyName, Func`2 func)
   at Skybrud.Umbraco.GridData.GridArea.Parse(GridRow row, JObject obj)
   at Skybrud.Umbraco.GridData.GridRow.<>c__DisplayClass14.<Parse>b__13(JObject x)
   at Skybrud.Umbraco.GridData.Extensions.Json.JObjectExtensionMethods.<>c__DisplayClass1`1.<GetArray>b__0(JObject child)
   at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
   at System.Linq.Buffer`1..ctor(IEnumerable`1 source)
   at System.Linq.Enumerable.ToArray[TSource](IEnumerable`1 source)
   at Skybrud.Umbraco.GridData.Extensions.Json.JObjectExtensionMethods.GetArray[T](JObject obj, String propertyName, Func`2 func)
   at Skybrud.Umbraco.GridData.GridRow.Parse(GridSection section, JObject obj)
   at Skybrud.Umbraco.GridData.GridSection.<>c__DisplayClass2.<Parse>b__1(JObject x)
   at Skybrud.Umbraco.GridData.Extensions.Json.JObjectExtensionMethods.<>c__DisplayClass1`1.<GetArray>b__0(JObject child)
   at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
   at System.Linq.Buffer`1..ctor(IEnumerable`1 source)
   at System.Linq.Enumerable.ToArray[TSource](IEnumerable`1 source)
   at Skybrud.Umbraco.GridData.Extensions.Json.JObjectExtensionMethods.GetArray[T](JObject obj, String propertyName, Func`2 func)
   at Skybrud.Umbraco.GridData.GridSection.Parse(GridDataModel model, JObject obj)
   at Skybrud.Umbraco.GridData.GridDataModel.<>c__DisplayClass2a.<Deserialize>b__29(JObject x)
   at Skybrud.Umbraco.GridData.Extensions.Json.JObjectExtensionMethods.<>c__DisplayClass1`1.<GetArray>b__0(JObject child)
   at System.Linq.Enumerable.WhereSelectEnumerableIterator`2.MoveNext()
   at System.Linq.Buffer`1..ctor(IEnumerable`1 source)
   at System.Linq.Enumerable.ToArray[TSource](IEnumerable`1 source)
   at Skybrud.Umbraco.GridData.Extensions.Json.JObjectExtensionMethods.GetArray[T](JObject obj, String propertyName, Func`2 func)
   at Skybrud.Umbraco.GridData.GridDataModel.Deserialize(String json)

The griddata which produced this error is the following

"{\r\n  \"name\": \"1 column layout\",\r\n  \"sections\": [\r\n    {\r\n      \"grid\": 12,\r\n      \"rows\": [\r\n        {\r\n          \"name\": \"Headline\",\r\n          \"areas\": [\r\n            {\r\n              \"grid\": 12,\r\n              \"editors\": [\r\n                \"headline\"\r\n              ],\r\n              \"controls\": [\r\n                {\r\n                  \"value\": \"<p>test headline</p>\",\r\n                  \"editor\": {\r\n                    \"name\": \"Rich text editor\",\r\n                    \"alias\": \"rte\",\r\n                    \"view\": \"rte\",\r\n                    \"icon\": \"icon-article\"\r\n                  }\r\n                }\r\n              ]\r\n            }\r\n          ],\r\n          \"id\": \"782155a3-775e-640e-5998-de9f8d677cc6\"\r\n        },\r\n        {\r\n          \"name\": \"Content\",\r\n          \"areas\": [\r\n            {\r\n              \"grid\": 12,\r\n              \"allowAll\": true,\r\n              \"controls\":
 [\r\n                {\r\n                  \"value\": \"<p>test content</p>\",\r\n                  \"editor\": {\r\n                    \"name\": \"Rich text editor\",\r\n                    \"alias\": \"rte\",\r\n                    \"view\": \"rte\",\r\n                    \"icon\": \"icon-article\"\r\n                  }\r\n                }\r\n              ]\r\n            }\r\n          ],\r\n          \"id\": \"0d570fba-c971-499b-32c6-80cf6bbc8c6b\"\r\n        }\r\n      ]\r\n    }\r\n  ]\r\n}"

Do you have a suggestion as to what might have gone wrong here?

Regards Peter

EDIT:
I downloaded the source code, built the dll and used it in my project - works like a charm. I guess maybe the Nuget package is not quite up to date?

EDIT#2:
Looks like the package has now been updated, https://github.com/skybrud/Skybrud.Umbraco.GridData/releases/tag/v1.5.1

@darrenst

This comment has been minimized.

Copy link

@darrenst darrenst commented May 6, 2016

I have manged to get this going. Thought I'd share my notes for newbee's as the instructions need a degree of modification for your own websites.

Create a new class that inherits from ApplicationEventHandler

protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) { // Register events for Examine <NamespaceToClass>.ExamineIndexer _examineIndexer = new ExamineIndexer(); Log.Info("Examine SkyBrid Indexer is starting"); }

This will run a class ExamineIndexer() on startup.

Next Create your ExamineIndexer() class

Copy the example above but amend to account for your property names.

In the ExamineIndexer Class the example uses the externalIndexer but amend if you are rolling your own as I am (MySearchIndexer).

BaseIndexProvider externalIndexer = ExamineManager.Instance.IndexProviderCollection["MySearchIndexer"];

In the OnExamineGatheringNodeData method only include the Document Type ALIAS names you want to search

if (nodeTypeAlias == "Landing" || nodeTypeAlias == "StandardPage")

You only want to include document types that actually have the grid property included

Next amend the if statement to include the name of the grid property alias. Mine is called "BodyGrid"

if (e.Fields.TryGetValue("bodyGrid", out value))

is also here on the deserialize method

GridDataModel grid = GridDataModel.Deserialize(e.Fields["bodyGrid"]);

next the method is looking for Grid editor alias matching. If you have changed or added new editors you will need to include in the case statement else it won't be included in the index. The additions i made were:

                            case "paragraph":
                            case "headline_centered":

These values are rolled up into the existing field "bodyGrid"

e.Fields["bodyGrid"] = combined.ToString();

OK that's it Build and test

NOTES:

Ensure your custom indexer is actually indexing your customer property (bodyGrid)

In your Search View ensure your search criteria is selecting your grid property. This is the code that handles my search page:

var Searcher = Examine.ExamineManager.Instance.SearchProviderCollection["MySearchSearcher"];

var SearchCriteria = Searcher.CreateSearchCriteria(Examine.SearchCriteria.BooleanOperation.Or);

var Query = SearchCriteria.Field("heroTitle", searchTerm.Boost(3)) .Or() .Field("title", searchTerm.Fuzzy().Value.Boost(2)) .Or() .Field("subtitle", searchTerm.Fuzzy().Value.Boost(1)) .Or() .Field("bodyGrid", searchTerm.Fuzzy()) ;

That should do it.

@naepalm

This comment has been minimized.

Copy link

@naepalm naepalm commented Dec 2, 2016

Just want to say I used this in 7.5.4 and it works like a charm. Thanks so much @abjerner :)

@slseverance

This comment has been minimized.

Copy link

@slseverance slseverance commented Nov 9, 2017

I'm having an issue with the basic installation. I read the instruction on the grid.skybrud.dk site that says simply drop the dlls in the bin folder. So I have downloaded the ZIP and pulled the xmls and dlls out of the latest release folder and dropped them into the bin folder. I restarted the app pool. It's not working so I assume the installation is more complicated than that? I have built a template using your example code and my aliases so i feel confident I have simply not installed it correctly. Must this be built and recompiled before operation or will a simple restart of service pick up the new packages? I don't have a viz studio solution set up. Thank you.

@zipswich

This comment has been minimized.

Copy link

@zipswich zipswich commented Dec 30, 2017

Thank you for the example. I have largely copied the code, and made sure ExamineIndexer() is called. Unfortunately, OnExamineGatheringNodeData is never invoked. Could you provide a tip on how to debug this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.