abstracted lucene/search stuff into Portoa

This commit is contained in:
tmont 2011-02-26 01:08:38 +00:00
parent 866964a764
commit d2894435a1
20 changed files with 15 additions and 311 deletions

View File

@ -1,19 +0,0 @@
using System.Collections.Generic;
namespace VideoGameQuotes.Api.Search {
/// <summary>
/// Exposes an interface to search for quotes
/// </summary>
public interface IQuoteSearcher {
/// <summary>
/// Searches for quote based on the given search query
/// </summary>
IEnumerable<SearchResult> Search(string query);
/// <summary>
/// (Re)builds the search index
/// </summary>
/// <param name="replaceOldIndex">Set to false to not overwrite the old index with a brand new one.</param>
void BuildIndex(bool replaceOldIndex = true);
}
}

View File

@ -1,17 +0,0 @@
namespace VideoGameQuotes.Api.Search {
/// <summary>
/// Exposes an interface to build and update a search index
/// </summary>
public interface ISearchIndexBuilder<T> {
/// <summary>
/// (Re)builds the search index
/// </summary>
void BuildIndex();
/// <summary>
/// Updates the index for the specified <paramref name="indexableObject"/>
/// </summary>
/// <param name="indexableObject">The object that needs its index updated</param>
void UpdateIndex(T indexableObject);
}
}

View File

@ -1,15 +0,0 @@
namespace VideoGameQuotes.Api.Search {
public interface ISearchIndexLocator {
string IndexDirectory { get; }
}
public class SearchIndexLocator : ISearchIndexLocator {
private readonly string indexDirectory;
public SearchIndexLocator(string indexDirectory) {
this.indexDirectory = indexDirectory;
}
public string IndexDirectory { get { return indexDirectory; } }
}
}

View File

@ -1,15 +0,0 @@
using System.Collections.Generic;
namespace VideoGameQuotes.Api.Search {
/// <summary>
/// Exposes an interface to perform a full-text search
/// </summary>
public interface ISearcher<T> {
/// <summary>
/// Searches for records based on the given search query
/// </summary>
/// <param name="query">The search term(s) to search for</param>
/// <param name="maxResults">The maximum number of results to return (<c>0</c> is unlimited); the default is <c>10</c></param>
IEnumerable<SearchResult<T>> Search(string query, int maxResults = 10);
}
}

View File

@ -1,9 +0,0 @@
using Lucene.Net.Documents;
using Lucene.Net.Index;
namespace VideoGameQuotes.Api.Search.Lucene {
public interface ILuceneDocumentHandler<T> {
Document BuildDocument(T source);
Term GetIdTerm(T source);
}
}

View File

@ -1,45 +0,0 @@
using Lucene.Net.Index;
using Portoa.Logging;
using Portoa.Persistence;
using VideoGameQuotes.Api.Persistence;
namespace VideoGameQuotes.Api.Search.Lucene {
public class LuceneEntityIndexBuilder<T> : ISearchIndexBuilder<T> where T : Entity<T, int> {
private readonly ILogger logger;
private readonly IndexWriter indexWriter;
private readonly ISearchService<T> searchService;
private readonly ILuceneDocumentHandler<T> documentHandler;
public LuceneEntityIndexBuilder(IndexWriter indexWriter, ISearchService<T> searchService, ILuceneDocumentHandler<T> documentHandler, ILogger logger) {
this.indexWriter = indexWriter;
this.searchService = searchService;
this.documentHandler = documentHandler;
this.logger = logger;
}
public void BuildIndex() {
logger.Info("Building lucene index");
foreach (var quote in searchService.GetAllIndexableRecords()) {
indexWriter.AddDocument(documentHandler.BuildDocument(quote));
}
indexWriter.Optimize();
indexWriter.Commit();
logger.Info("Finished building lucene index");
}
public void UpdateIndex(T entity) {
if (entity.IsTransient()) {
throw new SearchIndexException(string.Format("Cannot add a transient entity to the index ({0})", entity));
}
logger.Info(string.Format("Updating index for {0}", entity));
//delete current document, if it exists
indexWriter.DeleteDocuments(documentHandler.GetIdTerm(entity));
indexWriter.AddDocument(documentHandler.BuildDocument(entity));
indexWriter.Commit();
logger.Info(string.Format("Finished updating index for {0}", entity));
}
}
}

View File

@ -1,50 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Lucene.Net.QueryParsers;
using Lucene.Net.Search;
using Portoa.Persistence;
using VideoGameQuotes.Api.Persistence;
using Directory = Lucene.Net.Store.Directory;
namespace VideoGameQuotes.Api.Search.Lucene {
/// <summary>
/// <see cref="ISearcher{T}"/> implementation for entities based on <c>Lucene.NET</c>
/// </summary>
public class LuceneEntitySearcher<T> : ISearcher<T> where T : Entity<T, int> {
private readonly QueryParser queryParser;
private readonly Directory indexDirectory;
private readonly ISearchService<T> searchService;
public LuceneEntitySearcher(QueryParser queryParser, Directory indexDirectory, ISearchService<T> searchService) {
this.queryParser = queryParser;
this.indexDirectory = indexDirectory;
this.searchService = searchService;
}
public IEnumerable<SearchResult<T>> Search(string searchString, int maxResults = 10) {
if (string.IsNullOrWhiteSpace(searchString)) {
return Enumerable.Empty<SearchResult<T>>();
}
if (maxResults < 0) {
throw new ArgumentOutOfRangeException("maxResults", maxResults, "Maximum number of results must be greater than or equal to zero");
}
if (maxResults == 0) {
maxResults = int.MaxValue;
}
var query = queryParser.Parse(QueryParser.Escape(searchString));
var searcher = new IndexSearcher(indexDirectory, true);
try {
var docs = searcher
.Search(query, maxResults)
.scoreDocs;
var quotes = searchService.FindByIds(docs.Select(doc => int.Parse(searcher.Doc(doc.doc).GetField("id").StringValue())));
return quotes.Zip(docs, (entity, doc) => new SearchResult<T> { Entity = entity, Score = doc.score }).ToArray();
} finally {
searcher.Close();
}
}
}
}

View File

@ -1,13 +0,0 @@
using System.Collections.Generic;
using Lucene.Net.Search;
namespace VideoGameQuotes.Api.Search.Lucene {
public static class LuceneExtensions {
public static IEnumerable<Hit> ToEnumerable(this Hits hits) {
var iterator = hits.Iterator();
while (iterator.MoveNext()) {
yield return (Hit)iterator.Current;
}
}
}
}

View File

@ -1,62 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Lucene.Net.Analysis;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.QueryParsers;
using Lucene.Net.Search;
using Portoa.Persistence;
namespace VideoGameQuotes.Api.Search.Lucene {
/// <summary>
/// <see cref="IQuoteSearcher"/> implementation based on <c>Lucene.NET</c>
/// </summary>
public class LuceneQuoteSearcher : IQuoteSearcher {
private readonly ISearchIndexLocator indexLocator;
private readonly IRepository<Quote> quoteRepository;
private readonly Analyzer analyzer = new StandardAnalyzer();
private readonly QueryParser queryParser;
public LuceneQuoteSearcher(ISearchIndexLocator indexLocator, IRepository<Quote> quoteRepository) {
this.indexLocator = indexLocator;
this.quoteRepository = quoteRepository;
queryParser = new QueryParser("text", analyzer);
}
[UnitOfWork]
public IEnumerable<SearchResult> Search(string searchString) {
if (string.IsNullOrWhiteSpace(searchString)) {
return Enumerable.Empty<SearchResult>();
}
var query = queryParser.Parse(QueryParser.Escape(searchString));
var searcher = new IndexSearcher(indexLocator.IndexDirectory);
return searcher
.Search(query)
.ToEnumerable()
.Select(hit => new SearchResult {
Score = hit.GetScore(),
Quote = quoteRepository.FindById(int.Parse(hit.GetDocument().GetField("id").StringValue()))
});
}
[UnitOfWork]
public void BuildIndex(bool replaceOldIndex = true) {
var indexWriter = new IndexWriter(indexLocator.IndexDirectory, analyzer, replaceOldIndex);
foreach (var quote in quoteRepository.Records) {
indexWriter.AddDocument(CreateDocument(quote));
}
indexWriter.Optimize();
indexWriter.Close();
}
private static Document CreateDocument(Quote quote) {
var document = new Document();
document.Add(new Field("id", quote.Id.ToString(), Field.Store.YES, Field.Index.NO));
document.Add(new Field("text", quote.Text, Field.Store.YES, Field.Index.TOKENIZED));
return document;
}
}
}

View File

@ -1,10 +0,0 @@
using System;
namespace VideoGameQuotes.Api.Search {
/// <summary>
/// Raised when an error occurs while reading/writing a search index
/// </summary>
public class SearchIndexException : Exception {
public SearchIndexException(string message = null, Exception innerException = null) : base(message, innerException) { }
}
}

View File

@ -1,17 +0,0 @@
namespace VideoGameQuotes.Api.Search {
/// <summary>
/// Represents a search result
/// </summary>
public class SearchResult<T> {
/// <summary>
/// A value (between 0 and 1, the higher the better) representing how good
/// the match is between the search query and the value
/// </summary>
public double Score { get; set; }
/// <summary>
/// The matched quote
/// </summary>
public T Entity { get; set; }
}
}

View File

@ -37,9 +37,6 @@
<Reference Include="log4net">
<HintPath>..\..\Lib\log4net.dll</HintPath>
</Reference>
<Reference Include="Lucene.Net">
<HintPath>..\..\Lib\Lucene.Net.dll</HintPath>
</Reference>
<Reference Include="MySql.Data">
<HintPath>..\..\Lib\MySql.Data.dll</HintPath>
</Reference>
@ -69,13 +66,6 @@
<Compile Include="Category.cs" />
<Compile Include="CriterionHandler.cs" />
<Compile Include="Game.cs" />
<Compile Include="Persistence\SearchService.cs" />
<Compile Include="Search\ISearcher.cs" />
<Compile Include="Search\ISearchIndexBuilder.cs" />
<Compile Include="Search\ISearchIndexLocator.cs" />
<Compile Include="Search\Lucene\ILuceneDocumentHandler.cs" />
<Compile Include="Search\Lucene\LuceneEntityIndexBuilder.cs" />
<Compile Include="Search\Lucene\LuceneEntitySearcher.cs" />
<Compile Include="Persistence\IUserRepository.cs" />
<Compile Include="Persistence\UserService.cs" />
<Compile Include="QuoteFlag.cs" />
@ -85,9 +75,6 @@
<Compile Include="QuoteFlagType.cs" />
<Compile Include="Region.cs" />
<Compile Include="GamingSystem.cs" />
<Compile Include="Search\Lucene\QuoteDocumentHandler.cs" />
<Compile Include="Search\SearchIndexException.cs" />
<Compile Include="Search\SearchResult.cs" />
<Compile Include="User.cs" />
<Compile Include="UserGroup.cs" />
<Compile Include="Vote.cs" />

View File

@ -4,8 +4,8 @@ using System.Reflection;
using Microsoft.Practices.Unity;
using Microsoft.Practices.Unity.InterceptionExtension;
using Portoa.Persistence;
using Portoa.Search;
using VideoGameQuotes.Api;
using VideoGameQuotes.Api.Search;
namespace VideoGameQuotes.Web.Configuration {

View File

@ -1,15 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web.Mvc;
using Portoa.Persistence;
using Portoa.Search;
using Portoa.Validation.DataAnnotations;
using Portoa.Web;
using Portoa.Web.Controllers;
using Portoa.Web.Results;
using VideoGameQuotes.Api;
using VideoGameQuotes.Api.Search;
using VideoGameQuotes.Web.Models;
using VideoGameQuotes.Web.Security;
using VideoGameQuotes.Web.Services;

View File

@ -13,7 +13,9 @@ using Lucene.Net.Util;
using Microsoft.Practices.Unity;
using Microsoft.Practices.Unity.InterceptionExtension;
using Portoa.Logging;
using Portoa.Lucene;
using Portoa.Persistence;
using Portoa.Search;
using Portoa.Web;
using Portoa.Web.Models;
using Portoa.Web.Security;
@ -21,11 +23,10 @@ using Portoa.Web.Unity;
using UnityGenerics;
using VideoGameQuotes.Api;
using VideoGameQuotes.Api.Persistence;
using VideoGameQuotes.Api.Search;
using VideoGameQuotes.Api.Search.Lucene;
using VideoGameQuotes.Web.Configuration;
using VideoGameQuotes.Web.Controllers;
using VideoGameQuotes.Web.Models;
using VideoGameQuotes.Web.Search;
using VideoGameQuotes.Web.Security;
using VideoGameQuotes.Web.Services;
using Directory = Lucene.Net.Store.Directory;

View File

@ -1,6 +1,6 @@
using System.Collections.Generic;
using Portoa.Search;
using VideoGameQuotes.Api;
using VideoGameQuotes.Api.Search;
namespace VideoGameQuotes.Web.Models {
public class SearchModel {

View File

@ -1,9 +1,10 @@
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Portoa.Lucene;
using VideoGameQuotes.Api;
namespace VideoGameQuotes.Api.Search.Lucene {
namespace VideoGameQuotes.Web.Search {
public class QuoteDocumentHandler : ILuceneDocumentHandler<Quote> {
public Document BuildDocument(Quote quote) {
var document = new Document();
document.Add(new Field("id", quote.Id.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));

View File

@ -1,13 +1,9 @@
using System.Collections.Generic;
using System.Linq;
using Portoa.Persistence;
using Portoa.Search;
namespace VideoGameQuotes.Api.Persistence {
public interface ISearchService<T> where T : Entity<T, int> {
IEnumerable<T> FindByIds(IEnumerable<int> ids);
IEnumerable<T> GetAllIndexableRecords();
}
namespace VideoGameQuotes.Web.Search {
public class SearchService<T> : ISearchService<T> where T : Entity<T, int> {
private readonly IRepository<T> repository;
@ -18,7 +14,6 @@ namespace VideoGameQuotes.Api.Persistence {
[UnitOfWork]
public IEnumerable<T> FindByIds(IEnumerable<int> ids) {
return repository
.Records
.Where(entity => ids.ToArray().Contains(entity.Id));

View File

@ -49,6 +49,7 @@
<Reference Include="Portoa.Log4Net">
<HintPath>..\..\Lib\Portoa.Log4Net.dll</HintPath>
</Reference>
<Reference Include="Portoa.Lucene, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL" />
<Reference Include="Portoa.NHibernate, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\Lib\Portoa.NHibernate.dll</HintPath>
@ -57,8 +58,6 @@
<HintPath>..\..\Lib\Portoa.Web.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Drawing" />
<Reference Include="System.Web.DynamicData" />
<Reference Include="System.Web.Entity" />
<Reference Include="System.Web.ApplicationServices" />
@ -68,23 +67,15 @@
<Reference Include="System.Core">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
<Reference Include="System.Data.DataSetExtensions">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
<Reference Include="System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" />
<Reference Include="System.Xml.Linq">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
<Reference Include="System.Web" />
<Reference Include="System.Web.Extensions">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
<Reference Include="System.Web.Abstractions" />
<Reference Include="System.Web.Routing" />
<Reference Include="System.Xml" />
<Reference Include="System.Configuration" />
<Reference Include="System.Web.Services" />
<Reference Include="System.EnterpriseServices" />
<Reference Include="UnityGenerics">
<HintPath>..\..\Lib\UnityGenerics.dll</HintPath>
</Reference>
@ -101,6 +92,8 @@
<Compile Include="Models\EditCategoryModel.cs" />
<Compile Include="Models\EditGameModel.cs" />
<Compile Include="Models\EditPublisherModel.cs" />
<Compile Include="Search\QuoteDocumentHandler.cs" />
<Compile Include="Search\SearchService.cs" />
<Compile Include="Services\CategoryService.cs" />
<Compile Include="Services\GameService.cs" />
<Compile Include="Services\PublisherService.cs" />

View File

@ -8,7 +8,7 @@
<%
foreach (var result in Model.Results) {
Html.RenderPartial("SingleQuote", new QuoteModel {Quote = result.Entity, User = Model.User });
Html.RenderPartial("SingleQuote", new QuoteModel { Quote = result.Record, User = Model.User });
}
%>
</asp:Content>