|
@@ -16,8 +16,13 @@ CollectionMeta requireVectorCollection(ServerDeps* d, const httplib::Request& re
|
|
|
if (m->kind != "vector") throw ApiError(ErrCode::Unprocessable, "not_vector", "collection is not a vector collection");
|
|
if (m->kind != "vector") throw ApiError(ErrCode::Unprocessable, "not_vector", "collection is not a vector collection");
|
|
|
return *m;
|
|
return *m;
|
|
|
}
|
|
}
|
|
|
|
|
+bool wantsNoStore(const httplib::Request& req) {
|
|
|
|
|
+ if (!req.has_header("Cache-Control")) return false;
|
|
|
|
|
+ const std::string cc = req.get_header_value("Cache-Control");
|
|
|
|
|
+ return cc.find("no-store") != std::string::npos;
|
|
|
|
|
+}
|
|
|
std::vector<float> resolveVector(ServerDeps* d, const CollectionMeta& meta, const nlohmann::json& body,
|
|
std::vector<float> resolveVector(ServerDeps* d, const CollectionMeta& meta, const nlohmann::json& body,
|
|
|
- const char* vecField, const char* textField) {
|
|
|
|
|
|
|
+ const char* vecField, const char* textField, bool noStore) {
|
|
|
std::vector<float> v;
|
|
std::vector<float> v;
|
|
|
if (body.contains(vecField) && body[vecField].is_array()) {
|
|
if (body.contains(vecField) && body[vecField].is_array()) {
|
|
|
v = body[vecField].get<std::vector<float>>();
|
|
v = body[vecField].get<std::vector<float>>();
|
|
@@ -26,16 +31,19 @@ std::vector<float> resolveVector(ServerDeps* d, const CollectionMeta& meta, cons
|
|
|
std::string model = meta.embeddingModel.empty() ? snap->defaultEmbeddingModel : meta.embeddingModel;
|
|
std::string model = meta.embeddingModel.empty() ? snap->defaultEmbeddingModel : meta.embeddingModel;
|
|
|
// This helper is shared by /search (query_text) AND /vectors insert (text), so the cache
|
|
// This helper is shared by /search (query_text) AND /vectors insert (text), so the cache
|
|
|
// intentionally covers both paths — re-embedding identical text is wasted work either way.
|
|
// intentionally covers both paths — re-embedding identical text is wasted work either way.
|
|
|
|
|
+ // When noStore is true (Cache-Control: no-store), skip both the cache get and put.
|
|
|
const std::string text = body[textField].get<std::string>();
|
|
const std::string text = body[textField].get<std::string>();
|
|
|
EmbeddingCache::Key key{snap->openaiApiBase, model, text};
|
|
EmbeddingCache::Key key{snap->openaiApiBase, model, text};
|
|
|
- if (auto hit = embeddingCache().get(key)) {
|
|
|
|
|
|
|
+ std::optional<std::vector<float>> hit;
|
|
|
|
|
+ if (!noStore) hit = embeddingCache().get(key);
|
|
|
|
|
+ if (hit) {
|
|
|
v = std::move(*hit);
|
|
v = std::move(*hit);
|
|
|
} else {
|
|
} else {
|
|
|
EmbeddingClient emb(snap->openaiApiBase, snap->openaiApiKey,
|
|
EmbeddingClient emb(snap->openaiApiBase, snap->openaiApiKey,
|
|
|
snap->embeddingConnectTimeoutSec, snap->embeddingReadTimeoutSec,
|
|
snap->embeddingConnectTimeoutSec, snap->embeddingReadTimeoutSec,
|
|
|
snap->embeddingClientPoolSize);
|
|
snap->embeddingClientPoolSize);
|
|
|
v = emb.embed(model, text);
|
|
v = emb.embed(model, text);
|
|
|
- embeddingCache().put(key, v); // copy, not move: v is still needed for the dimension check below
|
|
|
|
|
|
|
+ if (!noStore) embeddingCache().put(key, v); // copy, not move: v is still needed for the dimension check below
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
throw ApiError(ErrCode::Unprocessable, "validation",
|
|
throw ApiError(ErrCode::Unprocessable, "validation",
|
|
@@ -53,7 +61,7 @@ void registerVectorRoutes(ApiServer& s) {
|
|
|
svr.Post(R"(/api/v1/projects/([^/]+)/collections/([^/]+)/vectors)", [d](const httplib::Request& req, httplib::Response& res) {
|
|
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);
|
|
std::string project, name; CollectionMeta meta = requireVectorCollection(d, req, project, name);
|
|
|
auto body = bodyJson(req);
|
|
auto body = bodyJson(req);
|
|
|
- std::vector<float> vec = resolveVector(d, meta, body, "vector", "text");
|
|
|
|
|
|
|
+ std::vector<float> vec = resolveVector(d, meta, body, "vector", "text", wantsNoStore(req));
|
|
|
nlohmann::json doc = body.value("metadata", nlohmann::json::object());
|
|
nlohmann::json doc = body.value("metadata", nlohmann::json::object());
|
|
|
if (!doc.is_object()) doc = nlohmann::json::object();
|
|
if (!doc.is_object()) doc = nlohmann::json::object();
|
|
|
if (body.contains("text")) doc["text"] = body["text"];
|
|
if (body.contains("text")) doc["text"] = body["text"];
|
|
@@ -66,7 +74,7 @@ void registerVectorRoutes(ApiServer& s) {
|
|
|
svr.Post(R"(/api/v1/projects/([^/]+)/collections/([^/]+)/search)", [d](const httplib::Request& req, httplib::Response& res) {
|
|
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);
|
|
std::string project, name; CollectionMeta meta = requireVectorCollection(d, req, project, name);
|
|
|
auto body = bodyJson(req);
|
|
auto body = bodyJson(req);
|
|
|
- std::vector<float> q = resolveVector(d, meta, body, "query_vector", "query_text");
|
|
|
|
|
|
|
+ std::vector<float> q = resolveVector(d, meta, body, "query_vector", "query_text", wantsNoStore(req));
|
|
|
uint32_t topK = body.value("top_k", 5u);
|
|
uint32_t topK = body.value("top_k", 5u);
|
|
|
float minScore = body.value("min_score", 0.0f);
|
|
float minScore = body.value("min_score", 0.0f);
|
|
|
auto results = d->db.client().similaritySearch(qualify(project, name), q, topK, minScore);
|
|
auto results = d->db.client().similaritySearch(qualify(project, name), q, topK, minScore);
|