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
//Less than a minute
Це лише швидка стаття, яка будується на інших у повному текстовому пошуку, як-от locatedown typeahead і Postgres повний текстовий пошук. У цьому дописі я покажу вам, як реалізувати просту сторінку пошуку за допомогою HTMX і EF Core у програмі ASP.NET.
Ну так, у заголовку сайту у мене є функція пошуку, яка забезпечує типагед (де, як ви вводите результати приходять в реальному часі). Але я приховую це у режимі мобільного, а також хочу мати змогу пов' язати результати пошуку (на зразок / search/ 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
};
}
Я використовую просту перевірку для того, чи є пробіли у запиті, щоб визначити, який метод викликати.
Контролер пошуку стежить за шаблоном, який буде використано для більшості моїх контролерів, за допомогою якого він визначатиме, чи прийде виклик з GTMX, чи ні, щоб увімкнути надсилання часткової або повної сторінки розкладки (це означає, що він працює для безпосередньої навігації, а також запитів до 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
перегляд результатів, якщо запит є запитом на GTMX.
Це доволі простий перегляд паріталії, який має біти розмазування і результати.
@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 тут досить просте. Я просто зв' яжуся з цією кнопкою (на цей час я вирішив НЕ змінювати вхідні дані за допомогою клавіші keyup / змінити адресу URL) і надіслати запит після натискання кнопки.
Я включаю запит до запиту за допомогою hx-include
і ціль #content
Дрв, щоб замінити результати.
<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>
Отже, після деяких відгуків Халідafghanistan. kgm Я вирішив збільшити цю функціональність, щоб уможливити пошук:
В майбутньому мені потрібно додати функціональні можливості розміром сторінки, це бік хакера, і це потребує підтримки подальших вимог.
Для цього я спочатку загорнув вхідні дані у форму і використав альпійську.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
заголовок, щоб вказати, що це запит з пагінцями.
Я також використовую альпійські.js x-on:keydown.enter.prevent
Щоб увімкнути натискання кнопки, натисніть її клавішу Enter і скасуйте ввід, щоб запобігти надмірній кількості запитів.
У контролері Я вилучив дію 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);
}
Потім я отримую результати і знаходжу цей заголовок, щоб визначити, який перегляд / частковий перегляд повернутися.
Я додала запасні заходи, щоб впоратися з такими, як /search/umami
для переспрямування на головну сторінку пошуку за допомогою запиту.
[HttpGet]
[Route("{query}")]
public IActionResult InitialSearch([FromRoute] string query)
{
return RedirectToAction("Search", new { query });
}
Так, доволі просто, правильно? Це очевидна реалізація сторінки пошуку за допомогою HTMX і EF Core у програмі для роботи з ядром ASP. NET. Ви можете просто розширити цю можливість, щоб включити більше можливостей, зокрема фільтрування, впорядкування або навіть інтеграції з іншими службами пошуку. Ключем є те, як використати HTMX для здорового користувача, зберігаючи логічну логіку сервера чистою і ефективною.