summary refs log tree commit diff
path: root/nixos/modules/services/search
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/services/search')
-rw-r--r--nixos/modules/services/search/elasticsearch-curator.nix95
-rw-r--r--nixos/modules/services/search/elasticsearch.nix239
-rw-r--r--nixos/modules/services/search/hound.nix127
-rw-r--r--nixos/modules/services/search/kibana.nix213
-rw-r--r--nixos/modules/services/search/meilisearch.md39
-rw-r--r--nixos/modules/services/search/meilisearch.nix132
-rw-r--r--nixos/modules/services/search/meilisearch.xml85
-rw-r--r--nixos/modules/services/search/solr.nix110
8 files changed, 1040 insertions, 0 deletions
diff --git a/nixos/modules/services/search/elasticsearch-curator.nix b/nixos/modules/services/search/elasticsearch-curator.nix
new file mode 100644
index 00000000000..bb2612322bb
--- /dev/null
+++ b/nixos/modules/services/search/elasticsearch-curator.nix
@@ -0,0 +1,95 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+    cfg = config.services.elasticsearch-curator;
+    curatorConfig = pkgs.writeTextFile {
+      name = "config.yaml";
+      text = ''
+        ---
+        # Remember, leave a key empty if there is no value.  None will be a string,
+        # not a Python "NoneType"
+        client:
+          hosts: ${builtins.toJSON cfg.hosts}
+          port: ${toString cfg.port}
+          url_prefix:
+          use_ssl: False
+          certificate:
+          client_cert:
+          client_key:
+          ssl_no_validate: False
+          http_auth:
+          timeout: 30
+          master_only: False
+        logging:
+          loglevel: INFO
+          logfile:
+          logformat: default
+          blacklist: ['elasticsearch', 'urllib3']
+        '';
+    };
+    curatorAction = pkgs.writeTextFile {
+      name = "action.yaml";
+      text = cfg.actionYAML;
+    };
+in {
+
+  options.services.elasticsearch-curator = {
+
+    enable = mkEnableOption "elasticsearch curator";
+    interval = mkOption {
+      description = "The frequency to run curator, a systemd.time such as 'hourly'";
+      default = "hourly";
+      type = types.str;
+    };
+    hosts = mkOption {
+      description = "a list of elasticsearch hosts to connect to";
+      type = types.listOf types.str;
+      default = ["localhost"];
+    };
+    port = mkOption {
+      description = "the port that elasticsearch is listening on";
+      type = types.int;
+      default = 9200;
+    };
+    actionYAML = mkOption {
+      description = "curator action.yaml file contents, alternatively use curator-cli which takes a simple action command";
+      type = types.lines;
+      example = ''
+        ---
+        actions:
+          1:
+            action: delete_indices
+            description: >-
+              Delete indices older than 45 days (based on index name), for logstash-
+              prefixed indices. Ignore the error if the filter does not result in an
+              actionable list of indices (ignore_empty_list) and exit cleanly.
+            options:
+              ignore_empty_list: True
+              disable_action: False
+            filters:
+            - filtertype: pattern
+              kind: prefix
+              value: logstash-
+            - filtertype: age
+              source: name
+              direction: older
+              timestring: '%Y.%m.%d'
+              unit: days
+              unit_count: 45
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.elasticsearch-curator = {
+      startAt = cfg.interval;
+      serviceConfig = {
+        ExecStart =
+          "${pkgs.elasticsearch-curator}/bin/curator" +
+          " --config ${curatorConfig} ${curatorAction}";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/search/elasticsearch.nix b/nixos/modules/services/search/elasticsearch.nix
new file mode 100644
index 00000000000..041d0b3c43f
--- /dev/null
+++ b/nixos/modules/services/search/elasticsearch.nix
@@ -0,0 +1,239 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.elasticsearch;
+
+  es7 = builtins.compareVersions cfg.package.version "7" >= 0;
+
+  esConfig = ''
+    network.host: ${cfg.listenAddress}
+    cluster.name: ${cfg.cluster_name}
+    ${lib.optionalString cfg.single_node "discovery.type: single-node"}
+    ${lib.optionalString (cfg.single_node && es7) "gateway.auto_import_dangling_indices: true"}
+
+    http.port: ${toString cfg.port}
+    transport.port: ${toString cfg.tcp_port}
+
+    ${cfg.extraConf}
+  '';
+
+  configDir = cfg.dataDir + "/config";
+
+  elasticsearchYml = pkgs.writeTextFile {
+    name = "elasticsearch.yml";
+    text = esConfig;
+  };
+
+  loggingConfigFilename = "log4j2.properties";
+  loggingConfigFile = pkgs.writeTextFile {
+    name = loggingConfigFilename;
+    text = cfg.logging;
+  };
+
+  esPlugins = pkgs.buildEnv {
+    name = "elasticsearch-plugins";
+    paths = cfg.plugins;
+    postBuild = "${pkgs.coreutils}/bin/mkdir -p $out/plugins";
+  };
+
+in
+{
+
+  ###### interface
+
+  options.services.elasticsearch = {
+    enable = mkOption {
+      description = "Whether to enable elasticsearch.";
+      default = false;
+      type = types.bool;
+    };
+
+    package = mkOption {
+      description = "Elasticsearch package to use.";
+      default = pkgs.elasticsearch;
+      defaultText = literalExpression "pkgs.elasticsearch";
+      type = types.package;
+    };
+
+    listenAddress = mkOption {
+      description = "Elasticsearch listen address.";
+      default = "127.0.0.1";
+      type = types.str;
+    };
+
+    port = mkOption {
+      description = "Elasticsearch port to listen for HTTP traffic.";
+      default = 9200;
+      type = types.int;
+    };
+
+    tcp_port = mkOption {
+      description = "Elasticsearch port for the node to node communication.";
+      default = 9300;
+      type = types.int;
+    };
+
+    cluster_name = mkOption {
+      description = "Elasticsearch name that identifies your cluster for auto-discovery.";
+      default = "elasticsearch";
+      type = types.str;
+    };
+
+    single_node = mkOption {
+      description = "Start a single-node cluster";
+      default = true;
+      type = types.bool;
+    };
+
+    extraConf = mkOption {
+      description = "Extra configuration for elasticsearch.";
+      default = "";
+      type = types.str;
+      example = ''
+        node.name: "elasticsearch"
+        node.master: true
+        node.data: false
+      '';
+    };
+
+    logging = mkOption {
+      description = "Elasticsearch logging configuration.";
+      default = ''
+        logger.action.name = org.elasticsearch.action
+        logger.action.level = info
+
+        appender.console.type = Console
+        appender.console.name = console
+        appender.console.layout.type = PatternLayout
+        appender.console.layout.pattern = [%d{ISO8601}][%-5p][%-25c{1.}] %marker%m%n
+
+        rootLogger.level = info
+        rootLogger.appenderRef.console.ref = console
+      '';
+      type = types.str;
+    };
+
+    dataDir = mkOption {
+      type = types.path;
+      default = "/var/lib/elasticsearch";
+      description = ''
+        Data directory for elasticsearch.
+      '';
+    };
+
+    extraCmdLineOptions = mkOption {
+      description = "Extra command line options for the elasticsearch launcher.";
+      default = [ ];
+      type = types.listOf types.str;
+    };
+
+    extraJavaOptions = mkOption {
+      description = "Extra command line options for Java.";
+      default = [ ];
+      type = types.listOf types.str;
+      example = [ "-Djava.net.preferIPv4Stack=true" ];
+    };
+
+    plugins = mkOption {
+      description = "Extra elasticsearch plugins";
+      default = [ ];
+      type = types.listOf types.package;
+      example = lib.literalExpression "[ pkgs.elasticsearchPlugins.discovery-ec2 ]";
+    };
+
+    restartIfChanged  = mkOption {
+      type = types.bool;
+      description = ''
+        Automatically restart the service on config change.
+        This can be set to false to defer restarts on a server or cluster.
+        Please consider the security implications of inadvertently running an older version,
+        and the possibility of unexpected behavior caused by inconsistent versions across a cluster when disabling this option.
+      '';
+      default = true;
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    systemd.services.elasticsearch = {
+      description = "Elasticsearch Daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      path = [ pkgs.inetutils ];
+      inherit (cfg) restartIfChanged;
+      environment = {
+        ES_HOME = cfg.dataDir;
+        ES_JAVA_OPTS = toString cfg.extraJavaOptions;
+        ES_PATH_CONF = configDir;
+      };
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/elasticsearch ${toString cfg.extraCmdLineOptions}";
+        User = "elasticsearch";
+        PermissionsStartOnly = true;
+        LimitNOFILE = "1024000";
+        Restart = "always";
+        TimeoutStartSec = "infinity";
+      };
+      preStart = ''
+        ${optionalString (!config.boot.isContainer) ''
+          # Only set vm.max_map_count if lower than ES required minimum
+          # This avoids conflict if configured via boot.kernel.sysctl
+          if [ `${pkgs.procps}/bin/sysctl -n vm.max_map_count` -lt 262144 ]; then
+            ${pkgs.procps}/bin/sysctl -w vm.max_map_count=262144
+          fi
+        ''}
+
+        mkdir -m 0700 -p ${cfg.dataDir}
+
+        # Install plugins
+        ln -sfT ${esPlugins}/plugins ${cfg.dataDir}/plugins
+        ln -sfT ${cfg.package}/lib ${cfg.dataDir}/lib
+        ln -sfT ${cfg.package}/modules ${cfg.dataDir}/modules
+
+        # elasticsearch needs to create the elasticsearch.keystore in the config directory
+        # so this directory needs to be writable.
+        mkdir -m 0700 -p ${configDir}
+
+        # Note that we copy config files from the nix store instead of symbolically linking them
+        # because otherwise X-Pack Security will raise the following exception:
+        # java.security.AccessControlException:
+        # access denied ("java.io.FilePermission" "/var/lib/elasticsearch/config/elasticsearch.yml" "read")
+
+        cp ${elasticsearchYml} ${configDir}/elasticsearch.yml
+        # Make sure the logging configuration for old elasticsearch versions is removed:
+        rm -f "${configDir}/logging.yml"
+        cp ${loggingConfigFile} ${configDir}/${loggingConfigFilename}
+        mkdir -p ${configDir}/scripts
+        cp ${cfg.package}/config/jvm.options ${configDir}/jvm.options
+        # redirect jvm logs to the data directory
+        mkdir -m 0700 -p ${cfg.dataDir}/logs
+        ${pkgs.sd}/bin/sd 'logs/gc.log' '${cfg.dataDir}/logs/gc.log' ${configDir}/jvm.options \
+
+        if [ "$(id -u)" = 0 ]; then chown -R elasticsearch:elasticsearch ${cfg.dataDir}; fi
+      '';
+      postStart = ''
+        # Make sure elasticsearch is up and running before dependents
+        # are started
+        while ! ${pkgs.curl}/bin/curl -sS -f http://${cfg.listenAddress}:${toString cfg.port} 2>/dev/null; do
+          sleep 1
+        done
+      '';
+    };
+
+    environment.systemPackages = [ cfg.package ];
+
+    users = {
+      groups.elasticsearch.gid = config.ids.gids.elasticsearch;
+      users.elasticsearch = {
+        uid = config.ids.uids.elasticsearch;
+        description = "Elasticsearch daemon user";
+        home = cfg.dataDir;
+        group = "elasticsearch";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/search/hound.nix b/nixos/modules/services/search/hound.nix
new file mode 100644
index 00000000000..ef62175b0a3
--- /dev/null
+++ b/nixos/modules/services/search/hound.nix
@@ -0,0 +1,127 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.hound;
+in {
+  options = {
+    services.hound = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable the hound code search daemon.
+        '';
+      };
+
+      user = mkOption {
+        default = "hound";
+        type = types.str;
+        description = ''
+          User the hound daemon should execute under.
+        '';
+      };
+
+      group = mkOption {
+        default = "hound";
+        type = types.str;
+        description = ''
+          Group the hound daemon should execute under.
+        '';
+      };
+
+      extraGroups = mkOption {
+        type = types.listOf types.str;
+        default = [ ];
+        example = [ "dialout" ];
+        description = ''
+          List of extra groups that the "hound" user should be a part of.
+        '';
+      };
+
+      home = mkOption {
+        default = "/var/lib/hound";
+        type = types.path;
+        description = ''
+          The path to use as hound's $HOME. If the default user
+          "hound" is configured then this is the home of the "hound"
+          user.
+        '';
+      };
+
+      package = mkOption {
+        default = pkgs.hound;
+        defaultText = literalExpression "pkgs.hound";
+        type = types.package;
+        description = ''
+          Package for running hound.
+        '';
+      };
+
+      config = mkOption {
+        type = types.str;
+        description = ''
+          The full configuration of the Hound daemon. Note the dbpath
+          should be an absolute path to a writable location on disk.
+        '';
+        example = literalExpression ''
+          '''
+            {
+              "max-concurrent-indexers" : 2,
+              "dbpath" : "''${services.hound.home}/data",
+              "repos" : {
+                  "nixpkgs": {
+                    "url" : "https://www.github.com/NixOS/nixpkgs.git"
+                  }
+              }
+            }
+          '''
+        '';
+      };
+
+      listen = mkOption {
+        type = types.str;
+        default = "0.0.0.0:6080";
+        example = "127.0.0.1:6080 or just :6080";
+        description = ''
+          Listen on this IP:port / :port
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.groups = optionalAttrs (cfg.group == "hound") {
+      hound.gid = config.ids.gids.hound;
+    };
+
+    users.users = optionalAttrs (cfg.user == "hound") {
+      hound = {
+        description = "hound code search";
+        createHome = true;
+        home = cfg.home;
+        group = cfg.group;
+        extraGroups = cfg.extraGroups;
+        uid = config.ids.uids.hound;
+      };
+    };
+
+    systemd.services.hound = {
+      description = "Hound Code Search";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        WorkingDirectory = cfg.home;
+        ExecStartPre = "${pkgs.git}/bin/git config --global --replace-all http.sslCAinfo /etc/ssl/certs/ca-certificates.crt";
+        ExecStart = "${cfg.package}/bin/houndd" +
+                    " -addr ${cfg.listen}" +
+                    " -conf ${pkgs.writeText "hound.json" cfg.config}";
+
+      };
+      path = [ pkgs.git pkgs.mercurial pkgs.openssh ];
+    };
+  };
+
+}
diff --git a/nixos/modules/services/search/kibana.nix b/nixos/modules/services/search/kibana.nix
new file mode 100644
index 00000000000..e4ab85be9ef
--- /dev/null
+++ b/nixos/modules/services/search/kibana.nix
@@ -0,0 +1,213 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.kibana;
+  opt = options.services.kibana;
+
+  ge7 = builtins.compareVersions cfg.package.version "7" >= 0;
+  lt6_6 = builtins.compareVersions cfg.package.version "6.6" < 0;
+
+  cfgFile = pkgs.writeText "kibana.json" (builtins.toJSON (
+    (filterAttrsRecursive (n: v: v != null && v != []) ({
+      server.host = cfg.listenAddress;
+      server.port = cfg.port;
+      server.ssl.certificate = cfg.cert;
+      server.ssl.key = cfg.key;
+
+      kibana.index = cfg.index;
+      kibana.defaultAppId = cfg.defaultAppId;
+
+      elasticsearch.url = cfg.elasticsearch.url;
+      elasticsearch.hosts = cfg.elasticsearch.hosts;
+      elasticsearch.username = cfg.elasticsearch.username;
+      elasticsearch.password = cfg.elasticsearch.password;
+
+      elasticsearch.ssl.certificate = cfg.elasticsearch.cert;
+      elasticsearch.ssl.key = cfg.elasticsearch.key;
+      elasticsearch.ssl.certificateAuthorities = cfg.elasticsearch.certificateAuthorities;
+    } // cfg.extraConf)
+  )));
+
+in {
+  options.services.kibana = {
+    enable = mkEnableOption "kibana service";
+
+    listenAddress = mkOption {
+      description = "Kibana listening host";
+      default = "127.0.0.1";
+      type = types.str;
+    };
+
+    port = mkOption {
+      description = "Kibana listening port";
+      default = 5601;
+      type = types.int;
+    };
+
+    cert = mkOption {
+      description = "Kibana ssl certificate.";
+      default = null;
+      type = types.nullOr types.path;
+    };
+
+    key = mkOption {
+      description = "Kibana ssl key.";
+      default = null;
+      type = types.nullOr types.path;
+    };
+
+    index = mkOption {
+      description = "Elasticsearch index to use for saving kibana config.";
+      default = ".kibana";
+      type = types.str;
+    };
+
+    defaultAppId = mkOption {
+      description = "Elasticsearch default application id.";
+      default = "discover";
+      type = types.str;
+    };
+
+    elasticsearch = {
+      url = mkOption {
+        description = ''
+          Elasticsearch url.
+
+          Defaults to <literal>"http://localhost:9200"</literal>.
+
+          Don't set this when using Kibana >= 7.0.0 because it will result in a
+          configuration error. Use <option>services.kibana.elasticsearch.hosts</option>
+          instead.
+        '';
+        default = null;
+        type = types.nullOr types.str;
+      };
+
+      hosts = mkOption {
+        description = ''
+          The URLs of the Elasticsearch instances to use for all your queries.
+          All nodes listed here must be on the same cluster.
+
+          Defaults to <literal>[ "http://localhost:9200" ]</literal>.
+
+          This option is only valid when using kibana >= 6.6.
+        '';
+        default = null;
+        type = types.nullOr (types.listOf types.str);
+      };
+
+      username = mkOption {
+        description = "Username for elasticsearch basic auth.";
+        default = null;
+        type = types.nullOr types.str;
+      };
+
+      password = mkOption {
+        description = "Password for elasticsearch basic auth.";
+        default = null;
+        type = types.nullOr types.str;
+      };
+
+      ca = mkOption {
+        description = ''
+          CA file to auth against elasticsearch.
+
+          It's recommended to use the <option>certificateAuthorities</option> option
+          when using kibana-5.4 or newer.
+        '';
+        default = null;
+        type = types.nullOr types.path;
+      };
+
+      certificateAuthorities = mkOption {
+        description = ''
+          CA files to auth against elasticsearch.
+
+          Please use the <option>ca</option> option when using kibana &lt; 5.4
+          because those old versions don't support setting multiple CA's.
+
+          This defaults to the singleton list [ca] when the <option>ca</option> option is defined.
+        '';
+        default = if cfg.elasticsearch.ca == null then [] else [ca];
+        defaultText = literalExpression ''
+          if config.${opt.elasticsearch.ca} == null then [ ] else [ ca ]
+        '';
+        type = types.listOf types.path;
+      };
+
+      cert = mkOption {
+        description = "Certificate file to auth against elasticsearch.";
+        default = null;
+        type = types.nullOr types.path;
+      };
+
+      key = mkOption {
+        description = "Key file to auth against elasticsearch.";
+        default = null;
+        type = types.nullOr types.path;
+      };
+    };
+
+    package = mkOption {
+      description = "Kibana package to use";
+      default = pkgs.kibana;
+      defaultText = literalExpression "pkgs.kibana";
+      type = types.package;
+    };
+
+    dataDir = mkOption {
+      description = "Kibana data directory";
+      default = "/var/lib/kibana";
+      type = types.path;
+    };
+
+    extraConf = mkOption {
+      description = "Kibana extra configuration";
+      default = {};
+      type = types.attrs;
+    };
+  };
+
+  config = mkIf (cfg.enable) {
+    assertions = [
+      {
+        assertion = ge7 -> cfg.elasticsearch.url == null;
+        message =
+          "The option services.kibana.elasticsearch.url has been removed when using kibana >= 7.0.0. " +
+          "Please use option services.kibana.elasticsearch.hosts instead.";
+      }
+      {
+        assertion = lt6_6 -> cfg.elasticsearch.hosts == null;
+        message =
+          "The option services.kibana.elasticsearch.hosts is only valid for kibana >= 6.6.";
+      }
+    ];
+    systemd.services.kibana = {
+      description = "Kibana Service";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "elasticsearch.service" ];
+      environment = { BABEL_CACHE_PATH = "${cfg.dataDir}/.babelcache.json"; };
+      serviceConfig = {
+        ExecStart =
+          "${cfg.package}/bin/kibana" +
+          " --config ${cfgFile}" +
+          " --path.data ${cfg.dataDir}";
+        User = "kibana";
+        WorkingDirectory = cfg.dataDir;
+      };
+    };
+
+    environment.systemPackages = [ cfg.package ];
+
+    users.users.kibana = {
+      isSystemUser = true;
+      description = "Kibana service user";
+      home = cfg.dataDir;
+      createHome = true;
+      group = "kibana";
+    };
+    users.groups.kibana = {};
+  };
+}
diff --git a/nixos/modules/services/search/meilisearch.md b/nixos/modules/services/search/meilisearch.md
new file mode 100644
index 00000000000..98e7c542cb9
--- /dev/null
+++ b/nixos/modules/services/search/meilisearch.md
@@ -0,0 +1,39 @@
+# Meilisearch {#module-services-meilisearch}
+
+Meilisearch is a lightweight, fast and powerful search engine. Think elastic search with a much smaller footprint.
+
+## Quickstart
+
+the minimum to start meilisearch is
+
+```nix
+services.meilisearch.enable = true;
+```
+
+this will start the http server included with meilisearch on port 7700.
+
+test with `curl -X GET 'http://localhost:7700/health'`
+
+## Usage
+
+you first need to add documents to an index before you can search for documents.
+
+### Add a documents to the `movies` index
+
+`curl -X POST 'http://127.0.0.1:7700/indexes/movies/documents' --data '[{"id": "123", "title": "Superman"}, {"id": 234, "title": "Batman"}]'`
+
+### Search documents in the `movies` index
+
+`curl 'http://127.0.0.1:7700/indexes/movies/search' --data '{ "q": "botman" }'` (note the typo is intentional and there to demonstrate the typo tolerant capabilities)
+
+## Defaults
+
+- The default nixos package doesn't come with the [dashboard](https://docs.meilisearch.com/learn/getting_started/quick_start.html#search), since the dashboard features makes some assets downloads at compile time.
+
+- Anonimized Analytics sent to meilisearch are disabled by default.
+
+- Default deployment is development mode. It doesn't require a secret master key. All routes are not protected and accessible.
+
+## Missing
+
+- the snapshot feature is not yet configurable from the module, it's just a matter of adding the relevant environment variables.
diff --git a/nixos/modules/services/search/meilisearch.nix b/nixos/modules/services/search/meilisearch.nix
new file mode 100644
index 00000000000..f6210f6f16e
--- /dev/null
+++ b/nixos/modules/services/search/meilisearch.nix
@@ -0,0 +1,132 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.meilisearch;
+
+in
+{
+
+  meta.maintainers = with maintainers; [ Br1ght0ne happysalada ];
+  # Don't edit the docbook xml directly, edit the md and generate it:
+  # `pandoc meilisearch.md -t docbook --top-level-division=chapter --extract-media=media -f markdown+smart > meilisearch.xml`
+  meta.doc = ./meilisearch.xml;
+
+  ###### interface
+
+  options.services.meilisearch = {
+    enable = mkEnableOption "MeiliSearch - a RESTful search API";
+
+    package = mkOption {
+      description = "The package to use for meilisearch. Use this if you require specific features to be enabled. The default package has no features.";
+      default = pkgs.meilisearch;
+      defaultText = "pkgs.meilisearch";
+      type = types.package;
+    };
+
+    listenAddress = mkOption {
+      description = "MeiliSearch listen address.";
+      default = "127.0.0.1";
+      type = types.str;
+    };
+
+    listenPort = mkOption {
+      description = "MeiliSearch port to listen on.";
+      default = 7700;
+      type = types.port;
+    };
+
+    environment = mkOption {
+      description = "Defines the running environment of MeiliSearch.";
+      default = "development";
+      type = types.enum [ "development" "production" ];
+    };
+
+    # TODO change this to LoadCredentials once possible
+    masterKeyEnvironmentFile = mkOption {
+      description = ''
+        Path to file which contains the master key.
+        By doing so, all routes will be protected and will require a key to be accessed.
+        If no master key is provided, all routes can be accessed without requiring any key.
+        The format is the following:
+        MEILI_MASTER_KEY=my_secret_key
+      '';
+      default = null;
+      type = with types; nullOr path;
+    };
+
+    noAnalytics = mkOption {
+      description = ''
+        Deactivates analytics.
+        Analytics allow MeiliSearch to know how many users are using MeiliSearch,
+        which versions and which platforms are used.
+        This process is entirely anonymous.
+      '';
+      default = true;
+      type = types.bool;
+    };
+
+    logLevel = mkOption {
+      description = ''
+        Defines how much detail should be present in MeiliSearch's logs.
+        MeiliSearch currently supports four log levels, listed in order of increasing verbosity:
+        - 'ERROR': only log unexpected events indicating MeiliSearch is not functioning as expected
+        - 'WARN:' log all unexpected events, regardless of their severity
+        - 'INFO:' log all events. This is the default value
+        - 'DEBUG': log all events and including detailed information on MeiliSearch's internal processes.
+          Useful when diagnosing issues and debugging
+      '';
+      default = "INFO";
+      type = types.str;
+    };
+
+    maxIndexSize = mkOption {
+      description = ''
+        Sets the maximum size of the index.
+        Value must be given in bytes or explicitly stating a base unit.
+        For example, the default value can be written as 107374182400, '107.7Gb', or '107374 Mb'.
+        Default is 100 GiB
+      '';
+      default = "107374182400";
+      type = types.str;
+    };
+
+    payloadSizeLimit = mkOption {
+      description = ''
+        Sets the maximum size of accepted JSON payloads.
+        Value must be given in bytes or explicitly stating a base unit.
+        For example, the default value can be written as 107374182400, '107.7Gb', or '107374 Mb'.
+        Default is ~ 100 MB
+      '';
+      default = "104857600";
+      type = types.str;
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    systemd.services.meilisearch = {
+      description = "MeiliSearch daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      environment = {
+        MEILI_DB_PATH = "/var/lib/meilisearch";
+        MEILI_HTTP_ADDR = "${cfg.listenAddress}:${toString cfg.listenPort}";
+        MEILI_NO_ANALYTICS = toString cfg.noAnalytics;
+        MEILI_ENV = cfg.environment;
+        MEILI_DUMPS_DIR = "/var/lib/meilisearch/dumps";
+        MEILI_LOG_LEVEL = cfg.logLevel;
+        MEILI_MAX_INDEX_SIZE = cfg.maxIndexSize;
+      };
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/meilisearch";
+        DynamicUser = true;
+        StateDirectory = "meilisearch";
+        EnvironmentFile = mkIf (cfg.masterKeyEnvironmentFile != null) cfg.masterKeyEnvironmentFile;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/search/meilisearch.xml b/nixos/modules/services/search/meilisearch.xml
new file mode 100644
index 00000000000..c1a73f358c2
--- /dev/null
+++ b/nixos/modules/services/search/meilisearch.xml
@@ -0,0 +1,85 @@
+<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-meilisearch">
+  <title>Meilisearch</title>
+  <para>
+    Meilisearch is a lightweight, fast and powerful search engine. Think
+    elastic search with a much smaller footprint.
+  </para>
+  <section xml:id="quickstart">
+    <title>Quickstart</title>
+    <para>
+      the minimum to start meilisearch is
+    </para>
+    <programlisting language="bash">
+services.meilisearch.enable = true;
+</programlisting>
+    <para>
+      this will start the http server included with meilisearch on port
+      7700.
+    </para>
+    <para>
+      test with
+      <literal>curl -X GET 'http://localhost:7700/health'</literal>
+    </para>
+  </section>
+  <section xml:id="usage">
+    <title>Usage</title>
+    <para>
+      you first need to add documents to an index before you can search
+      for documents.
+    </para>
+    <section xml:id="add-a-documents-to-the-movies-index">
+      <title>Add a documents to the <literal>movies</literal>
+      index</title>
+      <para>
+        <literal>curl -X POST 'http://127.0.0.1:7700/indexes/movies/documents' --data '[{&quot;id&quot;: &quot;123&quot;, &quot;title&quot;: &quot;Superman&quot;}, {&quot;id&quot;: 234, &quot;title&quot;: &quot;Batman&quot;}]'</literal>
+      </para>
+    </section>
+    <section xml:id="search-documents-in-the-movies-index">
+      <title>Search documents in the <literal>movies</literal>
+      index</title>
+      <para>
+        <literal>curl 'http://127.0.0.1:7700/indexes/movies/search' --data '{ &quot;q&quot;: &quot;botman&quot; }'</literal>
+        (note the typo is intentional and there to demonstrate the typo
+        tolerant capabilities)
+      </para>
+    </section>
+  </section>
+  <section xml:id="defaults">
+    <title>Defaults</title>
+    <itemizedlist>
+      <listitem>
+        <para>
+          The default nixos package doesn’t come with the
+          <link xlink:href="https://docs.meilisearch.com/learn/getting_started/quick_start.html#search">dashboard</link>,
+          since the dashboard features makes some assets downloads at
+          compile time.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Anonimized Analytics sent to meilisearch are disabled by
+          default.
+        </para>
+      </listitem>
+      <listitem>
+        <para>
+          Default deployment is development mode. It doesn’t require a
+          secret master key. All routes are not protected and
+          accessible.
+        </para>
+      </listitem>
+    </itemizedlist>
+  </section>
+  <section xml:id="missing">
+    <title>Missing</title>
+    <itemizedlist spacing="compact">
+      <listitem>
+        <para>
+          the snapshot feature is not yet configurable from the module,
+          it’s just a matter of adding the relevant environment
+          variables.
+        </para>
+      </listitem>
+    </itemizedlist>
+  </section>
+</chapter>
diff --git a/nixos/modules/services/search/solr.nix b/nixos/modules/services/search/solr.nix
new file mode 100644
index 00000000000..ea76bfc9298
--- /dev/null
+++ b/nixos/modules/services/search/solr.nix
@@ -0,0 +1,110 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.solr;
+
+in
+
+{
+  options = {
+    services.solr = {
+      enable = mkEnableOption "Solr";
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.solr;
+        defaultText = literalExpression "pkgs.solr";
+        description = "Which Solr package to use.";
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 8983;
+        description = "Port on which Solr is ran.";
+      };
+
+      stateDir = mkOption {
+        type = types.path;
+        default = "/var/lib/solr";
+        description = "The solr home directory containing config, data, and logging files.";
+      };
+
+      extraJavaOptions = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "Extra command line options given to the java process running Solr.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "solr";
+        description = "User under which Solr is ran.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "solr";
+        description = "Group under which Solr is ran.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ cfg.package ];
+
+    systemd.services.solr = {
+      after = [ "network.target" "remote-fs.target" "nss-lookup.target" "systemd-journald-dev-log.socket" ];
+      wantedBy = [ "multi-user.target" ];
+
+      environment = {
+        SOLR_HOME = "${cfg.stateDir}/data";
+        LOG4J_PROPS = "${cfg.stateDir}/log4j2.xml";
+        SOLR_LOGS_DIR = "${cfg.stateDir}/logs";
+        SOLR_PORT = "${toString cfg.port}";
+      };
+      path = with pkgs; [
+        gawk
+        procps
+      ];
+      preStart = ''
+        mkdir -p "${cfg.stateDir}/data";
+        mkdir -p "${cfg.stateDir}/logs";
+
+        if ! test -e "${cfg.stateDir}/data/solr.xml"; then
+          install -D -m0640 ${cfg.package}/server/solr/solr.xml "${cfg.stateDir}/data/solr.xml"
+          install -D -m0640 ${cfg.package}/server/solr/zoo.cfg "${cfg.stateDir}/data/zoo.cfg"
+        fi
+
+        if ! test -e "${cfg.stateDir}/log4j2.xml"; then
+          install -D -m0640 ${cfg.package}/server/resources/log4j2.xml "${cfg.stateDir}/log4j2.xml"
+        fi
+      '';
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart="${cfg.package}/bin/solr start -f -a \"${concatStringsSep " " cfg.extraJavaOptions}\"";
+        ExecStop="${cfg.package}/bin/solr stop";
+      };
+    };
+
+    users.users = optionalAttrs (cfg.user == "solr") {
+      solr = {
+        group = cfg.group;
+        home = cfg.stateDir;
+        createHome = true;
+        uid = config.ids.uids.solr;
+      };
+    };
+
+    users.groups = optionalAttrs (cfg.group == "solr") {
+      solr.gid = config.ids.gids.solr;
+    };
+
+  };
+
+}