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
//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 هنا بسيط جدا. ببساطة أعلق في الزر (هذه المرة قررت عدم تغيير المدخل على المفتاح / غيّر العنوان) وأرسل الطلب عند النقر على الزر.
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>
(ب) متابعة بعض التعليقات من: خالد خالد قررت تعزيز هذه الخاصية لتمكين البحث من أن يكون:
في المستقبل أنا بحاجة إلى إضافة وظيفة حجم الصفحة مرة أخرى في؛ انها خطوة من اختراق ويحتاج لدعم متطلبات أخرى.
إلى القيام بهذا قمت أولاً بلف الإدخال في استمارة واستخدمت 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 لتجربة مستخدم سلسة مع الحفاظ على المنطق الخلفي نظيفة وكفؤة.