Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 0 additions & 133 deletions crates/migrations/20251020000001_create_fts5_search_tables.sql

This file was deleted.

63 changes: 32 additions & 31 deletions crates/migrations/20251021000001_unified_search_index.sql
Original file line number Diff line number Diff line change
@@ -1,19 +1,3 @@
-- Drop existing FTS5 tables and triggers
DROP TRIGGER IF EXISTS achievements_fts_insert;
DROP TRIGGER IF EXISTS achievements_fts_update;
DROP TRIGGER IF EXISTS achievements_fts_delete;
DROP TABLE IF EXISTS achievements_fts;

DROP TRIGGER IF EXISTS controllers_fts_insert;
DROP TRIGGER IF EXISTS controllers_fts_update;
DROP TRIGGER IF EXISTS controllers_fts_delete;
DROP TABLE IF EXISTS controllers_fts;

DROP TRIGGER IF EXISTS token_attributes_fts_insert;
DROP TRIGGER IF EXISTS token_attributes_fts_update;
DROP TRIGGER IF EXISTS token_attributes_fts_delete;
DROP TABLE IF EXISTS token_attributes_fts;

-- ========================================
-- Unified Search Index (FTS5)
-- ========================================
Expand Down Expand Up @@ -227,24 +211,41 @@ WHERE token_id IS NULL;
-- - token_attribute: Searchable by trait_name, trait_value (NFT traits)
-- - token: Searchable by name, symbol (ERC20 tokens only, token_id IS NULL)
--
-- Search all types:
-- SELECT * FROM search_index WHERE search_index MATCH 'dragon' ORDER BY rank;
-- Query Examples:
--
-- Search all types:
-- SELECT * FROM search_index WHERE search_index MATCH 'dragon' ORDER BY rank;
--
-- Search specific type:
-- SELECT * FROM search_index
-- WHERE search_index MATCH 'USDC' AND entity_type = 'token'
-- ORDER BY rank;
--
-- Search tokens by symbol:
-- SELECT * FROM search_index
-- WHERE search_index MATCH 'ETH' AND entity_type = 'token'
-- ORDER BY rank;
--
-- Phrase search:
-- SELECT * FROM search_index
-- WHERE search_index MATCH '"dragon slayer"'
-- ORDER BY rank;
--
-- Search specific type:
-- SELECT * FROM search_index
-- WHERE search_index MATCH 'USDC' AND entity_type = 'token'
-- ORDER BY rank;
-- Boolean search:
-- SELECT * FROM search_index
-- WHERE search_index MATCH 'dragon OR knight'
-- ORDER BY rank;
--
-- Search with metadata filter:
-- SELECT * FROM search_index
-- WHERE search_index MATCH 'dragon'
-- AND json_extract(metadata, '$.namespace') = 'my_game'
-- ORDER BY rank;
-- Prefix search:
-- SELECT * FROM search_index
-- WHERE search_index MATCH 'dra*'
-- ORDER BY rank;
--
-- Search tokens by symbol:
-- SELECT * FROM search_index
-- WHERE search_index MATCH 'ETH' AND entity_type = 'token'
-- ORDER BY rank;
-- Filter by metadata (if needed):
-- SELECT * FROM search_index
-- WHERE search_index MATCH 'dragon'
-- AND json_extract(metadata, '$.namespace') = 'my_game'
-- ORDER BY rank;
--
-- Add new searchable entity type:
-- 1. Create triggers for INSERT/UPDATE/DELETE with optional WHEN clause
Expand Down
4 changes: 0 additions & 4 deletions crates/proto/proto/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -657,10 +657,6 @@ message SearchQuery {
string query = 1;
// Maximum results to return per table
uint32 limit = 2;
// Filter by world addresses (optional)
repeated bytes world_addresses = 3;
// Filter by namespaces (optional)
repeated string namespaces = 4;
}

// A single search result match
Expand Down
14 changes: 0 additions & 14 deletions crates/proto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2360,8 +2360,6 @@ impl TryFrom<proto::types::TransactionQuery> for TransactionQuery {
pub struct SearchQuery {
pub query: String,
pub limit: u32,
pub world_addresses: Vec<Felt>,
pub namespaces: Vec<String>,
}

#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
Expand Down Expand Up @@ -2391,12 +2389,6 @@ impl From<SearchQuery> for proto::types::SearchQuery {
Self {
query: value.query,
limit: value.limit,
world_addresses: value
.world_addresses
.into_iter()
.map(|addr| addr.to_bytes_be().to_vec())
.collect(),
namespaces: value.namespaces,
}
}
}
Expand All @@ -2407,12 +2399,6 @@ impl TryFrom<proto::types::SearchQuery> for SearchQuery {
Ok(Self {
query: value.query,
limit: value.limit,
world_addresses: value
.world_addresses
.into_iter()
.map(|addr| Felt::from_bytes_be_slice(&addr))
.collect(),
namespaces: value.namespaces,
})
}
}
Expand Down
6 changes: 5 additions & 1 deletion crates/runner/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,11 @@ impl Runner {
trait_counts: self.args.erc.trait_counts,
achievement_registration_model_name: self.args.achievement.registration_model_name,
achievement_progression_model_name: self.args.achievement.progression_model_name,
search: self.args.search.clone(),
search_max_results: self.args.search.max_results,
search_min_query_length: self.args.search.min_query_length,
search_prefix_matching: self.args.search.prefix_matching,
search_return_snippets: self.args.search.return_snippets,
search_snippet_length: self.args.search.snippet_length,
};

let (mut executor, sender) = Executor::new_with_config(
Expand Down
1 change: 0 additions & 1 deletion crates/sqlite/sqlite/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ version.workspace = true

[dependencies]
torii-broker.workspace = true
torii-cli.workspace = true
torii-sqlite-types.workspace = true

anyhow.workspace = true
Expand Down
8 changes: 6 additions & 2 deletions crates/sqlite/sqlite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,12 @@ pub struct SqlConfig {
// Achievement tracking configuration
pub achievement_registration_model_name: String,
pub achievement_progression_model_name: String,
// Search API configuration
pub search: torii_cli::SearchOptions,
// Search configuration
pub search_max_results: usize,
pub search_min_query_length: usize,
pub search_prefix_matching: bool,
pub search_return_snippets: bool,
pub search_snippet_length: usize,
}

impl SqlConfig {
Expand Down
59 changes: 17 additions & 42 deletions crates/sqlite/sqlite/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1554,62 +1554,37 @@ impl ReadOnlyStorage for Sql {
}

// Validate against min_query_length from config
if search_term.len() < self.config.search.min_query_length {
if search_term.len() < self.config.search_min_query_length {
return Ok(SearchResponse {
total: 0,
results: vec![],
});
}

// Apply prefix matching if enabled
let fts_query = if self.config.search.prefix_matching && !search_term.ends_with('*') {
let fts_query = if self.config.search_prefix_matching && !search_term.ends_with('*') {
format!("{}*", search_term)
} else {
search_term.to_string()
};

let limit = if query.limit > 0 && query.limit <= self.config.search.max_results as u32 {
let limit = if query.limit > 0 && query.limit <= self.config.search_max_results as u32 {
query.limit
} else {
self.config.search.max_results as u32
self.config.search_max_results as u32
};

// Build unified search query
let mut sql = "SELECT entity_type, entity_id, primary_text, secondary_text, metadata, \
bm25(search_index) as rank \
FROM search_index \
WHERE search_index MATCH ?"
.to_string();
let mut bind_values: Vec<String> = vec![fts_query];

// Add world_address filter if provided
if !query.world_addresses.is_empty() {
let addr_conditions: Vec<String> = query
.world_addresses
.iter()
.map(|_| "json_extract(metadata, '$.world_address') = ?".to_string())
.collect();
sql.push_str(&format!(" AND ({})", addr_conditions.join(" OR ")));
for addr in &query.world_addresses {
bind_values.push(felt_to_sql_string(addr));
}
}

// Add namespace filter if provided
if !query.namespaces.is_empty() {
let ns_conditions: Vec<String> = query
.namespaces
.iter()
.map(|_| "json_extract(metadata, '$.namespace') = ?".to_string())
.collect();
sql.push_str(&format!(" AND ({})", ns_conditions.join(" OR ")));
for ns in &query.namespaces {
bind_values.push(ns.clone());
}
}

// Order by relevance and limit results
sql.push_str(&format!(" ORDER BY rank ASC LIMIT {}", limit * 3)); // Get more for grouping
let sql = format!(
"SELECT entity_type, entity_id, primary_text, secondary_text, metadata, \
bm25(search_index) as rank \
FROM search_index \
WHERE search_index MATCH ? \
ORDER BY rank ASC \
LIMIT {}",
limit * 3 // Get more results for proper grouping by entity type
);
let bind_values: Vec<String> = vec![fts_query];

// Execute query
let mut sqlx_query = sqlx::query(&sql);
Expand Down Expand Up @@ -1661,14 +1636,14 @@ impl ReadOnlyStorage for Sql {
}

// Add snippet if enabled
if self.config.search.return_snippets {
if self.config.search_return_snippets {
let text = if !secondary_text.is_empty() {
&secondary_text
} else {
&primary_text
};
let snippet = if text.len() > self.config.search.snippet_length {
format!("{}...", &text[..self.config.search.snippet_length])
let snippet = if text.len() > self.config.search_snippet_length {
format!("{}...", &text[..self.config.search_snippet_length])
} else {
text.clone()
};
Expand Down
Loading