waffle_db 0.0.2+1 copy "waffle_db: ^0.0.2+1" to clipboard
waffle_db: ^0.0.2+1 copied to clipboard

An advanced, local vector database for Flutter, powered by Rust and HNSW graph indexing.

example/lib/main.dart

import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:waffle_db/waffle_db.dart';
import 'package:path_provider/path_provider.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await WaffleDB.init();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Waffle-DB Color Search',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.teal,
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  WaffleDatabase? _db;
  bool _isInitializing = true;
  int _totalColors = 0;
  String? _dbPath;

  // Generation state
  bool _isGenerating = false;
  double _generationProgress = 0.0;
  String _generationMessage = '';
  int _selectedCount = 1000000; // Default to 1 Million colors

  final List<int> _colorCountOptions = [10000, 100000, 1000000, 12000000];

  // Target search color
  double _targetRed = 0.0;
  double _targetGreen = 150.0;
  double _targetBlue = 255.0;
  int _k = 150;

  // Results state
  List<WaffleQueryResult> _searchResults = [];
  double? _searchTimeMs;

  @override
  void initState() {
    super.initState();
    _initDatabase();
  }

  Future<void> _initDatabase() async {
    setState(() {
      _isInitializing = true;
    });

    try {
      final dir = await getApplicationDocumentsDirectory();
      _dbPath = '${dir.path}/waffle_color_db';

      final config = WaffleConfig(
        dimension: 3,
        path: _dbPath!,
        graphConfig: const WaffleGraphConfig(
          m: 16,
          metric: WaffleMetric.euclidean, // Euclidean is perfect for RGB space
          efConstruction: 64,
          efSearch: 32,
        ),
        maxElements: 13000000, // Support up to 12M+ colors
        useQuantization: false,
        cacheSizeBytes: BigInt.from(64 * 1024 * 1024), // 64 MB cache
        workerThreads: 4,
      );

      _db = await WaffleDatabase.open(config);
      _totalColors = _db!.count();
    } catch (e) {
      _showErrorSnackBar('Error opening database: $e');
    } finally {
      setState(() {
        _isInitializing = false;
      });
    }
  }

  Future<void> _rebuildDatabase() async {
    if (_isGenerating || _db == null || _dbPath == null) return;

    setState(() {
      _isGenerating = true;
      _generationProgress = 0.0;
      _generationMessage = 'Preparing directory...';
    });

    try {
      // 1. Close current DB
      await _db!.close();
      _db = null;

      // 2. Clean directory
      final dir = Directory(_dbPath!);
      if (dir.existsSync()) {
        dir.deleteSync(recursive: true);
      }

      // 3. Open new clean DB
      final config = WaffleConfig(
        dimension: 3,
        path: _dbPath!,
        graphConfig: const WaffleGraphConfig(
          m: 16,
          metric: WaffleMetric.euclidean,
          efConstruction: 64,
          efSearch: 32,
        ),
        maxElements: 13000000,
        useQuantization: false,
        cacheSizeBytes: BigInt.from(64 * 1024 * 1024),
        workerThreads: 4,
      );
      _db = await WaffleDatabase.open(config);

      // 4. Generate colors in chunks of 25,000 to keep UI responsive
      final random = Random();
      final total = _selectedCount;
      const chunkSize = 25000;
      int inserted = 0;

      while (inserted < total) {
        final currentChunk = min(chunkSize, total - inserted);
        final records = <WaffleRecord>[];

        for (int i = 0; i < currentChunk; i++) {
          final r = random.nextDouble();
          final g = random.nextDouble();
          final b = random.nextDouble();

          final rInt = (r * 255).toInt();
          final gInt = (g * 255).toInt();
          final bInt = (b * 255).toInt();

          // Encode RGB in ID to retrieve values instantly on query
          records.add(
            WaffleRecord(
              id: 'color_${rInt}_${gInt}_${bInt}_${inserted + i}',
              vector: Float32List.fromList([r, g, b]),
            ),
          );
        }

        await _db!.insertBatch(records);
        inserted += currentChunk;

        setState(() {
          _generationProgress = inserted / total;
          _generationMessage =
              'Generated ${inserted.toString().replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]},')} / ${total.toString().replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]},')} colors...';
        });

        // Yield execution to allow UI rendering
        await Future.delayed(Duration.zero);
      }

      setState(() {
        _generationMessage = 'Flushing database to disk...';
      });
      await _db!.flush();
      _totalColors = _db!.count();
      _searchResults = [];
      _searchTimeMs = null;
    } catch (e) {
      _showErrorSnackBar('Rebuild failed: $e');
    } finally {
      setState(() {
        _isGenerating = false;
      });
    }
  }

  void _searchNearestColors() {
    if (_db == null || _totalColors == 0) {
      _showErrorSnackBar('Database is empty! Please generate colors first.');
      return;
    }

    final queryVector = Float32List.fromList([
      _targetRed / 255.0,
      _targetGreen / 255.0,
      _targetBlue / 255.0,
    ]);

    final sw = Stopwatch()..start();
    final results = _db!.query(
      queryVector,
      k: _k,
      efSearch: 48, // slightly higher search resolution
      includeMetadata: false,
    );
    sw.stop();

    setState(() {
      _searchResults = results;
      _searchTimeMs = sw.elapsedMicroseconds / 1000.0;
    });
  }

  void _randomizeTargetColor() {
    final random = Random();
    setState(() {
      _targetRed = random.nextInt(256).toDouble();
      _targetGreen = random.nextInt(256).toDouble();
      _targetBlue = random.nextInt(256).toDouble();
    });
    if (_searchResults.isNotEmpty) {
      _searchNearestColors();
    }
  }

  void _showErrorSnackBar(String message) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(message), backgroundColor: Colors.redAccent),
    );
  }

  Color _parseColorFromId(String id) {
    try {
      final parts = id.split('_');
      if (parts.length >= 4 && parts[0] == 'color') {
        final r = int.parse(parts[1]);
        final g = int.parse(parts[2]);
        final b = int.parse(parts[3]);
        return Color.fromARGB(255, r, g, b);
      }
    } catch (_) {}
    return Colors.grey;
  }

  String _parseRgbTextFromId(String id) {
    try {
      final parts = id.split('_');
      if (parts.length >= 4 && parts[0] == 'color') {
        return 'RGB(${parts[1]}, ${parts[2]}, ${parts[3]})';
      }
    } catch (_) {}
    return 'RGB(?, ?, ?)';
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final targetColor = Color.fromARGB(
      255,
      _targetRed.toInt(),
      _targetGreen.toInt(),
      _targetBlue.toInt(),
    );

    return Scaffold(
      appBar: AppBar(
        title: const Text('🧇 Waffle-DB: Color Semantic Search'),
        centerTitle: true,
        backgroundColor: theme.colorScheme.primaryContainer,
        foregroundColor: theme.colorScheme.onPrimaryContainer,
      ),
      body: _isInitializing
          ? const Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  CircularProgressIndicator(),
                  SizedBox(height: 16),
                  Text('Loading native database components...'),
                ],
              ),
            )
          : LayoutBuilder(
              builder: (context, constraints) {
                final isMobile = constraints.maxWidth < 720;
                if (isMobile) {
                  return _buildMobileLayout(theme, targetColor);
                } else {
                  return _buildDesktopLayout(theme, targetColor);
                }
              },
            ),
    );
  }

  Widget _buildMobileLayout(ThemeData theme, Color targetColor) {
    return DefaultTabController(
      length: 2,
      child: Column(
        children: [
          TabBar(
            tabs: const [
              Tab(icon: Icon(Icons.settings), text: 'Setup & Selection'),
              Tab(icon: Icon(Icons.palette), text: 'Search Results'),
            ],
            labelColor: theme.colorScheme.primary,
            unselectedLabelColor: theme.colorScheme.onSurfaceVariant,
            indicatorColor: theme.colorScheme.primary,
          ),
          Expanded(
            child: TabBarView(
              children: [
                // Tab 1: Configuration & Color Selection
                SingleChildScrollView(
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      _buildDatabaseStatusCard(theme),
                      const SizedBox(height: 16),
                      _buildGeneratorSection(theme),
                      const Divider(height: 32),
                      _buildTargetColorSection(theme, targetColor),
                    ],
                  ),
                ),
                // Tab 2: Results Grid
                Padding(
                  padding: const EdgeInsets.all(16),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      _buildResultsHeader(theme),
                      const SizedBox(height: 16),
                      Expanded(
                        child: _buildResultsGrid(theme, crossAxisCount: 3),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildDesktopLayout(ThemeData theme, Color targetColor) {
    return Row(
      children: [
        // Left Panel: Configuration & Generation Controls
        Expanded(
          flex: 3,
          child: Container(
            color: theme.colorScheme.surfaceContainerLow,
            child: SingleChildScrollView(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  _buildDatabaseStatusCard(theme),
                  const SizedBox(height: 16),
                  _buildGeneratorSection(theme),
                  const Divider(height: 32),
                  _buildTargetColorSection(theme, targetColor),
                ],
              ),
            ),
          ),
        ),

        // Right Panel: Results Grid
        Expanded(
          flex: 5,
          child: Container(
            padding: const EdgeInsets.all(16),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                _buildResultsHeader(theme),
                const SizedBox(height: 16),
                Expanded(child: _buildResultsGrid(theme, crossAxisCount: 5)),
              ],
            ),
          ),
        ),
      ],
    );
  }

  Widget _buildDatabaseStatusCard(ThemeData theme) {
    return Card(
      elevation: 0,
      color: theme.colorScheme.surfaceContainerHighest,
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'Database Status',
              style: theme.textTheme.titleMedium?.copyWith(
                fontWeight: FontWeight.bold,
                color: theme.colorScheme.primary,
              ),
            ),
            const SizedBox(height: 8),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                const Text('Stored Elements:'),
                Text(
                  _totalColors.toString().replaceAllMapped(
                    RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
                    (Match m) => '${m[1]},',
                  ),
                  style: theme.textTheme.titleLarge?.copyWith(
                    fontWeight: FontWeight.bold,
                    color: theme.colorScheme.secondary,
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildGeneratorSection(ThemeData theme) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          '1. Populate Color Database',
          style: theme.textTheme.titleMedium?.copyWith(
            fontWeight: FontWeight.bold,
          ),
        ),
        const SizedBox(height: 8),
        DropdownButtonFormField<int>(
          initialValue: _selectedCount,
          decoration: const InputDecoration(
            labelText: 'Target Database Size',
            border: OutlineInputBorder(),
          ),
          items: _colorCountOptions.map((count) {
            String label = count.toString();
            if (count == 10000) {
              label = '10,000 (Quick)';
            }
            if (count == 100000) {
              label = '100,000 (Medium)';
            }
            if (count == 1000000) {
              label = '1,000,000 (1 Million)';
            }
            if (count == 12000000) {
              label = '12,000,000 (12 Million)';
            }
            return DropdownMenuItem(value: count, child: Text(label));
          }).toList(),
          onChanged: _isGenerating
              ? null
              : (val) {
                  if (val != null) {
                    setState(() {
                      _selectedCount = val;
                    });
                  }
                },
        ),
        const SizedBox(height: 12),
        SizedBox(
          width: double.infinity,
          height: 48,
          child: ElevatedButton.icon(
            onPressed: _isGenerating ? null : _rebuildDatabase,
            icon: const Icon(Icons.palette),
            label: const Text('Generate Random Colors'),
            style: ElevatedButton.styleFrom(
              backgroundColor: theme.colorScheme.primary,
              foregroundColor: theme.colorScheme.onPrimary,
            ),
          ),
        ),
        const SizedBox(height: 16),
        if (_isGenerating) ...[
          Text(_generationMessage, style: theme.textTheme.bodySmall),
          const SizedBox(height: 8),
          LinearProgressIndicator(
            value: _generationProgress,
            color: theme.colorScheme.primary,
            backgroundColor: theme.colorScheme.surfaceContainerHighest,
          ),
          const SizedBox(height: 16),
        ],
      ],
    );
  }

  Widget _buildTargetColorSection(ThemeData theme, Color targetColor) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          '2. Target Color Selection',
          style: theme.textTheme.titleMedium?.copyWith(
            fontWeight: FontWeight.bold,
          ),
        ),
        const SizedBox(height: 12),
        Container(
          width: double.infinity,
          height: 100,
          decoration: BoxDecoration(
            color: targetColor,
            borderRadius: BorderRadius.circular(12),
            border: Border.all(color: theme.colorScheme.outline, width: 2),
          ),
          child: Center(
            child: Text(
              'Target Color\nRGB(${_targetRed.toInt()}, ${_targetGreen.toInt()}, ${_targetBlue.toInt()})',
              textAlign: TextAlign.center,
              style: TextStyle(
                color:
                    ThemeData.estimateBrightnessForColor(targetColor) ==
                        Brightness.dark
                    ? Colors.white
                    : Colors.black,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
        ),
        const SizedBox(height: 16),
        _buildColorSlider(
          label: 'Red',
          value: _targetRed,
          color: Colors.red,
          onChanged: (val) {
            setState(() {
              _targetRed = val;
            });
          },
        ),
        _buildColorSlider(
          label: 'Green',
          value: _targetGreen,
          color: Colors.green,
          onChanged: (val) {
            setState(() {
              _targetGreen = val;
            });
          },
        ),
        _buildColorSlider(
          label: 'Blue',
          value: _targetBlue,
          color: Colors.blue,
          onChanged: (val) {
            setState(() {
              _targetBlue = val;
            });
          },
        ),
        const SizedBox(height: 8),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: const [Text('Nearest Neighbors (k):')],
        ),
        Slider(
          value: _k.toDouble(),
          min: 1,
          max: 200,
          divisions: 199,
          label: '$_k',
          onChanged: (val) {
            setState(() {
              _k = val.toInt();
            });
          },
        ),
        const SizedBox(height: 12),
        Row(
          children: [
            Expanded(
              child: OutlinedButton(
                onPressed: _randomizeTargetColor,
                child: const Text('Random'),
              ),
            ),
            const SizedBox(width: 8),
            Expanded(
              child: ElevatedButton(
                onPressed: _searchResults.isEmpty || _totalColors > 0
                    ? _searchNearestColors
                    : null,
                child: const Text('Search'),
              ),
            ),
          ],
        ),
      ],
    );
  }

  Widget _buildResultsHeader(ThemeData theme) {
    if (_searchTimeMs == null) return const SizedBox.shrink();
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Text(
          'Query Results',
          style: theme.textTheme.titleLarge?.copyWith(
            fontWeight: FontWeight.bold,
          ),
        ),
        Card(
          color: theme.colorScheme.secondaryContainer,
          child: Padding(
            padding: const EdgeInsets.symmetric(
              horizontal: 12.0,
              vertical: 6.0,
            ),
            child: Text(
              'Search completed in ${_searchTimeMs!.toStringAsFixed(3)} ms',
              style: theme.textTheme.bodyMedium?.copyWith(
                fontWeight: FontWeight.bold,
                color: theme.colorScheme.onSecondaryContainer,
              ),
            ),
          ),
        ),
      ],
    );
  }

  Widget _buildResultsGrid(ThemeData theme, {required int crossAxisCount}) {
    if (_searchResults.isEmpty) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.search, size: 64, color: theme.colorScheme.outline),
            const SizedBox(height: 16),
            const Text(
              'Select a target color and click Search\nto find nearest matches in milliseconds!',
              textAlign: TextAlign.center,
            ),
          ],
        ),
      );
    }

    return GridView.builder(
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: crossAxisCount,
        crossAxisSpacing: 8,
        mainAxisSpacing: 8,
        childAspectRatio: 0.9,
      ),
      itemCount: _searchResults.length,
      itemBuilder: (context, index) {
        final result = _searchResults[index];
        final color = _parseColorFromId(result.id);
        final rgbText = _parseRgbTextFromId(result.id);
        final isDark =
            ThemeData.estimateBrightnessForColor(color) == Brightness.dark;

        return Tooltip(
          message:
              '${result.id}\nDistance: ${result.distance.toStringAsFixed(6)}',
          child: Card(
            clipBehavior: Clip.antiAlias,
            color: color,
            child: Padding(
              padding: const EdgeInsets.all(8.0),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.spaceBetween,
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                    children: [
                      CircleAvatar(
                        radius: 10,
                        backgroundColor: isDark
                            ? Colors.white24
                            : Colors.black26,
                        child: Text(
                          '${index + 1}',
                          style: TextStyle(
                            fontSize: 10,
                            fontWeight: FontWeight.bold,
                            color: isDark ? Colors.white : Colors.black,
                          ),
                        ),
                      ),
                    ],
                  ),
                  Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        rgbText,
                        style: TextStyle(
                          fontSize: 10,
                          fontWeight: FontWeight.bold,
                          color: isDark ? Colors.white : Colors.black,
                        ),
                      ),
                      const SizedBox(height: 2),
                      Text(
                        'Dist: ${result.distance.toStringAsFixed(4)}',
                        style: TextStyle(
                          fontSize: 9,
                          color: isDark ? Colors.white70 : Colors.black87,
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
        );
      },
    );
  }

  Widget _buildColorSlider({
    required String label,
    required double value,
    required Color color,
    required ValueChanged<double> onChanged,
  }) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(label),
            Text(
              value.toInt().toString(),
              style: TextStyle(color: color, fontWeight: FontWeight.bold),
            ),
          ],
        ),
        Slider(
          value: value,
          min: 0,
          max: 255,
          activeColor: color,
          onChanged: onChanged,
        ),
      ],
    );
  }
}
0
likes
160
points
94
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

An advanced, local vector database for Flutter, powered by Rust and HNSW graph indexing.

Repository (GitHub)
View/report issues
Contributing

Topics

#ffi #vector #ai #database #rust

Funding

Consider supporting this project:

github.com
buymeacoffee.com
ipn.eg

License

GPL-3.0 (license)

Dependencies

flutter_rust_bridge

More

Packages that depend on waffle_db

Packages that implement waffle_db