mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-26 09:28:35 +08:00
## Overview
This PR introduces comprehensive search functionality for chat messages,
enabling users to search through their chat history both globally across
all accessible channels and within specific channels.
### Search Capabilities
**All-Channel Search**: When no channel is specified, users can search
across all channels they have access to. The search respects channel
permissions through `ChannelFetcher.all_secured_channel_ids`, ensuring
users only see results from channels they can view.
**Per-Channel Search**: Users can scope their search to a specific
channel by providing a `channel_id` parameter, useful for finding
messages within a particular conversation context.
**Search Features**:
- Full-text search using PostgreSQL's tsvector/tsquery
- Advanced filters: `@username` to filter by author, `#channel` to
filter by channel slug
- Sort options: relevance (default) or latest
- Pagination support
- Search data weighted by relevance
## Site Setting: `chat_search_enabled`
This feature is gated behind the `chat_search_enabled` site setting,
which is currently:
- **Default**: `false`
- **Hidden**: `true`
- **Client-accessible**: `true`
### Deployment Strategy
Due to the need for chat messages to be indexed before search becomes
useful, we're implementing a two-phase deployment:
**Phase 1 (Initial Merge)**:
- `chat_search_enabled` remains `false` and hidden
- The `register_search_index` uses default (true) instead of `chat_search_enabled` value
- This allows the reindexing infrastructure to begin indexing existing
chat messages even if we don't show the UI yet
**Wait Period**:
- Wait at least one week after Phase 1 deployment
- `Jobs::ReindexSearch` runs every 2 hours and will progressively index
all chat messages
- This ensures most sites have a significant part of their chat history indexed
**Phase 2 (Follow-up Merge)**:
- Set `chat_search_enabled` default to `true` and unhide it
- Update the `register_search_index` enabled proc uses the default
(true) instead of using the `chat_search_enabled` setting
- Users can now access search with pre-indexed data
**Rationale**: Without this phased approach, users would see the search
UI immediately but receive no results until the reindexing job runs,
creating a confusing experience. By pre-indexing while the UI is hidden,
we ensure search works immediately when enabled.
## New Plugin API: `register_search_index`
This PR introduces a new plugin API that allows plugins to register
custom search indexes that integrate seamlessly with Discourse's search
infrastructure.
### API Signature
```ruby
register_search_index(
model_class:, # The ActiveRecord model to index
search_data_class:, # The model for storing search data
index_version:, # Version number for re-indexing
search_data:, # Proc that returns weighted search data
load_unindexed_record_ids:,# Proc that finds records needing indexing
enabled: # Optional proc to enable/disable (default: -> { true })
)
```
### How It Works
**Integration with SearchIndexer**: When `SearchIndexer.index(obj)` is
called, it checks registered search handlers for the object's type. If a
handler matches, it:
1. Calls the `search_data` proc with the object and an `IndexerHelper`
instance
2. Receives weighted search data (`:a_weight`, `:b_weight`, `:c_weight`,
`:d_weight`)
3. Updates the corresponding search data table with PostgreSQL's
tsvector
**Integration with Jobs::ReindexSearch**: The scheduled job (runs every
2 hours) calls `rebuild_registered_search_handlers`, which:
1. Iterates through all registered search handlers
2. Skips handlers where `enabled` proc returns `false`
3. Calls `load_unindexed_record_ids` to find records needing indexing
4. Indexes up to `limit` records per handler (default: 10,000)
### Chat Implementation Example
```ruby
register_search_index(
model_class: Chat::Message,
search_data_class: Chat::MessageSearchData,
index_version: 1,
search_data: proc { |message, indexer_helper|
{
a_weight: message.message,
d_weight: indexer_helper.scrub_html(message.cooked)[0..600_000]
}
},
load_unindexed_record_ids: proc { |limit:, index_version:|
Chat::Message
.joins("LEFT JOIN chat_message_search_data ON chat_message_id = chat_messages.id")
.where(
"chat_message_search_data.locale IS NULL OR
chat_message_search_data.locale != ? OR
chat_message_search_data.version != ?",
SiteSetting.default_locale,
index_version
)
.order("chat_messages.id ASC")
.limit(limit)
.pluck(:id)
}
)
```
Co-authored-by: Martin Brennan <mjrbrennan@gmail.com>
Co-authored-by: Loïc Guitaut <5648+Flink@users.noreply.github.com>
131 lines
6.1 KiB
Ruby
Vendored
131 lines
6.1 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
Chat::Engine.routes.draw do
|
|
namespace :api, defaults: { format: :json } do
|
|
get "/chatables" => "chatables#index"
|
|
get "/channels" => "channels#index"
|
|
get "/me/channels" => "current_user_channels#index"
|
|
get "/me/threads" => "current_user_threads#index"
|
|
post "/channels" => "channels#create"
|
|
put "/channels/read" => "channels_read#update_all"
|
|
put "/channels/:channel_id/read" => "channels_read#update"
|
|
post "/channels/:channel_id/messages/:message_id/flags" => "channels_messages_flags#create"
|
|
post "/channels/:channel_id/drafts" => "channels_drafts#create"
|
|
post "/channels/:channel_id/messages/:message_id/interactions" =>
|
|
"channels_messages_interactions#create"
|
|
delete "/channels/:channel_id" => "channels#destroy"
|
|
put "/channels/:channel_id" => "channels#update"
|
|
get "/channels/:channel_id" => "channels#show"
|
|
put "/channels/:channel_id/status" => "channels_status#update"
|
|
get "/channels/:channel_id/messages" => "channel_messages#index"
|
|
get "/search" => "search#index"
|
|
put "/channels/:channel_id/messages/:message_id" => "channel_messages#update"
|
|
post "/channels/:channel_id/messages/moves" => "channels_messages_moves#create"
|
|
delete "/channels/:channel_id/messages/:message_id/streaming" =>
|
|
"channels_messages_streaming#destroy"
|
|
post "/channels/:channel_id/invites" => "channels_invites#create"
|
|
post "/channels/:channel_id/archives" => "channels_archives#create"
|
|
get "/channels/:channel_id/memberships" => "channels_memberships#index"
|
|
post "/channels/:channel_id/memberships" => "channels_memberships#create"
|
|
delete "/channels/:channel_id/memberships/me" => "channels_current_user_membership#destroy"
|
|
delete "/channels/:channel_id/memberships/:user_id" => "channels_memberships#destroy"
|
|
delete "/channels/:channel_id/memberships/me/follows" =>
|
|
"channels_current_user_membership_follows#destroy"
|
|
put "/channels/:channel_id/memberships/me" => "channels_current_user_membership#update"
|
|
post "/channels/:channel_id/memberships/me" => "channels_current_user_membership#create"
|
|
put "/channels/:channel_id/notifications-settings/me" =>
|
|
"channels_current_user_notifications_settings#update"
|
|
|
|
# Category chatables controller hints. Only used by staff members, we don't want to leak category permissions.
|
|
get "/category-chatables/:id/permissions" => "category_chatables#permissions",
|
|
:format => :json,
|
|
:constraints => StaffConstraint.new
|
|
|
|
# Hints for JIT warnings.
|
|
get "/mentions/groups" => "hints#check_group_mentions", :format => :json
|
|
|
|
get "/channels/:channel_id/threads" => "channel_threads#index"
|
|
post "/channels/:channel_id/threads" => "channel_threads#create"
|
|
put "/channels/:channel_id/threads/:thread_id" => "channel_threads#update"
|
|
get "/channels/:channel_id/threads/:thread_id" => "channel_threads#show"
|
|
get "/channels/:channel_id/threads/:thread_id/messages" => "channel_thread_messages#index"
|
|
put "/channels/:channel_id/threads/:thread_id/read" => "channels_threads_read#update"
|
|
post "/channels/:channel_id/threads/:thread_id/drafts" => "channels_threads_drafts#create"
|
|
put "/channels/:channel_id/threads/:thread_id/notifications-settings/me" =>
|
|
"channel_threads_current_user_notifications_settings#update"
|
|
post "/channels/:channel_id/threads/:thread_id/mark-thread-title-prompt-seen/me" =>
|
|
"channel_threads_current_user_title_prompt_seen#update"
|
|
post "/direct-message-channels" => "direct_messages#create"
|
|
|
|
put "/channels/:channel_id/messages/:message_id/restore" => "channel_messages#restore"
|
|
delete "/channels/:channel_id/messages/:message_id" => "channel_messages#destroy"
|
|
delete "/channels/:channel_id/messages" => "channel_messages#bulk_destroy"
|
|
end
|
|
|
|
namespace :admin, defaults: { format: :json, constraints: StaffConstraint.new } do
|
|
post "export/messages" => "export#export_messages"
|
|
end
|
|
|
|
# direct_messages_controller routes
|
|
get "/direct_messages" => "direct_messages#index"
|
|
|
|
# incoming_webhooks_controller routes
|
|
post "/hooks/:key" => "incoming_webhooks#create_message",
|
|
:constraints => {
|
|
format: :json,
|
|
},
|
|
:defaults => {
|
|
format: :json,
|
|
}
|
|
|
|
# incoming_webhooks_controller routes
|
|
post "/hooks/:key/slack" => "incoming_webhooks#create_message_slack_compatible",
|
|
:constraints => {
|
|
format: :json,
|
|
},
|
|
:defaults => {
|
|
format: :json,
|
|
}
|
|
|
|
# chat_controller routes
|
|
get "/" => "chat#respond"
|
|
get "/search" => "chat#respond"
|
|
get "/new-message" => "chat#respond"
|
|
get "/direct-messages" => "chat#respond"
|
|
get "/channels" => "chat#respond"
|
|
get "/threads" => "chat#respond"
|
|
get "/browse" => "chat#respond"
|
|
get "/browse/all" => "chat#respond"
|
|
get "/browse/closed" => "chat#respond"
|
|
get "/browse/open" => "chat#respond"
|
|
get "/browse/archived" => "chat#respond"
|
|
post "/dismiss-retention-reminder" => "chat#dismiss_retention_reminder"
|
|
put ":chat_channel_id/react/:message_id" => "chat#react"
|
|
put "/:chat_channel_id/:message_id/rebake" => "chat#rebake"
|
|
post "/:chat_channel_id/quote" => "chat#quote_messages"
|
|
put "/user_chat_enabled/:user_id" => "chat#set_user_chat_status"
|
|
post "/:chat_channel_id" => "api/channel_messages#create"
|
|
|
|
base_c_route = "/c/:channel_title/:channel_id"
|
|
get base_c_route => "chat#respond", :as => "channel"
|
|
get "#{base_c_route}/:message_id" => "chat#respond"
|
|
|
|
%w[info info/about info/members info/settings info/search].each do |route|
|
|
get "#{base_c_route}/#{route}" => "chat#respond"
|
|
end
|
|
|
|
# /channel -> /c redirects
|
|
get "/channel/:channel_id", to: redirect("/chat/c/-/%{channel_id}")
|
|
|
|
get "#{base_c_route}/t/:thread_id" => "chat#respond"
|
|
get "#{base_c_route}/t/:thread_id/:message_id" => "chat#respond"
|
|
|
|
base_channel_route = "/channel/:channel_id/:channel_title"
|
|
redirect_base = "/chat/c/%{channel_title}/%{channel_id}"
|
|
|
|
get base_channel_route, to: redirect(redirect_base)
|
|
|
|
%w[info info/about info/members info/settings info/search].each do |route|
|
|
get "#{base_channel_route}/#{route}", to: redirect("#{redirect_base}/#{route}")
|
|
end
|
|
end
|