nostrdb

an unfairly fast embedded nostr database backed by lmdb
git clone git://jb55.com/nostrdb
Log | Files | Refs | Submodules | README | LICENSE

commit b69e24109072e6c9712e254b751d886d810b4ab7
parent 0e2e18d12d7792e4b9cf47bf05b3add2fa2dc6f5
Author: William Casarin <jb55@jb55.com>
Date:   Sun, 30 Mar 2025 09:09:30 -0700

query: implement profile search query plans

The basic idea of this is to allow you to use the standard
nip50 query interface to search for profiles using our profile
index.

query: {"search":"jb55", "kinds":[0]}

will result in a profile_search query plan that searches kind0 profiles
for the corresponding `name` or `display_name`.

Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Msrc/nostrdb.c | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest.c | 44+++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 117 insertions(+), 1 deletion(-)

diff --git a/src/nostrdb.c b/src/nostrdb.c @@ -239,6 +239,7 @@ enum ndb_query_plan { NDB_PLAN_TAGS, NDB_PLAN_SEARCH, NDB_PLAN_RELAY_KINDS, + NDB_PLAN_PROFILE_SEARCH, }; // A id + u64 + timestamp @@ -4027,6 +4028,67 @@ next: return 1; } +static int ndb_query_plan_execute_profile_search( + struct ndb_txn *txn, + struct ndb_filter *filter, + struct ndb_query_results *results, + int limit) +{ + const char *search; + int i; + + // The filter pubkey is updated inplace for each note search + unsigned char *filter_pubkey; + unsigned char pubkey[32] = {0}; + struct ndb_filter_elements *els; + struct ndb_search profile_search; + struct ndb_filter note_filter, *f = &note_filter; + + if (!(search = ndb_filter_find_search(filter))) + return 0; + + if (!ndb_filter_init_with(f, 1)) + return 0; + + ndb_filter_start_field(f, NDB_FILTER_KINDS); + ndb_filter_add_int_element(f, 0); + ndb_filter_end_field(f); + + ndb_filter_start_field(f, NDB_FILTER_AUTHORS); + ndb_filter_add_id_element(f, pubkey); + ndb_filter_end_field(f); + ndb_filter_end(f); + + // get the authors element after we finalize the filter, since + // the data could have moved + if (!(els = ndb_filter_find_elements(f, NDB_FILTER_AUTHORS))) + return 0; + + // grab pointer to pubkey in the filter so that we can + // update the filter as we go + if (!(filter_pubkey = ndb_filter_get_id_element(f, els, 0))) + return 0; + + for (i = 0; !query_is_full(results, limit); i++) { + if (i == 0) { + if (!ndb_search_profile(txn, &profile_search, search)) + break; + } else { + if (!ndb_search_profile_next(&profile_search)) + break; + } + + // Copy pubkey into filter + memcpy(filter_pubkey, profile_search.key->id, 32); + + // Look up the corresponding note associated with that pubkey + if (!ndb_query_plan_execute_author_kinds(txn, f, results, limit)) + return 0; + } + + return 1; +} + static int ndb_query_plan_execute_relay_kinds( struct ndb_txn *txn, struct ndb_filter *filter, @@ -4239,6 +4301,11 @@ static enum ndb_query_plan ndb_filter_plan(struct ndb_filter *filter) tags = ndb_filter_find_elements(filter, NDB_FILTER_TAGS); relays = ndb_filter_find_elements(filter, NDB_FILTER_RELAYS); + // profile search + if (kinds && kinds->count == 1 && kinds->elements[0] == 0 && search) { + return NDB_PLAN_PROFILE_SEARCH; + } + // this is rougly similar to the heuristic in strfry's dbscan if (search) { return NDB_PLAN_SEARCH; @@ -4270,6 +4337,7 @@ static const char *ndb_query_plan_name(enum ndb_query_plan plan_id) case NDB_PLAN_AUTHORS: return "authors"; case NDB_PLAN_RELAY_KINDS: return "relay_kinds"; case NDB_PLAN_AUTHOR_KINDS: return "author_kinds"; + case NDB_PLAN_PROFILE_SEARCH: return "profile_search"; } return "unknown"; @@ -4308,6 +4376,12 @@ static int ndb_query_filter(struct ndb_txn *txn, struct ndb_filter *filter, if (!ndb_query_plan_execute_search(txn, filter, &results, limit)) return 0; break; + + case NDB_PLAN_PROFILE_SEARCH: + if (!ndb_query_plan_execute_profile_search(txn, filter, &results, limit)) + return 0; + break; + // We have just kinds, just scan the kind index case NDB_PLAN_KINDS: if (!ndb_query_plan_execute_kinds(txn, filter, &results, limit)) diff --git a/test.c b/test.c @@ -1857,7 +1857,48 @@ static void test_note_relay_index() // Cleanup ndb_destroy(ndb); - printf("test_note_relay_index passed!\n"); + printf("ok test_note_relay_index\n"); +} + +static void test_nip50_profile_search() { + struct ndb *ndb; + struct ndb_txn txn; + struct ndb_config config; + struct ndb_filter filter, *f = &filter; + int count; + struct ndb_query_result result; + + // Initialize NDB + ndb_default_config(&config); + assert(ndb_init(&ndb, test_dir, &config)); + + // 1) Ingest the note from “relay1”. + // Use ndb_ingest_meta_init to record the relay. + + unsigned char expected_id[32] = { + 0x22, 0x05, 0x0b, 0x6d, 0x97, 0xbb, 0x9d, 0xa0, 0x9e, 0x90, 0xed, 0x0c, + 0x6d, 0xd9, 0x5e, 0xed, 0x1d, 0x42, 0x3e, 0x27, 0xd5, 0xcb, 0xa5, 0x94, + 0xd2, 0xb4, 0xd1, 0x3a, 0x55, 0x43, 0x09, 0x07 }; + assert(ndb_filter_init(f)); + assert(ndb_filter_start_field(f, NDB_FILTER_SEARCH)); + assert(ndb_filter_add_str_element(f, "Selene")); + ndb_filter_end_field(f); + assert(ndb_filter_start_field(f, NDB_FILTER_KINDS)); + assert(ndb_filter_add_int_element(f, 0)); + ndb_filter_end_field(f); + ndb_filter_end(f); + + ndb_begin_query(ndb, &txn); + ndb_query(&txn, f, 1, &result, 1, &count); + ndb_end_query(&txn); + + assert(count == 1); + assert(!memcmp(ndb_note_id(result.note), expected_id, 32)); + + // Cleanup + ndb_destroy(ndb); + + printf("ok test_nip50_profile_search\n"); } int main(int argc, const char *argv[]) { @@ -1887,6 +1928,7 @@ int main(int argc, const char *argv[]) { test_profile_updates(); test_reaction_counter(); test_load_profiles(); + test_nip50_profile_search(); test_basic_event(); test_empty_tags(); test_parse_json();