بسيط البحث باستخدام HTMX و EF أساس لـ ASP.net (العربية (Arabic))

بسيط البحث باستخدام 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

//

11 minute read

أولاً

هذه مجرد مقالة سريعة لأنها تبني على المواد الأخرى في سلسلة بحث النص الكامل مثل & _______ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ائل وقد عقد مؤتمراً بشأن مُنز نص. في هذه الوظيفة، سوف أريكم كيفية تنفيذ صفحة بحث بسيطة باستخدام HTMX و EF conre في تطبيق ASP.NET الأساسي.

أليس لديّ بالفعل بحث؟

حسناً، نعم، في عنوان الموقع لدي دالة بحث التي توفر نوع رأس (حيث عندما تطبع النتائج تأتي في الوقت الحقيقي). لكنّي أخفي ذلك في الوضع المتنقّل وأردت أيضاً أن أكون قادراً على ربط نتائج البحث (مثل /البحث/الوفاة) لصفحة بحث مخصصة. وهذا يعطي خبرة أفضل للمستعملين فضلا عن العمل على الأجهزة المحمولة.

دعم خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات خدمات

للقيام بهذا قمت بتعديل الطريقة التي قمت بها بعمليات التفتيش الخاصة بي. لقد خلقت BlogSearchServiceوهذا يستند إلى طريقتي الاستعلام عن النص الكامل. ومن المؤسف أن هذه الأمور تحتاج إلى تقسيمها إلى طريقتين نظراً للطريقة التي تنظم بها الاستفسارات مع تمديدات البحث في النصوص الكاملة للبريد، 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 للحصول على النتائج.

هذا عرض بارتيالي بسيط جداً والذي يحتوي على البتات و النتائج.

@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 هنا بسيط جدا. ببساطة أعلق في الزر (هذه المرة قررت عدم تغيير المدخل على المفتاح / غيّر العنوان) وأرسل الطلب عند النقر على الزر. hx-include واهـدف #content 4 الاستعاضة عن النتائج.

<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. مُثبَّتة عند إدخال أكثر من حرفين (يصبح بالتالي حرفاً فضفاضاً).

في المستقبل أنا بحاجة إلى إضافة وظيفة حجم الصفحة مرة أخرى في؛ انها خطوة من اختراق ويحتاج لدعم متطلبات أخرى.

مور مور_

إلى القيام بهذا قمت أولاً بلف الإدخال في استمارة واستخدمت Alpin.js لتقديم الاستمارة عندما يكون المستخدم مطبعاً. يمكنك أن ترى أنني أستخدم x-data إلى إ_ نشئ a مُتغير لـ إقتراح و تأكّد الطول من إقتراح إلى تحديد إلى تقديم استمارة.

<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 (ب) أن تشير إلى أن هذا الطلب هو طلب تناسلي.

أنا أيضاً أستخدم الـ Alpin.js x-on:keydown.enter.prevent إلى زِر زر انقر عند ضغط مفتاح إدخال و a يعمل إدخال إلى منع الكثير من الطلبات.

المبلغ المحدث

في المتحكّم قمت بإزالة عمل البحث في النتائج و أضفت بدلاً من ذلك المزيد من "التجسس" إلى الرئيس 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