Thursday, April 29, 2010

SP 2010 - Phonetic Search Only for People Scope?

I have a custom scope I've created that I am querying against with the FullTextSqlQuery object. Recently I was asked if we could allow a phonetic search. With all the hype around this new feature in SharePoint 2010, I thought it would be very easy.

I looked, and sure enough, there is an EnablePhonetic property right on FullTextSqlQuery. I set it to true, ran my search and got back...nothing. I figured maybe it didn't like my query, which had a LIKES keyword. That wasn't it. I tried looking on the web and it seems all the mentions of phonetic search seem to be closer to press releases than coding snippets.

Finally I found this nugget on MSDN:
For FAST Search Server 2010 for SharePoint, this property is only applicable for People Search.
Well, I'm not using FAST, but I am using a custom search scope. It would appear that might be the limiting factor here. While I am searching for people, they are stored in a custom list, since they are not members of my SharePoint site. Therefore I was using a custom scope to get to them.

It would seem phonetic search is not available yet to customize in this way.

Wednesday, April 28, 2010

SP 2010 - Configure and Use a TaxonomyWebTaggingControl

If you are using the Managed Metadata Service and developing a custom web part, you may be interested in using that nice term picker that you see when you create a new list item that contains a Managed Metadata Column.

First, let's cover the bare minimum code you'll need to get the term picker to show up in your web part. First you'll want to add a reference to Microsoft.SharePoint.Taxonomy to your project. Then you'll need a Register directive in your ascx, like this:

<%@ Register Tagprefix="Taxonomy" Namespace="Microsoft.SharePoint.Taxonomy" Assembly="Microsoft.SharePoint.Taxonomy, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> 

Now you can add the picker to your web part just like any other control.



You'll notice that I didn't set any properties on the TaxonomyWebTaggingControl. Well there are three properties you'll need to set, but we'll do it programatically. Those properties are:
  • SSPList - This property parses your string and builds a List to set the SspId property
  • GroupId - A Guid
  • TermSetList - This property parses your string and builds a List to set the TermSetId property

Now, you can set these properties declaratively if you wish, but it won't be a very portable web part, as the Guids aren't part of the import CSV format for the Managed Metadata Service, nor do any of the Taxonomy Create methods allow setting of Ids. Because of this, it's best to have your web part set these properties on the TaxonomyWebTaggingControl programatically. If you just want to see how you would go about setting these properties declaratively, scroll to the bottom of this post, as I've included it for reference.

In order to make your web part portable, you're going to need to query the Managed Metadata Service and get these values yourself. Moreover, you're going to have to do it on every load of your web part since these properties are not stored anywhere between page loads.

Note: If you only set these properties on page load, this will work as long as there are no scenarios where you need to redraw the control again, such as having a custom server validator. In that instance, your picker would work before the postback, but the look-ahead feature would be broken after the postback.

In order to make loading our properties a little easier, here is an extension method for TaxonomyWebTaggingControl that allows you to quickly set the properties. The path part of this was inspired by PHolpar. Also, remember that extension methods must be defined in static classes.

public static bool Configure(this TaxonomyWebTaggingControl twtc, string termSetPath)
{
 bool configured = false;

 TaxonomySession session = new TaxonomySession(SPContext.Current.Site);

 string[] parts = termSetPath.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
 if (parts.Length > 0)
 {
  TermStore termStore = GetTermStore(session, parts[0]);
  if (termStore != null && parts.Length > 1)
  {
   twtc.SSPList = termStore.Id.ToString();

   Group group = GetGroup(termStore, parts[1]);
   if (group != null && parts.Length > 2)
   {
    twtc.GroupId = group.Id;

    TermSet termSet = GetTermSet(group, parts[2]);
    if (termSet != null)
    {
     twtc.TermSetList = termSet.Id.ToString();
     configured = true;
    }
   }
  }
 }

 return configured;
}

In order to load MyTermSet, which lives within MyGroup, which resides within MyTermStore, you will use this extension method similar to the following:

protected void Page_Load(object sender, EventArgs e)
{
 bool configured = twtcSpecialty.Configure("MyTermStore/MyGroup/MyTermSet");
 if (!configured)
 {
  throw new ApplicationException("Unable to find target TermSet");
 }
}

The result looks just like the picker from other parts of SharePoint, with functioning look-ahead.


As promised, here is the markup if you want to configure your TaxonomyWebTaggingControl declaratively.


    0d8382cf-2d63-4421-a37a-b9386d0be5c8
    7adeced1-b73e-47dc-b695-82bb28e2f48f

Tuesday, April 27, 2010

SP 2010 - Randomize the order of your search results from FullTextSqlQuery

I had a need recently to perform a query with FullTextSqlQuery but to limit the results and then display them in a random order. The FullTextSqlQuery class does support a RowLimit property, though this would not work in my case because I didn't want to get the exact same answers everytime.

Remembering that LINQ to SharePoint often requires Two-Stage Queries, I decided to employ a similar approach here and pull back the data I could then perform more culling server side.

DataTable searchResults = PerformSearch(query, queryRowLimit);
if (searchResults != null && searchResults.Rows.Count > 0)
{
 int limit = 10; //todo: this should be a webpart property
 var results = (from row in searchResults.AsEnumerable()
       orderby Guid.NewGuid()
       select row).Take(limit);
}

The PerformSearch method is just a standard setup for using FullTextSqlQuery to return a DataTable. Note that I limit my resultset as much as I can with the FullTextSqlQuery.QueryText property to try to avoid hitting a throttling exception.

With the limited results returned, I then want to randomize the order of the records. LINQ allows us to do this the same way we would in SQL, and I just order by a random Guid. Once the results are randomized, we just grab the number of records we need with the Take() extension method.

Friday, April 23, 2010

SP 2010 Managed Metadata TermSet.CreateTerm Throws Error

I've been working on importing a set of Terms into my Managed Metadata Term Store from a 3rd party database. However, I ran into a snag. When I execute the following code, I get an error, "There is already a term with the same default label and parent term."

public static void CreateTermIfNotExists(TermSet termSet, string termName)
{
 if (termSet != null && !string.IsNullOrEmpty(termName))
 {
  Term term = null;

  //This throws an exception if the Term doesn't exist
  try
  {
   term = termSet.Terms[termName];
  }
  catch { }

  if (term == null)
  {
   Term t = termSet.CreateTerm(termName, 1033);
   termSet.TermStore.CommitAll();
  }
 }
}

I attached my debugger and found out that even though I was checking for my term, the code would say it wasn't there, even though it was! The problem, was that the specific term causing me problems had an ampersand. The term name I supplied was "Foo & Bar", but the value put into the TermStore actually contained unicode version of the ampersand.

Looking through the documentation, I found this relevant comment:

The name value will be normailized to trim consecutive spaces into one and replace the & character with the wide character version of the character (\uFF06). The leading and trailing spaces will be trimmed. It must be non-empty and cannot exceed 255 characters, and cannot contain any of the following characters ; "<>|&tab.

That was indeed the behavior I was seeing. Thinking I had just found a limitation of the TermSet.Terms collection, I changed my code to this:

public static void CreateTermIfNotExists(TermSet termSet, string termName)
{
 if (termSet != null && !string.IsNullOrEmpty(termName))
 {
  Term term = null;

  TermCollection tc = termSet.GetTerms(termName, 1033, true, StringMatchOption.ExactMatch, 1, false);
  if (tc != null && tc.Count > 0)
  {
   term = tc[0];
  }

  if (term == null)
  {
   Term t = termSet.CreateTerm(termName, 1033);
   termSet.TermStore.CommitAll();
  }
 }
}

I ran again, and this time instead of blowing up on just that one case, my code went crazy trying to insert a bunch of different terms that were working fine before and throwing way more exceptions. A little research on this method led me to this post, which suggests the TermSet.GetTerms method does not work in Beta 2, which seems to be what I just discovered as well.

I decided to explore the reference to normalizing term names from the MSDN link. My final pass at the code became:

public static void CreateTermIfNotExists(TermSet termSet, string termName)
{
    if (termSet != null && !string.IsNullOrEmpty(termName))
    {
        Term term = null;

        try
        { 
            string normalizedTermName = TermSet.NormalizeName(termName);
            term = termSet.Terms[normalizedTermName];
        }
        catch { }

        if (term == null)
        {
            Term t = termSet.CreateTerm(termName, 1033);
            termSet.TermStore.CommitAll();
        }
    }
} 

Finally, success! Moral of the story - when you query your TermSet for a specific Term you need to normalize your name first, because that is how it will be stored internally.

Monday, April 19, 2010

SP 2010 - Using LINQ to SharePoint to Find List Items with Specific Managed Metadata Terms

Now that I can effectively use LINQ to access my Managed Metadata Columns, I'd like to only pull back those columns that contain values I need. For single valued Managed Metadata Columns, this is very straightforward:

var examples = from d in context.Examples
      where d.Specialty is TaxonomyFieldValue && ((TaxonomyFieldValue)d.Specialty).Label == "Value One"
               select d;

For multi valued Managed Metadata Columns, my first attempt was a bust. I tried the following expression, but received a compiler error, "An expression tree may not contain an anonymous method expression".

var examples = from d in context.Examples
               where ((TaxonomyFieldValueCollection)d.Specialty).Exists(delegate(TaxonomyFieldValue tfv){
             return tfv.Label == "Value One";
               }) != null
      select d;

The hint here from the compiler is that I can do this as long as I don't use an anonymous method. So I created a method to test for the term I wanted and changed my expression to call this method.

private static bool ContainsMetadataTerm(object o, string termLabel)
{
 bool exists = false;

 if (o is TaxonomyFieldValueCollection)
 {
  TaxonomyFieldValueCollection tfvc = (TaxonomyFieldValueCollection)o;
  exists = tfvc.Exists(delegate(TaxonomyFieldValue tfv)
  {
   return tfv.Label == termLabel;
  });
 }

 return exists;
}

var examples = from d in context.Examples
      where d.Specialty is TaxonomyFieldValueCollection && ContainsMetadataTerm(d.Specialty, "Value One")
      select d;

Now I can query specifically for list items that use specific terms. Keep in mind that this filtering does not happen until after the list items have been pulled down, so you may want to do some additional filtering to narrow the results so you don't run afoul of the new Throttling feature of SharePoint 2010.

Here was the CAML generated by the last LINQ query, which shows that the additional filtering for Term was done after the records were pulled.


  
    
      
        
        0x0100
      
    
  
  
    
    
    
    
    
    
    
    
    2147483647

SP 2010 - Managed Metadata Columns ARE Supported in LINQ to SharePoint

I apparently spoke too soon! You can get to Managed Metadata Columns in LINQ to SharePoint with SPMetal, just not directly off the command line. You need to supply a parameters option.

First you'll want to create your parameters file. I've found two different ways to get the Managed Metadata Column to show up. The first attempt I used this XML in my parameter file.


  
    
      
    
  



I saved this file to metaloptions.xml and then ran the following command.

SPMetal.exe /web:http://mysite /code:SPMySite.cs /namespace:SPMySite /parameters:metaloptions.xml

The output of that command will include a warning, but it seems to have no impact on LINQ working. I think it's purely informational and not relevant to what we are working with.

Warning: All content types for list Form Templates were excluded.

Now when I query with LINQ I see the hidden fields that power my Managed Metadata Column, which look familiar.

On my typed list item object, I now had three new fields:
  • Specialty_0 - String
  • TaxonomyCatchAllColumnCatchAllData - IList
  • TaxonomyCatchAllColumnId - IList

Those fields hold values that look like this, respectively. Unfortunately, this isn't terribly useful.
  • Value One|e203149a-6852-46fb-9d8e-9c21d350068d
  • z4KDDWMtIUSjerk4bQvlyA==|0c7eej633Ee2lYK7KOL0jw==|mhQD4lJo+0adjpwh01AGjQ==
  • 4

I tried another pass on my parameters file. This time I used the following XML.



  
    
      
    
  


On my typed list item object, I now have the one extra Column that I specified in my parameters file. LINQ will create a property for this field that is just an object. However, if you access this property, you can cast it to a TaxonomyFieldValueCollection or TaxonomyFieldValue, as appropriate. This provides exactly the information we wanted:

using (SPMySiteDataContext context = new SPMySiteDataContext("http://mysite"))
{
 var examples = from d in context.examples
      select d;

 foreach (var example in examples)
 {
  if (example.Specialty is TaxonomyFieldValueCollection)
  {
   foreach (TaxonomyFieldValue tfv in (TaxonomyFieldValueCollection)example.Specialty)
   {
    Console.WriteLine(tfv.Label);
   }
  }
  else if (example.Specialty is TaxonomyFieldValue)
  {
   TaxonomyFieldValue tfv = (TaxonomyFieldValue)example.Specialty;
   Console.WriteLine(tfv.Label);
  }
 }
}

SP 2010 - Managed Metadata Columns Not Supported in LINQ to SharePoint?

Today I am playing with LINQ to SharePoint. I created a simple list and added a Managed Metadata Column to my list. I then used SPMetal to generate a DataContext class. My Managed Metadata Column is nowhere to be found. I looked through options for SPMetal to see if maybe it just needed a switch to capture those Managed Metadata Columns, but I don't see one.

So it appears that Managed Metadata Columns have no support in LINQ to SharePoint. They are also not eligible to be Projected Fields. It would seem Microsoft didn't really flesh out all the ways the new Managed Metadata Service might be used.

Update: I figured out how to do this.