|
@@ -1,2 +1,66 @@
|
|
|
|
|
+#include "collection_registry.hpp"
|
|
|
|
|
+#include "embeddings.hpp"
|
|
|
|
|
+#include "errors.hpp"
|
|
|
|
|
+#include "json_http.hpp"
|
|
|
#include "server.hpp"
|
|
#include "server.hpp"
|
|
|
-namespace svapi { void registerVectorRoutes(ApiServer&) {} }
|
|
|
|
|
|
|
+namespace svapi {
|
|
|
|
|
+namespace {
|
|
|
|
|
+CollectionMeta requireVectorCollection(ServerDeps* d, const httplib::Request& req,
|
|
|
|
|
+ std::string& projectOut, std::string& nameOut) {
|
|
|
|
|
+ ApiKey k = requireKey(*d, req);
|
|
|
|
|
+ projectOut = req.matches[1]; nameOut = req.matches[2];
|
|
|
|
|
+ requireProjectAccess(k, projectOut);
|
|
|
|
|
+ auto m = d->registry.get(projectOut, nameOut);
|
|
|
|
|
+ if (!m) throw ApiError(ErrCode::NotFound, "not_found", "no such collection");
|
|
|
|
|
+ if (m->kind != "vector") throw ApiError(ErrCode::Unprocessable, "not_vector", "collection is not a vector collection");
|
|
|
|
|
+ return *m;
|
|
|
|
|
+}
|
|
|
|
|
+std::vector<float> resolveVector(ServerDeps* d, const CollectionMeta& meta, const nlohmann::json& body,
|
|
|
|
|
+ const char* vecField, const char* textField) {
|
|
|
|
|
+ std::vector<float> v;
|
|
|
|
|
+ if (body.contains(vecField) && body[vecField].is_array()) {
|
|
|
|
|
+ v = body[vecField].get<std::vector<float>>();
|
|
|
|
|
+ } else if (body.contains(textField) && body[textField].is_string()) {
|
|
|
|
|
+ auto snap = d->settings.snapshot();
|
|
|
|
|
+ std::string model = meta.embeddingModel.empty() ? snap->defaultEmbeddingModel : meta.embeddingModel;
|
|
|
|
|
+ EmbeddingClient emb(snap->openaiApiBase, snap->openaiApiKey);
|
|
|
|
|
+ v = emb.embed(model, body[textField].get<std::string>());
|
|
|
|
|
+ } else {
|
|
|
|
|
+ throw ApiError(ErrCode::Unprocessable, "validation",
|
|
|
|
|
+ std::string("provide '") + vecField + "' or '" + textField + "'");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (v.size() != meta.vectorDimension)
|
|
|
|
|
+ throw ApiError(ErrCode::Unprocessable, "dimension_mismatch",
|
|
|
|
|
+ "expected dimension " + std::to_string(meta.vectorDimension) + ", got " + std::to_string(v.size()));
|
|
|
|
|
+ return v;
|
|
|
|
|
+}
|
|
|
|
|
+}
|
|
|
|
|
+void registerVectorRoutes(ApiServer& s) {
|
|
|
|
|
+ auto& svr = s.raw(); ServerDeps* d = &s.deps();
|
|
|
|
|
+
|
|
|
|
|
+ svr.Post(R"(/api/v1/projects/([^/]+)/collections/([^/]+)/vectors)", [d](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ std::string project, name; CollectionMeta meta = requireVectorCollection(d, req, project, name);
|
|
|
|
|
+ auto body = bodyJson(req);
|
|
|
|
|
+ std::vector<float> vec = resolveVector(d, meta, body, "vector", "text");
|
|
|
|
|
+ nlohmann::json doc = body.value("metadata", nlohmann::json::object());
|
|
|
|
|
+ if (!doc.is_object()) doc = nlohmann::json::object();
|
|
|
|
|
+ if (body.contains("text")) doc["text"] = body["text"];
|
|
|
|
|
+ doc["_vector"] = vec;
|
|
|
|
|
+ std::string newId = d->db.client().insert(qualify(project, name), doc, body.value("id", ""));
|
|
|
|
|
+ if (newId.empty()) throw ApiError(ErrCode::Unavailable, "db_error", "insert failed");
|
|
|
|
|
+ sendJson(res, 201, {{"id", newId}});
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ svr.Post(R"(/api/v1/projects/([^/]+)/collections/([^/]+)/search)", [d](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ std::string project, name; CollectionMeta meta = requireVectorCollection(d, req, project, name);
|
|
|
|
|
+ auto body = bodyJson(req);
|
|
|
|
|
+ std::vector<float> q = resolveVector(d, meta, body, "query_vector", "query_text");
|
|
|
|
|
+ uint32_t topK = body.value("top_k", 5u);
|
|
|
|
|
+ float minScore = body.value("min_score", 0.0f);
|
|
|
|
|
+ auto results = d->db.client().similaritySearch(qualify(project, name), q, topK, minScore);
|
|
|
|
|
+ nlohmann::json arr = nlohmann::json::array();
|
|
|
|
|
+ for (const auto& r : results) arr.push_back({{"id", r.id}, {"score", r.score}, {"data", r.data}});
|
|
|
|
|
+ sendJson(res, 200, {{"results", arr}});
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+}
|