tanvity not working yet its blocking
This commit is contained in:
@@ -8,9 +8,16 @@ set -e # Exit on any error
|
|||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
REDIS_HOST="localhost"
|
REDIS_HOST="localhost"
|
||||||
REDIS_PORT="6381"
|
REDIS_PORT="6382"
|
||||||
REDIS_CLI="redis-cli -h $REDIS_HOST -p $REDIS_PORT"
|
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
|
# Colors for output
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
@@ -85,7 +92,7 @@ main() {
|
|||||||
print_info "Creating a product catalog search index with various field types"
|
print_info "Creating a product catalog search index with various field types"
|
||||||
|
|
||||||
# Create search index with schema
|
# 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"
|
"Creating search index"
|
||||||
|
|
||||||
print_success "Search index 'product_catalog' created successfully"
|
print_success "Search index 'product_catalog' created successfully"
|
||||||
@@ -94,23 +101,17 @@ main() {
|
|||||||
print_header "Step 2: Add Sample Products"
|
print_header "Step 2: Add Sample Products"
|
||||||
print_info "Adding sample products to demonstrate different search scenarios"
|
print_info "Adding sample products to demonstrate different search scenarios"
|
||||||
|
|
||||||
# Add sample products
|
# Add sample products using FT.ADD
|
||||||
products=(
|
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"
|
||||||
"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'"
|
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"
|
||||||
"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'"
|
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"
|
||||||
"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'"
|
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"
|
||||||
"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'"
|
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"
|
||||||
"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'"
|
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"
|
||||||
"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'"
|
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"
|
||||||
"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'"
|
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"
|
||||||
"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'"
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
pause
|
||||||
|
|
||||||
print_header "Step 3: Basic Text Search"
|
print_header "Step 3: Basic Text Search"
|
||||||
@@ -120,39 +121,39 @@ main() {
|
|||||||
pause
|
pause
|
||||||
|
|
||||||
print_header "Step 4: Search with Filters"
|
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
|
pause
|
||||||
|
|
||||||
print_header "Step 5: Numeric Range Search"
|
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
|
pause
|
||||||
|
|
||||||
print_header "Step 6: Sorting Results"
|
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
|
pause
|
||||||
|
|
||||||
print_header "Step 7: Limiting Results"
|
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
|
pause
|
||||||
|
|
||||||
print_header "Step 8: Complex Query"
|
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
|
pause
|
||||||
|
|
||||||
print_header "Step 9: Geographic Search"
|
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
|
pause
|
||||||
|
|
||||||
print_header "Step 10: Aggregation Example"
|
print_header "Step 10: Aggregation Example"
|
||||||
@@ -175,34 +176,34 @@ main() {
|
|||||||
pause
|
pause
|
||||||
|
|
||||||
print_header "Step 12: Fuzzy Search"
|
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
|
pause
|
||||||
|
|
||||||
print_header "Step 13: Phrase Search"
|
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
|
pause
|
||||||
|
|
||||||
print_header "Step 14: Boolean Queries"
|
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
|
echo
|
||||||
execute_cmd "FT.SEARCH product_catalog 'coffee OR tea'" "Boolean OR search"
|
execute_cmd "FT.SEARCH product_catalog tea" "Text search"
|
||||||
pause
|
pause
|
||||||
|
|
||||||
print_header "Step 15: Cleanup"
|
print_header "Step 15: Cleanup"
|
||||||
print_info "Removing test data"
|
print_info "Removing test data"
|
||||||
|
|
||||||
# Delete the search index
|
# 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
|
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
|
done
|
||||||
|
|
||||||
print_success "Cleanup completed"
|
print_success "Cleanup completed"
|
||||||
|
@@ -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 tokio::time::{timeout, Duration};
|
||||||
use futures::future::select_all;
|
use futures::future::select_all;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Cmd {
|
pub enum Cmd {
|
||||||
@@ -689,6 +690,109 @@ impl Cmd {
|
|||||||
index_name,
|
index_name,
|
||||||
schema,
|
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()),
|
_ => Cmd::Unknow(cmd[0].clone()),
|
||||||
},
|
},
|
||||||
@@ -807,40 +911,30 @@ impl Cmd {
|
|||||||
|
|
||||||
// Full-text search commands
|
// Full-text search commands
|
||||||
Cmd::FtCreate { index_name, schema } => {
|
Cmd::FtCreate { index_name, schema } => {
|
||||||
// TODO: Implement the actual logic for creating a full-text search index.
|
search_cmd::ft_create_cmd(server, index_name, schema).await
|
||||||
// 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()))
|
|
||||||
}
|
}
|
||||||
Cmd::FtAdd { index_name, doc_id, score: _, fields: _ } => {
|
Cmd::FtAdd { index_name, doc_id, score, fields } => {
|
||||||
// TODO: Implement adding a document to the index.
|
search_cmd::ft_add_cmd(server, index_name, doc_id, score, fields).await
|
||||||
println!("Adding document '{}' to index '{}'", doc_id, index_name);
|
|
||||||
Ok(Protocol::SimpleString("OK".to_string()))
|
|
||||||
}
|
}
|
||||||
Cmd::FtSearch { index_name, query, .. } => {
|
Cmd::FtSearch { index_name, query, filters, limit, offset, return_fields } => {
|
||||||
// TODO: Implement search functionality.
|
search_cmd::ft_search_cmd(server, index_name, query, filters, limit, offset, return_fields).await
|
||||||
println!("Searching index '{}' for query '{}'", index_name, query);
|
|
||||||
Ok(Protocol::SimpleString("OK".to_string()))
|
|
||||||
}
|
}
|
||||||
Cmd::FtDel(index_name, doc_id) => {
|
Cmd::FtDel(index_name, doc_id) => {
|
||||||
println!("Deleting doc '{}' from index '{}'", doc_id, index_name);
|
search_cmd::ft_del_cmd(server, index_name, doc_id).await
|
||||||
Ok(Protocol::SimpleString("OK".to_string()))
|
|
||||||
}
|
}
|
||||||
Cmd::FtInfo(index_name) => {
|
Cmd::FtInfo(index_name) => {
|
||||||
println!("Getting info for index '{}'", index_name);
|
search_cmd::ft_info_cmd(server, index_name).await
|
||||||
Ok(Protocol::SimpleString("OK".to_string()))
|
|
||||||
}
|
}
|
||||||
Cmd::FtDrop(index_name) => {
|
Cmd::FtDrop(index_name) => {
|
||||||
println!("Dropping index '{}'", index_name);
|
search_cmd::ft_drop_cmd(server, index_name).await
|
||||||
Ok(Protocol::SimpleString("OK".to_string()))
|
|
||||||
}
|
}
|
||||||
Cmd::FtAlter { index_name, .. } => {
|
Cmd::FtAlter { .. } => {
|
||||||
println!("Altering index '{}'", index_name);
|
// Not implemented yet
|
||||||
Ok(Protocol::SimpleString("OK".to_string()))
|
Ok(Protocol::err("FT.ALTER not implemented yet"))
|
||||||
}
|
}
|
||||||
Cmd::FtAggregate { index_name, .. } => {
|
Cmd::FtAggregate { .. } => {
|
||||||
println!("Aggregating on index '{}'", index_name);
|
// Not implemented yet
|
||||||
Ok(Protocol::SimpleString("OK".to_string()))
|
Ok(Protocol::err("FT.AGGREGATE not implemented yet"))
|
||||||
}
|
}
|
||||||
Cmd::Unknow(s) => Ok(Protocol::err(&format!("ERR unknown command `{}`", s))),
|
Cmd::Unknow(s) => Ok(Protocol::err(&format!("ERR unknown command `{}`", s))),
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@ pub mod crypto;
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod options;
|
pub mod options;
|
||||||
pub mod protocol;
|
pub mod protocol;
|
||||||
|
pub mod search_cmd; // Add this
|
||||||
pub mod server;
|
pub mod server;
|
||||||
pub mod storage;
|
pub mod storage;
|
||||||
pub mod storage_trait; // Add this
|
pub mod storage_trait; // Add this
|
||||||
|
272
herodb/src/search_cmd.rs
Normal file
272
herodb/src/search_cmd.rs
Normal 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)))
|
||||||
|
}
|
||||||
|
}
|
@@ -4,6 +4,7 @@ use std::sync::Arc;
|
|||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::io::AsyncWriteExt;
|
||||||
use tokio::sync::{Mutex, oneshot};
|
use tokio::sync::{Mutex, oneshot};
|
||||||
|
use std::sync::RwLock;
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
@@ -14,10 +15,12 @@ use crate::protocol::Protocol;
|
|||||||
use crate::storage::Storage;
|
use crate::storage::Storage;
|
||||||
use crate::storage_sled::SledStorage;
|
use crate::storage_sled::SledStorage;
|
||||||
use crate::storage_trait::StorageBackend;
|
use crate::storage_trait::StorageBackend;
|
||||||
|
use crate::tantivy_search::TantivySearch;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Server {
|
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 option: options::DBOption,
|
||||||
pub client_name: Option<String>,
|
pub client_name: Option<String>,
|
||||||
pub selected_db: u64, // Changed from usize to u64
|
pub selected_db: u64, // Changed from usize to u64
|
||||||
@@ -43,7 +46,8 @@ pub enum PopSide {
|
|||||||
impl Server {
|
impl Server {
|
||||||
pub async fn new(option: options::DBOption) -> Self {
|
pub async fn new(option: options::DBOption) -> Self {
|
||||||
Server {
|
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,
|
option,
|
||||||
client_name: None,
|
client_name: None,
|
||||||
selected_db: 0,
|
selected_db: 0,
|
||||||
@@ -100,6 +104,11 @@ impl Server {
|
|||||||
// DB 0-9 are non-encrypted, DB 10+ are encrypted
|
// DB 0-9 are non-encrypted, DB 10+ are encrypted
|
||||||
self.option.encrypt && db_index >= 10
|
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 -----
|
// ----- BLPOP waiter helpers -----
|
||||||
|
|
||||||
|
@@ -228,7 +228,7 @@ impl TantivySearch {
|
|||||||
let tokenizer_manager = TokenizerManager::default();
|
let tokenizer_manager = TokenizerManager::default();
|
||||||
index.set_tokenizers(tokenizer_manager);
|
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)))?;
|
.map_err(|e| DBError(format!("Failed to create index writer: {}", e)))?;
|
||||||
|
|
||||||
let reader = index
|
let reader = index
|
||||||
|
101
herodb/test_tantivy_integration.sh
Executable file
101
herodb/test_tantivy_integration.sh
Executable 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"
|
Reference in New Issue
Block a user