Skip to content

Commit 97497be

Browse files
committed
Add single fingerprint APIs
1 parent 0f46984 commit 97497be

File tree

5 files changed

+186
-1
lines changed

5 files changed

+186
-1
lines changed

src/Index.zig

+20
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const Change = @import("change.zig").Change;
99
const SearchResult = @import("common.zig").SearchResult;
1010
const SearchResults = @import("common.zig").SearchResults;
1111
const SegmentInfo = @import("segment.zig").SegmentInfo;
12+
const DocInfo = @import("common.zig").DocInfo;
1213

1314
const Oplog = @import("Oplog.zig");
1415

@@ -475,6 +476,25 @@ pub fn search(self: *Self, hashes: []const u32, allocator: std.mem.Allocator, de
475476
return results;
476477
}
477478

479+
pub fn getDocInfo(self: *Self, doc_id: u32) !?DocInfo {
480+
var snapshot = self.acquireSegments();
481+
defer self.releaseSegments(&snapshot); // FIXME this possibly deletes orphaned segments, do it in a separate thread
482+
483+
var result: ?DocInfo = null;
484+
inline for (segment_lists) |n| {
485+
const segments = @field(snapshot, n);
486+
if (segments.value.getDocInfo(doc_id)) |res| {
487+
result = res;
488+
}
489+
}
490+
if (result) |res| {
491+
if (!res.deleted) {
492+
return res;
493+
}
494+
}
495+
return null;
496+
}
497+
478498
pub const IndexInfo = struct {
479499
version: u64,
480500
attributes: std.AutoHashMapUnmanaged(u64, u64),

src/common.zig

+5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ const testing = std.testing;
44
const msgpack = @import("msgpack");
55
const SegmentInfo = @import("segment.zig").SegmentInfo;
66

7+
pub const DocInfo = struct {
8+
version: u64,
9+
deleted: bool,
10+
};
11+
712
pub const KeepOrDelete = enum {
813
keep,
914
delete,

src/segment_list.zig

+10
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const SearchResults = @import("common.zig").SearchResults;
66
const Change = @import("change.zig").Change;
77
const KeepOrDelete = @import("common.zig").KeepOrDelete;
88
const SegmentInfo = @import("segment.zig").SegmentInfo;
9+
const DocInfo = @import("common.zig").DocInfo;
910

1011
const Deadline = @import("utils/Deadline.zig");
1112

@@ -100,6 +101,15 @@ pub fn SegmentList(Segment: type) type {
100101
}
101102
}
102103

104+
pub fn getDocInfo(self: Self, doc_id: u32) ?DocInfo {
105+
var result: ?DocInfo = null;
106+
for (self.nodes.items) |node| {
107+
const active = node.value.docs.get(doc_id) orelse continue;
108+
result = .{ .version = node.value.info.version, .deleted = !active };
109+
}
110+
return result;
111+
}
112+
103113
pub fn search(self: Self, hashes: []const u32, results: *SearchResults, deadline: Deadline) !void {
104114
for (self.nodes.items) |node| {
105115
if (deadline.isExpired()) {

src/server.zig

+110-1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ pub fn run(allocator: std.mem.Allocator, indexes: *MultiIndex, address: []const
8282
// Bulk API
8383
router.post("/:index/_update", handleUpdate);
8484

85+
// Fingerprint API
86+
router.head("/:index/:id", handleHeadFingerprint);
87+
router.get("/:index/:id", handleGetFingerprint);
88+
router.put("/:index/:id", handlePutFingerprint);
89+
router.delete("/:index/:id", handleDeleteFingerprint);
90+
8591
// Index API
8692
router.head("/:index", handleHeadIndex);
8793
router.get("/:index", handleGetIndex);
@@ -113,8 +119,34 @@ const SearchResultsJSON = struct {
113119
results: []SearchResultJSON,
114120
};
115121

122+
fn getId(req: *httpz.Request, res: *httpz.Response, send_body: bool) !?u32 {
123+
const id_str = req.param("id") orelse {
124+
if (send_body) {
125+
try writeErrorResponse(400, error.MissingId, req, res);
126+
} else {
127+
res.status = 400;
128+
}
129+
return null;
130+
};
131+
return std.fmt.parseInt(u32, id_str, 10) catch |err| {
132+
if (send_body) {
133+
try writeErrorResponse(400, err, req, res);
134+
} else {
135+
res.status = 400;
136+
}
137+
return null;
138+
};
139+
}
140+
116141
fn getIndex(ctx: *Context, req: *httpz.Request, res: *httpz.Response, send_body: bool) !?*IndexData {
117-
const index_name = req.param("index") orelse return null;
142+
const index_name = req.param("index") orelse {
143+
if (send_body) {
144+
try writeErrorResponse(400, error.MissingIndexName, req, res);
145+
} else {
146+
res.status = 400;
147+
}
148+
return null;
149+
};
118150
const index = ctx.indexes.getIndex(index_name) catch |err| {
119151
if (err == error.IndexNotFound) {
120152
if (send_body) {
@@ -278,6 +310,83 @@ fn handleUpdate(ctx: *Context, req: *httpz.Request, res: *httpz.Response) !void
278310
return writeResponse(EmptyResponse{}, req, res);
279311
}
280312

313+
fn handleHeadFingerprint(ctx: *Context, req: *httpz.Request, res: *httpz.Response) !void {
314+
const index_ref = try getIndex(ctx, req, res, false) orelse return;
315+
const index = &index_ref.index;
316+
defer releaseIndex(ctx, index_ref);
317+
318+
const id = try getId(req, res, false) orelse return;
319+
const info = try index.getDocInfo(id);
320+
321+
res.status = if (info == null) 404 else 200;
322+
}
323+
324+
const GetFingerprintResponse = struct {
325+
version: u64,
326+
327+
pub fn msgpackFormat() msgpack.StructFormat {
328+
return .{ .as_map = .{ .key = .{ .field_name_prefix = 1 } } };
329+
}
330+
};
331+
332+
fn handleGetFingerprint(ctx: *Context, req: *httpz.Request, res: *httpz.Response) !void {
333+
const index_ref = try getIndex(ctx, req, res, true) orelse return;
334+
const index = &index_ref.index;
335+
defer releaseIndex(ctx, index_ref);
336+
337+
const id = try getId(req, res, true) orelse return;
338+
const info = try index.getDocInfo(id) orelse {
339+
return writeErrorResponse(404, error.FingerprintNotFound, req, res);
340+
};
341+
342+
return writeResponse(GetFingerprintResponse{ .version = info.version }, req, res);
343+
}
344+
345+
const PutFingerprintRequest = struct {
346+
hashes: []u32,
347+
348+
pub fn msgpackFormat() msgpack.StructFormat {
349+
return .{ .as_map = .{ .key = .{ .field_name_prefix = 1 } } };
350+
}
351+
};
352+
353+
fn handlePutFingerprint(ctx: *Context, req: *httpz.Request, res: *httpz.Response) !void {
354+
const body = try getRequestBody(PutFingerprintRequest, req, res) orelse return;
355+
356+
const index_ref = try getIndex(ctx, req, res, true) orelse return;
357+
const index = &index_ref.index;
358+
defer releaseIndex(ctx, index_ref);
359+
360+
const id = try getId(req, res, true) orelse return;
361+
const change: Change = .{ .insert = .{
362+
.id = id,
363+
.hashes = body.hashes,
364+
} };
365+
366+
metrics.update(1);
367+
368+
try index.update(&[_]Change{change});
369+
370+
return writeResponse(EmptyResponse{}, req, res);
371+
}
372+
373+
fn handleDeleteFingerprint(ctx: *Context, req: *httpz.Request, res: *httpz.Response) !void {
374+
const index_ref = try getIndex(ctx, req, res, true) orelse return;
375+
const index = &index_ref.index;
376+
defer releaseIndex(ctx, index_ref);
377+
378+
const id = try getId(req, res, true) orelse return;
379+
const change: Change = .{ .delete = .{
380+
.id = id,
381+
} };
382+
383+
metrics.update(1);
384+
385+
try index.update(&[_]Change{change});
386+
387+
return writeResponse(EmptyResponse{}, req, res);
388+
}
389+
281390
const Attributes = struct {
282391
attributes: std.AutoHashMapUnmanaged(u64, u64),
283392

tests/test_fingerprint_api.py

+41
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ def test_insert(client, index_name, create_index):
2020
'results': [{'id': 1, 'score': 3}]
2121
}
2222

23+
# verify we can get id
24+
req = client.get(f'/{index_name}/1')
25+
assert req.status_code == 200, req.content
26+
assert json.loads(req.content) == {
27+
'version': 1,
28+
}
29+
2330

2431
def test_update_full(client, index_name, create_index):
2532
# insert fingerprint
@@ -58,6 +65,13 @@ def test_update_full(client, index_name, create_index):
5865
'results': [{'id': 1, 'score': 3}]
5966
}
6067

68+
# verify we can get id
69+
req = client.get(f'/{index_name}/1')
70+
assert req.status_code == 200, req.content
71+
assert json.loads(req.content) == {
72+
'version': 2,
73+
}
74+
6175

6276
def test_update_partial(client, index_name, create_index):
6377
# insert fingerprint
@@ -96,6 +110,13 @@ def test_update_partial(client, index_name, create_index):
96110
'results': [{'id': 1, 'score': 3}]
97111
}
98112

113+
# verify we can get id
114+
req = client.get(f'/{index_name}/1')
115+
assert req.status_code == 200, req.content
116+
assert json.loads(req.content) == {
117+
'version': 2,
118+
}
119+
99120

100121
def test_delete(client, index_name, create_index):
101122
# insert fingerprint
@@ -125,6 +146,13 @@ def test_delete(client, index_name, create_index):
125146
'results': []
126147
}
127148

149+
# verify we can get id
150+
req = client.get(f'/{index_name}/1')
151+
assert req.status_code == 404, req.content
152+
assert json.loads(req.content) == {
153+
'error': 'FingerprintNotFound',
154+
}
155+
128156

129157
def test_persistence_after_soft_restart(server, client, index_name, create_index):
130158
# insert fingerprint
@@ -150,6 +178,13 @@ def test_persistence_after_soft_restart(server, client, index_name, create_index
150178
'results': [{'id': 1, 'score': 3}]
151179
}
152180

181+
# verify we can get id
182+
req = client.get(f'/{index_name}/1')
183+
assert req.status_code == 200, req.content
184+
assert json.loads(req.content) == {
185+
'version': 100,
186+
}
187+
153188

154189
def test_persistence_after_hard_restart(server, client, index_name, create_index):
155190
# insert fingerprint
@@ -177,3 +212,9 @@ def test_persistence_after_hard_restart(server, client, index_name, create_index
177212
'results': [{'id': 1, 'score': 3}]
178213
}
179214

215+
# verify we can get id
216+
req = client.get(f'/{index_name}/1')
217+
assert req.status_code == 200, req.content
218+
assert json.loads(req.content) == {
219+
'version': 100,
220+
}

0 commit comments

Comments
 (0)