diff --git a/jstests/aggregation/sources/merge/requires_unique_index.js b/jstests/aggregation/sources/merge/requires_unique_index.js
index 38f8aa27f6..1565381c6c 100644
--- a/jstests/aggregation/sources/merge/requires_unique_index.js
+++ b/jstests/aggregation/sources/merge/requires_unique_index.js
@@ -237,12 +237,6 @@ function dropWithoutImplicitRecreate(coll) {
     assertMergeFailsWithoutUniqueIndex({source: source, onFields: ["a", "geo"], target: target});
     assertMergeFailsWithoutUniqueIndex({source: source, onFields: "geo", target: target});
 
-    dropWithoutImplicitRecreate(target);
-    assert.commandWorked(
-        target.createIndex({geo: "geoHaystack", a: 1}, {unique: true, bucketSize: 5}));
-    assertMergeFailsWithoutUniqueIndex({source: source, onFields: ["a", "geo"], target: target});
-    assertMergeFailsWithoutUniqueIndex({source: source, onFields: ["geo", "a"], target: target});
-
     dropWithoutImplicitRecreate(target);
     // MongoDB does not support unique hashed indexes.
     assert.commandFailedWithCode(target.createIndex({a: "hashed"}, {unique: true}), 16764);
diff --git a/jstests/auth/lib/commands_lib.js b/jstests/auth/lib/commands_lib.js
index 830dbf4527..2afbe7b873 100644
--- a/jstests/auth/lib/commands_lib.js
+++ b/jstests/auth/lib/commands_lib.js
@@ -3820,31 +3820,6 @@ var authCommandsLib = {
               {runOnDb: secondDbName, roles: {}}
           ]
         },
-        {
-          testname: "geoSearch",
-          command: {geoSearch: "x", near: [50, 50], maxDistance: 6, limit: 1, search: {}},
-          skipSharded: true,
-          setup: function(db) {
-              db.x.drop();
-              assert.writeOK(db.x.save({loc: {long: 50, lat: 50}}));
-              assert.commandWorked(db.x.ensureIndex({loc: "geoHaystack", type: 1}, {bucketSize: 1}));
-          },
-          teardown: function(db) {
-              db.x.drop();
-          },
-          testcases: [
-              {
-                runOnDb: firstDbName,
-                roles: roles_read,
-                privileges: [{resource: {db: firstDbName, collection: "x"}, actions: ["find"]}]
-              },
-              {
-                runOnDb: secondDbName,
-                roles: roles_readAny,
-                privileges: [{resource: {db: secondDbName, collection: "x"}, actions: ["find"]}]
-              }
-          ]
-        },
         {
           testname: "getCmdLineOpts",
           command: {getCmdLineOpts: 1},
diff --git a/jstests/core/commands_namespace_parsing.js b/jstests/core/commands_namespace_parsing.js
index 1eda20c57f..fcc79d4960 100644
--- a/jstests/core/commands_namespace_parsing.js
+++ b/jstests/core/commands_namespace_parsing.js
@@ -139,15 +139,6 @@ assertFailsWithInvalidNamespacesForField("out.reduce",
                                          isNotFullyQualified,
                                          isNotAdminCommand);
 
-if (!isMongos) {
-    // Test geoSearch fails with an invalid collection name.
-    assertFailsWithInvalidNamespacesForField(
-        "geoSearch",
-        {geoSearch: "", search: {}, near: [0.0, 0.0], maxDistance: 10},
-        isNotFullyQualified,
-        isNotAdminCommand);
-}
-
 // Test find fails with an invalid collection name.
 assertFailsWithInvalidNamespacesForField(
     "find", {find: ""}, isNotFullyQualified, isNotAdminCommand);
diff --git a/jstests/core/geo_big_polygon2.js b/jstests/core/geo_big_polygon2.js
index d41377bb46..7b36807294 100644
--- a/jstests/core/geo_big_polygon2.js
+++ b/jstests/core/geo_big_polygon2.js
@@ -559,7 +559,6 @@ objects.forEach(function(o) {
 assert.commandWorked(coll.ensureIndex({geo: "2dsphere", a: 1}), "compound index, geo");
 // These other index types will fail because of the GeoJSON documents
 assert.commandFailed(coll.ensureIndex({geo: "2dsphere", a: "text"}), "compound index, geo & text");
-assert.commandFailed(coll.ensureIndex({geo: "geoHaystack"}, {bucketSize: 1}), "geoHaystack index");
 assert.commandFailed(coll.ensureIndex({geo: "2d"}), "2d index");
 
 totalObjects = coll.count();
diff --git a/jstests/core/geo_haystack1.js b/jstests/core/geo_haystack1.js
deleted file mode 100644
index eaf9641d3c..0000000000
--- a/jstests/core/geo_haystack1.js
+++ /dev/null
@@ -1,64 +0,0 @@
-// @tags: [
-//     # Haystack index is not available on embedded
-//     incompatible_with_embedded,
-// ]
-
-t = db.geo_haystack1;
-t.drop();
-
-function distance(a, b) {
-    var x = a[0] - b[0];
-    var y = a[1] - b[1];
-    return Math.sqrt((x * x) + (y * y));
-}
-
-function distanceTotal(a, arr, f) {
-    var total = 0;
-    for (var i = 0; i < arr.length; i++) {
-        total += distance(a, arr[i][f]);
-    }
-    return total;
-}
-
-queries = [
-    {near: [7, 8], maxDistance: 3, search: {z: 3}},
-];
-
-answers = queries.map(function() {
-    return {totalDistance: 0, results: []};
-});
-
-n = 0;
-for (x = 0; x < 20; x++) {
-    for (y = 0; y < 20; y++) {
-        t.insert({_id: n, loc: [x, y], z: n % 5});
-
-        for (i = 0; i < queries.length; i++) {
-            var d = distance(queries[i].near, [x, y]);
-            if (d > queries[i].maxDistance)
-                continue;
-            if (queries[i].search.z != n % 5)
-                continue;
-            answers[i].results.push({_id: n, loc: [x, y]});
-            answers[i].totalDistance += d;
-        }
-
-        n++;
-    }
-}
-
-t.ensureIndex({loc: "geoHaystack", z: 1}, {bucketSize: .7});
-
-for (i = 0; i < queries.length; i++) {
-    print("---------");
-    printjson(queries[i]);
-    res = t.runCommand("geoSearch", queries[i]);
-    print("\t" + tojson(res.stats));
-    print("\tshould have: " + answers[i].results.length + "\t actually got: " + res.stats.n);
-    assert.eq(answers[i].results.length, res.stats.n, "num:" + i + " number matches");
-    assert.eq(answers[i].totalDistance,
-              distanceTotal(queries[i].near, res.results, "loc"),
-              "num:" + i + " totalDistance");
-    // printjson( res );
-    // printjson( answers[i].length );
-}
diff --git a/jstests/core/geo_haystack2.js b/jstests/core/geo_haystack2.js
deleted file mode 100644
index b9946ef3cd..0000000000
--- a/jstests/core/geo_haystack2.js
+++ /dev/null
@@ -1,64 +0,0 @@
-// @tags: [
-//     # Haystack index is not available on embedded
-//     incompatible_with_embedded,
-// ]
-
-t = db.geo_haystack2;
-t.drop();
-
-function distance(a, b) {
-    var x = a[0] - b[0];
-    var y = a[1] - b[1];
-    return Math.sqrt((x * x) + (y * y));
-}
-
-function distanceTotal(a, arr, f) {
-    var total = 0;
-    for (var i = 0; i < arr.length; i++) {
-        total += distance(a, arr[i][f]);
-    }
-    return total;
-}
-
-queries = [
-    {near: [7, 8], maxDistance: 3, search: {z: 3}},
-];
-
-answers = queries.map(function() {
-    return {totalDistance: 0, results: []};
-});
-
-n = 0;
-for (x = 0; x < 20; x++) {
-    for (y = 0; y < 20; y++) {
-        t.insert({_id: n, loc: [x, y], z: [n % 10, (n + 5) % 10]});
-
-        for (i = 0; i < queries.length; i++) {
-            var d = distance(queries[i].near, [x, y]);
-            if (d > queries[i].maxDistance)
-                continue;
-            if (queries[i].search.z != n % 10 && queries[i].search.z != (n + 5) % 10)
-                continue;
-            answers[i].results.push({_id: n, loc: [x, y]});
-            answers[i].totalDistance += d;
-        }
-
-        n++;
-    }
-}
-
-t.ensureIndex({loc: "geoHaystack", z: 1}, {bucketSize: .7});
-
-for (i = 0; i < queries.length; i++) {
-    print("---------");
-    printjson(queries[i]);
-    res = t.runCommand("geoSearch", queries[i]);
-    print("\t" + tojson(res.stats));
-    print("\tshould have: " + answers[i].results.length + "\t actually got: " + res.stats.n);
-    assert.eq(answers[i].results.length, res.stats.n, "num:" + i + " number matches");
-    assert.eq(answers[i].totalDistance,
-              distanceTotal(queries[i].near, res.results, "loc"),
-              "num:" + i + " totalDistance");
-    // printjson( res );
-    // printjson( answers[i].length );
-}
diff --git a/jstests/core/geo_haystack3.js b/jstests/core/geo_haystack3.js
deleted file mode 100644
index 3fe0da348b..0000000000
--- a/jstests/core/geo_haystack3.js
+++ /dev/null
@@ -1,59 +0,0 @@
-// @tags: [
-//     # Haystack index is not available on embedded
-//     incompatible_with_embedded,
-// ]
-
-t = db.geo_haystack3;
-t.drop();
-
-//
-// Tests for geo haystack validity
-//
-
-t.insert({pos: "invalid"});
-assert.commandFailed(t.ensureIndex({pos: "geoHaystack", type: 1}, {bucketSize: 1}));
-t.drop();
-
-t.insert({pos: []});
-assert.commandFailed(t.ensureIndex({pos: "geoHaystack", type: 1}, {bucketSize: 1}));
-t.drop();
-
-t.insert({pos: [1, 2]});
-assert.commandWorked(t.ensureIndex({pos: "geoHaystack", type: 1}, {bucketSize: 1}));
-t.drop();
-
-t.insert({pos: {x: 1, y: 2}});
-assert.commandWorked(t.ensureIndex({pos: "geoHaystack", type: 1}, {bucketSize: 1}));
-t.drop();
-
-t.insert({pos: {x: 1, y: 2, z: 3}});
-assert.commandWorked(t.ensureIndex({pos: "geoHaystack", type: 1}, {bucketSize: 1}));
-t.drop();
-
-//
-// Tests for geo haystack search
-//
-
-t.insert({pos: {long: 34, lat: 33}});
-t.insert({pos: {long: 34.2, lat: 33.3}, type: ["bar", "restaurant"]});
-t.insert({pos: {long: 34.2, lat: 37.3}, type: ["bar", "chicken"]});
-t.insert({pos: {long: 59.1, lat: 87.2}, type: ["baz", "office"]});
-t.ensureIndex({pos: "geoHaystack", type: 1}, {bucketSize: 1});
-
-// This only matches the first insert.  What do we want?  First 3 or just the first?
-res = t.runCommand("geoSearch", {near: [33, 33], maxDistance: 6, search: {}, limit: 30});
-assert.eq(1, res.stats.n, "Right # of matches");
-assert.eq(34, res.results[0].pos.long, "expected longitude");
-assert.eq(33, res.results[0].pos.lat, "expected latitude");
-
-// This matches the middle 2 of the 4 elements above.
-res = t.runCommand("geoSearch", {near: [33, 33], maxDistance: 6, search: {type: "bar"}, limit: 2});
-assert.eq(2, res.stats.n, "Right # of matches");
-assert.eq("bar", res.results[0].type[0], "expected value for type");
-assert.eq("bar", res.results[1].type[0], "expected value for type");
-assert.neq(res.results[0].type[1], res.results[1].type[1], "should get 2 diff results");
-
-// This is a test for the limit being reached/only 1 returned.
-res = t.runCommand("geoSearch", {near: [33, 33], maxDistance: 6, search: {type: "bar"}, limit: 1});
-assert.eq(1, res.stats.n, "Right # of matches");
-assert.eq("bar", res.results[0].type[0], "expected value for type");
diff --git a/jstests/core/geo_mindistance_boundaries.js b/jstests/core/geo_mindistance_boundaries.js
index 32977ac4b1..32417cd757 100644
--- a/jstests/core/geo_mindistance_boundaries.js
+++ b/jstests/core/geo_mindistance_boundaries.js
@@ -5,7 +5,7 @@ var t = db.geo_mindistance_boundaries;
 t.drop();
 t.insert({loc: [1, 0]});  // 1 degree of longitude from origin.
 
-/* $minDistance is supported for 2dsphere index only, not 2d or geoHaystack. */
+/* $minDistance is supported for 2dsphere index only, not 2d*/
 t.ensureIndex({loc: "2dsphere"});
 
 //
diff --git a/jstests/core/index_plugins.js b/jstests/core/index_plugins.js
index 56681d9a64..9add14bfe1 100644
--- a/jstests/core/index_plugins.js
+++ b/jstests/core/index_plugins.js
@@ -17,8 +17,6 @@ coll.dropIndexes();
 assert.commandWorked(coll.createIndex({a: "text"}));
 coll.dropIndexes();
 
-assert.commandFailed(coll.createIndex({a: "geoHaystack"}, {bucketSize: 1}));  // compound required
-
 // Test compounding special index types with an ascending index.
 
 assert.commandWorked(coll.createIndex({a: "2dsphere", b: 1}));
@@ -38,10 +36,6 @@ assert.commandWorked(coll.createIndex({a: 1, b: "hashed"}));
 coll.dropIndexes();
 assert.commandFailed(coll.createIndex({a: 1, b: "2d"}));  // unsupported
 
-assert.commandWorked(coll.createIndex({a: "geoHaystack", b: 1}, {bucketSize: 1}));
-coll.dropIndexes();
-assert.commandFailed(coll.createIndex({a: 1, b: "geoHaystack"}, {bucketSize: 1}));  // unsupported
-
 // Test compound index where multiple fields have same special index type.
 coll.dropIndexes();
 assert.commandWorked(coll.createIndex({a: "2dsphere", b: "2dsphere"}));
@@ -52,7 +46,7 @@ assert.commandFailedWithCode(coll.createIndex({a: "hashed", b: "hashed"}), 31303
 assert.commandFailedWithCode(coll.createIndex({c: 1, a: "hashed", b: "hashed"}), 31303);
 
 // Test compounding different special index types with each other.
-const incompatableIndexTypes = ["2d", "2dsphere", "geoHaystack", "hashed", "text"];
+const incompatableIndexTypes = ["2d", "2dsphere", "hashed", "text"];
 for (let indexType1 of incompatableIndexTypes) {
     for (let indexType2 of incompatableIndexTypes) {
         if (indexType1 == indexType2) {
@@ -69,8 +63,5 @@ for (let indexType1 of incompatableIndexTypes) {
     }
     assert.commandFailedWithCode(coll.createIndex({"$**": 1, b: indexType1}),
                                  ErrorCodes.CannotCreateIndex);
-    assert.commandFailedWithCode(
-        coll.createIndex({a: "geoHaystack", b: indexType1}, {bucketSize: 1}),
-        [ErrorCodes.CannotCreateIndex, 16770]);
 }
 })();
diff --git a/jstests/core/txns/commands_banning_txnnumber_outside_transactions.js b/jstests/core/txns/commands_banning_txnnumber_outside_transactions.js
index de3749f14f..3f2c2e5df7 100644
--- a/jstests/core/txns/commands_banning_txnnumber_outside_transactions.js
+++ b/jstests/core/txns/commands_banning_txnnumber_outside_transactions.js
@@ -36,7 +36,6 @@ const nonRetryableWriteCommands = [
 const nonRetryableWriteCommandsMongodOnly = [
     // Commands that are allowed in transactions.
     {coordinateCommitTransaction: 1, participants: []},
-    {geoSearch: 1},
     {prepareTransaction: 1},
     // A selection of commands that are not allowed in transactions.
     {applyOps: 1}
diff --git a/jstests/core/txns/commands_not_allowed_in_txn.js b/jstests/core/txns/commands_not_allowed_in_txn.js
index 8d161c8b21..96f9ba71c7 100644
--- a/jstests/core/txns/commands_not_allowed_in_txn.js
+++ b/jstests/core/txns/commands_not_allowed_in_txn.js
@@ -27,14 +27,6 @@ const sessionDb = session.getDatabase(dbName);
 const isMongos = assert.commandWorked(db.runCommand("ismaster")).msg === "isdbgrid";
 
 assert.commandWorked(testDB.createCollection(testColl.getName(), {writeConcern: {w: "majority"}}));
-assert.commandWorked(testDB.runCommand({
-    createIndexes: collName,
-    indexes: [
-        {name: "geo_2d", key: {geo: "2d"}},
-        {key: {haystack: "geoHaystack", a: 1}, name: "haystack_geo", bucketSize: 1}
-    ],
-    writeConcern: {w: "majority"}
-}));
 
 function setup() {
     testColl.dropIndex({a: 1});
diff --git a/jstests/core/txns/disallow_operations_on_prepared_transaction.js b/jstests/core/txns/disallow_operations_on_prepared_transaction.js
index 0e6a8453aa..5841c085ed 100644
--- a/jstests/core/txns/disallow_operations_on_prepared_transaction.js
+++ b/jstests/core/txns/disallow_operations_on_prepared_transaction.js
@@ -86,11 +86,6 @@ assert.commandFailedWithCode(
                  }),
                  ErrorCodes.PreparedTransactionInProgress);
 
-jsTestLog("Test that you can't run geoSearch on a prepared transaction.");
-assert.commandFailedWithCode(
-    sessionDB.runCommand({geoSearch: collName, near: [0, 0], search: {a: 1}}),
-    ErrorCodes.PreparedTransactionInProgress);
-
 jsTestLog("Test that you can't insert on a prepared transaction.");
 res = assert.commandFailedWithCode(sessionColl.insert({_id: 5}),
                                    ErrorCodes.PreparedTransactionInProgress);
diff --git a/jstests/core/txns/statement_ids_accepted.js b/jstests/core/txns/statement_ids_accepted.js
index bf73cbff55..ae9b31a254 100644
--- a/jstests/core/txns/statement_ids_accepted.js
+++ b/jstests/core/txns/statement_ids_accepted.js
@@ -168,38 +168,6 @@ const isMongos = assert.commandWorked(db.runCommand("ismaster")).msg === "isdbgr
 if (!isMongos) {
     // Skip commands that do not exist on mongos.
 
-    jsTestLog("Check that geoSearch accepts a statement ID");
-    assert.commandWorked(testColl.insert({geo: {type: "Point", coordinates: [0, 0]}, a: 0}),
-                         {writeConcern: {w: "majority"}});
-    assert.commandWorked(testColl.insert({geoh: {lat: 0, long: 0}, b: 0}),
-                         {writeConcern: {w: "majority"}});
-    assert.commandWorked(sessionDb.runCommand({
-        createIndexes: collName,
-        indexes: [
-            {name: "geo", key: {geo: "2dsphere"}},
-            {name: "geoh", key: {geoh: "geoHaystack", b: 1}, bucketSize: 1}
-        ],
-        writeConcern: {w: "majority"}
-    }));
-    // Ensure the snapshot is available following the index creation.
-    assert.soonNoExcept(function() {
-        testColl.find({}, {readConcern: {level: "snapshot"}});
-        return true;
-    });
-
-    jsTestLog("Check that geoSearch accepts a statement ID");
-    assert.commandWorked(sessionDb.runCommand({
-        geoSearch: collName,
-        search: {b: 0},
-        near: [0, 0],
-        maxDistance: 1,
-        readConcern: {level: "snapshot"},
-        txnNumber: NumberLong(txnNumber++),
-        stmtId: NumberInt(0),
-        startTransaction: true,
-        autocommit: false
-    }));
-
     jsTestLog("Check that prepareTransaction accepts a statement ID");
     assert.commandWorked(sessionDb.runCommand({
         insert: collName,
diff --git a/jstests/core/txns/transactions_profiling.js b/jstests/core/txns/transactions_profiling.js
index 55f63ab6cf..455269f439 100644
--- a/jstests/core/txns/transactions_profiling.js
+++ b/jstests/core/txns/transactions_profiling.js
@@ -28,11 +28,6 @@ assert.commandWorked(sessionColl.insert({_id: "read-doc"}));
 assert.commandWorked(sessionColl.insert({_id: "update-doc"}));
 assert.commandWorked(sessionColl.insert({_id: "multi-update-doc-1"}));
 assert.commandWorked(sessionColl.insert({_id: "multi-update-doc-2"}));
-assert.commandWorked(testDB.runCommand({
-    createIndexes: collName,
-    indexes: [{key: {haystack: "geoHaystack", a: 1}, name: "haystack_geo", bucketSize: 1}],
-    writeConcern: {w: "majority"}
-}));
 
 jsTestLog("Test commands that can use shell helpers.");
 session.startTransaction({readConcern: {level: "snapshot"}, writeConcern: {w: "majority"}});
@@ -97,14 +92,6 @@ assert.eq(profileObj.command.findandmodify, sessionColl.getName(), tojson(profil
 assert.eq(profileObj.nMatched, 1, tojson(profileObj));
 assert.eq(profileObj.nModified, 1, tojson(profileObj));
 
-jsTestLog("Test geoSearch.");
-assert.commandWorked(
-    sessionDB.runCommand({geoSearch: collName, near: [0, 0], maxDistance: 1, search: {a: 1}}));
-profileObj = getLatestProfilerEntry(testDB);
-assert.eq(profileObj.op, "command", tojson(profileObj));
-assert.eq(profileObj.ns, sessionColl.getFullName(), tojson(profileObj));
-assert.eq(profileObj.command.geoSearch, sessionColl.getName(), tojson(profileObj));
-
 jsTestLog("Test getMore.");
 let res = assert.commandWorked(
     sessionDB.runCommand({find: collName, filter: {_id: "read-doc"}, batchSize: 0}));
diff --git a/jstests/core/views/views_all_commands.js b/jstests/core/views/views_all_commands.js
index d828c7432a..a38966b745 100644
--- a/jstests/core/views/views_all_commands.js
+++ b/jstests/core/views/views_all_commands.js
@@ -270,14 +270,6 @@ let viewsCommandTests = {
     fsync: {skip: isUnrelated},
     fsyncUnlock: {skip: isUnrelated},
     getDatabaseVersion: {skip: isUnrelated},
-    geoSearch: {
-        command: {
-            geoSearch: "view",
-            search: {},
-            near: [-50, 37],
-        },
-        expectFailure: true
-    },
     getCmdLineOpts: {skip: isUnrelated},
     getDefaultRWConcern: {skip: isUnrelated},
     getDiagnosticData: {skip: isUnrelated},
diff --git a/jstests/libs/override_methods/enable_hedged_reads.js b/jstests/libs/override_methods/enable_hedged_reads.js
index f0fbe80329..718bc73164 100644
--- a/jstests/libs/override_methods/enable_hedged_reads.js
+++ b/jstests/libs/override_methods/enable_hedged_reads.js
@@ -19,7 +19,6 @@ const kCommandsSupportingReadPreference = new Set([
     "dbStats",
     "distinct",
     "find",
-    "geoSearch",
 ]);
 const kDatabasesOnConfigServers = new Set(["config", "admin"]);
 
diff --git a/jstests/libs/override_methods/read_and_write_concern_helpers.js b/jstests/libs/override_methods/read_and_write_concern_helpers.js
index c8c0e57367..25d381f89a 100644
--- a/jstests/libs/override_methods/read_and_write_concern_helpers.js
+++ b/jstests/libs/override_methods/read_and_write_concern_helpers.js
@@ -6,7 +6,6 @@ var kCommandsSupportingReadConcern = new Set([
     "count",
     "distinct",
     "find",
-    "geoSearch",
 ]);
 
 var kCommandsOnlySupportingReadConcernSnapshot = new Set([
diff --git a/jstests/libs/override_methods/set_read_preference_secondary.js b/jstests/libs/override_methods/set_read_preference_secondary.js
index 9bed1fdad4..9685449103 100644
--- a/jstests/libs/override_methods/set_read_preference_secondary.js
+++ b/jstests/libs/override_methods/set_read_preference_secondary.js
@@ -16,7 +16,6 @@ const kCommandsSupportingReadPreference = new Set([
     "dbStats",
     "distinct",
     "find",
-    "geoSearch",
 ]);
 const kDatabasesOnConfigServers = new Set(["config", "admin"]);
 
diff --git a/jstests/multiVersion/libs/data_generators.js b/jstests/multiVersion/libs/data_generators.js
index 5f3ccf20e1..4a30ca2e46 100644
--- a/jstests/multiVersion/libs/data_generators.js
+++ b/jstests/multiVersion/libs/data_generators.js
@@ -453,14 +453,6 @@ function IndexDataGenerator(options) {
         return index;
     }
 
-    function GenHaystackIndex(seed) {
-        var index = {};
-        index[getNextUniqueKey()] = "geoHaystack";
-        // Haystack indexes need a non geo field, and the geo field must be first
-        index[getNextUniqueKey()] = (seed % 2) == 1 ? 1 : -1;
-        return index;
-    }
-
     function GenTextIndex(seed) {
         var index = {};
         index[getNextUniqueKey()] = "text";
@@ -519,14 +511,6 @@ function IndexDataGenerator(options) {
         return attributes;
     }
 
-    function GenHaystackIndexOptions(seed) {
-        var attributes = GenIndexOptions(seed);
-        // When using a haystack index, the following additional index properties are required:
-        // { "bucketSize" : <bucket value> }
-        attributes["bucketSize"] = (seed * 10000) % 100 + 10;
-        return attributes;
-    }
-
     function GenTextIndexOptions(seed) {
         return GenIndexOptions(seed);
     }
@@ -562,8 +546,6 @@ function IndexDataGenerator(options) {
         {"spec": Gen2dsphereIndex(7), "options": Gen2dSphereIndexOptions(12)},
         //   2d
         {"spec": Gen2dIndex(8), "options": Gen2dIndexOptions(13)},
-        //   Haystack
-        {"spec": GenHaystackIndex(9), "options": GenHaystackIndexOptions(13)},
 
         // Text Indexes
         {"spec": GenTextIndex(10), "options": GenTextIndexOptions(14)},
diff --git a/jstests/noPassthrough/geo_full.js b/jstests/noPassthrough/geo_full.js
index 87cc666158..44ab373fd1 100644
--- a/jstests/noPassthrough/geo_full.js
+++ b/jstests/noPassthrough/geo_full.js
@@ -35,7 +35,6 @@ var randEnvironment = function() {
             min: -180,
             bits: Math.floor(Random.rand() * 32) + 1,
             earth: true,
-            bucketSize: 360 / (4 * 1024 * 1024 * 1024)
         };
     }
 
@@ -48,9 +47,8 @@ var randEnvironment = function() {
     var bits = Math.floor(Random.rand() * 32) + 1;
     var bits = Math.floor(Random.rand() * 32) + 1;
     var range = max - min;
-    var bucketSize = range / (4 * 1024 * 1024 * 1024);
 
-    return {max: max, min: min, bits: bits, earth: false, bucketSize: bucketSize};
+    return {max: max, min: min, bits: bits, earth: false};
 };
 
 var randPoint = function(env, query) {
@@ -324,23 +322,6 @@ var locsArray = function(locs) {
     }
 };
 
-var minBoxSize = function(env, box) {
-    return env.bucketSize * Math.pow(2, minBucketScale(env, box));
-};
-
-var minBucketScale = function(env, box) {
-    if (box.length && box[0].length)
-        box = [box[0][0] - box[1][0], box[0][1] - box[1][1]];
-
-    if (box.length)
-        box = Math.max(box[0], box[1]);
-
-    print(box);
-    print(env.bucketSize);
-
-    return Math.ceil(Math.log(box / env.bucketSize) / Math.log(2));
-};
-
 // TODO:  Add spherical $uniqueDocs tests
 var numTests = 100;
 
@@ -430,7 +411,6 @@ for (var test = 0; test < numTests; test++) {
 
     // $center
     print("Center query...");
-    print("Min box : " + minBoxSize(env, query.radius));
     assert.eq(results.center.docsIn,
               t.find({
                    "locs.loc": {$within: {$center: [query.center, query.radius], $uniqueDocs: 1}},
diff --git a/jstests/noPassthrough/readConcern_snapshot.js b/jstests/noPassthrough/readConcern_snapshot.js
index 8386ad51f6..74d200d72b 100644
--- a/jstests/noPassthrough/readConcern_snapshot.js
+++ b/jstests/noPassthrough/readConcern_snapshot.js
@@ -101,11 +101,6 @@ rst.initiate();
 let testDB = rst.getPrimary().getDB(dbName);
 let coll = testDB.coll;
 assert.commandWorked(coll.createIndex({geo: "2d"}));
-assert.commandWorked(testDB.runCommand({
-    createIndexes: collName,
-    indexes: [{key: {haystack: "geoHaystack", a: 1}, name: "haystack_geo", bucketSize: 1}],
-    writeConcern: {w: "majority"}
-}));
 
 session = testDB.getMongo().startSession({causalConsistency: false});
 sessionDb = session.getDatabase(dbName);
@@ -120,10 +115,6 @@ assert.commandWorked(sessionDb.runCommand({aggregate: collName, pipeline: [], cu
 // readConcern 'snapshot' is supported by distinct.
 assert.commandWorked(sessionDb.runCommand({distinct: collName, key: "x"}));
 
-// readConcern 'snapshot' is supported by geoSearch.
-assert.commandWorked(
-    sessionDb.runCommand({geoSearch: collName, near: [0, 0], maxDistance: 1, search: {a: 1}}));
-
 // readConcern 'snapshot' is not supported by non-CRUD commands.
 assert.commandFailedWithCode(sessionDb.runCommand({dropIndexes: collName, index: "a_1"}),
                              ErrorCodes.OperationNotSupportedInTransaction);
diff --git a/jstests/noPassthrough/read_concern_snapshot_catalog_invalidation.js b/jstests/noPassthrough/read_concern_snapshot_catalog_invalidation.js
index b0d13cfaf0..59c4c44785 100644
--- a/jstests/noPassthrough/read_concern_snapshot_catalog_invalidation.js
+++ b/jstests/noPassthrough/read_concern_snapshot_catalog_invalidation.js
@@ -43,10 +43,6 @@ function waitForOp(curOpFilter) {
 
 function testCommand(cmd, curOpFilter) {
     coll.drop({writeConcern: {w: "majority"}});
-    assert.commandWorked(testDB.runCommand({
-        createIndexes: kCollName,
-        indexes: [{key: {haystack: "geoHaystack", a: 1}, name: "haystack_geo", bucketSize: 1}]
-    }));
     assert.commandWorked(coll.insert({x: 1}, {writeConcern: {w: "majority"}}));
 
     // Start a command with readConcern "snapshot" that hangs after establishing a storage
@@ -101,8 +97,6 @@ testCommand({findAndModify: kCollName, query: {x: 1}, update: {$set: {x: 2}}}, {
     "command.update.$set": {x: 2},
     "command.readConcern.level": "snapshot"
 });
-testCommand({geoSearch: kCollName, near: [0, 0], maxDistance: 1, search: {a: 1}},
-            {"command.geoSearch": kCollName, "command.readConcern.level": "snapshot"});
 testCommand({insert: kCollName, documents: [{x: 1}]},
             {"command.insert": kCollName, "command.readConcern.level": "snapshot"});
 testCommand({update: kCollName, updates: [{q: {x: 1}, u: {$set: {x: 2}}}]},
diff --git a/jstests/noPassthrough/read_majority_reads.js b/jstests/noPassthrough/read_majority_reads.js
index 065db43f42..814b2e1a71 100644
--- a/jstests/noPassthrough/read_majority_reads.js
+++ b/jstests/noPassthrough/read_majority_reads.js
@@ -6,7 +6,6 @@
  *  - aggregation
  *  - distinct
  *  - count
- *  - geoSearch
  *
  * Each operation is tested on a single node, and (if supported) through mongos on both sharded and
  * unsharded collections. Mongos doesn't directly handle readConcern majority, but these tests
@@ -64,8 +63,8 @@ var cursorTestCases = {
 
 // These test cases have a run method that will be passed a collection with a single object with
 // _id: 1 and a state field that equals either "before" or "after". The collection will also
-// contain both a 2dsphere and a geoHaystack index to enable testing commands that depend on
-// them. The return value from the run method is expected to be the value of expectedBefore or
+// contain a 2dsphere index to enable testing commands that depend on having a 2d index.
+// The return value from the run method is expected to be the value of expectedBefore or
 // expectedAfter depending on the state of the state field.
 var nonCursorTestCases = {
     count_before: {
@@ -98,21 +97,6 @@ var nonCursorTestCases = {
         expectedBefore: 'before',
         expectedAfter: 'after',
     },
-    geoSearch: {
-        run: function(coll) {
-            var res = coll.runCommand('geoSearch', {
-                readConcern: {level: 'majority'},
-                near: [0, 0],
-                search: {_id: 1},  // Needed due to SERVER-23158.
-                maxDistance: 1,
-            });
-            assert.commandWorked(res);
-            assert.eq(res.results.length, 1, tojson(res));
-            return res.results[0].state;
-        },
-        expectedBefore: 'before',
-        expectedAfter: 'after',
-    },
 };
 
 function runTests(coll, mongodConnection) {
@@ -155,7 +139,6 @@ function runTests(coll, mongodConnection) {
         assert.eq(oldCursor.next().state, 'after');
     }
 
-    assert.commandWorked(coll.ensureIndex({point: 'geoHaystack', _id: 1}, {bucketSize: 1}));
     for (var testName in nonCursorTestCases) {
         jsTestLog('Running ' + testName + ' against ' + coll.toString());
         var getResult = nonCursorTestCases[testName].run;
@@ -215,13 +198,6 @@ assert(shardingTest.adminCommand({addShard: replTest.getURL()}));
 
 // Remove tests of commands that aren't supported at all through mongos, even on unsharded
 // collections.
-['geoSearch'].forEach(function(cmd) {
-    // Make sure it really isn't supported.
-    assert.eq(shardingTest.getDB('test').coll.runCommand(cmd).code, ErrorCodes.CommandNotFound);
-    delete cursorTestCases[cmd];
-    delete nonCursorTestCases[cmd];
-});
-
 (function testUnshardedDBThroughMongos() {
     var db = shardingTest.getDB("throughMongos");
     runTests(db.unshardedDB, mongod);
diff --git a/jstests/noPassthrough/shell_can_use_read_concern.js b/jstests/noPassthrough/shell_can_use_read_concern.js
index 73facc0921..a68ec7854b 100644
--- a/jstests/noPassthrough/shell_can_use_read_concern.js
+++ b/jstests/noPassthrough/shell_can_use_read_concern.js
@@ -190,19 +190,6 @@ function runTests({withSession}) {
         });
     }
 
-    //
-    // Tests for the "geoSearch" command.
-    //
-
-    testCommandCanBeCausallyConsistent(function() {
-        assert.commandWorked(coll.createIndex({loc: "geoHaystack", other: 1}, {bucketSize: 1}));
-    }, {expectedSession: withSession, expectedAfterClusterTime: false});
-
-    testCommandCanBeCausallyConsistent(function() {
-        assert.commandWorked(
-            db.runCommand({geoSearch: coll.getName(), near: [0, 0], maxDistance: 1, search: {}}));
-    });
-
     //
     // Tests for the "explain" command.
     //
diff --git a/jstests/replsets/db_reads_while_recovering_all_commands.js b/jstests/replsets/db_reads_while_recovering_all_commands.js
index f01f0c2382..8f0a8822ab 100644
--- a/jstests/replsets/db_reads_while_recovering_all_commands.js
+++ b/jstests/replsets/db_reads_while_recovering_all_commands.js
@@ -154,15 +154,6 @@ const allCommands = {
     flushRouterConfig: {skip: isNotAUserDataRead},
     fsync: {skip: isNotAUserDataRead},
     fsyncUnlock: {skip: isNotAUserDataRead},
-    geoSearch: {
-        command: {
-            geoSearch: collName,
-            search: {},
-            near: [-42, 42],
-        },
-        expectFailure: true,
-        expectedErrorCode: ErrorCodes.NotMasterOrSecondary
-    },
     getCmdLineOpts: {skip: isNotAUserDataRead},
     getDatabaseVersion: {skip: isNotAUserDataRead},
     getDefaultRWConcern: {skip: isNotAUserDataRead},
diff --git a/jstests/replsets/transactions_only_allowed_on_primaries.js b/jstests/replsets/transactions_only_allowed_on_primaries.js
index 910c37740c..b5892ebc8d 100644
--- a/jstests/replsets/transactions_only_allowed_on_primaries.js
+++ b/jstests/replsets/transactions_only_allowed_on_primaries.js
@@ -33,13 +33,6 @@ secondary.setSlaveOk(true);
 // Create a test collection that we can run commands against.
 const primaryDB = primary.getDB(dbName);
 assert.commandWorked(primary.getDB(dbName).createCollection(collName));
-assert.commandWorked(primaryDB.runCommand({
-    createIndexes: collName,
-    indexes: [
-        {name: "geo_2d", key: {geo: "2d"}},
-        {key: {haystack: "geoHaystack", a: 1}, name: "haystack_geo", bucketSize: 1}
-    ]
-}));
 replTest.awaitLastOpCommitted();
 
 /**
@@ -80,7 +73,6 @@ let readCommands = [
     {find: collName},
     {aggregate: collName, pipeline: [{$project: {_id: 1}}], cursor: {}},
     {distinct: collName, key: "_id"},
-    {geoSearch: collName, near: [0, 0]}
 ];
 
 jsTestLog("Testing read commands.");
diff --git a/jstests/sharding/causal_consistency_shell_support.js b/jstests/sharding/causal_consistency_shell_support.js
index 8466209d36..1eba040e6f 100644
--- a/jstests/sharding/causal_consistency_shell_support.js
+++ b/jstests/sharding/causal_consistency_shell_support.js
@@ -164,8 +164,6 @@ runCommandAndCheckLogicalTimes(
 runCommandAndCheckLogicalTimes(geoNearCmd, testDB, false);
 commandWorksAndUpdatesOperationTime(geoNearCmd, testDB);
 
-// GeoSearch is not supported for sharded clusters.
-
 // MapReduce doesn't currently support read concern majority.
 
 // Verify that the server rejects commands when operation time is invalid by running a command
diff --git a/jstests/sharding/read_pref_cmd.js b/jstests/sharding/read_pref_cmd.js
index 91729d438a..f0a1ae7034 100644
--- a/jstests/sharding/read_pref_cmd.js
+++ b/jstests/sharding/read_pref_cmd.js
@@ -309,8 +309,6 @@ let testConnReadPreference = function(conn, isMongos, rsNodes, {readPref, expect
         {dbStats: 1}, allowedOnSecondary.kAlways, true, formatProfileQuery(kDbName, {dbStats: 1}));
 
     assert.commandWorked(shardedColl.ensureIndex({loc: '2d'}));
-    assert.commandWorked(
-        shardedColl.ensureIndex({position: 'geoHaystack', type: 1}, {bucketSize: 10}));
 
     // TODO: SERVER-38961 Remove when simultaneous index builds complete.
     // Run a no-op command and wait for it to be applied on secondaries. Due to the asynchronous
@@ -319,19 +317,6 @@ let testConnReadPreference = function(conn, isMongos, rsNodes, {readPref, expect
     assert.commandWorked(testDB.runCommand({collMod: kShardedCollName}));
     assert.commandWorked(testDB.runCommand({getLastError: 1, w: nodeCount}));
 
-    // Mongos doesn't implement geoSearch; test it only with ReplicaSetConnection.
-    if (!isMongos) {
-        cmdTest({
-            geoSearch: kShardedCollName,
-            near: [1, 1],
-            search: {type: 'restaurant'},
-            maxDistance: 10
-        },
-                allowedOnSecondary.kAlways,
-                true,
-                formatProfileQuery(kShardedNs, {geoSearch: kShardedCollName}));
-    }
-
     // Test on sharded
     cmdTest({aggregate: kShardedCollName, pipeline: [{$project: {x: 1}}], cursor: {}},
             allowedOnSecondary.kAlways,
diff --git a/jstests/sharding/read_write_concern_defaults_application.js b/jstests/sharding/read_write_concern_defaults_application.js
index e11cd97c77..5d8b7f7ee7 100644
--- a/jstests/sharding/read_write_concern_defaults_application.js
+++ b/jstests/sharding/read_write_concern_defaults_application.js
@@ -384,18 +384,6 @@ let testCases = {
     forceerror: {skip: "test command"},
     fsync: {skip: "does not accept read or write concern"},
     fsyncUnlock: {skip: "does not accept read or write concern"},
-    geoSearch: {
-        setUp: function(conn) {
-            assert.commandWorked(conn.getDB(db).runCommand({
-                createIndexes: coll,
-                indexes: [{key: {loc: "geoHaystack", foo: 1}, bucketSize: 1, name: "foo"}],
-                writeConcern: {w: 1}
-            }));
-        },
-        command: {geoSearch: coll, search: {}, near: [0, 0], maxDistance: 1},
-        checkReadConcern: true,
-        checkWriteConcern: false,
-    },
     getCmdLineOpts: {skip: "does not accept read or write concern"},
     getDatabaseVersion: {skip: "does not accept read or write concern"},
     getDefaultRWConcern: {skip: "does not accept read or write concern"},
diff --git a/jstests/sharding/safe_secondary_reads_drop_recreate.js b/jstests/sharding/safe_secondary_reads_drop_recreate.js
index b3c4a1bb9a..df184eab9f 100644
--- a/jstests/sharding/safe_secondary_reads_drop_recreate.js
+++ b/jstests/sharding/safe_secondary_reads_drop_recreate.js
@@ -173,7 +173,6 @@ let testCases = {
     forceerror: {skip: "does not return user data"},
     fsync: {skip: "does not return user data"},
     fsyncUnlock: {skip: "does not return user data"},
-    geoSearch: {skip: "not supported in mongos"},
     getCmdLineOpts: {skip: "does not return user data"},
     getDefaultRWConcern: {skip: "does not return user data"},
     getDiagnosticData: {skip: "does not return user data"},
diff --git a/jstests/sharding/safe_secondary_reads_single_migration_suspend_range_deletion.js b/jstests/sharding/safe_secondary_reads_single_migration_suspend_range_deletion.js
index 387ba1d08c..59c33a9549 100644
--- a/jstests/sharding/safe_secondary_reads_single_migration_suspend_range_deletion.js
+++ b/jstests/sharding/safe_secondary_reads_single_migration_suspend_range_deletion.js
@@ -207,7 +207,6 @@ let testCases = {
     forceerror: {skip: "does not return user data"},
     fsync: {skip: "does not return user data"},
     fsyncUnlock: {skip: "does not return user data"},
-    geoSearch: {skip: "not supported in mongos"},
     getCmdLineOpts: {skip: "does not return user data"},
     getDefaultRWConcern: {skip: "does not return user data"},
     getDiagnosticData: {skip: "does not return user data"},
diff --git a/jstests/sharding/safe_secondary_reads_single_migration_waitForDelete.js b/jstests/sharding/safe_secondary_reads_single_migration_waitForDelete.js
index f0af23b678..3ae307f1fa 100644
--- a/jstests/sharding/safe_secondary_reads_single_migration_waitForDelete.js
+++ b/jstests/sharding/safe_secondary_reads_single_migration_waitForDelete.js
@@ -178,7 +178,6 @@ let testCases = {
     forceerror: {skip: "does not return user data"},
     fsync: {skip: "does not return user data"},
     fsyncUnlock: {skip: "does not return user data"},
-    geoSearch: {skip: "not supported in mongos"},
     getCmdLineOpts: {skip: "does not return user data"},
     getDefaultRWConcern: {skip: "does not return user data"},
     getDiagnosticData: {skip: "does not return user data"},
diff --git a/src/mongo/client/dbclient_rs.cpp b/src/mongo/client/dbclient_rs.cpp
index d7744d2c84..d119d2421b 100644
--- a/src/mongo/client/dbclient_rs.cpp
+++ b/src/mongo/client/dbclient_rs.cpp
@@ -75,7 +75,6 @@ public:
         _secOkCmdList.insert("explain");
         _secOkCmdList.insert("find");
         _secOkCmdList.insert("geoNear");
-        _secOkCmdList.insert("geoSearch");
         _secOkCmdList.insert("group");
     }
 } _populateReadPrefSecOkCmdList;
diff --git a/src/mongo/client/index_spec.cpp b/src/mongo/client/index_spec.cpp
index 4562a8fe17..5cc1348ef7 100644
--- a/src/mongo/client/index_spec.cpp
+++ b/src/mongo/client/index_spec.cpp
@@ -38,7 +38,6 @@ namespace mongo {
 
 const char IndexSpec::kIndexValText[] = "text";
 const char IndexSpec::kIndexValGeo2D[] = "2d";
-const char IndexSpec::kIndexValGeoHaystack[] = "geoHaystack";
 const char IndexSpec::kIndexValGeo2DSphere[] = "2dsphere";
 const char IndexSpec::kIndexValHashed[] = "hashed";
 
@@ -50,7 +49,6 @@ const char* const kIndexTypeStrings[] = {nullptr,
                                          nullptr,
                                          IndexSpec::kIndexValText,
                                          IndexSpec::kIndexValGeo2D,
-                                         IndexSpec::kIndexValGeoHaystack,
                                          IndexSpec::kIndexValGeo2DSphere,
                                          IndexSpec::kIndexValHashed};
 
@@ -198,13 +196,6 @@ IndexSpec& IndexSpec::geo2DMax(double value) {
     return *this;
 }
 
-IndexSpec& IndexSpec::geoHaystackBucketSize(double value) {
-    uassert(
-        ErrorCodes::InvalidOptions, kDuplicateOption, !_options.asTempObj().hasField("bucketSize"));
-    _options.append("bucketSize", value);
-    return *this;
-}
-
 IndexSpec& IndexSpec::addOption(const BSONElement& option) {
     uassert(ErrorCodes::InvalidOptions,
             kDuplicateOption,
diff --git a/src/mongo/client/index_spec.h b/src/mongo/client/index_spec.h
index 678e84e7e5..62180daadf 100644
--- a/src/mongo/client/index_spec.h
+++ b/src/mongo/client/index_spec.h
@@ -47,7 +47,6 @@ public:
         kIndexTypeDescending,
         kIndexTypeText,
         kIndexTypeGeo2D,
-        kIndexTypeGeoHaystack,
         kIndexTypeGeo2DSphere,
         kIndexTypeHashed,
     };
@@ -57,7 +56,6 @@ public:
     static const int kIndexValDescending = -1;
     static const char kIndexValText[];
     static const char kIndexValGeo2D[];
-    static const char kIndexValGeoHaystack[];
     static const char kIndexValGeo2DSphere[];
     static const char kIndexValHashed[];
 
@@ -176,15 +174,6 @@ public:
     /** Sets the maximum value for keys in a geo2d index. */
     IndexSpec& geo2DMax(double value);
 
-
-    //
-    // Geo Haystack Options
-    //
-
-    /** Sets the bucket size for haystack indexes. */
-    IndexSpec& geoHaystackBucketSize(double value);
-
-
     //
     // Support for adding generic options. This is here so that if new index options
     // become available on the server those options can be set independently of the
diff --git a/src/mongo/db/catalog/index_catalog_impl.cpp b/src/mongo/db/catalog/index_catalog_impl.cpp
index a7af998534..43a8023656 100644
--- a/src/mongo/db/catalog/index_catalog_impl.cpp
+++ b/src/mongo/db/catalog/index_catalog_impl.cpp
@@ -111,13 +111,6 @@ Status IndexCatalogImpl::init(OperationContext* opCtx) {
             durableCatalog->getIndexSpec(opCtx, _collection->getCatalogId(), indexName).getOwned();
         BSONObj keyPattern = spec.getObjectField("key");
 
-        if (spec.hasField(IndexDescriptor::kGeoHaystackBucketSize)) {
-            LOGV2_OPTIONS(4670602,
-                          {logv2::LogTag::kStartupWarnings},
-                          "Found an existing geoHaystack index in the catalog. Support for "
-                          "geoHaystack indexes has been deprecated. Instead create a 2d index. See "
-                          "https://dochub.mongodb.org/core/4.4-deprecate-geoHaystack");
-        }
         auto descriptor =
             std::make_unique<IndexDescriptor>(_collection, _getAccessMethodName(keyPattern), spec);
         if (spec.hasField(IndexDescriptor::kExpireAfterSecondsFieldName)) {
@@ -320,9 +313,6 @@ void IndexCatalogImpl::_logInternalState(OperationContext* opCtx,
     }
 }
 
-namespace {
-std::string lastHaystackIndexLogged = "";
-}
 StatusWith<BSONObj> IndexCatalogImpl::prepareSpecForCreate(OperationContext* opCtx,
                                                            const BSONObj& original) const {
     auto swValidatedAndFixed = _validateAndFixIndexSpec(opCtx, original);
@@ -333,17 +323,6 @@ StatusWith<BSONObj> IndexCatalogImpl::prepareSpecForCreate(OperationContext* opC
 
     auto validatedSpec = swValidatedAndFixed.getValue();
     auto indexName = validatedSpec.getField("name").String();
-    // This gets hit twice per index, so we keep track of what we last logged to avoid logging the
-    // same line for the same index twice.
-    if (validatedSpec.hasField(IndexDescriptor::kGeoHaystackBucketSize) &&
-        lastHaystackIndexLogged.compare(indexName) != 0) {
-        LOGV2_OPTIONS(4670601,
-                      {logv2::LogTag::kStartupWarnings},
-                      "Support for "
-                      "geoHaystack indexes has been deprecated. Instead create a 2d index. See "
-                      "https://dochub.mongodb.org/core/4.4-deprecate-geoHaystack");
-        lastHaystackIndexLogged = indexName;
-    }
 
     // Check whether this is a non-_id index and there are any settings disallowing this server
     // from building non-_id indexes.
diff --git a/src/mongo/db/catalog/index_key_validate.cpp b/src/mongo/db/catalog/index_key_validate.cpp
index 66f91fdd65..0b7c4c996c 100644
--- a/src/mongo/db/catalog/index_key_validate.cpp
+++ b/src/mongo/db/catalog/index_key_validate.cpp
@@ -82,7 +82,6 @@ static std::set<StringData> allowedFieldNames = {
     IndexDescriptor::kDefaultLanguageFieldName,
     IndexDescriptor::kDropDuplicatesFieldName,
     IndexDescriptor::kExpireAfterSecondsFieldName,
-    IndexDescriptor::kGeoHaystackBucketSize,
     IndexDescriptor::kIndexNameFieldName,
     IndexDescriptor::kIndexVersionFieldName,
     IndexDescriptor::kKeyPatternFieldName,
diff --git a/src/mongo/db/commands.cpp b/src/mongo/db/commands.cpp
index 52363e5f2b..0ba9bc7210 100644
--- a/src/mongo/db/commands.cpp
+++ b/src/mongo/db/commands.cpp
@@ -121,7 +121,6 @@ const StringMap<int> txnCmdWhitelist = {{"abortTransaction", 1},
                                         {"find", 1},
                                         {"findandmodify", 1},
                                         {"findAndModify", 1},
-                                        {"geoSearch", 1},
                                         {"getMore", 1},
                                         {"insert", 1},
                                         {"killCursors", 1},
diff --git a/src/mongo/db/commands/SConscript b/src/mongo/db/commands/SConscript
index fc737ef2bf..6cc9b4bb48 100644
--- a/src/mongo/db/commands/SConscript
+++ b/src/mongo/db/commands/SConscript
@@ -384,7 +384,6 @@ env.Library(
         "dbcommands_d.cpp",
         "dbhash.cpp",
         "driverHelpers.cpp",
-        "haystack.cpp",
         "internal_rename_if_options_and_indexes_match_cmd.cpp",
         "map_reduce_command.cpp",
         "oplog_application_checks.cpp",
diff --git a/src/mongo/db/commands/haystack.cpp b/src/mongo/db/commands/haystack.cpp
deleted file mode 100644
index 93d3df3ff6..0000000000
--- a/src/mongo/db/commands/haystack.cpp
+++ /dev/null
@@ -1,175 +0,0 @@
-/**
- *    Copyright (C) 2018-present MongoDB, Inc.
- *
- *    This program is free software: you can redistribute it and/or modify
- *    it under the terms of the Server Side Public License, version 1,
- *    as published by MongoDB, Inc.
- *
- *    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
- *    Server Side Public License for more details.
- *
- *    You should have received a copy of the Server Side Public License
- *    along with this program. If not, see
- *    <http://www.mongodb.com/licensing/server-side-public-license>.
- *
- *    As a special exception, the copyright holders give permission to link the
- *    code of portions of this program with the OpenSSL library under certain
- *    conditions as described in each individual source file and distribute
- *    linked combinations including the program with the OpenSSL library. You
- *    must comply with the Server Side Public License in all respects for
- *    all of the code used other than as permitted herein. If you modify file(s)
- *    with this exception, you may extend this exception to your version of the
- *    file(s), but you are not obligated to do so. If you do not wish to do so,
- *    delete this exception statement from your version. If you delete this
- *    exception statement from all source files in the program, then also delete
- *    it in the license file.
- */
-
-#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kCommand
-
-#include "mongo/platform/basic.h"
-
-#include <vector>
-
-#include "mongo/db/auth/action_set.h"
-#include "mongo/db/auth/action_type.h"
-#include "mongo/db/auth/privilege.h"
-#include "mongo/db/catalog/collection.h"
-#include "mongo/db/catalog/database.h"
-#include "mongo/db/catalog/index_catalog.h"
-#include "mongo/db/commands.h"
-#include "mongo/db/curop.h"
-#include "mongo/db/db_raii.h"
-#include "mongo/db/index/haystack_access_method.h"
-#include "mongo/db/index/index_access_method.h"
-#include "mongo/db/index/index_descriptor.h"
-#include "mongo/db/index_names.h"
-#include "mongo/db/jsobj.h"
-#include "mongo/db/namespace_string.h"
-#include "mongo/db/query/find_common.h"
-#include "mongo/logv2/log.h"
-
-
-/**
- * Examines all documents in a given radius of a given point.
- * Returns all documents that match a given search restriction.
- * See http://dochub.mongodb.org/core/haystackindexes
- *
- * Use when you want to look for restaurants within 25 miles with a certain name.
- * Don't use when you want to find the closest open restaurants.
- */
-namespace mongo {
-namespace {
-Rarely geoSearchDeprecationSampler;  // Used to occasionally log deprecation messages.
-bool loggedStartupWarning = false;
-}  // namespace
-using std::string;
-using std::vector;
-
-class GeoHaystackSearchCommand : public ErrmsgCommandDeprecated {
-public:
-    GeoHaystackSearchCommand() : ErrmsgCommandDeprecated("geoSearch") {}
-
-    virtual bool supportsWriteConcern(const BSONObj& cmd) const override {
-        return false;
-    }
-
-    AllowedOnSecondary secondaryAllowed(ServiceContext*) const override {
-        return AllowedOnSecondary::kAlways;
-    }
-
-    ReadConcernSupportResult supportsReadConcern(const BSONObj& cmdObj,
-                                                 repl::ReadConcernLevel level) const final {
-        return ReadConcernSupportResult::allSupportedAndDefaultPermitted();
-    }
-
-    ReadWriteType getReadWriteType() const {
-        return ReadWriteType::kRead;
-    }
-
-    std::size_t reserveBytesForReply() const override {
-        return FindCommon::kInitReplyBufferSize;
-    }
-
-    virtual void addRequiredPrivileges(const std::string& dbname,
-                                       const BSONObj& cmdObj,
-                                       std::vector<Privilege>* out) const {
-        ActionSet actions;
-        actions.addAction(ActionType::find);
-        out->push_back(Privilege(parseResourcePattern(dbname, cmdObj), actions));
-    }
-
-    bool errmsgRun(OperationContext* opCtx,
-                   const string& dbname,
-                   const BSONObj& cmdObj,
-                   string& errmsg,
-                   BSONObjBuilder& result) {
-        if (!loggedStartupWarning) {
-            LOGV2_OPTIONS(4670603,
-                          {logv2::LogTag::kStartupWarnings},
-                          "Support for geoSearch has been deprecated. Instead, create a 2d index "
-                          "and use $geoNear or $geoWithin. See "
-                          "https://dochub.mongodb.org/core/4.4-deprecate-geoHaystack");
-            loggedStartupWarning = true;
-            geoSearchDeprecationSampler.tick();
-        } else if (geoSearchDeprecationSampler.tick()) {
-            LOGV2_WARNING(4670604,
-                          "Support for geoSearch has been deprecated. Instead, create a 2d index "
-                          "and use $geoNear or $geoWithin. See "
-                          "https://dochub.mongodb.org/core/4.4-deprecate-geoHaystack");
-        }
-        const NamespaceString nss = CommandHelpers::parseNsCollectionRequired(dbname, cmdObj);
-
-        AutoGetCollectionForReadCommand ctx(opCtx, nss);
-
-        // Check whether we are allowed to read from this node after acquiring our locks.
-        auto replCoord = repl::ReplicationCoordinator::get(opCtx);
-        uassertStatusOK(replCoord->checkCanServeReadsFor(
-            opCtx, nss, ReadPreferenceSetting::get(opCtx).canRunOnSecondary()));
-
-        Collection* collection = ctx.getCollection();
-        if (!collection) {
-            errmsg = "can't find ns";
-            return false;
-        }
-
-        vector<const IndexDescriptor*> idxs;
-        collection->getIndexCatalog()->findIndexByType(opCtx, IndexNames::GEO_HAYSTACK, idxs);
-        if (idxs.size() == 0) {
-            errmsg = "no geoSearch index";
-            return false;
-        }
-        if (idxs.size() > 1) {
-            errmsg = "more than 1 geosearch index";
-            return false;
-        }
-
-        BSONElement nearElt = cmdObj["near"];
-        BSONElement maxDistance = cmdObj["maxDistance"];
-        BSONElement search = cmdObj["search"];
-
-        uassert(13318, "near needs to be an array", nearElt.isABSONObj());
-        uassert(13319, "maxDistance needs a number", maxDistance.isNumber());
-        uassert(13320, "search needs to be an object", search.type() == Object);
-
-        unsigned limit = 50;
-        if (cmdObj["limit"].isNumber())
-            limit = static_cast<unsigned>(cmdObj["limit"].numberInt());
-
-        const IndexDescriptor* desc = idxs[0];
-        auto ham = static_cast<const HaystackAccessMethod*>(
-            collection->getIndexCatalog()->getEntry(desc)->accessMethod());
-        ham->searchCommand(opCtx,
-                           collection,
-                           nearElt.Obj(),
-                           maxDistance.numberDouble(),
-                           search.Obj(),
-                           &result,
-                           limit);
-        return 1;
-    }
-} nameSearchCommand;
-
-}  // namespace mongo
diff --git a/src/mongo/db/index/SConscript b/src/mongo/db/index/SConscript
index fcff88d1e1..fa3e3275cd 100644
--- a/src/mongo/db/index/SConscript
+++ b/src/mongo/db/index/SConscript
@@ -149,7 +149,6 @@ env.Library(
         "btree_access_method.cpp",
         "fts_access_method.cpp",
         "hash_access_method.cpp",
-        "haystack_access_method.cpp",
         "index_access_method_factory_impl.cpp",
         "s2_access_method.cpp",
         "wildcard_access_method.cpp",
diff --git a/src/mongo/db/index/expression_keys_private.cpp b/src/mongo/db/index/expression_keys_private.cpp
index b0e1570dd7..4254b57b18 100644
--- a/src/mongo/db/index/expression_keys_private.cpp
+++ b/src/mongo/db/index/expression_keys_private.cpp
@@ -448,87 +448,6 @@ long long int ExpressionKeysPrivate::makeSingleHashKey(const BSONElement& e, Has
     return BSONElementHasher::hash64(e, seed);
 }
 
-// static
-void ExpressionKeysPrivate::getHaystackKeys(const BSONObj& obj,
-                                            const std::string& geoField,
-                                            const std::vector<std::string>& otherFields,
-                                            double bucketSize,
-                                            KeyStringSet* keys,
-                                            KeyString::Version keyStringVersion,
-                                            Ordering ordering,
-                                            boost::optional<RecordId> id) {
-    BSONElement loc = dps::extractElementAtPath(obj, geoField);
-
-    if (loc.eoo()) {
-        return;
-    }
-
-    // NOTE: We explicitly test nFields >= 2 to support legacy users who may have indexed
-    // (intentionally or unintentionally) objects/arrays with more than two fields.
-    uassert(16775,
-            str::stream() << "cannot extract [lng, lat] array or object from " << obj,
-            loc.isABSONObj() && loc.Obj().nFields() >= 2);
-
-    string root;
-    {
-        BSONObjIterator i(loc.Obj());
-        BSONElement x = i.next();
-        BSONElement y = i.next();
-        root = makeHaystackString(hashHaystackElement(x, bucketSize),
-                                  hashHaystackElement(y, bucketSize));
-    }
-
-    invariant(otherFields.size() == 1);
-
-    BSONElementSet all;
-
-    // The object we're indexing may be an array.
-    dps::extractAllElementsAlongPath(obj, otherFields[0], all);
-
-    if (all.size() == 0) {
-        // We're indexing a document that doesn't have the secondary non-geo field present.
-        // XXX: do we want to add this even if all.size() > 0?  result:empty search terms
-        // match everything instead of only things w/empty search terms)
-        KeyString::HeapBuilder keyString(keyStringVersion, ordering);
-        keyString.appendString(root);
-        keyString.appendNull();
-        if (id) {
-            keyString.appendRecordId(*id);
-        }
-        keys->insert(keyString.release());
-    } else {
-        // Ex:If our secondary field is type: "foo" or type: {a:"foo", b:"bar"},
-        // all.size()==1.  We can query on the complete field.
-        // Ex: If our secondary field is type: ["A", "B"] all.size()==2 and all has values
-        // "A" and "B".  The query looks for any of the fields in the array.
-        for (const auto& elem : all) {
-            KeyString::HeapBuilder keyString(keyStringVersion, ordering);
-            keyString.appendString(root);
-            keyString.appendBSONElement(elem);
-            if (id) {
-                keyString.appendRecordId(*id);
-            }
-            keys->insert(keyString.release());
-        }
-    }
-}
-
-// static
-int ExpressionKeysPrivate::hashHaystackElement(const BSONElement& e, double bucketSize) {
-    uassert(16776, "geo field is not a number", e.isNumber());
-    double d = e.numberDouble();
-    d += 180;
-    d /= bucketSize;
-    return static_cast<int>(d);
-}
-
-// static
-std::string ExpressionKeysPrivate::makeHaystackString(int hashedX, int hashedY) {
-    str::stream ss;
-    ss << hashedX << "_" << hashedY;
-    return ss;
-}
-
 void ExpressionKeysPrivate::getS2Keys(const BSONObj& obj,
                                       const BSONObj& keyPattern,
                                       const S2IndexingParams& params,
diff --git a/src/mongo/db/index/expression_keys_private.h b/src/mongo/db/index/expression_keys_private.h
index a4cb319c04..3fc93e0474 100644
--- a/src/mongo/db/index/expression_keys_private.h
+++ b/src/mongo/db/index/expression_keys_private.h
@@ -104,34 +104,6 @@ public:
      */
     static long long int makeSingleHashKey(const BSONElement& e, HashSeed seed, int v);
 
-    //
-    // Haystack
-    //
-
-    /**
-     * Generates keys for haystack access method.
-     */
-    static void getHaystackKeys(const BSONObj& obj,
-                                const std::string& geoField,
-                                const std::vector<std::string>& otherFields,
-                                double bucketSize,
-                                KeyStringSet* keys,
-                                KeyString::Version keyStringVersion,
-                                Ordering ordering,
-                                boost::optional<RecordId> id = boost::none);
-
-    /**
-     * Returns a hash of a BSON element.
-     * Used by getHaystackKeys and HaystackAccessMethod::searchCommand.
-     */
-    static int hashHaystackElement(const BSONElement& e, double bucketSize);
-
-    /**
-     * Joins two strings using underscore as separator.
-     * Used by getHaystackKeys and HaystackAccessMethod::searchCommand.
-     */
-    static std::string makeHaystackString(int hashedX, int hashedY);
-
     //
     // S2
     //
diff --git a/src/mongo/db/index/expression_params.cpp b/src/mongo/db/index/expression_params.cpp
index 29977a5baa..9a3b677e32 100644
--- a/src/mongo/db/index/expression_params.cpp
+++ b/src/mongo/db/index/expression_params.cpp
@@ -108,33 +108,6 @@ void ExpressionParams::parseHashParams(const BSONObj& infoObj,
             numHashFields == 1);
 }
 
-void ExpressionParams::parseHaystackParams(const BSONObj& infoObj,
-                                           std::string* geoFieldOut,
-                                           std::vector<std::string>* otherFieldsOut,
-                                           double* bucketSizeOut) {
-    BSONElement e = infoObj["bucketSize"];
-    uassert(16777, "need bucketSize", e.isNumber());
-    *bucketSizeOut = e.numberDouble();
-    uassert(16769, "bucketSize cannot be zero", *bucketSizeOut != 0.0);
-
-    // Example:
-    // db.foo.ensureIndex({ pos : "geoHaystack", type : 1 }, { bucketSize : 1 })
-    BSONObjIterator i(infoObj.getObjectField("key"));
-    while (i.more()) {
-        BSONElement e = i.next();
-        if (e.type() == String && IndexNames::GEO_HAYSTACK == e.valuestr()) {
-            uassert(16770, "can't have more than one geo field", geoFieldOut->size() == 0);
-            uassert(16771, "the geo field has to be first in index", otherFieldsOut->size() == 0);
-            *geoFieldOut = e.fieldName();
-        } else {
-            uassert(16772,
-                    "geoSearch can only have 1 non-geo field for now",
-                    otherFieldsOut->size() == 0);
-            otherFieldsOut->push_back(e.fieldName());
-        }
-    }
-}
-
 void ExpressionParams::initialize2dsphereParams(const BSONObj& infoObj,
                                                 const CollatorInterface* collator,
                                                 S2IndexingParams* out) {
diff --git a/src/mongo/db/index/expression_params.h b/src/mongo/db/index/expression_params.h
index 157a933c0b..71e611bed0 100644
--- a/src/mongo/db/index/expression_params.h
+++ b/src/mongo/db/index/expression_params.h
@@ -50,11 +50,6 @@ void parseHashParams(const BSONObj& infoObj,
                      int* versionOut,
                      BSONObj* keyPattern);
 
-void parseHaystackParams(const BSONObj& infoObj,
-                         std::string* geoFieldOut,
-                         std::vector<std::string>* otherFieldsOut,
-                         double* bucketSizeOut);
-
 void initialize2dsphereParams(const BSONObj& infoObj,
                               const CollatorInterface* collator,
                               S2IndexingParams* out);
diff --git a/src/mongo/db/index/haystack_access_method.cpp b/src/mongo/db/index/haystack_access_method.cpp
deleted file mode 100644
index b0ff4d3099..0000000000
--- a/src/mongo/db/index/haystack_access_method.cpp
+++ /dev/null
@@ -1,172 +0,0 @@
-/**
- *    Copyright (C) 2018-present MongoDB, Inc.
- *
- *    This program is free software: you can redistribute it and/or modify
- *    it under the terms of the Server Side Public License, version 1,
- *    as published by MongoDB, Inc.
- *
- *    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
- *    Server Side Public License for more details.
- *
- *    You should have received a copy of the Server Side Public License
- *    along with this program. If not, see
- *    <http://www.mongodb.com/licensing/server-side-public-license>.
- *
- *    As a special exception, the copyright holders give permission to link the
- *    code of portions of this program with the OpenSSL library under certain
- *    conditions as described in each individual source file and distribute
- *    linked combinations including the program with the OpenSSL library. You
- *    must comply with the Server Side Public License in all respects for
- *    all of the code used other than as permitted herein. If you modify file(s)
- *    with this exception, you may extend this exception to your version of the
- *    file(s), but you are not obligated to do so. If you do not wish to do so,
- *    delete this exception statement from your version. If you delete this
- *    exception statement from all source files in the program, then also delete
- *    it in the license file.
- */
-
-#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kQuery
-
-#include "mongo/platform/basic.h"
-
-#include "mongo/db/index/haystack_access_method.h"
-
-
-#include "mongo/base/status.h"
-#include "mongo/db/bson/dotted_path_support.h"
-#include "mongo/db/catalog/index_catalog.h"
-#include "mongo/db/exec/working_set_common.h"
-#include "mongo/db/geo/hash.h"
-#include "mongo/db/index/expression_keys_private.h"
-#include "mongo/db/index/expression_params.h"
-#include "mongo/db/index/haystack_access_method_internal.h"
-#include "mongo/db/jsobj.h"
-#include "mongo/db/query/internal_plans.h"
-#include "mongo/logv2/log.h"
-
-namespace mongo {
-
-using std::unique_ptr;
-
-namespace dps = ::mongo::dotted_path_support;
-
-HaystackAccessMethod::HaystackAccessMethod(IndexCatalogEntry* btreeState,
-                                           std::unique_ptr<SortedDataInterface> btree)
-    : AbstractIndexAccessMethod(btreeState, std::move(btree)) {
-    const IndexDescriptor* descriptor = btreeState->descriptor();
-
-    ExpressionParams::parseHaystackParams(
-        descriptor->infoObj(), &_geoField, &_otherFields, &_bucketSize);
-
-    uassert(16773, "no geo field specified", _geoField.size());
-    uassert(16774, "no non-geo fields specified", _otherFields.size());
-}
-
-void HaystackAccessMethod::doGetKeys(SharedBufferFragmentBuilder& pooledBufferBuilder,
-                                     const BSONObj& obj,
-                                     GetKeysContext context,
-                                     KeyStringSet* keys,
-                                     KeyStringSet* multikeyMetadataKeys,
-                                     MultikeyPaths* multikeyPaths,
-                                     boost::optional<RecordId> id) const {
-    ExpressionKeysPrivate::getHaystackKeys(obj,
-                                           _geoField,
-                                           _otherFields,
-                                           _bucketSize,
-                                           keys,
-                                           getSortedDataInterface()->getKeyStringVersion(),
-                                           getSortedDataInterface()->getOrdering(),
-                                           id);
-}
-
-void HaystackAccessMethod::searchCommand(OperationContext* opCtx,
-                                         Collection* collection,
-                                         const BSONObj& nearObj,
-                                         double maxDistance,
-                                         const BSONObj& search,
-                                         BSONObjBuilder* result,
-                                         unsigned limit) const {
-    Timer t;
-
-    LOGV2_DEBUG(20680,
-                1,
-                "SEARCH near:{nearObj} maxDistance:{maxDistance} search: {search}",
-                "nearObj"_attr = redact(nearObj),
-                "maxDistance"_attr = maxDistance,
-                "search"_attr = redact(search));
-    int x, y;
-    {
-        BSONObjIterator i(nearObj);
-        x = ExpressionKeysPrivate::hashHaystackElement(i.next(), _bucketSize);
-        y = ExpressionKeysPrivate::hashHaystackElement(i.next(), _bucketSize);
-    }
-    int scale = static_cast<int>(ceil(maxDistance / _bucketSize));
-
-    GeoHaystackSearchHopper hopper(opCtx, nearObj, maxDistance, limit, _geoField, collection);
-
-    long long btreeMatches = 0;
-
-    for (int a = -scale; a <= scale && !hopper.limitReached(); ++a) {
-        for (int b = -scale; b <= scale && !hopper.limitReached(); ++b) {
-            BSONObjBuilder bb;
-            bb.append("", ExpressionKeysPrivate::makeHaystackString(x + a, y + b));
-
-            for (unsigned i = 0; i < _otherFields.size(); i++) {
-                // See if the non-geo field we're indexing on is in the provided search term.
-                BSONElement e = dps::extractElementAtPath(search, _otherFields[i]);
-                if (e.eoo())
-                    bb.appendNull("");
-                else
-                    bb.appendAs(e, "");
-            }
-
-            BSONObj key = bb.obj();
-
-            stdx::unordered_set<RecordId, RecordId::Hasher> thisPass;
-
-
-            auto exec = InternalPlanner::indexScan(opCtx,
-                                                   collection,
-                                                   _descriptor,
-                                                   key,
-                                                   key,
-                                                   BoundInclusion::kIncludeBothStartAndEndKeys,
-                                                   PlanExecutor::NO_YIELD);
-            PlanExecutor::ExecState state;
-            BSONObj obj;
-            RecordId loc;
-            while (PlanExecutor::ADVANCED == (state = exec->getNext(&obj, &loc))) {
-                if (hopper.limitReached()) {
-                    break;
-                }
-                pair<stdx::unordered_set<RecordId, RecordId::Hasher>::iterator, bool> p =
-                    thisPass.insert(loc);
-                // If a new element was inserted (haven't seen the RecordId before), p.second
-                // is true.
-                if (p.second) {
-                    hopper.consider(loc);
-                    btreeMatches++;
-                }
-            }
-
-            // Non-yielding collection scans from InternalPlanner will never error.
-            invariant(PlanExecutor::ADVANCED == state || PlanExecutor::IS_EOF == state);
-        }
-    }
-
-    BSONArrayBuilder arr(result->subarrayStart("results"));
-    int num = hopper.appendResultsTo(&arr);
-    arr.done();
-
-    {
-        BSONObjBuilder b(result->subobjStart("stats"));
-        b.append("time", t.millis());
-        b.appendNumber("btreeMatches", btreeMatches);
-        b.append("n", num);
-        b.done();
-    }
-}
-
-}  // namespace mongo
diff --git a/src/mongo/db/index/haystack_access_method.h b/src/mongo/db/index/haystack_access_method.h
deleted file mode 100644
index f7e92c0cea..0000000000
--- a/src/mongo/db/index/haystack_access_method.h
+++ /dev/null
@@ -1,92 +0,0 @@
-/**
- *    Copyright (C) 2018-present MongoDB, Inc.
- *
- *    This program is free software: you can redistribute it and/or modify
- *    it under the terms of the Server Side Public License, version 1,
- *    as published by MongoDB, Inc.
- *
- *    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
- *    Server Side Public License for more details.
- *
- *    You should have received a copy of the Server Side Public License
- *    along with this program. If not, see
- *    <http://www.mongodb.com/licensing/server-side-public-license>.
- *
- *    As a special exception, the copyright holders give permission to link the
- *    code of portions of this program with the OpenSSL library under certain
- *    conditions as described in each individual source file and distribute
- *    linked combinations including the program with the OpenSSL library. You
- *    must comply with the Server Side Public License in all respects for
- *    all of the code used other than as permitted herein. If you modify file(s)
- *    with this exception, you may extend this exception to your version of the
- *    file(s), but you are not obligated to do so. If you do not wish to do so,
- *    delete this exception statement from your version. If you delete this
- *    exception statement from all source files in the program, then also delete
- *    it in the license file.
- */
-
-#pragma once
-
-#include "mongo/base/status.h"
-#include "mongo/db/index/index_access_method.h"
-#include "mongo/db/index/index_descriptor.h"
-#include "mongo/db/jsobj.h"
-
-namespace mongo {
-
-class Collection;
-class OperationContext;
-
-/**
- * Maps (lat, lng) to the bucketSize-sided square bucket that contains it.
- * Examines all documents in a given radius of a given point.
- * Returns all documents that match a given search restriction.
- * See http://dochub.mongodb.org/core/haystackindexes
- *
- * Use when you want to look for restaurants within 25 miles with a certain name.
- * Don't use when you want to find the closest open restaurants; see 2d.cpp for that.
- *
- * Usage:
- * db.foo.ensureIndex({ pos : "geoHaystack", type : 1 }, { bucketSize : 1 })
- *   pos is the name of the field to be indexed that has lat/lng data in an array.
- *   type is the name of the secondary field to be indexed.
- *   bucketSize specifies the dimension of the square bucket for the data in pos.
- * ALL fields are mandatory.
- */
-class HaystackAccessMethod : public AbstractIndexAccessMethod {
-public:
-    HaystackAccessMethod(IndexCatalogEntry* btreeState, std::unique_ptr<SortedDataInterface> btree);
-
-protected:
-    friend class GeoHaystackSearchCommand;
-    void searchCommand(OperationContext* opCtx,
-                       Collection* collection,
-                       const BSONObj& nearObj,
-                       double maxDistance,
-                       const BSONObj& search,
-                       BSONObjBuilder* result,
-                       unsigned limit) const;
-
-private:
-    /**
-     * Fills 'keys' with the keys that should be generated for 'obj' on this index.
-     *
-     * This function ignores the 'multikeyPaths' and 'multikeyMetadataKeys' pointers because
-     * geoHaystack indexes don't support tracking path-level multikey information.
-     */
-    void doGetKeys(SharedBufferFragmentBuilder& pooledBufferBuilder,
-                   const BSONObj& obj,
-                   GetKeysContext context,
-                   KeyStringSet* keys,
-                   KeyStringSet* multikeyMetadataKeys,
-                   MultikeyPaths* multikeyPaths,
-                   boost::optional<RecordId> id) const final;
-
-    std::string _geoField;
-    std::vector<std::string> _otherFields;
-    double _bucketSize;
-};
-
-}  // namespace mongo
diff --git a/src/mongo/db/index/haystack_access_method_internal.h b/src/mongo/db/index/haystack_access_method_internal.h
deleted file mode 100644
index 18fe86ad0f..0000000000
--- a/src/mongo/db/index/haystack_access_method_internal.h
+++ /dev/null
@@ -1,99 +0,0 @@
-/**
- *    Copyright (C) 2018-present MongoDB, Inc.
- *
- *    This program is free software: you can redistribute it and/or modify
- *    it under the terms of the Server Side Public License, version 1,
- *    as published by MongoDB, Inc.
- *
- *    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
- *    Server Side Public License for more details.
- *
- *    You should have received a copy of the Server Side Public License
- *    along with this program. If not, see
- *    <http://www.mongodb.com/licensing/server-side-public-license>.
- *
- *    As a special exception, the copyright holders give permission to link the
- *    code of portions of this program with the OpenSSL library under certain
- *    conditions as described in each individual source file and distribute
- *    linked combinations including the program with the OpenSSL library. You
- *    must comply with the Server Side Public License in all respects for
- *    all of the code used other than as permitted herein. If you modify file(s)
- *    with this exception, you may extend this exception to your version of the
- *    file(s), but you are not obligated to do so. If you do not wish to do so,
- *    delete this exception statement from your version. If you delete this
- *    exception statement from all source files in the program, then also delete
- *    it in the license file.
- */
-
-#pragma once
-
-#include <vector>
-
-#include "mongo/db/bson/dotted_path_support.h"
-#include "mongo/db/catalog/collection.h"
-#include "mongo/db/geo/shapes.h"
-#include "mongo/db/record_id.h"
-
-namespace mongo {
-
-namespace dps = ::mongo::dotted_path_support;
-
-class GeoHaystackSearchHopper {
-public:
-    /**
-     * Constructed with a point, a max distance from that point, and a max number of
-     * matched points to store.
-     * @param n  The centroid that we're searching
-     * @param maxDistance  The maximum distance to consider from that point
-     * @param limit  The maximum number of results to return
-     * @param geoField  Which field in the provided RecordId has the point to test.
-     */
-    GeoHaystackSearchHopper(OperationContext* opCtx,
-                            const BSONObj& nearObj,
-                            double maxDistance,
-                            unsigned limit,
-                            const std::string& geoField,
-                            const Collection* collection)
-        : _opCtx(opCtx),
-          _collection(collection),
-          _near(nearObj),
-          _maxDistance(maxDistance),
-          _limit(limit),
-          _geoField(geoField) {}
-
-    // Consider the point in loc, and keep it if it's within _maxDistance (and we have space for
-    // it)
-    void consider(const RecordId& loc) {
-        if (limitReached())
-            return;
-        Point p(dps::extractElementAtPath(_collection->docFor(_opCtx, loc).value(), _geoField));
-        if (distance(_near, p) > _maxDistance)
-            return;
-        _locs.push_back(loc);
-    }
-
-    int appendResultsTo(BSONArrayBuilder* b) {
-        for (unsigned i = 0; i < _locs.size(); i++)
-            b->append(_collection->docFor(_opCtx, _locs[i]).value());
-        return _locs.size();
-    }
-
-    // Have we stored as many points as we can?
-    bool limitReached() const {
-        return _locs.size() >= _limit;
-    }
-
-private:
-    OperationContext* _opCtx;
-    const Collection* _collection;
-
-    Point _near;
-    double _maxDistance;
-    unsigned _limit;
-    const std::string _geoField;
-    std::vector<RecordId> _locs;
-};
-
-}  // namespace mongo
diff --git a/src/mongo/db/index/index_access_method.cpp b/src/mongo/db/index/index_access_method.cpp
index 938e0261a8..7b9f713552 100644
--- a/src/mongo/db/index/index_access_method.cpp
+++ b/src/mongo/db/index/index_access_method.cpp
@@ -729,9 +729,6 @@ void AbstractIndexAccessMethod::getKeys(SharedBufferFragmentBuilder& pooledBuffe
                                               16766,
                                               // Ambiguous array field name
                                               16746,
-                                              // Haystack
-                                              16775,
-                                              16776,
                                               // 2dsphere geo
                                               16755,
                                               16756,
diff --git a/src/mongo/db/index/index_access_method_factory_impl.cpp b/src/mongo/db/index/index_access_method_factory_impl.cpp
index 93328fa7c8..09592e8aba 100644
--- a/src/mongo/db/index/index_access_method_factory_impl.cpp
+++ b/src/mongo/db/index/index_access_method_factory_impl.cpp
@@ -37,7 +37,6 @@
 #include "mongo/db/index/btree_access_method.h"
 #include "mongo/db/index/fts_access_method.h"
 #include "mongo/db/index/hash_access_method.h"
-#include "mongo/db/index/haystack_access_method.h"
 #include "mongo/db/index/s2_access_method.h"
 #include "mongo/db/index/wildcard_access_method.h"
 #include "mongo/logv2/log.h"
@@ -56,8 +55,6 @@ std::unique_ptr<IndexAccessMethod> IndexAccessMethodFactoryImpl::make(
         return std::make_unique<S2AccessMethod>(entry, std::move(sortedDataInterface));
     else if (IndexNames::TEXT == type)
         return std::make_unique<FTSAccessMethod>(entry, std::move(sortedDataInterface));
-    else if (IndexNames::GEO_HAYSTACK == type)
-        return std::make_unique<HaystackAccessMethod>(entry, std::move(sortedDataInterface));
     else if (IndexNames::GEO_2D == type)
         return std::make_unique<TwoDAccessMethod>(entry, std::move(sortedDataInterface));
     else if (IndexNames::WILDCARD == type)
diff --git a/src/mongo/db/index/index_descriptor.cpp b/src/mongo/db/index/index_descriptor.cpp
index 248e87e8b9..2fbcd96890 100644
--- a/src/mongo/db/index/index_descriptor.cpp
+++ b/src/mongo/db/index/index_descriptor.cpp
@@ -81,7 +81,6 @@ constexpr StringData IndexDescriptor::kCollationFieldName;
 constexpr StringData IndexDescriptor::kDefaultLanguageFieldName;
 constexpr StringData IndexDescriptor::kDropDuplicatesFieldName;
 constexpr StringData IndexDescriptor::kExpireAfterSecondsFieldName;
-constexpr StringData IndexDescriptor::kGeoHaystackBucketSize;
 constexpr StringData IndexDescriptor::kIndexNameFieldName;
 constexpr StringData IndexDescriptor::kIndexVersionFieldName;
 constexpr StringData IndexDescriptor::kKeyPatternFieldName;
diff --git a/src/mongo/db/index/index_descriptor.h b/src/mongo/db/index/index_descriptor.h
index b8d61b2056..e0d4f10dc1 100644
--- a/src/mongo/db/index/index_descriptor.h
+++ b/src/mongo/db/index/index_descriptor.h
@@ -70,7 +70,6 @@ public:
     static constexpr StringData kDefaultLanguageFieldName = "default_language"_sd;
     static constexpr StringData kDropDuplicatesFieldName = "dropDups"_sd;
     static constexpr StringData kExpireAfterSecondsFieldName = "expireAfterSeconds"_sd;
-    static constexpr StringData kGeoHaystackBucketSize = "bucketSize"_sd;
     static constexpr StringData kIndexNameFieldName = "name"_sd;
     static constexpr StringData kIndexVersionFieldName = "v"_sd;
     static constexpr StringData kKeyPatternFieldName = "key"_sd;
diff --git a/src/mongo/dbtests/clienttests.cpp b/src/mongo/dbtests/clienttests.cpp
index a18f4c259b..d4b4bc6a70 100644
--- a/src/mongo/dbtests/clienttests.cpp
+++ b/src/mongo/dbtests/clienttests.cpp
@@ -329,22 +329,6 @@ public:
     }
 };
 
-class CreateHaystackIndex : public Base {
-public:
-    CreateHaystackIndex() : Base("CreateHaystackIndex") {}
-    void run() {
-        const ServiceContext::UniqueOperationContext opCtxPtr = cc().makeOperationContext();
-        OperationContext& opCtx = *opCtxPtr;
-        DBDirectClient db(&opCtx);
-
-        db.createIndex(ns(),
-                       IndexSpec()
-                           .addKey("aField", IndexSpec::kIndexTypeGeoHaystack)
-                           .addKey("otherField", IndexSpec::kIndexTypeDescending)
-                           .geoHaystackBucketSize(1.0));
-    }
-};
-
 class Create2DSphereIndex : public Base {
 public:
     Create2DSphereIndex() : Base("Create2DSphereIndex") {}
@@ -403,7 +387,6 @@ public:
         add<CreateUniqueSparseDropDupsIndexInBackground>();
         add<CreateComplexTextIndex>();
         add<Create2DIndex>();
-        add<CreateHaystackIndex>();
         add<Create2DSphereIndex>();
         add<CreateHashedIndex>();
         add<CreateIndexFailure>();
diff --git a/src/mongo/dbtests/commandtests.cpp b/src/mongo/dbtests/commandtests.cpp
index 1066fe6ddb..140c81540b 100644
--- a/src/mongo/dbtests/commandtests.cpp
+++ b/src/mongo/dbtests/commandtests.cpp
@@ -295,55 +295,6 @@ public:
     }
 };
 
-class GeoSearch : Base {
-public:
-    void run() {
-        // Subset of geo_haystack1.js
-
-        int n = 0;
-        for (int x = 0; x < 20; x++) {
-            for (int y = 0; y < 20; y++) {
-                db.insert(nss().ns(),
-                          BSON("_id" << n << "loc" << BSON_ARRAY(x << y) << "z" << n % 5));
-                n++;
-            }
-        }
-
-        // Build geoHaystack index. Can's use db.ensureIndex, no way to pass "bucketSize".
-        // So run createIndexes command instead.
-        //
-        // Shell example:
-        // t.ensureIndex( { loc : "geoHaystack" , z : 1 }, { bucketSize : .7 } );
-
-        {
-            BSONObjBuilder cmd;
-            cmd.append("createIndexes", nsColl());
-            cmd.append("indexes",
-                       BSON_ARRAY(BSON("key" << BSON("loc"
-                                                     << "geoHaystack"
-                                                     << "z" << 1.0)
-                                             << "name"
-                                             << "loc_geoHaystack_z_1"
-                                             << "bucketSize" << static_cast<double>(0.7))));
-
-            BSONObj result;
-            ASSERT(db.runCommand(nsDb(), cmd.obj(), result));
-        }
-
-        {
-            BSONObjBuilder cmd;
-            cmd.appendSymbol("geoSearch", nsColl());  // Use Symbol for SERVER-16260
-            cmd.append("near", BSON_ARRAY(7 << 8));
-            cmd.append("maxDistance", 3);
-            cmd.append("search", BSON("z" << 3));
-
-            BSONObj result;
-            bool ok = db.runCommand(nsDb(), cmd.obj(), result);
-            LOGV2(24187, "{result_jsonString}", "result_jsonString"_attr = result.jsonString());
-            ASSERT(ok);
-        }
-    }
-};
 }  // namespace SymbolArgument
 
 /**
@@ -375,7 +326,6 @@ public:
         add<SymbolArgument::DropIndexes>();
         add<SymbolArgument::FindAndModify>();
         add<SymbolArgument::Drop>();
-        add<SymbolArgument::GeoSearch>();
         add<SymbolArgument::CreateIndexWithNoKey>();
         add<SymbolArgument::CreateIndexWithDuplicateKey>();
         add<SymbolArgument::CreateIndexWithEmptyStringAsValue>();
