Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
brig user search can now be served from PostgreSQL (`searchBackend: postgres`); ElasticSearch remains the default backend and is deprecated.
22 changes: 22 additions & 0 deletions docs/src/developer/reference/config-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -1875,6 +1875,28 @@ accessible to services (and not the private key.)
The corresponding Cassandra options are described in Cassandra’s documentation:
[client_encryption_options](https://cassandra.apache.org/doc/stable/cassandra/configuration/cass_yaml_file.html#client_encryption_options)

## Configure brig's user search backend

brig's user search can be served from two backends, selected with the
`searchBackend` option in brig's config:

- `elasticsearch` (the default): users are indexed into ElasticSearch and
search is served from there. This backend is **deprecated** and will be
removed in a future release.
- `postgres`: search is served directly from brig's PostgreSQL user store and
brig stops writing to the ElasticSearch index (so the index becomes stale).

```yaml
brig:
config:
searchBackend: postgres
```

The `elasticsearch` config block remains required, but it is ignored when
`searchBackend: postgres`. See
[the migration notes](../../how-to/install/infrastructure-configuration.md)
for how to migrate an existing installation.

## Configure Elasticsearch basic authentication

When the Wire backend is configured to work against a custom Elasticsearch
Expand Down
37 changes: 37 additions & 0 deletions docs/src/how-to/install/infrastructure-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,43 @@ adding this information for your cloud provider, feel free to read the
creating such automation, feel free to read the [contributing
guidelines](https://github.com/wireapp/wire-server-deploy/blob/master/CONTRIBUTING.md) and open a PR.

## ElasticSearch user-search backend deprecation

The ElasticSearch-based user search backend of brig is **deprecated** and will
be removed in a future release. brig can now serve user search directly from
its PostgreSQL user store by setting `searchBackend: postgres` in brig's
config; ElasticSearch remains the default (`searchBackend: elasticsearch`).

To migrate an existing installation:

1. Upgrade wire-server: the database migration
`20260911000000-user-search-postgres.sql` adds the `wire_user.name_normalized`
column (with indexes) and the `team_search_visibility` table.
2. Run the one-off backfill (this only writes to PostgreSQL, the
ElasticSearch index keeps serving search in the meantime):

```bash
brig-index backfill-normalized-names --pg-settings "host=... dbname=... user=... password=..."
```

Note: brig itself maintains `name_normalized` on every user creation and
name change, so the one-off backfill only needs to cover users that
existed before this upgrade.
3. Set `searchBackend: postgres` in brig's config. brig will stop writing
to the ElasticSearch index.
4. After verifying that search works as expected, ElasticSearch can be
decommissioned.

Note: the `team_search_visibility` table starts out empty. Teams that
currently have the inbound search-visibility feature enabled should
toggle it off and on again after cutover; otherwise inbound search
silently falls back to own-team-only for those teams.

To roll back, set `searchBackend: elasticsearch` again: the index is only kept
fresh while brig runs with the ElasticSearch backend, so user changes made
while running with `searchBackend: postgres` will only appear in the index
after a re-index (e.g. `brig-index reindex`).

## Persistence and high-availability

Currently, due to the way kubernetes and cassandra
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
-- Stores the ICU-transliterated, lowercased display name
-- (see 'Wire.UserSearch.Normalize.normalized'). Required for
-- case/diacritic-insensitive prefix search of user names, which plain
-- lower() cannot provide ("Björn" -> "bjorn").
ALTER TABLE wire_user ADD COLUMN name_normalized text;

-- The CREATE INDEX CONCURRENTLY statements for this migration live in
-- 2026091100000{1,2,3}-user-search-*.sql: they cannot run inside a
-- transaction and are registered in 'Wire.PostgresMigrations.nonTransactionMigrations'
-- (one statement per file: statements within a single script run in one
-- implicit transaction, which CREATE INDEX CONCURRENTLY forbids).

-- Replaces the per-user search_visibility_inbound field formerly
-- denormalized into the ElasticSearch user documents. One row per team;
-- a missing row means 'SearchableByOwnTeam'.
CREATE TABLE team_search_visibility (
team uuid PRIMARY KEY,
search_visibility_inbound integer NOT NULL
);

-- Deployment note: rows created before this migration have a NULL
-- name_normalized. Run `brig-index backfill-normalized-names` once before
-- switching user search over to Postgres, otherwise name search misses
-- every pre-migration user (handle/email search are unaffected).
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2026 Wire Swiss GmbH <opensource@wire.com>
--
-- This program is free software: you can redistribute it and/or modify it under
-- the terms of the GNU Affero General Public License as published by the Free
-- Software Foundation, either version 3 of the License, or (at your option) any
-- later version.
--
-- This program is distributed in the hope that it will be useful, but WITHOUT
-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-- details.
--
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see <https://www.gnu.org/licenses/>.

-- Prefix-search index for name_normalized (LIKE 'abc%', no pg_trgm needed);
-- companion to 20260911000000-user-search-postgres.sql. Runs outside a
-- transaction (CREATE INDEX CONCURRENTLY); see
-- 'Wire.PostgresMigrations.nonTransactionMigrations'. One statement per file:
-- statements within a single script run in one implicit transaction, which
-- CREATE INDEX CONCURRENTLY forbids.
CREATE INDEX CONCURRENTLY IF NOT EXISTS wire_user_name_normalized_pattern_idx ON wire_user (name_normalized text_pattern_ops);
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2026 Wire Swiss GmbH <opensource@wire.com>
--
-- This program is free software: you can redistribute it and/or modify it under
-- the terms of the GNU Affero General Public License as published by the Free
-- Software Foundation, either version 3 of the License, or (at your option) any
-- later version.
--
-- This program is distributed in the hope that it will be useful, but WITHOUT
-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-- details.
--
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see <https://www.gnu.org/licenses/>.

-- Prefix-search index on lower(handle); companion to
-- 20260911000000-user-search-postgres.sql. Runs outside a transaction
-- (CREATE INDEX CONCURRENTLY); see
-- 'Wire.PostgresMigrations.nonTransactionMigrations'. One statement per file:
-- statements within a single script run in one implicit transaction, which
-- CREATE INDEX CONCURRENTLY forbids.
CREATE INDEX CONCURRENTLY IF NOT EXISTS wire_user_lower_handle_pattern_idx ON wire_user (lower(handle) text_pattern_ops);
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2026 Wire Swiss GmbH <opensource@wire.com>
--
-- This program is free software: you can redistribute it and/or modify it under
-- the terms of the GNU Affero General Public License as published by the Free
-- Software Foundation, either version 3 of the License, or (at your option) any
-- later version.
--
-- This program is distributed in the hope that it will be useful, but WITHOUT
-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-- details.
--
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see <https://www.gnu.org/licenses/>.

-- Index on wire_user.team; companion to
-- 20260911000000-user-search-postgres.sql. Runs outside a transaction
-- (CREATE INDEX CONCURRENTLY); see
-- 'Wire.PostgresMigrations.nonTransactionMigrations'. One statement per file:
-- statements within a single script run in one implicit transaction, which
-- CREATE INDEX CONCURRENTLY forbids.
CREATE INDEX CONCURRENTLY IF NOT EXISTS wire_user_team_idx ON wire_user (team);
5 changes: 4 additions & 1 deletion libs/wire-subsystems/src/Wire/PostgresMigrations.hs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ nonTransactionMigrations =
Set.fromList
[ "20260428072649-create-conv-parent-index.sql",
"20260708090000-meetings-recurrence-eff-end-index.sql",
"20260708100000-meetings-end-time-nonrecurring-index.sql"
"20260708100000-meetings-end-time-nonrecurring-index.sql",
"20260911000001-user-search-name-normalized-index.sql",
"20260911000002-user-search-handle-index.sql",
"20260911000003-user-search-team-index.sql"
]

data PostgresMigrationError
Expand Down
35 changes: 35 additions & 0 deletions libs/wire-subsystems/src/Wire/UserSearch/Normalize.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2026 Wire Swiss GmbH <opensource@wire.com>
--
-- This program is free software: you can redistribute it and/or modify it under
-- the terms of the GNU Affero General Public License as published by the Free
-- Software Foundation, either version 3 of the License, or (at your option) any
-- later version.
--
-- This program is distributed in the hope that it will be useful, but WITHOUT
-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-- details.
--
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see <https://www.gnu.org/licenses/>.

module Wire.UserSearch.Normalize
( normalized,
)
where

import Data.Text (Text)
import Data.Text.ICU.Translit (trans, transliterate)

-- | Normalizes a name (or search term) for matching: transliterate to
-- Latin, strip diacritics, lowercase. ("Björn" -> "bjorn")
--
-- This is the same function that used to be applied when writing the
-- ElasticSearch user documents (formerly 'Wire.UserStore.IndexUser.normalized');
-- it is applied wherever @wire_user.name_normalized@ is written (brig's
-- user store inserts/updates and the one-off brig-index backfill) and
-- when building search queries.
normalized :: Text -> Text
normalized = transliterate (trans "Any-Latin; Latin-ASCII; Lower")
71 changes: 71 additions & 0 deletions libs/wire-subsystems/src/Wire/UserSearchStore.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{-# LANGUAGE TemplateHaskell #-}

-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2026 Wire Swiss GmbH <opensource@wire.com>
--
-- This program is free software: you can redistribute it and/or modify it under
-- the terms of the GNU Affero General Public License as published by the Free
-- Software Foundation, either version 3 of the License, or (at your option) any
-- later version.
--
-- This program is distributed in the hope that it will be useful, but WITHOUT
-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-- details.
--
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see <https://www.gnu.org/licenses/>.

module Wire.UserSearchStore where

import Data.Id
import Data.Qualified (Local)
import Imports
import Polysemy
import Wire.API.Team.Size
import Wire.API.User.Search
import Wire.UserSearch.Types (BrowseTeamFilters, SearchVisibilityInbound, TeamSearchInfo)

-- | User search queries backed directly by brig's user store (Postgres),
-- replacing the former ElasticSearch-backed 'Wire.IndexedUserStore'.
data UserSearchStore m a where
-- | Full-text search over user names and handles (prefix matching, see the
-- swagger docs of @/users/search@ for rank ordering). Excludes the searcher
-- and exact-handle matches (the latter are fetched from the user store by
-- the caller).
SearchUsers ::
-- | The searcher; its domain qualifies the returned contacts.
Local UserId ->
Maybe TeamId ->
TeamSearchInfo ->
Text ->
Int ->
Maybe [UserTypeFilter] ->
UserSearchStore m (SearchResult Contact)
-- | Team member browse (formerly @/teams/:tid/browse@ via ES). Fills
-- 'TeamContact.teamContactRole'; 'teamContactUserGroups' is left empty and is
-- filled by the caller.
PaginateTeamMembers ::
BrowseTeamFilters ->
Int ->
Maybe PagingState ->
UserSearchStore m (SearchResult TeamContact)
-- | Number of activated, non-deleted team members, split by regular users
-- and apps. Used for max-team-size enforcement.
GetTeamSize :: TeamId -> UserSearchStore m TeamSize
-- | Inbound federated search (a remote backend searching this backend).
-- @Nothing@ allows non-team users and members of teams opted in to
-- searchable-by-all-teams; @Just []@ matches nothing; @Just teams@ allows
-- only opted-in members of the given teams.
SearchUsersFederated ::
Maybe [TeamId] ->
Text ->
Int ->
Maybe [UserTypeFilter] ->
UserSearchStore m (SearchResult Contact)
-- | Upsert the inbound search visibility setting of a team (pushed from
-- galley when the corresponding team feature changes).
SetTeamSearchVisibilityInbound :: TeamId -> SearchVisibilityInbound -> UserSearchStore m ()

makeSem ''UserSearchStore
60 changes: 60 additions & 0 deletions libs/wire-subsystems/src/Wire/UserSearchStore/ElasticSearch.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2026 Wire Swiss GmbH <opensource@wire.com>
--
-- This program is free software: you can redistribute it and/or modify it under
-- the terms of the GNU Affero General Public License as published by the Free
-- Software Foundation, either version 3 of the License, or (at your option) any
-- later version.
--
-- This program is distributed in the hope that it will be useful, but WITHOUT
-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-- details.
--
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see <https://www.gnu.org/licenses/>.

-- | ElasticSearch adapter for 'UserSearchStore'.
--
-- Delegates to the 'IndexedUserStore' effect (the ElasticSearch-backed
-- implementation), converting 'UserDoc' results to contacts exactly like the
-- pre-rework local search did. This keeps ElasticSearch the default search
-- backend while 'Wire.UserSearchStore.Postgres' is being rolled out.
module Wire.UserSearchStore.ElasticSearch
( interpretUserSearchStoreElasticSearch,
)
where

import Data.Id
import Data.Qualified
import Imports
import Polysemy
import Wire.API.User (fromName)
import Wire.API.User.Search
import Wire.IndexedUserStore (IndexedUserStore)
import Wire.IndexedUserStore qualified as IndexedUserStore
import Wire.UserSearch.Types
import Wire.UserSearchStore

interpretUserSearchStoreElasticSearch ::
(Member IndexedUserStore r) =>
InterpreterFor UserSearchStore r
interpretUserSearchStoreElasticSearch = interpret \case
SearchUsers luid mtid info q n mtypes ->
fmap (fmap (docToContact luid)) (IndexedUserStore.searchUsers (tUnqualified luid) mtid info q n mtypes)
PaginateTeamMembers filters maxResults paging ->
fmap (userDocToTeamContact []) <$> IndexedUserStore.paginateTeamMembers filters maxResults paging
GetTeamSize tid -> IndexedUserStore.getTeamSize tid
SearchUsersFederated {} ->
error "Wire.UserSearchStore.ElasticSearch: federated search goes through Brig.User.Search.SearchIndex when searchBackend=elasticsearch"
SetTeamSearchVisibilityInbound tid vis ->
IndexedUserStore.updateTeamSearchVisibilityInbound tid vis

docToContact :: Local UserId -> UserDoc -> Contact
docToContact luid userDoc =
runIdentity $
userDocToContact
(tUntagged $ qualifyAs luid userDoc.udId)
(Identity . maybe "" fromName)
userDoc
Loading
Loading