-
Notifications
You must be signed in to change notification settings - Fork 26
Querying for content
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.
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.
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
.
var result = QueryHelper<BaseModel>.SimilarImages(Guid.Parse("84f9d72e-63a1-43cb-b7c0-7b53cdf13e59"), "en-gb");
<img src="@(result.Cast<ImageVM>()?.FirstOrDefault()?.Image.Path)"/>
var qh = new QueryHelper<Homepage>();
@qh.Regex(x => x.Title, "/hom.*/").GetAllNoCast().Count
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).
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
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.
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".
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
.
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();
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.
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".