Searching on Extended Attributes in Mura CMS Using the Feed API

CFML , ColdFusion , Mura CMS Add comments

One of Mura CMS's nifty features is the Class Extension Manager. From the Mura docs, this tool "allows you to create 'attribute sets' of strictly-typed data which extend the base types (users, pages, portals, etc.)." Basically, with it you can create subtypes of content (or user) types and "extend" them with custom attributes if the need arises. For an example of how to use this tool, check out this post in the Mura forums.
While working with Mura, one of the things I discovered is that Mura's built-in search component only searches in the default fields of content objects. So if you extend the default content object with custom attributes, it doesn't search those extended attributes for the keywords that are entered. After asking in the Mura support forums, it was pointed out to me how one could use a relatively new Mura feature to build a custom search component that will allow you to search on extended attributes. Basically, you can have your search return a collection of content objects using the feedManager API. This API includes an addAdvancedParam() method which allows you to build a query that filters for content by searching in default as well as extended attributes. I'm going to show a simple example of how one can implement this.
In my case I was building an Events Calendar application. For the purposes of this example, let's say I created an "Event" subtype of the Default content type and added two custom atttributes: Venue and ZipCode. I'll build my search interface as a custom display object and include it into my site as a Mura component. In your /[siteid]/includes/display_objects/custom/ directory, set up the following heirarchy of files/directories ("eventsearch" and "inc" are directories):

eventsearch
  -inc
      -processSearchForm.cfm
  -index.cfm

Here's the code for index.cfm:

<cfif event.getValue('searchEvents') eq "true">
    <cfinclude template="inc/processSearchForm.cfm" />
</cfif>
<cfoutput>
    <form name="searchEvents" id="searchEvents" method="post">
       <fieldset>
          <legend>Keyword Search</legend>
          <ul>
              <li>
                  <label for="txtKeyword">Keyword</label>
                  <input type="text" name="keywords" id="txtKeyword" value="#event.getValue('keywords')#" />
              </li>
          </ul>
       </fieldset>
       <input type="hidden" name="searchEvents" value="true"/>
       <button type="submit" class="submit" type="submit" name="submit">Search</button>
    </form>
</cfoutput>

Nothing too complicated here. Just a self-submitting form with a text input box for a keyword search. There is a hidden form field which flags whether the form was submitted or not. Before the form, we check this flag and process the form if it has been submitted.
Now here is the code for inc/processSearchForm.cfm:

<cfsilent>
<!--- get a 'blank' feed object from the feed manager --->
<cfset feed=application.feedManager.read('') />
<!--- set the siteID of the feed to the current site --->
<cfset feed.setSiteID(event.getValue("siteID")) />
<!--- the feed will be sorted by the start date 
      of the item (in ascending order) --->
<cfset feed.setSortBy("displayStart") />
<cfset feed.setSortDirection("asc") />
<!--- only search for content of the Event subtype --->
<cfset feed.addAdvancedParam(relationship="AND"
       ,field="tcontent.subType"
       ,criteria="Event"
       ,dataType="varchar") />
<!--- only search for events ending today or later --->
<cfset feed.addAdvancedParam(relationship="AND"
       ,field="tcontent.displayStop"
       ,criteria="#now()#"
       ,condition="GTE"
       ,dataType="timestamp") />
<!--- if there are keywords, loop through them and build the query --->
<cfif len(event.getValue("keywords"))>
 <!--- andOpenGrouping starts a logic grouping with " and ( "--->
  <cfset feed.addAdvancedParam(relationship="andOpenGrouping") />
 <cfloop list="#event.getValue('keywords')#" index="keyword">
  <cfset feed.addAdvancedParam(relationship="OR"
         ,field="tcontent.title"
         ,criteria=trim(keyword)
         ,condition="CONTAINS"
         ,dataType="varchar") />
  <cfset feed.addAdvancedParam(relationship="OR"
         ,field="tcontent.body"
         ,criteria=trim(keyword)
         ,condition="CONTAINS"
         ,dataType="varchar") />
  <cfset feed.addAdvancedParam(relationship="OR"
         ,field="tcontent.summary"
         ,criteria=trim(keyword)
         ,condition="CONTAINS"
         ,dataType="varchar") />
  <cfset feed.addAdvancedParam(relationship="OR"
         ,field="tcontent.tags"
         ,criteria=trim(keyword)
         ,condition="CONTAINS"
         ,dataType="varchar") />
  <!--- if you do not specify a table in your 'field' argument, 
  it will look at extended attributes --->
   <cfset feed.addAdvancedParam(relationship="OR"
          ,field="venue"
          ,criteria=trim(keyword)
          ,condition="CONTAINS"
          ,dataType="varchar") />
 </cfloop>
 <!--- close the grouping --->
 <cfset feed.addAdvancedParam(relationship="closeGrouping") /> 
</cfif>
<!--- exectute the query using getFeed() --->
<cfset qSearchResults=application.feedManager.getFeed(feed) />
</cfsilent>
<cfif qSearchResults.recordCount>
<!--- display the query results with paging --->
<p>Your search retrieved <cfoutput>#qSearchResults.recordCount#</cfoutput> results.</p>
<cfoutput query="qSearchResults">
<dl<cfif qSearchResults.CurrentRow eq 1> class="first"<cfelseif qSearchResults.currentRow eq qSearchResults.recordCount> class="last"</cfif>>
 <dt class="releaseDate">#dateFormat(qSearchResults.displayStart,"mmmm, dd, yyyy")#</dt> 
 <dt><a href="#application.configBean.getContext()##request.contentRenderer.getURLStem(request.siteid,qSearchResults.filename)#">#qSearchResults.title#</a></dt>
 <cfif len(qSearchResults.summary)>
 <dd>#qSearchResults.summary#</dd>
 </cfif>
</dl>
</cfoutput>
<cfelse>
There are no upcoming events matching your search criteria.
</cfif>
<h3>Search Again</h3>

The comments in the code explain what's going on. The key to this is the addAdvancedParam() method of the feed object. First, I use it to limit the search to content that is of the "Event" subtype and to only display upcoming events. The method arguments are:

Relationship
This can be either "AND" or "OR" (defaults to "AND") and specifies what logic will be applied to the criteria that is passed in. Can also be used to form query logic groupings:
openGrouping is equivalent to "("
andOpenGrouping is equivalent to "and ("
orOpenGrouping is equivalent to "or ("
closeGrouping is equivalent to ")"

Field
This specifies the field in the tcontent table that the criteria will be compared against. If the table name isn't specified, then this specifies an extended attribute.

Criteria
This specifies the value that will be compared to the attribute specified by the "field" argument

Condition
This specifies how the criteria will be compared to the field value. Examples: "EQUALS" (the default), "NEQ", "GT, "LT","GTE","LTE","CONTAINS","IN"

Datatype
This argument specifies the cf_sql_ datatype that is used for proper cfqueryparaming of the query that is built

The search form takes a comma-delmited list of keywords. This list is looped over (within an "and" grouping) and for each keyword we look for content where the title, body, summary, tags, OR venue (the extended attribute) contains the given keyword. We use "OR" as the condition because we are searching for matches in any of those fields (if we used "AND", the keyword would have to be found in ALL of the fields that we searched in).
Once all the parameters are passed in, we use the feedManager.getFeed() method to retrieve the query from the feed object and then display the results.
Let's add a filter to our keyword search. Say we wanted to only search within a certain zip code. Add this code in the eventsearch/index.cfm file after the first fieldset:

Then in eventsearch/inc/processSearchForm.cfm, add this after the first <cfif> block:

There will be an "AND" condition added to the WHERE clause of the query to only include events within the passed in area code. Again, we're using the "field" argument to search within an extended attribute.

So now that we have our files set up, how do we include the search form in our site? Like I mentioned earlier, I'll use a Mura component. (You can also use this code in a plugin, but a component is a bit simpler and more straightforward. For more info on coding a plugin, Bob Silverberg is writing an excellent series on it.) In the Mura admin, click on "Components" in the left hand menu and then click on "Add Component" under where it says "Component Manager."  Name your component whatever you want (for example "Custom Search") and in the content editor enter this line:

Now, go back to the Site Manager and add a page. In the "Content Objects" tab, select "Components" from the "Available Content Objects" pulldown and then assign your "Custom Search" component to your main display region. That should be it. Your new page will display your new search form.

This is, of course, a VERY basic example, but I hope there's enough here to get people started. (You'll notice, for instance, that I didn't talk about form validation, but I wanted to stick to the subject of the post and not get off on any tangents.) Let me know if you have any questions.

14 responses to “Searching on Extended Attributes in Mura CMS Using the Feed API”

  1. Steve Withington Says:
    @Tony,
    Awesome! Thanks for sharing this ... I'm finally trying to bend my brain around Mura's Class Extension Manager.
  2. Sumit Verma Says:
    @Tony,

    Great article! Got me going in no time.

    Question: How can we create an extended attribute and search it as Date? RIght now seems like all the extended attributes as stored as varchar, so date search doesn't seem possible. Only thing I can think of is re-filtering the search result...
  3. Tony Garcia Says:
    @Sumit
    I see you got your question answered in the Mura forums:
    http://www.getmura.com/forum/messages.cfm?threadid=D2D0B1E8-C4AC-8215-4F26F9FEEC77FB48&page=2
  4. Elizabeth Says:
    This is very interesting, thanks Tony.

    Anyway I can get this to work on extended attributes for a user subtype?
  5. Tony Garcia Says:
    @Elizabeth,
    Sorry for my delay in responding. For user feeds it works pretty much the same way. There's an example of it in the developer documentation PDF that was just released. So you would do (using the new mura scope notation):

    <cfset userFeed = $.getBean( "userFeed" )/>
    <cfset userFeed.setSiteID( $.event("siteID") ) />

    and then call userFeed.addAdvancedParam() in pretty much the same way as for a content bean to filter on extended attributes.
    I also need to post an update to this post which explains how you can use an iterator to pull extended attributes out of the query.
  6. David Says:
    Hi Tony,
    I tried to get this to work, but need some additional help. I posted it on the mura forum here:
    http://www.getmura.com/forum/messages.cfm?threadid=FC33C555-01CE-424B-9050A6FC4F7605F4

    any ideas?
    Thanks,
    David
  7. cms web development Says:
    Thanks for updating us with the nice information.
  8. Andy J Says:
    Hello -
    This is a great example! I am trying to use this as a base for an interface I'm making to allow users to search for Site members of a particular subtype I made, "User/ServiceProvider" (basically anyone of this user sub type is part of a searchable directory).

    I changed your basic example to use the userFeeds like so:
    <!--- get a 'blank' feed object from the feed manager --->
    <cfset feed=$.getBean( "userFeed" ) />
    <!--- set the siteID of the feed to the current site --->
    <cfset feed.setSiteID( $.event("siteID")) />
    But I'm not quite getting it right. Any chance you might be able to shed some more insight on making this work for users? Thanks a TON for this example to get started.
  9. Andy J Says:
    Okay, I guess I spoke to soon - for anyone else looking to get started with a custom user search interface, this might get you going (this code would replace the code in the processSearchForm.cfm in the example from this post:
    <cfsilent>
    <!--- get a 'blank' userFeed object from the feed manager --->
    <cfset userFeed=$.getBean('userFeed')>

    <!--- set the siteID of the feed to the current site --->
    <cfset userFeed.setSiteID($.event("siteID"))>
    <!--- only search for content of the specific User subtype --->
    <cfset userFeed.addAdvancedParam(relationship="AND"
    ,field="tusers.subType"
    ,criteria="[SubTypeName]"
    ,dataType="varchar") />
    <!--- exectute the query using getFeed() --->
    <cfset qSearchResults=userFeed.getQuery() />
    </cfsilent>

    I took out the loop, but you can easily add that back in. Thanks again for this great post!
  10. Tony Garcia Says:
    Andy,
    Sorry for not getting back to you sooner. I'm happy that you found my post useful and that you were able to figure things out. One tip: if you have a fairly recent version of Mura, you can save yourself a tiny bit of typing now by using userFeed.addParam() instead of userFeed.addAdvancedParam(). AddParam() does the same thing.
  11. Andy J Says:
    Tony -
    Thanks for the tip! As I'm working on this, I have run across something I can't quite get - maybe you can help. I have extended "Page" with a subtype "ServiceItem". Service Item sub type has a bunch of extended fields, one of which is "ServiceTypes" which is a multi-select. So, on my UI I want to create a "menu" that displays all the ServiceTypes that have been selected on all of the ServiceItem subtype object. I was thinking I could use the same idea of an iterator, then loop over the iterator (or query like in your example) and create a list of ServiceTypes, then filter out dupes - but was wondering if there is a better way to get a list of all the ServiceItems that have been used?
  12. Tony Garcia Says:
    Hi Andy,
    I'm not sure if I understand your question fully. Do you just want easy access to a list of the selected ServiceTypes for each ServiceItem?
    You would have to use and iterator for your ServiceItems, because with a query you don't have access to extended attributes. So you could do something like this (once you have your ServiceItem feed):

    <cfset itServiceItems = serviceItemFeed.getIterator() />
    <cfloop condition="itServiceItems.hasNext()">
    <cfset thisItem = itServiceItems.next()>
    <!--- Output a list of the ServiceTypes for the current ServiceItem --->
    <cfoutput>#thisItem.getValue( "ServiceTypes" )#</cfoutput>
    </cfloop>

    Does this help?
  13. Manuel King Says:
    Looking at

    Condition
    This specifies how the criteria will be compared to the field value. Examples: "EQUALS" (the default), "NEQ", "GT, "LT","GTE","LTE","CONTAINS","IN"

    I was wondering if this is an all inclusive list as I used LIKE and it gives me a MYSQL error yet like is an acceptable condition? The I looked at the error and corresponding sql statement and see


    queryError

    You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''b%') ' at line 366

    (param 8) = [type='IN', class='java.lang.String', value='b%',

    It would appear that LIKE is set to EQUALS as maybe I Know that equals is the default so where do you think I
  14. Manuel King Says:
    I added
    <cfcase value="LIKE">
                 <cfset variables.condition="like" />
              </cfcase>
    to line 122
    in requirements/mura/queryparam.cfc and now LIKE works

Leave a Reply

Leave this field empty:

Powered by Mango Blog. Design and Icons by N.Design Studio