JustSearch 0.4.0

dotnet add package JustSearch --version 0.4.0
                    
NuGet\Install-Package JustSearch -Version 0.4.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="JustSearch" Version="0.4.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="JustSearch" Version="0.4.0" />
                    
Directory.Packages.props
<PackageReference Include="JustSearch" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add JustSearch --version 0.4.0
                    
#r "nuget: JustSearch, 0.4.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package JustSearch@0.4.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=JustSearch&version=0.4.0
                    
Install as a Cake Addin
#tool nuget:?package=JustSearch&version=0.4.0
                    
Install as a Cake Tool

JustSearch

This library provides an easy integration to Algolia, TypeSense and MeiliSearch engines. Easy to switch between them or combine them for redundancy.

Usage

Add JustSearch and your data sources in your startup file.

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddJustSearch(new JustSearchOptions()
    {
        // Run indexing on app startup (optional)
        SyncOnStartup = true,
        
        // Prefix indexes/documents with environment name (optional)
        Prefix = env switch
        {
            not null when env.IsDevelopment() => "Dev_",
            not null when env.IsStaging() => "Test_",
            not null when env.IsProduction()  => "",
            _ => "Unk_"
        }
    })
    
    // Add your data source
    .AddSearchIndexDataProvider<ProductIndexDataProvider>()
    // You can add multiple source
    .AddSearchIndexDataProvider<CategoryIndexDataProvider>()
    
    // Add a provider
    .AddAlgoliaProvider("appId", "writeKey")
    
    // Or add another provider
    // (you can have multiple providers at the same time)
    .AddTypeSenseProvider(config =>
    {
        config.ApiKey = "API_KEY";
        config.Nodes = new List<Node>
        {
            new Node("localhost", "8108", "http")
        };
    })
    
    // Or MeiliSearch
    .AddMeiliSearchProvider("http://localhost:7700", "masterKey");

Configuring Data Source


// define your model
public record SearchableBrand : Searchable
{
    public string Url { get; init; }
    
    public string Name { get; init; }
    
    public string Logo { get; init; }
}

// implement ISearchIndexDataProvider
public class BrandIndexDataProvider : ISearchIndexDataProvider
{
    // This example uses EntityFrameworkCore, but you can use any data source
    private readonly AppDbContext db;
    
    public BrandIndexDataProvider(AppDbContext db)
    {
        this.db = db;
    }

    // Define your index name
    public string Name => "Brands";

    // Define your fields
    public async IAsyncEnumerable<ISearchField> GetFields()
    {
        yield return new SearchField("url", Type: SearchFieldType.String);
        yield return new SearchField("name", Type: SearchFieldType.String, IsSearchable: true);
        yield return new SearchField("logo", Type: SearchFieldType.String);
    }

    // You can optionally define synonyms, or return empty list
    public IAsyncEnumerable<ISynonym> GetSynonyms()
    {
        return AsyncEnumerable.Empty<ISynonym>();
    }
    
    // Retrive your data.
    // updatedSince is optional, but highly recommended for performance reasons.
    // if updatedSince is not null, only return items that have been updated since the given date.
    public IAsyncEnumerable<ISearchable> Get(DateTimeOffset? updatedSince = null)
    {
        return db.brands
            .OrderBy(a => a.id)
            .Where(a => a.enabled)
            .Where(a => updatedSince == null || a.updated_at > updatedSince || a.created_at > updatedSince)
            .Select(a => new SearchableBrand()
            {
                Id = a.id.ToString(), // Searchable comes with a required string Id field
                Url = a.url,
                Name = a.name,
                Logo = a.logo_url
            })
            .AsAsyncEnumerable();
    }
    
    // Retrive the id of the deleted items since the given date.
    // (you can leave it empty, if you manually delete items from trigger.)
    public IAsyncEnumerable<string> GetDeleted(DateTimeOffset since)
    {
        return db.brands
            
            // Gets disabled items since the given date
            .Where(a => !a.enabled)
            .Where(a => a.updated_at > since || a.created_at > since)
            
            .Select(a => a.id.ToString())
            .AsAsyncEnumerable()
            .Concat(
                // Gets deleted items since given date
                db.deleted_items
                    .Where(a => a.entity == "brands")
                    .Where(a => a.deleted_at > since)
                    .Select(a => a.entity_id.ToString())
                    .AsAsyncEnumerable()
            );
    }
}

The GetDeleted method might be confusing. Here is a sample implementation of deleted_items entity in EntityFrameworkCore with a library called Laraue.EfCoreTriggers.

public class brand
{
    [Key]
    public int id { get; set; }
    
    // ...
    
    public DateTimeOffset created_at { get; set; }
    
    // useful for data source
    public DateTimeOffset? updated_at { get; set; }
    
    // useful for soft deletes (optional if you use deleted_items table)
    // public DateTimeOffset? deleted_at { get; set; }
}

public class deleted_item
{
    public string entity { get; set; }
    
    public int entity_id { get; set; }
    
    public DateTimeOffset deleted_at { get; set; }
}

public class AppDbContext : DbContext
{
    public DbSet<brand> brands { get; set; }
    
    public DbSet<deleted_item> deleted_items { get; set; }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // ...
        
        modelBuilder.Entity<brand>(brand =>
        {
            // ...
            
            // This will create trigger for the DELETE operation in the database itself.
            brand.AfterDelete(op => op
                .Action(a => a
                    .Insert(e => new deleted_item
                    {
                        entity = "brands",
                        entity_id = e.Old.id
                    })));
        });
        
        modelBuilder.Entity<deleted_item>(deleted_item =>
        {
            deleted_item
                .HasKey(a => new {a.entity, a.entity_id});

            deleted_item
                .Property(a => a.deleted_at)
                .HasDefaultValueSql("now()");

            deleted_item
                .HasIndex(a => a.deleted_at)
                .IsDescending();
        });
    }
}

Triggers

Use triggers to keep your search indexes up-to-date.

public class BrandController : ControllerBase
{
    // inject the trigger on your controller or service
    private readonly ISearchIndexTrigger _searchIndexTrigger;
    
    // when an action is taken on brand, trigger the index update
    [HttpPost]
    [HttpPatch("{id}")]
    [HttpDelete("{id}")]
    public void DoStuffWithBrand(int id) {
        // ... (do database operation here)
        
        // Or update a specific index
        _searchIndexTrigger.Sync<BrandIndexDataProvider>();
        
        // Update all of the indexes.
        // Useful if you have multiple data sources that depend on each other.
        // (such as some products needs to be deleted from the index when its brand is deactivated)
        _searchIndexTrigger.SyncAll();
        
        // If you have implemented SearchIndexDataProvider correctly,
        // then all you need is the first two (SyncAll and Sync<T>)
        
        // If not, you can be more specific like:
        
        // Upsert specific item to the index
        _searchIndexTrigger.Upsert<BrandIndexDataProvider>(new SearchableBrand() {
            Id = id.ToString(),
            Name = "New Name",
            Url = "new-url",
            Logo = "new-logo"
        });
        
        // Upsert multiple items to the index
        _searchIndexTrigger.Upsert<BrandIndexDataProvider>(new List<SearchableBrand>() {
            new SearchableBrand() {
                Id = id.ToString(),
                Name = "New Name",
                Url = "new-url",
                Logo = "new-logo"
            },
            new SearchableBrand() {
                Id = (id + 1).ToString(),
                Name = "New Name 2",
                Url = "new-url-2",
                Logo = "new-logo-2"
            }
        });
        
        // Delete specific item from the index
        _searchIndexTrigger.Delete<BrandIndexDataProvider>(id.ToString());
        
        // Or delete multiple items from the index
        _searchIndexTrigger.Delete<BrandIndexDataProvider>(new List<string>() {
            id.ToString(),
            (id + 1).ToString()
        });
    }
}

The operations are queued in-memory and executed in the background service serially. If you need to ensure their success, you can use the WaitAll method returned from trigger actions.

    public void DoStuffWithBrand(int id) {
        // ...
        
        // Update all of the indexes and wait the operation to complete.
        await _searchIndexTrigger.SyncAll()
            .WaitAsync(CancellationToken.None);
        
        // This will work for rest of the operations as well.
        await _searchIndexTrigger.Delete<BrandIndexDataProvider>(id.ToString())
            .WaitAsync(CancellationToken.None);
    }
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.4.0 281 11/11/2025
0.3.2 234 11/10/2025
0.3.1 235 11/10/2025
0.3.0 196 11/18/2024
0.2.1 186 3/26/2024
0.2.0 185 3/25/2024
0.1.9 185 3/24/2024
0.1.8 170 3/23/2024
0.1.7 183 3/23/2024
0.1.6 179 3/23/2024
0.1.5 179 3/22/2024
0.1.4 173 3/22/2024
0.1.3 175 3/22/2024
0.1.2 187 3/22/2024
0.1.1 175 3/22/2024
0.1.0 182 3/22/2024