Прості пошуки за допомогою HTMX & EF Core для ASP.NET Core (Українська (Ukrainian))

Прості пошуки за допомогою HTMX & EF Core для ASP.NET Core

Tuesday, 17 September 2024

//

9 minute read

Введение

Це лише коротка стаття, яка базується на інших із серії пошуків у повному тексті, таких як вказівник і Поиск повного тексту Postgres. В цьому пості я покажу вам, як реалізувати просту пошукову сторінку за допомогою HTMX та EF Core в ASP

Don't У мене вже є пошукM SK1

Ну так, в заголовку сайту я маю пошукову функцію, яка надає заголовок M SK1 де, коли ви набираєте, результати приходять в реальному часі ). Однак я приховую це у мобільному режимі і також хотів бути спроможним поєднати результати пошуку МSK3 як /searchM SK1umami) до спеціальної сторінки пошукуM SK1 Це дає кращий досвід користувача, а також дає змогу працювати на мобільних пристроях

Служба пошуку

Для цього я змінив те, як я робив свої пошуки. Я створив BlogSearchService, це базується на моїх двох методах пошуку повного тексту . На жаль, їх треба розділити на два способи через те, як запроси структуруються з розширеннями пошуку повних текстів Postgres , EF.Functions.WebSearchToTsQuery("english", processedQuery) і EF.Functions.ToTsQuery("english", query + ":*").

Перший має правильні терміни пошуку, а другий - пошуки wildcards.

    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");

Знову ж таки, вадою такого підходу є те, що він працює лише для англійської мови: МSK0 , ".". Мені потрібна радикальна перебудова бази даних, щоб вона працювала для інших мов.

Потім я використовую ці методи 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
        };
        
    }

Я використовую просту перевірку на те, чи є зайві місця в питанні, щоб визначити, який метод викликати.

Kontrolер пошуків

Проверка керує шаблоном, який я використовую для більшості моїх контроллерів, де він визначає, чи виклик приходить з HTMX, чи ні, щоб umożliwiти відправлення або частинної, або цілої сторінки макету. ( це означає, що він працює як для прямої навігації, так і для HTTPX запитів.

[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);
    }
}

Це весь контролер , ви бачите, що у мене є дві дії , одна, яка повертає сторінку МSK2 опціонально заповнена результатами МSK3 і одна, що повертається лише результатами для HTTPMX запитів

 if (Request.IsHtmx()) return PartialView("_SearchResultsPartial", searchModel.SearchResults);

Результати пошуку Частково

Ви бачите, що ця опція повертає _SearchResultsPartial подивитися, чи це HTMX-запрос щодо результатів.

Це досить простий Початковий вид, який має біти та результати

@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>

Список постчастий widok

Я використовую те ж саме _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 тут досить просте. Я просто підключюся до кнопки ( цього разу я вирішила не змінювати вхід на keyup МSK2 змінити URL Я включив запит у запит, використовуючи 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>

Обновляти

Отже, після кількох коментарів від Халід Я вирішив покращити цю функцію, щоб пошук був

  1. Триггерування при натисненні на кнопку Ввести
  2. При вмиканні більше двох символів триггерується ( тож стає розслабленим шрифтом

У майбутньому мені потрібно додати функцію розміру сторінки назад в ; це ' це спроба хакера і потребує підтримки додаткових потреб МSK2

Обновлений бланк пошуку

Для цього я спочатку загорнув вхідні дані у бланк і використав 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 заголовок, що вказує на те, що це веб-запрос.

Я також використовую Alpine.js x-on:keydown.enter.prevent щоб відштовхнути клацання кнопки, коли вмикається клавиша Ввести , і відкидати на вході, щоб запобігти надлишку запитів.

Обновлення контроллера

У контроллері я відібрав функцію SearchResults і натомість додав до головної 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);
    }

Потім я отримую результати і виявлю цей заголовок, щоб визначити, який вид / частинний вид повернеться

Я додав ще один відокремлений retirect акт для того, щоб справитись з такими /search/umami перенаправляти до головної сторінки пошуку за допомогою запиту.

   [HttpGet]
    [Route("{query}")]
    public  IActionResult InitialSearch([FromRoute] string query)
    {
        return RedirectToAction("Search", new { query });
    }

На завершення

Тож, досить простий правий ? Це простое вdrożenie сторінки пошуку, яка використовує HTMX та EF Core у ASPM SK2 NET Core application. Ви можете легко розширити це, щоб включити ще більше funkcji, таких як фильтруванняMSC4 сортуванняМSK5 або навіть інтегрування з іншими пошуковими службамиMska6 Головним уроком є те, як використати HT MX для гладкого досвіду користувача, зберігаючи чисту і ефективну логіку зворотнього заходу.

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.