• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 12+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Step-by-Step Guide to building a custom Elasticsearch search bar block for Gutenberg using Vanilla CSS shadow DOM style layers

Step-by-Step Guide to building a custom Elasticsearch search bar block for Gutenberg using Vanilla CSS shadow DOM style layers

Setting Up the WordPress Plugin and Elasticsearch Connection

We’ll begin by establishing the foundational plugin structure and configuring the Elasticsearch client. This involves creating a basic WordPress plugin and then integrating the official Elasticsearch PHP client to interact with your search cluster.

First, create a new directory for your plugin, for example, custom-es-search, within your WordPress installation’s wp-content/plugins/ directory. Inside this directory, create the main plugin file, custom-es-search.php.

Plugin Initialization

The main plugin file will contain the plugin header and the initialization logic for our Elasticsearch connection.

<?php
/**
 * Plugin Name: Custom Elasticsearch Search Block
 * Description: A Gutenberg block for custom Elasticsearch search with Shadow DOM styling.
 * Version: 1.0
 * Author: Antigravity
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly.
}

// Include the Elasticsearch client library.
require_once __DIR__ . '/vendor/autoload.php';

use Elasticsearch\ClientBuilder;

/**
 * Initialize Elasticsearch client.
 */
function ces_init_elasticsearch_client() {
    // Ensure the client is only initialized once.
    if ( defined( 'CES_ES_CLIENT' ) ) {
        return CES_ES_CLIENT;
    }

    $hosts = [
        'http://localhost:9200', // Replace with your Elasticsearch host(s)
    ];

    try {
        $client = ClientBuilder::create()
            ->setHosts($hosts)
            ->build();

        // Optional: Ping the cluster to verify connection.
        $client->ping();
        define( 'CES_ES_CLIENT', $client );
        return $client;

    } catch ( Exception $e ) {
        // Log the error or display a user-friendly message.
        error_log( 'Elasticsearch connection failed: ' . $e->getMessage() );
        // In a production environment, you might want to disable the block or show a notice.
        define( 'CES_ES_CLIENT_ERROR', true );
        return false;
    }
}

// Hook into WordPress initialization to set up the client.
add_action( 'plugins_loaded', 'ces_init_elasticsearch_client' );

/**
 * Check if Elasticsearch client is available.
 *
 * @return bool
 */
function ces_is_es_available() {
    return defined( 'CES_ES_CLIENT' ) && ! defined( 'CES_ES_CLIENT_ERROR' );
}

// Include block registration and other functionalities later.
?>

To use the Elasticsearch PHP client, you’ll need to install it via Composer. Navigate to your plugin directory in your terminal and run:

composer require elasticsearch/elasticsearch

This will create a vendor directory and download the necessary client libraries. Ensure your WordPress environment has Composer installed and that the vendor/autoload.php file is correctly included.

Registering the Gutenberg Block

Next, we’ll register our custom Gutenberg block. This involves defining the block’s attributes, its editor view, and its frontend rendering. We’ll use JavaScript for the block’s interface and PHP for server-side rendering and data fetching.

Block Registration (PHP)

In your custom-es-search.php file, add the following code to register the block:

/**
 * Register the Elasticsearch Search block.
 */
function ces_register_es_search_block() {
    // Only register if Elasticsearch is available.
    if ( ! ces_is_es_available() ) {
        return;
    }

    register_block_type( 'custom-es-search/es-search-block', array(
        'editor_script' => 'custom-es-search-editor-script',
        'editor_style'  => 'custom-es-search-editor-style',
        'style'         => 'custom-es-search-style',
        'render_callback' => 'ces_render_es_search_block',
        'attributes' => array(
            'indexName' => array(
                'type' => 'string',
                'default' => 'your_wordpress_index', // Default Elasticsearch index
            ),
            'placeholderText' => array(
                'type' => 'string',
                'default' => 'Search...',
            ),
            'maxResults' => array(
                'type' => 'number',
                'default' => 5,
            ),
        ),
    ) );
}
add_action( 'init', 'ces_register_es_search_block' );

/**
 * Enqueue block editor assets.
 */
function ces_enqueue_block_editor_assets() {
    // Only enqueue if Elasticsearch is available.
    if ( ! ces_is_es_available() ) {
        return;
    }

    wp_enqueue_script(
        'custom-es-search-editor-script',
        plugin_dir_url( __FILE__ ) . 'build/index.js',
        array( 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n' ),
        filemtime( plugin_dir_path( __FILE__ ) . 'build/index.js' )
    );

    wp_enqueue_style(
        'custom-es-search-editor-style',
        plugin_dir_url( __FILE__ ) . 'build/index.css',
        array( 'wp-edit-blocks' ),
        filemtime( plugin_dir_path( __FILE__ ) . 'build/index.css' )
    );

    // Pass Elasticsearch index name to the editor script.
    wp_localize_script( 'custom-es-search-editor-script', 'customEsSearch', array(
        'indexName' => 'your_wordpress_index', // This will be overridden by block attributes
    ) );
}
add_action( 'enqueue_block_editor_assets', 'ces_enqueue_block_editor_assets' );

/**
 * Enqueue frontend block assets.
 */
function ces_enqueue_frontend_assets() {
    // Only enqueue if Elasticsearch is available.
    if ( ! ces_is_es_available() ) {
        return;
    }

    wp_enqueue_style(
        'custom-es-search-style',
        plugin_dir_url( __FILE__ ) . 'build/style-index.css',
        array(),
        filemtime( plugin_dir_path( __FILE__ ) . 'build/style-index.css' )
    );
}
add_action( 'wp_enqueue_scripts', 'ces_enqueue_frontend_assets' );

/**
 * Server-side rendering callback for the block.
 *
 * @param array $attributes Block attributes.
 * @return string HTML output.
 */
function ces_render_es_search_block( $attributes ) {
    // Only render if Elasticsearch is available.
    if ( ! ces_is_es_available() ) {
        return '<p>Elasticsearch is not available.</p>';
    }

    $index_name = $attributes['indexName'] ?? 'your_wordpress_index';
    $placeholder_text = $attributes['placeholderText'] ?? 'Search...';
    $max_results = $attributes['maxResults'] ?? 5;

    // The actual search logic will be handled by JavaScript for dynamic results.
    // This PHP callback is primarily for initial rendering and passing attributes.
    ob_start();
    ?>
    <div class="wp-block-custom-es-search-es-search-block">
        <input type="text" class="es-search-input" placeholder="<?php echo esc_attr( $placeholder_text ); ?>" data-index-name="<?php echo esc_attr( $index_name ); ?>" data-max-results="<?php echo esc_attr( $max_results ); ?>" />
        <div class="es-search-results"></div>
    </div>
    <?php
    return ob_get_clean();
}

This code registers a block named custom-es-search/es-search-block. It defines attributes for indexName, placeholderText, and maxResults. It also specifies the JavaScript and CSS files to be enqueued for the editor and frontend, and a render_callback for server-side rendering. The wp_localize_script is used to pass initial data to the JavaScript, though block attributes will take precedence.

Block Editor JavaScript (React)

You’ll need a JavaScript build process (like Webpack) to compile your block’s JavaScript and CSS. Create a src directory in your plugin folder and add index.js and editor.scss (or .css) files.

Here’s a simplified example of src/index.js using React, which is standard for Gutenberg development:

/**
 * WordPress dependencies
 */
const { registerBlockType } = wp.blocks;
const { InspectorControls } = wp.editor;
const { PanelBody, TextControl, RangeControl } = wp.components;
const { __ } = wp.i18n;

/**
 * Internal dependencies
 */
import './editor.scss';

const blockIcon = (
    <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path d="M17.3333 15.3333L13.6667 11.6667C13.3333 11.9333 12.9333 12.1333 12.5 12.2667C11.6667 12.6667 10.6667 12.8667 9.66667 12.8667C7.06667 12.8667 4.66667 10.6667 4.66667 8C4.66667 5.33333 7.06667 3.13333 9.66667 3.13333C12.3333 3.13333 14.6667 5.33333 14.6667 8C14.6667 8.93333 14.4667 9.86667 14.1333 10.6667C13.9333 11.1333 13.7333 11.5333 13.3333 11.8667L17.3333 15.8667C17.5333 16.0667 17.5333 16.3333 17.3333 16.5333C17.1333 16.7333 16.8667 16.7333 16.6667 16.5333L12.6667 12.5333C12.1333 12.7333 11.6667 12.8667 11.1333 12.8667C10.6667 12.8667 10.1333 12.8 9.66667 12.6667C9.13333 12.5333 8.66667 12.3333 8.13333 12C7.66667 11.6667 7.33333 11.1333 7.13333 10.6667C6.93333 10.1333 6.86667 9.66667 6.86667 9.13333C6.86667 7.66667 7.33333 6.33333 8.33333 5.33333C9.33333 4.33333 10.6667 3.86667 12 3.86667C13.3333 3.86667 14.6667 4.33333 15.6667 5.33333C16.6667 6.33333 17.1333 7.66667 17.1333 9C17.1333 9.53333 17.0667 10.0667 16.9333 10.6667C16.8 11.1333 16.6 11.6 16.3333 12C16.0667 12.2667 15.8667 12.5333 15.6667 12.6667L19.6667 16.6667C19.8667 16.8667 19.8667 17.1333 19.6667 17.3333C19.4667 17.5333 19.1333 17.5333 18.9333 17.3333L17.3333 15.7333C17.3333 15.7333 17.3333 15.3333 17.3333 15.3333Z" fill="#333333"/>
    </svg>
);

registerBlockType( 'custom-es-search/es-search-block', {
    title: __( 'Elasticsearch Search Bar', 'custom-es-search' ),
    icon: blockIcon,
    category: 'widgets',
    attributes: {
        indexName: {
            type: 'string',
            default: 'your_wordpress_index',
        },
        placeholderText: {
            type: 'string',
            default: 'Search...',
        },
        maxResults: {
            type: 'number',
            default: 5,
        },
    },

    edit: ( { attributes, setAttributes } ) => {
        const { indexName, placeholderText, maxResults } = attributes;

        return (
            <div className="custom-es-search-editor">
                <InspectorControls>
                    <PanelBody title={ __( 'Elasticsearch Settings', 'custom-es-search' ) } initialOpen={ true }>
                        <TextControl
                            label={ __( 'Elasticsearch Index Name', 'custom-es-search' ) }
                            value={ indexName }
                            onChange={ ( newIndexName ) => setAttributes( { indexName: newIndexName } ) }
                        />
                        <TextControl
                            label={ __( 'Placeholder Text', 'custom-es-search' ) }
                            value={ placeholderText }
                            onChange={ ( newPlaceholderText ) => setAttributes( { placeholderText: newPlaceholderText } ) }
                        />
                        <RangeControl
                            label={ __( 'Max Results', 'custom-es-search' ) }
                            value={ maxResults }
                            onChange={ ( newMaxResults ) => setAttributes( { maxResults: newMaxResults } ) }
                            min={ 1 }
                            max={ 20 }
                        />
                    </PanelBody>
                </InspectorControls>
                <div className="es-search-block-preview">
                    <input
                        type="text"
                        className="es-search-input"
                        placeholder={ placeholderText }
                        readOnly // In editor, we don't perform live search
                        />
                    <div className="es-search-results-preview">
                        { __( 'Search results will appear here...', 'custom-es-search' ) }
                    </div>
                </div>
            </div>
        );
    },

    save: () => {
        // The save function should return null for dynamic blocks.
        // The rendering is handled by the PHP render_callback.
        return null;
    },
} );

In the editor, we use InspectorControls to provide settings for the Elasticsearch index name, placeholder text, and maximum results. The edit function renders a preview of the search bar. The save function returns null because this is a dynamic block, meaning its content is rendered on the server by PHP.

Editor Stylesheet (SCSS)

Create src/editor.scss for the block’s editor styles:

.custom-es-search-editor {
    .es-search-block-preview {
        display: flex;
        flex-direction: column;
        gap: 8px;
        padding: 16px;
        border: 1px dashed #ccc;
        background-color: #f9f9f9;

        .es-search-input {
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 16px;
        }

        .es-search-results-preview {
            padding: 10px;
            border: 1px solid #eee;
            background-color: #fff;
            min-height: 50px;
            color: #888;
            font-style: italic;
        }
    }
}

Implementing Frontend Search Functionality with Shadow DOM

The core of our custom search bar will be its frontend implementation. We’ll use JavaScript to handle user input, query Elasticsearch, and display results. Crucially, we’ll encapsulate the search bar’s styles within a Shadow DOM to prevent conflicts with the WordPress theme or other plugins.

Frontend JavaScript (Vanilla JS)

Create a new file, e.g., src/frontend.js, for the frontend logic. This script will be enqueued when the block appears on the frontend.

document.addEventListener( 'DOMContentLoaded', () => {
    const searchBars = document.querySelectorAll( '.wp-block-custom-es-search-es-search-block' );

    searchBars.forEach( searchBar => {
        const inputElement = searchBar.querySelector( '.es-search-input' );
        const resultsContainer = searchBar.querySelector( '.es-search-results' );

        if ( ! inputElement || ! resultsContainer ) {
            return; // Skip if elements are not found
        }

        const indexName = inputElement.dataset.indexName;
        const maxResults = parseInt( inputElement.dataset.maxResults, 10 );

        // Create Shadow DOM
        const shadowRoot = searchBar.attachShadow( { mode: 'open' } );

        // Clone the input and results container into the Shadow DOM
        const clonedInput = inputElement.cloneNode( true );
        const clonedResultsContainer = resultsContainer.cloneNode( true );

        // Remove original elements from the light DOM
        inputElement.remove();
        resultsContainer.remove();

        // Append cloned elements to the Shadow DOM
        shadowRoot.appendChild( clonedInput );
        shadowRoot.appendChild( clonedResultsContainer );

        // Inject styles into the Shadow DOM
        const style = document.createElement('style');
        style.textContent = `
            /* Styles for the search bar within Shadow DOM */
            :host {
                display: block;
                font-family: sans-serif;
            }
            .es-search-input {
                width: 100%;
                padding: 12px 15px;
                border: 1px solid #ccc;
                border-radius: 5px;
                font-size: 16px;
                box-sizing: border-box; /* Include padding and border in the element's total width and height */
                margin-bottom: 10px;
            }
            .es-search-results {
                border: 1px solid #eee;
                border-radius: 5px;
                background-color: #fff;
                max-height: 300px;
                overflow-y: auto;
                padding: 10px;
            }
            .es-search-results ul {
                list-style: none;
                padding: 0;
                margin: 0;
            }
            .es-search-results li {
                padding: 8px 0;
                border-bottom: 1px solid #f0f0f0;
            }
            .es-search-results li:last-child {
                border-bottom: none;
            }
            .es-search-results a {
                text-decoration: none;
                color: #333;
                display: block;
            }
            .es-search-results a:hover {
                color: #0073aa;
            }
            .es-search-results .no-results {
                color: #888;
                font-style: italic;
            }
        `;
        shadowRoot.appendChild(style);

        // Get references to elements within the Shadow DOM
        const shadowInput = shadowRoot.querySelector( '.es-search-input' );
        const shadowResultsContainer = shadowRoot.querySelector( '.es-search-results' );

        let debounceTimer;

        shadowInput.addEventListener( 'input', () => {
            clearTimeout( debounceTimer );
            const query = shadowInput.value.trim();

            if ( query.length < 2 ) { // Minimum characters to trigger search
                shadowResultsContainer.innerHTML = '';
                return;
            }

            debounceTimer = setTimeout( () => {
                performSearch( query, indexName, maxResults, shadowResultsContainer );
            }, 300 ); // Debounce delay in milliseconds
        } );
    } );
} );

/**
 * Performs the search against Elasticsearch.
 *
 * @param {string} query The search query.
 * @param {string} indexName The Elasticsearch index name.
 * @param {number} maxResults The maximum number of results to return.
 * @param {HTMLElement} resultsContainer The DOM element to display results in.
 */
function performSearch( query, indexName, maxResults, resultsContainer ) {
    // Use WordPress REST API to proxy Elasticsearch requests for security and CORS.
    // This is a crucial step for production environments.
    // The endpoint '/wp-json/custom-es-search/v1/search' needs to be created.
    const endpoint = '/wp-json/custom-es-search/v1/search';

    fetch( endpoint, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify( {
            index: indexName,
            query: query,
            size: maxResults,
        } ),
    } )
    .then( response => {
        if ( ! response.ok ) {
            throw new Error( `HTTP error! status: ${response.status}` );
        }
        return response.json();
    } )
    .then( data => {
        displayResults( data, resultsContainer );
    } )
    .catch( error => {
        console.error( 'Error performing search:', error );
        resultsContainer.innerHTML = '<p class="no-results">Error performing search.</p>';
    } );
}

/**
 * Displays search results in the provided container.
 *
 * @param {object} data The Elasticsearch search response data.
 * @param {HTMLElement} resultsContainer The DOM element to display results in.
 */
function displayResults( data, resultsContainer ) {
    resultsContainer.innerHTML = ''; // Clear previous results

    if ( data.hits && data.hits.hits && data.hits.hits.length > 0 ) {
        const ul = document.createElement( 'ul' );
        data.hits.hits.forEach( hit => {
            const li = document.createElement( 'li' );
            const a = document.createElement( 'a' );

            // Assuming your Elasticsearch documents have 'title' and 'url' fields.
            // Adjust these based on your actual Elasticsearch index mapping.
            const title = hit._source.title || 'Untitled Document';
            const url = hit._source.url || '#'; // Fallback URL

            a.textContent = title;
            a.href = url;
            li.appendChild( a );
            ul.appendChild( li );
        } );
        resultsContainer.appendChild( ul );
    } else {
        resultsContainer.innerHTML = '<p class="no-results">No results found.</p>';
    }
}

This script:

  • Waits for the DOM to be fully loaded.
  • Finds all instances of our block on the page.
  • For each instance, it creates a Shadow DOM.
  • Clones the input and results elements into the Shadow DOM.
  • Injects CSS directly into the Shadow DOM using a <style> tag. This ensures styles are scoped and don’t leak out.
  • Adds an event listener to the input field for the input event.
  • Debounces the input to avoid excessive API calls.
  • Calls performSearch on input.

Frontend Stylesheet (SCSS)

Create src/style.scss for the block’s frontend styles. Note that these styles will be applied to the elements *outside* the Shadow DOM if any, or can be used as a base. The primary styling for the search bar itself is now within the Shadow DOM’s <style> tag in frontend.js.

.wp-block-custom-es-search-es-search-block {
    /* Styles for the container in the light DOM, if needed */
    position: relative; /* Example: for positioning tooltips or other overlays */
}

Securing Elasticsearch Requests via WordPress REST API

Directly exposing Elasticsearch to the frontend is a significant security risk. We must proxy requests through the WordPress REST API. This allows us to validate requests, handle authentication (if necessary), and manage CORS issues.

Creating the REST API Endpoint

Add the following code to your custom-es-search.php file to create a new REST API endpoint:

/**
 * Register REST API route for Elasticsearch search.
 */
function ces_register_rest_route() {
    // Only register if Elasticsearch is available.
    if ( ! ces_is_es_available() ) {
        return;
    }

    register_rest_route( 'custom-es-search/v1', '/search', array(
        'methods'             => 'POST',
        'callback'            => 'ces_handle_es_search_request',
        'permission_callback' => '__return_true', // Adjust for authentication if needed
    ) );
}
add_action( 'rest_api_init', 'ces_register_rest_route' );

/**
 * Handles the Elasticsearch search request from the REST API.
 *
 * @param WP_REST_Request $request Full data about the request.
 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
 */
function ces_handle_es_search_request( WP_REST_Request $request ) {
    // Ensure Elasticsearch client is available.
    if ( ! ces_is_es_available() ) {
        return new WP_Error( 'es_unavailable', 'Elasticsearch client is not available.', array( 'status' => 503 ) );
    }

    $client = CES_ES_CLIENT; // Access the globally defined client

    $index = $request->get_param( 'index' );
    $query = $request->get_param( 'query' );
    $size = (int) $request->get_param( 'size' );

    // Basic validation
    if ( empty( $index ) || empty( $query ) || $size <= 0 ) {
        return new WP_Error( 'invalid_params', 'Invalid parameters provided.', array( 'status' => 400 ) );
    }

    // Sanitize parameters to prevent injection attacks
    $index = sanitize_text_field( $index );
    $query = sanitize_text_field( $query );
    $size = max( 1, min( $size, 20 ) ); // Limit size to prevent abuse

    $params = [
        'index' => $index,
        'body'  => [
            'query' => [
                'multi_match' => [
                    'query' => $query,
                    // Specify fields to search across. Adjust 'title', 'content', etc.
                    // based on your Elasticsearch index mapping.
                    'fields' => [ 'title^3', 'content', 'excerpt' ],
                    'fuzziness' => 'AUTO', // Example: enable fuzzy matching
                ],
            ],
            'size' => $size,
        ],
    ];

    try {
        $response = $client->search( $params );
        return new WP_REST_Response( $response, 200 );

    } catch ( Exception $e ) {
        error_log( 'Elasticsearch search error: ' . $e->getMessage() );
        return new WP_Error( 'es_search_error', 'An error occurred during the search.', array( 'status' => 500 ) );
    }
}

This endpoint:

  • Listens for POST requests on /wp-json/custom-es-search/v1/search.
  • Retrieves the index, query, and size parameters from the request body.
  • Performs basic validation and sanitization.
  • Constructs a multi_match query for Elasticsearch. You’ll need to adjust the fields array to match the fields in your Elasticsearch index that contain searchable content (e.g., post titles, content, custom fields).
  • Uses the CES_ES_CLIENT (our globally available Elasticsearch client) to execute the search.
  • Returns the Elasticsearch

Primary Sidebar

A little about the Author

Having 12+ Years of Experience in Software Development, Vinay is a principal software architect, senior systems engineer, and elite technical consultant. He specializes in bespoke PHP/WordPress development, high-performance Magento 2 & Shopify architectures, custom plugin/theme development from scratch, and legacy code modernization (including VB6, VB.NET, PyQt, and Crystal Reports). Known for solving complex database bottlenecks, speed optimization (Core Web Vitals), and advanced security code auditing, Vinay engineers production-ready systems designed to scale under heavy concurrent load conditions.



Chat on WhatsApp

Recent Posts

  • Debugging Guide: Diagnosing PHP-FPM child process pool exhaustion in multi-site network environments with modern tools
  • Debugging and Resolving complex namespace class loading collisions issues during heavy concurrent database traffic
  • Step-by-Step Guide: Offloading high-frequency customer support tickets metadata writes to a Redis KV store
  • How to refactor legacy event ticket registers queries using modern WP_Query and custom Transient caching
  • Step-by-Step Guide: Offloading high-frequency member profile directories metadata writes to a Redis KV store

Categories

  • apache (1)
  • Business & Monetization (390)
  • Centos (4)
  • Comparisons & Decision Making (55)
  • Debian (2)
  • Debugging & Troubleshooting (662)
  • Desktop Applications (14)
  • DevOps (7)
  • DevOps & Cloud Scaling (962)
  • Django (1)
  • Laravel (4)
  • Migration & Architecture (192)
  • Mobile Applications (24)
  • MySQL (1)
  • Performance & Optimization (873)
  • PHP (5)
  • PHP Development (49)
  • Plugins & Themes (244)
  • Programming Languages (9)
  • Python (20)
  • Ruby on Rails (1)
  • Security & Compliance (647)
  • SEO & Growth (492)
  • Server (118)
  • Ubuntu (9)
  • VB6 & VB.NET (8)
  • Web Applications & Frontend (19)
  • Web Assembly (Wasm) (2)
  • WordPress (22)
  • WordPress Plugin Development (726)
  • WordPress Theme Development (357)

Recent Posts

  • Debugging Guide: Diagnosing PHP-FPM child process pool exhaustion in multi-site network environments with modern tools
  • Debugging and Resolving complex namespace class loading collisions issues during heavy concurrent database traffic
  • Step-by-Step Guide: Offloading high-frequency customer support tickets metadata writes to a Redis KV store

Top Categories

  • DevOps & Cloud Scaling (962)
  • Performance & Optimization (873)
  • WordPress Plugin Development (726)
  • Debugging & Troubleshooting (662)
  • Security & Compliance (647)
  • SEO & Growth (492)

Our Products

  • ERP & LMS Systems (4)
  • Directories & Marketplaces (4)
  • Healthcare Portals (3)
  • Point of Sale (POS) (2)
  • E-Commerce Engines (2)

Our Services

  • E-Commerce Development (10)
  • WordPress Development (8)
  • Python & Desktop GUI (7)
  • General Consulting (7)
  • Legacy Modernization (5)
  • Mobile App Development (4)

Copyright © 2026 · Vinay Vengala