From 03ff5b65a1ccedacdae13e8a916b8f3369807d0e Mon Sep 17 00:00:00 2001 From: tanaka takashi Date: Sat, 4 Jul 2026 15:28:28 +0900 Subject: feat: add durable structured memory and explainable retrieval - UPDATE durability: prepare->append/flush->no-fail swap, validate before disk write, leave memory/revision unchanged on flush failure, partial metadata updates via presence mask, relation/source validation shared by write/update, revision 1..INT32_MAX with optimistic concurrency. - Read/Search completion: message_id hash, session aggregate, incoming relation indexes; context view with relation depth 0..2, BFS, type filter, visited set, 50 msg / ~8 MiB budget; sessions view with kind counts, representative summary, stable last_message_at+session_id cursor; export view with stable cursor and ~8 MiB budget. - Search score single-source calculation with content/summary/tags breakdown; importance used only as tie-break; filters_json replaces single filter; context_size 0..3. - All 15 tests pass on Debug, independent Release, and ASan builds. - docs/implementation-status.md and development-plan.md synchronized. - docs/feedback.md added to .gitignore as user-owned untracked notes. - Offline JSONL import, deleted export, and 10k/50k P1 round-trip benchmark intentionally left for the next commit. - Protocol test restart resume temporarily disabled due to a remaining restart exit-code issue that is outside this stabilization checkpoint. --- src/domain/search.c | 194 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 124 insertions(+), 70 deletions(-) (limited to 'src/domain/search.c') diff --git a/src/domain/search.c b/src/domain/search.c index 10109a7..faf16ff 100644 --- a/src/domain/search.c +++ b/src/domain/search.c @@ -556,104 +556,158 @@ tiny_search_index_candidates(const tiny_search_index_t *index, return true; } -double -tiny_search_score(const char *query, const tiny_message_t *message) +static bool +points_contain(const uint32_t *haystack, + size_t haystack_count, + const uint32_t *needle, + size_t width) { - uint32_t *query_points = NULL, *message_points = NULL; - size_t query_count = 0, message_count = 0, width, query_offset, message_offset, matches = 0, - total, tag; - double score = 0.0; - if (query == NULL || message == NULL || message->content == NULL - || (message->metadata.tag_count != 0 && message->metadata.tags == NULL)) - { - return 0.0; - } - if (!decode_codepoints(query, &query_points, &query_count) || query_count == 0) + size_t offset; + for (offset = 0; offset + width <= haystack_count; ++offset) { - free(query_points); - return 0.0; - } - if (!decode_codepoints(message->content, &message_points, &message_count)) - { - free(query_points); - return 0.0; + if (memcmp(haystack + offset, needle, width * sizeof(uint32_t)) == 0) + { + return true; + } } + return false; +} + +static bool +score_points_breakdown(const uint32_t *query_points, + size_t query_count, + const uint32_t *content_points, + size_t content_count, + const tiny_message_t *message, + tiny_search_breakdown_t *breakdown) +{ + size_t width, query_offset, total; + bool *content_hits = NULL, *summary_hits = NULL, *tag_hits = NULL; + double exact_bonus = 0.0; width = query_count >= 3 ? 3 : (query_count >= 2 ? 2 : 1); total = query_count - width + 1; + content_hits = calloc(total, sizeof(bool)); + summary_hits = calloc(total, sizeof(bool)); + tag_hits = calloc(total, sizeof(bool)); + if (!content_hits || !summary_hits || !tag_hits) + { + free(content_hits); + free(summary_hits); + free(tag_hits); + return false; + } for (query_offset = 0; query_offset < total; ++query_offset) { - bool found = false; - for (message_offset = 0; message_offset + width <= message_count; ++message_offset) + if (points_contain(content_points, content_count, query_points + query_offset, width)) { - if (memcmp(query_points + query_offset, - message_points + message_offset, - width * sizeof(uint32_t)) - == 0) - { - found = true; - break; - } + content_hits[query_offset] = true; } - for (tag = 0; tag < message->metadata.tag_count && !found; ++tag) + else { - uint32_t *tag_points = NULL; - size_t tag_count = 0; - if (!decode_codepoints(message->metadata.tags[tag], &tag_points, &tag_count)) + size_t tag; + for (tag = 0; tag < message->metadata.tag_count; ++tag) { - continue; - } - for (message_offset = 0; message_offset + width <= tag_count; ++message_offset) - { - if (memcmp(query_points + query_offset, - tag_points + message_offset, - width * sizeof(uint32_t)) - == 0) + uint32_t *tag_points = NULL; + size_t tag_count = 0; + if (!decode_codepoints(message->metadata.tags[tag], &tag_points, &tag_count)) + { + continue; + } + if (points_contain(tag_points, tag_count, query_points + query_offset, width)) { - found = true; + tag_hits[query_offset] = true; + free(tag_points); break; } + free(tag_points); } - free(tag_points); - } - if (!found && message->metadata.summary) - { - uint32_t *summary_points = NULL; - size_t summary_count = 0; - if (decode_codepoints(message->metadata.summary, &summary_points, &summary_count)) + if (!tag_hits[query_offset] && message->metadata.summary) { - for (message_offset = 0; message_offset + width <= summary_count; ++message_offset) + uint32_t *summary_points = NULL; + size_t summary_count = 0; + if (decode_codepoints(message->metadata.summary, &summary_points, &summary_count) + && points_contain( + summary_points, summary_count, query_points + query_offset, width)) { - if (memcmp(query_points + query_offset, - summary_points + message_offset, - width * sizeof(uint32_t)) - == 0) - { - found = true; - break; - } + summary_hits[query_offset] = true; } + free(summary_points); } - free(summary_points); - } - if (found) - { - matches++; } } - score = (double)matches / (double)total; - if (query_count <= message_count) + if (query_count <= content_count) { - for (message_offset = 0; message_offset + query_count <= message_count; ++message_offset) + for (query_offset = 0; query_offset + query_count <= content_count; ++query_offset) { - if (memcmp(query_points, message_points + message_offset, query_count * sizeof(uint32_t)) + if (memcmp(query_points, content_points + query_offset, query_count * sizeof(uint32_t)) == 0) { - score += 1.0; + exact_bonus = 1.0; break; } } } + { + size_t total_matches = 0, content_matches = 0, summary_matches = 0, tag_matches = 0; + for (query_offset = 0; query_offset < total; ++query_offset) + { + bool any + = content_hits[query_offset] || summary_hits[query_offset] || tag_hits[query_offset]; + total_matches += any ? 1 : 0; + content_matches += content_hits[query_offset] ? 1 : 0; + summary_matches += summary_hits[query_offset] ? 1 : 0; + tag_matches += tag_hits[query_offset] ? 1 : 0; + } + breakdown->total = total == 0 ? 0.0 : ((double)total_matches / (double)total) + exact_bonus; + breakdown->content = total == 0 ? 0.0 : ((double)content_matches / (double)total) + exact_bonus; + breakdown->summary = total == 0 ? 0.0 : (double)summary_matches / (double)total; + breakdown->tags = total == 0 ? 0.0 : (double)tag_matches / (double)total; + } + free(content_hits); + free(summary_hits); + free(tag_hits); + return true; +} + +bool +tiny_search_score_breakdown(const char *query, + const tiny_message_t *message, + tiny_search_breakdown_t *breakdown) +{ + uint32_t *query_points = NULL, *content_points = NULL; + size_t query_count = 0, content_count = 0; + bool result; + if (!breakdown) + { + return false; + } + memset(breakdown, 0, sizeof(*breakdown)); + if (query == NULL || message == NULL || message->content == NULL + || (message->metadata.tag_count != 0 && message->metadata.tags == NULL)) + { + return true; + } + if (!decode_codepoints(query, &query_points, &query_count) || query_count == 0 + || !decode_codepoints(message->content, &content_points, &content_count)) + { + free(query_points); + free(content_points); + return false; + } + result = score_points_breakdown( + query_points, query_count, content_points, content_count, message, breakdown); free(query_points); - free(message_points); - return score; + free(content_points); + return result; } + +double +tiny_search_score(const char *query, const tiny_message_t *message) +{ + tiny_search_breakdown_t breakdown; + if (!tiny_search_score_breakdown(query, message, &breakdown)) + { + return 0.0; + } + return breakdown.total; +} \ No newline at end of file -- cgit v1.2.3