3 releases

Uses new Rust 2024

0.1.4 Jul 21, 2025
0.1.1 May 7, 2025
0.1.0 Oct 4, 2024

#525 in Database interfaces

Download history 19/week @ 2025-06-11 36/week @ 2025-06-18 54/week @ 2025-06-25 31/week @ 2025-07-02 55/week @ 2025-07-09 131/week @ 2025-07-16 95/week @ 2025-07-23 6/week @ 2025-07-30 4/week @ 2025-08-06 62/week @ 2025-08-20 73/week @ 2025-08-27 137/week @ 2025-09-03

273 downloads per month
Used in 24 crates (6 directly)

MPL-2.0 license

295KB
6K SLoC

MoosicBox Search

A high-performance full-text search engine for the MoosicBox music ecosystem. Built on Tantivy, this package provides fast, indexed search across music libraries with support for complex queries, faceted search, and real-time indexing.

Features

  • Full-Text Search: Fast text search across artists, albums, tracks, and metadata
  • Tantivy Integration: Built on Rust's high-performance search library
  • Real-Time Indexing: Automatic index updates as music library changes
  • Faceted Search: Filter results by genre, year, artist, album, and more
  • Fuzzy Matching: Find results even with typos or partial matches
  • Ranking & Scoring: Relevance-based result ordering with customizable scoring
  • Multi-Field Search: Search across multiple metadata fields simultaneously
  • Async Operations: Non-blocking search operations with Tokio
  • API Integration: RESTful API endpoints for web applications
  • Index Management: Efficient index building, updating, and optimization

Installation

Add this to your Cargo.toml:

[dependencies]
moosicbox_search = "0.1.1"

Usage

use moosicbox_search::{SearchEngine, SearchQuery, SearchResults};

// Initialize search engine
let search_engine = SearchEngine::new("/path/to/index")?;

// Perform simple text search
let query = SearchQuery::new("Pink Floyd");
let results = search_engine.search(&query).await?;

// Process results
for hit in results.hits {
    println!("Found: {} - {} (score: {})",
             hit.artist, hit.title, hit.score);
}

Advanced Search Queries

use moosicbox_search::{SearchQuery, SearchFilter, SortBy};

// Complex search with filters and sorting
let query = SearchQuery::new("rock")
    .with_filter(SearchFilter::Genre("progressive rock".to_string()))
    .with_filter(SearchFilter::YearRange(1970, 1980))
    .with_sort(SortBy::Relevance)
    .with_limit(50)
    .with_offset(0);

let results = search_engine.search(&query).await?;
use moosicbox_search::{FacetQuery, FacetResults};

// Get facet counts for filtering
let facet_query = FacetQuery::new()
    .with_field("genre")
    .with_field("year")
    .with_field("artist");

let facets = search_engine.get_facets(&facet_query).await?;

// Display facet options
for (genre, count) in facets.genres {
    println!("{}: {} tracks", genre, count);
}

Real-Time Indexing

use moosicbox_search::{IndexWriter, Document};

// Add new documents to index
let mut writer = search_engine.get_writer()?;

let doc = Document::new()
    .with_field("title", "Bohemian Rhapsody")
    .with_field("artist", "Queen")
    .with_field("album", "A Night at the Opera")
    .with_field("year", 1975)
    .with_field("genre", "rock");

writer.add_document(doc)?;
writer.commit()?;

Search with Highlighting

use moosicbox_search::{SearchQuery, HighlightOptions};

let query = SearchQuery::new("bohemian")
    .with_highlight(HighlightOptions::new()
        .with_pre_tag("<mark>")
        .with_post_tag("</mark>")
        .with_max_fragments(3));

let results = search_engine.search(&query).await?;

for hit in results.hits {
    if let Some(highlights) = hit.highlights {
        println!("Title: {}", highlights.title.unwrap_or_default());
    }
}

Programming Interface

Core Types

pub struct SearchEngine {
    index: Index,
    reader: IndexReader,
    schema: Schema,
}

impl SearchEngine {
    pub fn new<P: AsRef<Path>>(index_path: P) -> Result<Self, SearchError>;
    pub async fn search(&self, query: &SearchQuery) -> Result<SearchResults, SearchError>;
    pub async fn get_facets(&self, query: &FacetQuery) -> Result<FacetResults, SearchError>;
    pub fn get_writer(&self) -> Result<IndexWriter, SearchError>;
    pub async fn rebuild_index(&self) -> Result<(), SearchError>;
}

#[derive(Debug, Clone)]
pub struct SearchQuery {
    pub text: String,
    pub filters: Vec<SearchFilter>,
    pub sort: Option<SortBy>,
    pub limit: Option<usize>,
    pub offset: Option<usize>,
    pub highlight: Option<HighlightOptions>,
}

#[derive(Debug, Clone)]
pub struct SearchResults {
    pub hits: Vec<SearchHit>,
    pub total_count: usize,
    pub query_time_ms: u64,
    pub facets: Option<FacetResults>,
}

Search Filters

#[derive(Debug, Clone)]
pub enum SearchFilter {
    Artist(String),
    Album(String),
    Genre(String),
    Year(i32),
    YearRange(i32, i32),
    Duration(std::time::Duration),
    DurationRange(std::time::Duration, std::time::Duration),
    HasLyrics(bool),
    Rating(f32),
    RatingRange(f32, f32),
}

#[derive(Debug, Clone)]
pub enum SortBy {
    Relevance,
    Title,
    Artist,
    Album,
    Year,
    Duration,
    Rating,
    DateAdded,
}

Document Management

#[derive(Debug, Clone)]
pub struct Document {
    fields: HashMap<String, FieldValue>,
}

impl Document {
    pub fn new() -> Self;
    pub fn with_field<T: Into<FieldValue>>(mut self, name: &str, value: T) -> Self;
    pub fn get_field(&self, name: &str) -> Option<&FieldValue>;
}

#[derive(Debug, Clone)]
pub enum FieldValue {
    Text(String),
    Integer(i64),
    Float(f64),
    Date(DateTime<Utc>),
    Boolean(bool),
}

Configuration

Environment Variables

  • SEARCH_INDEX_PATH: Path to search index directory (default: ./search_index)
  • SEARCH_INDEX_MEMORY_MB: Memory budget for indexing in MB (default: 128)
  • SEARCH_WRITER_THREADS: Number of indexing threads (default: 4)
  • SEARCH_COMMIT_INTERVAL_SEC: Auto-commit interval in seconds (default: 30)

Index Schema Configuration

use moosicbox_search::{SchemaBuilder, FieldType};

let schema = SchemaBuilder::new()
    .add_text_field("title", FieldType::Text { stored: true, indexed: true })
    .add_text_field("artist", FieldType::Text { stored: true, indexed: true })
    .add_text_field("album", FieldType::Text { stored: true, indexed: true })
    .add_facet_field("genre", FieldType::Facet)
    .add_integer_field("year", FieldType::Integer { stored: true, indexed: true })
    .add_float_field("duration", FieldType::Float { stored: true, indexed: true })
    .build()?;

Web API Endpoints

When the api feature is enabled:

GET    /search?q={query}&limit={limit}&offset={offset}
GET    /search/facets?fields={fields}
POST   /search/advanced
GET    /search/suggest?q={query}
POST   /index/rebuild
GET    /index/stats

API Usage Examples

# Simple search
curl "http://localhost:8000/search?q=pink%20floyd&limit=10"

# Advanced search with filters
curl -X POST http://localhost:8000/search/advanced \
  -H "Content-Type: application/json" \
  -d '{
    "text": "rock",
    "filters": [
      {"Genre": "progressive rock"},
      {"YearRange": [1970, 1980]}
    ],
    "sort": "Relevance",
    "limit": 20
  }'

# Get search facets
curl "http://localhost:8000/search/facets?fields=genre,artist,year"

Performance Optimization

Index Tuning

use moosicbox_search::{IndexSettings, CompressionType};

let settings = IndexSettings::new()
    .with_memory_budget_mb(256)
    .with_compression(CompressionType::Lz4)
    .with_merge_policy_max_segments(10)
    .with_commit_interval_sec(60);

let search_engine = SearchEngine::with_settings("/path/to/index", settings)?;

Query Optimization

// Use specific field searches for better performance
let query = SearchQuery::new("")
    .with_field_query("artist", "Pink Floyd")  // Faster than full-text
    .with_field_query("album", "Dark Side");

// Limit result size for pagination
let query = SearchQuery::new("rock")
    .with_limit(20)  // Don't fetch more than needed
    .with_offset(0);

// Use filters instead of text search when possible
let query = SearchQuery::new("")
    .with_filter(SearchFilter::Genre("rock".to_string()))  // Faster
    .with_filter(SearchFilter::YearRange(1970, 1980));

Index Management

Building Initial Index

use moosicbox_search::{IndexBuilder, ProgressCallback};

let builder = IndexBuilder::new("/path/to/index");

// Build from music library
let progress = |indexed: usize, total: usize| {
    println!("Indexed {} of {} tracks", indexed, total);
};

builder.build_from_library("/path/to/music", Some(progress)).await?;

Index Maintenance

// Optimize index (merge segments)
search_engine.optimize().await?;

// Get index statistics
let stats = search_engine.get_stats().await?;
println!("Index size: {} MB", stats.size_mb);
println!("Document count: {}", stats.document_count);
println!("Segments: {}", stats.segment_count);

// Rebuild index from scratch
search_engine.rebuild_index().await?;

Testing

# Run all tests
cargo test

# Run with specific features
cargo test --features "api,db"

# Run performance benchmarks
cargo bench

# Test with sample data
cargo test --test integration -- --ignored

Error Handling

use moosicbox_search::SearchError;

match search_engine.search(&query).await {
    Ok(results) => {
        println!("Found {} results", results.total_count);
    }
    Err(SearchError::IndexNotFound) => {
        eprintln!("Search index not found. Run index rebuild.");
    }
    Err(SearchError::QueryParseError(msg)) => {
        eprintln!("Invalid query: {}", msg);
    }
    Err(SearchError::IndexCorrupted) => {
        eprintln!("Index corrupted. Rebuilding required.");
        search_engine.rebuild_index().await?;
    }
    Err(e) => eprintln!("Search error: {}", e),
}

Troubleshooting

Common Issues

Index Not Found

  • Ensure index directory exists and is readable
  • Run initial index build if this is first use
  • Check file permissions on index directory

Poor Search Performance

  • Increase memory budget for indexing
  • Optimize index to reduce segment count
  • Use more specific search queries
  • Consider index warming strategies

Out of Memory During Indexing

  • Reduce memory budget in settings
  • Process library in smaller batches
  • Increase system swap space
  • Use streaming indexing for large libraries

Index Corruption

  • Enable regular index backups
  • Use atomic commits
  • Check disk space and file system health
  • Rebuild index from source data

See Also

Dependencies

~36–50MB
~836K SLoC