# Yksinkertainen haku käyttäen HTMX- ja EF-ydintä ASP.NET-ytimeen

<!--category-- HTMX, Postgres, Entity Framework, ASP.NET -->
<datetime class="hidden">2024-09-17T17:36</datetime>

# Johdanto

Tämä on vain nopea artikkeli, koska se perustuu muihin tekstihakusarjoihin, kuten [Typeahead dropdown](/blog/textsearchingpt11) sekä [Postgres koko tekstin haku](/blog/textsearchingpt1).
Tässä viestissä näytän, miten voit toteuttaa yksinkertaisen hakusivun käyttämällä HTMX:ää ja EF Corea ASP.NET Core -sovelluksessa.

[TOC]

## Eikö minulla ole jo etsintää?

No kyllä, sivuston otsikossa Minulla on hakutoiminto, joka tarjoaa hakusanoja (jossa tulokset kirjoitetaan reaaliajassa). Piilotan sen kuitenkin mobiilitilassa ja halusin myös linkittää hakutulokset (kuten [/search/umami](/search/umami)) omalle hakusivulle. Tämä antaa paremman käyttökokemuksen sekä mobiililaitteiden parissa työskentelyn.

# Hakupalvelu

Tätä varten muokkasin, miten tein kotietsinnät. Minä loin `BlogSearchService`Tämä perustuu kahteen Full Text -kyselymenetelmääni. Nämä on valitettavasti jaettava kahteen menetelmään, koska kyselyt on järjestetty Postgres Full Text Search -laajennuksilla. `EF.Functions.WebSearchToTsQuery("english", processedQuery)` sekä `EF.Functions.ToTsQuery("english", query + ":*")`.

Ensimmäinen vaatii oikeat hakuehdot ja toinen villikorttihaut.

```csharp
    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
    }
```

Taas nämä käyttävät esilaukaisuani `SearchVector` kolumni, joka päivitetään jälkiluonnista ja päivityksestä. Tämä on luotu minun `DbContext` Käyttäen darbepoetiini alfaa ja darbepoetiini alfaa (ks. kohta 5. 2). `OnModelCreating` menetelmä.

```csharp
      entity.Property(b => b.SearchVector)
                .HasComputedColumnSql("to_tsvector('english', coalesce(\"Title\", '') || ' ' || coalesce(\"PlainTextContent\", ''))", stored: true);
            
           entity.HasIndex(b => b.SearchVector)
                .HasMethod("GIN");
```

Tämän lähestymistavan haittapuoli on jälleen se, että se toimii vain englantilaisille. Tarvitsisin tietokannan radikaalin uudelleenrakennuksen, jotta se toimisi muilla kielillä (todennäköisesti taulukko jokaiselle kielelle).

Sitten käytän näitä menetelmiä minun `BlogSearchService` palauttaa tulokset hakukyselyn perusteella.

```csharp
    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
        };
        
    }
```

Käytän yksinkertaista tarkistaa, onko välilyöntejä kyselyssä selvittää, mitä menetelmää soittaa.

## Etsi ohjain

Haku-ohjain noudattaa useimmille ohjaimilleni käyttämääni kaavaa, jossa se havaitsee, tuleeko puhelu HTMX:ltä vai ei, jotta se voi lähettää joko osittaisen tai täyden layout-sivun (mikä tarkoittaa, että se toimii suoran navigoinnin sekä HTMX-pyyntöjen vuoksi).

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

Tämä on koko ohjain, näet, että minulla on kaksi toimintoa, joista toinen palauttaa sivun (valinnaisesti asutut tulokset) ja toinen palauttaa vain tulokset HTMX-pyyntöihin.

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

### Etsinnän tulokset osittain

Huomaat, että tämä palauttaa valinnaisesti `_SearchResultsPartial` katso, jos pyyntö on HTMX-tulospyyntö.

Tämä on aika yksinkertainen Paritial View, jossa on haku bittejä ja tuloksia.

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

### Luettele osittainen näkymä@ info: whatsthis

Käytän samaa `_ListPost` Osittainen näkymä aina, kun minun tarvitsee listata virkoja.

```razor
@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 ja hakusivu

HtMX:n käyttö täällä on jälleen aika yksinkertaista. Kiinnitän vain nappiin (tällä kertaa päätin, etten muuta näppäimistön syötettä tai muuta URL-osoitetta) ja lähetän pyynnön, kun painiketta napsautetaan.
Sisällytän kyselyn pyyntöön käyttäen `hx-include` ja kohdistaa `#content` Div korvaa tulokset.

```razor
<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>
```

# Päivitä

Joten seuraavat joitakin kommentteja [Khalid](https://khalidabuhakmeh.com/)  Päätin tehostaa tätä toiminnallisuutta, jotta haku olisi:

1. Käynnistetty Enter-näppäinpainikkeella
2. Käynnistettiin yli kahden merkin syöttämisessä (joten siitä tulee löyhä tyyppi edessä).

Tulevaisuudessa minun täytyy lisätä sivun koko toiminnallisuus takaisin; se on biisi hakata ja tarvitsee tukea lisävaatimuksia.

## Päivitetty hakulomake

Tätä varten käärin syötteen ensin muotoon ja käytin Alpine.js:ia lähettääkseni lomakkeen käyttäjän kirjoittaessa.
Huomaat, että käytän `x-data` Luon kyselylle reaktiivisen muuttujan ja tarkistan sitten kyselyn pituuden, jotta voin päättää, toimitetaanko lomake.

```razor
<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>
```

Jotta sama ohjaintoiminto voitaisiin käyttää uudelleen, asetin myös `pagerequest` Otsikko osoittaa, että kyseessä on paginoitu pyyntö.

Käytän myös Alpine.js `x-on:keydown.enter.prevent` Käynnistääksesi painikkeen painalluksen, kun Enter-näppäintä painetaan, ja poistaaksesi syötteestä liikaa pyyntöjä.

## Hallintapäivitys

Ohjaimessa poistin SearchResults -toiminnon ja lisäsin sen sijaan pääosaan lisää "älykkyyttä". `Search` toimia sekä alustavan haun että paginoitujen pyyntöjen käsittelemiseksi.

Tässä näet Lisään ylimääräisen parametrin nimeltä `pagerequest` Määrittääkseen, onko kyseessä paginoitu pyyntö vai ei, ja ilmoittaakseen, että tämä pitäisi kirjata otsikkokokoelmasta.

```csharp

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

Sitten saan tulokset ja havaitsen tämän otsikon selvittääkseni, mikä näkymä / osittainen näkymä palaa.

Lisäsin lisäksi itsenäisen eläkkeelle siirtymistoimen, jolla hoidellaan sellaisia asioita kuin `/search/umami` Voit suunnata päähakusivulle kyselyn myötä.

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

# Johtopäätöksenä

Aika yksinkertaista, vai mitä? Tämä on suoraviivainen toteutus hakusivulle, jossa käytetään HTMX:ää ja EF Corea ASP.NET Core -sovelluksessa. Voit helposti laajentaa tämän koskemaan useampia ominaisuuksia, kuten suodatusta, lajittelua tai jopa integroitumista muihin hakupalveluihin. Keskeistä takea on se, miten HTMX:ää voidaan hyödyntää sujuvan käyttökokemuksen saavuttamiseksi ja samalla pitää backend-logiikka puhtaana ja tehokkaana.