Skip to content
23 changes: 20 additions & 3 deletions src/server/rdb_load.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2825,11 +2825,28 @@ void RdbLoader::LoadSearchIndexDefFromAux(string&& def) {
return;
}

// Prepend FT.CREATE to index definiton
// Determine command type and prepend appropriate prefix
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the aux format for these definitions?
will it stay compatible with previous snapshots?
will it be extendable with more commands?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Aux format: Two separate fields - "search-index" for FT.CREATE, "search-synonyms" for FT.SYNUPDATE
Backward compatible: Yes. Tested with the main branch.
Extensible: Yes. Can add search-aliases, search-config, etc., as separate aux fields using the same pattern.

CmdArgVec arg_vec;
facade::RespExpr::VecToArgList(resp_vec, &arg_vec);
string ft_create = "FT.CREATE";
arg_vec.insert(arg_vec.begin(), MutableSlice{ft_create.data(), ft_create.size()});

// Check if this is a SYNUPDATE command or a CREATE command
string command_name;
if (!arg_vec.empty()) {
string_view first_token = facade::ToSV(arg_vec[0]);
if (first_token == "SYNUPDATE") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just pass a flag to LoadSearchIndexDefFromAux instead of all the ifs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a separate method.

// Replace first token with FT.SYNUPDATE
command_name = "FT.SYNUPDATE";
arg_vec[0] = MutableSlice{command_name.data(), command_name.size()};
} else {
// Prepend FT.CREATE for index definition
command_name = "FT.CREATE";
arg_vec.insert(arg_vec.begin(), MutableSlice{command_name.data(), command_name.size()});
}
} else {
// Default: prepend FT.CREATE
command_name = "FT.CREATE";
arg_vec.insert(arg_vec.begin(), MutableSlice{command_name.data(), command_name.size()});
}

service_->DispatchCommand(absl::MakeSpan(arg_vec), &crb, &cntx);

Expand Down
20 changes: 19 additions & 1 deletion src/server/rdb_save.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1367,9 +1367,27 @@ RdbSaver::GlobalData RdbSaver::GetGlobalData(const Service* service) {
if (shard->shard_id() == 0) {
auto* indices = shard->search_indices();
for (const auto& index_name : indices->GetIndexNames()) {
auto index_info = indices->GetIndex(index_name)->GetInfo();
auto* index = indices->GetIndex(index_name);
auto index_info = index->GetInfo();

// Save index definition
search_indices.emplace_back(
absl::StrCat(index_name, " ", index_info.BuildRestoreCommand()));

// Save synonym groups for this index
const auto& synonym_groups = index->GetSynonyms().GetGroups();
for (const auto& [group_id, terms] : synonym_groups) {
if (!terms.empty()) {
// Convert set<string> to vector for joining
std::vector<std::string_view> terms_vec(terms.begin(), terms.end());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm almost sure that StrJoin works with as set as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed


// Format: "SYNUPDATE index_name group_id term1 term2 term3"
std::string syn_cmd = absl::StrCat("SYNUPDATE ", index_name, " ", group_id, " ",
absl::StrJoin(terms_vec, " "));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we also don't need the SYNUPDATE prefix if we have a special aux header for it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed


search_indices.emplace_back(std::move(syn_cmd));
}
}
}
}
#endif
Expand Down
5 changes: 4 additions & 1 deletion src/server/search/doc_index.cc
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,11 @@ void ShardDocIndex::Rebuild(const OpArgs& op_args, PMR_NS::memory_resource* mr)

void ShardDocIndex::RebuildForGroup(const OpArgs& op_args, const std::string_view& group_id,
const std::vector<std::string_view>& terms) {
if (!indices_)
// Always update synonyms, even if indices are not yet built (during RDB load)
if (!indices_) {
synonyms_.UpdateGroup(group_id, terms);
return;
}

absl::flat_hash_set<DocId> docs_to_rebuild;
std::vector<search::TextIndex*> text_indices = indices_->GetAllTextIndices();
Expand Down
26 changes: 26 additions & 0 deletions tests/dragonfly/search_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -572,3 +572,29 @@ def make_car(producer, description, speed):

for index in client.execute_command("FT._LIST"):
client.ft(index.decode()).dropindex()


@dfly_args({"proactor_threads": 4, "dbfilename": "synonym-persistence"})
async def test_synonym_persistence(df_server):
"""Test that synonyms are persisted across server restarts"""
client = aioredis.Redis(port=df_server.port)

# Create index and add documents
idx = client.ft("idx")
await idx.create_index([TextField("txt")], definition=IndexDefinition(prefix=["d:"]))
await client.hset("d:1", mapping={"txt": "car"})
await client.hset("d:2", mapping={"txt": "automobile"})

# Add synonyms and verify they work
await client.execute_command("FT.SYNUPDATE", "idx", "grp", "car", "automobile")
assert (await idx.search(Query("car"))).total == 2

# Restart server
df_server.stop()
df_server.start()
client = aioredis.Redis(port=df_server.port)
await wait_available_async(client)
idx = client.ft("idx")

# Verify synonyms still work after restart
assert (await idx.search(Query("car"))).total == 2
Loading