NOTE: Apart from
(and even then it's questionable, I'm Scottish). These are machine translated in languages I don't read. If they're terrible please contact me.
You can see how this translation was done in this article.
Tuesday, 17 September 2024
//Less than a minute
这只是一篇简短的文章,因为它借鉴了全文搜索系列中的其他文章,例如: typeahead 下调 和 Postgres 全文搜索. 使用 HTMX 和 EF Core 在 ASP.NET 核心应用程序中执行一个简单的搜索页面。
嗯,是的,在网站的页眉中,我有一个搜索功能,提供打印头(当您输入结果时,其位置是实时的)。 但我却以移动模式隐藏, 我也希望能将搜索结果连接起来(例如, /搜索/umami)的搜索专页。 这既能提供更好的用户经验,也能提供使用移动设备的经验。
为了做到这一点,我改变了我的搜索方法。 我创造了一个 BlogSearchService
,这是基于我的两个完整文本查询方法。 不幸的是,这些方法需要分为两种方法,因为查询的编排方式与Postgres全文搜索扩展程序相同, EF.Functions.WebSearchToTsQuery("english", processedQuery)
和 EF.Functions.ToTsQuery("english", query + ":*")
.
第一种采用适当的搜索条件,第二种采用通配符搜索。
private IQueryable<BlogPostEntity> QueryForSpaces(string processedQuery)
{
return context.BlogPosts
.Include(x => x.Categories)
.Include(x => x.LanguageEntity)
.AsNoTrackingWithIdentityResolution()
.Where(x =>
// Search using the precomputed SearchVector
(x.SearchVector.Matches(EF.Functions.WebSearchToTsQuery("english",
processedQuery)) // Use precomputed SearchVector for title and content
|| x.Categories.Any(c =>
EF.Functions.ToTsVector("english", c.Name)
.Matches(EF.Functions.WebSearchToTsQuery("english", processedQuery)))) // Search in categories
&& x.LanguageEntity.Name == "en") // Filter by language
.OrderByDescending(x =>
// Rank based on the precomputed SearchVector
x.SearchVector.Rank(EF.Functions.WebSearchToTsQuery("english",
processedQuery)));
}
private IQueryable<BlogPostEntity> QueryForWildCard(string query)
{
return context.BlogPosts
.Include(x => x.Categories)
.Include(x => x.LanguageEntity)
.AsNoTrackingWithIdentityResolution()
.Where(x =>
// Search using the precomputed SearchVector
(x.SearchVector.Matches(EF.Functions.ToTsQuery("english",
query + ":*")) // Use precomputed SearchVector for title and content
|| x.Categories.Any(c =>
EF.Functions.ToTsVector("english", c.Name)
.Matches(EF.Functions.ToTsQuery("english", query + ":*")))) // Search in categories
&& x.LanguageEntity.Name == "en") // Filter by language
.OrderByDescending(x =>
// Rank based on the precomputed SearchVector
x.SearchVector.Rank(EF.Functions.ToTsQuery("english",
query + ":*"))); // Use precomputed SearchVector for ranking
}
这些又用我的预数 SearchVector
该列更新了员额创建和更新的情况。 这个是我创造的 DbContext
使用 OnModelCreating
方法。
entity.Property(b => b.SearchVector)
.HasComputedColumnSql("to_tsvector('english', coalesce(\"Title\", '') || ' ' || coalesce(\"PlainTextContent\", ''))", stored: true);
entity.HasIndex(b => b.SearchVector)
.HasMethod("GIN");
这种办法的缺点是,它只对英语适用。 我需要彻底重建数据库,使其适用于其他语文(可能每种语文都有一个表格)。
然后我用这些方法 BlogSearchService
返回基于搜索查询的结果。
public async Task<PostListViewModel> GetPosts(string? query, int page = 1, int pageSize = 10)
{
if(string.IsNullOrEmpty(query))
{
return new PostListViewModel();
}
IQueryable<BlogPostEntity> blogPostQuery = query.Contains(" ") ? QueryForSpaces(query) : QueryForWildCard(query);
var totalPosts = await blogPostQuery.CountAsync();
var results = await blogPostQuery
.Select(x => x.ToListModel())
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return new PostListViewModel()
{
Posts = results,
TotalItems = totalPosts,
Page = page,
PageSize = pageSize
};
}
我使用简单检查查询中是否有空格来确定调用哪种方法。
搜索控制器遵循我对大多数我的控制器所使用的模式, 它检测呼叫是否来自 HTMX, 以便发送部分或完整的布局页面( 意思是它用于直接导航以及 HTMX 请求) 。
[Route("search")]
public class SearchController(
BaseControllerService baseControllerService,
BlogSearchService searchService,
ILogger<SearchController> logger)
: BaseController(baseControllerService, logger)
{
[HttpGet]
[Route("{query?}")]
public async Task<IActionResult> Search([FromRoute] string? query)
{
var searchResults = await searchService.GetPosts(query);
var searchModel = new SearchResultsModel
{
Query = query,
SearchResults = searchResults
};
searchModel = await PopulateBaseModel(searchModel);
searchModel.SearchResults.LinkUrl = Url.Action("SearchResults", "Search");
if (Request.IsHtmx()) return PartialView("SearchResults", searchModel);
return View("SearchResults", searchModel);
}
[HttpGet]
[Route("results")]
public async Task<IActionResult> SearchResults([Required] string query, int page = 1, int pageSize = 10)
{
var searchResults = await searchService.GetPosts(query, page, pageSize);
var searchModel = new SearchResultsModel
{
Query = query,
SearchResults = searchResults
};
searchModel = await PopulateBaseModel(searchModel);
searchModel.SearchResults.LinkUrl = Url.Action("SearchResults", "Search");
if (Request.IsHtmx()) return PartialView("_SearchResultsPartial", searchModel.SearchResults);
return View("SearchResults", searchModel);
}
}
这是整个控制器, 您可以看到我有两个动作, 一个返回页面( 可能包含结果), 一个只返回 HTMX 请求的结果 。
if (Request.IsHtmx()) return PartialView("_SearchResultsPartial", searchModel.SearchResults);
您可以看到此可选返回 _SearchResultsPartial
查看请求是HTMX结果请求。
这是一个非常简单的Paritial View, 它有传呼比特和结果。
@model Mostlylucid.Models.Blog.PostListViewModel
<div class="pt-2" id="content">
@if (Model.Posts?.Any() is true)
{
<div class="inline-flex w-full items-center justify-center print:!hidden">
@if (Model.TotalItems > Model.PageSize)
{
<pager
x-ref="pager"
link-url="@Model.LinkUrl"
hx-boost="true"
hx-target="#content"
hx-swap="show:none"
page="@Model.Page"
page-size="@Model.PageSize"
total-items="@Model.TotalItems"
hx-headers='{"pagerequest": "true"}'>
</pager>
}
<partial name="_Pager" model="Model"/>
</div>
@foreach (var post in Model.Posts)
{
<partial name="_ListPost" model="post"/>
}
}
</div>
我用同用同用同用同用同用 _ListPost
只要我需要列出职位,
@model Mostlylucid.Models.Blog.PostListModel
<div class="border-b border-grey-lighter pb-8 mb-8">
<a asp-controller="Blog" asp-action="Show" hx-boost="true" hx-swap="show:window:top" hx-target="#contentcontainer" asp-route-slug="@Model.Slug"
class="block font-body text-lg font-semibold transition-colors hover:text-green text-blue-dark dark:text-white dark:hover:text-secondary">@Model.Title</a>
<div class="flex flex-wrap space-x-2 items-center py-4 print:!hidden">
@foreach (var category in Model.Categories)
{
<partial name="_Category" model="category"/>
}
@{ var languageModel = (Model.Slug, Model.Languages, Model.Language); }
<partial name="_LanguageList" model="languageModel"/>
</div>
<div class="block font-body text-black dark:text-white">@Model.Summary</div>
<div class="flex items-center pt-4">
<p class="pr-2 font-body font-light text-primary light:text-black dark:text-white">
@Model.PublishedDate.ToString("f")
</p>
<span class="font-body text-grey dark:text-white">//</span>
<p class="pl-2 font-body font-light text-primary light:text-black dark:text-white">
@Model.ReadingTime
</p>
</div>
</div>
我在这里使用HTMX也很简单。 我只是点击按钮(这次我决定不更改按键上输入/ 更改 URL) 并在单击按键时发送请求 。
I 将查询包含在请求中使用的请求中 hx-include
目标目标 #content
div 替换结果。
<div class="flex items-center gap-2 bg-neutral-500 bg-opacity-10 p-2 rounded-lg">
<button
hx-get="@Url.Action("SearchResults", "Search")"
hx-target="#content"
hx-include="[name='query']"
hx-swap="outerHTML"
class="btn btn-outline btn-sm flex items-center gap-2 text-black dark:text-white">
Search
<i class="bx bx-search text-lg"></i>
</button>
<input
type="text"
placeholder="Search..."
value="@Model.Query"
name="query"
class="input input-sm border-0 grow text-black dark:text-white bg-transparent focus:outline-none"/>
</div>
因此,继一些评论之后, 哈立德( Khalid) 我决定加强这一功能,使搜索能够做到:
未来我需要把页码大小功能 重新加进去; 它是一个黑客的元件, 需要支持进一步的需求。
为此,我首先将输入包装成表格,然后使用Alpine.js在用户打字时提交表格。
你可以看到我用的是 x-data
来为查询创建一个反应变量,然后我检查查询的长度以确定是否提交表格。
<form
x-data="{ query: '@Model.Query', checkSubmit() { if (this.query.length > 2) { $refs.searchButton.click(); } } }"
class="flex items-center gap-2 bg-neutral-500 bg-opacity-10 p-2 rounded-lg"
action="@Url.Action("Search", "Search")"
hx-push-url="true"
hx-boost="true"
hx-target="#content"
hx-swap="outerHTML show:window:top"
hx-headers='{"pagerequest": "true"}'>
<button
type="submit"
x-ref="searchButton"
class="btn btn-outline btn-sm flex items-center gap-2 text-black dark:text-white">
Search
<i class="bx bx-search text-lg"></i>
</button>
<input
type="text"
placeholder="Search..."
name="query"
value="@Model.Query"
x-model="query"
x-on:input.debounce.200ms="checkSubmit"
x-on:keydown.enter.prevent="$refs.searchButton.click()"
class="input input-sm border-0 grow text-black dark:text-white bg-transparent focus:outline-none"
/>
</form>
为了重新使用同样的控制器动作,我还设定了 pagerequest
标头以表示此请求为缓冲请求 。
我也使用阿尔卑斯山 x-on:keydown.enter.prevent
在按下 Enter 键时单击按钮并按下输入以阻止过多请求。
在控制器中,我删除了搜索Results 动作, 相反在主控器中添加了更多的“ 情报” 。 Search
处理初始搜索和已处理的请求的动作 。
在这里您可以看到我加了一个额外的参数 pagerequest
以便确定这是否是一份请求书,并表明应该从信头收藏中填入这个请求书。
[HttpGet]
[Route("")]
public async Task<IActionResult> Search(string? query, int page = 1, int pageSize = 10,[FromHeader] bool pagerequest=false)
{
var searchResults = await searchService.GetPosts(query, page, pageSize);
var searchModel = new SearchResultsModel
{
Query = query,
SearchResults = searchResults
};
searchModel = await PopulateBaseModel(searchModel);
var linkUrl = Url.Action("Search", "Search");
searchModel.SearchResults.LinkUrl = linkUrl;
if(pagerequest && Request.IsHtmx()) return PartialView("_SearchResultsPartial", searchModel.SearchResults);
if (Request.IsHtmx()) return PartialView("SearchResults", searchModel);
return View("SearchResults", searchModel);
}
然后我得到结果 检测这个信头 以确定返回的视图/部分视图 。
我还添加了一个精细的退休者行动 来处理类似 /search/umami
,可重定向到带有查询的主搜索页面。
[HttpGet]
[Route("{query}")]
public IActionResult InitialSearch([FromRoute] string query)
{
return RedirectToAction("Search", new { query });
}
很简单吧? 这是使用 HTMX 和 EF Core 在 ASP.NET 核心应用程序中直接执行的搜索网页。 您可以轻松扩展此功能, 以包含更多功能, 如过滤、 排序, 甚至与其他搜索服务整合 。 关键取而代之的是,如何利用HTMX来获得平稳的用户经验,同时保持后端逻辑的清洁和效率。