diff --git a/include/collection_manager.h b/include/collection_manager.h index 80509c65..d50b442c 100644 --- a/include/collection_manager.h +++ b/include/collection_manager.h @@ -64,6 +64,8 @@ private: spp::sparse_hash_map collection_symlinks; + spp::sparse_hash_map preset_configs; + // Auto incrementing ID assigned to each collection // Using a ID instead of a collection's name makes renaming possible std::atomic next_collection_id; @@ -95,6 +97,7 @@ public: static constexpr const char* NEXT_COLLECTION_ID_KEY = "$CI"; static constexpr const char* SYMLINK_PREFIX = "$SL"; + static constexpr const char* PRESET_PREFIX = "$PS"; static constexpr const char* BATCHED_INDEXER_STATE_KEY = "$BI"; static CollectionManager & get_instance() { @@ -159,6 +162,8 @@ public: static std::string get_symlink_key(const std::string & symlink_name); + static std::string get_preset_key(const std::string & preset_name); + Store* get_store(); ThreadPool* get_thread_pool() const; @@ -177,4 +182,13 @@ public: Option upsert_symlink(const std::string & symlink_name, const std::string & collection_name); Option delete_symlink(const std::string & symlink_name); + + // presets + spp::sparse_hash_map get_presets() const; + + Option get_preset(const std::string & preset_name) const; + + Option upsert_preset(const std::string & preset_name, const nlohmann::json& preset_config); + + Option delete_preset(const std::string & preset_name); }; \ No newline at end of file diff --git a/include/core_api.h b/include/core_api.h index a283cc16..91862be7 100644 --- a/include/core_api.h +++ b/include/core_api.h @@ -45,6 +45,16 @@ bool put_upsert_alias(const std::shared_ptr& req, const std::shared_pt bool del_alias(const std::shared_ptr& req, const std::shared_ptr& res); +// Presets + +bool get_presets(const std::shared_ptr& req, const std::shared_ptr& res); + +bool get_preset(const std::shared_ptr& req, const std::shared_ptr& res); + +bool put_upsert_preset(const std::shared_ptr& req, const std::shared_ptr& res); + +bool del_preset(const std::shared_ptr& req, const std::shared_ptr& res); + // Overrides bool get_overrides(const std::shared_ptr& req, const std::shared_ptr& res); diff --git a/src/collection_manager.cpp b/src/collection_manager.cpp index 691e2bd9..19e8113c 100644 --- a/src/collection_manager.cpp +++ b/src/collection_manager.cpp @@ -207,6 +207,8 @@ Option CollectionManager::load(const size_t collection_batch_size, const s return num_processed == num_collections; }); + // load aliases + std::string symlink_prefix_key = std::string(SYMLINK_PREFIX) + "_"; rocksdb::Iterator* iter = store->scan(symlink_prefix_key); while(iter->Valid() && iter->key().starts_with(symlink_prefix_key)) { @@ -218,6 +220,19 @@ Option CollectionManager::load(const size_t collection_batch_size, const s delete iter; + // load presets + + std::string preset_prefix_key = std::string(PRESET_PREFIX) + "_"; + iter = store->scan(preset_prefix_key); + while(iter->Valid() && iter->key().starts_with(preset_prefix_key)) { + std::vector parts; + StringUtils::split(iter->key().ToString(), parts, preset_prefix_key); + preset_configs[parts[0]] = iter->value().ToString(); + iter->Next(); + } + + delete iter; + LOG(INFO) << "Loaded " << num_collections << " collection(s)."; loading_pool.shutdown(); @@ -245,6 +260,8 @@ void CollectionManager::dispose() { } collections.clear(); + collection_symlinks.clear(); + preset_configs.clear(); store->close(); } @@ -596,6 +613,8 @@ Option CollectionManager::do_search(std::map& re const char *EXHAUSTIVE_SEARCH = "exhaustive_search"; const char *SPLIT_JOIN_TOKENS = "split_join_tokens"; + const char *PRESET = "preset"; + if(req_params.count(NUM_TYPOS) == 0) { req_params[NUM_TYPOS] = "2"; } @@ -1234,3 +1253,47 @@ Option CollectionManager::load_collection(const nlohmann::json &collection return Option(true); } + +spp::sparse_hash_map CollectionManager::get_presets() const { + std::shared_lock lock(mutex); + return preset_configs; +} + +Option CollectionManager::get_preset(const string& preset_name) const { + std::shared_lock lock(mutex); + + const auto& it = preset_configs.find(preset_name); + if(it != preset_configs.end()) { + return Option(it->second); + } + + return Option(404, "Not found."); +} + +Option CollectionManager::upsert_preset(const string& preset_name, const nlohmann::json& preset_config) { + std::unique_lock lock(mutex); + + bool inserted = store->insert(get_preset_key(preset_name), preset_config.dump()); + if(!inserted) { + return Option(500, "Unable to insert into store."); + } + + preset_configs[preset_name] = preset_config; + return Option(true); +} + +std::string CollectionManager::get_preset_key(const string& preset_name) { + return std::string(PRESET_PREFIX) + "_" + preset_name; +} + +Option CollectionManager::delete_preset(const string& preset_name) { + std::unique_lock lock(mutex); + + bool removed = store->remove(get_preset_key(preset_name)); + if(!removed) { + return Option(500, "Unable to delete from store."); + } + + preset_configs.erase(preset_name); + return Option(true); +} diff --git a/src/core_api.cpp b/src/core_api.cpp index 934c0dd0..9dc225d6 100644 --- a/src/core_api.cpp +++ b/src/core_api.cpp @@ -323,13 +323,23 @@ bool post_multi_search(const std::shared_ptr& req, const std::shared_p } nlohmann::json req_json; + const auto preset_it = req->params.find("preset"); - try { - req_json = nlohmann::json::parse(req->body); - } catch(const std::exception& e) { - LOG(ERROR) << "JSON error: " << e.what(); - res->set_400("Bad JSON."); - return false; + if(preset_it != req->params.end()) { + const auto preset_op = CollectionManager::get_instance().get_preset(preset_it->second); + if(preset_op.ok()) { + req_json = preset_op.get(); + } + } + + if(req_json.empty()) { + try { + req_json = nlohmann::json::parse(req->body); + } catch(const std::exception& e) { + LOG(ERROR) << "JSON error: " << e.what(); + res->set_400("Bad JSON."); + return false; + } } if(req_json.count("searches") == 0) { @@ -1486,3 +1496,95 @@ bool is_doc_del_route(uint64_t route_hash) { bool found = server->get_route(route_hash, &rpath); return found && (rpath->handler == del_remove_document || rpath->handler == del_remove_documents); } + +bool get_presets(const std::shared_ptr& req, const std::shared_ptr& res) { + CollectionManager & collectionManager = CollectionManager::get_instance(); + const spp::sparse_hash_map & presets = collectionManager.get_presets(); + nlohmann::json res_json = nlohmann::json::object(); + res_json["presets"] = nlohmann::json::array(); + + for(const auto& preset_kv: presets) { + nlohmann::json preset; + preset["name"] = preset_kv.first; + preset["value"] = preset_kv.second; + res_json["presets"].push_back(preset); + } + + res->set_200(res_json.dump()); + return true; +} + +bool get_preset(const std::shared_ptr& req, const std::shared_ptr& res) { + const std::string & preset_name = req->params["preset_name"]; + CollectionManager & collectionManager = CollectionManager::get_instance(); + Option preset_op = collectionManager.get_preset(preset_name); + + if(!preset_op.ok()) { + res->set_404(); + return false; + } + + nlohmann::json res_json; + res_json["name"] = preset_name; + res_json["value"] = preset_op.get(); + + res->set_200(res_json.dump()); + return true; +} + +bool put_upsert_preset(const std::shared_ptr& req, const std::shared_ptr& res) { + nlohmann::json req_json; + + try { + req_json = nlohmann::json::parse(req->body); + } catch(const std::exception& e) { + LOG(ERROR) << "JSON error: " << e.what(); + res->set_400("Bad JSON."); + return false; + } + + CollectionManager & collectionManager = CollectionManager::get_instance(); + const std::string & preset_name = req->params["name"]; + + const char* PRESET_VALUE = "value"; + + if(req_json.count(PRESET_VALUE) == 0) { + res->set_400(std::string("Parameter `") + PRESET_VALUE + "` is required."); + return false; + } + + Option success_op = collectionManager.upsert_preset(preset_name, req_json[PRESET_VALUE]); + if(!success_op.ok()) { + res->set_500(success_op.error()); + return false; + } + + req_json["name"] = preset_name; + + res->set_200(req_json.dump()); + return true; +} + +bool del_preset(const std::shared_ptr& req, const std::shared_ptr& res) { + const std::string & preset_name = req->params["name"]; + CollectionManager & collectionManager = CollectionManager::get_instance(); + + Option preset_op = collectionManager.get_preset(preset_name); + if(!preset_op.ok()) { + res->set_404(); + return false; + } + + Option delete_op = collectionManager.delete_preset(preset_name); + + if(!delete_op.ok()) { + res->set_500(delete_op.error()); + return false; + } + + nlohmann::json res_json; + res_json["name"] = preset_name; + res_json["value"] = preset_op.get(); + res->set_200(res_json.dump()); + return true; +} diff --git a/src/main/typesense_server.cpp b/src/main/typesense_server.cpp index d70958b6..d22ae714 100644 --- a/src/main/typesense_server.cpp +++ b/src/main/typesense_server.cpp @@ -58,6 +58,11 @@ void master_server_routes() { server->post("/keys", post_create_key); server->del("/keys/:id", del_key); + server->get("/presets", get_presets); + server->get("/presets/:name", get_preset); + server->put("/presets/:name", put_upsert_preset); + server->del("/presets/:name", del_preset); + // meta server->get("/metrics.json", get_metrics_json); server->get("/stats.json", get_stats_json); diff --git a/test/collection_manager_test.cpp b/test/collection_manager_test.cpp index 7872fbb2..0534d7c3 100644 --- a/test/collection_manager_test.cpp +++ b/test/collection_manager_test.cpp @@ -673,3 +673,56 @@ TEST_F(CollectionManagerTest, ParseSortByClause) { sort_by_parsed = CollectionManager::parse_sort_by_str(",,", sort_fields); ASSERT_FALSE(sort_by_parsed); } + +TEST_F(CollectionManagerTest, Presets) { + // try getting on a blank slate + auto presets = collectionManager.get_presets(); + ASSERT_TRUE(presets.empty()); + + // insert some presets + nlohmann::json preset_obj; + + preset_obj["query_by"] = "foo"; + collectionManager.upsert_preset("preset1", preset_obj); + + preset_obj["query_by"] = "bar"; + collectionManager.upsert_preset("preset2", preset_obj); + + ASSERT_EQ(2, collectionManager.get_presets().size()); + + // try fetching individual presets + auto preset_op = collectionManager.get_preset("preset1"); + ASSERT_TRUE(preset_op.ok()); + ASSERT_EQ(1, preset_op.get().size()); + ASSERT_EQ("foo", preset_op.get()["query_by"]); + + preset_op = collectionManager.get_preset("preset2"); + ASSERT_TRUE(preset_op.ok()); + ASSERT_EQ(1, preset_op.get().size()); + ASSERT_EQ("bar", preset_op.get()["query_by"]); + + // delete a preset + auto del_op = collectionManager.delete_preset("preset2"); + ASSERT_TRUE(del_op.ok()); + + std::string val; + auto status = store->get(CollectionManager::get_preset_key("preset2"), val); + ASSERT_EQ(StoreStatus::NOT_FOUND, status); + + ASSERT_EQ(1, collectionManager.get_presets().size()); + preset_op = collectionManager.get_preset("preset2"); + ASSERT_FALSE(preset_op.ok()); + ASSERT_EQ(404, preset_op.code()); + + // should be able to restore state on init + collectionManager.dispose(); + delete store; + + store = new Store("/tmp/typesense_test/coll_manager_test_db"); + collectionManager.init(store, 1.0, "auth_key", quit); + collectionManager.load(8, 1000); + + ASSERT_EQ(1, collectionManager.get_presets().size()); + preset_op = collectionManager.get_preset("preset1"); + ASSERT_TRUE(preset_op.ok()); +}