Groonga 16
Overview
Groonga is an open-source full-text search engine and column store. It is best known as the engine inside Mroonga (a MySQL and MariaDB storage engine) and PGroonga (a PostgreSQL index method). It also runs standalone as an HTTP server, with a command API under /d/* and a browser admin UI.
Groonga indexes text with a choice of tokenizers: TokenBigram for mixed-language corpora that need no dictionary, and MeCab for real Japanese morphological analysis. It normalizes text with a MySQL-compatible normalizer and stems English with Snowball. Because it stores data column by column, it can aggregate large result sets quickly with drilldown.
The SecureAnyCloud image installs the pinned Groonga 16.0.8 from the official Groonga APT repository. It then locks Groonga down for a marketplace appliance, because Groonga's security model needs care. Groonga has no authentication of its own, and its /d/* command API is fully privileged. Anything that can reach the Groonga port can create, read, alter, drop or dump every table. The bundled admin UI is a static client that drives the same API. Upstream's documented position is to run Groonga on a trusted network only.
On this image, Groonga is therefore bound to 127.0.0.1:10043 and is not reachable from the network at all. An nginx reverse proxy on port 80 is the only public listener. It enforces HTTP Basic Auth with a password generated uniquely on first boot, and it denies the destructive /d/shutdown command outright.
What you get:
- Groonga 16.0.8 installed from the official Groonga APT repository, running as the
groonga-server-httpsystemd service - The command API and the bundled admin UI on
:80, fronted by nginx, with Groonga itself bound to loopback only - Per-VM HTTP Basic Auth (user
admin) protecting the admin UI and the entire/d/*command API, with a unique password generated on first boot - A systemd guard that refuses to start Groonga at all if its bind address is ever changed away from loopback
/d/shutdowndenied at the proxy for anonymous and authenticated clients alike, so the daemon lifecycle stays with systemd- nginx held closed until first boot has written the credential, so the public port is never open unprotected
- The
TokenBigramand MeCab tokenizers, the MySQL-compatible normalizer and the Snowball stemming token filter - An empty database at
/var/lib/groonga/db/db, ready for your own collections - The Groonga APT repository left configured, so security updates arrive through unattended-upgrades
- An unauthenticated
/healthzendpoint for Azure Load Balancer health probes - 24/7 SecureAnyCloud support
Prerequisites
You need:
- An active Azure subscription
- An SSH key pair
- A VNet and subnet in the target region
- Standard_B2s (2 vCPU / 4 GiB RAM) as a comfortable starting size. Groonga is a compact C daemon and very light on resources.
- NSG inbound rules that allow
22/tcpfrom your management network and80/tcpfor the admin UI and command API
Do not open 10043/tcp. By design, nothing listens on it externally.
Groonga serves plain HTTP on port 80. For production, terminate TLS in front of it with your own domain and restrict access to trusted IP ranges (see Maintenance).
Step 1 - Deploy from the Azure Marketplace
Sign in to the Azure Portal and choose Create a resource. Search the Marketplace for Groonga by SecureAnyCloud and select Create.
On the Basics tab, pick your subscription, resource group, region and size. Under Administrator account, choose SSH public key and paste your key. Under Inbound port rules, allow SSH (22) and HTTP (80).
Click Review + create, then Create.
Step 2 - Deploy from the Azure CLI
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:groonga-ubuntu-24-04:default:latest
az vm create \
--resource-group <your-rg> \
--name groonga \
--image sac1647283583153:groonga-ubuntu-24-04:default:latest \
--size Standard_B2s \
--admin-username azureuser \
--ssh-key-values ~/.ssh/id_ed25519.pub \
--vnet-name <your-vnet> --subnet <your-subnet> \
--public-ip-sku Standard
az vm open-port --resource-group <your-rg> --name groonga --port 80 --priority 1010Step 3 - Connect to your VM
Connect over SSH:
ssh azureuser@<vm-public-ip>Step 4 - Confirm the services and the loopback binding
Two services matter: groonga-server-http (the search engine) and nginx (the authenticating front door). Check the listener addresses carefully. nginx is on 0.0.0.0:80 and [::]:80, while Groonga is on 127.0.0.1:10043 only.
systemctl is-active groonga-server-http.service nginx.service ss -ltn | grep -E ':(80|10043)\b' groonga --versionStep 5 - Retrieve your per-VM password
First boot generates a unique 24-character password for the Basic Auth user admin and writes it to a root-only file. There is no default password anywhere in the image.
Read the credentials file:
sudo cat /root/groonga-credentials.txtStep 6 - Confirm the health endpoint
nginx serves /healthz statically and leaves it unauthenticated on purpose, so that an Azure Load Balancer probe can reach it. It never touches Groonga and reveals nothing about your data.
Request the endpoint:
curl -s http://127.0.0.1/healthzStep 7 - Confirm the authentication gate
This check matters most for running Groonga safely. Every privileged path must reject an anonymous request with 401. That includes the admin UI at / and the whole command API, including /d/status, /d/table_list and /d/dump. Only the per-VM password gets through.
Send anonymous and authenticated requests:
GROONGA_PASSWORD=$(sudo grep '^GROONGA_PASSWORD=' /root/groonga-credentials.txt | cut -d= -f2-) for path in / /admin/ /d/status /d/table_list /d/dump; do printf 'anonymous %-14s -> HTTP %s\n' "$path" \
"$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1$path")" done curl -s -o /dev/null -w 'wrong password /d/status -> HTTP %{http_code}\n' \
-u 'admin:wrong-password' http://127.0.0.1/d/status curl -s -o /dev/null -w 'per-VM password /d/status -> HTTP %{http_code}\n' \
-u "admin:$GROONGA_PASSWORD" http://127.0.0.1/d/statusStep 8 - Confirm /d/shutdown is denied
Groonga's shutdown command stops the daemon over HTTP. On this image, systemd owns the daemon lifecycle. The proxy therefore denies that one command with 403 for everybody, including authenticated clients, and Groonga keeps running.
Try the command:
GROONGA_PASSWORD=$(sudo grep '^GROONGA_PASSWORD=' /root/groonga-credentials.txt | cut -d= -f2-)
curl -s -o /dev/null -w 'anonymous /d/shutdown -> HTTP %{http_code}\n' http://127.0.0.1/d/shutdown
curl -s -o /dev/null -w 'authenticated /d/shutdown -> HTTP %{http_code}\n' \
-u "admin:$GROONGA_PASSWORD" http://127.0.0.1/d/shutdown systemctl is-active groonga-server-http.serviceTo stop or restart Groonga, use systemd over SSH:
sudo systemctl stop groonga-server-http
sudo systemctl restart groonga-server-httpStep 9 - Confirm Groonga is not reachable from the network
The loopback binding is real, not cosmetic. A connection to this VM's own routable address on the Groonga port is refused. Only nginx answers from outside.
ROUTABLE_IP=$(hostname -I | awk '{print $1}')
curl -s -m 5 -o /dev/null "http://$ROUTABLE_IP:10043/d/status" 2>/dev/null echo "connect to $ROUTABLE_IP:10043 -> curl exit $? (7 means connection refused)" echo "listeners on port 10043:" ss -Hltn 'sport = :10043' | awk '{print " " $4}'Do not edit the bind address if you need Groonga on a different address. An ExecStartPre guard refuses to start the daemon on anything other than loopback, because any other address would expose the unauthenticated privileged command API to the network. Put your own authenticating proxy in front instead.
Step 10 - Create a collection
Build something searchable. A Groonga collection is a table plus columns. Here, Docs is keyed by a short text identifier and holds a title and a body.
GROONGA_PASSWORD=$(sudo grep '^GROONGA_PASSWORD=' /root/groonga-credentials.txt | cut -d= -f2-) grn() { curl -s -u "admin:$GROONGA_PASSWORD" --get "http://127.0.0.1/d/$1" "${@:2}"; } grn table_create --data-urlencode 'name=Docs' --data-urlencode 'flags=TABLE_HASH_KEY' \
--data-urlencode 'key_type=ShortText'; echo grn column_create --data-urlencode 'table=Docs' --data-urlencode 'name=title' \
--data-urlencode 'flags=COLUMN_SCALAR' --data-urlencode 'type=ShortText'; echo grn column_create --data-urlencode 'table=Docs' --data-urlencode 'name=body' \
--data-urlencode 'flags=COLUMN_SCALAR' --data-urlencode 'type=Text'; echoEach command answers [[0,<timestamp>,<elapsed>],true]. The leading 0 is Groonga's return code, and 0 means success.
Step 11 - Add a lexicon and an inverted index
A table on its own can only be scanned. Full-text search needs a lexicon. A lexicon is a second table whose keys are the tokens, with an index column that points back at your documents.
TokenBigram splits text into overlapping two-character grams, so it works without a dictionary. NormalizerAuto folds case and width differences, so a search for Cloud also matches CLOUD.
GROONGA_PASSWORD=$(sudo grep '^GROONGA_PASSWORD=' /root/groonga-credentials.txt | cut -d= -f2-) grn() { curl -s -u "admin:$GROONGA_PASSWORD" --get "http://127.0.0.1/d/$1" "${@:2}"; } grn table_create --data-urlencode 'name=Lexicon' --data-urlencode 'flags=TABLE_PAT_KEY' \
--data-urlencode 'key_type=ShortText' \
--data-urlencode 'default_tokenizer=TokenBigram' \
--data-urlencode 'normalizer=NormalizerAuto'; echo grn column_create --data-urlencode 'table=Lexicon' --data-urlencode 'name=docs_index' \
--data-urlencode 'flags=COLUMN_INDEX|WITH_POSITION|WITH_SECTION' \
--data-urlencode 'type=Docs' --data-urlencode 'source=title,body'; echoWITH_POSITION records where each token occurs, which makes phrase search possible. WITH_SECTION lets one index column cover several source columns, here both title and body.
Step 12 - Load records
Load records as a JSON array with /d/load.
Set Content-Type: application/json. This header is not optional, and omitting it is the most common way to lose an import. With curl's default application/x-www-form-urlencoded, Groonga loads zero records and still answers with a success envelope, so the load looks like it worked.
Compare the number after the closing bracket. That is the count actually loaded, and it must match the number of records you sent.
GROONGA_PASSWORD=$(sudo grep '^GROONGA_PASSWORD=' /root/groonga-credentials.txt | cut -d= -f2-)
curl -s -u "admin:$GROONGA_PASSWORD" -X POST \
-H 'Content-Type: application/json' \
--data-binary '[ {"_key":"d1","title":"Indexing text with a bigram tokenizer","body":"A bigram tokenizer splits text into overlapping two character grams, so it can index any language without a dictionary."}, {"_key":"d2","title":"Normalizing before you index","body":"A normalizer folds case and width differences so a query matches regardless of how it was typed."}, {"_key":"d3","title":"Storing telemetry in a column store","body":"Because values of one column sit together on disk, aggregating a large result set stays fast."} ]' \
'http://127.0.0.1/d/load?table=Docs'; echoThe response ends in ,3] - three records loaded.
Step 13 - Run a full text search
Use match_columns to say which columns to search, query for what to look for, and output_columns to select what comes back.
GROONGA_PASSWORD=$(sudo grep '^GROONGA_PASSWORD=' /root/groonga-credentials.txt | cut -d= -f2-) grn() { curl -s -u "admin:$GROONGA_PASSWORD" --get "http://127.0.0.1/d/$1" "${@:2}"; } grn select --data-urlencode 'table=Docs' --data-urlencode 'match_columns=title,body' \
--data-urlencode 'query=tokenizer' \
--data-urlencode 'output_columns=_key,title'; echoOnly d1 comes back, because the index discriminates instead of returning everything. Try query=normalizer or query=column store to see the match set change.
Step 14 - Aggregate results with drilldown
drilldown groups the matching set by a column value and returns counts alongside the results. This is where the column store pays off. The response carries two result sets: your records, then the aggregation.
GROONGA_PASSWORD=$(sudo grep '^GROONGA_PASSWORD=' /root/groonga-credentials.txt | cut -d= -f2-) grn() { curl -s -u "admin:$GROONGA_PASSWORD" --get "http://127.0.0.1/d/$1" "${@:2}"; } grn select --data-urlencode 'table=Docs' --data-urlencode 'match_columns=title,body' \
--data-urlencode 'query=index OR column' \
--data-urlencode 'output_columns=_key,title' \
--data-urlencode 'drilldown=title' | python3 -c ' import json,sys d = json.load(sys.stdin); body = d[1] print("matched:", body[0][0][0]) print("grouped by title:") for row in body[1][2:]: print(" ", row[0], "->", row[1]) 'The bundled admin UI sends the drilldown parameters but renders only the record set. Use the command API to see the aggregation itself.
Step 15 - Tidy up the tutorial collection
Remove the index and the tables. This leaves the database exactly as the image shipped it.
GROONGA_PASSWORD=$(sudo grep '^GROONGA_PASSWORD=' /root/groonga-credentials.txt | cut -d= -f2-) grn() { curl -s -u "admin:$GROONGA_PASSWORD" --get "http://127.0.0.1/d/$1" "${@:2}"; } grn table_remove --data-urlencode 'name=Lexicon'; echo grn table_remove --data-urlencode 'name=Docs'; echo grn table_list; echoStep 16 - Open the admin UI
Browse to http://<vm-public-ip>/ and sign in with admin and the password from Step 5. The Summary view reports the daemon's start time, uptime, number of queries served and cache hit rate.
The admin UI screenshots below show a sample Articles collection paired with a Terms lexicon. It has the same shape as the Docs and Lexicon pair you built in Steps 10 and 11, with more records.
Step 17 - Inspect your tables
List table shows every table with its flags, key type, tokenizer and normalizer. A document table is typically TABLE_HASH_KEY. A lexicon is TABLE_PAT_KEY with a tokenizer and normalizer set, here TokenBigram and NormalizerAuto.
Step 18 - Search from the browser
Pick a table in the side menu to open List records. The Query box accepts Groonga's query syntax, so body:@index means "the body column contains the word index".
Tick Advanced search to fill in match_columns, filter, sortby, output_columns and the drilldown parameters directly.
Step 19 - Inspect the inverted index
Select the lexicon and open List columns. The index column is what makes search work. Its type is index, its flags are COLUMN_INDEX|WITH_SECTION|WITH_POSITION, its domain is the lexicon and its range is the document table it points at.
Maintenance
Back up and restore. dump writes the schema and every record as a stream of Groonga commands. To restore, replay the stream into a new database.
GROONGA_PASSWORD=$(sudo grep '^GROONGA_PASSWORD=' /root/groonga-credentials.txt | cut -d= -f2-)
curl -s -u "admin:$GROONGA_PASSWORD" http://127.0.0.1/d/dump > groonga-backup.grn
sudo systemctl stop groonga-server-http
sudo -u groonga groonga /var/lib/groonga/db/db < groonga-backup.grn
sudo systemctl start groonga-server-httpRotate the Basic Auth password. Replace the htpasswd entry and record the new value.
sudo htpasswd -B /etc/nginx/.groonga.htpasswd admin
sudo chown root:www-data /etc/nginx/.groonga.htpasswd
sudo chmod 640 /etc/nginx/.groonga.htpasswd
sudo systemctl reload nginxAdd TLS and a domain. Point a DNS record at the VM, open 443/tcp, then let certbot configure nginx. Do this before you expose the VM to the internet, because Basic Auth over plain HTTP sends the password in every request.
sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d <your-domain> --redirectRestrict who can reach port 80. Limit the source ranges even though Basic Auth is in place.
az network nsg rule update --resource-group <your-rg> --nsg-name <your-nsg> \
--name allow-http --source-address-prefixes <your-office-cidr>Keep Groonga patched. The official Groonga APT repository stays configured on this image, so Groonga updates arrive with the rest of the system through unattended-upgrades. To apply them manually:
sudo apt-get update && sudo apt-get upgrade -y sudo systemctl restart groonga-server-httpMove the database to a larger disk. The database lives on the OS disk at /var/lib/groonga/db. For a large corpus, attach an Azure data disk, then format and mount it. Relocate the database and keep its ownership and mode intact.
sudo systemctl stop groonga-server-http
sudo rsync -a /var/lib/groonga/ /mnt/groonga-data/
sudo mv /var/lib/groonga /var/lib/groonga.old
sudo ln -s /mnt/groonga-data /var/lib/groonga
sudo chown -R groonga:groonga /mnt/groonga-data
sudo chmod 750 /mnt/groonga-data/db
sudo systemctl start groonga-server-httpLogs. Groonga writes a daemon log and a query log; nginx records access and errors.
sudo tail -n 50 /var/log/groonga/groonga-http.log
sudo tail -n 50 /var/log/groonga/query-http.log
sudo journalctl -u groonga-server-http -n 50 --no-pager sudo tail -n 50 /var/log/nginx/error.logSupport
SecureAnyCloud provides 24/7 support for this image. Visit secureanycloud.com or email sales@secureanycloud.com.
Deploy on Azure
Launch Groonga 16 on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com