| 490 | } |
| 491 | |
| 492 | int cbm_lsh_query_into(const cbm_lsh_index_t *idx, const cbm_minhash_t *fp, |
| 493 | const cbm_lsh_entry_t **out_buf, int out_cap) { |
| 494 | if (!idx || !fp || !out_buf || out_cap <= 0) { |
| 495 | return 0; |
| 496 | } |
| 497 | |
| 498 | /* Thread-local dedup — no shared state touched. */ |
| 499 | seen_set_t seen; |
| 500 | seen_set_init(&seen); |
| 501 | |
| 502 | int count = 0; |
| 503 | for (int b = 0; b < CBM_LSH_BANDS; b++) { |
| 504 | uint32_t h = band_hash(fp, b); |
| 505 | const lsh_bucket_t *bucket = &idx->bands[b][h]; |
| 506 | if (bucket->count > MAX_BUCKET_SIZE) { |
| 507 | continue; |
| 508 | } |
| 509 | for (int i = 0; i < bucket->count && count < out_cap; i++) { |
| 510 | const cbm_lsh_entry_t *candidate = &idx->entries[bucket->items[i]]; |
| 511 | if (!seen_set_insert(&seen, candidate->node_id)) { |
| 512 | continue; |
| 513 | } |
| 514 | out_buf[count++] = candidate; |
| 515 | } |
| 516 | if (count >= out_cap) { |
| 517 | break; |
| 518 | } |
| 519 | } |
| 520 | |
| 521 | seen_set_free(&seen); |
| 522 | return count; |
| 523 | } |
| 524 | |
| 525 | void cbm_lsh_free(cbm_lsh_index_t *idx) { |
| 526 | if (!idx) { |
no test coverage detected