使用 HTMX & EF 核心进行 ASP. NET 核心的简单搜索 (中文 (Chinese Simplified))

使用 HTMX & EF 核心进行 ASP. NET 核心的简单搜索

Comments

NOTE: Apart from English (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

//

6 minute read

一. 导言 导言 导言 导言 导言 导言 一,导言 导言 导言 导言 导言 导言

这只是一篇简短的文章,因为它借鉴了全文搜索系列中的其他文章,例如: 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和搜索页面

我在这里使用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) 我决定加强这一功能,使搜索能够做到:

  1. 在 Enter 键按键触发
  2. 在输入两个以上的字符时触发( 因此变成一个松散的字头) 。

未来我需要把页码大小功能 重新加进去; 它是一个黑客的元件, 需要支持进一步的需求。

更新搜索表单

为此,我首先将输入包装成表格,然后使用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来获得平稳的用户经验,同时保持后端逻辑的清洁和效率。

logo

©2024 Scott Galloway