Skip to content

Commit

Permalink
Search UI
Browse files Browse the repository at this point in the history
  • Loading branch information
petervandenhout committed Jan 12, 2021
1 parent efbe212 commit addb94f
Show file tree
Hide file tree
Showing 12 changed files with 218 additions and 88 deletions.
21 changes: 11 additions & 10 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public void ConfigureServices(IServiceCollection services)
{
...
services.AddPineBlog(Configuration);

services.AddRazorPages().AddPineBlogRazorPages();
// or services.AddMvcCore().AddPineBlogRazorPages();
// or services.AddMvc().AddPineBlogRazorPages();
Expand All @@ -29,21 +29,21 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
}
```

NOTE: Make sure you enable static file serving `app.UseStaticFiles();`, to enable the serving of the css and javascript from the `Opw.PineBlog.RazorPages` packages.
NOTE: Make sure you enable static file serving `app.UseStaticFiles();`, to enable the serving of the css and javascript from the `Opw.PineBlog.RazorPages` packages.

See [Customizing the layout](https://github.com/ofpinewood/pineblog/tree/master/docs/custom-layout.md) on how to setup the layout pages, css and javascript.
See [Customizing the layout](https://github.com/ofpinewood/pineblog/tree/master/docs/custom-layout.md) on how to setup the layout pages, css and javascript.

## Configuration
A few properties need to be configured before you can run your web application with PineBlog.

**Title:** the title of your blog/website.
**CoverUrl:** the URL for the cover image of your blog, this can be a relative or absolute URL.
**ConnectionStringName:** this is the name to the connection string used in your application.
**Title:** the title of your blog/website.
**CoverUrl:** the URL for the cover image of your blog, this can be a relative or absolute URL.
**ConnectionStringName:** this is the name to the connection string used in your application.
**CreateAndSeedDatabases:** to automatically create and seed the tables for the blog set this property to `true`, if you want to create and seed your
database in any other way set this property to `false`.
**AzureStorageConnectionString:** your Azure Blog Storage connection string.
**AzureStorageBlobContainerName:** the name of the blob container to use for file storage.
**FileBaseUrl:** the base URL for the files, this should be the URL for your Azure Blob Storage, e.g. `https://<storage-account>.blob.core.windows.net`.
database in any other way set this property to `false`.
**AzureStorageConnectionString:** your Azure Blog Storage connection string.
**AzureStorageBlobContainerName:** the name of the blob container to use for file storage.
**FileBaseUrl:** the base URL for the files, this should be the URL for your Azure Blob Storage, e.g. `https://<storage-account>.blob.core.windows.net`.

The rest of the properties are optional and will be set with default values if you don't specify them.

Expand All @@ -59,6 +59,7 @@ The rest of the properties are optional and will be set with default values if y
"CoverCaption": "Battle background for the Misty Woods in the game Shadows of Adam by Tim Wendorf",
"CoverLink": "http://pixeljoint.com/pixelart/94359.htm",
"ItemsPerPage": 5,
"EnableSearch": true, // default: false
"CreateAndSeedDatabases": true,
"ConnectionStringName": "DefaultConnection",
"AzureStorageConnectionString": "UseDevelopmentStorage=true",
Expand Down
1 change: 1 addition & 0 deletions samples/Opw.PineBlog.Sample/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"CoverCaption": "Battle background for the Misty Woods in the game Shadows of Adam by Tim Wendorf",
"CoverLink": "http://pixeljoint.com/pixelart/94359.htm",
"ItemsPerPage": 2,
"EnableSearch": true,
"CreateAndSeedDatabases": true,
"ConnectionStringName": "DefaultConnection",
"MongoDbDatabaseName": "pineblog-db",
Expand Down
6 changes: 6 additions & 0 deletions src/Opw.PineBlog.Abstractions/PineBlogOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ public class PineBlogOptions
public string PagingUrlPartFormat { get; set; }
public string CategoryUrlPartFormat { get; set; }
public string SearchQueryUrlPartFormat { get; set; }

/// <summary>
/// Enable search, or not.
/// </summary>
public bool EnableSearch { get; set; }

public bool CreateAndSeedDatabases { get; set; }

/// <summary>
Expand Down
83 changes: 73 additions & 10 deletions src/Opw.PineBlog.Core/Posts/SearchPostsQuery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Web;

// TODO: improve test coverage
namespace Opw.PineBlog.Posts
{
/// <summary>
Expand Down Expand Up @@ -73,15 +75,20 @@ public async Task<Result<PostListModel>> Handle(SearchPostsQuery request, Cancel
var predicates = new List<Expression<Func<Post, bool>>>();
predicates.Add(p => p.Published != null);

IEnumerable<Post> posts;
if (!string.IsNullOrWhiteSpace(request.SearchQuery))
{
predicates.Add(BuildSearchExpression(request.SearchQuery));
pagingUrlPartFormat += "&" + string.Format(_blogOptions.Value.SearchQueryUrlPartFormat, HttpUtility.UrlEncode(request.SearchQuery));
}

var posts = await GetPagedListAsync(predicates, pager, pagingUrlPartFormat, cancellationToken);

// TODO: add more weight to the title and categories
posts = await _uow.Posts.GetAsync(predicates, 0, int.MaxValue, cancellationToken);
posts = RankPosts(posts, request.SearchQuery);
posts = await GetPagedListAsync(posts, predicates, pager, pagingUrlPartFormat, cancellationToken);
}
else
{
posts = await GetPagedListAsync(predicates, pager, pagingUrlPartFormat, cancellationToken);
}

posts = posts.Select(p => _postUrlHelper.ReplaceUrlFormatWithBaseUrl(p));

Expand All @@ -104,18 +111,24 @@ public async Task<Result<PostListModel>> Handle(SearchPostsQuery request, Cancel
return Result<PostListModel>.Success(model);
}

private IEnumerable<string> ParseTerms(string query)
{
// convert multiple spaces into one space
query = Regex.Replace(query, @"\s+", " ").Trim();
return query.ToLower().Split(' ').ToList();
}

private Expression<Func<Post, bool>> BuildSearchExpression(string query)
{
var parameterExp = Expression.Parameter(typeof(Post), "p");
Expression exp = null;

var termList = query.ToLower().Split(' ').ToList();
foreach (var term in termList)
foreach (var term in ParseTerms(query))
{
exp = ConcatOr(exp, GetContainsExpression(nameof(Post.Title), term, parameterExp));
exp = ConcatOr(exp, GetContainsExpression(nameof(Post.Description), term, parameterExp));
exp = ConcatOr(exp, GetContainsExpression(nameof(Post.Categories), term, parameterExp));
exp = ConcatOr(exp, GetContainsExpression(nameof(Post.Content), term, parameterExp));
exp = ConcatOr(exp, GetContainsExpression(nameof(Post.Title), term.Trim(), parameterExp));
exp = ConcatOr(exp, GetContainsExpression(nameof(Post.Description), term.Trim(), parameterExp));
exp = ConcatOr(exp, GetContainsExpression(nameof(Post.Categories), term.Trim(), parameterExp));
exp = ConcatOr(exp, GetContainsExpression(nameof(Post.Content), term.Trim(), parameterExp));
}

return Expression.Lambda<Func<Post, bool>>(exp, parameterExp);
Expand All @@ -139,6 +152,46 @@ private Expression GetContainsExpression(string propertyName, string term, Param
return Expression.Call(propertyExp, method, someValue);
}

// TODO: test ranking
private IEnumerable<Post> RankPosts(IEnumerable<Post> posts, string query)
{
var terms = ParseTerms(query);
var rankedPosts = new List<Tuple<Post, int>>();

foreach (var post in posts)
{
var rank = 0;
foreach (var term in terms)
{
int hits;
if (post.Title.ToLower().Contains(term))
{
hits = Regex.Matches(post.Title.ToLower(), term).Count;
rank += hits * 10;
}
if (post.Categories.ToLower().Contains(term))
{
hits = Regex.Matches(post.Categories.ToLower(), term).Count;
rank += hits * 10;
}
if (post.Description.ToLower().Contains(term))
{
hits = Regex.Matches(post.Description.ToLower(), term).Count;
rank += hits * 3;
}
if (post.Content.ToLower().Contains(term))
{
hits = Regex.Matches(post.Content.ToLower(), term).Count;
rank += hits * 1;
}
}

rankedPosts.Add(new Tuple<Post, int>(post, rank));
}

return rankedPosts.OrderByDescending(t => t.Item2).Select(t => t.Item1).ToList();
}

private async Task<IEnumerable<Post>> GetPagedListAsync(IEnumerable<Expression<Func<Post, bool>>> predicates, Pager pager, string pagingUrlPartFormat, CancellationToken cancellationToken)
{
var skip = (pager.CurrentPage - 1) * pager.ItemsPerPage;
Expand All @@ -148,6 +201,16 @@ private async Task<IEnumerable<Post>> GetPagedListAsync(IEnumerable<Expression<F

return await _uow.Posts.GetAsync(predicates, skip, pager.ItemsPerPage, cancellationToken);
}

private async Task<IEnumerable<Post>> GetPagedListAsync(IEnumerable<Post> posts, IEnumerable<Expression<Func<Post, bool>>> predicates, Pager pager, string pagingUrlPartFormat, CancellationToken cancellationToken)
{
var skip = (pager.CurrentPage - 1) * pager.ItemsPerPage;
var count = await _uow.Posts.CountAsync(predicates, cancellationToken);

pager.Configure(count, pagingUrlPartFormat);

return posts.Skip(skip).Take(pager.ItemsPerPage);
}
}
}
}
26 changes: 21 additions & 5 deletions src/Opw.PineBlog.RazorPages/Areas/Blog/Pages/Index.cshtml
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
@page
@model IndexModel
@{
ViewData["ShowSearch"] = true;
var isSearch = Model.PostList.PostListType == PineBlog.Models.PostListType.Search;

var linkToNewerText = "Newer";
var linkToOlderText = "Older";
if (isSearch)
{
linkToNewerText = "Previous";
linkToOlderText = "Next";
}
}
@section head {
<partial name="_Metadata" model="@Model.Metadata" />
Expand All @@ -11,6 +19,14 @@

<div class="page-content">
<div class="container">
@if (isSearch && !Model.PostList.Posts.Any())
{
<article class="post">
<div class="post-content">
No results for "@ViewData["SearchQuery"]"
</div>
</article>
}
@foreach (var post in Model.PostList.Posts)
{
<partial name="_Post" model="post" />
Expand All @@ -21,16 +37,16 @@
<div class="item-next col-md-6">
@if (Model.PostList.Pager.ShowNewer)
{
<a href="[email protected]" title="Newer">
Newer
<a href="[email protected]" title="@linkToNewerText">
@linkToNewerText
</a>
}
</div>
<div class="item-prev col-md-6">
@if (Model.PostList.Pager.ShowOlder)
{
<a href="[email protected]" title="Older">
Older
<a href="[email protected]" title="@linkToOlderText">
@linkToOlderText
</a>
}
</div>
Expand Down
26 changes: 19 additions & 7 deletions src/Opw.PineBlog.RazorPages/Areas/Blog/Pages/Index.cshtml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,31 @@ namespace Opw.PineBlog.RazorPages.Areas.Blog.Pages
public class IndexModel : PageModelBase<IndexModel>
{
private readonly IMediator _mediator;
private readonly IOptionsSnapshot<PineBlogOptions> _options;

public PostListModel PostList { get; set; }

public Models.MetadataModel Metadata { get; set; }

public Models.PageCoverModel PageCover { get; set; }

[ViewData]
public bool ShowSearch { get; set; }

[ViewData]
public string SearchQuery { get; set; }

[ViewData]
public string Title { get; private set; }

public IndexModel(IMediator mediator, ILogger<IndexModel> logger)
[ViewData]
public string BlogTitle { get; private set; }

public IndexModel(IMediator mediator, IOptionsSnapshot<PineBlogOptions> options, ILogger<IndexModel> logger)
: base(logger)
{
_mediator = mediator;
_options = options;
}

public async Task<IActionResult> OnGetAsync(CancellationToken cancellationToken, [FromQuery] int page = 1, [FromQuery] string category = null, [FromQuery] string q = null)
Expand All @@ -40,16 +51,17 @@ public async Task<IActionResult> OnGetAsync(CancellationToken cancellationToken,
return GetPage(result);
}

public async Task<IActionResult> OnPostAsync(CancellationToken cancellationToken, [FromForm] string query = null)
{
var result = await _mediator.Send(new SearchPostsQuery { SearchQuery = query }, cancellationToken);
return GetPage(result);
}

private IActionResult GetPage(Result<PostListModel> result)
{
PostList = result.Value;
Title = result.Value.Blog.Title;
ShowSearch = _options.Value.EnableSearch;

if (PostList.PostListType == PostListType.Search)
{
BlogTitle = result.Value.Blog.Title;
SearchQuery = PostList.SearchQuery;
}

Metadata = new Models.MetadataModel
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
<span class="my-auto">@ViewData["BlogTitle"]</span>
}
</a>
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse" id="navbarSupportedContent"></div>
@*
<div class="navbar-collapse collapse d-inline-flex flex-row-reverse" id="navbarSupportedContent"></div>
@*
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
<div class="modal fade blog-search" id="blog-search" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body p-0">
<form role="search" asp-page="Index" asp- method="post">
<input type="search" id="query" name="query" class="form-control form-control-lg" placeholder="Search..." autocomplete="off">
</form>
@{
var showSearch = ViewData["ShowSearch"] != null && true;
}
@if (showSearch)
{
<div class="modal fade blog-search" id="blog-search" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body p-0">
<form role="search" asp-page="Index" asp- method="GET">
<input type="search" id="q" name="q" class="form-control form-control-lg" placeholder="Search .." value="@ViewData["SearchQuery"]" autocomplete="off">
</form>
</div>
</div>
</div>
</div>
</div>
@*TODO: remove link https://github.com/blogifierdotnet/Blogifier/blob/14c4303a3981bcd0262e0ceaa71df0f31a3bd0e4/src/App/Views/Themes/Standard/_Shared/_Footer.cshtml*@
}
Loading

0 comments on commit addb94f

Please sign in to comment.