got search working with lucene.net
This commit is contained in:
parent
973b8196d9
commit
f7de96eb63
19
Src/VideoGameQuotes.Api/Search/IQuoteSearcher.cs
Normal file
19
Src/VideoGameQuotes.Api/Search/IQuoteSearcher.cs
Normal file
@ -0,0 +1,19 @@
|
||||
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);
|
||||
}
|
||||
}
|
15
Src/VideoGameQuotes.Api/Search/ISearchIndexLocator.cs
Normal file
15
Src/VideoGameQuotes.Api/Search/ISearchIndexLocator.cs
Normal file
@ -0,0 +1,15 @@
|
||||
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; } }
|
||||
}
|
||||
}
|
13
Src/VideoGameQuotes.Api/Search/Lucene/LuceneExtensions.cs
Normal file
13
Src/VideoGameQuotes.Api/Search/Lucene/LuceneExtensions.cs
Normal file
@ -0,0 +1,13 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
58
Src/VideoGameQuotes.Api/Search/Lucene/LuceneQuoteSearcher.cs
Normal file
58
Src/VideoGameQuotes.Api/Search/Lucene/LuceneQuoteSearcher.cs
Normal file
@ -0,0 +1,58 @@
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
17
Src/VideoGameQuotes.Api/Search/SearchResult.cs
Normal file
17
Src/VideoGameQuotes.Api/Search/SearchResult.cs
Normal file
@ -0,0 +1,17 @@
|
||||
namespace VideoGameQuotes.Api.Search {
|
||||
/// <summary>
|
||||
/// Represents a search result
|
||||
/// </summary>
|
||||
public class SearchResult {
|
||||
/// <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 Quote Quote { get; set; }
|
||||
}
|
||||
}
|
@ -37,6 +37,9 @@
|
||||
<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>
|
||||
@ -58,11 +61,7 @@
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.ComponentModel.DataAnnotations" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="ApiException.cs" />
|
||||
@ -71,6 +70,10 @@
|
||||
<Compile Include="CriterionHandler.cs" />
|
||||
<Compile Include="Game.cs" />
|
||||
<Compile Include="ICurrentUserProvider.cs" />
|
||||
<Compile Include="Search\IQuoteSearcher.cs" />
|
||||
<Compile Include="Search\ISearchIndexLocator.cs" />
|
||||
<Compile Include="Search\Lucene\LuceneQuoteSearcher.cs" />
|
||||
<Compile Include="Search\Lucene\LuceneExtensions.cs" />
|
||||
<Compile Include="Persistence\IUserRepository.cs" />
|
||||
<Compile Include="Persistence\UserService.cs" />
|
||||
<Compile Include="QuoteFlag.cs" />
|
||||
@ -80,6 +83,7 @@
|
||||
<Compile Include="QuoteFlagType.cs" />
|
||||
<Compile Include="Region.cs" />
|
||||
<Compile Include="GamingSystem.cs" />
|
||||
<Compile Include="Search\SearchResult.cs" />
|
||||
<Compile Include="User.cs" />
|
||||
<Compile Include="UserGroup.cs" />
|
||||
<Compile Include="Vote.cs" />
|
||||
|
@ -8,6 +8,7 @@ using Portoa.Persistence;
|
||||
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;
|
||||
@ -16,10 +17,12 @@ namespace VideoGameQuotes.Web.Controllers {
|
||||
public class QuoteController : Controller {
|
||||
private readonly IQuoteService quoteService;
|
||||
private readonly ICurrentUserProvider currentUserProvider;
|
||||
private readonly IQuoteSearcher quoteSearcher;
|
||||
|
||||
public QuoteController(IQuoteService quoteService, ICurrentUserProvider currentUserProvider) {
|
||||
public QuoteController(IQuoteService quoteService, ICurrentUserProvider currentUserProvider, IQuoteSearcher quoteSearcher) {
|
||||
this.quoteService = quoteService;
|
||||
this.currentUserProvider = currentUserProvider;
|
||||
this.quoteSearcher = quoteSearcher;
|
||||
}
|
||||
|
||||
public ActionResult Browse(BrowseModel model) {
|
||||
@ -27,7 +30,10 @@ namespace VideoGameQuotes.Web.Controllers {
|
||||
return View("DefaultBrowse");
|
||||
}
|
||||
|
||||
return View("QualifiedBrowse", new QualifiedBrowseModel(model) { Quotes = quoteService.GetBrowsableQuotes(model) });
|
||||
return View("QualifiedBrowse", new QualifiedBrowseModel(model) {
|
||||
Quotes = quoteService.GetBrowsableQuotes(model),
|
||||
User = currentUserProvider.CurrentUser
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost, IsValidUser]
|
||||
@ -237,5 +243,15 @@ namespace VideoGameQuotes.Web.Controllers {
|
||||
return Json(this.CreateJsonErrorResponse(e));
|
||||
}
|
||||
}
|
||||
|
||||
public ActionResult Search(string searchQuery) {
|
||||
var model = new SearchModel {
|
||||
User = currentUserProvider.CurrentUser,
|
||||
Results = quoteSearcher.Search(searchQuery),
|
||||
SearchQuery = searchQuery
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,18 @@
|
||||
using System.Web.Mvc;
|
||||
using System.Collections.Specialized;
|
||||
using System.Configuration;
|
||||
using System.Web.Mvc;
|
||||
using System.Web.Routing;
|
||||
using Microsoft.Practices.Unity;
|
||||
using Portoa.Logging;
|
||||
using Portoa.Persistence;
|
||||
using Portoa.Web;
|
||||
using Portoa.Web.Models;
|
||||
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.Controllers;
|
||||
using VideoGameQuotes.Web.Models;
|
||||
using VideoGameQuotes.Web.Security;
|
||||
@ -36,23 +42,33 @@ namespace VideoGameQuotes.Web {
|
||||
.RegisterType<IUserService, UserService>()
|
||||
.RegisterType<IQuoteService, QuoteService>()
|
||||
.RegisterType<IApiService, ApiService>()
|
||||
.RegisterType<IUserRepository, UserRepository>();
|
||||
.RegisterType<IQuoteSearcher, LuceneQuoteSearcher>()
|
||||
.RegisterType<ISearchIndexLocator, SearchIndexLocator>(
|
||||
new ContainerControlledLifetimeManager(),
|
||||
new InjectionFactory(container => new SearchIndexLocator(((NameValueCollection)ConfigurationManager.GetSection("vgquotes"))["luceneIndexDirectory"]))
|
||||
).RegisterType<IUserRepository, UserRepository>();
|
||||
}
|
||||
|
||||
protected override void AfterStartUp() {
|
||||
var logger = Container.Resolve<ILogger>();
|
||||
logger.Info("Building lucene index");
|
||||
Container.Resolve<IQuoteSearcher>().BuildIndex();
|
||||
logger.Info("Done building lucene index");
|
||||
}
|
||||
|
||||
protected override void RegisterRoutes(RouteCollection routes) {
|
||||
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
|
||||
routes.IgnoreRoute("media/{*anything}");
|
||||
|
||||
|
||||
routes.MapRoute("api", "api/{action}/{id}/{*criteria}", new { controller = "Api" }, new { action = "game|system|category|publisher|quote", id = @"\d+|all" });
|
||||
|
||||
routes.MapRoute("home", "{action}", new { controller = "Home", action = "Index" }, new { action = "about|contact" });
|
||||
|
||||
routes.MapRoute("best", "best/{start}-{end}/", new { controller = "Quote", action = "Best" }, new { start = @"\d+", end = @"\d+" });
|
||||
routes.MapRoute("browse", "browse/{*qualifiers}", new { controller = "Quote", action = "Browse" });
|
||||
routes.MapRoute("quote", "{action}", new { controller = "Quote" }, new { action = "submit|search|recent|random|best|vote|report" });
|
||||
routes.MapRoute("search", "search/{*searchQuery}", new { controller = "Quote", action = "Search" });
|
||||
routes.MapRoute("quote", "{action}", new { controller = "Quote" }, new { action = "submit|recent|random|best|vote|report" });
|
||||
routes.MapRoute("individual-quote", "quote/{id}/{*text}", new { controller = "Quote", action = "Quote" }, new { id = @"\d+" });
|
||||
routes.MapRoute("create-category", "category/create", new { controller = "Quote", action = "CreateCategory" });
|
||||
routes.MapRoute("default", "{controller}", new { controller = "home", action = "index" });
|
||||
}
|
||||
}
|
||||
}
|
11
Src/VideoGameQuotes.Web/Models/SearchModel.cs
Normal file
11
Src/VideoGameQuotes.Web/Models/SearchModel.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using VideoGameQuotes.Api;
|
||||
using VideoGameQuotes.Api.Search;
|
||||
|
||||
namespace VideoGameQuotes.Web.Models {
|
||||
public class SearchModel {
|
||||
public User User { get; set; }
|
||||
public IEnumerable<SearchResult> Results { get; set; }
|
||||
public string SearchQuery { get; set; }
|
||||
}
|
||||
}
|
@ -94,6 +94,7 @@
|
||||
<Compile Include="Models\QuoteCollectionModel.cs" />
|
||||
<Compile Include="Models\QuoteModel.cs" />
|
||||
<Compile Include="Models\ReportModel.cs" />
|
||||
<Compile Include="Models\SearchModel.cs" />
|
||||
<Compile Include="Models\VoteModel.cs" />
|
||||
<Compile Include="Validation\NonEmptyText.cs" />
|
||||
<Compile Include="Security\IsValidUserAttribute.cs" />
|
||||
@ -134,6 +135,7 @@
|
||||
<Content Include="Views\Quote\Quote.aspx" />
|
||||
<Content Include="Views\Quote\QuoteNotFound.aspx" />
|
||||
<Content Include="Views\Quote\Recent.aspx" />
|
||||
<Content Include="Views\Quote\Search.aspx" />
|
||||
<Content Include="Views\Quote\Submit.aspx" />
|
||||
<Content Include="Views\Shared\ExceptionView.ascx" />
|
||||
<Content Include="Views\Shared\Forbidden.aspx" />
|
||||
|
14
Src/VideoGameQuotes.Web/Views/Quote/Search.aspx
Normal file
14
Src/VideoGameQuotes.Web/Views/Quote/Search.aspx
Normal file
@ -0,0 +1,14 @@
|
||||
<%@ Page Title="" Language="C#" Inherits="System.Web.Mvc.ViewPage<VideoGameQuotes.Web.Models.SearchModel>" MasterPageFile="~/Views/Shared/Site.Master" %>
|
||||
<%@ Import Namespace="VideoGameQuotes.Web.Models" %>
|
||||
<asp:Content runat="server" ID="Title" ContentPlaceHolderID="TitleContent">Search<%= !string.IsNullOrEmpty(Model.SearchQuery) ? " - " + Model.SearchQuery : "" %></asp:Content>
|
||||
<asp:Content runat="server" ID="Main" ContentPlaceHolderID="MainContent">
|
||||
<p>
|
||||
Search results for: <span class="Search-query"><%: Model.SearchQuery %></span>
|
||||
</p>
|
||||
|
||||
<%
|
||||
foreach (var result in Model.Results) {
|
||||
Html.RenderPartial("SingleQuote", new QuoteModel {Quote = result.Quote, User = Model.User });
|
||||
}
|
||||
%>
|
||||
</asp:Content>
|
@ -29,9 +29,9 @@
|
||||
<li><%= Html.ActionLink("Submit", "submit", "Quote", null, new { title = "Submit a new quote" }) %></li>
|
||||
<li><%= Html.ActionLink("About", "about", "Home", null, new { title = "About the site" })%></li>
|
||||
<li class="searchbox">
|
||||
<% using (Html.BeginForm("search", "quote", FormMethod.Get)) { %>
|
||||
<% using (Html.BeginForm("search", "quote", FormMethod.Get, new { id = "search-form" })) { %>
|
||||
<div>
|
||||
<%= Html.TextBox("term") %>
|
||||
<%= Html.TextBox("searchQuery", null, new { id = "search-query" })%>
|
||||
<input type="image" src="/media/images/search.png" alt="search" title="search quotes, games, systems" />
|
||||
</div>
|
||||
<% } %>
|
||||
|
@ -4,7 +4,12 @@
|
||||
<configSections>
|
||||
<!--<section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection"/>-->
|
||||
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>
|
||||
<section name="vgquotes" type="System.Configuration.NameValueSectionHandler"/>
|
||||
</configSections>
|
||||
|
||||
<vgquotes>
|
||||
<add key="luceneIndexDirectory" value="c:\users\tmont\code\VideoGameQuotes\lucene_index"/>
|
||||
</vgquotes>
|
||||
|
||||
<log4net>
|
||||
<appender name="DebugAppender" type="log4net.Appender.DebugAppender">
|
||||
|
@ -7,11 +7,19 @@
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
$("#search-form").submit(function() {
|
||||
var searchQuery = $.trim($("#search-query").val());
|
||||
if (searchQuery.length > 0) {
|
||||
window.location = "/search/" + searchQuery;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
var getQuoteId = function($container) {
|
||||
return $container.find("input.quote-id").val();
|
||||
};
|
||||
|
||||
|
||||
var voting = false;
|
||||
$(".vote-for, .vote-against").live("click", function() {
|
||||
if (voting) {
|
||||
|
53
Tests/VideoGameQuotes.Api.Tests/LuceneTests.cs
Normal file
53
Tests/VideoGameQuotes.Api.Tests/LuceneTests.cs
Normal file
@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using Lucene.Net.Analysis.Standard;
|
||||
using Lucene.Net.Documents;
|
||||
using Lucene.Net.QueryParsers;
|
||||
using Lucene.Net.Search;
|
||||
using NUnit.Framework;
|
||||
using VideoGameQuotes.Api.Search.Lucene;
|
||||
|
||||
namespace VideoGameQuotes.Api.Tests {
|
||||
|
||||
|
||||
|
||||
[TestFixture, Ignore]
|
||||
public class LuceneTests {
|
||||
|
||||
[Test]
|
||||
public void Should_use_lucene() {
|
||||
var analyzer = new StandardAnalyzer(new [] { "the", "of", "a", "an" });
|
||||
const string indexDirectory = @"c:\users\tmont\code\lucene_index_test";
|
||||
var indexWriter = new Lucene.Net.Index.IndexWriter(indexDirectory, analyzer, true);
|
||||
|
||||
indexWriter.AddDocument(CreateDocument(4, "cat"));
|
||||
indexWriter.AddDocument(CreateDocument(5, "catty"));
|
||||
indexWriter.AddDocument(CreateDocument(6, "Catherine"));
|
||||
indexWriter.AddDocument(CreateDocument(7, "vacate"));
|
||||
indexWriter.AddDocument(CreateDocument(8, "cat cat cat"));
|
||||
indexWriter.AddDocument(CreateDocument(9, "i'm trying to herd a cat, a cat, I say!"));
|
||||
indexWriter.AddDocument(CreateDocument(10, "the cat"));
|
||||
|
||||
indexWriter.Optimize();
|
||||
indexWriter.Close();
|
||||
|
||||
var parser = new QueryParser("text", analyzer);
|
||||
|
||||
var query = parser.Parse("cat");
|
||||
|
||||
|
||||
var searcher = new IndexSearcher(indexDirectory);
|
||||
var hits = searcher.Search(query).ToEnumerable();
|
||||
foreach (var hit in hits) {
|
||||
Console.WriteLine("id: {0}, document: {1}, score: {2}", hit.GetId(), hit.GetDocument(), hit.GetScore());
|
||||
}
|
||||
}
|
||||
|
||||
private static Document CreateDocument(int id, string text) {
|
||||
var document = new Document();
|
||||
document.Add(new Field("id", id.ToString(), Field.Store.YES, Field.Index.UN_TOKENIZED));
|
||||
document.Add(new Field("text", text, Field.Store.YES, Field.Index.TOKENIZED));
|
||||
return document;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -34,6 +34,9 @@
|
||||
<Reference Include="log4net">
|
||||
<HintPath>..\..\Lib\log4net.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Lucene.Net">
|
||||
<HintPath>..\..\Lib\Lucene.Net.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Moq">
|
||||
<HintPath>..\..\Lib\Moq.dll</HintPath>
|
||||
</Reference>
|
||||
@ -63,6 +66,7 @@
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="LuceneTests.cs" />
|
||||
<Compile Include="QuoteTests.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="NHibernate\SchemaExporter.cs" />
|
||||
|
Loading…
Reference in New Issue
Block a user