Skip to content
This repository has been archived by the owner on Sep 13, 2023. It is now read-only.

Commit

Permalink
Add CaptureStatistics extension method to allow clients to be called …
Browse files Browse the repository at this point in the history
…back with statistics whenever a query is executed.
  • Loading branch information
chriseldredge committed Jun 17, 2014
1 parent 97749c4 commit 389b965
Show file tree
Hide file tree
Showing 16 changed files with 348 additions and 27 deletions.
2 changes: 1 addition & 1 deletion IntegratedBuild.proj
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VersionPrefix>3.2.1</VersionPrefix>
<VersionPrefix>3.3.0</VersionPrefix>
<VersionSuffix></VersionSuffix>
<SolutionDir>$(MSBuildProjectDirectory)/source/</SolutionDir>
<TestsEnabled>true</TestsEnabled>
Expand Down
2 changes: 1 addition & 1 deletion LICENSE.txt
@@ -1,4 +1,4 @@
Copyright 2012 The Motley Fool, LLC
Copyright 2012-2014 The Motley Fool, LLC

This library is licensed both under the GNU Lesser General Public License (LGPL), version 2.1
and the Apache License, version 2.0. You may not use this software except in compliance with those Licenses.
Expand Down
49 changes: 49 additions & 0 deletions source/Lucene.Net.Linq.Tests/Integration/StatisticTests.cs
@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;

namespace Lucene.Net.Linq.Tests.Integration
{
[TestFixture]
public class StatisticTests : IntegrationTestBase
{
private IQueryable<SampleDocument> documents;

[SetUp]
public void AddDocuments()
{
AddDocument(new SampleDocument { Name = "c", Scalar = 3, Flag = true, Version = new Version(100, 0, 0) });
AddDocument(new SampleDocument { Name = "a", Scalar = 1, Version = new Version(20, 0, 0) });
AddDocument(new SampleDocument { Name = "b", Scalar = 2, Flag = true, Version = new Version(3, 0, 0) });

documents = provider.AsQueryable<SampleDocument>();
}

[Test]
public void CountsTotalHits()
{
LuceneQueryStatistics stats = null;

documents.CaptureStatistics(s => { stats = s; }).Skip(1).Take(1).Count();

Assert.That(stats, Is.Not.Null, "stats");
Assert.That(stats.TotalHits, Is.EqualTo(documents.Count()));
}

[Test]
public void InvokesOncePerExecution()
{
var list = new List<LuceneQueryStatistics>();

documents = documents.CaptureStatistics(list.Add);

documents.Where(doc => doc.Scalar == 1).ToList();
documents.Where(doc => doc.Scalar != 1).ToList();

Assert.That(list.Count, Is.EqualTo(2));
Assert.That(list[0].TotalHits, Is.EqualTo(1));
Assert.That(list[1].TotalHits, Is.EqualTo(2));
}
}
}
1 change: 1 addition & 0 deletions source/Lucene.Net.Linq.Tests/Lucene.Net.Linq.Tests.csproj
Expand Up @@ -118,6 +118,7 @@
<Compile Include="Integration\OrderByTests.cs" />
<Compile Include="Integration\PorterStemAnalyzer.cs" />
<Compile Include="Integration\RangeTests.cs" />
<Compile Include="Integration\StatisticTests.cs" />
<Compile Include="LuceneQueryExecutorTests.cs" />
<Compile Include="Mapping\FieldMappingInfoBuilderSortFieldTests.cs" />
<Compile Include="ReadOnlyLuceneDataProviderTests.cs" />
Expand Down
12 changes: 4 additions & 8 deletions source/Lucene.Net.Linq.sln
@@ -1,26 +1,22 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2012
# Visual Studio 2013
VisualStudioVersion = 12.0.30324.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lucene.Net.Linq", "Lucene.Net.Linq\Lucene.Net.Linq.csproj", "{77AD18CC-93A3-4BC9-9F31-2C16D873F088}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lucene.Net.Linq.Tests", "Lucene.Net.Linq.Tests\Lucene.Net.Linq.Tests.csproj", "{40D157FA-178B-4906-9179-E5D623F5BA03}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Files", "Files", "{91DBBF52-CD3F-4737-91DC-CDB7564C32B9}"
ProjectSection(SolutionItems) = preProject
..\default.ps1 = ..\default.ps1
..\IntegratedBuild.proj = ..\IntegratedBuild.proj
..\LICENSE.txt = ..\LICENSE.txt
..\psake.ps1 = ..\psake.ps1
..\psake.psm1 = ..\psake.psm1
..\psake_ext.ps1 = ..\psake_ext.ps1
..\README.markdown = ..\README.markdown
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{95CA6EE1-3FC1-466E-821F-E48BE7DCCA6E}"
ProjectSection(SolutionItems) = preProject
.nuget\NuGet.Config = .nuget\NuGet.Config
.nuget\NuGet.exe = .nuget\NuGet.exe
.nuget\NuGet.targets = .nuget\NuGet.targets
.nuget\packages.config = .nuget\packages.config
EndProjectSection
EndProject
Global
Expand Down
@@ -0,0 +1,35 @@
using System;
using System.Linq.Expressions;
using System.Reflection;
using Remotion.Linq;
using Remotion.Linq.Parsing.Structure.IntermediateModel;

namespace Lucene.Net.Linq.Clauses.ExpressionNodes
{
internal class QueryStatisticsCallbackExpressionNode : MethodCallExpressionNodeBase
{
public static readonly MethodInfo[] SupportedMethods =
{
GetSupportedMethod (() => LuceneMethods.CaptureStatistics<object>(null, null))
};

private readonly ConstantExpression callback;

public QueryStatisticsCallbackExpressionNode(MethodCallExpressionParseInfo parseInfo, ConstantExpression callback)
: base(parseInfo)
{
this.callback = callback;
}

public override Expression Resolve(ParameterExpression inputParameter, Expression expressionToBeResolved, ClauseGenerationContext clauseGenerationContext)
{
return Source.Resolve(inputParameter, expressionToBeResolved, clauseGenerationContext);
}

protected override QueryModel ApplyNodeSpecificSemantics(QueryModel queryModel, ClauseGenerationContext clauseGenerationContext)
{
queryModel.BodyClauses.Add(new QueryStatisticsCallbackClause(callback));
return queryModel;
}
}
}
Expand Up @@ -7,10 +7,10 @@ namespace Lucene.Net.Linq.Clauses.ExpressionNodes
{
internal class TrackRetrievedDocumentsExpressionNode : MethodCallExpressionNodeBase
{
public static readonly MethodInfo[] SupportedMethods = new[]
{
GetSupportedMethod (() => LuceneMethods.TrackRetrievedDocuments<object>(null, null))
};
public static readonly MethodInfo[] SupportedMethods =
{
GetSupportedMethod (() => LuceneMethods.TrackRetrievedDocuments<object>(null, null))
};

private readonly ConstantExpression tracker;

Expand Down
30 changes: 30 additions & 0 deletions source/Lucene.Net.Linq/Clauses/QueryStatisticsCallbackClause.cs
@@ -0,0 +1,30 @@
using System;
using System.Linq.Expressions;
using Remotion.Linq;
using Remotion.Linq.Clauses;

namespace Lucene.Net.Linq.Clauses
{
internal class QueryStatisticsCallbackClause : ExtensionClause<ConstantExpression>
{
public QueryStatisticsCallbackClause(ConstantExpression expression)
: base(expression)
{
}

public Action<LuceneQueryStatistics> Callback
{
get { return (Action<LuceneQueryStatistics>) expression.Value; }
}

protected override void Accept(ILuceneQueryModelVisitor visitor, QueryModel queryModel, int index)
{
visitor.VisitQueryStatisticsCallbackClause(this, queryModel, index);
}

public override IBodyClause Clone(CloneContext cloneContext)
{
return new QueryStatisticsCallbackClause(expression);
}
}
}
5 changes: 3 additions & 2 deletions source/Lucene.Net.Linq/ILuceneQueryModelVisitor.cs
Expand Up @@ -5,7 +5,8 @@ namespace Lucene.Net.Linq
{
internal interface ILuceneQueryModelVisitor : IQueryModelVisitor
{
void VisitBoostClause(BoostClause boostClause, QueryModel queryModel, int index);
void VisitTrackRetrievedDocumentsClause(TrackRetrievedDocumentsClause trackRetrievedDocumentsClause, QueryModel queryModel, int index);
void VisitBoostClause(BoostClause clause, QueryModel queryModel, int index);
void VisitTrackRetrievedDocumentsClause(TrackRetrievedDocumentsClause clause, QueryModel queryModel, int index);
void VisitQueryStatisticsCallbackClause(QueryStatisticsCallbackClause clause, QueryModel queryModel, int index);
}
}
3 changes: 3 additions & 0 deletions source/Lucene.Net.Linq/Lucene.Net.Linq.csproj
Expand Up @@ -62,6 +62,7 @@
<Compile Include="AnalyzerExtensions.cs" />
<Compile Include="Clauses\ExpressionNodes\BoostExpressionNode.cs" />
<Compile Include="Clauses\BoostClause.cs" />
<Compile Include="Clauses\ExpressionNodes\QueryStatisticsCallbackExpressionNode.cs" />
<Compile Include="Clauses\ExpressionNodes\TrackRetrievedDocumentsExpressionNode.cs" />
<Compile Include="Clauses\Expressions\AllowSpecialCharactersExpression.cs" />
<Compile Include="Clauses\Expressions\BoostBinaryExpression.cs" />
Expand All @@ -74,6 +75,7 @@
<Compile Include="Clauses\Expressions\LuceneQueryPredicateExpression.cs" />
<Compile Include="Clauses\Expressions\LuceneRangeQueryExpression.cs" />
<Compile Include="Clauses\ExtensionClause.cs" />
<Compile Include="Clauses\QueryStatisticsCallbackClause.cs" />
<Compile Include="Clauses\TrackRetrievedDocumentsClause.cs" />
<Compile Include="Clauses\TreeVisitors\LuceneExpressionTreeVisitor.cs" />
<Compile Include="Fluent\ClassMap.cs" />
Expand All @@ -85,6 +87,7 @@
<Compile Include="IQueryExecutionContext.cs" />
<Compile Include="IRetrievedDocumentTracker.cs" />
<Compile Include="LuceneDataProviderSettings.cs" />
<Compile Include="LuceneQueryStatistics.cs" />
<Compile Include="Mapping\DocumentKey.cs" />
<Compile Include="Mapping\DocumentKeyFieldMapper.cs" />
<Compile Include="Mapping\DocumentMapperBase.cs" />
Expand Down
2 changes: 1 addition & 1 deletion source/Lucene.Net.Linq/Lucene.Net.Linq.nuspec
Expand Up @@ -6,7 +6,7 @@
<version>$version$</version>
<owners>The Motley Fool, LLC</owners>
<authors>Chris Eldredge</authors>
<copyright>Copyright 2012-2013 The Motley Fool, LLC</copyright>
<copyright>Copyright 2012-2014 The Motley Fool, LLC</copyright>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<projectUrl>https://github.com/themotleyfool/Lucene.Net.Linq</projectUrl>
<licenseUrl>https://github.com/themotleyfool/Lucene.Net.Linq/blob/master/LICENSE.txt</licenseUrl>
Expand Down
11 changes: 11 additions & 0 deletions source/Lucene.Net.Linq/LuceneMethods.cs
Expand Up @@ -64,6 +64,17 @@ public static bool Fuzzy(this bool predicate, float similarity)
throw new InvalidOperationException(UnreachableCode);
}

/// <summary>
/// Registers a callback to be invoked when the query is executed to provide access to
/// metadata including total hits and execution time.
/// </summary>
public static IQueryable<T> CaptureStatistics<T>(this IQueryable<T> source, Action<LuceneQueryStatistics> callback)
{
return source.Provider.CreateQuery<T>(
Expression.Call(((MethodInfo)MethodBase.GetCurrentMethod()).MakeGenericMethod(typeof(T)),
source.Expression, Expression.Constant(callback)));
}

internal static IQueryable<T> TrackRetrievedDocuments<T>(this IQueryable<T> source, IRetrievedDocumentTracker<T> tracker)
{
return source.Provider.CreateQuery<T>(
Expand Down
57 changes: 52 additions & 5 deletions source/Lucene.Net.Linq/LuceneQueryExecutor.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using Common.Logging;
Expand Down Expand Up @@ -101,6 +102,9 @@ protected LuceneQueryExecutorBase(Context context)

public T ExecuteScalar<T>(QueryModel queryModel)
{
var watch = new Stopwatch();
watch.Start();

var luceneQueryModel = PrepareQuery(queryModel);

var searcherHandle = CheckoutSearcher();
Expand All @@ -111,23 +115,40 @@ public T ExecuteScalar<T>(QueryModel queryModel)
var skipResults = luceneQueryModel.SkipResults;
var maxResults = Math.Min(luceneQueryModel.MaxResults, searcher.MaxDoc - skipResults);

var executionContext = new QueryExecutionContext(searcher, luceneQueryModel.Query, luceneQueryModel.Filter);
TopFieldDocs hits;

TimeSpan elapsedPreparationTime;
TimeSpan elapsedSearchTime;

if (maxResults > 0)
{
var executionContext = new QueryExecutionContext(searcher, luceneQueryModel.Query, luceneQueryModel.Filter);
PrepareSearchSettings(executionContext);

elapsedPreparationTime = watch.Elapsed;

hits = searcher.Search(executionContext.Query, executionContext.Filter, maxResults, luceneQueryModel.Sort);

elapsedSearchTime = watch.Elapsed - elapsedPreparationTime;
}
else
{
hits = new TopFieldDocs(0, new ScoreDoc[0], new SortField[0], 0);
elapsedPreparationTime = watch.Elapsed;
elapsedSearchTime = TimeSpan.Zero;
}

executionContext.Phase = QueryExecutionPhase.ConvertResults;
executionContext.Hits = hits;

var handler = ScalarResultHandlerRegistry.Instance.GetItem(luceneQueryModel.ResultSetOperator.GetType());

return handler.Execute<T>(luceneQueryModel, hits);
var result = handler.Execute<T>(luceneQueryModel, hits);

var elapsedRetrievalTime = watch.Elapsed - elapsedPreparationTime - elapsedSearchTime;
RaiseStatisticsCallback(luceneQueryModel, executionContext, elapsedPreparationTime, elapsedSearchTime, elapsedRetrievalTime, 0, 0);

return result;
}
}

Expand All @@ -145,6 +166,9 @@ public class ItemHolder

public IEnumerable<T> ExecuteCollection<T>(QueryModel queryModel)
{
var watch = new Stopwatch();
watch.Start();

var itemHolder = new ItemHolder();

var currentItemExpression = Expression.Property(Expression.Constant(itemHolder), "Current");
Expand Down Expand Up @@ -177,7 +201,12 @@ public IEnumerable<T> ExecuteCollection<T>(QueryModel queryModel)

PrepareSearchSettings(executionContext);

var elapsedPreparationTime = watch.Elapsed;
var hits = searcher.Search(executionContext.Query, executionContext.Filter, maxResults + skipResults, luceneQueryModel.Sort);
var elapsedSearchTime = watch.Elapsed - elapsedPreparationTime;

executionContext.Phase = QueryExecutionPhase.ConvertResults;
executionContext.Hits = hits;

if (luceneQueryModel.Last)
{
Expand All @@ -186,14 +215,32 @@ public IEnumerable<T> ExecuteCollection<T>(QueryModel queryModel)
}

var tracker = luceneQueryModel.DocumentTracker as IRetrievedDocumentTracker<TDocument>;
var retrievedDocuments = 0;

executionContext.Phase = QueryExecutionPhase.ConvertResults;
executionContext.Hits = hits;
foreach (var p in EnumerateHits(hits, executionContext, searcher, tracker, itemHolder, skipResults, projector))
{
yield return p;
retrievedDocuments++;
}

foreach (var p in EnumerateHits(hits, executionContext, searcher, tracker, itemHolder, skipResults, projector)) yield return p;
var elapsedRetrievalTime = watch.Elapsed - elapsedSearchTime - elapsedPreparationTime;
RaiseStatisticsCallback(luceneQueryModel, executionContext, elapsedPreparationTime, elapsedSearchTime, elapsedRetrievalTime, skipResults, retrievedDocuments);
}
}

private void RaiseStatisticsCallback(LuceneQueryModel luceneQueryModel, QueryExecutionContext executionContext, TimeSpan elapsedPreparationTime, TimeSpan elapsedSearchTime, TimeSpan elapsedRetrievalTime, int skipResults, int retrievedDocuments)
{
var statistics = new LuceneQueryStatistics(executionContext.Query,
executionContext.Filter,
luceneQueryModel.Sort,
elapsedPreparationTime,
elapsedSearchTime,
elapsedRetrievalTime,
executionContext.Hits.TotalHits,
skipResults, retrievedDocuments);
luceneQueryModel.RaiseCaptureQueryStatistics(statistics);
}

private IEnumerable<T> EnumerateHits<T>(TopDocs hits, QueryExecutionContext executionContext, Searchable searcher, IRetrievedDocumentTracker<TDocument> tracker, ItemHolder itemHolder, int skipResults, Func<TDocument, T> projector)
{
for (var i = skipResults; i < hits.ScoreDocs.Length; i++)
Expand Down
15 changes: 15 additions & 0 deletions source/Lucene.Net.Linq/LuceneQueryModel.cs
Expand Up @@ -46,6 +46,8 @@ public Sort Sort

public object DocumentTracker { get; set; }

public event Action<LuceneQueryStatistics> OnCaptureQueryStatistics;

public void AddQuery(Query additionalQuery)
{
if (query == null)
Expand Down Expand Up @@ -154,6 +156,19 @@ public void AddBoostFunction(LambdaExpression expression)
customScoreFunction = scoreFunction;
}

public void AddQueryStatisticsCallback(Action<LuceneQueryStatistics> callback)
{
OnCaptureQueryStatistics += callback;
}

public void RaiseCaptureQueryStatistics(LuceneQueryStatistics statistics)
{
if (OnCaptureQueryStatistics != null)
{
OnCaptureQueryStatistics(statistics);
}
}

public Func<TDocument, float> GetCustomScoreFunction<TDocument>()
{
if (customScoreFunction == null) return null;
Expand Down

0 comments on commit 389b965

Please sign in to comment.