abstracted lucene/search stuff into Portoa
This commit is contained in:
parent
866964a764
commit
d2894435a1
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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; } }
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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) { }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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" />
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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));
|
@ -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));
|
@ -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" />
|
||||
|
@ -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>
|
Loading…
Reference in New Issue
Block a user