266 lines
8.8 KiB
C
266 lines
8.8 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/*
|
|
* test_rosa_db_retrieve.c — Phase 3: Retrieval quality test
|
|
*
|
|
* Indexes all conversations, then runs 10 test queries.
|
|
* For each query, finds the longest suffix match and retrieves
|
|
* a context window around the match point.
|
|
*
|
|
* Usage:
|
|
* ./examples/test_rosa_db_retrieve \
|
|
* --vocab <vocab_path> --db <db_path> --context-window 256
|
|
*/
|
|
|
|
#include "rosa/rosa.h"
|
|
#include "rwkvx/rwkvx.h"
|
|
#include "data/data.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#define MAX_TOKENS_PER_CONV (1024 * 64)
|
|
#define MAX_QUERY_TOKENS 512
|
|
|
|
/* ---- Index builder (same as Phase 2) ---- */
|
|
|
|
typedef struct {
|
|
sa_token_automaton *sa;
|
|
sa_position_map *pmap;
|
|
int total_tokens;
|
|
int n_conv;
|
|
/* Per-conversation token offsets for decoding context */
|
|
int *conv_start; /* global position where each conv starts */
|
|
int *conv_len; /* token count per conv */
|
|
uint32_t *all_tokens; /* full token stream for decoding */
|
|
int all_tokens_cap;
|
|
} rosa_index;
|
|
|
|
static rosa_index* build_index(fd_dataset *ds, rx_tokenizer *tok) {
|
|
rosa_index *idx = (rosa_index *)calloc(1, sizeof(rosa_index));
|
|
idx->n_conv = fd_len(ds);
|
|
idx->sa = sa_token_new(1024, 65536);
|
|
idx->pmap = sa_position_map_new(4096);
|
|
idx->conv_start = (int *)malloc(idx->n_conv * sizeof(int));
|
|
idx->conv_len = (int *)malloc(idx->n_conv * sizeof(int));
|
|
idx->all_tokens_cap = 1024 * 1024;
|
|
idx->all_tokens = (uint32_t *)malloc(idx->all_tokens_cap * sizeof(uint32_t));
|
|
|
|
uint32_t *buf = (uint32_t *)malloc(MAX_TOKENS_PER_CONV * sizeof(uint32_t));
|
|
|
|
for (int c = 0; c < idx->n_conv; c++) {
|
|
fd_sample *sample = fd_get(ds, c);
|
|
const char *text = fd_sample_get(sample, "text");
|
|
if (!text) { idx->conv_start[c] = idx->total_tokens; idx->conv_len[c] = 0; continue; }
|
|
|
|
int n = rx_tokenizer_encode(tok, text, buf, MAX_TOKENS_PER_CONV);
|
|
if (n <= 0) { idx->conv_start[c] = idx->total_tokens; idx->conv_len[c] = 0; continue; }
|
|
|
|
idx->conv_start[c] = idx->total_tokens;
|
|
idx->conv_len[c] = n;
|
|
|
|
/* Grow all_tokens if needed */
|
|
while (idx->total_tokens + n > idx->all_tokens_cap) {
|
|
idx->all_tokens_cap *= 2;
|
|
idx->all_tokens = (uint32_t *)realloc(idx->all_tokens,
|
|
idx->all_tokens_cap * sizeof(uint32_t));
|
|
}
|
|
|
|
for (int t = 0; t < n; t++) {
|
|
int gpos = idx->total_tokens + t;
|
|
sa_token_extend(idx->sa, (int)buf[t], gpos);
|
|
sa_position_map_append(idx->pmap, c, 0, t);
|
|
idx->all_tokens[gpos] = buf[t];
|
|
}
|
|
idx->total_tokens += n;
|
|
}
|
|
|
|
free(buf);
|
|
return idx;
|
|
}
|
|
|
|
static void free_index(rosa_index *idx) {
|
|
sa_token_free(idx->sa);
|
|
sa_position_map_free(idx->pmap);
|
|
free(idx->conv_start);
|
|
free(idx->conv_len);
|
|
free(idx->all_tokens);
|
|
free(idx);
|
|
}
|
|
|
|
/* ---- Query and retrieve context ---- */
|
|
|
|
typedef struct {
|
|
int match_len;
|
|
int endpos;
|
|
int conv_idx;
|
|
int token_offset;
|
|
char *context; /* decoded context text (malloc'd) */
|
|
double query_time_ms;
|
|
} retrieve_result;
|
|
|
|
static retrieve_result query_and_retrieve(rosa_index *idx, rx_tokenizer *tok,
|
|
const char *query_text, int context_window) {
|
|
retrieve_result res = {0};
|
|
res.endpos = -1;
|
|
res.conv_idx = -1;
|
|
|
|
/* Tokenize query */
|
|
uint32_t query_tokens[MAX_QUERY_TOKENS];
|
|
int qlen = rx_tokenizer_encode(tok, query_text, query_tokens, MAX_QUERY_TOKENS);
|
|
if (qlen <= 0) return res;
|
|
|
|
/* Run query through SA */
|
|
clock_t t0 = clock();
|
|
int state = 0, match_len = 0;
|
|
int best_endpos = -1, best_match = 0;
|
|
|
|
for (int i = 0; i < qlen; i++) {
|
|
int ep = sa_token_query_step(idx->sa, (int)query_tokens[i], &state, &match_len);
|
|
if (match_len > best_match) {
|
|
best_match = match_len;
|
|
best_endpos = ep;
|
|
}
|
|
}
|
|
clock_t t1 = clock();
|
|
res.query_time_ms = (double)(t1 - t0) / CLOCKS_PER_SEC * 1000.0;
|
|
res.match_len = best_match;
|
|
res.endpos = best_endpos;
|
|
|
|
if (best_endpos < 0) return res;
|
|
|
|
/* Look up position */
|
|
const sa_position_info *info = sa_position_map_get(idx->pmap, best_endpos);
|
|
if (!info) return res;
|
|
res.conv_idx = info->conv_idx;
|
|
res.token_offset = info->token_offset;
|
|
|
|
/* Extract context window around the match */
|
|
int conv_start = idx->conv_start[res.conv_idx];
|
|
int conv_len = idx->conv_len[res.conv_idx];
|
|
|
|
/* Match ends at token_offset within the conversation */
|
|
int match_end_in_conv = info->token_offset;
|
|
int ctx_start = match_end_in_conv - best_match - context_window / 2;
|
|
if (ctx_start < 0) ctx_start = 0;
|
|
int ctx_end = match_end_in_conv + context_window / 2;
|
|
if (ctx_end > conv_len) ctx_end = conv_len;
|
|
|
|
int ctx_len = ctx_end - ctx_start;
|
|
if (ctx_len > 0) {
|
|
res.context = rx_tokenizer_decode(tok,
|
|
&idx->all_tokens[conv_start + ctx_start], ctx_len);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
/* ---- Main ---- */
|
|
|
|
int main(int argc, char **argv) {
|
|
const char *vocab_path = NULL;
|
|
const char *db_path = NULL;
|
|
int context_window = 256;
|
|
|
|
for (int i = 1; i < argc; i++) {
|
|
if (strcmp(argv[i], "--vocab") == 0 && i + 1 < argc)
|
|
vocab_path = argv[++i];
|
|
else if (strcmp(argv[i], "--db") == 0 && i + 1 < argc)
|
|
db_path = argv[++i];
|
|
else if (strcmp(argv[i], "--context-window") == 0 && i + 1 < argc)
|
|
context_window = atoi(argv[++i]);
|
|
}
|
|
|
|
if (!vocab_path || !db_path) {
|
|
fprintf(stderr, "Usage: %s --vocab <path> --db <path> [--context-window N]\n", argv[0]);
|
|
return 1;
|
|
}
|
|
|
|
/* Load tokenizer and dataset */
|
|
printf("Loading tokenizer...\n");
|
|
rx_tokenizer *tok = rx_tokenizer_new(vocab_path);
|
|
if (!tok) { fprintf(stderr, "ERROR: tokenizer\n"); return 1; }
|
|
|
|
printf("Loading conversations...\n");
|
|
fd_dataset *ds = fd_load_chat_sqlite(db_path, 0);
|
|
if (!ds) { fprintf(stderr, "ERROR: database\n"); return 1; }
|
|
printf("Loaded %d conversations\n", fd_len(ds));
|
|
|
|
/* Build index */
|
|
printf("Building ROSA index...\n");
|
|
clock_t t0 = clock();
|
|
rosa_index *idx = build_index(ds, tok);
|
|
clock_t t1 = clock();
|
|
printf("Indexed %d tokens in %.2f s\n\n", idx->total_tokens,
|
|
(double)(t1 - t0) / CLOCKS_PER_SEC);
|
|
|
|
/* ---- 10 test queries ---- */
|
|
const char *queries[] = {
|
|
"suffix automaton retrieval",
|
|
"Metal GPU acceleration kernel",
|
|
"RWKV-7 WKV kernel implementation",
|
|
"HybridMem HDC binary vectors",
|
|
"LoRA training attention blocks",
|
|
"Apple Neural Engine ANE CoreML",
|
|
"INT8 quantization NEON dot product",
|
|
"DeepEmbed per-token embedding",
|
|
"UEFI boot application bare metal",
|
|
"tokenizer BPE encode decode vocabulary",
|
|
};
|
|
int n_queries = 10;
|
|
|
|
printf("=== Phase 3: Retrieval Quality ===\n\n");
|
|
printf("Context window: %d tokens\n\n", context_window);
|
|
|
|
printf("| # | Query | Match (tok) | Conv | Time (ms) | Context snippet |\n");
|
|
printf("|---|-------|-------------|------|-----------|------------------|\n");
|
|
|
|
int total_matched = 0;
|
|
|
|
for (int q = 0; q < n_queries; q++) {
|
|
retrieve_result res = query_and_retrieve(idx, tok, queries[q], context_window);
|
|
|
|
/* Truncate context for display */
|
|
char snippet[201] = "(no match)";
|
|
if (res.context) {
|
|
/* Find first interesting part (skip whitespace/tags) */
|
|
int slen = (int)strlen(res.context);
|
|
int start = 0;
|
|
if (slen > 200) {
|
|
/* Try to start near the match point */
|
|
start = slen / 2 - 100;
|
|
if (start < 0) start = 0;
|
|
}
|
|
int copy_len = slen - start;
|
|
if (copy_len > 200) copy_len = 200;
|
|
memcpy(snippet, res.context + start, copy_len);
|
|
snippet[copy_len] = '\0';
|
|
/* Replace newlines with spaces for table display */
|
|
for (int i = 0; i < copy_len; i++)
|
|
if (snippet[i] == '\n' || snippet[i] == '\r') snippet[i] = ' ';
|
|
/* Replace pipes for markdown table */
|
|
for (int i = 0; i < copy_len; i++)
|
|
if (snippet[i] == '|') snippet[i] = '/';
|
|
}
|
|
|
|
if (res.match_len > 0) total_matched++;
|
|
|
|
printf("| %d | %s | %d | %d | %.2f | %.80s... |\n",
|
|
q + 1, queries[q], res.match_len,
|
|
res.conv_idx, res.query_time_ms, snippet);
|
|
|
|
free(res.context);
|
|
}
|
|
|
|
printf("\n**Queries with matches: %d/%d**\n", total_matched, n_queries);
|
|
printf("\n**Verdict: %s**\n", total_matched >= 7 ? "PASS" : (total_matched >= 4 ? "MARGINAL" : "FAIL"));
|
|
|
|
/* Cleanup */
|
|
free_index(idx);
|
|
fd_free(ds);
|
|
rx_tokenizer_free(tok);
|
|
|
|
return total_matched >= 4 ? 0 : 1;
|
|
}
|