Skip to content

Querying for content

Simon Yohannes edited this page Nov 14, 2023 · 31 revisions

using the QueryHelper class

the QueryHelper<T> class is in the puck.core.Helpers namespace and is used to query for content stored in Lucene. Puck stores your content revisions in SQL Server using entity framework but that's for the backoffice. your web pages/templates retrieve content from Lucene, which is where the last published revision is stored and can be queried from.

the T parameter is the Type of ViewModel you're trying to retrieve. you'd think that this:

var qh = new QueryHelper<Page>();

would return all ViewModels of type Page but actually it returns all ViewModels who have Page in their inheritance chain. so basically all ViewModels which can be cast successfully to Page (ViewModels derived from Page). This is Polymorphic search. If you want to return only Page content specifically, you can do the following:

   var qh = new QueryHelper<Page>()
       .ExplicitType();

you can search fields in a strongly typed way:

   qh.Must().Field(x => x.MainContent, "london")

you can also chain multiple fields with Or() and the search will use the Lucene analyzers specified (per field) in the ViewModel being searched for.

if you want to search for ViewModels that implement a certain interface, you can do this:

    var qh = new QueryHelper<BaseModel>()
        .Implements<IMainContent>();
    var results = qh.GetAllNoCast()
        .Cast<IMainContent>();
    foreach(var viewModel in results){
        @viewModel.MainContent
    }

notice that the type parameter for QueryHelper is BaseModel but that you specify the desired interface in the Implements method. you can then Cast the results to your interface. there is also a non generic overload for the Implements method which accepts params Type[] allowing you to specify multiple interfaces, so you can do this:

var qh = new QueryHelper<BaseModel>()
   .Implements(typeof(IMainContent),typeof(IGallery));

or this

var interfaces = new Type[]{typeof(IMainContent),typeof(IGallery)};
var qh = new QueryHelper<BaseModel>()
   .Implements(interfaces);

only ViewModels which implement all specified interfaces will be returned. since you're using a QueryHelper<BaseModel> instance, you'll only be able to use BaseModel properties in your expressions, to get around this, you can do this:

var qh = new QueryHelper<BaseModel>()
    .Implements<IMainContent>()
    .Must().Field<IMainContent>(x=>x.MainContent,"london");

in the example above, you use the generic version of the Field method and specify your interface as its generic Type argument, you'll then be able to use expressions with properties from that interface.

if you wanted to limit the search to the current root (if you have a multi-site setup) and also get only content of the current language (taking into account localisation settings for the current url) you would do the following:

   qh.CurrentRoot(Model)
     .CurrentLanguage();    

notice how you can chain methods. the Model argument above is the Model property of your template.

there are also methods for range queries:

qh.GreaterThanEqualTo(x => x.Age, 18);
qh.Range(x => x.Updated, new DateTime(2019, 7, 12, 11, 28, 20), DateTime.Now, true, true);

the two boolean arguments for Range() are inclusive start and inclusive end, respectively.

Searching Lists

public class Page:BaseModel
{
    [Display(ShortName ="input",GroupName ="Content")]
    [UIHint("ListEditor")]
    public List<string> Names { get; set; }
}

given the model above has a Names property which is a List<string>, you can search for any names in the list by doing the following:

    var results = new QueryHelper<Page>()
        .Must().Field(x=>x.Names,"Joe")
        .GetAllNoCast()

the above query will search for Page ViewModels with name "Joe" present in its Names list.

you can also search in lists of complex types. Let's say the Page ViewModel now has a List<Person> property, "People", and that Person class has a property Age. to search for people with an age of 21 you would do the following:

    var results = new QueryHelper<Page>()
        .Must().Field(x=>x.People[0].Age,21)
        .GetAllNoCast()

to clarify, the square brackets in x.People[0].Age is used just to access the Age property, it doesn't mean only search in index 0 of the list. it will search the whole list.

Execute a query

you can execute your query by using the Get() or GetAll() methods on the QueryHelper instance. However, i would recommend using GetAllNoCast() and GetNoCast() so you always get the original type returned. this is useful since by default, QueryHelper<T> is returning any ViewModels which can be cast to type T, unless you use the ExplicitType() method to specify that you want only type T returned. so if you have the ViewModel inheritance chain NewsPage:Page:BaseModel, searching using QueryHelper<Page> without specifying ExplicitType() will also return NewsPage ViewModels and if you use GetAllNoCast() they will be returned as actual NewsPage types but if you just use GetAll() they will be returned as type Page.

Image Similarity Search

var result = QueryHelper<BaseModel>.SimilarImages(Guid.Parse("84f9d72e-63a1-43cb-b7c0-7b53cdf13e59"), "en-gb");
<img src="@(result.Cast<ImageVM>()?.FirstOrDefault()?.Image.Path)"/>
    

Regex Search

var qh = new QueryHelper<Homepage>();
@qh.Regex(x => x.Title, "/hom.*/").GetAllNoCast().Count

Geo Queries

Puck also supports Geo queries. the first thing you need to do is include a GeoPosition property in your ViewModel. GeoPosition can be found in the puck.core.Models namespace.

public class Page:BaseModel
    {
        public GeoPosition Location { get; set; }                
    }

you can see in the ViewModel above, the location property is of type GeoPosition and it uses a google maps editor template to set the Longitude and Latitude values.

to search this field, you would do the following:

    var geoQuery = new QueryHelper<Page>()
       .WithinMiles(x => x.Location.LongLat, -0.1277582, 51.5073509, 10)
       .SortByDistanceFromPoint(x=> x.Location.LongLat,-0.127782, 51.5073509);
    var georesults = geoQuery.GetAll();

the query above searches for Page ViewModels which are within 10 miles of Longitude -0.1277582 and Latitude 51.5073509. there is also a WithinKilometers method if you prefer. it also specifies a sort to make sure results are returned in order of closest distance from a particular longitude and latitude and there is an optional parameter to reverse the sort.

if you don't want to use the GeoPosition class, you can specify your own Property as being a spatial field:

    [IndexSettings(Spatial=true)]
    public string LongLat { get; set; }

use the IndexSettings attribute to mark the property as a spatial field and the value for a spatial field must be a string in the format of "Longitude,Latitude". eg "-0.12,51.50" - notice the comma separating the Longitude from the Latitude (x,y).

BaseModel extension methods

your ViewModels will all have access to extensions methods to help you get Parent, Ancestors, Children, Descendants, Siblings and Variants.

here's an example of getting Descendants of the current ViewModel:

var descendants = Model.Descendants<Page>();

if you're using QueryHelper rather than the extension methods, you also have access to AncestorsOf, SiblingsOf, ChildrenOf and DescendantsOf methods. for example:

    var qh = new QueryHelper<Section>()
        .DescendantsOf(Model)
        .ExplicitType()
        .GetAll();

the query above will get the descendants of the current Model which are of type Section

Sorting

you can sort on one or more fields by doing the following:

var searchQuery = new QueryHelper<Page>()
    .Must()
    .Field(x => x.Title, "london")
    .Sort(x=> x.Title)
    .Sort(x=> x.SortOrder);
    var results = searchQuery.GetAllNoCast();

in the above query you're searching the title field for the term "london" and then sorting by title then sort order.

Inner queries and Groups

you can pass in an inner query to the Group(), And(), Or() and Not() methods for more advanced queries. here's an example:

    var qh = new QueryHelper<Page>();
    var innerQuery = qh.New();
    qh.Must().Group(innerQuery.Field(x=>x.MainContent,"news").Field(x=>x.MainContent,"events"));
    qh.GetAll();
            

in the above query, you use New() to get a new inner query and then have a query where MainContent must contain either "news" or "events".

Getting selected content from Content Picker

if you've got List<PuckReference> properties in your ViewModels and have selected content/images which you want to retrieve, you can use the GetAll<T>() and Get<T>() methods to retrieve selected content. these are extensions methods so you will need to have a using statement for the namespace puck.core.Helpers.

Getting the current page from a controller

if you've intercepted the current page and want to retrive its ViewModel from your controller action, you can do this:

var currentNode = QueryHelper<Page>.Current();

String extension methods

you can modify your search terms by using the following string extension methods:

var qh = new QueryHelper<Page>()
   .Field(x=>x.Title,"london".Wrap())
   .Field(x=>x.Title,"london".WildCardSingle())
   .Field(x=>x.Title,"london".WildCardMulti())
   .Field(x=>x.Title,"london".Fuzzy(fuzzyness:2))
   .Field(x=>x.Title,"london".Boost(boost:2))
   .Field(x=>x.Title,"london".Proximity(proximity:2))
   .Field(x=>x.Title,"london".Escape());

in the examples above, string literals are used but the same applies for string variables. if you're unfamiliar with these Lucene modifiers, try this reference.

Highlighting text

you can use the string extension method Highlight in the puck.core.Extensions namespace (in the ExtensionsMisc class) to highlight search terms within a body of text. it will wrap the terms within a <span> tag with the css class "search_highlight", which you can then style using css.

here's an example usage: @Html.Raw(Model.MainContent.Highlight("news")). this will output the MainContent field with all occurrences of "news" encapsulated in a span tag with the css class "search_highlight".