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:
M | src/nostrdb.c | | | 74 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
M | test.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 = ¬e_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();