diff options
| author | tanaka takashi <isazaka@gmail.com> | 2026-06-29 02:36:40 +0900 |
|---|---|---|
| committer | tanaka takashi <isazaka@gmail.com> | 2026-06-29 02:36:40 +0900 |
| commit | 4c6a831aa2a57e2bcc20aa3fc2f1b7172f5be285 (patch) | |
| tree | 074021381abf57cad434430f25027115b681b8a9 /src/domain/search.c | |
| parent | 8d38b06f11c713e547d70468340242f23322152c (diff) | |
feat: 会話メモリのプロトタイプを実装
- 6種類のMCPツールと入力検証を追加
- バイナリログ、スナップショット、復旧、圧縮処理を実装
- Unicode n-gram検索とドメインモデルを追加
- 単体テストと統合テストを追加
- アーキテクチャと実装状況を文書化
- Cコードの記述スタイルを統一
Diffstat (limited to 'src/domain/search.c')
| -rw-r--r-- | src/domain/search.c | 350 |
1 files changed, 350 insertions, 0 deletions
diff --git a/src/domain/search.c b/src/domain/search.c new file mode 100644 index 0000000..a24b667 --- /dev/null +++ b/src/domain/search.c @@ -0,0 +1,350 @@ +#include "domain/search.h" + +#include <stdint.h> +#include <stdlib.h> +#include <string.h> + +typedef struct gram_bucket +{ + uint64_t hash; + uint32_t points[3]; + unsigned width; + size_t *messages; + size_t count; + size_t capacity; +} gram_bucket_t; + +struct tiny_search_index +{ + gram_bucket_t *buckets; + size_t count; + size_t capacity; + size_t message_count; +}; + +static bool +decode_codepoints(const char *text, uint32_t **output, size_t *count) +{ + const size_t bytes = strlen(text); + size_t offset = 0, used = 0; + uint32_t *points = malloc((bytes + 1) * sizeof(uint32_t)); + if (points == NULL) + { + return false; + } + while (offset < bytes) + { + unsigned char first = (unsigned char)text[offset++]; + uint32_t point; + unsigned continuation; + if (first < 0x80) + { + point = first; + continuation = 0; + } + else if ((first & 0xe0) == 0xc0) + { + point = first & 0x1f; + continuation = 1; + } + else if ((first & 0xf0) == 0xe0) + { + point = first & 0x0f; + continuation = 2; + } + else + { + point = first & 0x07; + continuation = 3; + } + while (continuation-- > 0) + { + point = (point << 6) | ((unsigned char)text[offset++] & 0x3f); + } + if (point >= 'A' && point <= 'Z') + { + point += 'a' - 'A'; + } + points[used++] = point; + } + *output = points; + *count = used; + return true; +} + +static uint64_t +gram_hash(const uint32_t *points, size_t width) +{ + uint64_t hash = 1469598103934665603ULL; + size_t index; + for (index = 0; index < width; ++index) + { + hash ^= points[index]; + hash *= 1099511628211ULL; + } + return hash; +} + +static gram_bucket_t * +find_bucket(const tiny_search_index_t *index, const uint32_t *points, unsigned width) +{ + const uint64_t hash = gram_hash(points, width); + size_t position; + for (position = 0; position < index->count; ++position) + { + if (index->buckets[position].hash == hash && index->buckets[position].width == width + && memcmp(index->buckets[position].points, points, width * sizeof(uint32_t)) == 0) + { + return &index->buckets[position]; + } + } + return NULL; +} + +static bool +add_gram(tiny_search_index_t *index, const uint32_t *points, unsigned width, size_t message_index) +{ + gram_bucket_t *bucket = find_bucket(index, points, width); + if (bucket == NULL) + { + gram_bucket_t *resized; + size_t capacity; + if (index->count == index->capacity) + { + capacity = index->capacity ? index->capacity * 2 : 64; + resized = realloc(index->buckets, capacity * sizeof(*resized)); + if (!resized) + { + return false; + } + index->buckets = resized; + index->capacity = capacity; + } + bucket = &index->buckets[index->count++]; + memset(bucket, 0, sizeof(*bucket)); + bucket->hash = gram_hash(points, width); + bucket->width = width; + memcpy(bucket->points, points, width * sizeof(uint32_t)); + } + if (bucket->count > 0 && bucket->messages[bucket->count - 1] == message_index) + { + return true; + } + if (bucket->count == bucket->capacity) + { + const size_t capacity = bucket->capacity ? bucket->capacity * 2 : 8; + size_t *resized = realloc(bucket->messages, capacity * sizeof(*resized)); + if (!resized) + { + return false; + } + bucket->messages = resized; + bucket->capacity = capacity; + } + bucket->messages[bucket->count++] = message_index; + return true; +} + +static bool +index_text(tiny_search_index_t *index, const char *text, size_t message_index) +{ + uint32_t *points = NULL; + size_t count = 0, offset; + unsigned width; + if (!decode_codepoints(text, &points, &count)) + { + return false; + } + for (width = 2; width <= 3; ++width) + { + for (offset = 0; offset + width <= count; ++offset) + { + if (!add_gram(index, points + offset, width, message_index)) + { + free(points); + return false; + } + } + } + free(points); + return true; +} + +tiny_search_index_t * +tiny_search_index_create(void) +{ + return calloc(1, sizeof(tiny_search_index_t)); +} + +void +tiny_search_index_free(tiny_search_index_t *index) +{ + size_t position; + if (!index) + { + return; + } + for (position = 0; position < index->count; ++position) + { + free(index->buckets[position].messages); + } + free(index->buckets); + free(index); +} + +bool +tiny_search_index_add(tiny_search_index_t *index, + const tiny_message_t *message, + size_t message_index) +{ + size_t tag; + if (!index_text(index, message->content, message_index)) + { + return false; + } + for (tag = 0; tag < message->metadata.tag_count; ++tag) + { + if (!index_text(index, message->metadata.tags[tag], message_index)) + { + return false; + } + } + if (message_index >= index->message_count) + { + index->message_count = message_index + 1; + } + return true; +} + +bool +tiny_search_index_candidates(const tiny_search_index_t *index, + const char *query, + size_t **output, + size_t *count) +{ + uint32_t *points = NULL; + size_t point_count = 0, width, offset, used = 0; + bool *seen; + size_t *items; + *output = NULL; + *count = 0; + if (!decode_codepoints(query, &points, &point_count)) + { + return false; + } + items = malloc((index->message_count ? index->message_count : 1) * sizeof(*items)); + seen = calloc(index->message_count ? index->message_count : 1, sizeof(*seen)); + if (!items || !seen) + { + free(points); + free(items); + free(seen); + return false; + } + if (point_count < 2) + { + for (offset = 0; offset < index->message_count; ++offset) + { + seen[offset] = true; + items[used++] = offset; + } + } + else + { + gram_bucket_t *bucket; + width = point_count >= 3 ? 3 : 2; + for (offset = 0; offset + width <= point_count; ++offset) + { + if ((bucket = find_bucket(index, points + offset, (unsigned)width)) != NULL) + { + size_t item; + for (item = 0; item < bucket->count; ++item) + { + if (!seen[bucket->messages[item]]) + { + seen[bucket->messages[item]] = true; + items[used++] = bucket->messages[item]; + } + } + } + } + } + free(points); + free(seen); + *output = items; + *count = used; + return true; +} + +double +tiny_search_score(const char *query, const tiny_message_t *message) +{ + 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 (!decode_codepoints(query, &query_points, &query_count) + || !decode_codepoints(message->content, &message_points, &message_count) || query_count == 0) + { + goto done; + } + width = query_count >= 3 ? 3 : (query_count >= 2 ? 2 : 1); + total = query_count - width + 1; + for (query_offset = 0; query_offset < total; ++query_offset) + { + bool found = false; + for (message_offset = 0; message_offset + width <= message_count; ++message_offset) + { + if (memcmp(query_points + query_offset, + message_points + message_offset, + width * sizeof(uint32_t)) + == 0) + { + found = true; + break; + } + } + for (tag = 0; tag < message->metadata.tag_count && !found; ++tag) + { + uint32_t *tag_points = NULL; + size_t tag_count = 0; + if (!decode_codepoints(message->metadata.tags[tag], &tag_points, &tag_count)) + { + 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) + { + found = true; + break; + } + } + free(tag_points); + } + if (found) + { + matches++; + } + } + score = (double)matches / (double)total; + if (query_count <= message_count) + { + for (message_offset = 0; message_offset + query_count <= message_count; ++message_offset) + { + if (memcmp(query_points, message_points + message_offset, query_count * sizeof(uint32_t)) + == 0) + { + score += 1.0; + break; + } + } + } +done: + free(query_points); + free(message_points); + return score; +} |
