Index messages locally in an encrypted on-device store so encrypted rooms can be searched at all - #7249
Conversation
|
Thank you for your contribution! Here are a few things to check in the PR to ensure it's reviewed as quickly as possible:
|
|
@jmartinesp @ganfra Is there any plan to merge this pr anytime soon, or will it take time to review and test? |
e929bce to
6a03f5c
Compare
We're about to start working in this feature on Android, and while we appreciate the conrtibutions, the problem is some of the PRs clash with our strategy (like adding the logic on Rust instead of Android) or designs, so we'd rather build those ourselves. Also, merging huge AI-generated PRs has come back to bite us in the past, so we'd rather proceed with caution with this feature. That said, using this PR as a base seems fine: it looks like it provides some nice scaffolding for the feature. Let's see if we can make it mergeable. |
|
Odd, the failing CI flows pass locally, and a rebase didn't seem to help. It also happened in a different PR. |
jmartinesp
left a comment
There was a problem hiding this comment.
After a quick look, I have a few questions and possible improvements to the code.
jmartinesp
left a comment
There was a problem hiding this comment.
Thanks for the changes, I think the code looks good now! Don't mind the Konsist tests, I think I know where they're coming from and there's no way they could work for forks, so I'll just force merge this.
|
Actually, the PR that should fix the issues with Konsist was just merged, so let's use this PR to test it works as expected for fork PRs. |
Wraps the SDK's search service so the rest of the app never sees MatrixRustSDK types, following the existing convention in libraries/matrix. - MessageSearch, MessageSearchService, MessageSearchResult and MessageSearchPaginationState describe the surface in the api module. - RustMessageSearch and RustMessageSearchService implement it, with MessageSearchResultMapper and MessageSearchResultsProcessor turning SDK results into Kotlin data classes. - TantivyQueryEscaper escapes the query so a user typing ":)" or "AND" searches for that literal text rather than tripping the tantivy parser, and joins every word as a Must clause. - Fakes and unit tests cover the mapping, the processor and the escaper, the last checked against a real tantivy 0.26.1 parser.
Gives the client an on-device tantivy index encrypted with the session's client secret, so encrypted rooms can be searched at all. - The index store is attached at ClientBuilder time via withSearchIndexStore, in a search-index directory alongside the session, keyed on the client secret. A session without one gets no index. - MatrixClient exposes isMessageSearchAvailable so callers gate on the real indexing capability rather than on the raw flag, and getCacheSize() counts the index directory so it stays visible in the settings figures. - FeatureFlags.MessageSearch (feature.message_search) is off by default and isFinished = false. It is read once at ClientBuilder time, so develop stays releasable and nothing reaches a user.
Tie search availability to the MessageSearch feature flag alone and let the index be created without a client secret, encrypting it only when the session has one, matching the SDK's SQLite stores. Make getBaseClientBuilder take the flag as a required parameter and return ClientBuilder directly again, dropping the BaseClientBuilder wrapper and the mid-restore snapshot parameter. Check the flag through FeatureFlagService in RustMatrixAuthenticationService instead of caching it in a field, and revert the extra matrix_sdk_search tracing target, which belongs in the SDK's defaults.
2cd1534 to
aefc595
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## develop #7249 +/- ##
===========================================
- Coverage 80.56% 80.54% -0.02%
===========================================
Files 2758 2768 +10
Lines 80310 80510 +200
Branches 10951 10975 +24
===========================================
+ Hits 64698 64844 +146
- Misses 11379 11428 +49
- Partials 4233 4238 +5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Brings the fork up to date with element-hq/element-x-android develop (131 commits, up to the 26.08.0 release), including Gradle 9.6.1, AGP 9.3.1, Kotlin 2.4.10, Metro 1.3.2 and matrix-rust-sdk 26.07.28. PR element-hq#7249 landed upstream as a squashed commit, so the matrix search layer now exists on both sides. Most of those files are byte-identical and merged cleanly; the ones reviewers reshaped were resolved in upstream's favour: - getBaseClientBuilder takes a required isMessageSearchAvailable and returns a ClientBuilder; BaseClientBuilder is gone. - Search availability is the feature flag alone. Sessions without a client secret now get an unencrypted index, matching the SDK's own SQLite stores. - RustMatrixAuthenticationService injects FeatureFlagService instead of caching availability in a mutable field. The work that has not landed upstream yet is re-seated on that shape: the background backfill sweep still schedules from RustMatrixClient, and the event-cache coverage bootstrap, its marker file and deleteEventCacheStore are re-implemented against the new API. Also adopted upstream's createDM(userId, isEncrypted), the customReactionBottomSheet slot, dmUserStatus in the top bar and the scaffoldScrollableContentInsets rework, while keeping the search icon and canSearch plumbing in MessagesView. Two follow-on fixes: RustMatrixClientFactoryTest asserted the old secret-gated availability contract and now asserts the new one, and computeCodeBlockOverlays returns an ImmutableList so the code block copy chrome satisfies the Compose stability rule.
Content
Problem
Element X cannot search messages in encrypted rooms. The server-side
/searchendpoint only eversees ciphertext, so the one search path the app has is structurally unable to answer for E2EE
rooms — it returns nothing rather than reporting that it cannot help. The visible symptom is
#4149: the in-room search option is present in unencrypted rooms and
absent in encrypted ones. The workaround users describe ends in searching from another client
instead (#7072).
The Rust SDK now ships a local, encrypted, tantivy-backed message index — the
matrix-sdk-searchcrate tracked by matrix-org/matrix-rust-sdk#5350 — but nothing in this app has ever wired it
up. This adds the matrix-layer plumbing to do so.
Fix
MatrixClientgainsisMessageSearchAvailableandmessageSearchService.RustMatrixClientFactorydecides availability once, fromfeatureFlagService.isFeatureEnabled(MessageSearch) && clientSecret != null, and callswithSearchIndexStore(path = <fileDir>/search-index, password = clientSecret)on theClientBuilder. A session with no client secret gets no index — see Scope.libraries/matrix/api/.../search/package keeps the SDK out of the UI, per the wrappingconvention:
MessageSearchis one stateful cursor exposingresults: StateFlow<ImmutableList<MessageSearchResult>>andpaginationState: StateFlow<MessageSearchPaginationState>, withsetQueryandpaginatereturning
Result. Its lifetime is theCoroutineScopethat created it, so there is noclose()to forget.RustMessageSearchfunnels the SDK's non-suspend listener batches through a singleChannelconsumer so diffs stay ordered, and disposes itsTaskHandles viascope.coroutineContext.job.invokeOnCompletion.MessageSearchResultsProcessorappliesSearchServiceResultsUpdatediffs under a mutex against a list kept index-parallel with theSDK's, because the diffs are positional. Both it and
MessageSearchResultMapperuse anexhaustive
whenwith noelse, so an SDK bump fails compilation rather than silentlydropping a variant.
TantivyQueryEscaperescapes query syntax so what the user types is searched as text. Todaya query containing
:), a stray", or the wordANDis parsed as tantivy query syntax andcan fail the whole search. Escaping runs in a single forward pass so added backslashes are not
re-escaped, and the
+Must prefix is applied after escaping — prefixing first would emit aliteral
\+that parses cleanly and silently restores OR semantics.getCacheSize()now counts the index directory, and disabling the flag deletes it.Everything is behind
FeatureFlags.MessageSearch(feature.message_search,defaultValue = { false },isFinished = false). With the flag off — the default — no indexstore is attached, no index directory is created, and no other code path changes.
Motivation and context
Relates to #3709 (search messages in room) — the canonical tracker,
and the thread that argues for this design.
Relates to #6557 (in-room keyword search with timestamps and
jump-to-message).
Relates to #4149 (closed as a duplicate of #3709; a defect report,
with screenshots).
Relates to #7072.
None of these is closed here — the change ships behind a disabled feature flag, so they are cited
as context rather than as fixes.
Screenshots / GIFs
There is no UI change in this pull request.
Tests
Covered by unit tests rather than by manual steps.
TantivyQueryEscaperTest— expectations checked against a real tantivy 0.26.1 parser, coveringeach escaped character in leading and non-leading position, the bare-word operators
(
AND/OR/NOT/IN/TO), the no-re-escaping property, and the deliberate case where a tokenwith no letter or digit is left optional because the default tokenizer indexes no terms for it.
MessageSearchResultsProcessorTest— everySearchServiceResultsUpdatevariant:Append, PushBack, PushFront, Set, Insert, Remove, Truncate, Reset, PopBack, PopFront and Clear,
asserting the local list stays index-parallel with the SDK's.
RustMessageSearchTest— lazy subscription on firstsetQuery; thatsetQueryresetspaginationStatetoIdle(endReached = false)before results arrive, since a fresh SDK cursorreports
endReached = true; ordered delivery of batched listener callbacks; and handle disposalwhen the scope completes.
RustMatrixClientFactoryTest— the index store is attached when the flag is on and a clientsecret exists, and not attached when either is missing.
New fakes for downstream modules:
FakeMessageSearch,FakeMessageSearchService,FakeFfiSearchService.Scope
Deliberately not attempted here, so it is not mistaken for an oversight:
/searchfallback. Anything the index never saw is unfindable.
+Must clause, so one typoreturns zero results where an OR query would have degraded to partial matches. The trade-off
is deliberate — OR over a local index returned mostly noise — but it is a trade-off.
*prefix operator is escaped, so partial wordsdo not match, and
photodoes not matchphotos. Tokenisation is fixed when the index iscreated and is effectively English-first, so a script the default tokenizer does not segment is
served poorly; both are worth their own follow-ups rather than a fix buried here.
"phrase",-exclude, ranges, regex and fieldlookups are all escaped to literal text. None of it was advertised in the UI, and malformed
use of it used to kill the whole search.
through the SDK's event cache. Media and sticker indexing, edit semantics, and
removal-on-redaction — currently broken upstream on Meta: Full text search support matrix-org/matrix-rust-sdk#5350 — are all
SDK-side.
them: legacy pre-schema-v5 sessions, and sessions created by a release build between
2024-01-18 and 2024-04-15.
restart.
useful.
Notes for reviewers
First of four. The rest, each depending only on this one: the search screen and its top-bar
entry point; the background history backfill; and the event-cache coverage bootstrap. Since a
pull request here must be based on a branch in this repository, they follow serially rather than
as a stack.
This is ~1,430 insertions, above the 500 guideline. Roughly half is tests and fakes, and
splitting the tantivy escaping or the API definition away from the implementation they cover
would make review harder rather than easier — but if you would prefer
TantivyQueryEscaperandits test as a separate ~200-line change, say so and I will pull it out.
Tested devices
Checklist