Jeremy Davis
Jeremy Davis
Sitecore, C# and web development
Article printed from: https://blog.jermdavis.dev/posts/2021/peeking-into-search-queries

Peeking into search queries

Published 20 January 2021
Updated 01 February 2021

An issue I've bumped into a number of times over the years, is that sometimes developers want to be able to look at the query that got generated when they did something with Sitecore's ContentSearch APIs. The traditional answer of "go look in the logs" is one way to deal with this, but some recent project work got me wondering if there were alternatives...

What's the issue?

When you fire up some code for a query, you're generally using something vaguely along the lines of:

ISearchIndex index = ContentSearchManager.GetIndex("your_index_name");
using (var context = index.CreateSearchContext())
{
    var query = context.GetQueryable<SearchResultItem>();
    
    // add some parameters to your filter your query
    // add some sort order
    // add some pagination

    var results = query.GetResults();

    // do somethig with the results
}

					

But what happens if you need to debug this query? A call to GetQueryable<T>() gets you back an IQueryable<T>. And based on that type there's no obvious "here's your query" property. And that makes sense, when you think about it. The IQueryable<T> interface is a generic representation of a query - it doesn't know anything specific about Solr, Azure Search or any other Sitecore provider. But that raw quey must get generated at some point in order to run the search – and we know it turns up in the logs. You can look at any of the Search.log.*.*.txt files that Sitecore writes and see things like:

3044 15:25:54 INFO  Warming and Caching your search indexes
3044 15:25:54 INFO  /******* Warming Queries ********/
3044 15:25:54 INFO  /*************************/
2780 15:36:15 INFO  Solr Query - ?q=(_name:(item*) OR (_content:(*item*) OR _name:(item) OR (_content:(item) AND _language:(en)))) AND _val_:__boost&start=0&rows=1000000&fq=_indexname:(sitecore_master_index)&wt=xml

					

That's got your debugging needs covered in most scenarios – but it doesn't help when your code wants to know what the query looks like at runtime...

Why might you need that? Well one example might be something like this: My colleagues are working on some fancy search UI which involves grouping. If you look up how to do grouping with Solr and ContentSearch in the Sitecore docs you get this example:

Solr Query

The grouping code example provided wants the query as text to perform grouping on – not as an IQueryable. And that's how I got wrapped up in this: is it possible to take a query object and extract its query text?

Working towards an answer...

When you're looking at your code in Visual Studio, the query you generate by calling GetQueryable<T>() is of type IQuerable<T>. But because that's an interface, it can't be the actual type that's used at runtime. What happens if you look at the query while the code is running?

Data Type

Well at runtime has we have GenericQueryable<T, SolrCompositeQuery> as it's concrete type. That looks like it might be more helpful. What does that give us?

public class GenericQueryable<TElement, TQuery> : IOrderedQueryable<TElement>, IQueryable<TElement>, IEnumerable<TElement>, IEnumerable, IQueryable, IOrderedQueryable, IQueryProvider, IHasNativeQuery<TQuery>, IHasNativeQuery, IHasTraceWriter, IContentSearchQueryable
{
    public GenericQueryable(Index<TElement, TQuery> index, QueryMapper<TQuery> queryMapper, IQueryOptimizer queryOptimizer, FieldNameTranslator fieldNameTranslator, IExpressionParser parser);
    protected GenericQueryable(Index<TQuery> index, QueryMapper<TQuery> queryMapper, IQueryOptimizer queryOptimizer, Expression expression, Type itemType, FieldNameTranslator fieldNameTranslator, IExpressionParser parser);
    public FieldNameTranslator FieldNameTranslator { get; set; }
    public IQueryOptimizer QueryOptimizer { get; set; }
    public virtual IQueryProvider Provider { get; }
    public virtual Expression Expression { get; }
    public virtual Type ElementType { get; }
    protected Type ItemType { get; set; }
    protected Index<TQuery> Index { get; set; }
    protected IExpressionParser Parser { get; set; }
    protected QueryMapper<TQuery> QueryMapper { get; set; }
    public virtual IQueryable<TElement> CreateQuery<TElement>(Expression expression);
    public virtual TResult Execute<TResult>(Expression expression);
    public virtual IEnumerator<TElement> GetEnumerator();
    public TQuery GetQuery();
    protected virtual TQuery GetQuery(Expression expression);
    protected virtual void Trace(IDumpable dumpable, string title);
    protected virtual void Trace(Expression expression, string title);
}

					

There's a lot going on there... But one thing stands out: the GetQuery() method. It's returning an object of the SolrCompositeQuery type, and poking about inside that shows it has a very interesting looking ToString() method:

public override string ToString()
{
	DefaultQuerySerializerEx defaultQuerySerializerEx = new DefaultQuerySerializerEx((ISolrFieldSerializer)(object)new DefaultFieldSerializer());
	return defaultQuerySerializerEx.Serialize(Query);
}

					

A query serialiser seems like exactly what would help...

So what happens if we pull that together into a code snippet and give it a try? Well you need to add a reference to Sitecore.ContentSearch.Linq.Solr.dll in order to do this, but it works out fairly simply:

ISearchIndex index = ContentSearchManager.GetIndex("your_index_name");
using (var context = index.CreateSearchContext())
{
    var q = context.GetQueryable<SearchResultItem>();

    // Set up your query with something appropriate to your work..
    q = q.Where(r => r.Name == "test");
    // add whatever else is necessary for ordering etc.

    var genericQuery = q as GenericQueryable<SearchResultItem, SolrCompositeQuery>;
    var solrCompositeQuery = genericQuery.GetQuery();
    string queryText = solrCompositeQuery.ToString();

    return queryText;
}

					

And when you run that you do get back something that looks like a query. For example, the test above returns _name:(test) AND _val_:__boost. And pasting that into Solr's UI gives results:

Query Test

That looks like success...

I've not tested this with Azure search, but based on a bit of poking around with ILSpy it looks like Sitecore.ContentSearch.Azure.Query.CloudQuery is the equivalent of SolrCompositeQuery if you're not using Solr. That type returns a string that looks like it should be the query text.

↑ Back to top