This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Прості пошуки за допомогою HTMX & EF Core для ASP.NET Core
Tuesday, 17 September 2024
Введение
Це лише коротка стаття, яка базується на інших із серії пошуків у повному тексті, таких як вказівник і Поиск повного тексту 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>
Обновляти
Отже, після кількох коментарів від Халід Я вирішив покращити цю функцію, щоб пошук був
- Триггерування при натисненні на кнопку Ввести
- При вмиканні більше двох символів триггерується ( тож стає розслабленим шрифтом
У майбутньому мені потрібно додати функцію розміру сторінки назад в ; це ' це спроба хакера і потребує підтримки додаткових потреб М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 для гладкого досвіду користувача, зберігаючи чисту і ефективну логіку зворотнього заходу.