| title | Client-Side Filtering Implementation | ||||
|---|---|---|---|---|---|
| description | The client-side filtering feature allows authenticated clients to filter which graph nodes are visible based on quality and authority scores stored in node metadata. | ||||
| category | how-to | ||||
| tags |
|
||||
| updated-date | 2025-12-18 | ||||
| difficulty-level | advanced |
The client-side filtering feature allows authenticated clients to filter which graph nodes are visible based on quality and authority scores stored in node metadata.
-
ClientFilter (
src/actors/client_coordinator_actor.rs)- Per-client filter configuration
- Stores threshold settings and filtered node IDs
- Persisted to Neo4j for authenticated users
-
Filter Logic (
src/actors/client_filter.rs)recompute_filtered_nodes()- Main filtering functionnode_passes_filter()- Helper for individual node checks
-
Integration Points
- Client authentication: Load saved filter
- Filter updates: Recompute when client changes settings
- Position broadcasts: Apply filter before sending to client
flowchart TD
A["Client Auth"] -->|Load filter from Neo4j| B["Recompute Filtered Nodes"]
B -->|Fetch GraphData with metadata| C["ClientFilter.filtered_node_ids\n(HashSet<u32>)"]
C --> D["Position Broadcast"]
D -->|Only send nodes in filtered_node_ids| E["Client"]
Nodes store quality metrics in metadata:
pub struct Metadata {
pub quality_score: Option<f64>, // 0.0-1.0
pub authority_score: Option<f64>, // 0.0-1.0
// ... other fields
}pub struct ClientFilter {
pub enabled: bool,
pub quality_threshold: f64, // Default: 0.7
pub authority_threshold: f64, // Default: 0.5
pub filter_by_quality: bool, // Default: true
pub filter_by_authority: bool, // Default: false
pub filter_mode: FilterMode, // And/Or
pub max_nodes: Option<usize>, // Default: Some(10000)
pub filtered_node_ids: HashSet<u32>, // Computed
}- And Mode: Node must pass BOTH quality AND authority thresholds
- Or Mode: Node must pass EITHER quality OR authority threshold
pub fn recompute_filtered_nodes(
filter: &mut ClientFilter,
graph_data: &GraphData
) {
filter.filtered_node_ids.clear();
if !filter.enabled {
// All nodes visible
for node in &graph_data.nodes {
filter.filtered_node_ids.insert(node.id);
}
return;
}
let mut candidates = Vec::new();
for node in &graph_data.nodes {
let metadata = graph_data.metadata.get(&node.metadata_id);
let quality = metadata
.and_then(|m| m.quality_score)
.unwrap_or(0.5);
let authority = metadata
.and_then(|m| m.authority_score)
.unwrap_or(0.5);
let passes_quality = !filter.filter_by_quality
|| quality >= filter.quality_threshold;
let passes_authority = !filter.filter_by_authority
|| authority >= filter.authority_threshold;
let passes = match filter.filter_mode {
FilterMode::And => passes_quality && passes_authority,
FilterMode::Or => passes_quality || passes_authority,
};
if passes {
candidates.push((node.id, quality, authority));
}
}
// Apply max_nodes limit
if let Some(max) = filter.max_nodes {
if candidates.len() > max {
// Sort by combined score (quality * authority) DESC
candidates.sort_by(|a, b| {
let score_a = a.1 * a.2;
let score_b = b.1 * b.2;
score_b.partial_cmp(&score_a)
.unwrap_or(std::cmp::Ordering::Equal)
});
candidates.truncate(max);
}
}
for (node_id, _, _) in candidates {
filter.filtered_node_ids.insert(node_id);
}
}{
"type": "authenticate",
"token": "session_token",
"pubkey": "nostr_pubkey"
}Response:
{
"type": "authenticate_success",
"pubkey": "nostr_pubkey",
"is_power_user": false
}{
"type": "filter_update",
"filter": {
"enabled": true,
"quality_threshold": 0.8,
"authority_threshold": 0.7,
"filter_by_quality": true,
"filter_by_authority": true,
"filter_mode": "and",
"max_nodes": 5000
}
}Response:
{
"type": "filter_update_success",
"enabled": true,
"timestamp": 1234567890
}When a client authenticates:
impl Handler<AuthenticateClient> for ClientCoordinatorActor {
fn handle(&mut self, msg: AuthenticateClient, ctx: &mut Self::Context) {
// ... set client.pubkey, client.is_power_user
// TODO: Load saved filter from Neo4j
// Recompute if filter enabled
if client.filter.enabled {
// Fetch graph data and recompute
}
}
}When a client updates their filter:
impl Handler<UpdateClientFilter> for ClientCoordinatorActor {
fn handle(&mut self, msg: UpdateClientFilter, ctx: &mut Self::Context) {
// Update filter settings
client.filter.enabled = msg.enabled;
client.filter.quality_threshold = msg.quality_threshold;
// ... etc
// Recompute filtered nodes with new settings
recompute_filtered_nodes(&mut client.filter, &graph_data);
// TODO: Save to Neo4j
}
}When broadcasting positions:
pub fn broadcast_with_filter(&self, positions: &[BinaryNodeDataClient]) {
for (_, client_state) in &self.clients {
let filtered_positions = if client_state.filter.enabled {
positions.iter()
.filter(|pos| client_state.filter
.filtered_node_ids.contains(&pos.node_id))
.copied()
.collect::<Vec<_>>()
} else {
positions.to_vec()
};
if !filtered_positions.is_empty() {
let binary_data = self.serialize_positions(&filtered_positions);
client_state.addr.do_send(SendToClientBinary(binary_data));
}
}
}Nodes without metadata receive default scores:
- quality_score:
0.5 - authority_score:
0.5
This ensures nodes without explicit scores are treated neutrally.
Tests are located in src/actors/client_filter.rs:
test_filter_disabled_shows_all- Filter off shows all nodestest_filter_by_quality_only- Quality-only filteringtest_filter_by_authority_only- Authority-only filteringtest_filter_and_mode- AND mode requires both thresholdstest_filter_or_mode- OR mode requires either thresholdtest_max_nodes_limit- Truncation by max_nodestest_default_values_for_missing_metadata- Default score handling
Run tests:
cargo test --lib client_filter{
"enabled": true,
"quality_threshold": 0.9,
"authority_threshold": 0.8,
"filter_by_quality": true,
"filter_by_authority": true,
"filter_mode": "and",
"max_nodes": 1000
}Result: Shows top 1000 nodes with quality ≥ 0.9 AND authority ≥ 0.8
{
"enabled": true,
"quality_threshold": 0.7,
"filter_by_quality": true,
"filter_by_authority": false,
"filter_mode": "or",
"max_nodes": 10000
}Result: Shows up to 10,000 nodes with quality ≥ 0.7
- Recomputation is O(n) where n = total nodes
- Triggered only on filter changes and authentication
- Uses HashSet for O(1) lookup during broadcasts
- Max_nodes sorting is O(n log n) but only on filtered candidates
- Incremental Updates: Only recompute when graph data changes
- Filter Presets: Power users can create named filter presets
- Additional Metrics: Filter by recency, connectivity, domain
- Client-Side Caching: Send filter rules to client for local filtering
- Analytics: Track which filters are most commonly used