Gå til indholdet

Introduktion til Meilisearch

Hvad er Meilisearch?

Meilisearch er en open-source søgemaskine skrevet i Rust, der er designet til at levere hurtige, relevante og brugervenlige søgeoplevelser. Den er specielt egnet til instant-search funktionalitet, hvor søgeresultater skal vises i realtid, mens brugeren taster.

Info

Der findes en del eksempler på brug af databasen her.

Historik

Meilisearch blev oprindeligt inspireret af Algolia, en kommerciel søgeløsning, der satte standarden for instant-search og typo-tolerance. Siden Elasticsearchs lancering i 2010 har der været et stigende behov for enklere alternativer til mindre projekter. Meilisearch blev skabt for at give udviklere et værktøj, der kombinerer kraftfuld søgefunktionalitet med nem opsætning og vedligeholdelse.

Hvorfor denne type database?

Search engines som Meilisearch er specialiserede databaser optimeret til fuld-tekst søgning. De adskiller sig fra traditionelle relationelle databaser ved:

  • Instant-search: Resultater returneres under 50 millisekunder, optimeret til søg-mens-du-taster oplevelser
  • Typo-tolerance: Automatisk håndtering af stavefejl uden ekstra konfiguration
  • Relevans: Avancerede ranking-algoritmer sikrer, at de mest relevante resultater vises først
  • Faceting & Filtrering: Indbygget support for at indsnævre søgeresultater baseret på kategorier og attributter

Sammenligning med Elasticsearch

Feature Meilisearch Elasticsearch
Målgruppe Front-end instant-search til slutbrugere Backend log-analyse og generel søgning
Kompleksitet Simpel opsætning, minimal konfiguration Kræver omfattende konfiguration
Ressourceforbrug ~290MB RAM (typisk) ~1.3GB+ RAM
Søgehastighed 10-30ms (optimeret til instant-search) Kan være langsommere for bruger-søgning
Typo-tolerance Out-of-the-box Kræver manuel konfiguration
Use case Webshops, dokumentation, media-platforme Log-data, analytics, komplekse queries
Teknologi Skrevet i Rust Baseret på Apache Lucene (Java)
Skalering Egnet til små-mellemstore datasets Håndterer massive datasets

Når skal man vælge Meilisearch?

Vælg Meilisearch når du:

  • Bygger en søgefunktion til slutbrugere (website, app)
  • Har brug for hurtig opsætning uden dybdegående konfiguration
  • Ønsker instant-search med typo-tolerance ud af boksen
  • Har begrænsede ressourcer (mindre server, mindre team)
  • Arbejder med små til mellemstore datasets (op til millioner af dokumenter)

Når skal man vælge Elasticsearch?

Vælg Elasticsearch når du:

  • Skal analysere store mængder log-data
  • Har brug for avancerede analytics og aggregeringer
  • Arbejder med meget store datasets (milliarder af dokumenter)
  • Har ressourcer til at konfigurere og vedligeholde systemet
  • Skal bruge det fulde Elastic Stack (Kibana, Logstash, Beats)

Docker installation til test

Den nemmeste måde at teste Meilisearch på er via Docker:

# Pull og kør Meilisearch container
docker run -d --name meilisearch -p 7700:7700 -e MEILI_MASTER_KEY=supersecret getmeili/meilisearch:latest

# Verificer at containeren kører
docker ps

# Se logs
docker logs meilisearch

Meilisearch er nu tilgængelig på http://localhost:7700

Du kan også teste API’et direkte i browseren eller via curl:

# Tjek serverens health
curl http://localhost:7700/health

# Forventet svar: {"status":"available"}

Simple C# eksempler

Installation af NuGet pakke

dotnet add package MeiliSearch

Eksempel 1: Opret index og tilføj dokumenter

using Meilisearch;



// Opret klient
var client = new MeilisearchClient("http://localhost:7700", "supersecret");

// Opret og hent index
await client.CreateIndexAsync("movies");
var index = await client.GetIndexAsync("movies");

// Tilføj dokumenter
var movies = new[]
{
    new Movie 
    { 
        Id = "1", 
        Title = "The Shawshank Redemption", 
        Genres = new[] { "Drama" }, 
        Year = 1994,
        Director = "Frank Darabont"
    },
    new Movie 
    { 
        Id = "2", 
        Title = "The Godfather", 
        Genres = new[] { "Crime", "Drama" }, 
        Year = 1972,
        Director = "Francis Ford Coppola"
    },
    new Movie 
    { 
        Id = "3", 
        Title = "The Dark Knight", 
        Genres = new[] { "Action", "Crime", "Drama" }, 
        Year = 2008,
        Director = "Christopher Nolan"
    }
};

var task = await index.AddDocumentsAsync(movies);
Console.WriteLine($"Task UID: {task.TaskUid}");

// Vent på at indexeringen er færdig
await client.WaitForTaskAsync(task.TaskUid);

public class Movie
{
    public string? Id { get; set; }
    public string? Title { get; set; }
    public string[]? Genres { get; set; }
    public int Year { get; set; }
    public string? Director { get; set; }
}

Eksempel 2: Søgning

using Meilisearch;

var client = new MeilisearchClient("http://localhost:7700", "supersecret");
var index = await client.GetIndexAsync("movies");

// Simpel søgning
var searchResult = await index.SearchAsync<Movie>("godfather");

Console.WriteLine($"Fundet {searchResult.Hits.Count} film:");
foreach (var movie in searchResult.Hits)
{
    Console.WriteLine($"- {movie.Title} ({movie.Year}) - {movie.Director}");
}

// Søgning med typo
var typoResult = await index.SearchAsync<Movie>("godfater"); // Bemærk stavefejl
Console.WriteLine($"Fundet {typoResult.Hits.Count} film:");
foreach (var movie in typoResult.Hits)
{
    Console.WriteLine($"- {movie.Title} ({movie.Year}) - {movie.Director}");
}

public class Movie
{
    public string? Id { get; set; }
    public string? Title { get; set; }
    public string[]? Genres { get; set; }
    public int Year { get; set; }
    public string? Director { get; set; }
}

Eksempel 3: Avanceret søgning med filtre

using Meilisearch;

var client = new MeilisearchClient("http://localhost:7700", "supersecret");
var index = await client.GetIndexAsync("movies");

// Først skal vi definere filtrerbare attributter
var settings = new Settings
{
    FilterableAttributes = new string[] { "year", "genres" }
};
await index.UpdateSettingsAsync(settings);

// Søg efter action-film fra efter 2000
var filteredSearch = await index.SearchAsync<Movie>(
    "knight",
    new SearchQuery
    {
        Filter = "year > 2000 AND genres = Action",
        Limit = 10
    }
);

foreach (var movie in filteredSearch.Hits)
{
    Console.WriteLine($"{movie.Title} ({movie.Year}) - Genres: {string.Join(", ", movie.Genres)}");
}

public class Movie
{
    public string? Id { get; set; }
    public string? Title { get; set; }
    public string[]? Genres { get; set; }
    public int Year { get; set; }
    public string? Director { get; set; }
}

Eksempel 4: Faceted search (e-commerce)

using Meilisearch;

var client = new MeilisearchClient("http://localhost:7700", "supersecret");

await client.CreateIndexAsync("products");
var productIndex = await client.GetIndexAsync("products");

// Opret sample produkter - flere laptops under 10000 DKK på lager
var sampleProducts = new List<Product>
{
    new Product { Id = "1", Name = "Dell XPS 13 Laptop", Price = 8999m, Category = "Electronics", Brand = "Dell", InStock = true },
    new Product { Id = "2", Name = "MacBook Air M2", Price = 12999m, Category = "Electronics", Brand = "Apple", InStock = true },
    new Product { Id = "3", Name = "HP Pavilion Laptop", Price = 6499m, Category = "Electronics", Brand = "HP", InStock = true }, // Ændret til på lager
    new Product { Id = "4", Name = "Lenovo ThinkPad X1", Price = 15999m, Category = "Electronics", Brand = "Lenovo", InStock = true },
    new Product { Id = "5", Name = "ASUS ROG Gaming Laptop", Price = 18999m, Category = "Electronics", Brand = "ASUS", InStock = true },
    new Product { Id = "6", Name = "Samsung Galaxy Tab", Price = 3999m, Category = "Electronics", Brand = "Samsung", InStock = true },
    new Product { Id = "7", Name = "iPad Pro", Price = 9999m, Category = "Electronics", Brand = "Apple", InStock = false },
    new Product { Id = "8", Name = "Microsoft Surface Pro", Price = 11999m, Category = "Electronics", Brand = "Microsoft", InStock = true },
    new Product { Id = "9", Name = "Acer Aspire 5 Laptop", Price = 4999m, Category = "Electronics", Brand = "Acer", InStock = true }, // Tilføjet "Laptop" til navnet
    new Product { Id = "10", Name = "Gaming Chair", Price = 2999m, Category = "Furniture", Brand = "SecretLab", InStock = true },
    new Product { Id = "11", Name = "Standing Desk", Price = 4499m, Category = "Furniture", Brand = "IKEA", InStock = false },
    new Product { Id = "12", Name = "Office Chair", Price = 1999m, Category = "Furniture", Brand = "Herman Miller", InStock = true },
    // Tilføjer flere laptops under 10000 DKK på lager
    new Product { Id = "13", Name = "Lenovo IdeaPad 3 Laptop", Price = 5499m, Category = "Electronics", Brand = "Lenovo", InStock = true },
    new Product { Id = "14", Name = "ASUS VivoBook 15 Laptop", Price = 7999m, Category = "Electronics", Brand = "ASUS", InStock = true },
    new Product { Id = "15", Name = "HP Envy x360 Laptop", Price = 9499m, Category = "Electronics", Brand = "HP", InStock = true },
    new Product { Id = "16", Name = "Dell Inspiron 15 Laptop", Price = 5999m, Category = "Electronics", Brand = "Dell", InStock = true },
    new Product { Id = "17", Name = "Acer Swift 3 Laptop", Price = 7499m, Category = "Electronics", Brand = "Acer", InStock = true },
    new Product { Id = "18", Name = "MSI Modern 14 Laptop", Price = 8499m, Category = "Electronics", Brand = "MSI", InStock = true }
};

// Tilføj produkter til indekset
await productIndex.AddDocumentsAsync(sampleProducts);
Console.WriteLine($"Tilføjede {sampleProducts.Count} produkter til indekset");

// Vent lidt for at sikre indeksering er færdig
await Task.Delay(2000);

// Konfigurer indstillinger - inklusive searchable attributes
await productIndex.UpdateSettingsAsync(new Settings
{
    FilterableAttributes = new[] { "category", "brand", "price", "inStock" },
    SortableAttributes = new[] { "price" },
    SearchableAttributes = new[] { "name", "brand", "category" } // Specificer hvilke felter der søges i
});

Console.WriteLine("Indstillinger opdateret - venter på at de træder i kraft...");
await Task.Delay(2000); // Øget ventetid for at sikre indstillinger er aktiveret

// Søg med facets
var result = await productIndex.SearchAsync<Product>(
    "laptop",
    new SearchQuery
    {
        Facets = new[] { "category", "brand" },
        Filter = "price < 10000 AND inStock = true",
        Sort = new[] { "price:asc" }
    }
);

Console.WriteLine($"\nSøgeresultater for 'laptop' (pris < 10000 og på lager):");
Console.WriteLine($"Fandt {result.Hits.Count()} produkter:");

foreach (var product in result.Hits)
{
    Console.WriteLine($"- {product.Name} ({product.Brand}) - {product.Price:C} DKK");
}

// Debug: Vis alle laptops uanset filter
Console.WriteLine("\n" + new string('-', 40));
Console.WriteLine("DEBUG: Alle produkter der matcher 'laptop':");
var allLaptops = await productIndex.SearchAsync<Product>("laptop");
foreach (var product in allLaptops.Hits)
{
    Console.WriteLine($"- {product.Name} ({product.Brand}) - {product.Price:C} DKK - På lager: {product.InStock}");
}

// Vis facet-information kun hvis der er resultater
if (result.Hits.Any() && result.FacetDistribution?.ContainsKey("category") == true)
{
    Console.WriteLine("\nKategorier:");
    foreach (var facet in result.FacetDistribution["category"])
    {
        Console.WriteLine($"  {facet.Key}: {facet.Value} produkter");
    }
}

if (result.Hits.Any() && result.FacetDistribution?.ContainsKey("brand") == true)
{
    Console.WriteLine("\nBrands:");
    foreach (var facet in result.FacetDistribution["brand"])
    {
        Console.WriteLine($"  {facet.Key}: {facet.Value} produkter");
    }
}

// Lav en bredere søgning for at vise alle produkter
Console.WriteLine("\n" + new string('=', 50));
Console.WriteLine("Alle produkter på lager:");
var allInStockResult = await productIndex.SearchAsync<Product>(
    "",
    new SearchQuery
    {
        Filter = "inStock = true",
        Sort = new[] { "price:asc" },
        Limit = 20
    }
);

foreach (var product in allInStockResult.Hits)
{
    Console.WriteLine($"- {product.Name} ({product.Category}, {product.Brand}) - {product.Price:C} DKK");
}

public class Product
{
    public string? Id { get; set; }
    public string? Name { get; set; }
    public decimal Price { get; set; }
    public string? Category { get; set; }
    public string? Brand { get; set; }
    public bool InStock { get; set; }
}

Fordele ved Meilisearch

  • Nemt at komme i gang: Minimal konfiguration nødvendig
  • Lynhurtig: Optimeret til instant-search med responstider under 50ms
  • Ressource-venlig: Lavt hukommelsesforbrug sammenlignet med alternativer
  • Developer-friendly: Enkel REST API og god dokumentation
  • Open source: Gratis at bruge, aktivt community
  • Typo-tolerance: Håndterer stavefejl automatisk
  • Multi-sprog support: Indbygget support for mange sprog inkl. dansk

Konklusion

Meilisearch er det ideelle valg, når du skal implementere en hurtig og brugervenlig søgefunktion i din applikation. Den kombinerer det bedste fra Elasticsearch’s kraft med Algolia’s enkelhed, og giver udviklere et værktøj, der kan implementeres på minutter frem for dage.

For mere information, besøg Meilisearch dokumentation.