diff --git a/set_version.sh b/set_version.sh
index e256b67a5a899e5e83caae26456572d5ce25843a..990e58aa2bda000829e6f119d8600fecf49fcc4b 100755
--- a/set_version.sh
+++ b/set_version.sh
@@ -1,3 +1,3 @@
 #!/bin/bash
-export version=0.5.1-alpha.6
+export version=0.5.1-alpha.7
 echo "pub const VERSION: &str = \"${version}\";" > src/version.rs
diff --git a/src/database/migrations/init.rs b/src/database/migrations/init.rs
index 1bb7277063d62aee17f7a2123674407465cd34bb..be71b3994d8c7434dca3e9bcabe915846554879d 100644
--- a/src/database/migrations/init.rs
+++ b/src/database/migrations/init.rs
@@ -122,6 +122,23 @@ pub async fn create_database() {
     .await
     .expect("Failed to create username index.");
 
+    db.run_command(
+        doc! {
+            "createIndexes": "messages",
+            "indexes": [
+                {
+                    "key": {
+                        "content": "text"
+                    },
+                    "name": "content"
+                }
+            ]
+        },
+        None,
+    )
+    .await
+    .expect("Failed to create message index.");
+
     db.collection("migrations")
         .insert_one(
             doc! {
diff --git a/src/database/migrations/scripts.rs b/src/database/migrations/scripts.rs
index 770c109603a282ce4625d13909aeeaedb1cc6180..342f52f7349e0a0b608b23199a9a7ea64f2036c8 100644
--- a/src/database/migrations/scripts.rs
+++ b/src/database/migrations/scripts.rs
@@ -11,7 +11,7 @@ struct MigrationInfo {
     revision: i32,
 }
 
-pub const LATEST_REVISION: i32 = 6;
+pub const LATEST_REVISION: i32 = 7;
 
 pub async fn migrate_database() {
     let migrations = get_collection("migrations");
@@ -181,6 +181,28 @@ pub async fn run_migrations(revision: i32) -> i32 {
             .expect("Failed to migrate servers.");
     }
 
+    if revision <= 6 {
+        info!("Running migration [revision 6 / 2021-07-09]: Add message text index.");
+
+        get_db()
+        .run_command(
+            doc! {
+                "createIndexes": "messages",
+                "indexes": [
+                    {
+                        "key": {
+                            "content": "text"
+                        },
+                        "name": "content"
+                    }
+                ]
+            },
+            None,
+        )
+        .await
+        .expect("Failed to create message index.");
+    }
+
     // Reminder to update LATEST_REVISION when adding new migrations.
     LATEST_REVISION
 }
diff --git a/src/routes/channels/message_search.rs b/src/routes/channels/message_search.rs
new file mode 100644
index 0000000000000000000000000000000000000000..214d937bd31baed60b130481a376aa168ed2d1cf
--- /dev/null
+++ b/src/routes/channels/message_search.rs
@@ -0,0 +1,121 @@
+use std::collections::HashSet;
+
+use crate::database::*;
+use crate::util::result::{Error, Result};
+
+use futures::StreamExt;
+use mongodb::{
+    bson::{doc, from_document},
+    options::FindOptions,
+};
+use rocket_contrib::json::{Json, JsonValue};
+use serde::{Deserialize, Serialize};
+use validator::Validate;
+
+#[derive(Serialize, Deserialize, FromFormValue)]
+pub enum Sort {
+    Relevance,
+    Latest,
+    Oldest,
+}
+
+#[derive(Validate, Serialize, Deserialize, FromForm)]
+pub struct Options {
+    #[validate(length(min = 1, max = 64))]
+    query: String,
+
+    #[validate(range(min = 1, max = 100))]
+    limit: Option<i64>,
+    #[validate(length(min = 26, max = 26))]
+    before: Option<String>,
+    #[validate(length(min = 26, max = 26))]
+    after: Option<String>,
+    sort: Option<Sort>,
+    include_users: Option<bool>,
+}
+
+#[post("/<target>/search", data = "<options>")]
+pub async fn req(user: User, target: Ref, options: Json<Options>) -> Result<JsonValue> {
+    options
+        .validate()
+        .map_err(|error| Error::FailedValidation { error })?;
+
+    let target = target.fetch_channel().await?;
+    target.has_messaging()?;
+
+    let perm = permissions::PermissionCalculator::new(&user)
+        .with_channel(&target)
+        .for_channel()
+        .await?;
+    if !perm.get_view() {
+        Err(Error::MissingPermission)?
+    }
+
+    let mut messages = vec![];
+    let limit = options.limit.unwrap_or(50);
+
+    let mut cursor = get_collection("messages")
+        .find(
+            doc! {
+                "channel": target.id(),
+                "$text": {
+                    "$search": &options.query
+                }
+            },
+            FindOptions::builder()
+                .projection(doc! {
+                    "score": {
+                        "$meta": "textScore"
+                    }
+                })
+                .limit(limit)
+                .sort(doc! {
+                    "score": {
+                        "$meta": "textScore"
+                    }
+                })
+                .build(),
+        )
+        .await
+        .map_err(|_| Error::DatabaseError {
+            operation: "find",
+            with: "messages",
+        })?;
+    
+    while let Some(result) = cursor.next().await {
+        if let Ok(doc) = result {
+            messages.push(
+                from_document::<Message>(doc).map_err(|_| Error::DatabaseError {
+                    operation: "from_document",
+                    with: "message",
+                })?,
+            );
+        }
+    }
+
+    if options.include_users.unwrap_or_else(|| false) {
+        let mut ids = HashSet::new();
+        for message in &messages {
+            ids.insert(message.author.clone());
+        }
+
+        ids.remove(&user.id);
+        let user_ids = ids.into_iter().collect();
+        let users = user.fetch_multiple_users(user_ids).await?;
+
+        if let Channel::TextChannel { server, .. } = target {
+            Ok(json!({
+                "messages": messages,
+                "users": users,
+                "members": Server::fetch_members(&server).await?
+            }))
+        } else {
+            Ok(json!({
+                "messages": messages,
+                "users": users,
+            }))
+        }
+    } else {
+        Ok(json!(messages))
+    }
+}
diff --git a/src/routes/channels/mod.rs b/src/routes/channels/mod.rs
index 29f75f9aed47dd8f744f2878c1909dcdc38814cf..cc3a4d192f132926b2f1694cce886d277227386d 100644
--- a/src/routes/channels/mod.rs
+++ b/src/routes/channels/mod.rs
@@ -14,6 +14,7 @@ mod message_delete;
 mod message_edit;
 mod message_fetch;
 mod message_query;
+mod message_search;
 mod message_query_stale;
 mod message_send;
 mod permissions_set;
@@ -29,6 +30,7 @@ pub fn routes() -> Vec<Route> {
         invite_create::req,
         message_send::req,
         message_query::req,
+        message_search::req,
         message_query_stale::req,
         message_fetch::req,
         message_edit::req,
diff --git a/src/version.rs b/src/version.rs
index 818c98d6d10aa5e88dbd922716480a8990f5eac6..99ae5c62b0d2606e370a8fffe0cdf5dbf7b92c70 100644
--- a/src/version.rs
+++ b/src/version.rs
@@ -1 +1 @@
-pub const VERSION: &str = "0.5.1-alpha.6";
+pub const VERSION: &str = "0.5.1-alpha.7";