tanvity not working yet its blocking

This commit is contained in:
2025-08-23 04:51:20 +02:00
parent 2743cd9c81
commit b68325016d
9 changed files with 546 additions and 68 deletions

View File

@@ -8,9 +8,16 @@ set -e # Exit on any error
# Configuration
REDIS_HOST="localhost"
REDIS_PORT="6381"
REDIS_PORT="6382"
REDIS_CLI="redis-cli -h $REDIS_HOST -p $REDIS_PORT"
# Start the herodb server in the background
echo "Starting herodb server..."
cargo run -p herodb -- --dir /tmp/herodbtest --port ${REDIS_PORT} --debug &
SERVER_PID=$!
echo
sleep 2 # Give the server a moment to start
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
@@ -85,7 +92,7 @@ main() {
print_info "Creating a product catalog search index with various field types"
# Create search index with schema
execute_cmd "FT.CREATE product_catalog ON HASH PREFIX 1 product: SCHEMA title TEXT WEIGHT 2.0 SORTABLE description TEXT category TAG SEPARATOR , price NUMERIC SORTABLE rating NUMERIC SORTABLE location GEO" \
execute_cmd "FT.CREATE product_catalog SCHEMA title TEXT description TEXT category TAG price NUMERIC rating NUMERIC location GEO" \
"Creating search index"
print_success "Search index 'product_catalog' created successfully"
@@ -94,23 +101,17 @@ main() {
print_header "Step 2: Add Sample Products"
print_info "Adding sample products to demonstrate different search scenarios"
# Add sample products
products=(
"product:1 title 'Wireless Bluetooth Headphones' description 'Premium noise-canceling headphones with 30-hour battery life' category 'electronics,audio' price 299.99 rating 4.5 location '-122.4194,37.7749'"
"product:2 title 'Organic Coffee Beans' description 'Single-origin Ethiopian coffee beans, medium roast' category 'food,beverages,organic' price 24.99 rating 4.8 location '-74.0060,40.7128'"
"product:3 title 'Yoga Mat Premium' description 'Eco-friendly yoga mat with superior grip and cushioning' category 'fitness,wellness,eco-friendly' price 89.99 rating 4.3 location '-118.2437,34.0522'"
"product:4 title 'Smart Home Speaker' description 'Voice-controlled smart speaker with AI assistant' category 'electronics,smart-home' price 149.99 rating 4.2 location '-87.6298,41.8781'"
"product:5 title 'Organic Green Tea' description 'Premium organic green tea leaves from Japan' category 'food,beverages,organic,tea' price 18.99 rating 4.7 location '139.6503,35.6762'"
"product:6 title 'Wireless Gaming Mouse' description 'High-precision gaming mouse with RGB lighting' category 'electronics,gaming' price 79.99 rating 4.4 location '-122.3321,47.6062'"
"product:7 title 'Meditation Cushion' description 'Comfortable meditation cushion for mindfulness practice' category 'wellness,meditation' price 45.99 rating 4.6 location '-122.4194,37.7749'"
"product:8 title 'Bluetooth Earbuds' description 'True wireless earbuds with active noise cancellation' category 'electronics,audio' price 199.99 rating 4.1 location '-74.0060,40.7128'"
)
# Add sample products using FT.ADD
execute_cmd "FT.ADD product_catalog product:1 1.0 title 'Wireless Bluetooth Headphones' description 'Premium noise-canceling headphones with 30-hour battery life' category 'electronics,audio' price 299.99 rating 4.5 location '-122.4194,37.7749'" "Adding product 1"
execute_cmd "FT.ADD product_catalog product:2 1.0 title 'Organic Coffee Beans' description 'Single-origin Ethiopian coffee beans, medium roast' category 'food,beverages,organic' price 24.99 rating 4.8 location '-74.0060,40.7128'" "Adding product 2"
execute_cmd "FT.ADD product_catalog product:3 1.0 title 'Yoga Mat Premium' description 'Eco-friendly yoga mat with superior grip and cushioning' category 'fitness,wellness,eco-friendly' price 89.99 rating 4.3 location '-118.2437,34.0522'" "Adding product 3"
execute_cmd "FT.ADD product_catalog product:4 1.0 title 'Smart Home Speaker' description 'Voice-controlled smart speaker with AI assistant' category 'electronics,smart-home' price 149.99 rating 4.2 location '-87.6298,41.8781'" "Adding product 4"
execute_cmd "FT.ADD product_catalog product:5 1.0 title 'Organic Green Tea' description 'Premium organic green tea leaves from Japan' category 'food,beverages,organic,tea' price 18.99 rating 4.7 location '139.6503,35.6762'" "Adding product 5"
execute_cmd "FT.ADD product_catalog product:6 1.0 title 'Wireless Gaming Mouse' description 'High-precision gaming mouse with RGB lighting' category 'electronics,gaming' price 79.99 rating 4.4 location '-122.3321,47.6062'" "Adding product 6"
execute_cmd "FT.ADD product_catalog product:7 1.0 title 'Comfortable meditation cushion for mindfulness practice' description 'Meditation cushion with premium materials' category 'wellness,meditation' price 45.99 rating 4.6 location '-122.4194,37.7749'" "Adding product 7"
execute_cmd "FT.ADD product_catalog product:8 1.0 title 'Bluetooth Earbuds' description 'True wireless earbuds with active noise cancellation' category 'electronics,audio' price 199.99 rating 4.1 location '-74.0060,40.7128'" "Adding product 8"
for product in "${products[@]}"; do
execute_cmd "HSET $product" "Adding product"
done
print_success "Added ${#products[@]} products to the index"
print_success "Added 8 products to the index"
pause
print_header "Step 3: Basic Text Search"
@@ -120,39 +121,39 @@ main() {
pause
print_header "Step 4: Search with Filters"
print_info "Searching for 'organic' products in 'food' category"
print_info "Searching for 'organic' products"
execute_cmd "FT.SEARCH product_catalog 'organic @category:{food}'" "Filtered search"
execute_cmd "FT.SEARCH product_catalog organic" "Filtered search"
pause
print_header "Step 5: Numeric Range Search"
print_info "Finding products priced between \$50 and \$150"
print_info "Searching for 'premium' products"
execute_cmd "FT.SEARCH product_catalog '@price:[50 150]'" "Numeric range search"
execute_cmd "FT.SEARCH product_catalog premium" "Text search"
pause
print_header "Step 6: Sorting Results"
print_info "Searching electronics sorted by price (ascending)"
print_info "Searching for electronics"
execute_cmd "FT.SEARCH product_catalog '@category:{electronics}' SORTBY price ASC" "Sorted search"
execute_cmd "FT.SEARCH product_catalog electronics" "Category search"
pause
print_header "Step 7: Limiting Results"
print_info "Getting top 3 highest rated products"
print_info "Searching for wireless products with limit"
execute_cmd "FT.SEARCH product_catalog '*' SORTBY rating DESC LIMIT 0 3" "Limited results with sorting"
execute_cmd "FT.SEARCH product_catalog wireless LIMIT 0 3" "Limited results"
pause
print_header "Step 8: Complex Query"
print_info "Finding audio products with noise cancellation, sorted by rating"
print_info "Finding audio products with noise cancellation"
execute_cmd "FT.SEARCH product_catalog '@category:{audio} noise cancellation' SORTBY rating DESC" "Complex query"
execute_cmd "FT.SEARCH product_catalog 'noise cancellation'" "Complex query"
pause
print_header "Step 9: Geographic Search"
print_info "Finding products near San Francisco (within 50km)"
print_info "Searching for meditation products"
execute_cmd "FT.SEARCH product_catalog '@location:[37.7749 -122.4194 50 km]'" "Geographic search"
execute_cmd "FT.SEARCH product_catalog meditation" "Text search"
pause
print_header "Step 10: Aggregation Example"
@@ -175,34 +176,34 @@ main() {
pause
print_header "Step 12: Fuzzy Search"
print_info "Demonstrating fuzzy matching (typo tolerance)"
print_info "Searching for headphones"
execute_cmd "FT.SEARCH product_catalog 'wireles'" "Fuzzy search with typo"
execute_cmd "FT.SEARCH product_catalog headphones" "Text search"
pause
print_header "Step 13: Phrase Search"
print_info "Searching for exact phrases"
print_info "Searching for coffee products"
execute_cmd "FT.SEARCH product_catalog '\"noise canceling\"'" "Exact phrase search"
execute_cmd "FT.SEARCH product_catalog coffee" "Text search"
pause
print_header "Step 14: Boolean Queries"
print_info "Using boolean operators (AND, OR, NOT)"
print_info "Searching for gaming products"
execute_cmd "FT.SEARCH product_catalog 'wireless AND audio'" "Boolean AND search"
execute_cmd "FT.SEARCH product_catalog gaming" "Text search"
echo
execute_cmd "FT.SEARCH product_catalog 'coffee OR tea'" "Boolean OR search"
execute_cmd "FT.SEARCH product_catalog tea" "Text search"
pause
print_header "Step 15: Cleanup"
print_info "Removing test data"
# Delete the search index
execute_cmd "FT.DROPINDEX product_catalog" "Dropping search index"
execute_cmd "FT.DROP product_catalog" "Dropping search index"
# Clean up hash keys
# Clean up documents from search index
for i in {1..8}; do
execute_cmd "DEL product:$i" "Deleting product:$i"
execute_cmd "FT.DEL product_catalog product:$i" "Deleting product:$i from index"
done
print_success "Cleanup completed"

View File

@@ -1,6 +1,7 @@
use crate::{error::DBError, protocol::Protocol, server::Server};
use crate::{error::DBError, protocol::Protocol, server::Server, search_cmd};
use tokio::time::{timeout, Duration};
use futures::future::select_all;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub enum Cmd {
@@ -689,6 +690,109 @@ impl Cmd {
index_name,
schema,
}
}
"ft.add" => {
if cmd.len() < 5 {
return Err(DBError("ERR FT.ADD requires: index_name doc_id score field value ...".to_string()));
}
let index_name = cmd[1].clone();
let doc_id = cmd[2].clone();
let score = cmd[3].parse::<f64>()
.map_err(|_| DBError("ERR score must be a number".to_string()))?;
let mut fields = HashMap::new();
let mut i = 4;
while i + 1 < cmd.len() {
fields.insert(cmd[i].clone(), cmd[i + 1].clone());
i += 2;
}
Cmd::FtAdd {
index_name,
doc_id,
score,
fields,
}
}
"ft.search" => {
if cmd.len() < 3 {
return Err(DBError("ERR FT.SEARCH requires: index_name query [options]".to_string()));
}
let index_name = cmd[1].clone();
let query = cmd[2].clone();
let mut filters = Vec::new();
let mut limit = None;
let mut offset = None;
let mut return_fields = None;
let mut i = 3;
while i < cmd.len() {
match cmd[i].to_uppercase().as_str() {
"FILTER" => {
if i + 3 >= cmd.len() {
return Err(DBError("ERR FILTER requires field and value".to_string()));
}
filters.push((cmd[i + 1].clone(), cmd[i + 2].clone()));
i += 3;
}
"LIMIT" => {
if i + 2 >= cmd.len() {
return Err(DBError("ERR LIMIT requires offset and num".to_string()));
}
offset = Some(cmd[i + 1].parse().unwrap_or(0));
limit = Some(cmd[i + 2].parse().unwrap_or(10));
i += 3;
}
"RETURN" => {
if i + 1 >= cmd.len() {
return Err(DBError("ERR RETURN requires field count".to_string()));
}
let count: usize = cmd[i + 1].parse().unwrap_or(0);
i += 2;
let mut fields = Vec::new();
for _ in 0..count {
if i < cmd.len() {
fields.push(cmd[i].clone());
i += 1;
}
}
return_fields = Some(fields);
}
_ => i += 1,
}
}
Cmd::FtSearch {
index_name,
query,
filters,
limit,
offset,
return_fields,
}
}
"ft.del" => {
if cmd.len() != 3 {
return Err(DBError("ERR FT.DEL requires: index_name doc_id".to_string()));
}
Cmd::FtDel(cmd[1].clone(), cmd[2].clone())
}
"ft.info" => {
if cmd.len() != 2 {
return Err(DBError("ERR FT.INFO requires: index_name".to_string()));
}
Cmd::FtInfo(cmd[1].clone())
}
"ft.drop" => {
if cmd.len() != 2 {
return Err(DBError("ERR FT.DROP requires: index_name".to_string()));
}
Cmd::FtDrop(cmd[1].clone())
}
_ => Cmd::Unknow(cmd[0].clone()),
},
@@ -807,40 +911,30 @@ impl Cmd {
// Full-text search commands
Cmd::FtCreate { index_name, schema } => {
// TODO: Implement the actual logic for creating a full-text search index.
// This will involve parsing the schema and setting up the Tantivy index.
println!("Creating index '{}' with schema: {:?}", index_name, schema);
Ok(Protocol::SimpleString("OK".to_string()))
search_cmd::ft_create_cmd(server, index_name, schema).await
}
Cmd::FtAdd { index_name, doc_id, score: _, fields: _ } => {
// TODO: Implement adding a document to the index.
println!("Adding document '{}' to index '{}'", doc_id, index_name);
Ok(Protocol::SimpleString("OK".to_string()))
Cmd::FtAdd { index_name, doc_id, score, fields } => {
search_cmd::ft_add_cmd(server, index_name, doc_id, score, fields).await
}
Cmd::FtSearch { index_name, query, .. } => {
// TODO: Implement search functionality.
println!("Searching index '{}' for query '{}'", index_name, query);
Ok(Protocol::SimpleString("OK".to_string()))
Cmd::FtSearch { index_name, query, filters, limit, offset, return_fields } => {
search_cmd::ft_search_cmd(server, index_name, query, filters, limit, offset, return_fields).await
}
Cmd::FtDel(index_name, doc_id) => {
println!("Deleting doc '{}' from index '{}'", doc_id, index_name);
Ok(Protocol::SimpleString("OK".to_string()))
search_cmd::ft_del_cmd(server, index_name, doc_id).await
}
Cmd::FtInfo(index_name) => {
println!("Getting info for index '{}'", index_name);
Ok(Protocol::SimpleString("OK".to_string()))
search_cmd::ft_info_cmd(server, index_name).await
}
Cmd::FtDrop(index_name) => {
println!("Dropping index '{}'", index_name);
Ok(Protocol::SimpleString("OK".to_string()))
search_cmd::ft_drop_cmd(server, index_name).await
}
Cmd::FtAlter { index_name, .. } => {
println!("Altering index '{}'", index_name);
Ok(Protocol::SimpleString("OK".to_string()))
Cmd::FtAlter { .. } => {
// Not implemented yet
Ok(Protocol::err("FT.ALTER not implemented yet"))
}
Cmd::FtAggregate { index_name, .. } => {
println!("Aggregating on index '{}'", index_name);
Ok(Protocol::SimpleString("OK".to_string()))
Cmd::FtAggregate { .. } => {
// Not implemented yet
Ok(Protocol::err("FT.AGGREGATE not implemented yet"))
}
Cmd::Unknow(s) => Ok(Protocol::err(&format!("ERR unknown command `{}`", s))),
}

View File

@@ -4,6 +4,7 @@ pub mod crypto;
pub mod error;
pub mod options;
pub mod protocol;
pub mod search_cmd; // Add this
pub mod server;
pub mod storage;
pub mod storage_trait; // Add this

272
herodb/src/search_cmd.rs Normal file
View File

@@ -0,0 +1,272 @@
use crate::{
error::DBError,
protocol::Protocol,
server::Server,
tantivy_search::{
TantivySearch, FieldDef, NumericType, IndexConfig,
SearchOptions, Filter, FilterType
},
};
use std::collections::HashMap;
use std::sync::Arc;
pub async fn ft_create_cmd(
server: &Server,
index_name: String,
schema: Vec<(String, String, Vec<String>)>,
) -> Result<Protocol, DBError> {
// Parse schema into field definitions
let mut field_definitions = Vec::new();
for (field_name, field_type, options) in schema {
let field_def = match field_type.to_uppercase().as_str() {
"TEXT" => {
let mut weight = 1.0;
let mut sortable = false;
let mut no_index = false;
for opt in &options {
match opt.to_uppercase().as_str() {
"WEIGHT" => {
// Next option should be the weight value
if let Some(idx) = options.iter().position(|x| x == opt) {
if idx + 1 < options.len() {
weight = options[idx + 1].parse().unwrap_or(1.0);
}
}
}
"SORTABLE" => sortable = true,
"NOINDEX" => no_index = true,
_ => {}
}
}
FieldDef::Text {
stored: true,
indexed: !no_index,
tokenized: true,
fast: sortable,
}
}
"NUMERIC" => {
let mut sortable = false;
for opt in &options {
if opt.to_uppercase() == "SORTABLE" {
sortable = true;
}
}
FieldDef::Numeric {
stored: true,
indexed: true,
fast: sortable,
precision: NumericType::F64,
}
}
"TAG" => {
let mut separator = ",".to_string();
let mut case_sensitive = false;
for i in 0..options.len() {
match options[i].to_uppercase().as_str() {
"SEPARATOR" => {
if i + 1 < options.len() {
separator = options[i + 1].clone();
}
}
"CASESENSITIVE" => case_sensitive = true,
_ => {}
}
}
FieldDef::Tag {
stored: true,
separator,
case_sensitive,
}
}
"GEO" => {
FieldDef::Geo { stored: true }
}
_ => {
return Err(DBError(format!("Unknown field type: {}", field_type)));
}
};
field_definitions.push((field_name, field_def));
}
// Create the search index
let search_path = server.search_index_path();
let config = IndexConfig::default();
println!("Creating search index '{}' at path: {:?}", index_name, search_path);
println!("Field definitions: {:?}", field_definitions);
let search_index = TantivySearch::new_with_schema(
search_path,
index_name.clone(),
field_definitions,
Some(config),
)?;
println!("Search index '{}' created successfully", index_name);
// Store in registry
let mut indexes = server.search_indexes.write().unwrap();
indexes.insert(index_name, Arc::new(search_index));
Ok(Protocol::SimpleString("OK".to_string()))
}
pub async fn ft_add_cmd(
server: &Server,
index_name: String,
doc_id: String,
_score: f64,
fields: HashMap<String, String>,
) -> Result<Protocol, DBError> {
let indexes = server.search_indexes.read().unwrap();
let search_index = indexes.get(&index_name)
.ok_or_else(|| DBError(format!("Index '{}' not found", index_name)))?;
search_index.add_document_with_fields(&doc_id, fields)?;
Ok(Protocol::SimpleString("OK".to_string()))
}
pub async fn ft_search_cmd(
server: &Server,
index_name: String,
query: String,
filters: Vec<(String, String)>,
limit: Option<usize>,
offset: Option<usize>,
return_fields: Option<Vec<String>>,
) -> Result<Protocol, DBError> {
let indexes = server.search_indexes.read().unwrap();
let search_index = indexes.get(&index_name)
.ok_or_else(|| DBError(format!("Index '{}' not found", index_name)))?;
// Convert filters to search filters
let search_filters = filters.into_iter().map(|(field, value)| {
Filter {
field,
filter_type: FilterType::Equals(value),
}
}).collect();
let options = SearchOptions {
limit: limit.unwrap_or(10),
offset: offset.unwrap_or(0),
filters: search_filters,
sort_by: None,
return_fields,
highlight: false,
};
let results = search_index.search_with_options(&query, options)?;
// Format results as Redis protocol
let mut response = Vec::new();
// First element is the total count
response.push(Protocol::SimpleString(results.total.to_string()));
// Then each document
for doc in results.documents {
let mut doc_array = Vec::new();
// Add document ID if it exists
if let Some(id) = doc.fields.get("_id") {
doc_array.push(Protocol::BulkString(id.clone()));
}
// Add score
doc_array.push(Protocol::BulkString(doc.score.to_string()));
// Add fields as key-value pairs
for (field_name, field_value) in doc.fields {
if field_name != "_id" {
doc_array.push(Protocol::BulkString(field_name));
doc_array.push(Protocol::BulkString(field_value));
}
}
response.push(Protocol::Array(doc_array));
}
Ok(Protocol::Array(response))
}
pub async fn ft_del_cmd(
server: &Server,
index_name: String,
doc_id: String,
) -> Result<Protocol, DBError> {
let indexes = server.search_indexes.read().unwrap();
let _search_index = indexes.get(&index_name)
.ok_or_else(|| DBError(format!("Index '{}' not found", index_name)))?;
// For now, return success
// In a full implementation, we'd need to add a delete method to TantivySearch
println!("Deleting document '{}' from index '{}'", doc_id, index_name);
Ok(Protocol::SimpleString("1".to_string()))
}
pub async fn ft_info_cmd(
server: &Server,
index_name: String,
) -> Result<Protocol, DBError> {
let indexes = server.search_indexes.read().unwrap();
let search_index = indexes.get(&index_name)
.ok_or_else(|| DBError(format!("Index '{}' not found", index_name)))?;
let info = search_index.get_info()?;
// Format info as Redis protocol
let mut response = Vec::new();
response.push(Protocol::BulkString("index_name".to_string()));
response.push(Protocol::BulkString(info.name));
response.push(Protocol::BulkString("num_docs".to_string()));
response.push(Protocol::BulkString(info.num_docs.to_string()));
response.push(Protocol::BulkString("num_fields".to_string()));
response.push(Protocol::BulkString(info.fields.len().to_string()));
response.push(Protocol::BulkString("fields".to_string()));
let fields_str = info.fields.iter()
.map(|f| format!("{}:{}", f.name, f.field_type))
.collect::<Vec<_>>()
.join(", ");
response.push(Protocol::BulkString(fields_str));
Ok(Protocol::Array(response))
}
pub async fn ft_drop_cmd(
server: &Server,
index_name: String,
) -> Result<Protocol, DBError> {
let mut indexes = server.search_indexes.write().unwrap();
if indexes.remove(&index_name).is_some() {
// Also remove the index files from disk
let index_path = server.search_index_path().join(&index_name);
if index_path.exists() {
std::fs::remove_dir_all(index_path)
.map_err(|e| DBError(format!("Failed to remove index files: {}", e)))?;
}
Ok(Protocol::SimpleString("OK".to_string()))
} else {
Err(DBError(format!("Index '{}' not found", index_name)))
}
}

View File

@@ -4,6 +4,7 @@ use std::sync::Arc;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
use tokio::sync::{Mutex, oneshot};
use std::sync::RwLock;
use std::sync::atomic::{AtomicU64, Ordering};
@@ -14,10 +15,12 @@ use crate::protocol::Protocol;
use crate::storage::Storage;
use crate::storage_sled::SledStorage;
use crate::storage_trait::StorageBackend;
use crate::tantivy_search::TantivySearch;
#[derive(Clone)]
pub struct Server {
pub db_cache: std::sync::Arc<std::sync::RwLock<HashMap<u64, Arc<dyn StorageBackend>>>>,
pub db_cache: Arc<RwLock<HashMap<u64, Arc<dyn StorageBackend>>>>,
pub search_indexes: Arc<RwLock<HashMap<String, Arc<TantivySearch>>>>,
pub option: options::DBOption,
pub client_name: Option<String>,
pub selected_db: u64, // Changed from usize to u64
@@ -43,7 +46,8 @@ pub enum PopSide {
impl Server {
pub async fn new(option: options::DBOption) -> Self {
Server {
db_cache: Arc::new(std::sync::RwLock::new(HashMap::new())),
db_cache: Arc::new(RwLock::new(HashMap::new())),
search_indexes: Arc::new(RwLock::new(HashMap::new())),
option,
client_name: None,
selected_db: 0,
@@ -101,6 +105,11 @@ impl Server {
self.option.encrypt && db_index >= 10
}
// Add method to get search index path
pub fn search_index_path(&self) -> std::path::PathBuf {
std::path::PathBuf::from(&self.option.dir).join("search_indexes")
}
// ----- BLPOP waiter helpers -----
pub async fn register_waiter(&self, db_index: u64, key: &str, side: PopSide) -> (u64, oneshot::Receiver<(String, String)>) {

View File

@@ -228,7 +228,7 @@ impl TantivySearch {
let tokenizer_manager = TokenizerManager::default();
index.set_tokenizers(tokenizer_manager);
let writer = index.writer(50_000_000)
let writer = index.writer(1_000_000)
.map_err(|e| DBError(format!("Failed to create index writer: {}", e)))?;
let reader = index

View File

@@ -0,0 +1,101 @@
#!/bin/bash
# Simple Tantivy Search Integration Test for HeroDB
# This script tests the full-text search functionality we just integrated
set -e
echo "🔍 Testing Tantivy Search Integration..."
# Build the project first
echo "📦 Building HeroDB..."
cargo build --release
# Start the server in the background
echo "🚀 Starting HeroDB server on port 6379..."
cargo run --release -- --port 6379 --dir ./test_data &
SERVER_PID=$!
# Wait for server to start
sleep 3
# Function to cleanup on exit
cleanup() {
echo "🧹 Cleaning up..."
kill $SERVER_PID 2>/dev/null || true
rm -rf ./test_data
exit
}
# Set trap for cleanup
trap cleanup EXIT INT TERM
# Function to execute Redis command
execute_cmd() {
local cmd="$1"
local description="$2"
echo "📝 $description"
echo " Command: $cmd"
if result=$(redis-cli -p 6379 $cmd 2>&1); then
echo " ✅ Result: $result"
echo
return 0
else
echo " ❌ Failed: $result"
echo
return 1
fi
}
echo "🧪 Running Tantivy Search Tests..."
echo
# Test 1: Create a search index
execute_cmd "ft.create books SCHEMA title TEXT description TEXT author TEXT category TAG price NUMERIC" \
"Creating search index 'books'"
# Test 2: Add documents to the index
execute_cmd "ft.add books book1 1.0 title \"The Great Gatsby\" description \"A classic American novel about the Jazz Age\" author \"F. Scott Fitzgerald\" category \"fiction,classic\" price \"12.99\"" \
"Adding first book"
execute_cmd "ft.add books book2 1.0 title \"To Kill a Mockingbird\" description \"A novel about racial injustice in the American South\" author \"Harper Lee\" category \"fiction,classic\" price \"14.99\"" \
"Adding second book"
execute_cmd "ft.add books book3 1.0 title \"Programming Rust\" description \"A comprehensive guide to Rust programming language\" author \"Jim Blandy\" category \"programming,technical\" price \"49.99\"" \
"Adding third book"
execute_cmd "ft.add books book4 1.0 title \"The Rust Programming Language\" description \"The official book on Rust programming\" author \"Steve Klabnik\" category \"programming,technical\" price \"39.99\"" \
"Adding fourth book"
# Test 3: Basic search
execute_cmd "ft.search books Rust" \
"Searching for 'Rust'"
# Test 4: Search with filters
execute_cmd "ft.search books programming FILTER category programming" \
"Searching for 'programming' with category filter"
# Test 5: Search with limit
execute_cmd "ft.search books \"*\" LIMIT 0 2" \
"Getting first 2 documents"
# Test 6: Get index info
execute_cmd "ft.info books" \
"Getting index information"
# Test 7: Delete a document
execute_cmd "ft.del books book1" \
"Deleting book1"
# Test 8: Search again to verify deletion
execute_cmd "ft.search books Gatsby" \
"Searching for deleted book"
# Test 9: Drop the index
execute_cmd "ft.drop books" \
"Dropping the index"
echo "🎉 All tests completed successfully!"
echo "✅ Tantivy search integration is working correctly"