pelisearch 0.1.0

Official Rust SDK for PeliSearch
Documentation
use pelisearch::{Client, SearchRequest};
use std::collections::HashMap;
use std::env;
use std::process::{Child, Command};
use std::time::{Duration, Instant};

fn test_url() -> String {
    env::var("PELISEARCH_TEST_URL").unwrap_or_else(|_| "http://127.0.0.1:7700".into())
}

async fn setup_client() -> Client {
    let client = Client::from_url(&test_url()).expect("client");
    client.health().await.expect("health");
    client
}

async fn reset_index(client: &Client, name: &str) {
    let _ = client.delete_index(name).await;
    client.create_index(name).await.unwrap();
}

#[tokio::test]
async fn index_management() {
    let client = setup_client().await;
    let index = "sdk_rs_index_mgmt";
    reset_index(&client, index).await;

    let indexes = client.list_indexes().await.unwrap();
    assert!(indexes.iter().any(|n| n == index));

    let info = client.get_index(index).await.unwrap();
    assert_eq!(info.name, index);

    client.delete_index(index).await.unwrap();
}

#[tokio::test]
async fn document_operations() {
    let client = setup_client().await;
    let index = "sdk_rs_docs";
    reset_index(&client, index).await;

    client
        .add_document(
            index,
            "d1",
            HashMap::from([
                ("title".into(), "Mouse".into()),
                ("category".into(), "electronics".into()),
            ]),
        )
        .await
        .unwrap();

    let doc = client.get_document(index, "d1").await.unwrap();
    assert!(doc.contains_key("title") || doc.get("fields").is_some());

    client.delete_document(index, "d1").await.unwrap();
    client.delete_index(index).await.unwrap();
}

#[tokio::test]
async fn search_operations() {
    let client = setup_client().await;
    let index = "sdk_rs_search";
    reset_index(&client, index).await;
    client
        .add_document(
            index,
            "p1",
            HashMap::from([
                ("title".into(), "Wireless Mouse".into()),
                ("category".into(), "electronics".into()),
            ]),
        )
        .await
        .unwrap();

    let results = client
        .search(
            index,
            &SearchRequest {
                q: Some("mouse".into()),
                ..Default::default()
            },
        )
        .await
        .unwrap();
    assert!(!results.hits.is_empty());
    assert!(results.total > 0);
    for hit in &results.hits {
        assert_eq!(hit.index, index);
        assert!(!hit.document_id.is_empty());
    }

    client.delete_index(index).await.unwrap();
}

#[tokio::test]
async fn recovery_persistence() {
    let data_dir = tempfile::tempdir().unwrap();
    let data_path = data_dir.path().to_str().unwrap();
    let port = pick_port();

    let mut child = start_server(port, data_path);
    let url = format!("http://127.0.0.1:{port}");
    wait_for_health(&url).await;

    let client = Client::from_url(&url).unwrap();
    client.create_index("recipes").await.unwrap();
    client
        .add_document(
            "recipes",
            "r1",
            HashMap::from([("title".into(), "Pancakes".into())]),
        )
        .await
        .unwrap();

    stop_server(&mut child);

    let mut child2 = start_server(port, data_path);
    wait_for_health(&url).await;
    let client2 = Client::from_url(&url).unwrap();

    let indexes = client2.list_indexes().await.unwrap();
    assert!(indexes.contains(&"recipes".into()));

    let results = client2
        .search(
            "recipes",
            &SearchRequest {
                q: Some("pancakes".into()),
                ..Default::default()
            },
        )
        .await
        .unwrap();
    assert_eq!(results.hits.len(), 1);

    stop_server(&mut child2);
}

fn pick_port() -> u16 {
    std::net::TcpListener::bind("127.0.0.1:0")
        .unwrap()
        .local_addr()
        .unwrap()
        .port()
}

fn start_server(port: u16, data_path: &str) -> Child {
    let mut bin = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    bin.push("../../target/debug/pelisearch-server");
    Command::new(&bin)
        .arg("--port")
        .arg(port.to_string())
        .arg("--data-path")
        .arg(data_path)
        .spawn()
        .expect("start server")
}

async fn wait_for_health(base_url: &str) {
    let client = reqwest::Client::new();
    let start = Instant::now();
    while start.elapsed() < Duration::from_secs(10) {
        if client
            .get(format!("{base_url}/health"))
            .send()
            .await
            .map(|r| r.status().is_success())
            .unwrap_or(false)
        {
            return;
        }
        tokio::time::sleep(Duration::from_millis(100)).await;
    }
    panic!("server not ready");
}

fn stop_server(child: &mut Child) {
    let _ = child.kill();
    let _ = child.wait();
}